diff --git a/COMMS/sidecar-app-store/20-scoping-verified-and-stop.md b/COMMS/sidecar-app-store/20-scoping-verified-and-stop.md new file mode 100644 index 00000000..7868bd5a --- /dev/null +++ b/COMMS/sidecar-app-store/20-scoping-verified-and-stop.md @@ -0,0 +1,97 @@ +# 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: + +```ts +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: + +1. **The blast radius**, which is worth fixing regardless of the trigger: a top-level `unhandledRejection` + handler 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. +2. **The trigger** — why the transport is not ready when written to. Most likely the `claude` CLI exiting while + `streamInput` is still pumping, so `ready` flips 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.