CapDAG carries the identity and location of a diagnostic structurally. Source code declares the meaning; transport and execution layers preserve it without parsing prose or inventing missing values.
Attribution class
AttributionClass identifies the domain responsible for a diagnostic. It is
used by terminal ERR frames, by every non-progress LOG frame, and by Cancel
frames (§Cancellation speaks the taxonomy).
| Class | Meaning | Terminal retry policy |
|---|---|---|
input |
Deterministic on supplied input or arguments. | Permanent. |
resource |
A compute resource was exhausted. | Retryable. |
environment |
Deployment, network, registry, model, or process environment failed. | Retryable. |
internal |
An engine or cartridge defect. | Retryable; the fault is in the engine or cartridge. |
user |
The operator decided it: a cancel, or a policy the operator set that refuses the work (NETWORK_POLICY, Policy). Not a failure. |
Permanent (never retried automatically). |
The retry policy column is acted on in one place: a step whose wiring carries retry=N
(Machine notation) is attempted again only when its
failure’s class is retryable here (Execution). Nothing
retries a step that asked for none, and nothing retries input or user whatever was asked.
on_error=absent tolerates a failure of any class but user.
Classification is declared at the typed error definition or diagnostic emit
site. Wrapping layers carry it verbatim. An ordinary handler error that reaches
the terminal emit boundary without a typed classification is explicitly
emitted as HANDLER_ERROR / internal; that is a source-boundary decision, not
a receiver fallback.
Wire contract
Protocol v6 uses one attribution shape for source diagnostics:
| Frame | Required metadata | Optional metadata |
|---|---|---|
| non-progress LOG | non-empty level, non-empty message, valid attribution_class |
non-empty arg_urn |
| ERR | non-empty code, non-empty message, valid attribution_class |
non-empty arg_urn |
| progress LOG | level = progress, progress |
none of the diagnostic attribution fields |
The wire key is attribution_class. Missing or
unknown class tokens, empty required text, an empty arg_urn, or diagnostic
metadata on a progress frame are protocol violations. Receivers fail the
malformed request or stream; they never infer internal or an argument.
Graph coordinate
The location of a diagnostic is assembled by the execution layer from stable machine notation and the source frame:
token_idis the stableStrandStep.token_idand addresses the graph step.body_index, when present, is only the zero-based invocation ordinal within a ForEach region. It never identifies a graph node.arg_urn, when present, identifies the one argument named by the emit source.
The complete coordinate is therefore token_id plus optional body_index plus
optional arg_urn. A diagnostic without a step token is machine-scoped. A body
or argument cannot exist without a step token, and an argument is never inferred
from message text.
ForEach region failures use the region step’s stable token. Internal executor
names such as foreach_region_1 are not user-facing addresses.
Terminal failure path
sequenceDiagram
participant H as Handler
participant R as Cartridge Runtime
participant W as ERR Frame
participant E as Executor
participant A as Application
H->>R: typed or ordinary error
R->>W: code + attribution_class + message + optional arg_urn
W->>E: strict protocol v6 frame
E->>E: attach token_id and optional body_index
E->>A: attributed ExecutionError
A->>A: persist terminal run failure
Terminal failures remain durable. The engine persists their class, code,
reason, step token, optional body ordinal, and optional argument using the
existing run-failure model. The database column attribution_class stores the
same AttributionClass token carried by the protocol; it does not define a
second taxonomy.
Live diagnostic path
Non-progress LOG frames take the same route through the cartridge host, relay, executor, and engine, where the stable step coordinate is attached. They are then broadcast to connected clients as ordered live run diagnostics.
These ordinary diagnostics are transient:
- they are not written to the structured run database;
- they are not replayed after reconnect or application restart;
- clients retain them in bounded memory and may report dropped ranges;
- the normal logging sinks remain responsible for long-term operational logs.
Progress LOG frames are excluded because they already drive progress state and have a separate functional lifecycle.
Capacity and process loss
Admission is per POOL, keyed by full install identity plus pool name, and is
driven by the mandatory pool-state map declared in HELLO and refreshed on
every heartbeat reply. A dispatch is held against its cap’s whole pool CHAIN
(the cap’s own singleton pool, every declared shared pool containing it,
then all), admitted only when every chain pool has room, in one atomic
decision. Zero capacity means unlimited; for a positive effective capacity
(min(configured, available)), work beyond it waits in one line per install,
and the request that has waited longest among those whose whole chain has room
goes next, in order of arrival across all of the install’s caps, the same
rule the cartridge runtime admits by (the proved model’s,
formal/CapDAG/Bifaci/Pools.lean). Several process records carrying the same full install
identity share those admission targets; host registration order selects the
authoritative map, matching host-side request dispatch. Distinct install
identities claiming the same selected cap are an ambiguity and fail hard.
A NOT-RUNNING record’s all pool is clamped to one (the cold-start canary):
the first body proves the spawn before the advertised capacities apply.
Caps whose chains share a bounded pool are materialization boundaries in DAG execution. The executor feeds and completes one such cap before admitting a dependent cap contending for the same pool; it never pre-acquires multiple bounded requests for a live pipeline. The admission permit denotes actively owned pool slots, so pre-acquisition could otherwise wait for a downstream slot while the upstream owner remains unfed. Chains sharing no bounded pool (different cartridges, or same-cartridge caps in disjoint bounded pools) continue to stream live.
If a cartridge process dies, only requests actively owned by that process fail.
Queued bodies are not assigned terminal failure from another body’s process
loss. An admission target that goes unavailable does NOT reject its waiters:
they stay queued for ADMISSION_UNAVAILABLE_GRACE (60s, measured from the start
of the outage, not from each waiter’s arrival), so once a replacement instance
advertises capacity the queued work is admitted to that live instance. The
window is a bound, not a retry: an outage that outlives it fails its waiters
hard, classified environment (RelaySwitchError::CartridgeUnavailable →
ExecutionError::CartridgeUnavailable), so a target that is genuinely gone
surfaces promptly instead of stalling the run. How much longer a waiter may
wait is the model’s patience: the time is the outage’s, so a request that
joins the line late into an outage gets what is left of it. Admission is mirrored in Rust,
Go, Python and ObjC; capdag-js has no Bifaci runtime. This preserves body isolation in
ForEach execution while keeping process death visible as an environment
failure on the affected body.
A host that RETIRES a cartridge (a roster sync says the install is no longer
wanted) drains it rather than killing it: it leaves the cap table at once, and
its process survives until the requests it is already serving terminate. Work
killed by a retirement is attributed CARTRIDGE_RETIRED / environment, never
CANCELLED, which claims a user cancelled something nobody cancelled.
Cancellation releases an admission waiter or active permit exactly once. It does not create diagnostic history.
Cancellation speaks the taxonomy
A Cancel is attributed exactly as a failure is (code, attribution_class,
message in the frame’s meta, 12.2 §Cancel), and the taxonomy has one
class for it: user, the operator’s own decision. A user-attributed
outcome is permanent (never retried automatically: the human said stop) and
is the one class under which “cancelled” is the truth. Who cancels decides
the attribution, and the terminal the cancelled request ends with carries it:
- collateral (
ABORTED_COLLATERAL): the executor tore the chain down because another step failed. The Cancel carries the originating failure’s class; the run’s failure record is that originating error, and the collateral terminals name the failed step in their message. A collateral is never the run’s failure and never blamed on the user. - host abort (
ABORTED): the host decided. The task layer attributes its own reason: stale-task reaping →environment,ABORTED_STALE; memory-pressure relief →resource,ABORTED_MEMORY_PRESSURE, on the Cancel frames and on the run record (Failed). The existing auto-retry gate on class applies unchanged. - operator cancel (
CANCELLED,user):Cancelledis the run’s status; no failure record. A run’s terminal attributed touserfrom its cartridges (the cancel racing the run’s own terminal) recordsCancelled, neverFailed. - unattributed: a Cancel with no meta still cancels; its terminal is
CANCELLED/internal(unclassified means “ours”), never a guess at who decided.
Surfaces render the attribution (“aborted — step X failed”, tinted by the
originating class), never the word “cancelled” for anything but a
user-attributed cancel. A failure that reaches the task layer after a
cancellation is recorded on the task as an annotation (Cancelled →
Cancelled on execution_failed), never rejected. Closing a live input is not
a cancel at all: it is a CloseStream frame (12.2 §CloseStream), and the
request ends normally.
Presentation
A host UI groups live info, warning, and error diagnostics at the most specific available coordinate. A durable failure and its live error diagnostics share one node or machine panel rather than producing duplicate error surfaces. Machine-scoped diagnostics remain visible at machine scope. Node badges expose counts; bounded, scrollable panels preserve chronological detail.
Invariants
- Source code declares class and optional argument; downstream layers do not guess.
token_idaddresses a step;body_indexaddresses only a ForEach invocation.- One body failure does not terminate unrelated queued bodies, and neither does a transient outage of the cartridge they are queued on.
- Errors are durable; ordinary logs are live-only; progress remains functional state.
- Unknown protocol values fail hard instead of activating compatibility behavior.
A host may map these protocol-level identities into a broader product failure taxonomy. That host mapping is outside the CapDAG contract.