930 lines
30 KiB
Markdown
930 lines
30 KiB
Markdown
# 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:
|
||
|
||
1. Resume leaves the prior selection, Zustand state, document panel, and EventSource untouched
|
||
until `POST /resume` succeeds. 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.
|
||
2. 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.
|
||
3. 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 returns
|
||
`Promise<{ id: string; alreadyActive: boolean }>` via `ResumeSessionResult`.
|
||
- Backend successful Resume always returns the same shape:
|
||
- running/waiting runtime: `{ id, alreadyActive: true }`
|
||
- validated cold runtime: `{ id, alreadyActive: false }`
|
||
- `SseHub.publish(sessionId, event, data): number` returns the assigned SSE id.
|
||
- `SseHub.subscribe(sessionId, send, { afterId, pending })` calls
|
||
`send(event, data, id)` for replay/live frames with `id > afterId`.
|
||
- `GET /sessions/:id/events` accepts native `Last-Event-ID` and 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 }`, and `event` must be a
|
||
non-empty string.
|
||
|
||
## Files
|
||
|
||
Backend production:
|
||
|
||
- `backend/src/bridge/session-bridge.ts`
|
||
- `backend/src/routes/sessions.ts`
|
||
- `backend/src/sse/sse-hub.ts`
|
||
|
||
Backend tests:
|
||
|
||
- `backend/test/routes-sessions.test.ts`
|
||
- `backend/test/session-bridge.test.ts`
|
||
- `backend/test/sse-hub.test.ts`
|
||
- `backend/test/sse-route.test.ts` (new)
|
||
|
||
Frontend production/support:
|
||
|
||
- `frontend/src/api/sessions.ts`
|
||
- `frontend/src/api/types.ts`
|
||
- `frontend/src/shell/AppShell.tsx`
|
||
- `frontend/src/store/sessionStore.ts`
|
||
- `frontend/src/stream/useSessionStream.ts`
|
||
- `frontend/src/test/fakeEventSource.ts`
|
||
|
||
Frontend tests:
|
||
|
||
- `frontend/src/api/sessions.test.ts`
|
||
- `frontend/src/shell/AppShell.session-mgmt.test.tsx`
|
||
- `frontend/src/store/sessionStore.test.ts`
|
||
- `frontend/src/stream/useSessionStream.test.tsx`
|
||
|
||
## TDD RED/GREEN evidence
|
||
|
||
### 1. Backend Resume result and client-boundary allowlists
|
||
|
||
RED command:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/routes-sessions.test.ts test/session-bridge.test.ts
|
||
```
|
||
|
||
RED output (exit 1):
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/routes-sessions.test.ts test/session-bridge.test.ts
|
||
```
|
||
|
||
GREEN output (exit 0):
|
||
|
||
```text
|
||
✓ 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:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/sse-hub.test.ts test/sse-route.test.ts
|
||
```
|
||
|
||
RED output (exit 1):
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/sse-hub.test.ts test/sse-route.test.ts
|
||
```
|
||
|
||
GREEN output (exit 0):
|
||
|
||
```text
|
||
✓ 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:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx src/store/sessionStore.test.ts
|
||
```
|
||
|
||
RED output (exit 1):
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx src/store/sessionStore.test.ts
|
||
```
|
||
|
||
GREEN output (exit 0):
|
||
|
||
```text
|
||
✓ 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:
|
||
|
||
```text
|
||
cd frontend && npx tsc -b
|
||
```
|
||
|
||
Typed API RED output (exit 1):
|
||
|
||
```text
|
||
src/api/sessions.test.ts(43,9): error TS2322: Type 'void' is not assignable to type
|
||
'{ id: string; alreadyActive: boolean; }'.
|
||
```
|
||
|
||
Lifecycle RED command:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/api/sessions.test.ts src/shell/AppShell.session-mgmt.test.tsx
|
||
```
|
||
|
||
Lifecycle RED output (exit 1):
|
||
|
||
```text
|
||
✓ 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:
|
||
|
||
```text
|
||
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):
|
||
|
||
```text
|
||
✓ 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:
|
||
|
||
```text
|
||
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):
|
||
|
||
```text
|
||
Test Files 6 passed (6)
|
||
Tests 52 passed (52)
|
||
```
|
||
|
||
Backend typecheck:
|
||
|
||
```text
|
||
cd backend && npx tsc --noEmit -p .
|
||
```
|
||
|
||
Output: no output, exit 0.
|
||
|
||
Frontend command:
|
||
|
||
```text
|
||
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):
|
||
|
||
```text
|
||
Test Files 8 passed (8)
|
||
Tests 74 passed (74)
|
||
```
|
||
|
||
Frontend typecheck:
|
||
|
||
```text
|
||
cd frontend && npx tsc -b
|
||
```
|
||
|
||
Output: no output, exit 0.
|
||
|
||
## Full verification
|
||
|
||
Backend full suite:
|
||
|
||
```text
|
||
cd backend && npx vitest run
|
||
```
|
||
|
||
```text
|
||
Test Files 22 passed (22)
|
||
Tests 177 passed (177)
|
||
```
|
||
|
||
Frontend full suite:
|
||
|
||
```text
|
||
cd frontend && npx vitest run
|
||
```
|
||
|
||
```text
|
||
Test Files 43 passed (43)
|
||
Tests 271 passed (271)
|
||
```
|
||
|
||
Backend production build:
|
||
|
||
```text
|
||
cd backend && npm run build
|
||
> tsc -p tsconfig.json
|
||
exit 0
|
||
```
|
||
|
||
Frontend production build:
|
||
|
||
```text
|
||
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:
|
||
|
||
1. `AppShell` passes a dedicated cursor-reset epoch to `useSessionStream`. A cold same-session
|
||
Resume increments it only after `alreadyActive: false`; a high cursor such as `901` is 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.
|
||
2. `useSessionStream` no 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.
|
||
3. 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
|
||
first `Resuming session` publish. Reopen/create failure returns exactly
|
||
`Session 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 returns `alreadyActive: true`.
|
||
4. `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 use `clear`, which
|
||
preserves the id sequence.
|
||
|
||
### Re-review files
|
||
|
||
Production:
|
||
|
||
- `backend/src/pi/pi-process-manager.ts`
|
||
- `backend/src/routes/sessions.ts`
|
||
- `backend/src/sse/sse-hub.ts`
|
||
- `frontend/src/shell/AppShell.tsx`
|
||
- `frontend/src/stream/useSessionStream.ts`
|
||
|
||
Tests/support:
|
||
|
||
- `backend/test/pi-process-manager.test.ts`
|
||
- `backend/test/routes-sessions.test.ts`
|
||
- `backend/test/sse-hub.test.ts`
|
||
- `frontend/src/shell/AppShell.session-mgmt.test.tsx`
|
||
- `frontend/src/stream/useSessionStream.test.tsx`
|
||
- `frontend/src/test/fakeEventSource.ts`
|
||
|
||
### Re-review TDD RED/GREEN evidence
|
||
|
||
Frontend RED command:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx \
|
||
src/shell/AppShell.session-mgmt.test.tsx
|
||
```
|
||
|
||
RED output (exit 1):
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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):
|
||
|
||
```text
|
||
Test Files 2 passed (2)
|
||
Tests 24 passed (24)
|
||
TypeScript: no output, exit 0
|
||
```
|
||
|
||
Backend RED command:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/sse-hub.test.ts test/routes-sessions.test.ts
|
||
```
|
||
|
||
RED output (exit 1):
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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):
|
||
|
||
```text
|
||
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)
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
git diff --check
|
||
no output, exit 0
|
||
```
|
||
|
||
## Self-review
|
||
|
||
- Resume sequencing: reopen and runtime binding precede backend `clear` and 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`;
|
||
`clear` retains 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.error` is unused in public responses; generic Pi system events are
|
||
reconstructed rather than spread; frontend type mirrors the two-field event.
|
||
- Scope: `git diff` contains no CTE/Card/harness/workflow/persistence/model changes. Four pre-existing
|
||
modified `.superpowers/sdd/{progress,task-2-report,task-3-report,task-4-report}.md` files 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 `clear` by design so later in-process
|
||
cold same-id Resume cannot reuse ids. Permanent DELETE removes the counter via `forget`.
|
||
- The Delete-then-Resume adversarial route test proves the deleted session is not resurrected but
|
||
currently receives the runner's generic HTTP 500 when `sessionShow` can no longer find it. A
|
||
future API cleanup can normalize that missing-session response to 404 or 409.
|
||
- Frontend tests still print pre-existing MSW unhandled-request and React ref/`act` warnings 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 unmanaged `agent_end`, its binding is released and later events are rejected.
|
||
- `PiProcessManager` kills 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: false` then `alreadyActive: true` results 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:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/pi-process-manager.test.ts test/routes-sessions.test.ts
|
||
```
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/stream/useSessionStream.test.tsx \
|
||
src/shell/AppShell.session-mgmt.test.tsx
|
||
```
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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.
|
||
|
||
## Stale-bootstrap, lifecycle-lock, and competing-Resume hardening
|
||
|
||
Date: 2026-07-15
|
||
Base: `08b1f4909e8eb7538156cecc2e7a6cafb46ddfc7`
|
||
|
||
This follow-up closes asynchronous identity/order and multi-client transport gaps found in the
|
||
pre-deployment review:
|
||
|
||
- `PiProcessManager.teardownIfCurrent(id, runtime)` makes teardown an identity-checked operation.
|
||
Bootstrap re-checks identity after configuration/retrieval and before both the public
|
||
`Starting model` event and model start. Its failure continuation acquires the same session
|
||
lifecycle lock, claims only its own runtime identity, and holds serialization through persisted
|
||
failure and the public terminal sequence. A continuation left behind by Close or DELETE cannot
|
||
target a replacement or recreate forgotten SSE state.
|
||
- The former Resume-only promise tail is now a per-session lifecycle lock shared by Resume, Close,
|
||
and DELETE. Each route reads the current runtime inside the lock immediately before replacement
|
||
or removal and uses identity-checked teardown. Deferred route tests prove both orderings:
|
||
Resume then Close/Delete finishes removed with no post-removal bootstrap event; Close then Resume
|
||
creates only after Close completes; DELETE then Resume cannot recreate a deleted session.
|
||
- AppShell assigns each Resume invocation a monotonic token and records the latest target. A
|
||
completion for a different, superseding session id cannot reset the store, select a source, close
|
||
the panel, or repaint phase from a late manifest. Same-id invocations are per-target single-flight
|
||
operations through the POST and local binding commit: repeated pre-commit clicks update the
|
||
shared operation's latest token but issue no second POST or commit path. The operation becomes
|
||
joinable again before its manifest fetch, whose repaint remains token/id/selection guarded. Start
|
||
new, Stop, streamed session exit, and active-session deletion invalidate pending Resume work.
|
||
This prevents stale-source preservation and reverse/non-Resume intent overwrite without allowing
|
||
a slow manifest to suppress a later explicit rebind.
|
||
- `SseHub` subscriber registrations now carry idempotent transport-close callbacks. `clear` and
|
||
`forget` snapshot and actively close every response before discarding runtime transport state;
|
||
callback-driven unsubscription during that iteration is safe. The SSE route ends its response so
|
||
native EventSource reconnects with `Last-Event-ID`. Post-clear events retain monotonic ids and are
|
||
buffered for replay; `forget` additionally resets the id state.
|
||
|
||
Production files:
|
||
|
||
- `backend/src/pi/pi-process-manager.ts`
|
||
- `backend/src/routes/sessions.ts`
|
||
- `backend/src/sse/sse-hub.ts`
|
||
- `frontend/src/shell/AppShell.tsx`
|
||
|
||
Regression tests:
|
||
|
||
- `backend/test/pi-process-manager.test.ts`
|
||
- `backend/test/routes-sessions.test.ts`
|
||
- `backend/test/sse-hub.test.ts`
|
||
- `backend/test/sse-route.test.ts`
|
||
- `frontend/src/shell/AppShell.session-mgmt.test.tsx`
|
||
|
||
### TDD RED/GREEN evidence
|
||
|
||
Runtime identity API RED:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/pi-process-manager.test.ts -t "identity-checked teardown"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 1 failed | 39 skipped (40)
|
||
TypeError: mgr.teardownIfCurrent is not a function
|
||
```
|
||
|
||
Runtime identity API GREEN:
|
||
|
||
```text
|
||
Test Files 1 passed (1)
|
||
Tests 1 passed | 39 skipped (40)
|
||
```
|
||
|
||
Deferred bootstrap RED:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/routes-sessions.test.ts \
|
||
-t "stale bootstrap|bootstrap that"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 6 failed | 35 skipped (41)
|
||
|
||
close/delete + replacement: stale continuation removed the replacement runtime
|
||
delete without replacement: stale continuation called failSession after forget
|
||
```
|
||
|
||
Deferred bootstrap GREEN:
|
||
|
||
```text
|
||
Test Files 1 passed (1)
|
||
Tests 6 passed | 35 skipped (41)
|
||
```
|
||
|
||
Shared lifecycle ordering RED:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/routes-sessions.test.ts \
|
||
-t "Resume followed|Close followed|Delete followed"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 4 failed | 41 skipped (45)
|
||
|
||
All four deferred assertions observed the competing route settle before the first lifecycle
|
||
operation released.
|
||
```
|
||
|
||
Bootstrap plus lifecycle GREEN:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/routes-sessions.test.ts \
|
||
-t "Resume followed|Close followed|Delete followed|stale bootstrap|bootstrap that"
|
||
|
||
Test Files 1 passed (1)
|
||
Tests 10 passed | 35 skipped (45)
|
||
```
|
||
|
||
Competing frontend Resume RED:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx \
|
||
-t "competing Resume|stale Resume manifest"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 2 failed | 16 skipped (18)
|
||
|
||
reverse POST completion opened a second, stale EventSource
|
||
late s1 manifest repainted the selected s3 phase from F3 to F7
|
||
```
|
||
|
||
Competing and same-id Resume GREEN:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx \
|
||
-t "competing Resume|stale Resume manifest|false then true"
|
||
|
||
Test Files 1 passed (1)
|
||
Tests 3 passed | 15 skipped (18)
|
||
```
|
||
|
||
### Independent-review hardening RED/GREEN
|
||
|
||
The first final review reported no Critical findings and three Important edge cases: bootstrap
|
||
could start during an in-progress Close; bootstrap-owned failure was persisted twice; and an older
|
||
same-id result could overwrite newer state. The integrated reviewer also required non-Resume
|
||
navigation to invalidate pending Resume work. The final main review tightened the same-ID contract
|
||
to true single-flight so a second same-target click cannot preserve a dead pre-restart source.
|
||
|
||
Backend review RED:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/routes-sessions.test.ts \
|
||
-t "Close suppresses|bootstrap failure persists once"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 2 failed | 45 skipped (47)
|
||
|
||
deferred configure started Pi while closeSession was still pending
|
||
bootstrap/public failure called failSession twice
|
||
```
|
||
|
||
Backend review GREEN:
|
||
|
||
```text
|
||
Test Files 1 passed (1)
|
||
Tests 2 passed | 45 skipped (47)
|
||
```
|
||
|
||
Same-id single-flight RED:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx \
|
||
-t "share one cold request"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 1 failed | 18 skipped (19)
|
||
|
||
two concurrent same-ID invocations issued two cold POSTs (three total including initial activation)
|
||
```
|
||
|
||
Non-Resume invalidation RED:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx \
|
||
-t "starting a new question invalidates"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 1 failed | 19 skipped (20)
|
||
|
||
the late Resume opened an EventSource after Start new returned to the landing state
|
||
```
|
||
|
||
Frontend review GREEN:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx \
|
||
-t "share one cold request|competing Resume|stale Resume manifest|starting a new question invalidates"
|
||
|
||
Test Files 1 passed (1)
|
||
Tests 4 passed | 15 skipped (19)
|
||
```
|
||
|
||
Post-commit single-flight lifetime RED:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx \
|
||
-t "releases same-id single-flight"
|
||
|
||
Test Files 1 failed (1)
|
||
Tests 1 failed | 19 skipped (20)
|
||
|
||
s1 committed and waited on its manifest; after s3 superseded it, a new s1 Resume reused the old
|
||
operation and issued no second s1 POST (expected 2, received 1).
|
||
```
|
||
|
||
Same-id and manifest lifetime GREEN:
|
||
|
||
```text
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx -t "same-id|manifest"
|
||
Test Files 1 passed (1)
|
||
Tests 4 passed | 16 skipped (20)
|
||
|
||
cd frontend && npx tsc -b
|
||
no output, exit 0
|
||
```
|
||
|
||
Multi-client SSE disconnect RED:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/sse-hub.test.ts test/sse-route.test.ts
|
||
|
||
Test Files 2 failed (2)
|
||
Tests 3 failed | 6 passed (9)
|
||
|
||
clear/forget invoked zero of two registered close callbacks, and two live HTTP SSE responses timed
|
||
out instead of reaching EOF after clear.
|
||
```
|
||
|
||
Multi-client SSE disconnect GREEN:
|
||
|
||
```text
|
||
cd backend && npx vitest run test/sse-hub.test.ts test/sse-route.test.ts
|
||
Test Files 2 passed (2)
|
||
Tests 9 passed (9)
|
||
|
||
cd backend && npx tsc --noEmit -p .
|
||
no output, exit 0
|
||
```
|
||
|
||
The Hub tests use two subscribers whose close callbacks immediately unsubscribe themselves, proving
|
||
safe snapshot iteration and exactly-once closure. The live-route test opens two HTTP streams, proves
|
||
both receive EOF on clear, publishes a new event and gate, then reconnects after id 1 and replays
|
||
exactly ids 2 and 3. The forget test closes both subscribers and proves the next id resets to 1.
|
||
|
||
Close now removes the observed runtime identity before awaiting persistence. Failure persistence is
|
||
claimed once per runtime and lifecycle-serialized; bootstrap's public `session_failed` cannot start
|
||
a duplicate. A per-target in-flight map owns the only same-ID POST and commit while its mutable
|
||
latest token keeps s1→s2→s1 ordering correct; it is removed immediately after the binding commit,
|
||
before awaiting the independently guarded manifest. One shared invalidation helper is called when
|
||
active deletion, streamed exit, Start new, or Stop begins.
|
||
|
||
### Focused verification
|
||
|
||
```text
|
||
cd backend && npx vitest run test/routes-sessions.test.ts test/pi-process-manager.test.ts \
|
||
test/sse-hub.test.ts test/sse-route.test.ts
|
||
Test Files 4 passed (4)
|
||
Tests 96 passed (96)
|
||
|
||
cd backend && npx tsc --noEmit -p .
|
||
no output, exit 0
|
||
|
||
cd frontend && npx vitest run src/shell/AppShell.session-mgmt.test.tsx \
|
||
src/shell/AppShell.new-session.test.tsx src/stream/useSessionStream.test.tsx
|
||
Test Files 3 passed (3)
|
||
Tests 36 passed (36)
|
||
|
||
cd frontend && npx tsc -b
|
||
no output, exit 0
|
||
```
|
||
|
||
### Full verification
|
||
|
||
```text
|
||
cd backend && npx vitest run
|
||
Test Files 22 passed (22)
|
||
Tests 202 passed (202)
|
||
|
||
cd frontend && npx vitest run
|
||
Test Files 43 passed (43)
|
||
Tests 280 passed (280)
|
||
|
||
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 frontend suite/build retain the previously documented MSW, React ref/`act`, experimental type
|
||
stripping, and large-chunk warnings. No warning class was introduced by this wave. No harness,
|
||
workflow, persistence, SQL/CTE viewer, model-selection, or deployment file changed. The four
|
||
pre-existing modified `.superpowers/sdd/{progress,task-2-report,task-3-report,task-4-report}.md`
|
||
files remain excluded from staging.
|
||
|
||
### Final independent-review verdict
|
||
|
||
After the multi-client transport fix, the independent reviewer reported no Critical, Important, or
|
||
Minor findings. Its own focused verification passed 96 backend transport/lifecycle tests, 31
|
||
frontend Resume/stream tests, both TypeScript checks, and `git diff --check`. Final assessment:
|
||
**Ready to deploy: Yes.**
|