summaryrefslogtreecommitdiffhomepage
path: root/packages/conversation-store/src/store.test.ts
AgeCommit message (Collapse)Author
3 daysfix(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.
5 daysstyle: switch from tabs to 2-space indentationAdam Malczewski
7 daysfix(broken-chat): read-time self-repair of unrecoverable chatsAdam Malczewski
reconcile() only repaired orphaned tool-calls. Two other broken states made chats uncontinuable, and load() had no parse-error guard: - A trailing assistant message whose only chunk is 'error' (a failed- generation marker) serializes to empty content -> provider rejects/empty -> chat never continues. 6 of 140 production conversations were stuck. - A tool-call whose input is a raw malformed-JSON string (model emitted broken JSON) re-sent as OpenAI arguments -> provider 400s on every continuation (the 77574596 break). - load() JSON.parse had no try/catch -> one corrupt row bricked the chat. Fix = read-time repair (no DB surgery; append-only preserved). reconcile runs on every load() BEFORE any provider sees messages, so Layer 1 protects ALL providers. Layer 1 (conversation-store reconcile): strip error chunks from assistant messages + drop the now-empty error-only messages (safe: never followed by a tool message); orphaned-tool-call synthesis unchanged; ReconcileReport +2 additive counts. loadSince (FE reads) intentionally unreconciled so the user still SEES the error. load() wraps JSON.parse in try/catch (skip corrupt rows). Layer 2 (openai-stream): serializeToolArguments ensures tool-call arguments is always valid JSON (malformed string -> fallback object), neutralizing already-stored malformed args. Layer 2 equiv (../claude provider-anthropic): safeJson returns a valid object fallback on parse failure, not the raw string. (Separate repo.) Live-verified: reproduced 77574596's real broken tail in the dev DB; POST /chat continued it cleanly (no 400, model replied) — the provider accepted the reconciled history. tsc -b EXIT 0, biome clean, 1453 vitest pass.
8 daysfeat: persistent per-conversation model selectionAdam Malczewski
A chat's selected provider + model is now persisted per conversation (like cwd and reasoningEffort). Opening a conversation in a new browser recalls the originally selected model instead of defaulting. - transport-contract 0.19.0→0.20.0: ModelResponse + SetModelRequest types for GET/PUT /conversations/:id/model. - conversation-store: getModel/setModel (model:<id> key, mirrors getReasoningEffort/setReasoningEffort); forkHistory copies model; empty string clears. - session-orchestrator: resolve model from persisted store when no per-turn override; persist the resolved model so it sticks; warm path parity. - transport-http: GET/PUT /conversations/:id/model endpoints with validation. 1433 vitest pass; tsc + biome clean.
9 daysfeat: workspaces contract + conversation-store implementation (Wave 0+1)Adam Malczewski
Wire 0.12.0: Workspace, WorkspaceEntry, ConversationMeta.workspaceId Transport-contract 0.16.0: workspaceId on ChatRequest/QueueRequest/ChatQueueMessage; workspace endpoint types (EnsureWorkspaceRequest, WorkspaceResponse, etc.) Kernel: re-export Workspace/WorkspaceEntry from contracts Conversation-store: workspace persistence + service methods (getWorkspace, ensureWorkspace, setWorkspaceTitle, setWorkspaceDefaultCwd, deleteWorkspace, listWorkspaces, getWorkspaceId, setWorkspaceId, getEffectiveCwd, isValidWorkspaceSlug); listConversations filter by workspaceId; forkHistory/replaceHistory preserve workspaceId. 111 tests pass. FE handoff: frontend-workspaces-handoff.md (courier doc) 18 typecheck errors in session-orchestrator/transport-http/cli test fakes (expected fan-out — fixed in Wave 2+3).
10 daysfeat: conversation lifecycle status (active/idle/closed) for tab persistenceAdam Malczewski
Implement roadmap item 9: tab persistence across devices. Wire (0.10.0): - Add ConversationStatus type (active | idle | closed) - Add status field to ConversationMeta Transport-contract (0.14.0): - Add conversation.statusChanged WS message to WsServerMessage union - Re-export ConversationStatus Conversation-store: - Track status in ConversationMetaRow (default: idle) - getConversationStatus / setConversationStatus methods - listConversations accepts { status: ConversationStatus[] } filter - Old meta rows without status default to idle on read Session-orchestrator: - conversationStatusChanged hook descriptor - Emit on transitions: idle→active (turn start), active→idle (turn settle), →closed (closeConversation) - Persist status to store as fire-and-forget side effect - Declare hook in manifest contributes.hooks Transport-ws: - Subscribe to conversationStatusChanged hook - Broadcast conversation.statusChanged WS message to all clients Transport-http: - GET /conversations?status=active,idle filter (parseStatusFilter pure helper) - POST /conversations/:id/close now sets status to closed CLI: - dispatch list defaults to active,idle (excludes closed) - --status <state> flag to filter by single status - --all flag to include closed FE handoff: frontend-conversation-lifecycle-handoff.md
10 daysfeat(conversation-store): conversation metadata + list + title (Wave 1)Adam Malczewski
Implement listConversations(), getConversationMeta(), setConversationTitle() on the ConversationStore. Auto-track createdAt (first write), lastActivityAt (every append), and title (first user message, truncated 80 chars). A conv-index key tracks all conversation IDs. 21 new tests (81 total).
11 daysfix(history): harden loadSince sinceSeq lower bound (forgiving, like ↵Adam Malczewski
beforeSeq/limit) Coerce sinceSeq to a non-negative integer lower bound in loadSince (omitted/0/ non-positive/non-integer/NaN/Infinity -> 0; valid as-is). The transport layer 400s these upstream, but loadSince stays total for direct callers. Byte-identical to the prior ?? 0 for the only values any caller ever passed. 58 bun tests pass.
2026-06-12feat(reasoning-effort): persisted per-conversation + per-turn override, ↵Adam Malczewski
threaded to providers - conversation-store: get/setReasoningEffort (own key space, mirrors cwd) - session-orchestrator: resolveReasoningEffort (override -> stored -> 'high'), StartTurnInput.reasoningEffort, warm() parity (cache-safe) - transport-http: /chat validation (400 on bad level) + GET/PUT /conversations/:id/reasoning-effort - transport-ws: chat.send threading + validation - cli: --effort <low|medium|high|xhigh|max> 993 vitest + 189 bun tests green; typecheck + biome clean.
2026-06-12feat(history): CR-5 windowed reads — ?limit= / ?beforeSeq= on GET ↵Adam Malczewski
/conversations/:id Selection sinceSeq < seq < beforeSeq; newest-limit window, ascending; positive- integer validation (400, store never sees an invalid window); 1-based gap-free seq codified as the contractual has-older mechanism (no earliestSeq field). transport-contract 0.9.0->0.10.0, wire 0.6.0->0.6.1 (doc-only). conversation-store +8 tests, transport-http +20; 935 vitest + 112 bun green. Live-verified: 6/6 probe checks OK. FE courier: frontend-history-windowing-handoff.md
2026-06-11feat(lsp,cwd): LSP integration + per-conversation cwd; fix cache-warming ↵Adam Malczewski
cache bust LSP + per-conversation CWD feature: - new bundled `lsp` extension: hand-rolled JSON-RPC codec (framing/rpc), lazy one-server-per-(serverID,root), per-cwd config resolution, on-demand `lsp` tool - `conversation-store`: getCwd/setCwd (cwdKey); `session-orchestrator` defaults a turn's cwd from the store - `transport-http`: cwd + lsp status endpoints; wire types in transport-contract - host-bin: register lsp; config wiring Cache-warming fix (the warm read 0% on the first reheat after a message): - warm assembled tools under a different cwd than the real turn (a reheat sends no cwd, and the warm service had no store fallback). The skills filter rewrites the cwd-sensitive `load_skill` description, so the tools block — the first bytes of the prompt-cache prefix — diverged and the cache missed entirely. Warm now resolves cwd as opts.cwd ?? conversationStore.getCwd(), mirroring handleMessage. - capture warm sends as `provider.request` spans flagged `warm:true` (thread a child logger into providerOpts) so warm vs real bodies are diffable (obs §3.1). - kernel logger: span-close now merges child-bound attrs like span-open, so a `warm:true` query finds the closed span (with usage/status), not just the open. Tests: warm forwards a warm-flagged logger; warm falls back to stored cwd; logger open/close attr consistency. Full suite green (873).
2026-06-10feat(conversation-store): reconcile.repair span (logging-audit #1)Adam Malczewski
Load-time history repair was invisible (createConversationStore got no logger). Now: optional logger injected (extension passes host.logger); reconcile logic moved into pure reconcileWithReport() returning a ReconcileReport (reconcile() stays a thin byte-identical wrapper); load() emits a reconcile.repair span (childed with conversationId, flat attrs repairedCount/firstRepairedToolCallId) ONLY when a real repair occurs. No contract fan-out (factory is package-internal). typecheck EXIT 0, biome clean, 550 vitest (+4) + 89 bun.
2026-06-10feat(metrics): durable per-turn/step token+timing metrics (observability ↵Adam Malczewski
spans + persisted replay) Two-part token-data improvement: #2 Observability spans (kernel run-turn): turn & step span-close now stamp ALL four Usage fields — added usage.cacheReadTokens/cacheWriteTokens (were silently dropped) and normalized usage_* -> usage.* to match the provider.request span (consistent D9 GROUP BY). No contract change. #3 Persisted replay metrics (conversation-store + read endpoint): new StepMetrics/TurnMetrics wire types; conversation-store persists per-turn metrics in a separate key space (appendMetrics/loadMetrics, turn-append order); session-orchestrator accumulates per-step+turn metrics from the event stream (pure metrics.ts) and persists after seal; transport-http serves GET /conversations/:id/metrics -> ConversationMetricsResponse. Contracts: @dispatch/wire + @dispatch/transport-contract bumped 0.3.0->0.4.0 (additive). GLOSSARY: turn metrics / step metrics. typecheck EXIT 0, biome clean, 546 vitest + 89 bun = 635 tests.
2026-06-07feat(wire,kernel,conversation-store): step grouping via stepId for batched ↵Adam Malczewski
tool calls Expose a per-step grouping key so a client can render a model's batched/parallel tool calls (those emitted in one step) as one unit, on both the live stream and replayed history. Key = branded StepId, derived turnId#stepIndex (0-based). - [email protected]: required stepId on Turn{Tool,ToolResult}Event; optional stepId on Tool{Call,Result}Chunk (generation provenance on the chunk, not the StoredChunk envelope — StoredChunk unchanged). [email protected] (re-export bump). - kernel-runtime: mint stepId per step; stamp on tool chunks + tool events. - conversation-store: chunk-carried stepId round-trips append/load/loadSince for free; reconcile copies it onto synthesized (interrupted) results. - cli: stepId added to event test fixtures (renderer unchanged). typecheck clean; 509 vitest + 89 bun; biome 0/0. FE courier reply + reference snapshots regenerated in ../dispatch-web.
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-04feat(core-ext): conversation-store — append-only multi-turn persistence on ↵Adam Malczewski
StorageNamespace + pure reconcile (16 tests)