360 lines
10 KiB
Markdown
360 lines
10 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
|
||
```
|
||
|
||
Final whitespace verification:
|
||
|
||
```text
|
||
git diff --check
|
||
no output, exit 0
|
||
```
|
||
|
||
## Self-review
|
||
|
||
- Resume sequencing: backend `clear` and runtime binding precede the HTTP success; frontend state
|
||
mutation and stream generation 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 a same-id generation,
|
||
and resets it on session-id 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 cold
|
||
same-id Resume cannot reuse ids. This is one numeric map entry per session id for the process
|
||
lifetime.
|
||
- Frontend tests still print pre-existing MSW unhandled-request and React ref/`act` warnings even
|
||
though all 271 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.
|