Post-mortem shell
Open an interactive shell inside a run's preserved worktree, from the studio, to inspect what a failed (or paused/cancelled) run left behind: read files, git log/git diff, re-run a failing test — without hunting for <store-dir>/worktrees/<run-id> by hand.
Local mode only (server_info.run_shell_enabled). Cloud mode never offers it: spawning an interactive host shell from a multi-tenant API is not a thing.
Where it appears
On a run's header, the row “worktree preserved → Open post-mortem shell” shows when all of these hold:
- the run is at rest —
failed,failed_resumable,cancelled,paused_*orfinished(a live run's worktree belongs to the engine; a concurrent interactive writer would race its git operations); - the run recorded a working directory and it still exists on disk (the engine preserves worktrees on error precisely for this);
- the studio talks to the daemon that owns the store (cross-store runs are read-only).
The button and the endpoint share the same gate, so the affordance never 409s.
Semantics
- One shell per connection. Closing the panel ends the shell; reopening spawns a fresh one. The worktree is the state — the shell is a viewer, not a persistent tmux session.
$SHELL -l(/bin/bashfallback),TERM=xterm-256color,COLORTERM=truecolor, cwd = the preserved worktree,ITERION_RUN_IDexported.- Idle timeout 30 min — counting BOTH directions, so a running
htopor a long build's output keeps the session alive without keystrokes. Absolute cap 2 h. Overridable viaITERION_RUN_SHELL_IDLE_TIMEOUT/ITERION_RUN_SHELL_MAX_LIFETIME(Go durations). On teardown the whole process group is killed — asleep 999 &left behind does not outlive the socket. The same watchdog tick asks again whether the credential the shell was opened with still stands AND whether its opener still holds the run's scope — at most one store read every 30 s,ITERION_RUN_SHELL_WATCH_PERIOD: an account disabled, a session revoked, a membership removed or a super-admin demoted after the upgrade ends the shell within a tick. - Unix only; the Windows desktop build answers 501.
Wire protocol (for tooling)
GET /api/ws/runs/{id}/shell?cols=&rows= upgrades to a WebSocket:
| Direction | Frame | Payload |
|---|---|---|
| both | binary | raw PTY bytes (no envelope, no base64) |
| C→S | text | {"type":"resize","cols":N,"rows":N} · {"type":"ping"} |
| S→C | text | {"type":"pong"} · {"type":"exit"} · {"type":"error","message":"…"} |
A refused credential, a lost scope or the lifetime cap also sends a WebSocket close frame: 1008 with a reason starting credential_refused or scope_lost, 1000 with a reason starting lifetime_reached — every such reason is the token followed by its cause, as in scope_lost: the run this socket watches is gone, never the bare token, and for a refused credential or a lost scope the same cause rides an error frame ahead of the close.
Refusals answer plain HTTP before the upgrade: 403 cloud mode, 404 unknown run, 409 live run / no workdir / cross-store, 410 worktree gone or run deleted, 501 unsupported platform.
Relation to other affordances
The Files panel (read/edit via Monaco) stays the right tool for targeted file edits; the shell is for investigation — running the repo's own commands in the exact state the run left. For a run that needs code changes then a retry, combine: inspect in the shell, edit, then Resume.
