97 lines
3.8 KiB
Markdown
97 lines
3.8 KiB
Markdown
# 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)
|