The MODE-CONTROL feature is implemented: position_manager (PM) is now the single execution authority for the off / dry / live kill switch. All 12 tasks ran via subagent-driven development with per-task opus reviews on the logic tasks and a final whole-branch opus review (verdict: READY TO MERGE, no Critical/Important). Companion to the design spec 2026-06-21-position-manager-mode-control-design.md and follows the prior execution migration in position-manager-interface-design.
For Agents
PM is the kill switch. A buggy/compromised caller cannot trade when PM reads Off/disabled/breaker-tripped. The runner (
polymarket_fetch) is now mode-agnostic: it builds anEntryIntent/ExitIntent, callspm.place_order, and maps the typedExecOutcometo a NATS ack. Mode is resolved fresh per call at the POST boundary.
What it does
PM place_order resolves the authoritative mode fresh per call (global × per-source (mode, enabled), plus the breaker) and gates at the irreversible POST boundary:
- Off →
Skipped— zero work (no reserve, no caps, no signer touch, no row). - Dry → reserve a
dry_runrow + build+sign rehearsal — no POST, no caps, no breaker bump. - Live → reserve a
liverow + caps check → place (the migratedplace_buy/place_sellflow).
The reserve + caps + breaker check moved out of polymarket_fetch’s engine/runner and into PM. The runner is now mode-agnostic: it builds an EntryIntent/ExitIntent, calls pm.place_order, and maps the typed ExecOutcome to the NATS ack — it no longer resolves mode, reserves, checks caps, or checks the breaker.
Key correctness points
- Entries gate on
effective_mode(global, source)(most-restrictive: Off=0 < Dry=1 < Live=2). - Exits gate on the position row’s recorded
mode— a live position stays sellable even when global is flipped Off. This is the E1 bug fix (a live position must remain exitable after a halt). enabled=false ⇒ Offis now enforced (previously a dangling gate that the consumer path discarded).- A pre-POST reserve/caps DB error →
ExecOutcome::Transient→Nak(30s)retry — nothing reserved, so retrying is safe (preserves today’stry_reserve-error → Nak). - Recorded-failure (row already marked failed) and benign-race (
mark_submitted/mark_selling==false, no-signer) → Ack — no double-trade, no redelivery loop. - Fail-closed-to-Off via two paths: a
config::loaderror → Off; a missing/unparseable source row →(Off, false). cap:eventliteral preserved (notcap:max_copies).- Reserve/caps SQL is byte-identical — moved into PM, not rewritten.
ExecOutcome → ack mapping (runner-owned)
ExecOutcome | Ack action |
|---|---|
Skipped(off/breaker/disabled/cap:*) | Ack |
DryRehearsed | Ack |
Placed | Ack |
RecordedFailure(class) | Ack (must NOT redeliver) |
BenignRace(reason) | Ack |
Transient(reason) (pre-POST DB error) | Nak(30s) |
Stale / past valid_until_ts (runner pre-call gate) | Term |
Execution: 12 tasks via subagent-driven development
All 12 tasks ran as subagents with per-task opus reviews on the logic-bearing tasks and a final whole-branch opus review.
- Tasks 1-6 (PM authority side): relocate
mode.rs(effective_mode);resolve_entry_modefail-closed two-path +enabled⇒Off; reserve/caps/4-cap-checks moved byte-identical (Transient path preserved,ledger::Capsreused);EntryIntent/ExitIntent/OrderIntent+ExecOutcome(6 variants incl.Transient); notify strings + control flow byte-identical with oldOrderRequest/Modedeleted; gate order (breaker→off clean-skip), Transient at all 4 pre-POST DB sites, dry rehearsal + no-signer-live byte-identical,cap:eventliteral, 12-fieldReserveSpecmap. - Tasks 7-10 (combined atomic fetch rewire): fetch is non-buildable until the runner is rewired (same shape as migration M2), so done as one commit —
exec_to_consumerack mapping, 16-fieldEntryIntent, all strategy gates preserved, exits gate onorder_id+row.mode,build_autotrade_depsalways-PM. - Task 11: test-assert fixes; both crates build + test green.
- Task 12: db-tests — 7/7 mode scenarios ran vs real Postgres (off/dry/breaker/
cap:event/disabled exactExecOutcome+rows; exit-live not-skipped-on-global-off; exit-dry no-post). Signer-host limit: full live-POST/sell path is not e2e (gate logic fully tested). - Final review (opus, whole-branch): READY TO MERGE — no Critical/Important. Kill switch correct end-to-end: authoritative PM gate, mode-agnostic runner, exhaustive
exec_to_consumerack mapping, exits onrow.mode(E1), fail-closed two paths,cap:event, dry no-breaker,enabled⇒Off, caps live-only.
State
Quick Reference
- PM
main: 351a755 (+11 commits from baseline)- fetch
main: 4fde695- NOT pushed, committed on
mainin both repos- Autotrade mode is OFF — nothing armed
- Gate: 66 PM offline + 532 fetch + 7 db-tests (vs real Postgres) pass
Open items
Open Items
- Commit contamination —
4fde695swept in the user’s unrelated in-progressexit_signal/cohort_exit_signal_idwork. This is more entangled than a simple stray file: the user has uncommittedfirstmover.rs/alert.rs/alerts_consumer.rs/cohort.rsthat callexit_signal. Splitting requires coordinating with that uncommitted work — pending user split/keep decision.- Follow-ups: delete the dead
orders.rs/EngineConfigcap-helpers (currently under#[allow(dead_code)]— PM owns caps now); add a host-injectable signer seam so the live-POST path can be db-tested end-to-end; clear residual fetch pedantic clippy in untouched code.- Pre-arm: live-test that PM’s
notifyArc routes to the autotrade Telegram channel before flipping live (the reviewer could not verify this statically; mode is currently OFF).
Related
- position-manager-interface-design — the prior execution migration this builds on
- position-manager-execution-migration-implemented — the execution-migration outcome record
2026-06-21-position-manager-mode-control-design.md(repo spec/design)