src/orchestrator/task-hash.ts — cache-key derivation
Purpose
Section titled “Purpose”The single place that selects and assembles the parts of a task’s
cache key: resolved input files and workspaceFiles, env and runtime
values, the task-config digest, the project package.json digest,
the workspace fingerprint, the upstream hashes inputs.tasks keeps
(a group’s own upstream expanded through it), forwarded -- args on
a requested task, and the plugin parts the key stage attached
(node.keyParts). Split out of execute-task.ts because the plan,
the prepare step, the prefetch, the local short-circuit, the miss
save, admission and the stable-key probe all need the hashing surface
without the execution glue.
It lives in orchestrator (not cache) deliberately: key-part
selection composes graph types (TaskNode, TaskOutcome), and
pushing it into cache would force a cache → graph edge the
dependency matrix forbids. The byte-level folding itself stays in
Cache.key().
Public surface
Section titled “Public surface”// Per-run memos of derived values, shared across every task's computeTaskHash call.export interface HashCache { packageJson: Map<string, Promise<string>> // project package.json digest, by projectDir taskConfig: WeakMap<TaskConfig, string> // task-config digest, by config object identity runtime: Map<string, Promise<string>> // `inputs.runtime` output, by projectDir + '\0' + command workspaceRuntime: Map<string, Promise<string>> // `workspaceRuntime` output, by command workspaceFiles: WorkspaceFilesCache // `inputs.workspaceFiles` resolution, by declaration projectFiles: ProjectFilesCache // `inputs.files` resolution, by projectDir + declaration}export function createHashCache(): HashCache
// One key component at hash time: mirrors `Cache.key()`'s fold-site rows one for one.export interface TaskInputComponent { kind: string name: string hash: string}
export interface ComputeHashArgs { node: TaskNode upstream: TaskOutcome[] workspaceRoot: string workspaceFingerprint: string cache: CacheLayer forwardArgs?: readonly string[] | undefined nestedProjectDirs: string[] gitFilesCache?: GitFilesCache hashCache?: HashCache captureInto?: TaskInputComponent[] // filled at each fold site inside `cache.key()`; no effect on the hash}
export async function computeTaskHash(args: ComputeHashArgs): Promise<string>export async function describeTaskInputs( args: ComputeHashArgs,): Promise<{ hash: string; inputs: TaskInputs; facts: InputFact[]; describedAt: number }>
// A file the key folded, its digest, and since when that digest is known true (ms epoch).export interface InputFact { path: string digest: string since: number}export async function movedInput( facts: readonly InputFact[], cache: CacheLayer, commandFrom?: number, // the describe's start, just before the command): Promise<string | undefined>export function computeGroupHash(upstream: TaskOutcome[]): stringcomputeTaskHash— resolvescache.inputs.files(git-backed) andworkspaceFiles, readscache.inputs.envhost values, runs theruntimecommands once per run, hashes the resolved task config and the projectpackage.json, folds in the filtered upstream hashes, and callscache.key({...}). Timed as thetask hashspan.captureInto— on a miss the orchestrator persists the captured components toentry_inputsinside the save transaction, so a later run can diff its inputs against this one (vx why); a hit captures nothing, so the warm path is free.describeTaskInputs— the key AND the structured input set behind it, for the executor seam on the miss path: it re-runs the memoized resolution and keeps the values the key folded (env, runtime output, per-file digests), whichcaptureIntoreduces to digests because its rows are persisted. Itsfactsdate each digest: an index OID from the git enumeration’s start (GitFilesCache.enumeratedAtMs), a hashed file from the describe’s own start (describedAt), thepackage.jsondigest (a per-run memo) from the enumeration.movedInput— the post-command re-check (item 743): onelstatper fact; a file whose ctime is not older than its fact byFILE_HASH_RACY_MS(plus a second for a whole-second stamp, two for an even one,racyWindowMs) is hashed again and compared, and a missing file has moved. So has one whose ctime is at or aftercommandFrom(less that widening for a whole-second stamp), whatever it holds now: an input changed and changed BACK while the command ran matches its digest again (item 1015). Returns the first moved path; execute-task then withholds the save.computeGroupHash— for group tasks (noexec): rolls up upstream hashes only, so downstream keys still cascade through the group.
Invariants
Section titled “Invariants”- Any change to what participates in the key requires a
CACHE_VERSIONbump (see../caching.md). - Hash algorithm is xxHash3 via
util/hash.ts(16-hex keys).
tests/task-hash.test.ts and tests/task-hash-derive.test.ts (what
the key folds and how it cascades), tests/orchestrator.test.ts
(cache-hit / invalidation end to end) and tests/plan-predict.test.ts
(predicted keys match executed keys).