pause, resume and restart a background job without asking the agent
Three buttons in the expanded task panel, beside Stop. Not on the chip: the panel is where you decide what to do with a job, and a row of unlabelled glyphs on a pill is where you misclick. ── Pause / Resume ── SIGSTOP and SIGCONT — the only pair that means "stop doing things and keep everything". The process keeps its memory, its open files and its bound port, and picks up mid-instruction. Signals go to every pid holding the job's output file, because for a shell job that is the shell AND what it spawned, and freezing one of them would be worse than freezing neither. Verified by watching a job that writes a line a second: 3 lines, frozen at 3 across three seconds of pause, 6 after resume. Worth knowing before pressing it: a paused server does not REFUSE connections, it stops answering them, so a browser pointed at it hangs rather than erroring. That is what pausing is, not a shortcoming here. ── Restart, and the cost it accepts ── The command is recovered by joining the transcript to itself: the harness records the `tool_use` carrying `command`, and separately its result — the text `Command running in background with ID: <id>`. Neither alone is enough; together they are a join on `tool_use_id`, and that is the only place the command survives. The transcript is found FROM the output path rather than searched for, since a task file at `<tmp>/claude-<uid>/<slug>/<session>/tasks/<id>.output` names both. The respawned process appends to that same output file. That is what keeps everything else working untouched — the log view reads that file, and Stop finds the process by asking which pids hold it open. The real cost, chosen deliberately: the platform becomes a SECOND owner of a task. The harness stops watching when its own child dies, so the platform supervises what it spawned and emits the same `task:started` / `task:notification` pair everything downstream is built on. `emitToSession` is the seam, and it is the only one — nothing else learns that a task can have two kinds of parent. A restart re-announces an id already in the transcript, so `task:started` now RESETS an existing task row rather than appending a second one. Without that the tray showed the job twice, once finished and once running. Restart is offered for a finished job too, which is the case that matters most: a dev server that fell over is exactly what you want back without typing a sentence at an agent that may be mid-turn. Verified end to end against a planted task: command recovered, old process killed, new one spawned, output APPENDED (`first run output` then `RESTARTED-OK`), exit reported through the callback, supervision registry released afterwards. `[open]` A restarted job does not survive an `officer` restart — the platform is its parent now. The harness's own jobs never did either. tsgo clean, frontend builds, 890 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -318,6 +318,25 @@ function createMessageHandler(sessionId: string, model: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Push an event into a live session from OUTSIDE a turn.
|
||||
*
|
||||
* The restart button is the only caller: the platform respawns a process the harness is no longer watching,
|
||||
* so nothing else would emit the `task:started` / `task:notification` pair that the tray, the log view and
|
||||
* every reconnect are built on. Folded into the session first and then sent, in that order, so a browser
|
||||
* that reconnects a second later sees the same thing as one that was already listening.
|
||||
*
|
||||
* Returns false when the session is not live — a restart is still legitimate then, it simply has nobody to
|
||||
* tell.
|
||||
*/
|
||||
export function emitToSession(sessionId: string, msg: TurnMessage): boolean {
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
if (!session) return false;
|
||||
foldIntoSession(session, msg, session.meta.model ?? '');
|
||||
for (const socket of session.sockets) sendToClient(socket as ServerWebSocket<WSData>, msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleChat(
|
||||
ws: ServerWebSocket<WSData>,
|
||||
msg: {
|
||||
|
||||
Reference in New Issue
Block a user