Expand description
Durable promise adapter for subagent spawn/await (spec-064 §P4, INV-9, FR-DE-05).
This module is a thin Layer-2 adapter. It wires the parent’s DurableContext promise
lifecycle to the subagent spawn/collect path. Domain meaning (“subagent result”) lives here;
the cryptographic and journaling mechanics live in zeph-durable (Layer 0).
§Scope boundary
This adapter covers the finished-child replay case only: when a parent resumes after a
crash and the child already resolved its promise, try_replay_durable_subagent returns the
journaled SubagentResult immediately so the call site can skip re-spawning the child (spec
§1038, acceptance test item 4).
The still-running-child-on-parent-crash case is intentionally out of scope. Only the BLAKE3
hash of the resolver token is persisted (INV-9); the raw 32-byte token is Zeroizing and
never stored. A crashed parent therefore cannot re-mint a valid token for an in-flight child’s
promise row. This is the direct consequence of INV-9’s hash-only persistence guarantee —
inventing a token-recovery path would violate INV-9. The general crash-recovery gap is
declared out of v1 scope in spec §862 and §1226.
§INV-9 channel rule
The DurableResolverSeat (holding the backend handle + token) is carried through a new
field on SpawnContext::durable_resolver. It is handed to the spawned background task only.
It MUST NOT be accessible from the child’s tool executor or LLM surface at any point.
§Gate pattern
The gate check lives at the call site in zeph-core (where DurableConfig and the parent
DurableContext are available). When durable.enabled && durable.subagent, the call site:
- Calls
make_durable_promiseto create the promise and optionally a resolver seat. - On a fresh run (
seatisSome) it places the seat incrate::manager::SpawnContext::durable_resolverbefore spawning, so the child resolves the promise on exit viaresolve_durable_promise. - On a resumed run (
seatisNone) it callstry_replay_durable_subagentagainst the re-derived promise. If the child already resolved, the call site replays the journaledSubagentResultand skipsspawnentirely — the fix this module exists for. If the promise is still pending, the call site falls back to a plain spawn (documented “Scope boundary” gap above: a genuinely in-flight child cannot be re-attached to after a crash).
When durable.enabled && durable.subagent is false, SpawnContext::durable_resolver stays
None and the plain spawn/collect path runs byte-identically to today (opt-in, zero
overhead when disabled).
Structs§
- Durable
Resolver Seat - The out-of-band resolver seat carried from parent to child background task (INV-9).
- Subagent
Result - The payload stored in the durable promise for a subagent’s terminal result.
Functions§
- await_
durable_ subagent - Await a durable promise for a subagent result, with an adapter-level tracing span.
- make_
durable_ promise - Create a durable promise in the parent’s execution and return the resolver seat for the child.
- resolve_
durable_ promise - Called from the child’s background task after the agent loop terminates.
- try_
replay_ durable_ subagent - Non-blocking check for a resumed subagent promise’s journaled result (spec §1038).