summaryrefslogtreecommitdiffhomepage
path: root/packages
AgeCommit message (Collapse)Author
2026-05-31feat(debug): wire LLM debug logger end-to-endAdam Malczewski
The debug-logger.ts module existed but was completely orphaned — none of its functions had any callsites, so DISPATCH_DEBUG_LLM=1 did nothing. Wires it in across the stack: - llm/debug-logger.ts: add wrapFetchWithLogging() that tees SSE bodies via TransformStream + response.clone() so we capture every chunk without draining the body the AI SDK consumes. Redacts authorization / x-api-key / cookie headers in logs. Also exports nextDebugSeq() so requests and log files share an id. - llm/provider.ts: all 3 factories (Claude OAuth, plain-API-key Anthropic, OpenAI-compatible) now pass fetch: wrapFetchWithLogging(globalThis.fetch). For Claude OAuth the wrap goes on the inner base fetch so logged bodies reflect the post-transform shape + Claude-Code session headers. Added tabId to ProviderConfig for log labelling. - agent/agent.ts: threads tabId through createProvider and emits logAgentLoop / logStepLifecycle / logStreamEvent at every meaningful point in the run loop — step start/end, tool count, every fullStream event. All are no-ops when DISPATCH_DEBUG_LLM is unset. - core/index.ts: re-exports the debug helpers. - tests/llm/provider.test.ts: switch one full-object equality assertion to property assertions so the test survives the new fetch: wrapper. Plumbing the env var into the container required three more fixes: - bin/up: re-export DISPATCH_DEBUG_LLM* so docker compose forwards them (compose only forwards vars referenced in the environment: block). Also pre-creates /tmp/dispatch/llm-debug and chowns it on first run so the container's UID-1000 bun process can write into it without EACCES. - docker-compose.yml: declare the debug vars on api.environment and bind-mount /tmp/dispatch/llm-debug:/tmp/dispatch/llm-debug so logs are inspectable from the host without docker exec. - docker/entrypoint.dev.sh: explicitly forward DISPATCH_DEBUG_* through the 'su -' login-shell barrier — su - resets the environment to TERM/ PATH/HOME/SHELL/USER/LOGNAME only, silently stripping everything else. This is why the vars appeared via 'docker exec env' (which spawns a new process inheriting the container env) but were absent from the actual bun process's /proc/<pid>/environ. bin/build: drop stray sudo for consistency with bin/up and bin/down.
2026-05-31feat: implement user agents (top-level tabs via summon)Adam Malczewski
- agent parameter is now required on summon tool - new top_level param spawns independent fire-and-forget user agent tabs - gated by perm_user_agent permission (UI checkbox added) - agent definition type validation (subagent vs user-agent slug mismatch) - context-aware error messages when agent slug not found - read_file_slice added to summon tool's allowed tools enum - updated and expanded summon tests
2026-05-30fix(agent): stream thinking for all adaptive Claude models, not just Opus 4.7Adam Malczewski
Extended thinking was gated on a hardcoded `model === "claude-opus-4-7"` check, so newer/other adaptive models (Opus 4.8, Opus/Sonnet 4.6) fell into the classic `thinking: { type: "enabled" }` branch. Adaptive models default thinking display to "omitted", so no thinking was streamed — the UI showed nothing for Claude while DeepSeek (a separate openai-compatible path) worked. Replace the string check with a pure helper `anthropicThinkingProviderOptions` that mirrors opencode's transform.ts detection: - adaptive (`type: "adaptive"`) for Opus 4.7+ (version-parsed) and Opus/Sonnet 4.6 (id substring; handles dash and dot forms); - `display: "summarized"` ONLY for Opus 4.7+ (they default to omitted and must be forced); Opus/Sonnet 4.6 stream thinking without it; - all other Claude models keep classic `enabled` + budgetTokens. Pure function (no provider/streamText/network), unit-tested directly: Opus 4.8 (the reported bug), Opus 4.7, Sonnet/Opus 4.6, Opus 4.5 + dated Sonnet (enabled), a future Opus 4.9 (proves version-parse), and effort->budget mapping.
2026-05-30fix(frontend): sidebar fill panels overflow with 3+ views openAdam Malczewski
Key Usage (plus Tasks and Cache Rate) used flex-1 + min-h-0, letting flex shrink the panel below its content's natural height. The content wrapper was a plain block, so inner scroll regions never got a bounded height and their bars/lists spilled into neighbouring panels or past the window edge. Drop the flex-1 fill entirely: every panel now sizes to its content and the sidebar's own overflow-y-auto handles scrolling.
2026-05-30chore(notes): collect loose root docs into notes/; add reconcile edge-cases noteAdam Malczewski
Move all loose root-level .md files (plans, reports, gemini reviews, incident notes) into a single notes/ directory, and update the doc-reference breadcrumbs in code comments/test labels to the notes/ path. Add notes/queue-interrupt-reconcile-edge-cases.md: documents why the queue/interrupt/turn-sealed reconcile path keeps surfacing edge cases (a catalog of the four review-pass bugs, the no-loss/no-duplicate invariants, the recommended membership-based reconcile refactor, and interleaving-test guidance).
2026-05-30feat(chunks): chunk-native frontend store with turn-sealed reconcile + ↵Adam Malczewski
per-chunk eviction Replace the stored ChatMessage[] with a chunk-native model: tab.chunks (sealed ChunkRow[]) + tab.live (transient in-flight turn buffer) + derived tab.renderGroups. This enables per-chunk eviction (trimming WITHIN a large turn) and raw-chunk pagination (loadOlderChunks), removing the whole-message eviction limitation. Backend: - Emit turn-start/turn-sealed around each turn; expose currentTurnId in the status snapshot. turn-sealed fires after the durable write (status:idle fires before it). - New GET /tabs/:id/chunks raw paginated endpoint (limit/before). - Wrap appendChunks in a single SQLite transaction. Frontend: - turn-sealed drives a turn-aware reconcile that folds the sealed turn into chunks while preserving a concurrent newer in-flight turn and pending queued messages; deferred while the user is scrolled up. - Stable turn-scoped render keys (${turnId}:${role}:${n}) avoid remount/flash. Reconcile correctness (three review passes): - preserve a concurrent newer turn when an earlier deferred reconcile flushes; - keep optimistic queued user messages (no loss); - turn-start backfill skips pending queued rows and tags only the turn initiator; - bind consumed interrupt messages to the in-flight turn so they collapse on seal (no lingering/duplicated bubble). Tests: chat-store reconcile/eviction/pagination suite; api chunks endpoint + events.
2026-05-30refactor(chunks): append-only chunk log with per-step cache-stable wireAdam Malczewski
Replace the message-as-container model with a flat, append-only chunk log. - chunks table (id, tab_id, seq, turn_id, step, role, type, data_json): one row per chunk; tool_call (assistant) and tool_result (tool) are SEPARATE rows linked by callId. Message/turn are derived groupings, not stored. - chunks/transform.ts: DB-free explode (Chunk[] -> rows) / group (rows -> messages), shared by backend and the browser frontend. - Cache fix: toModelMessages segments each turn at tool-batch boundaries into stable [assistant, tool] pairs per step, so earlier steps serialize byte-identically across requests (kills the prompt-cache churn). - agent-manager persists a turn's chunks on seal (once), discarding a failed fallback attempt's partial chunks; rebuilds agent history from the log. - GET /messages windows the log by chunk seq then groups; loadMoreMessages merges a turn split across the window boundary by turnId. - One-shot migration drops the legacy messages table and clears tabs; settings/credentials/keys/usage preserved. Full suite green (317 tests); biome, tsc, and svelte-check clean.
2026-05-30feat(cache): Anthropic prompt caching, usage telemetry, and Cache Rate viewAdam Malczewski
- send prompt-caching + oauth anthropic-beta headers on the Claude OAuth provider - restructure the OAuth request body (billing header, identity split, relocate third-party system prompt to the first user message) to match Claude Code - apply rolling cache_control breakpoints and group a turn's tool results into a single role:tool message for correct breakpoint placement - emit per-step usage events (cache read/write split) and add the Cache Rate sidebar panel - dedup byte-identical tool calls within a single batch
2026-05-29fix(claude): eliminate /home mount race that blanks Claude credentials at bootAdam Malczewski
On hosts where /home is a separate filesystem, the dispatch-api service could start before /home was mounted. The API's first DB access then failed (EACCES: mkdir '/home/tradam'), Claude account discovery silently caught the error and left claudeAccounts empty, and -- because discovery only ran in the constructor -- it stayed empty for the whole process lifetime. Every Claude message then fell back to the deepseek-v4-flash / empty-key defaults, producing a 401 'Missing API key' from OpenCode Zen. Fixes: - s6 run script waits (capped ~30s) for /home/tradam before exec'ing bun; passes instantly where /home is on the root filesystem. - systemd unit gains RequiresMountsFor=/home and After=...home.mount. - agent-manager re-runs _refreshClaudeAccounts() on config hot-reload and lazily on an empty cache in the Anthropic path, so a process that lost the boot race self-heals on the next request instead of staying broken.
2026-05-29Make chat input an auto-growing textareaAdam Malczewski
Replace the single-line input with a textarea that starts at one line, grows vertically as text wraps (up to 7 lines), then scrolls. Override daisyUI's .textarea min-height:5rem so the resting state is one line. Enter sends, Shift+Enter inserts a newline.
2026-05-29fix: preserve per-tab working directory on sendAdam Malczewski
refreshAgentConfig (added to pick up model/key edits on send) also overwrote the tab's workingDirectory with the agent's default cwd every time, discarding the directory the user set via setWorkingDirectory. Remove that line so refreshing agent config no longer clobbers the per-tab working directory. Also merge top-level wishlist.md into dispatch/wishlist.md and drop the items completed today (subagent summon, stop button).
2026-05-29fix: override pointer-events on stop button spinner for iOS touch supportAdam Malczewski
daisyUI's .loading class sets pointer-events:none on the spinner. On iOS Safari, when a child inside a <button> has pointer-events:none, the synthesized click event from touch sequences can fail to dispatch to the parent button entirely - a well-known WebKit quirk. Since the spinner covers ~20x20px of the button, mobile taps that land on it are silently dropped. Setting pointer-events:auto on the spinner lets touch events pass through to the parent <button> correctly.
2026-05-29feat: stop generation button with abort signal plumbingAdam Malczewski
- Add POST /chat/stop endpoint on API - Thread abortSignal from agent-manager through Agent.run() to streamText - Thread abortSignal option through the Agent.run() signature - Emit status:idle on stopTab() so frontend WS gets the update - Add stopGeneration() store method on frontend tabStore - Add stop button in ChatInput (btn-sm lg:btn-xs for mobile tap target) - Add tests for /chat/stop endpoint - Refactor processMessage to pass abortSignal to agent.run
2026-05-29fix: handle unavailable tool calls via native v6 tool-error event, not ↵Adam Malczewski
synthetic invalid tool - Removed __invalid__ tool definition, experimental_repairToolCall, and v4-era NoSuchToolError catch block — AI SDK v6 already emits a native tool-error stream event with the original tool name - Added synthesizeResidualToolResults() helper to fill orphaned tool-call IDs with isError: true results for abort/error terminal paths - tool-error handler now break's instead of return's — lets sibling tools execute normally via the manual executor loop - Added final safety net after execution loop to catch any genuinely orphaned tool-call IDs before round-tripping to the LLM - Propagated isError through toModelMessages so error results are properly flagged in conversation history - Updated tests: tool-error event now continues to idle (not error), added sibling-orphan prevention test
2026-05-29fix: include agent_id in foreground summon results for Open Tab button; ↵Adam Malczewski
preserve slider selection on agent config refresh
2026-05-29feat: subagent summon — catalog filter, error hints, system prompt, ↵Adam Malczewski
AgentBuilder default, SubAgent mode display - Filter summon tool catalog to is_subagent-flagged agents only - Return fresh subagent list in error when slug not found - Add subagent hint to system prompt when summon tool available - Default is_subagent checkbox to true in AgentBuilder - Fix tab-created event to include agentSlug and agentModels - Add SubAgent read-only mode to ModelSelector with model slider
2026-05-29fix: refresh agent config on send; widen fallback retry detectionAdam Malczewski
- Refresh agent config from API before sending a message so edits in AgentBuilder (changed keyId/modelId/agentModels) take effect immediately on existing tabs instead of using stale snapshots - Broaden isRetryable check to also match 'usage limit' and 'exhausted' so fallback keys are actually tried on quota errors
2026-05-29feat: disappearing chat history — chunk-limited frontend window with ↵Adam Malczewski
backend pagination Frontend keeps only a bounded window of chunks in memory (configurable via settings slider, default 100). Older messages are evicted when at the bottom and re-fetched from the backend on scroll-up. - Backend: paginated GET /tabs/:id/messages with ?limit=N&before=seq - Store: evictMessages trims oldest messages until total chunks ≤ limit - Store: loadMoreMessages fetches next page and prepends with dedup - ChatPanel: smart scroll hooks trigger eviction on return-to-bottom - ChatPanel: onNearTop loads older history with scroll-position maintenance - Settings: chunk limit slider in Memory section - Fix: oldestLoadedSeq recalculated after eviction (pagination cursor stays valid) - Fix: seq preserved on ChatMessage for cursor tracking - Fix: scrolledUpTabs cleaned up on tab switch (no memory leak) - Fix: evictMessages reads appSettings.chunkLimit directly (live updates)
2026-05-28fix(core): normalize tool schemas for Anthropic, add toolChoice=auto; ↵Adam Malczewski
feat(summon): agent definition support; docs: cc/ research findings - registry.ts: add normalizeForAnthropic() to strip , additionalProperties, default, nullable from zodToJsonSchema output so Anthropic doesn't silently reject tool definitions - agent.ts: add toolChoice=auto for Claude OAuth to prevent Opus thinking forever without calling tools - summon.ts: add agentSlug parameter, build agents catalog in description, add toAvailableAgents helper - agent-manager.ts: wire agent definition loading into spawnChildAgent, agent model fallback - loader.ts: export loadAgent, expandAgentToolNames, getAgentDirPaths; add getAgentDirPaths for permission gate - agent.ts: auto-allow read-only tools in agent definition directories - packaging/PKGBUILD: exclude ARM64 prebuilds from x86_64 package - cc/: research findings on Claude Opus tool calling issues - tests: loader tests, summon tool tests
2026-05-28fix(core): strip stale [USER INTERRUPT] from LLM history; inject into last ↵Adam Malczewski
tool of batch The interrupt block embedded in a tool-result was persistent in the assistant message history, so the imperative 'You MUST address these before continuing' got re-evaluated as fresh on every subsequent LLM step. Result: the model repeatedly thought about and re-acknowledged the same interrupt 5-10+ times per chat (verified in production DB traces — e.g. tab 4c5727aa had 11 thinking chunks quoting a single interrupt verbatim). agent.ts (toModelMessages): strip [USER INTERRUPT] from every tool- result except the one in the freshest tool-batch (last chunk of the last assistant message, which itself must be the last message). The strip is a serialization-time transform only — this.messages, the DB row, and the UI display all keep the full text. The LLM sees the imperative exactly once: the step immediately after injection. agent.ts (tool execution loop): batch queued messages across the group's tool calls and inject them only into the LAST executable tool's result. Previously the first tool to dequeue won; now the interrupt lands in a single deterministic spot regardless of timing. Tool-level handlers (run-shell/youtube/retrieve) are untouched — they still embed their own interrupt text when they background work. Also fix pre-existing tabs.test.ts: it referenced a getDescendantIds function that didn't exist (added: BFS, leaf-first, cycle-safe, skips archived) and imported bun:sqlite directly which vite couldn't resolve (rewrote with a minimal FakeDatabase + vi.mock pattern matching the rest of the suite).
2026-05-28feat: add agent status indicator in chat input, db tabs tests, and service ↵Adam Malczewski
management script
2026-05-28feat(frontend): persist sidebar panel layout across browser refreshes via ↵Adam Malczewski
localStorage Carries the in-app sidebar layout (which views are open and in what order) across page reloads. Closes the natural follow-up to the tab- restore feature in d2e2e67: tabs survive, but until now the sidebar panels (Chat Settings / Tasks / Skills / Tools / etc.) reset to a single default panel on every load. Scope (explicitly bounded by the user): - Persistence target: localStorage. Matches the precedent for UI preferences (`dispatch-theme`, `dispatch-api-url`). Per-device layout; no backend round-trip. - sidebarOpen (the Header button that hides the whole sidebar column) is NOT persisted; always starts open on every load. - No drag-to-reorder UI added — persistence captures whatever order the user established via the existing add/remove buttons. Implementation: • New `packages/frontend/src/lib/sidebar-storage.ts` — pure functions `loadSidebarPanels(): string[]` and `saveSidebarPanels(selected: string[]): void`. localStorage key is `dispatch-sidebar-panels` (canonical `dispatch-` prefix). `loadSidebarPanels` is defensive against every failure mode (missing key, malformed JSON, non-array root, non-string entries, empty-after-filter, localStorage.getItem throwing under SecurityError). Returns a fresh array on every call so mutations by the caller don't pollute the module-level default constant. `saveSidebarPanels` swallows storage errors (quota / disabled / SecurityError) — best-effort. • `packages/frontend/src/lib/components/SidebarPanel.svelte`: seed the `panels: $state` from `loadSidebarPanels().map(s => ({ id: nextId++, selected: s }))` and add a `$effect` that calls `saveSidebarPanels(panels.map(p => p.selected))` whenever `panels` changes. The session-ephemeral `id` field is regenerated on every mount; only the `selected` strings round-trip. • Existing addPanel / remove / dropdown handlers untouched — they all reassign `panels` (`panels = [...panels, ...]`, `panels = panels.filter(...)`, `panels = panels.map(...)`), which triggers the new $effect. The minimum-one-panel invariant (X button hidden on idx 0) is preserved at the UI layer and reinforced by the loader's empty-fallback to the default layout. Tests: 15 new in `packages/frontend/tests/sidebar-storage.test.ts` — load with empty / valid / malformed / non-array / null / mixed-type / empty-after-filter / throwing-getItem; save round-trip; save error swallowing; overwrite semantics; empty-save / load-fallback; mutation isolation. Frontend total: 59 tests (was 44; +15). API 31, core 168 unchanged. Typecheck clean (svelte-check 0 errors), biome clean (126 files). Gemini code review (yolo mode, prompt-level write restriction to report.md only): SHIP, no findings.
2026-05-28feat: restore tab layout + in-flight chunks on browser reopen; agents keep ↵Adam Malczewski
running in background Implements the 'background-running agents + restore-layout-on-reopen' feature. Full design and parallel-implementation plan in `plan-bg-restore.md`; Gemini code review (SHIP verdict, no findings) in `report.md`. User-visible behaviors: 1. Browser-close keeps agents alive. If an agent is mid-stream when the browser closes / reloads / loses the network, it continues processing on the backend. (This was already the case in code — agents run fire-and-forget in app.ts:77-79 — but it was previously pointless because the UI never restored the tab to receive the output.) 2. Layout restore on browser reopen. Every tab that existed at the time the window was closed is restored, in original `position` order, with full persisted message history. Tabs whose agents finished while disconnected appear with the completed message. Tabs whose agents are still running appear streaming live — the in-flight assistant message is reconstructed from the backend's in-memory `currentChunks` (sent over the wire on connect) and accumulates new deltas as they arrive. 3. Explicit tab-close cancels + forgets. Clicking the X still cancels the agent (existing `stopTab` in DELETE /tabs/:id) and archives the row (`is_open = 0`), so it is not restored. No change to that path. The gap that the implementation closes: previously, App.svelte:onMount unconditionally called `createNewTab()` with a fresh UUID, ignoring every existing row in the `tabs` table. Every browser open was a clean slate. The DB had the conversation history but no way for the UI to discover it. Implementation: • New `TabStatusSnapshot` interface in packages/core/src/types/index.ts (auto-exported via existing `export * from "./types"`): interface TabStatusSnapshot { status: AgentStatus; currentChunks?: Chunk[]; // present iff running currentAssistantId?: string; // present iff running } • `agent-manager.ts:getAllStatuses()` rewritten to return `Record<string, TabStatusSnapshot>` (was `Record<string, AgentStatus>`). For running tabs only, attaches a defensive shallow copy of `tabAgent.currentChunks` (the live streaming array the per-message loop appends to) plus the DB id of the in-flight assistant message. The defensive copy is the consumer's to mutate. Idle / error tabs get `{ status }` only. `GET /status` and the WS `onOpen` snapshot both pick up the new shape automatically — neither call site changed. • Frontend mirror of `TabStatusSnapshot` in packages/frontend/src/lib/types.ts; `AgentEvent.statuses` variant updated to use `Record<string, TabStatusSnapshot>`. • New `hydrateFromBackend()` on the tab store (packages/frontend/src/lib/tabs.svelte.ts). Sequence on app mount: 1. Bail with 0 if `tabs.length > 0` (hot-reload idempotency). 2. GET /tabs → list of `is_open=1` rows in `position` order. 3. GET /status → in-flight TabStatusSnapshot map. 4. GET /tabs/:id/messages for each tab in parallel via Promise.all → persisted ChatMessage[]. 5. Build the Tab objects, splicing the snapshot's live chunks into the in-flight assistant message for every running tab (two paths: merge into the existing DB row with matching id, or append a fresh in-flight message if no row matches). 6. `tabs = restored; activeTabId = restored[0]?.id ?? null;` Every fetch is wrapped in try/catch so one tab's failure can't destroy the whole restore pass. • WS `statuses` handler in `tabs.svelte.ts:handleEvent` rewritten for the new shape. Still fires `reloadTabMessagesFromApi` on the desync case (frontend thinks running, backend says idle — the pre-existing recovery path is preserved). When backend says running, seeds in-flight chunks into the assistant message matching `snap.currentAssistantId` (creating it if needed). When backend says non-running, clears `isStreaming` on the previous in-flight message and nulls `currentAssistantId`. • `App.svelte:onMount` now awaits `tabStore.hydrateFromBackend()` before deciding whether to fall back to `createNewTab()`. Fallback condition is the doubly-defensive `restored === 0 && tabStore.tabs.length === 0`. `wsClient.connect()` fires in parallel with hydration — the resulting WS `statuses` event is per-tab idempotent against the hydrated state, so there is no race even if it arrives mid-hydration. What was NOT done (deliberately, deferred to wishlist): • Pre-existing inconsistency: core `AgentStatus` includes "waiting_for_key" but frontend `TabStatusSnapshot.status` uses only the existing 3-state pattern ("idle" | "running" | "error"). Not introduced here; mirrored the existing precedent. • Restored tabs use defaults for `reasoningEffort`, `agentSlug`, `agentScope`, `agentModels`, `workingDirectory` — these are not in the DB `tabs` schema. Future schema expansion. • Per-delta DB flushing — not needed; the in-memory snapshot covers the gap between flushAssistant calls. • LocalStorage cache of tab ids — backend DB is the source of truth. Process notes: • Implemented via parallel programmer subagents (flash agents were requested but unavailable in this environment — substituted with "programmer" agents, which share the "reads a plan, implements a single step" charter). Backend (Segment A: getAllStatuses + 5 tests) and frontend (Segment B: types + hydrateFromBackend + statuses handler + onMount + 8 tests) ran disjoint-file-ownership in parallel. • Gemini code review (yolo mode for tool access, explicit prompt-level write restriction to `report.md` only) returned a SHIP verdict with no findings against the plan. • Self-review surfaced one followup gap that Gemini's earlier plan-mode pass also caught: no explicit test for `/tabs/:id/messages` failure isolation. Added a test covering both HTTP-500 and network-error variants alongside a healthy tab, asserting per-tab failures don't destroy the whole restore. Tests: • api/tests/agent-manager.test.ts: +5 (snapshot empty record, idle-tab field omission, running-tab field inclusion, defensive copy invariant, omits chunks for running tab with null currentChunks). 31 total (was 26). • frontend/tests/chat-store.test.ts: +9 (restore-with-messages, in-flight seeding, /tabs failure → 0 returned, empty /tabs array, idempotency when tabs already exist, idle-status when /status omits, running-snapshot statuses handler seeding, idle-snapshot statuses handler clearing, per-tab failure isolation across HTTP-500 and network-error). 44 total (was 35). Totals: 243 tests across 3 packages all green; typecheck clean on core + api + frontend; biome clean across 124 files.
2026-05-28fix(api): pre-populate Agent.messages from DB on construction so model ↵Adam Malczewski
switches preserve chat history Before this change, swapping the model mid-conversation via the sidebar slider lost all prior turns: the new model saw only the current user message and treated the conversation as brand-new. Root cause: `getOrCreateAgentForTab` invalidates the cached Agent (`tabAgent.agent = null`) whenever the effective keyId, modelId, permission key, or working directory differs from the cached values. The replacement Agent was then constructed with `messages: []` and the post-construction step that loads prior turns from the SQLite `messages` table simply did not exist. `processMessage` had already appended the current turn's user message to the DB (line 960) before calling `getOrCreateAgentForTab` (line 1015), so the DB held the full context — it was just never read. Fix: after every `new Agent(...)` in `getOrCreateAgentForTab`, call `getMessagesForTab(tabId)`, walk backwards to the most recent user-role row, and assign all strictly-prior rows to `tabAgent.agent.messages`. The walk-backwards strategy correctly handles two boundary cases: 1. Simple model switch — last DB row is the current user message; drop it (`Agent.run()` will push it again at line 546). 2. Agent-mode auto-fallback retry — last DB row may be a partial assistant response flushed by the previous failed attempt; we drop both that and the current user message in one pass. System-role rows (config-reload notices, etc.) are preserved verbatim; `toModelMessages` already strips them before the wire payload, so this is safe. The fix covers every Agent-reconstruction trigger, not just the model slider: - Sidebar model/key change (the reported case) - Permission setting change - Working-directory change (`processMessage` line 951) - dispatch.toml config-watcher reload (lines 236–237) - Skills directory watcher reload (lines 249–250) - `stopTab` after user cancellation (line 775) If `getMessagesForTab` throws (e.g. DB locked, schema mismatch), we swallow the error and leave `messages: []` — matching pre-fix behaviour for that case so this commit never regresses. Tests (+6 in `packages/api/tests/agent-manager.test.ts`, total 26): - pre-populates Agent.messages from DB history - leaves messages empty when DB has only the current turn (first msg) - excludes a partial assistant trail from a prior fallback attempt - preserves system-role rows in pre-populated history - survives a getMessagesForTab failure without crashing - reloads history on every Agent reconstruction (simulated slider switch from Opus to DeepSeek across two processMessage calls) The test rig was extended with: - `fakeMessagesByTab` map + `setFakeMessages` helper letting tests inject arbitrary DB rows for the mocked `getMessagesForTab`. - `constructedAgents` array captured at `run()` entry (not at construction) so each test sees the post-pre-populate snapshot; the production code reassigns `agent.messages` after `new Agent()` returns, so capturing at construction yielded a stale empty array. - Pluggable `runImpl` hook for tests that want a custom event stream (not yet exercised; staged for the next round of agent-mode fallback tests). Totals: 229 tests across 3 packages all green; typecheck clean on core + api + frontend; biome clean across 124 files.
2026-05-28refactor(core): upgrade ai-sdk v4 → v6 + Anthropic/openai-compatible ↵Adam Malczewski
reasoning round-trip + max-thinking budget audit Migrates the LLM stack from [email protected] + @ai-sdk/[email protected] + @ai-sdk/[email protected] to [email protected] + @ai-sdk/[email protected] + @ai-sdk/[email protected]. Full design in plan-v6-upgrade.md; two rounds of Gemini code review captured in report.md. Motivation: the recurring 'reasoning-signature without reasoning' error on Claude Opus 4.7 was a v4 SDK artefact — @ai-sdk/[email protected] emitted Anthropic signature_delta as a separate stream chunk that orphaned when the model produced a signed-but-empty thinking block, and our chunk store had no signature field so the round-trip back to Anthropic was rejected on the next turn. In v6, signatures arrive inside providerMetadata on the reasoning-end event, and the orphan-signature class of bug is gone at the SDK level. Core changes: • ThinkingChunk gains optional metadata?: Record<string, unknown> (the v6 providerMetadata blob). A non-undefined metadata 'seals' the chunk: subsequent reasoning-delta opens a new chunk rather than extending the sealed one. • AgentEvent gains { type: 'reasoning-end'; metadata? } (replaces the v4 reasoning-signature variant). • toModelMessages (replaces toCoreMessages): - returns ModelMessage[] (was CoreMessage[]) - thinking → { type: 'reasoning', text, providerOptions: metadata } - tool-batch entries → { type: 'tool-call', input } (was 'args') - tool results → { output: { type: 'text', value } } ToolResultOutput • Claude OAuth uses createAnthropic({ authToken }) natively — no more custom-fetch x-api-key → Bearer swap. • rewriteBodyForOpus47 deleted — Opus 4.7 adaptive thinking is native via providerOptions.anthropic.thinking = { type: 'adaptive' }. • V1 middleware → V3 (specificationVersion: 'v3'). • v4-era normalizeMessages openai-compatible middleware deleted; the v6 openai-compatible provider extracts reasoning_content natively from { type: 'reasoning' } content parts. • applyAnthropicStructuralNormalisations (mirrors opencode provider/transform.ts:53-148): drops empty text/reasoning parts, scrubs non-[a-zA-Z0-9_-] toolCallIds, splits [tool-call, non-tool] assistant turns (Anthropic rejects tool_use followed by text). • applyOpenAICompatibleReasoningNormalisation (mirrors opencode transform.ts:217-249): lifts reasoning text into providerOptions.openaiCompatible.reasoning_content (always, even empty). Solves DeepSeek 'The reasoning_content in the thinking mode must be passed back' — the v6 SDK skips emitting reasoning_content when text is empty (dist/index.mjs:245), but DeepSeek requires the field present once thinking was used. • Tools: tool({ inputSchema: jsonSchema(zodToJsonSchema(...)) }) (was parameters: ZodSchema). AI SDK tools have no execute callback — the agent runs tools manually for permission prompts and shell-output streaming. New dep: zod-to-json-schema@^3.25.2. • fullStream event loop rewritten for v6 event shape: text-delta (text not textDelta), reasoning-start/delta/end, tool-input-*, tool-call (input not args), tool-result, tool-error (new), abort (new), start-step/finish-step, finish. Max-thinking audit (matches opencode transform.ts:642-671 budgets): • Claude enabled-thinking max budget 16000 → 31999 (Anthropic ceiling) • Claude enabled-thinking high budget 10000 → 16000 • maxOutputTokens 'budget + 8000' → fixed 32000 (matches opencode's OUTPUT_TOKEN_MAX; model self-allocates thinking vs response within) • Opus 4.7 adaptive thinking gains display: 'summarized' and sibling effort field (without these, thinking content is hidden by Anthropic and the model barely thinks). Frontend mirrors: • types.ts — ThinkingChunk.metadata?, AgentEvent reasoning-end • tabs.svelte.ts — routes reasoning-end through applyChunkEvent • ChatMessage.svelte — hides empty thinking chunks; hides the entire assistant bubble when no chunk has renderable content Gemini-review-driven fixes: • tool-error and abort stream events now surface as error chunks (were silently ignored) • toolCallId scrubbing pass (opencode transform.ts:96-122 parity) • Empty-reasoning-cull explicit test coverage for both Anthropic structural normalisation and DeepSeek path Test counts (223 tests across 3 packages, all green): • tests/chunks/append.test.ts: 44 (was 38) — reasoning-end sealing, orphan walk-back, multi-block interleaving • tests/agent/agent.test.ts: 24 (was 5) — exhaustive v6 event mappings, structural normalisations, signature/reasoning_content round-trip, tool-error/abort branches, DeepSeek scenario, empty reasoning edge case • tests/llm/provider.test.ts: 9 (was 22) — dropped 13 obsolete v4 middleware tests; new minimal tests confirm no middleware wrapping on default openai-compat path and that createAnthropic gets authToken vs apiKey correctly for OAuth vs api-key flows • tests/tools/registry.test.ts: 10 (was 4) — v6 tool() contract (inputSchema, no execute, JSON Schema for nested zod) • packages/api/tests/agent-manager.test.ts: 12 (was 7) — mock Agent emits v6 reasoning events; reasoning-end broadcast + ordering • packages/frontend/tests/chat-store.test.ts: 35 (was 32) — reasoning-end flow through Svelte $state store typecheck clean (tsc --noEmit on core + api, svelte-check on frontend), biome clean across 124 files.
2026-05-27refactor(packaging): split into dispatch/dispatch-systemd/dispatch-s6, ↵Adam Malczewski
separate dispatch-electron, add bun-based frontend serve PKGBUILD is now a split package producing three .pkg.tar.zst files in one makepkg run: - dispatch base application files (/opt/dispatch), CLI wrappers (dispatch-api, dispatch-frontend), env configs (/etc/dispatch/dispatch-api.conf, /etc/dispatch/dispatch-frontend.conf) - dispatch-systemd systemd user units for dispatch-api + dispatch-frontend (conflicts with dispatch-s6) - dispatch-s6 s6-rc service pipelines (-srv + -log) for both services (conflicts with dispatch-systemd) The Electron desktop wrapper moved to its own self-contained PKGBUILD at packaging/electron/, producing dispatch-electron. It bundles its own copy of the frontend dist + electron entry points under /opt/dispatch-electron and does not depend on the base dispatch package, so users can install it standalone and point VITE_API_URL at any backend at build time. Files under packaging/ are now read directly from ${_projectdir}/packaging/ rather than via source=(); the two s6 service dirs share basenames (run, type) which would collide inside ${srcdir}. New frontend static server: packages/frontend/serve.ts uses Bun's built-in HTTP server (no extra runtime deps) with SPA fallback to index.html and path-traversal protection. PORT/HOST/DIST_DIR overridable via env. Exposed as 'bun run --cwd packages/frontend serve' and via /usr/bin/dispatch-frontend. Build scripts: - bin/build-pkg now prints the freshest built packages - bin/install-pkg installs dispatch + dispatch-systemd by default; accepts package names or --all; searches both packaging/ and packaging/electron/ - bin/build-pkg-electron new, builds the electron split - bin/build-pkg-windows replaces bin/windows-pkg; drops the hard-coded WSL copy step, just prints the win-unpacked path .gitignore extended to cover packaging/*.tar.zst and packaging/electron/{src,pkg,*.pkg.tar.zst,*.tar.zst}.
2026-05-27fix(core): handle empty file in read_file line countingAdam Malczewski
2026-05-27fix(api): apply correct model+baseURL when API key env var is missing, ↵Adam Malczewski
preventing silent fallback to OpenCode Go endpoint
2026-05-27test(frontend): drive tabStore through real $state via exported ↵Adam Malczewski
createTabStore + handleEvent (replaces POJO harness)
2026-05-27fix(frontend): structuredClone→$state.snapshot, WS reconnect resyncs from ↵Adam Malczewski
API, surface callback errors instead of swallowing
2026-05-27fix(frontend): openAgentTab reads chunks[] from wire; copyConversation ↵Adam Malczewski
includes store-state + per-message chunk summary for diagnosability
2026-05-27refactor: ChatMessage.chunks[] union — interleaved thinking, tool ↵Adam Malczewski
batching, error/system chunks
2026-05-27feat: tool-output truncation+spill, read_file pagination, read_file_slice, ↵Adam Malczewski
symlink-safe path resolution
2026-05-24fix: prompt caching, OpenCode Go MiniMax/Qwen support, Opus 4.7 thinking, ↵Adam Malczewski
SDK compat - Implement Anthropic prompt caching: first system message + last 2 non-system messages get cache_control: ephemeral, mirroring OpenCode's applyCaching strategy. Move system prompt inline into messages array so providerOptions can attach. - Add opencode-anthropic provider variant routing MiniMax/Qwen models through the /messages endpoint with x-api-key auth, distinct from the Claude OAuth flow's Bearer auth and Claude Code mimicry. - Split isAnthropic into isClaudeOAuth (billing header, mcp_ tool prefix, thinking config) and usesAnthropicSDK (cache markers) so non-OAuth Anthropic-format gateways get the right treatment. - Pin @ai-sdk/anthropic to ^1.2.12: v3 returns LanguageModelV3-spec models that ai v4's streamText rejects at runtime ('AI SDK 4 only supports models that implement specification version v1'). Drop unnecessary V1 casts. - Restore Opus 4.7 extended thinking by rewriting the outgoing /messages body in the Claude OAuth fetch interceptor: inject thinking: { type: 'adaptive' } (v1 SDK can't emit it), strip temperature/top_p/top_k (Anthropic rejects them with thinking enabled). Gated on max_tokens > 4096 so effort=none still works. - Bump MAX_STEPS from 10 to 50 to align with AI SDK's stepCountIs(20) default and reduce mid-task halts. - Fix pre-existing typecheck errors in agent-manager.ts (entry/nextEntry narrowing), app.ts (agentModels body field), KeyUsage.svelte (m guards), and a TS2742 in provider.ts via explicit ModelFactory return type. - buildFallbackSequence now always returns at least one entry so processMessage runs the agent loop even without keyId/modelId (fixes 4 broken agent-manager tests).
2026-05-23feat: google gemini provider, adaptive thinking for opus 4.7, model search ↵Adam Malczewski
filter - Added Google (Gemini) as a provider: add-key UI, env var resolution via resolveApiKey, usage tracking via native models endpoint + gemini.google.com cookie scraping - @ai-sdk/anthropic upgraded to v3 (adaptive thinking support) with LanguageModelV1 cast for ai v4 compat - Claude Opus 4.7 uses adaptive thinking (type: adaptive); all other models keep explicit budget tokens - Model selector modal: search filter with space matching dash/underscore - Copy button: all tool results truncated at 300 chars - Sidebar layout fix: Claude Reset panel removed from flex-1 fill to prevent overlap
2026-05-23fix: sidebar panel layout overlap by removing Claude Reset from flex-1 fillAdam Malczewski
- SidebarPanel root div now has min-h-0 so sidebar scroll container can constrain it - Claude Reset removed from flex-1 fill list; its wake schedule grid is fixed-size - Only Key Usage and Tasks remain as flex-1 fill panels
2026-05-23feat: fallback model range slider with live label, model-changed eventAdam Malczewski
- Added model-changed event: backend emits it on fallback, frontend updates tab keyId/modelId - Range slider embedded inside active agent card when >1 model configured - Live label updates on drag (oninput), backend call only on release (onchange) - Slider auto-positions when fallback occurs via model-changed WS event
2026-05-23feat: key fallback using agent models[] hierarchy, background tool modes, ↵Adam Malczewski
copy truncation - Agent rate-limit fallback now iterates through agent's configured models[] in strict order - Frontend sends agentModels with each /chat request; backend uses buildFallbackSequence() - Emits notice event on fallback so chat shows which key failed and what's being tried next - Child agents inherit parent's agentModels for fallback - Added statusCode propagation from AI SDK errors for programmatic 429 detection - Copy button truncates all tool results at 300 chars (was 200 for 4 specific tools) - run_shell, summon, youtube_transcribe: background mode support - summon: blocking mode by default with getResult callback
2026-05-23feat: relative working directory support and subagent tab cwd propagationAdam Malczewski
- Resolve relative cwd paths (e.g. ./subtask) against parent's working directory at runtime - check-dir endpoint resolves relative paths and returns the resolved absolute path - AgentBuilder shows resolved path below input for relative paths, updated helper text - tab-created event now includes workingDirectory so subagent tabs display their cwd in sidebar - Add workingDirectory to tab-created AgentEvent type definition - spawnChildAgent stores resolved absolute path instead of raw relative path
2026-05-23feat: add is_subagent flag to agents, fix all lint/type/test issuesAdam Malczewski
- Add is_subagent checkbox to agent editor; subagents are hidden from Chat Settings - Add is_subagent field to AgentDefinition type, TOML serialization, and API route - Filter subagents from ModelSelector agent list - Fix all biome lint/format errors across codebase (useLiteralKeys, noNonNullAssertion, noExplicitAny, formatting, import sorting) - Fix svelte-check errors (type narrowing in SkillsBrowser, ToolPermissions, SidebarPanel) - Fix a11y warnings in App.svelte (label-control associations) - Fix test mocks missing BackgroundShellStore, BackgroundTranscriptStore, createWebSearchTool, createYoutubeTranscribeTool - Update stale 409 test to match current message-queuing behavior - Exclude packaging/ and release/ dirs from biome to avoid linting stale build artifacts
2026-05-23feat: youtube_transcribe blocks with polling, interruptible with background ↵Adam Malczewski
retrieve - youtube_transcribe now polls until transcript is ready (waits estimated_seconds - 2s, min 2s) - Times out after 10 minutes of polling - When user interrupts, polling continues in background with youtube_transcribe_<uuid> job ID - BackgroundTranscriptStore holds polling jobs, retrieve tool resolves them - ToolCallDisplay shows 'interrupted' badge (blue) when result contains [USER INTERRUPT] - Applies to all interruptible tools: run_shell, youtube_transcribe, retrieve
2026-05-23feat: web_search + youtube_transcribe tools, shell interrupt backgrounding, ↵Adam Malczewski
fixes - Add web_search tool (Firecrawl POST to /v1/search with query, limit, lang, country, scrapeOptions) - Add youtube_transcribe tool (GET to transcriber service, handles completed/queued/failed statuses) - Both tools registered for parent agents (always) and child agents (permission-gated) - Added to summon enum, TOOL_DESCRIPTIONS, and core exports - Shell interrupt: run_shell now races against user queue interrupt - When interrupted, command continues in background with run_shell_<uuid> job ID - BackgroundShellStore holds running processes, auto-cleans 10min after completion - retrieve tool extended to handle both agent IDs and shell job IDs - Tool error detection: results starting with 'Error:' now marked isError in UI - Fix TS error: cast unavailMatch[1] regex capture group to string - Docker: network_mode host for Tailscale/LAN access to external services - Bun.serve idleTimeout set to 60s (was default 10s) - KeyUsage: clearer message when OpenCode usage data unavailable - Firecrawl: only send scrapeOptions when scrape=true (avoid 400 on instances without scrape support)
2026-05-22feat: message queue/interrupt system, CORS fix, mobile fixes, chat splittingAdam Malczewski
- Add message queue allowing users to send messages while agent is running - Queue messages are injected into tool results as [USER INTERRUPT] - Retrieve tool interrupted via Promise.race when user message arrives - Queued messages show with 'queued' badge and cancel button - Consumed messages repositioned and chat splits at interrupt point - New assistant message block created after interrupt for clean flow - Add POST /chat/cancel endpoint for cancelling queued messages - Fix CORS to allow any origin (Tailscale/LAN access) - Fix crypto.randomUUID fallback for non-secure contexts (HTTP) - Fix frontend API URL derivation from page hostname - Auto-create DB tab if missing on processMessage (foreign key fix) - Add error logging to processMessage catch block - Fix working directory input sync on agent switch - Fix agent mode button to re-apply agent settings
2026-05-22feat: add/remove keys from UI, backend URL setting, user service, Docker fixAdam Malczewski
- Add POST /models/add-key and POST /models/remove-key API endpoints - Add 'Add New Key' modal (page-level) with provider selection - Add remove button per key in Model Status view - Add configurable backend URL setting in Settings panel with localStorage persistence - Convert systemd service from system to user service (systemctl --user) - Fix Docker entrypoint to chown all nested node_modules dirs - Update dispatch.toml credential paths to use -pro/-max naming - Make API port configurable via PORT env var (default 3000, prod 18390)
2026-05-22feat: Arch Linux packaging with Electron frontend, systemd backend service, ↵Adam Malczewski
and Windows exe build - Add Electron wrapper (main.cjs, preload.cjs) for desktop frontend - Add systemd service unit, env config, and sysusers for backend API - Add PKGBUILD and .install for Arch package (makepkg -si) - Add desktop entry, SVG/PNG icon, and wrapper scripts - Add bin/build-pkg, bin/install-pkg, bin/windows-pkg scripts - Make API port configurable via PORT env var (default 3000, prod 18390) - Add electron-builder config for Windows exe cross-compilation - Set vite base to './' for Electron file:// loading
2026-05-22feat: agent builder, CWD support, auto-save, UI polish, unavailable tool ↵Adam Malczewski
handling - Agent Builder: full CRUD with card grid, drag-and-drop model reorder, edit/delete - Auto-save on edit with 600ms debounce, AbortController for concurrency, fieldset disabled until name entered - Agent definitions stored as TOML with cwd field, loaded from global/project dirs - Working directory: per-tab CWD override in Chat Settings, agent default CWD, auto-create on first message - CWD validation: check-dir endpoint with ~ expansion, real-time validity indicator - Subagent CWD validated against parent's effective CWD using path.relative - Unavailable tool calls: caught gracefully, shown as tool call with error badge, model retries - UI: tab bar border radius, sidebar border removed, chat input ghost style, scroll-to-bottom rectangle - Skills dir collapse uses CSS rotation, Model Choice renamed to Chat Settings, System Prompt view removed - Reusable SkillsBrowser/ToolPermissions with external mode for Agent Builder - ModelSelector: Agent/Manual toggle, agent list, Agent Settings link - Page router, skills recursive scanning, bin/up gopass removed, docker volume mounts
2026-05-22feat: two-row tab bar with temp/persistent subagent tabs, tab persistenceAdam Malczewski
- Split tab bar into user tabs (top) and subagent tabs (bottom row) - Bottom row only visible when active user tab has child agents - Parent user tab stays highlighted when viewing a subagent tab - Temp tabs auto-disappear when agent completes, click to promote to persistent - 'Open Tab' button on summon tool calls to view/reopen subagent history - Reopening archived tabs fetches full chat history from backend - Add parent_tab_id column to DB with safe ALTER TABLE migration - Persist keyId, modelId, parentTabId on child tab creation - Flush partial assistant messages on abort/error (finally block) - Defensive JSON.parse in message restoration - Allow all Vite hosts for local development - Fix nested button in ToolCallDisplay (button inside button)
2026-05-22fix: child tabs now show correct key/model in Model Choice viewAdam Malczewski
Include keyId and modelId in the tab-created WebSocket event so child agent tabs inherit the parent's model selection on the frontend.
2026-05-22feat: agent summoning system, todo improvements, security fixes, ↵Adam Malczewski
double-execution bug fix - Add summon/retrieve tools for spawning child agents in new tabs - summon: non-blocking, returns agent_id immediately - retrieve: blocking, waits for child to finish, returns result - Child tools are intersected with parent permissions (no privilege escalation) - Working directory validated to stay within workspace - Abort controller stops orphaned agents on tab close - Rename task_list tool to todo with comprehensive usage guidance in system prompt - Rename PermissionLog.svelte to ToolPermissions.svelte - Add 'Summon agents' toggle to tool permissions UI - Redesign TaskListPanel with DaisyUI checkboxes (indeterminate for in-progress) - Remove 'blocked' status from task system - Add tab-created WebSocket event for child agent tab visibility - Add HMR cleanup for WebSocket connections (close stale connections on hot reload) - Fix ensureAssistantMessage to not throw on closed tabs - Fix double tool execution: remove execute from AI SDK tool() in registry.ts (agent.ts already executes tools manually via executeToolWithStreaming) - Fix all pre-existing test failures (missing mocks, stale API signatures) - Add debug info to copy button (tab ID, injected skills, all tab IDs) - Add tab ID and tools to conversation copy output
2026-05-21feat: skills system with toggle/inject, tab bar UX, streaming dedup fixAdam Malczewski
- Add skills toggle system: check skills in sidebar to inject with next message - Auto-check default skills on new tab creation for first-message injection - Track injected skills per tab with visual highlights in skills browser - Redesign tab bar: double-click background for new tab, larger close button - Update default system prompt - Fix streaming text duplication: change WS callbacks from array to Set - Fix biome config: exclude references/ directory - Auto-format with biome