# 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.
| `session.starting` | A harness session or top-level lifecycle is about to start, attach, or resume. |
| `session.started` | The harness session 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 session is about to close or needs close-out coordination. |
| `session.ended` | The top-level harness session 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.*` events describe a top-level harness lifecycle.
- `frame.*` events describe one execution or inference frame inside a session.
- A simple chat or coding harness may have one frame per prompt turn.
- A recursive client may create nested frames under the same session.
- `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.
## 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": "synthesized", "modes": ["native_hook"] },
"context.compacted": { "support": "unavailable", "modes": [] },
"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": "synthesized",
"evidence": "managed Codex lifecycle hooks observe context.pressure_observed when telemetry signals are available"
},
"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 the opt-in ccd-renewal host-hook profile proves wrapper-mediated reset prepare and out-of-band continuation-token delivery"
},
"failure_modes": ["transport_error", "payload_too_large"],
"known_degradations": []
}
```
### Required manifest fields
| `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
| `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:
| `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 and is scoped to the opt-in
`ccd-renewal` profile. Lifeloop's host-hook broker observes CCD's `session_boundary.action =
"renew"`, invokes `ccd session renew prepare --adapter codex --reset-path
wrapper`, stores the opaque continuation token outside hook stdout, and consumes
that token on the next Codex `SessionStart` by invoking `ccd start --refresh
--continuation <token>`. The token remains client-owned; Lifeloop records only
delivery evidence and thread-binding checks.
### Support states
Support values describe how strongly the adapter satisfies a capability claim.
| `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.
| `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:
| `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:
| `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.
Placement classes:
| `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 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.
| `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:
| 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
| `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:
| `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 is one client of this contract: it exists to connect installed
host assets to lifecycle normalization and to mediate opt-in CCD renewal
without making CCD renewal leases 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`.