# 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.
