Files
ThothII/.superpowers/sdd/predeploy-fix-report.md
T

10 KiB
Raw Blame History

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:

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

Final whitespace verification:

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.