diff --git a/docs/open-threads-after-per-user-claude.md b/docs/open-threads-after-per-user-claude.md new file mode 100644 index 00000000..642a5aea --- /dev/null +++ b/docs/open-threads-after-per-user-claude.md @@ -0,0 +1,80 @@ +# Open threads after per-user Claude + +Three things found on 2026-08-11/12 that are understood but not finished. They were written up in the +`COMMS/sidecar-app-store` channel, which was deleted when the feature merged — this file is what survives. +None of them blocks per-user Claude; all three were found while proving it worked. + +--- + +## 1. The web terminal renders a long URL unreadably + +**Half fixed.** `2a8f0049` added an OSC 52 handler, so a program's "press `c` to copy" now reaches the +browser clipboard. That is the path a user is meant to take, and it works. + +**Not fixed:** the URL itself renders as fragments. Claude Code's first-run login prints an OAuth URL of +~400 characters; in the web terminal it appeared as scattered characters with large gaps (`h : l`), with +nothing selectable or readable. On a normal terminal the same output wraps and reads fine. + +Why it matters: first-run login is every new member's first five minutes, and the workaround was running +`claude` under `tmux` on the server, capturing the pane, and reassembling the URL by hand across three wrapped +lines. That is not something a member can be asked to do, and without OSC 52 there was no other way out. + +Not diagnosed. What is known: + +- the frontend loads `FitAddon`, `Unicode11Addon` and `WebLinksAddon`, and `allowProposedApi` is on +- `cols`/`rows` are sent on connect (`Terminal.tsx`) and on resize, so it is not obviously a sizing problem +- the pty gives `cols: Number(...) || 0` (`sidecar/pty/server.mjs:62`), so a client that omits them yields 0 + +Where I would start: capture the raw bytes the pty emits for that line and compare against what xterm renders. +Either the TUI is positioning with escapes xterm handles differently, or the width the program believes it has +disagrees with the width the terminal has. + +## 2. Agent sessions do not survive a restart — one property behind three symptoms + +Worth fixing as one thing, because it currently presents as three and invites three separate fixes: + +- **Blast radius.** An unhandled rejection used to kill the agent sidecar and every live session with it. + `8c4f150c` made that survivable, but any *real* restart still loses every session. +- **The restart sweep must skip.** `endTurnIfAgentIsGone` asks the agent whether a session is really still + generating. Scoped by `userId` since `d59adbf1`, so a session with no recorded `userId` has no safe identity + to ask as and is skipped — correct, and it leaves that session marked generating. +- **Stuck "generating".** The user-visible face of the above. A spinner that never resolves after an agent + restart is this, not the UI. + +The missing property is that a session does not survive a restart with its identity intact. Given that, the +sweep would not need to skip, a restart would be an inconvenience rather than a loss, and the spinner would +resolve itself. + +## 3. `ProcessTransport is not ready for writing` — survivable, still unexplained + +``` +error: ProcessTransport is not ready for writing + at write (…/claude-agent-sdk/sdk.mjs) + at streamInput (…/claude-agent-sdk/sdk.mjs) +``` + +Four fatal crashes on 2026-08-11, one of which truncated a turn mid-sentence. 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. With +no handler registered it reached the top level and Bun exited, taking every session on the machine. + +`8c4f150c` registered an `unhandledRejection` handler in `sidecar/claude/user-instance.ts`, which is the +process `ecosystem.config.cjs` starts as `officer-agent`. Verified on Bun 1.3.9: the handler fires and the +process survives. It has fired once in production since. + +**The cause is still unknown.** Best hypothesis: the `claude` CLI exits while `streamInput` is still pumping, +so the transport's `ready` flips false mid-write. Unconfirmed. + +It no longer needs to be caught in the act — it needs someone to look after it happens. The next occurrence +logs a full rejection in a *live* process with every other session still attached, which is a much better +vantage point than a corpse. + +Markers in `~/.pm2/logs/officer-agent-error.log`: + +``` +grep -c 'Bun v1.3' → fatal exits. Was 4. A fifth means the backstop stopped working. +grep -c 'UNHANDLED REJECTION' → caught and survived. Was 1. +``` + +`uncaughtException` is deliberately not handled the same way: a rejection leaves the process's state intact, +whereas a synchronous throw that unwound to the top supports no such claim, and continuing on a possibly +corrupted heap is worse than restarting. That asymmetry is an argument for §2 rather than against itself.