src/orchestrator/deferred-outputs.ts — the deferred-output registry
Purpose
Section titled “Purpose”Run-scoped home for tasks whose outputs were left in the remote store
(--download=none / toplevel), plus the lazy materialisation that
fetches them when a locally-placed task turns out to need them.
Public surface
Section titled “Public surface”export interface DeferredEntry { materialize: () => Promise<void> // fetches the producer's outputs into its project dir hash: string entry: { taskId: string; command: string; durationMs: number; stdout: string }}
export interface DeferredOutputsArgs { nodes: Map<string, TaskNode> cache: CacheLayer workspaceRoot: string nestedDirsByProject: Map<string, string[]> gitFilesCache?: GitFilesCache localWrite: boolean}
export class DeferredOutputs { constructor(args: DeferredOutputsArgs) register(taskId: string, entry: DeferredEntry): void pending(): string[] get size(): number materializeFor(node: TaskNode): Promise<void>}register(taskId, {materialize, hash, entry})— execute-task calls this instead of saving, when a deferred result comes back.materializeFor(node)— fetch every deferred producer innode’s TRANSITIVE dependency closure. Which upstream bytes a command reads is unknowable (that is whatdependsOndeclares), so the whole closure is taken; each producer materialises at most once per run and they run concurrently. The closure is walked pre-order on an explicit stack: it is as deep as the graph, and a recursion per edge threwRangeErrorat 50,000.pending()— task ids whose outputs are still remote, for the run summary (sizeis their count). An entry is cleared only on SUCCESS, so this covers both “nothing needed them” and “fetching them FAILED” — the second is exactly when a user needs telling their tree is not current.
Invariants
Section titled “Invariants”- A deferred task writes NOTHING locally — no artifact, no
entriesrow, nooutput_files. A row without an artifact is the corrupt-entry shaperestoreOutputsrefuses. - Materialisation CONVERGES: clean → closure writes →
markOutputsChanged→ ordinarycache.save, mirroringrestoreHit’s sequence so the two cannot drift. Afterwards the machine is indistinguishable from a--download=allrun, so no third storage state persists. - A failed fetch is the CONSUMER’s failure, named with the producer and the remedy — executing against a half-materialised tree is the stale-input class with extra steps.
- Only core calls
materialize(), and only before a locally-placed, cache-missing task. A cache HIT reads no inputs and triggers nothing; a remote-placed consumer grafts by reference.
tests/download-policy.test.ts — no-local-entry, lazy materialisation,
memoisation (two consumers, one fetch), convergence to a local hit,
never-clean, fail-loud, the --continue interactions, and a producer
50,000 tasks below its consumer.