Skip to content

Restore compaction status and summary in dashboard on session re-entry - #503

Merged
aebrer merged 3 commits into
masterfrom
feature/issue-502-compaction-reentry
Sep 4, 2026
Merged

Restore compaction status and summary in dashboard on session re-entry#503
aebrer merged 3 commits into
masterfrom
feature/issue-502-compaction-reentry

Conversation

@aebrer

@aebrer aebrer commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Closes #502

Restore the compaction indication and the compaction summary in the dashboard when a session view is left and re-entered: the "compacting" status entry is synced from the authoritative snapshot state on hydrate/resync, and compactionSummary transcript messages are rendered as summary cards.

Implementation plan posted as a comment below.

@aebrer

aebrer commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Implementation Plan

Analysis

Both symptoms in issue 502 come from how the dashboard client reconstructs session state when a session view is re-entered. Re-entry remounts SessionScreen, which runs hydrateSession (drill-in); full recovery uses hydrateSnapshot (resync). Both snapshot-apply paths restore the compacting flag from the authoritative snapshot, but the user-visible compaction UI is built from two ephemeral things the snapshots do not carry:

  1. Compaction indication. The "compacting context…" banner and the "stop compaction" button are driven only by an ephemeral statusEntries entry with key "compaction", created solely by the live auto_compaction_start reducer handler (reducer.ts). A start event consumed before the snapshot barrier is never replayed, and hydrateSnapshot clears statusEntries wholesale. On re-entry during compaction the user sees only the generic "■ stop" button (driven by the restored compacting flag) — no indication that compaction is running.
  2. Summary card. The transcript is rebuilt from snapshot messages via messagesToEntries, which handles only user / assistant / toolResult / bashExecution / custom roles. The snapshot messages do contain the summary — role: "compactionSummary", emitted by buildSessionContext from the persisted CompactionEntry — but the client drops it. The only place a kind: "summary" card is created is the live auto_compaction_end handler, and its in-memory entry is replaced on re-entry.

Fix: make the authoritative snapshot state the source of truth for both — (A) sync the compaction status entry from the restored compacting flag in every snapshot-apply path, and (B) render compactionSummary messages as summary cards in messagesToEntries, plus a dedup guard for the race where a compaction finishes between the snapshot barrier and the snapshot read.

Verified during planning: after compaction completes, agent-session.ts calls replaceMessages(buildSessionContext().messages) on both the manual and auto paths, so the in-memory session.messages (the streaming snapshot path) also carries the summary message — no residual gap on the streaming path.

Deliverables

  1. Re-entering a session that is compacting shows the compaction indication ("compacting context…" banner + "stop compaction" button), in both the drill-in hydrate path and the full-resync path.
  2. The compaction summary card (summary text + tokens-before) is present in the transcript built from snapshot messages, so it survives leaving and re-entering the session view.
  3. No duplicate summary card when a compaction completes in the window between the snapshot barrier and the snapshot read (summary in both snapshot messages and a replayed end event).
  4. Unit and store-level tests for all of the above; existing dashboard suites pass unchanged.

Acceptance Criteria (from issue 502)

  • Re-entering a session that is compacting shows an indication of the compaction (the same indication shown when compaction starts while the session view is open).
  • After compaction completes, the compaction summary remains visible in the session transcript after the user leaves the session view and returns, with the same content as before leaving (summary text and tokens-before count).

Verification: targeted vitest runs, full npm test workspace pass, biome clean, npm run build, plus a manual dashboard pass (trigger compaction, navigate to fleet, return — summary card present; re-enter mid-compaction — banner + stop control present).

Files to Create or Modify

  1. packages/dashboard/src/client/state/reducer.ts

    • messagesToEntries: new branch for role === "compactionSummary" producing { kind: "summary", label: "compaction", text: summary ?? "context compacted", tokensBefore } — identical shape to the live end-handler card.
    • New exported helper syncing the compaction status entry from state.compacting: when true, upsert { key: "compaction", text: "compacting context…", tone: "info" } (replacing any existing compaction entry, including dismissed ones, so re-entry restores the indication); when false, remove it.
    • applySessionEvent auto_compaction_start / auto_compaction_end: route through the helper (start keeps its overflow provider-error clear; end keeps its compaction-error status entry).
    • auto_compaction_end: guard the summary push — skip if an existing compaction summary entry already matches (label, tokensBefore, text), preventing the duplicate from a replayed post-barrier end event.
  2. packages/dashboard/src/client/state/store.ts

    • restoreSnapshotOutcomeState (already called from both hydrateSession and hydrateSnapshot, after session.compacting is set): call the sync helper there, alongside the existing retry / provider-error restoration.

No changes needed in session.tsx (it already renders status entries as banners and summary entries as cards; abortableStatuses / showStopControls work off the status entry and the compacting flag), the dashboard server (snapshots already carry isCompacting and the summary message), or the coding-agent core (CompactionEntry persistence and buildSessionContext already work). Docs: docs/dashboard.md already describes the intended behavior ("compaction/branch summaries" in the transcript; "retry/compaction/paused" status banners) — no doc changes.

Testing Approach

Existing infrastructure: packages/dashboard/test/reducer.test.ts (node env, pure unit tests over createSessionViewState, existing compaction block at L787–869) and packages/dashboard/test/client/store.test.ts (jsdom, api.js fully mocked, local runtimeSnapshot / hydrationSnapshot / emit / flushAsyncWork helpers).

test/reducer.test.ts (add to existing describes):

  • messagesToEntries: a compactionSummary message becomes a summary entry with text and tokensBefore; ordering in a mixed transcript (summary first, then kept messages); missing summary text falls back to "context compacted".
  • Sync helper: compacting = true with no entry creates it; with an existing entry it stays exactly one (no duplicates); with a dismissed entry it is re-added undismissed; compacting = false removes it.
  • Dedup: auto_compaction_end applied to a state whose entries already contain a matching compaction summary (same tokensBefore + text) pushes no second summary entry (the barrier-race scenario).
  • Existing compaction tests must pass unchanged, including "aborted compaction produces no summary entry".

test/client/store.test.ts (add to "app store hydration" / "app store SSE sync"):

  • hydrateSession with snapshot isCompacting: truecompacting true and the compaction status entry present (extend the local runtimeSnapshot / hydrationSnapshot helpers to accept an isCompacting parameter).
  • hydrateSession with isCompacting: false after an in-memory auto_compaction_start (emit the envelope first) → compaction status entry cleared.
  • hydrateSession with snapshot messages containing a compactionSummary message → entries contains the summary card (acceptance criterion 2 at the store level).
  • Resync path: dashboard_resync envelope with a resync snapshot of isCompacting: true (existing api.resync mock pattern) → compaction status entry restored despite statusEntries being cleared.
  • Dedup through the store: hydrate snapshot carrying the compactionSummary message, then emit a replayed matching auto_compaction_end → exactly one summary entry.

Regression: test/event-hub.test.ts (live vs replayed reducer parity) and test/client/hard-refresh.integration.test.ts (real-server hydrate) must pass unchanged. Optional: extend the hard-refresh integration test with a compaction-summary message in the fake snapshot and assert the card is present end-to-end.

Run: npx vitest --run packages/dashboard/test/reducer.test.ts, npx vitest --run packages/dashboard/test/client/store.test.ts, then npm test; npx biome check on touched files; npm run build before any manual dashboard check.

Risks and Open Questions

  • Dismiss semantics: banner dismissal is presentation-only (dismissed: true on the entry). Re-hydrating while compaction still runs re-adds a fresh, undismissed entry — that is the desired "re-entry restores the indication" behavior and mirrors the existing retry-banner restoration.
  • Placement: the snapshot-built transcript places the summary card at the compaction boundary (first message of the kept context, per buildSessionContext), while the live event appends it at the transcript end at event time. Both are "where the compaction happened" — no normalization needed.
  • Dedup key: (label, tokensBefore, text); a false positive would require two compactions with identical summary text and token count, in which case the cards are indistinguishable anyway.
  • Scope note (not implemented): the branchSummary role has the same drop in messagesToEntries, and fleet cards show a generic "running" chip while compacting — both were called out in issue 502 as related-but-not-requested; a follow-up issue is appropriate if desired.

Plan created by mach6

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Vitest coverage

Metric Covered Total Coverage
Statements 41107 56779 72.39%
Branches 22339 35777 62.43%
Functions 8719 11910 73.2%
Lines 29677 40818 72.7%

View full coverage run

@aebrer

aebrer commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Progress Update

Implemented both parts of the plan:

  • reducer.tsmessagesToEntries now renders compactionSummary snapshot messages as the same kind: "summary" / label: "compaction" card the live end handler creates (fixes the missing summary card after re-entry). New exported syncCompactionStatusEntry(state) upserts the "compacting context…" status entry from the authoritative compacting flag; both live start/end handlers route through it. The end handler gained a dedup guard (label + text + tokensBefore) so the barrier race — summary already in snapshot messages plus a replayed end event — cannot produce a duplicate card.
  • store.tsrestoreSnapshotOutcomeState now calls syncCompactionStatusEntry, so both snapshot-apply paths (drill-in hydrateSession and full hydrateSnapshot resync) restore the compaction banner and the "stop compaction" control whenever the snapshot state says compaction is in flight.

Tests added: 9 in test/reducer.test.ts (summary hydration, fallback text, helper upsert/clear/dismiss-recreate, dedup + differing-compaction) and 6 in test/client/store.test.ts (hydrate-while-compacting, replayed-start no-dup, live end after hydrate, summary-in-snapshot-messages, barrier-race dedup, resync restore).

Verification: full dashboard package 1178/1178 passing (including event-hub live-vs-replayed parity and hard-refresh integration), biome clean, npm run build clean.

Commit: 3aae84c


Progress tracked by mach6

@aebrer
aebrer marked this pull request as ready for review September 4, 2026 18:26
@aebrer

aebrer commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Unverified Review Candidates — Pending Assessment

Review round: 1
Reviewed commit: 3aae84c

These are unverified candidates. Severity reflects reviewer confidence; do not treat any item as a merge blocker until the assessment comment is posted.

Critical

(none)

Important

(none)

Suggestions

Finding 1 — Missing store test: non-compacting hydrate clears a stale in-memory compaction entry (confidence 85, test-reviewer)

The plan's Testing Approach lists "hydrateSession with isCompacting: false after an in-memory auto_compaction_start (emit the envelope first) → compaction status entry cleared," but none of the six new store tests covers this direction. The drill-in path (store.ts hydrateSession) is the only snapshot-apply path that does not wholesale-reset statusEntrieshydrateSnapshot does session.statusEntries = [] first — so it relies entirely on syncCompactionStatusEntry to filter a stale key: "compaction" entry when the authoritative snapshot flag is false. The untested regression: if the helper's filter were broken, or a caller were reordered so session.compacting is assigned after restoreSnapshotOutcomeState, a stale "compacting context…" banner plus an inert "stop compaction" button would survive re-entry into a session that is not compacting (clicking it is a safe no-op abort). The true-side store test pins the caller ordering and the reducer test pins the helper's removal, but nothing combines a memory-born entry (from a live start consumed while the fleet view was open) with a snapshot asserting isCompacting: false. Fix: emit auto_compaction_start, hydrate with isCompacting: false, assert no key: "compaction" entry remains.

Finding 2 — Extract a shared compactionSummaryEntry builder (confidence 80, simplifier)

Both new sites in reducer.ts — the compactionSummary branch of messagesToEntries (L443–449) and the auto_compaction_end handler (L808–826) — construct an identical 4-field summary card with the identical "context compacted" fallback. The file already has a small private entry-builder idiom (userMessageToEntry, providerErrorText, …). Extracting a compactionSummaryEntry(summary, tokensBefore): SummaryEntry builder puts the shape and fallback string in one place, and the dedup predicate would read from the builder's output. Pure refactor; behavior unchanged.

Strengths

  • Minimal, well-targeted fix — ~26 lines of production code addressing both root causes from the issue, with no out-of-scope changes (branchSummary and the fleet chip untouched, as scoped in the issue and plan).
  • Single convergence pointsyncCompactionStatusEntry makes live start/end and both hydrate paths (drill-in + full resync) converge on the authoritative compacting flag; the idempotent filter+push makes replay races trivially safe. It also fixes a latent pre-PR bug where a replayed auto_compaction_start could stack a second "compacting context…" banner (the old code pushed without filtering).
  • Byte-identical indication — the hydrated status entry has exactly the same shape as the live one ({ key: "compaction", text: "compacting context…", tone: "info" }), so AC1's "same indication" holds by construction.
  • Dedup is order-safe and collision-safe — the guard matches only the documented snapshot-barrier race; tokensBefore is a required number end-to-end, and auto_compaction_end is emitted only after appendCompaction + replaceMessages, so snapshot and live values are always identical for the same compaction; a genuinely different compaction is still appended (tested).
  • No runtime throw risk — new code reads only optional fields with ??; the statusEntries reassignment (filter + push) is the established mutation idiom under Solid produce/mutateSession, with no reactive-tracking breakage.
  • Lifecycle consistent — the close path (retainClosedSession) already filters the compaction key, so closed sessions show no stale banner; an aborted/errored end correctly removes the banner and leaves the distinct compaction-error entry.
  • Tests match the plan — the progress comment's counts (9 reducer + 6 store) match the diff exactly; every planned test is implemented except Finding 1. Both hydrate paths are store-tested for the restore direction; dedup is tested in both directions at the reducer level.
  • Docs claim verifiedpackages/coding-agent/docs/dashboard.md already describes "compaction/branch summaries" in the transcript and the compaction status banner/abort control, so no doc change is needed.
  • Verification green — full dashboard suite (1178 tests) including event-hub live-vs-replayed parity and the real-server hard-refresh integration test; biome and type-check clean (per agent runs).

Agents run: code-reviewer, error-auditor (retry after a context-overflow failure on first attempt; retry succeeded), test-reviewer, completeness-checker, simplifier


Reviewed by mach6

@aebrer

aebrer commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Review Assessment

Unverified review candidates (findings comment)

Classifications

Finding Classification Reasoning
Finding 1 — Missing store test: non-compacting hydrate clears a stale in-memory compaction entry useful follow-up Factual: PASS — the plan's Testing Approach explicitly lists this store test and none of the six new store tests covers the isCompacting: false-after-live-start direction; the drill-in path (store.ts:1288:1305) really does not wholesale-reset statusEntries, unlike hydrateSnapshot (store.ts:993). Scope: PASS — part of the approved plan's deliverable 4 (unit and store tests). Practical: FAIL — the current code already prevents the stale banner in that exact scenario: hydrateSession assigns session.compacting from the snapshot before restoreSnapshotOutcomeState, and syncCompactionStatusEntry always filters key !== "compaction" first, re-adding only when state.compacting is true (reducer.ts:477-480). No reachable trigger sequence in the current code produces a stale compaction banner on re-entry; the missing test protects against a future reorder/filter regression, not a present defect.
Finding 2 — Extract a shared compactionSummaryEntry builder nitpick Factual: PASS — both new sites (reducer.ts:439-449, reducer.ts:807-824) construct the identical 4-field card with the identical "context compacted" fallback, and the file has an established small-builder idiom. Scope: FAIL — a pure refactor not required by issue 502 or the approved deliverables. Practical: FAIL — the duplication changes no runtime behavior, violates no acceptance criterion, and causes no supported-user harm before merge.

Additional verification: the independent assessor re-traced the changed code itself and found no PR-introduced issue the candidates missed.

Action Plan

No merge blockers. Nothing is required before merge.


Assessment by mach6

@aebrer
aebrer merged commit 2c2814e into master Sep 4, 2026
3 checks passed
@aebrer
aebrer deleted the feature/issue-502-compaction-reentry branch September 4, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restore compaction status and summary in dashboard on session re-entry

1 participant