19 KiB
Pre-deployment Fix Wave Report
Date: 2026-07-14
Worktree: /home/chirone/ThothII/.worktrees/activity-log-cte-layout
Base: e5366d14a6da8fb331d94be60b8929cefb1fe3e0
Outcome
All three reviewed findings are implemented in one coherent backend/frontend wave:
- Resume leaves the prior selection, Zustand state, document panel, and EventSource untouched
until
POST /resumesucceeds. Cold Resume changes state and reconnects only after backend clear/rebind; already-active same-session Resume preserves the existing binding; failure is a no-op apart from the fixed toast. - SSE uses monotonically increasing per-session ids, cursor-filtered replay, native and manual
reconnect cursors, id continuity across
hub.clear, and descriptor-id pending-gate idempotence at both backend and frontend layers. - Generic Pi system events and readiness errors are projected through explicit public allowlists. Sentinel URLs, paths, tokens, stderr, commands, and extra fields do not reach HTTP or SSE.
No harness, workflow, persistence, model, CTE viewer, CTE card, or shared Card file changed.
Interfaces
- Frontend
resumeSession(id)now returnsPromise<{ id: string; alreadyActive: boolean }>viaResumeSessionResult. - Backend successful Resume always returns the same shape:
- running/waiting runtime:
{ id, alreadyActive: true } - validated cold runtime:
{ id, alreadyActive: false }
- running/waiting runtime:
SseHub.publish(sessionId, event, data): numberreturns the assigned SSE id.SseHub.subscribe(sessionId, send, { afterId, pending })callssend(event, data, id)for replay/live frames withid > afterId.GET /sessions/:id/eventsaccepts nativeLast-Event-IDand manual?lastEventId=<integer>; when both are valid it uses the greater cursor.- Every emitted SSE frame is
id: <n>\nevent: <name>\ndata: <json>\n\n. - Public readiness failure is exactly:
Session services are not ready. Check configuration and connectivity, then try again. - Generic Pi system events are exactly
{ type: "system_event", event }, andeventmust be a non-empty string.
Files
Backend production:
backend/src/bridge/session-bridge.tsbackend/src/routes/sessions.tsbackend/src/sse/sse-hub.ts
Backend tests:
backend/test/routes-sessions.test.tsbackend/test/session-bridge.test.tsbackend/test/sse-hub.test.tsbackend/test/sse-route.test.ts(new)
Frontend production/support:
frontend/src/api/sessions.tsfrontend/src/api/types.tsfrontend/src/shell/AppShell.tsxfrontend/src/store/sessionStore.tsfrontend/src/stream/useSessionStream.tsfrontend/src/test/fakeEventSource.ts
Frontend tests:
frontend/src/api/sessions.test.tsfrontend/src/shell/AppShell.session-mgmt.test.tsxfrontend/src/store/sessionStore.test.tsfrontend/src/stream/useSessionStream.test.tsx
TDD RED/GREEN evidence
1. Backend Resume result and client-boundary allowlists
RED command:
cd backend && npx vitest run test/routes-sessions.test.ts test/session-bridge.test.ts
RED output (exit 1):
Test Files 2 failed (2)
Tests 6 failed | 35 passed (41)
expected { id: 's1' } to deeply equal { id: 's1', alreadyActive: false }
expected raw readiness URL/token/path to equal the fixed public message
expected three raw generic system events to equal [{ type: 'system_event', event: 'session_exit' }]
GREEN command:
cd backend && npx vitest run test/routes-sessions.test.ts test/session-bridge.test.ts
GREEN output (exit 0):
✓ test/session-bridge.test.ts (14 tests)
✓ test/routes-sessions.test.ts (27 tests)
Test Files 2 passed (2)
Tests 41 passed (41)
2. Backend exact-once SseHub and route framing
RED command:
cd backend && npx vitest run test/sse-hub.test.ts test/sse-route.test.ts
RED output (exit 1):
Test Files 2 failed (2)
Tests 7 failed (7)
expected [undefined, undefined, undefined] to deeply equal [1, 2, 3]
expected unconditional replay not to contain "one" / "two"
expected one buffered pending gate, received replay plus a second pending emission
GREEN command:
cd backend && npx vitest run test/sse-hub.test.ts test/sse-route.test.ts
GREEN output (exit 0):
✓ test/sse-hub.test.ts (4 tests)
✓ test/sse-route.test.ts (3 tests)
Test Files 2 passed (2)
Tests 7 passed (7)
3. Frontend cursor tracking and gate idempotence
RED command:
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx src/store/sessionStore.test.ts
RED output (exit 1):
Test Files 2 failed (2)
Tests 2 failed | 28 passed (30)
expected /sessions/s1/events to be /sessions/s1/events?lastEventId=7
expected duplicate gate pendingWidget to remain null, received gate-1
GREEN command:
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx src/store/sessionStore.test.ts
GREEN output (exit 0):
✓ src/store/sessionStore.test.ts (23 tests)
✓ src/stream/useSessionStream.test.tsx (7 tests)
Test Files 2 passed (2)
Tests 30 passed (30)
4. Frontend typed Resume and AppShell ordering/preservation
Typed API RED command:
cd frontend && npx tsc -b
Typed API RED output (exit 1):
src/api/sessions.test.ts(43,9): error TS2322: Type 'void' is not assignable to type
'{ id: string; alreadyActive: boolean; }'.
Lifecycle RED command:
cd frontend && npx vitest run src/api/sessions.test.ts src/shell/AppShell.session-mgmt.test.tsx
Lifecycle RED output (exit 1):
✓ src/api/sessions.test.ts (9 tests)
❯ src/shell/AppShell.session-mgmt.test.tsx (15 tests | 4 failed)
Test Files 1 failed | 1 passed (2)
Tests 4 failed | 20 passed (24)
already-active same-session Resume created two EventSources instead of one
deferred cold Resume closed the document panel before POST completion
failed same-session Resume closed the prior EventSource
failed Resume with no active session opened an EventSource
GREEN commands:
cd frontend && npx vitest run src/api/sessions.test.ts src/shell/AppShell.session-mgmt.test.tsx
cd frontend && npx tsc -b
GREEN output (exit 0):
✓ src/api/sessions.test.ts (9 tests)
✓ src/shell/AppShell.session-mgmt.test.tsx (15 tests)
Test Files 2 passed (2)
Tests 24 passed (24)
TypeScript: no output, exit 0
The AppShell cold-reconnect test additionally proves that the old source accepts an event while
Resume is pending, the replacement URL carries lastEventId=8, the replacement receives one
post-resume transcript/activity row, and two deliveries of the same descriptor id yield one gate.
Affected verification
Backend command:
cd backend && npx vitest run test/routes-sessions.test.ts test/session-bridge.test.ts \
test/sse-hub.test.ts test/sse-route.test.ts test/health.test.ts test/e2e-f1.test.ts
Output (exit 0):
Test Files 6 passed (6)
Tests 52 passed (52)
Backend typecheck:
cd backend && npx tsc --noEmit -p .
Output: no output, exit 0.
Frontend command:
cd frontend && npx vitest run src/api/sessions.test.ts src/store/sessionStore.test.ts \
src/stream/useSessionStream.test.tsx src/shell/AppShell.session-mgmt.test.tsx \
src/shell/CentralStatus.test.tsx src/shell/ModelActivityPanel.test.tsx \
src/shell/f1-loop.test.tsx src/shell/AppShell.new-session.test.tsx
Output (exit 0):
Test Files 8 passed (8)
Tests 74 passed (74)
Frontend typecheck:
cd frontend && npx tsc -b
Output: no output, exit 0.
Full verification
Backend full suite:
cd backend && npx vitest run
Test Files 22 passed (22)
Tests 177 passed (177)
Frontend full suite:
cd frontend && npx vitest run
Test Files 43 passed (43)
Tests 271 passed (271)
Backend production build:
cd backend && npm run build
> tsc -p tsconfig.json
exit 0
Frontend production build:
cd frontend && npm run build
> tsc -b && vite build
✓ 4835 modules transformed.
✓ built in 8.25s
exit 0
Integrated re-review closure (2026-07-15)
This section supersedes the earlier cold same-session assertion that the replacement URL carries
lastEventId=8. That behavior was correct only while the backend process and its in-memory id
sequence survived. A restarted backend begins a fresh sequence, so a successful cold Resume now
explicitly discards the browser's cursor before replacing the EventSource.
All four integrated re-review findings are closed:
AppShellpasses a dedicated cursor-reset epoch touseSessionStream. A cold same-session Resume increments it only afteralreadyActive: false; a high cursor such as901is omitted from the replacement URL and fresh low-id events/gates are consumed. An already-active same-session Resume still preserves its source, cursor, and store.useSessionStreamno longer mutates the cursor ref during render. Effect setup resets cursor state on session/reset-epoch changes, callbacks are guarded by a captured active-source identity, and cleanup clears only its own active identity. A queued event from the replaced source cannot write the new store or poison its next reconnect URL.- Backend Resume is serialized per session and rechecks runtime state inside the lock. Manifest,
readiness, and reopen validation precede the transport commit. Idle/failed replacement creates
and binds the new runtime before
hub.clear, which occurs synchronously immediately before the firstResuming sessionpublish. Reopen/create failure returns exactlySession could not be resumed. Check configuration and connectivity, then try again., keeps the prior hub buffer/subscribers attached, and does not expose exception sentinels. Concurrent calls perform one cold start and the waiter returnsalreadyActive: true. SseHub.forget(id)removes subscribers, buffered events, and the last id. Permanent session DELETE invokes it after disk deletion; ordinary close and Resume continue to useclear, which preserves the id sequence.
Re-review files
Production:
backend/src/pi/pi-process-manager.tsbackend/src/routes/sessions.tsbackend/src/sse/sse-hub.tsfrontend/src/shell/AppShell.tsxfrontend/src/stream/useSessionStream.ts
Tests/support:
backend/test/pi-process-manager.test.tsbackend/test/routes-sessions.test.tsbackend/test/sse-hub.test.tsfrontend/src/shell/AppShell.session-mgmt.test.tsxfrontend/src/stream/useSessionStream.test.tsxfrontend/src/test/fakeEventSource.ts
Re-review TDD RED/GREEN evidence
Frontend RED command:
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx \
src/shell/AppShell.session-mgmt.test.tsx
RED output (exit 1):
Test Files 2 failed (2)
Tests 3 failed | 21 passed (24)
reset epoch: expected the old source to close, received false
cold same-session: expected /sessions/s1/events, received ?lastEventId=901
stale source: expected an empty transcript, received "stale session one"
Frontend GREEN command:
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx \
src/shell/AppShell.session-mgmt.test.tsx
cd frontend && npx tsc -b
GREEN output (exit 0):
Test Files 2 passed (2)
Tests 24 passed (24)
TypeScript: no output, exit 0
Backend RED command:
cd backend && npx vitest run test/sse-hub.test.ts test/routes-sessions.test.ts
RED output (exit 1):
Test Files 2 failed (2)
Tests 8 failed | 28 passed (36)
three Resume ordering assertions observed clear before reopen/create
reopen and create sentinels escaped as raw HTTP 500 responses
the concurrent waiter cold-started again instead of returning alreadyActive: true
SseHub.forget was absent and DELETE did not invoke permanent cleanup
Backend GREEN command:
cd backend && npx vitest run test/sse-hub.test.ts test/routes-sessions.test.ts
cd backend && npx tsc --noEmit -p .
GREEN output (exit 0):
Test Files 2 passed (2)
Tests 36 passed (36)
TypeScript: no output, exit 0
The failure tests publish a post-failure probe through the same hub and prove that a subscriber attached before either reopen or create rejection still receives it. The concurrency test overlaps two same-id requests behind a deferred reopen and proves one manifest/readiness/reopen/create/clear sequence.
Initial re-review verification (before independent-review hardening)
cd backend && npx vitest run
Test Files 22 passed (22)
Tests 182 passed (182)
cd frontend && npx vitest run
Test Files 43 passed (43)
Tests 273 passed (273)
cd backend && npm run build
> tsc -p tsconfig.json
exit 0
cd frontend && npm run build
> tsc -b && vite build
✓ 4835 modules transformed.
✓ built in 8.46s
exit 0
git diff --check produced no output (exit 0). The frontend build retains its pre-existing
large-chunk warning; no new build or type errors were introduced.
Final whitespace verification:
git diff --check
no output, exit 0
Self-review
- Resume sequencing: reopen and runtime binding precede backend
clearand HTTP success; frontend state mutation and cursor-reset epoch follow it. Failure catch only emits fixed UI copy. - Already active: same-session returns before reset/generation/manifest repaint; different session resets the single-session store and binds the new id only after success.
- SSE exact-once: ids are transport identity, not content hashes; replay is strictly
id > cursor;clearretains the counter; pending gate matching uses only descriptor id. - Cursor behavior: hook tracks
MessageEvent.lastEventId, carries it only to an ordinary same-id generation, and resets it on session-id/cold-runtime epoch change. Native EventSource reconnect remains supported by the route header. - Gate defense: the Zustand set survives pending clear but resets with the session store.
- Client boundary: raw
ensure.erroris unused in public responses; generic Pi system events are reconstructed rather than spread; frontend type mirrors the two-field event. - Scope:
git diffcontains no CTE/Card/harness/workflow/persistence/model changes. Four pre-existing modified.superpowers/sdd/{progress,task-2-report,task-3-report,task-4-report}.mdfiles are user work and are excluded from staging.
Remaining concerns
- The 200-event SSE ring limit remains intentional. A brand-new page can reconstruct only retained backlog; an in-memory same-session reconnect is exact-once from its cursor.
- Per-session sequence counters remain in backend memory after
clearby design so later in-process cold same-id Resume cannot reuse ids. Permanent DELETE removes the counter viaforget. - Frontend tests still print pre-existing MSW unhandled-request and React ref/
actwarnings even though all 276 tests pass. The frontend production build still reports pre-existing large chunk warnings. Neither warning class was introduced or expanded by this change. - No live Pi/DWH smoke was run; this wave changes only REST/SSE/frontend lifecycle boundaries and is covered by fake-Pi, live Fastify SSE, component, full-suite, typecheck, and production-build gates.
Independent-review hardening
The required independent review was run repeatedly against the uncommitted diff. Its first pass found four Important lifecycle edges beyond the integrated findings: queued old-runtime callbacks, post-spawn construction cleanup, concurrent frontend Resume completions, and the passive-effect commit window. Its second pass confirmed those fixes and identified one remaining Important retention issue in the new runtime-identity map. The final pass reported no Critical, Important, or Minor findings and assessed the diff ready to merge.
The resulting hardening is:
- Runtime bridge callbacks are gated by the bound runtime identity. Replacement, close, and DELETE
invalidate the old identity, so queued old events cannot publish or call
failSession. An active runtime removed by the manager can still publish its complete public failure sequence; after the terminal unmanagedagent_end, its binding is released and later events are rejected. PiProcessManagerkills the spawned child and removes any registered map entry if either spawn-boundary stderr setup or later RPC/bridge/map initialization throws.- Resume completion compares against synchronously maintained current active-session identity.
Concurrent
alreadyActive: falsethenalreadyActive: trueresults preserve the cold source, cursor, store, and replayed gate. - Stream source replacement uses a layout effect. A deterministic later-layout-effect test delivers a queued old event inside the former commit-to-passive-cleanup window and proves it is ignored.
- Cursor tests cover both a restarted backend's fresh low ids and an in-process hub's preserved high ids followed by a cursor-bearing ordinary reconnect.
Hardening TDD RED/GREEN evidence
Backend identity/construction RED command:
cd backend && npx vitest run test/pi-process-manager.test.ts test/routes-sessions.test.ts
Test Files 2 failed (2)
Tests 3 failed | 69 passed (72)
post-spawn reader initialization did not kill the child
replaced and deleted runtime callbacks still called failSession/published
Additional spawn-boundary and terminal-release RED checks:
cd backend && npx vitest run test/pi-process-manager.test.ts \
-t "spawn boundary initialization"
Tests 1 failed | 38 skipped (39)
cd backend && npx vitest run test/routes-sessions.test.ts -t "terminal sequence"
Tests 1 failed | 34 skipped (35)
Frontend concurrency/layout RED command:
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx \
src/shell/AppShell.session-mgmt.test.tsx
Test Files 2 failed (2)
Tests 2 failed | 25 passed (27)
the later-layout-effect event wrote "commit-window stale text"
the false→true completion pair erased pending gate "cold-gate"
Final focused GREEN commands:
cd backend && npx vitest run test/pi-process-manager.test.ts \
test/routes-sessions.test.ts test/sse-hub.test.ts
cd backend && npx tsc --noEmit -p .
Test Files 3 passed (3)
Tests 79 passed (79)
TypeScript: no output, exit 0
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx \
src/shell/AppShell.session-mgmt.test.tsx
cd frontend && npx tsc -b
Test Files 2 passed (2)
Tests 27 passed (27)
TypeScript: no output, exit 0
Final full verification after review hardening
cd backend && npx vitest run
Test Files 22 passed (22)
Tests 188 passed (188)
cd frontend && npx vitest run
Test Files 43 passed (43)
Tests 276 passed (276)
cd backend && npm run build
> tsc -p tsconfig.json
exit 0
cd frontend && npm run build
> tsc -b && vite build
✓ 4835 modules transformed.
✓ built in 8.47s
exit 0
The final frontend run retains the repository's pre-existing MSW/ref/act warnings, and the build
retains the pre-existing large-chunk warning. No test, typecheck, or build failures remain.