Skip to content

fix(core): Phase 2 Omni-Audit remediation -- 9 defects (A1-1..A3-3)#30

Closed
mkalhitti-cloud wants to merge 4 commits into
mainfrom
fix/phase2-omni-audit-remediation
Closed

fix(core): Phase 2 Omni-Audit remediation -- 9 defects (A1-1..A3-3)#30
mkalhitti-cloud wants to merge 4 commits into
mainfrom
fix/phase2-omni-audit-remediation

Conversation

@mkalhitti-cloud
Copy link
Copy Markdown
Owner

@mkalhitti-cloud mkalhitti-cloud commented Mar 8, 2026

Summary

Full remediation of all 9 defects identified by the Codex + Gemini 3.1 Pro Omni-Audit (phase_2_mission_brief.md). Standards reference: .agent/standards_manifesto.md Sections 1, 2, 6, 7, 8.

Commit 1 -- Concurrency (A1-1 + A2-1)

  • All activePositions[entryName] and entryOrders[entryName] writes wrapped in lock(stateLock) across FFMA, MOMO, OR, RMA, Trend, Trailing, Orders.Management, Symmetry
  • Null-abort rollback unified in FFMA/MOMO/OR/RMA: staged state moved AFTER null-check; delta rolled back; return on null path
  • pos.EntryFilled = true in Symmetry moved inside existing lock(stateLock) block

Commit 2 -- FSM (A1-2 + A1-3)

  • Symmetry target replace + Trailing stop/target moves: StampReaperMoveGrace() stamped before cancel to suppress false desync during the cancel-resubmit gap (mirrors PropagateMasterTargetMove pattern)
  • _followerReplaceSpecs dict write wrapped in lock(stateLock) in Symmetry
  • Orders.Callbacks FSM snapshot: PendingQty/PendingPrice/state transition captured atomically under lock(stateLock) before TriggerCustomEvent

Commit 3 -- Null safety & deferred cleanup (A2-1 tail + A2-2 + A2-3)

  • PositionInfo extended with PendingCleanup (bool) and FlattenAttemptCount (int) fields
  • RequestStopCancelLifecycleSafe: immediate activePositions.TryRemove replaced with pos.PendingCleanup = true; actual purge deferred to broker-confirmed terminal state in both HandleOrderCancelled (master) and OnAccountOrderUpdate (follower)
  • SymmetryGuardCascadeFollowerCleanup: premature DeltaExpectedPositionLocked removed; rollback moved to OnAccountOrderUpdate confirmed-cancel handler (direction-aware signed delta)

Commit 4 -- Queue/thread architecture (A3-1 + A3-2 + A3-3)

  • PumpFleetDispatch: hard guard at top drains _pendingFleetDispatches and returns if SIMA disabled or flatten running
  • ApplySimaState(false): queue drained after StopReaperAudit() + UnsubscribeFromFleetAccounts()
  • ExecuteReaperRepair: isFlattenRunning guard as very first statement
  • REAPER background thread: _repairInFlight.Add(repairKey) moved before TriggerCustomEvent to block double-enqueue across audit cycles
  • UpdateStopOrder null-stop + catch paths: FlattenAttemptCount circuit breaker caps consecutive emergency flattens at 3; counter reset on successful stop submission

Verification

  • ASCII gate: deploy-sync.ps1 -- PASS (zero non-ASCII byte warnings)
  • StateLock coverage: all activePositions[ and entryOrders[ writes inside lock(stateLock)
  • REAPER guard: isFlattenRunning is first statement in ExecuteReaperRepair
  • Queue drain: _pendingFleetDispatches cleared on SIMA disable and on PumpFleetDispatch abort
  • NT8 compile: F5 in NinjaTrader 8 -- zero compile errors (run locally before merge)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced order submission reliability with improved null-check handling and automatic entry abort recovery.
    • Improved state consistency through deferred cleanup and atomic state updates for position tracking.
    • Added safeguards to prevent repeated emergency flatten attempts during failed submissions.
  • Chores

    • Strengthened thread-safety mechanisms across order and position management operations.

mkalhitti-cloud and others added 4 commits March 8, 2026 15:44
…ks across all entry modules

- FFMA, MOMO, OR, RMA (ExecuteRMAEntry/Custom/TrendSplit), Trend (ExecuteTRENDEntry/ManualEntry):
  deferred activePositions/entryOrders writes to AFTER null check on SubmitOrderUnmanaged;
  wrapped both assignments in lock(stateLock); added clean rollback + return on null submit.
- Trailing.cs: wrapped three stopOrders[entryName] write sites in lock(stateLock).
- Orders.Management.cs: wrapped two stopOrders[entryName] write sites in lock(stateLock).
- Symmetry.cs: moved pos.EntryFilled = true inside existing lock(stateLock) block so
  EntryFilled and RemainingContracts are mutated atomically.

Resolves Build 960 audit defects: Area 1 item 1 and Area 2 item 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A1-2: Added StampReaperMoveGrace() before Cancel in follower target replacements
to open a 5-second REAPER suppression window, matching the established
PropagateMasterTargetMove pattern:
- Symmetry.cs SymmetryGuardReplaceExistingFollowerTarget: guard before Cancel,
  wrapped dict[fleetEntryName] write in lock(stateLock).
- Trailing.cs MoveSpecificTarget: guard before follower target Cancel.

A1-3: Wrapped the FSM PendingCancel->Submitting state transition snapshot in
lock(stateLock) inside OnAccountOrderUpdate. This closes the TOCTOU window
where PropagateFollowerEntryReplace (holding stateLock) could write to
PendingQty/PendingPrice simultaneously with the callback thread reading them.

Resolves Build 960 audit defects: Area 1 items 2 and 3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ncel expectedPositions rollback

A2-2: Added PendingCleanup and FlattenAttemptCount fields to PositionInfo. Replaced
immediate activePositions.TryRemove after RequestStopCancelLifecycleSafe in the
final-target and trim-close flows with pos.PendingCleanup = true. Added deferred
TryRemove logic to both HandleOrderCancelled (master stop) and OnAccountOrderUpdate
(follower stop) -- purge fires only on broker-confirmed stop terminal state.

A2-3: Removed DeltaExpectedPositionLocked from SymmetryGuardCascadeFollowerCleanup
(was firing immediately on cancel request). Moved the confirmed-cancel delta rollback
into the non-FSM follower entry cancelled branch in OnAccountOrderUpdate -- rollback
now fires only after OrderState.Cancelled confirmed by the broker, eliminating the
microsecond fill-race desync between the master cancel request and broker confirmation.

Resolves Build 960 audit defects: Area 2 items 2 and 3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A3-1 (V12_002.SIMA.cs):
- PumpFleetDispatch: hard guard drains queue if SIMA disabled or flatten running
- ApplySimaState disable path: drain _pendingFleetDispatches after Reaper stop

A3-2 (V12_002.REAPER.cs):
- ExecuteReaperRepair: isFlattenRunning guard as first statement
- Background REAPER thread: _repairInFlight.Add moved before TriggerCustomEvent
  to prevent double-enqueue in next audit cycle

A3-3 (V12_002.Trailing.cs):
- UpdateStopOrder null-stop path: FlattenAttemptCount circuit breaker (cap at 3)
- UpdateStopOrder catch path: same circuit breaker guards emergency flatten
- Reset FlattenAttemptCount to 0 on successful stop submission

Build 960 Phase 2 Omni-Audit -- Area 3 complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

This PR introduces thread-safe state updates and null-abort handling across order submission and callback paths. Key changes include wrapping position and order updates in stateLock blocks, adding null-checks for failed order submissions with early returns, introducing deferred cleanup logic for stop-terminal positions, and implementing a circuit-breaker mechanism for emergency flatten attempts.

Changes

Cohort / File(s) Summary
Entry submission null-abort handling
src/V12_002.Entries.FFMA.cs, src/V12_002.Entries.MOMO.cs, src/V12_002.Entries.OR.cs, src/V12_002.Entries.RMA.cs, src/V12_002.Entries.Trend.cs
Added null-checks on entryOrder results from SubmitOrderUnmanaged; on null, log ENTRY_ABORT and return early. Moved activePositions and entryOrders updates into stateLock blocks to ensure atomic state transitions only after confirming successful order submission.
Order management thread-safety
src/V12_002.Orders.Management.cs, src/V12_002.Trailing.cs
Wrapped stopOrders and related order dictionary mutations inside stateLock in SubmitBracketOrders and CreateNewStopOrder to prevent concurrent modification races.
Order callbacks and deferred cleanup
src/V12_002.Orders.Callbacks.cs
Introduced deferred cleanup with PendingCleanup flag for stop-terminal positions; added atomic snapshotting under stateLock during follower replacement; applied delta rollback for cancelled followers; aligned removal points to defer until broker-confirmed terminal state.
Position state synchronization
src/V12_002.Symmetry.cs
Moved EntryFilled assignment inside stateLock in SymmetryGuardSkipFollower; wrapped replacement entry updates under stateLock; added StampReaperMoveGrace before follower target replacement to suppress desync.
Emergency flatten circuit-breaker
src/V12_002.Trailing.cs
Added FlattenAttemptCount circuit breaker capped at 3 consecutive attempts; increments on failed submissions, resets on success.
System state management
src/V12_002.REAPER.cs, src/V12_002.SIMA.cs
In REAPER, mark repair in-flight before enqueuing to prevent double-enqueue; added early abort if flatten is running. In SIMA, added queue-draining on disable/flatten to clear pending dispatches.
PositionInfo metadata
src/V12_002.cs
Added PendingCleanup (bool) field for deferred position removal and FlattenAttemptCount (int) field for circuit-breaker tracking.

Sequence Diagram

sequenceDiagram
    participant Caller as Entry Path
    participant SubmitOrder as SubmitOrderUnmanaged
    participant StateMgr as State Manager
    participant Lock as stateLock
    participant Pos as activePositions / entryOrders

    Caller->>SubmitOrder: Submit order with entryName & position
    activate SubmitOrder
    SubmitOrder-->>Caller: entryOrder (null or valid)
    deactivate SubmitOrder

    alt entryOrder is null
        Caller->>Caller: Log [ENTRY_ABORT]
        Caller->>Caller: Return early (no state mutation)
    else entryOrder is valid
        Caller->>Lock: Acquire stateLock
        activate Lock
        Lock->>StateMgr: Begin atomic update
        StateMgr->>Pos: Update activePositions[entryName]
        StateMgr->>Pos: Update entryOrders[entryName]
        StateMgr-->>Lock: State synchronized
        Lock->>Caller: Release lock
        deactivate Lock
        Caller->>Caller: Continue with visual confirmation & tracking
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • mkalhitti-cloud/universal-or-strategy#29: Applies the same thread-safety remediation pattern (stateLock wrapping and null-abort handling) to entry submission paths, specifically in RETEST.cs module.

Suggested labels

Core Strategy, Orders / Callbacks

Poem

🐰 Locks now guard our treasured state,
No more races, no more wait—
Null aborts keep paths true,
Atomically, we're through and through!
Cleanup deferred, circuits reign,
Order flows without refrain. 🎯

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: Phase 2 Omni-Audit remediation addressing 9 specific defects (A1-1..A3-3) across concurrency, FSM, null safety, and queue handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/phase2-omni-audit-remediation

Comment @coderabbitai help to get the list of available commands and usage tips.

@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Mar 8, 2026

DeepSource Code Review

We reviewed changes in 5763f19...67e94d0 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade  

Focus Area: Complexity
Security  

Reliability  

Complexity  

Hygiene  

Feedback

  • Monolithic handler functions accumulating business rules
    • Several large handlers cram routing, validation and state changes into one method, creating many branches and duplicated logic; extract focused domain services and small pure helpers so each path is simple and testable.
  • Implicit state passed through flags and parameters
    • Behavior toggles via numerous booleans and scattered params, producing combinatorial branching across handlers; consolidate into an explicit state object or small state machine/strategy to make choices explicit and reduce branches.
  • Error handling intertwines with the happy path
    • Guards, retries and error branches are mixed into core logic, nesting and raising cyclomatic counts; isolate validation/error handling (early returns or result types) so the main flow is linear and conditional complexity falls.

Code Review Summary

Analyzer Status Updated (UTC) Details
C# Mar 8, 2026 10:57p.m. Review ↗
Secrets Mar 8, 2026 10:57p.m. Review ↗

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 8, 2026

🤖 Antigravity Auto-Auditor (Gemini 2.5 Pro via Vertex AI)

Accessing GEMINI.md Standards... V12.Phase7 Protocols Engaged.
Initializing Antigravity Auto-Auditor... Scan Commencing.
PR diff analysis initiated.

AUDIT REPORT

Subject: PR Audit (Build 960 Hardening Pass)
Status: PASSED
Disposition: Recommend Immediate Merge.

This pull request represents a comprehensive and exceptionally high-quality hardening of the strategy's core concurrency and state management logic. The changes demonstrate a deep understanding of potential race conditions and strictly adhere to the project's GEMINI.md standards.


Systematic Review:

1. V12_002.Entries.*.cs (FFMA, MOMO, OR, RMA, TREND)

  • Standard: Concurrency: All state mutations (activePositions, expectedPositions) MUST be guarded by lock(stateLock).
  • Analysis: The previous implementation mutated activePositions and entryOrders before the order was submitted and without a lock. This PR corrects this critical flaw across all entry types. The mutations are now performed atomically inside a lock(stateLock) block, and only after SubmitOrderUnmanaged returns a non-null Order object.
  • Verdict: PASS. This change perfectly aligns with the Concurrency standard, eliminating a significant race condition where the strategy's state could diverge from the broker's if an order submission failed.

2. V12_002.Orders.Callbacks.cs

  • Standard: FSM Required, Concurrency.
  • Analysis (A1-3): The Follower Replace FSM state transition (fsm.State = FollowerReplaceState.Submitting) and the snapshotting of its pending values are now wrapped in lock(stateLock). This correctly closes a Time-of-check-to-time-of-use (TOCTOU) vulnerability where a background thread could read torn state.
  • Analysis (A2-2): The introduction of the PendingCleanup flag is a sophisticated and robust improvement. Instead of immediately removing a position from activePositions upon a final target fill, the removal is now deferred until the broker confirms the corresponding stop order has been cancelled. The final activePositions.TryRemove is correctly performed inside a lock(stateLock). This prevents numerous race conditions related to state cleanup.
  • Analysis (A2-3): The rollback of expectedPositions for a cancelled follower entry has been moved from the point of cancellation request to the broker's OnAccountOrderUpdate confirmation. This correctly prevents a REAPER desync if the order were to fill microseconds before the cancel request was processed.
  • Verdict: PASS. These changes represent a major architectural improvement in state management, demonstrating a mature approach to handling asynchronous broker feedback.

3. V12_002.Orders.Management.cs & V12_002.Trailing.cs

  • Standard: Concurrency.
  • Analysis: All mutations to the shared stopOrders dictionary are now correctly wrapped in lock(stateLock).
  • Verdict: PASS. A direct and necessary adherence to the standard.

4. V12_002.Trailing.cs (Circuit Breaker)

  • Standard: General robustness (Implied by all standards).
  • Analysis (A3-3): The introduction of the FlattenAttemptCount circuit breaker is an excellent proactive safety measure. It prevents infinite loops of emergency flatten attempts in the face of persistent broker-side submission failures, halting the automation and flagging the need for manual intervention after 3 failed attempts.
  • Verdict: PASS. Exceeds standards. This is a model example of defensive programming required for live trading systems.

5. V12_002.Symmetry.cs

  • Standard: Concurrency, REAPER integration.
  • Analysis (A1-1, A1-2): Mutations to pos.EntryFilled and follower order dictionaries are now correctly locked. The call to StampReaperMoveGrace() before cancelling an order during a replacement operation is a clever and correct way to suppress false desync alerts from the REAPER audit system.
  • Verdict: PASS. Shows a holistic understanding of how different system components (State, REAPER) interact.

6. V12_002.REAPER.cs & V12_002.SIMA.cs

  • Standard: Concurrency, Zero-Trust IPC.
  • Analysis (A3-1, A3-2): Race conditions in the REAPER repair queue and SIMA dispatch queue have been resolved by adding safety checks (isFlattenRunning) and correctly timing state-marking operations (_repairInFlight) to occur before asynchronous events are triggered. Draining queues on shutdown prevents ghost operations on restart.
  • Verdict: PASS. These changes prevent invalid state transitions and resource contention during critical operations like global flattens or system shutdown.

Final Verdict

The changes are surgically precise, well-documented with comments explaining the rationale (A1-1, A2-2, etc.), and show an uncompromising commitment to the GEMINI.md standards. The PR addresses multiple subtle but critical race conditions, significantly enhancing the strategy's stability and reliability.

This submission passes the Zero-Regression Bot Audit. No violations were found.

Antigravity Auto-Auditor: Scan Complete.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 8, 2026

────────────────────────────────────────────────────────────────────────────────
Update git name with: git config user.name "Your Name"
Update git email with: git config user.email "you@example.com"
You can skip this check with --no-gitignore
Added .aider* to .gitignore
Aider v0.86.2
Main model: gpt-4o with diff edit format
Weak model: gpt-4o-mini
Git repo: .git with 216 files
Repo-map: using 4096 tokens, auto refresh

https://aider.chat/HISTORY.html#release-notes

Initial repo scan can be slow in larger repos, but only happens once.
Repo-map can't include
/home/runner/work/universal-or-strategy/universal-or-strategy/AntigravityMobile
Has it been deleted from the file system but not from git?
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 0.2 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 0.5 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 1.0 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 2.0 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 4.0 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 8.0 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 16.0 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.
Retrying in 32.0 seconds...
litellm.RateLimitError: RateLimitError: OpenAIException - You exceeded your
current quota, please check your plan and billing details. For more information
on this error, read the docs:
https://platform.openai.com/docs/guides/error-codes/api-errors.
The API provider has rate limited you. Try again later or check your quotas.

https://platform.openai.com/docs/guides/error-codes/api-errors

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67e94d0872

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/V12_002.REAPER.cs
Comment on lines +533 to +536
if (isFlattenRunning)
{
Print("[REAPER REPAIR] Aborted -- flatten in progress.");
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear in-flight repair flag when flatten guard aborts

ExecuteReaperRepair now returns before entering the inner try/finally, but _repairInFlight is set earlier in AuditApexPositions before enqueueing the repair. If a queued repair is processed while isFlattenRunning is true, this early return leaves the key permanently in _repairInFlight, and subsequent audits skip that account (_repairInFlight.Contains(repairKey)), so follower desync repairs can be blocked indefinitely after flatten completes.

Useful? React with 👍 / 👎.

Comment on lines 1085 to +1088
RequestStopCancelLifecycleSafe(entryName);
activePositions.TryRemove(entryName, out _);
SymmetryGuardForgetEntry(entryName);
PositionInfo closedPos;
if (activePositions.TryGetValue(entryName, out closedPos) && closedPos != null)
closedPos.PendingCleanup = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Purge closed entries when stop is already terminal

This branch always defers cleanup by setting PendingCleanup, but RequestStopCancelLifecycleSafe immediately removes the stop reference when the stop is already terminal (Cancelled/Filled/Rejected/Unknown). In that timing, no later stop-terminal callback can match the order and run the deferred purge, so activePositions can retain zero-quantity stale entries indefinitely, which can pollute later symmetry/reaper decisions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Phase 2 Omni-Audit remediations across the V12 strategy, focusing on hardening concurrency/state mutation rules, tightening REAPER/SIMA lifecycle behavior, and deferring metadata cleanup until broker-confirmed terminal states.

Changes:

  • Wraps strategy state mutations (notably activePositions/entryOrders/stopOrders) under lock(stateLock) and adds null-abort rollback patterns across multiple entry paths.
  • Adds deferred cleanup via PositionInfo.PendingCleanup and routes final purges to broker-confirmed stop terminal/cancel handlers.
  • Hardens queue/thread behavior (SIMA dispatch draining/aborts, REAPER repair in-flight gating) and adds a stop-submit emergency-flatten circuit breaker.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/V12_002.cs Extends PositionInfo with PendingCleanup and FlattenAttemptCount to support deferred purge + circuit breaker behavior.
src/V12_002.Trailing.cs Locks stopOrders writes, adds emergency-flatten circuit breaker, stamps REAPER grace before follower target replace.
src/V12_002.Symmetry.cs Stamps REAPER grace before follower target cancel/replace; locks follower replace dict mutation; moves EntryFilled write under stateLock; defers delta rollback.
src/V12_002.SIMA.cs Drains/aborts pending fleet dispatch queue when SIMA disabled or flattening; drains queue on SIMA shutdown.
src/V12_002.REAPER.cs Moves _repairInFlight marking earlier; adds flatten guard at start of repair execution; adjusts in-flight comment/flow.
src/V12_002.Orders.Management.cs Wraps stopOrders mutations under stateLock in bracket/stop creation paths.
src/V12_002.Orders.Callbacks.cs Adds deferred PendingCleanup purge on stop terminal; snapshots FSM fields under stateLock; defers follower expected-position rollback to confirmed cancel; sets PendingCleanup on close/trim.
src/V12_002.Entries.Trend.cs Applies null-abort rollback and stateLock wraps for TREND entries; stages state after submit success.
src/V12_002.Entries.RMA.cs Applies null-abort + stateLock wraps for RMA entries; adjusts TrendSplit submit handling.
src/V12_002.Entries.OR.cs Applies null-abort rollback and stateLock wraps for OR entries; stages state after submit success.
src/V12_002.Entries.MOMO.cs Applies null-abort rollback and stateLock wraps for MOMO entries; stages state after submit success.
src/V12_002.Entries.FFMA.cs Applies null-abort handling and stateLock wraps for FFMA entries; stages state after submit success.

Comment thread src/V12_002.Trailing.cs
Comment on lines +634 to +638
newStop = pos.ExecutingAccount.CreateOrder(Instrument, pos.Direction == MarketPosition.Long ? OrderAction.Sell : OrderAction.BuyToCover,
OrderType.StopMarket, TimeInForce.Gtc, pos.RemainingContracts, 0, validatedStopPrice, "Stop_" + entryName, "Stop_" + entryName, null);
pos.ExecutingAccount.Submit(new[] { newStop });
stopOrders[entryName] = newStop;
// A1-1: stopOrders mutation inside stateLock (Build 960 audit fix)
lock (stateLock) { stopOrders[entryName] = newStop; }
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the follower-account path, CreateOrder(...) can return null, but Submit(new[] { newStop }) is called unconditionally. If newStop is null this will throw before the later newStop == null handling, leaving the position unprotected and bypassing the new circuit-breaker logic. Add a null-guard before Submit and route the failure through the existing null-stop handling path.

Copilot uses AI. Check for mistakes.
Comment thread src/V12_002.Trailing.cs
Comment on lines +662 to +672
// A3-3: Circuit breaker -- cap consecutive flatten attempts to 3 (Build 960 audit fix)
PositionInfo cbPos;
if (activePositions.TryGetValue(entryName, out cbPos) && cbPos != null)
{
cbPos.FlattenAttemptCount++;
if (cbPos.FlattenAttemptCount > 3)
{
Print(string.Format("[CIRCUIT BREAKER] Emergency flatten halted after 3 consecutive failures for {0}. Manual intervention required.", entryName));
return;
}
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlattenAttemptCount is incremented/read without lock(stateLock) / Interlocked, but PositionInfo is explicitly mutated from multiple threads elsewhere (e.g., RemainingContracts is volatile). As written, the counter can race (lost increments / stale reads), making the circuit-breaker unreliable. Protect FlattenAttemptCount with Interlocked.Increment/Volatile.Read+Volatile.Write, or mutate/read it under lock(stateLock) consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +131
// A1-1/A2-1: Null-abort + stateLock wrap for E2 (Build 960 audit fix)
if (entryOrder2 == null)
{
Print("[ENTRY_ABORT] TrendSplit E2 SubmitOrderUnmanaged returned null for " + entry2Name + ". Rolling back.");
// E1 already submitted -- log but continue without E2 tracking
}
else
{
lock (stateLock) { activePositions[entry2Name] = pos2; entryOrders[entry2Name] = entryOrder2; }
masterEntryNames.Add(entry2Name);
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entryOrder2 == null is logged but the method continues using the original qty15/finalTotalQty and can still call ExecuteSmartDispatchEntry(...). This can dispatch follower quantity that includes the missing EMA15 leg, creating a master/follower mismatch and potential REAPER repair loop. On E2 submit failure, either (a) cancel/tear down E1 and abort, or (b) adjust quantities and skip SIMA dispatch for the missing leg (and avoid registering/linking E2 metadata).

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +104
if (entryOrder1 == null)
{
Print("[ENTRY_ABORT] TrendSplit E1 SubmitOrderUnmanaged returned null for " + entry1Name + ". Rolling back.");
return;
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log message says "Rolling back" but there is no rollback action in this branch (no expectedPositions reservation and no state to undo). Either remove the rollback wording or add the actual rollback/cleanup that’s intended.

Copilot uses AI. Check for mistakes.
Comment on lines +401 to +405
PositionInfo cleanupPos;
if (activePositions.TryGetValue(kvp.Key, out cleanupPos) && cleanupPos != null
&& cleanupPos.PendingCleanup && cleanupPos.RemainingContracts <= 0)
{
lock (stateLock) { activePositions.TryRemove(kvp.Key, out _); }
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deferred purge check reads cleanupPos.PendingCleanup (non-volatile) and cleanupPos.RemainingContracts outside lock(stateLock). If these fields can change concurrently, the purge decision can be inconsistent (missed purge or premature purge). Snapshot the fields under lock(stateLock) (or make PendingCleanup volatile and use Volatile.Read) before deciding to remove.

Suggested change
PositionInfo cleanupPos;
if (activePositions.TryGetValue(kvp.Key, out cleanupPos) && cleanupPos != null
&& cleanupPos.PendingCleanup && cleanupPos.RemainingContracts <= 0)
{
lock (stateLock) { activePositions.TryRemove(kvp.Key, out _); }
bool shouldPurge = false;
PositionInfo cleanupPos;
lock (stateLock)
{
if (activePositions.TryGetValue(kvp.Key, out cleanupPos) && cleanupPos != null
&& cleanupPos.PendingCleanup && cleanupPos.RemainingContracts <= 0)
{
activePositions.TryRemove(kvp.Key, out _);
shouldPurge = true;
}
}
if (shouldPurge)
{

Copilot uses AI. Check for mistakes.
Comment on lines +677 to +681
PositionInfo _scPos;
if (activePositions.TryGetValue(_sc.Key, out _scPos) && _scPos != null
&& _scPos.PendingCleanup && _scPos.RemainingContracts <= 0)
{
lock (stateLock) { activePositions.TryRemove(_sc.Key, out _); }
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deferred purge race on the follower-stop terminal path: PendingCleanup/RemainingContracts are read outside synchronization before deciding to purge activePositions. Use the same snapshot/synchronization approach here as in the master-stop cancel path.

Suggested change
PositionInfo _scPos;
if (activePositions.TryGetValue(_sc.Key, out _scPos) && _scPos != null
&& _scPos.PendingCleanup && _scPos.RemainingContracts <= 0)
{
lock (stateLock) { activePositions.TryRemove(_sc.Key, out _); }
bool _purged = false;
lock (stateLock)
{
PositionInfo _scPos;
if (activePositions.TryGetValue(_sc.Key, out _scPos) && _scPos != null
&& _scPos.PendingCleanup && _scPos.RemainingContracts <= 0)
{
activePositions.TryRemove(_sc.Key, out _);
_purged = true;
}
}
if (_purged)
{

Copilot uses AI. Check for mistakes.
Comment thread src/V12_002.REAPER.cs
// A3-2: Mark in-flight BEFORE TriggerCustomEvent to block double-enqueue in next audit cycle (Build 960 audit fix)
lock (stateLock) { _repairInFlight.Add(repairKey); }
_reaperRepairQueue.Enqueue(acct.Name);
try { TriggerCustomEvent(o => ProcessReaperRepairQueue(), null); } catch { }
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_repairInFlight is set before scheduling TriggerCustomEvent, but if TriggerCustomEvent(...) throws, the code swallows the exception and never clears _repairInFlight. That can permanently block future repairs for this repairKey (the audit loop will see it as already in-flight). Clear _repairInFlight (and optionally the queued item) in the catch path, or only set the in-flight flag after TriggerCustomEvent is successfully scheduled.

Suggested change
try { TriggerCustomEvent(o => ProcessReaperRepairQueue(), null); } catch { }
try
{
TriggerCustomEvent(o => ProcessReaperRepairQueue(), null);
}
catch (Exception ex)
{
// If scheduling fails, clear the in-flight guard so future audits can re-enqueue repairs.
lock (stateLock)
{
_repairInFlight.Remove(repairKey);
}
if (shouldLog)
Print($"[REAPER] ERROR: Failed to schedule repair via TriggerCustomEvent for {acct.Name}: {ex.Message}");
}

Copilot uses AI. Check for mistakes.
Comment thread src/V12_002.REAPER.cs
Comment on lines +532 to 539
// A3-2: Abort immediately if a flatten is in progress (Build 960 audit fix)
if (isFlattenRunning)
{
Print("[REAPER REPAIR] Aborted -- flatten in progress.");
return;
}

string repairKey = accountName + "_" + Instrument.FullName;
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExecuteReaperRepair returns early when isFlattenRunning is true, but _repairInFlight for this account was already set on the background thread. Because this return happens before the inner try/finally that removes the flag, the in-flight guard can remain set indefinitely and block all subsequent repairs. Ensure this early-abort path removes _repairInFlight for repairKey (or defer setting it until after the flatten guard).

Suggested change
// A3-2: Abort immediately if a flatten is in progress (Build 960 audit fix)
if (isFlattenRunning)
{
Print("[REAPER REPAIR] Aborted -- flatten in progress.");
return;
}
string repairKey = accountName + "_" + Instrument.FullName;
string repairKey = accountName + "_" + Instrument.FullName;
// A3-2: Abort immediately if a flatten is in progress (Build 960 audit fix)
if (isFlattenRunning)
{
Print("[REAPER REPAIR] Aborted -- flatten in progress.");
// REAPER Safety: ensure any pre-set in-flight guard is cleared on early abort
_repairInFlight.TryRemove(repairKey, out _);
return;
}

Copilot uses AI. Check for mistakes.
Comment thread src/V12_002.Trailing.cs
Comment on lines +699 to +709
// A3-3: Circuit breaker -- cap consecutive flatten attempts to 3 (Build 960 audit fix)
PositionInfo exCbPos;
bool flattenBlocked = false;
if (activePositions.TryGetValue(entryName, out exCbPos) && exCbPos != null)
{
Print(string.Format("(!) Attempting emergency flatten for {0}...", entryName));
FlattenPositionByName(entryName);
exCbPos.FlattenAttemptCount++;
if (exCbPos.FlattenAttemptCount > 3)
{
Print(string.Format("[CIRCUIT BREAKER] Emergency flatten halted after 3 consecutive failures for {0}. Manual intervention required.", entryName));
flattenBlocked = true;
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same race as above in the exception path: exCbPos.FlattenAttemptCount++ and the threshold check are not synchronized, so the circuit-breaker behavior can be inconsistent under concurrent access. Use the same synchronization approach (Interlocked or lock(stateLock)) here as in the non-exception path.

Copilot uses AI. Check for mistakes.
Comment on lines +1087 to +1089
if (activePositions.TryGetValue(entryName, out closedPos) && closedPos != null)
closedPos.PendingCleanup = true;
else
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PendingCleanup is written without lock(stateLock)/Volatile.Write, but it is later read in other handlers to decide whether to purge metadata. To avoid visibility races (and to keep the stateLock discipline consistent with other PositionInfo mutations), set PendingCleanup under lock(stateLock) or make the field volatile and use Volatile.Read/Write at all access sites.

Suggested change
if (activePositions.TryGetValue(entryName, out closedPos) && closedPos != null)
closedPos.PendingCleanup = true;
else
lock (stateLock)
{
activePositions.TryGetValue(entryName, out closedPos);
if (closedPos != null)
{
closedPos.PendingCleanup = true;
}
}
if (closedPos == null)

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 8, 2026

Greptile Summary

This PR remediates 9 defects (A1-1 through A3-3) identified in a concurrency/safety audit across the NinjaTrader V12 strategy: it adds lock(stateLock) guards around all activePositions, entryOrders, and stopOrders mutations; defers activePositions.TryRemove to broker-confirmed terminal state via a new PendingCleanup flag; atomically snapshots FSM state before transitions; stamps the REAPER grace window before cancel-resubmit gaps; and adds a circuit breaker capping consecutive emergency flatten attempts.

Key changes:

  • A1-1 / A2-1 (FFMA, MOMO, OR, RMA, Trend, Management, Trailing): All entry-order and stop-order writes moved inside lock(stateLock) and deferred to after a null-check, ensuring state is never written for an order that was not successfully submitted.
  • A1-2 (Symmetry, Trailing): StampReaperMoveGrace() called before target/stop cancel to suppress false desync during the cancel-resubmit gap.
  • A1-3 (Orders.Callbacks): PendingQty, PendingPrice, and FSM state transition captured atomically under lock(stateLock) before TriggerCustomEvent.
  • A2-2 (Orders.Callbacks, V12_002.cs): PositionInfo gains PendingCleanup; activePositions.TryRemove deferred to the broker-confirmed stop cancel handler in both master (HandleOrderCancelled) and follower (OnAccountOrderUpdate) paths.
  • A2-3 (Symmetry, Orders.Callbacks): Direction-aware delta rollback deferred from SymmetryGuardCascadeFollowerCleanup to the confirmed-cancel handler to prevent REAPER desync on fill races.
  • A3-1 (SIMA): PumpFleetDispatch drains queue and aborts early when SIMA is disabled or flatten is running; queue also drained during ApplySimaState(false) shutdown.
  • A3-2 (REAPER): _repairInFlight.Add(repairKey) moved to the background thread before TriggerCustomEvent; a new isFlattenRunning guard added at the top of ExecuteReaperRepair. However, moving the Add out of the function creates a leak: all early returns inside ExecuteReaperRepair (including the new guard and the existing guards for null position, price fence, etc.) now exit without clearing _repairInFlight, permanently blocking future repair attempts for the affected account.
  • A3-3 (Trailing, V12_002.cs): FlattenAttemptCount field added to PositionInfo; circuit breaker caps consecutive emergency flattens at 3 and resets on a successful stop submission.

Confidence Score: 2/5

  • Not safe to merge — a regression in the REAPER A3-2 fix can permanently disable account repairs, and the A2-3 delta rollback can silently drop, both of which can lead to stuck desync states in production.
  • The vast majority of the per-file changes (A1-1, A1-2, A1-3, A2-2, A3-1, A3-3) are well-structured and address the stated defects correctly. However, the A3-2 change introduces a regression where every early-return path in ExecuteReaperRepair — including the new isFlattenRunning guard and five pre-existing guard returns — leaks _repairInFlight entries, permanently preventing the REAPER from re-attempting repairs for affected accounts without a strategy restart. In a live trading context this is a high-severity defect. Additionally, the A2-3 delta rollback can be silently skipped, causing persistent REAPER desync loops.
  • src/V12_002.REAPER.cs requires close attention for the _repairInFlight leak regression. src/V12_002.Orders.Callbacks.cs needs the silent-skip case addressed in the A2-3 delta rollback. src/V12_002.Entries.Trend.cs should align its E2 failure handling with the graceful pattern used in the RMA TrendSplit path.

Important Files Changed

Filename Overview
src/V12_002.REAPER.cs A3-2 fix moves _repairInFlight.Add to the background thread before TriggerCustomEvent, but ExecuteReaperRepair still only clears the key inside the inner try/finally (line 702). The new isFlattenRunning early return at line 536 exits before both repairKey is declared and before the cleanup block, permanently leaking the in-flight entry and hard-blocking all future REAPER repairs for that account.
src/V12_002.Orders.Callbacks.cs A1-3 FSM snapshot atomicity fix and A2-2 deferred PendingCleanup purge look correct. The A2-3 delta rollback is valid but silently no-ops if the position is already absent from activePositions, which can happen under the new concurrent cleanup paths; a missing-position log guard would make this detectable.
src/V12_002.Entries.Trend.cs Null-abort + stateLock pattern applied consistently for E1, E2, and TRENDManual. E2 null path returns early leaving E1 tracked but never fleet-dispatched and linkedTRENDEntries holding stale references — inconsistent with the graceful E2-skip pattern used in the analogous RMA TrendSplit path.
src/V12_002.SIMA.cs A3-1: PumpFleetDispatch gains a hard guard that drains the pending queue when SIMA is disabled or flatten is running. Queue also drained on ApplySimaState(false). Both changes are correct and symmetric.
src/V12_002.Symmetry.cs StampReaperMoveGrace called before cancel in both target-replace and trailing stop-move paths (A1-2). pos.EntryFilled moved inside stateLock (A1-1). _followerReplaceSpecs dict write wrapped in stateLock. DeltaExpectedPositionLocked correctly deferred to confirmed-cancel. All changes appear correct.
src/V12_002.Trailing.cs All stopOrders mutations wrapped in stateLock. A3-3 circuit breaker for FlattenAttemptCount applied in both null-stop and catch paths, with counter reset on success. Logic is correct.

Sequence Diagram

sequenceDiagram
    participant BG as REAPER Background Thread
    participant ST as Strategy Thread (ExecuteReaperRepair)
    participant IFM as _repairInFlight (ConcurrentDictionary)

    Note over BG: Audit detects flat account with expected ≠ 0
    BG->>IFM: lock(stateLock) { Add(repairKey) }  [A3-2 NEW]
    BG->>BG: _reaperRepairQueue.Enqueue(acct.Name)
    BG->>ST: TriggerCustomEvent → ProcessReaperRepairQueue

    alt isFlattenRunning == true  [NEW GUARD]
        ST-->>ST: return early (line 536)
        Note over IFM: repairKey NEVER removed — permanent leak
    else repairPos == null  [pre-existing guard]
        ST-->>ST: return early (line 571)
        Note over IFM: repairKey NEVER removed — permanent leak
    else price fence / distance exceeded  [pre-existing guards]
        ST-->>ST: return early (lines 608–642)
        Note over IFM: repairKey NEVER removed — permanent leak
    else all guards pass
        ST->>ST: enter inner try block (line 645)
        ST->>ST: submit repair order
        ST->>IFM: finally: Remove(repairKey)  ✓ cleaned up
    end

    Note over BG: Next audit cycle
    BG->>IFM: Contains(repairKey) → true
    BG-->>BG: "repair already in-flight -- skipping" (infinite skip)
Loading

Last reviewed commit: 67e94d0

Comment on lines 246 to 251
if (entryOrder2 == null)
{
AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaE2);
Print("[ERROR][1102Y-V3] TREND E2 SubmitOrderUnmanaged NULL for " + entry2Name + " -- rolled back.");
Print("[ENTRY_ABORT] TREND E2 SubmitOrderUnmanaged NULL for " + entry2Name + " -- rolled back.");
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E2 null-abort leaves E1 tracked but never fleet-dispatched

When entryOrder2 == null, the function returns immediately at line 250. At this point:

  • activePositions[entry1Name] and entryOrders[entry1Name] are already set (line 234).
  • linkedTRENDEntries[entry1Name] = entry2Name and linkedTRENDEntries[entry2Name] = entry1Name are already set (lines 215-216).
  • The call to ExecuteSmartDispatchEntry(entry1Name, masterEntryNames, ...) that follows line 252 is never reached.

This leaves E1 as a live master position in activePositions with a stale forward-link to a non-existent E2, and without a fleet dispatch. The REAPER audit will eventually detect the follower accounts as flat (expected ≠ 0) and attempt repairs indefinitely.

By contrast, the analogous TrendSplit path in RMA.cs handles E2 failure gracefully — it logs, skips E2 from masterEntryNames, and lets ExecuteSmartDispatchEntry proceed with only E1:

if (entryOrder2 == null)
{
    AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaE2);
    Print("[ENTRY_ABORT] TREND E2 SubmitOrderUnmanaged NULL for " + entry2Name + " -- rolled back.");
    // Do NOT return -- E1 already submitted; continue dispatch with E1-only list
}
else
{
    lock (stateLock) { activePositions[entry2Name] = pos2; entryOrders[entry2Name] = entryOrder2; }
    masterEntryNames.Add(entry2Name);
}

The linkedTRENDEntries stale entries should also be cleaned up if E2 is skipped.

Comment thread src/V12_002.REAPER.cs
Comment on lines +532 to +537
// A3-2: Abort immediately if a flatten is in progress (Build 960 audit fix)
if (isFlattenRunning)
{
Print("[REAPER REPAIR] Aborted -- flatten in progress.");
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_repairInFlight permanently leaked on early return

The isFlattenRunning guard exits before repairKey is declared (line 539) and before the inner try/finally cleanup block (line 702). Because the background thread already called _repairInFlight.Add(repairKey) at line 289 before calling TriggerCustomEvent, this early return leaves the key permanently in the set with no cleanup path.

The same leak affects every other early return inside the outer try block that precedes the inner try/finally at line 645 — specifically the guards for repairPos == null (line 571), !TryGetRepairDistanceLimitPoints (line 608), hardBoundDiff > repairLimitPoints (line 616), the market-order price fence (line 633), and targetAcct == null (line 642).

After any of these exits, _repairInFlight.Contains(repairKey) returns true forever. The REAPER audit loop skips enqueuing when the key is present (line 307 log: "repair already in-flight"), so the affected account is silently blocked from ever being repaired again — even after a flatten completes — until the strategy is restarted.

Before this PR the Add happened inside ExecuteReaperRepair at the old step-4 position, after all guards, so every early return was clean. Moving the Add to the background thread introduced this gap.

The fix is to move repairKey above the isFlattenRunning check and wrap the entire method body (not just the inner repair block) in a top-level try/finally that always calls _repairInFlight.Remove(repairKey). The inner try/finally at line 645 can then be folded into the outer one.

Comment on lines +621 to +631
// A2-3: Direction-aware delta rollback on CONFIRMED cancel -- deferred from SymmetryGuardCascadeFollowerCleanup
// to prevent REAPER desync on microsecond fill race (Build 960 audit fix).
PositionInfo cancelledFollowerPos;
if (activePositions.TryGetValue(matchedEntry, out cancelledFollowerPos) && cancelledFollowerPos != null)
{
string cancelAcctKey = cancelledFollowerPos.ExecutingAccount != null
? cancelledFollowerPos.ExecutingAccount.Name : Account.Name;
int cancelDelta = (cancelledFollowerPos.Direction == MarketPosition.Long)
? -cancelledFollowerPos.TotalContracts : cancelledFollowerPos.TotalContracts;
DeltaExpectedPositionLocked(ExpKey(cancelAcctKey), cancelDelta);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta rollback silently dropped when position has already left activePositions

The A2-3 rollback is guarded by activePositions.TryGetValue(matchedEntry, ...). If the position was already removed from activePositions before this confirmed-cancel callback fires — for instance by CleanupPosition, an early flatten, or a race between two cancel confirmations — TryGetValue returns false and DeltaExpectedPositionLocked is never called, with no log message.

In the removed SymmetryGuardCascadeFollowerCleanup code (Build 930.1 P1) the rollback used the pos parameter already in scope and was unconditional. Moving it here makes correctness dependent on activePositions still holding the entry at callback time, which is not guaranteed under the new concurrent deferred-cleanup paths added by this same PR.

If the delta is silently skipped, expectedPositions for the follower account remains non-zero while the broker is flat. The REAPER audit will continuously detect a desync and keep enqueuing repairs.

Adding a Print warning in the else branch when the position is not found would at minimum make the silent skip detectable in logs, and allow targeted debugging of any resulting desync loop.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/V12_002.Entries.Trend.cs (1)

214-216: ⚠️ Potential issue | 🟠 Major

Any TREND submit failure still needs a full rollback.

The new null aborts are better, but linkedTRENDEntries is populated before either leg is proven live, so an E2 abort can strand a one-sided TREND pair. Separately, the dual-leg and manual paths only undo the master reservation on null; a thrown SubmitOrderUnmanaged still leaves expectedPositions dirty. Roll back the links, any already-created E1 state, and the reserved delta on every failure after E1 setup.

Also applies to: 227-250, 278-280, 403-417, 442-444

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.Entries.Trend.cs` around lines 214 - 216, linkedTRENDEntries is
populated before both TREND legs are proven live, so partial failures
(SubmitOrderUnmanaged or null aborts) can leave one-sided state and dirty
expectedPositions; fix by deferring writes to linkedTRENDEntries until both legs
are fully created/confirmed, and on any failure after E1 setup (including within
dual-leg and manual paths) undo: remove any entries from linkedTRENDEntries,
delete/rollback any created E1 state, and revert the reserved delta in
expectedPositions (and the master reservation) so no partial reservations
remain; update the failure/exception handling paths around the functions that
perform E1/E2 creation and the SubmitOrderUnmanaged catch blocks to perform
these compensating removals/rollbacks atomically.
src/V12_002.Entries.MOMO.cs (1)

146-160: ⚠️ Potential issue | 🟠 Major

Rollback masterDeltaMOMO when SubmitOrderUnmanaged throws, not only when it returns null.

The reservation happens before submit, but the outer catch only logs. A thrown submit leaves expectedPositions non-zero even though no master order exists, which can cascade into false REAPER state downstream.

Also applies to: 182-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.Entries.MOMO.cs` around lines 146 - 160, The code reserves
masterDeltaMOMO via AddExpectedPositionDeltaLocked before calling
SubmitOrderUnmanaged but only rolls it back if SubmitOrderUnmanaged returns
null; you must also rollback when SubmitOrderUnmanaged throws: wrap the
SubmitOrderUnmanaged call in a try/catch around the place where masterDeltaMOMO
is added (referencing masterDeltaMOMO, AddExpectedPositionDeltaLocked,
SubmitOrderUnmanaged, entryOrder, ExpKey(Account.Name)), and in the catch call
AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaMOMO), log the
exception with context (entryName) and then rethrow or return as the surrounding
error handling expects; apply the same pattern to the other SubmitOrderUnmanaged
usage mentioned (the similar block around the other entry path).
src/V12_002.Symmetry.cs (1)

555-579: ⚠️ Potential issue | 🔴 Critical

Route this target replace through the follower Replace FSM.

This still does a raw Cancel() followed immediately by CreateOrder/Submit(). If the cancel confirm lags, you can end up with overlapping follower targets, which is exactly the ghost-order case the existing _followerReplaceSpecs flow is meant to prevent. Based on learnings: "Follower order cancel+resubmit must use the two-phase Replace FSM with states: PendingCancel -> wait for OnAccountOrderUpdate confirm -> Submitting -> SubmitFollowerReplacement. Use _followerReplaceSpecs dictionary. Never use raw Cancel() followed by Submit()".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.Symmetry.cs` around lines 555 - 579, This code is doing a raw
cancel+create/submit (StampReaperMoveGrace(); pos.ExecutingAccount.Cancel(...);
then CreateOrder/Submit) which can produce overlapping follower targets; instead
route this through the follower Replace FSM using the existing
_followerReplaceSpecs flow: replace the direct call to
pos.ExecutingAccount.Cancel and immediate CreateOrder/Submit in the block where
you call StampReaperMoveGrace() and compute newPrice/GetTargetPrice(), by
creating or updating an entry in _followerReplaceSpecs for the fleetEntryName
with a spec that moves the FSM to PendingCancel, wait for the cancel
confirmation in OnAccountOrderUpdate, transition to Submitting, then invoke
SubmitFollowerReplacement to create/submit the replacement order (keep using
signalName and the rounded price logic from CreateOrder but perform it only in
the SubmitFollowerReplacement step); remove the direct
pos.ExecutingAccount.Submit and the dict[fleetEntryName] = replacement write and
instead let the FSM manage dict/_followerReplaceSpecs lifecycle so only the FSM
updates dict with the live replacement order.
src/V12_002.Orders.Management.cs (1)

521-531: ⚠️ Potential issue | 🔴 Critical

Guard the follower replacement stop before calling Submit().

In CreateNewStopOrder, CreateOrder() can still return null, but the follower path calls Submit(new[] { newStop }) before the later null check. If that happens, the outer catch just logs and returns, so this path never reaches the emergency flatten and can leave the follower naked.

Also applies to: 545-558

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.Orders.Management.cs` around lines 521 - 531, The follower branch
in CreateNewStopOrder calls pos.ExecutingAccount.Submit(new[] { newStop })
without checking if CreateOrder returned null, which can leave a follower naked;
update the logic around activePositions/pos and CreateOrder/CreateOrder(...) so
you capture the return value into newStop, immediately check if newStop is null
before calling pos.ExecutingAccount.Submit, and if null run the same emergency
flatten/error handling used later in the method (log and flatten/cleanup)
instead of submitting; make the identical null-guard change for the second
follower block referenced (lines ~545-558) so both follower paths avoid calling
Submit with a null newStop.
🧹 Nitpick comments (1)
src/V12_002.Trailing.cs (1)

678-686: Redundant stopOrders write on line 686.

The stopOrders[entryName] = newStop assignment at line 686 is executed after a successful stop submission. However, this write already occurred at line 638 (ExecutingAccount path) or line 650 (SubmitOrderUnmanaged path). The code flow reaches line 686 only after passing through one of those earlier assignments.

This is harmless (idempotent write of the same value) but adds unnecessary locking overhead.

Consider removing the redundant write
 // A3-3: Reset circuit breaker counter on successful stop submission
 {
     PositionInfo cbReset;
     if (activePositions.TryGetValue(entryName, out cbReset) && cbReset != null)
         cbReset.FlattenAttemptCount = 0;
 }

-// A1-1: stopOrders final write inside stateLock (Build 960 audit fix)
-lock (stateLock) { stopOrders[entryName] = newStop; }
 pos.CurrentStopPrice = validatedStopPrice;
 pos.CurrentTrailLevel = newTrailLevel;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.Trailing.cs` around lines 678 - 686, Remove the redundant write
to stopOrders inside the stateLock: the assignment lock (stateLock) {
stopOrders[entryName] = newStop; } is unnecessary because stopOrders[entryName]
is already set earlier in the code paths (ExecutingAccount and
SubmitOrderUnmanaged) before reaching this block; delete that locked assignment
but keep the surrounding circuit-breaker reset code
(activePositions.TryGetValue/ cbReset.FlattenAttemptCount = 0) intact to
preserve the A3-3 behavior and the lock usage only where required for other
state mutations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/V12_002.Entries.FFMA.cs`:
- Around line 172-185: The POSITION_ENTERED notification is sent before the
order is created, causing false-positive panel events; move the
SendResponseToRemote("POSITION_ENTERED|FFMA|{0}", contracts) call to after
SubmitOrderUnmanaged and after the null-check of entryOrder (i.e., only call
SendResponseToRemote when entryOrder != null), referencing the
SubmitOrderUnmanaged call, the entryOrder variable and entryName so the
notification is sent only on successful order creation/assignment.

In `@src/V12_002.REAPER.cs`:
- Around line 288-291: The _repairInFlight entry is added under stateLock before
enqueueing but is not always removed if TriggerCustomEvent throws or the new
flatten guard returns early; ensure every code path that aborts submitting a
repair clears the guard. Update the logic around _repairInFlight.Add(repairKey)
(protected by stateLock) and the enqueue/submit sequence (references:
_repairInFlight, stateLock, _reaperRepairQueue.Enqueue(acct.Name),
TriggerCustomEvent(o => ProcessReaperRepairQueue(), null),
ProcessReaperRepairQueue) so that removal (lock(stateLock) {
_repairInFlight.Remove(repairKey); }) happens in a finally or on every
early-return/exception including the flatten-return path and any catch after
TriggerCustomEvent; apply same fix pattern to the other occurrences noted
(around the blocks currently at lines ~532-537 and ~702-705).

In `@src/V12_002.SIMA.cs`:
- Around line 829-834: The shutdown drain currently just dequeues
_pendingFleetDispatches and drops FleetDispatchRequest entries, leaking their
reservation/barrier state; instead, replicate the rollbacking drain used in
PumpFleetDispatch's abort path: while dequeuing each FleetDispatchRequest from
_pendingFleetDispatches, invoke the same rollback/release logic that clears its
reservation from _dispatchSyncPendingExpKeys (the same code path or helper used
by PumpFleetDispatch to abort a request) before discarding it, then log the
shutdown message; reference PumpFleetDispatch, _pendingFleetDispatches, and
_dispatchSyncPendingExpKeys to locate and reuse the exact rollback behavior.
- Around line 640-646: The abort path that drains _pendingFleetDispatches when
isFlattenRunning || !EnableSIMA silently drops requests and skips the finally
that clears per-submit barriers and decrements _pendingFleetDispatchCount;
instead, when dequeuing each stale entry from _pendingFleetDispatches, perform
the same rollback used on submit failure: call
AddExpectedPositionDeltaLocked(...) with the negative of the reserved delta to
return expectedPositions, clear/remove the entry from
_dispatchSyncPendingExpKeys (remove the barrier), and decrement
_pendingFleetDispatchCount accordingly so state stays consistent; ensure this
rollback logic mirrors the Submit exception handling and that any per-submit
cleanup that normally runs in the finally still occurs (or is replicated) for
these drained items.

In `@src/V12_002.Symmetry.cs`:
- Around line 555-556: The call to StampReaperMoveGrace() writes a global REAPER
grace timestamp and must be replaced with a per-account stamp: compute the
follower's expKey via ExpKey(...) and set _accountFillGraceTicks[expKey] to the
appropriate grace value (instead of touching the legacy global stamp). Update
the code that currently calls StampReaperMoveGrace() to use ExpKey(...) and
assign into the _accountFillGraceTicks dictionary/array so REAPER fill-grace is
scoped to that account only.

---

Outside diff comments:
In `@src/V12_002.Entries.MOMO.cs`:
- Around line 146-160: The code reserves masterDeltaMOMO via
AddExpectedPositionDeltaLocked before calling SubmitOrderUnmanaged but only
rolls it back if SubmitOrderUnmanaged returns null; you must also rollback when
SubmitOrderUnmanaged throws: wrap the SubmitOrderUnmanaged call in a try/catch
around the place where masterDeltaMOMO is added (referencing masterDeltaMOMO,
AddExpectedPositionDeltaLocked, SubmitOrderUnmanaged, entryOrder,
ExpKey(Account.Name)), and in the catch call
AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaMOMO), log the
exception with context (entryName) and then rethrow or return as the surrounding
error handling expects; apply the same pattern to the other SubmitOrderUnmanaged
usage mentioned (the similar block around the other entry path).

In `@src/V12_002.Entries.Trend.cs`:
- Around line 214-216: linkedTRENDEntries is populated before both TREND legs
are proven live, so partial failures (SubmitOrderUnmanaged or null aborts) can
leave one-sided state and dirty expectedPositions; fix by deferring writes to
linkedTRENDEntries until both legs are fully created/confirmed, and on any
failure after E1 setup (including within dual-leg and manual paths) undo: remove
any entries from linkedTRENDEntries, delete/rollback any created E1 state, and
revert the reserved delta in expectedPositions (and the master reservation) so
no partial reservations remain; update the failure/exception handling paths
around the functions that perform E1/E2 creation and the SubmitOrderUnmanaged
catch blocks to perform these compensating removals/rollbacks atomically.

In `@src/V12_002.Orders.Management.cs`:
- Around line 521-531: The follower branch in CreateNewStopOrder calls
pos.ExecutingAccount.Submit(new[] { newStop }) without checking if CreateOrder
returned null, which can leave a follower naked; update the logic around
activePositions/pos and CreateOrder/CreateOrder(...) so you capture the return
value into newStop, immediately check if newStop is null before calling
pos.ExecutingAccount.Submit, and if null run the same emergency flatten/error
handling used later in the method (log and flatten/cleanup) instead of
submitting; make the identical null-guard change for the second follower block
referenced (lines ~545-558) so both follower paths avoid calling Submit with a
null newStop.

In `@src/V12_002.Symmetry.cs`:
- Around line 555-579: This code is doing a raw cancel+create/submit
(StampReaperMoveGrace(); pos.ExecutingAccount.Cancel(...); then
CreateOrder/Submit) which can produce overlapping follower targets; instead
route this through the follower Replace FSM using the existing
_followerReplaceSpecs flow: replace the direct call to
pos.ExecutingAccount.Cancel and immediate CreateOrder/Submit in the block where
you call StampReaperMoveGrace() and compute newPrice/GetTargetPrice(), by
creating or updating an entry in _followerReplaceSpecs for the fleetEntryName
with a spec that moves the FSM to PendingCancel, wait for the cancel
confirmation in OnAccountOrderUpdate, transition to Submitting, then invoke
SubmitFollowerReplacement to create/submit the replacement order (keep using
signalName and the rounded price logic from CreateOrder but perform it only in
the SubmitFollowerReplacement step); remove the direct
pos.ExecutingAccount.Submit and the dict[fleetEntryName] = replacement write and
instead let the FSM manage dict/_followerReplaceSpecs lifecycle so only the FSM
updates dict with the live replacement order.

---

Nitpick comments:
In `@src/V12_002.Trailing.cs`:
- Around line 678-686: Remove the redundant write to stopOrders inside the
stateLock: the assignment lock (stateLock) { stopOrders[entryName] = newStop; }
is unnecessary because stopOrders[entryName] is already set earlier in the code
paths (ExecutingAccount and SubmitOrderUnmanaged) before reaching this block;
delete that locked assignment but keep the surrounding circuit-breaker reset
code (activePositions.TryGetValue/ cbReset.FlattenAttemptCount = 0) intact to
preserve the A3-3 behavior and the lock usage only where required for other
state mutations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5dbd6186-e9e5-45a0-9d91-fc423d3e5b8e

📥 Commits

Reviewing files that changed from the base of the PR and between 5763f19 and 67e94d0.

📒 Files selected for processing (12)
  • src/V12_002.Entries.FFMA.cs
  • src/V12_002.Entries.MOMO.cs
  • src/V12_002.Entries.OR.cs
  • src/V12_002.Entries.RMA.cs
  • src/V12_002.Entries.Trend.cs
  • src/V12_002.Orders.Callbacks.cs
  • src/V12_002.Orders.Management.cs
  • src/V12_002.REAPER.cs
  • src/V12_002.SIMA.cs
  • src/V12_002.Symmetry.cs
  • src/V12_002.Trailing.cs
  • src/V12_002.cs

Comment on lines 172 to +185
// V12.13-D: Notify connected panel clients of position entry
string syncMsg = string.Format("POSITION_ENTERED|FFMA|{0}", contracts);
SendResponseToRemote(syncMsg);


// Submit MARKET order (immediate execution)
Order entryOrder = direction == MarketPosition.Long
? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Market, contracts, 0, 0, "", entryName)
: SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Market, contracts, 0, 0, "", entryName);

entryOrders[entryName] = entryOrder;
// A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix)
if (entryOrder == null)
{
Print("[ENTRY_ABORT] FFMA SubmitOrderUnmanaged returned null for " + entryName + ". Rolling back.");
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Delay the POSITION_ENTERED notification until the order is actually created.

SendResponseToRemote("POSITION_ENTERED|FFMA|...") fires before SubmitOrderUnmanaged. On the new abort path, the panel still gets a false-positive entry event even though no order exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.Entries.FFMA.cs` around lines 172 - 185, The POSITION_ENTERED
notification is sent before the order is created, causing false-positive panel
events; move the SendResponseToRemote("POSITION_ENTERED|FFMA|{0}", contracts)
call to after SubmitOrderUnmanaged and after the null-check of entryOrder (i.e.,
only call SendResponseToRemote when entryOrder != null), referencing the
SubmitOrderUnmanaged call, the entryOrder variable and entryName so the
notification is sent only on successful order creation/assignment.

Comment thread src/V12_002.REAPER.cs
Comment on lines +288 to 291
// A3-2: Mark in-flight BEFORE TriggerCustomEvent to block double-enqueue in next audit cycle (Build 960 audit fix)
lock (stateLock) { _repairInFlight.Add(repairKey); }
_reaperRepairQueue.Enqueue(acct.Name);
try { TriggerCustomEvent(o => ProcessReaperRepairQueue(), null); } catch { }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Always clear _repairInFlight when a queued repair is aborted.

The key is now added before queueing, but the new flatten guard returns before the finally that removes it. If a repair is dequeued during flatten, or TriggerCustomEvent throws after the add, that account can stay permanently "in flight" and REAPER will never retry it. Based on learnings: "_repairInFlight guard must be set before and cleared after every REAPER repair submit".

Also applies to: 532-537, 702-705

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.REAPER.cs` around lines 288 - 291, The _repairInFlight entry is
added under stateLock before enqueueing but is not always removed if
TriggerCustomEvent throws or the new flatten guard returns early; ensure every
code path that aborts submitting a repair clears the guard. Update the logic
around _repairInFlight.Add(repairKey) (protected by stateLock) and the
enqueue/submit sequence (references: _repairInFlight, stateLock,
_reaperRepairQueue.Enqueue(acct.Name), TriggerCustomEvent(o =>
ProcessReaperRepairQueue(), null), ProcessReaperRepairQueue) so that removal
(lock(stateLock) { _repairInFlight.Remove(repairKey); }) happens in a finally or
on every early-return/exception including the flatten-return path and any catch
after TriggerCustomEvent; apply same fix pattern to the other occurrences noted
(around the blocks currently at lines ~532-537 and ~702-705).

Comment thread src/V12_002.SIMA.cs
Comment on lines +640 to +646
// A3-1: Abort and drain queue if SIMA is disabled or flatten is running (Build 960 audit fix)
if (isFlattenRunning || !EnableSIMA)
{
FleetDispatchRequest stale;
while (_pendingFleetDispatches.TryDequeue(out stale)) { }
Print("[PUMP] Abort: SIMA inactive or flatten running. Queue drained.");
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Rollback drained dispatches instead of dropping them.

These requests have already reserved expectedPositions, marked _dispatchSyncPendingExpKeys, and registered tracking dictionaries before they reach the queue. This abort path silently discards them, so flatten/disable can leave ghost follower state behind, and _pendingFleetDispatchCount never gets decremented because the early return skips the existing finally.

Proposed fix
+        private void DrainPendingFleetDispatches(string logMessage)
+        {
+            FleetDispatchRequest stale;
+            while (_pendingFleetDispatches.TryDequeue(out stale))
+            {
+                ClearDispatchSyncPending(stale.ExpectedKey);
+                if (stale.ReservedDelta != 0)
+                    AddExpectedPositionDeltaLocked(stale.ExpectedKey, -stale.ReservedDelta);
+
+                activePositions.TryRemove(stale.FleetEntryName, out _);
+                entryOrders.TryRemove(stale.FleetEntryName, out _);
+                stopOrders.TryRemove(stale.FleetEntryName, out _);
+                for (int tNum = 1; tNum <= 5; tNum++)
+                {
+                    var targetDict = GetTargetOrdersDictionary(tNum);
+                    if (targetDict != null)
+                        targetDict.TryRemove(stale.FleetEntryName, out _);
+                }
+
+                Interlocked.Decrement(ref _pendingFleetDispatchCount);
+            }
+
+            Print(logMessage);
+        }
...
-            if (isFlattenRunning || !EnableSIMA)
-            {
-                FleetDispatchRequest stale;
-                while (_pendingFleetDispatches.TryDequeue(out stale)) { }
-                Print("[PUMP] Abort: SIMA inactive or flatten running. Queue drained.");
-                return;
-            }
+            if (isFlattenRunning || !EnableSIMA)
+            {
+                DrainPendingFleetDispatches("[PUMP] Abort: SIMA inactive or flatten running. Queue drained.");
+                return;
+            }

As per coding guidelines, _dispatchSyncPendingExpKeys barriers must be set before and cleared after every fleet submit, and reserved quantities must be rolled back via AddExpectedPositionDeltaLocked(key, -delta) on any fleet Submit exception.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.SIMA.cs` around lines 640 - 646, The abort path that drains
_pendingFleetDispatches when isFlattenRunning || !EnableSIMA silently drops
requests and skips the finally that clears per-submit barriers and decrements
_pendingFleetDispatchCount; instead, when dequeuing each stale entry from
_pendingFleetDispatches, perform the same rollback used on submit failure: call
AddExpectedPositionDeltaLocked(...) with the negative of the reserved delta to
return expectedPositions, clear/remove the entry from
_dispatchSyncPendingExpKeys (remove the barrier), and decrement
_pendingFleetDispatchCount accordingly so state stays consistent; ensure this
rollback logic mirrors the Submit exception handling and that any per-submit
cleanup that normally runs in the finally still occurs (or is replicated) for
these drained items.

Comment thread src/V12_002.SIMA.cs
Comment on lines +829 to +834
// A3-1: Drain ghost dispatch queue on SIMA disable (Build 960 audit fix)
{
FleetDispatchRequest ignored;
while (_pendingFleetDispatches.TryDequeue(out ignored)) { }
Print("[SIMA] Dispatch queue cleared on shutdown.");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the same rollbacking drain on SIMA shutdown.

This block has the same leak as the new abort path in PumpFleetDispatch: queued requests are discarded after their reservation/barrier state was created, so disabling SIMA can carry ghost entries into the next lifecycle.

Proposed fix
-                    {
-                        FleetDispatchRequest ignored;
-                        while (_pendingFleetDispatches.TryDequeue(out ignored)) { }
-                        Print("[SIMA] Dispatch queue cleared on shutdown.");
-                    }
+                    DrainPendingFleetDispatches("[SIMA] Dispatch queue cleared on shutdown.");

Based on learnings, all fleet dispatches must use the _dispatchSyncPendingExpKeys barrier for SIMA Synchronicity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.SIMA.cs` around lines 829 - 834, The shutdown drain currently
just dequeues _pendingFleetDispatches and drops FleetDispatchRequest entries,
leaking their reservation/barrier state; instead, replicate the rollbacking
drain used in PumpFleetDispatch's abort path: while dequeuing each
FleetDispatchRequest from _pendingFleetDispatches, invoke the same
rollback/release logic that clears its reservation from
_dispatchSyncPendingExpKeys (the same code path or helper used by
PumpFleetDispatch to abort a request) before discarding it, then log the
shutdown message; reference PumpFleetDispatch, _pendingFleetDispatches, and
_dispatchSyncPendingExpKeys to locate and reuse the exact rollback behavior.

Comment thread src/V12_002.Symmetry.cs
Comment on lines +555 to +556
// A1-2: Stamp REAPER grace window before cancel to suppress false desync during replace gap (Build 960 audit fix)
StampReaperMoveGrace();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use an account-scoped REAPER grace stamp here.

StampReaperMoveGrace() updates the legacy global timestamp, so one follower retarget can suppress REAPER checks for unrelated accounts. This needs the per-account grace path keyed by the follower's ExpKey(...) instead. As per coding guidelines: "REAPER fill-grace must be per-account via _accountFillGraceTicks[expKey], never a single global timestamp".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/V12_002.Symmetry.cs` around lines 555 - 556, The call to
StampReaperMoveGrace() writes a global REAPER grace timestamp and must be
replaced with a per-account stamp: compute the follower's expKey via ExpKey(...)
and set _accountFillGraceTicks[expKey] to the appropriate grace value (instead
of touching the legacy global stamp). Update the code that currently calls
StampReaperMoveGrace() to use ExpKey(...) and assign into the
_accountFillGraceTicks dictionary/array so REAPER fill-grace is scoped to that
account only.

@mkalhitti-cloud
Copy link
Copy Markdown
Owner Author

Closing this PR to trigger a clean bot audit sweep on the new implementation. Superseded by new PR #31

@mkalhitti-cloud mkalhitti-cloud deleted the fix/phase2-omni-audit-remediation branch April 21, 2026 23:57
mkalhitti-cloud added a commit that referenced this pull request May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants