Skip to content

feat(exchange): add true simulation mode to RunAutoExchange; make pending-cancellation coordination explicit #29

Description

@cristim

Discovered during LeanerCloud/cloud-commitments-cli#1333 phase-2 (AWSLadder write side, review finding): pkg/exchange has no true simulation mode. RunAutoExchange's ExchangeModeManual is NOT a dry run - it (a) persists pending ExchangeRecords with live approval tokens (actionable money instruments), and (b) begins every run with Store.CancelAllPendingExchanges, cancelling unrelated pendings (including ones created by the standalone ri_exchange_reshape scheduled task that a user may be mid-approval on).

Wanted: a simulate/preview mode that runs AnalyzeReshaping + GetQuote but performs ZERO store writes and returns proposed outcomes as data. Until it exists, providers/aws/ladder.ReshapeBuffer rejects DryRun=true with an explicit error, and the ladder engine previews reshapes via ActionReshape rationales only.

Also worth fixing under the same issue: the unconditional CancelAllPendingExchanges side effect deserves an explicit parameter or documented coordination contract, since two schedulers (ladder + standalone reshape) sharing one store can now stomp each other's pendings.

Findings from the 2026-09-02 codebase audit

Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.

A09-015 (low)

This issue's premise has moved on and the remedy is now smaller. RunAutoExchange no longer begins with CancelAllPendingExchanges; it calls CancelPendingExchangesByOrigin (auto.go:171), and the doc comment on the interface method (pkg/exchange/auto.go:49) says so. What is left is an uncalled method that every implementer still carries: the store implementation (internal/config/store_postgres.go:2914), the adapter passthrough (internal/server/handler_ri_exchange.go:296), four mocks and six tests, with no production caller anywhere. So the coordination contract this issue asks for is already there by construction, and the remaining action is to remove the unscoped "cancel every pending regardless of origin" primitive from RIExchangeStore so it is not one call away from the cross-origin contamination the replacement was written to prevent. The simulation-mode half of this issue is unaffected. (audit finding A09-015)

Activity

  1. cristim commented on Jul 16, 2026

    @cristim
    MemberAuthor

    PR LeanerCloud/cloud-commitments-cli#1368 (merged as 35410ee42) delivers the core of this issue: pending-exchange cancellation is now scoped by origin (standalone ri_exchange task vs ladder-originated, via ladder_run_id partition + a typed validated ExchangeOrigin enum) so the two flows cannot cancel each other's pendings, and RunAutoExchange has a true dry-run (zero mutations, no cancellation, no tokens, Simulated=true). LadderRunID is plumbed through the exchange records end-to-end. Remaining related work is tracked separately: per-run/per-config cancellation scoping before ladder reshapes create pendings (#1367, needed with L16 reshape sizing). Recommend closing this issue once the owner confirms the delivered scope matches intent.

  2. cristim commented on Jul 27, 2026

    @cristim
    MemberAuthor

    Verification sweep against main (101f099fb) on 2026-07-27 finds this already resolved.
    RunAutoExchange now has a Simulated flag and CancelPendingExchangesByOrigin is scoped to its origin.
    Evidence: commit 35410ee42 (PR LeanerCloud/cloud-commitments-cli#1368).
    Recommending close.

  3. cristim commented on Oct 7, 2026

    @cristim
    MemberAuthor

    Closing as resolved: the 2026-07-27 verification sweep against main (101f099fb) confirmed RunAutoExchange has a Simulated flag and CancelPendingExchangesByOrigin is origin-scoped (delivered by LeanerCloud/cloud-commitments-cli#1368, merged as 35410ee42). No remaining work items were noted on this issue.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions