The pmv2 monorepo is being split into independent services/repos by concern — execution (the position_manager, owned by @deploy), signal producers, and a notification service. This session carved the repo down to ONE concern: the cohort-intelligence signal producer (scoring + consensus + first-mover entry + exit detection), deleting ~13,400 lines of everything else. It then added cohort exit signals: a tracked cohort trader selling on-chain now publishes an action:Sell to NATS cohort.signals + alerts, so the downstream executor can close the copied position — closing the loop with the first-mover ENTRY lane.
For Agents — state at a glance
Branch
worktree-cohort-signals, carved frommain@43023ba.
- Carve commit:
8cd6f58— repo reduced to cohort scoring + consensus + first-mover + exit detection; ~13,400 lines deleted; 3 workspace crates dropped (polymarket-clob,pm-ws,lag-probe). Gate green.- Exit-signals commit:
e762f5d—on_cohort_sellnow publishesaction:Selltocohort.signals+ Telegram, in addition to its prior DB bookkeeping. Gate green: polymarket-fetch 153 / polymarket-data 107.- This is the producer half of a producer→executor split over NATS. The executor is @deploy’s
position_manager(position-manager-interface-design, position-manager-execution-migration-implemented). The transport (cohort.signalssubject,Pmv2Consumer) is the lane established in cohort-firstmover-nats-migration + pmv2-position-manager-nats-consumer.- NOT merged, NOT deployed. Introduces a PROVISIONAL cross-service contract (executor must consume
action:Sellidempotently) — still to be coordinated with @deploy.
The re-architecture: monorepo → services by concern
The pmv2 monorepo mixed three unrelated concerns in one repo (and the watcher process mixed them in one binary). The split assigns each concern to an owner:
| Concern | Owner | What it does |
|---|---|---|
| Execution | @deploy’s position_manager crate | build/sign/POST CLOB orders, position ledger, caps + breaker, reconcile, self-wallet watch |
| Signal producers | this repo (cohort-signals) | cohort scoring, consensus, first-mover ENTRY signals, cohort EXIT signals |
| Notification service | (separate, future) | Telegram / operator alerts |
This worktree-cohort-signals branch is the first producer carve: it owns ONLY the cohort-intelligence signal producer. Producers emit TradeSignals onto NATS; the executor consumes them and owns the money-path. Neither side calls the other in-process anymore.
Producer → executor over NATS (post-split topology)
graph TD subgraph PROD["cohort-signals (this repo) — PRODUCER"] SCORE["cohort scoring<br/>Pmv2Collect"] CONS["cohort consensus<br/>Pmv2Consensus cron + ConsensusTracker"] FM["first-mover ENTRY<br/>sources::enter_signal"] EX["cohort EXIT<br/>on_cohort_sell → sources::exit_signal"] end FM -->|"action:Buy"| SUBJ["NATS cohort.signals<br/><i>durable pmv2</i>"] EX -->|"action:Sell"| SUBJ SUBJ --> EXEC["position_manager (@deploy) — EXECUTOR<br/>Pmv2Consumer → money-path<br/>reserve→sign→submit→fills→reconcile→exits→settle"] style PROD fill:#264653,stroke:#2a9d8f,color:#fff style SUBJ fill:#2d2d2d,stroke:#888,color:#fff style EXEC fill:#3d2020,stroke:#a55,color:#fff
Part 1 — the carve (8cd6f58)
The repo was reduced to only the cohort-intelligence signal producer, deleting ~13,400 lines. Gate green.
Kept (the cohort-intelligence slice)
- Cohort scoring —
Pmv2Collect. Kept deliberately so the slice is self-contained (a producer that can rank its own cohort, not one that depends on an external scoring service). - Cohort consensus — the
Pmv2Consensuscron and the on-chainConsensusTrackerinpmv2-watch. - First-mover signal production — the ENTRY lane.
- Cohort exit detection — the SELL detection substrate (extended in Part 2).
- On-chain gap-backfill sweep — the 30s
eth_getLogsreconciliation (watcher-onchain-gap-backfill) preserved byte-for-byte (it backfills the cohort detection feed the producer depends on).
Deleted (~13,400 lines)
- The entire autotrade execution engine:
autotrade/{engine, execute, exits, runner, reconcile, settle, preflight, sizing, pricing, orders, …}. - The
onchain_booktriggers: fill / settle / self-trade. - The Telegram bot.
- Self-trade & overlap watchers (first-mover-overlap-watcher).
- Resolution-detection (autotrade-resolution-proposed-entry-guard).
- Trending, bet/market scoring, and the independent tracked-detector lane.
Dropped workspace crates
polymarket-clob(CLOB signing / execution) — notably NOT a dependency of cohort signals at all. A signal producer never signs or POSTs an order, so the CLOB crate has zero pull from this slice. (It already moved into theposition_managerworkspace per pmv2-polymarket-clob-crate-migration — this carve confirms the producer doesn’t need it.)pm-wslag-probe(lag-probe)
The watcher was ONE service mixing two concern-classes
pmv2-watchwas a single binary mixing cohort-signal lanes (consensus, first-mover, exit) with execution-trigger lanes (self-trade alerts, fill/reconcile, settlement). The carve strips the execution-trigger lanes and preserves the cohort-signal lanes + the gap-backfill sweep byte-for-byte. This is the watcher-level expression of the producer/executor split: the same binary previously straddled both sides of the boundary now being drawn.
Method — compile-driven carve, KEEP-WHOLE verified diff-clean
The deletion was compiler-driven: delete the migrated callers, then follow the compiler errors to the next thing that can go. Files in the KEEP set were verified diff-clean vs base (
43023ba) so the preserved cohort/consensus/first-mover/exit/backfill code is provably unchanged — the carve is pure subtraction, not a rewrite. This is the same “preserve byte-for-byte, prove it” discipline the NATS migration used for the money-path.
Part 2 — cohort exit signals (e762f5d)
New requirement: “watch trade exits the same way as first-mover entries.” The first-mover ENTRY lane already publishes action:Buy to cohort.signals (cohort-firstmover-nats-migration). The EXIT side did not — it was DB-only.
Before → after
Before (the pmv2-phase4-chain-cohort-sell-fast-exit behavior): when a tracked cohort trader SOLD / exited a position on-chain, the handler on_cohort_sell did DB bookkeeping only — logged an EXITED pmv2_position_events row and deleted the tracked pmv2_positions rows. It neither alerted nor published. The intent there was to ride the existing exit cursors (the cron’s pmv2_exit_alert + autotrade_exits cursors would pick up the early-inserted row). That model assumed the executor lived in-process and read those cursors.
After (this work): the executor now lives out-of-process (the position_manager over NATS), so the EXIT must be published as a signal, exactly like the ENTRY. on_cohort_sell now:
- Publishes an
action:SellTradeSignalto NATScohort.signals. - Sends a Telegram alert.
- Then does its prior bookkeeping (log
EXITED+ deletepmv2_positions).
sources::exit_signal(PositionRow) builds the Sell signal — Execution::Context (same arm as the ENTRY), reference_price = position.cur_price.
Load-bearing — the exit signal_id MUST carry an
exitdiscriminator distinct from the entry signal_idThe consumer dedups via
ON CONFLICT(dedup_key) DO NOTHINGkeyed onsignal_id. The ENTRY for(condition, outcome)already published a Buy withsignal_id == orders::dedup_key(.., "enter", ..). If the EXIT for the same(condition, outcome)reused that key, the consumer’sON CONFLICTwould silently swallow the Sell as a duplicate of the entry Buy — the position would never be closed = capital trap.The fix: the exit
signal_idcarries anexitdiscriminator (distinct from"enter") so the Buy and the Sell for the same market are two distinct dedup keys → both persist → both execute. Test-locked:exit_signal_id_differs_from_enter_signal_for_same_market.
This is the exit-side analogue of the dedup-identity work in cohort-firstmover-nats-migration (where the ENTRY signal_id was made byte-identical to the legacy orders::dedup_key(.., "enter", ..)): the dedup key is THE correctness surface for the at-least-once NATS lane, and entry vs exit must occupy different keys for the same market.
Correctness details
Publish BEFORE delete — a publish failure can't silently lose the signal
Ordering is publish-then-delete. If publish were attempted after deleting the
pmv2_positionsrow, a publish failure would leave the position gone from the snapshot (so it can never be re-detected) AND the Sell never on the wire → a permanent silent miss of the exit. Publishing first means a publish failure leaves the position row intact, so the next pass can retry. (Same “a dropped publish is a permanent miss → make it loud / don’t lose it” lesson as the ENTRY publisher in cohort-firstmover-nats-migration.)
DRY — entry + exit share
publish_cohort_with_retryThe ENTRY and EXIT publishers now share a single
publish_cohort_with_retryhelper (retry + backoff, with a drop-alert on give-up so an exhausted publish is never silent). One publish path, one retry policy, one give-up alarm for both directions.
The score-based
eligible_sell+ExitDebouncegate is unchangedThe existing exit gate from pmv2-phase4-chain-cohort-sell-fast-exit —
eligible_sell(sig, score)(cohort member ANDscore > MIN_COHORT_SCORE) + the per-wallet 60sExitDebouncecoalesce — was kept as-is. It was NOT narrowed to Core-tier (the ENTRY lane gates on Core tier; the EXIT lane intentionally does not — you want to exit any cohort sell you’re mirroring, not only Core-tier ones).
The provisional cross-service contract
PROVISIONAL contract — @deploy's executor must consume
action:Sellidempotently (NOT yet coordinated)This work introduces a new cross-service contract that is not yet coordinated:
- @deploy’s
position_managermust consumeaction:Selloncohort.signals(today it consumesaction:Buyentries + weather directives).- It must treat repeated Sells as idempotent — the at-least-once lane + redelivery means the same Sell
signal_idcan arrive more than once; closing an already-closed position must be a no-op, never a double-close or an error.Until @deploy wires the Sell consumer, the EXIT signals publish into a consumer that doesn’t act on them. Treat “executor consumes action:Sell idempotently” as a hard prerequisite to arming the exit lane live. (This is the SELL-side analogue of the ENTRY-side pre-arm gate noted in cohort-firstmover-nats-migration.)
Gate
scripts/gate.sh green at both commits:
- Carve
8cd6f58: gate green. - Exit-signals
e762f5d: polymarket-fetch 153 tests / polymarket-data 107 tests, including theexit_signal_id_differs_from_enter_signal_for_same_marketlock.
(The test count drop vs the pre-carve ~563 / 107 reflects the ~13,400 lines — and their tests — deleted in Part 1.)
Status & open items
Built + gate-green on
worktree-cohort-signals; NOT merged, NOT deployed
- Coordinate the provisional contract — @deploy’s
position_managerconsumesaction:Selloncohort.signals, idempotently.- Deploy — both the carved producer and the executor changes are off
main.- The cohort-publisher NKey +
cohort.signalsstream subject work from cohort-firstmover-nats-migration’s T5 still applies (the EXIT publishes to the same subject as the ENTRY — no new subject needed).
Related
- cohort-firstmover-nats-migration — the first-mover ENTRY lane (
action:Buy→cohort.signals) this exit work mirrors; defines thesignal_id/dedup_keyidentity and the publish-retry/permanent-miss lessons - pmv2-phase4-chain-cohort-sell-fast-exit — the prior
on_cohort_sellbehavior (logEXITED+ delete, NO publish/alert); this work supersedes that by adding theaction:Sellpublish + alert for the out-of-process executor - pmv2-position-manager-nats-consumer — the
Pmv2Consumer/cohort.signalslane + theON CONFLICT(dedup_key)idempotency the exit dedup-discriminator protects - position-manager-interface-design — @deploy’s
position_manager(the EXECUTOR half of this producer/executor split) - position-manager-execution-migration-implemented — the execution carve into
position_manager(the sibling subtraction on the executor side) - pmv2-polymarket-clob-crate-migration —
polymarket-clobmoved intoposition_manager; this carve confirms the producer has zero CLOB dependency - watcher-onchain-gap-backfill — the gap-backfill sweep preserved byte-for-byte in the carve (feeds the cohort detection the producer needs)
- first-mover-overlap-watcher — the overlap/self-trade watcher deleted in the carve
- autotrade-sources-signal-emitter —
sources::enter_signal(and nowsources::exit_signal), theTradeSignalbuilders - autotrade-resolution-proposed-entry-guard — resolution-detection, deleted in the carve
- lag-probe — one of the dropped workspace crates
- pmv2-autotrade-engine-design — the locked engine design now wholly on the executor side
- polymarket-fetch — project overview