summaryrefslogtreecommitdiffhomepage
path: root/packages
AgeCommit message (Collapse)Author
2026-06-06feat(transport-http): GET /conversations/:id?sinceSeq= read-side history ↵Adam Malczewski
endpoint Incremental rehydration endpoint for long-lived clients. Returns ConversationHistoryResponse { chunks: StoredChunk[], latestSeq } — the RAW, append-order, seq-filtered slice from conversation-store.loadSince, NOT reconciled (reconcile conflicts with the per-chunk seq cursor, so it stays on the turn path; the read path is a pure sync primitive). - transport-contract: add ConversationHistoryResponse + StoredChunk re-export. - transport-http: GET /conversations/:id route reaching the log directly via conversationStoreHandle (dependsOn conversation-store); pure parseSinceSeq (absent->0, invalid->400). - build wiring: conversation-store dep + project ref. FE Slice 2 backend prereq (read-side). typecheck clean, 481 vitest, biome clean.
2026-06-06feat(wire,conversation-store): per-chunk seq sync cursor (StoredChunk)Adam Malczewski
Add StoredChunk { seq, role, chunk } to @dispatch/wire (re-exported via the kernel contract shims). Keeps Chunk pure (provider-facing, no cursor); the sync cursor lives only on the envelope. conversation-store: rekey conv:<id>:msg:<seq> -> conv:<id>:chunk:<seq>; append explodes messages into role-tagged seq'd chunks (1-based, gap-free, monotonic) with internal boundary metadata so load() round-trips ChatMessage[] losslessly and still reconciles; new loadSince(id, sinceSeq?) raw sync stream. session-orchestrator test fake conforms to the widened interface. FE Slice 2 backend prereq (per-chunk seq). typecheck clean, 469 vitest, biome clean.
2026-06-06feat(frontend,wire): surface system (FE slice 1) + @dispatch/wire types-only ↵Adam Malczewski
split (B2) FE slice 1 — backend-declared, frontend-agnostic surface system (verified live): new types-only @dispatch/ui-contract (SurfaceSpec / field kinds / region / ActionRef / catalog), surface-registry (typed service handle), transport-ws (Bun WS :24205, path-agnostic upgrade), surface-loaded-extensions (first real surface); kernel HostAPI.getExtensions; host-bin wiring; bin/up. Harness: retire AGENTS 'backend only', ORCHESTRATOR §3/§7/§8, frontend-design.md locked. B2 — wire-types split (chat-slice prerequisite): new types-only @dispatch/wire single-sources the wire ABI (AgentEvent + 11 variants; conversation model Chunk/ChatMessage/Role/TurnId/StepId + 6 chunk variants; Usage) with zero @dispatch/* deps. @dispatch/kernel re-exports via shims so its public surface is byte-identical (zero consumer blast radius). transport-contract re-exports AgentEvent from @dispatch/wire and drops its @dispatch/kernel dependency, so HTTP clients (the web frontend) consume the wire without the kernel runtime. tsc -b + biome clean; 460 vitest + 77 bun pass.
2026-06-05feat(cli): one-shot terminal client (models, chat, ↵Adam Malczewski
--text/--file/--cwd/--conversation) HTTP client of transport-contract; pure-core arg/render/ndjson + injected fetch/fs shell. Docs: GLOSSARY (credential/key/model name/model catalog), tasks.md milestone, ORCHESTRATOR geography.
2026-06-05feat(backend): credential-store + model selection/catalog (GET /models) + ↵Adam Malczewski
per-turn cwd through orchestrator/transport/host-bin
2026-06-05feat(kernel): listModels/ModelInfo + per-turn cwd contracts; add ↵Adam Malczewski
transport-contract wire package
2026-06-05feat(observability): map DeepSeek nested cache tokens ↵Adam Malczewski
(prompt_tokens_details.cached_tokens) -> Usage.cacheReadTokens The real flash fixture showed flash reports cache usage in the NESTED prompt_tokens_details.cached_tokens form (384 cached of 665 prompt); the parser only mapped the flat cache_read_tokens form, so cache tokens never surfaced. Now: cacheReadTokens = usage.cache_read_tokens ?? usage.prompt_tokens_details?.cached_tokens (flat wins; cacheWriteTokens flat-only, never fabricated; partial/null *_details safe). No kernel contract change (Usage already has the fields). +5 parser tests + a real-fixture regression (cacheReadTokens === 384). These counts (+ a future prefix.fingerprint) are the cheap signals for body de-duplication. The broader trace-body storage-growth concern (verbatim body stored per request -> ~O(N^2) for long conversations) is logged DEFERRED in tasks.md; mitigation already designed (D5 volume control + §6 retention/rotation), not yet built. 339 tests, typecheck + biome 0/0.
2026-06-05feat(observability): replace synthetic text-turn fixture with a real ↵Adam Malczewski
sanitized flash capture (D5) Installed a real DISPATCH_RECORD_FIXTURE capture (200 text/event-stream, deepseek-v4-flash, finish_reason stop, reply 'Hello there friend') as src/__fixtures__/flash-text-turn.json, replacing the hand-authored one. Auth header masked (Bearer sk-…redacted…UN0); fixture re-verified secret-free before commit. Text-turn replay assertions updated to real values (inputTokens 665 / outputTokens 90); structural assertions kept (multi-chunk replay, getCapturedRequest deep-equals the outgoing request, finish/stop event, concatenated deltas). The provider's request-building + SSE parsing now run against genuine flash bytes. Real-data finding (logged in tasks.md): flash reports cache tokens via DeepSeek's NESTED prompt_tokens_details.cached_tokens (384 cached of 665 prompt); the parser only maps the FLAT cache_read/creation form, so cache tokens don't surface — an observability gap vs the §3.1 cache-debugging goal. Deferred pending decision. 334 tests, typecheck + biome 0/0.
2026-06-05fix(observability): record-mode redaction leaked capitalized Authorization ↵Adam Malczewski
header — case-insensitive mask (+3 regression tests) A live capture exposed that provider record mode saved the request Authorization header with the API key in CLEARTEXT: onExchange redaction matched lowercase 'authorization', but streamChat sends 'Authorization' (capital A) and recordFetch captures headers verbatim, so the real header slipped through. The provider.request SPAN redaction was unaffected (it masks from config.apiKey directly — journal + trace-DB showed zero leaks); the leak was record-mode-only and caught PRE-COMMIT (fixture was /tmp-only, scrubbed). Fix: redact auth case-insensitively across all captured header keys (strip Bearer, maskSecret the token, re-prepend, preserve key casing). New tests: reproduce the exact capital-Authorization leak (would have caught it), a lowercase case, and a guard that no authorization header of ANY casing survives carrying a raw sk- token. 334 tests (331 -> +3), typecheck + biome 0/0. This is the live-capture step (D5) earning its keep — real data exposed what the synthetic redaction test assumed away.
2026-06-05feat(observability): provider record/replay via @dispatch/trace-replay — ↵Adam Malczewski
env-gated capture + hermetic fixture tests (331 tests) provider-openai-compat now consumes @dispatch/trace-replay. (A) Opt-in record mode: when DISPATCH_RECORD_FIXTURE is set, the fetch edge wraps recordFetch and saves a fixture of the verbatim post-transform request + raw SSE response, self-redacting the auth header in the provider's OWN code (reuses its existing maskSecret graduated-tier mask — no shared helper, isolation over DRY). Zero overhead when unset; fail-safe. (B) Hermetic replay tests: stream.test.ts drives the provider off committed SSE fixtures via replayFetch (chunk-split to exercise SSE parsing across boundaries), asserting ProviderEvents + that the outgoing request still matches the recorded one (transform-drift regression). Injectable fetch via an internal StreamConfig.fetchFn — NO kernel contract change. 2 committed fixtures (text-turn + tool-call, currently hand-authored-faithful; a real flash text-turn swap follows). Verified: tsc -b clean, 331 vitest (327 -> +4: 2 replay + 2 redaction), biome 0/0. Provider 44 -> 48.
2026-06-05feat(observability): trace-replay — generic HTTP-exchange record/replay ↵Adam Malczewski
library (39 tests) New standalone package @dispatch/trace-replay: replayFetch (pure — fixture -> fetch double + captured request, optional chunking to simulate streaming), recordFetch (tees a real fetch into a fixture WITHOUT consuming the caller's stream), and serialize/parse + save/load fixture I/O. Redaction-free by design: calling extensions self-redact in their OWN code before saving (isolation over DRY, D5/§9). Zero @dispatch/* deps, no bun:sqlite (runs under vitest). The shared unit realizing the §7/D5 replay affordance for hermetic provider tests; provider-openai-compat will consume it next. Root tsconfig ref wired. Verified: tsc -b clean, 327 vitest (288 -> +39: replay 12 / record 8 / fixture 19), biome 0/0. Agent stayed in lane (packages/trace-replay only).
2026-06-05feat(observability): host-bin supervises the collector (spawn-first / ↵Adam Malczewski
drain-last / restart) — 288 tests host-bin spawns the out-of-process collector before serving (real Bun.spawn adapted to a ChildHandle), restarts on unexpected exit (backoff + restart-guard cap), drains on SIGINT/SIGTERM (collector final-drain, SIGKILL fallback on timeout). createCollectorSupervisor takes an injected spawn so the lifecycle is unit-tested with a fake (no real subprocess). Collector failures never crash the app (D3 subordinate/fail-safe). New env DISPATCH_TRACE_DB (default ./.dispatch-data/traces.db). Verified: tsc -b clean, 288 tests (279 + 9 supervisor), biome 0/0. Live (clean single run): 1 collector during, trace DB auto-populated (nested easy-view), 0 collectors after shutdown.
2026-06-05fix(observability): nest turn/step/prompt/provider.request spans into a tree ↵Adam Malczewski
(+ buildSpanOpen parent propagation) run-turn: step is now turnSpan.child; prompt/provider.request/tool-call are step's children (stepSpan.log passed into provider.stream). logger.ts: buildSpanOpen now propagates the child's computed parentSpanId onto the span-open record — a latent bug where span.child(...) never set parentSpanId on open (close was already correct). Verified: tsc -b clean, 279 tests, biome 0/0. Live: span tree turn->step->{prompt,provider.request}; the trace CLI easy-view renders the nesting.
2026-06-05feat(observability): Phase B — trace-store (SQLite) + out-of-process ↵Adam Malczewski
collector + trace CLI (345 tests) trace-store (bun:sqlite): records+bodies schema (thin/fat split), idempotent insertRecords (FNV-1a id + INSERT OR IGNORE), getTurn/getBody, pure renderEasyView (D8 timeline skeleton), trace CLI. Its own DB, isolated from storage-sqlite. observability-collector: out-of-process bin — tail journal -> splitLines/drainOnce -> trace-store.insertRecords; offset sidecar; at-least-once + idempotent; fail-safe; clean SIGINT/SIGTERM drain. Build-config (orchestrator): root tsconfig refs; both excluded from vitest + added to test:bun (bun:sqlite); bun install. Verified: tsc -b clean, 345 tests (273 vitest + 72 bun), biome 0 warnings/0 infos. Pipeline proven end-to-end: app -> journal -> collector -> SQLite -> 'trace <turnId>' easy-view. Known follow-up (next commit): kernel spans are flat (parent=ROOT) — run-turn nesting fix.
2026-06-05refactor(observability): pure-types contracts/logging + Span body channel; ↵Adam Malczewski
verbatim before/after -> LogRecord.body (273 tests) contracts/logging.ts reduced to pure types; createLogger (+ helpers) moved to kernel/src/logging/ — @dispatch/kernel still exports it (host-bin/tool-read-file unaffected). Span body channel (Option A): Logger.span / Span.child / Span.end accept an optional body string -> SpanOpenRecord.body / SpanCloseRecord.body. Large verbatim payloads now use body, not stringified attributes (store-fat-serve-thin; attributes stay thin/queryable for D9). before: run-turn emits a 'prompt' span with the verbatim messages+tools in body (small scalars in attrs). after: provider.request span carries the verbatim request in body; attrs thin, auth self-redacted. Verified: tsc -b clean, 273 tests, biome 0 warnings/0 infos. Live boot: prompt + provider.request bodies present and correlated (shared turnId); request.body no longer in attributes; auth-key leak count = 0.
2026-06-05feat(observability): Phase A.2 — verbatim provider.request "after" capture ↵Adam Malczewski
+ self-redaction (267 tests) Threads the step span's correlated logger into provider.stream (new optional ProviderStreamOptions.logger) so provider-openai-compat opens a child provider.request span at the fetch edge, capturing the verbatim post-transform request + response status/cache-tokens/raw-error. Auth header self-redacted in the provider's OWN code (graduated mask tiers; no shared helper). Capture is fail-safe (never throws into the turn). Adds the first hermetic provider HTTP test (stream.test.ts: fetch mocked, 15 cases). Large payloads use attributes for now; the LogRecord.body channel is a deferred ABI design (notes §10). Verified: tsc -b clean, 267 tests (250->+17), biome 0 warnings/0 infos. Live boot: provider.request shares turnId with prompt:before (before<->after diffable); auth-key leak count = 0 (self-redaction proven on a real request).
2026-06-05feat(observability): Phase A logging substrate — Logger/Span ABI + journal ↵Adam Malczewski
sink (250 tests) Structured, agent-first logging captured durably to an append-only journal file. Kernel (contracts/logging.ts): leveled/attributed Logger + Span, auto-scoped per extension (host stamps manifest.id, unspoofable), incremental span records (open/close) for crash-reconstructable traces, injected LogSink (pure record-builder). ctx.log on ToolContract; runTurn opens turn/step/tool-call spans and captures the verbatim pre-mutation prompt (the 'before') on the step span. journal-sink (new package, bootstrap dep — not an extension): LogSink appending NDJSON to a rotating journal; pure serialize + thin fs edge; fail-safe drop, never blocks a turn. host-bin injects it via HostDeps; session-orchestrator threads host.logger (childed per turn) into runTurn. Redaction is per-extension self-redaction (no shared helper — isolation over DRY). The out-of-process collector + SQLite store + the verbatim 'after' provider.request capture are Phase B / next (notes/observability-design.md §10/§11). Verified: tsc -b clean, 250 tests (218→+32), biome clean. Live boot: a turn's journal holds host logs + turn/step spans (open+close) + the prompt:before record with the verbatim messages array. Harness: ORCHESTRATOR §3 rule-scoping map; .dispatch/rules/isolation-over-dry.md; notes/observability-design.md (design D1–D10 + Phase A/B plan).
2026-06-05refactor(kernel): rename tabId → conversationId across contracts + ↵Adam Malczewski
consumers (218 tests) Step 4 of the post-MVP backlog: resolve the last vocab drift. The canonical term for a thread of turns is `conversationId` (GLOSSARY), but `AgentEvent` variants and `RunTurnInput` still used the legacy `tabId` from the old frontend "tab" concept, with session-orchestrator bridging `conversationId → tabId`. Atomic, type-driven rename across the full 10-file consumer set: - contracts/events.ts: all 11 AgentEvent variants tabId → conversationId - contracts/runtime.ts: RunTurnInput.tabId → conversationId - runtime/{events,run-turn,dispatch}.ts: factory params, ctx field, locals - session-orchestrator: drop the redundant `tabId: conversationId` bridge line - transport-http: emit wiring; external /chat field + X-Conversation-Id header unchanged (already canonical) — only the emitted NDJSON event field flips - tests (run-turn, app, logic): inputs + assertions now use conversationId Pure rename, zero behavior change: typecheck clean, 218 tests pass (unchanged count), biome clean, `grep tabId packages/` → zero matches. Verified live: multi-turn curl emits conversationId-keyed NDJSON and threads history correctly. GLOSSARY drift note removed. Closes the post-MVP backlog (Steps 1–4).
2026-06-05refactor(host): expose getHostAPI(); host-bin drops duplicate adapter; ↵Adam Malczewski
storage-sqlite manifest honesty host CR-1: createHost.getHostAPI() returns the canonical post-activation HostAPI (registration closed) via a single builder — host-bin deletes its buildPostActivationHostAPI duplicate and calls host.getHostAPI(). storage-sqlite CR-2: remove false contributes.services:["storage"] (backend is a kernel bootstrap dep injected as HostDeps.storageFactory, not a bus service); document the intentional no-op activate. typecheck clean, 218 tests pass, biome clean; live boot + curl verified.
2026-06-05feat(tool-read-file): add read_file tool extension + wire into host-binAdam Malczewski
First TOOL extension (standard tier, fs capability). Pure-core/shell split with workdir containment (realpath symlink guard). host-bin registers it in CORE_EXTENSIONS; flows into runTurn via session-orchestrator's resolveTools. Verified: typecheck clean, 214 tests pass (was 185), biome clean. Live curl against flash produced a real tool-call + tool-result round-trip with correct final answer. Proves the kernel tool-dispatch loop end-to-end (plan §3.3).
2026-06-05feat(auth): provider resolves credentials via AuthContract, not configAdam Malczewski
- kernel HostAPI: add getAuthProviders()/getAuthProvider(id) read-views (mirrors getProviders) - provider-openai-compat: activate() resolves creds via host.getAuthProvider("apikey").resolve(); dependsOn auth-apikey; model stays config-driven - host-bin: mirror the new getters in post-activation HostAPI stub - auth-apikey is no longer vestigial; auth seam exercised end-to-end - 185 tests pass; typecheck + biome clean; verified live (curl returns real response)
2026-06-05chore(cleanup): align glossary (conversation/conversationId + drift note), ↵Adam Malczewski
ORCHESTRATOR↔plan §5/§3.6, add HANDOFF.md, host-bin reads BACKEND_PORT (24203)
2026-06-05feat(host-bin): composition root — boot, discover+activate extensions, ↵Adam Malczewski
Bun.serve; full-fidelity wiring (178 tests)
2026-06-04fix(kernel): expose getProviders/getTools on HostAPI (CR-2) + runTurn uses ↵Adam Malczewski
input tabId/turnId (CR-3); simplify orchestrator wiring (167 tests)
2026-06-04feat(core-ext): session-orchestrator + transport-http (parallel); wire into ↵Adam Malczewski
build graph (164 tests)
2026-06-04feat(core-ext): conversation-store — append-only multi-turn persistence on ↵Adam Malczewski
StorageNamespace + pure reconcile (16 tests)
2026-06-04feat(kernel): extension host — discovery, DAG resolve, apiVersion check, ↵Adam Malczewski
activate, HostAPI (wraps bus); 50 tests
2026-06-04feat(core-ext): storage-sqlite, auth-apikey, provider-openai-compatAdam Malczewski
- storage-sqlite: bun:sqlite StorageNamespace backend + migrations (21 bun tests) - auth-apikey: pure resolver from env → ApiKeyCredentials (4 tests) - provider-openai-compat: OpenAI-compatible SSE stream → ProviderEvents - orchestrator fixes: provider imports (@dispatch/kernel), missing dep, exactOptionalPropertyTypes (omit-when-undefined), root tsconfig refs - vitest excludes storage-sqlite (bun:sqlite); test:bun runs it under bun
2026-06-04feat(contracts): RunTurnInput gains tabId/turnId/providerOpts; FinishReason ↵Adam Malczewski
union (resolves runtime CR-1/2/3)
2026-06-04feat(kernel): runTurn turn loop — tool dispatch policy ↵Adam Malczewski
(eager/semaphore/dedup/concurrencySafe/abort), 16 tests
2026-06-04feat(kernel): event/hook bus (events, filters, services) — pure dispatch + ↵Adam Malczewski
stateful shell
2026-06-04feat(kernel): define the ABI contracts (conversation, tool, provider, auth, ↵Adam Malczewski
dispatch, hooks, extension/HostAPI, runtime, events)
2026-06-04chore: scaffold monorepo + AI harness (constitution, rules, glossary, kernel ↵Adam Malczewski
stub)
2026-06-04chore: genesis — remove all files to rebuild from scratch (arch rewrite)Adam Malczewski
2026-06-04feat(config): add subdirectory LSP config watchers and fix permission orderingv2-deprecatedAdam Malczewski
- Add watchDirConfig() for per-directory config watching - Register watchers for subdirectories with their own dispatch.toml - Fix permission ordering: move "*" wildcard to front so findLast reaches specific rules first (was silently breaking all specific bash permission rules) - Add comprehensive tests for watcher functionality - Update mocks in test files
2026-06-03fix(config): emit local permission patterns after global ones in mergeAdam Malczewski
Gemini review caught a precedence-inversion bug in mergePermissions: when a nested permission group exists in BOTH global and local configs, the previous `{ ...existing, ...value }` spread updated an overridden pattern IN PLACE, leaving its original (global) insertion slot. Since configToRuleset flattens patterns in iteration order and evaluate() uses findLast (last match wins), a more-general global pattern declared lower (e.g. "*") would sit AFTER the local override and silently shadow it. Example: global bash { "npm test"=allow, "*"=ask } + local bash { "npm test"=deny } resolved "npm test" to "ask" instead of "deny". Fix: drop global patterns the local block also defines, keep remaining global patterns in order, then append ALL local patterns last — reproducing a clean "global rules then local rules" concatenation so local always wins. Adds a regression test asserting order and evaluation outcome.
2026-06-03feat(config): merge home-directory global dispatch.toml under project configAdam Malczewski
Load an optional global config at ~/.config/dispatch/dispatch.toml (override via DISPATCH_GLOBAL_CONFIG) and deep-merge it underneath every project/working-directory dispatch.toml, so machine-wide settings — most notably globally available LSP servers — work in any repo without per-repo config. Local always wins on conflicts. - loader: add getGlobalConfigPath(), loadGlobalConfig(), mergeConfigs(); loadConfig(dir) now loads+merges global. [lsp] and [[keys]] merge by id; [permissions] merge per-group with global patterns emitted first so local rules win at evaluation time (findLast). A malformed global config is downgraded to empty rather than breaking every repo. - watcher: watch BOTH global and local dispatch.toml so hot-reload re-merges on either change (dedupes when paths coincide). - export new loader fns from config/index and core index. - types/agent-manager: doc updates reflecting merged LSP resolution. - dispatch.toml: document global-default merge behavior; activate biome and typescript-language-server LSP entries. - tests: merge precedence, lsp/keys merge-by-id, permissions merge, filesystem integration, malformed-global resilience; isolate global path in existing loader tests.
2026-06-03Merge branch 'dev' into warm/prompt-cache-warmingAdam Malczewski
# Conflicts: # packages/api/src/agent-manager.ts # packages/api/tests/agent-manager.test.ts # packages/frontend/src/lib/tabs.svelte.ts
2026-06-03fix: warm the SAME Anthropic message-cache bucket as real turnsAdam Malczewski
Root cause of the 'first warmup misses' + 'switch to chat misses' bugs: Anthropic keys the MESSAGE-level prompt cache on `tool_choice` AND the extended-thinking parameters (both rows in their cache-invalidation table mark the messages cache as invalidated on change). The original warmCache() sent toolChoice:'none' and NO thinking providerOptions, while real turns send toolChoice:'auto' + thinking config for the effort. So warming and chat wrote TWO different message-cache buckets: - warmup #1 missed (no warm-only bucket existed yet), every later warmup hit its own bucket; - the next real chat message read the OTHER bucket → miss. Fix: extract a shared buildStreamOptions() that produces the cache-affecting params (toolChoice + thinking providerOptions + maxOutputTokens). Both run() and warmCache() now call it with the SAME resolved reasoning effort, so the warming replay refreshes the exact cache the next real message reads. The trivial probe turn is still appended AFTER the last cache breakpoint, so it never disturbs the cached prefix. Threaded the per-tab reasoning effort (per-model -> per-tab selector -> default, mirroring processMessage) from the frontend resolver through POST /chat/warm to warmCacheForTab to warmCache. Tests: updated the warmCache toolChoice test to assert it MATCHES a real turn, added an invariant test driving run() and warmCache() and asserting identical cache-affecting params, and assert effort forwarding in the frontend store. check / test (780) / frontend build / typecheck all green.
2026-06-03feat: prompt cache warming for idle tabsAdam Malczewski
Keep a tab's provider prompt-cache warm while idle by periodically replaying the exact cached conversation prefix plus a single trivial throwaway turn, resetting the provider's ~5-min cache TTL so the user's next real message hits a warm cache. Backend: - Agent.warmCache(history): extracts buildLlmContext() shared with run(), then re-sends the identical system+tools+history prefix (same Anthropic cache_control breakpoints) plus a 'reply with just a .' probe turn via toolChoice:none. Returns the request usage; mutates no history, emits/persists nothing. - AgentManager.warmCacheForTab(): resolves the same agent the next real turn would use, replays the FULL genuine history, refuses while a turn is running. - POST /chat/warm: returns ONLY the warming request's usage (never persisted, never folded into the real usage aggregate). Frontend: - cache-warming.svelte.ts store: per-tab 4-min repeating idle timer with countdown, warming-specific last-request cache %, and error capture. Arms on turn end, pauses during a turn, disables+resets on a real user message. - cache-warm-storage.ts: per-tab localStorage persistence of the toggle. - Lifecycle hooks wired into tabs.svelte.ts (status/statuses/sendMessage/ hydrate/create/open/close). - ModelSelector: bottom-of-panel checkbox + debug strip (last-% / countdown / error), shown only when enabled. Warming cache data never touches the real Cache Rate view. Tests: core warmCache (5), api warm route (3) + warmCacheForTab (3), frontend store (12) + storage (10). check / test (779) / frontend build / typecheck all green.
2026-06-03Merge branch 'dev' into cmp7/compaction-toolAdam Malczewski
# Conflicts: # packages/frontend/src/lib/components/ChatInput.svelte
2026-06-03feat(compaction): add UI-driven conversation compactionAdam Malczewski
Summarize a conversation's older "head" into a structured anchored Markdown summary while preserving the most recent turns verbatim, shrinking context size while keeping the information needed to continue coherently. Triggered by a "Compact conversation" button in Chat Settings (not an agent tool). Approach informed by OpenCode's session/compaction.ts: - Ported SUMMARY_TEMPLATE (Goal / Constraints / Progress / Key Decisions / Next Steps / Critical Context / Relevant Files) and the anchored-summary buildPrompt (re-summarizes a prior summary when present). - Ported the TOOL_OUTPUT_MAX_CHARS (2000) cap on tool results in the summary request. - Simplified tail selection to a fixed recent-turn count (DEFAULT_TAIL_TURNS=2) instead of OpenCode's token-budget splitTurn. core: - New src/compaction/ module (pure, DB-free): template, prompt builder, head/tail selection, transcript renderer with tool-output capping, prior summary extraction. Generic over ChatMessage so callers keep turnId/seq. - db/chunks.ts: rekeyChunks(from,to) relocates a tab's full history to a backup tab (reversible — nothing is deleted). - AgentEvent: compaction-started / -complete / -error variants. api: - AgentManager.compactTab(tempTabId, sourceTabId): side-effect-free resolveConnection() for the compactor model (configured compaction_model_*, else the source tab's own key+model), one-shot tool-less summary generation via a transient Agent, then relocate full history to a fresh backup tab and re-seed the canonical source id with [summary turn + preserved tail]. Source tab is locked (messages queue) during the run; queue drains afterward. - Routes: POST /tabs/:id/compact, GET/PUT /tabs/settings/compaction-model. frontend: - "Compact conversation" button in ModelSelector (Chat Settings), between Working Directory and the agent toggle; idle-gated. - Compaction-model key+model selector in Settings, beside the title model. - Transient placeholder tab shows a large, non-faded "Please wait, compacting conversation…" screen; closing it cancels. Source input locked while running. - Handle compaction-* events: reload compacted source, insert backup tab, refocus source, discard placeholder. tests: core compaction unit tests, rekeyChunks DB test, AgentManager.compactTab orchestration tests, and compaction route tests. All green (713 tests), biome clean, all typechecks pass, frontend builds.
2026-06-03Merge branch 'dev' into img8/image-attachmentsAdam Malczewski
2026-06-03style: sort imports in TabBar.svelte (biome) after merging devAdam Malczewski
2026-06-03Merge branch 'dev' into k10/key-usage-toolAdam Malczewski
2026-06-03feat(tabs): pulsing status dot to grab attention when agent needs the userAdam Malczewski
Use DaisyUI's status-with-ping pattern on the tab status dot so it pings when the agent has stopped and is likely waiting on the user: - idle with incomplete (pending/in_progress) tasks remaining, or - stopped due to an error. Implements wishlist item #21.
2026-06-02feat(chat): paste-to-attach images/PDFs with model capability checkAdam Malczewski
Add multimodal image/PDF input to the chat box via clipboard paste, gated by a graceful per-model capability check. UX: a pasted image/PDF inserts an inline token (【image:…】 / 【pdf:…】) into the draft, so attachments have ORDER relative to typed text and can be referenced positionally. The token is the only handle — deleting it (atomic Backspace/ Delete, or selection overlap) detaches the file; an input-reconciliation safety net detaches any attachment whose token is no longer intact. No preview strip. Capability check: resolveModelCapabilities reads models.dev modalities.input (new GET /models/capabilities, mirrors /context-limit). The input blocks Send (no tokens spent) only on a definitive 'no'; unknown capability (catalog offline / unmapped provider) stays permissive. Attachments require a fresh turn — Send is blocked while generating and /chat rejects content mid-turn (409). Attachments are EPHEMERAL: forwarded to the model for the turn via ordered AI SDK ImagePart/FilePart content, but never persisted (history keeps the text with [image]/[pdf] markers). Text-only turns serialize byte-identically to before. Limits (Anthropic-aligned, enforced at paste + re-validated server-side): PNG/JPEG/WebP/GIF/PDF; image ≤5MB, PDF ≤32MB, ≤20 attachments, ≤32MB total. core: UserContentPart types, models/attachments validator, capability resolver, agent.run+toModelMessages thread ordered content. api: /chat content validation + passthrough. frontend: attachment-tokens helper, ChatInput paste/token/gating, per-tab staged attachments, App.svelte capability fetch. +44 tests.
2026-06-02feat(tools): add key_usage tool reporting API-key usage levelsAdam Malczewski
Adds an agent-callable `key_usage` tool that reports current usage for configured API keys so the agent can pick a key with headroom, warn before hitting a rate limit, and diagnose exhausted-key failures. Per key it reports: provider, active/exhausted status (with last error + when it was exhausted), remaining rate-limit headroom and reset timestamp per window (5-hour, weekly, and monthly where the provider exposes it), and whether the figures are live or served from cache (with the cache's last-fetched-from-source time). Supports anthropic and opencode-go keys (live with cache fallback for anthropic; live scrape for opencode-go). Optional `key_id` reports one key; omitted reports all. Hard permission gate `perm_key_usage` (default off): when disabled the tool is completely removed from the toolset/context. Registered in both the parent permission-gated path and the child whitelist path, advertised in the system prompt (TOOL_DESCRIPTIONS), grantable to subagents via the summon enum, and exposed as a frontend tool-permission checkbox. To report data freshness, claude.ts gains `getAccountUsageWithSource` + `ClaudeUsageResult` (live vs cache + cachedAt from usage_cache.cached_at); the existing `getAccountUsage` now delegates to it, preserving behavior. Tests: core key-usage tool suite (windows, %-conversion, freshness, exhausted status, unsupported/unavailable, filtering) + agent-manager perm-gate test.
2026-06-02Fix build scripts and deployment port issueAdam Malczewski
2026-06-02Merge branch 'dev' into feat/cs-code-search-toolAdam Malczewski
# Conflicts: # packages/api/src/agent-manager.ts # packages/api/tests/agent-manager.test.ts # packages/frontend/src/lib/components/ToolPermissions.svelte # packages/frontend/src/lib/settings.svelte.ts