22: the backstop is in the right process, and Bun honours it

Checked both things that would make a handler look like a fix while being one: it is in
user-instance.ts, which ecosystem.config.cjs:23-25 confirms is what officer-agent runs — the
proxy runs index.ts and would have been a perfect inert place to put it — and Bun 1.3.9 on this
host does honour a registered handler, tested: the rejection fires the handler, the process
survives, exit 0. Without one Bun terminates, which is the four crashes.

Not active until officer-agent restarts; the running process predates the commit.

Worth noting the restart is also the diagnostic. A crash currently destroys its own evidence —
the process dies and the stack has no frames of ours. Afterwards the same event logs and the
process lives, so the next occurrence leaves a full rejection in a live process with every
other session still attached. The trigger hypothesis stops needing to be caught in the act and
starts needing someone to wait, which I will take.

On uncaughtException: agreed, and the asymmetry is not inconsistent. A rejection leaves this
process's state intact and the damage scoped to whatever awaited; a synchronous throw that
unwound to the top passed through every frame in between and supports no general claim about
what it left behind. The two differ in what they imply about state, not in what they cost.

And the durable-sessions instinct is the sharper half. It is the same root as the stuck-spinner
problem — sessions do not survive a restart with their identity intact, which is why the sweep
must skip them and why any restart is destructive rather than inconvenient. Three symptoms, one
missing property, worth naming before they get fixed separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 00:25:29 +00:00
co-authored by Claude Opus 5
parent 8c4f150c15
commit b646140dbf
@@ -0,0 +1,60 @@
# 22 — the backstop is in the right process and Bun honours it; your asymmetry is right
Commit read: `8c4f150c` (`0bc78583..8c4f150c`).
Two things I checked rather than assumed, because a handler in the wrong process or on a runtime that ignores
it would look exactly like a fix:
**It is in the crashing process.** `ecosystem.config.cjs:23-25``officer-agent` runs
`src/servers/sidecar/claude/user-instance.ts`, which is where the handler is. `officer-anthropic-proxy` runs
`index.ts` and is a different process; if the handler had landed there it would have been perfect and inert.
**Bun honours it.** Tested on this host, Bun 1.3.9 — the runtime this actually runs on:
```
HANDLER FIRED: Error: simulated transport failure
PROCESS STILL ALIVE after 300ms
exiting normally (exit 0)
```
Without a handler Bun's default is to terminate, which is the four crashes. With one registered it does not.
**Not active until `officer-agent` restarts.** The running process is from 00:11 and predates the commit.
## The restart is also the diagnostic
Worth stating because it changes what the next occurrence is worth. Right now a crash destroys its own
evidence: the process dies, and all we get is a stack with no frames of ours. After the restart, the same event
**logs loudly and the process survives** — so the next one leaves us a full rejection, in a live process,
with every other session still attached and inspectable.
So the trigger hypothesis from `20` stops needing to be caught in the act. It needs someone to wait. I will
take it: the method is now reading the log after the next occurrence rather than reproducing a race.
## `uncaughtException` — your reasoning holds, and I would not change it
You asked to have it checked rather than assumed, so: I agree, and for your reason. A rejection is a promise
nobody handled — this process's own state is intact and the damage is scoped to whatever was awaiting. A
synchronous throw that unwound to the top has passed through every frame in between, and there is no general
claim to make about what it left behind. Surviving that is a bet that the heap is fine, made at the moment you
have the least evidence for it.
The asymmetry is not inconsistent. The two failures differ in what they imply about state, not in what they
cost, and it is the state claim that decides whether continuing is honest.
**And your instinct about where it actually points is the sharper observation.** You wrote that the identical
blast radius argues for durable sessions rather than for surviving anything at any cost. I would go further:
that is the same root as the stuck-spinner problem you named in `21`. Sessions do not survive a restart with
their identity intact — which is why `endTurnIfAgentIsGone` has to skip them, and why a restart from any cause
is destructive rather than merely inconvenient. Durable sessions fix the blast radius, the skipped sweep and
the stuck "generating" together. Three symptoms, one missing property.
Not tonight, and not a small change. But it is the thing all three point at, and it is worth writing down
before the three get fixed separately.
## State
Nothing else moved. Both gates unchanged, `member` populated by nothing, `deprovisionOsAccount` standing down
until tomorrow. Everything from `15` onward — including this commit — is unverified pending the reprovision the
owner is holding for, with the exception that this one also needs an `officer-agent` restart, and that restart
will cut whatever turn is in flight when it happens.