The big change today was pulling web sessions out of the web server and into a long-lived daemon. Until now, restarting assist sessions killed every running terminal with it — fine when the server never changed, painful the moment I was iterating on it. So I split the two: web sessions are now owned by a daemon process, and the server is a thin client that relays WebSocket traffic to it over a local IPC socket (a unix domain socket at ~/.assist/daemon/daemon.sock, or a named pipe on Windows). Restart the server and the sessions keep running with their scrollback intact. The daemon logs to its own file, auto-exits once no sessions remain and no client has connected for 60 seconds, and gets respawned on demand by the server when it’s needed again.

This was a large move — SessionManager, createSession, wirePtyEvents, writeToSession and the rest of the session core all migrated from web/ into daemon/, with new pieces for connecting (connectToDaemon, handleConnection, greetClient), persistence (loadPersistedSessions, restoreSession), and lifecycle (runDaemon, restartDaemon, shutdownSessions). It comes with a small command surface: assist daemon run, status, stop, and restart. The mental model I landed on: web server changes only need assist sessions restarted and sessions survive, while daemon or session-core changes need assist daemon restart to load the new code — which kills the PTYs but auto-respawns claude sessions via claude --resume, with run sessions reappearing as not-restored tiles you can retry.

Rearchitecture like that always shakes out races. assist daemon restart and concurrent assist sessions invocations could both try to spawn a daemon and orphan one of them, so I added a proper bring-up path — a PID-file watchdog (startPidFileWatchdog), socket-theft detection (reportStolenSocket), and an onListening handshake — so only one daemon ever owns the socket and the loser steps aside cleanly. I also fixed a PTY spawn failure that was crashing the whole server: the spawn helper now gets its executable bit ensured up front, and a spawn error surfaces as an error snackbar in the UI over the WebSocket instead of taking the process down with it.

The other feature was --once. The launcher commands — assist next, draft, bug, refine — normally chain: finish one item and they prompt for the next. With --once they exit after the first completed run instead, which is what I want when I’m spawning a session for a single task and don’t want it sitting around waiting. That’s backed by a new assist signal done marker that --once sessions watch for and plain sessions ignore.

A few smaller things rounded out the day. assist review now ensures .assist/reviews is gitignored before it writes there, so review artifacts don’t leak into commits. The web UI got a pointer cursor on buttons and links (a small thing that bugged me every time), and backlog items that are in progress now get a highlight and an InProgressChip so the active work stands out in the list. And I tightened the /draft and /refine prompts to enforce vertical slicing in the plans they produce.