Skip to content

fix(backend): fail fast on disconnected sessions + honest error taxonomy#5

Merged
gcharang merged 1 commit into
masterfrom
m3-qa-hardening
Jul 16, 2026
Merged

fix(backend): fail fast on disconnected sessions + honest error taxonomy#5
gcharang merged 1 commit into
masterfrom
m3-qa-hardening

Conversation

@gcharang

Copy link
Copy Markdown
Contributor

Summary

Closes the three pre-existing QA follow-ups from the attended M3 e2e, plus the hardening the review lanes drove on top. Pure backend change; protocol/extension/connector untouched.

1. No-extension fail-fast (503, was a 30s-timeout 500)

CfSessionCoordinator.send() now rejects immediately unless the host reports a live, onConnect-authorized extension socket (CoordinatorHost.hasAuthorizedConnection()), and the commands route maps that to 503 {error:"extension not connected"}.

Two load-bearing details:

  • The gate reads the delivery predicate, not SessionState.status. The status scalar is a lossy last-writer-wins echo: a late onClose from a replaced socket stamps detached over a healthy reconnected session, which would wedge it into permanent 503 with no backstop (architect-lane catch). onClose now also only detaches when no other authorized socket remains.
  • Non-2xx on purpose. A 200 ok:false Event would be cached by consumers' breakwater idempotency stores and replayed even after the extension reconnects; a 503 makes @understudy/connector's callUnderstudy throw, releasing the reservation so the retry re-executes.

Ref-less dryRun writes (e.g. navigate) keep their documented zero-wire simulated-ok semantic — regression-tested.

2. Honest error taxonomy at the route

Mapped by message-prefix constants in the portable src/coordinator.ts (a prefix is the only failure signal that survives the DO RPC boundary): 503 not-connected · 504 {error:"command timed out"} · 400 {error:"invalid body"} for unparseable JSON (was a masked 500) · uniform JSON 500 via app.onError for everything else.

3. The rest

  • fill_secret checks the predicate before resolveSecret — no vault read / plaintext materialization for an undeliverable command (DL-004), spy-tested.
  • Dry-run probe failures carry the extension's own reason (dry-run: stale or unknown ref: s1e9, unexpected probe response '<type>') instead of one collapsed string.
  • Env.CALLER_TOKENS/EXTENSION_TOKENS are now required types (they were always in secrets.required; wrangler deploy --dry-run validates clean). Runtime empty-string guards stay.

Test plan

  • Backend 53 → 64 tests (5 files): fail-fast unit test; full lifecycle pending→connected→detached→reconnected; replaced-socket close ≠ detach; vault-untouched disconnected fill_secret; probe-reason fidelity incl. wrong-event-type branch; 400/500 route tests; the old 35s DO-timeout test converted into the route-level 504 proof (same wall-clock cost, full route→RPC→DO path, awaiting-marker clear re-asserted)
  • Full workspace: pnpm -r typecheck (4/4) + pnpm -r test = 142 passed
  • Quality gate: quality-reviewer APPROVE, architect should-fixes fixed with regression tests, QA round-2 adversarial SIGN-OFF (3-socket close orders, bad-token sockets never authorized, prefix-collision survey of every throw site)

Note for wrangler tail readers: fail-fast rejections print Uncaught (in promise) via workerd's DO RPC instrumentation — these are handled 503s, not real uncaught errors.

🤖 Generated with Claude Code

Closes the three QA follow-ups from the attended M3 e2e, hardened further
by this round's review lanes:

- No-extension fail-fast: CfSessionCoordinator.send() rejects immediately
  unless the host reports a live, onConnect-authorized extension socket
  (CoordinatorHost.hasAuthorizedConnection). The gate reads that delivery
  predicate - the exact precondition sendToExtension relies on - NOT the
  persisted SessionState.status scalar: a late onClose from a replaced
  socket stamps "detached" over a healthy reconnect, which would wedge a
  live session into permanent 503 (architect-lane catch). onClose likewise
  only detaches when no other authorized socket remains.

- Error taxonomy at the commands route, mapped by message-prefix constants
  (src/coordinator.ts - the only failure signal that survives the DO RPC
  boundary): 503 {extension not connected} (non-2xx on purpose: a 200
  ok:false Event would be cached by consumers' idempotency stores and
  replayed after reconnect), 504 {command timed out}, 400 {invalid body}
  for unparseable JSON, uniform JSON 500 via app.onError for the rest.
  Ref-less dryRun writes keep their documented zero-wire simulated-ok
  semantic (never a liveness signal) - regression-tested.

- fill_secret gates on the same predicate BEFORE resolveSecret: no vault
  read and no plaintext materialization for a command that cannot
  dispatch (DL-004).

- Dry-run probe failures surface the extension's own reason
  ("dry-run: stale or unknown ref: s1e9"; "unexpected probe response
  '<type>'") instead of one collapsed string.

- Env.CALLER_TOKENS/EXTENSION_TOKENS are required types - they were
  always in wrangler.jsonc secrets.required; the optional typing lied.
  auth.ts keeps the empty-string runtime guards.

Tests 53 -> 64: fail-fast unit + adversarial liveness (full
pending->connected->detached->reconnected lifecycle; a replaced socket's
close is not a detach), vault-untouched fill_secret gate, probe-reason
fidelity, 400/500 route tests, and the old 35s DO-timeout test converted
into the route-level 504 proof (same wall-clock, full route->RPC->DO
path, marker-clear re-asserted). Workspace: 142 tests green.

Known noise: fail-fast rejections log "Uncaught (in promise)" via
workerd's DO RPC instrumentation in vitest/wrangler tail - these are
handled 503s, not real uncaught errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gcharang
gcharang merged commit a825e4b into master Jul 16, 2026
1 check passed
@gcharang
gcharang deleted the m3-qa-hardening branch July 16, 2026 20:35
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