103 lines
4.5 KiB
Markdown
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)
|