# T5.1 — `TaskId` at spawn | Field | Value | |---|---| | Phase | P5 — Grading | | Size | S — under 1 day | | Status | Not started | | Flags | — | | Spec | inlined below | | Blocks | T5.5 onward. Cannot be retrofitted | ## Goal Every run carries a `TaskId` from spawn. Required, no default, no derivation from `RunId`. ## Facts (inlined — no spec read needed) - `TaskId` is the **comparison-group key**: a content hash of the task input **before any workflow touches it**. - Hashing the prompt does not work — the workflow changes the prompt by construction, which is the entire point of a variant. - **`TaskId` cannot be backfilled.** A run recorded without one is permanently ungroupable and useless to the learning loop. Hence: required at spawn, no `Default`, no `From` (T0.1). - **Land this during P1**, even though nothing consumes it until P5. It is a small task deep in P5 and it is the one item on the critical path that a later phase cannot repair. - What defines `TaskId` for a given user is open — ticket id, input fixture, or a hash of the pre-workflow goal. The framework provides the type and a default hasher and lets it be overridden. Settle it before any run is recorded. ## Steps 1. Add `task: TaskId` as a **required positional field** of the spawn parameters. Not `Option`, not a builder method that can be omitted. 2. Provide a default hasher over the pre-workflow task input, and a hook to override it. Document that the choice is permanent for existing data. 3. Record `TaskId` on the run's spawn event so it lands in the log, not only in materialized state. 4. Expose it on `EpisodeView` for the grading path. 5. Add the `trybuild` compile-fail case: a spawn call omitting `TaskId`. ## Acceptance - **Spawning without a `TaskId` fails to compile.** ## Verify **Harness:** `trybuild`, plus a full run to prove the id reaches storage. **Integration test** — `tests/it_taskid_required.rs`: 1. `trybuild` compile-fail: a spawn call omitting `TaskId`. Assert the expected stderr **names the missing field**, not merely "does not compile". 2. Run a real run; read the **log** (not materialized state) and assert the spawn event carries the `TaskId`. 3. Assert `EpisodeView` exposes it, so the grading path can group on it. 4. Determinism: hash the same task input twice, in two processes; assert equal `TaskId`. 5. Independence: assert two runs of the **same** task input share a `TaskId`, and that two different inputs do not. Both directions. 6. Assert the `TaskId` is **not** derived from the prompt: change the workflow version (which changes the prompt) and assert the `TaskId` is unchanged for the same input. 7. `trybuild`: `TaskId::default()` and `TaskId::from(run_id)` both fail. **Command:** `cargo test -p kernel taskid && cargo test -p kernel --test compile_fail` **False pass:** - Step 2 read from materialized state. If the id lives only in state it cannot be re-derived after a cold fold, and it cannot be backfilled — read the log. - Step 6 omitted: hashing the prompt passes steps 1–5 completely and makes every variant its own group, which is the exact failure that renders the learning loop inert. - A builder with a runtime `expect("task_id required")`. It passes a test that checks for a panic and still allows runs to be recorded without one in any code path the test did not cover. ## Traps - A builder with `.task_id(...)` optional and a runtime check. Runs recorded before someone notices are unrecoverable. - Deriving it from the prompt or from `RunId`. Both compile; both make every run its own group of one. --- Background (not required to do this task): [rust-agentic-sys.md](../../../rust-agentic-sys.md) §3, §11.5, §18 · [rust-agentic-task.md](../../../rust-agentic-task.md)