Repository navigation
Description
Problem
In pkgs/id/src/currency_poc, the terminal intent states are not enforced by the executor. A record the recovery scanner (or an operator) deliberately abandoned can be posted afterwards by nothing more than an identical retry — the abandon decision is silently undone and the money moves.
intent.rs:33-37(origin/main) —IntentState::Abandonedis documented as "The scanner (or an operator) decided not to resubmit", andis_unresolved()(:41-43) treats it as resolved/terminal: the scanner will never look at the record again.executor.rs:256-266—prepare()'s never-replace guard compares a persisted record against the new request with the state deliberately normalised away (comparable.state = record.state,:262-263), so an identical request against an Abandoned (or already-Posted) record returns the existing record as-is, terminal state included.executor.rs:284-307—submit()never inspectsrecord.stateat all: it unconditionally overwrites it withSubmitted(:285), submits the legs, and persistsPosted(:302).executor.rs:310-314—execute()isprepare()+submit(), so the normal retry path composes the two gaps: retrying an abandoned conversion posts it.
recovery.rs:129-136 is where Abandon lands: the record is persisted Abandoned precisely so the conversion does not happen (e.g. the rate authorisation is stale, an operator intervened). Nothing downstream of that decision checks it.
Verified this run (laptop probe, verbatim origin/main sources)
Probe crate ~/git/fleet/probe-2026-11-02-laneA-abandoned/ on the laptop compiles the currency_poc modules extracted from origin/main (10e4ec1; blob-verified — executor.rs blob 0582e8ea, md5 500df828; intent.rs md5 d276f6cc; recovery.rs md5 8d40cf83) against the in-repo InMemoryLedger/InMemoryIntentStore:
- Probe A (retry resurrects):
prepare()a conversion (id 777) →reconcile(RecoveryPolicy::Abandon)returns[(777, Abandoned)], stored stateAbandoned, 0 legs in the ledger → an identicalexecute()retry returnsOkwith statePostedand 3 legs in the ledger. - Probe B (no guard in
submititself): a record hand-set toAbandonedand persisted, passed directly tosubmit(), returnsOk(()), endsPosted, 3 legs posted.
cargo test on the probe: 2 passed (both probes assert the buggy outcome).
Note on provenance: the laptop primary's working tree is currently a dirty repo-keeper snapshot (local HEAD 04286381a, off origin/main), so the probe was built from git show origin/main:... extractions, not the working tree; the cited line numbers are origin/main's.
Why it matters
Abandoned is the recovery system's only "do not move this money" verdict. If any later identical retry — a client timeout retry, a caller re-running execute() after a crash it misread, an operator script — silently reverses that verdict and posts the conversion, the state machine's terminal states are decorative. The asymmetry is the tell: the scanner respects Abandoned forever (is_unresolved excludes it), while the executor ignores it completely, so the two halves of the same module disagree about what the state means. (Re-submitting an already-Posted record is harmless today only because every leg returns Exists; the missing guard is the same hole.)
Acceptance criteria
-
ConversionExecutor::submitrefuses a record in a terminal state (Posted,Abandoned) with an error naming the conversion and its state, instead of overwriting the state and submitting. -
execute()on a conversion whose persisted intent isAbandonedfails (does not post, state staysAbandoned); on an already-Postedintent it returns the posted record without resubmitting, or fails explicitly — either way the behaviour is deliberate and tested. - Regression tests in
currency_poc/tests.rs: abandon-then-retry (viareconcile+execute) posts nothing and leaves the recordAbandoned; directsubmit()on anAbandonedrecord errors; existing scenario tests still pass.
Related: #2289 (the PoC), #2063 (design), #4133 (leg-vector vacuous Posted, fixed). Distinct from #4669 (short result vector), #4664 (preflight gaps), #4578 (FileIntentStore listing).
Activity
dezren39 commentedon Nov 3, 2026
🤖 Agent note · filed
~/git/fleet/probe-2026-11-02-laneA-abandoned/(laptop); related issues id: PoC: conversion executor — linked transfer pairs, write-ahead intent, recovery scanner #2289, id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063, id(currency_poc): reconcile counts legs per entry, not per distinct transfer — an empty or duplicate leg list is marked Posted with nothing (or half) in the ledger #4133dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
git show origin/main:), not a re-run of the creator's probe: inpkgs/id/src/currency_poc/executor.rs,prepare()handles an identical retry by cloning the persisted record, settingcomparable.state = record.state(normalising its state away for the equality check), and returningOk(existing)whatever that state is — includingAbandoned.submit()then unconditionally setsSubmitted, callsbackend.create_transfers(&record.legs), and setsPosted; it never reads the incomingrecord.state. Andrecovery.rs:129-134showsRecoveryPolicy::Abandondeliberately persistingIntentState::Abandoned— the terminal decision the executor silently overrides. This matches the creator's probe results on the same head (abandon-then-retry → Ok/Posted, 3 legs; direct submit on Abandoned → Ok/Posted, 3 legs).61 remaining items
dezren39 commentedon Nov 5, 2026
agent-note:v1
state: triaged
task: 8123
queue: repo-health-code-pilot
worker: repo-worker-code
execution_lane: opencode
session: ses_e5e55953603ffennLm9a7k769C
at: 2026-11-05T06:46:00Z
verdict: confirmed-open; routed-to-fix
follow_up: task 9912
Triage verdict: CONFIRMED on fresh current main (
origin/maine5d459e).prepare()still normalizes the persisted state during identity comparison and reuses an identical terminal intent;submit()still overwrites to Submitted and calls the ledger without checking state. The issue is OPEN. Fresh checks found no hard-stop labels, no open PR referencing #4678, no matching remote branch, and no live fix task using the standard key. The earlier fix handoff (task 6657) had already been reported as evaporated; no current branch/PR supersedes it.Routing: enqueued task 9912 (
small_fix) with dedupe keyrepo-health:v1:fix:developing-today/code:issue:4678. It covers terminal-state refusal, deliberate Posted retry behavior, and the abandon-then-retry/direct-submit regression tests. #2063, #2289, #4133, #4664, and #4578 are closed context; #4669 remains open but is explicitly separate. No code edits were made in this triage task; verification was the live source/repo/PR/queue check plus the probe evidence recorded above.dezren39 commentedon Nov 5, 2026
🤖 Agent note · picked up
test(id): live retry test expects terminal-state refusal (#4678, Codex)
Merge pull request #6105 from developing-today/queue/9912-4678-termin…
dezren39 commentedon Nov 5, 2026
🤖 Agent note · done
cargo test --manifest-path pkgs/id/Cargo.toml currency_poc47 passed / 0 failed incl. 3 new regressions; feature-gated live test updated per Codex P2 (dispositioned on its thread, reply 4477548618) and compile-verified with--features currency-poc-tb; Codex re-review clean. PR-tier checks stayed QUEUED in the runner backlog — merged under merge authority on laptop verification; main-tier CI is the backstop.