Skip to content

Orion Tasking — FLAG-037: Reconciler Conservative Phantom Fill

To: Orion (he/him) From: Vesper (she/her) CC: Katja (Captain), Atlas (he/him) Date: 2026-04-19 Branch: fix/reconciler-disappeared-order-conservative Priority: HIGH — next in sequence after Directional Drift Guard (merged)


Context

The reconciler's current logic when an order disappears from the open set with no cancel tx: assume fill → apply phantom fill → log action_taken='phantom_fill_applied' to reconciler_anomaly_log.

Post-S41 analysis of the anomaly log revealed two distinct cases:

Case 1 — S41 fills (rows 3–5): Orders 11–19 seconds old. Reconciler correctly detected 3 real fills in the first 24 seconds of session. Phantom fill application was correct. Inventory math closes exactly on-chain.

Case 2 — S38 stale orders (rows 1–2): Orders ~86,000 seconds old (~24 hours). Placed during S38, offers sat live on XRPL ledger during the session gap, consumed by the market overnight. Phantom fill happened to be correct — but only verifiable after the fact. Blindly auto-filling a 24-hour-old order is dangerous.

Atlas ruling (Apr 19): Implement age-based conservative handling. Young orders (< threshold) → current phantom fill behavior. Old orders (≥ threshold) → do NOT auto-fill. Log as held_pending_review. Surface alert. Require resolution.


Spec (Atlas-locked Apr 19)

Age threshold: 300 seconds (configurable in YAML).

Case 1 — Young order (age < age_threshold_seconds): Current behavior unchanged. Apply phantom fill. Log action_taken='phantom_fill_applied' to reconciler_anomaly_log. No change to existing code path.

Case 2 — Old order (age ≥ age_threshold_seconds): - Do NOT apply phantom fill - Do NOT update inventory / fills table - Log action_taken='held_pending_review' to reconciler_anomaly_log with full context (order_id, age_seconds, side, quantity_rlusd, xrp_equivalent) - Emit a WARNING log: [RECONCILER] HELD_PENDING_REVIEW — order_id=<id> age=<age>s ≥ threshold=<threshold>s — manual resolution required - Enter DEGRADED mode via _enter_degraded_mode("reconciler_held_pending_review") — cancel all orders, stop quoting, continue observation. The engine must not continue trading with unresolved stale-order exposure.

One-shot per session: If DEGRADED is already active (e.g., a guard fired first), the held_pending_review log and WARNING still emit — the state record is always written. But _enter_degraded_mode is idempotent and will no-op if already in DEGRADED.


YAML Config Block

Add under strategy: in all three config files:

reconciler_conservative:
  enabled: true
  age_threshold_seconds: 300    # orders older than this are held, not auto-filled

Pre-Code Investigation Required

Before writing any code, investigate and report on the following:

Q1 — Phantom fill decision point. Locate the exact method and line number in ledger_reconciler.py where the current phantom fill decision is made. What is the condition check? What data is available at that point (order dict fields, timestamps)?

Q2 — Order age calculation. What timestamp field is available on the disappeared order that can be used to calculate age? Is it created_at, submitted_at, or something else? What data type (ISO string, Unix float, epoch int)? Confirm how to compute age_seconds = now - order_timestamp.

Q3 — reconciler_anomaly_log write path. Confirm the existing writer method signature for reconciler_anomaly_log. What fields does it accept? Is action_taken a free-text string or an enum? Confirm that adding action_taken='held_pending_review' requires no schema change.

Q4 — _enter_degraded_mode availability. The reconciler runs inside the main loop engine context. Confirm that _enter_degraded_mode is accessible from the reconciler's call site, or whether the held_pending_review signal needs to be returned up the call stack to the main loop for DEGRADED entry. Identify the cleanest pattern given the existing architecture.

Q5 — Config access in reconciler. How does the reconciler currently access config values? Is the config object passed directly, or accessed via the engine? Confirm the pattern so age_threshold_seconds can be wired correctly.

Report findings before writing any code.


Implementation Guidance

Age calculation: Use wall-clock time.time() as "now". Parse the order's timestamp field to a comparable value. If the field is an ISO string, use datetime.fromisoformat() and .timestamp(). Age in seconds = time.time() - order_timestamp_unix.

Guard placement: The age check should wrap the existing phantom fill logic. If age < threshold → current behavior (no change). If age ≥ threshold → held_pending_review path. The threshold check should happen before the phantom fill is applied, not after.

held_pending_review log fields: Same shape as existing reconciler_anomaly_log rows, with action_taken='held_pending_review'. Include order_id, age_seconds, side, quantity_rlusd, xrp_equivalent, and optionally a threshold_seconds context field.

DEGRADED entry: Use reason string "reconciler_held_pending_review". This gives operators immediate context at restart about why the session degraded.

Persist-failure safety: If the reconciler_anomaly_log write fails, swallow the exception (log at ERROR) and still enter DEGRADED. The safety function (stopping trading) must not be blocked by a logging failure. Same pattern as anchor guard and drift guard.

enabled: false path: If the config block is disabled, the age check is skipped entirely — revert to current phantom fill behavior for all orders regardless of age. Useful for testing / debugging without the conservative gate active.


Test Requirements

Minimum 8 tests for this branch:

  1. Young order (age < threshold) → phantom fill applied, no DEGRADED
  2. Old order (age ≥ threshold) → held_pending_review logged, DEGRADED entered, no phantom fill
  3. Old order → WARNING log emitted with correct order_id and age
  4. enabled: false → old order treated as young (current behavior), no held_pending_review
  5. Persist failure on held_pending_review write → swallowed, DEGRADED still entered
  6. Exactly at threshold (age == threshold) → held_pending_review (boundary: ≥ threshold triggers)
  7. Multiple disappeared orders in one tick — one young, one old → phantom fill for young, held_pending_review for old
  8. (Bonus) reconciler_anomaly_log row written with correct action_taken and session_id populated
  9. (Bonus) Windows teardown fix — use addCleanup pattern for any integration tests using real StateManager

Commit Plan (suggested)

  1. feat(config): ReconcilerConservativeConfig dataclass + YAML defaults (FLAG-037)
  2. fix(reconciler): age-based conservative phantom fill — held_pending_review path (FLAG-037)
  3. test(reconciler): conservative phantom fill — 8+ tests (FLAG-037)

Constraints

  • Do not modify the phantom fill path for young orders — current behavior must be preserved exactly
  • held_pending_review must never silently drop — it must always log + DEGRADED, even if DB write fails
  • Parameters configurable in YAML — no hardcoded thresholds
  • Use existing _enter_degraded_mode infrastructure
  • Windows teardown fix required in any integration tests using real StateManager (use addCleanup pattern)
  • No reconciler logic changes beyond the age threshold gate

Deliverable

Standard format: branch name, commit list with hashes and messages, test count and pass rate, pre-code investigation findings, any deviations from spec flagged explicitly. Vesper reviews before merge.

— Vesper