Add durable effect identity and observation ledger #78
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
nsaspy/prolog-rlm!78
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/issue-57-effect-identity"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
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:
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 canonicalCall.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:
dispatchingis 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.
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:
Replay / retry / resample
Authority (#53)
rlm_effect_authoritycomposes the exact effect fingerprint and attempt identity into the existing #53 authority operation. There is noauthority_v2.The canonical tiers remain exactly:
approve_diffallow_onceallow_sessiondangerousTests preserve the required behavior: changed payload, retry, resample, or edited proposal cannot silently inherit stale
allow_once;dangerousstill obeys identity, accounting, confinement, persistence, and cancellation invariants.Async (#54)
The canonical direction remains:
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
Non-reconcilable provider
The same crash window yields
indeterminatewithsubmit_count == 1; no automatic resubmission occurs.Accounting / trajectory data
Durable attempt and observation events retain execution evidence even if later Prolog logic fails.
attempt_dispatchedproves 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:
The deterministic check has zero annotations. The REAL job's annotations are evidence notices, not warnings/errors.
Guarantee 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
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 SWIlibrary(persistency)/file-lock behavior, and downstreamstarintel-social-presencePR #11.Resolved during review — cross-process store ownership
Initial head
6dd72fbb17c6510ee40485fc8685f26a94c1779dserialized 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:
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_toolstill goes from #53 authority directly toperform_tool_effect.rlm_chain:model_complete_execute/3still dispatches torlm_openai_compatible, whose real provider path crosses the network withhttp_post/ streaming transport.mcp_transport_exchangedirectly.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/1verifies a groundeffect_ticketwith 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_onceseparation, 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:
Do not upgrade that into generic exactly-once semantics.