| Age | Commit message (Collapse) | Author |
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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'.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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").
|
|
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.
|
|
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).
|
|
This reverts commit 48c6d85c3cc5a57a729f14068e2346b17ed62088.
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
- 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.
|
|
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.
|