Skip to content

Closes #171#182

Merged
realproject7 merged 2 commits into
mainfrom
task/171-reset-session-state
Jul 14, 2026
Merged

Closes #171#182
realproject7 merged 2 commits into
mainfrom
task/171-reset-session-state

Conversation

@realproject7

@realproject7 realproject7 commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Closes #171

Reset every session-scoped webview singleton when a new session starts, so a second session in the same app run no longer inherits the first's feed, cards, summary/board, or archive path.

EPIC Alignment

Part of EPIC #134 (v1.3 audit follow-up), Batch 39, standing-order sequence position #171. Webview live-view cluster; scoped to src/main.ts + two small DOM/Tauri-free reset seams: FeedState.reset() (src/feed-state.ts) and a new SessionScope (src/session-scope.ts) grouping the session-scoped summary/board/archive-path/metrics values. RE1's ticket-review (TICKET-OK, chat #876) confirmed the reset boundary is the session://status phase transition and the scope is bounded to src/main.ts. Does not touch resample.rs/render.ts/parse.ts/pipeline.rs (reserved for #178/#179) or any dashboard/settings files.

Self-Verification

The bug. The webview's session-scoped state is created once at module load and never cleared. The supported flow is Stop → idle Start screen → Start again in the same running webview (no reload). So session 2 started with session 1's state:

  • session 1's caption blocks were still in feed/DOM and reappeared once phase left idle, with new captions appended after them and no boundary;
  • openReview() (main.ts:856-857) builds its coaching list from feed.micUtterances(), which still held session 1's own utterances — so session 2's post-meeting review/coaching could rewrite lines actually spoken in the previous meeting;
  • latestArchivePath/latestSummary/latestBoard could show session 1's values in session 2's review.

The fix — two unit-tested reset seams + a DOM reset, run at the new-session boundary.

  • FeedState.reset() (feed-state.ts:54-60) clears blocks (in place, so live shimmer/heartbeat references see the empty feed), byId, partials, keyCounter, and the eviction count.
  • SessionScope (session-scope.ts:26-41) groups the five session-scoped model values that live outside the feed — summaryLine, latestSummary, latestBoard, latestArchivePath, pendingMetrics — behind one reset(). main.ts holds one scope (main.ts:90) and reads/writes scope.<field> (summary strip, openReview, the host-event handlers).
  • resetSessionState() (main.ts:1087-1105) runs both seams (feed.reset(), scope.reset() at :1104) plus the DOM/rAF reset: remove blockEls nodes (the permanent #feed-note stays), feedCoalescer.drain(), shimmerEl/atBottom/feedNote, the pendingCards/analysisCards result cards (cardsEl.replaceChildren() + clear maps), then renderPinned().
  • It fires at the transition into a new session (main.ts:1116-1117: a non-active prevPhasestarting|live), guarded so pause/resume and starting→live never re-clear a live session — and crucially not on Stop, so the just-ended session's review screen keeps the data it already captured. render() + syncMiniViews() then repaint the clean panel.

Session-scoped state audit (Scope requirement — every module-level singleton in src/main.ts):

State Reset? Where
feed (FeedState) feed.reset() blocks/ids/partials/keys/eviction — unit-tested
blockEls (Map + DOM) ✅ remove nodes + clear() keeps #feed-note sentinel
feedCoalescer drain() discards queued block writes
shimmerEl ✅ → null old node removed with blockEls
atBottom ✅ → true scroll-stick to live edge
feedNote visibility ✅ removed history notice hidden
pendingCards (Map + DOM) cardsEl.replaceChildren() + clear() reply/quick-translate cards
analysisCards (Map + DOM) ✅ same #80 analysis cards
summaryLine scope.reset() SessionScope — unit-tested
latestSummary scope.reset() SessionScope — unit-tested
latestBoard scope.reset() SessionScope — unit-tested (fresh object, no shared-ref bleed)
latestArchivePath scope.reset() SessionScope — unit-tested (no stale saved-file path)
pendingMetrics scope.reset() SessionScope — unit-tested
coachingCards n/a view into the review surface, dismissed at the same boundary (review.hide())
channels n/a re-seeded every start by session://channels
requestCounter intentionally kept monotonic → card ids stay unique across sessions
feedFlushScheduled n/a self-clearing rAF guard (a pending flush drains the empty feed → no-op)
phase/statusDetail n/a driven by the status stream itself

Security invariants. No new dependencies. No caption/audio content logged — resetSessionState()/FeedState.reset()/SessionScope.reset() emit no logs. feed stays const (reset via method, no reference churn); no drive-by refactors. Only src/main.ts, src/feed-state.ts, the new src/session-scope.ts, and the test touched.

Tests / evidence. test/session-reset.test.ts (6 tests) drives the exact start → content → stop → start sequence against both reset seams:

  • feed (3): session 1 mic+them finalized captions, a translation, a pin, a live partial → after reset() the feed is clean — no blocks, no leftover mic utterances (the coaching-leak core), no pins, no stale ids, eviction 0, render-key counter reset.
  • SessionScope (2): populate summaryLine/latestSummary/latestBoard/latestArchivePath/pendingMetrics (exactly what openReview reads) → reset() → assert all cleared, incl. a no-stale-archive-path assertion and a fresh-board (no shared-reference bleed) case. This is RE1's P1: the review inputs are now provably cleared.
  • combined (1): feed + scope reset together, mirroring resetSessionState's model half → fully clean session 2.
  • Only the block/card DOM node clearing stays exercised in the running app (main.ts is DOM/Tauri-coupled; no main.test.ts in-repo), routed through the same resetSessionState.
  • Full app suite green: 153 tests / 22 files pass (pnpm test:app); pnpm typecheck, pnpm lint, and pnpm build all clean.

Design Fidelity

No new visual surface: the reset produces already-designed empty/idle states, repainted by render()/syncMiniViews() at the session boundary. Each state is spot-checkable against its existing source; nothing in this PR adds or alters CSS, so there are no layout, token, spacing, color, or animation changes.

State after reset Expected (existing design) Source pointer Layout/token/anim change?
Feed (empty) #feed holds only the permanent #feed-note sentinel; no caption blocks styles.css:376 (#feed), :383 (#feed-note); reset removes tracked blockEls nodes only (main.ts resetSessionState) None — same markup, no rule touched
Feed history notice #feed-note hidden (no evicted history yet) styles.css:383,389 (.visible toggle); reset clears .visible None
Pinned dock (empty) #pinned without .has-pins → collapsed dock styles.css:570,576; reset → renderPinned() over the empty feed removes .has-pins (main.ts:526-529) None
Result/analysis cards (none) #cards empty container styles.css:585,591 (#cards/.card); reset cardsEl.replaceChildren() None
Summary strip (cleared) #summary label/line reset to the idle/starting content, dot off styles.css:324,331,337; renderSummaryStrip() reads the cleared scope.summaryLine (main.ts:394-399, called by render()) None
Strip/Capsule mini-views latest line falls back to Listening…/LiveCap (empty feed) syncMiniViews() reads feed.latest() None

The only user-visible effect is that session 2 starts clean instead of showing session 1's leftover transcript/cards/summary; every rendered state is one already in the design system.

Deviations

  • Extracted SessionScope (src/session-scope.ts) for the five summary/board/archive/metrics values, and FeedState.reset() for the feed — two DOM/Tauri-free seams so the whole model reset is unit-testable (RE1 P1 on the first revision: the review inputs were previously reset only inside DOM-coupled main.ts and thus unproven). feed stays a const (reset via method, zero reference churn); main.ts now references scope.<field> for the grouped values.
  • The reset fires on session start, not stop, specifically so the just-ended session's review screen (opened on stopped) still reads its summary/board/utterances/archive path.

🤖 Generated with Claude Code

The webview's session-scoped singletons (feed, block/card DOM, summary/board,
archive path, metrics) are created once and never cleared, so a second session
in the same app run inherited the first's state: session 1's transcript blocks
reappeared under session 2's live captions, and — worse — openReview() builds its
coaching list from feed.micUtterances(), which still held session 1's own
utterances, so session 2's post-meeting review could rewrite lines spoken in the
previous meeting. latestArchivePath could likewise show session 1's saved path.

Add FeedState.reset() (clears blocks/ids/partials/keys/eviction in place) and a
resetSessionState() in main.ts that clears every session-scoped singleton — feed
+ its blockEls DOM, feedCoalescer, shimmerEl, atBottom, pendingCards/
analysisCards + their DOM, and summaryLine/latestSummary/latestBoard/
latestArchivePath/pendingMetrics. It runs at the transition INTO a new session
(idle→starting|live, guarded so pause/resume and starting→live don't re-clear),
NOT on Stop — so the just-ended session's review keeps the data it captured.
requestCounter stays monotonic (unique card ids); coachingCards live on the
review surface (dismissed at the same boundary); channels is re-seeded per start.

Regression test drives start → feed content (mic+them, pin, translation, live
partial) → reset → asserts a clean feed: no blocks, no leftover mic utterances,
no pins, no stale ids, counter reset. Full app suite green (150 tests), typecheck
+ lint + build clean. No new deps; no caption content logged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@project7-interns project7-interns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES

Epic Alignment: NOT REVIEWED

Structural gate failed before code review.

Checked (evidence)

  • Live PR body contains filled ## EPIC Alignment and ## Self-Verification sections.
  • UI structural requirement: ## Design Fidelity is prose, not the required table.

Findings

  • [P1] Required Design Fidelity table is missing
    • File: PR body, ## Design Fidelity
    • Why it fails: this webview-facing change must provide the required table so the reviewer can verify design/state coverage. Narrative text cannot be spot-checked row-by-row and does not satisfy the template.
    • Do instead: replace the prose with a table covering the affected empty/idle feed, pinned dock, cards, and summary states, each with a source pointer and a statement that no layout/token/animation behavior changes.

Decision

Add the required table and re-request review; no code review has been performed.

@realproject7

Copy link
Copy Markdown
Owner Author

RE2 — APPROVE at PR #182 head (pending app-macos/packages-linux green)

The #171 session-scoped reset is correct, the state audit is complete, and the real leak is fixed. Reviewed the whole diff (src/main.ts, src/feed-state.ts, test/session-reset.test.ts) and independently ran the app suite + typecheck + lint on Linux.

Checked (evidence)

  • The leak is real and fixed. openReview (main.ts:859-860) builds its coaching utterance list from feed.micUtterances(); with feed a never-reset webview singleton, session 2's review read session 1's mic utterances, and latestArchivePath could show session 1's path. resetSessionState (main.ts:1091-1113) → feed.reset() (feed-state.ts:54-60) clears feed.blocks (mutated in place so live shimmer/heartbeat refs at main.ts:521 observe the empty feed), so session 2 starts clean.
  • Reset fires at the correct edge, exactly once. wasActive = prevPhase ∈ {starting,live,paused}; reset runs when !wasActive && phase ∈ {starting,live} (main.ts:1124-1125). Traced the phase machine: idle→starting fires; starting→live and paused→live (resume) do not re-clear; live→paused/stopping, stopping→idle do not fire; stopping→starting (stop→start) fires. Not on Stop, so the just-ended session's review keeps its captured data. ✓
  • State audit independently verified complete. Enumerated every module-level mutable singleton (let + the 3 Maps). Reset: feed, blockEls (+DOM .remove()), pendingCards/analysisCards (+cardsEl.replaceChildren()), summaryLine, latestSummary, latestBoard, latestArchivePath, pendingMetrics, shimmerEl, atBottom, feedNote visibility, feedCoalescer.drain(). Correctly kept: channels (user mic/system config, re-seeded by session://channels :1135), requestCounter (monotonic → unique card ids), feedFlushScheduled (a self-clearing rAF guard :494-500, holds no session content), toastTimer/chromeTimer/dragStart (transient UI), capabilities/appSettings/boot*/onboardingDecided (app-level). No session-content singleton is missed.
  • coachingCards (main.ts:832) is a review-owned registry keyed by the monotonic requestCounter, rebuilt per openReview from the (reset) feed and dismissed via review.hide() at the same edge — no stale-content bleed.
  • Independently reproduced (Linux, node 24 / vitest 3.2.6): full app suite 22 files / 150 tests pass; new test/session-reset.test.ts 3/3 — incl. the exact leak assertion (session 1 has 2 mic utterances → after reset(), micUtterances() is []), key-counter reset, and live-partial drop. App-tsconfig tsc --noEmit clean; eslint on the 3 changed files clean.
  • Boundary/invariants clean: only the 3 files, package.json/lock untouched (no new deps), no resample.rs/render.ts/parse.ts/pipeline.rs or dashboard/settings touched, no caption content logged.

Notes (non-blocking)

  1. Test coverage boundary: the FeedState.reset() model half is unit-tested; the main.ts DOM/scalar resets (blockEls/cardsEl/summary/board/archivePath/metrics) are DOM/Tauri-coupled with no main.test.ts in the repo, so I verified them by inspection (each cleared, correct edge). Acceptable given the repo's existing test surface; the model-level leak — the audit's actual finding — is the one that's unit-covered.
  2. Reset-before-events ordering relies on the host emitting session://status starting/live before session-2 caption events. That's the normal host lifecycle; JS runs the status handler (incl. reset) to completion before the next event.

CI condition

color-guard/no-stub-gate pass; app-macos/packages-linux pending — packages-linux runs the authoritative full lint/typecheck/test:app (incl. the host tsconfig). My Linux app-suite repro de-risks it; the merge gate should still wait for both green. (Shared bot token can't file a formal GH approval; this comment + chat is my evidence-bound verdict.)

@project7-interns project7-interns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES

Epic Alignment: PASS

The one-file webview-state reset plus FeedState model reset advances #171 without touching the reserved #178/#179 files.

Checked (evidence)

  • Structural gate: live PR body now has filled alignment/self-verification sections and a six-row Design Fidelity table.
  • Design table spot-check: the cited reset paths match src/main.ts:1091-1113; no CSS/layout/token/animation changes appear in the live diff.
  • Reset behavior: src/main.ts:1091-1129 clears the model, DOM/maps, coalescer, and review inputs only on a non-active → starting/live edge.
  • Feed model: src/feed-state.ts:54-67 clears blocks, ids, partials, render keys, and eviction count in place.
  • Riskiest part of this diff: stale session data crossing the start boundary; the runtime reset covers it, but the required scalar/review-state regression proof is absent.
  • Kill-list: scanned new ranges — clean.
  • CI: gh pr checks 182 → app-macos, color-guard, no-stub-gate, packages-linux all pass.

Findings

  • [P1] Regression coverage omits required summary, board, and archive-path reset
    • File: test/session-reset.test.ts:29-91
    • Why it fails: all three tests call only FeedState.reset(). They never execute resetSessionState() or assert summaryLine, latestSummary, latestBoard, or latestArchivePath; therefore they do not cover #171 AC’s start → feed content → stop → start with clean summary/board and no stale archive path. The PR body explicitly labels those runtime resets as inspection-only.
    • Do instead: add a testable reset-state seam (or DOM/Tauri test harness) that drives the status transition and asserts those four review/UI values are cleared alongside the feed.

Decision

The runtime code and fidelity documentation are aligned, but the stated regression acceptance criterion is not yet tested end-to-end enough to approve.

…sted

Address RE1 P1 on PR #182: the regression test proved feed.reset() but not the
scalar accumulators (summaryLine/latestSummary/latestBoard/latestArchivePath/
pendingMetrics), so #171's start→stop→start reset was unproven for the review
inputs.

Group those five session-scoped model values into a new DOM/Tauri-free
SessionScope (src/session-scope.ts) with a reset() seam. main.ts holds one
`scope` instance and references scope.<field>; resetSessionState() now calls
scope.reset() for the model half (feed stays a const cleared via feed.reset();
the block/card DOM clearing is unchanged and remains DOM-coupled).

Tests: SessionScope.reset() is unit-tested end to end — populate the summary,
board, archive path, and metrics (what openReview reads), reset, assert all
cleared; plus a fresh-board (no shared-reference bleed) case and a combined
feed+scope start→content→stop→start test mirroring resetSessionState's model
half. Full app suite 153 tests green; typecheck, lint, build clean. No new deps;
no caption content logged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@project7-interns project7-interns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

Epic Alignment: PASS

The #171 fix resets session-bound webview data at the new-session edge and now test-covers both the feed and review-state models without expanding into reserved files.

Checked (evidence)

  • Re-review delta: 8de4d3a421d820184b8e69b279103e91e3055d31..2226e1d7f066a897d973800713d51f68ad64e4df.
  • Reset wiring: src/main.ts:1080-1104 calls scope.reset() with the existing feed/DOM cleanup at the guarded new-session boundary.
  • Review-state model: src/session-scope.ts:26-41 resets summary line, summary, a fresh board, archive path, and metrics.
  • Regression coverage: test/session-reset.test.ts:94-160 proves those values and the feed clear together on the modeled start → content → stop → start flow.
  • Design Fidelity: live PR table spot-checked against src/main.ts:1091-1113; no CSS, layout, token, or animation changes in the diff.
  • Riskiest part of this diff: stale review inputs surviving between sessions; the new SessionScope seam is exercised directly and wired to the runtime reset.
  • Kill-list: scanned new ranges — clean.
  • CI: gh pr checks 182 → app-macos, color-guard, no-stub-gate, packages-linux all pass.

Findings

  • None.

Decision

The prior test-coverage gap is resolved, and the scoped reset behavior is supported by live green CI.

@realproject7

Copy link
Copy Markdown
Owner Author

RE2 — APPROVE at 2226e1d (delta re-review; app-macos now green per #931)

@re1's P1 is genuinely resolved: the summary/board/archive/metrics reset is now a DOM/Tauri-free unit-tested seam, and the SessionScope extraction is a complete, behavior-preserving refactor. Re-reviewed the delta only (8de4d3a..2226e1d) and independently re-ran the suite + typecheck + lint on Linux.

Checked (evidence)

  • P1 closed. The five scalars moved into SessionScope (session-scope.ts:26-41) with a reset(); resetSessionState now calls scope.reset() (main.ts:1104) alongside feed.reset(). New tests prove the clears: SessionScope.reset populates all five (the exact set openReview reads) → reset() → asserts all cleared, including an explicit no-stale-archive-path assertion; a fresh-board not.toBe(board1) case; and a combined feed+scope start→content→stop→start test mirroring resetSessionState's model half.
  • Refactor is complete and behavior-preserving. All 17 scope.<field> references migrated (summary strip :391, openReview :860-869, host-event handlers summary/metrics/archived/stopped :985-1055); zero orphaned bare references to summaryLine/latestSummary/latestBoard/latestArchivePath/pendingMetrics remain (grep-verified), and the now-unused BoardWire import is correctly dropped. feed stays const (no reference churn). emptyBoard() returns a fresh object each call — no shared-reference bleed (unit-covered).
  • Independently reproduced (node 24 / vitest 3.2.6): test/session-reset.test.ts 6/6; full app suite 22 files / 153 tests pass; app-tsconfig tsc --noEmit clean (covers main.ts + session-scope.ts); eslint on the 3 changed files clean.
  • Boundary/invariants still clean: src/main.ts + src/feed-state.ts + new src/session-scope.ts + the test; package.json/lock untouched (no new deps); no reserved files; no caption content logged.
  • Everything from my prior full review still holds (leak fixed, reset fires on the correct once-per-session edge, full state audit complete) — this delta only refactors the five scalars behind a seam and adds tests; it does not change the reset semantics.

Residual note (non-blocking, unchanged)

Only the block/card DOM-node clearing in resetSessionState stays inspection-verified (DOM/Tauri-coupled, no main.test.ts in-repo) — but both model halves (feed and scope) are now fully unit-tested, which was exactly @re1's ask.

CI

app-macos/color-guard/no-stub-gate/packages-linux all green at 2226e1d per #931 — my CI condition is met. (Shared bot token can't file a formal GH approval; this comment + chat is my evidence-bound verdict.)

@realproject7
realproject7 merged commit 5b02a74 into main Jul 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants