summaryrefslogtreecommitdiffhomepage
path: root/src/core
AgeCommit message (Collapse)Author
7 daysfix: duplicate user message on first send in a new tabdevAdam Malczewski
When a draft is promoted to a real conversation, send() appends the user message as a provisional chunk (optimistic echo). But load() also fires syncTail, which fetches the CR-6 persisted user message as a committed chunk — showing the message twice until turn seal. Fix: applyHistory now removes provisional chunks that duplicate the last committed chunk (matching role + text content) when new committed chunks arrive during generation. The optimistic echo is dropped once the authoritative committed version arrives. 684 tests green.
7 daysfix: trim provisional chunks during long turns (browser stays responsive)Adam Malczewski
trimTranscript now drops oldest provisional chunks (the in-flight turn) when committed chunks are exhausted. Previously it bailed with drop=0 when committed was empty, allowing unbounded provisional growth during long generating turns (300+ chunks → browser crawls). Root cause of the syncTail approach failing: the kernel emits step-complete (line 360) BEFORE calling onStepComplete (line 542) — chunks are persisted only after tool results come back, not when step-complete fires. So syncTail on step-complete found nothing. Reverted the applyHistory + syncTail-on-step-complete changes from 4e1d041. The new approach is simpler: trim provisional directly in trimTranscript. Dropped chunks are lost temporarily (no Show Earlier) but come back as committed when the turn seals and syncTail fetches everything from the server. 686 tests green.
7 daysfeat: trim chunks during generation via step-complete syncTail (CR-6)Adam Malczewski
The backend now persists chunks at step boundaries during generation (CR-6). The FE calls syncTail on each step-complete event to fetch the newly committed chunks. applyHistory clears the provisional array when new committed chunks arrive mid-generation (they're duplicates of what was folded from live events). The accumulating chunk (current in-progress step) is kept. This means trimTranscript can now drop oldest committed chunks uniformly during a long turn — no unbounded provisional growth. The browser never holds more than chatLimit chunks, even mid-generation. 3 new tests: clears provisional on new committed during generation, keeps provisional when no new chunks, keeps accumulating when clearing. 689 tests green.
7 daysfix(metrics): show turn/step metrics for compacted conversationsAdam Malczewski
Compacted conversations start with a system summary (role: "system") instead of a user message (role: "user"). The interleaveTurnMetrics function only detected segments by role === "user", so compacted conversations got zero segments and no metrics were emitted. Fix: when no user messages are found but metrics entries exist, treat the entire transcript as one segment. This places turn-metrics at the end and anchors step-metrics to any tool-batch groups by stepId. 686 tests green.
7 daysfix(metrics): tail-align when stepId matching fails — prevent misaligned turnsAdam Malczewski
When stepIds are absent on persisted chunks (or don't match), the sequential fallback was HEAD-aligning — matching the OLDEST entries (trimmed turns) to the NEWEST segments. This showed 'turn 1' on turn 20's content and placed the wrong metrics on the wrong segments. Fix: when stepId matching produces ZERO matches, use TAIL-ALIGNMENT instead — match the LAST T entries to the T segments (the loaded chunks are always the newest). The oldest entries (trimmed turns) are unmatched and emit standalone turn-metrics rows at the top. 686 tests green.
7 daysfix(metrics): preserve turn-metrics for trimmed turnsAdam Malczewski
When the chat limit unloads a turn's content (user message + chunks), the segment disappears and the turn-metrics row was lost. Now unmatched entries (fully trimmed turns) emit a standalone turn-metrics row at the top of the transcript, so the user still sees 'turn N · X tok' for unloaded turns. Note: trimming during generation only affects COMMITTED chunks (old turns). Provisional chunks (the in-flight turn) are never trimmed — the big trim happens at seal when provisional → committed. This is by design.
7 daysfeat(metrics): show turn number in metrics bubble (turn N · ...)Adam Malczewski
The turn number comes from the entry's position in the metrics array (1-based), which is correct regardless of trimming since stepId matching aligns segments to the right entry. Now displays 'turn 3 · 12k tok' instead of just 'turn · 12k tok'.
7 daysfix(metrics): skip unanchored step-metrics — no more empty bubbles at tailAdam Malczewski
Step-metrics are only shown when anchored to their tool content (inline after the tool-call/result group). Steps whose chunks were trimmed (or text-only steps with no tool chunks) are now SKIPPED instead of piling up at the segment tail as empty 'step N · X tok' bubbles. The turn-total metrics row still shows the aggregate (tokens, duration, cache rate), so the conversation-level summary is preserved. Updated place.test.ts + ui.test.ts to anchor steps with tool-call groups where step-metrics are expected.
7 daysfeat: show 5-char git hash next to hamburger for cache-bust detectionAdam Malczewski
Vite define bakes __APP_VERSION__ (git rev-parse --short=5 HEAD) at build time — survives bundling into the arch package deploy. Falls back to 'dev' when not in a git repo. Also fixes two noNonNullAssertion warnings in place.ts.
7 daysfix(metrics): stepId-based segment matching — robust against chat-limit ↵Adam Malczewski
trimming When the chat limit trims old chunks, head-aligning turn metrics to segments by position breaks (a trimmed user message removes a segment boundary, shifting all subsequent alignments by one). Fix: match segments to metrics entries by stepId overlap (pass 1), falling back to sequential matching for text-only segments with no stepId-bearing groups (pass 2). This prevents step/turn metrics from being placed on the wrong segment after trimming, while preserving the original behavior for text-only turns. 686 tests green.
7 daysfeat(compaction): conversation compacting + auto-compact thresholdAdam Malczewski
Consume the compaction handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - New 'Compaction' sidebar view (CompactionView.svelte): - 'Compact now' button → POST /conversations/:id/compact (loading indicator + result: 'N messages summarized, M kept') - Auto-compact threshold number input → GET/PUT /conversations/:id/compact-threshold (0 = disabled, default 350000) - Re-mounts per conversation via {#key} - App store: compactNow() + compactThreshold reactive state + setCompactThreshold(), seeded on focus change (like reasoning-effort + cwd) - conversation.compacted WS handler: reloads the SAME conversation's history (ID unchanged — old history forked to an archive, not a tab switch) - WS adapter parses newConversationId field on ConversationCompactedMessage - conformance guards + tests cover the new type 686 tests green.
7 daysfeat(tabs): cross-device tab sync via conversation lifecycleAdam Malczewski
Consume the conversation lifecycle handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - fetchOpenConversations() on connect: GET /conversations?status=active,idle restores the tab bar across devices (merges with localStorage — opens new tabs, removes closed ones, updates titles from backend) - conversation.statusChanged WS handler: closed → removeTabLocally (no re-POST); active → open tab + spinner; idle → update status map - conversation.compacted WS handler: dispose stale store + cache, reload history from server - TabBar shows a spinner on active conversations (statusFor prop) - closeTab refactored to use removeTabLocally (extracted cleanup) - conformance guards + WS adapter tests cover all 3 new WsServerMessage types 686 tests green.
7 daysfeat(ws): handle conversation.open broadcast — open/focus tab from CLI --openAdam Malczewski
Consume the conversation.open handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - WS adapter (logic.ts + index.ts): parse + route the new top-level "conversation.open" WsServerMessage to an onConversationOpen handler - app store: openConversation(id) opens (or focuses) a tab — creates a chat store, loads history, subscribes to live turns, creates+selects the tab - conformance guard + WS adapter tests cover the new type - backend also shipped conversation metadata endpoints (GET /conversations, GET /conversations/:id/last, GET/PUT /conversations/:id/title) — mirrored but not yet consumed by the FE 682 tests green.
8 daysfeat(chat): message queue + steering — mid-turn injection at tool-result ↵Adam Malczewski
boundaries Consume the message-queue + steering handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - fold steering AgentEvent into the transcript as a provisional user bubble (after the tool-result it followed; no de-dup — the queue surface carried it) - add rendererId: "message-queue" custom renderer (pure parser + MessageQueueList) rendered as a compact panel above the Composer (hidden when queue is empty) - add ChatStore.queueMessage / AppStore.queueMessage — sends chat.queue WS op (trim/validate non-empty; auto-starts a turn if idle) - Composer switches to chat.queue while generating (button → Queue, placeholder → Steer the conversation...) - exhaustiveness guards updated for steering + chat.queue - carry-to-new-turn needs no special handling (normal new turn) 664 tests green.
2026-06-12feat(chat): consume CR-5 history windowing — server-windowed cold loads + ↵Adam Malczewski
show-earlier backfill Re-pinned [email protected]>0.10.0 + [email protected]>0.6.1 (reply frontend-history-windowing-handoff.md); re-mirrored both .dispatch references. - HistorySync port gains optional { limit?, beforeSeq? } (CR-5 params); the app's createHistorySync appends them to GET /conversations/:id. - COLD-cache fresh load now fetches ?sinceSeq=0&limit=<floor(0.75xL)> — a huge conversation no longer ships whole to show 192 chunks. A warm-cache tail sync stays unwindowed (windowing a tail that outgrew the limit would leave a silent seq gap behind the cache). - hasEarlier now derives from the [email protected] CONTRACT (1-based gap-free seqs): loaded window starting above seq 1 => older history exists — covering both locally-trimmed AND server-windowed transcripts (the watermark stays as the merge floor only). - showEarlier(): local cache first; when the cache doesn't reach far enough back, backfills the missing older run via ?beforeSeq=<oldestKnown>&limit= and persists it (next page-in is local). latestSeq windowed-read caveat is satisfied structurally (tail cursor derives from the cache's max seq). - live-probe: +6 CR-5 checks (seq origin, newest-k ascending, short-chat exactness, beforeSeq paging, 400 validation x2). NOT yet run live — backend was down at commit time; run pending. - backend-handoff.md: CR-5 RESOLVED, pins/mirrors current. 602 tests green x2.
2026-06-12feat(chat): chat limit — bulk quarter-unload, 75% fresh-load window, ↵Adam Malczewski
show-earlier page-in Long transcripts no longer grow unbounded: past the chat limit (default 256 chunks, localStorage dispatch.chatLimit) the oldest ceil(limit/4) committed chunks are unloaded in ONE bulk pass — never one-per-delta (old Dispatch's scroll-jump-per-step bug) — and only while the reader is stuck to the bottom (scrolled-up readers defer the trim; it catches up in whole quarters). A fresh page load windows to the newest floor(0.75*limit). Unloading is purely local (IndexedDB cache + server keep everything); a hiddenBeforeSeq watermark keeps history merges from resurrecting unloaded chunks, and a 'Show earlier messages' affordance pages a quarter back in from the cache with scroll-anchor preservation. Thinking-collapse render keys stay stable across trims via a hiddenThinkingCount ordinal base. - core/chunks/trim.ts: pure policy (trim/window/restore/normalize) + tests - chat store: chatLimit + canUnload deps, windowed load, showEarlier() - composition root: dispatch.chatLimit localStorage knob + unload gate wired to smart-scroll isAtBottom() - backend CR-5 OPENED (not a blocker): ?limit=/?beforeSeq= on GET /conversations/:id (courier backend-handoff-chat-limit.md) - scripts/live-probe.ts: fix pre-existing stale TurnMetricsEntry reads (m1.usage -> total.usage) that crashed the probe; 17/17 live checks pass
2026-06-12feat(chat): multi-client live view — watch in-flight turns + user prompt ↵Adam Malczewski
on stream - subscribe every open conversation on load + WS reconnect (resync), unsubscribe on tab close - derive a stream-based 'generating' state for watchers (Composer running indicator) - fold the user-message turn event so watchers render the prompt mid-turn (de-dup vs sender's optimistic echo) - re-pin [email protected] / [email protected]; re-mirror contracts; add user-message to the exhaustiveness guard
2026-06-12feat(chat): old-Dispatch composer layout — textarea + send + status barAdam Malczewski
Restore the ergonomic composer from old Dispatch: an auto-resizing textarea (1→7 lines) with a fixed-width Send button beside it, and a status bar BELOW holding a status icon · context-window fill bar (escalating success/warning/ error color) · compact token count (current / limit · pct%). The bar reuses the latest turn's contextSize as current usage and HARDCODES a 1,000,000-token window limit as a placeholder (real per-model limit is the next backend ask). Absorbs the standalone ContextSizeBadge (removed). Pure helpers computeContextUsage + formatCompactTokens added to core/metrics (tested). 540 tests green.
2026-06-12feat(metrics): consume contextSize — current context-usage readoutAdam Malczewski
Backend context-size handoff: re-pin [email protected] / [email protected] (+ re-mirror .dispatch reference snapshots). Thread the optional contextSize through core/metrics (done fold + durable + selectCurrentContextSize: latest turn's defined value, undefined=>unknown never 0, durable-wins-over-live). Chat store exposes currentContextSize; ContextSizeBadge renders "N tokens in context" / "context size unknown" above the composer. GLOSSARY: add context size / context window. 533 tests green.
2026-06-11feat(cache-warming,surfaces,metrics,markdown): conversation-scoped surfaces, ↵Adam Malczewski
cache warming + retention, markdown Consumes the backend cache-warming + cache-rate handoffs end-to-end and adds supporting infra: - protocol/transport: conversation-scoped surfaces (conversationId on subscribe/invoke/surface + staleness routing); store auto-subscribes the catalog with the focused conversation and re-scopes on switch. - surface-host: generic Number field renderer + custom rendererId dispatch (graceful skip on unknown). - cache-warming feature: enabled toggle, min+sec interval, AUTHORITATIVE countdown from the surface's cache-warming-timer nextWarmAt, manual Warm now (POST /chat/warm), lastWarmAt-keyed history, cache-retention stat, expectedCacheRate headline. - metrics: cross-turn expected-cache (retention) derivation + bubble badge; cache-rate fix needs no code change (inputTokens now total). - markdown feature: marked + marked-highlight + highlight.js + dompurify, rendered in ChatView. - fixes (gemini review): {#key activeConversationId} remount of CacheWarmingView to stop history/feedback leaking across tabs; guard NaN interval inputs from committing 0. - docs/contracts: regenerated transport/ui-contract mirrors; backend-handoff updated (CR-3 resolved). Verified: svelte-check 0 errors, biome clean, 494 tests pass, vite build OK.
2026-06-10feat(metrics): inline cache hit-rate badges (last turn + chat total)Adam Malczewski
Derive cache hit rate (cacheReadTokens / inputTokens) from data already folded in core/metrics — no backend/contract change. - core/metrics: computeCachePct + viewCacheRate (pct + success/warning/error level by 66/33 thresholds + isHit); thread a running cumulativeUsage onto each finalized turn-metrics row for the conversation total. - ChatView: render two labelled, colour-coded percentage badges in the turn-total bubble — "Last turn:" (that turn) and "Chat Total:" (cumulative). - Honour backend caveats: absent cache fields -> 0, divide-by-zero guarded, a legitimate 0% renders plainly (not "no data").
2026-06-10feat(metrics): per-turn + per-step token/timing metrics bubblesAdam Malczewski
Consume [email protected] / [email protected] metrics: usage.stepId, step-complete (ttft/decode/genTotal), done.durationMs/usage, and the durable GET /conversations/:id/metrics endpoint. - core/metrics: pure live-fold + durable-merge reducer; decode-rate TPS; head-aligned, stable placement; progressive per-step rows (each shown as its step ends) with the turn-total row gated on the done event. - features/chat: store folds metric events + hydrates durable TurnMetrics; ChatView renders inline step bubbles + a turn-total bubble. - app: MetricsSync HTTP effect (tolerates 404) injected into chat stores. - scripts/live-probe: drives the metrics path; live-verified 17/17 vs bin/up. - docs: regenerate .dispatch wire/transport mirrors to 0.4.0; glossary terms (turn/step metrics, TTFT, decode time, TPS, metrics bubble); trim handoff.
2026-06-07fix(core): add step-complete contract guard for [email protected]Adam Malczewski
The backend's AgentEvent union now includes TurnStepCompleteEvent (wire/transport-contract 0.3.0). Add: no-op case in foldEvent (transcript reducer ignores timing metadata), exhaustiveness case in assertAgentEventExhaustive, and a step-complete sample in the conformance test (now 12 variants).
2026-06-07Revert "feat(chat): live turn metrics — telemetry reducer + rendering"Adam Malczewski
This reverts commit 48c6d85c3cc5a57a729f14068e2346b17ed62088.
2026-06-07feat(chat): live turn metrics — telemetry reducer + renderingAdam Malczewski
Consume wire/transport-contract 0.3.0 (step-complete event + timing fields on usage/tool-result/done). Pure core/telemetry module: foldMetricEvent (reducer) + derived selectors (stepTps, turnTps, etc). TelemetryState is pure data, no active-turn tracking — consumers pass turnId to selectors. ChatStore wires foldMetricEvent into handleDelta and exposes telemetry + currentTurnId. ChatView shows step-metrics footer (time/TPS/tokens) on assistant text bubbles and durationMs badge on tool cards. New TurnSummary component renders turn-level stats (wall-clock, tokens, steps, TPS) in a DaisyUI stats block. Extended live-probe to verify telemetry events against bin/up (pending backend restart). 336 tests, typecheck 0, biome clean, build ok.
2026-06-07feat(chat): restyle thinking — visible bubble, collapse, title swap, ↵Adam Malczewski
persisted open Thinking renders inside a visible rounded-card bubble (like tool calls), capped to the same max-w-5xl column as assistant text. Uses a DaisyUI checkbox collapse (no arrow/plus icon) with smooth animation. Title reads "Thinking" + loading-dots while the model is actively generating, then flips to "Thoughts" with no dots once done. Open/closed state persists across the generating→completed→sealed transition via stable ordinal keys (per-conversation isolation via {#key} in App). Added optional streaming flag to RenderedChunk (pure selector, only on the accumulating chunk).
2026-06-07feat(chat): group batched tool calls into one DaisyUI listAdam Malczewski
Consume the backend's new stepId grouping key (wire/transport-contract 0.1.0 -> 0.2.0). foldEvent copies event.stepId onto live tool chunks so live and replay group identically. New pure selector groupRenderedChunks (core/chunks) folds a step's 2+ tool calls into one tool-batch group, pairing each call with its result by toolCallId; single/no-stepId calls stay as cards. ChatView renders a batch as a DaisyUI list (list-row per pair). Fixtures updated for the now-required event stepId.
2026-06-07fix: optimistic user message echo + tabs persistenceAdam Malczewski
Bug 1 (sent message didn't appear until turn end): the transcript only folded assistant AgentEvents, so the user's own message showed only after turn-sealed resync. Add core/chunks appendUserMessage() (provisional user chunk, superseded on history sync) and call it in chat send() — the message now renders instantly. Bug 2 (tabs didn't persist on refresh): the app passed { storage: undefined } to createLocalStore, which the adapter treats as a no-op store, so nothing was saved. Default to globalThis.localStorage. Regression test exercises the non-injected path. Also updated app store tests for the echo (assistant-vs-user chunk filtering). Verified: svelte-check 0/0, vitest 288 (stable x2), biome clean, build ok.
2026-06-07Slice 2 wave 1: transcript reducer, wire conformance, ws chat, cache coreAdam Malczewski
- core/chunks: the one pure transcript reducer (foldEvent live deltas + applyHistory seq-keyed reconcile + selectChunks/selectMessages); 27 tests - core/wire: FE-side contract-conformance exhaustiveness guards + drift smoke tests over wire/transport-contract unions (§2.9 drift signal); 10 tests - adapters/ws: additively multiplex chat.send/chat.delta/chat.error on the existing surface socket (onChat + widened send); surface API unchanged - features/conversation-cache: pure reconcileCache/nextSinceSeq/selectEvictions + ConversationChunkStore port + injected createConversationCache; 26 tests Verified green: svelte-check 0/0, vitest 169, biome clean, build ok.
2026-06-06Slice 1: surface system + WS transport + composition rootAdam Malczewski
Pure-core feature libraries assembled at the composition root: - core/protocol: pure reducer over surface catalog/spec/error messages - features/surface-host: generic field-kind interpreter (toggle/progress/ selector/stat/button) + pure plan logic; no surface-id special-casing - adapters/ws: injected WebSocket client (effects at the edge) - app: composition root store (Svelte 5 runes over the pure reducer), host-relative surface WS URL resolution (resolveWsUrl), root App.svelte Verified green: svelte-check 0/0, vitest 84 passed, biome clean, vite build ok.