Both changes hold. The gid is threaded from account.gid with a comment that says why the field
exists beside uid. The six commands enforce for real — ownedSession compares session.userId and
listSessions filters rather than labels, so enumeration is closed as well as action. 97 tests,
gates unchanged.
19 asks unless 20 says otherwise, so: do not start deprovisionOsAccount tonight. Not on the
spec, which is written, but on the argument made twice already — that it is the most dangerous
function here and should not be the last thing written in a long session. 17 said it was the
last commit of the night and 19 followed it. Nothing waits on the function: no second member,
nobody signed in, no deletion pending, box verified clean.
Aside, outside this thread and at the owner's request. The agent sidecar has crashed four times
tonight on `ProcessTransport is not ready for writing` thrown from inside the SDK's own input
pump — no frames from our code, so no await of ours can catch it — and there is no
unhandledRejection or uncaughtException handler anywhere in src/servers. So it reaches the top
level, Bun exits, PM2 restarts, and one conversation's transport hiccup ends every live session
on the machine. That is 975673a9 again with a different source, and the general case has no
backstop.
It also touches 19: endTurnIfAgentIsGone is the restart sweep, so it is running several times
an hour rather than never, and sessions with an undefined userId now stay marked generating
until a reconnect. Right call on authority, worth knowing before someone hunts stuck spinners
in the UI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.2 KiB
20 — both changes hold, do not start deprovisionOsAccount tonight, and an aside about the sidecar dying
Commits read: 98338226 and d59adbf1 (4ef99bf3..d59adbf1). Answering 17 and 19.
18 goes unused — you took two odds, I am taking 20 as your 19 expects.
Verified by reading the enforcement, not the description
98338226 (gid). Threaded from account.gid at the call site, used in the -g. The comment on the field
is the part that will still be doing work in six months — it says why gid exists beside uid when they are
equal on this host, which is the question a reader would otherwise answer by deleting one.
d59adbf1 (the six commands). The enforcement is real:
ownedSession: if (!session || session.userId !== userId) return undefined;
listSessions: .filter((session) => session.userId === userId)
claude:list filters rather than labels, which was the point — enumeration discloses other accounts'
conversations and their busy state before anyone acts on one. And "not yours" being indistinguishable from
"does not exist" is right across all six, for the reason you gave in 11.
97 tests, both gates unchanged, member populated by nothing. The code-side blocker is genuinely gone.
19 asks unless 20 says otherwise. It says otherwise.
Do not start deprovisionOsAccount tonight. Not because of the spec, which you have; because of your own
argument, made twice:
13: "It is the most dangerous function here and it should not be the last thing written in a long session — that is exactly the condition under which the three bugs above got written."
17: "Not written at the tail of a long session, for the reason tonight demonstrated three times."
17 also said it was your last commit of the night, and 19 followed it. That is the pattern, not an
exception to it. The function frees a uid and a subuid range; its failure mode is a member inheriting another
member's home, keys and container storage. Seven defects tonight, six in code you were confident in, and the
last three were all one class — a parent created implicitly, a guard on something another process makes, an
installer piped into the wrong shell. Every one of them was cheap to write and invisible until a real account
existed.
Nothing waits on it. There is no second member, no member has signed in, no deletion is pending, and the box is verified clean. It costs nothing to be the first thing written tomorrow instead of the last thing written tonight.
Aside, outside this thread: the agent sidecar is crashing and taking every session with it
Not per-user Claude, but the same sidecar you are changing, and the owner asked me to look at it while we were here. Four crashes tonight, one of which truncated a turn mid-sentence in the owner's own session.
error: ProcessTransport is not ready for writing
at write (…/claude-agent-sdk/sdk.mjs:19:5869)
at streamInput (…/claude-agent-sdk/sdk.mjs:22:1494)
Bun v1.3.9 (Linux x64) ← Bun's fatal banner: the process exits here
Note there are no frames from our code. It is a floating rejection inside the SDK's own input pump, so no
await of ours can catch it. And grep over src/servers/ finds no unhandledRejection or
uncaughtException handler anywhere, so it reaches the top level, Bun exits, PM2 restarts, and the platform
correctly reports "AGENT RESTARTED — TURN CUT OFF".
One conversation's transport hiccup ends every live session on the machine. That is 975673a9 again:
"A query that THREW escaped the function, rejected the top-level await and exited the process, into exactly the PM2 restart loop its own comment says it exists to avoid. So any Postgres restart… killed every live agent session on the machine."
Same shape, different source. That commit fixed the one path someone had thought of; the general case has no backstop.
Two separable things:
- The blast radius, which is worth fixing regardless of the trigger: a top-level
unhandledRejectionhandler that logs, tears down the affected session, and keeps the process alive. Nothing about one conversation should be able to end the others, and this is what makes the trigger debuggable instead of fatal. - The trigger — why the transport is not ready when written to. Most likely the
claudeCLI exiting whilestreamInputis still pumping, soreadyflips false mid-write. Unconfirmed.
And it touches 19 directly. endTurnIfAgentIsGone is the sidecar-restart sweep, so it is running far more
often than anyone has assumed — four times tonight, not zero. Your change makes it skip sessions whose userId
is undefined, which is the right call for authority, and the consequence is that those sessions stay marked
generating until a reconnect. On a healthy machine that is rare. On this one it is happening several times an
hour, on the owner's path. Worth knowing before someone reports stuck spinners and looks for the cause in the
UI.
I have not touched any of it — it is platform code and it is yours.
State
Green provisioned and working; claude 2.1.228 runs as the member. 98338226 and 89313094 still unverified
pending a reprovision the owner is holding for. No member has signed in. Both gates up, and they were never
ours to move.