Lane D follow-up to D5 (#50). The F1 runner lifecycle contract (#49, docs/implementation/runner-lifecycle.md) needs these D changes. D5 merged the real-Docker isolation gate but did not change agents/contract.ts, so none of them exist yet.
The F1 implementation can start against fakes now. It must not merge until items 1–5 land. Item 6 is needed before F2's writable attempts.
Before the F1 implementation merges
- Settlement always ends. The supervisor retries unfinished container, network and setup cleanup every second with no limit (
agents/adapters/supervisor.ts; D5's hand-off doc says "The supervisor retries cleanup until then"). If Docker is unreachable, settled never resolves, and shutdown waits forever with the Store open. Either bound the retries and settle with a terminal cleanup-failure result, or prove in the real-Docker suite that every cleanup path ends, including when the daemon is unreachable. (F1 decision 4 depends on this.)
- Asynchronous launch. The Claude and Codex start calls do their Docker setup synchronously (
createVendorNetwork, profile creation and startProfileInvocation use execFileSync/spawnSync). While that runs, the server cannot begin shutdown or cancel. The start call must return its handle at once and run setup asynchronously inside the handle: cancel() must work during setup, and settled must resolve only after setup cleanup.
- Owner, attempt and allocation labels on every resource.
InvocationInput gains runnerOwner, a 32-character hex ID that F creates once per database. Every allocator that runs before an invocation also takes it, together with the attempt ID and a caller-provided allocation ID: prepareTaskFilesystems (which takes no owner today) and vendor network creation. Every resource carries io.codeboost.runner, io.codeboost.attempt and io.codeboost.allocation: containers, networks, egress proxies, task-storage volumes and keepers.
- Scoped crash recovery. Add
recoverLeftovers(runnerOwner): Promise<RecoveryReport>. It acts only on resources labelled with that owner: it stops and removes agent containers, egress proxies and networks. It keeps task storage whole (volumes plus keeper) and returns an authenticated recovery handle for each, registered in D's ownership map after checking the labels. exportTaskDiff and removeTaskFilesystems must accept these handles, because ownership today lives in a process-local WeakMap that is empty after a restart. It rejects with a bounded diagnostic if anything cannot be stopped or removed. Unlabelled resources from older builds are reported, never removed.
- Abortable preparation helpers.
createTaskClone (git/clone.ts, execFileSync) and prepareTaskFilesystems (agents/container/storage.ts, execFileSync/spawnSync) block the event loop and cannot be cancelled. Add asynchronous variants that take an AbortSignal, spawn their subprocesses in their own process group, expose the group ID, and settle only after those processes have exited.
Before F2's writable attempts
- Bounded task-volume export. F cannot read Docker volumes from the host. Add
exportTaskDiff(filesystems | recoveryHandle, maxBytes): it runs git diff against the last codeboost commit in a read-only container and returns at most 1 MiB. It must accept an AbortSignal and stop, and await, its own container and subprocesses when the signal aborts, because F gives each export a 60-second deadline (maxBytes bounds only the returned data, not the Docker work). F saves this as partial output for stopped attempts.
Done when
agents/contract.ts and the storage, network and adapter APIs expose the items above, with contract tests.
- The real-Docker suite covers: cleanup against an unreachable daemon ending in settlement; cancel during asynchronous setup; recovery for two databases on one machine touching only its own labelled resources; recovery handles being accepted by export and removal after a simulated restart; and aborting clone and storage allocation with a child that ignores SIGTERM.
Refs: #49 (contract, sections "Shutdown" and "Launch"), #50 (D5), #22.
Lane D follow-up to D5 (#50). The F1 runner lifecycle contract (#49,
docs/implementation/runner-lifecycle.md) needs these D changes. D5 merged the real-Docker isolation gate but did not changeagents/contract.ts, so none of them exist yet.The F1 implementation can start against fakes now. It must not merge until items 1–5 land. Item 6 is needed before F2's writable attempts.
Before the F1 implementation merges
agents/adapters/supervisor.ts; D5's hand-off doc says "The supervisor retries cleanup until then"). If Docker is unreachable,settlednever resolves, and shutdown waits forever with theStoreopen. Either bound the retries and settle with a terminal cleanup-failure result, or prove in the real-Docker suite that every cleanup path ends, including when the daemon is unreachable. (F1 decision 4 depends on this.)createVendorNetwork, profile creation andstartProfileInvocationuseexecFileSync/spawnSync). While that runs, the server cannot begin shutdown or cancel. The start call must return its handle at once and run setup asynchronously inside the handle:cancel()must work during setup, andsettledmust resolve only after setup cleanup.InvocationInputgainsrunnerOwner, a 32-character hex ID that F creates once per database. Every allocator that runs before an invocation also takes it, together with the attempt ID and a caller-provided allocation ID:prepareTaskFilesystems(which takes no owner today) and vendor network creation. Every resource carriesio.codeboost.runner,io.codeboost.attemptandio.codeboost.allocation: containers, networks, egress proxies, task-storage volumes and keepers.recoverLeftovers(runnerOwner): Promise<RecoveryReport>. It acts only on resources labelled with that owner: it stops and removes agent containers, egress proxies and networks. It keeps task storage whole (volumes plus keeper) and returns an authenticated recovery handle for each, registered in D's ownership map after checking the labels.exportTaskDiffandremoveTaskFilesystemsmust accept these handles, because ownership today lives in a process-localWeakMapthat is empty after a restart. It rejects with a bounded diagnostic if anything cannot be stopped or removed. Unlabelled resources from older builds are reported, never removed.createTaskClone(git/clone.ts,execFileSync) andprepareTaskFilesystems(agents/container/storage.ts,execFileSync/spawnSync) block the event loop and cannot be cancelled. Add asynchronous variants that take anAbortSignal, spawn their subprocesses in their own process group, expose the group ID, and settle only after those processes have exited.Before F2's writable attempts
exportTaskDiff(filesystems | recoveryHandle, maxBytes): it runsgit diffagainst the last codeboost commit in a read-only container and returns at most 1 MiB. It must accept anAbortSignaland stop, and await, its own container and subprocesses when the signal aborts, because F gives each export a 60-second deadline (maxBytesbounds only the returned data, not the Docker work). F saves this as partial output for stopped attempts.Done when
agents/contract.tsand the storage, network and adapter APIs expose the items above, with contract tests.Refs: #49 (contract, sections "Shutdown" and "Launch"), #50 (D5), #22.