Add durable effect identity and observation ledger #78

Merged
lost-rob0t merged 46 commits from feature/issue-57-effect-identity into main 2026-08-17 19:41:19 +00:00
lost-rob0t commented 2026-08-17 17:58:53 +00:00 (Migrated from github.com)

Purpose

Implements the generic durable effect-identity / authoritative-observation substrate required by #57.

The runtime handles the dangerous window where an external provider may have accepted an effect and the local process dies before committing the terminal observation. Missing local observation is not treated as permission to submit again.

This PR does not claim protocol-independent exactly-once external execution, and it does not yet route every canonical provider/tool/MCP/process surface through the boundary. Canonical adoption remains tracked in #79. Merging this PR does not close #57.

Runtime invariant

logical call identity
-> normalized executable fingerprint
-> explicit attempt identity
-> retry/resample lineage
-> authority identity
-> durable admission
-> durable dispatch intent
-> external adapter boundary
-> immutable authoritative observation
   OR conservative indeterminate state

Ordinary Prolog backtracking never manufactures another external submission.

Raw ticket integrity hardening

Effect tickets are constructor-produced records, not trusted merely because a caller supplied a dict with plausible fields.

Admission and pre-claim cancellation now validate the exact ticket shape and recompute/check:

  • canonical normalized request;
  • semantics-bearing options carried by the ticket;
  • executable fingerprint;
  • logical call ID/logical key;
  • attempt mode, parent and sequence;
  • deterministic attempt ID;
  • provider idempotency key;
  • agreement with the durable call record created by rlm_effect_prepare/4.

Adversarial coverage rejects forged fingerprint, attempt ID, sequence, parent, idempotency key, and changed request with stale identity while accepting a constructor-produced ticket.

Canonical adapter request contract

The external boundary no longer receives the caller's pre-normalization request after preparation.

The submit/cancel path receives Ticket.request, and later reconciliation receives the same persisted canonical Call.request. This pins immediate submission and fresh-process reconciliation to one normalized executable representation rather than two subtly different contracts.

Tests cover deterministic dict normalization, equivalent supported requests sharing identity, normalized submit/reconcile inputs, and preservation of semantics-bearing values.

Durable lifecycle

Attempts use append-only revisions on SWI-Prolog persistency with synchronous journal writes. The critical order is:

normalize/identity
-> authority/capability/policy boundary
-> atomic local admission
-> durable admitted state
-> durable dispatching state
-> external adapter may act
-> durable immutable observation
-> durable observed revision

dispatching is committed before adapter submit. The observation is committed before the final observed attempt revision, so a crash between those local writes still leaves the observation authoritative and recoverable.

Transport exceptions after durable dispatch do not imply non-execution. Unsupported/failed reconciliation remains conservative and indeterminate. Cancellation after dispatch does not imply remote cancellation.

Writable-store ownership

The persistent SWI backend intentionally enforces one cooperating writer using local OS advisory locking on a dedicated sidecar for the canonical store path.

process A owns writable store
-> process B opening same canonical store fails closed
-> A closes or dies
-> OS ownership is released
-> fresh process may acquire store and reconcile existing work

Hardening coverage includes normal close, SIGKILL, repeated same-process open, switching stores, attach-failure cleanup, lock-failure cleanup, relative/absolute path aliases, and deterministic simultaneous process start using explicit barriers rather than sleeps.

This backend does not provide:

  • distributed consensus;
  • arbitrary multi-host CAS;
  • universal exactly-once external execution;
  • safety on filesystems whose locking semantics do not support this contract.

Replay / retry / resample

  • replay/reuse returns the existing observation with no external execution;
  • retry is an explicit linked child attempt;
  • resample is an explicit fresh execution distinct from retry;
  • changed payload produces a new executable fingerprint;
  • repeated entry into the same explicit retry/resample resolves to the same deterministic child identity instead of creating another child through backtracking.

Authority (#53)

rlm_effect_authority composes the exact effect fingerprint and attempt identity into the existing #53 authority operation. There is no authority_v2.

The canonical tiers remain exactly:

  • approve_diff
  • allow_once
  • allow_session
  • dangerous

Tests preserve the required behavior: changed payload, retry, resample, or edited proposal cannot silently inherit stale allow_once; dangerous still obeys identity, accounting, confinement, persistence, and cancellation invariants.

Async (#54)

The canonical direction remains:

effect_execute_execute/6 -> rlm_async Future -> sync facade awaits same Future

There is no second synchronous business-logic implementation. Existing tests cover repeated status/await, callbacks, continuations, sync-after-async replay, wrapping already-running work, and conservative cancellation without resubmission.

Fresh-process crash acceptance

The suite retains independent SWI-process fixtures with an externally observable fake remote system.

Reconcilable provider

process 1 durably dispatches
-> fake remote records effect and flushes state
-> explicit marker emitted
-> parent SIGKILLs process 1 before local observation
-> process 2 opens same store
-> process 2 reconciles original attempt
-> recovered observation is persisted
-> submit_count == 1

Non-reconcilable provider

The same crash window yields indeterminate with submit_count == 1; no automatic resubmission occurs.

Accounting / trajectory data

Durable attempt and observation events retain execution evidence even if later Prolog logic fails. attempt_dispatched proves the external boundary was crossed; observations retain usage/provenance. Stable call/attempt/parent/mode/sequence/fingerprint data remains available for #44/#45 without creating competing ledgers.

This PR does not claim to close #44, #45, or remaining cross-library work in #54.

Canonical adoption still required

The generic substrate is intentionally not the system-wide adoption slice. At this boundary, canonical paths such as provider/model execution, perform_tool_effect, effectful MCP exchanges, and MCP/process lifecycle work still require coherent adoption through #57.

#79 owns that work. Do not implement it as part of this PR and do not close #57 merely because #78 merges.

Verification

Final reviewed head: d9ed793d5a49547604317def8bad8e7236d9ac4a.

The canonical CI workflow is green on that head:

  • deterministic static/load checks;
  • full PlUnit suite including adversarial effect hardening;
  • deterministic benchmark/conformance;
  • deep recursion experiment;
  • CLI/trace smoke;
  • graph/artifact fresh-process restart checks;
  • whitespace checks;
  • configured REAL OpenRouter core, structured repair, benchmark, depth, and CLI gates.

The deterministic check has zero annotations. The REAL job's annotations are evidence notices, not warnings/errors.

Guarantee boundary

prolog-rlm prevents implicit duplicate execution and provides durable attempt/observation identity with explicit reconciliation for uncertain external outcomes when externally effectful execution is routed through this boundary.

A stronger external exactly-once claim is justified only when the external protocol and implemented idempotency/reconciliation behavior genuinely provide it.

Refs #57
Refs #44
Refs #45
Refs #53
Refs #54
Refs #79

## Purpose Implements the **generic durable effect-identity / authoritative-observation substrate** required by #57. The runtime handles the dangerous window where an external provider may have accepted an effect and the local process dies before committing the terminal observation. Missing local observation is **not** treated as permission to submit again. This PR does **not** claim protocol-independent exactly-once external execution, and it does **not** yet route every canonical provider/tool/MCP/process surface through the boundary. Canonical adoption remains tracked in #79. **Merging this PR does not close #57.** ## Runtime invariant ```text logical call identity -> normalized executable fingerprint -> explicit attempt identity -> retry/resample lineage -> authority identity -> durable admission -> durable dispatch intent -> external adapter boundary -> immutable authoritative observation OR conservative indeterminate state ``` Ordinary Prolog backtracking never manufactures another external submission. ## Raw ticket integrity hardening Effect tickets are constructor-produced records, not trusted merely because a caller supplied a dict with plausible fields. Admission and pre-claim cancellation now validate the exact ticket shape and recompute/check: - canonical normalized request; - semantics-bearing options carried by the ticket; - executable fingerprint; - logical call ID/logical key; - attempt mode, parent and sequence; - deterministic attempt ID; - provider idempotency key; - agreement with the durable call record created by `rlm_effect_prepare/4`. Adversarial coverage rejects forged fingerprint, attempt ID, sequence, parent, idempotency key, and changed request with stale identity while accepting a constructor-produced ticket. ## Canonical adapter request contract The external boundary no longer receives the caller's pre-normalization request after preparation. The submit/cancel path receives `Ticket.request`, and later reconciliation receives the same persisted canonical `Call.request`. This pins immediate submission and fresh-process reconciliation to one normalized executable representation rather than two subtly different contracts. Tests cover deterministic dict normalization, equivalent supported requests sharing identity, normalized submit/reconcile inputs, and preservation of semantics-bearing values. ## Durable lifecycle Attempts use append-only revisions on SWI-Prolog persistency with synchronous journal writes. The critical order is: ```text normalize/identity -> authority/capability/policy boundary -> atomic local admission -> durable admitted state -> durable dispatching state -> external adapter may act -> durable immutable observation -> durable observed revision ``` `dispatching` is committed before adapter submit. The observation is committed before the final observed attempt revision, so a crash between those local writes still leaves the observation authoritative and recoverable. Transport exceptions after durable dispatch do not imply non-execution. Unsupported/failed reconciliation remains conservative and indeterminate. Cancellation after dispatch does not imply remote cancellation. ## Writable-store ownership The persistent SWI backend intentionally enforces **one cooperating writer using local OS advisory locking** on a dedicated sidecar for the canonical store path. ```text process A owns writable store -> process B opening same canonical store fails closed -> A closes or dies -> OS ownership is released -> fresh process may acquire store and reconcile existing work ``` Hardening coverage includes normal close, SIGKILL, repeated same-process open, switching stores, attach-failure cleanup, lock-failure cleanup, relative/absolute path aliases, and deterministic simultaneous process start using explicit barriers rather than sleeps. This backend does **not** provide: - distributed consensus; - arbitrary multi-host CAS; - universal exactly-once external execution; - safety on filesystems whose locking semantics do not support this contract. ## Replay / retry / resample - replay/reuse returns the existing observation with no external execution; - retry is an explicit linked child attempt; - resample is an explicit fresh execution distinct from retry; - changed payload produces a new executable fingerprint; - repeated entry into the same explicit retry/resample resolves to the same deterministic child identity instead of creating another child through backtracking. ## Authority (#53) `rlm_effect_authority` composes the exact effect fingerprint and attempt identity into the existing #53 authority operation. There is no `authority_v2`. The canonical tiers remain exactly: - `approve_diff` - `allow_once` - `allow_session` - `dangerous` Tests preserve the required behavior: changed payload, retry, resample, or edited proposal cannot silently inherit stale `allow_once`; `dangerous` still obeys identity, accounting, confinement, persistence, and cancellation invariants. ## Async (#54) The canonical direction remains: ```text effect_execute_execute/6 -> rlm_async Future -> sync facade awaits same Future ``` There is no second synchronous business-logic implementation. Existing tests cover repeated status/await, callbacks, continuations, sync-after-async replay, wrapping already-running work, and conservative cancellation without resubmission. ## Fresh-process crash acceptance The suite retains independent SWI-process fixtures with an externally observable fake remote system. ### Reconcilable provider ```text process 1 durably dispatches -> fake remote records effect and flushes state -> explicit marker emitted -> parent SIGKILLs process 1 before local observation -> process 2 opens same store -> process 2 reconciles original attempt -> recovered observation is persisted -> submit_count == 1 ``` ### Non-reconcilable provider The same crash window yields `indeterminate` with `submit_count == 1`; no automatic resubmission occurs. ## Accounting / trajectory data Durable attempt and observation events retain execution evidence even if later Prolog logic fails. `attempt_dispatched` proves the external boundary was crossed; observations retain usage/provenance. Stable call/attempt/parent/mode/sequence/fingerprint data remains available for #44/#45 without creating competing ledgers. This PR does not claim to close #44, #45, or remaining cross-library work in #54. ## Canonical adoption still required The generic substrate is intentionally not the system-wide adoption slice. At this boundary, canonical paths such as provider/model execution, `perform_tool_effect`, effectful MCP exchanges, and MCP/process lifecycle work still require coherent adoption through #57. **#79 owns that work. Do not implement it as part of this PR and do not close #57 merely because #78 merges.** ## Verification Final reviewed head: `d9ed793d5a49547604317def8bad8e7236d9ac4a`. The canonical CI workflow is green on that head: - deterministic static/load checks; - full PlUnit suite including adversarial effect hardening; - deterministic benchmark/conformance; - deep recursion experiment; - CLI/trace smoke; - graph/artifact fresh-process restart checks; - whitespace checks; - configured REAL OpenRouter core, structured repair, benchmark, depth, and CLI gates. The deterministic check has zero annotations. The REAL job's annotations are evidence notices, not warnings/errors. ## Guarantee boundary > `prolog-rlm` prevents implicit duplicate execution and provides durable attempt/observation identity with explicit reconciliation for uncertain external outcomes **when externally effectful execution is routed through this boundary**. A stronger external exactly-once claim is justified only when the external protocol and implemented idempotency/reconciliation behavior genuinely provide it. Refs #57 Refs #44 Refs #45 Refs #53 Refs #54 Refs #79
lost-rob0t commented 2026-08-17 19:02:39 +00:00 (Migrated from github.com)

Adversarial #57 review — final state after fixes

Reviewed the live PR against issue #57, #53/#54, #44/#45, current canonical tool/provider/MCP/lifecycle execution, RLM-RESEARCH-010, fresh-process crash fixtures, persistence semantics, current SWI library(persistency)/file-lock behavior, and downstream starintel-social-presence PR #11.

Resolved during review — cross-process store ownership

Initial head 6dd72fbb17c6510ee40485fc8685f26a94c1779d serialized effect admission only with process-local mutexes and did not enforce the single-writer assumption needed by the SWI persistency backend.

I added a deterministic multi-process regression first, then implemented a lifetime OS advisory lock on a dedicated effect-store sidecar. The invariant is now explicit and executable:

process A owns writable effect store
-> process B opening same canonical store fails closed
-> process A dies, including SIGKILL
-> OS releases ownership
-> fresh process may open and reconcile

The implementation deliberately does not claim distributed CAS or generic multi-host consensus. It enforces one cooperating writer for this backend.

Current fix head: cbc58d4c9c6bc63284ed67065d0f39b5d121c150.

The canonical deterministic job is green, including the new two-process fixture, and the configured REAL OpenRouter integration job is green on that head.

P1 — canonical effectful execution still bypasses #57

This remains the blocking runtime-level finding.

  • rlm_tool still goes from #53 authority directly to perform_tool_effect.
  • rlm_chain:model_complete_execute/3 still dispatches to rlm_openai_compatible, whose real provider path crosses the network with http_post / streaming transport.
  • MCP command paths still cross through mcp_transport_exchange directly.
  • MCP installer/run/process lifecycle still reaches process_create / transport open after policy + authority without #57 admission.

PR #78's effect runtime, adapter API, authority helper, persistence layer, and crash fixtures are therefore a generic substrate, not yet a system-wide invariant for existing core execution surfaces.

I created #79 — Route canonical effectful execution through the #57 effect boundary — to own that coherent adoption slice while preserving #53 authority ordering and #54 canonical async direction. #57 must remain open until #79 (or equivalent canonical adoption work) is complete.

P2 — raw effect tickets are structurally validated, not cryptographically/self-derived

validate_ticket/1 verifies a ground effect_ticket with required fields, but does not recompute every derived fingerprint/attempt/idempotency/lineage relationship before admission.

Current model-facing code cannot mint arbitrary tickets or register arbitrary adapters, so I do not classify this as an ambient model authority bypass. It is a trusted-host integration hardening gap: either document tickets as opaque constructor-owned capabilities or verify derived identity fields at the raw admission boundary. Add forged-ticket tests when that API contract is hardened.

This does not block merge of the generic substrate under the existing trusted-host boundary, but it should not silently evolve into a model-writable API later.

P3 — adapter request representation / documentation precision

The executor prepares and persists a normalized request but immediate submit is passed the original supported ground request representation, while reconciliation loads the persisted normalized request. With the current normalizer these supported values are semantically equivalent, so I did not manufacture a fake failing test for this. Still, the adapter docs should eventually make the representation contract exact (or consistently pass the persisted normalized request).

The persistence docs should also explicitly mention the new single-writer sidecar lock and its advisory/local-filesystem limitation rather than only saying that the backend lacks distributed CAS.

What is strong

The destructive crash fixture is legitimate: phase 1 durably marks dispatch, writes an externally observable fake remote effect, flushes a marker, then is SIGKILLed without a normal local observation or store close. A fresh process reconciles the same attempt or leaves it indeterminate; it does not submit again. The completed-observation restart case is also real.

Same-process replay, backtracking, retry/resample lineage, immutable observations, conservative transport/cancellation uncertainty, repeated Future awaits/status/callbacks/continuations, allow_once separation, deterministic thread races, retention, and post-effect logical failure evidence are meaningfully tested.

Closure decision

C. The generic #57 substrate is correct for its stated single-writer backend and trusted-host API boundary, but canonical integration remains and #57 should stay open.

PR #78 can merge after normal human review of the ownership fix. Do not close #57 on merge. #79 owns the next implementation slice.

The guarantee remains:

prolog-rlm prevents implicit duplicate execution and provides durable attempt/observation identity with explicit reconciliation for uncertain external outcomes when effectful execution is routed through this boundary.

Do not upgrade that into generic exactly-once semantics.

## Adversarial #57 review — final state after fixes Reviewed the live PR against issue #57, #53/#54, #44/#45, current canonical tool/provider/MCP/lifecycle execution, `RLM-RESEARCH-010`, fresh-process crash fixtures, persistence semantics, current SWI `library(persistency)`/file-lock behavior, and downstream `starintel-social-presence` PR #11. ### Resolved during review — cross-process store ownership Initial head `6dd72fbb17c6510ee40485fc8685f26a94c1779d` serialized effect admission only with process-local mutexes and did not enforce the single-writer assumption needed by the SWI persistency backend. I added a deterministic multi-process regression first, then implemented a lifetime OS advisory lock on a dedicated effect-store sidecar. The invariant is now explicit and executable: ```text process A owns writable effect store -> process B opening same canonical store fails closed -> process A dies, including SIGKILL -> OS releases ownership -> fresh process may open and reconcile ``` The implementation deliberately does **not** claim distributed CAS or generic multi-host consensus. It enforces one cooperating writer for this backend. Current fix head: `cbc58d4c9c6bc63284ed67065d0f39b5d121c150`. The canonical deterministic job is green, including the new two-process fixture, and the configured REAL OpenRouter integration job is green on that head. ### P1 — canonical effectful execution still bypasses #57 This remains the blocking runtime-level finding. - `rlm_tool` still goes from #53 authority directly to `perform_tool_effect`. - `rlm_chain:model_complete_execute/3` still dispatches to `rlm_openai_compatible`, whose real provider path crosses the network with `http_post` / streaming transport. - MCP command paths still cross through `mcp_transport_exchange` directly. - MCP installer/run/process lifecycle still reaches `process_create` / transport open after policy + authority without #57 admission. PR #78's effect runtime, adapter API, authority helper, persistence layer, and crash fixtures are therefore a **generic substrate**, not yet a system-wide invariant for existing core execution surfaces. I created #79 — **Route canonical effectful execution through the #57 effect boundary** — to own that coherent adoption slice while preserving #53 authority ordering and #54 canonical async direction. #57 must remain open until #79 (or equivalent canonical adoption work) is complete. ### P2 — raw effect tickets are structurally validated, not cryptographically/self-derived `validate_ticket/1` verifies a ground `effect_ticket` with required fields, but does not recompute every derived fingerprint/attempt/idempotency/lineage relationship before admission. Current model-facing code cannot mint arbitrary tickets or register arbitrary adapters, so I do **not** classify this as an ambient model authority bypass. It is a trusted-host integration hardening gap: either document tickets as opaque constructor-owned capabilities or verify derived identity fields at the raw admission boundary. Add forged-ticket tests when that API contract is hardened. This does not block merge of the generic substrate under the existing trusted-host boundary, but it should not silently evolve into a model-writable API later. ### P3 — adapter request representation / documentation precision The executor prepares and persists a normalized request but immediate submit is passed the original supported ground request representation, while reconciliation loads the persisted normalized request. With the current normalizer these supported values are semantically equivalent, so I did not manufacture a fake failing test for this. Still, the adapter docs should eventually make the representation contract exact (or consistently pass the persisted normalized request). The persistence docs should also explicitly mention the new single-writer sidecar lock and its advisory/local-filesystem limitation rather than only saying that the backend lacks distributed CAS. ## What is strong The destructive crash fixture is legitimate: phase 1 durably marks dispatch, writes an externally observable fake remote effect, flushes a marker, then is SIGKILLed without a normal local observation or store close. A fresh process reconciles the same attempt or leaves it indeterminate; it does not submit again. The completed-observation restart case is also real. Same-process replay, backtracking, retry/resample lineage, immutable observations, conservative transport/cancellation uncertainty, repeated Future awaits/status/callbacks/continuations, `allow_once` separation, deterministic thread races, retention, and post-effect logical failure evidence are meaningfully tested. ## Closure decision **C. The generic #57 substrate is correct for its stated single-writer backend and trusted-host API boundary, but canonical integration remains and #57 should stay open.** PR #78 can merge after normal human review of the ownership fix. Do **not** close #57 on merge. #79 owns the next implementation slice. The guarantee remains: > `prolog-rlm` prevents implicit duplicate execution and provides durable attempt/observation identity with explicit reconciliation for uncertain external outcomes **when effectful execution is routed through this boundary**. Do not upgrade that into generic exactly-once semantics.
Sign in to join this conversation.
No description provided.