src/orchestrator/placement.ts — where each task runs
Purpose
Section titled “Purpose”The placement of a task graph over the resolved executors, and the
plan-mode view of it. Split from run.ts on 2026-09-10 (pure motion):
run() decides what to run and in what order; this module decides
where each task lands, and --dry asks it the same question without
running anything.
Public surface
Section titled “Public surface”export function pinnedLocalSet(nodes: Map<string, TaskNode>): Set<string>
export interface Placements { executors: Map<string, TaskExecutor> // every placed task → the executor that took it remoteOnlyNoop: Set<string> // `remote: 'only'` tasks no remote executor took remoteOnly: Set<string> // `remote: 'only'` tasks a remote executor took}export function placeTasks( nodes: Map<string, TaskNode>, executors: readonly TaskExecutor[],): Placements// `ids` whose executor is not `remote` — they write in placeexport function locallyPlaced(placements: Placements, ids: Iterable<string>): Set<string>
export async function planExecutorOf( prepared: PreparedRun, log: Logger, policy: 'all' | 'toplevel' | 'none', // the run's --download policy): Promise<{ executorOf?: (id: string) => string | undefined downloadOf?: (id: string) => 'eager' | 'deferred' | 'never' | undefined downloadDowngrades?: ReadonlyArray<{ taskId: string; reason: string }>}>
export function hasPooledExecutor(executors: readonly TaskExecutor[]): booleanexport function poolOfPlacement( placements: Placements,): (id: string) => { name: string; capacity: number } | undefinedexport const UNPLACED_EXECUTOR: TaskExecutor- Pinned to this machine: a task that transitively depends on a
persistent one (a worker cannot reach a port on the submitter), a
sandboxed task and its dependants (the sandbox is this machine’s
machinery; a boundary “verified” where it is not enforced passes
vacuously),
exec.remote: false, or a task whose key folds a runtime probe (cache.inputs.runtime/workspaceRuntime: this machine’s answer, which a worker’s own runtime may not match; its dependants stay free, C-2). Pinned tasks never reach a remote executor;selectExecutoris told so and a remote executor declines. A persistent task itself, like a group, is not placed at all — it runs on this machine outside the executor list. The pin flows up the dependant edges from the tasks that pin themselves, one walk on an explicit stack (a recursion per edge threwRangeErrorfromvx run --dryon a 50,000-deep chain); a graph where nothing pins itself builds no edge index at all. - Everything else asks the executors in declaration order
(
selectExecutor); the local floor takes what nothing claimed. exec.remote: 'only'with a remote executor that accepts it runs there and its outputs stay remote (remoteOnly); with no remote executor it is a no-op on this machine — never executed, outputs never cleaned or restored (remoteOnlyNoop) — and the run says so.- A pooled executor (one with
capacity) hands the scheduler its pool throughpoolOfPlacement, so it can run more tasks at once than the local worker count. UNPLACED_EXECUTORis the sentinel behind a task that reached an executor without being placed: an internal error, never a fallback.planExecutorOfresolves the executors for a plan and places the graph; a failing hook does not fail the plan (a plan never fails over a label) but is said on the status line in the plugin’s name.
tests/placement.test.ts (the pin rules, a 50,000-deep chain pinned
at its bottom, placeTasks’ gates); tests/plugin-capabilities.test.ts (placement end to end: pins,
decline-and-fall-through, 'only' taken / no-op / said, pools, --dry
labels and the failure notice); tests/download-policy.test.ts (the
download modes placement feeds); tests/plan-predict.test.ts.