summaryrefslogtreecommitdiffhomepage
AgeCommit message (Collapse)Author
2026-07-07Merge branch 'main' into predevdevAdam Malczewski
2026-07-01Merge branch 'feature/heartbeat-inactive-only' into predevAdam Malczewski
2026-07-01Merge branch 'feature/mcp-transport-fixes' into predevAdam Malczewski
2026-07-01Merge branch 'feature/summon-title' into predevAdam Malczewski
2026-07-01Merge branch 'feature/in-flight-compaction' into predevAdam Malczewski
2026-06-30chore: remove old handoff docs, plans, review reports, and task lists from rootHEADmainAdam Malczewski
Removed 40+ markdown files that were cluttering the repo root: - frontend-*-handoff.md (28 files) — historical API contract handoffs, features all implemented - backend-to-fe-handoff.md, backend-to-fe-handoff-2.md — old handoff docs - broken-chat-repair-handoff.md — old repair handoff - PLAN-mcp.md, PLAN-per-edit-diagnostics.md — old planning docs - ai-review-report.md, crash-review-report.md — one-time review reports - tasks.md, HANDOFF.md — outdated status docs (git log is the source of truth) Kept: AGENTS.md, GLOSSARY.md, ORCHESTRATOR.md, README.md Also: gitignored ai-review-report.md so future Gemini reviews don't commit it
2026-06-29feat(heartbeat): add inactiveOnly mode (skip fire while workspace has active ↵Adam Malczewski
agents) A new per-workspace heartbeat setting `inactiveOnly` (default true): when on, the heartbeat SKIPS a scheduled fire whenever the configured workspace has any active agents — a conversation whose persisted status is "active" (driving a turn) or "queued" (waiting on the message queue). The fire is silently skipped (no run recorded); the scheduler re-arms and retries at the next interval. Set false to fire unconditionally (the prior behavior). The check is wired against conversationStore.listConversations({ workspaceId, status: ["active","queued"] }) at fire time — the orchestrator sets "active" on turn start and "idle" on settle, so it is the live busy/idle signal. The spawned heartbeat conversation lives in the dedicated heartbeat workspace, so a heartbeat run never counts as an active agent of the configured workspace (no self-block). - transport-contract: add inactiveOnly to HeartbeatConfig + UpdateHeartbeatRequest - heartbeat config-store: default true, apply/persist, legacy-parse default true - heartbeat service: injectable hasActiveAgents dep + skip logic in fire() - heartbeat extension: wire hasActiveAgents against the conversation store - transport-http: validate inactiveOnly (boolean) on PUT /workspaces/:id/heartbeat - tests: config-store (+4), heartbeat service (+7), transport-http (+3) - notes/heartbeat-setting-handoff.md: API contract for the frontend Verification: typecheck EXIT 0; tests 2013 passed | 6 skipped; biome EXIT 0.
2026-06-29fix(in-flight-compaction): persist steering messages + use live messages ↵Adam Malczewski
array for compaction Fix two critical bugs found in code review: Bug A — Permanent loss of mid-turn steering messages: drainSteering injected queued messages into the kernel's in-memory messages array but never persisted them, so a user could never see them and in-flight compaction (which loaded the store) scrubbed them. Fix: drainSteering now persists the steering message to the store as part of the same critical section as the injection (await store.append). This required making drainSteering async — the kernel now awaits it (contract: return type allows Promise; backward- compatible with sync callbacks). Fire-and-forget was unsafe: the conversation- store append reads the seq counter then writes chunks across multiple awaits, so a concurrent steering append + next-step onStepComplete append would both read the same seq counter and collide (the msgIdx-collision class of bug). Bug B — Index misalignment (DB <-> LLM divergence): keepLastN was sliced independently from the store's array (no steering) and the kernel's array (with steering), so the slices dropped DIFFERENT messages. Fix (follows from A): performCompaction accepts the kernel's LIVE messages array instead of reloading the stale store; the SAME recentKept slice is used for both the store write (replaceHistory) and the value returned to the kernel (compactedMessages), so they stay byte-aligned by construction. The post-seal/ manual compact() path still loads the store (the turn has ended, so it is stable). Tests: kernel async-drainSteering-await contract test; orchestrator steering- persisted + store/LLM-alignment regression test; queue.test.ts asserts the steering is persisted; its fake runTurn now awaits drainSteering. Verification: typecheck clean; 2014 tests pass (was 2012; +2 new + 1 assertion); biome 0 errors (12 pre-existing warnings in untouched files).
2026-06-29fix(mcp): support newline-delimited JSON framing + timeouts + abort signal ↵Adam Malczewski
propagation
2026-06-29fix(summon-title): defer title set until after workspace initializationAdam Malczewski
Setting the title in the HTTP /chat route BEFORE the turn started pre-created the conversation meta, which made the orchestrator's getConversationMeta === null newness check falsely report an EXISTING conversation. As a result ensureWorkspace / setWorkspaceId / the first-turn systemPromptService.construct were ALL skipped for summoned conversations with a title — leaving them with no assigned workspace and breaking downstream cwd resolution + system-prompt init. Fix: the title is no longer set in the route. It is threaded through StartTurnInput -> handleMessage -> startTurn -> runTurnDetached and persisted via setConversationTitle INSIDE workspaceSetupPromise, AFTER the newness check + workspace assignment (so init still fires for new conversations) and BEFORE the first message append (so the append's auto-derived title does not overwrite it). The title set is best-effort: a failure is logged but does not break the turn (the workspace setup already succeeded). The /chat route now forwards title into the orchestrator input instead of calling setConversationTitle itself. Tests: - app.test.ts: route tests rewritten to assert the route forwards title to the orchestrator and does NOT call setConversationTitle itself. - orchestrator.test.ts: 5 new regression tests, including the critical 'titled new conversation still gets workspace assigned + system-prompt constructed + title set', an ordering test (title set after getMeta/ensureWorkspace/ setWorkspaceId), and a resilience test (turn completes if setConversationTitle throws). Verification: typecheck EXIT 0; test 2026 passed | 6 skipped | 0 failed; check EXIT 0 (12 pre-existing warnings in untouched files).
2026-06-28Merge branch 'feature/summon-title' into predevAdam Malczewski
2026-06-28Merge branch 'feature/cancel-queued-message' into predevAdam Malczewski
2026-06-28Merge branch 'feature/in-flight-compaction' into predevAdam Malczewski
2026-06-28feat: in-flight compaction — compact mid-turn at every step boundaryAdam Malczewski
At the end of every step that produces tool calls (the tool-result boundary), the orchestrator now checks whether the conversation's context size has reached the compaction threshold (compact-percent of the model's context window). If it has, it compacts the history mid-turn — summarizing old messages and replacing the running history with [summary, ...recent] — then continues the prompt, so a long-running turn (e.g. left overnight) does not run out of context. This mirrors Opencode's approach (check at step boundaries, compact, continue) and is DISTINCT from the pre-existing post-seal auto-compact, which runs only AFTER a turn ends and refuses while a conversation is active. The new path runs DURING the turn (saving the running turn) using the live step usage rather than persisted metrics (written only at turn end). Architecture (kernel purity preserved): - Kernel contract: add an optional, feature-agnostic `onStepBoundary` hook to RunTurnInput. The runtime calls it at each tool-result boundary with the step's usage + the current messages, and adopts whatever message list it returns (undefined/empty = unchanged). Mirrors `drainSteering`. - Kernel runtime: ~21 lines — call the hook after onStepComplete + drainSteering; replace working messages in place when a list is returned. No I/O, no feature names. - Orchestrator: extract a shared `performCompaction` helper (summarize + fork + replaceHistory + emit) used by both `compact()` and the new in-flight path. Wire `onStepBoundary` in `runTurnDetached` to check the threshold via stepUsage vs contextWindow*percent and, on exceed, compact and return [summary, ...kernel's recent N] (preserves mid-turn steering + the vision-transformed provider view; the store gets the canonical recent messages). `compact()` is refactored to use the shared core; its active-conversation guard and metrics-based auto-threshold are unchanged. Tests (+13): 8 kernel onStepBoundary tests (replacement adopted by next step; undefined/empty = unchanged; not called on text-only turns; called once per tool-call step after drainSteering; receives stepUsage; omitted = no-op; replacement persists across steps); 5 orchestrator in-flight tests (triggers above threshold + turn continues; no-op below threshold; no-op when percent=0; no-op on text-only turns; the manual compact() service still refuses while active but in-flight runs anyway). Also fixed the in-memory test store's getCompactPercent/setCompactPercent (were no-ops). Verification: typecheck clean; 2012 tests pass (was 1999; +13); biome 0 errors (12 pre-existing warnings in untouched files).
2026-06-28feat(message-queue): cancel a queued steering message by idAdam Malczewski
Add the ability to cancel/close a single queued message so it never runs: while a turn is GENERATING and a user message sits in the steering queue (waiting for delivery at the next tool-result boundary or carry into a new turn), a client can remove one message by id. The cancelled message is never delivered as steering and never carried into a new turn. Complements the existing chat.queue enqueue (enqueue adds; cancel removes one). Layers (all additive; nothing existing breaks): - message-queue pure core: `cancel(state, conversationId, messageId)` — splices a single message out by id, returns the post-cancel snapshot. Idempotent (no-op if not found). Drops the key when the queue empties (mirrors drain). - message-queue service: `MessageQueueService.cancel()` — wraps the pure op, pushes a surface update ONLY on a real change (queue shrank); a missing-id cancel is a no-op with no surface push (mirrors drain's no-notify-on-empty). - session-orchestrator: `cancelQueuedMessage({ conversationId, messageId })` -> `{ cancelled, queue }`. The single entry transports call; resolves the queue lazily (same as enqueue). Degrades to `{ cancelled: false, queue: [] }` when the message-queue extension isn't loaded. `cancelled` is derived from the queue length delta (true iff a message was removed). API contract (documented for the frontend agent; see frontend-cancel-queued-message-handoff.md): HTTP: DELETE /conversations/:id/queue/:messageId -> 200 QueueCancelResponse { conversationId, cancelled, queue } (cancelled:false is a 200 idempotent no-op, not an error) WS: chat.queue.cancel { type:"chat.queue.cancel", conversationId, messageId } (additive to WsClientMessage). Fire-and-forget like chat.queue: success is confirmed by the message-queue SURFACE updating (the cancelled message leaves payload.messages). A missing-id cancel is a silent no-op (no surface update, no error). Malformed (empty conversationId/ messageId) -> chat.error. No new AgentEvent: a cancelled message never appears in the transcript (it never runs). The existing message-queue surface already reflects the post-cancel snapshot. Race-safe by construction: if the kernel drains the queue (steering) or carries it into a new turn before the cancel runs, the message is already gone -> cancel returns cancelled:false (a no-op). Version bump: @dispatch/transport-contract 0.23.0 -> 0.24.0 (additive: QueueCancelResponse + ChatQueueCancelMessage added to WsClientMessage). @dispatch/wire unchanged (QueuedMessage.id is the cancel target). No CLI command added: the CLI has no queue-listing affordance to discover a messageId, so a CLI cancel would have no input source. The HTTP DELETE is available for any non-WS client that knows the id (e.g. from a prior enqueue response's queue[]). Verification: tsc -b EXIT 0; vitest 2024 passed / 6 skipped (25 new tests); biome EXIT 0 (0 errors).
2026-06-28feat(cli): add --title flag to set tab title when summoningAdam Malczewski
Add a --title <title> flag to the dispatch CLI so a summoned agent's conversation/tab gets a human-readable title at creation time instead of the auto-derived default (Untitled until the first message append). The title is carried as an additive optional ChatRequest.title field (the HTTP client-server contract), parsed + trimmed server-side in parseChatBody, and persisted via the conversation store's setConversationTitle in the POST /chat route BEFORE the turn starts — so the title is set atomically with creation and before --open signals the frontend to open the tab. No second HTTP round-trip is needed. A whitespace-only title is treated as absent (auto-derive); a non-string title yields HTTP 400. A title-set failure is logged but never blocks the turn (the title is a nicety, the answer is not). The change is purely additive — no existing behavior or assertion changes. Verification: typecheck EXIT 0; test 2021 passed | 6 skipped | 0 failed; check EXIT 0 (12 pre-existing warnings in untouched files).
2026-06-28fix(vision): tell vision agents not to use tools, just describe images directlyAdam Malczewski
Kimi was trying to use Python tools to analyze images rather than just describing them. Updated both vision system prompts (consult_vision and image compaction) to explicitly instruct: do not use any tools unless specifically asked to — just use your vision to see the image and describe it directly.
2026-06-28fix(conversation-store): msgIdx collision merges messages across turns + ↵Adam Malczewski
reconcile drops thinking-only messages Root cause of tool-calls-in-thinking bug: append() assigns msgIdx as a LOCAL index (reset to 0 per call), but load() grouped chunks by msgIdx alone. Since the orchestrator persists messages one at a time (append([user]) at turn start, then append([assistant, ...toolResults]) per step), all single-message appends share msgIdx=0 and collapse into one giant user-role message. The model loses its prior assistant responses and tool-call history, falls back to text-based tool-call syntax inside reasoning_content, and the turn ends with finish_reason stop (no structured tool_calls detected). Fix 1 (store.ts load()): split message boundaries on role change too, not just msgIdx. Handles the alternating user/assistant/tool pattern correctly. Fix 2 (reconcile.ts hasContent): include thinking chunks as valid content so thinking-only assistant messages are not silently dropped on load. The buggy seq-14 output (assistant, thinking-only) was being deleted by reconcile, destroying evidence of the bug. Verified: load() on the affected conversation now produces 9 correct messages (was 3 merged). All 1999 tests pass. See notes/tool-call-in-thinking-bug.md.
2026-06-28docs(bug): investigate tool calls appearing in thinking + turn ends abruptlyAdam Malczewski
2026-06-28fix(predev): resolve merge conflicts between concurrency-fixes and ↵Adam Malczewski
workspace-star
2026-06-28Merge branch 'feature/workspace-star' into predevAdam Malczewski
# Conflicts: # packages/provider-concurrency/src/concurrency-manager.ts # packages/provider-concurrency/src/extension.ts
2026-06-28fix(concurrency): usage-gate fast-path overshoot + fetchUsage rejection ↵Adam Malczewski
safety + persist auto-reduce notice Bug 1 (overshoot): acquire() fast-path now queues while a recycle-poll is in flight (gatePolling), and the recycle defers its inFlight decrement until the usage poll resolves — holding inFlight inflated through the poll window so a concurrent caller cannot sneak through before upstream confirms room. The gated path admits exactly one waiter per fresh poll (grantOne), robust against stale upstream counts. Common-case throughput preserved: the fast-path still grants immediately when no poll is in flight. Bug 2 (unhandled rejection): fetchUsage is wrapped in safeFetchUsage — a throwing getUsage() is caught, treated as undefined (cooldown-only fallback), and reported via the new onUsagePollError opt (warn-level). No unhandled promise rejection can crash the process. Bug 3 (lost notice): loadLimits now calls restoreLimit (a new lower-level state-seeding method that does NOT clear autoReduced) instead of setLimit (which is a manual user action that clears the notice). The auto-reduce marker (autoReducedFrom) is persisted under auto-reduce:<providerId> and restored on activate via loadAutoReduce, so the frontend banner survives a restart. +6 tests covering each bug.
2026-06-28fix(workspace-star): clean up starred cache on workspace delete + warn when ↵Adam Malczewski
concurrency service absent Bug 1 (MEDIUM): In-memory starred cache leaks IDs for deleted workspaces. The starredWorkspaces Set in the concurrency manager never cleaned up when a workspace was deleted. FIX: the DELETE /workspaces/:id route now calls concurrencyService.notifyWorkspaceStarred(id, false) after deleting, so the deleted workspace ID is removed from the in-memory cache (preventing stale IDs and preventing a re-created workspace with the same slug from inheriting the old starred state). Bug 2 (MEDIUM): Stale in-memory priority when concurrency extension is absent. If provider-concurrency is not loaded, the star toggle persisted but the in-memory priority cache was never updated (the optional chaining ?. silently skipped the call). Already-queued agents kept their old priority until restart. FIX: the star/unstar routes now check if concurrencyService is defined and log a warning when it is absent, making the degraded behavior visible. The starred state still persists correctly — it just does not affect in-memory scheduling until the extension is loaded. Tests: +5 (star round-trip with concurrency notification, invalid slug 400, delete cleans up cache, absent-service warning log). All 1970 tests pass.
2026-06-28Merge branch 'dev' into feature/concurrency-fixesAdam Malczewski
2026-06-28merge: bring dev crash fixes into feature/workspace-starAdam Malczewski
Merge dev to restore production-critical crash fixes that were committed after the branch was cut: SSH pool permanent error listener, uncaughtException/ unhandledRejection guards in host-bin, MemoryMax=24G circuit breaker, memory telemetry logging, LSP disable precaution, and crash investigation notes. The provider-wrapper.test.ts:102 test passes — it was already fixed by the acquire() signature change (adding workspaceId parameter) that realigned the test arguments. No additional test fix needed. Verification: typecheck 0 errors, 1965 tests pass, biome clean.
2026-06-28Merge branch 'dev' into feature/workspace-starAdam Malczewski
2026-06-28feat(workspace-star): starred workspace priority for concurrency limitingAdam Malczewski
2026-06-28feat(concurrency-fixes): usage-gate + adaptive headroom + configurable cooldownAdam Malczewski
2026-06-28docs(crash): definitive findings + Gemini review reportAdam Malczewski
2026-06-28fix(ssh,host-bin): permanent pooled-client error listener + ↵Adam Malczewski
uncaughtException/unhandledRejection guards Root cause of the live production crash (exit-1 'Timed out while waiting for handshake'): the pooled ssh2.Client had no permanent 'error' listener after connect, so a post-connect ssh2 error escaped as an uncaught EventEmitter 'error' with no process-level guard. See notes/crash-investigation-findings.md §1. - packages/ssh/src/pool.ts: attach a permanent 'error' listener to the pooled client in buildConnection that sets state=error, logs (alias, message, level), and does not throw; cleanup() no longer removes it. - packages/host-bin/src/main.ts: add process.on('uncaughtException') (graceful shutdown after logging) and process.on('unhandledRejection') (log + continue), both logging message/stack, memory snapshot, activeConversations count, and timestamp so the failure site is observable.
2026-06-28docs(memory-leak): comprehensive handoff for OpenCode investigationAdam Malczewski
2026-06-28fix: disable LSP + change memory telemetry interval to 15sAdam Malczewski
- Disable LSP extension (import + CORE_EXTENSIONS) due to crashes - Make transport-http tolerate LSP being absent (optional getService) - Remove lsp from transport-http dependsOn manifest - Change memory telemetry sample interval from 60s to 15s
2026-06-28feat(observability): periodic memory-usage logging to localize leak sourceAdam Malczewski
2026-06-28feat(bin): apply-memory-limits.sh — apply MemoryMax cgroup limits to live ↵Adam Malczewski
service Hands the user a one-command script to apply the systemd MemoryMax/MemoryHigh cgroup limits to the live dispatch.service without a full reinstall. Patches in the real user (same as bin/install), daemon-reloads, and restarts.
2026-06-28fix(systemd): add MemoryMax=24G circuit breaker to prevent segfault crashesAdam Malczewski
The server has a memory leak (~2.5 GB/h) that eventually triggers a Bun runtime segfault. These cgroup limits turn the uncontrolled crash into a controlled OOM-kill → clean restart via Restart=on-failure. MemoryHigh=20G (soft throttle) + MemoryMax=24G (hard cap). Machine has 33.24 GB total; 24G leaves OS headroom.
2026-06-28docs(server-crash): investigate LSP-related server crashAdam Malczewski
2026-06-27fix(lsp): bound pushDiagnostics cache — evict oldest entries for unopened ↵Adam Malczewski
background files
2026-06-27fix(lsp): fix crashes (optional chaining, fs.watch error) + memory leak ↵Adam Malczewski
(document lifecycle) + leaked init promises
2026-06-27Merge branch 'feature/vision-handoff' into devAdam Malczewski
# Conflicts: # packages/session-orchestrator/src/extension.ts # packages/session-orchestrator/src/orchestrator.ts
2026-06-27feat(vision): prefix consultation tab titles with 'IMAGE - 'Adam Malczewski
When a non-vision model (e.g. GLM) calls consult_vision, the new Kimi consultation tab now shows 'IMAGE - <question>' instead of the bare question-derived title, making image-consultation tabs visually distinguishable from normal conversation tabs. - Add formatConsultationTitle(question) pure helper (pure.ts): prefixes 'IMAGE - ' and truncates the question to 80 chars (matching the conversation store's TITLE_MAX) with an ellipsis. - Add setConversationTitle dep to VisionHandoffDeps, wired in the extension to the conversation store's setConversationTitle. - Call it in consultVision BEFORE the turn starts so the title is correct from the first moment (the store keeps a non-'Untitled' title on first message append). Best-effort: a title-write failure logs a warning but does not break the consultation. - Tests: 4 pure + 2 service (title set + optional-dep graceful).
2026-06-27feat(vision): store images in tmp dir instead of SQLite — compact URLs + ↵Adam Malczewski
purge on compaction/close
2026-06-27feat(provider-concurrency): persist concurrency limits across reboots via ↵Adam Malczewski
host storage
2026-06-27feat(concurrency): add "queued" ConversationStatus — emit when request ↵Adam Malczewski
blocks on acquire, re-emit "active" when slot granted
2026-06-27feat(vision): image compaction for vision-capable models + global vision ↵Adam Malczewski
settings
2026-06-27fix(vision): detect umans kimi + qwen models as vision-capable (not just kimi)Adam Malczewski
2026-06-27feat(vision-handoff): model-directed consult_vision tool replacing ↵Adam Malczewski
auto-transcription
2026-06-27fix(vision-handoff): omit temperature on vision transcription call (Kimi ↵Adam Malczewski
rejects temperature: 0) The vision handoff hardcoded temperature: 0 for the transcription sub-call, but the Moonshot/Kimi vision model only allows temperature: 1 (or omitted), causing an HTTP 400 "invalid temperature: only 1 is allowed for this model" that blocked the entire image analysis for non-vision models like GLM 5.2. Fix: omit temperature entirely so each vision provider uses its own default — the truly universal, provider-agnostic approach (different providers have different temperature constraints).
2026-06-27feat(provider-concurrency): add release cooldown (200ms) to prevent N+1 ↵Adam Malczewski
overshoot from provider accounting lag
2026-06-27Merge branch 'dev' into feature/vision-handoffAdam Malczewski
2026-06-27Merge branch 'dev' into feature/provider-concurrencyAdam Malczewski