Skip to content

fix(server): first-turn session-busy guard, composer draft preservation, test events isolation - #29

Merged
fengzhi09 merged 1 commit into
mainfrom
fix/first-turn-session-guard
Sep 25, 2026
Merged

fengzhi09 merged 1 commit into
mainfrom
fix/first-turn-session-guard

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

What

Three fixes from verified reproducitions (session-isolation/01):

  1. First-turn session-busy guard hole — beginRun(cid, sid) registered sid=null during a session's first turn (cs.mcodeSessionId not yet assigned), so runsBySid never guarded it: a second window sending to the same brand-new session got HTTP 200, the engine rejected it ("Session already has an active Turn") and the message was silently dropped. Fix: updateRunSid(cid, sid) backfills the run registry when the sid is first bound (idempotent, ownership-guarded, synchronous — no double-registration race; endRun semantics preserved).
  2. Composer wipes text + error on state push — a send failure looked like a silent vanish because SSE state pushes remounted the composer and cleared input/error. Fix: module-level draft store + useSyncExternalStore; typed text and error banners survive pushes/remounts and clear on successful send.
  3. Tests polluted the real ~/.mcode-webui/events.ndjson — 5 files write the production audit log during test runs. All now redirect MCODE_WEBUI_EVENTS_PATH to per-test tmp paths (production behavior unchanged).

Acceptance (independent agent)

PASS-WITH-CONCERNS — all 5 ticket criteria met. Live two-window verification: window B's send during window A's turn returns 409 {"reason":"session-busy"} with a red composer banner, typed text survives the state-push remount; after A completes, B continues the same engine session. events.ndjson byte-identical before/after test runs (zero-delta, 3×). Route tests construct the true first-turn shape (beginRun sid=null → updateRunSid → B send → 409) — 22/22 ×3 deterministic. Regression: single-window multi-turn and cross-cid parallel on different sessions clean.

Gates: typecheck 0 · full webui suite 1343/0 fail · test:webapp 198/198 · build ✓ · check:source ✓ (inventory +3). Note: router-auth-gate.check.mjs shows environment-dependent flakiness observed both failing and passing on clean trees; not related to this change.

Full pnpm verify deferred to CI.

…ft across state pushes, isolate test audit logs

First-turn session-busy guard hole: `handleSend` claimed the run with
`beginRun(cid, cs.mcodeSessionId)` before the turn existed, so on a
session's first turn the claim was registered with `sid: null` and
`runsBySid` never guarded the engine session. A second window that had
already learned the new sid (sidebar switch / restore after draft
promotion) sent to the same brand-new session and got a 200 instead of
a 409, then lost its prompt to the engine's "Session already has an
active Turn".

- state-bus.js: add `updateRunSid(cid, sid)` — backfills or re-points
  the live run's engine-session claim. It is idempotent, never steals
  another cid's claim, releases a stale claim only when this cid still
  owns it (the same ownership rule `endRun` applies), and creates no
  claim without a live run. All checks are synchronous, so a racing
  `beginRun` cannot double-register.
- mcode-acp.js: call `updateRunSid` the moment the turn's sid is
  determined (next to `bindDraftToMcodeSid`), covering the first-turn
  backfill and the load-failure fallback re-point.

Composer wipe on state push: page.tsx swaps the composer between the
home and chat tree positions the moment the first conversation line
lands — a remount that threw away the `useState`-held text, attachment
chips, and the send-error banner (a failed 409 looked like a silent
vanish). Move the draft into a module-scope store
(`webapp/lib/composer-draft.ts`) consumed via `useSyncExternalStore`,
so text, attachments, and the error banner survive any remount; only
the composer's own success/submit writes clear them.

Test audit isolation (chore): unit suites were appending junk to the
operator's real `~/.mcode-webui/events.ndjson`. Beyond the known
alert.info flood in test/lib/alerts.check.mjs, five more files
audit-wrote through the real lib/events.js: test/routes/alerts.check.mjs
(alert.*), test/lib/authorize.check.mjs (auth.* decisions),
test/routes/sessions-search.check.mjs (auth.* per search),
test/lib/mcode-session-delete.test.js (session.delete* for fixture
sids). All now redirect `MCODE_WEBUI_EVENTS_PATH` to a per-run tmp dir
(lazy per-append resolution; production behavior untouched) and verify
zero bytes land in the real log.

Tests: state-bus unit coverage for updateRunSid (backfill, race,
idempotence, re-point, no ghost claims) in send-run-guard.test.js; a
route-level contract check (chat-first-turn-session-guard.check.mjs)
driving the real chat.js → runMcodeAcp chain against a fake ACP
transport — first-turn 409 session-busy, multi-turn regression, and
cross-cid parallel sessions; composer-draft contract tests.

Gates: pnpm typecheck 0 errors; webui:typecheck 0 errors; full webui
suite 1343 tests / 0 failures (node --test --test-force-exit; one
lingering-handle singleton in mcode-acp-note.test.js predates this
change — verified on a clean tree); test:webapp 198/198; pnpm build
green; source inventory regenerated (3 new files) and check:source
green.
@fengzhi09
fengzhi09 merged commit bd2e392 into main Sep 25, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the fix/first-turn-session-guard branch September 25, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant