Files
poimen/tasks/T1.1-ctx-and-runscope.md
T

103 lines
4.5 KiB
Markdown

# 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<dyn EventLog>, blobs: Arc<dyn BlobStore> }`. 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<Mutex<Vec<_>>>` 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)