Phase 2: single per-user box container for every thread

Every thread (agent turns + terminal + project commands) now execs into one
persistent per-user container (spoon-box-{username}) instead of ephemeral
docker run --rm — so the agent and terminal share the exact same running
environment, filesystem, and in-session installs.

- docker.ts: ensureUserContainer (persistent box) + streamExecInContainer/
  runExecInContainer (docker exec, streaming) sharing a factored streamSubprocess
- user-container.ts: reference-counted box lifecycle (held while any thread
  workspace is active or a terminal is connected; idle-reaped after
  SPOON_AGENT_BOX_IDLE_MS, default 30m)
- worker.ts: runClaim acquires the box; codex turn + runProjectCommand exec into
  it; release on stop/PR/failure
- terminal.ts: execs into the shared box (dockerode TTY) instead of a per-job
  container; materializeUserHome runs the dotfiles setup in the box
- Verified: agent + terminal run in the same box, share fs, dotfiles + tmux load
This commit is contained in:
Gabriel Brown
2026-06-24 10:30:40 -04:00
parent c103430c7d
commit c6b27063a4
6 changed files with 281 additions and 152 deletions
+2
View File
@@ -46,6 +46,8 @@ export const env = {
process.env.SPOON_WORKER_TOKEN?.trim() ??
'',
terminalIdleMs: intEnv('SPOON_AGENT_TERMINAL_IDLE_MS', 1_800_000),
// How long a per-user box container survives with no active jobs/terminals.
boxIdleMs: intEnv('SPOON_AGENT_BOX_IDLE_MS', 1_800_000),
workdir: process.env.SPOON_AGENT_WORKDIR?.trim() ?? '.local/agent-work',
hostWorkdir: process.env.SPOON_AGENT_HOST_WORKDIR?.trim(),
network: process.env.SPOON_AGENT_NETWORK?.trim(),