# T1.1 — `Ctx` and `RunScope` | Field | Value | |---|---| | Phase | P1 — Walking skeleton | | Size | M — 1 to 3 days | | Status | Not started | | Flags | — | | Spec | inlined below | | Blocks | T1.3, T7.x | ## Goal Explicit context threaded through every call, and a run-scoped task tree that cannot be escaped through the public API. ## Facts (inlined — no spec read needed) - Runtime is **tokio**. Chosen for ecosystem access (`sqlx`, `rdkafka`, `aws-sdk`, `tonic`, `axum`, `reqwest`, object-store clients) — for a distributed framework those are the distribution layer, not optional deps. - `RunScope` wraps `TaskTracker` + `CancellationToken` from `tokio-util`, one tracker per run. It exists because tokio has no structural task tree: without the guard, a detached spawn outlives its run. - `Ctx` is an **explicit struct** passed to every call. Never `task_local!` for anything causal — that is the `AsyncLocalStorage` mistake in different clothes. Trace context propagates through `Ctx` too. - Cancellation is **cooperative**: a `select!`-dropped future stops at its next await and not before. Kernel code must never hold a lock or a half-applied state across an await that can be cancelled. - A dropped call resolves `Cancelled` if no intent reached `Dispatched`, and `Indeterminate` if one did. **The intent record decides, not the drop.** Collapsing the two is the easy implementation and it puts a permanent noisy floor under the `Indeterminate` alert, which is meant to sit near zero. ## Steps 1. Define `Ctx { tenant, run, branch, cancel: CancellationToken, deadline, trace, log: Arc, blobs: Arc }`. Pass by `&Ctx`. 2. Define `RunScope` owning a `TaskTracker` and a `CancellationToken`. Expose `spawn` only as a method on `RunScope`; keep the inner tracker private so no caller reaches `tokio::spawn` through the public API. 3. `RunScope::shutdown()` / `Drop`: cancel the token, `tracker.close()`, then await `tracker.wait()`. Returning before every child is complete or aborted is the bug this type exists to prevent. 4. In kernel code, `select!` the cancellation token at every await. Audit for locks or partially-applied state held across those awaits. 5. Give user tool calls a hard timeout in addition to the token — a non-cooperative call must still be bounded. 6. On cancellation, read the attempt's latest intent state to choose `Cancelled` vs `Indeterminate`. Do not decide at the drop site. ## Acceptance - Cancel a run mid-step; assert every spawned task has completed or aborted before `RunScope::drop` returns. - The cancelled attempt lands in `Cancelled`, **not** `Indeterminate` — asserted directly, since collapsing the two is the easy implementation and silently ruins the alert. ## Verify **Harness:** `tokio::time::pause` for deadlines; a scoped task that records its own completion into a shared `Arc>>` so "aborted" and "completed" are distinguishable. **Integration test** — `tests/it_runscope_cancel.rs`: 1. Start a run whose step spawns three children through `RunScope::spawn`, each parked on a long await. 2. Cancel mid-step. 3. Assert `RunScope::drop` (or `shutdown().await`) returns **only after** all three have recorded completion-or-abort. Assert the recorded set has size 3 — a scope that returns early leaves it at 0 or 1. 4. Assert the attempt landed in `Cancelled`, **not** `Indeterminate`, with no intent dispatched. 5. Second run: dispatch an intent, then cancel. Assert `Indeterminate`. 6. Grep test (or a lint): assert no `tokio::spawn` outside `RunScope`, and no `task_local!` anywhere in kernel crates. **Command:** `cargo test -p runtime runscope` **False pass:** - Step 3 asserting only that `drop` returned. It always returns. The recorded child set is the evidence. - Steps 4 and 5 written against the same fixture with `intent_dispatched` hardcoded — they must run through the real intent record, or the distinction is asserted against itself. - A child that finishes on its own before cancellation lands, making the test green regardless of the tracker. Park children on a token that only the cancel releases. ## Traps - A public `spawn` helper "for convenience" that bypasses the tracker. - Storing tenant or run id in a task-local. It works until a spawn boundary, then attributes work to the wrong run. --- Background (not required to do this task): [rust-agentic-sys.md](../../../rust-agentic-sys.md) §5.1, §6, §15 · [rust-agentic-task.md](../../../rust-agentic-task.md)