# AIWS SDK Implementation Handbook

SDK 0.3.0 · proposed standard 0.4 · finite-v1 · documentation revised 2026-10-02.

This handbook is generated from the current repository source and expands every runnable TypeScript, Rust and Python guide example. Previously built SDK 0.3.0 binary archives retain their original finite-v1 scope; later source-only additions are labeled explicitly. Praxis M9 protocol interoperability is complete for the scoped source profile: the pinned MCP 2026-07-28 + Tasks, A2A 1.0 and AG-UI 1.0 paths are verified, and M9.10 adds accepted non-authoritative cross-language protocol-correlation records, operator/recovery guidance, the exact compatibility matrix and a scalable SVG architecture package. M8 remains the independent platform/release/production qualification track. Unproven external effects remain UNKNOWN and held until reconciliation.



---

# AI Workflow Standard

Build workflows whose authority, spending, external effects and outcomes remain inspectable. SDK 0.3.0 implements the finite-v1 profile of our AIWS-001 edition 0.4 proposal in native TypeScript, native Rust and native Python.

All three libraries include trigger admission, 13 node kinds, SQLite persistence, authorization hooks, recovery fencing and audit replay. Shared scenarios compare the exact state after every accepted and rejected step.

[Understand the standard](https://www.lril.ai/standard/) · [Start with the examples](https://www.lril.ai/quickstart/) · [Read the implementation profile](https://www.lril.ai/profile/) · [Download the standard](https://www.lril.ai/downloads/AIWS-001-0.4.md)

## Implementation handbook

Follow a complete coordinator tutorial, then use the task-focused chapters for authorization, exact accounting, external effects, scheduling, all 13 node kinds, verification and recovery. Every worked application example has synchronized TypeScript, Rust and Python tabs. The source contains 29 recipes implemented as 87 executable programs with assertions.

- [What the AI Workflow Standard requires](https://www.lril.ai/standard/) and [the research behind it](https://www.lril.ai/standard-research/)
- [Application tutorial](https://www.lril.ai/tutorial/) and [example cookbook](https://www.lril.ai/cookbook/)
- [Ten interactive architecture and workflow diagrams](https://www.lril.ai/diagrams/) of the SDKs, Praxis and delivery pipeline
- [Native API reference](https://www.lril.ai/api-reference/) and [every wire command and record](https://www.lril.ai/wire-reference/)
- [Handoff mechanism](https://www.lril.ai/handoffs/) and [accepted Praxis design decisions](https://www.lril.ai/engine-design/)
- [Agent integration](https://www.lril.ai/agents/), [plain-text index](https://www.lril.ai/llms.txt) and [complete plain-text guide](https://www.lril.ai/llms-full.txt)
- [Download the implementation handbook](https://www.lril.ai/downloads/AIWS-SDK-Implementation-Handbook.md)
- [Download the engine handoff design](https://www.lril.ai/downloads/AIWS-Engine-Handoff-Design.md)

Handoff delivery and the scoped local engine are available from source. M6 is complete only for the approved D-M6-01 supervised Windows 11 x64 / Node 24 / local SQLite developer profile; M7 scoped local acceptance is complete. [M8 release qualification](https://www.lril.ai/engine-m8-qualification/) is active, with its planning slice complete and qualification slices still open. The [capability matrix](https://www.lril.ai/capabilities/) identifies which behavior is available in SDK 0.3.0, which the application must supply, and which remains engine design work.

## Downloads

- [Complete source, all three SDKs and Astro docs — ZIP](https://www.lril.ai/downloads/aiws-sdk-source.zip)
- [TypeScript package — TGZ](https://www.lril.ai/downloads/aiws-sdk-0.3.0.tgz)
- [Rust library package — CRATE](https://www.lril.ai/downloads/aiws-sdk-0.3.0.crate)
- [Python package — WHEEL](https://www.lril.ai/downloads/aiws_sdk-0.3.0-py3-none-any.whl)
- [Python source distribution](https://www.lril.ai/downloads/aiws_sdk-0.3.0.tar.gz)

The standard is a proposal. Application conformance also depends on the identity, policy, credentials, adapters and evidence assessment supplied by its operator. These packages are available here; they have not been published to npm, crates.io or PyPI.

All three SDKs include correlated control events, metrics, alert evaluation and durable OTLP/HTTP JSON export. [Configure observability](https://www.lril.ai/observability/).


---

# The AI Workflow Standard

AIWS-001, *AI workflows: governed execution, authority, evidence and interoperability*, is the proposed standard that every part of this project implements, tests or documents. Edition 0.4, dated 8 September 2026, is the current working draft. The SDKs implement its finite-v1 profile; the engine source implements the accepted engine design on top of it. This chapter explains the standard itself. The [research behind it](https://www.lril.ai/standard-research/) has its own chapter.

The standard is an independent proposal. It has not been adopted, approved or endorsed by ISO, IEC, OMG, NIST, AAIF, CNCF or any other standards organization. Its identifier is a working project identifier, and an organization adopts it through its own document-control process. External certification and interoperability with third-party runtimes have not been demonstrated. [Download edition 0.4](https://www.lril.ai/downloads/AIWS-001-0.4.md) or the [previous edition 0.3](https://www.lril.ai/downloads/AIWS-001-0.3.md).

## The problem it addresses

An AI workflow coordinates humans, AI agents, services and deterministic steps toward a declared outcome, often over hours or days, often with effects on external systems. Existing orchestration engines, agent protocols and observability tools each solve part of that, but they leave four questions to convention:

1. **May this participant act right now,** and who enforces that outside the model's own instructions?
2. **What actually happened** when a tool call timed out or a process died after the external change was committed?
3. **Did execution finish,** as distinct from a process exiting?
4. **Was the result accepted** by someone accountable, against criteria fixed before the work began?

AIWS-001 answers those questions with a semantic contract that can be implemented on existing engines and protocols. It specifies what must be recorded, checked and enforced. It does not prescribe a model, prompt format, user interface, transport, orchestration product, memory database or private reasoning representation, and it does not set domain safety limits or determine legal obligations. Those come from the adopting organization's controlled policies.

## How the document is organized

| Part | Content | Status |
|---|---|---|
| Part A | Research-driven revision history and the decisions each finding produced | Informative |
| Part B, clauses 1 to 19 | The numbered requirements | Normative |
| Annex A | 58 conformance scenarios, AT-01 to AT-58 | Normative |
| Annex B | A worked environmental health and safety corrective-action example | Informative |
| Annex C | Adoption guidance, release gates and the minimum adoption record | Informative |
| Annexes D and E | Source register, edition transition notes and primary references | Informative |
| Annex F | Minimum allocation of every requirement to a responsible role | Normative |
| Annex G | The companion finite-v1 implementation profile and its boundaries | Informative |

Requirements use the conventional keywords: **SHALL** is mandatory, **SHALL NOT** is a prohibition, **SHOULD** is a recommendation that a documented justification may override, and **MAY** is a permitted option. One rule governs all the others. If two mandatory requirements cannot both be satisfied, the deployment stops the affected work and reports the conflict; it never silently chooses the weaker interpretation.

## The object model

Clause 4 defines the objects every other clause refers to. The identity chain in [the execution model](https://www.lril.ai/concepts/) is the SDK's rendering of this table.

| Object | Role in the standard |
|---|---|
| Mission | The durable authorized objective and the container for cumulative resource accounting. It is OPEN, SUSPENDED or CLOSED, and closure never implies verified success. |
| ContractRevision and Intent | An immutable, approved combination of intent, definition revision, policy references and authorized envelope. A material change creates a new revision and a linked new run, never an edit in place. |
| Run, ExecutionSegment | One execution within a mission. A segment is an engine continuation or recovery boundary; starting one never creates fresh authority, budget or logical operations. |
| Plan, Task | A versioned strategy and its accountable units of work. Fixed workflows also have a plan, which may be derived mechanically from the definition. |
| Step, StepAttempt | A logical operation, and one dispatch of it. Retries create new attempts under the same step; a changed intended effect is a new step. |
| Participant, Capability | Who is responsible, and a versioned operation contract. Discovery of a capability describes availability; it never confers permission. |
| AuthorityGrant, Approval | A permission record with issuer, subject, scope, expiry and revocation state, and an attributable decision for one specific action. Approval does not create broader authority. |
| WaitCondition | A durable, task-scoped wait with owner, type, correlation, deadline and explicit ALL or ANY join membership. |
| Artifact, Evidence, VerificationRecord, AcceptanceRecord | Produced content, information with provenance about a criterion, an attributable verdict on that criterion, and an accountable decision to accept, reject or defer. |
| Event | An append-only record of an execution or control occurrence. Corrections are additional events. |
| Side effect, Reconciliation, Compensation | An externally observable change, the act of establishing its true outcome, and a separately authorized operation that counteracts it. |

## The requirement clauses

Each requirement has a stable identifier. The prefix says which clause it belongs to, which is also how the [capability matrix](https://www.lril.ai/capabilities/) and the conformance mapping refer to them.

| Clause | Identifiers | What it requires |
|---|---|---|
| 2 and 3, Normative language and conformance scope | N-01, C-01 to C-03 | Claims state edition, role, features, limits, exclusions and evidence. Unsupported features are rejected before execution. Every obligation is assigned to a named component and operator. |
| 4, Objects | M-01 to M-04 | Unambiguous identities, pinned revisions for every material action, mission continuity across runs and recovery, and segments that preserve operations, waits, budgets and decisions. |
| 5, Accountability and admission | G-01 to G-03 | Named owner, approval authority, operator and verification authority. A risk assessment before activation. Admission validates definition, policy, eligibility, capabilities, budgets and persistence before READY. |
| 6, Intent, definitions and plans | I-01, I-02, P-01 to P-03 | The intent names outcome, scope, prohibitions, criteria, acceptance methods and evidence. Each run pins one contract revision. Plan revisions are retained, validated and activated atomically, with explicit dependency and loop bounds. |
| 7, Lifecycle | L-01 to L-06 | Execution, verification and acceptance are separate state dimensions. A normative transition table governs execution. Terminal states never reopen. Cancellation does not imply rollback. Late callbacks cannot regress state or manufacture success. |
| 8, Authority | AU-01 to AU-08 | Grants are enforced at a trusted dispatch boundary outside the participant's instructions, attenuated on delegation, revalidated at dispatch and resumption, compared under a versioned authority profile, and blocked when a mandatory policy check cannot answer. |
| 9, Approvals and limits | H-01 to H-04, B-01 to B-03 | Approvals bind an eligible approver to the exact action, material revision and policy, with expiry and use counts. Budgets are reserved before dispatch and reconciled after, mission-wide. Deadlines use wall-clock time including waits. |
| 10, Effects | E-01 to E-09 | Capabilities declare their effect class. Attempts move through PREPARED, DISPATCHED, SUCCEEDED, FAILED or UNKNOWN, and uncertainty stays UNKNOWN until reconciled. Intent is recorded durably before dispatch. Retries need current authority and proof or a target guarantee. Compensation is its own authorized operation. |
| 11, Durability and replay | D-01 to D-04 | Control state is persisted before it is acknowledged. Recovery revalidates before dispatching. Stale executors are fenced. Audit playback, recovery continuation and new execution are distinct modes. |
| 12, Context and data | X-01 to X-05 | Inputs carry provenance and trust classification. Context survives participant replacement. Data destinations, retention and memory writes are governed. Retention is minimized. |
| 13, Evidence and outcomes | V-01 to V-08 | Evidence has provenance and validity limits. Every mandatory criterion gets PASS, FAIL or INCONCLUSIVE. A run is verified successful only when execution is COMPLETED, verification PASSED and acceptance is currently ACCEPTED for that verification revision. |
| 14, Records and observability | O-01 to O-09 | Required control events with a standard envelope, decision transparency, ledger integrity, telemetry correlation, defined metrics, alert governance, a privacy allowlist and durable export. |
| 15, Export, bindings and migration | EX-01, EX-02, BI-01, MG-01 to MG-03 | Evidence export manifests, declared semantic loss, versioned protocol bindings, and an experimental profile for moving runs between runtimes. |
| 16 and 17, Assessment and change control | T-01 to T-05, CH-01 to CH-03 | A requirement matrix, scenario execution, report contents, interoperability evidence, and separate tracks for control conformance, outcome quality, adversarial robustness and interoperability. Controlled adoption and impact assessment of material changes. |
| 18, Triggers | TR-01 to TR-07 | Occurrence receipt is distinct from authorization. Event data cannot select more privileged authority. Duplicates return their recorded disposition. Schedules declare time basis and catch-up policy. Resume responses correlate to one pending wait. |
| 19, Nodes | ND-01 to ND-06 | Node contracts, graph validation before activation, recorded branch selection, explicit joins, executors that receive only declared context and permissions, and completion that validates output and evidence. |

## Six ideas that recur throughout

**Three state dimensions, not one.** A run's execution state, its verification state and its acceptance state are recorded and reported separately. A process that exits cleanly is COMPLETED. It is verified successful only after every mandatory criterion has a current PASS and an accountable person has accepted that verification revision. Annex B shows a run that is COMPLETED, INCONCLUSIVE and DEFERRED at the same time, and why that is the honest answer.

**Authority is enforced outside the model.** Permission comes from grants and policy checked at a trusted dispatch boundary, never from a capability listing, a remote promise or the participant's own instructions. Delegated authority can only narrow. When the policy engine cannot answer, dispatch stops.

**Approvals bind to the exact action.** An approval names the approver's verified role, the run, task and operation, the intended effect and target, the material input revision and the policy revision. Change any of those and the approval no longer applies.

**Uncertainty is a first-class state.** A timeout, a lost acknowledgement or a crashed worker leaves an attempt UNKNOWN. Nothing retries it automatically. Reconciliation against the external system, or a target-level idempotency guarantee, is required before the operation can proceed, and budgets account for the uncertain effect conservatively.

**Everything material is durable before it is acted on.** Intent, authority decision, approval consumption, reservation and ownership epoch are committed before an external effect is dispatched. Recovery replays that record; it never re-derives it.

**Claims are scoped and assessed.** A library, a definition, a deployment and a binding are different claims. Each requirement is assigned to a role in Annex F, exercised by scenarios in Annex A, and reported on separate tracks. Passing one run proves nothing about an implementation.

## Lifecycle states

Clause 7 fixes the execution transitions. Verification and acceptance have their own small state machines in clause 13.

| Dimension | States |
|---|---|
| Execution | CREATED, READY, RUNNING, WAITING, PAUSED, CANCELING, then terminal COMPLETED, FAILED, CANCELED or REJECTED |
| Verification | NOT_STARTED, PENDING, PASSED, FAILED, INCONCLUSIVE, INVALIDATED |
| Acceptance | NOT_REQUESTED, PENDING, ACCEPTED, REJECTED, DEFERRED, INVALIDATED |
| Attempt and effect | PREPARED, DISPATCHED, SUCCEEDED, FAILED, UNKNOWN; effects settle to CONFIRMED_APPLIED or CONFIRMED_NOT_APPLIED |

A run with any active work remains RUNNING even while another branch waits. With no active work and something runnable it is READY. With neither it is WAITING. An unsatisfiable dependency is a recorded failure, never an unexplained indefinite wait.

## Conformance and adoption

The standard distinguishes five claims: a **definition** claim about a versioned workflow and its policy package, a **core execution** claim about an identified deployment boundary including runtime, enforcement, persistence and adapters, an **evidence export** claim, a **binding** claim about a named external protocol, and an **experimental migration** claim. A definition-only claim is never presented as runtime conformance, and a library that validates records may claim tested support for those checks but not core execution conformance.

Assessment follows clauses 16 and 17. The assessor enumerates every applicable requirement with an evidence reference and a PASS, FAIL or NOT_APPLICABLE verdict, executes the Annex A scenarios including crash and negative cases, and reports control conformance separately from domain outcome quality, adversarial robustness and interoperability. Adoption starts with a controlled record naming the edition, owner, scope, effective date, profiles and review interval, and every material change to runtime, model configuration, policy or persistence triggers reassessment. Annex C suggests starting with one bounded document-review or corrective-action workflow, using a simulated external target that can commit an update while dropping its acknowledgement, because that exposes failures a successful demo hides.

## Edition history

| Edition | Change |
|---|---|
| 0.1 | Initial reviewed draft. |
| 0.2 | Research-driven revision. Introduced Mission, ContractRevision and ExecutionSegment; the versioned authority profile and three-result containment; blocking on policy errors; measurable revocation freshness; the three replay modes; independent wait records with ALL and ANY joins; approval ownership, quorum and action fingerprints; the durable admission boundary with conservative uncertainty accounting; the AcceptanceRecord; and role-assigned assessment. |
| 0.3 | Trigger admission and event handling, TR-01 to TR-07, and the normative node catalog, ND-01 to ND-06, with scenarios AT-42 to AT-52. |
| 0.4 | Correlated observability, metric definitions, alert governance, telemetry privacy and reliable export, O-05 to O-09, with scenarios AT-53 to AT-58. |

Records from one edition are not automatically records of the next. Annex E describes the explicit mapping an import requires and forbids relabeling an old contract or recomputing its journal as a substitute for migration.

## How the SDKs and engine relate to it

SDK 0.3.0 and standard edition 0.4 are different version numbers with different purposes. The three SDKs implement the **finite-v1 profile** described in Annex G: exact finite authority sets, integer accounting units, immutable per-mission contracts, explicit joins, bounded loops, local JSON schemas and conservative UNKNOWN accounting. The [implementation profile](https://www.lril.ai/profile/) states exactly what is implemented, the [capability matrix](https://www.lril.ai/capabilities/) maps requirements to the SDK, the application or engine work, and the [conformance chapter](https://www.lril.ai/conformance/) describes the shared scenarios the three languages run.

The profile deliberately leaves things out. The libraries are not a hosted ingress service, do not authenticate event producers, do not provide calendar schedules, arbitrary policy comparison, cross-runtime migration or external exactly-once effects, and have no permissive authorization default. Unsupported requirements are rejected before execution. An organization claiming conformance assesses its complete composed deployment, including the identity, policy, adapters and evidence assessment it supplies around the SDK.

**Praxis**, implemented under `engine/src`, realizes the accepted [Praxis design](https://www.lril.ai/engine-design/): a governed coding workflow, control API, worker dispatch, backup and restore, and the dynamic workflow plane. The [architecture diagrams](https://www.lril.ai/diagrams/) show how those pieces embody the standard's boundaries.


---

# Research behind the standard

[The AI Workflow Standard](https://www.lril.ai/standard/) did not start from a blank page. Two research reports established what already existed, where it fell short, and which of the draft's own assumptions did not survive contact with primary sources. Both are preserved in the repository under `docs/research/` and are downloadable from this site. They are historical context: later accepted decisions supersede their recommendations, and the current requirements live in the standard, the [implementation profile](https://www.lril.ai/profile/) and the [capability matrix](https://www.lril.ai/capabilities/).

- [Workflow Standards for AI and Agentic Systems](https://www.lril.ai/downloads/deep-research-report.md), the landscape report, dated 4 September 2026.
- [AI Workflow Standards, supplemental research](https://www.lril.ai/downloads/AI-Workflow-Standards-Supplemental-Research.md), dated 7 September 2026, which tested edition 0.1 against primary sources before edition 0.2 was drafted.
- [The TypeScript and Rust SDK design](https://www.lril.ai/downloads/AIWS-TypeScript-Rust-SDK-Design.md), written before the Python SDK was added.

## The question the landscape report asked

Do vendor-neutral workflow standards for AI and agentic systems already exist, and if not, what exactly is missing? Its answer was that workflow standards certainly exist, but there is no single, broadly accepted, vendor-neutral standard for an end-to-end AI workflow. The report surveyed process notations, executable workflow languages, agent definition formats, interoperability protocols and governance bodies, mapped them against the needs of agentic work, and proposed a reference model and roadmap.

Its working definition became the basis of clause 1 of the standard: an AI workflow is a governed, stateful execution in which human, agentic and deterministic participants cooperate toward a declared intent, using delegated capabilities within explicit authority and policy boundaries, while producing durable artifacts, execution records and evidence sufficient to verify the outcome. Its shortest statement of purpose is that the standard defines what remains invariant while an autonomous execution is allowed to vary.

## What already existed

| Family | What it contributes | What it lacks for agentic work |
|---|---|---|
| BPMN 2.0, DMN 1.5, CMMN 1.1 | Mature vocabulary for tasks, events, gateways and lanes; deterministic, inspectable decisions; adaptive case management with discretionary work and milestones | Topology fixed in advance; no notion of model-mediated decisions, plan mutation or cross-agent delegation |
| WfMC, XPDL, Wf-XML | The historical reference model and interchange format | Teaches that notation, executable semantics and interchange are three different standardization problems |
| Open Workflow Specification 1.0.3 | The strongest candidate for a deterministic execution substrate: switch, fork, wait, retry, subworkflow, with a conformance test kit | Defines no autonomy, intent or authority semantics |
| OpenAPI Arazzo 1.1.0, AsyncAPI | Standard descriptions of API-operation sequences | Not an agent or workflow language |
| Agent Spec 26.3.0 | The closest thing to portable agent and flow interchange | Vendor-originated governance; adapter equivalence unproven |
| MCP 2026-07-28 | The capability, tool and context boundary | Should be a binding, not something to replace |
| A2A 1.0 | The delegated-task boundary: agent cards, tasks, messages, artifacts and a task state model | Does not define parent intent, topology or success criteria |
| AG-UI draft | Human and application interaction events, including human-in-the-loop | Draft, not ratified, and not the workflow definition |
| OpenTelemetry GenAI conventions | Observability vocabulary for invoking workflows, agents and tools | Terminology still contested; align rather than fork |
| OASF, AGNTCY | Participant and capability descriptions for discovery and matching | No execution semantics |
| AAIF Workflows and Process Integration working group | The most directly relevant venue; its charter names handoffs, persistence, retries, idempotency, recovery, human approval and portability | Chartered in May 2026; no published specification at the time of the survey |
| NIST AI Agent Standards Initiative, W3C community groups, ISO/IEC SC 42, IEEE P3777 | Agent identity and authority as formal policy; the governance, lifecycle and evaluation envelope | Policy and incubation, not executable specifications; workflow standards should reference them rather than duplicate them |

## The gaps that motivated the standard

The landscape report ranked the unresolved problems. The high-priority ones map directly onto clauses of AIWS-001.

1. **No common metamodel.** Workflow, run, task, step, agent, session, handoff, plan and artifact meant different things in every framework. Clause 4 fixes the vocabulary.
2. **Intent had no success contract.** Existing standards encode actions and conditions far more strongly than the desired outcome and acceptable evidence. Intent is not the same thing as an initial prompt. Clause 6 requires criteria, acceptance methods and evidence to be declared up front.
3. **Authority is not authentication.** Knowing who an actor is says nothing about what an autonomous participant may decide, delegate or cause. Clause 8 defines grants, attenuation and enforcement at a trusted boundary.
4. **Plans mutate.** An agentic execution constructs part of its own future topology, and nothing governed when an agent could create tasks or revise a plan. Clause 6 governs plan revisions inside a declared envelope.
5. **Durable state was not portable.** Checkpoint, suspend, resume and recovery were entirely per-runtime. Clause 11 defines what must be durable and how recovery behaves; migration stays experimental.
6. **Side effects, retries and compensation.** Retrying "reason about this document" is different from retrying "pay invoice." Clause 10 requires effect classification, attempt records, retry permission and separate compensation.
7. **Completion is not verification.** COMPLETED cannot safely mean "the agent said it is finished," and an artifact is not evidence. Clauses 7 and 13 separate execution, verification and acceptance.
8. **Conformance semantics.** A portable YAML file is worthless if two runtimes interpret delegation, failure, retry and completion differently. Clause 16 and Annex A define assessment.

Medium-priority gaps, approval ownership and expiry, memory versus state versus conversation, capability matching, resource envelopes across tokens, money, time, tool calls and delegation depth, and evaluation semantics, became clauses 9, 12 and 13 and parts of 14.

## What the supplemental research corrected

The second report asked a harder question of the edition 0.1 draft: which requirements are supported by existing standards or implementation evidence, which need correction, and which remain design choices? It read primary sources, from IETF RFCs and the Cedar authorization algorithm to Temporal, LangGraph, Step Functions, Stripe, WS-HumanTask, PROV-O, SACM, RATS and SCXML, and found several places where the draft had assumed more than the sources provide. Each finding produced a decision recorded in Part A of the standard.

| Finding | Source | Decision in edition 0.2 |
|---|---|---|
| A token-exchange actor chain is informational; it is not a permission chain, so carrying identities does not restrict delegation | RFC 8693 | Attenuation is computed and enforced, not inferred from identity |
| Rich authorization requests define no generic way to compare two permission objects; an intersection described in prose is insufficient | RFC 9396 | A versioned authority profile with a three-result containment check; an unsupported comparison is indeterminate, never permission |
| Cached introspection results go stale | RFC 7009, RFC 7662 | Declared, measurable revocation freshness, clock tolerance and in-flight boundaries |
| An authorization engine that skips a policy on error can return Allow alongside a policy error | Cedar | Mandatory policy checks block the affected dispatch when they cannot answer |
| Runtime continuation chains show that "run" was too tightly coupled to one engine invocation; a linked new run must not reset an exhausted budget | Temporal | Mission, ContractRevision and ExecutionSegment; cumulative accounting across runs |
| Resuming an interrupt reruns the node containing it; resume does not always mean continuing after the interrupted line | LangGraph | Recovery revalidates and replays recorded decisions rather than assuming a resume point |
| Engine delivery guarantees are scoped, and an idempotency key is a contract with namespace, retention, fingerprint and concurrency rules, not a field | Step Functions, Stripe, transactional outbox | Delivery claims are stated separately from business-effect claims; retry needs proof or a target guarantee |
| A durable pre-dispatch record does not make a remote write atomic | Outbox pattern | Outcome unknown is distinguished from effect not applied; UNKNOWN is a first-class state |
| A review that rejects is a successful review, not a failed execution | WS-HumanTask | Acceptance is its own record and state, separate from execution |
| A single wait reason loses information when branches wait in parallel | SCXML | Independent WaitCondition records with explicit ALL and ANY joins |
| Conformance tests, quality benchmarks and security evaluations answer different questions | Open Workflow test kit, agent benchmarks | Four separate assessment tracks: control conformance, outcome quality, adversarial robustness, interoperability |

The report also identified reuse candidates rather than inventions: PROV-O for provenance, with artifacts as entities, attempts as activities and participants as agents; SACM for assurance rationale and counter-evidence; the RATS separation of evidence producer, technical assessor and acceptance authority; WS-HumanTask for approval ownership, claim and escalation; and the OWASP transaction-authorization comparison of what was approved with what is about to be dispatched. None became a mandatory core dependency.

## What the research said not to do

- **Do not invent another notation, transport or telemetry protocol.** BPMN, MCP, A2A and OpenTelemetry exist; replacements would fragment rather than standardize. The standard defines bindings to them instead.
- **Do not standardize prompts, model internals, or the choice of model, framework, database, vendor or interface.**
- **Do not equate portability with identical execution.** Conformance means different valid trajectories preserve the same intent, policy invariants, authority boundaries, lifecycle semantics and verification obligations.
- **Do not start from a large serialization schema.** Establish semantic interoperability first and a wire format second. The SDK's closed JSON schema came after the semantics, as a profile.
- **Do not let the planning model's own judgment be the authority-containment mechanism.**
- **Keep experimental alignments experimental.** Mission-bound authorization and delegation receipts are individual Internet-Drafts with no IETF endorsement. Cross-runtime migration is deferred because the reviewed runtimes establish local recovery, not universal checkpoint interchange.
- **Make no prompt-injection-proof claim.** The research treated adaptive-attack results as disconfirming evidence that defenses can degrade under pressure, and assigned adversarial robustness its own assessment track.

Both reports state their limits. No runtime, policy engine, benchmark or adapter was installed or tested during the research; standards inspection was selective; some full texts were unavailable. The comparative matrices are analytical mappings, not official ratings. The supplemental report closes by noting that its findings do not turn the draft into a ratified or tested standard.

## How the research shaped the roadmap

The landscape report proposed a modular architecture so that protocol churn never forces a major version of the standard: a core of terminology, metamodel, lifecycle and conformance; a governance profile of intent, authority, autonomy, budgets and evidence; bindings for MCP, A2A, AG-UI, API workflows and asynchronous events; execution profiles for Open Workflow, Agent Spec and BPMN or CMMN mapping; and an observability profile aligned with OpenTelemetry. It recommended testing the standard against deliberately diverse use cases, such as incident escalation, vulnerability remediation, corrective action, invoice payment with irreversible effects, multi-agent research and long-running regulatory approval, because a standard tested only against a research agent that summarizes documents misses most of the hard semantics.

The supplemental report recommended a design-decision review before adding more requirements, then a small reference contract and simulator with four fixtures: a bounded document review with human acceptance, a delegated task whose authority is revoked while it waits, an external write that commits before its acknowledgement is lost, and a continuing segment that must preserve consumed budget and unresolved work. Those became edition 0.2, the shared scenario corpus in Annex A, and eventually the three native SDKs, whose [conformance scenarios](https://www.lril.ai/conformance/) still exercise exactly those cases.


---

# Install and run your first workflow

This guide targets SDK **0.3.0**, AIWS proposal **0.4**, profile **finite-v1**. Choose a language tab once; the site keeps matching examples synchronized. All examples are also present as plain source files and in the downloadable handbook for agents that do not execute browser JavaScript.

## Choose a starting point

For application developers, install one SDK and follow the [coordinator tutorial](https://www.lril.ai/tutorial/). For agent implementers, read [agent integration](https://www.lril.ai/agents/) and the [capability matrix](https://www.lril.ai/capabilities/) before generating code. For engine designers, use [handoffs](https://www.lril.ai/handoffs/) and [engine decisions](https://www.lril.ai/engine-design/), which explicitly identify features that are not SDK APIs.

The downloads are local distribution artifacts, not packages published to npm, crates.io or PyPI. Do not install an unrelated registry package with the same name. TypeScript requires Node 24 or newer. Python declares 3.11 or newer; the release was tested on 3.12. Rust examples use the current stable toolchain and a path dependency.

## Install from the source archive

Extract [the complete source](https://www.lril.ai/downloads/aiws-sdk-source.zip). Run the following from its root. These are shell setup commands; the application code below has a tab for each SDK.

```sh
npm ci
npm run build:sdk
python -m pip install ./packages/python
cargo build --locked
```

Only install the languages you intend to use. The documentation verification suite needs all three. A Python-only application does not require Node or Rust. A Rust-only application does not require Python or Node. The Node requirement belongs to the TypeScript SDK and the Astro documentation build.

To install individual releases, use `npm install ./aiws-sdk-0.3.0.tgz`, `python -m pip install ./aiws_sdk-0.3.0-py3-none-any.whl`, or extract the Rust crate and declare its directory as a Cargo path dependency. Package archives are linked from the [overview](https://www.lril.ai/).

## First successful control sequence

The example below is a **trusted simulation**, with a fixed clock and explicit recorded effect outcomes. It teaches the state machine without calling a provider or writing a repository. The eight stages start a run, register a grant, reserve an operation, dispatch, settle, complete execution, verify and accept. External applications must use the coordinator and real effect adapter described in the next tutorial.

**Execution, verification and acceptance** — Executable control simulation

### Typescript example: first-run

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
})), '10');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "transition",
  "runId": "r",
  "to": "COMPLETED"
})), '10');

// Step 7
state = reduce(state, parseCommand(JSON.stringify({
  "type": "verify",
  "runId": "r",
  "assessor": "reviewer",
  "revision": "v1",
  "results": {
    "review": "PASS"
  },
  "evidence": [
    "sha256:evidence"
  ],
  "rationale": "Review performed"
})), '10');

// Step 8
state = reduce(state, parseCommand(JSON.stringify({
  "type": "accept",
  "runId": "r",
  "authority": "owner",
  "verificationRevision": "v1",
  "decision": "ACCEPTED",
  "rationale": "Accepted deliverable"
})), '10');
assert.deepEqual(state["assessments"]["r"]["acceptance"], "ACCEPTED");
assert.deepEqual(state["spent"], "3");
assert.deepEqual(state["reserved"], "0");
console.log('PASS: first-run');

```

### Rust example: first-run

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
}))?, "10")?;

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "transition",
  "runId": "r",
  "to": "COMPLETED"
}))?, "10")?;

    // Step 7
    state = reduce(&state, &Command::from_value(json!({
  "type": "verify",
  "runId": "r",
  "assessor": "reviewer",
  "revision": "v1",
  "results": {
    "review": "PASS"
  },
  "evidence": [
    "sha256:evidence"
  ],
  "rationale": "Review performed"
}))?, "10")?;

    // Step 8
    state = reduce(&state, &Command::from_value(json!({
  "type": "accept",
  "runId": "r",
  "authority": "owner",
  "verificationRevision": "v1",
  "decision": "ACCEPTED",
  "rationale": "Accepted deliverable"
}))?, "10")?;
    assert_eq!(state.as_value()["assessments"]["r"]["acceptance"], json!("ACCEPTED"));
    assert_eq!(state.as_value()["spent"], json!("3"));
    assert_eq!(state.as_value()["reserved"], json!("0"));
    println!("PASS: first-run");
    Ok(())
}

```

### Python example: first-run

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 3
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 4
state = reduce(state, {'type': 'dispatch',
 'attemptId': 'a',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 5
state = reduce(state, {'type': 'settle',
 'attemptId': 'a',
 'effect': 'CONFIRMED_APPLIED',
 'actualCost': '3'}, '10')

# Step 6
state = reduce(state, {'type': 'transition', 'runId': 'r', 'to': 'COMPLETED'}, '10')

# Step 7
state = reduce(state, {'type': 'verify',
 'runId': 'r',
 'assessor': 'reviewer',
 'revision': 'v1',
 'results': {'review': 'PASS'},
 'evidence': ['sha256:evidence'],
 'rationale': 'Review performed'}, '10')

# Step 8
state = reduce(state, {'type': 'accept',
 'runId': 'r',
 'authority': 'owner',
 'verificationRevision': 'v1',
 'decision': 'ACCEPTED',
 'rationale': 'Accepted deliverable'}, '10')
assert state["assessments"]["r"]["acceptance"] == 'ACCEPTED'
assert state["spent"] == '3'
assert state["reserved"] == '0'
print('PASS: first-run')

```

The final assertions demonstrate a recorded accepted assessment and exact accounting. A positive record is only as trustworthy as the authenticated assessor and evidence behind it. A model saying “PASS” is not sufficient proof that tests ran.

## Execute the exact displayed source

Every example has the same ID as its source filename. For this example:

```sh
node examples/guide/typescript/first-run.ts
python examples/guide/python/first-run.py
cargo run --manifest-path examples/guide/rust/Cargo.toml --bin first-run
```

Commands run from the repository root. The Rust examples form a separate Cargo workspace that depends on `crates/aiws`; they do not call the TypeScript implementation. To run all documented examples use `node scripts/check-guide-examples.mjs`. Set `AIWS_PYTHON` or `AIWS_CARGO` when executable names differ.

## Next steps

Follow [application assembly](https://www.lril.ai/tutorial/) to dispatch a real local effect, then [authorization](https://www.lril.ai/authorization/), [recovery](https://www.lril.ai/recovery/) and [observability](https://www.lril.ai/observability/). Before using production data, understand the [security boundary](https://www.lril.ai/security/). Do not skip those pages merely because the first simulation passes.


---

# Assemble a governed application

The application owns identity, user interaction, credentials, adapters and evidence assessment. The SDK owns validation and the finite-v1 state transitions it checks. Keeping those responsibilities explicit avoids accidentally giving untrusted agent code a raw ledger connection.

## The execution path

A request enters your authenticated application. Your policy evaluates the requested command, actor, mission and current restrictions. The coordinator injects that decision, checks the expected state revision and asks SQLite to commit it. Only a successful dispatch commit permits the adapter call. The adapter reports a known outcome or uncertainty. Completion, verification and acceptance follow as separate recorded decisions.

Construct one store for one mission, supply a trusted clock and an authorizer, then pass only controlled command operations to callers. In Rust and Python the coordinator is synchronous; move blocking calls to dedicated workers in an async application. TypeScript awaits the authorizer and adapter, while its SQLite operations remain synchronous.

## Run an actual local effect

This complete example writes a local artifact in a new temporary directory, records a graph checkpoint, completes the run and demonstrates the verification/acceptance sequence. It reads `examples/review.json`. The identity, clock, cost and evidence values in this fixture are explicitly demonstrations; they must be replaced for a real coding service.

**Execute an adapter through the trusted coordinator** — Executable local adapter demonstration; fixed demo identity and fixture evidence

### Typescript example: coordinator

```typescript
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseContract, parseCommand, canonical, assessSuccess } from '@aiws/sdk';
import { SqliteStore } from '@aiws/sdk/sqlite';
import { Coordinator, type EffectAdapter } from '@aiws/sdk/runtime';
// A local demonstration. Replace this fixed demo identity with authenticated policy.
const demo = JSON.parse(readFileSync('examples/review.json', 'utf8'));
const directory = mkdtempSync(join(tmpdir(), 'aiws-review-'));
const store = new SqliteStore(join(directory, 'mission.db'), parseContract(canonical(demo.contract)));
const coordinator = new Coordinator(store, async () => ({ allowed: true, policy: 'ALLOW', mandatoryChecksOk: true,context:{actor:"demo:operator",policyRevision:"policy:1",decisionClass:"DETERMINISTIC"} }), () => '10');
const adapter: EffectAdapter = {
    async execute(operation) { writeFileSync(join(directory, 'review.txt'), canonical(operation.action.payload), { flag: 'wx' }); return { effect: 'CONFIRMED_APPLIED', actualCost: '3' }; },
    async reconcile(operation) { try {
        return { effect: readFileSync(join(directory, 'review.txt'), 'utf8') === canonical(operation.action.payload) ? 'CONFIRMED_APPLIED' : 'UNKNOWN', actualCost: '3' };
    }
    catch {
        return { effect: 'UNKNOWN', actualCost: '0' };
    } }
};
try {
    for (const raw of demo.commands) {
        const command = parseCommand(canonical(raw));
        if (command.type === 'dispatch')
            await coordinator.dispatch(command.attemptId, adapter, command.nodeId);
        else
            await coordinator.apply(command);
    }
    console.log(JSON.stringify({ directory, ...assessSuccess(store.snapshot(), 'r') }));
    writeFileSync(join(directory, 'audit.json'), store.exportAudit());
}
finally {
    store.close();
}

```

### Rust example: coordinator

```rust
use aiws_sdk::runtime::*;
use aiws_sdk::sqlite::SqliteStore;
use aiws_sdk::*;
use serde_json::{json, Value};
struct DemoIdentity;
impl Authorizer for DemoIdentity {
    fn authorize(&mut self, _: &Command, _: &Snapshot, _: &str) -> Result<Authorization> {
        Ok(Authorization {
            allowed: true,
            policy: "ALLOW".into(),
            mandatory_checks_ok: true,
            context: json!({"actor":"demo:operator","policyRevision":"policy:1","decisionClass":"DETERMINISTIC"}),
        })
    }
}
struct Fixed;
impl Clock for Fixed {
    fn now_ms(&self) -> String {
        "10".into()
    }
}
struct LocalReview(std::path::PathBuf);
impl EffectAdapter for LocalReview {
    fn execute(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        use std::io::Write;
        let mut file = std::fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&self.0)
            .map_err(|_| error("FILE_ERROR"))?;
        file.write_all(canonical(&operation["action"]["payload"])?.as_bytes())
            .map_err(|_| error("FILE_ERROR"))?;
        file.sync_all().map_err(|_| error("FILE_ERROR"))?;
        Ok(Outcome {
            effect: "CONFIRMED_APPLIED".into(),
            actual_cost: "3".into(),
        })
    }
    fn reconcile(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        let effect = if std::fs::read_to_string(&self.0).ok()
            == Some(canonical(&operation["action"]["payload"])?)
        {
            "CONFIRMED_APPLIED"
        } else {
            "UNKNOWN"
        };
        Ok(Outcome {
            effect: effect.into(),
            actual_cost: "3".into(),
        })
    }
}
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let demo: Value = serde_json::from_str(include_str!("../../../../../examples/review.json"))?;
    let directory = std::env::temp_dir().join(format!(
        "aiws-review-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_nanos()
    ));
    std::fs::create_dir(&directory)?;
    let store = SqliteStore::open(
        directory.join("mission.db").to_str().unwrap(),
        Some(&Contract::from_value(demo["contract"].clone())?),
    )?;
    let mut c = Coordinator::new(store, DemoIdentity, Fixed);
    let mut adapter = LocalReview(directory.join("review.txt"));
    for raw in demo["commands"].as_array().unwrap() {
        let command = Command::from_value(raw.clone())?;
        if raw["type"] == "dispatch" {
            c.dispatch(
                raw["attemptId"].as_str().unwrap(),
                &mut adapter,
                raw["nodeId"].as_str(),
            )?;
        } else {
            c.apply(&command, None)?;
        }
    }
    println!(
        "{}",
        json!({"directory":directory,"result":assess_success(&c.store.snapshot()?,"r")?})
    );
    std::fs::write(directory.join("audit.json"), c.store.export_audit()?)?;
    Ok(())
}

```

### Python example: coordinator

```python
"""Run a governed file effect with the independently implemented Python SDK."""
import json
import tempfile
from pathlib import Path
from aiws.core import canonical, assess_success
from aiws.sqlite import SqliteStore
from aiws.runtime import Coordinator

demo = json.loads(Path('examples/review.json').read_text())
directory = Path(tempfile.mkdtemp(prefix='aiws-python-review-'))
class Adapter:
    def execute(self, operation, attempt):
        with (directory/'review.txt').open('x') as out: out.write(canonical(operation['action']['payload']))
        return {'effect':'CONFIRMED_APPLIED','actualCost':'3'}
    def reconcile(self, operation, attempt):
        try: effect='CONFIRMED_APPLIED' if (directory/'review.txt').read_text()==canonical(operation['action']['payload']) else 'UNKNOWN'
        except OSError: effect='UNKNOWN'
        return {'effect':effect,'actualCost':'3' if effect=='CONFIRMED_APPLIED' else '0'}
with SqliteStore(str(directory/'mission.db'),demo['contract']) as store:
    # Demo identity only: replace with real authenticated and versioned policy.
    c=Coordinator(store,lambda *_:dict(allowed=True,policy='ALLOW',mandatoryChecksOk=True,context=dict(actor='demo:operator',policyRevision='policy:1',decisionClass='DETERMINISTIC')),lambda:'10')
    for command in demo['commands']:
        if command['type']=='dispatch': c.dispatch(command['attemptId'],Adapter(),command.get('nodeId'))
        else: c.apply(command)
    print(json.dumps({'directory':str(directory),**assess_success(store.snapshot(),'r')}))
    (directory/'audit.json').write_text(store.export_audit())

```

The expected output contains a temporary directory and `verifiedSuccessful: true`. Inspect the artifact, `mission.db` and `audit.json` there. The directory is intentionally retained for inspection. The mock evidence string in the supplied review fixture is not a cryptographic attestation; production assessment must compute and verify actual evidence.

## Turn the demonstration into a coding service

1. Authenticate the user outside the agent and resolve the repository and permitted branch from trusted application state.
2. Store an approved plan artifact with an immutable revision and explicit test criteria. The current SDK's plan ID records lineage; your application must enforce the human plan-approval gate.
3. Resolve capabilities such as repository editing or test execution through a fixed adapter registry. An agent must not select arbitrary shell commands or credential scopes merely by filling a JSON property.
4. Create an isolated working directory, record its base commit and give the adapter only authorized paths and tools.
5. Capture test command, exit status, output artifact digest, tool versions and repository revision. Read those results when creating the assessment.
6. Request acceptance from the configured human or authorized policy for that exact verification revision. A materially changed artifact requires reassessment.

The SDK does not execute an LLM, clone a repository, open a pull request, launch a sandbox or validate a domain claim automatically. It provides the checked command boundary around your implementations of those operations.

## Success and failure paths

Never put a generic “retry on every exception” wrapper around `dispatch`. A response can be lost after the external change happened. Let the coordinator preserve uncertainty, then use a human-controlled reconciliation path. If a result is known not to have applied, a checked retry can create another attempt of the same operation.

If the authorizer or ledger is unavailable, do not call the adapter through an alternate path. If recording settlement fails after the adapter returns, preserve the external receipt and reconcile after storage recovers. Re-running the whole HTTP request is not a substitute for identifying which phase committed.

Application integration is complete only when both its happy path and its interrupted path are exercised against the real adapter. Use [testing recipes](https://www.lril.ai/testing/) to build those checks.


---

# The execution model

[The AI Workflow Standard](https://www.lril.ai/standard/) separates the assignment, authority to act, observed effects and assessment of the result. Keeping these records distinct prevents a completed process from being confused with an accepted deliverable.

## Follow the identity chain

| Identity | What it represents | Lifetime |
|---|---|---|
| Mission identity | Durable assignment identity, mapped to engine workOrderId | Preserved across runs; current store is one mission |
| Contract | Immutable criteria, deadline, budget and supported profile | Contract.id is distinct from missionId |
| Run | An execution within that mission | Survives continuation |
| Plan revision | Approved or selected plan lineage | Changes through compare-and-swap activation |
| Segment | A continuation/ownership boundary | New identity on recovery |
| Graph / node | Dependency structure and a checkpoint in it | Graph fixed once attached |
| Operation | One logical external action and its admitted scope | Same operation across safe retries |
| Attempt | A particular dispatch and settlement | New identity for each retry |
| Wait | Correlated pending input, timer or authorization | Explicit expiry and satisfaction |
| Assessment | Verification or acceptance attached to the current subject | Can be invalidated |
| Handoff | Transfer of outputs and responsibility | Proposed engine protocol; see handoffs |

The accepted engine model maps workOrderId to the existing Contract.missionId value. Work order and mission are the same assignment; Contract.id identifies its governing contract. A separate engine WorkOrder record contains runs and optional children. Do not insert workOrderId into the closed current Contract schema. See WO-01 through WO-08 in Engine decisions for the versioned contract and compatibility rules.

## Four separate questions

1. **May this actor act?** The host authenticates the caller and checks current policy. Grants constrain finite resources, actions, purposes, validity and spending. An approval may be required for the exact action fingerprint.
2. **What happened?** The adapter reports confirmed application, confirmed nonapplication or uncertainty. A process exit or HTTP timeout alone may not establish the external effect.
3. **Did execution finish?** Explicit run transitions and graph checkpoints establish completion. Unsettled work blocks completion.
4. **Was the deliverable accepted?** Verification and acceptance must be current and consistent with the contract. Assess success only after execution has completed.

## Pure core and trusted runtime

The reducer is deterministic with an explicit clock value. It evaluates a command and returns a new snapshot or an error; it does not execute tools, start timers or authenticate a user. The SQLite store adds transactions and replayable history. The coordinator adds the application authorizer and the boundary around effect dispatch. A scheduler, identity service, artifact service and human inbox belong to the application or future engine.

Use the pure API for simulations, command planning and cross-language parity. Use the coordinator at an application's command boundary. Never let an untrusted agent obtain the database handle or call store.apply directly.

## Values that cross languages

Use canonical decimal strings for quantities and UTC epoch milliseconds. JSON numbers must be safe integers; floating-point prices, NaN and Infinity are not wire values. Choose an exact accounting unit, such as microcredits, outside the SDK and use it consistently. An SDK cost is not automatically a provider invoice, token counter or currency conversion.

Identifiers are immutable strings. Generate them once and persist them before a network retry. Distinguish a logical operation ID, attempt ID, trigger-event ID and handoff delivery ID; each deduplicates a different boundary.

Continue with the [implementation tutorial](https://www.lril.ai/tutorial/) and the [capability matrix](https://www.lril.ai/capabilities/).


---

# Architecture and workflow diagrams

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.


The first ten diagrams were generated from the repository at commit `13f323f` with Archify. Each one is a self-contained page with light and dark themes, pan and zoom, search, relationship tracing, guided views and PNG or SVG export. The two architecture pages link their nodes to the source files on GitHub. Use a diagram inside its frame, or open it full screen for the complete toolbar.

The JSON source of every diagram lives beside it under `docs/diagrams/` in the repository, together with the regeneration commands and the browser evidence from the last delivery.

## Facts the diagrams encode

- The engine under `engine/src` imports only Node built-ins. It does not depend on the SDK packages.
- The static control API accepts four commands: approve, pause, resume and cancel. The eleven governed dynamic-workflow decisions ride a separate `/engine/dynamic/v1/requests` plane.
- OTLP export is an SDK capability. The engine keeps a bounded telemetry queue but contains no OTLP code.
- A timeout, signal or null exit code is classified UNKNOWN on every platform. Only the signed Windows native helper can prove self-termination.
- The push and pull-request path of the native helper workflow builds and probes but never signs or uploads. Signing happens only on manual dispatch, and platform qualification is a manual step on a native Windows host.
- Every platform-matrix cell remains UNVERIFIED.

## System overview

The host application, the three native SDKs, the shared spec, the self-contained engine, this documentation site, CI and the signed Windows helper. Read with [engine decisions and implementation boundary](https://www.lril.ai/engine-design/) and the [capability matrix](https://www.lril.ai/capabilities/).

**Diagram: System Overview** (architecture) — Host application, three native SDKs, shared spec, self-contained engine, docs site, CI and the signed Windows helper. [Open the interactive diagram](https://www.lril.ai/diagrams/01-system-overview.html)

## Governed coding workflow

Human approval, implementation, validation, pinned evidence, bounded correction, holds and human resume. Read with [run a governed coding workflow](https://www.lril.ai/engine-coding-workflow/).

**Diagram: Governed Coding Workflow** (workflow) — Human approval, implementation, validation, pinned evidence, bounded correction, holds and human resume. [Open the interactive diagram](https://www.lril.ai/diagrams/02-coding-workflow.html)

## Control API decision lifecycle

Inspect, approval challenge, single-transaction command, receipt and exact-retry replay. Read with [engine API, CLI and web controls](https://www.lril.ai/engine-control-api/).

**Diagram: Control API Decision Lifecycle** (sequence) — Inspect, approval challenge, single-transaction command, receipt and exact-retry replay. [Open the interactive diagram](https://www.lril.ai/diagrams/03-control-decision.html)

## Coding work order lifecycle

The five `CodingStatus` values, bounded correction, holds and cancellation. Read with [run a governed coding workflow](https://www.lril.ai/engine-coding-workflow/) and [validation and correction](https://www.lril.ai/engine-validation-correction/).

**Diagram: Coding Work Order Lifecycle** (lifecycle) — The five CodingStatus values, bounded correction, holds and cancellation. [Open the interactive diagram](https://www.lril.ai/diagrams/04-work-order-lifecycle.html)

## Engine internals

Control plane, identity adapter, runner, bounded storage executor, SQLite worker thread, clock gate and telemetry queue. Read with [engine decisions and implementation boundary](https://www.lril.ai/engine-design/) and [SQLite persistence and recovery tests](https://www.lril.ai/engine-persistence/).

**Diagram: Praxis Internals** (architecture) — Control plane, identity adapter, runner, bounded storage executor, SQLite worker thread, clock gate and telemetry queue. [Open the interactive diagram](https://www.lril.ai/diagrams/05-engine-internals.html)

## Observability export pipeline

Event and trace intent committed together, leased outbox, OTLP/HTTP export, retained failures and alerts. Read with [observability, metrics and OTLP delivery](https://www.lril.ai/observability/).

**Diagram: Observability Export Pipeline** (dataflow) — Event and trace intent committed together, leased outbox, OTLP/HTTP export, retained failures and alerts. [Open the interactive diagram](https://www.lril.ai/diagrams/06-telemetry-dataflow.html)

## Windows native helper pipeline

Build and probe on push or pull request, Azure Artifact Signing and Authenticode verification on manual dispatch, then manual qualification on a native Windows host. Read with [deployment and platform qualification](https://www.lril.ai/engine-deployment-platforms/).

**Diagram: Windows Native Helper Pipeline** (workflow) — Build and probe on push or pull request, Azure Artifact Signing and Authenticode verification on manual dispatch, manual qualification. [Open the interactive diagram](https://www.lril.ai/diagrams/07-native-helper-pipeline.html)

## Operation effect lifecycle

PREPARED, DISPATCHED, SUCCEEDED and CONFIRMED_APPLIED; UNKNOWN reconciliation, retry after CONFIRMED_NOT_APPLIED and epoch fencing. Read with [effects, operation identities and retries](https://www.lril.ai/effects/) and [recovery](https://www.lril.ai/recovery/).

**Diagram: Operation Effect Lifecycle** (lifecycle) — PREPARED, DISPATCHED, SUCCEEDED and CONFIRMED_APPLIED; UNKNOWN reconciliation, retry after CONFIRMED_NOT_APPLIED and epoch fencing. [Open the interactive diagram](https://www.lril.ai/diagrams/08-operation-effect-lifecycle.html)

## Worker dispatch lease

Claim under the clock gate, supervised admission, acknowledge, execute without a shell and classify the outcome. Read with [worker dispatch and coding execution](https://www.lril.ai/engine-worker-execution/).

**Diagram: Worker Dispatch Lease** (sequence) — Claim under the clock gate, supervised admission, acknowledge, execute without a shell and classify the outcome. [Open the interactive diagram](https://www.lril.ai/diagrams/09-worker-dispatch.html)

## Engine backup and restore

Checkpoint, sealed manifest, atomic publication, host-verified restore, RESTORE HOLD and human activation. Read with [engine backups and same-machine restore](https://www.lril.ai/engine-backup-restore/).

**Diagram: Praxis Backup and Restore** (workflow) — Checkpoint, sealed manifest, atomic publication, host-verified restore, RESTORE HOLD and human activation. [Open the interactive diagram](https://www.lril.ai/diagrams/10-backup-restore.html)

## M9 protocol architecture package

M9.10 adds four scalable SVG maps. These are intentionally visual documentation assets rather than Mermaid/ASCII diagrams.

### Protocol system map

![Praxis protocol interoperability architecture](https://www.lril.ai/images/m9/protocol-architecture.svg)

### MCP / A2A / AG-UI mapping

![Protocol mapping across MCP, A2A and AG-UI](https://www.lril.ai/images/m9/protocol-mappings.svg)

### Durable identity and registry

![Durable protocol identity and registry](https://www.lril.ai/images/m9/protocol-registry.svg)

### UNKNOWN and recovery

![UNKNOWN and recovery lifecycle](https://www.lril.ai/images/m9/protocol-recovery.svg)

Read [Praxis protocol interoperability](https://www.lril.ai/engine-protocol-interoperability/), [protocol operations and recovery](https://www.lril.ai/engine-protocol-operations/) and [M9 completion and compatibility](https://www.lril.ai/engine-m9-completion/) with these maps.

## Regenerating a diagram

Each `docs/diagrams/<name>.<type>.json` file is the source of truth. After editing one, validate and deliver it with the Archify CLI, then run the browser check. Architecture pages need `--repo-root` because they carry source-file evidence.

```sh
node <archify>/bin/archify.mjs validate <type> docs/diagrams/<name>.json --quality showcase [--repo-root .]
node <archify>/bin/archify.mjs deliver  <type> docs/diagrams/<name>.json docs/diagrams/<name>.html --quality showcase [--repo-root .]
node <archify>/bin/archify.mjs visual-check docs/diagrams/<name>.html
```

The site build copies the delivered HTML from `docs/diagrams/` into `public/diagrams/`, so a regenerated diagram appears here after the next `npm run build` or `npm run dev`. Adding a diagram means adding its entry to `docs/diagrams/index.json` and a section on this page.


---

# Implemented capabilities and Praxis proposals

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

**Milestone status:** M6 is complete for the D-M6-01 supervised Windows 11 x64 / Node 24 / local SQLite developer profile. [M8 is now active](https://www.lril.ai/engine-m8-qualification/): its plan is complete; qualification and expanded release/support claims remain open.

This matrix is the first reference for an agent generating application code. **Implemented** means present in the finite-v1 baseline. **M2 source** means implemented in the opt-in handoff-v1 source profile, absent from the previously built 0.3.0 binaries. **Application-owned** means that the host must supply it. **Proposed** means that no corresponding SDK command or service exists yet; it does not mean the owner-approved design decision is unresolved.

| Capability | Status | Integration rule |
|---|---|---|
| Strict JSON, shared record schemas and decimal accounting | Implemented | Validate at ingress; see wire reference |
| Finite grants, approval fingerprints and dispatch gates | Implemented | Host must authenticate identity and bind policy |
| Deterministic reducer and success assessment | Implemented | No external effects occur during reduction |
| SQLite journal, revision comparison and audit playback | Implemented | One mission per database; replay cost grows with history |
| Coordinator dispatch, UNKNOWN settlement and safe retry | Implemented | Adapter must report truthful external outcomes |
| Recovery segment/epoch and unresolved-attempt report | Implemented | Not a complete worker or target fencing protocol |
| Seven trigger kinds and fixed-interval catch-up | Implemented | Host supplies event ingress and tick scheduling |
| Thirteen node kinds and readiness/completion checks | Implemented | Host supplies task execution and data mapping |
| Input/output JSON Schema checks | Implemented | Restricted schema support; validate artifact bytes separately |
| Control-event traces, metric snapshots and alerts | Implemented | Activity spans and notification routing are host-owned |
| Durable OTLP/HTTP outbox | Implemented | Host must flush it, monitor capacity and manage retention |
| Authentication, human identity and approval inbox | Application-owned | Do not trust an actor string in a command |
| Real test execution and acceptance evidence | Application-owned | SDK checks records, not the truth of an external test report |
| External idempotency, cancellation and read-back | Application-owned | Do not promise exactly-once effects |
| Cron/calendar/DST scheduling and live background workers | Application-owned | Current schedule is UTC fixed interval |
| Nested work-order limits and a protected handoff reserve | M3 source | Separate shared LimitStore; all external dispatch must pass ledger and existing host gates |
| Resumable holds at loop/resource exhaustion | M2/M3 source | Handoff holds plus nested admission gates; finite-v1 LOOP remains terminal FAILED |
| Atomic handoff manifests, delivery claims and acknowledgements | M2 source | Opt-in HandoffStore and authenticated host coordinator |
| Content-addressed artifacts and retention checks | M2 source | Native FileArtifacts; host owns backups, retention and cleanup |
| Automatic restart policy and task-aware pause orchestration | Proposed | Current records do not start/restart processes |
| Dynamic graph mutation, task injection and agent replacement | Proposed | Current attached graph is fixed |
| Long-history compaction and inspection | Implemented in local engine source | Lossless segments, indexed pages and resumable verification; see [replay limits](https://www.lril.ai/engine-replay-limits/). Native 0.3.0 SDK parser limits remain unchanged |
| Months-long Praxis qualification | Pending | Requires retention, platform and measured capacity evidence |
| Protocol binding abstraction for MCP, A2A and AG-UI | M9.1 complete | Common manifest/catalog/observation boundary is verified; protocol compatibility still requires protocol-specific conformance |
| MCP 2026-07-28 outbound integration | M9.2 complete for synchronous profile | Official v2.2.0 client/server conformance passed for tools/resources/prompts, host auth, timeout/cancel UNKNOWN handling and material capability pinning; Tasks/restartable remote handles are M9.3 |
| MCP Tasks durability/recovery | M9.3 complete | Durable remote-task mapping, persisted restart handoff, polling, revision-bound approved input, cancellation, retry fencing and UNKNOWN reconciliation are verified against pinned official `@modelcontextprotocol/ext-tasks` conformance |
| A2A 1.0 outbound client | M9.4 complete | Durable task identity, fresh Agent Card/skill material pinning, endpoint/time fencing, polling/resubscription and pinned official `@a2a-js/sdk@1.3.0` HTTP+JSON conformance are verified |
| A2A 1.0 Praxis server / Agent Card | M9.5 complete | Authenticated/authorized admission, durable inbound remote↔local identity, official AgentExecutor bridge, status/artifact streaming and terminal-task immutability are verified; A2A events never grant Praxis authority |
| AG-UI 1.0 event projection | M9.6 complete | Projection-only run/step/message/tool/state/activity/subagent events validate against pinned official `@ag-ui/core@1.0.0`; no Praxis mutation or approval/control ingress is introduced |
| AG-UI authenticated human control | M9.7 complete | Standard interrupt/resume maps to the existing authenticated, material-bound, durable Praxis control API; exact retry/receipt recovery and official AG-UI 1.0 schemas are verified |
| Durable cross-protocol registry and observability | M9.8 complete | Immutable binding revisions, separate local/remote identity, evidence correlation, stale-result fencing and allowlisted hashed-identity telemetry are verified |
| Cross-protocol compatibility campaign | M9.9 complete | MCP 2026-07-28 + Tasks, A2A 1.0 and AG-UI 1.0 passed together across 4 profiles, 16 required scenarios and 16 executable checks |
| Cross-language protocol correlation helper | M9.10 complete | TypeScript/Rust/Python source records correlate AIWS/Praxis identity with exact binding revision/remote identity and require `authoritative: false`; not present in older 0.3.0 binary archives |
| Recovery on another machine | Outside agreed Praxis scope | Do not infer distributed failover support |

## A useful current application boundary

Applications can use the libraries now to govern an adapter-driven workflow, persist its control history, retain uncertainty, inspect outcomes and emit telemetry. They still need a host that invokes the coordinator and supplies the trusted services above. The [tutorial](https://www.lril.ai/tutorial/) demonstrates this boundary without inventing scheduler or handoff APIs.

No npm, crates.io or PyPI publication is implied by the package names. Download the supplied packages or use the source workspace. SDK 0.3.0 and proposed standard edition 0.4 are different version numbers with different purposes.


## M2 source additions

All three SDKs implement immutable handoffs, per-consumer receipt claims, atomic admission, resumable RUN/BRANCH holds, pinned join inputs, artifact verification, deterministic reports, metric counts and a correlated durable notification outbox. See [handoffs](https://www.lril.ai/handoffs/) for equivalent examples and exact API boundaries. Work order = mission is an accepted identity. Full scheduling, worker lifecycle and dynamic graph changes remain Praxis work.

## M3 source additions

All three SDKs provide the opt-in `aiws-limits/1` ledger: hierarchical cost, active-time, elapsed-time and attempt accounting; protected handoff allowance; ancestor/dependency admission gates; durable reservations, stop/settlement evidence and human-only adjustment. Read [budgets](https://www.lril.ai/budgets/) for equivalent programs and required host ordering. No cross-database atomic commit, external cancellation service or automatic import of historical running work is claimed. M3 APIs are source additions, absent from the older 0.3.0 binary downloads.

## M5 Praxis source boundary

The separate TypeScript `engine/` package now has executable SQLite, scheduler, dispatch, worker, validation and control primitives. [Persistence and recovery tests](https://www.lril.ai/engine-persistence/) documents the verified storage slice and its limits. A [composed local coding workflow](https://www.lril.ai/engine-coding-workflow/) now executes approval through acceptance, including bounded correction and interrupted recovery. M5 remains in progress: full wire/authentication integration, CLI/web controls and conformance closure are not complete. Engine source APIs are not exports of the three released SDK 0.3.0 packages.


The M5 [local control profile](https://www.lril.ai/engine-control-api/) now provides authenticated HTTPS, CLI and web decisions for the composed workflow. Production enrollment/provider integrations, the complete command catalog, paged history and release qualification remain open.

## Protocol interoperability

M9 is complete as the scoped protocol-interoperability feature track; M8 remains the independent qualification track. The common Praxis source contract represents version-pinned MCP, A2A and AG-UI bindings while keeping external observations non-authoritative. MCP synchronous/Tasks, A2A 1.0 client/server and AG-UI 1.0 event-projection profiles are verified against pinned official packages. AG-UI authenticated human-control ingress is verified in M9.7; M9.8 verifies a durable cross-protocol registry and privacy-preserving telemetry projection in engine source; M9.9 verifies the executable pinned cross-protocol campaign: all 4 advertised profiles, all 16 required scenarios and all 16 executable checks passed. M9.10 completes the cross-language correlation helpers, runnable examples, operator/recovery documentation, exact compatibility matrix and four scalable SVG architecture maps. Read [Praxis protocol interoperability](https://www.lril.ai/engine-protocol-interoperability/) and the repository `docs/M9-BUILD-PLAN.md` before generating integration code.

## Dynamic client controls

M7 slice 8 adds native TypeScript/Rust/Python contracts and verified HTTPS clients, protected-input CLI and reviewed web controls. All eleven dynamic decisions share the existing authoritative Praxis semantics. See [native dynamic controls](https://www.lril.ai/engine-dynamic-controls/). Use current GitHub source; baseline downloadable SDK packages are unchanged.


---

# Documentation contract for coding agents

Use this page when an agent builds an application with AIWS. Start with the capability matrix and exact API reference, then select the corresponding tested example. Prefer the raw Markdown and downloadable handbook when browser tabs are unavailable.

## Machine-readable entry points

`/llms.txt` is a concise navigation index. `/llms-full.txt` contains the handbook with all three languages expanded. `/docs-raw/` contains individual Markdown pages. These are documentation conveniences, not a claim of conformance to a formal discovery protocol. Access follows this site's owner-private policy; an unauthenticated external agent may need the downloaded source or handbook.

Every language tab is rendered into the HTML, and the plain-text export includes every panel. Do not assume only the initially selected language exists. Example source lives in examples/guide/typescript, examples/guide/python and examples/guide/rust/src/bin. The displayed code and executable files are generated from the same example catalog.

## Required implementation sequence

1. Identify the SDK version and finite-v1 edition. Do not combine edition 0.3 journals with edition 0.4 commands.
2. Determine which requirements are implemented, supplied by the application or still proposed for the engine.
3. Choose integer accounting units and a trusted clock. Keep quantities as decimal strings.
4. Define input/output material, finite grants and acceptance criteria before dispatch.
5. Route untrusted commands through authenticated application policy and the coordinator.
6. Implement effects and read-only reconciliation using stable identities and target-specific protections.
7. Verify actual evidence before recording PASS and acceptance.
8. Exercise failed authorization, rejected budget, lost response and restart with the real adapter.

## Rules that prevent invented integrations

Do not invent Engine.run, workflow.execute, resumeAll, handoff.send, cron.parse, pauseWorker or automatic provider adapters. These do not exist in SDK 0.3.0.

M9 adds a **common Praxis protocol-binding source contract**. MCP 2026-07-28 synchronous and Tasks profiles are verified; preserve `capabilityDigest`/`expectedCapabilityDigest`, pre-dispatch re-discovery, durable remote-task mapping, task revision checks, authority/approval evidence, endpoint/version fencing and conservative `UNKNOWN` handling. A2A 1.0 is also verified in both directions. Outbound code must keep Praxis identity separate from A2A task/context identity, revalidate fresh Agent Card/skill material and treat task/message/stream data as non-authoritative evidence. Inbound code must use `PraxisA2AServerCore`/the official server bridge only behind host authentication and Praxis authorization; an Agent Card advertises capability but grants no authority. Terminal A2A tasks are immutable—do not append a message to a completed task; create new remote task identity for later work while preserving any allowed conversation context. AG-UI 1.0 projection exists in engine source and is verified against pinned `@ag-ui/core@1.0.0`; M9.7 verifies authenticated AG-UI interrupt/resume control ingress through `PraxisAguiControlBridge`; it passes the existing Praxis authentication, current-material, authorization, challenge and durable command/receipt boundaries. Do not turn AG-UI events or resume payloads directly into approvals or mutations. M9.8 verifies `ProtocolRegistry` for durable cross-protocol binding/operation identity and evidence correlation. M9.9 verifies the checked-in conformance manifest and campaign runner. Agents should only claim protocol compatibility for versions present in that manifest and backed by the passing M9.9 report (run `36995139650`, artifact `11221346823`). M9.10 source adds a portable `aiws-protocol-correlation/1` helper to TypeScript, Rust and Python; use it only to correlate AIWS/Praxis identity with binding/remote identity. It always remains non-authoritative, does not replace official protocol SDKs, and is newer than the previously built SDK 0.3.0 binary archives. Keep protocol-specific recovery stores; use the registry for shared identity/fencing, and export only its allowlisted hashed-identity telemetry projection. For incident handling and restart/UNKNOWN procedures, follow [protocol operations and recovery](https://www.lril.ai/engine-protocol-operations/); for the closed compatibility matrix use [M9 completion and compatibility](https://www.lril.ai/engine-m9-completion/). External MCP/A2A/AG-UI messages cannot grant authority, approve work, verify results or accept deliverables. Graph nodes describe obligations; the application implements scheduling and execution. A TASK with executionKind AGENT does not create a model client. A WorkOrder object is not a supported contract field.

Do not make a deny-all authorizer permissive to get an example working. Do not treat fixture policies, fixed clocks or placeholder evidence as production controls. Never change an agent's own permissions or limits without human approval. Record unavailable evidence as INCONCLUSIVE, and retain UNKNOWN when an external outcome cannot be established.

## Context to request from the application owner

Ask for the target resource scope, authenticated principal source, policy rules, desired effects, evidence criteria, accounting units, expected time horizon and storage location. Clarify what the adapter can cancel and how it proves external outcomes. If those are unknown, implement a simulation or interface with the missing dependency explicitly documented; do not fabricate credentials or a permissive production policy.

## Deliverable expectations

An agent-created application should include executable setup instructions, its tested SDK version, a concrete adapter contract, an operational recovery procedure, a verification report and explicit integration limitations. Tests should assert observable safety outcomes, not simply mirror the implementation. Include a handoff describing remaining work and current blockers if execution stops.


---

# Identity, authority and human approval

An authorization record has meaning only when supplied by a trusted component. The coordinator calls your authorizer for every command and replaces caller-supplied context. It also replaces `policy`, `mandatoryChecksOk` and `authorizationCheckedAt` on admission, dispatch and retry.

## Authorizer contract

Return `allowed`, `policy`, `mandatoryChecksOk` and `context`. Context requires actor, policy revision and decision class. TypeScript uses an async callback; Python uses a callable returning the same JSON-shaped fields; Rust implements `Authorizer` and returns `Authorization`, whose field names follow Rust conventions.

Use an authenticated session or workload identity established by the host. Never read a command's `context.actor` as proof of who sent it. Bind permission evaluation to the selected mission and resource, not just the command name. A valid “approve” command shape is not evidence that the approver is authorized.

**Deny agent attempts to issue new grants** — Executable application recipe

### Typescript example: authorization

```typescript
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {parseContract,type Command} from '@aiws/sdk';
import {SqliteStore} from '@aiws/sdk/sqlite';
import {Coordinator,type Authorizer} from '@aiws/sdk/runtime';
// Demo session comes from the application, never command.context or an event payload.
const session={actor:'agent:builder',isHuman:false};
const humanOnly=new Set(['grant','revokeGrant','approve','withdrawApproval']);
const allowedCommands=new Set(['startRun']); // deliberately narrow teaching policy
const authorize:Authorizer=async(command)=>{
  const allowed=allowedCommands.has(command.type)&&(!humanOnly.has(command.type)||session.isHuman);
  return {allowed,policy:allowed?'ALLOW':'DENY',mandatoryChecksOk:true,
    context:{actor:session.actor,policyRevision:'policy:example:1',decisionClass:'DETERMINISTIC'}};
};
const contract=parseContract(readFileSync('examples/guide/contract.json','utf8'));
const store=new SqliteStore(':memory:',contract);
const coordinator=new Coordinator(store,authorize,()=> '10');
try {
  await coordinator.apply({type:'startRun',runId:'r'});
  const grant:Command={type:'grant',grant:{id:'g',subject:session.actor,profile:'finite-v1',actions:['write'],resources:['doc'],notBefore:'0',expiresAt:'10000',limit:'100',canDelegate:false,depth:'0'}};
  await assert.rejects(coordinator.apply(grant),(e:any)=>e.code==='AUTHORITY_DENIED');
  assert.equal(store.snapshot().revision,'1');
  assert.equal(store.diagnostics()[0].command.code,'AUTHORITY_DENIED');
} finally {store.close();}

```

### Rust example: authorization

```rust
use aiws_sdk::*;
use aiws_sdk::runtime::*;
use aiws_sdk::sqlite::SqliteStore;
use serde_json::json;
struct SessionPolicy;
impl Authorizer for SessionPolicy {
    fn authorize(&mut self,command:&Command,_:&Snapshot,_:&str)->Result<Authorization>{
        // Deliberately narrow demo agent policy. Identity comes from trusted host state.
        let allowed=command.as_value()["type"]=="startRun";
        Ok(Authorization{allowed,policy:if allowed{"ALLOW"}else{"DENY"}.into(),mandatory_checks_ok:true,
            context:json!({"actor":"agent:builder","policyRevision":"policy:example:1","decisionClass":"DETERMINISTIC"})})
    }
}
struct FixedClock;
impl Clock for FixedClock{fn now_ms(&self)->String{"10".into()}}
fn main()->std::result::Result<(),Box<dyn std::error::Error>>{
    let contract=Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?;
    let store=SqliteStore::open(":memory:",Some(&contract))?;
    let mut coordinator=Coordinator::new(store,SessionPolicy,FixedClock);
    coordinator.apply(&Command::from_value(json!({"type":"startRun","runId":"r"}))?,None)?;
    let grant=Command::from_value(json!({"type":"grant","grant":{"id":"g","subject":"agent:builder","profile":"finite-v1","actions":["write"],"resources":["doc"],"notBefore":"0","expiresAt":"10000","limit":"100","canDelegate":false,"depth":"0"}}))?;
    assert_eq!(coordinator.apply(&grant,None).unwrap_err().code,"AUTHORITY_DENIED");
    assert_eq!(coordinator.store.snapshot()?.as_value()["revision"],"1");
    assert_eq!(coordinator.store.diagnostics()?[0]["command"]["code"],"AUTHORITY_DENIED");
    Ok(())
}

```

### Python example: authorization

```python
from pathlib import Path
from aiws import parse_contract,AiwsError
from aiws.sqlite import SqliteStore
from aiws.runtime import Coordinator
# Identity is trusted application state, not a caller-supplied command field.
session={'actor':'agent:builder','isHuman':False}
def authorize(command,state,now):
    allowed=command['type']=='startRun' # narrow demo policy; everything else is denied
    return dict(allowed=allowed,policy='ALLOW' if allowed else 'DENY',mandatoryChecksOk=True,
        context=dict(actor=session['actor'],policyRevision='policy:example:1',decisionClass='DETERMINISTIC'))
contract=parse_contract(Path('examples/guide/contract.json').read_text())
with SqliteStore(':memory:',contract) as store:
    coordinator=Coordinator(store,authorize,lambda:'10')
    coordinator.apply(dict(type='startRun',runId='r'))
    grant=dict(type='grant',grant=dict(id='g',subject=session['actor'],profile='finite-v1',actions=['write'],resources=['doc'],notBefore='0',expiresAt='10000',limit='100',canDelegate=False,depth='0'))
    try:
        coordinator.apply(grant)
        raise AssertionError('expected denial')
    except AiwsError as error:
        assert error.code=='AUTHORITY_DENIED'
    assert store.snapshot()['revision']=='1'
    assert store.diagnostics()[0]['command']['code']=='AUTHORITY_DENIED'

```

This example deliberately permits only run creation by a demo agent. It rejects grant creation, records a diagnostic, and leaves the mission revision unchanged by the rejection. It is a shape and enforcement example, not a complete organizational policy service. Replace its fixed principal and command allowlist before deployment.

## Grants and delegated scope

A grant defines an exact subject, finite actions/resources, validity interval, allowance, delegation flag and depth. Child grants cannot expand their parent's scope, lifetime or allocation. Wildcards, regular expressions and inferred resource containment are not supported. Ancestor revocation is checked again at dispatch.

**Reject an expanded child grant** — Executable control simulation

### Typescript example: grant-scope

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 2
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "child",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2",
    "parentId": "g"
  }
})), '10'), (error: any) => error.code === 'AUTHORITY_DENIED');
console.log('PASS: grant-scope');

```

### Rust example: grant-scope

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 2
    let rejected = Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "child",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2",
    "parentId": "g"
  }
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "AUTHORITY_DENIED");
    println!("PASS: grant-scope");
    Ok(())
}

```

### Python example: grant-scope

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 2
try:
    reduce(state, {'type': 'grant',
     'grant': {'id': 'child',
               'subject': 'agent',
               'profile': 'finite-v1',
               'actions': ['write'],
               'resources': ['doc'],
               'notBefore': '0',
               'expiresAt': '10000',
               'limit': '100',
               'canDelegate': True,
               'depth': '2',
               'parentId': 'g'}}, '10')
    raise AssertionError('expected AUTHORITY_DENIED')
except AiwsError as error:
    assert error.code == 'AUTHORITY_DENIED'
print('PASS: grant-scope')

```

`CONTAINED` means the supported finite comparison succeeded. `INDETERMINATE` is not a permissive result. Failure to interpret authority must block admission. Grant containment alone does not authenticate a principal or enforce operating-system and network access.

## Material-bound action approval

Approval covers a run and action fingerprint, with expiry and a use count. The fingerprint includes capability, resource, payload and preconditions. Changing those values requires a new material decision rather than reusing approval for the old action.

**Bind approval to action material and use count** — Executable control simulation

### Typescript example: approval

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": true,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "approve",
  "approval": {
    "id": "ap",
    "runId": "r",
    "approver": "owner",
    "fingerprint": "sha256:50487a924d1b8f40e5051f0f8d278469acb1cd15d08dbef8f1010e7f54ae9ef0",
    "expiresAt": "1000",
    "uses": "1"
  }
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "approvalId": "ap"
})), '10');

// Step 5
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o2",
  "attemptId": "a2",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "approvalId": "ap"
})), '10'), (error: any) => error.code === 'APPROVAL_CONSUMED');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "withdrawApproval",
  "approvalId": "ap"
})), '10');

// Step 7
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10'), (error: any) => error.code === 'APPROVAL_INVALID');
console.log('PASS: approval');

```

### Rust example: approval

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": true,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "approve",
  "approval": {
    "id": "ap",
    "runId": "r",
    "approver": "owner",
    "fingerprint": "sha256:50487a924d1b8f40e5051f0f8d278469acb1cd15d08dbef8f1010e7f54ae9ef0",
    "expiresAt": "1000",
    "uses": "1"
  }
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "approvalId": "ap"
}))?, "10")?;

    // Step 5
    let rejected = Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o2",
  "attemptId": "a2",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "approvalId": "ap"
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "APPROVAL_CONSUMED");

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "withdrawApproval",
  "approvalId": "ap"
}))?, "10")?;

    // Step 7
    let rejected = Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "APPROVAL_INVALID");
    println!("PASS: approval");
    Ok(())
}

```

### Python example: approval

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': True,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 3
state = reduce(state, {'type': 'approve',
 'approval': {'id': 'ap',
              'runId': 'r',
              'approver': 'owner',
              'fingerprint': 'sha256:50487a924d1b8f40e5051f0f8d278469acb1cd15d08dbef8f1010e7f54ae9ef0',
              'expiresAt': '1000',
              'uses': '1'}}, '10')

# Step 4
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True,
 'approvalId': 'ap'}, '10')

# Step 5
try:
    reduce(state, {'type': 'admit',
     'runId': 'r',
     'operationId': 'o2',
     'attemptId': 'a2',
     'grantId': 'g',
     'subject': 'agent',
     'action': {'capability': 'write',
                'resource': 'doc',
                'payload': {'value': 1},
                'preconditions': {'revision': 1}},
     'amount': '10',
     'authorizationCheckedAt': '10',
     'policy': 'ALLOW',
     'mandatoryChecksOk': True,
     'approvalId': 'ap'}, '10')
    raise AssertionError('expected APPROVAL_CONSUMED')
except AiwsError as error:
    assert error.code == 'APPROVAL_CONSUMED'

# Step 6
state = reduce(state, {'type': 'withdrawApproval', 'approvalId': 'ap'}, '10')

# Step 7
try:
    reduce(state, {'type': 'dispatch',
     'attemptId': 'a',
     'authorizationCheckedAt': '10',
     'policy': 'ALLOW',
     'mandatoryChecksOk': True}, '10')
    raise AssertionError('expected APPROVAL_INVALID')
except AiwsError as error:
    assert error.code == 'APPROVAL_INVALID'
print('PASS: approval')

```

The SDK binds approval use to logical operations, not every retry attempt. It rechecks current approval validity before dispatch. It does not send an approval notification or authenticate the approver. Store the human decision and relevant evidence in your application, and issue the command through a policy that verifies the human's authority.

## Engine rule: humans control permissions and resource limits

The proposed engine requires human approval for any agent-requested change to its permissions or resource limits. Enforce this in the trusted application policy; the low-level SDK contains no built-in concept of an authenticated human. Dynamic graph proposals must not smuggle in new grants, reset budgets or choose an alternate policy revision. Read [engine decisions](https://www.lril.ai/engine-design/) for the adopted design direction.


---

# Effects, operation identities and retries

An operation is one intended external change. An attempt is one authorized effort to perform it. Keeping those identities separate lets the application retry safely without pretending that a second attempt represents a different business action.

An interactive [operation effect lifecycle](https://www.lril.ai/diagrams/#operation-effect-lifecycle) diagram shows every effect state, UNKNOWN reconciliation and epoch fencing.

## Adapter responsibilities

Implement `execute(operation, attempt)` and `reconcile(operation, attempt)`. Execution may change the external world; reconciliation must discover external truth without repeating that change. Both return an effect disposition and decimal-string actual cost. Exceptions during execution become UNKNOWN in the coordinator. They do not prove that nothing happened.

Use a stable operation ID as the basis for an external idempotency key where the target supports it. Include material/precondition checks so that changed requests cannot reuse a key silently. A target's idempotency retention period is an integration contract; the SDK cannot discover it for you. If no safe target deduplication or reliable readback exists, do not retry an uncertain effect.

| Disposition | Meaning | Permitted next step |
|---|---|---|
| NOT_DISPATCHED | No dispatch has been committed for this operation | Current prepared owner may dispatch after checks |
| UNKNOWN | Dispatch occurred or its outcome cannot be established | Human-controlled reconciliation; retain reservation |
| CONFIRMED_NOT_APPLIED | The intended effect is known not to have occurred | Checked retry if authority and limits still permit |
| CONFIRMED_APPLIED | Intended effect is known to have occurred | Record dependent work; do not replay as a retry |

## Retry a known failure

**Retry confirmed nonapplication and handle duplicate results** — Executable control simulation

### Typescript example: retry

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_NOT_APPLIED",
  "actualCost": "0"
})), '10');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "retry",
  "operationId": "o",
  "attemptId": "a2",
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 7
state = reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a2",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 8
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_NOT_APPLIED",
  "actualCost": "0"
})), '10');

// Step 9
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "a2",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "2"
})), '10');
assert.deepEqual(state["spent"], "2");
assert.deepEqual(state["reserved"], "0");
console.log('PASS: retry');

```

### Rust example: retry

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_NOT_APPLIED",
  "actualCost": "0"
}))?, "10")?;

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "retry",
  "operationId": "o",
  "attemptId": "a2",
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 7
    state = reduce(&state, &Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a2",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 8
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_NOT_APPLIED",
  "actualCost": "0"
}))?, "10")?;

    // Step 9
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "a2",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "2"
}))?, "10")?;
    assert_eq!(state.as_value()["spent"], json!("2"));
    assert_eq!(state.as_value()["reserved"], json!("0"));
    println!("PASS: retry");
    Ok(())
}

```

### Python example: retry

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 3
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 4
state = reduce(state, {'type': 'dispatch',
 'attemptId': 'a',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 5
state = reduce(state, {'type': 'settle',
 'attemptId': 'a',
 'effect': 'CONFIRMED_NOT_APPLIED',
 'actualCost': '0'}, '10')

# Step 6
state = reduce(state, {'type': 'retry',
 'operationId': 'o',
 'attemptId': 'a2',
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 7
state = reduce(state, {'type': 'dispatch',
 'attemptId': 'a2',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 8
state = reduce(state, {'type': 'settle',
 'attemptId': 'a',
 'effect': 'CONFIRMED_NOT_APPLIED',
 'actualCost': '0'}, '10')

# Step 9
state = reduce(state, {'type': 'settle',
 'attemptId': 'a2',
 'effect': 'CONFIRMED_APPLIED',
 'actualCost': '2'}, '10')
assert state["spent"] == '2'
assert state["reserved"] == '0'
print('PASS: retry')

```

The example also checks a late duplicate settlement. An identical result is accepted without charging again, while inconsistent results are rejected. Accepted duplicates still produce a new control event; semantic idempotence does not mean the event count stays unchanged.

The retry must reuse the operation identity but have a fresh attempt identity. `maxAttempts` includes the initial attempt. Every attempt reserves exposure; prior recorded costs remain spent. Retrying never renews an expired grant, approval or contract deadline.

## Lost-response procedure

Keep the task under human review when the external outcome is uncertain. Read target-side receipts, repository state or provider request status using the original identity. If evidence proves the action applied, settle that attempt with its actual cost. If evidence proves nonapplication, settle accordingly before requesting a retry. If evidence remains ambiguous, keep UNKNOWN. Human intervention must record evidence and a decision; it must not fabricate certainty.

## Compensation

A compensating action is a new governed operation with its own cost, permission and outcome. It references an earlier applied operation but does not delete history or claim that the original action never happened. Some changes are not reversible. Verify the resulting state of the target rather than assuming successful compensation restored every property.

**Record reconciliation and a separate compensating effect** — Executable control simulation

### Typescript example: compensation

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        }
      },
      {
        "id": "reconcile",
        "kind": "RECONCILIATION",
        "dependsOn": [
          "task"
        ],
        "config": {}
      },
      {
        "id": "undo",
        "kind": "COMPENSATION",
        "dependsOn": [
          "reconcile"
        ],
        "config": {}
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "undo"
        ],
        "config": {}
      }
    ]
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
})), '10');

// Step 7
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
})), '10');

// Step 8
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "operationId": "o"
  }
})), '10');

// Step 9
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "reconcile",
  "result": {
    "operationId": "o"
  }
})), '10');

// Step 10
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "undo",
  "attemptId": "au",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 11
state = reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "au",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "undo"
})), '10');

// Step 12
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "au",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "2"
})), '10');

// Step 13
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "undo",
  "result": {
    "operationId": "undo",
    "originalOperationId": "o"
  }
})), '10');
console.log('PASS: compensation');

```

### Rust example: compensation

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        }
      },
      {
        "id": "reconcile",
        "kind": "RECONCILIATION",
        "dependsOn": [
          "task"
        ],
        "config": {}
      },
      {
        "id": "undo",
        "kind": "COMPENSATION",
        "dependsOn": [
          "reconcile"
        ],
        "config": {}
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "undo"
        ],
        "config": {}
      }
    ]
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
}))?, "10")?;

    // Step 7
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
}))?, "10")?;

    // Step 8
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "operationId": "o"
  }
}))?, "10")?;

    // Step 9
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "reconcile",
  "result": {
    "operationId": "o"
  }
}))?, "10")?;

    // Step 10
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "undo",
  "attemptId": "au",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 11
    state = reduce(&state, &Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "au",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "undo"
}))?, "10")?;

    // Step 12
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "au",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "2"
}))?, "10")?;

    // Step 13
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "undo",
  "result": {
    "operationId": "undo",
    "originalOperationId": "o"
  }
}))?, "10")?;
    println!("PASS: compensation");
    Ok(())
}

```

### Python example: compensation

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'registerGraph',
 'graph': {'id': 'graph',
           'revision': '1',
           'nodes': [{'id': 'task',
                      'kind': 'TASK',
                      'dependsOn': [],
                      'config': {'executionKind': 'FUNCTION'}},
                     {'id': 'reconcile',
                      'kind': 'RECONCILIATION',
                      'dependsOn': ['task'],
                      'config': {}},
                     {'id': 'undo',
                      'kind': 'COMPENSATION',
                      'dependsOn': ['reconcile'],
                      'config': {}},
                     {'id': 'end',
                      'kind': 'END',
                      'dependsOn': ['undo'],
                      'config': {}}]}}, '10')

# Step 3
state = reduce(state, {'type': 'attachGraph', 'runId': 'r', 'graphId': 'graph'}, '10')

# Step 4
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 5
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 6
state = reduce(state, {'type': 'dispatch',
 'attemptId': 'a',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True,
 'nodeId': 'task'}, '10')

# Step 7
state = reduce(state, {'type': 'settle',
 'attemptId': 'a',
 'effect': 'CONFIRMED_APPLIED',
 'actualCost': '3'}, '10')

# Step 8
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'task',
 'result': {'operationId': 'o'}}, '10')

# Step 9
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'reconcile',
 'result': {'operationId': 'o'}}, '10')

# Step 10
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'undo',
 'attemptId': 'au',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 11
state = reduce(state, {'type': 'dispatch',
 'attemptId': 'au',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True,
 'nodeId': 'undo'}, '10')

# Step 12
state = reduce(state, {'type': 'settle',
 'attemptId': 'au',
 'effect': 'CONFIRMED_APPLIED',
 'actualCost': '2'}, '10')

# Step 13
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'undo',
 'result': {'operationId': 'undo', 'originalOperationId': 'o'}}, '10')
print('PASS: compensation')

```

The example exercises current checkpoint semantics. It is not an automatic rollback manager. Your application chooses and authorizes a compensating adapter and must handle uncertainty or failure in that operation too.


---

# Budgets, attempts and stopping scope

The finite-v1 budget is mission-wide, with additional accounting on the grant chain. The **M3 source profile, aiws-limits/1**, adds a shared ledger for nested work-order, run, branch and node limits. Amounts are canonical unsigned decimal strings, representing units chosen by the contract owner. Do not put fractional currency into payloads or accounting fields; choose integer minor units and document the unit scale.

## Admission and reservation

Admission reserves exposure before an effect is allowed. Settlement releases the attempt's reservation and adds its actual cost. UNKNOWN preserves the full reservation. Actual cost above the admitted amount is rejected as `EXPOSURE_EXCEEDED`; this flags a control violation and does not make the real external charge disappear.

**Reject reservations beyond the shared allowance** — Executable control simulation

### Typescript example: budget

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "80",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 4
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o2",
  "attemptId": "a2",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "30",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10'), (error: any) => error.code === 'BUDGET_EXHAUSTED');
assert.deepEqual(state["reserved"], "80");
console.log('PASS: budget');

```

### Rust example: budget

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "80",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 4
    let rejected = Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o2",
  "attemptId": "a2",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "30",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "BUDGET_EXHAUSTED");
    assert_eq!(state.as_value()["reserved"], json!("80"));
    println!("PASS: budget");
    Ok(())
}

```

### Python example: budget

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 3
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '80',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 4
try:
    reduce(state, {'type': 'admit',
     'runId': 'r',
     'operationId': 'o2',
     'attemptId': 'a2',
     'grantId': 'g',
     'subject': 'agent',
     'action': {'capability': 'write',
                'resource': 'doc',
                'payload': {'value': 1},
                'preconditions': {'revision': 1}},
     'amount': '30',
     'authorizationCheckedAt': '10',
     'policy': 'ALLOW',
     'mandatoryChecksOk': True}, '10')
    raise AssertionError('expected BUDGET_EXHAUSTED')
except AiwsError as error:
    assert error.code == 'BUDGET_EXHAUSTED'
assert state["reserved"] == '80'
print('PASS: budget')

```

A rejected command leaves the input state unchanged. SQLite also rolls back the journal and outbox intent together. Concurrent admissions must use the durable store boundary; separate in-memory reducers do not create a shared atomic budget.

## Size allowances from enforceable bounds

A reservation should cover the maximum authorized exposure of that attempt, not an optimistic average. Enforce provider limits, process timeouts and restricted credentials at the target where possible. If an adapter cannot cap its cost, explain that limitation to the operator before claiming a hard bound. Reservations protect admission decisions; they cannot cancel an already submitted external operation by themselves.

## Nested limits: M3 source APIs

The accepted engine model has work orders containing runs and optional child work orders, with limits at work-order, run and branch scopes. Any applicable exhausted limit blocks additional work in its scope. A branch limit holds that branch and dependent tasks. A work-order limit holds all descendants. Independent branches may continue only if their own and all shared allowances permit it.

Use `LimitStore` and `LimitCoordinator` from `@aiws/sdk/limits`, `aiws.limits_store`, or `aiws_sdk::limits_store`. One ledger database owns every scope sharing a parent budget. It reserves cost and active time against all ancestors in one SQLite transaction. Immutable dependencies add admission gates without charging cost twice. The existing mission and handoff stores keep their own contracts and authority checks.

**Reserve ordinary work and protected handoff funds** — M3 source; fixed clock and simulated authenticated host, no external calls

### Typescript example: nested-limits

```typescript
import { readFileSync } from 'node:fs';
import assert from 'node:assert/strict';
import { LimitStore, LimitCoordinator, type LimitConfig } from '@aiws/sdk/limits';
// Demonstration only: a real host verifies the initial approval and each command.
const config = JSON.parse(readFileSync('examples/guide/limits-config.json', 'utf8')) as LimitConfig;
const store = new LimitStore(':memory:', config);
const coordinator = new LimitCoordinator(store, () => ({ allowed: true, actorId: 'demo-agent', actorKind: 'AGENT', policyRef: 'demo-policy', evidenceRef: null, authorizedAtMs: '0' }), () => '0');
for (const [attemptId, purpose, amount] of [['work', 'ORDINARY', '90'], ['summary', 'HANDOFF', '10']]) {
  await coordinator.apply({ requestId: attemptId, expectedRevision: store.snapshot().revision,
    command: { type: 'reserve', attemptId, operationId: attemptId, scopeId: 'run', runId: 'run', purpose, amount, maxActiveMs: '10' } });
}
const report = store.report('0');
assert.equal(report.scopes.find((s: any) => s.scopeId === 'wo').usage.cost, '100');
assert.equal(report.scopes.find((s: any) => s.scopeId === 'wo').usage.handoff, '10');
console.log(report); // Always available, even when paid work is blocked.
store.close();

```

### Rust example: nested-limits

```rust
use aiws_sdk::{limits_store::{LimitStore, LimitCoordinator, LimitAuthorizer}, Result};
use serde_json::{json, Value};
// Demonstration only: a real host verifies the initial approval and each command.
struct DemoHost;
impl LimitAuthorizer for DemoHost {
    fn authorize(&mut self, _: &Value, _: &Value, now: &str) -> Result<Value> {
        Ok(json!({"allowed":true,"actorId":"demo-agent","actorKind":"AGENT","policyRef":"demo-policy","evidenceRef":null,"authorizedAtMs":now}))
    }
}
fn main() -> Result<()> {
    let config: Value = serde_json::from_str(&std::fs::read_to_string("examples/guide/limits-config.json").unwrap()).unwrap();
    let store = LimitStore::open(":memory:", Some(&config))?;
    let mut coordinator = LimitCoordinator { store, authorize: DemoHost, clock: || "0".to_owned() };
    for (attempt, purpose, amount) in [("work", "ORDINARY", "90"), ("summary", "HANDOFF", "10")] {
        let revision = coordinator.store.snapshot()?["revision"].clone();
        coordinator.apply(&json!({"requestId":attempt,"expectedRevision":revision,"command":{"type":"reserve","attemptId":attempt,"operationId":attempt,"scopeId":"run","runId":"run","purpose":purpose,"amount":amount,"maxActiveMs":"10"}}))?;
    }
    let report = coordinator.store.report("0")?;
    let usage = &report["scopes"].as_array().unwrap().iter().find(|s|s["scopeId"]=="wo").unwrap()["usage"];
    assert_eq!(usage["cost"], "100"); assert_eq!(usage["handoff"], "10");
    println!("{report}"); // Always available, even when paid work is blocked.
    Ok(())
}

```

### Python example: nested-limits

```python
import json
from aiws.limits_store import LimitStore, LimitCoordinator
# Demonstration only: a real host verifies the initial approval and each command.
with open('examples/guide/limits-config.json') as file:
    config = json.load(file)
store = LimitStore(':memory:', config)
def authorize(request, state, now):
    return dict(allowed=True, actorId='demo-agent', actorKind='AGENT', policyRef='demo-policy', evidenceRef=None, authorizedAtMs=now)
coordinator = LimitCoordinator(store, authorize, lambda: '0')
for attempt, purpose, amount in [('work', 'ORDINARY', '90'), ('summary', 'HANDOFF', '10')]:
    coordinator.apply(dict(requestId=attempt, expectedRevision=store.snapshot()['revision'], command=dict(type='reserve', attemptId=attempt, operationId=attempt, scopeId='run', runId='run', purpose=purpose, amount=amount, maxActiveMs='10')))
report = store.report('0')
usage = next(s['usage'] for s in report['scopes'] if s['scopeId'] == 'wo')
assert usage['cost'] == '100' and usage['handoff'] == '10'
print(report)  # Always available, even when paid work is blocked.
store.close()

```

The example's `examples/guide/limits-config.json` defines a work order `wo` and its run `run`. The work order has a total cost ceiling of 100, with 10 protected for handoff. The run inherits its parent's bounds. Its fixed clock and simulated authorizer are demonstration inputs; production hosts must authenticate callers, approve the initial config and bind scope/purpose to the trusted workflow definition.

| Limit | Meaning |
|---|---|
| cost | Settled cost plus all unsettled maximum exposure |
| handoff | Protected partition inside cost; only host-authorized HANDOFF work consumes it |
| activeMs | Sum of concurrent execution time, with pre-dispatch maximum reservations |
| elapsedMs | Time since immutable scope start, including pause and downtime |
| attempts | One per successful reservation; release and continuation do not reset it |

Null bounds inherit ancestor restrictions. All bounds apply together. Two concurrent ten-millisecond attempts consume twenty active milliseconds at their parent. UNKNOWN continues accruing active time through downtime. A host-verified `stop` freezes time but keeps financial exposure; settlement releases only unused cost. Human reconciliation is required to resolve UNKNOWN.

## Pair the ledger with existing dispatch gates

Reserve in the ledger before preparing the mission attempt. Record ledger dispatch and pass existing mission/handoff dispatch checks before any external call. These databases do not share an atomic commit: a crash between them conservatively retains exposure and needs reconciliation. Never refund a DISPATCHED attempt merely because the host restarted. Current authority, ownership, artifacts, approvals and handoff inputs still apply. The ledger alone is not authorization to act.

`store.report(now)` identifies blocked scopes, affected dependents, usage and outstanding attempts without an LLM call. Stop and settlement remain available while new work is blocked. The host applies task-specific interruption policy to running effects; the SDK cannot stop an external provider by itself.

## Handoff reserve

The accepted design reserves a small human-approved amount inside the existing allowance for checkpointing and handoff. It is not extra money beyond the limit. Main work cannot consume that protected reserve. The engine produces a basic report from durable state even when no agent allowance remains; an agent-written explanation is optional and must fit the reserve.

No agent may raise permissions, budgets or retry limits, including by generating a new workflow or transferring work to another agent. Human approval is required. SDK contracts are immutable; do not change a persisted contract to bypass a rejected operation. See [migration](https://www.lril.ai/migration/) before moving into a different contract.

The new ledger's `adjust` command requires authenticated HUMAN facts and a host-verified evidence reference for the exact change. It preserves prior spending, attempts, active time and scope start times. Ordinary work and paid cleanup cannot consume the handoff partition. Paid summaries still obey time and attempt bounds; use the deterministic report when those are exhausted. Previously built 0.3.0 binary downloads do not include these source APIs.


---

# Graph construction and node semantics

A graph declares node dependencies and completion obligations. Registration validates its shape; attachment binds one graph to a READY run before any operations begin. The library exposes ready nodes and checks completion proofs, but the application supplies a scheduler and task implementations.

## Structure and readiness

A valid graph has unique node IDs, existing dependencies, no graph cycles, at least one END and an END path for every node. Use bounded LOOP nodes for repetition. Node configuration is closed by kind: extra configuration keys are rejected. An executionKind label does not create an LLM, human inbox, function registry or sandbox.

**Bind execution to a ready graph node** — Executable control simulation

### Typescript example: graph

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 6
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10'), (error: any) => error.code === 'NODE_NOT_READY');

// Step 7
state = reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
})), '10');

// Step 8
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
})), '10');

// Step 9
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "operationId": "o"
  }
})), '10');

// Step 10
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "end",
  "result": {
    "disposition": "COMPLETED"
  }
})), '10');

// Step 11
state = reduce(state, parseCommand(JSON.stringify({
  "type": "transition",
  "runId": "r",
  "to": "COMPLETED"
})), '10');
console.log('PASS: graph');

```

### Rust example: graph

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 6
    let rejected = Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "NODE_NOT_READY");

    // Step 7
    state = reduce(&state, &Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
}))?, "10")?;

    // Step 8
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
}))?, "10")?;

    // Step 9
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "operationId": "o"
  }
}))?, "10")?;

    // Step 10
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "end",
  "result": {
    "disposition": "COMPLETED"
  }
}))?, "10")?;

    // Step 11
    state = reduce(&state, &Command::from_value(json!({
  "type": "transition",
  "runId": "r",
  "to": "COMPLETED"
}))?, "10")?;
    println!("PASS: graph");
    Ok(())
}

```

### Python example: graph

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'registerGraph',
 'graph': {'id': 'graph',
           'revision': '1',
           'nodes': [{'id': 'task',
                      'kind': 'TASK',
                      'dependsOn': [],
                      'config': {'executionKind': 'FUNCTION'}},
                     {'id': 'end',
                      'kind': 'END',
                      'dependsOn': ['task'],
                      'config': {}}]}}, '10')

# Step 3
state = reduce(state, {'type': 'attachGraph', 'runId': 'r', 'graphId': 'graph'}, '10')

# Step 4
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 5
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 6
try:
    reduce(state, {'type': 'dispatch',
     'attemptId': 'a',
     'authorizationCheckedAt': '10',
     'policy': 'ALLOW',
     'mandatoryChecksOk': True}, '10')
    raise AssertionError('expected NODE_NOT_READY')
except AiwsError as error:
    assert error.code == 'NODE_NOT_READY'

# Step 7
state = reduce(state, {'type': 'dispatch',
 'attemptId': 'a',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True,
 'nodeId': 'task'}, '10')

# Step 8
state = reduce(state, {'type': 'settle',
 'attemptId': 'a',
 'effect': 'CONFIRMED_APPLIED',
 'actualCost': '3'}, '10')

# Step 9
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'task',
 'result': {'operationId': 'o'}}, '10')

# Step 10
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'end',
 'result': {'disposition': 'COMPLETED'}}, '10')

# Step 11
state = reduce(state, {'type': 'transition', 'runId': 'r', 'to': 'COMPLETED'}, '10')
print('PASS: graph')

```

A graph-bound dispatch names a ready TASK, LOOP or COMPENSATION node. The SDK checks dependencies and binds the operation to that node before execution. Completing a TASK requires a same-run, same-node operation with a confirmed applied effect. Writing a result object with a convenient string is not enough to bypass that check.

## Node catalog

| Kind | Configuration | Required completion result or condition |
|---|---|---|
| TASK | executionKind: AGENT, FUNCTION, TOOL or HUMAN | operationId of a confirmed applied operation bound to this node |
| DECISION | field, equals, then, else | data containing the field; SDK chooses one successor |
| FORK | empty object | Explicit checkpoint; downstream dependencies become eligible |
| JOIN | mode: ALL or ANY | Appropriate completed predecessors; ANY cannot discard an active operation |
| LOOP | maxIterations | Distinct applied operationId and boolean done |
| WAIT | empty object | waitId of a satisfied same-run wait |
| APPROVAL | empty object | approvalId for a current same-run approval |
| SUBWORKFLOW | empty object | childRunId with verified successful acceptance |
| VERIFICATION | empty object | Current passed assessment revision |
| ACCEPTANCE | empty object | Current accepted assessment revision |
| RECONCILIATION | empty object | operationId whose effect is known |
| COMPENSATION | empty object | Applied operationId and distinct applied originalOperationId |
| END | empty object | disposition: COMPLETED |

An APPROVAL node is a checkpoint, not proof that the next action matches its fingerprint; dispatch still performs material checks. END completion does not itself complete the run. The separate run transition verifies outstanding nodes, attempts and waits. Verification and acceptance can be recorded before an END node when those checkpoints appear inside the graph; the final success predicate still requires terminal execution.

## Input and output contracts

Use inputSchema for operation payloads before dispatch and outputSchema for completeNode results. Both must be within the SDK's JSON Schema subset. For TASK results, an output schema must permit the required operationId field. Validate actual artifact content in your adapter or assessor; a URI-shaped string is not proof that an artifact exists.

**Validate task input before dispatch** — Executable control simulation

### Typescript example: node-schema

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        },
        "inputSchema": {
          "type": "object",
          "required": [
            "requiredField"
          ]
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 6
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
})), '10'), (error: any) => error.code === 'DATA_SCHEMA');
console.log('PASS: node-schema');

```

### Rust example: node-schema

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        },
        "inputSchema": {
          "type": "object",
          "required": [
            "requiredField"
          ]
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 6
    let rejected = Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "DATA_SCHEMA");
    println!("PASS: node-schema");
    Ok(())
}

```

### Python example: node-schema

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'registerGraph',
 'graph': {'id': 'graph',
           'revision': '1',
           'nodes': [{'id': 'task',
                      'kind': 'TASK',
                      'dependsOn': [],
                      'config': {'executionKind': 'FUNCTION'},
                      'inputSchema': {'type': 'object',
                                      'required': ['requiredField']}},
                     {'id': 'end',
                      'kind': 'END',
                      'dependsOn': ['task'],
                      'config': {}}]}}, '10')

# Step 3
state = reduce(state, {'type': 'attachGraph', 'runId': 'r', 'graphId': 'graph'}, '10')

# Step 4
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 5
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 6
try:
    reduce(state, {'type': 'dispatch',
     'attemptId': 'a',
     'authorizationCheckedAt': '10',
     'policy': 'ALLOW',
     'mandatoryChecksOk': True,
     'nodeId': 'task'}, '10')
    raise AssertionError('expected DATA_SCHEMA')
except AiwsError as error:
    assert error.code == 'DATA_SCHEMA'
print('PASS: node-schema')

```

For branches, loops and child workflows continue to [control flow](https://www.lril.ai/control-flow/). For transfer of artifacts and authority between steps read [handoffs](https://www.lril.ai/handoffs/).


---

# Branches, loops and child workflows

The graph is a dependency structure. The application chooses when to execute eligible nodes; it cannot manufacture completion of dependencies to reach a desired branch. Independent ready nodes may be scheduled concurrently, but they still share mission and grant allowances.

## Decisions and joins

A DECISION compares one field using canonical JSON equality, with two declared successor IDs. Both successors must depend on the decision. The unselected successor becomes SKIPPED, and downstream unreachable nodes are skipped according to dependency rules.

**Choose a branch and complete an ANY join** — Executable control simulation

### Typescript example: branching

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "d",
        "kind": "DECISION",
        "dependsOn": [],
        "config": {
          "field": "ok",
          "equals": true,
          "then": "yes",
          "else": "no"
        }
      },
      {
        "id": "yes",
        "kind": "FORK",
        "dependsOn": [
          "d"
        ],
        "config": {}
      },
      {
        "id": "no",
        "kind": "FORK",
        "dependsOn": [
          "d"
        ],
        "config": {}
      },
      {
        "id": "j",
        "kind": "JOIN",
        "dependsOn": [
          "yes",
          "no"
        ],
        "config": {
          "mode": "ANY"
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "j"
        ],
        "config": {}
      }
    ]
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "d",
  "result": {
    "data": {
      "ok": true
    }
  }
})), '10');

// Step 5
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "no",
  "result": {}
})), '10'), (error: any) => error.code === 'NODE_NOT_READY');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "yes",
  "result": {}
})), '10');

// Step 7
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "j",
  "result": {}
})), '10');

// Step 8
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "end",
  "result": {
    "disposition": "COMPLETED"
  }
})), '10');
console.log('PASS: branching');

```

### Rust example: branching

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "d",
        "kind": "DECISION",
        "dependsOn": [],
        "config": {
          "field": "ok",
          "equals": true,
          "then": "yes",
          "else": "no"
        }
      },
      {
        "id": "yes",
        "kind": "FORK",
        "dependsOn": [
          "d"
        ],
        "config": {}
      },
      {
        "id": "no",
        "kind": "FORK",
        "dependsOn": [
          "d"
        ],
        "config": {}
      },
      {
        "id": "j",
        "kind": "JOIN",
        "dependsOn": [
          "yes",
          "no"
        ],
        "config": {
          "mode": "ANY"
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "j"
        ],
        "config": {}
      }
    ]
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "d",
  "result": {
    "data": {
      "ok": true
    }
  }
}))?, "10")?;

    // Step 5
    let rejected = Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "no",
  "result": {}
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "NODE_NOT_READY");

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "yes",
  "result": {}
}))?, "10")?;

    // Step 7
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "j",
  "result": {}
}))?, "10")?;

    // Step 8
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "end",
  "result": {
    "disposition": "COMPLETED"
  }
}))?, "10")?;
    println!("PASS: branching");
    Ok(())
}

```

### Python example: branching

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'registerGraph',
 'graph': {'id': 'graph',
           'revision': '1',
           'nodes': [{'id': 'd',
                      'kind': 'DECISION',
                      'dependsOn': [],
                      'config': {'field': 'ok',
                                 'equals': True,
                                 'then': 'yes',
                                 'else': 'no'}},
                     {'id': 'yes', 'kind': 'FORK', 'dependsOn': ['d'], 'config': {}},
                     {'id': 'no', 'kind': 'FORK', 'dependsOn': ['d'], 'config': {}},
                     {'id': 'j',
                      'kind': 'JOIN',
                      'dependsOn': ['yes', 'no'],
                      'config': {'mode': 'ANY'}},
                     {'id': 'end',
                      'kind': 'END',
                      'dependsOn': ['j'],
                      'config': {}}]}}, '10')

# Step 3
state = reduce(state, {'type': 'attachGraph', 'runId': 'r', 'graphId': 'graph'}, '10')

# Step 4
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'd',
 'result': {'data': {'ok': True}}}, '10')

# Step 5
try:
    reduce(state, {'type': 'completeNode', 'runId': 'r', 'nodeId': 'no', 'result': {}}, '10')
    raise AssertionError('expected NODE_NOT_READY')
except AiwsError as error:
    assert error.code == 'NODE_NOT_READY'

# Step 6
state = reduce(state, {'type': 'completeNode', 'runId': 'r', 'nodeId': 'yes', 'result': {}}, '10')

# Step 7
state = reduce(state, {'type': 'completeNode', 'runId': 'r', 'nodeId': 'j', 'result': {}}, '10')

# Step 8
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'end',
 'result': {'disposition': 'COMPLETED'}}, '10')
print('PASS: branching')

```

An ALL join requires all predecessors completed. An ANY join accepts a completed predecessor but refuses to abandon an active operation on another direct predecessor. It is not a general cancellation or first-result-wins implementation for arbitrary external tasks. Plan explicit cancellation and reconciliation before abandoning work that may have affected an external system.

## Bounded repetition

**Bound loop iterations and preserve the terminal outcome** — Executable control simulation

### Typescript example: loop

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "LOOP",
        "dependsOn": [],
        "config": {
          "maxIterations": "1"
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
})), '10');

// Step 7
state = reduce(state, parseCommand(JSON.stringify({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
})), '10');

// Step 8
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "operationId": "o",
    "done": false
  }
})), '10');
assert.deepEqual(state["runs"]["r"]["state"], "FAILED");
assert.deepEqual(state["runs"]["r"]["nodes"]["task"]["iterations"], "1");
console.log('PASS: loop');

```

### Rust example: loop

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "LOOP",
        "dependsOn": [],
        "config": {
          "maxIterations": "1"
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
}))?, "10")?;

    // Step 7
    state = reduce(&state, &Command::from_value(json!({
  "type": "settle",
  "attemptId": "a",
  "effect": "CONFIRMED_APPLIED",
  "actualCost": "3"
}))?, "10")?;

    // Step 8
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "operationId": "o",
    "done": false
  }
}))?, "10")?;
    assert_eq!(state.as_value()["runs"]["r"]["state"], json!("FAILED"));
    assert_eq!(state.as_value()["runs"]["r"]["nodes"]["task"]["iterations"], json!("1"));
    println!("PASS: loop");
    Ok(())
}

```

### Python example: loop

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'registerGraph',
 'graph': {'id': 'graph',
           'revision': '1',
           'nodes': [{'id': 'task',
                      'kind': 'LOOP',
                      'dependsOn': [],
                      'config': {'maxIterations': '1'}},
                     {'id': 'end',
                      'kind': 'END',
                      'dependsOn': ['task'],
                      'config': {}}]}}, '10')

# Step 3
state = reduce(state, {'type': 'attachGraph', 'runId': 'r', 'graphId': 'graph'}, '10')

# Step 4
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 5
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 6
state = reduce(state, {'type': 'dispatch',
 'attemptId': 'a',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True,
 'nodeId': 'task'}, '10')

# Step 7
state = reduce(state, {'type': 'settle',
 'attemptId': 'a',
 'effect': 'CONFIRMED_APPLIED',
 'actualCost': '3'}, '10')

# Step 8
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'task',
 'result': {'operationId': 'o', 'done': False}}, '10')
assert state["runs"]["r"]["state"] == 'FAILED'
assert state["runs"]["r"]["nodes"]["task"]["iterations"] == '1'
print('PASS: loop')

```

Every successful loop iteration has a distinct operation and increments the iteration counter. `done: false` at the configured bound marks the node and run FAILED. This is a terminal outcome in SDK 0.3.0; it is not the proposed engine's resumable “limit reached” hold. Do not document a PAUSED state for this existing behavior or try to reopen the failed run. Design the engine's intervention model separately, with a reviewed compatibility change.

A corrective loop must preserve the original acceptance criteria unless an authorized plan change replaces them. Re-running easier tests to obtain PASS is not corrective action. Separate infrastructure retries of a single operation from new corrective iterations that change the material work.

## Accepted child work

**Require accepted child work** — Executable control simulation

### Typescript example: subworkflow

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "SUBWORKFLOW",
        "dependsOn": [],
        "config": {}
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "child"
})), '10');

// Step 5
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "childRunId": "child"
  }
})), '10'), (error: any) => error.code === 'CHILD_NOT_ACCEPTED');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "transition",
  "runId": "child",
  "to": "COMPLETED"
})), '10');

// Step 7
state = reduce(state, parseCommand(JSON.stringify({
  "type": "verify",
  "runId": "child",
  "assessor": "reviewer",
  "revision": "v1",
  "results": {
    "review": "PASS"
  },
  "evidence": [
    "sha256:evidence"
  ],
  "rationale": "Review performed"
})), '10');

// Step 8
state = reduce(state, parseCommand(JSON.stringify({
  "type": "accept",
  "runId": "child",
  "authority": "owner",
  "verificationRevision": "v1",
  "decision": "ACCEPTED",
  "rationale": "Accepted deliverable"
})), '10');

// Step 9
state = reduce(state, parseCommand(JSON.stringify({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "childRunId": "child"
  }
})), '10');
console.log('PASS: subworkflow');

```

### Rust example: subworkflow

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "SUBWORKFLOW",
        "dependsOn": [],
        "config": {}
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "child"
}))?, "10")?;

    // Step 5
    let rejected = Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "childRunId": "child"
  }
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "CHILD_NOT_ACCEPTED");

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "transition",
  "runId": "child",
  "to": "COMPLETED"
}))?, "10")?;

    // Step 7
    state = reduce(&state, &Command::from_value(json!({
  "type": "verify",
  "runId": "child",
  "assessor": "reviewer",
  "revision": "v1",
  "results": {
    "review": "PASS"
  },
  "evidence": [
    "sha256:evidence"
  ],
  "rationale": "Review performed"
}))?, "10")?;

    // Step 8
    state = reduce(&state, &Command::from_value(json!({
  "type": "accept",
  "runId": "child",
  "authority": "owner",
  "verificationRevision": "v1",
  "decision": "ACCEPTED",
  "rationale": "Accepted deliverable"
}))?, "10")?;

    // Step 9
    state = reduce(&state, &Command::from_value(json!({
  "type": "completeNode",
  "runId": "r",
  "nodeId": "task",
  "result": {
    "childRunId": "child"
  }
}))?, "10")?;
    println!("PASS: subworkflow");
    Ok(())
}

```

### Python example: subworkflow

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'registerGraph',
 'graph': {'id': 'graph',
           'revision': '1',
           'nodes': [{'id': 'task',
                      'kind': 'SUBWORKFLOW',
                      'dependsOn': [],
                      'config': {}},
                     {'id': 'end',
                      'kind': 'END',
                      'dependsOn': ['task'],
                      'config': {}}]}}, '10')

# Step 3
state = reduce(state, {'type': 'attachGraph', 'runId': 'r', 'graphId': 'graph'}, '10')

# Step 4
state = reduce(state, {'type': 'startRun', 'runId': 'child'}, '10')

# Step 5
try:
    reduce(state, {'type': 'completeNode',
     'runId': 'r',
     'nodeId': 'task',
     'result': {'childRunId': 'child'}}, '10')
    raise AssertionError('expected CHILD_NOT_ACCEPTED')
except AiwsError as error:
    assert error.code == 'CHILD_NOT_ACCEPTED'

# Step 6
state = reduce(state, {'type': 'transition', 'runId': 'child', 'to': 'COMPLETED'}, '10')

# Step 7
state = reduce(state, {'type': 'verify',
 'runId': 'child',
 'assessor': 'reviewer',
 'revision': 'v1',
 'results': {'review': 'PASS'},
 'evidence': ['sha256:evidence'],
 'rationale': 'Review performed'}, '10')

# Step 8
state = reduce(state, {'type': 'accept',
 'runId': 'child',
 'authority': 'owner',
 'verificationRevision': 'v1',
 'decision': 'ACCEPTED',
 'rationale': 'Accepted deliverable'}, '10')

# Step 9
state = reduce(state, {'type': 'completeNode',
 'runId': 'r',
 'nodeId': 'task',
 'result': {'childRunId': 'child'}}, '10')
print('PASS: subworkflow')

```

The example uses another run in the same mission and checks its final assessment before completing the SUBWORKFLOW node. The SDK does not launch the child automatically, maintain a cross-database parent/child dispatcher, or implement global multi-mission budgets. Model child ownership and artifact lineage explicitly in the application.

## Dynamic changes

`activatePlan` updates the recorded plan identity with compare-and-swap protection. It does not replace the graph, change the contract, move already-completed nodes or grant permission to alter running work. Arbitrary graph mutation is not supported in SDK 0.3.0. The [engine design](https://www.lril.ai/engine-design/) considers bounded task addition and approved plan revision as separate capabilities.


---

# Event ingress and duplicate handling

Triggers turn authenticated occurrences into run admission or wait satisfaction. The supported kinds are MANUAL, SCHEDULED, EVENT, RESOURCE_CHANGE, CONDITION, WORKFLOW_LIFECYCLE and EXTERNAL_RESPONSE. These identify semantics; the SDK does not provide their HTTP, filesystem, broker or provider connectors.

## Registration contract

A trigger pins its own identity/revision, source, event type, contract ID, validity, maximum event age, concurrency, rate window and causal depth. It chooses START_RUN or RESUME_WAIT. RESUME_WAIT requires a wait ID. A condition trigger also requires a filter and EDGE or LEVEL mode. A scheduled trigger must include a schedule definition.

Trigger IDs are immutable after registration. There is no updateTrigger or disableTrigger command in this release. The `enabled` field is checked on admission but does not imply a mutable settings API. Suspend the containing mission or enforce ingress restrictions when you need an existing integration to stop admitting work.

## Authenticate before admission

Validate the transport signature or user session outside the SDK. Resolve source identity from that result. Treat event data as untrusted material, including any actor, authorization flag or claimed causal depth. Equality between the event source string and trigger source string is a consistency check, not authentication.

**Preserve event identity and reject changed duplicates** — Executable control simulation

### Typescript example: trigger

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "EVENT",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "START_RUN",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "2",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3"
  }
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
})), '10');

// Step 3
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "other",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": false
    },
    "depth": "0"
  }
})), '10'), (error: any) => error.code === 'EVENT_ID_CONFLICT');
console.log('PASS: trigger');

```

### Rust example: trigger

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "EVENT",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "START_RUN",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "2",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3"
  }
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
}))?, "10")?;

    // Step 3
    let rejected = Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "other",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": false
    },
    "depth": "0"
  }
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "EVENT_ID_CONFLICT");
    println!("PASS: trigger");
    Ok(())
}

```

### Python example: trigger

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'registerTrigger',
 'trigger': {'id': 't',
             'revision': '1',
             'kind': 'EVENT',
             'source': 'trusted',
             'eventType': 'doc.changed',
             'contractId': 'c',
             'action': 'START_RUN',
             'enabled': True,
             'notBefore': '0',
             'expiresAt': '10000',
             'maxAgeMs': '100',
             'maxConcurrent': '2',
             'rateLimit': '10',
             'rateWindowMs': '100',
             'maxDepth': '3'}}, '10')

# Step 2
state = reduce(state, {'type': 'fireTrigger',
 'triggerId': 't',
 'runId': 'r',
 'event': {'id': 'e',
           'source': 'trusted',
           'type': 'doc.changed',
           'occurredAt': '10',
           'data': {'flag': True},
           'depth': '0'}}, '10')

# Step 3
try:
    reduce(state, {'type': 'fireTrigger',
     'triggerId': 't',
     'runId': 'other',
     'event': {'id': 'e',
               'source': 'trusted',
               'type': 'doc.changed',
               'occurredAt': '10',
               'data': {'flag': False},
               'depth': '0'}}, '10')
    raise AssertionError('expected EVENT_ID_CONFLICT')
except AiwsError as error:
    assert error.code == 'EVENT_ID_CONFLICT'
print('PASS: trigger')

```

The duplicate key is trigger ID, revision, source and event ID. A repeated identical event does not create a second run; changed material under the same identity is rejected. An accepted duplicate may still add a ledger event. Preserve the original event ID on transport retries and acknowledge a message only after the intended durable admission succeeds.

## Filters and condition transitions

Filters compare one top-level data field by canonical equality. They do not evaluate SQL, arbitrary code or a general expression language. EDGE mode fires on a false-to-true transition and requires a later false sample to reset. LEVEL mode can fire for each matching event, within bounds.

**Edge-triggered conditions and reset samples** — Executable control simulation

### Typescript example: condition

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "CONDITION",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "START_RUN",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "2",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3",
    "conditionMode": "EDGE",
    "filter": {
      "field": "flag",
      "equals": true
    }
  }
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "ignored",
  "event": {
    "id": "e2",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "ignored2",
  "event": {
    "id": "e3",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": false
    },
    "depth": "0"
  }
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r2",
  "event": {
    "id": "e4",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
})), '10');
assert.deepEqual(state["triggers"]["t"]["count"], "2");
console.log('PASS: condition');

```

### Rust example: condition

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "CONDITION",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "START_RUN",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "2",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3",
    "conditionMode": "EDGE",
    "filter": {
      "field": "flag",
      "equals": true
    }
  }
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "ignored",
  "event": {
    "id": "e2",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "ignored2",
  "event": {
    "id": "e3",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": false
    },
    "depth": "0"
  }
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r2",
  "event": {
    "id": "e4",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
}))?, "10")?;
    assert_eq!(state.as_value()["triggers"]["t"]["count"], json!("2"));
    println!("PASS: condition");
    Ok(())
}

```

### Python example: condition

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'registerTrigger',
 'trigger': {'id': 't',
             'revision': '1',
             'kind': 'CONDITION',
             'source': 'trusted',
             'eventType': 'doc.changed',
             'contractId': 'c',
             'action': 'START_RUN',
             'enabled': True,
             'notBefore': '0',
             'expiresAt': '10000',
             'maxAgeMs': '100',
             'maxConcurrent': '2',
             'rateLimit': '10',
             'rateWindowMs': '100',
             'maxDepth': '3',
             'conditionMode': 'EDGE',
             'filter': {'field': 'flag', 'equals': True}}}, '10')

# Step 2
state = reduce(state, {'type': 'fireTrigger',
 'triggerId': 't',
 'runId': 'r',
 'event': {'id': 'e',
           'source': 'trusted',
           'type': 'doc.changed',
           'occurredAt': '10',
           'data': {'flag': True},
           'depth': '0'}}, '10')

# Step 3
state = reduce(state, {'type': 'fireTrigger',
 'triggerId': 't',
 'runId': 'ignored',
 'event': {'id': 'e2',
           'source': 'trusted',
           'type': 'doc.changed',
           'occurredAt': '10',
           'data': {'flag': True},
           'depth': '0'}}, '10')

# Step 4
state = reduce(state, {'type': 'fireTrigger',
 'triggerId': 't',
 'runId': 'ignored2',
 'event': {'id': 'e3',
           'source': 'trusted',
           'type': 'doc.changed',
           'occurredAt': '10',
           'data': {'flag': False},
           'depth': '0'}}, '10')

# Step 5
state = reduce(state, {'type': 'fireTrigger',
 'triggerId': 't',
 'runId': 'r2',
 'event': {'id': 'e4',
           'source': 'trusted',
           'type': 'doc.changed',
           'occurredAt': '10',
           'data': {'flag': True},
           'depth': '0'}}, '10')
assert state["triggers"]["t"]["count"] == '2'
print('PASS: condition')

```

Filtered occurrences are recorded as receipts. If the source omits a false sample, the engine cannot invent the reset. Persist and authenticate observations according to the source's actual delivery semantics. A rate-limit rejection may need delayed retry; do not alter occurredAt to make an old event appear current.

## Failed deliveries

Your ingress layer must distinguish transient storage/availability failure from permanent schema, source or material conflict. Persist enough failure information to diagnose a rejected message. Coordinator diagnostics are not a complete broker dead-letter service, and preflight errors do not all become SDK diagnostics. Route permanent invalid input to an operator-visible disposition rather than repeatedly retrying it without change.


---

# Schedules, waits and time

The implemented scheduler is a fixed UTC interval calculator with a durable trigger cursor. It is not a cron parser, operating-system service or daemon. A hosting application periodically calls tickTrigger through its trusted coordinator.

## Fixed intervals today

The schedule fields are startMs, intervalMs, catchUp and maxCatchUp. Time is a canonical UTC epoch-millisecond string. Catch-up is SKIP, LATEST or ALL. ALL processes at most the configured batch. The tick commits its nextSlot cursor with admitted run receipts, so rollback does not lose occurrences.

**Advance a scheduled trigger cursor atomically** — Executable control simulation

### Typescript example: schedule

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "SCHEDULED",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "START_RUN",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "10",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3",
    "schedule": {
      "startMs": "0",
      "intervalMs": "10",
      "catchUp": "ALL",
      "maxCatchUp": "2"
    }
  }
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "tickTrigger",
  "triggerId": "t"
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "tickTrigger",
  "triggerId": "t"
})), '10');
assert.deepEqual(state["triggers"]["t"]["count"], "2");
assert.deepEqual(state["triggers"]["t"]["nextSlot"], "2");
console.log('PASS: schedule');

```

### Rust example: schedule

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "SCHEDULED",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "START_RUN",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "10",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3",
    "schedule": {
      "startMs": "0",
      "intervalMs": "10",
      "catchUp": "ALL",
      "maxCatchUp": "2"
    }
  }
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "tickTrigger",
  "triggerId": "t"
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "tickTrigger",
  "triggerId": "t"
}))?, "10")?;
    assert_eq!(state.as_value()["triggers"]["t"]["count"], json!("2"));
    assert_eq!(state.as_value()["triggers"]["t"]["nextSlot"], json!("2"));
    println!("PASS: schedule");
    Ok(())
}

```

### Python example: schedule

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'registerTrigger',
 'trigger': {'id': 't',
             'revision': '1',
             'kind': 'SCHEDULED',
             'source': 'trusted',
             'eventType': 'doc.changed',
             'contractId': 'c',
             'action': 'START_RUN',
             'enabled': True,
             'notBefore': '0',
             'expiresAt': '10000',
             'maxAgeMs': '100',
             'maxConcurrent': '10',
             'rateLimit': '10',
             'rateWindowMs': '100',
             'maxDepth': '3',
             'schedule': {'startMs': '0',
                          'intervalMs': '10',
                          'catchUp': 'ALL',
                          'maxCatchUp': '2'}}}, '10')

# Step 2
state = reduce(state, {'type': 'tickTrigger', 'triggerId': 't'}, '10')

# Step 3
state = reduce(state, {'type': 'tickTrigger', 'triggerId': 't'}, '10')
assert state["triggers"]["t"]["count"] == '2'
assert state["triggers"]["t"]["nextSlot"] == '2'
print('PASS: schedule')

```

SKIP has precise behavior: it only emits the latest slot when evaluated exactly at that slot's timestamp; a late tick skips it. If a real polling service is normally late, choose LATEST or a deliberately bounded ALL policy. Do not describe SKIP as a lateness-tolerant cron scheduler. Retain original occurrence time when delivering late events; maxAgeMs can reject stale slots.

A batch may fail entirely because an occurrence exceeds age, concurrency or rate bounds. Repeating the same invalid tick will not make progress. Align the catch-up batch with configured admission limits, or intervene with an approved replacement trigger policy. New trigger identity means a new deduplication namespace; account for previously executed effects before changing it.

## Time model for the proposed engine

The agreed design distinguishes calendar deadlines, total work-order lifetime, aggregate task execution time, approval expiry, retry delays and schedule occurrences. Calendar deadlines, lifetime and authorization expiry advance during pause or outage. Active execution accounting excludes confirmed stopped intervals; it never assumes an unreachable remote task stopped spending resources. Retries retain previous cost and attempt counts.

A retry delay may elapse during an outage, but eligibility does not override pause, authority or limits. Clock corrections must not make recorded resource use negative. Use a monotonic clock for durations within one process and durable wall-clock instants for external deadlines; reconcile uncertain intervals after restart instead of recreating a monotonic timestamp from another process.

## Calendar schedules planned for the engine

Recommended defaults are an explicit time zone, no overlapping execution for a schedule, and skipping missed occurrences unless catch-up was configured. Calendar schedules also require explicit treatment of missing or repeated daylight-saving times, maximum lateness, catch-up count and age. These remain engine requirements; passing a cron expression to intervalMs is invalid.

Kubernetes documents distinct schedule suspension and already-running job behavior, plus missed-start deadlines and overlap policies. We use those distinctions as design inputs without adopting Kubernetes as the engine. [Primary reference](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/).

For durable human waits and callbacks continue to [waits and approvals](https://www.lril.ai/waits/).


---

# Human waits and external callbacks

A wait records what a run is waiting for, a correlation token, deadline and kind. It is durable state, not a sleeping thread. INPUT, AUTHORIZATION, EXTERNAL, TIMER and RECONCILIATION describe the wait's purpose. The host remains responsible for asking the human, receiving the callback or evaluating a timer.

## Create, satisfy and join

**Correlated waits and an ALL join** — Executable control simulation

### Typescript example: waits

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "wait",
  "runId": "r",
  "waitId": "w",
  "correlation": "corr",
  "deadline": "100",
  "kind": "INPUT"
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "transition",
  "runId": "r",
  "to": "WAITING"
})), '10');

// Step 4
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "resume",
  "waitId": "w",
  "correlation": "wrong"
})), '10'), (error: any) => error.code === 'CORRELATION_MISMATCH');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "resume",
  "waitId": "w",
  "correlation": "corr"
})), '10');

// Step 6
state = reduce(state, parseCommand(JSON.stringify({
  "type": "join",
  "runId": "r",
  "waitIds": [
    "w"
  ],
  "mode": "ALL"
})), '10');
assert.deepEqual(state["runs"]["r"]["state"], "READY");
console.log('PASS: waits');

```

### Rust example: waits

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "wait",
  "runId": "r",
  "waitId": "w",
  "correlation": "corr",
  "deadline": "100",
  "kind": "INPUT"
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "transition",
  "runId": "r",
  "to": "WAITING"
}))?, "10")?;

    // Step 4
    let rejected = Command::from_value(json!({
  "type": "resume",
  "waitId": "w",
  "correlation": "wrong"
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "CORRELATION_MISMATCH");

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "resume",
  "waitId": "w",
  "correlation": "corr"
}))?, "10")?;

    // Step 6
    state = reduce(&state, &Command::from_value(json!({
  "type": "join",
  "runId": "r",
  "waitIds": [
    "w"
  ],
  "mode": "ALL"
}))?, "10")?;
    assert_eq!(state.as_value()["runs"]["r"]["state"], json!("READY"));
    println!("PASS: waits");
    Ok(())
}

```

### Python example: waits

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'wait',
 'runId': 'r',
 'waitId': 'w',
 'correlation': 'corr',
 'deadline': '100',
 'kind': 'INPUT'}, '10')

# Step 3
state = reduce(state, {'type': 'transition', 'runId': 'r', 'to': 'WAITING'}, '10')

# Step 4
try:
    reduce(state, {'type': 'resume', 'waitId': 'w', 'correlation': 'wrong'}, '10')
    raise AssertionError('expected CORRELATION_MISMATCH')
except AiwsError as error:
    assert error.code == 'CORRELATION_MISMATCH'

# Step 5
state = reduce(state, {'type': 'resume', 'waitId': 'w', 'correlation': 'corr'}, '10')

# Step 6
state = reduce(state, {'type': 'join', 'runId': 'r', 'waitIds': ['w'], 'mode': 'ALL'}, '10')
assert state["runs"]["r"]["state"] == 'READY'
print('PASS: waits')

```

Creating a wait does not automatically transition the run to WAITING. The transition requires at least one pending wait and no unsettled attempts. `resume` satisfies a wait with the matching token; `join` moves a WAITING run to READY when the required waits are satisfied. ANY cancels unselected pending waits. These operations do not automatically dispatch subsequent work.

Correlation tokens should be opaque and scoped to the intended decision. Do not expose them as proof of identity. Authenticate the callback principal, check its mission/wait association and verify that its content is appropriate before allowing satisfaction. A replayed or late callback must not apply to a new wait with unrelated material.

## Correlated external-response trigger

**Satisfy one wait with a correlated event** — Executable control simulation

### Typescript example: external-response

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "wait",
  "runId": "r",
  "waitId": "w",
  "correlation": "corr",
  "deadline": "100",
  "kind": "INPUT"
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "EXTERNAL_RESPONSE",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "RESUME_WAIT",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "2",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3",
    "waitId": "w"
  }
})), '10');

// Step 4
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
})), '10'), (error: any) => error.code === 'WAIT_CORRELATION');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0",
    "correlation": "corr"
  }
})), '10');
assert.deepEqual(state["waits"]["w"]["status"], "SATISFIED");
console.log('PASS: external-response');

```

### Rust example: external-response

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "wait",
  "runId": "r",
  "waitId": "w",
  "correlation": "corr",
  "deadline": "100",
  "kind": "INPUT"
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerTrigger",
  "trigger": {
    "id": "t",
    "revision": "1",
    "kind": "EXTERNAL_RESPONSE",
    "source": "trusted",
    "eventType": "doc.changed",
    "contractId": "c",
    "action": "RESUME_WAIT",
    "enabled": true,
    "notBefore": "0",
    "expiresAt": "10000",
    "maxAgeMs": "100",
    "maxConcurrent": "2",
    "rateLimit": "10",
    "rateWindowMs": "100",
    "maxDepth": "3",
    "waitId": "w"
  }
}))?, "10")?;

    // Step 4
    let rejected = Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0"
  }
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "WAIT_CORRELATION");

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "fireTrigger",
  "triggerId": "t",
  "runId": "r",
  "event": {
    "id": "e",
    "source": "trusted",
    "type": "doc.changed",
    "occurredAt": "10",
    "data": {
      "flag": true
    },
    "depth": "0",
    "correlation": "corr"
  }
}))?, "10")?;
    assert_eq!(state.as_value()["waits"]["w"]["status"], json!("SATISFIED"));
    println!("PASS: external-response");
    Ok(())
}

```

### Python example: external-response

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'wait',
 'runId': 'r',
 'waitId': 'w',
 'correlation': 'corr',
 'deadline': '100',
 'kind': 'INPUT'}, '10')

# Step 3
state = reduce(state, {'type': 'registerTrigger',
 'trigger': {'id': 't',
             'revision': '1',
             'kind': 'EXTERNAL_RESPONSE',
             'source': 'trusted',
             'eventType': 'doc.changed',
             'contractId': 'c',
             'action': 'RESUME_WAIT',
             'enabled': True,
             'notBefore': '0',
             'expiresAt': '10000',
             'maxAgeMs': '100',
             'maxConcurrent': '2',
             'rateLimit': '10',
             'rateWindowMs': '100',
             'maxDepth': '3',
             'waitId': 'w'}}, '10')

# Step 4
try:
    reduce(state, {'type': 'fireTrigger',
     'triggerId': 't',
     'runId': 'r',
     'event': {'id': 'e',
               'source': 'trusted',
               'type': 'doc.changed',
               'occurredAt': '10',
               'data': {'flag': True},
               'depth': '0'}}, '10')
    raise AssertionError('expected WAIT_CORRELATION')
except AiwsError as error:
    assert error.code == 'WAIT_CORRELATION'

# Step 5
state = reduce(state, {'type': 'fireTrigger',
 'triggerId': 't',
 'runId': 'r',
 'event': {'id': 'e',
           'source': 'trusted',
           'type': 'doc.changed',
           'occurredAt': '10',
           'data': {'flag': True},
           'depth': '0',
           'correlation': 'corr'}}, '10')
assert state["waits"]["w"]["status"] == 'SATISFIED'
print('PASS: external-response')

```

The trigger must target the same run and a pending, unexpired wait, and the event must carry the matching correlation. Generic resume and trigger-based resume have distinct duplicate paths; application tests should exercise the API actually used by the integration.

## Human decisions

An AUTHORIZATION wait and an action Approval are different records. Satisfying the wait tells the application a response arrived. It does not create the material-bound approval used at effect dispatch. The application must authenticate, interpret and record that approval separately. Similarly, satisfying a verification wait does not itself establish PASS.

If a decision arrives after deadline or after material changes, preserve the receipt in the application audit trail and request an appropriate new decision. Do not bypass WAIT_EXPIRED by rewriting historical timestamps. If the human rejects a plan, use the workflow's authorized rejection/disposition path instead of pretending approval was never requested.

## Pause and wake-up

The SDK can record run PAUSED or mission SUSPENDED states. It cannot freeze an external process, stop provider billing or revoke an already-submitted request automatically. Engine-level pausing will distinguish preventing new work, requesting a checkpoint, and confirmed stopped work. Wake-up and automatic restart policies remain host/engine responsibilities.


---

# Verification, acceptance and evidence

Successful execution, successful verification and accepted delivery are separate outcomes. `assessSuccess` or `assess_success` checks that the run is COMPLETED, the current verification is PASSED, acceptance is ACCEPTED and no operation for the run remains UNKNOWN. It does not perform tests or inspect evidence bytes.

## What to record

A verification command names the assessor, a unique assessment revision, every mandatory criterion, evidence references and a rationale. Each criterion is PASS, FAIL or INCONCLUSIVE. A missing required criterion is rejected. Empty evidence prevents a PASSED assessment. Evidence strings are opaque identifiers in this profile; the application must ensure the evidence exists and supports the result.

A production coding assessment should bind results to the exact repository revision, test suite/configuration and generated artifact digests. Record failures and uncertainty honestly. The engine must not change acceptance criteria merely to obtain a passing result after corrective work.

Acceptance names the accountable authority and the exact verification revision. Recording a new verification resets the current acceptance; it cannot inherit approval for an old assessment. An authorized automatic acceptance policy is possible, but it must be explicit, versioned and independent of the agent's own claim of success.

## Invalidating evidence

**Invalidate verification and dependent acceptance** — Executable control simulation

### Typescript example: invalidation

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "verify",
  "runId": "r",
  "assessor": "reviewer",
  "revision": "v1",
  "results": {
    "review": "PASS"
  },
  "evidence": [
    "sha256:evidence"
  ],
  "rationale": "Review performed"
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "accept",
  "runId": "r",
  "authority": "owner",
  "verificationRevision": "v1",
  "decision": "ACCEPTED",
  "rationale": "Accepted deliverable"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "invalidate",
  "runId": "r",
  "reason": "Evidence changed"
})), '10');
assert.deepEqual(state["assessments"]["r"]["acceptance"], "INVALIDATED");
console.log('PASS: invalidation');

```

### Rust example: invalidation

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "verify",
  "runId": "r",
  "assessor": "reviewer",
  "revision": "v1",
  "results": {
    "review": "PASS"
  },
  "evidence": [
    "sha256:evidence"
  ],
  "rationale": "Review performed"
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "accept",
  "runId": "r",
  "authority": "owner",
  "verificationRevision": "v1",
  "decision": "ACCEPTED",
  "rationale": "Accepted deliverable"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "invalidate",
  "runId": "r",
  "reason": "Evidence changed"
}))?, "10")?;
    assert_eq!(state.as_value()["assessments"]["r"]["acceptance"], json!("INVALIDATED"));
    println!("PASS: invalidation");
    Ok(())
}

```

### Python example: invalidation

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'verify',
 'runId': 'r',
 'assessor': 'reviewer',
 'revision': 'v1',
 'results': {'review': 'PASS'},
 'evidence': ['sha256:evidence'],
 'rationale': 'Review performed'}, '10')

# Step 3
state = reduce(state, {'type': 'accept',
 'runId': 'r',
 'authority': 'owner',
 'verificationRevision': 'v1',
 'decision': 'ACCEPTED',
 'rationale': 'Accepted deliverable'}, '10')

# Step 4
state = reduce(state, {'type': 'invalidate', 'runId': 'r', 'reason': 'Evidence changed'}, '10')
assert state["assessments"]["r"]["acceptance"] == 'INVALIDATED'
print('PASS: invalidation')

```

When evidence becomes invalid, the SDK marks verification INVALIDATED and propagates invalidation to accepted status. It does not rewrite the terminal execution record. A workflow can have completed execution yet no longer qualify as verified successful.

## Planning and corrective work

**Activate a new plan identity using an expected revision** — Executable control simulation

### Typescript example: plan

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "activatePlan",
  "runId": "r",
  "planId": "p2",
  "expectedPlan": "initial"
})), '10');

// Step 3
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "activatePlan",
  "runId": "r",
  "planId": "p3",
  "expectedPlan": "initial"
})), '10'), (error: any) => error.code === 'REVISION_CONFLICT');
console.log('PASS: plan');

```

### Rust example: plan

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "activatePlan",
  "runId": "r",
  "planId": "p2",
  "expectedPlan": "initial"
}))?, "10")?;

    // Step 3
    let rejected = Command::from_value(json!({
  "type": "activatePlan",
  "runId": "r",
  "planId": "p3",
  "expectedPlan": "initial"
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "REVISION_CONFLICT");
    println!("PASS: plan");
    Ok(())
}

```

### Python example: plan

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'activatePlan', 'runId': 'r', 'planId': 'p2', 'expectedPlan': 'initial'}, '10')

# Step 3
try:
    reduce(state, {'type': 'activatePlan', 'runId': 'r', 'planId': 'p3', 'expectedPlan': 'initial'}, '10')
    raise AssertionError('expected REVISION_CONFLICT')
except AiwsError as error:
    assert error.code == 'REVISION_CONFLICT'
print('PASS: plan')

```

Plan activation is a compare-and-swap lineage operation. It does not authenticate a human approval, switch an attached graph or revise contract limits. Apply the user-defined reapproval rules in your trusted application. Keep the original approved plan and the reason for each revision available for assessment and recovery.

A failed verification does not always require repeating every earlier step. The proposed engine's handoff and artifact dependency model will identify the affected descendants, invalidate their stale assumptions and execute authorized corrective work. SDK 0.3.0 does not supply that incremental recomputation engine.


---

# Examples and application recipes

Every recipe below has a complete TypeScript, Rust and Python program in the source archive. The concept pages show synchronized language tabs; the offline handbook includes all three versions. Examples marked trusted simulation exercise SDK semantics without external effects. The coordinator, persistence, handoff and exporter recipes use local adapters or databases and explicitly label their remaining application boundaries.

Run all 75 programs from the source root after installing/building the libraries:

```bash
node scripts/check-guide-examples.mjs
```

Set AIWS_PYTHON or AIWS_CARGO if those executables have nonstandard paths. The runner stops at the first failed assertion and reports the failing language and recipe. Rust programs are compiled before execution.

## Select a recipe

| Recipe ID | Integration lesson | Start here |
| first-run | Execution, verification and acceptance | [Source and examples](https://www.lril.ai/quickstart/) |
| grant-scope | Reject an expanded child grant | [Source and examples](https://www.lril.ai/quickstart/) |
| approval | Bind approval to action material and use count | [Source and examples](https://www.lril.ai/quickstart/) |
| retry | Retry confirmed nonapplication and handle duplicate results | [Source and examples](https://www.lril.ai/quickstart/) |
| budget | Reject reservations beyond the shared allowance | [Source and examples](https://www.lril.ai/quickstart/) |
| waits | Correlated waits and an ALL join | [Source and examples](https://www.lril.ai/quickstart/) |
| trigger | Preserve event identity and reject changed duplicates | [Source and examples](https://www.lril.ai/quickstart/) |
| condition | Edge-triggered conditions and reset samples | [Source and examples](https://www.lril.ai/quickstart/) |
| external-response | Satisfy one wait with a correlated event | [Source and examples](https://www.lril.ai/quickstart/) |
| graph | Bind execution to a ready graph node | [Source and examples](https://www.lril.ai/quickstart/) |
| branching | Choose a branch and complete an ANY join | [Source and examples](https://www.lril.ai/quickstart/) |
| loop | Bound loop iterations and preserve the terminal outcome | [Source and examples](https://www.lril.ai/quickstart/) |
| node-schema | Validate task input before dispatch | [Source and examples](https://www.lril.ai/quickstart/) |
| subworkflow | Require accepted child work | [Source and examples](https://www.lril.ai/quickstart/) |
| compensation | Record reconciliation and a separate compensating effect | [Source and examples](https://www.lril.ai/quickstart/) |
| plan | Activate a new plan identity using an expected revision | [Source and examples](https://www.lril.ai/quickstart/) |
| invalidation | Invalidate verification and dependent acceptance | [Source and examples](https://www.lril.ai/quickstart/) |
| schedule | Advance a scheduled trigger cursor atomically | [Source and examples](https://www.lril.ai/quickstart/) |
| strict-json | Parse untrusted JSON and canonicalize action material | [Source and examples](https://www.lril.ai/quickstart/) |
| storage | Commit, reopen and verify an audit bundle | [Source and examples](https://www.lril.ai/quickstart/) |
| observability | Inspect telemetry without contacting a collector | [Source and examples](https://www.lril.ai/quickstart/) |
| coordinator | Execute an adapter through the trusted coordinator | [Source and examples](https://www.lril.ai/quickstart/) |
| handoff | Build a human-readable handoff from a verified audit snapshot | [Source and examples](https://www.lril.ai/quickstart/) |
| authorization | Deny agent attempts to issue new grants | [Source and examples](https://www.lril.ai/quickstart/) |
| exporter | Retry a telemetry delivery without re-running workflow work | [Source and examples](https://www.lril.ai/quickstart/) |

## Assemble a coding workflow

Start with the coordinator tutorial, then add authenticated approval, a grant constrained to the intended checkout and an adapter with a bounded operation. Attach a graph before admitting graph-bound work. Record the actual test command, source revision, artifact digest and observed validation result. Require the configured acceptance decision for the current deliverable. For failed validation, use a bounded corrective path and invalidate assessments whose subject changed.

Use the handoff recipe to produce a deterministic operator report from one audit snapshot. The proposed engine protocol extends that report with durable artifact delivery and acknowledgements; the current recipe does not provide that transport.

## Adapt to document review

Treat the reviewed document version as immutable material. Bind approval and acceptance to that version, record comments as artifacts, and require explicit disposition for unresolved review findings. A revised document can invalidate previous acceptance. An external reviewer response should enter through an authenticated correlated wait/trigger path, not through a forged actor field.

## Adapt to scheduled work

Use fixed-interval trigger ticks only when their UTC cadence and catch-up behavior match the requirement. A calendar schedule with timezone/DST rules needs a host scheduler. Preserve event identity across retries, limit catch-up and concurrency, and hold external outcomes that cannot be determined after an interruption. See scheduling for why SKIP is sensitive to exact slot timing.

## Adapt to a human approval queue

Create a durable authorization wait and route its identity to an authenticated application inbox. A notification is an application effect with its own delivery behavior. When the response arrives, verify the principal, correlation, expiry and scope; recording wait satisfaction alone is not a new approval for arbitrary actions. Reevaluate authorization when execution resumes.

Each adaptation needs application integration tests for its real external system. The runnable recipes establish local SDK behavior and API usage; they do not substitute for those tests.


---

# SQLite persistence and audit replay

Each SqliteStore holds one immutable mission contract and its event journal. SQLite is configured with WAL and FULL synchronization. State-changing commands are checked and appended in a transaction; their redacted trace outbox record commits with them. An effect adapter is called only after a successful coordinator dispatch commit.

## Reopen the same mission

**Commit, reopen and verify an audit bundle** — Executable example

### Typescript example: storage

```typescript
import assert from 'node:assert/strict';
import {mkdtempSync,readFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {parseContract,canonical} from '@aiws/sdk';
import {SqliteStore,importAudit} from '@aiws/sdk/sqlite';
const contract = parseContract(readFileSync('examples/guide/contract.json','utf8'));
const path = join(mkdtempSync(join(tmpdir(),'aiws-docs-')),'mission.db');
let store = new SqliteStore(path,contract);
store.apply({type:'startRun',runId:'r'},'10','0'); // trusted local simulation
store.close();
store = new SqliteStore(path);
try {
  assert.equal(store.snapshot().revision,'1');
  assert.equal(canonical(importAudit(store.exportAudit())),canonical(store.snapshot()));
  console.log(path);
} finally { store.close(); }

```

### Rust example: storage

```rust
use aiws_sdk::*;
use aiws_sdk::sqlite::{SqliteStore,import_audit};
use serde_json::json;
fn main() -> std::result::Result<(),Box<dyn std::error::Error>> {
    let contract = Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?;
    let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_nanos();
    let path = std::env::temp_dir().join(format!("aiws-docs-{nonce}.db"));
    let name = path.to_str().ok_or("non-UTF8 path")?;
    let mut store = SqliteStore::open(name,Some(&contract))?;
    store.apply(&Command::from_value(json!({"type":"startRun","runId":"r"}))?,"10",Some("0"))?;
    drop(store);
    let store = SqliteStore::open(name,None)?;
    assert_eq!(store.snapshot()?,import_audit(&store.export_audit()?)?);
    println!("{}",name);
    Ok(())
}

```

### Python example: storage

```python
from pathlib import Path
from tempfile import mkdtemp
from aiws import parse_contract
from aiws.sqlite import SqliteStore, import_audit
contract = parse_contract(Path('examples/guide/contract.json').read_text())
path = str(Path(mkdtemp(prefix='aiws-docs-'))/'mission.db')
with SqliteStore(path,contract) as store:
    store.apply({'type':'startRun','runId':'r'},'10','0') # trusted local simulation
with SqliteStore(path) as store:
    assert store.snapshot()['revision'] == '1'
    assert import_audit(store.export_audit()) == store.snapshot()
print(path)

```

Opening an existing store without a contract reads the stored contract. Supplying a conflicting contract is rejected. Do not delete and recreate the database when recovering a failed request; doing so loses the identities, reservations and evidence needed to determine what remains safe.

The example intentionally creates a temporary directory and keeps it for inspection. In an application use a stable, access-controlled data directory. For Docker, mount persistent storage for the database and artifact directory. Removing a container without retained state is not a recoverable workflow pause.

## Expected revisions

Every accepted command advances the mission revision, even some semantically idempotent duplicate commands. Use an expected revision to detect concurrent changes. The coordinator checks a snapshot and compares its revision again during commit. On REVISION_CONFLICT reload and reevaluate the command; do not blindly replace the expected revision with the latest value.

SQLite provides a local transaction boundary. It is not the proposed distributed work-order service, and this release does not implement remote failover. Avoid sharing a live database file through ad hoc network synchronization. Choose a deployment architecture with one authoritative mission store and tested file-locking behavior.

## Replay and integrity

Audit export includes contract, events, diagnostics, profile and a digest. Import verifies the bundle and replays accepted events without calling adapters. Altering an observation envelope while recomputing only the bundle checksum still fails semantic replay. Diagnostic export has a separate stream and is not re-executed as workflow commands.

A hash chain detects accidental or partial alteration. A party able to replace an entire database can recompute hashes; use external integrity roots or signatures when the deployment requires authenticated evidence. Do not call hash verification proof of who authorized an event.

## Backups and large histories

Use a consistent SQLite backup operation or a coordinated stopped-store backup, preserving artifacts separately. Copying only a live main database file can omit uncheckpointed WAL state. SQLite documents an online backup API for coherent snapshots. [SQLite backup reference](https://www.sqlite.org/backup.html).

Replay is linear in retained history on each store transaction. Large mission histories require measurement before production use. The strict text parser limits one input to 1 MiB and depth 64; a large audit bundle may need an application transport that validates and streams bounded events to audit playback. The SDK does not provide automatic checkpoint compaction, pagination or a multi-gigabyte import API.


---

# Recover interrupted execution safely

Recovery restores recorded workflow state and determines the next safe action. It does not resurrect a process instruction pointer or guarantee an external action can be repeated. The default safety boundary for the proposed engine is human intervention when an outcome is uncertain.

## Identify the interrupted phase

| Last durable phase | What is known | Recovery action |
|---|---|---|
| No admission | No reserved attempt exists in this mission | Reevaluate current request and authorization |
| PREPARED | Allowance reserved, no SDK dispatch committed | Check ownership and whether any alternate executor could have acted |
| DISPATCHED or UNKNOWN | An external effect may have occurred | Preserve reservation; obtain evidence and human resolution |
| SUCCEEDED or FAILED attempt | A known settlement is committed | Do not perform that attempt again |
| Node completed | Its checked result is durable | Validate artifact availability before transferring to a consumer |
| Run terminal | Execution disposition is final | Do not reopen; use a reviewed new linked run if needed |

## Native recovery APIs

| TypeScript | Rust | Python |
|---|---|---|
| `await coordinator.recover(runId, segmentId)` returns state and unresolved IDs | `coordinator.recover(run_id, segment_id)` returns a snapshot and unresolved IDs | `coordinator.recover(run_id, segment_id)` returns state and unresolved IDs |
| `await coordinator.reconcile(attemptId, adapter)` | `coordinator.reconcile(attempt_id, adapter)` | `coordinator.reconcile(attempt_id, adapter)` |

Use a new segment ID per continuation. Recovery increments the run epoch and fences old PREPARED attempts from dispatch. It does not kill an old worker or fence arbitrary target-side effects. In particular, it does not turn an uncertain external action into confirmed nonapplication. Do not use “recover” as an automatic retry loop.

The current settlement path does not carry a complete distributed worker fencing token. If your integration has multiple independently active workers, add its own ownership and external effect controls; this SDK release is not a distributed exactly-once engine.

## Restore context from durable state

Read the contract, current plan, graph results, attempts, waits and assessment. Resolve artifact locations and verify hashes before rebuilding an agent's context. Never treat a conversational summary as the entire mission state. If a sandbox was ephemeral, detect missing files and reconstruct from durable artifacts or require intervention.

The [handoff recipe](https://www.lril.ai/handoffs/) constructs a summary from an imported audit snapshot. A production handoff must also establish artifact availability and authorized consumer ownership. The report is a useful view of state, not a command to resume effects.

## Failure handling rules

Do not report an adapter exception as confirmed failure unless you have evidence the target did not act. Do not set actual cost to zero just because the response was lost. If accounting exceeds the reservation, preserve the incident and external receipt instead of editing the database until validation passes. If an approval expired during downtime, request an appropriate new decision before further effect dispatch.

After a power outage, automatic resume policy belongs to the engine configuration. It may permit automatic, human-authorized or rule-based continuation, but no mode overrides uncertainty, current authority, stop conditions or exhausted limits. Recovery on another machine is outside the current engine scope.


---

# Handoffs between steps and recovery boundaries

A handoff is the durable transfer of a specific result and responsibility from one workflow step to the next. It serves normal execution, observability, human review and recovery after interruption. It must identify what is being transferred, the evidence supporting it and what the consumer is authorized to do.

**Status:** M2 implements the opt-in `handoff-v1` source profile natively in TypeScript, Rust and Python. Use the source checkout for these APIs. The previously distributed **0.3.0 binary packages remain the finite-v1 baseline** and do not contain these additions. A background engine, nested work-order accounting and protected summary allowance are separate milestones.

## Runtime contract and identity

The [handoff contract](https://www.lril.ai/downloads/AIWS-Handoff-Contract-v1.md) defines `aiws-handoff/1`. Work order and mission identify the same assignment under the accepted WO-01–WO-08 decision. A run is an execution within that assignment. This profile supports same-run deliveries and RUN/BRANCH holds; WORK_ORDER hold resolution and nested resource accounting remain M3 and fail with `SCOPE_UNSUPPORTED`.

The [wire schema](https://www.lril.ai/downloads/aiws-handoff-v1.schema.json) validates shapes. The 40 original M1 scenarios are a requirements inventory. The executable M2 corpus separately compares committed events, state hashes, rejections, replay and transaction rollback across the native implementations. See the source `docs/M2-IMPLEMENTATION.md` for measured coverage and limits.

A receipt validates delivery only. Admission reserves work and fixes its inputs; dispatch authorizes an external attempt. Keep these three decisions separate. Releasing a hold preserves spending, attempts, approvals and outstanding exposure. A bounded loop produces a BLOCKED checkpoint at exhaustion; the old finite-v1 journal retains terminal FAILED semantics.

## Public API map

| Purpose | TypeScript | Rust | Python |
|---|---|---|---|
| Import | `@aiws/sdk/handoff` | `aiws_sdk::handoff_store` and `handoff` | `aiws.handoff_store` and `aiws.handoff` |
| Validate immutable record | `new HandoffManifest(value)` | `HandoffManifest::from_value(value)` | `HandoffManifest(value)` |
| Open/create journal | `new HandoffStore(path, contract, config)` | `HandoffStore::open(path, Some(&contract), Some(&config))` | `HandoffStore(path, contract, config)` |
| Reopen existing journal | `new HandoffStore(path)` | `HandoffStore::open(path, None, None)` | `HandoffStore(path)` |
| Application boundary | `HandoffCoordinator` | `HandoffCoordinator` | `HandoffCoordinator` |
| Durable bytes | `FileArtifacts.put/read` | `FileArtifacts::put/read` through `ArtifactProvider` | `FileArtifacts.put/read` |
| Apply/claim | `await host.apply/claim(request)` | `host.apply/claim(&request)` | `host.apply/claim(request)` |
| Committed report | `handoffReport(state)` | `handoff::report(&state)` | `report(state)` |
| Bounded metric counts | `handoffMetrics(state)` | `handoff::metrics(&state)` | `metrics(state)` |
| Notifications | `pendingEvents/acknowledgeEvent` | `pending_events/acknowledge_event` | `pending_events/acknowledge_event` |

`HandoffDelivery`, `HandoffConsumption`, `HandoffHold`, `HandoffSummary`, `HandoffCommand` and `HandoffInvalidation` use the same checked-record pattern. Construction validates shape, not caller authority or current state. Python records are imported from `aiws.handoff_records`; Rust from `aiws_sdk::handoff_records`.

**Validate an immutable handoff record** — Unreleased M2 source; shape validation only

### Typescript example: handoff-records

```typescript
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {HandoffManifest,sha} from '@aiws/sdk/handoff';
// Run from the repository root. This is a wire-shape example, not an admission.
const fixtures=JSON.parse(readFileSync('spec/handoff-v1/fixtures.json','utf8'));
const record=new HandoffManifest(fixtures.valid.find((x:any)=>x.id==='manifest').value);
const digest=sha(record.asValue());
const detached=record.asValue();detached.result.changed=true;
assert.equal(sha(record.asValue()),digest); // Caller mutations cannot change the record.
assert.throws(()=>new HandoffManifest({...record.asValue(),unknownField:true}));
console.log('PASS: handoff-records');

```

### Rust example: handoff-records

```rust
use aiws_sdk::{Result,handoff::sha,handoff_records::HandoffManifest};
use serde_json::{Value,json};
fn main()->Result<()> {
    // Run from the repository root. This validates shape, not execution authority.
    let fixtures:Value=serde_json::from_str(&std::fs::read_to_string("spec/handoff-v1/fixtures.json").unwrap()).unwrap();
    let value=fixtures["valid"].as_array().unwrap().iter().find(|x|x["id"]=="manifest").unwrap()["value"].clone();
    let record=HandoffManifest::from_value(value)?;
    let digest=sha(record.as_value())?;
    let mut detached=record.as_value().clone();detached["result"]["changed"]=json!(true);
    assert_eq!(sha(record.as_value())?,digest);
    detached["unknownField"]=json!(true);
    assert!(HandoffManifest::from_value(detached).is_err());
    println!("PASS: handoff-records");Ok(())
}

```

### Python example: handoff-records

```python
import json
from pathlib import Path
from aiws import AiwsError
from aiws.handoff import sha
from aiws.handoff_records import HandoffManifest
# Run from the repository root. Shape validation does not authorize work.
fixtures=json.loads(Path('spec/handoff-v1/fixtures.json').read_text())
record=HandoffManifest(next(x['value'] for x in fixtures['valid'] if x['id']=='manifest'))
digest=sha(record.as_value())
detached=record.as_value();detached['result']['changed']=True
assert sha(record.as_value())==digest
try:
    HandoffManifest({**record.as_value(),'unknownField':True})
except AiwsError:
    pass
else:
    raise AssertionError('Unknown field accepted')
print('PASS: handoff-records')

```

## Create durable material before announcing it

Use a dedicated host-owned artifact directory. `put` writes a temporary file, syncs it, publishes an exclusive digest-addressed file and syncs the directory. Existing content must match its digest. Unsupported filesystem durability operations fail. Do not let an untrusted agent write directly into the store directory.

The host is responsible for retention and backups. `retentionOwner` and `retainUntilMs` record responsibility and an access boundary; they do not schedule garbage collection. Retain bytes while deliveries, recovery or evidence obligations require them. Credentials and expiring signed URLs belong in a private resolver, never immutable locators.

A custom artifact provider returns bytes. The coordinator independently checks SHA-256, size and any referenced schema, with no automatic remote schema retrieval. Verification is repeated at receipt, admission and dispatch. Use the coordinator's verified byte copies for external execution. A digest check does not make artifact text safe instructions.

**Publish and verify immutable local artifacts** — Executable local filesystem example; directory sync required

### Typescript example: handoff-artifacts

```typescript
import assert from 'node:assert/strict';
import {mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {FileArtifacts} from '@aiws/sdk/handoff';
const root=mkdtempSync(join(tmpdir(),'aiws-material-example-'));
try {
  const provider=new FileArtifacts(root);
  // Retention is an owner's obligation; this adapter does not schedule deletion.
  const ref=provider.put(Buffer.from('reviewed output'),'output:1','text/plain','owner:operator','4102444800000');
  assert.equal(Buffer.from(provider.read(ref)).toString(),'reviewed output');
  assert.throws(()=>provider.read({...ref,sizeBytes:'1'}),e=>(e as any).code==='ARTIFACT_MISMATCH');
  // Attach ref to a manifest only after put succeeds; keep this directory durable.
  console.log('PASS: handoff-artifacts');
} finally {rmSync(root,{recursive:true,force:true});} // Demo cleanup only.

```

### Rust example: handoff-artifacts

```rust
use aiws_sdk::{Result,handoff_store::{ArtifactProvider,FileArtifacts}};
use serde_json::json;
use std::time::{SystemTime,UNIX_EPOCH};
fn main()->Result<()> {
    let root=std::env::temp_dir().join(format!("aiws-material-example-{}-{}",std::process::id(),SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()));
    let provider=FileArtifacts::new(root.to_str().unwrap())?;
    // The retention owner keeps these bytes available; there is no deletion scheduler.
    let reference=provider.put(b"reviewed output","output:1","text/plain","owner:operator","4102444800000",None)?;
    assert_eq!(provider.read(&reference)?,b"reviewed output");
    let mut changed=reference.clone();changed["sizeBytes"]=json!("1");
    assert_eq!(provider.read(&changed).unwrap_err().code,"ARTIFACT_MISMATCH");
    std::fs::remove_dir_all(root).unwrap(); // Demo cleanup, never a production retention policy.
    println!("PASS: handoff-artifacts");Ok(())
}

```

### Python example: handoff-artifacts

```python
from tempfile import TemporaryDirectory
from aiws import AiwsError
from aiws.handoff_store import FileArtifacts
with TemporaryDirectory(prefix='aiws-material-example-') as root:
    provider=FileArtifacts(root)
    # The retention owner must keep bytes available; no automatic deletion is scheduled.
    ref=provider.put(b'reviewed output','output:1','text/plain','owner:operator','4102444800000')
    assert provider.read(ref)==b'reviewed output'
    try:
        provider.read({**ref,'sizeBytes':'1'})
    except AiwsError as error:
        assert error.code=='ARTIFACT_MISMATCH'
    else:
        raise AssertionError('Changed reference accepted')
    # Attach ref to a manifest after put succeeds. Production storage outlives the process.
print('PASS: handoff-artifacts')

```

## Drive the coordinator from a trusted host

The equivalent examples below create a journal, commit a producer boundary, claim and acknowledge its delivery, admit its consumer, complete the run, reopen the journal and acknowledge safe notifications. They simulate adapter outcomes; they do not run a coding agent or authenticate a real person.

**Run the handoff, receipt and recovery protocol** — Executable local control simulation; fixed identity, clock and adapter outcomes

### Typescript example: handoff-coordinator

```typescript
import assert from 'node:assert/strict';
import {readFileSync,mkdtempSync,rmSync} from 'node:fs';
import {join} from 'node:path';
import {tmpdir} from 'node:os';
import {HandoffStore,HandoffCoordinator,FileArtifacts,activation,handoffReport,sha,type HandoffFacts} from '@aiws/sdk/handoff';
// The first acceptance case supplies an approved graph and command templates.
// It makes NO external effect calls: its settle command is a simulated adapter result.
const demo=JSON.parse(readFileSync('spec/handoff-v1/runtime-cases.json','utf8')).cases[0];
const root=mkdtempSync(join(tmpdir(),'aiws-handoff-example-')),path=join(root,'run.db');
let store=new HandoffStore(path,demo.contract,demo.config);
try {
  const artifacts=new FileArtifacts(join(root,'material'));
  artifacts.put(Buffer.from('hello'),'artifact:1','text/plain','human:operator','10000');
  // Demo host authority, never request-body assertions. Replace this fixed principal
  // and allow policy with authenticated middleware and the approved workflow policy.
  const authority:HandoffFacts={
    allowed:true,actorId:'human:operator',actorKind:'HUMAN',
    policy:{id:'policy',revision:'1'},authorizedAtMs:'10',
    materials:{},materialErrors:{},
    admissions:structuredClone(demo.steps.at(-1).facts.admissions),
    clearEvidence:['material:1','evidence:1'],restartAllowed:true
  };
  const coordinator=new HandoffCoordinator(store,()=>structuredClone(authority),()=>'10',artifacts);
  const tokens=new Map<string,string>();
  for(const template of demo.steps){
    const s=store.snapshot(),request=structuredClone(template.request);
    request.expectedRevision=s.revision;request.ownerEpoch=s.core.runs.r?.epoch??'0';
    const command=request.command,body=command.body;
    if(body){
      command.expectedRevision=request.expectedRevision;command.ownerEpoch=request.ownerEpoch;
      if(body.type==='commitBoundary'){
        const m=body.manifest;
        m.basis={revision:s.revision,eventHash:s.lastHash};
        m.controls.accounting.revision=s.revision;
        m.inputs=Object.values(s.consumptions).find((c:any)=>c.nodeId===m.producer.nodeId)!.inputs;
        m.producer.activationId=activation(s,'r',m.producer.nodeId);
      }
      if(body.type==='acknowledgeDelivery'){
        const d=s.deliveries[body.deliveryId];
        body.claimToken=tokens.get(body.deliveryId);body.generation=d.generation;body.handoff=d.handoff;
      }
      if(body.type==='admitConsumer'){
        const c=body.consumption;c.ownerEpoch=request.ownerEpoch;c.revision=(BigInt(s.revision)+1n).toString();
        c.inputs=c.inputs.map((i:any)=>({deliveryId:i.deliveryId,handoff:s.deliveries[i.deliveryId].handoff}));
      }
    }
    if(body?.type==='claimDelivery'){
      const response=await coordinator.claim(request);tokens.set(body.deliveryId,response.claimToken);
    }else await coordinator.apply(request);
  }
  const before=sha(store.snapshot());store.close();store=new HandoffStore(path);
  assert.equal(sha(store.snapshot()),before);assert.equal(handoffReport(store.snapshot()).spent,'3');
  // Export only safe projections. A real exporter acknowledges after durable delivery.
  for(const event of store.pendingEvents()){
    assert(event.eventId);store.acknowledgeEvent(event.sequence);
  }
  assert.equal(store.pendingEvents().length,0);
  console.log('PASS: handoff-coordinator');
}finally{store.close();rmSync(root,{recursive:true,force:true});}

```

### Rust example: handoff-coordinator

```rust
use aiws_sdk::{Contract,Result,handoff::{activation,report,sha,HandoffState},handoff_store::{HandoffStore,HandoffCoordinator,FileArtifacts}};
use serde_json::{Value,json};
use std::{collections::BTreeMap,time::{SystemTime,UNIX_EPOCH}};
fn main()->Result<()> {
    // Local control simulation: template settle outcomes do not perform real effects.
    let corpus:Value=serde_json::from_str(&std::fs::read_to_string("spec/handoff-v1/runtime-cases.json").unwrap()).unwrap();
    let demo=&corpus["cases"][0];let contract=Contract::from_value(demo["contract"].clone())?;
    let root=std::env::temp_dir().join(format!("aiws-handoff-example-{}-{}",std::process::id(),SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()));
    std::fs::create_dir(&root).unwrap();let path=root.join("run.db");
    let store=HandoffStore::open(path.to_str().unwrap(),Some(&contract),Some(&demo["config"]))?;
    let artifacts=FileArtifacts::new(root.join("material").to_str().unwrap())?;
    artifacts.put(b"hello","artifact:1","text/plain","human:operator","10000",None)?;
    // Fixed demo host principal. Production must authenticate and evaluate policy;
    // a client must never supply this authority object.
    let authority=json!({"allowed":true,"actorId":"human:operator","actorKind":"HUMAN",
        "policy":{"id":"policy","revision":"1"},"authorizedAtMs":"10","materials":{},"materialErrors":{},
        "admissions":demo["steps"].as_array().unwrap().last().unwrap()["facts"]["admissions"],
        "clearEvidence":["material:1","evidence:1"],"restartAllowed":true});
    let authorize=move|_:&Value,_:&HandoffState,_:&str|Ok(authority.clone());
    let mut coordinator=HandoffCoordinator::new(store,authorize,||"10".into(),artifacts);
    let mut tokens:BTreeMap<String,String>=BTreeMap::new();
    for template in demo["steps"].as_array().unwrap(){
        let snapshot=coordinator.store.snapshot()?;let s=snapshot.as_value();let mut request=template["request"].clone();
        let epoch=s["core"]["runs"]["r"].get("epoch").cloned().unwrap_or(json!("0"));
        request["expectedRevision"]=s["revision"].clone();request["ownerEpoch"]=epoch.clone();
        if request["command"].get("body").is_some(){
            request["command"]["expectedRevision"]=s["revision"].clone();request["command"]["ownerEpoch"]=epoch.clone();
            let b=&mut request["command"]["body"];
            if b["type"]=="commitBoundary"{
                let m=&mut b["manifest"];m["basis"]=json!({"revision":s["revision"],"eventHash":s["lastHash"]});
                m["controls"]["accounting"]["revision"]=s["revision"].clone();
                m["inputs"]=s["consumptions"].as_object().unwrap().values().find(|c|c["nodeId"]==m["producer"]["nodeId"]).unwrap()["inputs"].clone();
                m["producer"]["activationId"]=json!(activation(&snapshot,"r",m["producer"]["nodeId"].as_str().unwrap())?);
            }
            if b["type"]=="acknowledgeDelivery"{
                let id=b["deliveryId"].as_str().unwrap().to_owned();let d=&s["deliveries"][&id];
                b["claimToken"]=json!(tokens[&id]);b["generation"]=d["generation"].clone();b["handoff"]=d["handoff"].clone();
            }
            if b["type"]=="admitConsumer"{
                let c=&mut b["consumption"];c["ownerEpoch"]=epoch;c["revision"]=json!((s["revision"].as_str().unwrap().parse::<u64>().unwrap()+1).to_string());
                for input in c["inputs"].as_array_mut().unwrap(){input["handoff"]=s["deliveries"][input["deliveryId"].as_str().unwrap()]["handoff"].clone();}
            }
        }
        if request["command"]["body"]["type"]=="claimDelivery"{
            let (_,token)=coordinator.claim(&request)?;tokens.insert(request["command"]["body"]["deliveryId"].as_str().unwrap().to_owned(),token);
        }else{coordinator.apply(&request)?;}
    }
    let before=sha(coordinator.store.snapshot()?.as_value())?;drop(coordinator);
    let mut reopened=HandoffStore::open(path.to_str().unwrap(),None,None)?;
    assert_eq!(sha(reopened.snapshot()?.as_value())?,before);assert_eq!(report(&reopened.snapshot()?)?["spent"],"3");
    // A real exporter acknowledges after durable acceptance by its receiver.
    for event in reopened.pending_events(100)?{assert!(event["eventId"].is_string());reopened.acknowledge_event(event["sequence"].as_str().unwrap())?;}
    assert!(reopened.pending_events(100)?.is_empty());drop(reopened);std::fs::remove_dir_all(root).unwrap();
    println!("PASS: handoff-coordinator");Ok(())
}

```

### Python example: handoff-coordinator

```python
import copy
import json
from pathlib import Path
from tempfile import TemporaryDirectory
from aiws.handoff import activation, report, sha
from aiws.handoff_store import HandoffStore, HandoffCoordinator, FileArtifacts
# Approved command templates for a LOCAL CONTROL SIMULATION. No external effects
# run here: settle uses the case's explicitly simulated adapter outcome.
demo=json.loads(Path('spec/handoff-v1/runtime-cases.json').read_text())['cases'][0]
with TemporaryDirectory(prefix='aiws-handoff-example-') as root:
    path=str(Path(root)/'run.db')
    store=HandoffStore(path,demo['contract'],demo['config'])
    try:
        artifacts=FileArtifacts(Path(root)/'material')
        artifacts.put(b'hello','artifact:1','text/plain','human:operator','10000')
        # This is a fixed demo host identity. Production obtains identity from
        # authenticated middleware and evaluates the current approved policy.
        authority={'allowed':True,'actorId':'human:operator','actorKind':'HUMAN',
                   'policy':{'id':'policy','revision':'1'},'authorizedAtMs':'10',
                   'materials':{},'materialErrors':{},
                   'admissions':copy.deepcopy(demo['steps'][-1]['facts']['admissions']),
                   'clearEvidence':['material:1','evidence:1'],'restartAllowed':True}
        coordinator=HandoffCoordinator(store,lambda *_:copy.deepcopy(authority),lambda:'10',artifacts)
        tokens={}
        for template in demo['steps']:
            s=store.snapshot();request=copy.deepcopy(template['request'])
            request.update(expectedRevision=s['revision'],ownerEpoch=s['core']['runs'].get('r',{}).get('epoch','0'))
            command=request['command'];body=command.get('body')
            if body:
                command.update(expectedRevision=request['expectedRevision'],ownerEpoch=request['ownerEpoch'])
                if body['type']=='commitBoundary':
                    m=body['manifest'];m['basis']={'revision':s['revision'],'eventHash':s['lastHash']}
                    m['controls']['accounting']['revision']=s['revision']
                    m['inputs']=next(c['inputs'] for c in s['consumptions'].values() if c['nodeId']==m['producer']['nodeId'])
                    m['producer']['activationId']=activation(s,'r',m['producer']['nodeId'])
                if body['type']=='acknowledgeDelivery':
                    d=s['deliveries'][body['deliveryId']]
                    body.update(claimToken=tokens[body['deliveryId']],generation=d['generation'],handoff=d['handoff'])
                if body['type']=='admitConsumer':
                    c=body['consumption'];c.update(ownerEpoch=request['ownerEpoch'],revision=str(int(s['revision'])+1))
                    c['inputs']=[{'deliveryId':i['deliveryId'],'handoff':s['deliveries'][i['deliveryId']]['handoff']} for i in c['inputs']]
            if body and body['type']=='claimDelivery':
                response=coordinator.claim(request);tokens[body['deliveryId']]=response['claimToken']
            else:
                coordinator.apply(request)
        before=sha(store.snapshot());store.close();store=HandoffStore(path)
        assert sha(store.snapshot())==before and report(store.snapshot())['spent']=='3'
        # A real exporter acknowledges only after its receiver durably accepts an event.
        for event in store.pending_events():
            assert event['eventId'];store.acknowledge_event(event['sequence'])
        assert store.pending_events()==[]
        print('PASS: handoff-coordinator')
    finally:
        store.close()

```

The host authorizer receives the request, current snapshot and time. It must authenticate the actor, evaluate the exact command under current policy, resolve approved admission references, and validate evidence IDs against its trusted evidence store. Never copy client-supplied `allowed`, actor kind, evidence or admission facts into this callback. The callback's authorization timestamp is preserved through artifact reads and checked again at commit. A concurrent state change yields a revision conflict; refresh and reauthorize the request.

Pure reducers and direct `store.apply` calls are trusted/offline APIs. Do not expose them to clients. The coordinator overrides caller material assertions with independent verification. On a failed transaction, no new material handle is available. Serialize calls to a coordinator when using its last-result material accessor; each worker should have its own coordinator and request lifecycle.

**Duplicate dispatch rule:** TypeScript and Python responses contain `committed`; Rust responses contain an optional `event`. A duplicate returns its original logical result with `committed: false` or `event: None`. Never perform an external action from that response. A newly committed dispatch still needs an idempotent adapter and outcome reconciliation if its response is lost. The SDK cannot make an arbitrary external API exactly-once.

## Checkpoints, holds and intervention

- `commitBoundary` commits a checkpoint, immutable manifest, recipient intents and any required hold together. A rollback leaves none of those records visible. Artifacts written before a failed commit can remain orphaned; the host later cleans them up under retention policy.
- A non-success BLOCKED checkpoint must carry an appropriately scoped hold and every outstanding producing attempt. FAILED and CANCELED checkpoints terminate the run in this profile while retaining exposure and accepting truthful late settlements. They never resume terminal execution.
- `claimDelivery` establishes actor-bound, leased receipt ownership. `acknowledgeDelivery` requires the current claim, matching material and trusted evidence. Lost responses do not duplicate admission. Recovery fences stale epochs and restores unacknowledged claims to pending.
- `admitConsumer` atomically fixes the input set and reservation. An ALL join may accept a skipped input only when explicitly listed in immutable `optionalInputs`. An ANY join cannot abandon another predecessor's reserved or active work.
- `placeHold` derives RUN or branch-descendant membership from the pinned graph. `resolveHold(RECHECK)` releases only a demonstrably cleared hold. An invalidated input stays blocked pending a separately governed replacement; there is no force-clear command.
- On material failure the attempted command is rejected without a partial commit. The host records an owned artifact hold using `placeHold` and presents repair options. Repairing the identical bytes permits revalidation; changed bytes require a new result and invalidation/replanning.
- `recoverOwnership` applies AUTO, HUMAN or RULE policy and preserves admitted work. Uncertain effects require human intervention. It does not restart an agent process, restore a sandbox or schedule tasks.

A hold is an independent gate. The current run lifecycle field is not a complete scheduler aggregate: hosts should combine it with ready-node and hold projections. Task-aware draining, automated timer services and lifecycle aggregation belong to the engine.

## Reports, summaries and telemetry

Reports and metric counts derive from committed state without a model call. Counts cover delivery states, open holds, blocked nodes, manifests and consumptions. Notifications carry stable event IDs and mission/run/request correlation; the durable outbox survives restart. Acknowledge after the receiving system has stored an event. Delivery is at least once, so receivers deduplicate by event ID.

No material bytes, locator credentials, summary text or claim token is automatically included in notification projections. Host-assigned identifiers must themselves be non-secret. The full audit export is a privileged record containing command data; never treat it as a redacted telemetry payload. Hosts own export scheduling, rejection counters, alert routing and telemetry retention.

`recordSummary` records an optional explanation with exact manifest basis and authenticated author. HUMAN attribution requires a human actor. MODEL summaries require a matching, successfully settled governed operation in the same run. The host supplies generation through an already admitted and bounded task; the SDK never starts a paid model call automatically. If the generator is unavailable or no approved budget remains, omit the summary and use the deterministic report. This does not implement M3's protected summary allowance.

## Compatibility and recovery procedure

1. Open the original journal with its original profile. Legacy and handoff stores reject each other's databases; do not relabel or import historical terminal loops into the new decoder.
2. Reopen the handoff journal and inspect holds, pending deliveries, reservations and unresolved operations. Replay verifies every event against the native reducer.
3. Apply the authorized ownership recovery policy. Investigate uncertain external outcomes before considering another dispatch.
4. Restore required immutable bytes and recheck current authorization. Resolve only holds whose actual blockers are clear.
5. Resume through the coordinator with the original operation/consumption identities. Do not reconstruct control state from summary prose.

SQLite uses FULL synchronous WAL transactions. A failed callback before commit is tested for rollback; close/reopen and lost-response paths are tested separately. Hardware power-loss durability still depends on the filesystem and device honoring synchronization. History compaction and months-long operational qualification remain later work.

## Design rationale and remaining engine work

The following design rationale includes requirements for later engine milestones. The API boundary above states what M2 implements.

### Two layers of a handoff

The authoritative layer is machine-readable: immutable IDs, revisions, artifact digests, outcomes, outstanding effects, policy references and delivery state. The explanatory layer is a human/agent-readable summary derived from that record. The summary may explain intent and caveats, but cannot grant authority, change criteria or replace the underlying evidence.

The engine should create a minimal handoff automatically at every committed step boundary. An agent can supplement it within the reserved allowance. During an abrupt power failure no final agent call is possible; the next process must rebuild the handoff from already-committed state.

## Proposed handoff record

| Field family | Purpose and required interpretation |
|---|---|
| Identity | handoff ID, schema version and immutable revision |
| Ownership | work-order, workflow/run, producer node, logical operation and producing attempt IDs |
| Basis | committed source event/sequence, plan/graph revision and authoritative state reference |
| Delivery | recipient node or recipient set, delivery identity, claim generation and acknowledgement |
| Artifacts | immutable reference, digest, media type, schema/version, size and declared retention |
| Outcome | result disposition, completed criteria, pending obligations and uncertain effects |
| Controls | current policy/approval references and recorded resource accounting; these are references, not new grants |
| Context | approved intent, relevant decisions, assumptions and limitations; no hidden reasoning requirement |
| Resume | next eligible task/checkpoint and blockers, with required revalidation |
| Summary | human-readable narrative, its author/generator and the exact basis revision |

Artifact content must be durably available before the handoff announces it. For a local file adapter, that can require durable file writes and directory metadata before committing a reference. For an object store, verify successful upload and its immutable reference before the ledger transaction. Orphan uploads can be garbage-collected; a committed handoff must never point at an upload merely planned in memory.

## Proposed commit and delivery sequence

1. The producer completes or reconciles its effect and validates its output contract.
2. It durably stores output artifacts and obtains immutable references and digests.
3. The engine atomically records node completion, the handoff manifest and downstream delivery intents.
4. An eligible consumer claims its delivery under current ownership, authenticates its authority and verifies artifact/schema/plan compatibility.
5. The consumer durably acknowledges receipt of that exact handoff revision before starting governed work.
6. Its eventual completion creates another handoff linked to the consumed revision.

An acknowledgement means “received and validated,” not “task succeeded.” Separate consumer execution and acceptance records establish those outcomes. Use one delivery identity per consumer at fan-out; a join records exactly which predecessor handoffs it consumed. Retries preserve logical identity while changing attempt identity.

No consumer should execute untrusted instructions hidden inside artifact text as engine policy. Context is input material. The authorizer and approved workflow decide which capabilities may be used.

## Failure cases and required behavior

| Failure | Proposed behavior |
|---|---|
| Artifact stored, ledger commit fails | No delivery becomes visible; retain or collect orphan artifact |
| Node completion commits, process crashes | Delivery intent survives in the same transaction |
| Delivery is duplicated | Same consumer delivery ID prevents duplicate admission |
| Consumer crashes after claim | Resolve ownership before another consumer proceeds |
| Receipt acknowledged, execution crashes | Resume from consumer state; acknowledgement alone is not proof of an effect |
| Artifact missing or digest mismatches | Block consumer, retain evidence and require repair/review |
| Plan changes before consumption | Reject stale assumptions or explicitly authorize a compatible transition |
| Producer result is invalidated | Mark dependent handoffs stale; pause affected downstream work and assess already-applied effects |
| Branch skipped | Record the disposition so joins do not wait forever for a nonexistent success handoff |
| External effect remains uncertain | Issue a blocked handoff for human review; never represent it as ready work |
| Summary generation fails | Preserve the machine record and provide an engine-generated basic report |

## Current SDK recipe

**Build a human-readable handoff from a verified audit snapshot** — Executable application recipe

### Typescript example: handoff

```typescript
import {readFileSync} from 'node:fs';
import {parseContract} from '@aiws/sdk';
import {SqliteStore,importAudit} from '@aiws/sdk/sqlite';
const store=new SqliteStore(':memory:',parseContract(readFileSync('examples/guide/contract.json','utf8')));
try {
  store.apply({type:'startRun',runId:'r'},'10');
  const audit=store.exportAudit();
  const state=importAudit(audit); // derive the report from this exact exported state
  const handoff={
    format:'example-handoff/1', // application format, not an AIWS wire command
    missionId:state.contract.missionId,
    revision:state.revision,
    auditDigest:JSON.parse(audit).digest,
    spent:state.spent,reserved:state.reserved,
    unresolved:Object.values(state.attempts).filter(a=>['DISPATCHED','UNKNOWN'].includes(a.status)).map(a=>a.id),
    nextDecision:'Inspect current authority and pending work before resuming.'
  };
  console.log(JSON.stringify(handoff,null,2));
} finally { store.close(); }

```

### Rust example: handoff

```rust
use aiws_sdk::*;
use aiws_sdk::sqlite::{SqliteStore,import_audit};
use serde_json::{json,Value};
fn main() -> std::result::Result<(),Box<dyn std::error::Error>> {
    let contract=Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?;
    let mut store=SqliteStore::open(":memory:",Some(&contract))?;
    store.apply(&Command::from_value(json!({"type":"startRun","runId":"r"}))?,"10",None)?;
    let audit=store.export_audit()?;
    let state=import_audit(&audit)?;
    let s=state.as_value();
    let bundle:Value=serde_json::from_str(&audit)?;
    let unresolved:Vec<_>=s["attempts"].as_object().unwrap().values()
        .filter(|a|a["status"]=="DISPATCHED"||a["status"]=="UNKNOWN")
        .map(|a|a["id"].clone()).collect();
    println!("{}",json!({"format":"example-handoff/1","missionId":s["contract"]["missionId"],
        "revision":s["revision"],"auditDigest":bundle["digest"],"spent":s["spent"],"reserved":s["reserved"],
        "unresolved":unresolved,"nextDecision":"Inspect current authority and pending work before resuming."}));
    Ok(())
}

```

### Python example: handoff

```python
import json
from pathlib import Path
from aiws import parse_contract
from aiws.sqlite import SqliteStore,import_audit
contract=parse_contract(Path('examples/guide/contract.json').read_text())
with SqliteStore(':memory:',contract) as store:
    store.apply(dict(type='startRun',runId='r'),'10')
    audit=store.export_audit()
    state=import_audit(audit)
    handoff=dict(format='example-handoff/1',missionId=state['contract']['missionId'],
        revision=state['revision'],auditDigest=json.loads(audit)['digest'],
        spent=state['spent'],reserved=state['reserved'],
        unresolved=[a['id'] for a in state['attempts'].values() if a['status'] in ('DISPATCHED','UNKNOWN')],
        nextDecision='Inspect current authority and pending work before resuming.')
    print(json.dumps(handoff,indent=2))

```

The example imports one audit bundle and derives the report from that exact snapshot, avoiding a separate live-state read that could describe a later revision. The resulting example-handoff/1 object is application data, not an AIWS Command. It intentionally prints a report rather than pretending to implement the proposed queue or artifact protocol.

Today you can include additional application artifact references in a completeNode result and validate their shape with outputSchema. Verification of artifact bytes, cross-step input mapping, transactional handoff delivery and acknowledgements must be supplied by the application or future engine. Never add handoff fields to closed wire commands or contracts; they will be rejected.

## Budget and intervention rules

The user-approved design protects a small reserve within the existing allowance for checkpointing and handoff. Main work cannot borrow it automatically. Agent-written summaries use that reserve and stop when it is exhausted. The engine-generated record does not depend on another paid model call.

A branch limit holds that branch and dependent work; a work-order limit holds all descendants. Human intervention is required for uncertain outcomes and any change to agent permission or resource limits. Resuming later preserves spent resources, outstanding reservations and approval history.


---

# Observability, metrics and OTLP delivery

SDK 0.3.0 / proposed standard edition 0.4. Implemented in TypeScript, Rust and Python.

An interactive [observability export pipeline](https://www.lril.ai/diagrams/#observability-export-pipeline) diagram traces the path from an accepted command to the OTLP collector.

## Events and trust

Every accepted command carries an `observation` envelope: deterministic event identity, committed sequence, timestamp, decision class, actor, policy/definition revision, causal/correlation references, trace/span IDs and applicable mission/contract/run/node/operation/attempt/trigger/wait references. Coordinator authorization supplies context and overrides caller attribution. Core/store calls without context explicitly use `local:unattributed` and `unattested`; they do not establish authenticated provenance. The immutable contract ID is the definition revision for this finite profile.

`context` requires `actor`, `policyRevision`, `decisionClass` (`DETERMINISTIC`, `HUMAN`, `MODEL`, `DELEGATED`). Optional fields are `correlationId`, `causationId`, a nonzero 32-lowercase-hex `traceId` and nonzero 16-lowercase-hex `parentSpanId`. An application must validate inbound trust before passing remote context. Trace context does not grant authority. Default traces group a mission, with causal event parents; these are control-event spans with identical start/end times. Actual model, tool and node execution spans require adapter instrumentation. No hidden reasoning or automatic provider instrumentation is collected.

Coordinator authorization denial, authorizer failure and reducer/storage failures after authorization produce a separate hash-chained diagnostic stream where storage remains available. These diagnostics do not advance mission revision. Argument validation and preflight misuse are not a universal security audit stream. SDK state hashes detect corruption and changed event envelopes; a digest does not authenticate an author who can rewrite the entire database. Protect access to the ledger and backups.

## API mapping

| Capability | TypeScript (`@aiws/sdk/observability`) | Rust (`aiws_sdk::observability`) | Python (`aiws.observability`) |
|---|---|---|---|
| Event projection | `tracePayload(event)` | `trace_payload(&event)` | `trace_payload(event)` |
| Metrics and alerts | `operationalSummary(state, now, thresholds?, exportState?)` | `operational_summary(&state, now, thresholds, export_state)` | `operational_summary(state, now, thresholds=None, export_state=None)` |
| OTLP metrics | `metricPayload(summary, now)` | `metric_payload(&summary, now)` | `metric_payload(summary, now)` |
| HTTP export | `OtlpHttpExporter(endpoint, headers?, timeoutMs?)` | `OtlpHttpExporter::new(endpoint, headers, timeout_ms)` | `OtlpHttpExporter(endpoint, headers=None, timeout_ms=10000)` |
| Queue metrics | `store.enqueueMetrics(now)` | `store.enqueue_metrics(now)` | `store.enqueue_metrics(now)` |
| Flush queue | `await store.flushTelemetry(exporter, now, limit?)` | `store.flush_telemetry(&mut exporter, now, limit)` | `store.flush_telemetry(exporter, now, limit=100)` |
| Queue health | `store.exportState()` | `store.export_state()` | `store.export_state()` |
| Diagnostic records | `store.diagnostics()` | `store.diagnostics()` | `store.diagnostics()` |

## Measurements and alerts

Metric names have the `aiws.` prefix. All ten count/accounting metrics are snapshot gauges, including those ending `_total`; do not sum repeated snapshots as new events. They describe one mission database. A multi-mission collector must add an appropriate bounded deployment/resource grouping or aggregate before merging series. The default exporter has only `service.name=aiws-sdk`; it does not identify a multi-tenant service instance automatically.

| Names | Meaning |
|---|---|
| `commands_total`, `attempts_total`, `retries_total`, `failed_runs_total` | Accepted commands, distinct attempts, retry commands, currently failed runs |
| `spent_units`, `reserved_units` | Exact application-defined accounting units; no implicit currency or token estimate |
| `unknown_effects`, `pending_waits` | Current unresolved operations and pending waits |
| `export_pending`, `export_failed` | Queue health supplied by the store; direct summary defaults to zero if omitted |
| `run_ms` | First run-related event to explicit terminal transition |
| `attempt_ms` | Dispatch to first settlement that resolves the effect |
| `approval_wait_ms` | AUTHORIZATION wait to correlated satisfaction |

Durations are cumulative histograms over completed observations retained in the mission ledger, in milliseconds, with one bucket. They include elapsed waiting where it falls between the declared boundaries. They do not provide latency percentiles; send individual activity measurements through adapter instrumentation if that is needed. Telemetry integer range is signed 64-bit; ledger quantities remain exact decimal strings. Wall-clock regression or an unsupported telemetry range is an error, never a reason to round accounting values.

Default thresholds (canonical decimal strings): `stalledAfterMs=300000`, `unknownAfterMs=60000`, `approvalAfterMs=3600000`, `budgetPercent=90`, `exportBacklog=1000`. Supply all five fields to override. Alerts: `WORKFLOW_STALLED`, `EFFECT_UNRESOLVED`, `APPROVAL_OVERDUE`, `BUDGET_NEAR_LIMIT`, `BUDGET_EXHAUSTED`, `EXPORT_UNHEALTHY`. Comparison is inclusive at the threshold. Stall detection means no recent run control event, so long-running legitimate actions need suitable thresholds. Re-evaluation returns the current conditions; the application owns notification deduplication, scheduling, recipients, acknowledgement and escalation. Alerts never execute remediation by themselves.

## Export and privacy

The SDK supplies OTLP/HTTP JSON at `/v1/traces` and `/v1/metrics`, with optional headers and a 10-second request timeout. HTTPS certificate validation is enabled; redirects are disabled. No collector is contacted until the application explicitly creates and invokes an exporter. Endpoint and header configuration are trusted operator inputs.

Default trace attributes allow only event type/identity, sequence, decision class and SHA-256 pseudonyms for policy and object IDs. Raw actor identity, correlation tokens, payloads, prompts, evidence contents and credentials are excluded. Hashes are linkable and guessable for low-entropy identifiers; this is not anonymization. The local ledger and audit bundle retain richer records and require their own access and retention policy.

A redacted trace export intent is committed in the same SQLite transaction as the control event. Metric snapshots are explicitly enqueued. Delivery occurs outside that transaction. Batches default to 100 records, maximum 1000; each row is leased for 30 seconds and acknowledgement is fenced by its attempt counter. Network errors and HTTP 429/502/503/504 retry with exponential backoff (1–60 seconds) and numeric Retry-After seconds, capped at 24 hours. HTTP-date Retry-After is not supported. At most eight delivery attempts are allowed. Partial acceptance is retained as FAILED without retrying the whole batch. Other permanent/malformed responses are FAILED. Export exceptions do not repeat workflow effects.

Delivery is at least once, not exactly once. A lost acknowledgement or expired lease may duplicate telemetry; use event IDs to deduplicate downstream. Batches are sent sequentially, so a slow batch can outlive a later row's lease and duplicate it. The outbox is disk-backed without a built-in capacity limit; monitor disk and queue growth. Failure to commit required ledger/outbox data blocks the command before effect dispatch. `purgeSentTelemetry` / `purge_sent_telemetry` deletes only SENT rows. FAILED rows remain in `telemetry_outbox` for operator diagnosis; automatic requeue is deliberately absent because partial acceptance is ambiguous. No background worker, notification service, hosted collector, dashboard or metrics backend is bundled.

## Inspect events, metrics and redaction

**Inspect telemetry without contacting a collector** — Executable example

### Typescript example: observability

```typescript
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {parseContract} from '@aiws/sdk';
import {SqliteStore} from '@aiws/sdk/sqlite';
import {operationalSummary,tracePayload,metricPayload} from '@aiws/sdk/observability';
const store = new SqliteStore(':memory:',parseContract(readFileSync('examples/guide/contract.json','utf8')));
try {
  const state = store.apply({type:'startRun',runId:'r',context:{actor:'private:actor',policyRevision:'policy:1',decisionClass:'HUMAN'}},'10');
  const trace = tracePayload(state.events[0]);
  assert.equal(JSON.stringify(trace).includes('private:actor'),false);
  const summary = operationalSummary(state,'4000000',undefined,store.exportState());
  console.log(JSON.stringify({summary,metrics:metricPayload(summary,'4000000')}));
} finally { store.close(); }

```

### Rust example: observability

```rust
use aiws_sdk::*;
use aiws_sdk::{sqlite::SqliteStore,observability::*};
use serde_json::json;
fn main() -> std::result::Result<(),Box<dyn std::error::Error>> {
    let contract=Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?;
    let mut store=SqliteStore::open(":memory:",Some(&contract))?;
    let state=store.apply(&Command::from_value(json!({"type":"startRun","runId":"r","context":{"actor":"private:actor","policyRevision":"policy:1","decisionClass":"HUMAN"}}))?,"10",None)?;
    assert!(!trace_payload(&state.as_value()["events"][0])?.to_string().contains("private:actor"));
    let summary=operational_summary(&state,"4000000",None,Some(&store.export_state()?))?;
    println!("{}",metric_payload(&summary,"4000000")?);
    Ok(())
}

```

### Python example: observability

```python
from pathlib import Path
import json
from aiws import parse_contract
from aiws.sqlite import SqliteStore
from aiws.observability import operational_summary,trace_payload,metric_payload
contract=parse_contract(Path('examples/guide/contract.json').read_text())
with SqliteStore(':memory:',contract) as store:
    state=store.apply(dict(type='startRun',runId='r',context=dict(actor='private:actor',policyRevision='policy:1',decisionClass='HUMAN')),'10')
    assert 'private:actor' not in json.dumps(trace_payload(state['events'][0]))
    summary=operational_summary(state,'4000000',export_state=store.export_state())
    print(json.dumps(metric_payload(summary,'4000000')))

```

This executable example checks the trace projection's privacy boundary and inspects a mission summary without network access. Activity spans around model, tool and file operations belong inside your adapter instrumentation. Link those spans to the applicable control event and avoid recording secret inputs in attributes.

## Exercise exporter recovery

**Retry a telemetry delivery without re-running workflow work** — Executable application recipe

### Typescript example: exporter

```typescript
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {parseContract} from '@aiws/sdk';
import {SqliteStore} from '@aiws/sdk/sqlite';
import {classifyResponse} from '@aiws/sdk/observability';
const store=new SqliteStore(':memory:',parseContract(readFileSync('examples/guide/contract.json','utf8')));
try {
  store.apply({type:'startRun',runId:'r'},'10');
  let calls=0;
  const exporter={send:async()=>classifyResponse(++calls===1?503:200,'{}')};
  assert.equal((await store.flushTelemetry(exporter,'10'))[0].status,'PENDING');
  assert.equal((await store.flushTelemetry(exporter,'1010'))[0].status,'SENT');
  assert.equal(store.snapshot().revision,'1');
  store.purgeSentTelemetry();
} finally {store.close();}

```

### Rust example: exporter

```rust
use aiws_sdk::*;
use aiws_sdk::{sqlite::SqliteStore,observability::*};
use serde_json::{json,Value};
struct FakeCollector{calls:usize}
impl TelemetryExporter for FakeCollector{
    fn send(&mut self,_:&str,_:&Value)->Value{
        self.calls+=1;
        classify_response(if self.calls==1{503}else{200},"{}",None)
    }
}
fn main()->std::result::Result<(),Box<dyn std::error::Error>>{
    let contract=Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?;
    let mut store=SqliteStore::open(":memory:",Some(&contract))?;
    store.apply(&Command::from_value(json!({"type":"startRun","runId":"r"}))?,"10",None)?;
    let mut exporter=FakeCollector{calls:0};
    assert_eq!(store.flush_telemetry(&mut exporter,"10",100)?[0]["status"],"PENDING");
    assert_eq!(store.flush_telemetry(&mut exporter,"1010",100)?[0]["status"],"SENT");
    assert_eq!(store.snapshot()?.as_value()["revision"],"1");
    store.purge_sent_telemetry()?;
    Ok(())
}

```

### Python example: exporter

```python
from pathlib import Path
from aiws import parse_contract
from aiws.sqlite import SqliteStore
from aiws.observability import classify_response
class FakeCollector:
    def __init__(self): self.calls=0
    def send(self,signal,payload):
        self.calls+=1
        return classify_response(503 if self.calls==1 else 200,'{}')
contract=parse_contract(Path('examples/guide/contract.json').read_text())
with SqliteStore(':memory:',contract) as store:
    store.apply(dict(type='startRun',runId='r'),'10')
    exporter=FakeCollector()
    assert store.flush_telemetry(exporter,'10')[0]['status']=='PENDING'
    assert store.flush_telemetry(exporter,'1010')[0]['status']=='SENT'
    assert store.snapshot()['revision']=='1'
    store.purge_sent_telemetry()

```

The local collector stub returns a transient 503 followed by success. The example asserts that delivery retries while the mission revision remains unchanged: exporting telemetry must not repeat a workflow effect. Replace the stub with OtlpHttpExporter using a trusted endpoint, headers from runtime secret configuration and an explicit timeout. The API mapping above lists the native constructors. Never copy credentials into the workflow definition or the source example.

Run flushing on an application-owned bounded schedule. Export with a fresh timestamp, inspect exportState/export_state, and retain FAILED records for operator investigation. Purge acknowledged telemetry according to retention policy only after confirming the downstream requirements.

Collector behavior is checked using local HTTP test servers, including restart after 503 and permanent partial-success handling. The tests do not certify every OTLP collector, vendor backend, deployed alert service or the complete OpenTelemetry API.

References: [OTLP 1.11.0](https://opentelemetry.io/docs/specs/otlp/), [Collector security](https://opentelemetry.io/docs/security/config-best-practices/), reviewed 2026-09-08.


---

# Secure application integration

The SDK enforces finite record semantics. Your host establishes who is acting, which policy applies, what credentials an adapter receives and whether an external report is authentic. Keep that boundary explicit when agents generate applications.

## Authenticate before constructing authority

**Deny agent attempts to issue new grants** — Executable application recipe

### Typescript example: authorization

```typescript
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {parseContract,type Command} from '@aiws/sdk';
import {SqliteStore} from '@aiws/sdk/sqlite';
import {Coordinator,type Authorizer} from '@aiws/sdk/runtime';
// Demo session comes from the application, never command.context or an event payload.
const session={actor:'agent:builder',isHuman:false};
const humanOnly=new Set(['grant','revokeGrant','approve','withdrawApproval']);
const allowedCommands=new Set(['startRun']); // deliberately narrow teaching policy
const authorize:Authorizer=async(command)=>{
  const allowed=allowedCommands.has(command.type)&&(!humanOnly.has(command.type)||session.isHuman);
  return {allowed,policy:allowed?'ALLOW':'DENY',mandatoryChecksOk:true,
    context:{actor:session.actor,policyRevision:'policy:example:1',decisionClass:'DETERMINISTIC'}};
};
const contract=parseContract(readFileSync('examples/guide/contract.json','utf8'));
const store=new SqliteStore(':memory:',contract);
const coordinator=new Coordinator(store,authorize,()=> '10');
try {
  await coordinator.apply({type:'startRun',runId:'r'});
  const grant:Command={type:'grant',grant:{id:'g',subject:session.actor,profile:'finite-v1',actions:['write'],resources:['doc'],notBefore:'0',expiresAt:'10000',limit:'100',canDelegate:false,depth:'0'}};
  await assert.rejects(coordinator.apply(grant),(e:any)=>e.code==='AUTHORITY_DENIED');
  assert.equal(store.snapshot().revision,'1');
  assert.equal(store.diagnostics()[0].command.code,'AUTHORITY_DENIED');
} finally {store.close();}

```

### Rust example: authorization

```rust
use aiws_sdk::*;
use aiws_sdk::runtime::*;
use aiws_sdk::sqlite::SqliteStore;
use serde_json::json;
struct SessionPolicy;
impl Authorizer for SessionPolicy {
    fn authorize(&mut self,command:&Command,_:&Snapshot,_:&str)->Result<Authorization>{
        // Deliberately narrow demo agent policy. Identity comes from trusted host state.
        let allowed=command.as_value()["type"]=="startRun";
        Ok(Authorization{allowed,policy:if allowed{"ALLOW"}else{"DENY"}.into(),mandatory_checks_ok:true,
            context:json!({"actor":"agent:builder","policyRevision":"policy:example:1","decisionClass":"DETERMINISTIC"})})
    }
}
struct FixedClock;
impl Clock for FixedClock{fn now_ms(&self)->String{"10".into()}}
fn main()->std::result::Result<(),Box<dyn std::error::Error>>{
    let contract=Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?;
    let store=SqliteStore::open(":memory:",Some(&contract))?;
    let mut coordinator=Coordinator::new(store,SessionPolicy,FixedClock);
    coordinator.apply(&Command::from_value(json!({"type":"startRun","runId":"r"}))?,None)?;
    let grant=Command::from_value(json!({"type":"grant","grant":{"id":"g","subject":"agent:builder","profile":"finite-v1","actions":["write"],"resources":["doc"],"notBefore":"0","expiresAt":"10000","limit":"100","canDelegate":false,"depth":"0"}}))?;
    assert_eq!(coordinator.apply(&grant,None).unwrap_err().code,"AUTHORITY_DENIED");
    assert_eq!(coordinator.store.snapshot()?.as_value()["revision"],"1");
    assert_eq!(coordinator.store.diagnostics()?[0]["command"]["code"],"AUTHORITY_DENIED");
    Ok(())
}

```

### Python example: authorization

```python
from pathlib import Path
from aiws import parse_contract,AiwsError
from aiws.sqlite import SqliteStore
from aiws.runtime import Coordinator
# Identity is trusted application state, not a caller-supplied command field.
session={'actor':'agent:builder','isHuman':False}
def authorize(command,state,now):
    allowed=command['type']=='startRun' # narrow demo policy; everything else is denied
    return dict(allowed=allowed,policy='ALLOW' if allowed else 'DENY',mandatoryChecksOk=True,
        context=dict(actor=session['actor'],policyRevision='policy:example:1',decisionClass='DETERMINISTIC'))
contract=parse_contract(Path('examples/guide/contract.json').read_text())
with SqliteStore(':memory:',contract) as store:
    coordinator=Coordinator(store,authorize,lambda:'10')
    coordinator.apply(dict(type='startRun',runId='r'))
    grant=dict(type='grant',grant=dict(id='g',subject=session['actor'],profile='finite-v1',actions=['write'],resources=['doc'],notBefore='0',expiresAt='10000',limit='100',canDelegate=False,depth='0'))
    try:
        coordinator.apply(grant)
        raise AssertionError('expected denial')
    except AiwsError as error:
        assert error.code=='AUTHORITY_DENIED'
    assert store.snapshot()['revision']=='1'
    assert store.diagnostics()[0]['command']['code']=='AUTHORITY_DENIED'

```

The example uses a fixed trusted-host identity to demonstrate a denial. Replace it with a verified session, service identity or worker capability from your actual identity system. Do not read isHuman, actor, role or ALLOW from an untrusted request body. An approval record represents a decision only after the host has verified who made it and what they approved.

Permission and resource-limit changes for agents always require human approval under the agreed engine design. Enforce this in the authorizer for every relevant operation. A delegated agent must not edit the policy document or identity claims that would allow it to approve its own expansion.

## Limit adapter capabilities

Give each adapter only the filesystem roots, outbound hosts, credentials and tool actions it needs. Validate artifact references and resolved paths before access. For coding tasks, use an isolated checkout or execution environment with explicit command and network policy. A graph node named VERIFICATION does not sandbox the command used to run a test suite.

Treat source files, retrieved documents, webhook payloads, model output and handoff summaries as untrusted input. Embedded instructions cannot change engine policy, grant capabilities or approve new spending. Keep approved intent and machine-readable controls distinct from narrative context.

## Approval and evidence integrity

Bind approval to the exact canonical action fingerprint and valid use count. Reevaluate current policy at dispatch; admission is not a permanent right to act. Avoid logging approval secrets, callback correlation tokens or provider credentials. Verify callback signatures and replay protections in the host before constructing TriggerEvent.

A digest detects changed content under a trusted reference. It does not establish authorship if an attacker can rewrite both the content and digest. Protect the SQLite database, backups, artifact store and audit exports with access controls. Where attestation is required, add a reviewed application signature/provenance mechanism; the SDK does not supply a universal signature protocol.

## Operate with bounded resources

Enforce limits at ingress before parsing, and limit external response sizes and adapter execution time. Choose conservative reservations before dispatch. Monitor the journal and telemetry outbox: disk-backed storage is not an unlimited queue. Do not release an UNKNOWN reservation just to let a new request proceed.

Use secret references in configuration and resolve credentials at execution. Rotate credentials independently of durable workflow history. If a credential expires while a workflow is paused, reauthenticate and reauthorize at continuation rather than relying on a saved token.

## Incident handling

For suspected tampering or an uncertain side effect, block affected work, preserve IDs and receipts, and route a specific decision to an authorized human. Do not delete history, fabricate verification or automatically broaden permissions as a repair strategy. See [recovery](https://www.lril.ai/recovery/) and [handoffs](https://www.lril.ai/handoffs/) for the durable record needed to resume safely.


---

# Errors and recovery decisions

Errors are stable codes carried by AiwsError. TypeScript exposes error.code, Rust returns Result with an error code, and Python raises AiwsError with code. Shape errors often use INVALID_RECORD; the SDK does not expose every underlying schema diagnostic through a standardized rich error object.

## Diagnose by phase

| Code or family | Likely cause | Appropriate response |
|---|---|---|
| INVALID_RECORD / UNSAFE_NUMBER | Unsupported fields, wrong edition/types or unsafe numeric material | Validate against the exact shared schema; preserve decimal strings |
| DUPLICATE_KEY / INPUT_LIMIT | Ambiguous JSON or parser resource limit | Reject at ingress; use bounded transport for large records |
| AUTHORITY_DENIED / AUTHORITY_INDETERMINATE | Principal, scope, validity or policy does not permit work | Obtain authorized correction; never turn uncertainty into ALLOW |
| ATTRIBUTION_REQUIRED | Authorizer omitted actor or policy revision | Repair trusted authorizer output |
| REVISION_CONFLICT | State changed after inspection | Reload, reauthorize and decide whether the original command remains appropriate |
| APPROVAL_INVALID / APPROVAL_CONSUMED | Material, expiry, withdrawal or allowed uses | Obtain the correct new approval; do not mutate prior evidence |
| BUDGET_EXHAUSTED / GRANT_BUDGET_EXHAUSTED | Reservation would exceed an applicable allowance | Hold affected work and request a human decision |
| EXPOSURE_EXCEEDED | Observed cost exceeds reserved exposure | Retain incident and external receipt; fix integration/accounting through an authorized path |
| UNSAFE_RETRY / EFFECT_UNKNOWN | External outcome is not known to be nonapplication | Human-controlled reconciliation |
| STALE_OWNER | Prepared attempt belongs to an earlier recovery epoch | Do not reuse the old worker's dispatch claim |
| NODE_NOT_READY / NODE_EFFECT | Dependency or effect proof missing | Inspect graph and bound operation; do not bypass completion checks |
| GRAPH_CYCLE / NO_END_PATH | Unsupported graph topology | Use bounded LOOP and valid END paths |
| LOOP_LIMIT / RETRY_LIMIT | A configured attempt/iteration bound is reached | Preserve consumed allowance; follow stop/intervention rules |
| WAIT_EXPIRED / CORRELATION_MISMATCH | Late or unrelated response | Record and review; never rewrite the old wait's identity |
| EVENT_ID_CONFLICT / EVENT_SOURCE | Changed duplicate or source/type mismatch | Reject and investigate upstream identity handling |
| TRIGGER_RATE / TRIGGER_CONCURRENCY | Admission capacity bound | Defer with original identity only when policy permits |
| JOURNAL_INTEGRITY / EVENT_MISMATCH | Stored records disagree with digest/semantic replay | Stop execution and inspect an authoritative backup |
| CLOCK_REGRESSION / METRIC_RANGE | Unsupported telemetry time or numeric representation | Correct measurement source; do not silently round accounting |

For exact command requirements use the generated [wire reference](https://www.lril.ai/wire-reference/). A service should map these codes to suitable application responses without disclosing secrets or returning raw database exceptions to untrusted callers.

## Symptoms that are not SDK defects

A registered trigger does nothing if no host invokes it. A ready TASK does nothing if no scheduler dispatches its adapter. A TIMER wait does not wake a sleeping process by itself. An AUTHORIZATION wait does not send a human notification. A graph END node does not automatically accept a deliverable. A queued trace is not exported until the application flushes it.

## Preserve evidence while repairing

Never delete the mission database to remove a conflict, manually flip an UNKNOWN attempt to SUCCEEDED, overwrite the stored contract, or grant an agent broader authority merely to silence an error. Save the error, relevant IDs, current revision and operator decision. Sensitive prompts, tokens and evidence contents belong in protected records rather than indiscriminate log messages.


---

# Test recipes and release gates

A passing SDK suite establishes the tested implementation profile. An application also needs evidence that its authorizer, target adapters, storage and assessment logic behave correctly under failure. Documentation examples use local simulations and do not certify a production provider.

## Run the documented examples

```sh
npm ci
npm run build:sdk
python -m pip install ./packages/python
node scripts/check-guide-examples.mjs
```

The command runs every TypeScript and Python example and compiles/runs every Rust example. It stops on the first failed assertion or process error. `AIWS_PYTHON` and `AIWS_CARGO` select nondefault executables. Each code tab is backed by one of those source files. Isolated source simulations deliberately supply recorded outcomes and make no external calls.

## Run the SDK tests separately

```sh
npm test
python -m unittest discover -s packages/python/tests -v
cargo test --locked
npm run test:parity
```

The shared corpus checks positive and negative command sequences, expected state values and exact canonical state hashes across languages. It also checks shared SQLite state and representative observability projections. A test passing in one language is not evidence that an unexecuted example in another language compiles.

## Adapter fault matrix

| Injection point | Required observable result |
|---|---|
| Authorizer rejects | No external call; state does not advance through rejected action |
| Reservation exceeds allowance | No dispatch; prior accounting unchanged |
| Commit fails before dispatch | No adapter call and no orphan committed outbox intent |
| Target applies effect, response lost | UNKNOWN retained; no blind resend |
| Restart with PREPARED attempt | Old ownership cannot dispatch after recovery |
| Approval expires before retry | Retry/dispatch rejected until a valid authorized path exists |
| Duplicate delivery | No duplicate semantic work; changed material rejected |
| Artifact modified after assessment | Reassessment or invalidation before accepted use |
| Collector returns transient failure | Persistent export retry, no repeated workflow effect |
| Collector partially accepts | Failure retained, no full-request resend |
| Disk unavailable | No dispatch that depends on an uncommitted control event |

Use real target readback in adapter tests. A mock returning CONFIRMED_APPLIED without examining anything verifies only plumbing. For coding, compare repository revisions and test artifacts before and after a simulated lost response. For remote systems, use their request identity or audit receipt.

## Deployment gates

Measure ledger growth and transaction latency under expected mission histories, test backup/restore with WAL, verify credential isolation and configure alert evaluation and queue retention. Require meaningful evidence for claimed cancellation and budget caps. A successfully built documentation site is not a load test or a deployment conformance assessment.

## Documentation release checks

The accompanying DOCS-VALIDATION.md records the executed documentation gates separately from historical SDK tests. All 75 programs passed; TypeScript also passed strict type checking. The site checker verifies displayed/source equality, chapter links, tab-panel references and generated agent-readable downloads. These checks are now part of CI.


---

# TypeScript integration

Use Node 24 or newer, as declared by this release. Install the downloaded aiws-sdk-0.3.0.tgz with npm, or run npm ci and npm run build:sdk in the source workspace. The package is ESM. Import core/workflow APIs from @aiws/sdk and infrastructure APIs from @aiws/sdk/runtime, @aiws/sdk/sqlite and @aiws/sdk/observability. This is a native Node library; the SQLite/runtime modules are not browser APIs.

## Runnable coordinator

**Execute an adapter through the trusted coordinator** — Executable local adapter demonstration; fixed demo identity and fixture evidence

### Typescript example: coordinator

```typescript
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseContract, parseCommand, canonical, assessSuccess } from '@aiws/sdk';
import { SqliteStore } from '@aiws/sdk/sqlite';
import { Coordinator, type EffectAdapter } from '@aiws/sdk/runtime';
// A local demonstration. Replace this fixed demo identity with authenticated policy.
const demo = JSON.parse(readFileSync('examples/review.json', 'utf8'));
const directory = mkdtempSync(join(tmpdir(), 'aiws-review-'));
const store = new SqliteStore(join(directory, 'mission.db'), parseContract(canonical(demo.contract)));
const coordinator = new Coordinator(store, async () => ({ allowed: true, policy: 'ALLOW', mandatoryChecksOk: true,context:{actor:"demo:operator",policyRevision:"policy:1",decisionClass:"DETERMINISTIC"} }), () => '10');
const adapter: EffectAdapter = {
    async execute(operation) { writeFileSync(join(directory, 'review.txt'), canonical(operation.action.payload), { flag: 'wx' }); return { effect: 'CONFIRMED_APPLIED', actualCost: '3' }; },
    async reconcile(operation) { try {
        return { effect: readFileSync(join(directory, 'review.txt'), 'utf8') === canonical(operation.action.payload) ? 'CONFIRMED_APPLIED' : 'UNKNOWN', actualCost: '3' };
    }
    catch {
        return { effect: 'UNKNOWN', actualCost: '0' };
    } }
};
try {
    for (const raw of demo.commands) {
        const command = parseCommand(canonical(raw));
        if (command.type === 'dispatch')
            await coordinator.dispatch(command.attemptId, adapter, command.nodeId);
        else
            await coordinator.apply(command);
    }
    console.log(JSON.stringify({ directory, ...assessSuccess(store.snapshot(), 'r') }));
    writeFileSync(join(directory, 'audit.json'), store.exportAudit());
}
finally {
    store.close();
}

```

### Rust example: coordinator

```rust
use aiws_sdk::runtime::*;
use aiws_sdk::sqlite::SqliteStore;
use aiws_sdk::*;
use serde_json::{json, Value};
struct DemoIdentity;
impl Authorizer for DemoIdentity {
    fn authorize(&mut self, _: &Command, _: &Snapshot, _: &str) -> Result<Authorization> {
        Ok(Authorization {
            allowed: true,
            policy: "ALLOW".into(),
            mandatory_checks_ok: true,
            context: json!({"actor":"demo:operator","policyRevision":"policy:1","decisionClass":"DETERMINISTIC"}),
        })
    }
}
struct Fixed;
impl Clock for Fixed {
    fn now_ms(&self) -> String {
        "10".into()
    }
}
struct LocalReview(std::path::PathBuf);
impl EffectAdapter for LocalReview {
    fn execute(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        use std::io::Write;
        let mut file = std::fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&self.0)
            .map_err(|_| error("FILE_ERROR"))?;
        file.write_all(canonical(&operation["action"]["payload"])?.as_bytes())
            .map_err(|_| error("FILE_ERROR"))?;
        file.sync_all().map_err(|_| error("FILE_ERROR"))?;
        Ok(Outcome {
            effect: "CONFIRMED_APPLIED".into(),
            actual_cost: "3".into(),
        })
    }
    fn reconcile(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        let effect = if std::fs::read_to_string(&self.0).ok()
            == Some(canonical(&operation["action"]["payload"])?)
        {
            "CONFIRMED_APPLIED"
        } else {
            "UNKNOWN"
        };
        Ok(Outcome {
            effect: effect.into(),
            actual_cost: "3".into(),
        })
    }
}
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let demo: Value = serde_json::from_str(include_str!("../../../../../examples/review.json"))?;
    let directory = std::env::temp_dir().join(format!(
        "aiws-review-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_nanos()
    ));
    std::fs::create_dir(&directory)?;
    let store = SqliteStore::open(
        directory.join("mission.db").to_str().unwrap(),
        Some(&Contract::from_value(demo["contract"].clone())?),
    )?;
    let mut c = Coordinator::new(store, DemoIdentity, Fixed);
    let mut adapter = LocalReview(directory.join("review.txt"));
    for raw in demo["commands"].as_array().unwrap() {
        let command = Command::from_value(raw.clone())?;
        if raw["type"] == "dispatch" {
            c.dispatch(
                raw["attemptId"].as_str().unwrap(),
                &mut adapter,
                raw["nodeId"].as_str(),
            )?;
        } else {
            c.apply(&command, None)?;
        }
    }
    println!(
        "{}",
        json!({"directory":directory,"result":assess_success(&c.store.snapshot()?,"r")?})
    );
    std::fs::write(directory.join("audit.json"), c.store.export_audit()?)?;
    Ok(())
}

```

### Python example: coordinator

```python
"""Run a governed file effect with the independently implemented Python SDK."""
import json
import tempfile
from pathlib import Path
from aiws.core import canonical, assess_success
from aiws.sqlite import SqliteStore
from aiws.runtime import Coordinator

demo = json.loads(Path('examples/review.json').read_text())
directory = Path(tempfile.mkdtemp(prefix='aiws-python-review-'))
class Adapter:
    def execute(self, operation, attempt):
        with (directory/'review.txt').open('x') as out: out.write(canonical(operation['action']['payload']))
        return {'effect':'CONFIRMED_APPLIED','actualCost':'3'}
    def reconcile(self, operation, attempt):
        try: effect='CONFIRMED_APPLIED' if (directory/'review.txt').read_text()==canonical(operation['action']['payload']) else 'UNKNOWN'
        except OSError: effect='UNKNOWN'
        return {'effect':effect,'actualCost':'3' if effect=='CONFIRMED_APPLIED' else '0'}
with SqliteStore(str(directory/'mission.db'),demo['contract']) as store:
    # Demo identity only: replace with real authenticated and versioned policy.
    c=Coordinator(store,lambda *_:dict(allowed=True,policy='ALLOW',mandatoryChecksOk=True,context=dict(actor='demo:operator',policyRevision='policy:1',decisionClass='DETERMINISTIC')),lambda:'10')
    for command in demo['commands']:
        if command['type']=='dispatch': c.dispatch(command['attemptId'],Adapter(),command.get('nodeId'))
        else: c.apply(command)
    print(json.dumps({'directory':str(directory),**assess_success(store.snapshot(),'r')}))
    (directory/'audit.json').write_text(store.export_audit())

```

The full program uses a local adapter, fixed trusted demo identity and fixture evidence. Replace those three boundaries with real identity, a scoped adapter and measured validation evidence before using this pattern in production. The tab set includes equivalent Rust, TypeScript and Python implementations.

## Protocol-bound workflow correlation

The current repository source adds a small protocol-correlation helper after the previously built SDK 0.3.0 binary baseline. It records the exact Praxis binding revision and remote identity alongside the AIWS work/run/operation/attempt identity. It does **not** implement MCP, A2A or AG-UI transport behavior and always validates `authoritative: false`.

**Correlate governed work with a protocol binding** — Executable source-only SDK correlation example; no protocol client or authority is created

### Typescript example: protocol-correlation

```typescript
import assert from 'node:assert/strict';
import {createMission,reduce,protocolCorrelation,type Contract} from '@aiws/sdk';

const contract:Contract={id:'contract:protocol',missionId:'mission:protocol',aiwsEdition:'0.4',profile:'finite-v1',budget:'20',deadline:'1000',criteria:['remote-result-reviewed'],actions:['remote.invoke'],resources:['protocol:mcp'],requiresApproval:false,maxAttempts:'2',maxAuthorizationAgeMs:'100',features:[]};
let state=createMission(contract);
state=reduce(state,{type:'startRun',runId:'run:1'},'10');
state=reduce(state,{type:'grant',grant:{id:'grant:1',subject:'agent:1',profile:'finite-v1',actions:['remote.invoke'],resources:['protocol:mcp'],notBefore:'0',expiresAt:'1000',limit:'20',canDelegate:false,depth:'0'}},'10');
state=reduce(state,{type:'admit',runId:'run:1',operationId:'operation:1',attemptId:'attempt:1',grantId:'grant:1',subject:'agent:1',action:{capability:'remote.invoke',resource:'protocol:mcp',payload:{tool:'echo'},preconditions:{}},amount:'5',authorizationCheckedAt:'10',policy:'ALLOW',mandatoryChecksOk:true},'10');

const correlation=protocolCorrelation(
  {bindingId:'mcp:official-v2',bindingRevision:'1',protocol:'MCP',protocolVersion:'2026-07-28',manifestDigest:'a'.repeat(64),capabilityDigest:'b'.repeat(64)},
  {workOrderId:'mission:protocol',runId:'run:1',taskId:null,operationId:'operation:1',attemptId:'attempt:1',remoteIdentityRef:'mcp:praxis-m9-fixture@1.0.0',remoteOperationId:null}
);
assert.equal(correlation.operation.operationId,'operation:1');
assert.equal(correlation.authoritative,false);
assert.equal(state.operations['operation:1'].runId,'run:1');
console.log('PASS: protocol-correlation');

```

### Rust example: protocol-correlation

```rust
use aiws_sdk::*;
use serde_json::json;

fn main()->Result<()>{
    let contract=Contract::from_value(json!({"id":"contract:protocol","missionId":"mission:protocol","aiwsEdition":"0.4","profile":"finite-v1","budget":"20","deadline":"1000","criteria":["remote-result-reviewed"],"actions":["remote.invoke"],"resources":["protocol:mcp"],"requiresApproval":false,"maxAttempts":"2","maxAuthorizationAgeMs":"100","features":[]}))?;
    let mut state=create_mission(&contract)?;
    state=reduce(&state,&Command::from_value(json!({"type":"startRun","runId":"run:1"}))?,"10")?;
    state=reduce(&state,&Command::from_value(json!({"type":"grant","grant":{"id":"grant:1","subject":"agent:1","profile":"finite-v1","actions":["remote.invoke"],"resources":["protocol:mcp"],"notBefore":"0","expiresAt":"1000","limit":"20","canDelegate":false,"depth":"0"}}))?,"10")?;
    state=reduce(&state,&Command::from_value(json!({"type":"admit","runId":"run:1","operationId":"operation:1","attemptId":"attempt:1","grantId":"grant:1","subject":"agent:1","action":{"capability":"remote.invoke","resource":"protocol:mcp","payload":{"tool":"echo"},"preconditions":{}},"amount":"5","authorizationCheckedAt":"10","policy":"ALLOW","mandatoryChecksOk":true}))?,"10")?;

    let correlation=protocol_correlation(
        ProtocolBindingRef{binding_id:"mcp:official-v2".into(),binding_revision:"1".into(),protocol:"MCP".into(),protocol_version:"2026-07-28".into(),manifest_digest:"a".repeat(64),capability_digest:"b".repeat(64)},
        ProtocolOperationRef{work_order_id:"mission:protocol".into(),run_id:"run:1".into(),task_id:None,operation_id:"operation:1".into(),attempt_id:"attempt:1".into(),remote_identity_ref:Some("mcp:praxis-m9-fixture@1.0.0".into()),remote_operation_id:None}
    ).map_err(|code|AiwsError{code})?;
    assert_eq!(correlation.operation.operation_id,"operation:1");
    assert!(!correlation.authoritative);
    assert_eq!(state.as_value()["operations"]["operation:1"]["runId"],json!("run:1"));
    println!("PASS: protocol-correlation");
    Ok(())
}

```

### Python example: protocol-correlation

```python
from aiws import create_mission,reduce,protocol_correlation

contract={'id':'contract:protocol','missionId':'mission:protocol','aiwsEdition':'0.4','profile':'finite-v1','budget':'20','deadline':'1000','criteria':['remote-result-reviewed'],'actions':['remote.invoke'],'resources':['protocol:mcp'],'requiresApproval':False,'maxAttempts':'2','maxAuthorizationAgeMs':'100','features':[]}
state=create_mission(contract)
state=reduce(state,{'type':'startRun','runId':'run:1'},'10')
state=reduce(state,{'type':'grant','grant':{'id':'grant:1','subject':'agent:1','profile':'finite-v1','actions':['remote.invoke'],'resources':['protocol:mcp'],'notBefore':'0','expiresAt':'1000','limit':'20','canDelegate':False,'depth':'0'}},'10')
state=reduce(state,{'type':'admit','runId':'run:1','operationId':'operation:1','attemptId':'attempt:1','grantId':'grant:1','subject':'agent:1','action':{'capability':'remote.invoke','resource':'protocol:mcp','payload':{'tool':'echo'},'preconditions':{}},'amount':'5','authorizationCheckedAt':'10','policy':'ALLOW','mandatoryChecksOk':True},'10')

correlation=protocol_correlation(
 {'bindingId':'mcp:official-v2','bindingRevision':'1','protocol':'MCP','protocolVersion':'2026-07-28','manifestDigest':'a'*64,'capabilityDigest':'b'*64},
 {'workOrderId':'mission:protocol','runId':'run:1','taskId':None,'operationId':'operation:1','attemptId':'attempt:1','remoteIdentityRef':'mcp:praxis-m9-fixture@1.0.0','remoteOperationId':None}
)
assert correlation['operation']['operationId']=='operation:1'
assert correlation['authoritative'] is False
assert state['operations']['operation:1']['runId']=='run:1'
print('PASS: protocol-correlation')

```

Use official protocol SDKs and the Praxis adapters for wire behavior. Use this helper only for portable correlation in application code, evidence indexes or integration metadata.

## Integration details

TypeScript annotations do not validate external JSON. Use parseContract and parseCommand on original input text. Preserve amounts and times as strings; convert to BigInt only for local exact calculations, then serialize back to canonical decimal strings. Do not JSON.stringify a BigInt directly.

Authorizer and EffectAdapter callbacks are asynchronous. Await apply, dispatch, reconcile and flushTelemetry, and retain errors at the host boundary. Snapshot and SQLite methods are synchronous. Long-running blocking adapters belong in a suitable worker, and cancellation of a JavaScript promise is not proof that an external effect stopped.

Use AiwsError.code to decide how to report a failure. A REVISION_CONFLICT requires a fresh state/policy decision. An UNKNOWN outcome requires human-controlled reconciliation. Neither should be handled by a generic retry-every-exception wrapper.

The source workspace's Node 24 examples execute TypeScript directly. Applications may instead compile with their own TypeScript build. The package contains declarations; source example execution and static type checking are separate verification steps.

## Diagnostics and next steps

Use the [API mapping](https://www.lril.ai/api-reference/) to translate native calls without changing camelCase wire keys. Use [observability](https://www.lril.ai/observability/) for exporter configuration and [recovery](https://www.lril.ai/recovery/) for interrupted effects. All examples are included as individual executable source files under examples/guide/.

The supplied CLI validates contracts and graphs and replays audit records. Its observe projection uses a fixed test timestamp and is not a live monitoring service. Run operationalSummary/operational_summary with a trusted current timestamp in your application instead.


---

# Rust integration

Use the Rust toolchain compatible with the included Cargo.lock and Cargo.toml. Add aiws-sdk as a path dependency pointing to crates/aiws in the source download, or unpack the supplied .crate file and point to that directory. Add serde_json when constructing JSON-backed records. This release is not published to crates.io.

## Runnable coordinator

**Execute an adapter through the trusted coordinator** — Executable local adapter demonstration; fixed demo identity and fixture evidence

### Typescript example: coordinator

```typescript
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseContract, parseCommand, canonical, assessSuccess } from '@aiws/sdk';
import { SqliteStore } from '@aiws/sdk/sqlite';
import { Coordinator, type EffectAdapter } from '@aiws/sdk/runtime';
// A local demonstration. Replace this fixed demo identity with authenticated policy.
const demo = JSON.parse(readFileSync('examples/review.json', 'utf8'));
const directory = mkdtempSync(join(tmpdir(), 'aiws-review-'));
const store = new SqliteStore(join(directory, 'mission.db'), parseContract(canonical(demo.contract)));
const coordinator = new Coordinator(store, async () => ({ allowed: true, policy: 'ALLOW', mandatoryChecksOk: true,context:{actor:"demo:operator",policyRevision:"policy:1",decisionClass:"DETERMINISTIC"} }), () => '10');
const adapter: EffectAdapter = {
    async execute(operation) { writeFileSync(join(directory, 'review.txt'), canonical(operation.action.payload), { flag: 'wx' }); return { effect: 'CONFIRMED_APPLIED', actualCost: '3' }; },
    async reconcile(operation) { try {
        return { effect: readFileSync(join(directory, 'review.txt'), 'utf8') === canonical(operation.action.payload) ? 'CONFIRMED_APPLIED' : 'UNKNOWN', actualCost: '3' };
    }
    catch {
        return { effect: 'UNKNOWN', actualCost: '0' };
    } }
};
try {
    for (const raw of demo.commands) {
        const command = parseCommand(canonical(raw));
        if (command.type === 'dispatch')
            await coordinator.dispatch(command.attemptId, adapter, command.nodeId);
        else
            await coordinator.apply(command);
    }
    console.log(JSON.stringify({ directory, ...assessSuccess(store.snapshot(), 'r') }));
    writeFileSync(join(directory, 'audit.json'), store.exportAudit());
}
finally {
    store.close();
}

```

### Rust example: coordinator

```rust
use aiws_sdk::runtime::*;
use aiws_sdk::sqlite::SqliteStore;
use aiws_sdk::*;
use serde_json::{json, Value};
struct DemoIdentity;
impl Authorizer for DemoIdentity {
    fn authorize(&mut self, _: &Command, _: &Snapshot, _: &str) -> Result<Authorization> {
        Ok(Authorization {
            allowed: true,
            policy: "ALLOW".into(),
            mandatory_checks_ok: true,
            context: json!({"actor":"demo:operator","policyRevision":"policy:1","decisionClass":"DETERMINISTIC"}),
        })
    }
}
struct Fixed;
impl Clock for Fixed {
    fn now_ms(&self) -> String {
        "10".into()
    }
}
struct LocalReview(std::path::PathBuf);
impl EffectAdapter for LocalReview {
    fn execute(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        use std::io::Write;
        let mut file = std::fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&self.0)
            .map_err(|_| error("FILE_ERROR"))?;
        file.write_all(canonical(&operation["action"]["payload"])?.as_bytes())
            .map_err(|_| error("FILE_ERROR"))?;
        file.sync_all().map_err(|_| error("FILE_ERROR"))?;
        Ok(Outcome {
            effect: "CONFIRMED_APPLIED".into(),
            actual_cost: "3".into(),
        })
    }
    fn reconcile(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        let effect = if std::fs::read_to_string(&self.0).ok()
            == Some(canonical(&operation["action"]["payload"])?)
        {
            "CONFIRMED_APPLIED"
        } else {
            "UNKNOWN"
        };
        Ok(Outcome {
            effect: effect.into(),
            actual_cost: "3".into(),
        })
    }
}
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let demo: Value = serde_json::from_str(include_str!("../../../../../examples/review.json"))?;
    let directory = std::env::temp_dir().join(format!(
        "aiws-review-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_nanos()
    ));
    std::fs::create_dir(&directory)?;
    let store = SqliteStore::open(
        directory.join("mission.db").to_str().unwrap(),
        Some(&Contract::from_value(demo["contract"].clone())?),
    )?;
    let mut c = Coordinator::new(store, DemoIdentity, Fixed);
    let mut adapter = LocalReview(directory.join("review.txt"));
    for raw in demo["commands"].as_array().unwrap() {
        let command = Command::from_value(raw.clone())?;
        if raw["type"] == "dispatch" {
            c.dispatch(
                raw["attemptId"].as_str().unwrap(),
                &mut adapter,
                raw["nodeId"].as_str(),
            )?;
        } else {
            c.apply(&command, None)?;
        }
    }
    println!(
        "{}",
        json!({"directory":directory,"result":assess_success(&c.store.snapshot()?,"r")?})
    );
    std::fs::write(directory.join("audit.json"), c.store.export_audit()?)?;
    Ok(())
}

```

### Python example: coordinator

```python
"""Run a governed file effect with the independently implemented Python SDK."""
import json
import tempfile
from pathlib import Path
from aiws.core import canonical, assess_success
from aiws.sqlite import SqliteStore
from aiws.runtime import Coordinator

demo = json.loads(Path('examples/review.json').read_text())
directory = Path(tempfile.mkdtemp(prefix='aiws-python-review-'))
class Adapter:
    def execute(self, operation, attempt):
        with (directory/'review.txt').open('x') as out: out.write(canonical(operation['action']['payload']))
        return {'effect':'CONFIRMED_APPLIED','actualCost':'3'}
    def reconcile(self, operation, attempt):
        try: effect='CONFIRMED_APPLIED' if (directory/'review.txt').read_text()==canonical(operation['action']['payload']) else 'UNKNOWN'
        except OSError: effect='UNKNOWN'
        return {'effect':effect,'actualCost':'3' if effect=='CONFIRMED_APPLIED' else '0'}
with SqliteStore(str(directory/'mission.db'),demo['contract']) as store:
    # Demo identity only: replace with real authenticated and versioned policy.
    c=Coordinator(store,lambda *_:dict(allowed=True,policy='ALLOW',mandatoryChecksOk=True,context=dict(actor='demo:operator',policyRevision='policy:1',decisionClass='DETERMINISTIC')),lambda:'10')
    for command in demo['commands']:
        if command['type']=='dispatch': c.dispatch(command['attemptId'],Adapter(),command.get('nodeId'))
        else: c.apply(command)
    print(json.dumps({'directory':str(directory),**assess_success(store.snapshot(),'r')}))
    (directory/'audit.json').write_text(store.export_audit())

```

The full program uses a local adapter, fixed trusted demo identity and fixture evidence. Replace those three boundaries with real identity, a scoped adapter and measured validation evidence before using this pattern in production. The tab set includes equivalent Rust, TypeScript and Python implementations.

## Protocol-bound workflow correlation

The current repository source adds a small protocol-correlation helper after the previously built SDK 0.3.0 binary baseline. It records the exact Praxis binding revision and remote identity alongside the AIWS work/run/operation/attempt identity. It does **not** implement MCP, A2A or AG-UI transport behavior and always validates `authoritative: false`.

**Correlate governed work with a protocol binding** — Executable source-only SDK correlation example; no protocol client or authority is created

### Typescript example: protocol-correlation

```typescript
import assert from 'node:assert/strict';
import {createMission,reduce,protocolCorrelation,type Contract} from '@aiws/sdk';

const contract:Contract={id:'contract:protocol',missionId:'mission:protocol',aiwsEdition:'0.4',profile:'finite-v1',budget:'20',deadline:'1000',criteria:['remote-result-reviewed'],actions:['remote.invoke'],resources:['protocol:mcp'],requiresApproval:false,maxAttempts:'2',maxAuthorizationAgeMs:'100',features:[]};
let state=createMission(contract);
state=reduce(state,{type:'startRun',runId:'run:1'},'10');
state=reduce(state,{type:'grant',grant:{id:'grant:1',subject:'agent:1',profile:'finite-v1',actions:['remote.invoke'],resources:['protocol:mcp'],notBefore:'0',expiresAt:'1000',limit:'20',canDelegate:false,depth:'0'}},'10');
state=reduce(state,{type:'admit',runId:'run:1',operationId:'operation:1',attemptId:'attempt:1',grantId:'grant:1',subject:'agent:1',action:{capability:'remote.invoke',resource:'protocol:mcp',payload:{tool:'echo'},preconditions:{}},amount:'5',authorizationCheckedAt:'10',policy:'ALLOW',mandatoryChecksOk:true},'10');

const correlation=protocolCorrelation(
  {bindingId:'mcp:official-v2',bindingRevision:'1',protocol:'MCP',protocolVersion:'2026-07-28',manifestDigest:'a'.repeat(64),capabilityDigest:'b'.repeat(64)},
  {workOrderId:'mission:protocol',runId:'run:1',taskId:null,operationId:'operation:1',attemptId:'attempt:1',remoteIdentityRef:'mcp:praxis-m9-fixture@1.0.0',remoteOperationId:null}
);
assert.equal(correlation.operation.operationId,'operation:1');
assert.equal(correlation.authoritative,false);
assert.equal(state.operations['operation:1'].runId,'run:1');
console.log('PASS: protocol-correlation');

```

### Rust example: protocol-correlation

```rust
use aiws_sdk::*;
use serde_json::json;

fn main()->Result<()>{
    let contract=Contract::from_value(json!({"id":"contract:protocol","missionId":"mission:protocol","aiwsEdition":"0.4","profile":"finite-v1","budget":"20","deadline":"1000","criteria":["remote-result-reviewed"],"actions":["remote.invoke"],"resources":["protocol:mcp"],"requiresApproval":false,"maxAttempts":"2","maxAuthorizationAgeMs":"100","features":[]}))?;
    let mut state=create_mission(&contract)?;
    state=reduce(&state,&Command::from_value(json!({"type":"startRun","runId":"run:1"}))?,"10")?;
    state=reduce(&state,&Command::from_value(json!({"type":"grant","grant":{"id":"grant:1","subject":"agent:1","profile":"finite-v1","actions":["remote.invoke"],"resources":["protocol:mcp"],"notBefore":"0","expiresAt":"1000","limit":"20","canDelegate":false,"depth":"0"}}))?,"10")?;
    state=reduce(&state,&Command::from_value(json!({"type":"admit","runId":"run:1","operationId":"operation:1","attemptId":"attempt:1","grantId":"grant:1","subject":"agent:1","action":{"capability":"remote.invoke","resource":"protocol:mcp","payload":{"tool":"echo"},"preconditions":{}},"amount":"5","authorizationCheckedAt":"10","policy":"ALLOW","mandatoryChecksOk":true}))?,"10")?;

    let correlation=protocol_correlation(
        ProtocolBindingRef{binding_id:"mcp:official-v2".into(),binding_revision:"1".into(),protocol:"MCP".into(),protocol_version:"2026-07-28".into(),manifest_digest:"a".repeat(64),capability_digest:"b".repeat(64)},
        ProtocolOperationRef{work_order_id:"mission:protocol".into(),run_id:"run:1".into(),task_id:None,operation_id:"operation:1".into(),attempt_id:"attempt:1".into(),remote_identity_ref:Some("mcp:praxis-m9-fixture@1.0.0".into()),remote_operation_id:None}
    ).map_err(|code|AiwsError{code})?;
    assert_eq!(correlation.operation.operation_id,"operation:1");
    assert!(!correlation.authoritative);
    assert_eq!(state.as_value()["operations"]["operation:1"]["runId"],json!("run:1"));
    println!("PASS: protocol-correlation");
    Ok(())
}

```

### Python example: protocol-correlation

```python
from aiws import create_mission,reduce,protocol_correlation

contract={'id':'contract:protocol','missionId':'mission:protocol','aiwsEdition':'0.4','profile':'finite-v1','budget':'20','deadline':'1000','criteria':['remote-result-reviewed'],'actions':['remote.invoke'],'resources':['protocol:mcp'],'requiresApproval':False,'maxAttempts':'2','maxAuthorizationAgeMs':'100','features':[]}
state=create_mission(contract)
state=reduce(state,{'type':'startRun','runId':'run:1'},'10')
state=reduce(state,{'type':'grant','grant':{'id':'grant:1','subject':'agent:1','profile':'finite-v1','actions':['remote.invoke'],'resources':['protocol:mcp'],'notBefore':'0','expiresAt':'1000','limit':'20','canDelegate':False,'depth':'0'}},'10')
state=reduce(state,{'type':'admit','runId':'run:1','operationId':'operation:1','attemptId':'attempt:1','grantId':'grant:1','subject':'agent:1','action':{'capability':'remote.invoke','resource':'protocol:mcp','payload':{'tool':'echo'},'preconditions':{}},'amount':'5','authorizationCheckedAt':'10','policy':'ALLOW','mandatoryChecksOk':True},'10')

correlation=protocol_correlation(
 {'bindingId':'mcp:official-v2','bindingRevision':'1','protocol':'MCP','protocolVersion':'2026-07-28','manifestDigest':'a'*64,'capabilityDigest':'b'*64},
 {'workOrderId':'mission:protocol','runId':'run:1','taskId':None,'operationId':'operation:1','attemptId':'attempt:1','remoteIdentityRef':'mcp:praxis-m9-fixture@1.0.0','remoteOperationId':None}
)
assert correlation['operation']['operationId']=='operation:1'
assert correlation['authoritative'] is False
assert state['operations']['operation:1']['runId']=='run:1'
print('PASS: protocol-correlation')

```

Use official protocol SDKs and the Praxis adapters for wire behavior. Use this helper only for portable correlation in application code, evidence indexes or integration metadata.

## Integration details

Checked record constructors return Result and enforce the shared schema. Contract::parse and Command::parse preserve strict JSON ingress checks; from_value is suitable for already-decoded trusted values. Inspect snapshots with as_value(); do not circumvent the record constructors by manipulating internal state.

Implement runtime::Authorizer, runtime::Clock and runtime::EffectAdapter in your host. The coordinator owns a SqliteStore and its mutable methods serialize work through that object. These APIs are synchronous. In an async server, run blocking database and adapter work in a controlled blocking worker rather than on an executor thread that must stay responsive.

Propagate errors with ? until a boundary can inspect AiwsError.code, preserve state and choose a recovery decision. Do not unwrap external input. Dropping a coordinator does not cancel an already-applied external action. A timeout at the host still requires read-back or human intervention for ambiguous outcomes.

The guide examples form an independent Cargo workspace under examples/guide/rust. Use cargo run --manifest-path examples/guide/rust/Cargo.toml --bin coordinator from the repository root. Cargo builds native Rust; no Node or Python process implements these SDK operations.

## Diagnostics and next steps

Use the [API mapping](https://www.lril.ai/api-reference/) to translate native calls without changing camelCase wire keys. Use [observability](https://www.lril.ai/observability/) for exporter configuration and [recovery](https://www.lril.ai/recovery/) for interrupted effects. All examples are included as individual executable source files under examples/guide/.

The supplied CLI validates contracts and graphs and replays audit records. Its observe projection uses a fixed test timestamp and is not a live monitoring service. Run operationalSummary/operational_summary with a trusted current timestamp in your application instead.


---

# Python integration

Python 3.11 or newer is required. Install the downloaded aiws_sdk-0.3.0-py3-none-any.whl, or run python -m pip install ./packages/python from the repository root. jsonschema is the runtime dependency. This is a native Python implementation and is not published to PyPI in this release.

## Runnable coordinator

**Execute an adapter through the trusted coordinator** — Executable local adapter demonstration; fixed demo identity and fixture evidence

### Typescript example: coordinator

```typescript
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseContract, parseCommand, canonical, assessSuccess } from '@aiws/sdk';
import { SqliteStore } from '@aiws/sdk/sqlite';
import { Coordinator, type EffectAdapter } from '@aiws/sdk/runtime';
// A local demonstration. Replace this fixed demo identity with authenticated policy.
const demo = JSON.parse(readFileSync('examples/review.json', 'utf8'));
const directory = mkdtempSync(join(tmpdir(), 'aiws-review-'));
const store = new SqliteStore(join(directory, 'mission.db'), parseContract(canonical(demo.contract)));
const coordinator = new Coordinator(store, async () => ({ allowed: true, policy: 'ALLOW', mandatoryChecksOk: true,context:{actor:"demo:operator",policyRevision:"policy:1",decisionClass:"DETERMINISTIC"} }), () => '10');
const adapter: EffectAdapter = {
    async execute(operation) { writeFileSync(join(directory, 'review.txt'), canonical(operation.action.payload), { flag: 'wx' }); return { effect: 'CONFIRMED_APPLIED', actualCost: '3' }; },
    async reconcile(operation) { try {
        return { effect: readFileSync(join(directory, 'review.txt'), 'utf8') === canonical(operation.action.payload) ? 'CONFIRMED_APPLIED' : 'UNKNOWN', actualCost: '3' };
    }
    catch {
        return { effect: 'UNKNOWN', actualCost: '0' };
    } }
};
try {
    for (const raw of demo.commands) {
        const command = parseCommand(canonical(raw));
        if (command.type === 'dispatch')
            await coordinator.dispatch(command.attemptId, adapter, command.nodeId);
        else
            await coordinator.apply(command);
    }
    console.log(JSON.stringify({ directory, ...assessSuccess(store.snapshot(), 'r') }));
    writeFileSync(join(directory, 'audit.json'), store.exportAudit());
}
finally {
    store.close();
}

```

### Rust example: coordinator

```rust
use aiws_sdk::runtime::*;
use aiws_sdk::sqlite::SqliteStore;
use aiws_sdk::*;
use serde_json::{json, Value};
struct DemoIdentity;
impl Authorizer for DemoIdentity {
    fn authorize(&mut self, _: &Command, _: &Snapshot, _: &str) -> Result<Authorization> {
        Ok(Authorization {
            allowed: true,
            policy: "ALLOW".into(),
            mandatory_checks_ok: true,
            context: json!({"actor":"demo:operator","policyRevision":"policy:1","decisionClass":"DETERMINISTIC"}),
        })
    }
}
struct Fixed;
impl Clock for Fixed {
    fn now_ms(&self) -> String {
        "10".into()
    }
}
struct LocalReview(std::path::PathBuf);
impl EffectAdapter for LocalReview {
    fn execute(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        use std::io::Write;
        let mut file = std::fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&self.0)
            .map_err(|_| error("FILE_ERROR"))?;
        file.write_all(canonical(&operation["action"]["payload"])?.as_bytes())
            .map_err(|_| error("FILE_ERROR"))?;
        file.sync_all().map_err(|_| error("FILE_ERROR"))?;
        Ok(Outcome {
            effect: "CONFIRMED_APPLIED".into(),
            actual_cost: "3".into(),
        })
    }
    fn reconcile(&mut self, operation: &Value, _: &Value) -> Result<Outcome> {
        let effect = if std::fs::read_to_string(&self.0).ok()
            == Some(canonical(&operation["action"]["payload"])?)
        {
            "CONFIRMED_APPLIED"
        } else {
            "UNKNOWN"
        };
        Ok(Outcome {
            effect: effect.into(),
            actual_cost: "3".into(),
        })
    }
}
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let demo: Value = serde_json::from_str(include_str!("../../../../../examples/review.json"))?;
    let directory = std::env::temp_dir().join(format!(
        "aiws-review-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_nanos()
    ));
    std::fs::create_dir(&directory)?;
    let store = SqliteStore::open(
        directory.join("mission.db").to_str().unwrap(),
        Some(&Contract::from_value(demo["contract"].clone())?),
    )?;
    let mut c = Coordinator::new(store, DemoIdentity, Fixed);
    let mut adapter = LocalReview(directory.join("review.txt"));
    for raw in demo["commands"].as_array().unwrap() {
        let command = Command::from_value(raw.clone())?;
        if raw["type"] == "dispatch" {
            c.dispatch(
                raw["attemptId"].as_str().unwrap(),
                &mut adapter,
                raw["nodeId"].as_str(),
            )?;
        } else {
            c.apply(&command, None)?;
        }
    }
    println!(
        "{}",
        json!({"directory":directory,"result":assess_success(&c.store.snapshot()?,"r")?})
    );
    std::fs::write(directory.join("audit.json"), c.store.export_audit()?)?;
    Ok(())
}

```

### Python example: coordinator

```python
"""Run a governed file effect with the independently implemented Python SDK."""
import json
import tempfile
from pathlib import Path
from aiws.core import canonical, assess_success
from aiws.sqlite import SqliteStore
from aiws.runtime import Coordinator

demo = json.loads(Path('examples/review.json').read_text())
directory = Path(tempfile.mkdtemp(prefix='aiws-python-review-'))
class Adapter:
    def execute(self, operation, attempt):
        with (directory/'review.txt').open('x') as out: out.write(canonical(operation['action']['payload']))
        return {'effect':'CONFIRMED_APPLIED','actualCost':'3'}
    def reconcile(self, operation, attempt):
        try: effect='CONFIRMED_APPLIED' if (directory/'review.txt').read_text()==canonical(operation['action']['payload']) else 'UNKNOWN'
        except OSError: effect='UNKNOWN'
        return {'effect':effect,'actualCost':'3' if effect=='CONFIRMED_APPLIED' else '0'}
with SqliteStore(str(directory/'mission.db'),demo['contract']) as store:
    # Demo identity only: replace with real authenticated and versioned policy.
    c=Coordinator(store,lambda *_:dict(allowed=True,policy='ALLOW',mandatoryChecksOk=True,context=dict(actor='demo:operator',policyRevision='policy:1',decisionClass='DETERMINISTIC')),lambda:'10')
    for command in demo['commands']:
        if command['type']=='dispatch': c.dispatch(command['attemptId'],Adapter(),command.get('nodeId'))
        else: c.apply(command)
    print(json.dumps({'directory':str(directory),**assess_success(store.snapshot(),'r')}))
    (directory/'audit.json').write_text(store.export_audit())

```

The full program uses a local adapter, fixed trusted demo identity and fixture evidence. Replace those three boundaries with real identity, a scoped adapter and measured validation evidence before using this pattern in production. The tab set includes equivalent Rust, TypeScript and Python implementations.

## Protocol-bound workflow correlation

The current repository source adds a small protocol-correlation helper after the previously built SDK 0.3.0 binary baseline. It records the exact Praxis binding revision and remote identity alongside the AIWS work/run/operation/attempt identity. It does **not** implement MCP, A2A or AG-UI transport behavior and always validates `authoritative: false`.

**Correlate governed work with a protocol binding** — Executable source-only SDK correlation example; no protocol client or authority is created

### Typescript example: protocol-correlation

```typescript
import assert from 'node:assert/strict';
import {createMission,reduce,protocolCorrelation,type Contract} from '@aiws/sdk';

const contract:Contract={id:'contract:protocol',missionId:'mission:protocol',aiwsEdition:'0.4',profile:'finite-v1',budget:'20',deadline:'1000',criteria:['remote-result-reviewed'],actions:['remote.invoke'],resources:['protocol:mcp'],requiresApproval:false,maxAttempts:'2',maxAuthorizationAgeMs:'100',features:[]};
let state=createMission(contract);
state=reduce(state,{type:'startRun',runId:'run:1'},'10');
state=reduce(state,{type:'grant',grant:{id:'grant:1',subject:'agent:1',profile:'finite-v1',actions:['remote.invoke'],resources:['protocol:mcp'],notBefore:'0',expiresAt:'1000',limit:'20',canDelegate:false,depth:'0'}},'10');
state=reduce(state,{type:'admit',runId:'run:1',operationId:'operation:1',attemptId:'attempt:1',grantId:'grant:1',subject:'agent:1',action:{capability:'remote.invoke',resource:'protocol:mcp',payload:{tool:'echo'},preconditions:{}},amount:'5',authorizationCheckedAt:'10',policy:'ALLOW',mandatoryChecksOk:true},'10');

const correlation=protocolCorrelation(
  {bindingId:'mcp:official-v2',bindingRevision:'1',protocol:'MCP',protocolVersion:'2026-07-28',manifestDigest:'a'.repeat(64),capabilityDigest:'b'.repeat(64)},
  {workOrderId:'mission:protocol',runId:'run:1',taskId:null,operationId:'operation:1',attemptId:'attempt:1',remoteIdentityRef:'mcp:praxis-m9-fixture@1.0.0',remoteOperationId:null}
);
assert.equal(correlation.operation.operationId,'operation:1');
assert.equal(correlation.authoritative,false);
assert.equal(state.operations['operation:1'].runId,'run:1');
console.log('PASS: protocol-correlation');

```

### Rust example: protocol-correlation

```rust
use aiws_sdk::*;
use serde_json::json;

fn main()->Result<()>{
    let contract=Contract::from_value(json!({"id":"contract:protocol","missionId":"mission:protocol","aiwsEdition":"0.4","profile":"finite-v1","budget":"20","deadline":"1000","criteria":["remote-result-reviewed"],"actions":["remote.invoke"],"resources":["protocol:mcp"],"requiresApproval":false,"maxAttempts":"2","maxAuthorizationAgeMs":"100","features":[]}))?;
    let mut state=create_mission(&contract)?;
    state=reduce(&state,&Command::from_value(json!({"type":"startRun","runId":"run:1"}))?,"10")?;
    state=reduce(&state,&Command::from_value(json!({"type":"grant","grant":{"id":"grant:1","subject":"agent:1","profile":"finite-v1","actions":["remote.invoke"],"resources":["protocol:mcp"],"notBefore":"0","expiresAt":"1000","limit":"20","canDelegate":false,"depth":"0"}}))?,"10")?;
    state=reduce(&state,&Command::from_value(json!({"type":"admit","runId":"run:1","operationId":"operation:1","attemptId":"attempt:1","grantId":"grant:1","subject":"agent:1","action":{"capability":"remote.invoke","resource":"protocol:mcp","payload":{"tool":"echo"},"preconditions":{}},"amount":"5","authorizationCheckedAt":"10","policy":"ALLOW","mandatoryChecksOk":true}))?,"10")?;

    let correlation=protocol_correlation(
        ProtocolBindingRef{binding_id:"mcp:official-v2".into(),binding_revision:"1".into(),protocol:"MCP".into(),protocol_version:"2026-07-28".into(),manifest_digest:"a".repeat(64),capability_digest:"b".repeat(64)},
        ProtocolOperationRef{work_order_id:"mission:protocol".into(),run_id:"run:1".into(),task_id:None,operation_id:"operation:1".into(),attempt_id:"attempt:1".into(),remote_identity_ref:Some("mcp:praxis-m9-fixture@1.0.0".into()),remote_operation_id:None}
    ).map_err(|code|AiwsError{code})?;
    assert_eq!(correlation.operation.operation_id,"operation:1");
    assert!(!correlation.authoritative);
    assert_eq!(state.as_value()["operations"]["operation:1"]["runId"],json!("run:1"));
    println!("PASS: protocol-correlation");
    Ok(())
}

```

### Python example: protocol-correlation

```python
from aiws import create_mission,reduce,protocol_correlation

contract={'id':'contract:protocol','missionId':'mission:protocol','aiwsEdition':'0.4','profile':'finite-v1','budget':'20','deadline':'1000','criteria':['remote-result-reviewed'],'actions':['remote.invoke'],'resources':['protocol:mcp'],'requiresApproval':False,'maxAttempts':'2','maxAuthorizationAgeMs':'100','features':[]}
state=create_mission(contract)
state=reduce(state,{'type':'startRun','runId':'run:1'},'10')
state=reduce(state,{'type':'grant','grant':{'id':'grant:1','subject':'agent:1','profile':'finite-v1','actions':['remote.invoke'],'resources':['protocol:mcp'],'notBefore':'0','expiresAt':'1000','limit':'20','canDelegate':False,'depth':'0'}},'10')
state=reduce(state,{'type':'admit','runId':'run:1','operationId':'operation:1','attemptId':'attempt:1','grantId':'grant:1','subject':'agent:1','action':{'capability':'remote.invoke','resource':'protocol:mcp','payload':{'tool':'echo'},'preconditions':{}},'amount':'5','authorizationCheckedAt':'10','policy':'ALLOW','mandatoryChecksOk':True},'10')

correlation=protocol_correlation(
 {'bindingId':'mcp:official-v2','bindingRevision':'1','protocol':'MCP','protocolVersion':'2026-07-28','manifestDigest':'a'*64,'capabilityDigest':'b'*64},
 {'workOrderId':'mission:protocol','runId':'run:1','taskId':None,'operationId':'operation:1','attemptId':'attempt:1','remoteIdentityRef':'mcp:praxis-m9-fixture@1.0.0','remoteOperationId':None}
)
assert correlation['operation']['operationId']=='operation:1'
assert correlation['authoritative'] is False
assert state['operations']['operation:1']['runId']=='run:1'
print('PASS: protocol-correlation')

```

Use official protocol SDKs and the Praxis adapters for wire behavior. Use this helper only for portable correlation in application code, evidence indexes or integration metadata.

## Integration details

Core records are validated dictionaries, not Pydantic models or mutable workflow classes. Import parse_contract, parse_command, reduce and AiwsError from aiws or aiws.core, and use aiws.runtime, aiws.sqlite and aiws.observability for the runtime boundaries.

The authorizer is a callable returning allowed, policy, mandatoryChecksOk and context. The clock returns str(time.time_ns() // 1_000_000) in a real host. Demo clocks are deterministic test inputs and must not become production authorization clocks. Adapter execute and reconcile return dictionaries containing effect and actualCost.

The SDK is synchronous. If an async web framework is used, execute blocking work in a suitable worker with its own connection lifecycle. Do not move an existing SQLite connection freely between threads; create and use it under the host's defined ownership rules. A with SqliteStore(...) block closes the connection, but that does not imply cancellation of an external job.

Python integers are unbounded, but the cross-language wire format is deliberately bounded. Do not bypass the safe-number rule just because Python can decode a large integer. Costs and timestamps remain canonical decimal strings. Catch AiwsError and inspect code at an integration boundary; never turn a generic Exception into automatic permission to retry an external action.

## Diagnostics and next steps

Use the [API mapping](https://www.lril.ai/api-reference/) to translate native calls without changing camelCase wire keys. Use [observability](https://www.lril.ai/observability/) for exporter configuration and [recovery](https://www.lril.ai/recovery/) for interrupted effects. All examples are included as individual executable source files under examples/guide/.

The supplied CLI validates contracts and graphs and replays audit records. Its observe projection uses a fixed test timestamp and is not a live monitoring service. Run operationalSummary/operational_summary with a trusted current timestamp in your application instead.


---

# Native API reference

The tables map the intended application-facing APIs in SDK 0.3.0. TypeScript imports core/workflow symbols from @aiws/sdk, Rust from aiws_sdk plus its modules, and Python from aiws or aiws.core plus its modules. Source definitions and the shared schema are included in the source download.

## Core values and functions

| Purpose | TypeScript | Rust | Python |
|---|---|---|---|
| Checked contract from JSON text | parseContract(text): Contract | Contract::parse(text) → Result&lt;Contract&gt; | parse_contract(text) → dict |
| Checked command from JSON text | parseCommand(text): Command | Command::parse(text) → Result&lt;Command&gt; | parse_command(text) → dict |
| Strict JSON | parseStrict(text) | parse_strict(text) → Result&lt;Value&gt; | parse_strict(text) |
| Validate named record | validate(name, value) returns value | validate(name, &value) → Result&lt;()&gt; | validate(name, value) returns value |
| Canonical JSON | canonical(value): string | canonical(&value) → Result&lt;String&gt; | canonical(value) → str |
| Action fingerprint | fingerprint(action): string | fingerprint(&action) → Result&lt;String&gt; | fingerprint(action) → str |
| General canonical digest | sqlite.digest(value) | digest(&value) → Result&lt;String&gt; | core.digest(value) → str |
| Compare delegation | compareGrants(parent, child) | compare_grants(&parent, &child) → Containment | compare_grants(parent, child) |
| Create state | createMission(contract): Snapshot | create_mission(&contract) → Result&lt;Snapshot&gt; | create_mission(contract) → dict |
| Apply pure command | reduce(state, command, nowMs) | reduce(&state, &command, now_ms) → Result&lt;Snapshot&gt; | reduce(state, command, now_ms) |
| Assess completed result | assessSuccess(state, runId) | assess_success(&state, run_id) → Result&lt;Value&gt; | assess_success(state, run_id) |
| Replay events | auditPlayback(contract, events) | audit_playback(&contract, &events) → Result&lt;Snapshot&gt; | audit_playback(contract, events) |
| Compile a data validator | dataValidator(schema): (data) → boolean | data_validator(&schema) → Result&lt;Validator&gt; | data_validator(schema) → Draft202012Validator |
| Enforce a data schema | validateData(schema, data): void | validate_data(&schema, &data) → Result&lt;()&gt; | validate_data(schema, data) → None |

The Rust checked records also expose from_value(Value), parse(&str) and as_value(). These include Contract, Command, Grant, Approval, Action, TriggerDefinition, TriggerEvent and WorkflowGraph. Snapshot offers as_value() for inspection, rather than public mutable fields. TypeScript types and Python dictionary aliases still require runtime validation for untrusted input.

Containment is CONTAINED, NOT_CONTAINED or INDETERMINATE. Only confirmed containment permits delegated scope. Pure reduction returns a new snapshot; a rejected command must not advance its revision. Result assessment returns verifiedSuccessful and reasons; do not infer success solely from one terminal-state string.

## Protocol correlation helpers

These helpers are **current source additions after the previously built SDK 0.3.0 binary baseline**. They correlate governed AIWS/Praxis identity with an external protocol binding; they do not implement protocol transport and cannot grant authority.

| Purpose | TypeScript | Rust | Python |
|---|---|---|---|
| Binding identity record | `ProtocolBindingRef` | `ProtocolBindingRef` | validated dictionary passed to `protocol_correlation` |
| Operation/remote identity record | `ProtocolOperationRef` | `ProtocolOperationRef` | validated dictionary passed to `protocol_correlation` |
| Build checked correlation | `protocolCorrelation(binding, operation)` | `protocol_correlation(binding, operation)` | `protocol_correlation(binding, operation)` |
| Validate existing correlation | `validateProtocolCorrelation(value)` | `validate_protocol_correlation(&value)` | `validate_protocol_correlation(value)` |

Every checked correlation has profile `aiws-protocol-correlation/1` and literal `authoritative: false`. The record contains binding ID/revision, exact protocol/version, manifest/capability digests, local work/run/task/operation/attempt identity and optional remote identity/operation references. It has no dispatch, cancellation, reconciliation, verification or acceptance method.

## Workflow helpers

| TypeScript | Rust | Python | Result and ownership |
|---|---|---|---|
| validateGraph(value) | workflow::validate_graph(&value) | workflow.validate_graph(value) | Checked graph; structural and semantic validation |
| readyNodes(state, runId) | workflow::ready_nodes(&state, run_id) | workflow.ready_nodes(state, run_id) | Eligible node IDs; no dispatch |
| dueSlots(start, interval, nextSlot, now, policy, limit) | workflow::due_slots(start, interval, next_slot, now, policy, limit) | workflow.due_slots(start, interval, next_slot, now, policy, limit) | Low-level due-slot calculation; use tickTrigger for durable cursor/admission semantics |

The slot helper has language-specific low-level numeric types. It is not a cross-language wire command. Applications normally use tickTrigger, which commits admission with the cursor, rather than independently computing slots and guessing a persisted state update.

## SQLite store

| Operation | TypeScript | Rust | Python |
|---|---|---|---|
| Create/verify mission database | new SqliteStore(path, contract) | SqliteStore::open(path, Some(&contract)) | SqliteStore(path, contract) |
| Reopen existing mission | new SqliteStore(path) | SqliteStore::open(path, None) | SqliteStore(path) |
| Read verified state | store.snapshot() | store.snapshot()? | store.snapshot() |
| Commit a trusted command | store.apply(command, nowMs, expectedRevision?) | store.apply(&command, now_ms, expected_revision)? | store.apply(command, now_ms, expected_revision=None) |
| Export audit text | store.exportAudit() | store.export_audit()? | store.export_audit() |
| Replay audit text | importAudit(text) from /sqlite | sqlite::import_audit(text)? | sqlite.import_audit(text) |
| Read rejection stream | store.diagnostics() | store.diagnostics()? | store.diagnostics() |
| Release resources | store.close() | Drop store after outstanding work finishes | store.close() or a with block |

apply returns the committed Snapshot. Expected revisions are decimal strings (Option&lt;&str&gt; in Rust); a mismatch is REVISION_CONFLICT. Reload and reevaluate policy before retrying a changed-state command. A pre-commit hook is available for fault-injection tests: TypeScript/Python use the extra beforeCommit/before_commit argument, Rust uses apply_with_hook. It is not a public transaction handle for atomically inserting a custom engine queue.

recordRejection/record_rejection writes a protected diagnostic record and telemetry intent. It does not authorize a command or advance mission revision. Most applications let Coordinator use this hook; avoid arbitrary user-provided diagnostic content. Telemetry methods and their arguments are listed under [observability](https://www.lril.ai/observability/).

## Coordinator and adapters

| Operation | TypeScript | Rust | Python |
|---|---|---|---|
| Construct | new Coordinator(store, authorize, clock) | Coordinator::new(store, authorizer, clock) | Coordinator(store, authorize, clock) |
| Authorized command | await coordinator.apply(command, expectedRevision?) | coordinator.apply(&command, expected)? | coordinator.apply(command, expected_revision=None) |
| Execute admitted attempt | await coordinator.dispatch(attemptId, adapter, nodeId?) | coordinator.dispatch(attempt_id, &mut adapter, node_id)? | coordinator.dispatch(attempt_id, adapter, node_id=None) |
| Read back uncertainty | await coordinator.reconcile(attemptId, adapter) | coordinator.reconcile(attempt_id, &mut adapter)? | coordinator.reconcile(attempt_id, adapter) |
| Continue with ownership boundary | await coordinator.recover(runId, segmentId) | coordinator.recover(run_id, segment_id)? | coordinator.recover(run_id, segment_id) |

apply, dispatch and reconcile return a Snapshot. recover returns `{state, unresolved}` in TypeScript and Python, and a (Snapshot, Vec&lt;String&gt;) tuple in Rust. It records a new continuation segment and reports unresolved attempt IDs; it does not call the adapter or decide that a machine should restart automatically.

Authorization contains allowed, policy, mandatoryChecksOk and context. Context requires actor, policyRevision and decisionClass. The host supplies authenticated identity and returns a current decision. The clock returns UTC epoch milliseconds as a canonical decimal string. Rust uses Authorizer and Clock traits; Python uses a callable; TypeScript uses a promise-returning callback.

EffectAdapter.execute performs the side effect. EffectAdapter.reconcile reads external state without repeating the side effect. Both return Outcome with effect = CONFIRMED_APPLIED, CONFIRMED_NOT_APPLIED or UNKNOWN, and actualCost as a decimal string. TypeScript methods return promises; Rust and Python are synchronous. A thrown execute error becomes UNKNOWN, preserving exposure; a failed reconciliation remains an intervention problem, not permission to execute again.

## Errors and concurrency

TypeScript and Python raise AiwsError with code; Rust returns Result with AiwsError.code. I/O failures can also require host-level handling. Build error handling around phase, committed state and the stable code, not around matching a stack trace. Read [troubleshooting](https://www.lril.ai/troubleshooting/) before deciding that an exception is safe to retry.

Async TypeScript does not make SQLite calls nonblocking: DatabaseSync blocks its thread. Use suitable workers for synchronous database or adapter calls in a server. Do not share a Python SQLite connection across arbitrary threads or assume the Rust coordinator is a distributed execution lock. All three implementations require an application concurrency and ownership design.


## M3 source: nested resource ledger

Import TypeScript `LimitStore`/`LimitCoordinator` from `@aiws/sdk/limits`, Python equivalents from `aiws.limits_store`, and Rust equivalents from `aiws_sdk::limits_store`. Pure transitions are in the corresponding limits module. `LimitStore(path, config)` (Rust `LimitStore::open`) initializes a human-approved immutable scope tree; reopen without config to replay. `snapshot`, `apply`, `report` and `exportEvents` (Python/Rust `export_events`) expose committed state, commands, deterministic exhaustion reports and journal evidence.

Commands: reserve, dispatch, release, unknown, stop, settle and adjust. Request fields are requestId, expectedRevision and command. Raw apply is a trusted/offline interface; use the coordinator for application requests. Authorizers bind actor identity, current policy, scope/purpose, ownership and evidence to the exact command. Stop/settle need evidence; uncertain-outcome resolution and limit adjustments need authenticated humans. See [budgets](https://www.lril.ai/budgets/) for equivalent executable examples, accounting rules and required host ordering.


---

# Records, commands and strict input

The shared schema is JSON Schema Draft 2020-12 in spec/schema.json. Its closed records are the cross-language wire contract. The [generated wire reference](https://www.lril.ai/wire-reference/) lists every record, command and field. The [API reference](https://www.lril.ai/api-reference/) maps the native entry points.

## Parse once at the boundary

**Parse untrusted JSON and canonicalize action material** — Executable example

### Typescript example: strict-json

```typescript
import assert from 'node:assert/strict';
import {parseStrict, fingerprint} from '@aiws/sdk';
assert.throws(() => parseStrict('{"id":"a","id":"b"}'), (e: any) => e.code === 'DUPLICATE_KEY');
const action = {capability:'write',resource:'doc',payload:{value:1},preconditions:{revision:1}};
console.log(fingerprint(action));

```

### Rust example: strict-json

```rust
use aiws_sdk::*;
use serde_json::json;
fn main() -> Result<()> {
    assert_eq!(parse_strict(r#"{"id":"a","id":"b"}"#).unwrap_err().code, "DUPLICATE_KEY");
    let action = Action::from_value(json!({"capability":"write","resource":"doc","payload":{"value":1},"preconditions":{"revision":1}}))?;
    println!("{}", fingerprint(&action)?);
    Ok(())
}

```

### Python example: strict-json

```python
from aiws import parse_strict, fingerprint, AiwsError
try:
    parse_strict('{"id":"a","id":"b"}')
    raise AssertionError('expected duplicate-key rejection')
except AiwsError as error:
    assert error.code == 'DUPLICATE_KEY'
action = dict(capability='write',resource='doc',payload={'value':1},preconditions={'revision':1})
print(fingerprint(action))

```

This example intentionally rejects duplicate JSON keys and verifies canonical action fingerprinting. Parsing with an ordinary JSON decoder first can discard duplicate-key evidence. Hand the original text to parseContract/Contract::parse/parse_contract or parseCommand/Command::parse/parse_command. Validate already-trusted native values with checked record constructors before using them.

The strict parser bounds input to 1 MiB, nesting to 64, and JSON integer literals to the interoperable safe-integer range. Decimal accounting values and timestamps are strings, not floating-point numbers. Canonical unsigned decimal strings have no plus sign, leading zeroes or decimal point; the general quantity schema permits up to 100 digits. Specific telemetry APIs have tighter numeric limits.

## Command families

| Family | Commands | What the family does not do |
|---|---|---|
| Mission/run | startRun, continueRun, activatePlan, missionState, transition | Start a process or change an attached graph |
| Authority | grant, revokeGrant, approve, withdrawApproval | Authenticate a person or silently elevate an agent |
| Effects | admit, dispatch, settle, retry | Prove an external effect without adapter evidence |
| Waits | wait, resume, join | Send notifications or run a background timer |
| Outcomes | verify, accept, invalidate | Execute the tests named in an evidence record |
| Triggers | registerTrigger, fireTrigger, tickTrigger | Operate an ingress server or cron daemon |
| Graph | registerGraph, attachGraph, completeNode | Automatically schedule ready nodes |

Every command optionally carries observation context; the coordinator replaces it with trusted authorizer attribution. The wire schema alone does not prove permission. The reducer also enforces state-dependent invariants not expressible by the structural schema.

## Application data schemas

Node inputSchema and outputSchema validate data supplied to the relevant checkpoint. The supported subset deliberately excludes remote reference fetching and unsupported constructs. A schema that is invalid or outside the supported subset is an error, not a best-effort validation request. Review data_validator/validate_data in the selected SDK and pin fixtures for the schemas your application relies on.

**Validate task input before dispatch** — Executable control simulation

### Typescript example: node-schema

```typescript
import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';

// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}));
let state = createMission(contract);

// Step 1
state = reduce(state, parseCommand(JSON.stringify({
  "type": "startRun",
  "runId": "r"
})), '10');

// Step 2
state = reduce(state, parseCommand(JSON.stringify({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        },
        "inputSchema": {
          "type": "object",
          "required": [
            "requiredField"
          ]
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
})), '10');

// Step 3
state = reduce(state, parseCommand(JSON.stringify({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
})), '10');

// Step 4
state = reduce(state, parseCommand(JSON.stringify({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
})), '10');

// Step 5
state = reduce(state, parseCommand(JSON.stringify({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
})), '10');

// Step 6
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
})), '10'), (error: any) => error.code === 'DATA_SCHEMA');
console.log('PASS: node-schema');

```

### Rust example: node-schema

```rust
use aiws_sdk::*;
use serde_json::json;

fn main() -> Result<()> {
    // Trusted simulation: no external calls.
    let contract = Contract::from_value(json!({
  "id": "c",
  "missionId": "m",
  "aiwsEdition": "0.4",
  "profile": "finite-v1",
  "budget": "100",
  "deadline": "10000",
  "criteria": [
    "review"
  ],
  "actions": [
    "write"
  ],
  "resources": [
    "doc"
  ],
  "requiresApproval": false,
  "maxAttempts": "2",
  "maxAuthorizationAgeMs": "10",
  "features": [
    "triggers",
    "graphs"
  ]
}))?;
    let mut state = create_mission(&contract)?;

    // Step 1
    state = reduce(&state, &Command::from_value(json!({
  "type": "startRun",
  "runId": "r"
}))?, "10")?;

    // Step 2
    state = reduce(&state, &Command::from_value(json!({
  "type": "registerGraph",
  "graph": {
    "id": "graph",
    "revision": "1",
    "nodes": [
      {
        "id": "task",
        "kind": "TASK",
        "dependsOn": [],
        "config": {
          "executionKind": "FUNCTION"
        },
        "inputSchema": {
          "type": "object",
          "required": [
            "requiredField"
          ]
        }
      },
      {
        "id": "end",
        "kind": "END",
        "dependsOn": [
          "task"
        ],
        "config": {}
      }
    ]
  }
}))?, "10")?;

    // Step 3
    state = reduce(&state, &Command::from_value(json!({
  "type": "attachGraph",
  "runId": "r",
  "graphId": "graph"
}))?, "10")?;

    // Step 4
    state = reduce(&state, &Command::from_value(json!({
  "type": "grant",
  "grant": {
    "id": "g",
    "subject": "agent",
    "profile": "finite-v1",
    "actions": [
      "write"
    ],
    "resources": [
      "doc"
    ],
    "notBefore": "0",
    "expiresAt": "10000",
    "limit": "100",
    "canDelegate": true,
    "depth": "2"
  }
}))?, "10")?;

    // Step 5
    state = reduce(&state, &Command::from_value(json!({
  "type": "admit",
  "runId": "r",
  "operationId": "o",
  "attemptId": "a",
  "grantId": "g",
  "subject": "agent",
  "action": {
    "capability": "write",
    "resource": "doc",
    "payload": {
      "value": 1
    },
    "preconditions": {
      "revision": 1
    }
  },
  "amount": "10",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true
}))?, "10")?;

    // Step 6
    let rejected = Command::from_value(json!({
  "type": "dispatch",
  "attemptId": "a",
  "authorizationCheckedAt": "10",
  "policy": "ALLOW",
  "mandatoryChecksOk": true,
  "nodeId": "task"
})).and_then(|command| reduce(&state, &command, "10"));
    assert_eq!(rejected.unwrap_err().code, "DATA_SCHEMA");
    println!("PASS: node-schema");
    Ok(())
}

```

### Python example: node-schema

```python
from aiws import create_mission, reduce, AiwsError

# Trusted simulation: this example makes no external calls.
contract = {'id': 'c',
 'missionId': 'm',
 'aiwsEdition': '0.4',
 'profile': 'finite-v1',
 'budget': '100',
 'deadline': '10000',
 'criteria': ['review'],
 'actions': ['write'],
 'resources': ['doc'],
 'requiresApproval': False,
 'maxAttempts': '2',
 'maxAuthorizationAgeMs': '10',
 'features': ['triggers', 'graphs']}
state = create_mission(contract)

# Step 1
state = reduce(state, {'type': 'startRun', 'runId': 'r'}, '10')

# Step 2
state = reduce(state, {'type': 'registerGraph',
 'graph': {'id': 'graph',
           'revision': '1',
           'nodes': [{'id': 'task',
                      'kind': 'TASK',
                      'dependsOn': [],
                      'config': {'executionKind': 'FUNCTION'},
                      'inputSchema': {'type': 'object',
                                      'required': ['requiredField']}},
                     {'id': 'end',
                      'kind': 'END',
                      'dependsOn': ['task'],
                      'config': {}}]}}, '10')

# Step 3
state = reduce(state, {'type': 'attachGraph', 'runId': 'r', 'graphId': 'graph'}, '10')

# Step 4
state = reduce(state, {'type': 'grant',
 'grant': {'id': 'g',
           'subject': 'agent',
           'profile': 'finite-v1',
           'actions': ['write'],
           'resources': ['doc'],
           'notBefore': '0',
           'expiresAt': '10000',
           'limit': '100',
           'canDelegate': True,
           'depth': '2'}}, '10')

# Step 5
state = reduce(state, {'type': 'admit',
 'runId': 'r',
 'operationId': 'o',
 'attemptId': 'a',
 'grantId': 'g',
 'subject': 'agent',
 'action': {'capability': 'write',
            'resource': 'doc',
            'payload': {'value': 1},
            'preconditions': {'revision': 1}},
 'amount': '10',
 'authorizationCheckedAt': '10',
 'policy': 'ALLOW',
 'mandatoryChecksOk': True}, '10')

# Step 6
try:
    reduce(state, {'type': 'dispatch',
     'attemptId': 'a',
     'authorizationCheckedAt': '10',
     'policy': 'ALLOW',
     'mandatoryChecksOk': True,
     'nodeId': 'task'}, '10')
    raise AssertionError('expected DATA_SCHEMA')
except AiwsError as error:
    assert error.code == 'DATA_SCHEMA'
print('PASS: node-schema')

```

The example checks that a bad node input is rejected before the effect is dispatched. Schema validation establishes shape, not safety of executing a shell command contained in a string, authenticity of a URI or integrity of the bytes stored at that URI.

## Evolution

Unknown commands, fields, features and editions are rejected. Do not monkey-patch the schema to make an engine proposal appear available. Keep application handoff manifests outside closed SDK commands; store their references in permitted result material. A schema/profile change requires compatible implementations and shared fixtures in all three languages.


---

# Wire schema reference

Generated from spec/schema.json for SDK 0.3.0 / proposed standard 0.4. Field tables describe structure; state-dependent authorization, ordering and proof requirements also apply. All command families include optional Context. JSON below is schema data, not language-specific application code. Use the tabbed tutorials for executable native programs.

[Download the exact schema](https://www.lril.ai/downloads/aiws-schema-0.4.json).

## Contract

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| id | Yes | string; minLength=1; maxLength=256 |
| missionId | Yes | string; minLength=1; maxLength=256 |
| aiwsEdition | Yes | constant "0.4" |
| profile | Yes | constant "finite-v1" |
| budget | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| deadline | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| criteria | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| actions | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| resources | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| requiresApproval | Yes | boolean |
| maxAttempts | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| maxAuthorizationAgeMs | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| features | Yes | array of "triggers" / "graphs"; uniqueItems=true |

<details>
<summary>Exact Contract schema</summary>

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "missionId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "aiwsEdition": {
      "const": "0.4"
    },
    "profile": {
      "const": "finite-v1"
    },
    "budget": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "deadline": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "criteria": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "actions": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "resources": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "requiresApproval": {
      "type": "boolean"
    },
    "maxAttempts": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "maxAuthorizationAgeMs": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "features": {
      "type": "array",
      "items": {
        "enum": [
          "triggers",
          "graphs"
        ]
      },
      "uniqueItems": true
    }
  },
  "required": [
    "id",
    "missionId",
    "aiwsEdition",
    "profile",
    "budget",
    "deadline",
    "criteria",
    "actions",
    "resources",
    "requiresApproval",
    "maxAttempts",
    "maxAuthorizationAgeMs",
    "features"
  ],
  "additionalProperties": false
}
```

</details>

## Grant

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| id | Yes | string; minLength=1; maxLength=256 |
| parentId | No | string; minLength=1; maxLength=256 |
| subject | Yes | string; minLength=1; maxLength=256 |
| profile | Yes | string; minLength=1; maxLength=256 |
| actions | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| resources | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| notBefore | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| expiresAt | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| limit | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| canDelegate | Yes | boolean |
| depth | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |

<details>
<summary>Exact Grant schema</summary>

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "parentId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "subject": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "profile": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "actions": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "resources": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "notBefore": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "expiresAt": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "limit": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "canDelegate": {
      "type": "boolean"
    },
    "depth": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    }
  },
  "required": [
    "id",
    "subject",
    "profile",
    "actions",
    "resources",
    "notBefore",
    "expiresAt",
    "limit",
    "canDelegate",
    "depth"
  ],
  "additionalProperties": false
}
```

</details>

## Approval

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| id | Yes | string; minLength=1; maxLength=256 |
| runId | Yes | string; minLength=1; maxLength=256 |
| approver | Yes | string; minLength=1; maxLength=256 |
| fingerprint | Yes | string; minLength=1; maxLength=256 |
| expiresAt | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| uses | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |

<details>
<summary>Exact Approval schema</summary>

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "approver": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "fingerprint": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "expiresAt": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "uses": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    }
  },
  "required": [
    "id",
    "runId",
    "approver",
    "fingerprint",
    "expiresAt",
    "uses"
  ],
  "additionalProperties": false
}
```

</details>

## Action

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| capability | Yes | string; minLength=1; maxLength=256 |
| resource | Yes | string; minLength=1; maxLength=256 |
| payload | Yes | object |
| preconditions | Yes | object |

<details>
<summary>Exact Action schema</summary>

```json
{
  "type": "object",
  "properties": {
    "capability": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "resource": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "payload": {
      "type": "object"
    },
    "preconditions": {
      "type": "object"
    }
  },
  "required": [
    "capability",
    "resource",
    "payload",
    "preconditions"
  ],
  "additionalProperties": false
}
```

</details>

## Trigger

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| id | Yes | string; minLength=1; maxLength=256 |
| revision | Yes | string; minLength=1; maxLength=256 |
| kind | Yes | "MANUAL" / "SCHEDULED" / "EVENT" / "RESOURCE_CHANGE" / "CONDITION" / "WORKFLOW_LIFECYCLE" / "EXTERNAL_RESPONSE" |
| source | Yes | string; minLength=1; maxLength=256 |
| eventType | Yes | string; minLength=1; maxLength=256 |
| contractId | Yes | string; minLength=1; maxLength=256 |
| action | Yes | "START_RUN" / "RESUME_WAIT" |
| waitId | No | string; minLength=1; maxLength=256 |
| enabled | Yes | boolean |
| notBefore | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| expiresAt | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| maxAgeMs | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| maxConcurrent | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| rateLimit | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| rateWindowMs | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| maxDepth | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| conditionMode | No | "EDGE" / "LEVEL" |
| schedule | No | object; closed object |
| filter | No | object; closed object |

<details>
<summary>Exact Trigger schema</summary>

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "revision": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "kind": {
      "enum": [
        "MANUAL",
        "SCHEDULED",
        "EVENT",
        "RESOURCE_CHANGE",
        "CONDITION",
        "WORKFLOW_LIFECYCLE",
        "EXTERNAL_RESPONSE"
      ]
    },
    "source": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "eventType": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "contractId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "action": {
      "enum": [
        "START_RUN",
        "RESUME_WAIT"
      ]
    },
    "waitId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "enabled": {
      "type": "boolean"
    },
    "notBefore": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "expiresAt": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "maxAgeMs": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "maxConcurrent": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "rateLimit": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "rateWindowMs": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "maxDepth": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "conditionMode": {
      "enum": [
        "EDGE",
        "LEVEL"
      ]
    },
    "schedule": {
      "type": "object",
      "properties": {
        "startMs": {
          "type": "string",
          "pattern": "^(0|[1-9][0-9]*)$",
          "maxLength": 100
        },
        "intervalMs": {
          "type": "string",
          "pattern": "^(0|[1-9][0-9]*)$",
          "maxLength": 100
        },
        "catchUp": {
          "enum": [
            "SKIP",
            "LATEST",
            "ALL"
          ]
        },
        "maxCatchUp": {
          "type": "string",
          "pattern": "^(0|[1-9][0-9]*)$",
          "maxLength": 100
        }
      },
      "required": [
        "startMs",
        "intervalMs",
        "catchUp",
        "maxCatchUp"
      ],
      "additionalProperties": false
    },
    "filter": {
      "type": "object",
      "properties": {
        "field": {
          "type": "string",
          "minLength": 1,
          "maxLength": 256
        },
        "equals": {}
      },
      "required": [
        "field",
        "equals"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "id",
    "revision",
    "kind",
    "source",
    "eventType",
    "contractId",
    "action",
    "enabled",
    "notBefore",
    "expiresAt",
    "maxAgeMs",
    "maxConcurrent",
    "rateLimit",
    "rateWindowMs",
    "maxDepth"
  ],
  "additionalProperties": false
}
```

</details>

## TriggerEvent

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| id | Yes | string; minLength=1; maxLength=256 |
| source | Yes | string; minLength=1; maxLength=256 |
| type | Yes | string; minLength=1; maxLength=256 |
| occurredAt | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| data | Yes | object |
| correlation | No | string; minLength=1; maxLength=256 |
| depth | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |

<details>
<summary>Exact TriggerEvent schema</summary>

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "source": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "type": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "occurredAt": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "data": {
      "type": "object"
    },
    "correlation": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "depth": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    }
  },
  "required": [
    "id",
    "source",
    "type",
    "occurredAt",
    "data",
    "depth"
  ],
  "additionalProperties": false
}
```

</details>

## Node

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| id | Yes | string; minLength=1; maxLength=256 |
| kind | Yes | "TASK" / "DECISION" / "FORK" / "JOIN" / "LOOP" / "WAIT" / "APPROVAL" / "SUBWORKFLOW" / "VERIFICATION" / "ACCEPTANCE" / "RECONCILIATION" / "COMPENSATION" / "END" |
| dependsOn | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| config | Yes | object |
| inputSchema | No | object |
| outputSchema | No | object |

<details>
<summary>Exact Node schema</summary>

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "kind": {
      "enum": [
        "TASK",
        "DECISION",
        "FORK",
        "JOIN",
        "LOOP",
        "WAIT",
        "APPROVAL",
        "SUBWORKFLOW",
        "VERIFICATION",
        "ACCEPTANCE",
        "RECONCILIATION",
        "COMPENSATION",
        "END"
      ]
    },
    "dependsOn": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "config": {
      "type": "object"
    },
    "inputSchema": {
      "type": "object"
    },
    "outputSchema": {
      "type": "object"
    }
  },
  "required": [
    "id",
    "kind",
    "dependsOn",
    "config"
  ],
  "additionalProperties": false
}
```

</details>

## Graph

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| id | Yes | string; minLength=1; maxLength=256 |
| revision | Yes | string; minLength=1; maxLength=256 |
| nodes | Yes | array of Node; minItems=1; maxItems=1000 |

<details>
<summary>Exact Graph schema</summary>

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "revision": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "nodes": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Node"
      },
      "minItems": 1,
      "maxItems": 1000
    }
  },
  "required": [
    "id",
    "revision",
    "nodes"
  ],
  "additionalProperties": false
}
```

</details>

## Context

Shared validated record. Consult the concept guide for the trust and lifecycle meaning of its fields.

| Field | Required | Shape and bounds |
|---|---|---|
| actor | Yes | string; minLength=1; maxLength=256 |
| policyRevision | Yes | string; minLength=1; maxLength=256 |
| decisionClass | Yes | "DETERMINISTIC" / "HUMAN" / "MODEL" / "DELEGATED" |
| correlationId | No | string; minLength=1; maxLength=256 |
| causationId | No | string; minLength=1; maxLength=256 |
| traceId | No | string; pattern ^[0-9a-f]{32}$ |
| parentSpanId | No | string; pattern ^[0-9a-f]{16}$ |

<details>
<summary>Exact Context schema</summary>

```json
{
  "type": "object",
  "properties": {
    "actor": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "policyRevision": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "decisionClass": {
      "enum": [
        "DETERMINISTIC",
        "HUMAN",
        "MODEL",
        "DELEGATED"
      ]
    },
    "correlationId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "causationId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "traceId": {
      "type": "string",
      "pattern": "^[0-9a-f]{32}$"
    },
    "parentSpanId": {
      "type": "string",
      "pattern": "^[0-9a-f]{16}$"
    }
  },
  "required": [
    "actor",
    "policyRevision",
    "decisionClass"
  ],
  "additionalProperties": false
}
```

</details>

## startRun

Creates a run record; it does not start a process.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "startRun" |
| runId | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact startRun schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "startRun"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId"
  ],
  "additionalProperties": false
}
```

</details>

## continueRun

Records continuation and changes the recovery ownership epoch. Preserve unsettled exposure.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "continueRun" |
| runId | Yes | string; minLength=1; maxLength=256 |
| segmentId | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact continueRun schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "continueRun"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "segmentId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "segmentId"
  ],
  "additionalProperties": false
}
```

</details>

## activatePlan

Checks the expected current plan revision before activation. This does not mutate an attached graph.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "activatePlan" |
| runId | Yes | string; minLength=1; maxLength=256 |
| planId | Yes | string; minLength=1; maxLength=256 |
| expectedPlan | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact activatePlan schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "activatePlan"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "planId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "expectedPlan": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "planId",
    "expectedPlan"
  ],
  "additionalProperties": false
}
```

</details>

## missionState

Changes mission admission state subject to reducer rules; it is not an operating-system process control.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "missionState" |
| state | Yes | "OPEN" / "SUSPENDED" / "CLOSED" |
| context | No | Context |

<details>
<summary>Exact missionState schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "missionState"
    },
    "state": {
      "enum": [
        "OPEN",
        "SUSPENDED",
        "CLOSED"
      ]
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "state"
  ],
  "additionalProperties": false
}
```

</details>

## grant

Registers a finite grant. The host must enforce authenticated human approval for changes to agent permissions or resource limits.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "grant" |
| grant | Yes | Grant |
| context | No | Context |

<details>
<summary>Exact grant schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "grant"
    },
    "grant": {
      "$ref": "#/$defs/Grant"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "grant"
  ],
  "additionalProperties": false
}
```

</details>

## revokeGrant

Revokes authority. Already-applied effects remain facts that may require separately governed compensation.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "revokeGrant" |
| grantId | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact revokeGrant schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "revokeGrant"
    },
    "grantId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "grantId"
  ],
  "additionalProperties": false
}
```

</details>

## approve

Records approval of an exact action fingerprint; host authentication is required.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "approve" |
| approval | Yes | Approval |
| context | No | Context |

<details>
<summary>Exact approve schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "approve"
    },
    "approval": {
      "$ref": "#/$defs/Approval"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "approval"
  ],
  "additionalProperties": false
}
```

</details>

## withdrawApproval

Withdraws approval for future gated use; it does not undo an effect.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "withdrawApproval" |
| approvalId | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact withdrawApproval schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "withdrawApproval"
    },
    "approvalId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "approvalId"
  ],
  "additionalProperties": false
}
```

</details>

## admit

Reserves exposure and creates an operation/attempt under current finite policy. It does not call an adapter.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "admit" |
| runId | Yes | string; minLength=1; maxLength=256 |
| operationId | Yes | string; minLength=1; maxLength=256 |
| attemptId | Yes | string; minLength=1; maxLength=256 |
| grantId | Yes | string; minLength=1; maxLength=256 |
| subject | Yes | string; minLength=1; maxLength=256 |
| approvalId | No | string; minLength=1; maxLength=256 |
| action | Yes | Action |
| amount | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| authorizationCheckedAt | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| policy | Yes | "ALLOW" / "DENY" / "INDETERMINATE" |
| mandatoryChecksOk | Yes | boolean |
| context | No | Context |

<details>
<summary>Exact admit schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "admit"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "operationId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "attemptId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "grantId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "subject": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "approvalId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "action": {
      "$ref": "#/$defs/Action"
    },
    "amount": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "authorizationCheckedAt": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "policy": {
      "enum": [
        "ALLOW",
        "DENY",
        "INDETERMINATE"
      ]
    },
    "mandatoryChecksOk": {
      "type": "boolean"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "operationId",
    "attemptId",
    "grantId",
    "subject",
    "action",
    "amount",
    "authorizationCheckedAt",
    "policy",
    "mandatoryChecksOk"
  ],
  "additionalProperties": false
}
```

</details>

## dispatch

Commits the attempt claim and rechecks gates. Use Coordinator.dispatch for the external-call boundary.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "dispatch" |
| attemptId | Yes | string; minLength=1; maxLength=256 |
| nodeId | No | string; minLength=1; maxLength=256 |
| authorizationCheckedAt | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| policy | Yes | "ALLOW" / "DENY" / "INDETERMINATE" |
| mandatoryChecksOk | Yes | boolean |
| context | No | Context |

<details>
<summary>Exact dispatch schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "dispatch"
    },
    "attemptId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "nodeId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "authorizationCheckedAt": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "policy": {
      "enum": [
        "ALLOW",
        "DENY",
        "INDETERMINATE"
      ]
    },
    "mandatoryChecksOk": {
      "type": "boolean"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "attemptId",
    "authorizationCheckedAt",
    "policy",
    "mandatoryChecksOk"
  ],
  "additionalProperties": false
}
```

</details>

## settle

Records a reported external outcome and actual cost. UNKNOWN preserves unresolved exposure.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "settle" |
| attemptId | Yes | string; minLength=1; maxLength=256 |
| effect | Yes | "CONFIRMED_APPLIED" / "CONFIRMED_NOT_APPLIED" / "UNKNOWN" |
| actualCost | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| context | No | Context |

<details>
<summary>Exact settle schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "settle"
    },
    "attemptId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "effect": {
      "enum": [
        "CONFIRMED_APPLIED",
        "CONFIRMED_NOT_APPLIED",
        "UNKNOWN"
      ]
    },
    "actualCost": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "attemptId",
    "effect",
    "actualCost"
  ],
  "additionalProperties": false
}
```

</details>

## retry

Prepares a new attempt only when the previous effect is confirmed not applied and limits allow it.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "retry" |
| operationId | Yes | string; minLength=1; maxLength=256 |
| attemptId | Yes | string; minLength=1; maxLength=256 |
| amount | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| authorizationCheckedAt | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| policy | Yes | "ALLOW" / "DENY" / "INDETERMINATE" |
| mandatoryChecksOk | Yes | boolean |
| context | No | Context |

<details>
<summary>Exact retry schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "retry"
    },
    "operationId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "attemptId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "amount": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "authorizationCheckedAt": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "policy": {
      "enum": [
        "ALLOW",
        "DENY",
        "INDETERMINATE"
      ]
    },
    "mandatoryChecksOk": {
      "type": "boolean"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "operationId",
    "attemptId",
    "amount",
    "authorizationCheckedAt",
    "policy",
    "mandatoryChecksOk"
  ],
  "additionalProperties": false
}
```

</details>

## wait

Creates a correlated wait record. It does not change run execution state or start a timer service automatically.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "wait" |
| runId | Yes | string; minLength=1; maxLength=256 |
| waitId | Yes | string; minLength=1; maxLength=256 |
| correlation | Yes | string; minLength=1; maxLength=256 |
| deadline | Yes | string; pattern ^(0\|[1-9][0-9]*)$; maxLength=100 |
| kind | Yes | "INPUT" / "AUTHORIZATION" / "EXTERNAL" / "TIMER" / "RECONCILIATION" |
| context | No | Context |

<details>
<summary>Exact wait schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "wait"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "waitId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "correlation": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "deadline": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)$",
      "maxLength": 100
    },
    "kind": {
      "enum": [
        "INPUT",
        "AUTHORIZATION",
        "EXTERNAL",
        "TIMER",
        "RECONCILIATION"
      ]
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "waitId",
    "correlation",
    "deadline",
    "kind"
  ],
  "additionalProperties": false
}
```

</details>

## resume

Satisfies a matching wait; it does not grant general authority or execute the next step.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "resume" |
| waitId | Yes | string; minLength=1; maxLength=256 |
| correlation | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact resume schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "resume"
    },
    "waitId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "correlation": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "waitId",
    "correlation"
  ],
  "additionalProperties": false
}
```

</details>

## join

Checks the supplied wait set under ALL/ANY semantics and can move a WAITING run to READY.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "join" |
| runId | Yes | string; minLength=1; maxLength=256 |
| waitIds | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| mode | Yes | "ALL" / "ANY" |
| context | No | Context |

<details>
<summary>Exact join schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "join"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "waitIds": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "mode": {
      "enum": [
        "ALL",
        "ANY"
      ]
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "waitIds",
    "mode"
  ],
  "additionalProperties": false
}
```

</details>

## transition

Changes execution state subject to allowed transitions and unsettled-work checks.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "transition" |
| runId | Yes | string; minLength=1; maxLength=256 |
| to | Yes | "READY" / "RUNNING" / "WAITING" / "PAUSED" / "CANCELING" / "COMPLETED" / "FAILED" / "CANCELED" / "REJECTED" |
| context | No | Context |

<details>
<summary>Exact transition schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "transition"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "to": {
      "enum": [
        "READY",
        "RUNNING",
        "WAITING",
        "PAUSED",
        "CANCELING",
        "COMPLETED",
        "FAILED",
        "CANCELED",
        "REJECTED"
      ]
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "to"
  ],
  "additionalProperties": false
}
```

</details>

## verify

Records verification for the current subject and criteria. The host supplies truthful measured evidence.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "verify" |
| runId | Yes | string; minLength=1; maxLength=256 |
| assessor | Yes | string; minLength=1; maxLength=256 |
| revision | Yes | string; minLength=1; maxLength=256 |
| results | Yes | object |
| evidence | Yes | array of string; minLength=1; maxLength=256; maxItems=1000; uniqueItems=true |
| rationale | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact verify schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "verify"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "assessor": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "revision": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "results": {
      "type": "object",
      "additionalProperties": {
        "enum": [
          "PASS",
          "FAIL",
          "INCONCLUSIVE"
        ]
      }
    },
    "evidence": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256
      },
      "uniqueItems": true,
      "maxItems": 1000
    },
    "rationale": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "assessor",
    "revision",
    "results",
    "evidence",
    "rationale"
  ],
  "additionalProperties": false
}
```

</details>

## accept

Records an acceptance disposition for the current subject. The host authenticates the assessor.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "accept" |
| runId | Yes | string; minLength=1; maxLength=256 |
| authority | Yes | string; minLength=1; maxLength=256 |
| verificationRevision | Yes | string; minLength=1; maxLength=256 |
| decision | Yes | "ACCEPTED" / "REJECTED" / "DEFERRED" |
| rationale | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact accept schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "accept"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "authority": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "verificationRevision": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "decision": {
      "enum": [
        "ACCEPTED",
        "REJECTED",
        "DEFERRED"
      ]
    },
    "rationale": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "authority",
    "verificationRevision",
    "decision",
    "rationale"
  ],
  "additionalProperties": false
}
```

</details>

## invalidate

Invalidates an assessment and propagates dependent assessment invalidation.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "invalidate" |
| runId | Yes | string; minLength=1; maxLength=256 |
| reason | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact invalidate schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "invalidate"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "reason": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "reason"
  ],
  "additionalProperties": false
}
```

</details>

## tickTrigger

Computes and admits scheduled slots with the durable cursor in the same command transaction.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "tickTrigger" |
| triggerId | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact tickTrigger schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "tickTrigger"
    },
    "triggerId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "triggerId"
  ],
  "additionalProperties": false
}
```

</details>

## registerTrigger

Registers a fixed trigger definition. No updateTrigger or disableTrigger command exists in this profile.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "registerTrigger" |
| trigger | Yes | Trigger |
| context | No | Context |

<details>
<summary>Exact registerTrigger schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "registerTrigger"
    },
    "trigger": {
      "$ref": "#/$defs/Trigger"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "trigger"
  ],
  "additionalProperties": false
}
```

</details>

## fireTrigger

Validates an event against its trigger and retains receipt identity. The host authenticates the event source.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "fireTrigger" |
| triggerId | Yes | string; minLength=1; maxLength=256 |
| event | Yes | TriggerEvent |
| runId | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact fireTrigger schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "fireTrigger"
    },
    "triggerId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "event": {
      "$ref": "#/$defs/TriggerEvent"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "triggerId",
    "event",
    "runId"
  ],
  "additionalProperties": false
}
```

</details>

## registerGraph

Validates and registers the graph; this does not attach it or execute nodes.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "registerGraph" |
| graph | Yes | Graph |
| context | No | Context |

<details>
<summary>Exact registerGraph schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "registerGraph"
    },
    "graph": {
      "$ref": "#/$defs/Graph"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "graph"
  ],
  "additionalProperties": false
}
```

</details>

## attachGraph

Attaches a graph before graph-bound work. The current attached graph cannot be mutated.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "attachGraph" |
| runId | Yes | string; minLength=1; maxLength=256 |
| graphId | Yes | string; minLength=1; maxLength=256 |
| context | No | Context |

<details>
<summary>Exact attachGraph schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "attachGraph"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "graphId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "graphId"
  ],
  "additionalProperties": false
}
```

</details>

## completeNode

Records a node checkpoint after node-kind proof and schema checks. Completion does not imply final acceptance.

| Field | Required | Shape and bounds |
|---|---|---|
| type | Yes | constant "completeNode" |
| runId | Yes | string; minLength=1; maxLength=256 |
| nodeId | Yes | string; minLength=1; maxLength=256 |
| result | Yes | object |
| context | No | Context |

<details>
<summary>Exact completeNode schema</summary>

```json
{
  "type": "object",
  "properties": {
    "type": {
      "const": "completeNode"
    },
    "runId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "nodeId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "result": {
      "type": "object"
    },
    "context": {
      "$ref": "#/$defs/Context"
    }
  },
  "required": [
    "type",
    "runId",
    "nodeId",
    "result"
  ],
  "additionalProperties": false
}
```

</details>


---

# Implementation profile

This release targets the [AIWS-001 edition 0.4](https://www.lril.ai/standard/) **proposal**. The standard specifies responsibilities shared by definition authors, runtimes, enforcement boundaries, assessors and operators. Installing this SDK is not certification of an application or of every requirement in the standard.

## Implemented surface

All three SDKs implement the same closed JSON command schema, canonical action fingerprints, finite-set grant attenuation, mission-wide accounting, operation/attempt identities, material-bound approvals, current authorization checks, recovery fencing, explicit waits, verification, acceptance, invalidation, trigger admission and graph execution checkpoints. All three include native SQLite stores and coordinators. Rust runs independently of Node.

The shared schema is `spec/schema.json`, JSON Schema Draft 2020-12. Unknown command types, fields, editions and features are rejected. Runtime shape checks apply even if application code bypasses TypeScript's compiler. Rust record constructors return `Result` and preserve checked JSON internally.

Numeric accounting and UTC epoch millisecond fields use canonical unsigned decimal strings of at most 100 digits. JSON numeric literals must be safe integers; fractional values and unsafe integers are rejected. Amounts are abstract integer units chosen by the contract owner, not floating-point currency. IDs are opaque strings; scopes are exact finite sets, without wildcards or implicit inheritance. JSON boundaries reject duplicate keys, comments, trailing commas, excessive nesting and unpaired surrogates. The limit is 1 MiB per input and depth 64. Audit files exceeding 1 MiB require an application transport that streams bounded events through `auditPlayback`/`audit_playback`; raising the parser limit silently is not supported.

## Trigger profile

Supported kinds: MANUAL, SCHEDULED, EVENT, RESOURCE_CHANGE, CONDITION, WORKFLOW_LIFECYCLE and EXTERNAL_RESPONSE. A trigger is immutable under its registered ID and pins the mission's immutable contract. Updating it requires a new trigger identity/revision. The owner and authorization references are supplied by the enclosing application's controlled definition and trusted authorizer; they are not taken from event data.

`fireTrigger` validates source/type, age, validity, enabled status, causal depth, deterministic field equality, rate limit and live-run concurrency. Duplicate identity is `(trigger ID, revision, source, event ID)`; different content under that identity is an error. Receipts and run admission or wait satisfaction commit in one transaction. Receipts are retained for the mission lifetime. A duplicate cannot change the original target run. An external response must match one pending, unexpired wait and its correlation token.

Conditions support EDGE and LEVEL. EDGE fires on false-to-true; a false sample resets it. Sampling and truth acquisition belong to the authenticated producer. The source supplies trusted causal depth; the authorizer must prevent clients from resetting it to evade a bound. A lifecycle-event adapter must preserve parent-event lineage.

Scheduled triggers require `schedule: {startMs, intervalMs, catchUp, maxCatchUp}`. The clock is UTC epoch time and intervals are fixed; calendar cron expressions, time zones and daylight-saving calculations are unsupported. `tickTrigger` calculates occurrence IDs from stable slot numbers and commits its cursor with admitted runs. SKIP omits missed slots; LATEST admits the latest due slot; ALL admits a bounded batch. `maxCatchUp` is 1–1000. The whole tick rolls back on an admission failure, preserving the cursor. Choose batch/rate/concurrency/age limits that permit progress. A scheduler calls `tickTrigger` periodically; the library does not create an operating-system timer or start a daemon. Expired occurrences are rejected, never silently assigned a fresh occurrence time.

Rejected commands return a structured error without changing mission state. A production ingress adapter must durably record, reject or defer failed deliveries before acknowledging its transport, preserving the original event identity on retry. The SDK does not provide HTTP authentication, a Kafka consumer, a filesystem watcher or a dead-letter service. These are deployment bindings with different credentials and delivery semantics.

## Node profile and inherited defaults

Graphs pin an ID/revision and must have unique nodes, valid dependencies, no graph cycles and a path from every node to END. Repetition is expressed by a bounded LOOP. Graph activation is allowed only before operations begin. The graph/contract supply inherited revision, mission authority/budget and deadline. Nodes are required unless deliberately skipped by branching or an ANY join. Optional `inputSchema` and `outputSchema` provide material contracts; their explicit profile default is a JSON object. Record references used by checkpoint nodes are checked against same-run evidence, except the linked child run or original compensated operation. Custom schemas allow local references; remote references, asynchronous validation, dynamic references and format assertions are rejected.

| Kind | Config | Completion evidence |
|---|---|---|
| TASK | `executionKind`: AGENT, FUNCTION, TOOL, HUMAN | Same-run, same-node operation confirmed applied |
| DECISION | `field`, `equals`, `then`, `else` | `data` with the declared field; unselected branch skipped |
| FORK | `{}` | Enables downstream dependencies |
| JOIN | `mode`: ALL or ANY | Required predecessors complete; ANY cannot abandon an active operation |
| LOOP | `maxIterations` positive decimal | Distinct confirmed operations and boolean `done`; exhaustion marks node/run FAILED |
| WAIT | `{}` | `waitId` of a satisfied same-run wait |
| APPROVAL | `{}` | `approvalId` of a current same-run approval; dispatch still checks material binding |
| SUBWORKFLOW | `{}` | `childRunId` with completed execution, passing verification and accepted outcome |
| VERIFICATION | `{}` | Current passing assessment `revision` |
| ACCEPTANCE | `{}` | Current accepted assessment `revision` |
| RECONCILIATION | `{}` | `operationId` with known external effect |
| COMPENSATION | `{}` | New confirmed operation and distinct `originalOperationId` confirmed applied |
| END | `{}` | `disposition: COMPLETED`; run completion is a separate checked transition |

`readyNodes`/`ready_nodes` identifies runnable checkpoints. A graph-bound dispatch must name a ready TASK, LOOP or COMPENSATION node, binds the operation to that node and permits only one active operation per node. A caller cannot dispatch directly past a dependency and later fabricate completion. Retry uses the same logical operation only after confirmed nonapplication and within the contract's attempt limit. No automatic arbitrary graph interpreter, LLM invocation or shell execution is hidden in `completeNode`.

Node input schemas validate the operation action payload before dispatch; output schemas validate the completion result. Business output values can be carried in that result. Provider adapters and the authorizer must validate evidence truth, subworkflow parent lineage and domain material requirements. Schema validity alone is not domain verification. Approval, verification and acceptance remain distinct records.

## Persistence, effects and recovery

SQLite uses WAL, FULL synchronization, BEGIN IMMEDIATE, a hash-chained journal and optional expected revisions. Each store contains one immutable mission. Reads reconstruct state from the journal. Writes validate and append atomically. Hashes detect accidental alteration and enable cross-language comparison; they are not signatures and cannot prevent a database owner from rewriting an entire journal. Use trusted storage permissions and external checkpoints where required. Reference replay is linear in retained history on every transaction; this release prioritizes inspectable semantics over large-history throughput. SQLite backup must include a consistent WAL snapshot; do not copy a live database file alone.

The coordinator requires injected authorization and a clock; there is no permissive default. It checks every command and overwrites the caller's policy fields for admission, retry and dispatch. A checked snapshot revision is compared again inside the write transaction. An authorization failure or concurrent change blocks the action.

DISPATCHED is committed before calling an effect adapter. Exceptions and lost responses leave UNKNOWN with the reservation retained. Reconciliation is an authenticated, read-only lookup of the external effect. UNKNOWN is never automatically retried. Recovery increments the run's ownership epoch and returns unresolved attempts without calling an adapter. Prepared work from an old epoch is fenced; an operator can settle a never-dispatched attempt as confirmed not applied and explicitly admit a safe retry.

Audit playback/import executes no adapters. It verifies sequence and reconstructs semantic state; it never silently launches work. A fresh execution requires new admission. Live migration, arbitrary policy languages, remote schemas, target-specific exactly-once extensions and distributed storage plugins are not advertised by this profile.

## Application responsibilities

The application supplies authenticated actors and event sources, authoritative policy inputs, credential custody, egress enforcement, evidence assessment, calendar/transport integrations, durable rejected-delivery handling and operational monitoring. It must prevent untrusted callers from using raw store/reducer methods or an unrestricted second network client. Node `executionKind` and a Git worktree describe execution organization; neither creates a security sandbox. Treat adapters as trusted components and apply their own timeout/cancellation/target-fencing policies.

These responsibilities remain explicit integration interfaces. M9.1 provides the verified common, version-pinned Praxis protocol-binding contract. M9.2/M9.3 verify synchronous MCP 2026-07-28 and durable Tasks/recovery against pinned official MCP packages. M9.4/M9.5 verify A2A 1.0 in both directions: outbound Agent Card/skill pinning, durable task identity and resubscription, plus an authenticated/authorized Praxis Agent Card server with durable inbound identity, official AgentExecutor/HTTP+JSON streaming and terminal-task immutability. M9.6 verifies projection-only AG-UI 1.0 run/step/message/tool/state/activity/subagent events against pinned official `@ag-ui/core@1.0.0`; the projection cannot mutate Praxis state or create approval/control records. External protocol events remain non-authoritative. AG-UI authenticated human-control ingress is verified in M9.7: standard interrupt/resume maps to the existing authenticated Praxis control service with current-material fencing, durable idempotency and receipt recovery. M9.8 verifies one durable cross-protocol registry with explicit binding revisions, operation identity/evidence correlation, stale-result fencing and allowlisted hashed-identity telemetry. M9.9 verifies the checked-in pinned compatibility manifest and executable cross-protocol campaign: MCP 2026-07-28, MCP Tasks 2026-07-28, A2A 1.0 and AG-UI 1.0 all passed together across 16 required scenarios. M9.10 SDK/documentation/visual consolidation is complete for the scoped source interoperability profile. See `COVERAGE.md`, `TEST-REPORT.md` and `docs/M9-BUILD-PLAN.md` for tested behavior and remaining interoperability/deployment work.

## Release 0.3.0 observability extension

Native Python is an independent implementation of the same finite-v1 contract, graph, triggers, coordinator and SQLite journal. `OBSERVABILITY.md` defines the shared event, metric and outbox behavior. Coordinator authorizers now return authenticated actor, policy revision and decision class; caller context is replaced. Direct reducer/store calls without context are explicitly marked unattributed and are restricted to trusted/offline use. Application identity and policy truth are not supplied by the SDK.

Enriched edition 0.4 journals are not compatible with older event hashes. Keep historical journals with their original SDK. Review and reconcile any migration into a new linked contract. Alert notification delivery, scheduling and acknowledgements are deployment integrations; SDK evaluation does not automatically notify or remediate.


## Opt-in M3 source profile

`aiws-limits/1` adds a separate shared ledger for hierarchical cost, summed active time, elapsed time and attempts, with protected handoff funds inside total cost. All three native SDKs expose durable reservations and authenticated-host coordinators. It does not change finite-v1 contracts or old journal interpretation. Hosts must pair ledger gates with existing mission/handoff dispatch checks; separate databases are not one atomic transaction. See the budgets chapter and `docs/M3-IMPLEMENTATION.md`. Older binary downloads retain their baseline scope.


## M9 protocol source boundary

M9.1–M9.9 are verified in Praxis engine source for the pinned compatibility profiles documented in [M9 completion and compatibility](https://www.lril.ai/engine-m9-completion/). M9.10 adds a source-only `aiws-protocol-correlation/1` helper to all three SDK implementations so application code can carry exact binding revision and remote identity alongside AIWS/Praxis work identity without importing protocol wire schemas.

The helper is non-authoritative and is not part of the previously built SDK 0.3.0 binary archives. Official MCP/A2A/AG-UI packages remain host-owned at the Praxis adapter boundary. M8 still governs release/platform/production qualification.


---

# Praxis decisions and implementation boundary

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

This page records the accepted Praxis design. M5 source implementation is in progress; see [SQLite persistence and recovery tests](https://www.lril.ai/engine-persistence/) for executable APIs and current evidence. A [local composed workflow](https://www.lril.ai/engine-coding-workflow/) now runs through acceptance; the complete interface and conformance scope remains unfinished. SDK documentation elsewhere describes the released finite-v1 library behavior. Agent code generation must consult the capability matrix before choosing APIs.

Two interactive diagrams accompany this page: the [system overview](https://www.lril.ai/diagrams/#system-overview) and the [engine internals](https://www.lril.ai/diagrams/#engine-internals).

## Accepted direction

Praxis is independently developed rather than built on another workflow engine. Coding is the primary use case; business processes, document review, scheduling and other non-coding work remain possible. It must run on a developer computer, in Docker or on organizational infrastructure. Execution can last seconds through months. Recovery on another machine is outside the current scope.

A work order is the durable assignment also called a mission. It has its own engine identity, references its governing contract revision, and contains runs and optional child work orders. The mapping is accepted: existing SDK missionId denotes that same assignment, while contract and run IDs remain distinct. See the accepted WO-01 through WO-08 contract below. The current finite-v1 schema is unchanged.

Users configure automatic restart, human-authorized restart or rule-based continuation. Pause behavior is task-aware because some effects cannot be interrupted. Uncertain outcomes require human intervention. Dynamic workflows may include predefined branches, task addition and plan revision, with separately configured permissions and approval rules.

## Limits and authority

Limits may apply at several scopes. Reaching any applicable time, cost or attempt bound blocks new work within that scope. A branch limit holds affected and dependent work; a work-order limit holds all descendants. Independent branches can proceed only while every relevant shared and local bound permits it.

Only humans approve changes to an agent's permissions or resource limits. Workflow reapproval follows configured rules, but those rules cannot be edited by the agent to expand its own authority. A new branch, agent replacement, continuation or handoff cannot reset previously consumed allowance.

The agreed handoff reserve is human-approved and belongs inside the existing budget. Basic checkpoint/report production must remain possible without relying on an extra LLM request at the moment of failure.

## Accepted work-order hierarchy and identity contract — 2026-09-08

**Decision status:** accepted by the project owner. The SHALL rules in this section define the engine design baseline, not implemented SDK 0.3.0 behavior. Protocol representation below is draft `aiws-engine-identity/0.1`; its version is independent of SDK package versions and AIWS editions.

### WO-01 — Assignment and contract separation

Work order and mission SHALL denote the same durable assignment. The engine's canonical term is WorkOrder; applications MAY display Mission. A work order SHALL have its own immutable identity, separate from contract and run identities. It SHALL reference its governing immutable contract revision and MAY have zero runs before execution begins, then one or more runs. Each run SHALL belong to exactly one work order and pin one immutable contract reference. A revised governing contract SHALL preserve contract lineage, prior acceptance records, cumulative consumption and reservations. A run requiring a different contract SHALL be a linked new run; contract replacement SHALL NOT rewrite the original run.

The current SDK field `Contract.missionId` maps to the same value as the engine's `workOrderId`; `Contract.id` remains contract identity. This is a boundary mapping, not permission to insert or rename fields in finite-v1. A work order is a separate engine record, not a second entity above a mission. Multiple existing mission IDs SHALL NOT be silently merged into one work order.

### WO-02 — Containment and dependencies

A work order MAY have child work orders. Every child SHALL have exactly one parent; roots have none. Parentage SHALL be acyclic, immutable after admission, and confined to the declared identity namespace. Cross-work references SHALL be dependencies, not additional parents. Required child work SHALL be identified explicitly for acceptance. A workflow node does not automatically become a child work order; use a child only for an independently governed subassignment. A child run is an execution relationship and does not by itself create a child work order. A node instance identifies a particular loop iteration or fan-out member, not a new workflow definition.

### WO-03 — Identity, retries and revisions

| Identity or reference | Required meaning and lifetime |
|---|---|
| `workOrderId` | Durable assignment; same semantic identity as legacy `missionId` |
| `contractRef` | Immutable contract ID and explicit revision; distinct from assignment ID |
| `workflowRef` | Reusable workflow definition ID and revision |
| `runId` | One execution; preserved on continuation; new for an explicit fresh execution |
| `predecessorRunId` | Previous run in execution lineage; never a replacement for parent work-order identity |
| `planRef`, `graphRef` | Exact plan/graph revisions applicable to a material action |
| `segmentId` | Explicit continuation or ownership boundary within a run |
| `nodeId`, `nodeInstanceId` | Definition node and particular execution occurrence; occurrence differs across loop iterations/fan-out |
| `operationId` | Logical action with immutable material fingerprint; preserved across safe retries |
| `attemptId` | One execution attempt; changes for each actual retry |
| `waitId` and correlation reference | Pending condition and its expected response; survives continuation |
| assessment subject reference | Exact work/evidence revision evaluated; never mutable “latest” |
| `handoffId` and revision | Specific durable transfer manifest and immutable version |
| `deliveryId` | One handoff revision addressed to one consumer occurrence; stable across redelivery |
| external references | Provider, session, model, repository or other foreign IDs; never engine authority |

The engine SHALL assign durable execution IDs and persist them before dispatch or acknowledgement. SDKs SHALL preserve IDs byte-for-byte and use the same case-sensitive wire field names. The tuple `(namespace, record type, id)` SHALL resolve unambiguously. IDs SHALL be opaque nonempty strings; this revision does not mandate a UUID format. References to scoped objects SHALL include their run and definition/revision context where needed. Display names SHALL NOT serve as identity or authorization.

An operation's fingerprint SHALL be immutable. Materially changed work SHALL receive a new operation ID linked to the superseded operation. Unchanged work MAY retain its operation ID across a new plan revision only after recorded compatibility and current authorization checks. Repeated work on different loop iterations/fan-out members SHALL receive distinct operation IDs. A retry SHALL create a new attempt while preserving operation identity and applicable idempotency semantics. Redelivery of the same admission request SHALL return the recorded result rather than create an extra attempt. UNKNOWN effects SHALL require human intervention and reconciliation before any potentially duplicating action.

### WO-04 — Continuation and terminal runs

Resuming a nonterminal interrupted execution SHALL preserve its run ID, waits, completed effects, operation identities and accounting. Recovery or explicit executor ownership transfer SHALL record a new segment and ownership epoch before dispatch. Routine activity within unchanged ownership need not create a segment. Segment creation SHALL NOT refresh grants, approval expiry, attempt counts or budgets.

A terminal run SHALL never be reopened. An explicit fresh execution, including corrective execution following a terminal FAILED run, SHALL create a new run with predecessor lineage and fresh admission. The engine SHALL reconcile prior effects and retain cumulative work-order accounting. Reusing completed outputs requires explicit evidence/compatibility references; starting a new run is not permission to repeat effects. Cross-machine recovery remains out of scope.

### WO-05 — Scoped limits and holds

Limits MAY apply to a work order, run or branch. Admission SHALL check every applicable ancestor and local limit and atomically reserve against shared accounting. Children SHALL NOT create independent copies of a parent's allowance; aggregation SHALL avoid double counting. Branch exhaustion SHALL hold that branch and its dependents; run exhaustion SHALL hold that run and dependent work; work-order exhaustion SHALL hold all its descendants. Unaffected branches MAY proceed only if every applicable shared limit allows them.

A recoverable limit SHALL produce a durable nonterminal hold containing scope, reason, blocking limit revision and decision reference when resolved. Hold is a control record, not a new terminal run state. Multiple holds MAY coexist. Resolving one SHALL NOT clear others or dispatch work automatically. Eligibility SHALL be rechecked under current authority, acceptance basis, ownership and remaining allowances. Permission or resource-limit changes require authenticated human approval. An agent cannot bypass a bound through a new branch, child, run, segment or handoff.

Pause/hold SHALL prevent new ordinary dispatch. Already-committed or non-interruptible effects SHALL follow declared task-aware stopping rules and remain accounted for until settled. Only separately authorized bounded recovery may consume the protected reserve, which remains within the approved total. An expired deadline continues to block admission until an authorized change; merely clearing a hold cannot renew it.

### WO-06 — Handoff ownership and delivery

The engine SHALL persist manifests, delivery intents, claims, acknowledgements and recovery state. Applications supply task-specific content, artifact adapters and summaries. A source completion, its manifest and its delivery intents SHALL commit atomically after durable artifact availability. Every delivery SHALL identify one immutable manifest revision and exact consumer occurrence. Claim ownership SHALL be fenced; acknowledgement SHALL be durable and idempotent. Lost acknowledgements/redelivery SHALL NOT admit duplicate consumer work. Acknowledgement means receipt and validation, never execution success or acceptance. Revised manifests SHALL receive new revisions and new delivery identities; previous consumption history remains intact.

A join SHALL record the exact incoming revisions consumed. Context text SHALL NOT expand authority. Summaries are derived views; authoritative state SHALL remain recoverable without the originating conversation or another model call. The engine-generated basic handoff SHALL exist even when summary generation fails. These rules select engine ownership; they do not require separately deployed services or claim exactly-once external effects.

### WO-07 — Completion and cancellation

To preserve the standard's mission lifecycle, work-order lifecycle remains OPEN, SUSPENDED or CLOSED. Closing SHALL record a separate disposition, COMPLETED or CANCELED. Interfaces MAY display “completed” or “cancelled” from that disposition. A hold on the entire work order blocks ordinary work; a local branch hold does not suspend independent branches.

The COMPLETED disposition SHALL require all required work, including required child work orders, to satisfy the governing acceptance conditions, current verification and required approval. It also SHALL require terminal owned runs, settled reservations and no unresolved material effects. All children SHALL be safely closed before parent closure; optional work need not succeed but cannot remain active or unaccounted for. A successful individual run is insufficient. Acceptance SHALL pin the exact criteria/evidence revisions. Required work SHALL NOT be silently reclassified as optional to force completion.

Cancellation SHALL immediately block new ordinary dispatch and request task-aware stopping. CANCELED closure SHALL wait for safe settlement; while effects remain unresolved, cancellation stays pending and a reconciliation owner SHALL remain recorded. CANCELED SHALL NOT imply acceptance. CLOSED SHALL remain terminal. Later evidence invalidation SHALL append an assessment and invalidate the current acceptance view without rewriting historical closure; corrective work uses a linked newly authorized work order.

### WO-08 — Versioning and compatibility

SDK 0.3.0 finite-v1 remains a closed existing wire profile. Its LOOP exhaustion remains terminal FAILED, its attached graphs remain fixed, and its contracts do not acquire nested work-order fields. The future engine SHALL advertise its new identity/hold/handoff capability explicitly and reject unsupported required semantics. Historical journals SHALL retain their original fields, hashes and interpretation. A compatibility adapter MAY expose `missionId` as `workOrderId` in an external projection, but SHALL NOT alter the original bytes. When both aliases are supplied at that boundary, differing values SHALL be rejected. Legacy import SHALL preserve all known accounting and references and report unavailable revision/parent/delivery data instead of inventing it.

The companion `AIWS-Engine-Identity-Contracts.schema.json` defines draft structural records; `AIWS-Engine-Identity-Examples.json` provides a linked illustrative bundle. These are design artifacts, not finite-v1 commands. Semantic validation SHALL additionally enforce references, cycles, revision pinning, material identity, authorization, accounting, idempotency and closure predicates. Passing JSON Schema alone establishes none of those history-dependent guarantees.

### Required implementation verification

| Scenario | Required result |
|---|---|
| Legacy mission mapped to engine work order | Same assignment ID; original contract and journal bytes unchanged |
| Conflicting mission/work-order aliases | Rejected before persistence |
| Second parent or parent cycle | Rejected; dependencies do not change ownership |
| Resume after interruption | Same run/operation IDs; new recorded segment when recovery changes ownership |
| Resume terminal FAILED run | Rejected; explicit linked new run required |
| Safe retry and changed material input | New attempt for same operation; changed material requires new operation |
| Two fan-out members share node definition | Distinct node-instance/operation IDs; no accidental deduplication |
| Concurrent child reservations | Aggregate parent allowance cannot be oversubscribed |
| Branch hold plus runnable sibling | Held branch/dependents blocked; eligible sibling can proceed |
| Root hold and partial hold resolution | All descendants blocked until all applicable blockers clear |
| Duplicate handoff delivery/acknowledgement | One consumer admission; recorded acknowledgement reused |
| Manifest changes after consumption | New revision/delivery; original evidence basis retained |
| Completed run but missing required acceptance | Work-order completion rejected |
| Cancellation with UNKNOWN effect | Dispatch blocked; cancellation remains pending; no successful completion |
| Later evidence invalidation | Acceptance view invalidated; historical closure remains terminal |
| Python/TypeScript/Rust interchange | Identical wire identities, decimal values and semantic outcomes |

These scenarios are release gates to implement, not a claim of passing engine tests.

## Praxis components and M4 design status

| Component | Responsibility | Current SDK support |
|---|---|---|
| Definition registry | Immutable plans/graphs and compatibility | Graph/plan IDs, closed schemas; no full registry service |
| Admission and policy service | Identity, authority, human-only changes | Authorizer interface and finite grant checks |
| Scheduler | Ready-node dispatch, durable timers and wake-up | readyNodes and fixed trigger ticks; no daemon |
| Execution workers | Agent/tool/function/human task adapters | EffectAdapter interface; no worker fleet |
| State and event store | Atomic state, history and outbox | One-mission SQLite journal |
| Artifact and handoff service | Engine-owned durable material, transfer and acknowledgement | M2 source primitives implemented; full engine orchestration remains M5 |
| Human intervention service | Decisions, hold resolution and escalation | Approval/wait records; no inbox UI |
| Observability service | Traces, metrics and alerts | OTLP projections/outbox and alert evaluation |

The owner accepted a TypeScript/Node.js coordinator with separate worker processes on one machine, native Windows/macOS Intel/Apple Silicon/Linux operation and Docker deployment, human-selected concurrency, SQLite-first storage with optional verified PostgreSQL support, supervised CLI/web controls and pluggable identity/policy adapters. Components are modules within that topology, not separately deployed services. Multiple-machine execution is deferred. OS minimums/architectures and transport/bootstrap rules are now specified in the M4 profile; platform qualification and measured capacity remain implementation gates; Archon integration remains later scope.

## Compatibility work that a Praxis release must resolve

Current LOOP exhaustion is terminal FAILED, while the accepted engine intervention model needs a resumable hold. Current graphs cannot be mutated after attachment. Current contracts are immutable and have no nested work-order budget fields. Settlement and recovery epochs are not a complete distributed target-fencing protocol. The finite-v1 event store replays retained history and has no automatic compaction.

Address these as explicit versioned design changes, with migration and conformance tests. Do not promise them by merely adding documentation examples that call nonexistent methods. The [M1 contract candidate](https://www.lril.ai/downloads/AIWS-Handoff-Contract-v1.md) specifies handoff records, receipt/admission transitions, resumable holds, ownership, migration and 40 acceptance scenarios. WO-01 through WO-08 now settle the work-order hierarchy and identity mapping. The companion identity-only draft schema does not replace the M1 handoff wire schema. M2 handoff APIs and the M3 nested-accounting ledger are implemented as opt-in source profiles. M4 engine architecture is complete as a design specification under the accepted ED-01–ED-15 baseline; M5 implements it. Older 0.3.0 binary packages retain their earlier finite-v1 scope. See the budgets chapter for ledger integration and its conservative cross-database ordering.



## M4 accepted decisions and starting architecture — 2026-09-09

The owner approved all engine recommendations. The [accepted decision record](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/engine/DECISIONS.md) was committed before architecture drafting. It preserves supervised execution, all-ancestor accounting, human-only escalation, conservative UNKNOWN handling, reference-aware retention and credential refresh on resume.

The [M4 design directory](https://github.com/seanrobertwright/AI-WS-SDK/tree/main/spec/engine-v1) contains a finalized architecture, twelve semantic host contracts, a failure-boundary matrix and 50 specified acceptance scenarios. The architecture proposes one transaction for workflow state, reservations and dispatch intent; existing separate SDK stores are not silently reinterpreted as atomic engine storage.

M4 is complete as an engine design specification. Wire codec 1.1.0, owner/worker enrollment, platform minimums, migration/snapshot rules, conformance/clock/capacity plans and the final identity/accounting review are finalized. M5 is the next implementation milestone. Static traceability checks are not engine tests. No running engine, PostgreSQL adapter, platform support certification or new SDK binary is delivered by these documents.

## Finalized M4 protocol, enrollment and compatibility

The [wire contract](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/WIRE.md) defines closed `aiws-engine/1` commands, queries, results, worker IPC and trusted host-adapter DTOs. All SDKs preserve exact IDs and decimal strings. Current codec 1.1.0 has 228 checked shape fixtures (98 accepted, 130 rejected); another 201 fixtures preserve the frozen 1.0.0 design. These do not execute engine behavior. The new codec explicitly carries active/inherited limits, governing contract references and the correct work-order lifecycle/closure disposition.

[Authentication and enrollment](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/AUTHENTICATION.md) require independent verified human identity before owner enrollment, protected one-use setup challenges, scoped sessions, explicit revocation and private launcher-bound worker enrollment. CLI and web share server-side authority and approval checks. Remote human access requires an explicitly configured organization gateway; workers remain on one machine.

[Platform targets](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/PLATFORMS.md) are Node.js 24 LTS with a pinned patched runtime, Windows 11 25H2, macOS 15 on Intel/Apple Silicon, Ubuntu 24.04 LTS and Debian 13 on x64/arm64, plus qualified Linux Docker images. Each native architecture and container image requires installation, process, security and recovery evidence before support is claimed.

[Migration rules](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/MIGRATIONS.md) define engine database version 1, snapshot format `aiws-engine-snapshot/1`, verified offline upgrades and conservative restore. Existing SDK stores can be preserved as read-only legacy archives; they cannot automatically resume execution or start the same assignment with reset budgets. Uncertain external effects block unsafe rollback/resume.

## M4 completed: conformance plan and lifecycle review

The [conformance plan](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/CONFORMANCE.md) defines nine suites with independent accounting/state/provider-effect oracles and 15 fault points. The [clock protocol](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/FAKE-CLOCK.md) supplies 12 deterministic boundary vectors; the [capacity plan](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/CAPACITY.md) specifies eight workload ladders, persistent round-robin fairness, storage estimates and provisional engineering targets. Test load values are not product workflow/task limits or measured capacity claims.

The [completed review](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/REVIEW.md) traces WO-01–WO-08, all 18 M1 requirements and eight M3 accounting groups, with 11 resolved design findings. All 50 semantic scenarios remain specified-only. The [M5 handoff](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/COMPLETION.md) begins with the SQLite unit of work and independent test controller, then supervised admission, local workers, handoffs/recovery, and a real coding workflow. Engine scenarios executed: zero; platform, recovery and capacity qualification remain implementation gates.


## Executable control profile

The [M5 control API](https://www.lril.ai/engine-control-api/) implements a declared subset of wire 1.1.0 over HTTPS with shared CLI/web commands and atomic approval receipts. This is implementation evidence for the local profile; the broader architecture and its remaining qualification obligations still apply.


---

# Praxis SQLite persistence and recovery tests

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M5 is **in progress**. The TypeScript engine source is in `engine/`. SQLite persistence, scheduling, dispatch, worker execution, validation and human-control primitives exist. The [composed coding workflow](https://www.lril.ai/engine-coding-workflow/) now executes a local approval-to-acceptance sequence; wider M5 integration remains unfinished. These engine APIs are separate from the released TypeScript, Python and Rust SDK 0.3.0 packages.

## Run the persistence slice

Use Node.js 24 and a local, durable filesystem. From the repository root:

```sh
npm run test:engine:persistence
npm run test:engine
npm run engine:typecheck
node engine/examples/persistence.ts
```

The example creates a temporary installation, commits a work-order record with an event and queue intent, reopens the database and retries the same request. Its assertions require one committed event and one intent. It removes its temporary database afterward. It does not execute a tool or demonstrate authentication.

## Storage API and transaction boundary

| API | Behavior |
|---|---|
| `new SqliteEngineStore(filename, busyTimeoutMs?)` | Inspect schema compatibility, configure WAL/FULL/foreign keys and initialize transactionally |
| `apply(command)` | Commit aggregate mutations, events, queue/timer intents and the command response in one write transaction |
| `getAggregate(kind, id)` | Inspect one aggregate and its revision |
| `snapshot()` | Read the base store's tables from one committed database snapshot |
| `acquireEpoch(expectedPrevious?)` | Advance the durable ownership epoch with an optional compare-and-swap check |
| `close()` | Close this connection; committed records remain available on reopen |

The bounded storage executor exposes `runtimeApply`, `runtimeGetAggregate` and `runtimeSnapshot` for coordinator use. Production coordinator writes use the storage worker so SQLite work does not block the control event loop. Direct store calls are useful for tests and administrative embedding.

`EngineCommand` contains a host-established `principalId`, `requestId`, canonical `commandDigest`, expected aggregate revisions, mutations and a response. Optional events, queue intents and timers join the same transaction. The host must bind the digest to the complete command material. A repeated principal/request with the same digest returns the saved response; different material raises `COMMAND_ID_REUSE`.

Coordinator writes also supply `expectedEpoch`. The store checks this epoch **inside** `BEGIN IMMEDIATE`, before replay or mutation, so a superseded coordinator cannot write through a previous pre-check. This internal persistence API does not itself authenticate a human or authorize dispatch. Omitting the epoch is reserved for existing trusted storage-only callers; the coordinator always supplies it.

A queue intent is durable responsibility to do later work. Its presence alone does not authorize an external effect. Dispatch requires the separate approval, policy, budget and worker checks.

`snapshot()` covers the base store tables only. It is a consistent inspection result, not a backup manifest, an authenticated audit export, or a single snapshot of all engine subsystem tables. Native `ApplyResult.revisions` on a replay still describes currently observed revisions; full historical wire receipts remain a later integration obligation.

## Database compatibility and initialization

Every engine repository uses the same opener. It inspects metadata before persistent pragmas or schema writes. Supported installations retain schema version `1`, their installation ID and epoch. A fresh installation creates metadata and component tables in one transaction. A failed initializer rolls back both and closes its connection.

| Error | Meaning and action |
|---|---|
| `UNSUPPORTED_SCHEMA` | Version is not exactly `1`; use compatible software or a separately approved migration |
| `UNSUPPORTED_DATABASE` | Existing tables do not identify an engine installation; do not treat an SDK mission database as an engine database |
| `STORE_CORRUPT` | Required installation identity or epoch metadata is missing/invalid; stop and investigate |
| `SQLITE_PREFLIGHT_FAILED` | Required durability settings were not retained; do not start execution |
| `EPOCH_CONFLICT` | Coordinator ownership changed; the stale caller must stop writing |
| `EPOCH_EXHAUSTED` | The base store cannot increment the epoch exactly within its supported integer range |

Version `1` storage layout is unchanged. There is no executable legacy import, automatic schema upgrade or metadata repair. Older experimental component-only databases lacking installation metadata are rejected rather than assigned a new identity. Supported complete version `1` installations reopen without rewriting their logical records.

The opener verifies WAL, `synchronous=FULL`, foreign keys and bounded busy timeout. On macOS it also requests and reads back full-sync/checkpoint full-sync settings. Those settings do not constitute macOS qualification or proof of power-loss durability.

## What the harness proves

The persistence command test changes a work-order aggregate and a ledger aggregate together, with one event, intent, timer and response. Expected balances and counts are asserted independently of the storage implementation. The ledger values in this test are storage payloads; this test alone does not establish M3 accounting semantics.

`engine/test/sqlite-recovery.test.ts` adds 22 tests to the existing 50 engine tests:

- Unsupported schema rejection through all six repository constructors, including a before/after database-byte comparison.
- Rejection of legacy and incomplete metadata, initializer rollback, and preservation of existing installation records.
- Snapshot consistency while a second connection commits and stale-epoch rejection without partial writes.
- Exact-integer fencing boundaries.
- Nine real child-process terminations: during initialization, after transaction begin, after aggregate/event/queue/timer/result writes, before commit, and after commit before reply.
- Two independent connections released from a shared start barrier, with exactly one complete winner at a shared revision.

For each process termination the parent must observe the requested barrier and confirm termination. Reopening must show either no command writes or the complete committed result. After a post-commit lost reply, retry must return the saved response without duplicate records. SQLite integrity is checked on the actual reopened file.

The synchronous barrier hook is an internal constructor test seam, never a serialized worker/public command. Production callers leave it unset.

## Evidence and remaining work

The local Node 24 Linux run passes **72 engine tests**, including **29 persistence tests** (seven original plus 22 new). Nine cases terminate an actual process. These are process-crash tests, not simulated power loss or certification for Windows/macOS, PostgreSQL, months-long operation, every dispatch fault boundary, or the complete M4 conformance catalog.

The next source slice now provides a [composed coding workflow](https://www.lril.ai/engine-coding-workflow/) with real validation, bounded correction and restart tests. The counts above record the earlier persistence increment. CLI/web integration, exact public wire handlers, full historical receipts and remaining recovery/conformance obligations stay open.


---

# M5 Praxis completion and conformance boundary

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M5 closes the first executable Praxis profile: a **local Node.js 24 + SQLite workflow engine** that can take an immutable coding plan through human approval, governed dispatch, real implementation, validation, bounded correction, revalidation and explicit acceptance.

The important word is **profile**. M5 completion does not mean every future AIWS deployment target is qualified. It proves the accepted local SQLite implementation boundary from the M4 conformance plan. M6 covers broader recovery, capacity, platform and long-duration hardening.

## The continuously verified workflow

Every Praxis CI run executes:

```sh
npm run engine:typecheck
npm run test:engine
npm run test:engine:m5-conformance
npm run example:engine:workflow
```

The demo performs real local subprocess and filesystem work:

```text
human plan approval
        ↓
implementation
        ↓
validation fails
        ↓
bounded corrective work
        ↓
validation passes
        ↓
human acceptance
        ↓
SUCCEEDED
```

Success cannot be inferred from an exit code alone. Acceptance is bound to the current deliverable digest and successful validation evidence, and unresolved dispatch responsibility, open controls or stale artifacts block acceptance.

## Explicit M5 conformance subset

`npm run test:engine:m5-conformance` runs the M5 closure subset required by the M4 harness:

- SQLite transaction/reopen and real-process termination tests
- supervised dispatch admission and atomic responsibility tests
- durable human controls and handoff recovery
- authenticated local control API tests
- composed coding workflow and interrupted-recovery tests

The full Praxis regression suite is still required in addition to this focused gate.

## Recovery semantics

M5 distinguishes pre-dispatch work from material responsibility.

Before `DISPATCH_AUTHORIZED`, abandoned work may be recovered as scheduler work. After authorization, a missing worker reply is not proof that nothing happened. The attempt remains owned/accounted and may become `UNKNOWN` until reconciled.

The composed recovery test kills a real child coordinator after durable worker evidence exists, reopens the same SQLite installation under a new coordinator epoch, requires human restart approval and finishes the stage without repeating the implementation effect.

## What M5 proves

For the local profile, M5 provides executable evidence for:

- atomic SQLite state, queue, dispatch and command-result commits;
- coordinator/worker epoch fencing;
- supervised approval/policy/hold/limit admission;
- committed dispatch responsibility before material execution;
- durable worker evidence and artifact digests;
- actual validation and settlement;
- bounded corrections without resetting attempts or accounting;
- durable planning/implementation/validation/correction handoffs;
- human pause, resume, cancellation decisions and agent replacement;
- explicit final acceptance tied to current successful evidence;
- restart without silent duplicate execution when prior evidence is conclusive;
- `UNKNOWN` preservation when prior execution is ambiguous.

## What remains M6 or later

M5 does **not** claim:

- PostgreSQL conformance;
- cross-machine execution or recovery;
- Windows/macOS platform qualification;
- destructive power-loss or storage-device testing;
- sustained capacity/load qualification;
- retention, archival, history compaction or backup/restore qualification;
- long-duration calendar/timezone scheduling hardening;
- every remaining M4 semantic/fault scenario;
- release packaging/native SDK parity for the new engine APIs.

Those obligations remain explicit in M6–M8 rather than being hidden inside an overly broad "engine complete" statement.

See `docs/M5-COMPLETION.md` for the evidence matrix and `spec/engine-v1/CONFORMANCE.md` for the original M4 conformance contract.


---

# M6: bounded scheduler recovery

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

**Milestone status:** M6 is complete for the D-M6-01 supervised Windows 11 x64 / Node 24 / local SQLite developer profile. [M8 is now active](https://www.lril.ai/engine-m8-qualification/): its plan is complete; qualification and expanded release/support claims remain open.

M6 is **in progress**. This first increment hardens the scheduler in the [completed M5 local engine profile](https://www.lril.ai/engine-m5-completion/). It adds bounded abandoned-claim recovery, stricter lease checks and real process-termination tests. [Local backup/restore](https://www.lril.ai/engine-backup-restore/) was added in slice 3; history retention remains open. [Recurring schedule catch-up](https://www.lril.ai/engine-recurring-schedules/) is implemented in the later fixed-UTC scheduling slice. [Persistent clock tracking and trust recovery](https://www.lril.ai/engine-clock-recovery/) were added in the following slice.

These are TypeScript engine source APIs for Node.js 24 and SQLite. They are not new APIs in the released TypeScript, Python or Rust SDK 0.3.0 packages.

## Process a backlog without one unlimited transaction

The coordinator processes a configurable number of abandoned claims and pending one-time wake-ups per reconciliation pass. Each transaction commits the records it processed. Remaining work stays durable for the next pass, including after restart.

```typescript
import { EngineCoordinator } from './engine/src/coordinator.ts';
import { BoundedStorageExecutor } from './engine/src/storage-executor.ts';

const storage = new BoundedStorageExecutor('./engine.sqlite');
const coordinator = new EngineCoordinator(storage, {
  automatic: true,
  recoveryBatchSize: 100,
  wakeupBatchSize: 100,
});
await coordinator.start();
// Register authenticated workers through your host's integration.
// Later, during controlled shutdown:
await coordinator.stop();
await storage.close();
```

Both batch sizes default to **100** and must be positive safe integers. An explicit value above 1,000 is accepted; there is no hard-coded product ceiling on workflow or task counts. These settings bound rows processed in each scheduler write transaction, not total database size, every query, or total memory use. Large values can occupy SQLite's writer for longer; measure before increasing them. The coordinator still reads full snapshots elsewhere, so sustained-scale qualification remains open.

For direct repository use, `recoverAbandonedClaims(epoch, now, limit = 100)` and `drainDueWakeups(epoch, now, limit = 100)` return the identities processed by that call. The storage executor forwards the same optional limits. Do not repeatedly drain in a tight loop that starves human controls. Automatic reconciliation already revisits pending work; callers using `automatic: false` must call `reconcileOnce()` themselves.

## Recovery guarantees and limits

| Boundary | Behavior |
|---|---|
| Wake-up becomes due | `now >= dueAt` makes a waiting task ready and marks its occurrence fired in the same transaction |
| Process killed before commit | Neither occurrence firing nor task readiness survives |
| Process killed after commit, before response | Both survive; retry does not fire that occurrence again |
| Abandoned pre-dispatch claim | Claim expiry, scheduler reservation release and task requeue commit together |
| Large recovery backlog | Only the configured batch is processed; later passes retain the remaining responsibility |
| Old coordinator epoch | Writes are rejected, including duplicate drain calls |
| Expired worker claim | Renewal, completion and release reject at expiry equality, even before cleanup has run |
| Clock before original claim time | Claim renewal/completion/release reject; renewal also cannot shorten an existing lease |
| Clock moves backward after firing | Fired occurrences stay fired; pending occurrences wait until their stored UTC deadline is reached |

A scheduler claim is pre-dispatch responsibility. Once dispatch is authorized, the separate dispatch ledger owns the attempt and its resource exposure. Recovering a scheduler claim does not reconcile an external effect, refund that exposure, clear an `UNKNOWN` outcome, or grant permission to execute. Readiness still passes through current approval, worker, policy, hold and resource gates.

The scheduling rollback test proves occurrence deduplication only. The subsequent [clock recovery slice](https://www.lril.ai/engine-clock-recovery/) adds persisted high-water time, wall/monotonic anomaly detection, dispatch holds and verified restoration. Consult that guide for its trust boundary and platform limits.

## Timestamp and compatibility rules

Scheduler timestamp arguments must use canonical UTC with exactly three fractional digits, for example `2026-09-10T00:00:00.000Z`. Offsets, omitted milliseconds, invalid dates and extended years are rejected. This preserves the ordering used by SQLite's text deadline indexes. Convert a known valid instant with `new Date(value).toISOString()` before calling the API.

Lease durations must be positive safe integer milliseconds and produce a deadline no later than year 9999. Batch sizes of zero, fractions, negative values, `NaN` and infinity now fail instead of being silently clamped. Claim renewal cannot revive an expired claim or move its deadline backward. Existing engine callers using `SystemClock` or `ManualClock` already produce the supported timestamp form.

No database schema changes or automatic migrations are introduced. Existing rows are not rewritten or scanned for timestamp compatibility. If a custom host previously persisted noncanonical timestamps, inspect and migrate that installation offline before depending on lexical deadline ordering. Automatic claim recovery now handles at most 100 claims per call by default; custom recovery loops must accommodate partial results.

## Verification

```sh
npm run test:engine:m6-scheduling
npm run engine:typecheck
npm run test:engine
```

The focused suite covers a simulated year of downtime, partial drains across reopen, backward wall-clock movement, expiry equality, invalid arguments, batch settings through the storage worker, SQL write-failure rollback, and four real process kills before/after wake-up and claim-recovery commits. It checks durable state and retries after acquiring a new epoch. A configured batch above 1,000 is tested as well.

The repository's optional third constructor argument is a test boundary callback. Production hosts should omit it; no network or worker input configures it. Tests pause a child process at an exact boundary and kill it from the parent. An after-commit failure means the operation may already be committed: inspect or retry using durable identities.

A process kill is not a power failure. The SQL trigger fault is an injected write failure, not a full-disk or failed-device test. Accelerated time proves these transition rules, not months of production reliability. The broader M6 checklist remains open.


---

# Persistent clock tracking and trust recovery

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M6 clock recovery is implemented for the local Node.js 24 + SQLite engine source. The coordinator persists time observations, detects unsafe changes, and blocks new dispatch until a trusted host verifies recovery. This completes the first remaining slice after [bounded scheduler recovery](https://www.lril.ai/engine-m6-recovery/); M6 as a whole remains in progress.

## What the engine records

`ClockReading` contains `wallTime`, `monotonicMs`, `bootId` and `trusted`. `ClockState` adds a revision, coordinator epoch, `highWaterTime`, uncertainty reason and the configured drift tolerance. The high-water time never decreases. This implementation uses one installation-wide high-water mark: it conservatively applies to all accounting scopes, rather than allowing each scope to move time backward independently.

The latest observation is stored under `engine_meta.clock_v1`. Clock transitions and consumed restoration evidence are recorded in `clock_events` in the same SQLite transaction. Normal samples update the latest state without appending an event each time. Restart does not discard a hold or lower the stored time.

`SystemClock.read()` uses UTC wall time and `process.hrtime.bigint()` for monotonic milliseconds. All system-clock instances in one coordinator process share a boot identity. A new process receives a new identity, conservatively treating process restart as a boot boundary. A new coordinator epoch may accept a trusted UTC reading at or above the previous high-water mark without comparing monotonic counters across boots. A boot change within one epoch holds dispatch.

The default adapter treats the host OS clock as trusted under the local deployment profile. It does not verify NTP, authenticate a time server, or prove that the machine clock is correct. Hosts with different assurance requirements must supply an `EngineClock.read()` adapter and mark unverified readings `trusted: false`. A legacy adapter implementing only `now()` produces an untrusted reading and cannot authorize new dispatch.

## Anomaly behavior

| Observation | Durable behavior |
|---|---|
| Wall time moves backward, even 1 ms | Retain the high-water value and hold new dispatch |
| Wall/monotonic deltas differ by more than 1,000 ms | Advance the high-water value when necessary and hold dispatch |
| Difference equals 1,000 ms | No discrepancy hold; real deadline expiry still applies |
| Monotonic counter moves backward in one boot | Hold dispatch |
| Untrusted reading | Hold dispatch |
| New epoch and trusted UTC at/above high-water | Rebase the monotonic origin; preserve any existing uncertainty hold |
| Ordinary readings after an anomaly | Keep the hold until verified restoration |

Configure the initial threshold with `EngineCoordinator` option `clockDriftToleranceMs` (default 1,000). The selected value is persisted. A different value on a later observation is rejected; changing an installation's clock policy requires reviewed offline maintenance. It is not an agent-controlled way to dismiss an anomaly.

One-time wake-ups and abandoned pre-dispatch claims can still become due at the conservative high-water time during an anomaly. The coordinator does not issue new task claims while held. Final dispatch authorization, delivery claiming and delivery acknowledgement independently check the persisted clock state and current epoch. Thus an already-prepared request cannot bypass a hold introduced before its durable execution boundary.

Existing effects may still finish and report evidence. A clock hold does not prove they stopped, cancel them, settle an uncertain result, or refund exposure. Returning an existing dispatch receipt is not permission to deliver it through a clock hold.

## Accounting and control deadlines

Unfinished `DISPATCH_AUTHORIZED` and `UNKNOWN` attempts retain their original reservations. When admitting more work, each ancestor counts any additional elapsed exposure above an unfinished attempt's reserved maximum, using the conservative high-water time. Concurrent attempts are added separately; overlapping time is not counted as a single shared interval. Confirmed terminal outcomes follow the existing settlement/stop contracts.

Elapsed scope limits continue through waits, pauses and downtime. Approval, credential-admission and control-session/challenge checks cannot use a rollback to regain lifetime. The coding runner uses coordinator time; final control transactions also clamp host time to the persisted high-water value.

Clock restoration changes only clock state. It does not change limits, attempts, scope start times, approvals, manual holds or `UNKNOWN` outcomes. A large erroneous forward reading can therefore leave an installation conservatively held or its deadlines exhausted. Restoration cannot lower charged time to make work fit again.

## Inspect and restore trust

```typescript
const state = await coordinator.observeClock();
console.log(state.highWaterTime, state.uncertain, state.reason);
const transitions = await storage.clockEvents();
```

Existing control inspection includes the clock's uncertainty flag and reason, and projects affected work orders as suspended. It does not grant permission to clear the hold.

`coordinator.restoreClockTrust(credential, expectedRevision, adapter)` is the trusted-host recovery entry point. The adapter's `verify` method receives the credential and `{ epoch, revision, reading }`. It must authenticate an authorized human operator or trusted time provider and verify the reading independently. It returns `VerifiedTimeEvidence`, or `null` to deny:

| Evidence field | Requirement |
|---|---|
| `principalId` | Verified operator/provider identity |
| `kind` | `HUMAN` or `TIME_PROVIDER`; agents are not accepted |
| `evidenceRef` | Unique durable reference to the verification evidence |
| `wallTime` | Exact wall reading supplied in the verification context |
| `validUntil` | Evidence expiry, strictly later than the checked wall time |

The engine rejects stale revisions/epochs, expired evidence, reused evidence references, a lower high-water value, changed boot identity and unsafe clock changes while the verifier is running. State and verification evidence commit together. Read the state again after a lost response: a committed restoration cannot consume the same evidence twice.

The storage-level `clockRestore` API accepts already-verified evidence for trusted host composition. Never map an HTTP body, worker output or arbitrary JSON directly into it. Authentication and time-source verification are responsibilities of the injected host adapter, as with existing identity adapters. This slice does not add a public HTTP, CLI or browser command for restoration or a bundled external time provider.

## Compatibility and operation

No released SDK package API changes are implied. These are TypeScript engine source APIs; the released TypeScript, Rust and Python SDK 0.3.0 packages retain their earlier scope.

Clock metadata and its component audit table initialize transactionally within the existing engine schema-1 installation. A fresh installation has no historical clock evidence before its first observation. Existing data is not rewritten. A current coordinator must observe time before dispatch; the observation is epoch-bound.

Keep every component on this updated source revision. Older engine source does not enforce the new clock metadata, so downgrading would bypass these protections and is unsupported. Formal upgrade/downgrade qualification remains part of release readiness. [Backup and restore](https://www.lril.ai/engine-backup-restore/) now preserve the clock state and audit table with the complete installation. Restored clock state is held for fresh trust verification.

## Verification and remaining limits

```sh
npm run test:engine:m6-clock
npm run engine:typecheck
npm run test:engine
```

The focused tests cover rollback, forward drift and threshold equality, monotonic rollback, trusted/untrusted boot transitions, sticky holds, evidence rejection/replay, storage failure, process termination around clock commits, final-admission races, delivery holds, parallel unfinished exposure and elapsed deadlines.

`ManualClock.advance(ms)` advances wall and monotonic clocks together. `set(iso)` changes wall time only; `reboot(iso, trusted)` changes boot identity and resets monotonic time. Negative advancement is rejected; use `set` to test rollback. These are test tools, not remote worker commands.

Fake-clock and process-kill tests validate state transitions, not physical power-loss tolerance or months of production reliability. Real sleep/wake and OS clock qualification remain in the platform slice. [Recurring schedule catch-up](https://www.lril.ai/engine-recurring-schedules/) is implemented in the following slice. [Local backup/restore](https://www.lril.ai/engine-backup-restore/) is implemented in slice 3; history retention and capacity measurements remain open M6 work.


---

# Recurring schedules and missed-tick recovery

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M6 slice 2 implements **fixed UTC interval schedules** in the TypeScript engine source. Each occurrence creates a durable ready task. Its identity, the represented or skipped interval and the schedule cursor commit atomically. A timer notification merely prompts the coordinator to inspect SQLite.

This builds on [persistent clock tracking](https://www.lril.ai/engine-clock-recovery/) and [bounded scheduling recovery](https://www.lril.ai/engine-m6-recovery/). Cron expressions, time zones, daylight-saving transitions and calendar rules are unsupported and rejected. The released TypeScript, Python and Rust SDK 0.3.0 packages are unchanged.

## Choose what happens after missed ticks

Assume the first tick is at **100 ms**, the interval is **10 ms**, and recovery observes **145 ms**. Five ticks are due: ordinals 0–4, at 100–140 ms.

| Policy | Tasks created | Durable range record | Next tick |
|---|---|---|---|
| `COALESCE_ONE` (default) | One task for ordinal 4 | That task represents ordinals 0–4 | Ordinal 5 at 150 ms |
| `SKIP` | None | Ordinals 0–4 skipped | Ordinal 5 at 150 ms |
| `BOUNDED_REPLAY`, limit 2 | Tasks for ordinals 0 and 1 | Ordinals 2–4 skipped | Ordinal 5 at 150 ms |

`SKIP` suppresses ticks earlier than the observed time. A tick **exactly equal** to the observed time is on time and produces one task; for example, observing 150 ms skips earlier missed ticks and emits ordinal 5. With this policy, even a slightly late poll skips the late tick. Choose the default coalescing policy when eventual periodic execution is required despite polling jitter.

Bounded replay emits the oldest due ticks. Its effective limit is the smaller of the human-approved `replayLimit` and the coordinator's `recurringOccurrenceBatchSize`. The rest of that entire missed range is recorded as skipped. Increasing the batch size or calling reconciliation again at the same time cannot replay the skipped range. A later tick begins a new range.

Large ranges are stored as endpoints rather than millions of rows. Arithmetic uses integer milliseconds and `BigInt` for ordinals and interval calculations. When a next tick would exceed year 9999, the schedule becomes `EXHAUSTED` with no next due time.

## Define and approve a schedule

```typescript
const definition = {
  namespace: 'operations',
  scheduleId: 'document-review',
  revision: 1,
  firstTick: '2026-09-11T12:00:00.000Z',
  intervalMs: 3_600_000,
  catchUp: 'BOUNDED_REPLAY' as const,
  replayLimit: 2,
  task: {
    workOrderId: 'review-work-order',
    runId: 'review-run',
    taskClass: 'document-review',
    payload: { documentRef: 'approved-document-reference' },
  },
};

const schedule = await coordinator.createRecurringSchedule(
  definition,
  operatorCredential,
  scheduleApprovalAdapter,
);
```

The host supplies `operatorCredential` and `scheduleApprovalAdapter`. The adapter's `verify(credential, context)` method must authenticate and authorize a human for the exact `context.subjectDigest` and `context.material`, within `context.epoch`. It returns `null` to deny or a verified `ScheduleApproval`:

| Field | Meaning |
|---|---|
| `kind` | Must be `HUMAN` |
| `principalId` | Verified operator identity |
| `evidenceRef` | Durable approval evidence reference |
| `subjectDigest` | Exact digest supplied in the verification context |
| `expiresAt` | Canonical UTC expiry for applying this approval |

The approval must still be valid when the creation/control transaction runs. It authorizes installing or controlling that schedule, not future external effects. Worker claims, tool execution, artifacts, credentials, limits, manual holds and current policy still pass through the existing dispatch gates.

A schedule's task template names an existing host work-order/run identity and task class. It does not create a new composed coding-workflow instance per tick. A host adapter must consume the ready tasks, map their immutable inputs, and obtain the applicable dispatch authorization. The engine does not execute an arbitrary shell command just because a schedule became due.

## Identity, task inputs and revisions

An occurrence key is an unambiguous JSON tuple of installation ID, namespace, schedule ID, immutable definition revision and ordinal. Its task ID is derived from that key. Ordinals are canonical decimal strings. Separate installations, namespaces and revisions have distinct identities.

Each generated task's payload contains:

- `schedule`: the schedule reference, occurrence key, ordinal, represented range and due time;
- `input`: the approved template payload.

The work-order/run/task-class fields come from the approved template. No budget, permission or approval is silently copied into a new authorization.

Definition revisions are immutable. Repeating creation with identical material returns the existing record without reactivating it. Changing the interval, policy, payload or other material under the same revision fails. Cancel the previous active/paused revision before approving the next consecutive revision with an explicit `firstTick`. Old occurrences and tasks remain intact.

`stateVersion` is separate from the definition revision. Draining or controlling a schedule increments its state version, which makes stale human control requests fail instead of overriding concurrent progress.

## Pause, resume, cancel and inspect

```typescript
const inspection = await storage.recurringInspect({
  namespace: 'operations', scheduleId: 'document-review', revision: 1,
});

await coordinator.controlRecurringSchedule({
  ref: { namespace: 'operations', scheduleId: 'document-review', revision: 1 },
  expectedVersion: inspection.schedule!.stateVersion,
  action: 'PAUSE', // also RESUME or CANCEL
}, operatorCredential, scheduleControlApprovalAdapter);
```

The control adapter verifies a human approval bound to the action, schedule reference and expected state version. Pause stops future occurrence generation and preserves the cursor. Resume applies the original catch-up policy to the accumulated interval; it does not renew deadlines, reset budgets or change the interval. Cancel permanently stops future generation for that revision. It does not cancel, delete or refund tasks already emitted; use the applicable work-order/task controls for those responsibilities.

`recurringInspect` returns a consistent schedule record and its occurrence, recovery-range and decision history. Inspection currently returns the complete history for that schedule; pagination and retention remain later M6 work. A lost control response should be followed by inspection, since retrying the old state version can legitimately fail after a committed change.

## Coordinator and storage settings

```typescript
const coordinator = new EngineCoordinator(storage, {
  automatic: true,
  recurringScheduleBatchSize: 100,
  recurringOccurrenceBatchSize: 100,
});
```

Both values are positive safe integers with defaults of 100. They are deployment tuning settings, not total task/workflow ceilings. Per reconciliation, at most `recurringScheduleBatchSize` due schedules are selected oldest-first. Each schedule processes its missed range in a separate transaction and emits at most its effective occurrence limit. Earlier schedule commits survive a later schedule failure; retry inspects the durable cursor.

Startup and subsequent reconciliation both drain recurring schedules. `reconcileOnce()` reports the committed summaries in `recurringRecoveries`. Call it yourself when `automatic: false`; otherwise the existing coordinator loop supplies periodic reconciliation. Duplicate or lost notifications do not determine schedule truth.

The storage APIs are `recurringCreate`, `recurringControl`, `recurringDrain` and `recurringInspect`. Direct repository users can use `RecurringScheduleRepository`, after observing the current epoch's clock. These are trusted-host composition APIs. Never pass unverified HTTP or worker-provided approval objects into them. No new HTTP, CLI or browser schedule-management endpoint is introduced here.

During a clock uncertainty hold, due state can advance at the persisted high-water UTC, while new execution remains blocked by the clock gates. One-time waits retain their independent behavior: a repeating schedule's `SKIP` policy cannot discard an overdue one-time wake-up.

## Persistence and verification

Schedule definitions/cursors, control evidence, emitted occurrences and recovery ranges are persisted in four recurring-schedule tables. Task readiness and its scheduler sequence/fairness records commit with each occurrence and cursor advance. A unique occurrence key and transactional cursor prevent silent duplicate creation after a lost response or restart. Task-ID conflicts or storage failures roll back the entire schedule recovery transaction.

The component tables initialize transactionally in the existing schema-1 source profile. Older source revisions do not process these schedules; keep all engine components on the updated revision. [Local backup/restore](https://www.lril.ai/engine-backup-restore/) preserves occurrence identities and cursors and pauses active schedules on restoration. Formal upgrade/downgrade and retention qualification remain separate milestones.

```sh
npm run test:engine:m6-recurring
npm run example:engine:recurring
npm run engine:typecheck
npm run test:engine
```

The example uses an explicitly labeled human-approval fixture and a manual clock. It demonstrates all three policies and duplicate suppression without executing external work.

Tests cover the CT-09 vectors, due-time equality, bounded replay across reopen, a year-long millisecond backlog, pause/resume/revision controls, invalid approvals, epoch fencing, storage rollback, competing database connections, four actual process-kill boundaries, coordinator startup recovery and clock holds. These establish the tested transition guarantees; they are not power-loss, sustained-load or months-long production reliability measurements.


---

# Praxis backups and same-machine restore

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M6 slice 3 adds **local administrative backup and restore APIs** to the Node 24 + SQLite engine source. A backup contains a consistent SQLite checkpoint, immutable artifacts and a versioned manifest. Restore publishes a separate installation directory with execution held until verified reconciliation permits startup.

This builds on [clock recovery](https://www.lril.ai/engine-clock-recovery/) and [recurring schedules](https://www.lril.ai/engine-recurring-schedules/). It does not change the released TypeScript, Python or Rust SDK 0.3.0 packages. [Lossless history compaction](https://www.lril.ai/engine-history-compaction/) is implemented in slice 4. Cross-machine failover, database migrations and automatic deletion remain outside this slice.

An interactive [engine backup and restore](https://www.lril.ai/diagrams/#engine-backup-and-restore) diagram shows checkpoint, sealed manifest, publication, verified restore and human activation.

## What is preserved

| Material | Backup and restore behavior |
|---|---|
| Database | SQLite `VACUUM INTO` captures a consistent checkpoint including committed WAL contents; copying only a live `.sqlite` file is unsupported |
| History and state | Ordered audit prefix seal, full table projection seal, database size/digest, SQLite integrity and foreign-key checks |
| Accounting | Attempts, reservations, consumed resources and unresolved outcomes remain intact |
| Idempotency | Command receipts, occurrence identities and scheduler cursors remain intact |
| Artifacts | Complete content-addressed root copied; inventory records every digest and size; known workflow, handoff and execution-evidence references must exist |
| Credentials | External credentials stay disconnected; host reviews source content before staging and completed content before publication |
| Restored authority | Fresh ownership/auth epoch from an external protected allocator; old dispatch approvals revoked and policies made indeterminate |
| Future work | Active recurring schedules and running work-order controls paused; installation-wide hold blocks coordinator startup and final dispatch |
| Time | Restored high-water time is at least the snapshot, verified live high-water and current local time; existing clock state becomes uncertain |

The engine's current audit events are not a complete reducer for every operational table. The backup therefore seals the complete database projection as well as the audit prefix. It does **not** claim event-only reconstruction or automatic repair of inconsistent state.

## Host responsibilities

The APIs run in a local administrative process. There is no new public HTTP, worker, CLI or browser command. `RecoveryAdapter` is a trusted host boundary, comparable to the existing authentication and human-approval adapters. A raw user-submitted proof is not a verified adapter result.

Before calling the APIs, the host must:

1. Authenticate an authorized human and acquire exclusive installation maintenance ownership. Stop the host loop, quiesce workers and fence every old process. Keep that maintenance ownership through the operation and cutover. Closing a connection alone does not prove external work stopped.
2. Use protected, operator-owned directories outside worker workspaces. Paths with symlinks are rejected; protection against concurrent directory replacement still depends on host ownership. Existing destinations are rejected.
3. Establish the installation and machine identity through an external protected identity source. A caller-supplied machine name alone is insufficient.
4. Review database payloads, task output and all artifacts for secrets before backup staging. The engine cannot reliably identify arbitrary secrets in application content and does not silently redact state. Resolve sensitive content under an approved process or do not create the backup. Encryption/key management, if required, belongs to the OS/organization provider outside this bundle.
5. Maintain a durable ownership/auth-epoch high-water mark outside the files being restored. `reserveEpoch` must atomically reserve above **all** live/restored/previously reserved epochs, persist before returning and fence old owners. Failed restores consume their reservation. If the high-water is unavailable, fail and use owner recovery; never guess zero or use the old snapshot alone.
6. Retain the live clock high-water independently and provide it in verified restore evidence. Protect external provider/audit evidence for the interval the backup may omit.

The adapter's `verify(context)` receives the exact material digest, operation, installation, machine, destination and `inspectionPath`. For backup it is called twice: first for source review/maintenance, then for the completed staged snapshot. It returns fresh human evidence bound to that material. The host keeps maintenance ownership across both calls. A source projection change between review and copying rejects the backup.

## Create and verify a backup

```ts
import {
  createBackup,
  inspectBackup,
} from './engine/src/backup-restore.ts';

// recoveryAdapter is your authenticated maintenance/content-review adapter.
const manifest = await createBackup({
  database: '/srv/aiws/engine.sqlite',
  artifacts: '/srv/aiws/artifacts',
  destination: '/srv/aiws-backups/2026-09-10',
  namespace: 'development',
  machineId: protectedMachineIdentity,
  adapter: recoveryAdapter,
});

const verified = inspectBackup('/srv/aiws-backups/2026-09-10');
console.log(manifest.eventPosition, verified.manifestDigest);
```

The destination's parent must already exist. The final directory contains `engine.sqlite`, `artifacts/`, `inventory.json`, `configuration.json` and `manifest.json`. Configuration contains only the machine identity, source epoch, projection digest and clock high-water. No external credential store is copied.

The manifest implements the existing `SnapshotManifest` wire schema: `aiws-engine-snapshot/1`, engine/projection version 1 and codec 1.1.0. Its database, inventory and configuration descriptors bind exact bytes. Empty audit history uses position zero and the 64-zero event digest. Staging manifests with `completed=false` are rejected.

Files are written and flushed in a private staging directory; directory publication is an atomic rename followed by parent-directory flush on the qualified Linux host. A failure before publication leaves no final backup. A failure after publication may report an error despite a complete result: inspect the destination before retrying. A process kill can leave a `.recovery-*` staging directory; it is not a published backup. Remove abandoned staging only after verifying the owning administrative process has stopped and a valid published copy exists where required.

`inspectBackup` verifies integrity, not authenticity. A matching digest does not make an uploaded bundle trusted. Restore still requires the configured host's trusted-backup provenance verification.

## Restore into a new directory

```ts
import {
  restoreBackup,
  inspectRestore,
} from './engine/src/backup-restore.ts';

const result = await restoreBackup({
  backup: '/srv/aiws-backups/2026-09-10',
  destination: '/srv/aiws-recovery/restored-2026-09-10',
  installationId: existingInstallationId,
  machineId: protectedMachineIdentity,
  adapter: recoveryAdapter,
});

console.log(result.database, inspectRestore(result.database).status); // HELD
```

All material is copied and verified again after host approval. The epoch is reserved before changing restored state. SQLite then commits the installation-wide hold, authority rotation, conservative clock and work pauses with recovery audit evidence. Publication occurs only after that transaction commits.

The original database and backup remain intact. The transformed database is not falsely labeled with its source digest: the restored directory uses `restore-source.json` for provenance, and removes the source `manifest.json`. Use `inspectRestore` for this directory. Create a new backup to produce a new completed manifest.

The restored hold survives process restart. Backing up and restoring an already-held installation retains the earliest uncertain interval and prior recovery evidence. Normal workflow resume, clock restoration or coordinator startup cannot clear it. Old worker delivery is independently fenced. Even after the hold is verified, workflows/schedules stay paused and dispatch requires fresh current policy and approvals. Existing clock uncertainty requires the separate [clock-trust procedure](https://www.lril.ai/engine-clock-recovery/).

## Reconcile before startup

A daily backup can omit real effects, spending, attempts, permission revocations and deduplication records. Restoring old bytes cannot undo any of them.

```ts
import { activateRestore } from './engine/src/backup-restore.ts';

await activateRestore(result.database, recoveryAdapter);
```

This requires fresh human evidence bound to the current restored projection. The adapter must affirm both `noUnrecordedEffects` and `accountingAndAuthorityComplete`, supported by provider evidence and retained audit/dispatch records covering the omitted interval. It is not a checkbox that lets a human waive unknown exposure. All restored `DISPATCH_AUTHORIZED`, `UNKNOWN` and unsettled `STOPPED` attempts independently block activation.

The API rechecks the state under the writer lock and rechecks artifact bytes after verification. Changed state, expired proof or missing artifacts leaves the hold intact. Successful verification is recorded durably and allows a new coordinator epoch; it does not start workers or reconnect credentials.

If omitted effects or accounting/authority gaps exist, **keep the hold and recover forward using complete trusted records**. This slice does not implement an arbitrary missing-history importer, provider reconciliation engine or event-only fallback. When necessary evidence is unavailable, there is no supported activation shortcut. Reconcile known attempts through their existing evidence/settlement contracts under trusted maintenance, preserving all consumption.

Only after successful verification should the operator switch the stopped host to the new database/artifact directory, refresh identity and provider credentials, restore clock trust, review current policies and explicitly resume approved work. Retain the old files as recovery evidence; never run old and restored installations simultaneously.

## Schedule, retention and restore drills

The accepted default remains **daily backups retained for 30 days**. The host schedules the administrative backup job; this API does not silently install an OS job or start a background maintenance service. Use unique date/run destinations and monitor backup failures and the age of the last verified backup.

At each restore drill, use a fresh isolated directory with credentials disconnected. Verify the manifest and inventory, exercise the held restore, confirm preserved accounting/identities, and record the evidence. A drill with real active work must not release its hold merely to test startup; use a separate no-effect fixture for that check.

Automatic pruning is deliberately absent. Active/unresolved references and recovery dependencies override age. [Lossless history packing](https://www.lril.ai/engine-history-compaction/) is available in slice 4; destructive retention coordination remains slice 6. No database or artifact deletion should be enabled before restoration has been verified.

## Run the verification

```bash
npm run engine:typecheck
npm run test:engine:m6-backup
npm run test:engine
npm run example:engine:backup
```

The example uses a clearly labeled offline fixture with no external effects. Its in-memory epoch allocator is **not** a production `RecoveryAdapter`.

Tests cover committed WAL data, audit/projection seals, corrupt/missing artifacts, invalid manifests/schema/identity, stale verification, epoch/clock refusal, installation holds, preserved UNKNOWN/accounting, recurring identities, authority revocation, six actual process-kill boundaries and artifact loss during activation approval.

The verified execution profile is Node 24 on local Linux storage. Directory durability on Windows/macOS, device power loss, platform volume behavior and capacity/long-duration results remain slices 8–9. JSON inventory/metadata parsing is bounded to 20 MiB; database verification iterates rows, but individual SQLite rows and artifact files are read in memory. These are stated implementation limits, not benchmarked capacity claims.


---

# History compaction and continuation

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M6 slice 4 adds **lossless audit-history packing** to the local Node 24 + SQLite engine. It replaces a bounded prefix of ordinary audit rows with verified compressed segments in the **same database**. All event bytes remain available through the engine's history reader and in [backups](https://www.lril.ai/engine-backup-restore/).

Existing runs continue from their existing materialized state. Packing does not create a new run, reset a budget, change an attempt, release a hold or alter an approval. This is storage continuation for the same history, not a new workflow-version or cross-machine continuation protocol.

## Preserved invariants

| Material | Behavior |
|---|---|
| Audit events | Exact original row values, IDs, ordering, payload JSON and timestamps reconstruct from segments plus the ordinary tail |
| Current state | Aggregate identities/revisions, run IDs, accounting, reservations, UNKNOWN outcomes and approval state are untouched |
| Request idempotency | Original command receipts remain; retries return their original results |
| Event idempotency | Archived event IDs and sequence numbers retain permanent unique identity records; an insert cannot reuse them |
| Timers and recurrence | Wake-ups, occurrence identities, cursors and schedule revisions are untouched |
| Backup and restore | Full segments and identities are part of the SQLite snapshot; verification reads the complete logical audit prefix |
| Restart | Before allocating a new ownership epoch, the engine verifies packed history and its ordinary tail; corrupt/missing history blocks startup |

No event or artifact is discarded. The 90-day hot-history, one-year closed-history and indefinite live/unresolved-reference retention rules remain unchanged. Packing is transparent local storage compression; it does not move history to an unavailable archive or authorize age-based deletion. Automatic pruning belongs to later retention work.

## Bounded segments and atomic replacement

A plan selects the **oldest unpacked contiguous prefix**. Defaults are 500 events and 1 MiB of uncompressed canonical row data per transaction. Supported settings are 1–10,000 events and 2 bytes–4 MiB. Both caps apply; a row is never split.

If the first row exceeds the byte cap, planning fails with `HISTORY_ROW_TOO_LARGE`, and nothing is packed. Increase the cap within the supported limit or leave that prefix unpacked. Later smaller rows cannot bypass an oversized first row. SQL checks text sizes before fetching the complete row; canonical encoding is then checked against the exact byte limit.

Each transaction:

1. Checks the current epoch, installation restore hold, exact plan and fresh material-bound human approval.
2. Compresses the selected rows and verifies exact reconstruction, row digest and ordered prefix digest.
3. Inserts the segment and its durable approval evidence.
4. Inserts the event identity records, then removes the replaced ordinary rows.
5. Commits all changes together.

A process failure before commit retains the original rows. After commit, the segment and identities contain the complete history. A retry of the exact committed plan returns `replayed: true`; a conflicting plan for that range fails. New events appended during human verification do not invalidate an unchanged approved prefix. A changed prefix or stale epoch does.

Segments use zlib/deflate, stored as base64 text with compressed and uncompressed digests, declared byte sizes, sequence endpoints and linked prefix digests. Decompression is capped at the declared size and never above 4 MiB. Segment and identity updates/deletions are blocked by database triggers.

## Use the trusted host entry point

```ts
const packed = await coordinator.compactHistory(
  operatorCredential,
  historyApprovalAdapter,
  { maxEvents: 500, maxBytes: 1024 * 1024 },
);

if (packed) {
  console.log(packed.fromSequence, packed.throughSequence, packed.replayed);
}
```

`historyApprovalAdapter.verify(credential, context)` authenticates an authorized human and approves the exact `context.plan` and `context.subjectDigest`. It returns `HistoryApproval`, or `null` to deny:

```ts
{
  kind: 'HUMAN',
  principalId: authenticatedHumanId,
  evidenceRef: durableApprovalReference,
  materialDigest: context.subjectDigest,
  expiresAtMs: verifiedExpiry,
}
```

The host supplies the credential, identity verifier and durable evidence. Worker output or an HTTP body is not already-verified approval. The plan given to the adapter is a copy; mutation cannot change the plan submitted to storage. The original epoch stays bound while verification is pending. Expiry is checked inside the storage transaction against local time and persisted conservative clock high-water.

Work runs on the existing bounded storage worker. Each call packs at most one segment. There is no newly exposed HTTP/CLI/browser endpoint, automatic packing loop or implicit background retention policy. The host chooses when to request further plans and approvals.

## Inspect and continue

```ts
const status = await storage.historyInspect();
console.log(status.segments, status.archivedEvents, status.hotEvents);

// Existing SqliteEngineStore.snapshot() reconstructs the complete logical history.
const snapshot = engineStore.snapshot();
```

`historyInspect()` verifies the complete retained chain and reports the last sequence and prefix digest. `historyPlan(epoch, bounds)` and `historyCompact(plan, verifiedApproval)` are the lower-level trusted storage APIs. `HistoryRepository` provides equivalent synchronous methods for a local administrative process; keep that synchronous work off a live HTTP/coordinator event loop.

`SqliteEngineStore.snapshot()` keeps its existing shape. The raw `audit_events` SQL table now represents only unpacked rows. Direct queries against that table are not full-history inspection; use the engine reader. Control inspection uses a logical head lookup that includes a fully packed tail.

The existing materialized run state is the continuation checkpoint. A new event receives the next installation sequence even after all previous rows have been packed. Restart preserves run identity and accounting. Packing does not itself authorize a paused run to resume, change a graph or convert a terminal run into a new assignment.

## Backup, compatibility and operating limits

Take and verify a normal backup before introducing this format to an installation. Slice 3 backup/restore now reconstructs the packed prefix when validating event position/digest, and retains every segment and identity record in its database snapshot. Restore still enforces all existing fencing, clock and reconciliation holds.

History tables initialize transactionally under `history_format=1` within engine schema 1. Unknown history formats and missing versioned tables are rejected before normal writers mutate the database. Use matching updated engine source for every component; older engine code does not understand packed history, and downgrade is unsupported. No SDK package release or database migration edge is introduced.

Packing makes ordinary audit tables smaller and allows SQLite to reuse freed pages. It does not promise that the allocated database file immediately shrinks: no automatic `VACUUM` or filesystem truncation runs. Compression benefit depends on event contents, and identity records add overhead. Total retained history still grows.

Full inspection, snapshot output and startup integrity validation still scale with total retained history. Segments bound decoding and individual packing transactions, but `snapshot()` still assembles its complete result in memory. This slice does not claim bounded total replay cost, a complete event reducer or a capacity benchmark. [Slice 5](https://www.lril.ai/engine-replay-limits/) adds indexed pages, bounded startup-verification steps and record streaming. Total work remains proportional to retained history; retention/telemetry, platform and long-duration qualification remain later slices.

## Verification

```bash
npm run engine:typecheck
npm run test:engine:m6-history
npm run test:engine
npm run example:engine:history
```

The example packs twenty events into four segments and verifies exact reconstructed history and unchanged run/budget values. Its human evidence is a labeled offline fixture.

Focused tests cover whole-row bounds, exact reconstruction, appended tails, duplicate events/requests, stale approval, conservative expiry, empty history, five actual process-kill boundaries, two competing processes, immutable segments, corrupt/missing formats, coordinator restart and backup/restore continuation. Process-kill tests do not establish device power-loss durability; platform and storage-volume qualification remain separate work.


---

# Bounded history reads and parser limits

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M6 slice 5 adds **indexed history pages and resumable verification** to the local Node 24 + SQLite engine. It also separates control-message parser limits from stored-document limits and supplies an audit stream that can exceed 1 MiB without parsing the complete export as one JSON document.

This extends [lossless history compaction](https://www.lril.ai/engine-history-compaction/) and [backup/restore](https://www.lril.ai/engine-backup-restore/). It does not change the released native SDKs' finite-v1 audit bundle format or their 1 MiB strict-parser contract. The stream below is a separate engine inspection format, not an executable legacy import.

## Read a pinned history in bounded pages

```ts
let cursor = null;
do {
  const page = await storage.historyPage(cursor, {
    maxEvents: 500,
    maxBytes: 1024 * 1024,
  });
  await consumeInspectionPage(page.events);
  cursor = page.cursor;
} while (cursor);
```

A page returns the installation identity, raw audit rows, pinned head sequence, exact canonical row-array byte count, next cursor and `done`. Rows retain their original `payload_json` strings and sequence numbers. The cursor is an internal inspection cursor, not an authorization token.

The first page pins the current logical head. Later appends do not enter that export. Reopening the same installation can continue the cursor. Rewrites or compaction change its revision and produce `HISTORY_CURSOR_STALE`; discard the partial inspection and restart. Invalid installation identity, sequence bounds or cursor version fail explicitly.

Queries start at the cursor's indexed sequence and select only relevant packed segments and ordinary rows. They do not rescan the entire prefix to skip earlier events. A relevant segment is decoded as a unit, so up to two partly used segments add bounded decoding overhead beyond returned rows.

Default page limits are 500 events and 1 MiB. Supported limits remain 10,000 events and 4 MiB, matching compaction. Rows are never split. An event larger than the selected cap produces `HISTORY_ROW_TOO_LARGE`; select a larger supported cap or keep the history in its existing format. No rows are silently skipped. Bounds apply to the row-array payload, not every byte of transport/envelope overhead.

`SqliteEngineStore.snapshot()` remains a compatibility API that assembles a complete snapshot. Use pages for large-history inspection. A page alone is not a claim that all earlier history has been verified.

## Startup verification with durable checkpoints

The coordinator now verifies retained history in bounded storage-worker calls **before allocating its new ownership epoch**. Startup defaults are 500 events and 4 MiB per step; `historyVerificationBatchSize` and `historyVerificationByteLimit` configure them within the supported limits. Each step checks a contiguous range, segment integrity/anchors, event identities and aggregate revision references, then commits a checkpoint with the verified position and running prefix digest.

The checkpoint binds the ownership epoch, logical head and history mutation revision and has an integrity digest. If a process stops between steps, the next start resumes committed progress when the source is unchanged. Any history mutation resets verification; stale epochs cannot reuse progress. Repeated mutations during startup eventually fail with `HISTORY_BUSY` rather than loop indefinitely.

The final checkpoint is checked again inside epoch acquisition. Appends or rewrites between verification and acquisition invalidate it. No worker dispatch is authorized by a partial checkpoint.

For an administrative process, equivalent methods are available:

```ts
let verified;
do {
  verified = await storage.historyVerify(currentEpoch, {
    maxEvents: 500,
    maxBytes: 1024 * 1024,
  });
} while (!verified.complete);
```

`HistoryReplayRepository` provides synchronous `page` and `verify` methods for offline tools. Keep synchronous work off a live request/event loop. Direct epoch acquisition retains a small-history verification fallback; beyond 500 events or 4 MiB it requires staged verification and reports `HISTORY_VERIFICATION_REQUIRED`.

Total verification time is still proportional to retained history. This change bounds individual work steps and makes progress resumable; it does not make total work constant or establish a throughput benchmark. Current materialized workflow state remains authoritative for execution—there is no new event-only workflow reducer.

## Export and verify a record stream

```ts
import {
  exportAuditStream,
  verifyAuditStream,
} from './engine/src/audit-stream.ts';

const stream = exportAuditStream(storage, installationId, {
  maxEvents: 100,
  maxBytes: 512 * 1024,
});
const summary = await verifyAuditStream(stream);
console.log(summary.eventCount, summary.prefixDigest);
```

The format is newline-delimited `aiws-engine-audit-stream/1`: one header, sequential event records and one trailer carrying the complete count and prefix digest. Exports use pinned history pages. Missing/reordered events, changed digest, missing trailer, extra content and truncated records fail verification. If export is interrupted or its cursor becomes stale, its partial output is not a valid completed stream.

The verifier retains one bounded record buffer and returns a summary only after validating the trailer and EOF. Default record size is 1 MiB; an explicit record cap may be selected up to 4 MiB. Set page byte bounds below the desired record cap to leave room for the event envelope. A single event that exceeds the record cap cannot be made acceptable by splitting arbitrary JSON bytes into separate records.

A checksum is not authenticity. Use trusted provenance or external signatures when needed. Verification checks stream structure and its integrity chain; it does not establish human authority, independently enforce global event-ID uniqueness, semantically replay native SDK contracts, mutate a database or call effect providers. There is no executable stream-import API. Do not feed this format to the SDK's existing bundle importer.

## Explicit parser profiles

| Limit | Control messages | Stored documents |
|---|---:|---:|
| Encoded bytes | 1 MiB | 20 MiB |
| Nesting depth | 64 | 64 |
| Items per array | 1,024 | 100,000 |
| Keys per object | 4,096 | 100,000 |
| Total value nodes | 100,000 | 1,000,000 |
| Encoded string token bytes | 1 MiB | 20 MiB |

Both profiles reject malformed UTF-8, duplicate decoded keys, trailing JSON, unpaired surrogates and unsafe/nonintegral JSON number values. Object results have no prototype. Numeric resource budgets and timestamps remain decimal strings where required by the wire contract.

`parseWire` retains the existing control profile. `parseJsonDocument(bytes, limits)` exposes explicit validated limits; excessively large depth/resource settings are rejected. Compressed audit segments use a separate bounded specialization: 4 MiB, 10,000 rows and 160,000 value nodes. Their opaque `payload_json` strings retain the original engine payload representation.

Backup inventories use the stored-document profile. A valid content-addressed inventory with more than 1,024 artifacts no longer fails merely because it inherited the control-message array cap. Actual backup file reads, artifact memory usage and full snapshot verification retain their separately documented limits; this is not a general unlimited-upload facility.

The native TypeScript/Python/Rust SDK 0.3.0 audit parser still limits a single input to 1 MiB. That contract was deliberately preserved. Large engine audit inspection now has the bounded stream above; native legacy bundle transport/import improvements require their own compatibility work.

## Verification and remaining work

```bash
npm run engine:typecheck
npm run test:engine:m6-replay
npm run test:engine
npm run example:engine:replay
```

Tests cover packed/tail paging, cursor invalidation, exact budgets, durable verification progress, a real process kill, stale certificates, coordinator startup with growing history, segment corruption, streams larger than 1 MiB, fragmented/truncated streams, parser boundary failures and backup inventories above 1,024 artifacts.

History mutation counters and checkpoints initialize additively within schema 1. All engine components must use matching source; direct SQL edits or older writers that bypass the tracked format are unsupported. These integrity digests do not defend against an administrator who can rewrite all database metadata and triggers.

Retention/telemetry is slice 6. Comprehensive fault coverage, platform qualification and measured long-duration/capacity limits remain later slices. No source ZIP refresh, hosted-site deployment or new SDK package release is implied.


---

# Artifact retention and telemetry capacity

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M6 slice 6 adds `ArtifactRetention` and `TelemetryQueue` to the local Node 24 + SQLite source profile. Released native SDK 0.3.0 packages are unchanged.

## Retain references before considering age

`ArtifactRetention` scans persisted state, retained audit history (including packed segments), and host-provided SHA-256 pins. It follows digest references through artifact contents using 64 KiB reads. References from active work, unresolved outcomes, parent/shared manifests, retained closed history and recovery metadata override age. Conservative digest matching can retain extra files.

Only unreferenced immutable digest files are eligible. A first observation starts a **7-day orphan grace period**; filesystem modification times do not establish age. Approved candidates enter **logical quarantine for 30 days**: bytes remain readable at their original path and remain included in ordinary backups. A separately approved purge rechecks references and verifies file digests before unlinking. References discovered again reset eligibility. Republished digests start a new grace period.

The accepted history defaults (90 days hot, archival through one year), routine logs (30 days), failure logs (90 days), temporary execution files (7 days after closure with outputs preserved), and daily backups (30 days) remain minimum retention responsibilities. This implementation deliberately retains referenced history/artifacts longer. It does not delete audit segments, workspaces, temporary execution files, application logs or backup directories, and introduces no timer that silently deletes them.

## Trusted maintenance contract

```ts
import { ArtifactRetention } from './engine/src/artifact-retention.ts';
const retention = new ArtifactRetention(database, artifactRoot);
try {
  const result = retention.run(epoch, 'QUARANTINE', trustedHost, {
    maxArtifacts: 100,
  });
  // After quarantine ages, request a fresh review for action 'PURGE'.
} finally {
  retention.close();
}
```

The trusted host supplies `withMaintenance`, `pins`, and `approve`. Maintenance must fence every artifact publisher, reader, database writer and competing maintenance operation until the call returns, including after crash recovery. SQLite alone cannot fence filesystem publishers. This administrative API is not exposed as an agent control endpoint.

Approval must identify a human principal and evidence reference, bind `retentionMaterialDigest(plan)`, and remain unexpired. It must point to an actual same-installation restore database whose slice 3 reconciliation was activated as `VERIFIED`. The host verifies the operator identity and exclusive maintenance ownership; self-asserted agent data is not a substitute. Held restores and stale epochs reject cleanup.

Purge commits a `PREPARED` journal before touching bytes, fsyncs the containing directory after unlink, then commits `PURGED`. Interruption leaves a retryable responsibility record. A missing file is accepted only for a previously prepared purge; a new reference to a missing interrupted purge fails closed. Review and retry must occur under maintenance before publishers resume. Logical quarantine avoids an unbacked second artifact directory. Paths are installation-local; after restore into a different root, a new observation starts a fresh grace period.

A call handles at most 1,000 candidates (100 by default). Reference scanning is a full offline maintenance scan; total scan duration and directory/reference inventory memory are not bounded independently of installation size. Large artifact bytes are streamed. Broader capacity and filesystem/power-loss qualification remain slices 7–9.

## Best-effort telemetry with explicit pressure

The storage worker initializes a durable telemetry outbox. An audit insert transaction also attempts to enqueue only event identity, type, aggregate identity and timestamp. Authoritative event payloads and artifact bytes are excluded. Host identifiers must still follow the host's privacy policy. Queue overflow increments a persisted drop counter and allows the authoritative audit transaction to commit.

Default persisted limits are **1,000 records, 8 MiB, and 5 delivery attempts**. Trusted hosts may choose different limits when first initializing `TelemetryQueue`; conflicting reopen configuration rejects instead of silently expanding resource authority. Queue counters expose accepted, delivered, dropped, exhausted, live records and bytes. This bounds logical payload capacity, not SQLite file size, WAL growth or filesystem free space.

```ts
const batch = telemetry.claim(epoch, 'host-exporter', now, {
  maxRecords: 100, maxBytes: 1024 * 1024, leaseMs: 30_000,
});
for (const record of batch) {
  await approvedExporter.send(record.id, record.payload);
  telemetry.acknowledge(epoch, 'host-exporter', record.id,
    record.leaseToken, new Date().toISOString());
}
```

Transport, credentials and remote exporter approval remain host responsibilities; this API does not contact a collector. Delivery is at least once within the retry window and best effort overall. Stable record IDs support receiver deduplication. Acknowledgement requires the current epoch, owner and unexpired random lease token. Expired leases retry; exhausted records release capacity and increment `exhausted`. A new epoch can reclaim old leases. Restore holds prevent claiming or acknowledging delivery.

Custom `enqueue` is available to trusted hosts for sanitized JSON. Duplicate IDs are idempotent while queued; changed payloads reject. Delivered IDs have no permanent tombstones, so receivers must own long-term deduplication. A batch byte cap smaller than its oldest record rejects explicitly. Telemetry loss never authorizes deleting the authoritative audit history. Disk exhaustion itself can still prevent SQLite writes; reserved space and platform load qualification remain separate work.

## Verification

Run `npm run test:engine:m6-retention` for focused lifecycle, recovery, reference, approval, lease and capacity checks. See [backup/restore](https://www.lril.ai/engine-backup-restore/), [history compaction](https://www.lril.ai/engine-history-compaction/) and [bounded replay](https://www.lril.ai/engine-replay-limits/) for the prerequisites.


---

# Failure injection and recovery qualification

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M6 slice 7 qualifies the implemented local Node 24 + SQLite recovery paths with real process termination, deterministic transaction faults, storage capacity errors and independent writer contention. It extends the earlier clock, recurrence, backup, compaction and workflow tests. It does not certify power-loss durability or every scenario in the full standard catalog.

## Run and preserve evidence

```sh
npm run test:engine:m6-faults
npm run qualify:engine -- --output ./qualification-run-001
```

Use a new output directory for each qualification attempt. Existing directories are rejected; failed attempts are not overwritten by later successes. The runner executes engine typecheck and the entire engine test suite, writes their logs, and produces `report.json` with case names/results, runtime and filesystem identity, per-source SHA-256 fingerprints, log digests, timestamps, exit codes and explicit scope exclusions. Its exit status is nonzero if a check fails, times out, skips tests or produces incomplete/empty test accounting. Test-created databases and provider effects use disposable temporary directories.

The source digest binds the exact engine/test files, package scripts and qualification runner. Preserve the report and logs together with the source revision. The report is local implementation evidence; it is not the complete per-scenario certification report specified by the standard.

## Fault coverage

| Area | Injected fault | Required recovery result |
|---|---|---|
| Core persistence and scheduling | Process death around durable writes/commit; rollback exceptions | State, events, queue/timer intents and command results commit together or remain unchanged |
| Clock and recurrence | Process death before/after clock holds and occurrence/cursor commits | Conservative clock state and exactly-once occurrence identity |
| Backup and restoration | Death during staging, publication and restore epoch reservation | No incomplete published backup; restored installations remain held |
| History compaction | Death after segment, tombstone and deletion writes, and around commit | Complete identical logical history and durable identities |
| Telemetry | External kill before/after enqueue, claim and acknowledgement commits | Durable capacity counters; atomic leases; queued idempotency after lost replies |
| Artifact purge | External kill after prepared journal and after unlink | Prepared responsibility survives; retry finishes once without deleting referenced artifacts |
| Storage capacity | SQLite's real `SQLITE_FULL` using `max_page_count` | Whole transaction rolls back; original storage error survives; capacity restoration allows retry |
| Writer contention | Independent connection holds `BEGIN IMMEDIATE` | Real `SQLITE_BUSY`; no partial command; retry commits once |
| Coordinator | Process termination after execution evidence | Workflow resumes with no duplicate implementation effect |
| Worker | Process termination after a fsynced provider-effect marker | Durable `UNKNOWN` responsibility; no automatic redispatch |

The new external-kill harness waits for a named boundary emitted by the child before the parent terminates it. Early exit, missing boundary or watchdog expiry fails the test. Purge tests use real activated restore evidence in an isolated fixture and reopen the actual database after termination. The storage tests exercise SQLite itself, not a thrown imitation of its errors. `max_page_count` models database capacity exhaustion; it is not a full filesystem or an I/O fault.

## Runtime corrections

An execution terminated by a signal, or ending without an exit code, now produces `UNKNOWN`. An ordinary nonzero exit remains `FAILED`; successful exit remains `SUCCEEDED`. A killed process may have already caused effects, so treating its termination as a routine failure could incorrectly allow corrective execution. Durable worker evidence retains that uncertainty for reconciliation.

Telemetry and artifact-retention transactions now preserve the original exception if SQLite has already rolled back automatically or if a post-commit hook reports lost response. A second rollback must not replace `SQLITE_FULL` with a misleading “no transaction active” error.

## Qualification boundary

The recorded run passed **267 tests** plus typecheck on the local Linux environment. This includes 13 additional tests, with 18 tests in the focused faults/worker command. Artifact and telemetry hooks are trusted host/test instrumentation and are not agent controls.

Device power loss, filesystem ENOSPC/EIO, platform sleep/wake behavior, the Windows/macOS/Linux deployment matrix and sustained load remain separate qualification work. PostgreSQL, cross-machine recovery and unsupported migration/dynamic-workflow scenarios are not advertised by this result. The M4 conformance catalog remains a specification with separate execution obligations.

Next: deployment and platform qualification (slice 8), then capacity and long-duration qualification (slice 9). See [artifact retention](https://www.lril.ai/engine-artifact-retention/) and [backup/restore](https://www.lril.ai/engine-backup-restore/) for maintenance and reconciliation requirements.


---

# Deployment and platform qualification

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

**Milestone status:** M6 is complete for the D-M6-01 supervised Windows 11 x64 / Node 24 / local SQLite developer profile. [M8 is now active](https://www.lril.ai/engine-m8-qualification/): its plan is complete; qualification and expanded release/support claims remain open.

M6 slice 8 adds deployment tooling for the local Node 24 + SQLite engine. Its implementation and local Ubuntu checks are delivered; **platform qualification remains open**. Windows, macOS, other Linux architectures/distributions and Docker must pass their native acceptance gates before release claims change. The released native SDK 0.3.0 packages are unchanged.

The [Windows native helper pipeline](https://www.lril.ai/diagrams/#windows-native-helper-pipeline) diagram shows how the signed helper is built, signed and manually qualified.

## Install and launch a candidate bundle

From a reviewed source checkout using the chosen Node 24 patch:

```sh
npm run engine:package -- build ./engine-candidate
npm run engine:package -- verify ./engine-candidate
node scripts/smoke-engine-package.mjs ./engine-candidate ./package-smoke.json
```

The packager refuses to overwrite a destination. It copies the current Node executable, engine sources, web assets and wire schema into a separate bundle; users do not need a global Node/TypeScript installation to launch it. `manifest.json` records runtime, architecture, OS, SQLite/schema versions and every payload's size and SHA-256. Verification rejects missing, extra, altered and symlink payloads. Approve the manifest digest independently: an unsigned manifest is not an authenticity guarantee. The packager does not establish that the selected runtime is the latest security patch or produce signatures/notarization.

Use `launch.sh /private/config/host.mjs` on Unix or `launch.cmd C:\private\config\host.mjs` on Windows. The configuration is trusted executable code. Start from `deploy/engine/host.example.mjs`; its identity adapter denies all requests and its capability verifier rejects startup until the host integration is implemented. Production identity enrollment, TLS certificates/trust, secret providers, durable local storage and task containment must be supplied and verified by the host. Never replace those checks with agent assertions.

`npm run engine:deploy -- /private/config/host.mjs` launches directly from source. The lower-level `engine:serve` and runner APIs remain host-building primitives; applications using them must implement equivalent deployment ownership and capability checks.

## Keep installation state separate

| Location | Contents and rule |
|---|---|
| Bundle directory | Reviewed read-only code, web assets, schemas and runtime; replace as a unit |
| Private configuration directory | Host module, TLS files, identity/secret-provider configuration; outside task workspaces |
| `stateRoot/identity` | Installation writer-lock database; preserve its pathname while an owner runs |
| `stateRoot/database` | Engine SQLite database, WAL and shared-memory companions |
| `stateRoot/artifacts` | Immutable content-addressed evidence |
| `stateRoot/workspaces` | Approved task execution directories |

The launcher checks canonical directories and rejects symlink/junction paths. Unix state directories must belong to the service identity and have mode 0700; unsafe existing permissions are rejected rather than silently changed. Windows owner ACL/reparse assurance remains a required host verifier check. Do not place state on shared/synchronized folders or allow task tools to modify identity/database/artifact directories. The local worker executes approved commands as the service identity and is not itself a sandbox.

An exclusive SQLite writer transaction on the separate `identity/owner.sqlite` file prevents duplicate launchers without blocking normal engine database writes. The lock is held through shutdown and is released by the OS on process death. Do not unlink or replace that file to bypass a live owner. After a crash, retained engine epochs and recovery holds still apply; acquiring the launcher lock does not reconcile uncertain outcomes.

Startup probes file write/fsync/read, Unix directory fsync, SQLite WAL/FULL settings, independent-process writer exclusion and durable reopen. Their report includes runtime, OS, filesystem and free space, with `certified:false`. These are capability probes, not power-loss proof. The trusted host must still verify volume semantics, private IPC, certificates, identity/secret providers and containment.

## Low disk and shutdown

The human sets `minimumFreeBytes`; the template uses the accepted 10 GiB starting allowance. Before each new coding dispatch, the launcher checks available bytes on database, artifact and workspace volumes. Failure leaves the claim releasable, consumes no new dispatch attempt and keeps the control server available. Restoring space permits later admission. No automatic cleanup or permission expansion occurs.

This is an admission check, not a filesystem quota or reserved disk partition: another writer or a running task can consume space after the check. Actual disk exhaustion may still block SQLite/control writes. Configure workload quotas and reserve policy through the trusted host; capacity measurements remain slice 9.

SIGINT/SIGTERM requests stop new host ticks, close controls, wait for the current execution and close storage before releasing ownership. A supervisor's forced timeout may interrupt an effect; restart must honor the resulting UNKNOWN/recovery state. Templates do not auto-restart an installation. Inspect and reconcile before resuming after an abnormal stop.

## Docker and service candidates

`deploy/engine/Dockerfile` requires a reviewed Node 24 Debian 13 image reference ending in `@sha256:…`. Its build check rejects a mutable reference or wrong runtime/distribution. Pinning gives reproducible base bytes; updates still need review and rebuilding. See [Docker's image-pinning guidance](https://docs.docker.com/build/building/best-practices/#pin-base-image-versions).

```sh
# Set AIWS_NODE_IMAGE to the reviewed immutable image reference.
# Set AIWS_HOST_CONFIG to a private, already provisioned configuration directory.
docker compose -f deploy/engine/compose.yaml build
docker compose -f deploy/engine/compose.yaml up
```

The Compose candidate uses UID/GID 10001, a read-only image/configuration, dropped capabilities, a bounded temporary filesystem, no restart policy and the `engine-state` named volume at `/var/lib/aiws`. Provision readable private configuration and use a local volume with tested durability. Preserve the named volume across upgrades; `down --volumes` removes it and is not part of normal shutdown. These settings follow the [Compose service reference](https://docs.docker.com/reference/compose-file/services/).

Networking is disabled by default. HTTPS remains loopback-only inside the container, so host-browser access is intentionally unavailable in this template. An operator can use the existing CLI inside the container with a provisioned CA and trusted credentials. Host-browser access requires a separately reviewed loopback TCP forwarding arrangement that preserves TLS, Host and Origin checks; do not simply bind the engine publicly or disable TLS verification. Approved task egress also requires a host-selected network/containment configuration.

The candidate `aiws-engine.service` supplies a dedicated service identity, private persistent state, read-only system paths and explicit shutdown behavior for systemd. Provision the user, reviewed bundle and host verifier before installation. Windows/macOS console launchers are supplied; native service installation, signing/notarization and OS-specific containment remain platform qualification work. No service or container was installed on the user's infrastructure.

## Native qualification and remaining targets

```sh
npm run qualify:platform -- ubuntu-24.04-x64 ./platform-run-001
```

Run on the actual matching OS/architecture with a fresh evidence directory. The script rejects hardware/process emulation mismatches and incorrect minimum distributions. Windows requires a workstation edition of Windows 11 build 26200, matching [Microsoft's 25H2 release identity](https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information); Windows Server and newer unqualified versions are not substitutes. macOS 15 must run natively on the requested hardware.

The manual `engine-platforms.yml` workflow targets provisioned self-hosted runners labeled `aiws-qualification` and the exact matrix cell. It retains logs even on failure. Runner owners supply reviewed Node 24, OpenSSL, platform tooling and isolation. No missing runner is silently replaced by an emulated or differently versioned machine. Docker checks additionally require host-recorded immutable image identity and container-runtime version.

| Target | Current evidence |
|---|---|
| Ubuntu 24.04 x64 | Local container source checks: 276 tests/typecheck; bundled runtime, TLS/web shell, shutdown and restart passed. Not native-install/device certification |
| Ubuntu arm64; Debian 13 x64/arm64 | Unverified; matching native runners required |
| Windows 11 25H2 x64/arm64 | Unverified; native workstation runners, ACL/process and installer checks required |
| macOS 15 Intel/Apple Silicon | Unverified; native runners, signing/notarization and lifecycle checks required |
| Docker Debian 13 amd64/arm64 | Templates supplied; image build and durable-volume tests not run because Docker is unavailable here |

`deploy/engine/platform-matrix.json` therefore certifies zero cells. A successful automated run is `CHECKS_PASSED_NOT_CERTIFIED`, not a platform release approval. Sleep/wake, machine restart, persistent-volume restore, device durability, host integrations and minimum/newest release coverage remain mandatory evidence. Slice 8 stays open for those checks; [failure injection](https://www.lril.ai/engine-failure-injection/) and [backup/restore](https://www.lril.ai/engine-backup-restore/) supply the reusable lower-level tests.

## Repeatable package acceptance

The platform runner now also builds and checks an unsigned embedded-runtime candidate. It verifies package tamper detection, TLS/web assets, denied unauthenticated controls, exclusive coordinator ownership, graceful restart and recovery after coordinator termination. Retained work must remain unapproved and its plan artifact readable after both restarts.

On the target machine, install the project dependencies and run the matching platform ID with a new evidence directory:

```sh
npm ci
npm run qualify:engine:platform -- windows-11-25H2-x64 docs/evidence/m6-slice8/windows-local-01
```

The report binds tested source and runtime hashes, full engine results and package evidence. `CHECKS_PASSED_NOT_CERTIFIED` means the automated checks passed. Exit code 2 means recorded platform gaps; exit code 1 means a failure. Native certification is never granted automatically.

The worker-termination probe runs on Windows too. If an effect is durable but a terminated worker looks like a normal failure, the report names that gap instead of skipping the probe. Windows directory durability remains a separate gate. Signed installation, real ACL/containment adapters, machine restart/sleep/wake and device durability still need evidence from the actual target machines. S5 browser qualification does not replace these M6 requirements.

## M8 native delivery preparation

Slice 3 is in progress for platform inventory and delivery preparation. See the
[preparation record](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M8-SLICE3-DELIVERY.md)
for each target cell's access/signing blockers, schema-edge inventory and native
install/upgrade handoff. Full package signing and supported migration execution
remain separate from the existing automated signed-helper checks. Heavy local
testing is deferred while the same physical host runs slice 2 capacity measurements.
No platform support claim or D-M6-01 boundary changes.

## Pinned M8 candidates and offline delivery

Candidate tooling now records exact source, runtime and helper identities. From a
clean reviewed Git checkout, collect inputs with:

```sh
node scripts/package-engine.mjs inputs
```

Save and independently review the resulting JSON, add `candidateVersion` (for
example `0.0.0-m8.1`), and use that approved file:

```sh
npm run engine:package -- build-pinned /private/new-candidate /private/approved-pins.json
npm run engine:package -- verify /private/new-candidate
```

The pinned builder rejects dirty package inputs and mismatched source/runtime/helper
hashes. The final manifest remains unsigned; authenticate its digest through the
approved signing/review procedure. A signed helper alone does not authenticate the
complete package. Development `build` bundles remain available but cannot be
registered by the delivery tool.

The following administrative commands stage candidates without starting the engine:

```sh
npm run engine:delivery -- install /private/new-candidate APPROVED_MANIFEST_SHA256 /private/new-install /private/external-state
npm run engine:delivery -- replace /private/next-candidate NEXT_MANIFEST_SHA256 /private/new-install PREVIOUS_MANIFEST_SHA256
npm run engine:delivery -- uninstall /private/new-install CURRENT_MANIFEST_SHA256
```

Use absolute paths and an owner-controlled parent with native owner-only ACLs. The
package, installation and external state paths must not overlap or contain symlinks
or junctions. Install creates a new directory, verifies the copied package and records
a HELD selection. Replace requires the exact previous manifest digest, retains the
old payload, and selects a new HELD candidate. Uninstall deregisters the selection;
it retains package bytes, journals and all external state. It neither stops a running
service nor deletes its files. Do not use this offline tool as a live-service updater.

No command opens the engine database, executes host configuration, starts a process
or runs a schema migration. Selection metadata does not enforce a hold on manually
invoked launchers. Before manually launching a replacement, complete the reviewed
quiescence, backup/held-restore, component-schema compatibility and native acceptance
procedure. Equal schema `1` labels do not authorize an upgrade. No schema migration
edge is implemented, and these tools make no automatic rollback/resume promise.

On failure after locking, preserve `delivery.lock`, the attempt journals and partial
candidate. Do not remove the lock and retry blindly. Review selection and manifests,
then use a fresh installation root for a reviewed retry. Delivery interruption tests
are distinct from database migration phases and machine/device durability tests.

Docker now includes all three runtime schemas: wire, dynamic controls and readiness.
Hosted CI tests actual pinned runtime bundles on Windows and Ubuntu; those tests do
not qualify the native platform matrix. Signing, native install/upgrade and production
host integrations remain open under [M8](https://www.lril.ai/engine-m8-qualification/).

## Windows candidate catalog verification

The manual **Signed Windows x64 candidate** workflow runs only from `main` on a
hosted Windows runner. It pins Node 24.19.0 against the vendor checksum, signs the
helper through the existing Azure OIDC identity, builds the pinned payload, and
signs a SHA-256 Windows catalog covering the complete payload and manifest.
The expected publisher comes from the previously reviewed signing identity.

The catalog remains outside `payload/` to avoid circular hashes. The payload's
`signed:false` field continues to describe its unsigned JSON manifest; authenticate
that manifest and all payload files through the external signed catalog. Require
valid Authenticode trust, a timestamp, the expected publisher, exact catalog/file
hash agreement and ordinary package inventory verification. Do not trust a publisher
name or manifest digest taken only from the downloaded candidate itself.

From a reviewed source checkout and trusted Node installation, before running any
bundled code, use `scripts/verify-windows-candidate.ps1` with `-PackagePath`,
`-CatalogPath`, independently approved `-ExpectedPublisher`, and a new `-ReportPath`.
The workflow performs these checks before and after acceptance of the exact supplied
payload, including the favicon and catalog tamper rejection. It retains the payload,
catalog, input pins, signature reports and package/lifecycle evidence as a private
workflow artifact. Failed workflow artifacts are diagnostic evidence, not candidates
approved for installation.

Hosted Windows Server acceptance is not Windows 11 native qualification. Held
same-payload replacement is not a database upgrade. Native enrollment, upgrade,
service uninstall, OS-specific paths and machine/storage recovery remain open. This
workflow publishes no registry package or public release and does not touch the
capacity measurement host.


## Windows protected TLS provider

M8 slice 4 adds `WindowsDpapi` (`engine/src/windows-dpapi.ts`) and
`dpapiTlsProvider` (`engine/src/host-tls.ts`). This is an initial component integration;
the deployment template still refuses startup until a trusted host verifier supplies
the missing identity, ACL, private IPC and containment guarantees.

A trusted host configuration can supply `tlsProvider` instead of both `tlsKeyFile`
and `tlsCertificateFile`. Supplying both forms is an error. Provider credentials are
validated before the control host opens its engine database or creates work. The
outer deployment launcher can still create layout/lock/probe files before this check.
An unavailable provider, invalid certificate or mismatched key fails closed with a
redacted error; it never falls back to file credentials. Existing file configuration
remains an explicit option for the accepted supervised developer profile.

For Windows, construct `WindowsDpapi` with an absolute protected `helperPath`, an
independently authenticated `helperSha256`, a nonsecret `installationId` and a
nonsecret `purpose` such as `tls-key`. Obtain the pin from reviewed signed-package
evidence; hashing an untrusted executable does not establish trust. Protected parent
directories must prevent replacement between verification and execution. The old
slice 3 candidate predates this adapter; it cannot supply the new helper commands.

`protect(Buffer)` returns CurrentUser DPAPI ciphertext and `unprotect(Buffer)`
returns plaintext. Both require the same account/profile and installation/purpose
context. Plaintext is limited to 64 KiB, ciphertext to 256 KiB, and helper calls time
out after five seconds by default. Data travels through binary parent-child pipes;
arguments contain only the operation and context. There is no LocalMachine mode
or interactive prompt. The caller owns input/output buffers and should erase
plaintext as soon as it is no longer needed; no command-line secret input is provided.

Connect `dpapiTlsProvider(codec, loadProtectedKey, loadCertificate)` to `tlsProvider`.
The callbacks return fresh buffers containing ciphertext and the public PEM certificate.
The provider erases its ciphertext buffer after decryption. The host takes ownership
of the returned private-key buffer and erases it after creating the TLS context, or
on startup failure. Never return shared/cached key buffers. TLS necessarily retains
key material internally while serving; this does not protect against memory dumps,
a compromised same-account process, administrators or the operating system.

Real Windows CI covers CurrentUser component behavior with generated fixture data.
Separate-account rejection, chosen service/session-0 profile availability, rotation,
backup exclusion and full host integration still require native acceptance. No new
production or platform support follows from these component checks.


---

# Run a governed coding workflow

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

The M5 source now composes an executable **approval → implementation → validation → correction → revalidation → acceptance** workflow. The same contracts also run a document-review example. The released SDK 0.3.0 packages and public engine wire API have separate scope; these APIs live in the TypeScript `engine/` source.

M5 remains in progress. This slice proves a local, sequential workflow with trusted commands. CLI/web interfaces, organization enrollment, the complete wire codec/host contract integration and the remaining conformance/recovery obligations are not complete.

Two interactive diagrams accompany this page: the [governed coding workflow](https://www.lril.ai/diagrams/#governed-coding-workflow) and the [work order lifecycle](https://www.lril.ai/diagrams/#coding-work-order-lifecycle).

## Run the example

Use Node.js 24 from the repository root:

```sh
node engine/examples/coding-workflow.ts --demo
npm run test:engine:workflow
npm run engine:typecheck
```

The example deliberately implements subtraction where addition is required. A real Node assertion fails, one approved corrective command changes the implementation to addition, and the assertion passes. The workflow waits for acceptance before it records success. Four attempts are retained.

`--demo` explicitly selects a **simulated human session**. SQLite, subprocess execution, generated source, tests, artifact hashing and corrective work are real. This is not an organizational authentication example. The program prints its result and removes its temporary database, workspace and artifacts.

## Configure the runner and authentication boundary

```typescript
import { CodingWorkflowRunner } from './engine/src/coding-workflow.ts';
import { approvalSubject } from './engine/src/coding-workflow-types.ts';

const runner = await CodingWorkflowRunner.open({
  database: '/trusted/state/engine.sqlite',
  workspaceRoot: '/trusted/workspaces',
  artifactRoot: '/trusted/artifacts',
  authentication: yourAuthenticationAdapter,
});
```

`yourAuthenticationAdapter` must implement both operations:

| Method | Host responsibility |
|---|---|
| `authenticate(credential)` | Verify the real session/token and return a human principal with an auditable, nonsecret evidence reference, or return `null` |
| `authorize(human, action, workOrderId, subjectDigest)` | Apply current permission policy for `APPROVE`, `RESUME` or `ACCEPT` on the exact work and material |

The runner never treats a caller-supplied `{ kind: 'HUMAN' }` as authentication. The adapter is trusted host code, and adapter verification is what establishes identity. Credentials are passed to it and are not serialized into the workflow. A fabricated adapter can fabricate authority; repository DTO checks alone cannot prevent that.

The artifact directory must remain outside tool-writable workspaces and under host control. Each work order/run receives a dedicated workspace derived from its identity. Commands are argument arrays executed without a shell wrapper. They run with the host process's privileges: this is **process execution, not an OS sandbox**. Use only reviewed local commands in this slice; external provider billing and hostile-tool isolation need their respective adapters.

## The immutable plan

`create(plan)` records a draft and publishes its canonical plan bytes. It does not dispatch anything. Repeating creation with the same identity and plan returns the existing work; a changed plan is rejected instead of resetting its budget or history.

| Field | Meaning |
|---|---|
| `workOrderId`, `runId` | Stable assignment and execution identities |
| `description`, `criteriaRef` | Intended result and named acceptance criteria |
| `implementation`, `validation`, `correction` | Exact approved command/argument arrays for the three stages |
| `outputPaths` | Unique relative files that form the deliverable and validation subject |
| `maxCorrections` | Maximum corrective tasks for this work order |
| `reapproveCorrections` | Whether each correction waits for a new human approval |
| `maxAttempts` | Total dispatch attempts, including implementation, validation and correction |
| `maxTaskMs` | Per-attempt active-time reservation; must fit the local Node timer range |
| `maxTotalActiveMs` | Cumulative active-time ceiling for the work order |

The built-in adapter is for unmetered local execution: it reserves and settles cost as `"0"`. Active time and attempt counts still accumulate. It does not measure model/API charges or introduce a paid handoff summary. Handoffs are deterministic records.

The plan author must choose a trustworthy validation command. Keep validation logic outside agent-writable files or embed it in approved arguments, as the example does. The pinned deliverable consists of `outputPaths`; unspecified workspace files are not implicitly validated or accepted.

## Approve, execute and accept

```typescript
const draft = await runner.create(plan);
await runner.approve(
  plan.workOrderId,
  draft.revision,
  approvalSubject(draft.state),
  approvalExpiresAt,
  protectedSession,
);

const result = await runner.runUntilBlocked(plan.workOrderId);
```

`runUntilBlocked` advances the local sequence until a human gate, hold or acceptance is reached. `step(workOrderId)` executes or reconciles one stage. One runner performs one step at a time; this sequential executor is a slice implementation, not an advertised ceiling for the general scheduler.

| Status | Meaning |
|---|---|
| `AWAITING_APPROVAL` | Draft or correction requires a human decision; the task cannot dispatch |
| `READY` | Stage can progress if control, ownership, approval, inputs and resource checks permit it |
| `AWAITING_ACCEPTANCE` | Current deliverable passed its real validation; success is still pending |
| `HELD` | Execution failed, a limit was reached, an artifact changed, or responsibility is uncertain |
| `SUCCEEDED` | A permitted human accepted the exact validated deliverable and no owned work/responsibility remained unresolved |

For a correction requiring reapproval, read the current record and approve its new `approvalSubject`. The original attempt history, scope and usage remain. When correction reapproval is disabled, only the correction commands and bounds already contained in the human-approved immutable plan can run.

Final acceptance is explicit:

```typescript
const current = await runner.get(plan.workOrderId);
await runner.accept(
  plan.workOrderId,
  current.revision,
  current.state.deliverable!.digest,
  protectedSession,
);
await runner.close();
```

Acceptance checks current revision, successful validation, deliverable digest, immutable artifact bytes, current workspace outputs, unresolved attempts/tasks, holds and work-order controls. Passing a test or receiving a handoff is not acceptance. A canceled or paused work order cannot pass this acceptance gate.

This fixed-plan runner does not yet expose every remediation operation. An expired approval blocks dispatch. Held work requires an appropriate resolution capability; do not recreate it under a fresh ID to reset limits. Comprehensive limit-adjustment, reconciliation and approval-renewal interfaces remain integration work.

## What is atomic

The final dispatch transaction checks the composed workflow's ready state and epoch, immutable command material, scope and resource maxima, approval expiry and acknowledged input handoff. It consumes the handoff together with dispatch responsibility and ancestor reservations. A failed dispatch leaves the handoff unconsumed.

Stage completion is another single SQLite transaction. It records validation/settlement, marks the task complete, applies the correction bound, creates the next task and handoff when needed, and advances the workflow record and audit event. The validation repository now uses the scheduler's real `scheduler_sequence` and fairness tables; the former component-only test table mismatch is fixed.

Artifacts are published and fsynced before references commit. A crash before the database transaction may leave an unused artifact, which grants no authority. Manifest and content hashes are verified at input and acceptance. Validation is bound to the same declared output manifest before and after the test; changed source cannot borrow an older successful result.

The completed worker evidence contains the executed command, exit code, stdout/stderr, timestamps, measured monotonic duration and artifact digests. A repeated attempt cannot substitute different validation evidence.

## Pause and restart

The runner uses the existing durable controls through its bounded storage executor. `controlPause`, `controlResume` and cancellation APIs remain **trusted host APIs**; their actor DTOs do not authenticate a caller. A future CLI/web handler must verify identity before constructing those commands. Pause blocks new dispatch; already-authorized responsibility is preserved.

Opening the runner advances the coordinator epoch. That does not authorize a previously active workflow to resume. A subsequent `step` reports `RECOVERY_REQUIRED` until an authorized human records:

```typescript
const stopped = await runner.get(plan.workOrderId);
await runner.resume(
  plan.workOrderId,
  stopped.revision,
  approvalSubject(stopped.state),
  protectedSession,
);
```

If durable worker evidence already exists, the runner finishes the recorded stage without executing its command again. If dispatch was authorized but completion evidence is absent, restart approval returns `RECONCILIATION_REQUIRED`. It cannot establish that the external action never ran. Timeouts retain `UNKNOWN` responsibility and active-time exposure, with no automatic correction or retry.

## Verification and current limits

The source suite passes **87 engine tests**, including **15 composed-workflow integration tests**. It covers real failing/passing commands, correction reapproval/exhaustion, attempts, stale artifacts, pause/cancel, authentication/authorization failures, transaction rollback, timeout uncertainty, and a non-coding review workflow.

A real child coordinator is killed after it stores execution evidence. After reopening the actual database and recording a human restart decision, the workflow completes with exactly the expected four command executions. This supplements the earlier persistence crash tests; it does not complete all M4 failure scenarios or prove power-loss durability.

Native Windows/macOS qualification, process-tree cancellation and hostile-worker sandboxing, public wire/CLI/web handlers, full identity/enrollment integration, PostgreSQL, retention/restore and the full conformance catalog remain outside this slice's evidence. See [persistence](https://www.lril.ai/engine-persistence/), [worker execution](https://www.lril.ai/engine-worker-execution/), [validation and correction](https://www.lril.ai/engine-validation-correction/), and [human controls](https://www.lril.ai/engine-controls-handoffs/).


## Authenticated control follow-up

The next source increment exposes this lifecycle through the [engine API, CLI and web controls](https://www.lril.ai/engine-control-api/). It adds exact wire decoding, verified host sessions, single-use approval challenges and atomic command receipts. The 87-test count above is this composition slice's historical evidence; the control follow-up expands it. Production identity-provider enrollment and full engine conformance remain open.


---

# Praxis API, CLI and web controls

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

The engine source now exposes the composed coding workflow through a common **HTTPS API, CLI client and web control page**. A trusted host supplies identity verification and TLS configuration. Approval, pause, resume, cancellation requests and acceptance use the same transaction boundary regardless of interface.

This is the `aiws-local-coding-controls/1` implementation profile of the checked-in engine wire schema **1.1.0**. M5 remains in progress. The profile operates existing host-created local workflows; it is not the complete engine command catalog, a standalone enrollment installer, or a released SDK package.

An interactive [control API decision lifecycle](https://www.lril.ai/diagrams/#control-api-decision-lifecycle) diagram traces inspect, approval challenge, command, receipt and exact-retry replay.

## What is available

| Interface | Implemented behavior |
|---|---|
| Capabilities | Authenticated codec/storage/approval capabilities; exact schema and profile headers |
| Queries | Inspect a pinned work-order revision; list a single complete inventory page; retrieve your durable command receipt |
| Approval challenge | Recently verified human, exact proposal/material, principal and session binding, two-minute maximum expiry |
| Commands | Approve a plan/correction, accept validated work, pause, resume and request cancellation |
| Artifacts | Authorized download of the current projection, plan and deliverable manifest/files |
| CLI | Verified loopback TLS, credential input over a protected pipe, persisted request JSON, structured responses |
| Web | Inspect plan/evidence, see current revision and status, make explicit decisions, retry the same uncertain command |
| Local host | Starts HTTPS and polls configured workflows sequentially; no execution before approval |

Acceptance is an `approve` command over the current acceptance proposal, not an extra unversioned wire command. `APPROVAL` and `WORK_ORDER` references share the local work-order ID in this profile, with distinct kinds and an exact workflow revision. Inspect details supplies the authoritative proposal reference and binding. The immutable local execution contract is the approved plan identified by `local-plan:<planDigest>`; general definition/contract registration and additional runs are not exposed here.

`GET /engine/v1/capabilities` returns only installed profile features. Every response includes `AIWS-Schema-Version: 1.1.0` and `AIWS-Control-Profile: aiws-local-coding-controls/1`. Unsupported commands, enrollment, historical codecs, event streaming and pagination continuations fail explicitly. Advertising the codec means its shapes are understood; it does not mean every catalog operation is implemented.

## Start a configured host

The source requires Node 24. Supply a trusted configuration module, outside request-controlled workspaces. It exports a `ControlHostOptions` object:

```ts
// host-config.ts — schematic wiring; your adapter must verify real identities.
import { readFile } from 'node:fs/promises';
import type { ControlHostOptions } from './engine/src/control-host.ts';
import { identityAdapter } from './your-verified-identity-adapter.ts';

export default {
  database: '/protected/aiws/engine.sqlite',
  workspaceRoot: '/work/aiws',
  artifactRoot: '/protected/aiws/artifacts',
  namespace: 'local',
  tlsKeyFile: '/protected/aiws/tls-key.pem',
  tlsCertificateFile: '/protected/aiws/tls-cert.pem',
  port: 7443,
  identity: identityAdapter,
  plans: JSON.parse(await readFile('/protected/aiws/plans.json', 'utf8')),
} satisfies ControlHostOptions;
```

```sh
node engine/src/control-host.ts ./host-config.ts
```

Paths above are placeholders. Use native protected paths and OS ACLs for your platform. The host binds **127.0.0.1 only**, requires TLS key/certificate files and serves the controls at its printed HTTPS origin. It does not disable certificate verification or install a certificate authority. Configure browser trust explicitly; the CLI receives the pinned installation certificate/trust file separately. Remote gateways, IPv6 listener qualification and certificate rollover are not implemented in this slice.

The host creates only the configured immutable plans. Reopening the same work-order identity preserves its plan and accounting. The loop waits for a recorded approval and executes one local step at a time; paused, held or unapproved work cannot bypass admission. After restart, log in again and use the human resume decision before continuing eligible work. This sequential runner is an implementation slice, not a product concurrency ceiling.

## The identity adapter is a trust boundary

`ControlIdentityAdapter` lives in `engine/src/control-service.ts`. The host installs it directly; public callers cannot choose the provider or submit trusted identity facts.

| Method or field | Host obligation |
|---|---|
| `authenticate(credential, context)` | Verify the protected Authorization header or browser cookie against the configured issuer, audience, signature, revocation and session policy; return a verified human session or null |
| `authorize(session, action, workOrderId, context)` | Evaluate the exact operation and target under current host policy; inventory access needs namespace-wide list permission and inspect permission for every returned work order |
| `authMethod` | Identify the installed local user-verification or OIDC/PKCE method |
| Optional `handleLogin(request, response, context)` | Implement `/auth/start` and `/auth/callback` GET routes, including interactive verification, PKCE/state/nonce checks and protected session-cookie issuance |

The engine validates session expiry, verified-human evidence, installation boot epoch and durable auth epoch. Elevated approval/resume decisions require verification within five minutes. Authorization leases last at most five seconds and are checked again inside the SQLite transaction. A host can revoke all current control authority through the internal `controlRevoke` operation; the new auth epoch invalidates earlier sessions/challenges. Provider-specific logout, per-session revocation and refresh policy remain the adapter's responsibility.

The adapter must independently establish human identity. **A bearer token, a test fixture string, or an object containing `kind: "HUMAN"` is not sufficient proof.** Session and CSRF secrets never enter command JSON, workflow state or command receipts. Browser cookies must be Secure, HttpOnly, host-only and SameSite=Strict. Cookie-authenticated POSTs require exact Origin and session-bound `X-AIWS-CSRF`; Authorization and cookie credentials cannot be combined.

Owner enrollment, invitation flows, recovery enrollment and production provider implementations remain open. `startControlHost` is for integration with an already provisioned trusted host. It does not claim to implement the entire [M4 authentication design](https://www.lril.ai/engine-design/). Tests use explicit simulated identity providers and real HTTPS/SQLite/subprocesses.

## Inspect before deciding

All POST bodies use the selected envelope, including installation ID from capabilities, namespace and a fresh request ID. Quantities and revisions use canonical decimal strings. The server rejects unknown fields, duplicate JSON keys, malformed UTF-8, unpaired surrogates, unsafe numbers, excessive depth and unsupported versions.

An inventory query discovers current references without guessing their revisions:

```json
{
  "protocol": "aiws-engine/1",
  "schemaVersion": "1.1.0",
  "kind": "query",
  "installationId": "INSTALLATION_ID_FROM_CAPABILITIES",
  "namespace": "local",
  "requestId": "inventory-request-1",
  "query": {
    "name": "list",
    "payload": {
      "kind": "WORK_ORDER",
      "parent": null,
      "pageCursor": null,
      "pageSize": 1000
    }
  }
}
```

This initial inventory implementation returns a complete page or rejects the request if it cannot fit. It never silently truncates. More than 1,000 stored work orders can exist; multi-page inventory queries remain unimplemented. `inspect` takes the exact returned `WORK_ORDER` reference. A stale revision receives `STALE_REVISION`; refresh inventory and review changed material.

A record's `details` blob contains the complete local workflow projection, current controls, attempts, scope exposure, handoffs and holds. Its `approvalSubject` and `binding` define the decision material. Download it using:

```text
GET /engine/v1/artifacts/<encoded-work-order-id>/<details-digest>
```

The download rechecks work-order authorization and permits only currently referenced artifacts. Old projection/archive retrieval is not advertised; refresh the inventory if the current projection changed. A digest does not grant read access. Work orders project as OPEN, SUSPENDED or CLOSED, and close as COMPLETED only after acceptance. Cancellation remains CANCEL_PENDING until the remaining responsibility is resolved; it never asserts that a running process stopped.

## Make a human decision

For initial/correction approval or acceptance:

1. Inspect the current work order, its plan, validation and deliverable manifest.
2. POST `ApprovalChallengeRequest` to `/engine/v1/approval-challenges`, copying the current `APPROVAL` subject and binding with `action: "approve"`.
3. Persist an exact `CommandRequest` with `command.name: "approve"`, the proposal, binding and returned challenge ID. `expectedRevisions` contains the corresponding `WORK_ORDER` reference.
4. POST that file to `/engine/v1/commands`. The server consumes the challenge, applies the decision, records audit state and stores the command receipt in one transaction.

The challenge expires within two minutes and cannot move to another session, principal, revision or material. Execution approval lasts at most five minutes and no longer than the verified session returned by the adapter. A later expiry blocks new admissions; renewal/remediation is not implemented by silently creating a new identity.

For `pause`, send the pinned target; for `resume`, send the target and its current binding; for `cancel`, send the target and a reason code. These commands use the same expected-revision and durable-receipt rules. Resume requires recent human verification and refuses unresolved uncertain actions or terminal/canceling work. Pause prevents new admissions; existing execution evidence remains available for settlement. Cancellation is a request, not forceful process termination.

## CLI usage and uncertain responses

```sh
# The trusted login helper writes {"authorization":"Bearer ..."} to its pipe.
# Never put a real credential into this command line or a request file.
verified-login-helper | node engine/src/control-cli.ts \
  https://127.0.0.1:7443 /protected/aiws/tls-cert.pem queries ./inspect.json

verified-login-helper | node engine/src/control-cli.ts \
  https://127.0.0.1:7443 /protected/aiws/tls-cert.pem commands ./decision.json
```

`verified-login-helper` is an integration placeholder for your configured authenticator, not a bundled utility. The CLI validates TLS, sends the credential in the Authorization header, prints the structured response and returns a nonzero exit status for engine errors. It requires a persisted request file for mutations and does not invent another request ID after a timeout.

A receipt means the database accepted the decision, not that execution finished. If the response is lost or `commitStatus` is UNKNOWN, query `commandResult` with the original request ID and the same verified issuer/subject. You may retry the **identical request file**. The engine checks the durable key/digest before stale revisions or consumed challenges; an exact retry returns REPLAYED, while changed material under that ID returns REQUEST_ID_CONFLICT. Canonical request bytes use RFC 8785 key ordering without changing historical SDK/domain digests.

The web page uses these same commands and a separate explicit confirmation before each decision. It retains an uncertain request in memory for exact retry and warns before leaving the page. It does not persist sessions in browser storage. For recoverability across closing/reloading the page, use the CLI with a persisted request file; durable browser request recovery is still open. No command receipt or lookup from another principal is disclosed.

## Verification and remaining scope

All **105 engine tests pass**, including **18 control API/codec/host checks**. The control tests exercise the selected wire schema, real loopback TLS, command transactions, CLI subprocess, host execution, CSRF/origin/Host checks, authorization expiry/revocation, session-bound challenges, artifact changes, command replay and database reopen. The runtime codec checks all 429 current/historical shape fixtures; the runtime advertises only 1.1.0.

The control page's static assets and HTTP boundaries are tested. Visual browser verification is unclaimed because the available browser could not reach the local checkout. Full enrollment/provider integration, streamed artifact IO, event/paged history, dynamic policies, cancellation finalization, held-work remediation, retention, capacity and platform qualification remain future work. Continue with the [composed workflow](https://www.lril.ai/engine-coding-workflow/) and [persistence evidence](https://www.lril.ai/engine-persistence/).


---

# Worker dispatch and coding execution

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M5 now includes a durable worker-delivery layer for committed `DISPATCH_AUTHORIZED` attempts. This page describes engine source behavior in progress; it does not change the released SDK 0.3.0 finite-v1 API surface.

An interactive [worker dispatch lease](https://www.lril.ai/diagrams/#worker-dispatch-lease) diagram traces the claim, admission, acknowledgement, execution and outcome classification.

## Authority and delivery are separate

A scheduler claim does not authorize execution, and a worker delivery does not create new authority. The material action becomes authorized only when supervised admission commits `DISPATCH_AUTHORIZED`. The worker layer then transports that existing authority to the exact worker and coordinator epoch.

```text
CLAIMED
  -> supervised admission
  -> DISPATCH_AUTHORIZED
  -> durable dispatch intent
  -> worker delivery claim
  -> worker acknowledgement
  -> controlled command execution
  -> execution evidence
```

A worker may claim an intent only when the underlying attempt is still `DISPATCH_AUTHORIZED` and belongs to the same coordinator epoch. Each dispatch intent has at most one durable worker-delivery record.

## Controlled workspace execution

`LocalCodingWorker` executes an argument-vector command with `shell: false` under a configured workspace root. The relative working directory must resolve inside that root. Artifact paths are checked the same way. Parent traversal outside the approved workspace is rejected.

The worker records:

- exact command arguments;
- relative working directory;
- start and finish timestamps;
- exit code and signal;
- stdout and stderr;
- produced artifact paths, sizes, and SHA-256 digests;
- a durable evidence identity;
- execution outcome: `SUCCEEDED`, `FAILED`, or `UNKNOWN`.

Timeout is conservative. If a process is terminated because its time bound expired, the worker reports `UNKNOWN`; timeout is not proof that a material effect did not occur.

## Outcome semantics

`SUCCEEDED` and `FAILED` are worker execution observations. They are not equivalent to workflow verification or acceptance. The following M5 slice binds execution evidence to actual validation, limit settlement, correction rules, and handoffs.

`UNKNOWN` is also propagated to the governed dispatch attempt so the engine does not blindly retry an action whose external effect may be ambiguous.

## Current implementation files

- `engine/src/worker-execution.ts` — durable delivery repository and controlled local coding worker.
- `engine/src/storage-worker.ts` — storage-thread routing for worker delivery operations.
- `engine/src/storage-executor.ts` — coordinator-facing asynchronous delivery API.
- `engine/test/worker-execution.test.ts` — delivery, epoch, UNKNOWN, real process, artifact digest, and workspace containment tests.

The dedicated Node 24 engine CI runs strict TypeScript checking and the complete engine test suite for these sources.

## Composed workflow follow-up

The [governed coding workflow](https://www.lril.ai/engine-coding-workflow/) now connects these primitives into real approval, implementation, validation, correction and acceptance. Its source suite includes a real coordinator-termination/restart test. Earlier counts and next-step descriptions on this page record the original component delivery; M5 still needs broader interface and conformance integration.


---

# Praxis validation, settlement and correction

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

The executable engine now separates **worker execution success** from **workflow validation**. A command that exits successfully is evidence that the worker ran successfully; it does not by itself prove that the implementation satisfies the workflow's acceptance criteria.

## Validation boundary

Validation is recorded against an exact dispatch attempt and task. The record includes:

- validation outcome: `PASSED`, `FAILED` or `UNKNOWN`
- criteria reference
- evidence reference
- actual cost
- actual active time
- validation timestamp

`PASSED` and `FAILED` are settled outcomes. `UNKNOWN` is not.

## Settlement

Dispatch admission reserves the maximum authorized cost and active time across every applicable ancestor scope. After validation, the engine can reduce that conservative exposure to the actual measured usage.

For example, if an attempt was authorized for a maximum cost of `30` and `5000` ms active time but validation reports actual usage of `12` and `1200` ms, every ancestor scope retains `12` cost and `1200` ms active-time exposure for that attempt rather than the original maximum reservation.

This is a release of unused reservation, not a refund of work that actually happened.

HANDOFF-purpose attempts use the same settlement rule while remaining inside the protected handoff partition.

## Failed validation and correction

A work order may configure a bounded correction rule:

```ts
await storage.validationConfigureCorrectionRule({
  workOrderId: 'wo-123',
  maxCorrections: 2,
  reapprovalRequired: true,
});
```

When validation fails and allowance remains, the engine creates a new READY correction task. The correction receives a new task identity and carries durable references to the failed attempt, source validation, source task and evidence.

The correction payload also records whether reapproval is required. This flag does not authorize execution; the correction must still pass the normal supervised-admission and `DISPATCH_AUTHORIZED` gate.

## Correction exhaustion

If the configured correction count is exhausted, the failed attempt is still settled to actual usage. No further task is created, and the validation records `correction_exhausted=1`.

This is intentionally not an automatic terminal failure. The next human-control layer can turn that condition into a hold, cancellation choice, replanning request or human-approved resource change.

## UNKNOWN validation

`UNKNOWN` means the engine cannot truthfully establish whether the material outcome satisfies the criteria. In this case:

- maximum exposure remains reserved;
- the attempt moves to `UNKNOWN`;
- no correction task is generated;
- reconciliation or human intervention is required before potentially duplicating work.

A timeout, worker loss or incomplete external observation is never treated as proof that nothing happened.

## Idempotency

Validation is unique per dispatch attempt. Re-submitting the same attempt after the validation has committed returns the durable result instead of creating duplicate settlement or corrective work.

## Current M5 status

The Node 24 engine verification suite passes 44/44 tests covering persistence, scheduling, coordinator recovery, supervised dispatch, worker delivery/execution, validation settlement and bounded correction.

The remaining M5 work is primarily orchestration: durable stage handoffs, human pause/hold/cancel/resume controls, a complete coding workflow with correction and revalidation, and restart/conformance proof across the full scenario.

## Composed workflow follow-up

The [governed coding workflow](https://www.lril.ai/engine-coding-workflow/) now connects these primitives into real approval, implementation, validation, correction and acceptance. Its source suite includes a real coordinator-termination/restart test. Earlier counts and next-step descriptions on this page record the original component delivery; M5 still needs broader interface and conformance integration.


---

# Praxis human controls and handoffs

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

> **Implementation status:** M5 source implementation. These engine APIs are not part of the released finite-v1 SDK 0.3.0 package surface.

The M5 engine now has durable human controls and engine-owned handoffs layered on top of the scheduler, supervised dispatch, controlled worker execution, and validation/correction pipeline.

## Work-order control state

Each work order may have one durable control state:

- `RUNNING` — new material dispatch is allowed when every other admission check passes.
- `PAUSED` — new material dispatch is blocked, but existing responsibility remains.
- `CANCEL_PENDING` — new material dispatch is blocked while live/unknown responsibility is settled.
- `CANCELED` — cancellation is final after unresolved responsibility is cleared.

Pause and cancellation never imply that an external action stopped. A previously committed `DISPATCH_AUTHORIZED` or `UNKNOWN` attempt remains accountable until it is settled or reconciled.

## Dispatch race protection

The control subsystem installs a SQLite trigger at the authoritative dispatch table. If a work order becomes paused or cancel-pending while admission is racing toward commit, insertion of the material dispatch attempt is rejected.

This means a pre-check is not the final safety mechanism. The durable database boundary remains authoritative.

## Human-only control actions

The implementation records authenticated human evidence for:

- pause;
- resume;
- cancel request;
- cancel finalization;
- reconciliation retry decisions;
- agent replacement.

Rule/agent actors cannot use these APIs to change human control state or replace their own agent assignment. Agent replacement increments a durable assignment revision and does not reset budgets, attempts, approvals, or evidence.

## Cancellation and unknown outcomes

Cancellation may enter `CANCEL_PENDING` immediately, but it cannot become final while the work order still owns `DISPATCH_AUTHORIZED` or `UNKNOWN` material responsibility.

This keeps cancellation truthful: "cancel requested" is different from "all material work is safely settled."

## Engine-owned handoffs

M5 handoffs connect the major coding stages:

```text
planning -> implementation -> validation -> correction
```

A handoff records:

- work-order and run identity;
- producer task and consumer task;
- stage;
- immutable artifact reference;
- artifact digest;
- a non-authoritative summary;
- claim/acknowledgement/consumption state.

The durable lifecycle is:

```text
PENDING -> CLAIMED -> ACKNOWLEDGED -> CONSUMED
```

Acknowledgement means the intended consumer has durably received the handoff. It does **not** mean the consumer's work succeeded or was accepted.

## What survives pause, resume, or replacement

Control changes do not reset:

- cost or active-time usage;
- attempt counts;
- protected handoff usage;
- approval expiry/history;
- dispatch responsibility;
- validation evidence;
- correction history;
- consumed handoffs.

This is critical for resumability: a new worker or agent continues the same governed mission rather than receiving a fresh budget or authority window.

## Current tested behavior

The Node 24 engine suite currently covers 50 scenarios total. New control/handoff coverage verifies durable pause/resume, the database-level pause-vs-dispatch race, cancellation blocked by unresolved responsibility, human-only revisioned agent replacement, restart-safe handoff lifecycle, and append-only reconciliation decisions.

## Remaining M5 work

The next step is to compose the implemented primitives into the first complete coding workflow:

```text
plan
  -> human approval
  -> implementation
  -> handoff
  -> validation
  -> correction when needed
  -> revalidation
  -> acceptance
```

After that workflow works end to end, M5 still requires a restart/conformance closure pass that interrupts it at critical boundaries and proves recovery without duplicate material effects, lost responsibility, or stale approvals.

## Composed workflow follow-up

The [governed coding workflow](https://www.lril.ai/engine-coding-workflow/) now connects these primitives into real approval, implementation, validation, correction and acceptance. Its source suite includes a real coordinator-termination/restart test. Earlier counts and next-step descriptions on this page record the original component delivery; M5 still needs broader interface and conformance integration.


---

# Archon integration assessment

The approved direction is **Archon-inspired improvements to standalone AIWS**. Archon supplies ideas to evaluate; AIWS remains independently usable with its own interface, engine and SDKs. Decision **AS-01** supersedes the earlier AB-01 integration proposal. Integration remains an optional future project.

M7 slice 9's assessment and direction decision are complete for the documented local profile. No Archon adapter is required, implemented or certified. S1–S4 readiness, authoring, handoff diagnostics and governing context are implemented; S5 remains planned follow-up work.
The review uses `coleam00/Archon` commit `4d48a0325e64017aaaf2b3ad21c0faf9dc368520` and AIWS commit `f28d179409e61d9b03e03e877d000b76d5700d13`. Ten inspected source files were checked against their pinned Git blob hashes. This is a source assessment of that revision, not a claim about the latest branch or a tested Archon deployment.

## What fits, and what needs translation

| Archon capability found in the pinned source | AIWS integration rule |
|---|---|
| Atomic approval-gate resolution and audit writes | Retain concurrency safeguards, but independently verify the human and exact AIWS approval subject |
| Resume of failed/paused runs and eligible stale running rows | AIWS FAILED remains terminal. Use AIWS held-work reconciliation or an explicitly linked new run |
| Structured outputs with rejection of failed/skipped producers | Add immutable content digests and AIWS producer, attempt and revision pins before consuming material |
| Execution status separate from workflow outcome | Display execution, validation and human acceptance distinctly |
| Provider fallback, optional sandbox configuration and mixed AI/script/loop nodes | Verify effective capabilities and bounded exposure before execution; configuration and worktrees alone do not prove containment |

These findings support integration work; they do not establish conformance. Archon's source was inspected, but its application and provider integrations were not run in this assessment.

## Improvements to build directly into AIWS

| Order | Improvement | What it helps a user do |
|---|---|---|
| S1 | Capability checks and readiness explanations | Understand what an agent can run and what is missing before execution |
| S2 | Guided workflow authoring | Describe supported steps and dependencies with clear validation feedback |
| S3 | Clearer handoff contracts and diagnostics | Know what material each step needs and why a handoff is blocked |
| S4 | Context policy and session lineage | Preserve required instructions and evidence when work resumes or agents change |
| S5 | Native operator run view | See progress, ownership, human decisions, resource exposure and evidence together |

AIWS already has graph preflight, immutable material pins, handoffs, replacement acknowledgment and control APIs. These slices extend those foundations. [S1 readiness checks](https://www.lril.ai/engine-readiness/) are implemented; the other slices remain planned. The detailed gap analysis, sequence and acceptance gates are in `docs/AIWS-STANDALONE-IMPROVEMENTS.md`.

S1 provides a capability manifest and read-only readiness report for the supported local coding runner. A readiness report must never grant permissions or replace final dispatch checks. Later views and authoring tools can use the same explanation of what is supported.

## Optional integration, later

Connecting Archon to AIWS could help people who already use both products, but standalone AIWS does not depend on it. The previous AB-01 proposal is retained as a historical alternative. Any future integration needs a new scope decision, authenticated identity mapping and real qualification; a whole Archon workflow cannot automatically be treated as one bounded AIWS task.

## M7 acceptance review

The fresh AIWS engine typecheck and all 470 engine tests passed. The review traces authority, identity, pins, atomic activation, replacement, stale-result rejection, assessment invalidation, accounting and receipt recovery to exercised tests. Slice 8 provides prior native parity and verified HTTPS evidence against the same implementation.

M7's supported changes remain deliberately scoped: insertion and plan/dependency revisions before dispatch, whole-agent replacement at settled boundaries, and reviewed same-stage reconciliation. Conditional pruning, arbitrary in-flight graph changes and cross-machine recovery remain unsupported. The bounded D-M6-01 local developer profile is complete; full browser rendering and broader release/platform qualification remain open in M8.

The complete decision and evidence are in the repository:

- `docs/AIWS-STANDALONE-IMPROVEMENTS.md` — approved AS-01 direction and planned standalone slices.
- `spec/engine-v1/ARCHON-BOUNDARY.md` — superseded AB-01 proposal and pinned assessment sources.
- `docs/M7-ACCEPTANCE.md` — acceptance review and remaining gates.
- `docs/evidence/m7-slice9/` — pinned source manifest, test trace and regression results.

## Pinned sources

- [Workflow storage and approval/resume transactions](https://github.com/coleam00/Archon/blob/4d48a0325e64017aaaf2b3ad21c0faf9dc368520/packages/core/src/db/workflows.ts)
- [Run status, outcome and session schema](https://github.com/coleam00/Archon/blob/4d48a0325e64017aaaf2b3ad21c0faf9dc368520/packages/workflows/src/schemas/workflow-run.ts)
- [Structured output resolution](https://github.com/coleam00/Archon/blob/4d48a0325e64017aaaf2b3ad21c0faf9dc368520/packages/workflows/src/output-ref.ts)
- [DAG and sandbox configuration schema](https://github.com/coleam00/Archon/blob/4d48a0325e64017aaaf2b3ad21c0faf9dc368520/packages/workflows/src/schemas/dag-node.ts)
- [Workflow executor](https://github.com/coleam00/Archon/blob/4d48a0325e64017aaaf2b3ad21c0faf9dc368520/packages/workflows/src/dag-executor.ts)
- [Provider/model resolution](https://github.com/coleam00/Archon/blob/4d48a0325e64017aaaf2b3ad21c0faf9dc368520/packages/workflows/src/model-validation.ts)


---

# Versioning and safe upgrades

SDK 0.3.0 targets proposal edition 0.4. The SDK version and standard edition are different identifiers. The finite-v1 schema and journal event envelopes are version-bound; a package upgrade is not automatic migration of existing data.

## Existing journals

Edition 0.4 adds observation metadata and changes event hashes. Preserve edition 0.3 journals with the SDK version that produced them for historical playback. Do not relabel a contract or recompute historical events to make the new parser accept them. That would replace evidence rather than prove compatible migration.

Before changing execution ownership or format, reconcile in-flight effects and record the source's final disposition. Create an explicitly linked new mission only through a reviewed migration procedure, carrying forward authorized resource exposure, artifact lineage and relevant decisions. An audit import in this release produces a snapshot; it is not an import-into-store or live migration command.

## Application code and definitions

Keep the workflow definition revision, adapter implementation version and material artifact revisions in your application records. Updating a code package can change behavior even when the graph JSON is unchanged. A months-long workflow needs a strategy for compatibility and dependency availability; the current SDK does not bundle a worker-version router or historical runtime images.

`activatePlan` appends plan identity and checks expectedPlan. It does not migrate graph topology or modify mission limits. Registration of a new trigger ID changes its deduplication namespace; preserve the relationship to past occurrences and prevent repeated external work during transition.

## Release procedure

Run all language examples and the shared corpus, test reopening representative stored missions, review changed error semantics and verify package entrypoints. Test the actual adapter's result parsing and external deduplication contract. Back up databases with a consistent SQLite procedure and retain artifact bytes for the required period.

The documentation source contains a verification script so future changes can detect copied examples that no longer match the package. Update text exports, API tables and executable sources together, then build the site. Never leave a documentation tab showing a new API while the downloadable package still has the old one.


## Adding nested limits from current source

Enable `aiws-limits/1` explicitly using a new, human-approved scope tree and shared ledger. Do not reopen a mission/handoff database as a limit ledger or import running work with zero consumption. Automatic import and topology changes are unsupported. Every affected external call must pass both ledger and existing dispatch checks. The host reconciles crashes between databases conservatively; restarting never proves a dispatched effect did not occur. The old 0.3.0 binary packages do not contain this source addition.


---

# Tests and coverage

Install Python with `python -m pip install ./packages/python`, then run `python -m unittest discover -s packages/python/tests -v`, `npm test`, `cargo test --locked` and `npm run test:parity`. The shared corpus covers authority attenuation, revocation, approvals, budgets, uncertain effects, recovery epochs, waits, trigger identity/limits, scheduling, graph structure, all 13 node kinds, and material schemas.

The parity runner compares error codes and a canonical state digest after every accepted or rejected command. All three language suites also replay the journal and check independently specified expectations. Persistence tests inject transaction failures, reopen databases, conflict on expected revisions and detect changed journal entries. Coordinator tests exercise a lost external response and recovery without redispatch.

See the source archive's `TEST-REPORT.md` for the release results and `COVERAGE.md` for requirement mapping. Passing this suite establishes the tested implementation profile; it does not certify authenticated source integrations, external network enforcement, evidence truth, or every deployment requirement in the formal proposal.

Release 0.3.0: 70 TypeScript tests, 62 Python tests, six Rust test functions, and 57 shared scenarios with three-language state parity. Local HTTP tests in every language check transient failure, restart and partial success. See [observability](https://www.lril.ai/observability/) for the precise delivery and alert boundaries.


---

# Deploy the documentation and host SDK applications

The Astro site and an application using the SDKs are separate deployable artifacts. This repository builds static documentation. It does not deploy a workflow scheduler, agent fleet, database service or approval inbox.

## Build the Astro site

From the source workspace root, use the Node version declared in package.json:

```bash
npm ci
npm run build
```

The build regenerates plain-text documentation, the downloadable handbook and the source ZIP, then renders Astro/Starlight into dist/. Serve the contents of dist/ from your static host. Keep URL handling compatible with directory routes such as /handoffs/ and /api-reference/. No server-side secret or SDK database is needed by the documentation site.

For local writing, npm run dev starts Astro's development server. All concept examples use native Starlight Tabs and TabItem components. The selected language is synchronized across examples and retained on return visits by Starlight's syncKey mechanism. The downloadable handbook includes all languages sequentially so agents do not need to manipulate tabs.

## Install an SDK in your application

Use the individual package links on the home page, or the local source paths documented in each language guide. Package versions are pinned in the download names. Do not assume those names are public registry releases.

**Commit, reopen and verify an audit bundle** — Executable example

### Typescript example: storage

```typescript
import assert from 'node:assert/strict';
import {mkdtempSync,readFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {parseContract,canonical} from '@aiws/sdk';
import {SqliteStore,importAudit} from '@aiws/sdk/sqlite';
const contract = parseContract(readFileSync('examples/guide/contract.json','utf8'));
const path = join(mkdtempSync(join(tmpdir(),'aiws-docs-')),'mission.db');
let store = new SqliteStore(path,contract);
store.apply({type:'startRun',runId:'r'},'10','0'); // trusted local simulation
store.close();
store = new SqliteStore(path);
try {
  assert.equal(store.snapshot().revision,'1');
  assert.equal(canonical(importAudit(store.exportAudit())),canonical(store.snapshot()));
  console.log(path);
} finally { store.close(); }

```

### Rust example: storage

```rust
use aiws_sdk::*;
use aiws_sdk::sqlite::{SqliteStore,import_audit};
use serde_json::json;
fn main() -> std::result::Result<(),Box<dyn std::error::Error>> {
    let contract = Contract::parse(&std::fs::read_to_string("examples/guide/contract.json")?)?;
    let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_nanos();
    let path = std::env::temp_dir().join(format!("aiws-docs-{nonce}.db"));
    let name = path.to_str().ok_or("non-UTF8 path")?;
    let mut store = SqliteStore::open(name,Some(&contract))?;
    store.apply(&Command::from_value(json!({"type":"startRun","runId":"r"}))?,"10",Some("0"))?;
    drop(store);
    let store = SqliteStore::open(name,None)?;
    assert_eq!(store.snapshot()?,import_audit(&store.export_audit()?)?);
    println!("{}",name);
    Ok(())
}

```

### Python example: storage

```python
from pathlib import Path
from tempfile import mkdtemp
from aiws import parse_contract
from aiws.sqlite import SqliteStore, import_audit
contract = parse_contract(Path('examples/guide/contract.json').read_text())
path = str(Path(mkdtemp(prefix='aiws-docs-'))/'mission.db')
with SqliteStore(path,contract) as store:
    store.apply({'type':'startRun','runId':'r'},'10','0') # trusted local simulation
with SqliteStore(path) as store:
    assert store.snapshot()['revision'] == '1'
    assert import_audit(store.export_audit()) == store.snapshot()
print(path)

```

This example creates and reopens a mission database in a temporary directory to verify persistence. Real applications should choose an operator-configured persistent directory and an explicit retention/backup policy. Do not put a production mission database in an ephemeral temporary directory merely because the tutorial does so for cleanup.

## Desktop and Docker hosts

A desktop host must run its coordinator and scheduler process explicitly and store state on durable local storage. In Docker, mount the database and artifact directories on persistent volumes; the writable container layer is not a recovery plan. Preserve the SQLite database and its consistency requirements during backup. Do not share a live SQLite file through an arbitrary network filesystem or copy only its main file while ignoring an active WAL.

Process supervisors may restart a host after a crash, but the proposed workflow resume policy must still determine whether work may continue. Restarting a container is not the same event as authorizing an external action. Check state, policy validity, reservations and unresolved effects first.

## Organization-hosted services

Separate user authentication, command admission, worker capability, artifact access and telemetry configuration. Define the ownership/concurrency policy before allowing multiple processes to operate on the same mission. Current SQLite serialization is useful for state commits but does not by itself fence a stale external worker.

Do not claim a throughput or months-long retention guarantee from these examples. The current store replays retained history; large journals, long pauses, growing outboxes and the audit parser's 1 MiB input bound require explicit capacity and lifecycle planning. Cross-machine recovery is outside the currently agreed engine scope.

## Verify a release

Run the documented example suite and SDK checks before shipping your host. Validate local documentation links and rendered tabs with npm run docs:check after a build. Review authentication, effect truth and real validation evidence in your integration tests. A passing docs build demonstrates a usable reference site, not conformance of an arbitrary deployment.


---

# Capacity qualification

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

**Milestone status:** M6 is complete for the D-M6-01 supervised Windows 11 x64 / Node 24 / local SQLite developer profile. [M8 is now active](https://www.lril.ai/engine-m8-qualification/): its plan is complete; qualification and expanded release/support claims remain open.

M6 slice 9 has a reproducible **component qualification runner**, including bounded
CP-01 waiting-population, CP-02 provider-I/O, CP-03 CPU-worker, CP-04 contention/fairness,
CP-05 retained-history, CP-06 streamed-artifact, CP-07 recurring-recovery and CP-08
pressure/control adapters, plus a resumable exact-matrix campaign orchestrator. M6 is
complete for the bounded D-M6-01 Windows 11 x64 / Node 24 / local SQLite developer profile;
full release qualification remains open under M8.
A passing short run is evidence for the operations it exercised, not a supported
workflow ceiling or a long-duration reliability claim.

## Run a baseline

For the combined reference workload, use `npm run qualify:capacity:reference`: a dedicated
server process holds the waiting population and retained history on one database while
local provider requests overlap real authenticated HTTPS inspect/pause/resume/cancel.
The separate client checks the supplied certificate and hostname. It does not contact
external providers or spend AI API tokens.

Start with `--mode smoke --output <new-directory> --tls-key <test-key.pem> --tls-cert
<test-cert.pem>`. Qualification additionally requires fixed reference/constrained VM
allocation evidence, 120-second warmup, 600-second measurement and 1,000 samples per
required action. Threshold misses return a failing exit code. See the repository's
`docs/CAPACITY-REFERENCE-RUNBOOK.md` for Windows commands and VM evidence format.
The smoke fixture is not capacity qualification; simulated-provider dispatch is not
governed production admission and server RSS includes its SQLite and evidence I/O threads.

All three combined reference-VM reports from 2026-09-17 (seeds 1729/3253/7919, Windows
build 26200, fixed 4 vCPUs / 8 GiB) pass all seven numerical gates and the full sampling
protocol. Each completed 2,400 control cycles without misses and reconciled 100,000 seed
events and wakeups after restart. Event-loop p99 ranged from 21.725 to 22.184 ms.
All uploaded reports were reviewed, have identical source fingerprints and host evidence,
and show non-overlapping run times. This completes the three-report reference milestone;
raw latency/resource/progress logs and VM proof have now also been reviewed. Database
replay and reconstruction of the unretained event-loop histogram remain outside that review. The simulated providers
missed 24.62–25.99% of timer offers, so these reports do not establish sustained
100-offers/s throughput. Constrained runs, broader acceptance and soaks remain open.
See `docs/evidence/m6-slice9/reference-windows-seed-1729`,
`docs/evidence/m6-slice9/reference-windows-seed-3253`,
`docs/evidence/m6-slice9/reference-windows-seed-7919` and `docs/M6-CAPACITY.md`.

D-M6-01 acceptance is complete for the supervised Windows 11 x64 / Node 24 / SQLite
developer profile. Raw-evidence handoff review is complete. The local Linux restart discrepancy
was isolated to stale WAL sidecar restoration/re-exposure after the engine process had exited,
not an engine persistence write, and the final Engine plus SDK/documentation CI reruns passed.
Broader platform, capacity and endurance qualification remains tracked for M8; no production or long-duration readiness claim is made.

Use Node 24 from the repository root after `npm ci`:

```sh
npm run qualify:capacity -- --output capacity-evidence --seconds 60 --restart 15
```

The destination must not exist. Every attempt keeps its report, raw latency samples,
resource samples and SQLite database, including on failure. Use a private scratch
volume with sufficient free space. The runner generates synthetic data; it never opens
an existing installation or contacts an external provider.

The defaults seed 1,000 work orders with ten parked tasks each, run 1,000 deterministic
indexed waiting-task inspections, and create 10,000 durable audit events through the
normal storage API. CP-01 also records logical SQLite and RSS growth, restarts storage to
time its first checked inspection, proves waiting records consume no execution slots, and
drains the exact population in bounded batches. The measurement loop offers 20 cycles/second;
each cycle commits one command, inspects its state, inspects a seeded waiting task and
reads a bounded history page. Each completed measured cycle also offers one deterministic
local-provider request and one deterministic CPU task. Post-measurement CP-04 through
CP-08 probes check persisted scheduler fairness, transactional admission accounting, hot,
snapshot and archived-prefix history behavior, bounded artifact publication/read,
retention references, interrupted-upload cleanup and verified backup inventory. Waiting tasks
must not be claimable. Clean storage-worker
restarts verify state and idempotent command replay. CP-07 restarts a separate schedule
database, recovers every fixed-UTC catch-up policy in bounded pages, and proves real
`UNKNOWN` dispatch exposure remains owned and reserved across another restart. Final paged verification checks
every event ID in order; a wakeup burst must fire every parked task exactly once in
batches of at most 100.

CP-08 uses the authenticated `ControlService` path during delayed provider and SQLite
write pressure. It requires durable pause/resume/cancel results, explicit failure without a
false state transition when a storage deadline is exceeded, low-disk admission blocking,
live-reference retention, and a persisted high-to-low worker-capacity drain with no killed
authorized effects or oversubscription.

## Plan and resume the full host campaign

Planning is the default and never launches a measurement. It writes an immutable matrix
and a derived status file:

```sh
npm run qualify:capacity:matrix -- \
  --output capacity-campaign-reference \
  --mode plan \
  --host-profile reference \
  --host-evidence /private/host-evidence/reference.json \
  --host-note "4 cores; 8 GiB; SSD details; fixed power policy; applied limits"
```

The qualification matrix has 36 ascending cells and 108 logical runs: three fixed seeds,
120 seconds of warmup, 600 seconds of measurement, and at least 1,000 controls per cell.
After reviewing the plan and provisioning the default 16 GiB per-attempt evidence budget,
execute it:

```sh
npm run qualify:capacity:matrix -- \
  --output capacity-campaign-reference \
  --mode execute \
  --host-profile reference \
  --host-evidence /private/host-evidence/reference.json \
  --host-note "4 cores; 8 GiB; SSD details; fixed power policy; applied limits" \
  --acknowledge-long-run YES
```

Run the same command to resume. Passed attempts are skipped. Failures remain immutable and
stop all remaining workloads in that campaign pending diagnosis; after inspection, `--retry-failed true`
creates the next numbered attempt. Use different roots for reference and constrained
hosts. PostgreSQL plans are supported, but execution is deliberately blocked until the
candidate adapter exists. A completed host campaign still reports
`PASS_HOST_CAMPAIGN_NOT_CERTIFIED`.

Qualification execution now requires `--host-evidence` using the fixed-VM format in
`docs/CAPACITY-REFERENCE-RUNBOOK.md`. Preflight checks exact guest CPU allocation, fixed
RAM (within 5%), no lower process memory cap, disabled dynamic memory and an SSD/power
policy declaration. The nonempty VM configuration proof (at most 1 MiB) is hashed into
the immutable plan. Missing or mismatched allocation exits 2 before any measured attempt.
Plan mode remains available on an oversized host, but such plans are local previews: generate
the executable plan inside the actual guest with its proof. Source, runtime, allocation or
proof changes require a new campaign directory. Smoke mode remains explicitly unqualified.
These checks verify guest allocation plus operator evidence, not dedicated physical cores
or absence of competing host load. Retain the original configuration proof for review.
Then validate and compact the campaign without copying its multi-gigabyte raw artifacts:

```sh
npm run summarize:capacity:campaign -- \
  --input capacity-campaign-reference \
  --output capacity-campaign-reference-summary
```

The summarizer checks the plan digest, report count and hashes, common source/environment
provenance, target-specific protocol flags and declared-versus-observed host capacity.
Its output remains explicitly non-certified.

## Recorded full campaign

The first complete Windows campaign passed all 108 attempts across 36 cells and three
fixed seeds. Every CP-01 through CP-08 protocol flag and component safety assertion
passed, including the 1,000,000-event, 1-GiB artifact, 100,000-schedule/128-`UNKNOWN`,
64-contender/depth-16 and 1,000-control cells.

It is retained as **unconstrained-host component evidence**, not as the reference cell.
The plan described 4 cores / 8 GiB, while the reports observed 24 logical CPUs and
31.07 GiB with no resource-enforcement proof. Separate component proxies were below the
provisional thresholds, but the required combined 10,000-workflow + provider-ceiling-32 +
100,000-event cell was not executed. The constrained/reference target hosts, HTTP/TLS
transport, split coordinator/worker RSS, allocated disk, PostgreSQL comparison and real
soaks remain open. See the repository's compact
`docs/evidence/m6-slice9/windows-unconstrained-campaign-01` record.

## Choose a measurement budget

| Option | Default | Meaning |
|---|---|---|
| `--workflows` | 1000 | Parked work orders, ten tasks each; up to 100000 |
| `--waiting-inspections` | 1000 | CP-01 deterministic indexed waiting-task samples; 1–10000 |
| `--target-workload` | ALL | Isolate `CP-01` through `CP-08`; campaign runs set this automatically |
| `--events` | 10000 | Initial retained events; up to 1000000 |
| `--warmup` | 0 | Warmup seconds, excluded from measured cycles |
| `--seconds` | 10 | Measurement seconds; up to 604800 (seven days) |
| `--rate` | 20 | Offered cycles/second, up to 1000 |
| `--restart` | 60 | Seconds between clean storage-worker restarts |
| `--max-mib` | 1024 | Stop when sampled evidence-directory bytes exceed this budget |
| `--max-rss-mib` | 1024 | Stop when sampled whole-process RSS exceeds this budget |
| `--seed` | 1 | Published deterministic waiting-task selection seed |
| `--host-note` | not recorded | Record disk medium, power policy and host resource constraints |
| `--provider-concurrency` | 8 | CP-02 approved active ceiling; 1–128 |
| `--provider-fast-ms` | 100 | Seeded provider's normal service time |
| `--provider-slow-ms` | 1000 | Seeded provider's slow service time |
| `--provider-slow-every` | 10 | Deterministically make every Nth provider request slow |
| `--cpu-concurrency` | min(2, logical CPUs) | CP-03 worker-thread ceiling; 1–8 |
| `--cpu-iterations` | 20000 | Fixed SHA-256 operations per CPU task |
| `--effect-queue` | 1000 | Maximum queued requests per effect adapter |
| `--contention` | 16 | CP-04 continuously eligible contenders; 2–64 |
| `--contention-depth` | 4 | Ancestor depth per contender; 1–16 |
| `--contention-rounds` | 2 | Complete fairness rounds; 2–100 |
| `--contention-slots` | min(8, contenders) | Shared-root cost/active admission limit |
| `--contention-connections` | min(8, contenders) | Concurrent SQLite authorization connections |
| `--history-events` | 100 | CP-05 retained events; required ladder is 10000/100000/1000000 |
| `--history-page-events` | 100 | Maximum events per verification/export page |
| `--history-seed-batch` | 1000 | Events per legal fixture-seeding command |
| `--history-segment-events` | 1000 | Maximum events per archived segment |
| `--history-archive-percent` | 80 | Percentage packed in the archived-prefix variant |
| `--history-payload-bytes` | 64 | Deterministic payload padding per retained event |
| `--artifact-bytes` | 1048576 | CP-06 bytes per immutable object; required ladder is 1 MiB/100 MiB/1 GiB |
| `--artifact-chunk-bytes` | 65536 | Maximum publication/read chunk; 1 byte–4 MiB |
| `--artifact-concurrency` | 4 | Artifact stream concurrency; 4 runs both the 1 and 4 cells |
| `--schedules` | 1000 | CP-07 overdue schedules; required ladder is 1000/10000/100000 |
| `--schedule-backlog-ticks` | 10 | Due ticks presented to each catch-up policy after restart |
| `--schedule-interval-ms` | 1000 | Fixed-UTC interval used by the deterministic fixture |
| `--schedule-batch` | 100 | Maximum overdue schedules recovered per storage call |
| `--schedule-occurrence-batch` | 100 | Maximum occurrences emitted per schedule recovery |
| `--schedule-replay-limit` | 32 | Human-approved `BOUNDED_REPLAY` limit |
| `--unresolved-attempts` | 32 | Real `UNKNOWN` attempts retained across restart; 0–128 |
| `--control-samples` | 1000 | CP-08 authenticated control mutations; 4–10000 |
| `--pressure-ceiling` | 8 | Initial approved active worker ceiling; 2–128 |
| `--pressure-reduced-ceiling` | 4 | Reduced ceiling after drain; must be below the initial ceiling |
| `--pressure-queued` | 8 | Ready tasks retained behind the filled ceiling |
| `--pressure-provider-concurrency` | 8 | Deterministic delayed-provider operations overlapping controls |
| `--pressure-provider-delay-ms` | 250 | Delay per CP-08 provider-pressure operation |
| `--pressure-storage-delay-ms` | 50 | Independent SQLite write-lock duration; 20–1000 ms |

These are benchmark input bounds, not product limits. Resource budgets are sampled,
not OS-enforced quotas; provisioning must allow headroom between samples. Seeding and
final reconciliation take additional time. SIGINT/SIGTERM request a stop at the next
budget check. A killed process can leave `RUNNING` evidence, which never counts as a pass.

For the specification's minimum component measurement duration, use `--warmup 120
--seconds 600` and repeat in three different output directories with published seeds.
At least 1,000 completed samples are required per measured cell. Start with the smallest
scale and stop on resource exhaustion or a failed invariant. The runner can also collect
24-hour (`--seconds 86400`) and seven-day component soaks. Those do **not** substitute
for the required full-workflow soaks with production adapters, controls and accounting.

## Interpret the evidence

`report.json` contains source hashes, Git baseline, Node/SQLite versions, CPU, OS,
filesystem type, workload inputs, counts and explicit unmeasured scope. `PASS` means
the exercised component assertions passed. `qualification: NOT_QUALIFIED` deliberately
remains separate. Protocol flags describe duration, warmup and sample sufficiency;
they cannot close the full acceptance gate.

`latencies.ndjson` preserves every timed operation, including warmup and seed stages.
Summary percentiles are **histogram bucket upper bounds**, in milliseconds; an overflow
bucket is explicitly named. `resources.ndjson` records whole-process RSS (including the
storage worker and CPU worker threads), CPU/resource counters, pending storage calls,
effect queue/active snapshots and evidence bytes. Worker-thread RSS is included in the
whole process rather than reported separately. Disk bytes are logical file lengths,
including raw evidence and WAL, not allocated physical blocks or a retention estimate.

The `waiting` section records the exact workflow/node population, indexed sample count,
logical SQLite growth and bytes per workflow, RSS delta, restart-to-first-inspection,
empty scheduler/dispatch execution exposure, and exact wake reconciliation. Its required
ladder cells are 1,000, 10,000, and 100,000 workflows with exactly ten nodes each.

Offered, completed and missed storage cycles are reported separately. There is one
outstanding storage cycle at a time, with missed offers counted rather than an unbounded
catch-up queue. Provider and CPU effects have separate bounded queues and report offered,
admitted, completed, backpressured and failed counts; dispatch and service timings are
separate. The `contention` section records persisted fairness across a mid-cycle storage
restart, shared-root capacity rejections, independent-root progress, exact reservations
and settlement. CP-04 uses the real scheduler and admission paths, but remains a component
probe rather than an authenticated control benchmark.
The `history` section records the verified seed snapshot and its hashes, snapshot-copy
costs, hot/snapshot/archived-prefix counts, normal command and current-state inspection
latency, bounded first-page cost, resumable verification, exact streaming replay, logical
database bytes and reopen-to-inspection. Seed snapshots are built through legal engine
commands and fully verified before reuse.
The `artifacts` section records immutable object digests, publication and bounded-read
timings, throughput, maximum chunks, RSS delta, injected interruption cleanup, retention
reference protection, and the exact verified backup inventory. Artifact publication,
backup hashing/copying and inspection use bounded streams. Logical bytes are reported;
allocated filesystem blocks and retained-capacity forecasts still require target-host
measurement.
The `recurring` section records schedule creation and bounded recovery timings, restart
to first inspection/eligibility, policy-specific emissions, skipped/coalesced ticks,
cursor advancement and occurrence uniqueness. Its unresolved-attempt probe records exact
before/after dispatch snapshots, reserved cost/active time and active ownership. A ready
probe task must remain blocked when `UNKNOWN` exposure fills the configured ceiling; the
zero-exposure cell must admit it.
The `pressure` section records authenticated control and inspection latency, provider-delay
overlap, storage commit failure versus durable success, low-disk admission, reference-safe
retention, and the capacity drain/refill sequence. The normal control path is measured at
the service boundary; HTTP/TLS transport latency remains a target-host qualification item.
Storage inspection latency must
not be presented as authenticated API latency; clean worker reopening is not a process
crash, whole-coordinator restart or power-loss test.

Full acceptance still requires execution of the planned CP-01–CP-08 workload matrix on
reference and constrained hardware, full provider/CPU and CP-04
2/16/64-contender by 1/4/16-depth ladders, the full
CP-05 10000/100000/1000000 history ladder, CP-06 1 MiB/100 MiB/1 GiB artifact ladder,
the CP-07 1000/10000/100000 schedule by 0/32/128 `UNKNOWN` cross-product, CP-08 target-host
transport/disk-pressure cells, repeated
measurements and real-time soaks.
The provisional 250 ms inspection, 500 ms durable-control, 100 ms event-loop and 1 GiB
RSS gates remain unchanged. See the repository's `spec/engine-v1/CAPACITY.md` and
`docs/M6-CAPACITY.md` for the acceptance checklist and recorded baseline.

Continue with [deployment qualification](https://www.lril.ai/engine-deployment-platforms/),
[bounded replay](https://www.lril.ai/engine-replay-limits/) and [failure injection](https://www.lril.ai/engine-failure-injection/).

## M8 slice 2 execution

Slice 2 is in progress. The owner selected new Ubuntu 24.04 fixed Hyper-V guests on the
LIVING-ROOM host because the former Windows reference VM is unavailable. Their results
will be separate from Windows evidence. Each complete ladder has 36 cells × three seeds:
21.6 hours of prescribed warmup/measurement per host before seeding/reconciliation. Run
profiles sequentially on this shared physical host. No M8 capacity pass is claimed yet.


### Storage worker failure handling

The executor remembers a worker failure and rejects later requests instead of
queuing them to an exited worker. Unexpected exits, including exit code zero,
reject pending requests. Closing a failed executor does not wait for that worker.
An affected operation's commit outcome still requires reconciliation before retry.
This does not impose a deadline on a live worker or prove recovery from every stall.

The Ubuntu campaign remains pinned to its original candidate. Its CP-04 incident
and reviewed retry are recorded in `docs/M8-SLICE2-EXECUTION.md` in the repository.
The failure-handling fix is tested separately and does not inherit capacity
qualification from that campaign. D-M6-01 remains the bounded Windows developer
profile.

The campaign is stopped after a second contention stall, with 56 reference runs
completed. Separate diagnostics reproduced a telemetry connection timing out
during worker startup, followed by requests waiting on the dead worker. Telemetry
now uses the executor's configured SQLite busy timeout, alongside the terminal
worker-failure handling described above. This fixes a timeout mismatch; it does
not make lock contention impossible or retry an uncertain operation. The original
capacity evidence remains tied to its original candidate and cannot qualify this
changed engine. Host monitoring now runs through a Windows scheduled task with
lifecycle logging; gaps in the earlier host observations remain recorded.


---

# M8 release qualification plan

**M8 is the active milestone.** Slice 1 establishes the executable plan; slices 2–4 are in progress and slices 2–9
and all seven qualification items remain open. M6 remains complete only for the
**supervised Windows 11 x64 / Node 24 / local SQLite developer profile** accepted by
D-M6-01. M7's scoped local acceptance is complete. No new platform, production,
PostgreSQL or long-duration support is established by publishing this plan, and the
released SDK 0.3.0 binaries retain their existing scope.

The repository's [M8 build/qualification plan](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M8-BUILD-PLAN.md)
is the execution contract. It includes evidence fields, operator/reviewer responsibilities,
commands and per-slice stopping conditions. The
[backlog](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M8-QUALIFICATION-BACKLOG.md)
maps Q01–Q07 to it; [BUILD-PLAN](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/BUILD-PLAN.md)
tracks completion. These repository links require access to the private project.

## Ordered slices and claims

| Slice | Work and dependencies | What a passing result enables |
| --- | --- | --- |
| 1 — plan | Complete as planning; follows bounded M6 and scoped M7 | Execution can start; no support expansion |
| 2 — exact-host capacity (Q01) | After 1; reference 4-vCPU/8-GiB and constrained 2-vCPU/4-GiB CP-01–CP-08 ladders | Measured component envelope and overload behavior for exact hosts/configurations |
| 3 — native delivery candidates (Q05) | After 1; native hosts and signing access | Candidate install/upgrade evidence; platform support waits for 4/7/9 |
| 4 — real host boundaries (Q06) | After 1; concrete identity, secret, ACL, private IPC and containment adapters | Tested security contract for named adapters; unattended support waits for 5–7/9 |
| 5 — governed capacity/sizing (Q02) | After 2/3/4 for the same profile | Production-path measurements and allocated-storage retention estimates, not a general throughput promise |
| 6 — endurance (Q04) | After 5; frozen candidate at sustainable load | Tested 24-hour and seven-day real-time envelope after both pass, not months-long reliability |
| 7 — machine/storage recovery (Q07) | After 3/4 and representative load from 2 or 5 | Enumerated sleep/restart/interruption/restore guarantees for tested storage |
| 8 — PostgreSQL (Q03) | After 5's SQLite baseline and 4's host contract | Candidate conformance and identical-input comparison; support also needs backend-specific 3–7/9 |
| 9 — integrated release | After every gate applicable to the selected release profile | Exact version/platform/backend/adapter support statement backed by reviewed evidence |

Slices 2/3/4 can proceed in parallel. Slice 7 can overlap 5/6; PostgreSQL implementation
can overlap 6/7. Use separate measured hosts/allocations, installations, disks and evidence
destinations. Native delivery preparation does not wait for real host adapters; final
acceptance combines both, avoiding a circular dependency or fixture-based production claim.

The native target matrix remains Windows 11 25H2 x64/arm64, macOS 15 Intel/Apple Silicon,
Ubuntu 24.04 and Debian 13 x64/arm64, and Debian 13 Docker linux/amd64 and linux/arm64.
Test minimum and newest claimed OS versions separately, with current servicing verified
at execution. Container/emulated results do not qualify a native cell. A limited release
can list passing cells only; full M8 remains open until all Q-items and initial matrix
requirements pass. PostgreSQL can be excluded from a SQLite release without pretending
that Q03 is complete.

## Evidence and pass/fail gates

Every attempt retains source/runtime/package/adapter hashes, exact host and storage proof,
configuration/seeds, commands, budgets, real timing, raw latency/resource/fault logs,
independent provider/receipt oracles, pre/post accounting and state reconciliation, and a
named review decision. Keep aborted/failed attempts and immutable evidence inventories.
No credentials or live databases belong in the public documentation or source download.

Capacity qualification uses three seeds, two-minute warmup, at least ten-minute measurement
and 1,000 completed controls per cell. For the specified combined reference cell, inspect
p95 must be at most 250 ms, pause/cancel p95 each 500 ms, available-capacity dispatch p95
250 ms, event-loop p99 100 ms, coordinator RSS 1 GiB and first authorized inspection after
restart 30 seconds. Reference latency targets are diagnostics on constrained hosts; safe
bounded backpressure remains mandatory. Fairness, ceilings, accounting and UNKNOWN ownership
must hold at every load. [Capacity qualification](https://www.lril.ai/engine-capacity/) explains existing tools
and their measurement boundaries. Whole-server RSS and simulated providers do not satisfy
complete production resource attribution or governed dispatch measurements.

Stop a cell on its declared resource/time budget, safety failure or failed evidence capture.
Higher unrun cells remain NOT_MEASURED. Stop and diagnose lost committed work, unauthorized
dispatch, oversubscription, erased UNKNOWN responsibility, secret leaks or containment
escape; shared defects block all affected claims. Missing native access, genuine adapters,
signing capability or real elapsed duration is Blocked. A smoke pass, skipped test, fixture
identity or clean process restart cannot replace its missing qualification gate.

Repeat only after a recorded diagnosis/fix or environment change. Keep targets and failures
visible; approved target revisions apply to future attempts. Documentation-only updates do
not require rerunning unchanged benchmarks. Runtime/schema/adapter changes need an impact
review and reruns of invalidated evidence. One passing 24-hour soak leaves the seven-day
gate open; accelerated clocks do not supply endurance or power-loss evidence.

## Release decision

Slice 9 combines native acceptance, real enrollment and host boundaries, recovery,
capacity/endurance, all applicable conformance cases, native SDK and shared parity tests,
engine regressions, browser controls, package installs and supported migration/restore
paths. Final candidate CI, examples, standard/profile, coverage, compatibility, runbooks,
website/offline exports and source downloads must agree with the reviewed manifest.

The claim names the exact version, profile, OS/architecture, backend, adapters and tested
workload/recovery/endurance envelope. Production or unattended wording needs all applicable
gates, not just working authentication. Public publication still requires explicit owner
authorization, separate from technical readiness and private repository updates.

Slice 2 now uses owner-selected Ubuntu 24.04 fixed Hyper-V guests on LIVING-ROOM,
with separate 4-vCPU/8-GiB and 2-vCPU/4-GiB allocations. Provisioning is complete;
both guests passed all eight component smoke checks and combined HTTPS smoke checks.
The reference ladder is stopped after a recurring CP-04 stall, with 56 runs
completed. Separate diagnostics reproduced a telemetry startup lock timeout and
subsequent requests to dead workers. Timeout propagation and worker failure
handling are validated separately; the campaign retains its original candidate.
Monitoring continues diagnosis only until a reviewed decision on resumption.
The [execution record](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M8-SLICE2-EXECUTION.md)
tracks evidence and stopping conditions; no Ubuntu capacity pass is claimed yet. The existing D-M6-01 acceptance
and measurement-only reference reports remain unchanged. See
[deployment qualification](https://www.lril.ai/engine-deployment-platforms/) for current candidate tooling.

## Slice 3 delivery progress

The [delivery preparation record](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M8-SLICE3-DELIVERY.md)
inventories all ten cells, current signing inputs, native-host blockers and
install/upgrade evidence requirements. The available Windows x64 host is shared
with capacity qualification, so heavy local delivery testing is deferred. The
capacity guests, checkout and evidence remain reserved for slice 2.

Windows x64 candidate `0.0.0-m8.2.1` now has a signed full-package catalog and passed
all nine hosted package acceptance checks in [run 36791069855](https://github.com/seanrobertwright/AI-WS-SDK/actions/runs/36791069855).
This is hosted Windows Server evidence, not native Windows 11 acceptance. Native installation, macOS
notarization, supported binary/schema upgrade edges, migration interruption and
state-preserving uninstall still require implementation or acceptance evidence.
All native target cells remain unverified and D-M6-01 is unchanged.

Slice 3 now includes pinned package provenance, Docker runtime-schema parity and
[offline candidate install/replacement/deregistration tooling](https://www.lril.ai/engine-deployment-platforms/#pinned-m8-candidates-and-offline-delivery).
Replacement stays held for native acceptance; no schema migration edge or production
service updater is supplied. Existing platform and D-M6-01 claims remain unchanged.

Next is native acceptance of the selected signed candidate on an independent Windows
11 host or after slice 2 releases the local computer. Executable schema upgrades and
other platform cells remain open. Slice 4 real identity/secret/ACL/IPC/containment
adapter work can proceed separately while native access is unavailable. See the
[retained candidate review](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/evidence/m8/slice-3/windows-hosted-x64/20260930-run-36791069855/review.json).


## Slice 4 host boundary progress

Slice 4 has started with a Windows DPAPI CurrentUser secret adapter and an optional
in-memory TLS credential provider. Provider errors stop control-host startup before
opening the engine database; they never select a plaintext fallback. Hosted Windows
component tests exercise real DPAPI with disposable fixture values. They do not
qualify a service account, native Windows 11, or a complete production host.

The [implementation record](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M8-SLICE4-HOST-BOUNDARIES.md)
records selected mechanisms and infrastructure gaps. Owner enrollment/fresh human
verification, service-profile availability, owner-only ACLs, authenticated worker IPC
and restricted workloads remain open. Q06 and D-M6-01 are unchanged. The signed
slice 3 candidate remains evidence for its original source, not these new adapters.
See [TLS provider integration](https://www.lril.ai/engine-deployment-platforms/#windows-protected-tls-provider).


### Cross-platform local authentication direction

The selected enrollment direction is local OS-backed human verification on Windows,
macOS and Linux. Organization OIDC remains an optional contract path; Entra setup is
not required for this implementation direction. Concrete local authenticators and
recovery mechanisms still need selection and native tests. An existing desktop login
or a service credential cannot substitute for fresh verified human approval.

Windows DPAPI is the first secret adapter, not a restriction of the engine or SDKs
to Windows. Shared engine contracts cover enrollment, authorization, sessions and
worker access policy; platform adapters enforce secrets, permissions, private IPC
and containment. The full Windows/macOS/Linux/Docker matrix remains M8 scope, with
qualification per profile. Docker needs an explicitly bound human-verification
integration and cannot assume a desktop authenticator inside the container.

Native Windows acceptance waits until slice 2 releases the selected computer.
Other native acceptance hosts remain unallocated. The implementation record includes
a draft policy limiting workers to task workspaces, approved tools and explicitly
granted network/credential access. That policy is not yet an enforced capability.
D-M6-01 remains the existing Windows developer acceptance boundary.


### Slice 4 subslices

4A (complete as a contract increment) defines the [portable access-policy contract](https://www.lril.ai/engine-access-policy/),
SDK parity and example. 4B is in progress with concrete resource/coding-plan bindings,
human revocation and worker delivery fences. Policy-bound execution stays held pending
live authority and native enforcement; 4C supplies OS adapters
(the initial Windows DPAPI component is here); 4D integrates and qualifies each
profile. Neither a valid policy document nor the DPAPI component closes Q06.

4C now includes internal preparation of installation-, owner-, service- and action-bound
human-verification challenges. Their validity grants no identity or execution authority.
The [adapter preparation record](https://github.com/seanrobertwright/AI-WS-SDK/blob/m8-slice-4-host-boundaries/docs/M8-SLICE4C-ADAPTER-PREPARATION.md)
describes the candidate Windows, macOS, Ubuntu, Debian and Docker integrations and tests.
Durable one-time enrollment storage is implemented; genuine native verification and protected
registration remain open. Service account,
ACL, private IPC and containment acceptance follows; local Windows acceptance waits until
slice 2 releases the host. Production startup and policy-bound dispatch remain blocked.

The internal enrollment store atomically consumes the setup capability, exact challenge
and verified-identity handle with the owner grant and audit record. It checks current
revision, trusted time, boot/auth/coordinator epochs and identity/service bindings.
Competing commits produce one owner; replay rejects. Restart invalidates pending
credentials while preserving established ownership. Recovery lock cannot be used to
reset an owner. Capabilities and handles are stored as hashes and returned once.

These are trusted storage operations, not a public login/enrollment API. A genuine OS
adapter must supply verified identity; tests use fixture facts only. No human session or
execution authority is issued. Protect enrollment records as installation data, and do
not downgrade their optional schema to a candidate that does not understand it.

The Windows Hello source component now creates or opens an OS-held signing key and
requests a fresh Hello signature. Unsupported package/session contexts reject before
prompting. The companion's protected registration, service channel and native acceptance
remain open. A strict signature checker binds proofs to the approved public key and exact
live challenge, but explicitly does not claim verified human identity from a signature
alone. Hosted compilation/negative checks and software-key tests cannot substitute for
real interactive verification. Other platform adapters remain in M8 scope.


The Windows candidate now has an immutable public-key registration journal. Enrollment
binds its review to the approved registration and owner policy, checks the signed challenge
against that key plus trusted internal companion observations, and stores the proof receipt
and one-use identity handle atomically. Observations expire within 30 seconds; commit
rechecks expiry and key/channel revocation. Key revocation locks recovery without removing
an established owner or work. The generic verification path cannot bypass registered checks.

This storage boundary does not authenticate a native peer by itself. Independent key
approval, protected bootstrap/storage, companion packaging, authenticated IPC and automatic
channel-disconnect observation still need implementation and native acceptance. Test keys
and peer facts are fixtures only. No public login route, session provider or policy execution
is enabled. The build plan keeps 4C in progress and retains macOS, Ubuntu, Debian and Docker
as M8 targets; D-M6-01 remains unchanged.


Unsigned Windows companion packaging and a read-only owner setup review are now available
as preparation components. The packaging recipe validates x64/arm64 payloads with MakeAppx,
checks extracted payload hashes and uses the existing AIWS favicon for package assets.
Hosted native CI retains unsigned candidates and reports; it does not install or trust them.
The review shows installation/key fingerprints, pinned identities, proposed permissions and
recovery instructions, rejecting policy text that does not match the registered digest.
It escapes display text and has no approval controls, credentials or execution authority.

The initial preparation boundary described here is superseded by the opt-in integration below. Protected registration
bootstrap, signed publisher/package approval, authenticated service IPC and native acceptance
remain open. Full-trust desktop packaging is not worker containment. No local provisioning
or interactive testing is authorized on the capacity host until it is released; other OS
adapters and D-M6-01 boundaries remain unchanged.


The Windows companion now has a source-level transport foundation using local named pipes
with explicit access rules, two-way checks of independently pinned processes/SIDs/sessions,
identification-only client token checks and bounded messages/deadlines. Invalid or dropped
connections close permanently. Hosted tests exercise real pipes using same-account fixtures;
this does not establish service-account isolation or genuine native acceptance.

The initial transport component has since been connected to the helper and enrollment journal as described below. Trusted
broker launch, package/signer verification, protected setup/review, disconnect revocation
and per-platform acceptance remain open. No human identity or policy execution authority
is granted by a successful pipe connection. Capacity resources and D-M6-01 remain unchanged.


The Windows source now checks a pinned MSIX archive's signature, signer certificate,
manifest identity and helper bytes before permitting a suspended owner-side launch.
The installed helper must match, and the created process must have the expected package,
owner, session and executable path before resume. File locks, restricted handle inheritance
and an owned job protect this candidate's launch lifecycle. It cannot elevate or switch users.

Hosted checks cover unsigned-package rejection, changed pins, manifest/payload matching,
unpackaged-process rejection and file locks. The current unsigned artifacts cannot pass
successful launch verification. Trusted signed artifacts, supported packaged activation,
a broker watchdog, protected review/IPC and enrollment integration remain open. No login
or enrollment endpoint is enabled, and no native acceptance runs on the capacity host.


Internal companion-session wiring now connects registered challenge creation, proof storage
and owner commit. Disconnect/cancellation invalidates the channel; late replies are ignored,
and cleanup cancels only the same pending enrollment. Normal completion also closes and
revokes the channel. If commit wins a disconnect race, the owner remains enrolled; cleanup
failure is reported without reopening setup. Tests use explicit software-key/native-fact
fixtures and real storage workers.

The session port has no default provider or public endpoint. A qualified native broker must
still supply package/process verification, protected review, real OS observations and owned
helper shutdown. This wiring does not convert unsigned candidates or fixture facts into
native identity, issue login sessions, or release the policy execution hold.


Reviewed setup now has an internal sequence from the exact permissions/recovery review to
companion verification and durable owner enrollment. Declining the review does not open a
helper; changing the caller's data during review cannot change the signed setup request.
Cancellation and engine restart are checked before enrollment.

On Windows, the native helper exchange now joins verified package launch to exact challenge
delivery and bounded proof collection. The helper must acknowledge that it received the
complete request before the sender closes input; this receipt is not identity evidence.
An incomplete answer, missing/incorrect receipt, unexpected diagnostic output,
failed helper, timeout or cancellation rejects; the helper is stopped on cleanup. Hosted
fixtures cover these paths without invoking Windows Hello. They do not prove that a signed
installed companion can activate successfully.

The Windows candidate now includes a native setup window and an authenticated bridge to
the engine service. A first review creates a Windows-backed owner key; the second shows
its actual fingerprint and requests verification before saving the owner. Declining,
disconnecting or receiving an invalid proof leaves execution locked. Interrupted key
creation can leave an unused key/registration requiring explicit reconciliation.

Trusted deployment code opts in through `startControlHost.ownerBootstrap`, with no work
plans. It supplies an administrator-protected configuration and exact signed delivery pins;
`prepareWindowsOwnerConfiguration` produces the configuration bytes and identity digests.
The app checks the service process, while the service checks the owner process, account,
interactive session and package. The engine uses its existing coordinator and storage
writer, exposes setup completion through `ownerSetup`, and cancels setup during shutdown.
No public setup route or login session is enabled. Enrollment grants no workflow execution.

The native workflow builds the app/helper MSIX for x64 and arm64. Manual signing retains a
private installation candidate with exact package, signer and executable hashes. Headless
CI checks do not invoke Hello or establish native qualification. Installation must still
provision a distinct service account and verify protected paths/configuration/ACLs.
Real owner verification, activation, permission denials, shutdown and containment await an
allocated native host. This computer stays reserved for slice 2 until explicitly released.
4C remains open; D-M6-01, the policy execution hold and macOS/Linux M8 requirements remain.


PR #20 merged at `94393101db631a4b1ef0e820acb039d7109e95e1`. The
[main signing run](https://github.com/seanrobertwright/AI-WS-SDK/actions/runs/36996644649)
produced signed x64/arm64 owner-setup candidates; the earlier branch-only Azure rejection
is resolved. Downloaded package/app bytes and packaged app/helper hashes matched the
retained delivery records. Signing proves delivery provenance, not native qualification.

Offline preparation is available through `node scripts/engine-owner-install.mjs`:
`inspect <delivery-dir> <approved-report-sha256>` checks the approved report and package/app
hashes; `prepare <delivery-dir> <approved-report-sha256> <reviewed-request.json> <new-staging-dir>`
also writes public `owner-setup.bin` and `installation-plan.json`. Obtain the report pin
from the independently reviewed signing record, not the untrusted download itself.
The plan binds the selected architecture, owner/service identities, package identity and
installation paths. Target paths are data only; the tool never installs or launches binaries,
changes accounts/ACLs/services, opens a database or grants workflow authority.

Preparation does not validate actual account memberships, signatures on this host, package
identity, filesystem protections or service communication. Those checks, genuine owner
verification and containment acceptance still require an allocated native host. The fresh
slice 2 campaign retains this computer. 4C/4D remain open, execution stays held, and macOS/Linux
adapters remain required. D-M6-01 is unchanged.


---

# Portable worker access policy

M8 slice 4A defines a portable access-policy contract. It validates what workflows and
nodes request; it does not authorize execution or enforce operating-system isolation.
The existing accepted developer profile and D-M6-01 remain unchanged.

## Where policy belongs

Installation policy establishes the maximum permitted scope. A workflow declares its
reviewed access scope; each node declares the subset it needs. Host adapters resolve
logical resources and enforce restrictions on Windows, macOS or Linux. Missing host
capabilities must block the future governed execution path rather than weaken it.

A separate `WorkflowAccess` document identifies the work order/run, contains a versioned
`Policy`, and lists `NodeAccess` entries. Resources use logical names, such as `workspace`
or `package-registry`. The policy pins the resource catalog by digest; paths, account
identities and secret handles belong in protected host configuration. Nodes cannot
invent additional rights or place plaintext credentials in the document.

| Kind | Actions | Example |
| --- | --- | --- |
| FILESYSTEM | READ, WRITE | Task workspace or source snapshot |
| TOOL | EXECUTE | Reviewed build tool and invocation |
| NETWORK | CONNECT | Approved package registry |
| CREDENTIAL | USE | Credential reference scoped to one attempt |
| ARTIFACT | PUBLISH | Local governed output collection |
| EXTERNAL_EFFECT | COMMIT | Explicitly authorized code push or deployment |

Empty lists mean no access. Missing fields, wildcards, duplicate entries, unknown fields,
invalid actions and node requests beyond workflow scope are rejected. No access is
implied by a tool name, an output path, an agent instruction or an existing desktop login.

## Source SDK APIs

These experimental source modules provide strict decode, validate, canonical encode and
SHA-256 digest operations. They are not included in already published SDK binaries.

| SDK | Module | Decode / encode / digest |
| --- | --- | --- |
| TypeScript | `@aiws/sdk/access-policy` | `decodeAccessPolicy`, `encodeAccessPolicy`, `accessPolicyDigest` |
| Python | `aiws.access_policy` | `decode_access_policy`, `encode_access_policy`, `access_policy_digest` |
| Rust | `aiws_sdk::access_policy` | `decode_access_policy`, `encode_access_policy`, `access_policy_digest` |

Each operation accepts a document kind: `WorkflowAccess`, `Policy`, `NodeAccess`,
`Grant` or `BindingPreview`. TypeScript/Python default to `WorkflowAccess`; Rust requires
the kind argument. Generated records accompany the codecs. Runtime validation remains
required even when a language's record type accepts a value.

A `BindingPreview` lists proposed definition, policy, catalog and attempt pins. Its
`authorized` and `enforced` fields must both be `false`. Validation checks shape and
workflow/node consistency; it cannot establish that a catalog exists, a human approved
it, a host permits it or a sandbox actually enforces it.

## First example

The [example access document](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/examples/access-policy/build-workflow.json)
models preparation, build/test and artifact collection. Preparation can request registry
access; build/test has no network grant; collection can publish local artifacts. It has
no live credentials or external publication authority. Its catalog digest is a fixture
placeholder, and it is not executable deployment configuration.

## Delivery sequence

- **4A:** portable contract, schema, SDK parity, binding rules and example.
- **4B:** shared admission/enforcement integration, resource resolution, approval and attempt binding, policy changes and revocation.
- **4C:** real platform identity, secrets, permissions, IPC and containment adapters. Existing Windows DPAPI is an initial component here.
- **4D:** integrated positive/negative qualification per platform.

Existing workflow/control schemas and runtime behavior remain unchanged in 4A. Wiring
this policy into execution requires an explicit reviewed transition in 4B; existing
workflows do not acquire permissions or isolation claims automatically. Agent context
policy remains instruction/evidence selection and cannot enforce access restrictions.

See the [contract and mapping](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/ACCESS-POLICY.md)
and [M8 qualification plan](https://www.lril.ai/engine-m8-qualification/).


## Initial engine preflight (4B)

4A contract checks passed in [hosted CI](https://github.com/seanrobertwright/AI-WS-SDK/actions/runs/36834514327).
4B is in progress. The engine can now compare the host ceiling and workflow/node
requests against a pinned catalog of binding references and an exact workflow definition.
Missing resources, changed catalogs, excessive scope and identity/node mismatches block
the report. Even COMPATIBLE reports retain `authorized:false` and `enforced:false`.
The preflight checks references, not native resource existence or OS enforcement.

The internal dispatch material can carry an access-policy digest, which changes its
approval-bound material. Such requests are deliberately held before readiness checks
or reservation until the full enforcement path exists. Removing that digest cannot
reuse an approval recorded with it. Existing unmarked developer flows remain unchanged.
This is not a new executable workflow field or public control API.

Concrete descriptor validation and durable reviewed-intent records are now implemented.
Coding-plan binding and worker delivery/revocation fences are also implemented. Executable
attempt permits, native probes, current principal scope and OS cancellation remain open. See the
[4B execution record](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M8-SLICE4B-POLICY-ENFORCEMENT.md).


### Concrete host bindings and reviewed intent

Protected host configuration now describes canonical filesystem roots and identity pins,
exact tool executable hashes and arguments, specific HTTPS destinations, scoped credential
references, governed artifact stores and explicitly approved external operations. Catalog
hashes pin those descriptions. The engine rejects changed descriptions, unresolved entries
and external actions missing their per-node network or credential dependencies.

This validates declared constraints; it does not inspect real filesystem permissions,
classify resolved network addresses or establish a sandbox. Those checks require the
platform adapters. Credentials remain opaque references delivered through an attempt
channel; tool arguments and environment configuration must never carry secret values.

A trusted storage operation can record the reviewed pins together with matching approval
material, authorization revision and coordinator epoch in one transaction. Changing a
registered task requires a fresh approval ID and the expected review revision. Registered
tasks remain blocked at the repository even if a caller removes the optional policy marker.
Revocation preserves the historical reviews and revokes the matching dispatch approval.

These records represent reviewed intent, not running attempts. OS termination and actual
credential revocation remain open. The optional storage component is checked on reopen;
older binaries do not understand its hold, so using an older candidate on this data is
not a supported downgrade. No production or expanded platform claim is enabled.

### Bind a coding plan before approval

Trusted host code can preview and bind access policy before initial human approval using
`previewAccessPolicy` and `bindAccessPolicy`. The binding covers the plan, workspace,
exact stage commands and output paths, and changes the approval subject. The coding
interpreter currently uses one composite node whose scope covers all three stages;
separate stage scopes are not implemented. Bound plans cannot be edited or assigned a
replacement agent without creating separately reviewed work.

`revokeAccessPolicy` requires verified human identity and authorization for `REVOKE_ACCESS`
over the current subject. It holds the workflow and fences queued worker delivery. Late
worker results remain evidence with UNKNOWN responsibility, preserving reservations even
when the worker reports success. It does not yet terminate native processes or revoke
provider credentials. Policy-bound execution stays blocked pending platform enforcement.

Bindings store nonsecret host configuration and immutable history. The optional coding
access storage component must not be opened by an older candidate that ignores its hold.


---

# Governed dynamic workflows

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 adds governed changes to running workflows in stages. **Slices 1–7 provide change preflight, a supervised registry, coding binding, preparation-task insertion and plan/dependency revisions before dispatch, plus reviewed agent replacement at settled stage boundaries and explicit effect reconciliation with stale-result rejection.** See the [registry guide](https://www.lril.ai/engine-dynamic-registry/) for persistence, approval and atomic definition activation. Use the current GitHub source for these experimental engine APIs; they are not part of the published SDK 0.3.0 packages or the baseline source download.

## Slice map

| Slice | Deliverable | Status |
|---|---|---|
| 1 | Revision/proposal contracts and executable validation | Implemented |
| 2 | Durable proposal/revision records, approval and atomic activation | Implemented for UNBOUND registry |
| 3 | Task, attempt, handoff and result revision binding | Implemented for opt-in coding runs |
| 4 | Governed task insertion | Implemented for a serial prefix before dispatch |
| 5 | Plan revisions and branch changes | Implemented before dispatch; all tasks retained |
| 6 | Agent replacement and explicit handoff | Implemented at unstarted/settled boundaries; uncertain work held |
| 7 | Stale-result handling, assessment invalidation and prior-effect reconciliation | Implemented: rejection and reviewed same-stage retry; no refunds |
| 8 | SDK/client controls, examples, parity and recovery tests | Planned |
| 9 | Pinned Archon integration assessment and M7 acceptance review | Planned |

M6 native platform and capacity/soak qualification remain separate open work. Starting M7 does not close those gates.

See [agent replacement and handoff](https://www.lril.ai/engine-agent-replacement/) for transfer authority, adapter installation, acknowledgment and uncertain-work holds.

See [held-work reconciliation](https://www.lril.ai/engine-coding-reconciliation/) for effect proofs, stale assessments and retry without replenishing accounting.

## Run the first slice

From the repository root, on Node.js 24:

```sh
npm run example:engine:dynamic
npm run test:engine:m7-contract
```

The executable example is `engine/examples/dynamic-workflow.ts`. It proposes inserting a lint task between implementation and verification. Its result identifies lint and verification as affected, with `activation: NOT_AUTHORIZED`. It does not enqueue or execute tasks.

The experimental host API lives in `engine/src/dynamic-workflow.ts`:

```ts
import {
  workflowDefinitionDigest,
  preflightWorkflowChange,
} from './engine/src/dynamic-workflow.ts';

// base is a validated aiws-dynamic/1 definition loaded by your host.
const baseDigest = workflowDefinitionDigest(base);
const candidate = structuredClone(base);
candidate.revision = base.revision + 1;
candidate.parentDigest = baseDigest;
candidate.tasks.push({
  taskId: 'lint',
  materialDigest: lintMaterialDigest,
  agentId: 'tester',
  dependsOn: ['implement'],
});
candidate.tasks.find(task => task.taskId === 'verify')!.dependsOn = ['lint'];

const review = preflightWorkflowChange(base, {
  format: 'aiws-change/1',
  proposalId: 'insert-lint',
  baseDigest,
  proposedBy: { kind: 'AGENT', principalId: 'planner' },
  reason: 'Check style before validation',
  candidate,
});
```

This integration fragment assumes `base` and `lintMaterialDigest` are host-supplied. The executable example supplies a complete definition and explicitly labeled demonstration digests. Slice 8 adds [native TypeScript/Rust/Python clients and remote CLI/web controls](https://www.lril.ai/engine-dynamic-controls/) over the optional dynamic HTTPS profile.

## What the contract checks

A definition contains a stable work-order/run identity, revision and parent digest, immutable authority/limits/acceptance digests, and tasks with material digests, agent identities and dependency lists. Task material must eventually resolve to the exact intended command, inputs, outputs and credentials. The host must verify that material and enforce current authority before execution.

Preflight rejects missing/unknown fields, invalid identities or digests, duplicate tasks/edges, missing dependencies, cycles, stale base/parent references, revision gaps, no-op edits and changes to protected policy digests. It rejects protected-policy changes even when the proposer labels itself human. A proposer label is not authentication.

Definition and proposal digests are deterministic and domain-separated. Task/dependency order does not change identity. The proposal digest binds its candidate, proposer, reason and proposal ID. Returned objects are detached from the input.

## Impact and stale work

The affected set includes directly changed tasks and their downstream dependents in both the old and new graphs. Removing an edge therefore cannot hide its previous downstream impact. An independent branch is reported as structurally unaffected, but that does not establish safe reuse: shared files, external effects and other hidden dependencies still require host assessment.

All results carry `resultCompatibility: REJECT_UNLESS_EXPLICITLY_APPROVED`. This is a contract obligation for future consumption, not an implemented result-admission gate. Preflight does not invalidate assessments, cancel work, reconcile effects or reset resource consumption.

## Activation boundary

Successful preflight means the proposal can be reviewed. The Slice 2 registry checks the current persisted head again, verifies exact approval material, fences stale ownership epochs, and records a durable decision atomically. It leaves ledgers/holds untouched and rejects existing executable work. Slice 3 adds explicit [revision binding for initial coding runs](https://www.lril.ai/engine-execution-bindings/), with fresh human stage approval and immutable evidence pins. Existing coding workflows retain their immutable plans. Slice 4 adds [approved serial preparation-task insertion](https://www.lril.ai/engine-coding-insertion/) before dispatch; Slice 5 adds [plan and dependency revisions](https://www.lril.ai/engine-coding-revisions/) with retained nodes and sequential execution of preparation forks/joins. There is no implicit conversion into an arbitrary executing graph.

The full contract and future invariants are in `spec/engine-v1/DYNAMIC-WORKFLOWS.md`. Validation evidence and remaining scope are in `docs/M7-DYNAMIC-CONTRACT.md`.


---

# Durable dynamic workflow registry

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 Slice 2 stores definition revisions, proposals and human decisions in SQLite. **Activation advances the definition registry; it does not dispatch tasks or change an executing coding workflow.** Standalone registry heads report `execution: UNBOUND`. Slice 3 adds explicit [coding execution binding](https://www.lril.ai/engine-execution-bindings/) with `execution: CODING_V1`.

## Try the complete example

From the current repository source on Node.js 24:

```sh
npm run example:engine:dynamic-registry
npm run test:engine:m7-registry
```

`engine/examples/dynamic-registry.ts` registers a definition, submits an agent proposal, simulates an explicit human review, activates revision 2, reopens the database and retries the original command. It verifies that the retry returns the same receipt with `replayed: true`. Its temporary database is removed afterward. Fixed time, identities and material hashes are demonstration inputs, not a production identity/approval service.

## Commands and inspection

| API | Purpose |
|---|---|
| `review(epoch, command)` | Obtain exact subject material for human review; no decision is written |
| `execute({epoch,requestId,credential,command})` | Authorize and transact one command |
| `getHead({workOrderId,runId})` | Read the current registry definition/digest |
| `getProposal(ref,proposalId)` | Read proposal material and pending/activated/rejected status |
| `history(ref,after,limit)` | Read an immutable event page; default 100, maximum 1000 |

Supported command actions are `REGISTER`, `PROPOSE`, `ACTIVATE` and `REJECT`. Registration creates revision 1 for a new work order. Proposals preserve the work-order/run identity and protected policy digests. Activation requires the exact current base and next revision. Rejection stores a human decision and its reason without changing the head.

Integration fragment, assuming your host has already constructed `registry`, authenticated `session`, obtained `epoch`, and reviewed the named approval:

```ts
const command = {
  action: 'ACTIVATE' as const,
  ref: { workOrderId: 'mission', runId: 'run' },
  proposalId: 'change-1',
  approvalId: 'reviewed-change-approval',
};
const result = registry.execute({
  epoch,
  requestId: 'activate-change-1',
  credential: session,
  command,
});
// result.receipt identifies the committed revision; execution remains UNBOUND.
```

## Supply a trusted host adapter

The `DynamicWorkflowRepository` constructor requires `authority.authorize` and `authority.verifyApproval`. There is no allow-all default. The first callback verifies current identity and action/scope permission. The second resolves a separately reviewed human approval. Both return short-lived evidence that the repository validates, and both may deny by throwing.

Do not derive authorization by reading `kind: HUMAN` or approval flags from user input. Proposal attribution must match the authenticated principal. Agents can propose when permitted; this initial profile requires a verified human actor and human approval to register, activate or reject. Automatic/rule-based activation policies are not implemented here.

Approval is bound to installation, ownership and authority epochs, action, work-order/run, exact base/candidate/proposal material and rejection reason. An approval for a different proposal, installation or epoch fails. A consumed proof cannot be reused through an alias approval ID. Expired approvals or sessions fail, including expiry during the transaction.

Callbacks run synchronously under the database writer lock. Perform external sign-in and human interaction beforehand; use short local verification at the transaction boundary. The host must resolve the immutable task/policy material, verify external revocation state and maintain trusted clock observations. Raw credentials are not stored by the registry.

## What survives failure

One transaction writes the new revision, current head, human decision, consumed approval, audit event and request receipt. A process crash cannot expose only part of that activation. Two proposals can be reviewed against one base, but only one can activate; the loser stays pending for explicit rejection or replacement with a new proposal.

Retry with the same authenticated principal, request ID and command. The registry returns the original receipt without a second activation or approval consumption. A changed command under the same request ID fails. Retries still require current authorization, a current ownership epoch and a healthy clock/recovery state. An old receipt describes its original result; read the current head separately.

Old revisions, proposals, decisions and events remain immutable. History is paginated. Stored documents follow the existing bounded engine parser profile, and incomplete/future registry schemas fail without silent repair.

## Current boundaries

Registration and activation reject work orders that already have executable scheduler tasks or a composed coding workflow. They also respect paused/canceling/canceled controls. Restore and clock-uncertainty holds block commands. The registry does not clear holds, refund cost, reset attempts, invalidate assessments or settle uncertain effects.

Slice 3 now provides opt-in task/attempt/handoff/result revision binding; Slice 4 adds [executable preparation-task insertion](https://www.lril.ai/engine-coding-insertion/). Slice 5 adds [plan and dependency revisions before dispatch](https://www.lril.ai/engine-coding-revisions/). Slice 6 adds [agent replacement and handoff](https://www.lril.ai/engine-agent-replacement/); Slice 7 adds [reviewed effect reconciliation and stale-result rejection](https://www.lril.ai/engine-coding-reconciliation/). This API is TypeScript engine source only. Native SDK bindings, remote CLI/web controls and parity arrive in Slice 8. Current M7 work is available in GitHub source; baseline downloadable SDK/source bundles and live-site deployment have separate status.

The full contract is `spec/engine-v1/DYNAMIC-REGISTRY.md`, with evidence in `docs/M7-DYNAMIC-REGISTRY.md`. See the [M7 slice map](https://www.lril.ai/engine-dynamic-workflows/).


---

# Bind coding execution to a revision

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 Slice 3 connects the [durable definition registry](https://www.lril.ai/engine-dynamic-registry/) to the existing [coding workflow](https://www.lril.ai/engine-coding-workflow/). A human can explicitly bind a pre-dispatch run to its compiled revision-1 definition. Tasks, handoffs, attempts, worker results and validations then retain the exact revision and digest.

This is an experimental TypeScript engine source API for Node 24 + SQLite. Use current GitHub source. Native SDK packages, baseline source downloads and remote dynamic CLI/web controls do not yet expose it.

## Try the complete example

```sh
npm run example:engine:bound-coding
npm run test:engine:m7-bindings
```

The example writes and validates a real local artifact, binds the run, issues a fresh stage approval and accepts the result. It prints the committed receipt, definition and record counts. Human identity and review storage are explicitly simulated; production hosts must supply authenticated sessions, scoped permissions and independently verified approval evidence.

## Bind before dispatch

Create a coding run through `CodingWorkflowRunner`, then use `DynamicWorkflowRepository` against the same database:

```typescript
const command = {
  action: 'BIND_CODING' as const,
  ref: { workOrderId, runId },
  expectedWorkflowRevision: created.revision,
  approvalId: reviewedApprovalId,
};
const review = registry.review(runner.coordinator.epoch, command);
// Host presents this exact review and independently resolves human approval.
const result = registry.execute({
  epoch: runner.coordinator.epoch,
  requestId,
  credential: authenticatedHumanSession,
  command,
});
```

This fragment assumes initialized runner/registry instances and host-supplied identity, request and approval values. Reviewing alone grants no authority. `engine/examples/bound-coding.ts` supplies a complete runnable integration.

Binding creates or adopts only the exact compiled genesis definition. `definitionForCoding(plan, planArtifact)` represents the immutable coding plan as one composite node; the existing bounded implementation/validation/correction loop still executes its stages. A successful receipt and current head report `execution: CODING_V1`.

## Approval and migration rules

Only an initial run in AWAITING_APPROVAL or READY is eligible. Prior attempts, active claims, acquired handoffs, holds, nonzero resource exposure and replacement history prevent binding. Existing plan, task/handoff IDs, audit history and accounting are retained.

Binding clears any previous stage approval and returns the run to AWAITING_APPROVAL. Fetch the updated state and use the existing `approvalSubject(state)` and `runner.approve(...)` flow to approve the bound material. Binding approval does not authorize dispatch. In-flight or completed legacy runs remain unbound.

## Evidence and recovery

Dispatch envelopes and worker results carry definition identity plus task, attempt, worker and ownership epoch. The local worker preserves that snapshot. Missing or mismatched result bindings are rejected before delivery completion or accounting settlement. Rejection leaves outstanding responsibility intact.

Tasks and handoffs are checked before dispatch; durable results are checked again during composed validation and acceptance. Standalone validation and generic handoff/replacement APIs cannot bypass the bound coding path. After a restart, the existing human resume path may consume already committed evidence under its original pin without re-running its effect. A stale worker cannot publish a new result.

`registry.executionRecords({workOrderId, runId}, after, limit)` provides paginated immutable pins for TASK, HANDOFF, ATTEMPT, RESULT and VALIDATION. Retry the same binding request ID and command to recover its receipt; this does not reset a progressed run.

## Remaining scope

Slice 4 now supports [governed preparation-task insertion before dispatch](https://www.lril.ai/engine-coding-insertion/); Slice 5 adds [plan and dependency revisions before dispatch](https://www.lril.ai/engine-coding-revisions/). Replacement and explicit compatibility/effect handling remain later work. These tests establish local SQLite/process behavior, not device power-loss durability, production identity integration or M6 platform qualification.

The contract is `spec/engine-v1/EXECUTION-BINDINGS.md`; verification is recorded in `docs/M7-EXECUTION-BINDINGS.md`. See the [M7 slice map](https://www.lril.ai/engine-dynamic-workflows/).


---

# Insert governed coding tasks

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 Slice 4 lets a human activate additional preparation tasks before a [bound coding plan](https://www.lril.ai/engine-execution-bindings/) starts. An authorized agent may propose them. The tasks execute in order, then hand off to the original implementation → validation → correction workflow.

The first profile supports **insertion before the first dispatch**. It refuses changes to claimed, dispatched or completed work. Arbitrary branches and changes to running work need later lifecycle and effect controls.

## Run the example

```sh
npm run example:engine:insertion
npm run test:engine:m7-insertion
```

The example creates a coding plan that needs `prepared.txt`, binds it, proposes and approves a task to produce that file, then executes and validates the final output. It prints the insertion receipt and immutable record counts. Human identities and approvals are explicitly simulated; production hosts must independently authenticate and authorize them.

Use current GitHub source with Node 24. Native SDK packages, baseline source downloads and dynamic CLI/web commands do not yet expose this API.

## Propose executable material

```typescript
import { definitionWithInsertions } from './engine/src/coding-insertion.ts';

const tasks = [{
  taskId: 'prepare-input',
  command: [process.execPath, '-e',
    "require('node:fs').writeFileSync('prepared.txt','ready')"],
  outputPaths: ['prepared.txt'],
  criteriaRef: 'prepared input exists',
  maxTaskMs: 1000,
}];
const head = registry.getHead({ workOrderId, runId });
const candidate = definitionWithInsertions(head.definition, tasks);
```

This fragment assumes a bound run and initialized trusted registry. Store `candidate` through the existing [PROPOSE flow](https://www.lril.ai/engine-dynamic-registry/). Each new task declares its exact argument array, outputs, criteria reference and duration. The command must perform its required checks: a criteria reference alone does not run a semantic validator. Tasks execute with the existing local worker's permissions and workspace.

The helper preserves existing tasks and policies, creates a serial preparation chain, and connects it to the coding plan. Further insertions before dispatch append to that chain.

## Review and activate

```typescript
const command = {
  action: 'ACTIVATE_INSERTION' as const,
  ref: { workOrderId, runId },
  proposalId,
  expectedWorkflowRevision: currentRecord.revision,
  tasks,
  approvalId: reviewedApprovalId,
};
const review = registry.review(runner.coordinator.epoch, command);
// Host presents the exact review and resolves independently verified human approval.
const result = registry.execute({
  epoch: runner.coordinator.epoch,
  requestId,
  credential: authenticatedHumanSession,
  command,
});
```

The review includes the proposed graph, command material and current workflow state. An old approval cannot authorize changed commands or a changed coding state. Plain `ACTIVATE` cannot bypass the insertion checks.

After activation, fetch the new state and use `approvalSubject(state)` with `runner.approve(...)`. The run awaits this fresh stage approval; activation does not dispatch. The complete integration is in `engine/examples/coding-insertion.ts`.

## Limits and existing work

| Situation | Result |
|---|---|
| Initial bound run awaiting approval | Insert and await fresh approval |
| Initial queued task, no claim or attempt | Retain old task/handoff and pins, explicitly supersede them, create new execution IDs |
| Claimed task or acquired handoff | Refuse insertion |
| Dispatched, completed or uncertain work | Refuse insertion; preserve effects and accounting |
| Pause, cancel, open hold or stale owner | Refuse until the applicable control/recovery requirements are satisfied |

Inserted tasks inherit the plan's worker, authority and accounting scope. Their durations cannot exceed its per-task maximum. The prefix must fit the original attempt/time limits with room for core implementation and validation. This does not guarantee correction capacity; runtime admission still charges actual work and enforces the unchanged limits.

Superseded queued instances are marked CANCELED and their IDs are recorded. Their logical definition tasks and historical pins remain retained. No effects are refunded or silently deleted.

## Observe failures and recovery

Each inserted task gets its own task, handoff, attempt, result and validation pins. Inspect them with `registry.executionRecords(ref, after, limit)`. The aggregate exposes `insertions` and `insertionIndex`; preparation handoffs use the IMPLEMENTATION stage and identify the inserted logical task through its pin and dispatch action.

A failed inserted command holds the run before core implementation. An uncertain result keeps outstanding responsibility. After restart, the existing human resume flow can consume a committed result without re-running its effect. Retry an identical activation request to recover its receipt; this never resets execution progress.

The contract is `spec/engine-v1/CODING-INSERTION.md`; local verification is in `docs/M7-CODING-INSERTION.md`. Slice 5 now supports [plan/dependency revisions before dispatch](https://www.lril.ai/engine-coding-revisions/). Slice 6 adds [agent replacement at settled stage boundaries](https://www.lril.ai/engine-agent-replacement/). Slice 7 adds [stale-result rejection and reviewed reconciliation](https://www.lril.ai/engine-coding-reconciliation/); compatibility grants remain unsupported. M6 platform and long-duration qualification remains open.


---

# Revise coding plans and branches

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 Slice 5 lets a human revise a [bound coding plan](https://www.lril.ai/engine-execution-bindings/) and dependencies among its existing preparation tasks **before first dispatch**. Old plans, graphs and task records remain retained. Every activation requires a fresh stage approval before execution.

The local worker can follow preparation forks and joins while executing one task at a time in a stable dependency order. This does not enable conditional branch skipping, task removal, concurrent branch execution or changes to already dispatched work.

## Try it

```sh
npm run example:engine:revision
npm run test:engine:m7-revision
```

The example adds two preparation tasks, revises their serial dependencies into independent branches that join at the coding plan, and revises implementation to verify both inputs before writing the deliverable. It validates and accepts the result, then prints its revision and evidence counts. Identity and approval storage are explicitly simulated; production hosts must independently authenticate and authorize human decisions.

Use current GitHub source and Node 24. Native SDK packages and remote dynamic CLI/web controls remain later work.

## Define the revision

```typescript
import {
  definitionWithCodingRevision,
  normalizeCodingRevision,
} from './engine/src/coding-revision.ts';

const change = normalizeCodingRevision({
  plan: revisedPlan,
  tasks: existingPreparationTasks,
  dependencies: [
    { taskId: 'prepare', dependsOn: [] },
    { taskId: 'configuration', dependsOn: [] },
    { taskId: 'coding-plan', dependsOn: ['prepare', 'configuration'] },
  ],
});
await runner.publishPlan(change.plan);
const candidate = definitionWithCodingRevision(currentHead.definition, change);
```

This fragment assumes a bound run with those two preparation tasks and initialized runner/registry objects. Supply complete material for every existing preparation task and one dependency record for every node, including `coding-plan`. Publish the plan's exact immutable bytes before execution; publication alone grants no authority.

Store `candidate` through the [PROPOSE flow](https://www.lril.ai/engine-dynamic-registry/). The helper rejects cycles, detached tasks, output collisions and changes to protected policies. Every retained task must be required by the coding composite.

## Review and activate

```typescript
const command = {
  action: 'ACTIVATE_CODING_REVISION' as const,
  ref: { workOrderId, runId },
  proposalId,
  expectedWorkflowRevision: currentRecord.revision,
  change,
  approvalId: reviewedApprovalId,
};
const review = registry.review(runner.coordinator.epoch, command);
// Host obtains independently verified human approval for this exact review.
const result = registry.execute({
  epoch: runner.coordinator.epoch,
  requestId,
  credential: authenticatedHumanSession,
  command,
});
```

Review binds the exact commands, graph and current coding state. If the workflow changes while it is being reviewed, activation fails. An agent's proposal cannot activate itself, and plain `ACTIVATE` cannot bypass the revision checks.

After activation, fetch the updated state and use `approvalSubject(state)` with `runner.approve(...)`. The full runnable integration is `engine/examples/coding-revision.ts`.

## What can change?

| Material | Supported behavior |
|---|---|
| Plan description and implementation/validation/correction commands | May be revised with human approval |
| Existing preparation commands, outputs, criteria references and durations | May be revised within inherited limits |
| Dependencies among retained tasks | May be reordered, forked or joined in an acyclic graph |
| Run identity, authority, resource/correction limits and correction approval policy | Remain unchanged |
| Core output paths and acceptance criteria | Remain unchanged |
| Task identities | All retained; additions use [insertion](https://www.lril.ai/engine-coding-insertion/), removal stays disabled |

Criteria references do not perform semantic validation by themselves. Reviewed commands must actually check the required conditions. Tasks use the existing local worker's permissions and shared workspace.

## Existing work and recovery

Initial queued task/handoff instances are explicitly superseded and retained with their old pins. Claims, acquired handoffs, prior dispatches, completed work, pause/cancel controls, holds and stale ownership block revision. Accounting is preserved; activation adds no budget and refunds no work.

The worker executes a deterministic topological order. Before a preparation task or core implementation runs, all earlier preparation manifests are checked, including inputs from branches other than the immediately preceding task. A changed declared input holds the workflow as BRANCH_INPUT_CHANGED. Preparation outputs must use distinct paths; hidden dependencies remain the host's responsibility.

Results keep their exact revision, task, attempt and worker identity. Retrying an identical activation request recovers its committed receipt without resetting progress. After restart, existing human resume controls can consume saved evidence without repeating the effect. Uncertain work still requires reconciliation.

The contract is `spec/engine-v1/CODING-REVISIONS.md`; verification is recorded in `docs/M7-CODING-REVISIONS.md`. Slice 6 adds [agent replacement at settled stage boundaries](https://www.lril.ai/engine-agent-replacement/). Slice 7 adds [stale-result rejection and reviewed effect reconciliation](https://www.lril.ai/engine-coding-reconciliation/). M6 platform and long-duration qualification is still open.


---

# Agent replacement and handoff

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 Slice 6 supports **human-approved replacement before dispatch or between fully settled stages** in an opt-in [bound coding workflow](https://www.lril.ai/engine-execution-bindings/). The replacement receives an immutable handoff, acknowledges it under its authenticated agent identity, and executes through a host-installed adapter. Use current GitHub source on Node.js 24; baseline SDK packages and source downloads do not contain this API.

```sh
npm run engine:typecheck
npm run test:engine:m7-replacement
npm run example:engine:replacement
```

The example uses explicitly simulated human/agent identities and capability evidence. It runs implementation with the original worker, transfers validation to the replacement, and verifies human acceptance, preserved original evidence and cumulative attempts.

## Review, acknowledge and continue

The host installs a `CodingWorkflowRunner` adapter in `agentAdapters[agentId]` with the reviewed `capabilityDigest` and `execute(request)`. `DynamicWorkflowRepository` also requires host authorization, exact human approval verification and an independent `verifyReplacement(target, review)` capability check. Production hosts must establish these identities and proofs; the example's tokens are only local simulation.

1. Read the current bound definition and workflow state. Build a candidate with `definitionWithAgentReplacement(head.definition, {agentId, capabilityDigest})` and submit it through `PROPOSE`.
2. Present the exact `ACTIVATE_REPLACEMENT` review to the human. The command includes the proposal, expected workflow revision, replacement and approval ID. Reviewing does not itself authorize activation.
3. Execute the approved command. The engine preserves the plan, stage, inputs, old evidence and accounting; records the replacement and handoff; cancels superseded queued instances; and requires fresh stage approval.
4. Read `registry.replacementHandoff(assignmentId)` through the trusted host. The assigned agent submits `ACK_REPLACEMENT` with the exact assignment and handoff digest. A human session cannot substitute for the agent's acknowledgment.
5. Renew human stage approval and run the workflow. Dispatch requires both approvals and the exact installed adapter. After restart, use human resume and obtain an acknowledgment for the new coordinator epoch before new dispatch.

The complete runnable sequence is `engine/examples/agent-replacement.ts`. These are local TypeScript repository/runner APIs; remote CLI/web commands and native SDK parity remain Slice 8 work.

## What can be transferred?

| State | Behavior |
|---|---|
| Unstarted next stage, no live claim or attempt | Eligible for reviewed replacement |
| Previous stage fully settled, next stage unstarted | Eligible; prior evidence and accounting are retained |
| Active claim, acquired handoff or unresolved attempt | Replacement refused |
| Paused controls or open accounting hold | Replacement refused |
| Awaiting final acceptance or succeeded | Replacement refused |
| Target has not acknowledged | Dispatch blocked; another reviewed replacement is possible |
| Adapter missing or capability digest differs | Dispatch blocked before stage handoff acquisition |

Replacement changes the one coding agent across all graph nodes. Commands, task identities, dependencies, acceptance criteria, resource limits and correction policy stay fixed. The immutable handoff includes the exact source state, previous agent, new definition and queue supersession. Renewed human stage approval creates the next stage handoff from that reviewed transfer.

## Hold uncertain work

An exact human-approved `HOLD_REPLACEMENT` command moves the workflow to HELD and records a durable dispatch hold. It fences new delivery, acknowledgment and live result publication. The engine retains outstanding attempts and exposure: a late or disconnected worker may still have caused external effects.

A hold does not terminate the worker, undo effects, settle attempts or refund resources. Generic resume cannot release it. Slice 7 adds [explicit same-stage retry after verified effect reconciliation](https://www.lril.ai/engine-coding-reconciliation/). Stale results remain rejected; no compatibility grant is implemented. Do not treat entering a hold as successful agent transfer.

## Ownership and recovery

Worker identity includes the coordinator epoch and assignment generation. Dispatch material and result pins bind the logical agent, reviewed capability digest and exact handoff. Superseded owners cannot publish under the new assignment. Historical results keep their original ownership; completed work is not relabeled or rerun during transfer.

Activation, acknowledgment and hold receipts are durable and idempotent. Replacement commit tests cover rollback and real process termination before and after commit. Existing saved-result recovery avoids repeating committed effects under the same assignment. New execution after restart requires the current epoch acknowledgment.

Plan/dependency edits and preparation insertion after replacement are currently blocked until a combined capability-review path is defined. Repeated whole-agent replacement is supported. Concurrent heterogeneous agents, remote adapter transport, external process termination and cross-revision reuse of unsettled results remain outside this profile.

The contract is `spec/engine-v1/AGENT-REPLACEMENT.md`; verification is in `docs/M7-AGENT-REPLACEMENT.md`. M6 platform and long-duration qualification remains open independently.


---

# Reconcile held work and stale results

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 Slice 7 adds **reviewed recovery of held coding work** and separates historical evidence from evidence that can satisfy the current workflow. Stale or reconciled attempts cannot publish into a new task or satisfy current acceptance. Old evidence stays available for inspection.

Use current GitHub source on Node.js 24. These experimental TypeScript engine APIs are separate from the baseline SDK packages and source download.

```sh
npm run engine:typecheck
npm run test:engine:m7-reconciliation
npm run example:engine:reconciliation
```

## What the example proves

The demo interrupts execution after dispatch authorization but before worker delivery. It records a hold, simulates independent verification that no effect occurred, obtains exact human retry approval, and executes a new implementation/validation pair through final acceptance. The original attempt remains charged: three cumulative attempts, with the interrupted attempt's full time reservation retained.

The demo identities and effect proof are explicitly simulated. Production hosts must establish worker cessation and effect safety independently.

## Review the evidence before retry

1. Inspect the held [bound coding workflow](https://www.lril.ai/engine-execution-bindings/). If needed, use the exact human-approved `HOLD_REPLACEMENT` action described in [agent replacement](https://www.lril.ai/engine-agent-replacement/).
2. Choose a unique reconciliation ID and call `registry.inspectReconciliation(ref, reconciliationId)`. Inspect the workflow, attempts, deliveries, handoffs, claims, holds and accounting in this snapshot.
3. Establish that the prior owner has stopped, all relevant effects are known, retry is safe, and unresolved exposure stayed within its reservation. A timeout or missing result alone proves none of these.
4. Review a `RECONCILE_CODING` command with `decision: 'RETRY_STAGE'`, the exact work-order/run, expected workflow revision, reconciliation ID, evidence reference and approval ID. Match the inspection digest to `registry.review(...).reconciliationDigest` before issuing approval.
5. Execute through the configured human-authority adapter. Its independent `verifyReconciliation(review)` must return a matching, unexpired proof of cessation, bounded exposure and either `NO_EFFECT` or `RETRY_SAFE`.
6. Renew stage approval. The workflow retries the same stage under a new task identity. A replacement agent must also acknowledge its assignment in the current coordinator epoch.

The full runnable sequence is `engine/examples/coding-reconciliation.ts`. These are trusted local host APIs. Native SDKs, remote CLI/web controls and parity remain Slice 8 work.

## What changes—and what remains charged

| Record or condition | Reconciliation behavior |
|---|---|
| Old attempts and result evidence | Retained; permanently excluded from new publication or current acceptance |
| Unresolved attempt | Settled by explicit reconciliation; prior state and proof retained |
| Prior cost/time reservations and attempt count | Unchanged, including full reservations for uncertain attempts |
| Active scheduler claim | Released only after verified cessation |
| Replacement hold | Resolved with an immutable decision link |
| Separate accounting hold or paused/canceled control | Blocks reconciliation |
| Plan, stage inputs, ordinal and correction history | Preserved |
| Stage approval and final validation pointer | Cleared; new approval and fresh validation required |
| Retry task | New identity; ordinary admission and resource limits apply |

An exhausted budget can still block the new attempt. Reconciliation does not add attempts, expand permissions, reset correction limits, refund resources or compensate external effects. The engine records verified cessation; it does not terminate an external process itself.

## Stale assessments and historical results

A definition change conservatively invalidates all prior assessments in that bound run, covering downstream dependencies and possible shared effects. Reconciliation also invalidates prior assessments before retry. Invalidations record their cause while preserving the original validation/evidence records.

Historical integrity checks still work. Current completion and acceptance additionally require the exact current task instance, definition and agent assignment, with no reconciliation marker, invalidation or unresolved replacement hold. An artifact already consumed as a reviewed stage input can remain an input; its old assessment does not become proof that new work passed.

This slice rejects stale-result reuse. It provides no compatibility grant for using another revision's result as current evidence.

## Recovery limits

Uncertain effects, an owner that may still be running, unrelated holds, known excess exposure and exhausted correction rules remain blocked. Eligible recovery covers the current coding stage; it cannot skip branches or invent a successful outcome. A previously passing validation can be held and retried, but cannot retain its old acceptance authority.

Transactions and durable receipts cover retries and process termination before/after commit. An uncommitted decision needs fresh review after restart. Input-manifest checks continue to protect retry inputs. Full-snapshot review uses existing record-size limits; oversized decisions fail closed.

The contract is `spec/engine-v1/CODING-RECONCILIATION.md`; local verification is in `docs/M7-CODING-RECONCILIATION.md`. M6 platform and long-duration qualification remains open independently.


---

# Native dynamic workflow controls

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

M7 slice 8 exposes governed workflow decisions through the opt-in `aiws-dynamic-controls/1` HTTPS profile, version 1.0.0. TypeScript, Python and Rust clients speak the same closed contract. Use current GitHub source; baseline SDK binaries and source downloads do not contain these APIs.

## Connect a host

Configure `startControlHost` with `dynamic: {}` and your verified identity adapter. Supply independent replacement/reconciliation verifiers and installed agent adapters for those capabilities. Authorize dynamic operations and command actions per work order. The existing [human control surface](https://www.lril.ai/engine-controls-handoffs/) remains available, with separate stage approval and deliverable acceptance.

Fetch `/engine/dynamic/v1/session` over verified loopback HTTPS to obtain installation, namespace and current epochs. Keep credentials outside persisted messages. REVIEW returns exact material and impact; it does not confer approval. An explicit verified human decision permits CHALLENGE, followed by the identical EXECUTE command. Replacement requires the assigned agent to acknowledge its exact handoff. Reconciliation still requires independent host evidence.

## Native clients

| Language | Import | Prepare and send |
|---|---|---|
| TypeScript | `@aiws/sdk/dynamic-controls` | `DynamicClient`, `dynamicRequest`, `dynamicHttpsTransport` |
| Python | `aiws.dynamic_controls` | `DynamicClient`, `dynamic_request`, `https_transport` |
| Rust | `aiws_sdk::dynamic_controls` | `DynamicClient`, `dynamic_request`, `https_transport` |

Each client separates preparation from sending so the exact request can be persisted first. The supplied executables read `{ "authorization": "Bearer …", "request": {…} }` from protected stdin and take only the HTTPS origin and CA filename as arguments:

```sh
node examples/dynamic-client.ts https://127.0.0.1:7443 host-ca.pem < protected-input.json
python examples/dynamic_client.py https://127.0.0.1:7443 host-ca.pem < protected-input.json
cargo run --locked -p aiws-sdk --example dynamic_client -- https://127.0.0.1:7443 host-ca.pem < protected-input.json
```

Protect credential input using your host's secret handling. The complete reproducible three-language HTTPS demonstration is `npm run test:dynamic:transports`. It uses labeled test identities, commits one binding and recovers that exact receipt through both other clients. Binding leaves execution awaiting fresh human stage approval.

## CLI and web

The CLI reads `{ "authorization": "Bearer …" }` from protected stdin. `dynamic-session` fetches context; `dynamic-requests` sends a previously saved envelope:

```sh
node engine/src/control-cli.ts https://127.0.0.1:7443 host-ca.pem dynamic-session < credential.json
node engine/src/control-cli.ts https://127.0.0.1:7443 host-ca.pem dynamic-requests request.json < credential.json
```

In the local web interface, inspect work, prepare a binding or paste a SDK-produced command, then **Review change**. Inspect the displayed identities, revision, material and evidence before **Authorize and execute**. Editing the command requires another review. Cookies and CSRF stay within the configured origin.

## Recovery

The web interface saves the exact execute envelope in sessionStorage before transmission. After an uncertain response, use **Retry saved request** or **Look up saved result** as the original actor. Reloading the tab retains pending identity; download the request before closing the tab. Native clients and CLI require the caller to retain the prepared file. No client retries automatically.

A restart invalidates old sessions, envelopes and unused review proofs. Obtain fresh context and LOOKUP the original requestId. A committed receipt survives restart. If commitment remains uncertain, resolve it before issuing a new decision. Activation never refunds prior attempts, reservations or correction history.

The optional database component is additive and guarded. Existing workflows remain unchanged until explicitly bound. Follow the established offline backup/restore policy for upgrades. The shared schema, all eleven actions, exact message limits and authority rules are in `spec/engine-v1/DYNAMIC-CONTROLS.md` in the repository.

## Verification scope

The shared corpus contains 271 cases checked natively in three languages, including valid actions, missing/extra fields, version and JSON failures. Live HTTPS tests and engine recovery tests qualify the local source profile. M7 slice 9 covers integration acceptance. The bounded D-M6-01 local developer profile is complete; broader platform and long-duration qualification remains open in M8.


---

# Agent capability and readiness checks

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

S1 adds a readiness check to standalone AIWS. It answers **“Does this agent support what this work requires, and what is missing?”** Approval and permission checks still decide whether the work may run.

A report can say READY while execution remains AWAITING_APPROVAL. Every report explicitly contains `authorized: false`.

## What the report explains

| Result | What it means |
|---|---|
| Missing | The selected agent declares no matching capability |
| Unsupported | The implementation explicitly cannot meet the requirement |
| Unverified | Support is claimed, but independent host evidence is missing |
| Evidence expired | The capability needs verification again |
| Stale | The host observation is too old or dated in the future |
| Satisfied | The requested declaration or verification is present; approval is still required |

Requirements cover tools, outputs, context/resume support and isolation. Isolation requirements always demand verification. A caller-provided snapshot cannot prove its own claims; the execution host must obtain its observations independently.

The built-in local inspector checks the current Node executable and worker implementation without running a discovery command. It supports file artifacts, text output and fresh process context. It explicitly reports filesystem sandboxing, denied network access and provider-session resume as unsupported. Working in a directory does not establish an OS sandbox.

## Review, then pin

```typescript
// Integration fragment: runner is configured by a trusted host; plan is a CodingPlan.
const report = await runner.previewReadiness(plan, requirements);
if (report.status !== 'READY') {
  console.log(report.reasons, report.checks);
} else {
  const record = await runner.create(plan, {
    profile: 'aiws-readiness-binding/1',
    version: '1.0.0',
    manifestDigest: report.manifestDigest,
    requirements,
  });
  // record remains AWAITING_APPROVAL.
}
```

The complete runnable example is `npm run example:engine:readiness`. Its simulated human decisions are labeled; its implementation and validation commands really run in a temporary workspace.

The binding fixes the selected capabilities and requirements for the run. It participates in human approval and dispatch material, survives restart and is checked again before execution. A provider/model/tool configuration change cannot silently satisfy the old pin. If a change is detected after dispatch has already been recorded, the runner blocks delivery and retains resource responsibility for reconciliation.

S1 pins are immutable. Changing agents or repinning a run is not supported in this slice; create separately reviewed new work for different capabilities. Existing unpinned workflows keep their existing behavior. Inspection does not automatically migrate or bind them.

## Inspect existing work

```sh
node engine/src/control-cli.ts https://127.0.0.1:7443 host-ca.pem readiness work-order-id < protected-credential.json
```

The authenticated endpoint is `GET /engine/readiness/v1/<work-order-id>`. The host must authorize the `readiness` action for that work order. The report is read-only; it does not reserve resources or approve a task. The initial HTTP route uses the existing verified human session policy.

Hosts enable pins using `readinessBindings[workOrderId]` in `startControlHost` configuration, and can supply an independent read-only `capabilityInspector` for installed implementations. Supply the same bindings on restart. The runtime also adds minimum requirements from implementation, validation, applicable correction and preparation commands; an empty caller list cannot remove those checks.

## Native SDKs

| Language | Module | Main functions |
|---|---|---|
| TypeScript | `@aiws/sdk/readiness` | `evaluateReadiness`, `manifestDigest`, `decodeReadiness` |
| Python | `aiws.readiness` | `evaluate_readiness`, `manifest_digest`, `decode_readiness` |
| Rust | `aiws_sdk::readiness` | `evaluate_readiness`, `manifest_digest`, `decode_readiness` |

All three evaluate the same versioned JSON contracts. Use current GitHub source; baseline published packages do not include these additions. Fixed-input runnable examples are `examples/readiness.ts`, `examples/readiness.py` and Cargo example `readiness`, using `spec/engine-v1/readiness.example.json`. Those inputs are demonstration claims, not authenticated host evidence.

The shared corpus covers 94 codec cases and 13 semantic scenarios in each language. The optional guarded storage component, trust model, exact expiry rules and compatibility limits are documented in `spec/engine-v1/READINESS.md` in the repository. S2 will use this foundation to improve workflow authoring; it is not implemented yet.

## Guided plan authoring

[S2 guided authoring](https://www.lril.ai/engine-authoring/) now uses these readiness reports to inspect drafts and create exact reviewed coding plans. [S3 handoff diagnostics](https://www.lril.ai/engine-handoff-diagnostics/) are implemented; [S4 governing context](https://www.lril.ai/engine-context-policy/) is implemented; S5 is next.


---

# Guided coding workflow authoring

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

S2 helps you turn commands, expected output files and acceptance criteria into the coding plan AIWS already runs. It asks for the required fields, explains errors and shows the execution sequence. It uses [S1 readiness](https://www.lril.ai/engine-readiness/) to identify missing tools and unsupported requirements.

Authoring is read-only. It does not execute your commands, install tools, create a run or approve work. A successful preview means the plan is structurally valid and its requirements are supported by the observed environment. It does not prove that the commands will produce correct results.

## Start with the questionnaire

From the current GitHub source checkout, using Node 24:

```sh
npm run engine:author -- --interactive
```

The questionnaire asks for the work order and run identities, intended result, implementation/validation/correction commands, output files, acceptance criteria reference and resource limits. Correction approval defaults to `true` if you press Enter. Other answers must be supplied explicitly. Invalid answers produce a field-specific explanation and the same question is asked again.

Commands are **JSON argument arrays**. The first item identifies the executable; subsequent items are its arguments. The author does not split a shell string, run a shell for you, add commands or silently change your limits.

```json
["/absolute/path/to/node", "-e", "require('node:fs').writeFileSync('result.txt','ready')"]
```

Use the actual Node executable path printed by `node -p process.execPath`; on Windows, escape backslashes inside JSON. The built-in inspector recognizes that exact executable as the verified `node` capability. A bare `node` or another command needs a trusted host inspector that verifies the corresponding `command:<executable>` capability. There is no automatic PATH substitution.

Output paths use forward slashes and are relative to the run workspace, for example `["result.txt"]`. Acceptance criteria are a reference to what a human will use to judge the result. Validation must exit zero and preserve the delivered output files to support acceptance.

## Inspect an existing draft

```sh
npm run engine:author -- --input plan.json
node engine/src/authoring-cli.ts --stdin < plan.json
```

Use the direct Node command when another program needs clean JSON stdout; npm may print its own script banner. Questionnaire prompts go to stderr. The CLI does not overwrite input files. To save a new preview without overwriting an existing file, choose a new filename:

```sh
node engine/src/authoring-cli.ts --input plan.json > plan-preview.json
```

The CLI exits **0** only for a valid plan with READY capability inspection and a review digest. Invalid plans, blocked requirements, inspection failures and malformed input exit **1**. Read the report for the explanation.

| Output | Meaning |
|---|---|
| `valid` | Whether the draft satisfies authoring and existing coding-plan validation |
| `plan` | Exact validated plan; no command or budget substitutions |
| `diagnostics` | Field, code, severity and suggested correction; warnings leave your settings unchanged |
| `steps` | Existing execution stages, possible preceding stages (`afterAny`) and conditions |
| `readiness` | S1 report explaining declared/verified capabilities and gaps |
| `binding` | Current capability digest and effective requirements to pin when creating a run |
| `reviewDigest` | Digest identifying the exact plan and capability binding; null if unavailable |
| `authorized` | Always `false`; authoring cannot grant authority |

`afterAny` describes possible transitions in the existing state machine. It is not a custom dependency graph or an instruction to wait for every listed predecessor. Successful implementation leads to validation. Failed validation can enter a correction cycle, followed by validation again. Implementation or correction failures stop for inspection. Successful validation still needs separate human acceptance.

## Include additional capability requirements

Supply an existing S1 requirements document:

```sh
node engine/src/authoring-cli.ts --input plan.json --requirements requirements.json
```

For example, a requirements document requesting verified `ISOLATION / network-deny` will be blocked by the built-in local worker because it has no network isolation. Minimum command, file-artifact and fresh-context requirements are always retained and verified; an empty custom list cannot weaken them. Use the same requirements with the runner preview and creation calls.

The standalone CLI observes its own local runtime. A deployment host can inspect its installed environment through `runner.previewAuthoring`. If that environment differs, the host preview must be reviewed again.

## Create the exact reviewed plan

The TypeScript engine exposes the authoring API. This slice does not add a new workflow execution format or claim a new native SDK authoring codec. Python, Rust and other callers can generate the existing plan JSON and inspect it with the CLI; S1 native readiness evaluators remain available separately.

In a host with an already opened `CodingWorkflowRunner`:

```ts
const preview = await runner.previewAuthoring(draft, requirements);
// Present plan, diagnostics, steps, readiness and reviewDigest for review.
if (!preview.valid || preview.readiness?.status !== 'READY' || !preview.reviewDigest) {
  throw new Error('Resolve the preview findings before creating this run.');
}

// Call only after the exact preview has been reviewed.
const created = await runner.createAuthored(draft, preview.reviewDigest, requirements);
// created.state.status is AWAITING_APPROVAL.
// Use existing authenticated runner.approve(...) before execution,
// and runner.accept(...) after successful validation.
```

The review digest includes all plan fields and the effective capability binding. It excludes observation timestamps so a refreshed, unchanged observation does not require a different digest. Creation performs fresh host inspection: changed commands, paths, identities, limits, correction approval policy, requirements or manifest produce `STALE_AUTHORING_REVIEW`. Expired or unsupported observations produce `READINESS_BLOCKED` even if the digest is unchanged.

The digest is an exact-match check, not a credential or proof of human identity. The existing authentication adapter and human approval remain authoritative. Created runs retain S1's immutable pins and final dispatch/delivery checks.

## Edit and review again

Edit a draft, preview again and review the new result before creating it. Calling `createAuthored` with an existing work order does not replace that run. For a running workflow, use the existing governed M7 revision proposal and approval mechanism where supported; authoring is for new coding runs. S1 capability bindings cannot be rebound in place.

Arbitrary branches, parallel tasks, custom nodes/edges/dependencies, triggers, agent assignment and embedded approvals are explicitly refused by this author. Additional properties are errors. The supported built-in validation/correction loop remains available.

The author requires at least two attempts because implementation and validation each consume an attempt. Each complete correction cycle needs two more. It warns if your limit cannot cover every configured cycle, if the total time is smaller than a task reservation, or if you permit corrections without renewed approval. It does not increase budgets or change policy. The correction command remains required by the existing contract even when `maxCorrections` is zero; it will not run in that case.

Input is bounded to 1 MiB with duplicate-key, unsafe-number and malformed-Unicode rejection. Authoring also bounds field sizes and collections. It is not a command security audit or an OS sandbox.

## Run the complete example

```sh
npm run example:engine:authoring
```

The example uses real local runtime inspection and commands in a temporary workspace, with explicitly simulated human identity. It rejects an edited draft using an old review digest, creates the original plan awaiting approval, implements, validates and separately accepts the output. The temporary workspace is cleaned up afterward. These demo credentials are not production authentication.

S2 is implemented for this local source profile. [S3 handoff diagnostics](https://www.lril.ai/engine-handoff-diagnostics/) now expose input/output expectations and current material checks. External-provider, platform, browser and release qualification remain separate work.


---

# Handoff expectations and diagnostics

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

S3 makes the existing local coding handoff contract visible. It answers **“What does this step need, which task produced it, and do the current files still match?”** It derives expectations from the reviewed plan, immutable producer manifests and current workflow state. It does not guess missing inputs or silently replace them.

A handoff report is read-only and always says `authorized: false`. READY means the inspected handoff material passes these checks. The workflow may still be awaiting human approval, held, paused or awaiting acceptance. A report cannot clear any of those conditions.

## Inspect a handoff

With an already opened TypeScript engine runner:

```ts
const report = await runner.handoff(workOrderId);
console.log(report.inputs, report.outputs, report.checks);
console.log(report.provenance);
```

The authenticated control CLI accepts the work order ID as its final argument:

```sh
node engine/src/control-cli.ts https://127.0.0.1:8443 ca.pem handoff-diagnostics WORK_ORDER_ID < protected-session.json
```

The protected input file uses the existing credential format, such as an `authorization` field containing the session token. Do not put credentials in command arguments. The underlying endpoint is `GET /engine/handoff-diagnostics/v1/{encoded-work-order-id}`. The host checks the authenticated human's `handoff-diagnostics` permission for that work order before and after inspection.

The control CLI reports transport/service errors using its existing convention. A successfully retrieved BLOCKED report is still a successful inspection request: callers must inspect `report.status`. This differs from the standalone S2 authoring CLI's readiness exit code.

## Declarative expectations

| Report field | What it declares |
|---|---|
| `inputs` with `PLAN_JSON` | The exact current plan artifact and its content digest |
| `inputs` with `FILE_MANIFEST` | Required producer/preparation manifests, file paths, byte sizes and SHA-256 digests |
| `outputs.paths` | The current command's required regular output files, from the reviewed plan or insertion task |
| `outputs.criteriaRef` | The current task's acceptance criteria reference |
| `outputs.mustPreserveInput` | Validation must use and preserve the received deliverable files |
| `provenance` | Plan digest, definition revision when bound, producer task and settled attempt when available, input digest and validation attempt reference |
| `bindingError` | Existing engine error code when a durable handoff/task fails its pinned definition check; null otherwise |
| `contextDigest` | Identity of this report's run revision, consumer, expectations and provenance |

These are explicit views of existing declarations, not a new general-purpose input schema language. The supported formats are plan JSON and regular file manifests. Arbitrary MIME types, JSON-schema validation of file contents, optional inputs and automatic format conversion are outside S3.

The report includes completed preparation outputs when the implementation consumes them. A validation step expects paths compatible with its declared outputs. A missing producer manifest cannot be interpreted as an empty successful handoff.

Future output files need not exist before their producer runs. If execution is held because required outputs were unavailable, the report identifies the missing or incompatible output paths. A file that appears later does not retroactively recreate execution evidence or satisfy acceptance.

## Read the diagnostic checks

| Status | Meaning | Next action |
|---|---|---|
| SATISFIED | Current bytes or manifest match the expectation | Continue through the existing approval and execution gates |
| MISSING | Required workspace file, stored content or handoff is absent | Inspect the named path and producer history; restore only verified original content or use governed recovery |
| CHANGED | Current bytes differ from the expected digest or size | Compare expected and observed digests; obtain a governed new result if the change is intentional |
| INCOMPATIBLE | A path is a directory/symlink, or validation paths do not match the declared input contract | Correct the producing contract or material through the supported review process |
| STALE | The report context or durable handoff no longer identifies the current consumer/input | Request a fresh report and review the current revision |
| UNAVAILABLE | Inspection could not read the material for another reason | Check host access and availability before retrying inspection |

Checks expose paths, digests and explanations, not file contents or raw filesystem error messages. `source` identifies the plan, incoming input, preparation task, handoff, context or unavailable output.

## Detect stale observations

Save the context digest if a user or application is looking at an earlier report:

```ts
const first = await runner.handoff(workOrderId);
// Later, request current inspection while retaining the original context identity.
const current = await runner.handoff(workOrderId, first.contextDigest);
if (current.status === 'BLOCKED') {
  console.log(current.checks.filter(check => check.status !== 'SATISFIED'));
}
```

Advancing a stage or changing the run revision makes the old context stale. File changes can leave the context digest unchanged because the expectation is unchanged; they are detected by reading and hashing the current bytes again. If the run changes during inspection, the returned report is BLOCKED with a request to refresh.

The context digest is neither a credential nor an approval token. Producer references identify provenance, not proof that every acceptance criterion passed. In particular, failed validation can legitimately hand the same files to a correction step. Existing M7 result binding, assessment invalidation, replacement/reconciliation and final acceptance checks remain authoritative. Historical or invalidated assessments do not become current evidence merely because their files still match a hash.

## Execution behavior

Existing early input checks remain in place. S3 checks the complete handoff before acknowledgment, then checks again after the host's final dispatch preparation and again immediately before the adapter receives the command. These checks also verify that the inspected run revision matches the pending execution.

If material changes before admission, execution is refused and the unused claim is released. If dispatch has already committed, a failed delivery check marks the attempt UNKNOWN and holds the workflow while retaining responsibility. AIWS does not treat that uncertainty as proof that an attempt was free or safe to repeat.

Inspection does not publish artifacts, rewrite files, restore content, consume handoffs or alter accounting. The authenticated HTTP request may perform the existing authentication/clock bookkeeping. Reports are point-in-time observations; the host must serialize relevant changes. This is not OS-level filesystem sealing, sandboxing or protection against a privileged process racing file operations.

## Try the local example

```sh
npm run example:engine:handoff-diagnostics
```

The example creates an S2-authored run, produces a file and inspects its handoff. It deliberately changes the file and observes BLOCKED, explicitly restores the original producer bytes, then validates and separately accepts the result. The example uses real commands and artifact inspection in a temporary workspace, with explicitly simulated human identity. Inspection itself never repairs the changed file.

S3 is implemented for the local coding source profile. This report is an engine inspection surface; no new portable native SDK handoff codec or browser view is claimed. [S4 governing context](https://www.lril.ai/engine-context-policy/) now preserves original instructions and session lineage across restart and agent changes. M6 platform/capacity/durability and M8 release/browser qualification remain separate gates.


---

# Governing context and session lineage

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

S4 adds an optional governing-context policy to the local coding runner. It identifies the original instructions that every execution must receive, retained evidence and an optional advisory summary. The policy is immutable once a run is created, participates in human approval and dispatch material, and survives restart and agent replacement.

The engine verifies and delivers context. It does not prove that a model understood or followed the instructions. The existing permission, approval, validation and acceptance mechanisms remain authoritative.

## Define a policy

Publish the original documents to the host-owned artifact store, then reference their exact digests. In a host with an opened runner:

```ts
import type { ContextPolicy } from './engine/src/context-policy.ts';

const rules = await runner.artifacts.publish(
  'Preserve required approvals. Validate all declared outputs before acceptance.'
);
const evidence = await runner.artifacts.publish('Reviewed reference material.');
const policy: ContextPolicy = {
  format: 'coding-context-policy/1',
  instructions: [{ name: 'governing-rules', artifactDigest: rules }],
  retainedEvidence: [{ name: 'reference', artifactDigest: evidence }],
  summary: null,
  maxBytes: 8192,
};
const created = await runner.create(plan, undefined, policy);
// The run is still AWAITING_APPROVAL.
```

| Field | Requirement |
|---|---|
| `instructions` | 1–16 named artifact references; originals remain mandatory |
| `retainedEvidence` | 0–16 named artifact references; historical evidence is not automatically current acceptance proof |
| `summary` | Null, or an exact artifact digest plus `basedOn`, identifying every unique original instruction/evidence digest |
| `maxBytes` | Total raw document byte budget, including a configured summary; 1–32768 bytes |

Names must be nonblank, unique across instruction/evidence references and at most 128 characters. Records are closed: fields claiming permissions or authority are refused. Digests are lowercase SHA-256 strings.

A configured summary is delivered separately with `advisory: true`. It cannot replace originals, remove approval requirements or widen tool permissions. Missing originals still block execution when the summary is available. If a summary is configured, its exact bytes are also required. The engine does not silently truncate context or regenerate summaries to fit the budget.

## Include context in an authored run

S2's host API accepts the policy separately from the coding-plan JSON:

```ts
const preview = await runner.previewAuthoring(draft, requirements, policy);
// Review preview.plan, preview.contextPolicy and the readiness findings.
const created = await runner.createAuthored(
  draft, preview.reviewDigest!, requirements, policy
);
```

Changing the policy invalidates the authoring review digest. Capability readiness alone does not establish document availability: use `runner.context(workOrderId)` after creation, and the execution gates check again before delivery. The CLI questionnaire continues to author the existing coding plan; policy attachment is a host API/configuration operation.

Hosts that create plans during startup can supply `contextPolicies[workOrderId]` alongside their existing `plans` and optional `readinessBindings`. Supply the same policy on create retries; omitting or changing it cannot remove protection from an existing run.

## Inspect context without executing work

```ts
const report = await runner.context(workOrderId);
console.log(report.status, report.checks, report.lineage);
```

The report contains references and lineage, not document contents. It always has `authorized: false`.

| Status | Meaning |
|---|---|
| NOT_CONFIGURED | This run has no S4 policy; legacy behavior applies |
| READY | Required exact bytes and a current-epoch session are available |
| BLOCKED | Context is missing, changed, unreadable, over budget, or tied to an old session epoch |

READY does not approve a run, clear a hold, acknowledge a replacement or accept a deliverable. Reports also refuse to present a changing run as a stable observation.

The existing authenticated control CLI exposes the same inspection:

```sh
node engine/src/control-cli.ts https://127.0.0.1:8443 ca.pem context WORK_ORDER_ID < protected-session.json
```

The endpoint is `GET /engine/context/v1/{encoded-work-order-id}`. The host checks the authenticated human's scoped `context` permission before and after inspection. Callers must inspect `report.status`; successfully fetching a BLOCKED report is not a transport failure.

## Understand session lineage

Each context-bound run has immutable session records. They identify the work order/run, agent and assignment, policy digest, coordinator epoch, parent session and creation time.

| Kind | When it is recorded |
|---|---|
| FRESH | When the context-bound run is created |
| COLD_RESUME | After governed restart approval in a new coordinator epoch |
| REPLACED | In the existing human-approved agent replacement transaction |

Restarting the engine does not automatically approve a context session. Until governed resume occurs, context inspection is BLOCKED. Repeating resume in the same epoch does not create duplicate session records. A replaced agent still needs its existing exact handoff acknowledgment in the current epoch, as well as human approval.

COLD_RESUME means originals and retained evidence are supplied to a new execution after restart. S4 does not implement opaque provider-session resumption or assume that a previous model conversation survives. The stage handoff's task identity, plan digest, definition reference and incoming artifact digest are included in the delivery envelope; S3 independently verifies current input files.

S1's restriction remains: a capability-pinned run cannot replace its agent. S4 policies can be used on runs without an S1 capability pin when governed replacement is needed. This slice does not weaken either restriction.

## Deliver context to an agent

A context-capable host adapter receives `request.context`, a `coding-context-envelope/1` value containing the session, current handoff identity, original documents as base64 bytes, retained evidence and the separate advisory summary. It must explicitly declare `supportsContext: true`.

```ts
import { contextEnvelopeDigest } from './engine/src/context-policy.ts';

const adapter = {
  capabilityDigest: installedAdapterDigest,
  supportsContext: true as const,
  async execute(request) {
    const context = request.context;
    const receipt = context ? contextEnvelopeDigest(context) : undefined;
    // Deliver the original instructions/evidence and advisory summary to the
    // agent using the host integration; execute through its approved adapter.
    const evidence = await executeWithInstalledAgent(request);
    return { ...evidence, ...(receipt ? { contextDigest: receipt } : {}) };
  },
};
```

This is an integration sketch: `executeWithInstalledAgent` must actually deliver the context. Returning a digest is an adapter receipt, not proof of model comprehension or compliance. Compute the digest over the exact received envelope before asynchronous execution.

The built-in local worker supplies JSON through the child environment variable `AIWS_CONTEXT_BUNDLE`. Document bodies use base64; scripts can decode them explicitly. Its execution evidence records `contextDigest`. Both the expected receipt and the durable result remain associated with the current task and session.

## Failure and recovery behavior

Context is checked before acknowledgment, before dispatch admission and immediately before adapter execution. Missing required material blocks execution. If delivery has already committed, missing context or a wrong/missing adapter receipt retains UNKNOWN responsibility and holds the run. It cannot be interpreted as a free attempt or automatic permission to retry.

Policies and session rows have immutable database guards. Removing the policy from mutable run state cannot disable its durable binding. Unknown component versions, missing tables or missing guards are rejected on database open. The optional component is installed transactionally when a context-bound run is first created; inspecting legacy runs does not migrate them.

Policy edits require separately reviewed new work. Summaries cannot grant authority, and retaining evidence does not make invalidated assessments current. Hosts remain responsible for access controls, artifact retention and truthful adapter delivery. This local profile provides no OS isolation guarantee.

## Run the example

```sh
npm run example:engine:context
```

The example uses actual local execution and stored instruction bytes with explicitly simulated human identity. It creates an authored, context-bound run, restarts the engine, verifies that context is blocked until resume approval, and completes validation and separate acceptance. Its output shows the linked FRESH and COLD_RESUME sessions.

S4 is implemented as an optional local engine feature. It does not add a portable native SDK codec, external provider integration or browser view. S5—the operator run view—is next. M6 operational qualification and M8 release/browser gates remain open.


---

# Operator run view

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Existing `engine/*` source paths, `/engine/...` routes, `aiws-engine/*` protocol identifiers, and existing script names remain unchanged for compatibility.

The native control page now opens with a run overview. Sign in through the configured host identity provider, enter a work order and choose **Inspect work**. The view reads one transactional engine snapshot and pairs it with the same workflow revision's decision evidence. It grants no execution authority.

![Three independent milestones: produced output, current validation and human acceptance, with a correction loop after failed validation.](https://www.lril.ai/diagrams/operator-milestones.svg)

## Read the overview

| Area | What it tells you |
| --- | --- |
| Status and current stage | Where the workflow is now, separately from its execution control state |
| Three milestones | Whether output exists, current durable validation passed, and a human accepted the work |
| Next decisions | Approval, acceptance, restart review, agent acknowledgment, reconciliation or paused/cancellation control work |
| Ownership | Assigned agent, active workers and latest context session kind |
| Resource exposure | Attempts used; active time and cost used plus reserved; configured limits and unsettled responsibility |
| Evidence | Plan reference, input digest, validation attempt, definition revision and deliverable file hashes |
| Attempt history | Latest 100 retained attempts, with delivery, assessment and historical invalidation shown separately |

Cost values use the engine's configured units. They are not currency estimates. A stopped attempt can still carry unsettled exposure. The view preserves those reservations until the authoritative settlement path releases them. Historical passing assessments cannot satisfy current acceptance.

## Refresh and decide

**Inspect work** refreshes the view; there is no live push stream. A load-time label makes the snapshot's age visible. Reload restores this tab's selected work order and reads the engine again. Editing the selection, a failed inspection or a dynamic workflow change clears the old decision evidence. If the workflow revision changes between reads, refresh again before deciding.

Approve, pause, resume and cancellation use the existing scoped control service. Accepting validated work remains a distinct, explicitly confirmed human action. The server checks current revision, binding, identity and authorization when it handles the command.

Before sending a decision, the page saves its exact request in this tab's session storage. If the response is lost, **Retry saved decision** resends the same request ID and body; **Look up saved decision** retrieves its durable result. Both require a fresh authenticated connection under the original actor and installation. A later rejection does not erase an earlier uncertain outcome. Saved bodies contain neither session credentials nor CSRF tokens. Closing the tab or clearing browser storage can remove this local recovery copy; engine receipts remain authoritative.

## Host and CLI access

In a host with an opened coding runner:

```ts
const snapshot = await runner.operatorView('work-order-id');
// snapshot.format === 'coding-operator-view/1'
// snapshot.authorized === false
```

The host exposes `GET /engine/operator-view/v1/{workOrderId}` and authorizes the scoped `operator-view` action before and after the read. CLI credentials still arrive through protected standard input:

```sh
node engine/src/control-cli.ts https://127.0.0.1:PORT /secure/host-ca.pem operator-view work-order-id < /secure/credential.json
```

The operator response is a local engine inspection format; it adds no general workflow wire command or external integration.

## Qualification

Engine regression and DOM interactions against the real authenticated HTTPS engine cover reload, exact replay after a lost committed response, distinct validation/acceptance and clearing stale evidence. Playwright headless Chromium then verified the same engine in an actual browser: authenticated inspection, a lost reply with reload and exact retry, validation separate from acceptance, acceptance surviving reload, and full-page captures at 1280 px and 390 px with no horizontal overflow on mobile. Reproducible DOM and Playwright checks are `engine/scripts/verify-operator-ui.mjs` and `engine/scripts/verify-operator-browser.mjs`; static TypeScript checking and the site build also passed. S5 is qualified; M6 platform and M8 release gates remain separate.

## Application identity

The Praxis operator app and SDK documentation share the same SVG mark as their
header logo and browser favicon. The app serves the icon locally, and the candidate
bundle includes it with the web assets; no external image service is required.


---

# Praxis protocol interoperability

> **M9 status:** **Complete.** M9.1–M9.10 are accepted for the scoped source interoperability profile: MCP 2026-07-28 + Tasks, A2A 1.0 and AG-UI 1.0 are version-pinned and evidence-backed, with SDK correlation helpers, operator guidance and visual architecture included.

Praxis now has a common boundary for three complementary interoperability roles:

| Protocol | Role around Praxis | Initial target |
|---|---|---|
| **MCP** | Tools, resources, prompts and protocol extensions | 2026-07-28 |
| **A2A** | Remote agent discovery, delegation and task interaction | 1.0 |
| **AG-UI** | Agent/user event projection and later authenticated control intents | 1.0 |

The design rule is simple:

> **Praxis owns execution. Protocols provide connectivity.**

AIWS still defines work orders, runs, tasks, attempts, authority, human approval, budgets, handoffs, evidence, verification, acceptance and recovery. A protocol adapter cannot replace those semantics.

## The boundary

![Praxis protocol interoperability architecture](https://www.lril.ai/images/m9/protocol-architecture.svg)

MCP, A2A and AG-UI deliberately remain adapters around the engine rather than branches inside the core state machine. The [protocol mapping visual](https://www.lril.ai/images/m9/protocol-mappings.svg) shows the three roles side by side.

## What M9 slice 1 added

The source contract is `engine/src/protocol-binding.ts`.

It introduces:

- a version-pinned `ProtocolBindingManifest`;
- a runtime `ProtocolBindingCatalog`;
- descriptive protocol capabilities;
- an `AdmittedProtocolOperation` for work that has already passed Praxis governance;
- closed dispatch receipts;
- non-authoritative external observations;
- cancellation and reconciliation result shapes;
- tests that cover MCP, A2A and AG-UI manifests with the same common abstraction.

The formal contract is [`spec/engine-v1/PROTOCOL-BINDINGS.md`](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/spec/engine-v1/PROTOCOL-BINDINGS.md). The full delivery sequence is [M9](https://github.com/seanrobertwright/AI-WS-SDK/blob/main/docs/M9-BUILD-PLAN.md).

## Capability is not authority

A discovered capability means only that an endpoint reports or demonstrates support for something.

```text
Capability
  "I can do X"

Authority
  "Praxis permits this actor to do X in this scope"

Approval
  "A verified human approved this material action"

Execution
  "The action was attempted"

Evidence
  "Here is what was observed"

Acceptance
  "The accountable decision accepts the result"
```

Registration and discovery therefore grant nothing. The existing readiness, authority, approval, budget and dispatch gates remain in charge.

For protocol-bound work, discovery also produces a material SHA-256 capability snapshot. Praxis binds that digest into the admitted operation. The MCP adapter re-lists tools, resources and prompts immediately before dispatch and compares the full material metadata—including schemas and annotations supplied by the official SDK—to the reviewed digest. Any drift blocks before the remote call.

## External events are observations

Every common `ProtocolObservation` has:

```typescript
authoritative: false
```

This is a structural rule, not a convention.

An MCP result such as:

```json
{"approved": true}
```

is still just external data.

An A2A task state of `completed` is still just a remote task-state observation.

An AG-UI button event that says “Approve” is still just a UI interaction intent until a trusted host authenticates the human and executes the existing material-bound Praxis control flow.

The common validator rejects attempts to mark protocol observations authoritative.

![MCP, A2A and AG-UI protocol mapping](https://www.lril.ai/images/m9/protocol-mappings.svg)

## MCP direction

`McpOutboundBinding` implements the outbound MCP mapping core in `engine/src/mcp-binding.ts`. `OfficialMcpV2ClientPort` in `engine/src/mcp-official-client.ts` is the host-side wrapper for the official v2 client. It is runtime-loaded deliberately, so Praxis core remains independent of the transport package; deployments that use it install and pin `@modelcontextprotocol/client` in the host.

```text
Praxis attempt
      |
      v
MCP binding
      |
      +-- discover
      +-- tools/resources/prompts
      +-- tool invocation
      +-- optional Tasks extension
      +-- cancellation
      +-- read-back/reconciliation
```

The initial target is MCP 2026-07-28. Long-running tool work will use the Tasks extension where appropriate, while keeping the MCP task handle separate from the Praxis attempt identity.

The current source maps `mcp.tools/call`, `mcp.resources/read` and `mcp.prompts/get`, requires the connected port to report exactly `2026-07-28`, captures returned payloads as evidence, treats tool-level error payloads as external results rather than Praxis decisions, and returns `UNKNOWN` when transport failure leaves the external effect uncertain. A target-specific verifier can provide reconciliation evidence; otherwise reconciliation stays `UNKNOWN`.

Dispatch is also bound to the reviewed discovery snapshot: the operation carries `expectedCapabilityDigest`, the adapter re-discovers the current capability metadata, and a changed schema/capability set or missing selected capability fails closed before invocation.

A remote MCP task finishing does not automatically pass verification or acceptance.

### MCP Tasks durability and recovery

M9.3 adds `McpTaskStore` and `McpTaskCoordinator`.

```text
Praxis attempt
      |
      | tools/call
      v
MCP Tasks server
      |
      +-- immediate result ──────> evidence
      |
      +-- task handle
              |
              v
       durable task binding
       - local operation/attempt
       - work order/run/task
       - binding + endpoint
       - MCP + Tasks revisions
       - reviewed capability digest
       - remote task ID
       - resumable reference
              |
        restart / reconnect
              |
              v
          tasks/get
```

The remote task identity never replaces the Praxis operation/attempt identity. The mapping is persisted before a task-backed dispatch receipt is returned, so restart cannot lose responsibility for an already-created remote task.

When a task reports `input_required`, Praxis emits a non-authoritative control-request observation. Before any input is sent, the host must provide an approval ID and authority-evidence reference bound to the exact observed task revision. That decision is durable. If `tasks/update` has an ambiguous outcome, Praxis records the input decision as `UNKNOWN` and **fences any resend** until the uncertainty is reconciled; the same approval/input payload is not a safe retry token.

Task cancellation is cooperative. A successful remote cancellation request does not prove the underlying business effect was undone. Task completion likewise does not imply verification, effect confirmation or acceptance. Target-specific reconciliation remains independent.

The task's TTL and poll interval are remote protocol hints/backstops. They do not replace Praxis deadlines, budgets or accounting. Recovery also pins endpoint identity, base MCP revision, Tasks extension revision and capability digest; a changed endpoint or protocol cannot silently take ownership of the old remote task.

M9.3 is qualified with both deterministic fake-port recovery tests and pinned official `@modelcontextprotocol/ext-tasks` conformance. The official-extension gate covers create/get/update/cancel, persisted handoff and restart, missing extension negotiation, lost polling responses, remote-task deletion, TTL boundaries, stale terminal timestamps, duplicate/stale input, ambiguous input-update outcomes with retry fencing, ambiguous cancellation, and conservative `UNKNOWN` reconciliation. Remote completion remains evidence rather than Praxis acceptance.

## A2A direction

A2A 1.0 is now qualified in both directions.

Outbound, `A2AClientBinding` plus `OfficialA2AClientPort` discover Agent Cards/skills, pin the reviewed capability digest, create and persist a separate remote Task identity, normalize task/message/artifact observations as non-authoritative evidence, support resubscription after interruption and keep terminal effect reconciliation conservative.

Inbound, `PraxisA2AServerCore`, `A2AInboundStore` and the official AgentExecutor bridge expose selected governed workflows as Praxis Agent Card skills:

```text
External A2A client
       |
       v
Official A2A transport
       |
       v
Praxis Agent Card / server bridge
       |
       +-- authenticate caller
       +-- route requested skill
       +-- authorize exact message/material
       +-- admit new Praxis work identity
       +-- persist remote ↔ local identity
       +-- project status/artifacts
       v
Governed Praxis workflow
```

The official HTTP+JSON conformance test uses `@a2a-js/sdk@1.3.0` and verifies authenticated admission, durable inbound identity mapping, streaming status/artifact projection and A2A terminal-task immutability. A completed remote task cannot be mutated with another message; a later interaction must create new A2A task identity (while conversational context may remain related). A2A completion still does not equal Praxis verification or acceptance.

## AG-UI direction

AG-UI 1.0 is implemented as a read/projection boundary:

```text
Praxis state/events
       |
       v
AG-UI projection
       |
       +-- run lifecycle
       +-- step lifecycle
       +-- messages
       +-- tool/activity events
       +-- state snapshot/delta
       +-- subagent attribution
       |
       v
User interface
```

`engine/src/agui-projection.ts` emits only projection events and has no Praxis mutation/control API. It covers run/step lifecycle, text messages, tool calls/results, state snapshots and checked RFC 6902 deltas, structured activity snapshots/deltas and subagent attribution. The reserved `ag-ui` metadata namespace is rejected, and the emitted event set passes pinned official `@ag-ui/core@1.0.0` schema conformance. Holds, pauses and UNKNOWN conditions stay presentation data rather than new workflow authority.

M9.7 now adds `PraxisAguiControlBridge` as the inbound HITL seam. Praxis presents standard AG-UI interrupts, and the frontend answers with `RunAgentInput.resume[]`. The bridge treats those answers only as control intents: it re-authenticates the caller, revalidates current revision/material, rechecks authorization, consumes the existing single-use Praxis approval challenge for approve/accept, and submits the ordinary durable Praxis control command. `status: "cancelled"` is a no-op. Exact retries derive the same command request ID, and lost responses use the existing `commandResult` lookup. M9.7 is verified: focused tests cover forged identity, stale material, tampered interrupts, expired challenges, changed artifacts, lifecycle controls, duplicate/replayed resumes and lost-response receipt recovery; pinned official `@ag-ui/core@1.0.0` validates the Interrupt, ResumeEntry, RunAgentInput and structured interrupt outcome.

## UNKNOWN stays UNKNOWN

No protocol adapter is allowed to manufacture exactly-once semantics.

The common dispatch boundary supports `DISPATCHED`, `REJECTED` and `UNKNOWN`. Reconciliation supports `CONFIRMED_APPLIED`, `CONFIRMED_NOT_APPLIED` and `UNKNOWN`.

If a connection drops after a possible external effect, Praxis keeps responsibility exposed until reconciliation proves what happened or the existing human-governed recovery path resolves it.

![UNKNOWN and recovery lifecycle](https://www.lril.ai/images/m9/protocol-recovery.svg)

## Protocol identity stays separate

Local Praxis identity never collapses into an external endpoint, agent or task identity. The binding revision, exact protocol/version, endpoint identity and remote operation are correlated explicitly so restart, replacement and protocol upgrades cannot silently rewrite durable responsibility.

![Durable protocol identity and registry](https://www.lril.ai/images/m9/protocol-registry.svg)

## Durable protocol registry

M9.8 adds the verified `ProtocolRegistry` in engine source. It records immutable protocol-binding revisions plus durable operation identity, including protocol/version, endpoint reference, reviewed manifest/capability digests, separate local and remote identities, and evidence references.

Protocol-specific recovery stores remain responsible for their detailed task state. The registry adds one cross-protocol correlation/fencing layer: optimistic registry revisions reject stale writes, and an explicit `SUPERSEDED` fence rejects late remote results after responsibility changes.

Protocol telemetry is intentionally smaller than registry state. M9.8 acceptance verified restart survival, stale-result fencing, schema corruption rejection, registry revision conflicts and telemetry privacy across MCP, A2A and AG-UI. The existing `TelemetryQueue` receives protocol/version, binding revision, material digests, phase/status and hashed identity fields. It does **not** receive credentials, raw endpoint/agent/task identifiers, messages, tool arguments, artifacts, protocol payloads or evidence-reference strings. Telemetry remains best-effort and cannot roll back authoritative registry commits.

## Cross-protocol compatibility campaign

M9.9 verifies the protocol claims through one executable campaign. The checked-in `spec/engine-v1/protocol-conformance.json` manifest pins the compatibility surface and maps every required cross-cutting failure mode to concrete Engine/protocol checks.

Acceptance run `36995139650` ran the real pinned stacks for MCP 2026-07-28 + Tasks, A2A 1.0 and AG-UI 1.0, then composed those results with Praxis control, admission, restart, replacement and protocol-registry tests. All 4 advertised profiles, all 16 required cross-cutting scenarios and all 16 executable checks passed; machine-readable artifact `11221346823` preserves the matrix.

The machine-readable output is `praxis-m9-cross-protocol-report/1`, containing only the advertised protocol/package/version matrix, scenario IDs, check IDs, pass/fail status and durations. It is evidence of tested interoperability, not permission to bypass Praxis authority or acceptance semantics.

## SDK protocol correlation records

M9.10 adds a small source-only `aiws-protocol-correlation/1` record in TypeScript, Rust and Python. It lets application code correlate AIWS/Praxis work/run/operation/attempt identity with the exact binding revision and optional remote identity. The validator requires `authoritative: false`.

This helper is not a protocol client and does not duplicate MCP, A2A or AG-UI schemas. Official protocol SDKs and Praxis adapters remain responsible for transport behavior. Previously built SDK 0.3.0 binary archives retain their original scope; use current repository source for the new helper.

See [M9 completion and compatibility](https://www.lril.ai/engine-m9-completion/) for the exact matrix and [protocol operations and recovery](https://www.lril.ai/engine-protocol-operations/) for operator procedures.

## Current implementation boundary

What exists now:

- common version-pinned binding abstraction plus MCP, A2A and AG-UI protocol-specific source adapters;
- official-stack conformance for MCP `2026-07-28`, MCP Tasks, A2A `1.0` and AG-UI `1.0`;
- durable protocol-specific task stores plus the cross-protocol `ProtocolRegistry`;
- authenticated AG-UI human-control intent routed through existing Praxis controls;
- privacy-preserving protocol telemetry and stale-result fencing;
- M9.9 unified compatibility campaign with 4/4 profiles, 16/16 required scenarios and 16/16 executable checks;
- M9.10 source-only cross-language correlation records, runnable examples, operator runbook, completion record and SVG visual package.

What remains outside M9:

- M8 platform/release/production qualification;
- cross-machine Praxis recovery;
- exactly-once external-effect guarantees;
- silent compatibility with protocol/package versions not listed in the verified matrix.

Do not infer those capabilities from the presence of a protocol adapter. Use [M9 completion and compatibility](https://www.lril.ai/engine-m9-completion/) as the compatibility boundary.

## M8 remains independent

M9 is a feature/interoperability milestone running beside M8. Protocol functionality does not qualify the Windows/macOS/Linux matrix, installers, production containment, PostgreSQL, constrained hosts or endurance claims. Those gates remain owned by M8.


---

# Protocol operations and recovery runbook

> **Praxis naming:** This page documents **Praxis**, the AIWS workflow engine. Protocol adapters provide connectivity; AIWS/Praxis remains authoritative for identity, admission, budgets, approval, verification, acceptance and recovery.

This runbook is for operators and host implementers using the M9 source profile. It assumes the exact compatibility versions listed in [M9 completion and compatibility](https://www.lril.ai/engine-m9-completion/). It does **not** convert an M9 protocol pass into an M8 production/platform qualification claim.

## Normal operating sequence

Before any external effect:

1. Resolve the current Praxis work order, run, task and attempt.
2. Resolve the installed binding ID and exact binding revision from the durable `ProtocolRegistry`.
3. Confirm protocol/version and endpoint identity match the reviewed binding.
4. Re-observe the selected capability material where the adapter requires it and compare the current capability digest with the reviewed digest.
5. Re-check current authority, policy, applicable human approval, budget/limits and assignment/handoff state.
6. Resolve host-owned credentials outside durable workflow material.
7. Dispatch the already-admitted `AdmittedProtocolOperation`.
8. Persist remote identity/operation/task mapping before returning a task-backed receipt.
9. Treat all remote messages, task states, artifacts and UI events as evidence.
10. Reconcile ambiguous effects before retrying.
11. Run independent verification and the applicable acceptance decision after the effect is known.

![Praxis protocol architecture](https://www.lril.ai/images/m9/protocol-architecture.svg)

## Version or capability drift

**Signals:** `MCP_PROTOCOL_VERSION_MISMATCH`, `A2A_PROTOCOL_VERSION_MISMATCH`, interface mismatch, capability digest change, missing reviewed capability, changed Agent Card, changed endpoint identity.

**Procedure:**

1. Stop before remote dispatch. Do not silently migrate an in-flight operation.
2. Preserve the old binding revision and any active operation mapping.
3. Discover the new protocol/capability material as a new candidate binding revision.
4. Re-run readiness/review for the changed capability material.
5. Obtain any required fresh approval.
6. Admit new work only after the new binding revision is selected explicitly.
7. If an old remote operation is still active, keep it under its old binding revision until reconciled or explicitly fenced.

## Response loss or remote endpoint outage

**Signals:** timeout after possible dispatch, socket loss, remote 5xx/unavailability, missing response after a request may have taken effect.

**Procedure:**

1. Record/retain the outcome as `UNKNOWN`; do not translate transport failure into rejection or success.
2. Keep the original operation/attempt responsibility and budget exposure.
3. Do not create a new request ID merely because the response was lost.
4. Use the protocol-specific recovery path:
   - MCP Tasks: durable task ID/reference → `tasks/get`/resume.
   - A2A: durable remote task/context → get/resubscribe.
   - synchronous MCP: target-specific read-back when available.
   - Praxis control command: durable `commandResult` lookup using the same request identity.
5. If target truth cannot be established, remain held for human intervention.

![UNKNOWN and recovery lifecycle](https://www.lril.ai/images/m9/protocol-recovery.svg)

## Cancellation ambiguity

Cancellation is cooperative for MCP/A2A external work.

1. Send cancellation only for the exact bound remote operation/task.
2. Persist the cancellation receipt/evidence.
3. A remote `canceled` result does not prove the underlying business effect rolled back.
4. If the effect can still have occurred, keep reconciliation `UNKNOWN`.
5. Release responsibility only after target-specific evidence confirms the effect state.

AG-UI `status: "cancelled"` is different: it means the human dismissed/cancelled the UI interrupt. M9.7 treats that resume entry as a non-authoritative no-op; it does not mutate Praxis work.

## Restart before or after dispatch

On process restart:

1. Reopen Praxis durable state and acquire the new engine epoch.
2. Reopen the M9.8 `ProtocolRegistry`.
3. Reopen protocol-specific stores (`McpTaskStore`, `A2ATaskStore`, `A2AInboundStore`) where applicable.
4. Recover active operations by **local operation identity**, then compare their saved binding revision, endpoint, protocol version, capability digest and remote identity.
5. Refuse silent ownership transfer if any pinned material changed.
6. Resume polling/subscription only for the saved remote identity.
7. If dispatch was authorized but external truth is uncertain, enter/retain the existing reconciliation hold instead of treating the work as pre-dispatch.

## Stale remote result after reassignment

A late result from a superseded agent, endpoint or operation must not settle current work.

1. Put the affected responsibility into the existing replacement/reconciliation hold when required.
2. Mark the cross-protocol registry operation `SUPERSEDED` when responsibility has changed.
3. Reject later observations against the fenced registry operation.
4. Preserve late evidence for audit/reconciliation; do not discard it.
5. Never copy its result into the replacement attempt merely because the payload looks successful.
6. Verification must reference the current assignment/attempt and current artifact material.

![Durable protocol identity and evidence](https://www.lril.ai/images/m9/protocol-registry.svg)

## Credential revocation or authentication failure

1. Fail closed. Do not fall back to a weaker credential or anonymous mode.
2. Keep credentials out of plans, registry rows, evidence summaries, telemetry and AG-UI events.
3. Reauthenticate through the configured host/provider flow.
4. Re-check authorization after reauthentication.
5. If the external request may already have been sent before revocation was observed, preserve `UNKNOWN` and reconcile.

For AG-UI human control, a resume payload never supplies trusted identity. `PraxisAguiControlBridge` authenticates the host credential/session again and checks current authorization before command commit.

## Malformed or untrusted protocol payload

1. Reject closed-record/schema violations at the protocol boundary.
2. Do not accept unknown fields that would change authority, approval, verification, acceptance or identity.
3. Sanitize transport exceptions before durable evidence/telemetry.
4. Treat remote `approved`, `verified`, `accepted` or similar payload values as ordinary external data.
5. Never set `ProtocolObservation.authoritative` to true.

## Artifact mismatch

When a remote result refers to or produces material that differs from the reviewed/validated artifact:

1. Do not accept the deliverable.
2. Capture the current artifact bytes/digest.
3. Invalidate stale verification/acceptance material.
4. Re-run validation/verification against the current artifact.
5. Require fresh acceptance when policy/workflow rules require it.

The static control API and AG-UI human-control bridge both fail closed when the acceptance artifact changes after the human reviewed it.

## Incident evidence checklist

Retain enough evidence to answer:

- Which Praxis work/run/task/attempt and operation were responsible?
- Which binding ID and **binding revision** were selected?
- Which exact protocol/version and endpoint identity were pinned?
- Which manifest/capability digest was reviewed?
- Which remote identity and remote operation/task were bound?
- What dispatch evidence exists?
- What observations were received, in what order?
- Did cancellation occur, and what did it actually prove?
- Was reconciliation `CONFIRMED_APPLIED`, `CONFIRMED_NOT_APPLIED` or still `UNKNOWN`?
- Which verification evidence and assessor were current?
- Which human/policy authority made the final acceptance decision?

The telemetry pipeline intentionally cannot answer every item because it exports only an allowlisted, privacy-preserving projection. Use the durable registry, protocol-specific store, workflow journal and artifact store for investigation.

## Operator quick table

| Situation | Required action | Never do |
|---|---|---|
| Version/capability drift | New reviewed binding revision | Silent upgrade |
| Response lost after possible effect | Preserve `UNKNOWN`, read back | Blind retry |
| Remote task completed | Capture as evidence, verify independently | Mark accepted |
| Cancellation acknowledged | Reconcile actual effect | Assume rollback |
| Endpoint/agent changed after restart | Fence old mapping | Rebind silently |
| Credential revoked | Reauthenticate + authorize | Fall back to weaker auth |
| AG-UI resume arrives | Authenticate, bind current material, use control API | Treat click as approval |
| Artifact changed | Reverify and reaccept | Reuse stale acceptance |
| Late result after replacement | Preserve evidence, reject current settlement | Attach to new attempt |

For the executable evidence behind these procedures, see [M9 completion and compatibility](https://www.lril.ai/engine-m9-completion/) and the repository `spec/engine-v1/protocol-conformance.json`.


---

# M9 protocol interoperability completion

> **M9 status:** **Complete.** M9.1–M9.10 are accepted for the scoped source interoperability profile. M8 platform/release qualification remains independent and active.

M9 gives Praxis tested protocol boundaries for capabilities (MCP), agents (A2A) and humans (AG-UI) while preserving the same AIWS authority, budget, approval, UNKNOWN/reconciliation, verification and acceptance semantics.

![M9 protocol architecture](https://www.lril.ai/images/m9/protocol-architecture.svg)

## Exact compatibility matrix

Only the versions below are advertised by the M9 source profile.

| Profile | Protocol revision | Official compatibility packages used in M9.9 | Verified path |
|---|---|---|---|
| MCP synchronous | `2026-07-28` | `@modelcontextprotocol/client@2.2.0`, `@modelcontextprotocol/server@2.2.0` | discovery, tools, resources, prompts, auth, timeout/cancel ambiguity, capability pinning |
| MCP Tasks | base `2026-07-28`; Tasks schema `2026-07-28` | `@modelcontextprotocol/ext-tasks@0.1.0` | task creation/get/update/cancel, durable restart/resume, input fencing, ambiguity |
| A2A | `1.0` | `@a2a-js/sdk@1.3.0` | outbound client + inbound Praxis Agent Card/server over HTTP+JSON, streaming/resubscribe |
| AG-UI | `1.0` | `@ag-ui/core@1.0.0` | projection schemas plus structured human interrupt/resume control intent |

M9.9 run `36995139650` passed **4/4 profiles, 16/16 required scenarios and 16/16 executable checks**. Machine-readable evidence artifact `11221346823` is named `m9-cross-protocol-conformance`.

A dependency update does not automatically expand this matrix. A new protocol revision/package combination requires an explicit binding revision, review and conformance evidence before it becomes an advertised compatibility profile.

## M9 slice record

| Slice | Result |
|---|---|
| M9.1 | Common version-pinned protocol-binding abstraction and non-authoritative observation boundary |
| M9.2 | MCP `2026-07-28` synchronous outbound profile |
| M9.3 | MCP Tasks durable identity/restart/input/cancellation/reconciliation |
| M9.4 | A2A `1.0` outbound client/task profile |
| M9.5 | A2A Praxis server / Agent Card profile |
| M9.6 | AG-UI `1.0` event projection |
| M9.7 | AG-UI authenticated, material-bound human control intent |
| M9.8 | Durable cross-protocol registry and privacy-preserving observability |
| M9.9 | Unified official-stack conformance campaign |
| M9.10 | SDK correlation helpers, examples, runbook, compatibility docs, release notes and visual package |

## SDK source additions

M9.10 adds the same small **protocol correlation** helper to TypeScript, Rust and Python source:

- `ProtocolBindingRef`
- `ProtocolOperationRef`
- `ProtocolCorrelation`
- a validator/builder that requires `authoritative: false`

These records are intentionally not MCP, A2A or AG-UI clients. They let an application correlate an AIWS mission/run/operation/attempt with the exact Praxis binding revision and remote identity while official protocol SDKs remain responsible for protocol wire behavior.

The correlation record has no authority, dispatch, cancellation, verification, reconciliation or acceptance method. It cannot turn protocol metadata into permission.

## Source vs released SDK vs qualified deployment

| Surface | Status after M9 |
|---|---|
| Current repository source | Includes all M9 engine adapters/registry/conformance plus the new cross-language protocol-correlation helper |
| Previously built SDK `0.3.0` binary archives | Retain their original finite-v1 scope; do **not** assume the new source-only protocol helper is present |
| Praxis protocol adapters | TypeScript engine-source functionality; not duplicated as native Rust/Python protocol stacks |
| Official protocol SDK dependencies | Host-owned, pinned only in the tested adapter/conformance paths |
| Production/platform claims | Still governed by M8; M9 does not certify macOS/Linux/Windows production deployment, PostgreSQL, endurance, containment or cross-machine failover |

## Visual architecture package

![Protocol mapping across MCP, A2A and AG-UI](https://www.lril.ai/images/m9/protocol-mappings.svg)

![Durable protocol identity and registry](https://www.lril.ai/images/m9/protocol-registry.svg)

![UNKNOWN and recovery lifecycle](https://www.lril.ai/images/m9/protocol-recovery.svg)

The diagrams are scalable SVG assets checked into `public/images/m9/`. They are documentation artifacts, not executable protocol definitions.

## Definition-of-done evidence

M9.1–M9.10 satisfy the scoped source interoperability requirements below. Final acceptance is backed by the composed evidence chain recorded here:

- **Real paths:** MCP, A2A and AG-UI each have pinned official-stack test paths.
- **Explicit compatibility:** the matrix above is closed and evidence-backed.
- **Restart-safe identity:** M9.3/M9.4/M9.5 stores plus M9.8 registry retain separate local and remote identity across restart.
- **UNKNOWN semantics:** transport ambiguity, cancellation and external completion never manufacture exactly-once success.
- **Human approval:** AG-UI resumes pass through authenticated, current-material Praxis controls; UI events are not approval records.
- **Capability ≠ authority:** discovery remains descriptive and material-digest pinned.
- **M8 independence:** release/platform qualification remains a separate milestone.
- **Documentation boundary:** source-only additions, SDK 0.3.0 binaries and production qualification are stated separately.

### Closure evidence

- M9.10 source commit `12192c344be3f060e03604c9774d3e02eed9613e` passed cross-protocol run `36998149587` with **4/4 profiles, 16/16 scenarios and 16/16 checks**; artifact `11222721873`.
- Verification run `36998436863` on `7c437db57eb97598de367383aae6c34f62b53e74` passed Python tests/build, TypeScript package tests, Rust tests, shared parity/limits checks and the full engine job. The only failing step was `docs:typecheck`, which identified literal widening in the new TypeScript guide example.
- Commit `9328ef94cd57e944230c657f174b308addbb11cd` fixes that example by declaring the contract as `Contract` and expands M9 conformance workflow path coverage without changing protocol runtime behavior.
- The Vercel status check for `9328ef94cd57e944230c657f174b308addbb11cd` is successful, confirming the corrected repository state passes the documentation build/deployment path.
- GitHub-hosted runs `37002244314` and `37002243969` were retried twice but failed before runner allocation (`runner_id: 0`, zero steps). Because no test command executed, those runs are recorded as infrastructure unavailability rather than regression evidence.

M8 remains the sole release/platform/production qualification track; closing M9 does not expand those claims.

## Release notes

### Added

- MCP `2026-07-28` synchronous adapter and official v2 connector.
- MCP Tasks extension durability/recovery and official requester adapter.
- A2A `1.0` client and Praxis server/Agent Card profiles.
- AG-UI `1.0` projection plus authenticated human-control intent bridge.
- Durable `ProtocolRegistry` with immutable binding revisions, evidence correlation and stale-result fencing.
- Privacy-preserving protocol telemetry projection.
- Unified cross-protocol compatibility manifest and M9.9 campaign.
- Cross-language protocol-correlation records and runnable TypeScript/Rust/Python example.
- Operator/recovery runbook and high-resolution SVG architecture package.

### Preserved

- AIWS/Praxis remains the authority boundary.
- Remote capability/task/UI state is evidence, not authorization or acceptance.
- UNKNOWN remains explicit until target-specific reconciliation proves otherwise.
- Existing `engine/*`, `/engine/...` and `aiws-engine/*` compatibility-facing identifiers remain unchanged.

### Not claimed

- Exactly-once external effects.
- Silent protocol-version upgrades.
- Cross-machine Praxis recovery.
- Production support for every OS/deployment/database target.
- Inclusion of M9 source APIs in older SDK 0.3.0 binary artifacts.

Use [protocol operations and recovery](https://www.lril.ai/engine-protocol-operations/) for operator procedures and [Praxis protocol interoperability](https://www.lril.ai/engine-protocol-interoperability/) for the architecture and protocol-specific behavior.
