Durable decision review

DecisionReview is an append-only, project-scoped workflow record between probabilistic evidence and an effect. Evidence and policy pins are immutable.

Durable decision review

Prompt-first procedure: Describe the outcome you want in your agent conversation. The agent should select and load the appropriate AIWG assets, explain material changes, request any needed approval, and report verification evidence. Exact commands and flags appear only in the CLI reference.

`DecisionReview` is an append-only, project-scoped workflow record between probabilistic evidence and an effect. Evidence and policy pins are immutable. Editing an action requires authorization against the proposed action and a fresh resume token before appending a new proposal version; this invalidates prior proposal approvals and the old token. Resume rechecks action authorization again.

`PinnedReviewAuthorization` provides a concrete policy adapter: it binds tenant/project, policy ID/version/digest, risk-tier quorum, role eligibility, conflict-of-interest and requester/editor/reviewer/executor separation to live authenticated principal and role lookups. A policy replacement, compromised reviewer or role revocation blocks execution; the old approval remains auditable. Its `LiveReviewAuthority` and `actionAllowed` callbacks must be backed by the host's independently authenticated current identity and action policy. The review service permits only explicit state-machine operations. Reviewer eligibility, current eligibility of every recorded approver (`eligibleApproval`), and action authorization are separate mandatory callbacks and are rechecked at resume time. Implementations must query live authority (roles, conflicts of interest, pinned policy) rather than trusting stored reviewer roles; a denied resume appends an authorization-denied event without erasing approval history. Approval records accountable human judgment; it does not establish model correctness, grant permission, or bypass current policy.

Approval is not verification

Human approval is an accountable decision transition, not evidence that the underlying model output is correct. Approval never replaces the downstream checks around an effect, and both of them remain mandatory for every approved continuation:

  • Authorization. The executor re-runs current action authorization at resume time (`authorizeAction`), and the target system still enforces its own permissions.
  • Postcondition and outcome verification. After an effect, the host must verify that the intended outcome actually happened in the target system (a postcondition check or an authoritative lookup) before treating the work as done. A completed effect receipt records that the executor returned. It does not prove the external outcome. Only an executor-side lookup that authoritatively answers whether the effect happened can supply a completion receipt after a crash. That lookup is the AIWG effect ledger with its per-kind verifiers (see Effect ledger below); the target system stays the authority. A verifier result of `absent` or `unknown` keeps a post-dispatch crash uncertain and fails closed.

Resume stores only a SHA-256 digest of its secret token and derives a stable effect ID from the review, continuation, and approved proposal. Executors must honor that effect ID idempotently. A completed effect receipt is returned for every authorized duplicate resume without invoking the executor again. A `resuming` lease prevents concurrent local execution. After its deadline, a restarted service does not replay an uncertain effect: the caller must supply a trusted `reconcile(effectId)` callback returning the already completed, identity-matching receipt. Until reconciliation returns such a receipt, resume fails closed and the effect remains uncertain. A stable ID alone cannot prove the remote executor did not already run. The durable store preserves every authenticated revision, rejects history mutation, and uses atomic publication for concurrent transitions. Only one actor can claim a pending review; the winning claimant's duplicate claim returns the existing revision without appending another event. Fault-injection tests cover crashes before/after review publication and after the purge marker is durable. Only an executor-attested `ReviewDefinitiveExecutionError` can transition to `execution-failed` (persisted as the fixed `executor-failed` class). A generic executor error or receipt-persistence error leaves the effect `resuming`/uncertain for reconciliation; no automatic replay follows the lease. Exception messages are never persisted. The append-only validator reconstructs proposal versions, decisions and state from events and rejects forged transitions or mismatched effect identities.

Effect ledger (#2721)

D13 uses the AIWG effect ledger (`src/effects`, contract `docs/contracts/effect-ledger.v1.md`) as its executor-side completion lookup. This answers the #2677 owner question: the ledger is owned by the AIWG maintainers, and the target systems are the authorities, reached through verifiers.

  • Identity is unchanged. The effect kind is `decision.review.continuation`, with target `review:<tenantId>/<projectId>/<reviewId>` and context exactly `{reviewId, continuationId, proposalVersion}`. The `d13.review/v1` derivation is `reviewDigest({reviewId, continuationId, proposalVersion})`, the same ID `resume` derives and the validator enforces, so reviews persisted before the ledger still validate and resume. `reviewEffectId(identity)` computes it.
  • Scope. The ledger scope is `{tenant: tenantId, project: projectId, subsystem: 'review'}`. A query for another tenant or project returns nothing.
  • Intent before execution. `journaledReviewExecutor` takes a `ReviewEffectJournal` port. With the ledger adapter it appends a signed `intent` (payload digest `reviewDigest(action)`, which equals the proposal's `actionDigest`) before calling the effect. After the effect it reconciles through the verifier, with the receipt digest as the expected digest, and the ledger records `completed` only on `present`. The receipt body is archived beside the ledger and is bound to the signed record through the verifier evidence digest. A receipt whose body was never archived is rebuilt from the signed records, and its `result` is a digest reference (`aiwg.review-effect-reference/v1`).
  • Stale leases never replay. When the executor finds an earlier intent with no outcome, it reconciles instead of executing. `present` returns the recovered result. `absent` stays uncertain unless the executor declares `idempotentTarget` (the target deduplicates on the effect ID). `unknown` never replays. `ledgerReviewReconciler(...)` is the production `reconcile(effectId)` for `resume`. It returns a signed `completed` receipt, or reconciles through the verifier and returns a receipt only on `present`. With `actionDigest` it also records the missing intent of a review whose crash came before the intent (including reviews persisted before the ledger), then reconciles it.
  • The verifier. The built-in `decision.review.continuation` verifier (`1.0.0`) replaces the #2718 placeholder. It reads the review store. A persisted receipt is `present` (`state-match`, or `digest-match` against an expected digest). A continuation never dispatched under this effect ID, a missing review, or a definitive `execution-failed` is `absent`. While the review holds the dispatched continuation without a receipt, the store cannot settle the effect, so the verifier asks the host's `execution` probe at the effect's own target. Without a probe the answer is `unknown` / `consistency-lag`.
  • Key independence. The ledger signs with its own Ed25519 key (`credentialStoreKeyProvider` in production). Pass the review-store and session-index keys as `independentOf`: writes are refused when the ledger key was derived from any of them. The deprecated HMAC shim refuses an identical key the same way.

Production wiring:

const journal = openReviewEffectLedger({
  projectDir, tenantId, projectId, writer: 'review-executor',
  keyProvider: credentialStoreKeyProvider({ account: `ledger/${tenantId}/${projectId}` }),
  store: reviewStore, execution: probeTargetForEffectId, independentOf: [reviewStoreKey, sessionIndexKey],
});
const execute = journaledReviewExecutor({ ledger: journal, scope, reviewId, continuationId, proposalVersion, now, executeEffect });
const reconcile = ledgerReviewReconciler({ journal, scope, reviewId, continuationId, proposalVersion, actionDigest });
await service.resume(scope, reviewId, token, execute, reconcile);

Migrating from `FileVerifiedReviewEffectLedger`

`FileVerifiedReviewEffectLedger` is deprecated. It still satisfies `ReviewEffectJournal` and keeps its post-execution journaling, but it cannot record an intent. To migrate, open the legacy journal with its existing key and call `journal.importLegacyReceipts(legacy)`. Each legacy receipt becomes an `intent` and a `completed` record. The `completed` record is verified by a one-shot `legacy-hmac` importer verifier (version `0.2.0`, reason `digest-match`, evidence `method: legacy-hmac`), which is present only when the HMAC journal holds the exact receipt. The receipt body is archived, so the ledger adapter returns the same receipts afterwards. Receipts already completed in the ledger, and receipts outside the ledger scope, are skipped. Migration is idempotent. The effect-ledger v1 record schema has no `verification.method` member, so the method is carried in the verifier evidence and its version.

Crash recovery audit (opt-in)

The recovery sequence is crash → refresh the authorized workspace session index → audit the exact prior session → inspect authenticated `.aiwg` effect evidence → reconcile or remain uncertain. `auditedReviewReconciler` supplies an offline/testable adapter for `resume`'s reconciliation callback. Its host-provided catalog must verify coverage of the specific previous session after hydration; a healthy SQLite index or global coverage ratio does not prove that the preceding session was imported. The catalog audit locates a matching effect attempt by workspace, session, review and effect ID. Only a scoped executor-side ledger that authenticates and verifies a completed receipt may attest completion. A transcript claim, a dispatched tool call, a missing event, or a generated candidate cannot establish the result or justify replay.

Use the the agent-owned sessions operation public API to discover and preview only histories authorized for the workspace, then import the exact reviewed manifest if appropriate. Shared provider roots require separate authorization; do not broaden discovery to fix incomplete coverage. Resolve the canonical artifact destination with the agent-owned artifacts operation before inspecting or persisting `.aiwg` payload. Catalog status `partial`, `stale` or `unavailable` for the relevant preceding session, absent/contradictory attempt evidence, or a missing verified ledger receipt leaves the effect unknown and prevents automatic execution. `WorkspaceReviewSessionAudit` reads the production `SessionRepository` after an explicitly supplied workspace-scoped refresh; it checks the exact committed session, its source disposition in a current batch manifest, and a unique attempt marker located by the host. Workspace coverage can be partial because an unrelated provider needs export; the exact source must be committed and the session non-provisional. The host may supply `authorizedReviewCatalogRefresh({ repository, manifest, authorize })` with a previously reviewed `SessionDiscoveryManifest`: it refuses cross-workspace/unauthorized refresh before importing and does not discover new provider roots. The host must authorize the exact workspace and previous-session lookup for each attempt and map only structured attempt metadata to the marker; transcript prose cannot prove completion. Only complete `tool-control` events may supply a unique attempt marker; user-authored or assistant-generated transcript claims are ignored. Coverage and exact session are checked again at lookup to avoid using a stale hydration result. A catalog with no exact session, an incomplete/rejected exact source, a stale manifest, or ambiguous markers fails closed. The executor-side ledger is the effect-ledger adapter described in Effect ledger. Pass it to `auditedReviewReconciler` as `ledger`, and optionally as `verifier`: when coverage and the attempt check pass but no receipt exists, the reconciler reconciles through the `decision.review.continuation` verifier and accepts only a `present` result whose effect ID matches (with `expected`, a receipt for another continuation or proposal version is refused with `Reconciliation receipt mismatch`). `absent` and `unknown` return `null`. `FileVerifiedReviewEffectLedger`, the earlier HMAC-only completion journal, is a deprecated compatibility shim. Neither adapter installs a production importer, authorizes provider roots, or supplies an executor implementation. The service still checks the receipt's continuation and proposal identity before persisting it. Use only synthetic evidence in offline tests; never replay prior session tool calls.

Review presentations and actions must contain approved projections and references, not credentials, unrestricted provider payloads, private reasoning, or vault locations. Default validation rejects restricted field names and recognizable locator/token canaries across presentation, action, rationale, and result without echoing them. This is a conservative boundary, not comprehensive DLP; hosts must classify and project other sensitive data before creating a review. Reads and exports are object-authorized; list results are project-scoped and omit unauthorized objects, and all three use non-enumerating null/empty results. Claim, decide, edit, cancel, escalate, legal-hold, delete, resume and purge raise the same `ReviewAccessError("Review not found")` for a missing, out-of-scope or unauthorized review, so a denied caller cannot probe existence. `FileDecisionReviewStore` keeps each tenant/project scope in its own directory, named by a digest keyed with the store integrity key; reads and lists enumerate and open only that directory, so listing one tenant never parses another tenant's review files. Review IDs are unique within a scope. Revision files written by the earlier flat layout are not read by this layout. Deletion appends a tombstone and is denied under legal hold. Ordinary reads/lists hide tombstones; explicitly authorized export/audit can inspect them during retention. A caller may pin `retentionUntilEpochMs` at creation (it cannot precede expiry). Absent that pin, physical purge is disabled. After the pin's deadline, `purge` requires delete authority and an unheld tombstone; the file store atomically publishes a signed, scope-bound marker before removing all historical plaintext revisions. The marker retains only scope/ID digests, final revision and keyed integrity digest, prevents ID reuse, and makes interrupted erasure idempotently resumable. After purge even privileged export cannot recover the payload; audit retains only the keyed tombstone proof. An operator can retry interrupted erasure against the signed marker under scoped `purge` authorization even after the original review payload is gone; unknown and foreign IDs remain non-enumerating. External backups and replicas require an independent governed retention process; local purge does not claim to erase them.

Sensitive views (access-audited, retention-bounded)

Default review payloads hold only approved projections and references. When an accountable reviewer or auditor needs the underlying sensitive material, `openSensitiveView(scope, reviewId, { purpose, ttlMs }, source)` provides it under these rules:

  • Disabled by default. Only roles listed in the pinned policy's `sensitiveViewRoles` may open a view. When the list is absent, no role can open one.
  • Audited before any read. The service appends a `sensitive-view-accessed` event (actor, purpose, granted expiry) to the review journal with a compare-and-swap. It then reads the host-owned `ReviewSensitiveViewSource`. If the audit append fails, the source is never read. The event does not change the review status.
  • Never persisted. The returned content is not written to the review, its events, the #1567 journal or telemetry. The source is keyed by the review's scope, source receipt and evidence pins, and the host keeps the material under its own classification.
  • Retention-bounded. A view requires a retention deadline. The granted `expiresAtEpochMs` is the earliest of the grant time plus `ttlMs`, the review's `retentionUntilEpochMs` and, when bound, the D10 review rule. At or after the deadline the view is unavailable. Callers must discard the content when the view expires.
  • Non-enumerating. A missing, out-of-scope, tombstoned, past-retention or unauthorized review fails with the same `ReviewAccessError("Review not found")`. Denied attempts are not appended to the review journal, so an unauthorized caller cannot write to it. Hosts audit denials at their authentication boundary.
  • Projection-safe purpose. The `purpose` text passes the same restricted-content check as other review fields.

D10 lifecycle binding

Pass `{ lifecycle: { policy } }` in the service options to bind reviews to the shared D10 `review` lifecycle surface (`src/decision/lifecycle.ts`). With the binding:

  • Retention. The rule's `retentionMs` is the retention ceiling. A pinned `retentionUntilEpochMs` beyond `createdAt + retentionMs` is refused. With no pin, the rule sets the deadline. `PinnedReviewAuthorization` still requires the pin to equal its risk-tier window, so keep the two aligned.
  • Visibility and export. Reviews past retention read, list and export as absent. A rule with `export: 'denied'` makes `export` return null.
  • Erasure. `lifecycleReferences(scope, sourceReceiptId)` and `eraseLifecycleReference(scope, reference)` supply the `links` and `erase` steps of a host `DecisionLifecycleStore`, so `eraseDecisionSubject` can cascade from a source receipt to its reviews through opaque references. Erasure needs delete authority. It appends a tombstone, and when the rule says `deletion: 'erase'` it also physically purges the revisions behind a signed marker. D10 holds (checked by `eraseDecisionSubject`) and the review's own legal hold both refuse erasure.
  • No silent approval. Deletion never converts an unavailable review into approval. A tombstoned review cannot resume, and a purged one is not found, so an erased approval executes nothing.

Without the binding, the review's own tombstone, legal-hold and pinned-retention purge rules described above apply unchanged.

Operator decision audit linkage (#1567)

Each review event carries `operatorDecisionEventId`, deterministically bound to the review ID and event sequence. `reviewOperatorDecisionInput` maps approval, denial, escalation and revoked-authorization outcomes to #1567's existing operator-decision schema with that same event ID and caller-supplied mission/flow/session correlation; it does not create a second audit identity. Only policy/proposal/evidence digests travel as context, never raw action bodies. `replayReviewOperatorAudit` validates review lineage and the operator hash chain before writing, reuses existing matching records, rejects conflicting IDs, and repairs a missing record after a crash. Pass `{ operatorAudit: { store, correlation, classification } }` as the fourth `DecisionReviewService` constructor argument to persist and replay these records before any effect; `syncOperatorAudit(scope, reviewId)` repairs a failed append after authorization. An operator audit failure after a durable review transition raises an error but does not roll back the review; resume will re-sync before dispatch. The JSONL operator journal currently requires a single cross-process writer supplied by the host; the service serializes within one instance but does not claim a distributed transaction between review state and #1567.

Existing callers that stop on a `RulesetResult` with `status: review` remain unchanged. `durableReviewInputFromRuleset` is an explicit, side-effect-free migration bridge: it returns null unless enabled, accepts only a review result with a proposed outcome, and derives immutable source, evidence, and policy pins. The caller must still authorize and create the durable review through the service.

Offline acceptance and load fixture

`runOfflineReviewMatrixFixture(directory)` is an installed-package, real file-store example of low-confidence claim, policy-conflict rejection, edit-and-approve, expiry, escalation, restart/resume and duplicate resume. It uses synthetic inputs and no network.

`runOfflineReviewAuthorizationFixture(directory)` is the D18/G6 authorization case. It uses a non-permissive `PinnedReviewAuthorization` backed by a fake live authority, and attempts eleven unauthorized operations against the real file store:

  • resume before approval, self-approval, an unauthenticated reviewer and a claim by the requester role;
  • resume by a reviewer, by a foreign-project executor, by an unknown principal and with a stale token;
  • resume after action authorization is revoked, after the pinned policy is replaced and after the approving reviewer's role is revoked.

The executor counts calls by phase, and the fixture fails if any unauthorized attempt reaches it. After a restart, one authorized resume produces exactly one effect and a duplicate returns its receipt.

The packaging CI lane runs all three fixtures from the packed tarball with every network primitive disabled (`test/integration/decision-engine-clean-install.test.ts`), and so does `npm run smoke:decision:patterns:offline`.

`test/conformance/decision-v1/review-process.test.ts` covers durability across real processes:

  • Four child processes race `claim`, and four race `resume`, on one file store. They start from a stdin barrier, and exactly one claimant and one effect result.
  • Children are SIGKILLed at store publication seams (before creation publishes, and after creation, claim and decision publish) and inside the executor (after dispatch and before any effect, and after the effect is journaled but before the review receipt). The restarted service keeps every durable transition. It does not redispatch an unknown effect, and it reconciles a journaled effect without calling the executor again.

`test/conformance/decision-v1/review-effect-crash.test.ts` is the effect-ledger crash matrix (#2721). A child resumes through the ledger-backed executor and is SIGKILLed at four points: before the intent, after the intent and before the effect, after the effect and before `completed`, and after `completed` and before the review receipt. For each point the parent restarts with `ledgerReviewReconciler` and a verifier probe that is truthful, or forced to `absent` or `unknown`. A signed `completed`, or a truthful probe that finds the effect at the target, recovers the receipt with the effect run exactly once. Every other case throws `Effect outcome remains unknown`, leaves the review `resuming` and never replays.

After `npm run build:cli`, `node tools/decision/review-offline-benchmark.mjs the iterations option 5` reports measured per-fixture time, review count, override count and effect count. These local file-system measurements do not qualify reviewer staffing, live latency, authorization policy, production executor outcomes or external durability. Unknown remote effects require independent executor reconciliation; they cannot be inferred from an offline benchmark.

Production follow-up (#2677)

The `reconcile(effectId)` callback, `VerifiedReviewEffectLedger`, `LiveReviewAuthority` and `actionAllowed` are pluggable interfaces. The production executor-side completion lookup is the ledger-backed adapter (`openReviewEffectLedger`, `LedgerReviewEffectJournal` and `ledgerReviewReconciler`, #2721). #2677 still tracks the identity authority, the session importer, the distributed #1567 writer and the live staging crash matrix.