# T3.6 — Version pinning at spawn | Field | Value | |---|---| | Phase | P3 — Workflow as data | | Size | S — under 1 day | | Status | Not started | | Flags | — | | Spec | inlined below | | Blocks | — | ## Goal `VersionSelector::Latest` resolves exactly once, at run spawn, and the resolved hash is recorded. ## Facts (inlined — no spec read needed) - `SubWorkflow` pins by `VersionSelector`: `Exact(hash)` or `Latest`. - **`Latest` resolves once, at run spawn**, and the resolved hash is recorded. A run whose sub-workflow version can change mid-execution is a run whose results attribute to nothing. - The same rule applies to the top-level workflow: the run records the exact `WorkflowVersion` it executed, and a version published mid-run does not affect it. - Related failure the type system prevents: a placeholder that type-checks is invisible. A hardcoded `"current"` version hash compiled, passed tests, and made every result unattributable — which is why `WorkflowVersion` has no `Default` (T0.1). - Versions are never edited in place; a new version is a new hash with a parent pointer. ## Steps 1. At spawn, resolve the top-level workflow and every reachable `SubWorkflow` selector to concrete hashes. 2. Record the resolved set on the run's spawn event — the top-level version plus a map of `(WorkflowId → WorkflowVersion)` for sub-workflows. 3. Make the interpreter (T3.5) read only from that recorded map. It must have no path back to the registry's current `Latest`. 4. Resolve `Latest` against the registry once; if a new version is published afterwards, nothing re-reads it. 5. Test: start a run, publish a new version of the same workflow mid-run, assert the running run's recorded version is unchanged and it finishes on the pinned one. ## Acceptance - Publishing a new version mid-run does not change the running run's recorded version. ## Phase gate P3 closes when a user-authored YAML workflow runs with no framework recompile. ## Verify **Harness:** a workflow registry that can publish a new version while a run is in flight, plus a run long enough to straddle the publish (stub latency on a mid-run step). **Integration test** — `tests/it_version_pinning.rs`: 1. Register workflow `W` v1, containing a `SubWorkflow` pinned `Latest`. 2. Spawn a run; park it mid-execution **before** the sub-workflow step. 3. Publish `W` v2 and a new version of the sub-workflow. 4. Release the run. 5. Assert the run's recorded top-level `WorkflowVersion` is v1. 6. Assert the sub-workflow executed is the version resolved **at spawn**, not the newly published one — check the recorded resolution map, then check which version actually ran (they must agree). 7. Assert the spawn event carries the full `(WorkflowId → WorkflowVersion)` map, not a selector. 8. Registry-access audit: assert the interpreter makes **zero** registry lookups after spawn — instrument the registry with a call counter. **Command:** `cargo test -p executor version_pinning` **False pass:** - Parking the run **after** the sub-workflow step, so nothing could have re-resolved. The park point must precede the step. - Asserting only step 5. The top-level version is usually recorded correctly; the lazy-resolution bug lives in the sub-workflow path. Steps 6 and 8 are the test. - Step 6 checking the recorded map only. A map that is recorded and then ignored passes — assert what actually ran. ## Traps - Resolving `Latest` lazily at the point the sub-workflow is entered. It is the natural implementation and it makes long runs non-attributable. - Storing the selector rather than the resolved hash on the run record. --- Background (not required to do this task): [rust-agentic-sys.md](../../../rust-agentic-sys.md) §4.4, §12.2, §19 · [rust-agentic-task.md](../../../rust-agentic-task.md)