Durable approval gates
OMA can stop a checkpointed task run at an approval boundary, return a durable
request to the application, and continue from the same reviewed content after a
process restart. Existing synchronous decisions are unchanged: true and
{ action: 'allow' } continue, while false and { action: 'deny' } reject.
Return { action: 'suspend' } only when the decision must happen outside the
current callback lifetime.
Durable approval is an execution-state feature, not a telemetry feature. The
checkpoint owns the suspended continuation. A separate primary approval row in
the same MemoryStore owns the immutable request and first-wins decision.
Traces and execution receipts may derive approval facts, but losing telemetry
does not lose or change a decision.
Supported boundaries
Section titled “Supported boundaries”| Gate | Reviewed content | Resume behavior |
|---|---|---|
onPlanReady | The checkpoint-resumable coordinator task snapshots and whether the caller requested execution or planOnly | Runs exactly those snapshots, or returns them for planOnly |
onApproval | The completed tasks and next pending tasks at a legacy round barrier | Starts the reviewed next round without calling the gate again for that boundary |
onTaskDispatch | One fully assigned task snapshot immediately before dispatch | Dispatches that exact task without calling the gate again for that boundary |
onToolCall | Tool name, model-issued input, Zod-validated input, agent, task, tool-call ID, and consequential flag | Applies the durable decision instead of re-running the gate; approval executes the validated invocation, rejection returns a denied ToolResult without calling the tool |
Plan, round, and dispatch gates accept ApprovalGateDecision. Tool gates accept
ToolCallDecision; both include the same allow, deny, and suspend object
forms. The boolean forms remain supported on the three existing orchestration
gates for backward compatibility.
Suspend, decide, and restore
Section titled “Suspend, decide, and restore”Suspension requires checkpoint configuration and a checkpoint MemoryStore
that implements atomic compareAndSet. The store must remain available to the
reviewer and the resumed process.
import { decideApproval, FileStore, OpenMultiAgent,} from '@open-multi-agent/core'
const store = new FileStore('./.oma/release-run.json')const orchestrator = new OpenMultiAgent({ onTaskDispatch: async (task) => { if (task.priority === 'critical') { return { action: 'suspend', reason: 'Critical release review' } } return true },})
const suspended = await orchestrator.runTasks(team, tasks, { checkpoint: { store },})
if (suspended.status?.code === 'suspended') { for (const request of suspended.pendingApprovals ?? []) { // Present request.content and request.requestHash to the reviewer. await decideApproval(store, { requestId: request.id, requestHash: request.requestHash, decision: 'approve', // or 'reject' reviewer: { id: currentUser.id, displayName: currentUser.name }, }) }}
// A fresh process rebuilds the same team/backend wiring and uses the same store.const result = await new OpenMultiAgent({ onTaskDispatch: applicationDispatchPolicy,}).restore(resumedTeam, { checkpoint: { store } })A suspended result has success: false, status.code === 'suspended', and one
or more pendingApprovals. Do not call restore() to force progress while a
request is undecided: it returns suspended again and performs no reviewed
work.
The public reviewer helpers are:
getApprovalRecord(store, requestId)— read the primary request and any decision;decideApproval(store, input)— atomically approve or reject an exactrequestHash;DurableApprovalLedger— the lower-level equivalent for applications that want a long-lived ledger object.
decideApproval records the reviewer’s required id, optional displayName,
the normalized decision, and the decision-writer’s current time. Decisions are
immutable: the first successful compare-and-set wins; concurrent or repeated
decisions fail with APPROVAL_CONFLICT.
Exact-content binding
Section titled “Exact-content binding”Every request has a deterministic ID and a SHA-256 requestHash. The hash is
computed over canonical JSON containing the approval scope, boundary, and
review content. Object-key ordering cannot change it. The explanatory reason
and requestedAt are metadata and are not part of the reviewed-content hash.
The binding covers the serialized request.content, not live application
wiring. Agent adapters, prompts, tool implementations, schemas, and callbacks
must be rebuilt by the application and remain inside its deployment trust
boundary. Task-level suspension fails closed when a pending task has verify
configuration because that object can contain live judges, schemas, and prompt
callbacks that the current checkpoint schema cannot reconstruct.
The reviewer must submit both requestId and the hash it inspected. A changed
hash fails with APPROVAL_STALE_DECISION. During restore, OMA independently
compares the request with the checkpointed plan, task state, or pending tool
call. It also compares checkpointed decision history with the primary ledger.
A mismatch fails before the approved task or tool can execute.
For tool calls, restore validates the raw input again with the current Zod schema and compares the resulting validated value with the reviewed content. This prevents a changed input or changed validation result from inheriting an old approval. The reviewed content must be JSON-compatible: finite numbers, strings, booleans, nulls, arrays, and plain objects only.
This is integrity checking across OMA’s independent records, not a cryptographic signature against the storage administrator. A principal that can rewrite both the checkpoint and primary ledger remains inside the trust boundary. Protect and audit the store accordingly.
Rejection and recovery semantics
Section titled “Rejection and recovery semantics”- Rejecting a plan, round, or task dispatch produces a top-level
rejectedresult and skips remaining work. The terminal rejection survives repeated restores. - Rejecting a tool call produces the same kind of error
ToolResultas an inline gate denial. The tool is not invoked; the model may adapt on its next turn. - Approving a tool call executes it once, then the mid-task checkpoint records its returned result before the model continues. The ordinary external-side-effect idempotency window still applies if a process dies after an external system commits but before the tool returns.
- An ordinary checkpoint write remains best-effort. The write that makes a pending approval resumable is strict: OMA does not report suspension unless the exact continuation was saved and the primary request was created.
- A missing checkpoint, missing CAS capability, malformed record, or stale content fails closed. The protected task or tool is not executed.
approvalDecisions on the final result contains decisions consumed by the
logical run. buildExecutionReceipt() copies a bounded approval summary from
that result. The receipt is derived evidence; the primary row under
__oma_approval__/<requestId> remains authoritative.
Store requirements
Section titled “Store requirements”InMemoryStore provides process-local CAS and is useful for tests. FileStore
provides CAS for concurrent callers sharing one FileStore instance. It is
still a single-writer file store with no cross-process lock: an out-of-process
reviewer should decide after the suspended writer exits, or use a database
store whose compareAndSet is atomic across all writers.
Approval requests can contain complete task descriptions and raw/validated
tool arguments. They are persisted verbatim so the approved operation remains
exact. Use access controls and encryption appropriate for that data.
RedactingStore is intentionally unsupported for durable approvals because it
is lossy and would change the content hash; it remains available for ordinary
checkpoint and shared-memory redaction.
Explicit limits
Section titled “Explicit limits”- Tool-call suspension is supported only inside checkpointed built-in LLM
workers reached through
runTeam,runTasks, orrunFromPlan. StandalonerunAgent, the automaticrunTeamsimple-goal short circuit, process backends, and ACP backends do not expose resumable private tool-loop state; asuspendtool decision fails closed there. Orchestration-level plan, round, and dispatch gates still apply around eligible external-backend tasks. - An approval binds the serialized request data, not deployed code or live agent configuration. Changing an adapter, prompt, tool implementation, or schema is an application deployment boundary. Pin a deployment while a run is suspended, or include an application-owned version in reviewed task/tool data when that distinction matters.
- A task with
verifymay still use existing inline boolean approval, but its plan/round/dispatch gate cannot returnsuspend. Model verification as an explicit task when it must be part of a durable approval boundary. - Adaptive recovery’s
onPlanPatchcallback is not suspendable in this contract. It remains an inline boolean decision. - There is no built-in expiry, reassignment, revocation, quorum, or replacement of a recorded decision. Applications can decline to resume and start a new logical run when policy requires a new request.
- Checkpoints remain latest-snapshot state. Append-only event sourcing and transition replay are separate work tracked in #313.
See the no-key runnable
durable-approval
example for the complete suspend/decide/fresh-orchestrator flow.