# Lifeloop Lifecycle Contract

Lifeloop is the provider-neutral lifecycle normalization substrate. It turns
harness-specific hooks, launchers, reference adapters, and telemetry into one
small lifecycle surface that clients can consume without inheriting each
other's product semantics.

CCD is the first client. Recursive Language Models (RLM) are the known
second-client design pressure. This contract therefore defines only lifecycle
normalization: event vocabulary, adapter manifests, capability negotiation,
opaque payload delivery, receipts, degradation events, failure classes, and
retry classes.

## Goals

- Normalize lifecycle timing and delivery across harnesses.
- Make adapter capability gaps explicit and machine-readable.
- Carry opaque client payloads without interpreting their meaning.
- Emit bounded receipts that clients can project into their own state.
- Keep lifecycle failure and retry posture consistent across adapters.
- Preserve room for non-CCD clients such as RLM without importing their
  semantics.

## Non-Goals

- No CCD continuity product semantics inside Lifeloop.
- No recursive inference semantics inside Lifeloop.
- No tool, skill, model, provider, prompt-cache, approval, or memory
  abstraction unless it is directly required to classify a lifecycle event or
  deliver a lifecycle payload.
- No public crate, standalone repository, native Lifeloop CLI, or command
  rename in this contract.

## Ownership Boundary

Lifeloop owns lifecycle normalization. Clients own the meaning of actions they
take after receiving normalized lifecycle facts.

Lifeloop-owned concepts:

- adapter identity and aliases
- integration mode metadata
- lifecycle event names
- lifecycle capability manifests
- payload placement negotiation
- delivery facts and receipt shape
- context-pressure and activity observations when they affect lifecycle timing
- lifecycle failure classes and retry classes
- optional receipt-ledger, payload-store, and negotiation-cache capabilities
  when an adapter or Lifeloop deployment explicitly provides them

Client-owned concepts:

- any continuity, recovery, task, trajectory, memory, approval, policy, or
  recursive-inference model
- payload generation, priority, semantic interpretation, and persistence
- state stores that consume receipts
- product-specific escalation or fallback behavior

Lifeloop may transport a client payload with client-specific format labels,
but those strings are opaque routing facts. Lifeloop must not parse payload
formats or infer client policy from them.

## Boundary With Existing Specs

- `machine-runtime-api` remains the control-plane operation contract. Lifeloop
  narrows lifecycle normalization below it and does not replace runtime
  commands.
- `runtime-state-contract` remains the CCD-local state ownership contract.
  Lifeloop receipts are evidence and correlation data, not mutable CCD state.
- `mid-session-context-refresh` remains the CCD policy for when to refresh,
  checkpoint, or stop. Lifeloop only reports lifecycle pressure evidence.
- `lifeloop-ccd-client` is downstream of this contract. It maps CCD ceremonies
  to the neutral lifecycle vocabulary and owns CCD-specific reactions.

Call direction for the first CCD extraction is:

1. A harness transport invokes Lifeloop or a compatibility command backed by
   Lifeloop.
2. Lifeloop normalizes the lifecycle fact, evaluates adapter capabilities, and
   returns receipts.
3. The client reacts to those receipts and lifecycle facts through its own
   state and policy APIs.

Lifeloop must not call CCD continuity state modules directly. CCD may call
Lifeloop from inside a synchronous compatibility command and then produce the
same compatibility response the harness already expects.

## Lifecycle Event Vocabulary

Event names are stable lowercase dot-separated strings.

| Event | Meaning |
|---|---|
| `session.starting` | The top-level harness lifecycle is about to start, attach, or resume. |
| `session.started` | The top-level harness lifecycle is live enough for a client to bind correlation state. |
| `frame.opening` | A prompt, turn, recursive frame, or equivalent execution frame is about to receive client payloads. |
| `frame.opened` | The frame accepted delivery or explicitly skipped delivery. |
| `context.pressure_observed` | Lifecycle-relevant budget, compaction, idle-reset, or context-pressure evidence was observed. |
| `context.compacted` | A harness completed a context compaction or equivalent context-rewrite lifecycle moment. |
| `frame.ending` | A frame is about to close, finish, fail, or be interrupted. |
| `frame.ended` | A frame has closed, failed, or been abandoned. |
| `session.ending` | The top-level harness lifecycle is about to close or needs close-out coordination. |
| `session.ended` | The top-level harness lifecycle has closed or been abandoned. |
| `supervisor.tick` | A supervisor, scheduler, watchdog, or bridge is polling an active lifecycle. |
| `capability.degraded` | A previously negotiated lifecycle capability changed support state mid-session. |
| `receipt.emitted` | A lifecycle receipt was emitted and is available for client projection. |
| `receipt.gap_detected` | Lifeloop observed a receipt sequence gap it cannot reconstruct in a harness sequence or declared receipt ledger. |

Rules:

- `session.*` is the stable Lifeloop wire name for a top-level harness
  lifecycle. It does not require a client to expose its own concept named
  "session"; clients map the event to their own state machines.
- `frame.*` events describe one execution or inference frame inside a
  top-level lifecycle.
- A simple chat or coding harness may have one frame per prompt turn.
- A recursive client may create nested frames under the same top-level
  lifecycle.
- `context.pressure_observed` is not a checkpoint, compaction, or policy
  action. It is the neutral observation that a client may react to.
- `context.compacted` reports that the harness context was rewritten or
  compacted. It is still only a lifecycle fact; clients decide what that means.
- Lifeloop events do not grant write authority or policy approval.
- Lifeloop dispatch is not inherently asynchronous. A compatibility transport
  may observe an event, run the client reaction, and return a synchronous
  response in one invocation.

The `session.*` vocabulary decision is recorded in
[`docs/decisions/lifecycle-session-vocabulary.md`](../../decisions/lifecycle-session-vocabulary.md).

## Adapter Manifest

Every adapter exposes a manifest before client negotiation. The manifest is an
honest report of lifecycle behavior, not a lowest-common-denominator API.

Illustrative shape:

```json
{
  "contract_version": "lifeloop.v0.2",
  "adapter_id": "codex",
  "adapter_version": "0.1.0",
  "display_name": "Codex",
  "role": "primary_worker",
  "integration_modes": ["native_hook", "manual_skill"],
  "lifecycle_events": {
    "session.starting": { "support": "native", "modes": ["native_hook"] },
    "frame.opening":    { "support": "native", "modes": ["native_hook"] },
    "context.pressure_observed": { "support": "native", "modes": ["native_hook"] },
    "context.compacted": { "support": "native", "modes": ["native_hook"] },
    "supervisor.tick":  { "support": "unavailable", "modes": [] }
  },
  "placement": {
    "pre_session":         { "support": "native",      "max_bytes": 8192 },
    "pre_frame_leading":   { "support": "native",      "max_bytes": 8192 },
    "pre_frame_trailing":  { "support": "unavailable" },
    "tool_result":         { "support": "unavailable" },
    "manual_operator":     { "support": "manual" }
  },
  "context_pressure": {
    "support": "native",
    "evidence": "Codex CLI 0.129 exposes PreCompact before context pressure handling and PostCompact after context compacts"
  },
  "receipts": {
    "native": false,
    "lifeloop_synthesized": true,
    "receipt_ledger": "unavailable"
  },
  "session_identity": {
    "harness_session_id": "native",
    "harness_run_id":     "synthesized",
    "harness_task_id":    "unavailable"
  },
  "renewal": {
    "reset": {
      "native": "unavailable",
      "wrapper_mediated": "synthesized",
      "manual": "manual"
    },
    "continuation": {
      "observation": "native",
      "payload_delivery": "synthesized"
    },
    "profiles": ["ccd-renewal"],
    "evidence": "Codex Stop block-as-continuation evidence plus subprocess client callback evidence proves wrapper-mediated reset prepare and continuation payload delivery"
  },
  "failure_modes": ["transport_error", "payload_too_large"],
  "known_degradations": []
}
```

### Required manifest fields

| Field | Meaning |
|---|---|
| `contract_version` | The Lifeloop contract version this manifest targets, e.g. `lifeloop.v0.2`. |
| `adapter_id` | Stable wire identifier (e.g. `codex`, `claude`). Must round-trip through any `adapter_id` field elsewhere on the wire. |
| `adapter_version` | The adapter's own version string. Independent of the contract version so adapters can iterate without bumping the contract. |
| `display_name` | Human-facing label. |
| `role` | Single [adapter role](#adapter-roles) the manifest claims. Adapters that fill more than one role expose more than one manifest. |
| `integration_modes` | Non-empty list of integration modes the adapter supports. |
| `lifecycle_events` | Map of [lifecycle event names](#lifecycle-event-vocabulary) to per-event support claims. |
| `placement` | Map of [manifest placement classes](#manifest-placement-classes) to per-class support claims. |
| `context_pressure` | Single capability claim describing how the adapter surfaces `context.pressure_observed` evidence. |
| `receipts` | Capability claim describing receipt emission and ledger support. |

### Optional manifest fields

| Field | Meaning |
|---|---|
| `session_identity` | Per-id support claims for `harness_session_id`, `harness_run_id`, `harness_task_id`. |
| `session_rename` | Capability claim for the adapter's session-rename surface. Omitted when the adapter has no rename concept. |
| `renewal` | Capability claim for reset/continuation renewal. Omitted when the adapter has not evaluated renewal mechanics. |
| `approval_surface` | Capability claim for operator approval/intervention surfaces. |
| `failure_modes` | List of [failure classes](#failure-classes) the adapter is known to surface. Diagnostic; absence does not mean other failure classes cannot occur. |
| `telemetry_sources` | List of telemetry source descriptors the adapter exposes for lifecycle evidence. |
| `known_degradations` | Pre-declared capability degradations the adapter ships with (e.g. a previously native capability now unavailable in a specific build). |

### Renewal/reset capability

The optional `renewal` manifest field describes whether an adapter can prove a
safe reset/continuation lifecycle path. It is intentionally not a CCD renewal
lease, continuation token, thread binding, or fail-closed policy surface.
Clients decide whether a renewal is allowed; Lifeloop only reports adapter
capability and emits delivery receipts for lifecycle facts.

Shape:

```json
{
  "reset": {
    "native": "unavailable",
    "wrapper_mediated": "partial",
    "manual": "manual"
  },
  "continuation": {
    "observation": "partial",
    "payload_delivery": "unavailable"
  },
  "profiles": [],
  "evidence": "operator wrapper can observe continuation, but cannot inject payloads"
}
```

Fields:

| Field | Meaning |
|---|---|
| `reset.native` | The harness exposes a direct reset or renewal boundary. |
| `reset.wrapper_mediated` | A launcher, wrapper, reference adapter, or extension can mediate reset. |
| `reset.manual` | Reset depends on an operator/manual surface. |
| `continuation.observation` | The adapter can prove that the post-reset continuation boundary happened. |
| `continuation.payload_delivery` | The adapter can carry client-provided continuation facts across the boundary. |
| `profiles` | Optional non-empty host integration profile ids required for this claim. Absent or empty means the claim is profile-independent. |
| `evidence` | Optional human-readable evidence. It must not contain client-owned continuation tokens or policy state. |

All reset fields set to `unavailable` means the manifest declares no safe reset
path. `continuation.observation` and `continuation.payload_delivery` are
separate so observation-only integrations cannot be mistaken for integrations
that can deliver opaque client continuation payloads.

The first shipped positive claim is Codex. The current greenfield CCD flow
prepares through `ccd renewal prepare --adapter codex --reset-path wrapper`,
stores CCD-owned renewal state through `lifeloop continuation put` under the
`renewal-state` key, and consumes that blob on the next top-level lifecycle start
inside `ccd lifeloop client`. The blob remains client-owned; Lifeloop records
only lifecycle delivery evidence and opaque continuation-store operations.

### Renewal automation status

When a Lifeloop integration mediates reset/continuation renewal, it may expose a
token-safe status document. The status is lifecycle evidence. It is not a
client-owned lease, continuation token, or authorization to retry.

Renewal automation states:

| State | Meaning |
|---|---|
| `not_attempted` | Lifeloop has no durable renewal decision for this adapter/client pair. |
| `no_renewal` | A lifecycle boundary was observed and did not request renewal. |
| `prepare_started` | A renewal boundary was observed and reset prepare has started. |
| `receipt_written` | Lifeloop wrote reset-prepare receipt evidence. |
| `lease_created` | The client reported a renewal lease or equivalent prepare result. |
| `pending_continuation` | A continuation token is stored out of band for the next lifecycle start. |
| `fulfilled` | The continuation was consumed and the expected lifecycle binding was preserved. |
| `failed` | Renewal automation failed with a Lifeloop failure/retry classification. |
| `expired` | A pending renewal passed its lifecycle expiry window before fulfillment. |

Status document fields:

| Field | Requirement | Meaning |
|---|---|---|
| `schema_version` | Required | Lifeloop contract version. |
| `state` | Required | One renewal automation state. |
| `client_id` | Required | Client label that owns the renewal semantics. |
| `adapter_id` | Required | Harness adapter id. |
| `updated_at_epoch_s` | Required | Last Lifeloop observation time. |
| `pending_token_present` | Required | Whether a token-bearing pending file is currently present. The token itself must not appear in this document. |
| `reset_path` | Optional | Reset path label such as `native`, `wrapper`, or `manual`. |
| `thread_id` | Optional | Client-owned continuity binding id when the client reports one. |
| `renewal_lease_id` | Optional | Opaque client lease id when the client reports one. |
| `prepared_at_epoch_s` | Optional | Time the prepare result was recorded. |
| `fulfilled_at_epoch_s` | Optional | Time the continuation was fulfilled. |
| `pending_path` | Optional | Path to the token-bearing pending file, for operators only. |
| `reset_prepare_receipt_path` | Optional | Path to Lifeloop reset-prepare receipt evidence. |
| `failure_class` | Optional | Required when `state` is `failed` or `expired`; absent otherwise. |
| `retry_class` | Optional | Required when `state` is `failed` or `expired`; absent otherwise. |
| `message` | Optional | Human-readable diagnostic summary. |

The status document must never contain continuation tokens, opaque payload
bodies, or client policy decisions. If the status says `pending_continuation`
but the pending-token file is missing, Lifeloop must report `failed` with
`state_conflict`/`retry_after_reread` rather than inventing a continuation.

### Support states

Support values describe how strongly the adapter satisfies a capability claim.

| Value | Meaning |
|---|---|
| `native` | The harness exposes the behavior directly. |
| `synthesized` | Lifeloop can synthesize the behavior from another stable signal (telemetry, log scraping, derived hooks). |
| `manual` | The behavior depends on an operator or manual wrapper. |
| `partial` | The adapter exposes an incomplete or lossy form of the behavior. Clients must explicitly accept partial support before Lifeloop treats it as satisfied; otherwise it degrades. |
| `unavailable` | The adapter cannot provide the behavior. |

Note: pre-issue-#6 drafts of this section listed `simulated` and `inferred` as
separate values. `synthesized` replaces `simulated` (clearer about the
direction of derivation); `inferred` is folded into `partial` (telemetry-derived
behavior is partial behavior).

### Manifest placement classes

The manifest declares placement support using a trust-neutral, lifecycle-timing
vocabulary. These classes describe *where in the lifecycle* an adapter accepts
payload placement, independent of whether that placement is comparable to a
"developer" or "system" frame in any specific harness's UI.

| Class | Meaning |
|---|---|
| `pre_session` | Before any frame opens; e.g. session-init context. |
| `pre_frame_leading` | At the leading edge of a frame, before any user prompt or task input arrives. |
| `pre_frame_trailing` | At the trailing edge of a frame, after the prompt/input but before the model executes. |
| `tool_result` | Inside a tool-result envelope returned to the model. |
| `manual_operator` | Through an operator or manual surface (skill, command, wrapper). |

These classes are distinct from the [payload placement classes](#opaque-payload-envelope)
the runtime uses for routing concrete payloads on `acceptable_placements`. The
manifest placement vocabulary describes capability claims; the payload placement
vocabulary describes routing requests. A future revision may unify them; the
current contract keeps them separate.

### Integration modes

- `manual_skill`
- `launcher_wrapper`
- `native_hook`
- `reference_adapter`
- `telemetry_only`

`telemetry_only` is for integrations that can observe lifecycle evidence from
logs, activity files, or other telemetry, but cannot inject payloads or control
the harness lifecycle directly.

### Adapter roles

- `primary_worker`
- `worker`
- `supervisor`
- `observer`

### Manifest registry

Lifeloop ships a built-in manifest registry that lists adapters with shipped
support. The registry distinguishes:

- **v1 conformance adapters** — Codex and Claude have manifests whose claims
  must be backed by extracted code paths (asset rendering, telemetry,
  placement support). Registry tests verify each capability claim against the
  underlying implementation.
- **pre-conformance adapters** — Hermes, OpenClaw, Gemini, OpenCode may
  ship initial manifests whose claims are not yet test-verified end-to-end.
  These are useful for client negotiation against partially-supported adapters
  but should not be assumed to be exhaustive.

Registry lookup is by `adapter_id`; adapters do not import client modules.

## Capability Negotiation

A client request declares each lifecycle capability with a requirement level.

Requirement levels:

| Level | Meaning |
|---|---|
| `required` | Lifeloop must refuse before dispatch when the adapter cannot satisfy the requested support. |
| `preferred` | Lifeloop may continue with a degraded receipt and warning. |
| `optional` | Lifeloop may omit the capability without warning unless the adapter previously promised it and then degraded. |

Negotiation outcomes:

| Outcome | Meaning |
|---|---|
| `satisfied` | The adapter meets the requested capability and support level. |
| `degraded` | Lifeloop can proceed, but support is weaker than requested. |
| `unsupported` | The adapter cannot provide the capability. |
| `requires_operator` | Manual setup or approval is required before dispatch. |

Clients negotiate lifecycle capabilities, payload placements, identity
correlation, context-pressure observations, and receipt behavior before using
an adapter. Lifeloop owns the negotiation result. Clients own how strict they
are about the result.

## Negotiation Timing And Transport

Negotiation is a Lifeloop operation, but it does not require a new public CLI
command in the first extraction.

Rules:

- Every adapter must expose a manifest before an operation is dispatched.
- Lifeloop evaluates client requirements against the manifest before every
  lifecycle operation that can mutate client-visible state or deliver payloads.
- A synchronous compatibility invocation may cache a negotiation result only
  inside that invocation.
- Cross-invocation negotiation caching requires an explicit manifest capability
  and a Lifeloop-owned or adapter-owned cache boundary.
- A session implementation that advertises cross-invocation caching must
  recheck cached capabilities when telemetry or adapter state can degrade
  mid-session.
- For `host-hook` compatibility, negotiation happens inside the existing
  synchronous command invocation. Unsupported required capabilities fail before
  client-owned state mutation.
- For `host apply` compatibility, install/apply remains a CLI compatibility
  command, but the installed assets are lifecycle integration assets whose
  semantic target is Lifeloop normalization.
- A later native Lifeloop transport may expose explicit preflight negotiation,
  but it must preserve these same requirement levels and outcomes.

## Mid-Session Degradation

If a capability changes after negotiation, Lifeloop emits
`capability.degraded`.

Required degradation fields:

```json
{
  "event": "capability.degraded",
  "capability": "context_pressure",
  "previous_support": "native",
  "current_support": "unavailable",
  "observed_at_epoch_s": 1778100000,
  "evidence": "telemetry file missing for two consecutive polls",
  "retry_class": "retry_after_reconfigure"
}
```

Rules:

- Degradation is lifecycle evidence, not a client decision.
- Lifeloop must name the degraded capability and support transition.
- The same degradation may make one client stop and another continue.
- Lifeloop must not silently downgrade a `required` capability after dispatch;
  it must emit a receipt and a degradation event.

## Opaque Payload Envelope

Payloads are client-owned data delivered at lifecycle moments. Lifeloop handles
transport and placement only.

```json
{
  "schema_version": 1,
  "payload_id": "pay_01K...",
  "client_id": "example-client",
  "payload_kind": "instruction_frame",
  "format": "client-defined",
  "content_encoding": "utf8",
  "body": "opaque client payload",
  "body_ref": null,
  "byte_size": 1200,
  "content_digest": "sha256:...",
  "acceptable_placements": [
    {
      "placement": "developer_equivalent_frame",
      "requirement": "preferred"
    },
    {
      "placement": "pre_prompt_frame",
      "requirement": "required"
    }
  ],
  "idempotency_key": "idem_01K...",
  "expires_at_epoch_s": null,
  "redaction": "none",
  "metadata": {}
}
```

Rules:

- `body` and `body_ref` are mutually exclusive.
- Lifeloop validates size, digest, encoding, expiration, and placement support.
- Lifeloop does not parse `body` beyond transport-safe encoding checks.
- `metadata` is for client correlation only. Lifeloop may echo it but must not
  assign semantics to unknown keys.
- Payload priority is not a Lifeloop concept. Clients express placement needs
  through acceptable placements and requirement levels.

For harnesses whose hook protocol surfaces a single `additionalContext` slot
per event (e.g. Claude Code, Codex), Lifeloop renders a transport envelope of
the form `{"payloads": [...]}` carrying one object per eligible payload, in
input order, each containing `payload_id`, `payload_kind`, and either `body`
(as a verbatim JSON string) or `body_ref`. `body` is never parsed: a body
whose contents happen to be a JSON object literal is carried as a string, so
overlapping JSON keys across payloads remain distinguishable. `body_ref` is a
reference and is never dereferenced by the renderer. `payloads` is the only
Lifeloop-reserved key in the envelope.

For Codex `PostCompact` host-hook compatibility, `side_channel_context` is the
only eligible rendering placement. The host-hook bridge selects response
payloads that include `side_channel_context` in `acceptable_placements` and
renders them back through `hookSpecificOutput.additionalContext`. `PreCompact`
remains a quiet observation hook, and other declared placements for the same
payload do not cause prompt injection during `PostCompact`.

Placement classes:

| Placement | Meaning |
|---|---|
| `developer_equivalent_frame` | Strong instruction/context layer comparable to developer or system-adjacent harness context. |
| `pre_prompt_frame` | Context prepended before the next user or task prompt. |
| `side_channel_context` | Out-of-band context channel exposed by the harness or reference adapter. |
| `receipt_only` | No prompt injection; payload metadata is recorded or correlated only. |

Placement resolution:

- `acceptable_placements` is an ordered list of alternative placements, not a
  request to deliver the same payload everywhere.
- Lifeloop selects the first satisfiable placement in client order.
- If no placement is satisfiable and at least one acceptable placement is
  `required`, the operation fails with `placement_unavailable`.
- If only `preferred` or `optional` placements are unavailable, Lifeloop may
  continue with a degraded or skipped payload receipt according to the client
  request.
- Delivering to multiple placements requires a future explicit multi-delivery
  option; it is not implied by listing multiple acceptable placements.
- `receipt_only` stores `payload_id`, digest, placement decision, and metadata
  needed for correlation. It must not persist the opaque payload body unless an
  explicit `payload_store` capability is negotiated.

### Dispatch envelope (transport boundary)

The CLI and the subprocess invoker carry payload envelopes alongside the
callback request through a single transport-boundary shape, the dispatch
envelope:

```json
{
  "schema_version": "lifeloop.v0.2",
  "request": { "...CallbackRequest...": "..." },
  "payloads": [ { "...PayloadEnvelope...": "..." } ]
}
```

Rules:

- `request` is the validated `CallbackRequest`. `payloads` is the (possibly
  empty) list of envelopes the dispatch is delivering with; on the wire the
  key is omitted when the list is empty. The request includes its optional
  harness identity fields and opaque `metadata` map.
- The dispatch envelope is the contract between Lifeloop and any subprocess
  callback client: clients deserialize this single document from stdin and
  return a `CallbackResponse` on stdout. The same shape is what `lifeloop
  event invoke` reads on stdin.
- Lifeloop never parses `payloads[].body` — bodies are transported verbatim
  per the opacity rule above. Cross-correlation between
  `request.payload_refs` and `payloads[]` is intentionally not enforced at
  the transport boundary; that responsibility belongs to negotiation and
  receipt synthesis.

## Receipt Schema

Every lifecycle operation returns or emits a bounded receipt.

Required receipt fields:

```json
{
  "schema_version": 1,
  "receipt_id": "lfr_01K...",
  "idempotency_key": "idem_01K...",
  "client_id": "example-client",
  "adapter_id": "example-harness",
  "invocation_id": "inv_01K...",
  "event": "frame.opening",
  "event_id": "evt_01K...",
  "sequence": 42,
  "parent_receipt_id": "lfr_01K_parent",
  "integration_mode": "native_hook",
  "status": "delivered",
  "at_epoch_s": 1778100000,
  "harness_session_id": "session-123",
  "harness_run_id": "run-456",
  "harness_task_id": null,
  "payload_receipts": [
    {
      "payload_id": "pay_01K...",
      "payload_kind": "instruction_frame",
      "placement": "pre_prompt_frame",
      "status": "delivered",
      "byte_size": 1200,
      "content_digest": "sha256:..."
    }
  ],
  "telemetry_summary": {
    "context_pressure": "moderate"
  },
  "capability_degradations": [],
  "failure_class": null,
  "retry_class": "safe_retry",
  "warnings": []
}
```

Receipt statuses:

- `observed`
- `delivered`
- `skipped`
- `degraded`
- `failed`

Identifier rules:

- `client_id` is a client-declared stable label. CCD uses `ccd`; an RLM client
  uses its own non-CCD label such as `rlm`.
- Idempotency keys are scoped by `(client_id, adapter_id, idempotency_key)`.
- `invocation_id` identifies one transport invocation. A single synchronous
  command may emit multiple event receipts with the same `invocation_id`.
- `receipt_id` is a new opaque identifier for this emitted receipt.
- `event_id` identifies the observed lifecycle invocation. If a harness
  supplies a stable hook/run identifier, Lifeloop uses it; otherwise Lifeloop
  synthesizes one for that process invocation and records the weaker ordering
  in the receipt.
- `sequence` is required and nullable. It is monotonic within the strongest
  available durable session scope. When harness sequencing or a receipt ledger
  is unavailable, Lifeloop may set `sequence` to `null` rather than inventing a
  misleading cross-invocation order.
- `parent_receipt_id` is required and nullable. It is `null` for root receipts
  and set for nested or causally linked lifecycle receipts.
- `idempotency_key` is the client-supplied replay boundary when present.
  Lifeloop must not infer durable idempotency from timestamp-derived event IDs.
- `payload_receipts[]` entries identify the payload artifact Lifeloop placed.
  `payload_kind` is required and is scoped by the receipt's `client_id`.
  `content_digest` is optional and is omitted when the negotiated payload did
  not carry a digest. Lifeloop echoes the negotiated payload's digest; it does
  not invent or normalize one during receipt synthesis.

Ordering rules:

- Ordering is per harness session when stable harness sequencing exists.
- Otherwise ordering is per declared receipt ledger when that capability
  exists.
- Otherwise receipts are only diagnostic artifacts ordered by
  `(at_epoch_s, receipt_id)`.
- Clients may sort by `(harness_session_id, sequence)` when both are present.
- Clients may sort diagnostics by `(at_epoch_s, receipt_id)`.
- Clients must not assume a total order across adapters or harness sessions.

Idempotency rules:

- If the caller supplies `idempotency_key`, repeated delivery with identical
  receipt content is an idempotent replay when a receipt ledger is available.
- Reusing an `idempotency_key` with different content is a
  `duplicate_id_conflict` when a receipt ledger is available.
- Without an idempotency key, `receipt_id` is the replay boundary.
- Without a receipt ledger, Lifeloop validates and echoes idempotency keys but
  does not claim durable duplicate detection across synchronous process
  invocations. Durable client-side mutation safety remains client-owned.

Loss semantics:

- Lifeloop receipts are evidence, not the client's source of truth.
- Lifeloop does not silently reconstruct lost receipts.
- Lifeloop emits `receipt.gap_detected` only when stable harness sequencing or
  a receipt ledger makes a gap observable.
- Clients recover from their own state stores.

## Failure Classes

Failure classes are lifecycle classifications owned by Lifeloop.

| Failure class | Meaning |
|---|---|
| `adapter_unavailable` | The selected adapter cannot be reached or loaded. |
| `capability_unsupported` | A required capability is unavailable before dispatch. |
| `capability_degraded` | A previously available capability is now weaker or absent. |
| `placement_unavailable` | No acceptable payload placement can be satisfied. |
| `payload_too_large` | The payload exceeds the negotiated placement limit. |
| `payload_rejected` | The harness rejected a payload for lifecycle/transport reasons. |
| `identity_unavailable` | Required harness identity fields cannot be observed. |
| `transport_error` | The harness transport failed. |
| `timeout` | The lifecycle operation timed out. |
| `operator_required` | Manual setup, approval, or intervention is required. |
| `state_conflict` | Lifecycle correlation or idempotency state conflicted. |
| `invalid_request` | The client request failed schema or precondition validation. |
| `internal_error` | Lifeloop failed unexpectedly. |

Adapters provide raw evidence. Lifeloop maps that evidence into these classes.
Clients must not text-parse adapter errors for retry posture.

Renewal/reset flows use the existing failure classes:

| Renewal condition | Failure class | Meaning |
|---|---|---|
| Reset capability is unavailable before dispatch. | `capability_unsupported` | No safe adapter-owned reset path exists. |
| Continuation payload delivery was lost or rejected. | `payload_rejected` | The lifecycle fact was understood, but delivery failed. |
| A previously negotiated renewal capability is stale or weaker. | `capability_degraded` | The client should reread capability state before deciding. |
| The only reset path is operator/manual. | `operator_required` | A human or manual wrapper must act before retry. |

## Retry Classes

| Retry class | Meaning |
|---|---|
| `safe_retry` | The same request may be retried with the same idempotency key. |
| `retry_after_reread` | The client should reread lifecycle or client state before retrying. |
| `retry_after_reconfigure` | Adapter setup or capability configuration must change first. |
| `retry_after_operator` | Operator action is required before retry. |
| `do_not_retry` | Blind retry would repeat an unsafe or invalid operation. |

Default mapping:

| Failure class | Default retry class |
|---|---|
| `adapter_unavailable` | `retry_after_reconfigure` |
| `capability_unsupported` | `do_not_retry` |
| `capability_degraded` | `retry_after_reread` |
| `placement_unavailable` | `retry_after_reconfigure` |
| `payload_too_large` | `do_not_retry` |
| `payload_rejected` | `retry_after_reconfigure` |
| `identity_unavailable` | `retry_after_reconfigure` |
| `transport_error` | `safe_retry` |
| `timeout` | `safe_retry` |
| `operator_required` | `retry_after_operator` |
| `state_conflict` | `retry_after_reread` |
| `invalid_request` | `do_not_retry` |
| `internal_error` | `retry_after_reread` |

Adapters may tighten retry posture for known unsafe operations, but they must
not loosen it without a spec update or dedicated conformance proof.

## Conformance Expectations

An implementation of this contract should include:

- manifest schema validation for every shipped adapter
- registry-backed capability-claim verification for v1 conformance adapters:
  every claim in a Codex or Claude manifest that depends on extracted code
  (asset rendering, telemetry parsing, placement support) is paired with a
  test that runs the corresponding code path and asserts the manifest claim
  is true
- negotiation tests for `required`, `preferred`, and `optional`
- negotiation tests proving `partial` support degrades unless a client
  explicitly marks it acceptable
- payload placement tests for success, degradation, and refusal
- receipt ordering and idempotency tests
- receipt schema tests proving required nullable fields such as
  `sequence` and `parent_receipt_id` are present even when null
- identifier tests proving `receipt_id`, `event_id`, `sequence`, and
  `idempotency_key` semantics do not collapse into one field
- invocation tests proving multi-event synchronous invocations share an
  `invocation_id` and use `parent_receipt_id` for causal chaining
- synchronous dispatch tests proving compatibility transports can preserve
  existing command-response behavior while using Lifeloop internally
- stateless first-slice tests proving no cross-invocation sequence,
  gap-detection, negotiation-cache, or idempotency-store behavior is claimed
  unless the manifest advertises the needed stateful capability
- manifest tests covering `telemetry_only` adapters separately from native or
  reference adapters
- degradation tests proving mid-session capability loss is surfaced
- failure-class mapping tests for every adapter
- static ownership checks proving Lifeloop core does not import client-owned
  continuity or recursive-inference modules. In this repo, the current gate is
  `tests/kernel_purity.rs::lifeloop_static_boundary_proof_keeps_client_vocabulary_out`.

## Implementation Status

The `lifeloop.v0.2` slice of this contract is implemented in the
`lifeloop` crate. The repo carries the lifecycle event vocabulary,
adapter manifest registry (with v1 conformance manifests for Codex and
Claude plus pre-conformance manifests for Hermes, OpenClaw, Gemini, and
OpenCode), capability/placement negotiation, opaque payload envelopes,
the dispatch envelope transport-boundary shape, lifecycle receipts with
per-payload receipt provenance, and the 13-class `FailureClass` /
5-class `RetryClass` enums with the spec's failure-to-retry default
mapping.

The lifecycle router (`src/router/`) wires those pieces together as:
pre-dispatch validation + adapter resolution → capability/placement
negotiation → callback invocation (in-process or subprocess over JSON
stdio via the dispatch envelope) → receipt synthesis with idempotency.
The native Lifeloop transport surface is the `lifeloop` CLI plus the
documented subprocess callback contract. The synchronous `host-hook`
broker remains a compatibility bridge for installed host assets. Current
greenfield CCD renewal uses `ccd lifeloop client`, `ccd renewal prepare`, and
the `CONTINUATION-001` store's `renewal-state` blob without making CCD renewal
leases, thread binding, or continuation-token policy part of Lifeloop's
contract.

A non-CCD client class is shipping in this repo as the thread-sync publisher
(`crates/thread-sync-publisher/`); it consumes the same callback contract and
is the first proof that lifecycle reach is reusable beyond CCD. The #28
product pilot is the Fixity client (`crates/fixity-pilot/`), which consumes
`DispatchEnvelope` payload bodies or refs through the real subprocess dispatch
path while keeping repeated-signal policy outside Lifeloop core. A durable
receipt ledger and additional non-CCD client classes (an RLM prototype and
other lifecycle-only clients) remain follow-on work governed by the v1 freeze
gates in `docs/release-gates.md`.
