docs(architecture): resolve S1 — abort policy on OperationContext, not wire
ADR-016 Decision 6 specifies that the abort policy (abort-dependents vs continue-running) is set on OperationContext and propagated through OperationEnv::invoke() — the composing handler decides the child's policy, not the wire caller. The call.requested payload does not carry an abort policy field. This resolves the TBD that was masquerading as a two-way door: two of the three options ADR-016 floated (wire payload, per-operation declaration) were inconsistent with the ADR's own assumptions. Also marks review #001 as resolved — all 5 critical, 4 warning, and 4 suggestion findings are now addressed.
This commit is contained in:
@@ -121,6 +121,8 @@ The `payload` of a `call.requested` event has this shape:
|
||||
- `input` — the operation input, matching the operation's `input_schema` (JSON Schema). Always a `serde_json::Value`.
|
||||
- `auth_token` — optional. If present, the `CallAdapter` resolves it via `IdentityProvider::resolve_from_token()` and the resulting `Identity` takes precedence over the connection-level identity for this request. See [Identity Resolution](#authcontext-and-identity-resolution) below.
|
||||
|
||||
The `call.requested` payload does **not** carry an abort policy field. The abort policy (`abort-dependents` vs `continue-running`, ADR-016) is set on `OperationContext` and propagated through `OperationEnv::invoke()` — the composing handler decides the child's policy, not the wire caller. See [Abort Cascade and Nested Calls](#abort-cascade-and-nested-calls) below.
|
||||
|
||||
**Leading-slash convention**: `operationId` on the wire always has a leading slash (`/fs/readFile`). `OperationSpec.name` in the registry and in `services/list` responses never has a leading slash (`fs/readFile`). `OperationSpec.path()` produces the wire form (`/fs/readFile`). This is a single rule applied consistently — do not mix the two forms.
|
||||
|
||||
### `call.error` Payload
|
||||
@@ -340,6 +342,8 @@ When `call.aborted` arrives for a parent request, the protocol cascades the abor
|
||||
|
||||
An opt-in **`continue-running`** policy is available for cases where long-running work should survive a parent's abort (e.g., a subscription that should keep streaming). Under `continue-running`, descendants that have already started continue to completion; descendants that haven't started yet are aborted; no new descendants start.
|
||||
|
||||
The abort policy is set on `OperationContext` and propagated through `OperationEnv::invoke()` — the composing handler decides the child's policy, not the wire caller. The `call.requested` payload does not carry an abort policy field (the wire caller doesn't know the composition tree). The root context gets the default (`abort-dependents`); a handler can opt a child into `continue-running` at `invoke()` time. See ADR-016 Decision 6.
|
||||
|
||||
Handlers clean up resources when their call is cancelled (in Rust, the future is dropped and `Drop` guards release resources — HTTP streams, file handles, locks). This is a handler-level concern; the protocol's job is to cascade the abort. See ADR-016.
|
||||
|
||||
## Constraints
|
||||
|
||||
@@ -84,9 +84,10 @@ Use cases for `continue-running`: a long-running subscription that should keep
|
||||
streaming after its parent's sibling failed; a background task that was spawned
|
||||
by a handler and should survive the handler's abort.
|
||||
|
||||
The caller or handler specifies the policy at call time. The specific mechanism
|
||||
(a field in the `call.requested` payload, a field on `OperationContext`, or a
|
||||
per-operation declaration) is a two-way door for implementation.
|
||||
The caller or handler specifies the policy at call time. The policy is set
|
||||
on the `OperationContext` and propagated to children via `OperationEnv::invoke()`
|
||||
— see Decision 6 below. The default is `abort-dependents`; `continue-running`
|
||||
is an opt-in for long-running work that should survive a parent's abort.
|
||||
|
||||
### 4. Cleanup hooks
|
||||
|
||||
@@ -116,6 +117,52 @@ as a separate Rust crate for consumers that need richer call-tree visualization
|
||||
or reactive status tracking. It is not required for the protocol-level cascade
|
||||
— a parent-indexed map suffices.
|
||||
|
||||
### 6. The abort policy is set on `OperationContext`, not on the wire payload
|
||||
|
||||
The abort policy (`abort-dependents` vs `continue-running`) is set on
|
||||
`OperationContext` and propagated to children via `OperationEnv::invoke()`.
|
||||
It is NOT a field in the `call.requested` wire payload, and it is NOT a
|
||||
per-operation declaration on `OperationSpec`.
|
||||
|
||||
**Why not the wire payload**: the wire caller doesn't know the composition
|
||||
tree. The caller of `/agent/chat` cannot meaningfully decide whether
|
||||
`/fs/readFile` (composed internally by the agent handler) should survive an
|
||||
abort — the handler that composes the child knows that, not the wire caller.
|
||||
Putting the policy on the wire payload would give the wire caller control
|
||||
over internal composition behavior it can't see.
|
||||
|
||||
**Why not per-operation declaration**: ADR-016 Assumption 5 says the policy
|
||||
is per-call, not per-operation. The same operation may need
|
||||
`abort-dependents` in one composition context and `continue-running` in
|
||||
another. A static property on `OperationSpec` can't express that.
|
||||
|
||||
**How it works on `OperationContext`**: the root context
|
||||
(`build_root_context` in the CallAdapter) gets the default policy
|
||||
(`abort-dependents`). When a handler composes a child via
|
||||
`env.invoke()`, it can specify the policy for that child:
|
||||
|
||||
```rust
|
||||
// Default: abort-dependents (child aborts if parent aborts)
|
||||
context.env.invoke("fs", "readFile", input, &context).await
|
||||
|
||||
// Opt-in: continue-running (child survives parent's abort)
|
||||
context.env.invoke_with_policy(
|
||||
"fs", "readFile", input, &context, AbortPolicy::ContinueRunning
|
||||
).await
|
||||
```
|
||||
|
||||
The child's `OperationContext` carries the policy. If the child itself
|
||||
composes grandchildren, the policy propagates unless the child explicitly
|
||||
overrides it. This is consistent with the composition authority and scoped
|
||||
env propagation in ADR-022 — the parent handler decides the child's
|
||||
runtime context, including abort policy.
|
||||
|
||||
The `OperationEnv` trait gains an optional policy parameter. The specific
|
||||
API shape (a separate `invoke_with_policy` method, a policy field on an
|
||||
`InvokeOptions` struct, or a builder pattern) is a two-way door for
|
||||
implementation — but the policy enters through `OperationEnv::invoke()`,
|
||||
not through the wire and not through `OperationSpec`.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
@@ -175,10 +222,10 @@ or reactive status tracking. It is not required for the protocol-level cascade
|
||||
release, lock release).
|
||||
|
||||
5. **`continue-running` is per-call, not per-operation.** The policy is
|
||||
specified at call time, not declared at registration. If the policy should
|
||||
be a static property of the operation (declared in `OperationSpec`), the
|
||||
model changes. The assumption is that the caller or handler decides at call
|
||||
time based on the specific context.
|
||||
specified at call time via `OperationEnv::invoke()`, not declared at
|
||||
registration on `OperationSpec` and not set by the wire caller. The
|
||||
composing handler decides the child's policy based on the specific
|
||||
context. See Decision 6.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in New Issue
Block a user