Skip to content

id(currency_poc): FileIntentStore listing ignores canonical file names — duplicate copies reconcile twice, and a misnamed record is invisible to prepare()'s identity guard #4578

Closed

Description

@dezren39

Summary

FileIntentStore (pkgs/id/src/currency_poc/intent.rs) has two inconsistent notions of where an intent record lives:

  • get(conversion_id) reads only the canonical path path_for(id) = {id:032x}.json (intent.rs:175-177, 199-209).
  • unresolved() ignores file names entirely: it scans every *.json entry in the directory and returns whatever parses as an IntentRecord (intent.rs:211-256).

Nothing ties a listed record to its canonical file name, and nothing deduplicates by conversion_id. Two verified consequences:

  1. Duplicate listing / duplicate reconciliation. Every copy of a record file in the same directory (an operator backup copy, a copy under another name) is listed again. Recovery then reconciles the same conversion once per copy and emits duplicate (conversion_id, outcome) pairs.
  2. A misnamed-only record is invisible to the identity guard. A record that exists only under a non-canonical name (older naming scheme, operator rename) is seen by unresolved() but get(id) returns None for it. ConversionExecutor::prepare's identity guard — "a conversion ID is the identity of ONE conversion: never replace its persisted intent" (executor.rs:238-258) — is built on get(), so it happily persists a second, conflicting intent for the same conversion. The store then holds two records for one conversion with different legs, and unresolved() returns both.

This is the identity counterpart of #4125 (closed): that fix made the listing skip unparseable entries; parseable-but-misnamed or duplicated entries still pass through unchecked. It is also adjacent to #4133 (closed), which made reconcile distrust the leg vector; the record-identity layer has the same trust problem one level up.

Evidence (verified this run, main 874ca86)

Laptop probe ~/git/fleet/probe-2026-11-02-laneB-intentname/ — verbatim copies of currency_poc/{intent,ledger,recovery}.rs from origin/main (md5 intent d276f6cc, ledger 093c818d, recovery 8d40cf83, all matching the sandbox clone at 874ca86); cargo test 2/2 pass:

  • PROBE1: put record for conversion 7, then copy its file to backup.json and 999.json in the same directory → unresolved() returns 3 records for conversion 7; reconcile(.., Abandon) returns 3 outcomes [(7, Abandoned), (7, Abandoned), (7, Abandoned)].
  • PROBE2: write a Recorded record for conversion 9 only as intent-9.json → get(9) returns None while unresolved() returns [9]; after put of a conflicting record for conversion 9 (different leg ID), unresolved() returns 2 records for conversion 9.

Why it matters

The intent store is the write-ahead recovery surface for conversions: its whole contract is one durable record per conversion that the scanner can trust. Duplicate listings make recovery reports (and any consumer counting outcomes) double-count a conversion, and repeated puts from duplicate entries rewrite the canonical file once per copy. The misnamed case is worse: it silently defeats prepare()'s never-replace guard, the exact mechanism that prevents a retry from erasing/conflicting with the recovery record of an earlier, possibly-posted submission — two divergent leg sets for one conversion ID is precisely the state the recovery scanner cannot reconcile safely.

Acceptance criteria

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. 38 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  7. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 3, 2026
  8. added a commit that references this issue on Nov 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Resumed at the Codex gate: the re-review round on head 6002db2 had left three undispositioned findings, all fixed in bc0a779 and replied to on their threads — P1 4449053752 (uppercase-extension aliases were excluded from the scan: extension match is now case-insensitive, with a regression test), P2 4449053759 (unreadable canonical matching is now alias-aware via the new IntentScan::unreadable_at, with a regression test), P2 4449053762 (occupant resolution now goes through a per-scan index — by_path/by_file_id built once — instead of pairwise same_file() calls). Laptop verification at bc0a779: cargo test --lib currency_poc in pkgs/id (world) — 35 passed, including the two new regressions. Codex re-review on bc0a779 completed with no findings. PR #4622 MERGED (merge commit 1d4a448); issue #4578 closed by the merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions