Skip to content

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.

GateReviewed contentResume behavior
onPlanReadyThe checkpoint-resumable coordinator task snapshots and whether the caller requested execution or planOnlyRuns exactly those snapshots, or returns them for planOnly
onApprovalThe completed tasks and next pending tasks at a legacy round barrierStarts the reviewed next round without calling the gate again for that boundary
onTaskDispatchOne fully assigned task snapshot immediately before dispatchDispatches that exact task without calling the gate again for that boundary
onToolCallTool name, model-issued input, Zod-validated input, agent, task, tool-call ID, and consequential flagApplies 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.

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 exact requestHash;
  • 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.

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.

  • Rejecting a plan, round, or task dispatch produces a top-level rejected result and skips remaining work. The terminal rejection survives repeated restores.
  • Rejecting a tool call produces the same kind of error ToolResult as 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.

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.

  • Tool-call suspension is supported only inside checkpointed built-in LLM workers reached through runTeam, runTasks, or runFromPlan. Standalone runAgent, the automatic runTeam simple-goal short circuit, process backends, and ACP backends do not expose resumable private tool-loop state; a suspend tool 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 verify may still use existing inline boolean approval, but its plan/round/dispatch gate cannot return suspend. Model verification as an explicit task when it must be part of a durable approval boundary.
  • Adaptive recovery’s onPlanPatch callback 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.