You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/commands/debug-ponder.md
+26Lines changed: 26 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -163,6 +163,32 @@ Healthy output includes cache hit ratio and active owner count:
163
163
164
164
High `cacheHits` relative to `owners` means the cache is working. High `apiFetches` with low `activeOwners` may indicate many owners are not yet cached.
`HistoricalBootstrap` in `ponder.config.ts` uses `startBlock: "latest"` and `endBlock: "latest"` so it should run **once per chain** when the indexer reaches live (not every block). Use logs to confirm.
169
+
170
+
**Invocation counter (expect `#1` only per chain per process):**
If you see **`invocation=#2`** or a **`WARN`** line saying *invocation #2* / *repeats every block*, `endBlock: "latest"` is not behaving as a one-shot in your Ponder version — escalate or check Ponder release notes. A **full resync** resets the process: you will see `#1` again on the next run (expected).
183
+
184
+
**Full C4 trace (bootstrap work or empty):**
185
+
186
+
```bash
187
+
grep -n "\[COW:C4\]" ponder.log
188
+
```
189
+
190
+
You should see `invocation=#1`, then either `no generators need bootstrap` or `generators=...` + `DONE`, then **no further `[COW:C4]` lines** for that chain until restart.
191
+
166
192
### Backfill skip — verify poller is not running during historical sync
167
193
168
194
During backfill, the orderbook poller is silently skipped (no log line). To confirm live-only behavior, check that poll DONE lines only appear near the tip:
Copy file name to clipboardExpand all lines: agent_docs/m3-orderbook-integration-flow.md
+60-45Lines changed: 60 additions & 45 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,10 +15,10 @@ The system has seven components. Each has a single responsibility.
15
15
**Runs during**: Backfill AND live sync (two Ponder contract entries: `ComposableCow` for historical, `ComposableCowLive` for live).
16
16
17
17
**Key behavior difference by order type**:
18
-
-**Deterministic (TWAP, StopLoss)**: Pre-computes UIDs → fetches status from API → upserts `discreteOrder`→ if all terminal, marks generator `Invalid` (no further polling needed). This happens at both backfill and live sync.
18
+
-**Deterministic (TWAP, StopLoss)**: Pre-computes UIDs → fetches status from API → UIDs found on API go into `discreteOrder`, UIDs not found go into `candidateDiscreteOrder`→ if all are terminal on API, marks generator `Completed` (no further polling needed). This happens at both backfill and live sync.
19
19
-**Non-deterministic (PerpetualSwap, GoodAfterTime, TradeAboveThreshold, Unknown)**: Inserts generator only. Discrete orders will be discovered by the block handlers at live sync.
@@ -28,7 +28,7 @@ The system has seven components. Each has a single responsibility.
28
28
29
29
**Used by**: Creation Handler (both backfill and live). Also available to any future component that needs to know UIDs without RPC calls.
30
30
31
-
**Writes to**: `discreteOrder`, updates `conditionalOrderGenerator.status` to `Invalid` if all orders are terminal.
31
+
**Writes to**: `discreteOrder` (for UIDs found on API), `candidateDiscreteOrder` (for UIDs not yet on API). Updates `conditionalOrderGenerator.status` to `Completed` if all orders are terminal on API.
**Responsibility**: Checks if candidate discrete orders exist on the Orderbook API. When confirmed, moves them to `discreteOrder`.
51
+
**Responsibility**: Checks if candidate discrete orders exist on the Orderbook API. When confirmed, promotes them to `discreteOrder` and deletes the candidate row.
52
52
53
53
**When it runs**: Every block at live sync.
54
54
55
-
**How it works**: Queries `candidateDiscreteOrder` rows that don't yet have a corresponding `discreteOrder` row. Batch-fetches their UIDs from the API via `POST /orders/by_uids`. If the API has the order, upserts into `discreteOrder` with the API's authoritative status.
55
+
**How it works**: Queries `candidateDiscreteOrder` rows that don't yet have a corresponding `discreteOrder` row. Batch-fetches their UIDs from the API via `POST /orders/by_uids`. If the API has the order, upserts into `discreteOrder` with the API's authoritative status, then deletes the `candidateDiscreteOrder` row.
56
+
57
+
**Cleanup**: After promotion, confirmed candidates are deleted from `candidateDiscreteOrder`. Stale candidates past their `validTo` are also cleaned up — if the watch-tower never submitted them, they're expired and won't appear on the API.
56
58
57
59
**Why a separate handler?** The Contract Poller (C1) discovers orders on-chain, but the API may not have them yet (watch-tower submission delay). This handler polls the API repeatedly until the candidate is confirmed. Separation means C1 focuses on RPC, C2 focuses on API — different cost profiles, can be tuned independently.
Orders discovered on-chain by the Contract Poller but not yet confirmed on the Orderbook API. Same schema as `discreteOrder`. The Candidate Confirmer (C2) promotes them to `discreteOrder` once the API has them.
144
+
Orders discovered on-chain (by C1 or UID pre-computation) but not yet confirmed on the Orderbook API. Same schema as `discreteOrder` minus the `status` column — candidates are pending by definition. The Candidate Confirmer (C2) promotes them to `discreteOrder` once the API has them.
- Not cached → batch-fetch via `POST /orders/by_uids`
172
176
- Cache newly terminal results
173
177
174
-
5.**Upsert discrete orders.** For each UID, insert/update `discreteOrder` with API status. If not found on API, defaults to `open`.
178
+
5.**Insert results.** For each UID:
179
+
-**Found on API** → upsert into `discreteOrder` with API-authoritative status.
180
+
-**Not found on API** → insert into `candidateDiscreteOrder`. C2 will promote to `discreteOrder` when the API has it.
175
181
176
-
6.**Generator deactivation.** If ALL orders are terminal → set `status = 'Invalid'`, `allCandidatesKnown = true`, `lastPollResult = 'precompute:allTerminal'`. No further polling needed.
182
+
6.**Generator deactivation.** If ALL orders are terminal on the API → set `status = 'Completed'`, `allCandidatesKnown = true`, `lastPollResult = 'precompute:allTerminal'`. No further polling needed. If some UIDs are candidates (not yet on API), the generator stays Active — `allCandidatesKnown` is still set to `true` so C1 skips it.
177
183
178
184
**Result**: Deterministic orders are fully discovered at creation time. They never need the Contract Poller (C1). The Status Updater (C3) handles any open orders that haven't settled yet.
|**PollNever(reason)**|`status = 'Invalid'`. Do NOT expire discrete orders. |
216
+
|**PollNever(reason)**|`status = 'Completed'`. Do NOT expire discrete orders. |
211
217
212
-
4.**Note on `allCandidatesKnown`**: For non-deterministic single-part orders (StopLoss created at live, GoodAfterTime, TradeAboveThreshold), once the contract returns success once, set `allCandidatesKnown = true` — the order UID is now known and C2/C3 handle the rest. For repeating orders (PerpetualSwap), this flag stays false because new orders keep appearing.
218
+
4.**Note on `allCandidatesKnown`**: For confirmed single-shot non-deterministic types (**GoodAfterTime**, **TradeAboveThreshold**), once the contract returns success once, set `allCandidatesKnown = true` — the order UID is now known and C2/C3 handle the rest. For repeating orders (**PerpetualSwap**) and **Unknown** types (which may be multi-part), this flag stays `false`.
4.**For each NOT found:** Leave as candidate. Will be checked again next block. The watch-tower may not have submitted it yet.
225
231
232
+
5.**Cleanup:** Delete promoted candidates from `candidateDiscreteOrder`. Also delete stale candidates past their `validTo` — the watch-tower likely never submitted them.
233
+
226
234
### 3.5 Status Updater (C3) — Tracking Open Orders
227
235
228
236
**When**: Every block at live sync.
@@ -275,15 +283,15 @@ The Orderbook Client is used by all other components. Two main entry points:
275
283
276
284
### TWAP — Deterministic, multi-part
277
285
278
-
**Backfill:** Generator created → all N part UIDs pre-computed → API status fetched → `discreteOrder` rows created → if all terminal, generator marked Invalid.
286
+
**Backfill:** Generator created → all N part UIDs pre-computed → API status fetched → UIDs found on API go into `discreteOrder`, UIDs not found go into `candidateDiscreteOrder`→ if all terminal on API, generator marked Completed.
279
287
280
-
**Live sync:** Same as backfill (UID pre-computation works at both). If some parts are still open, C3 (Status Updater) tracks them until fulfilled/expired.
288
+
**Live sync:** Same as backfill (UID pre-computation works at both). UIDs not yet on API are candidates; C2 promotes when API confirms. C3 tracks open `discreteOrder` rows.
281
289
282
290
**The Contract Poller (C1) is NEVER involved for TWAP.** All discovery happens via UID pre-computation.
283
291
284
292
### StopLoss — Deterministic, single-part
285
293
286
-
**Backfill:** Generator created → single UID pre-computed → API status fetched → `discreteOrder` created→ if terminal, generator marked Invalid.
294
+
**Backfill:** Generator created → single UID pre-computed → API status fetched → if found on API, `discreteOrder` created; if not, `candidateDiscreteOrder`→ if terminal on API, generator marked Completed.
287
295
288
296
**Live sync:** Same as backfill. If the order is still open (price hasn't triggered yet), C3 tracks it. The Contract Poller is not involved.
289
297
@@ -310,17 +318,18 @@ The Orderbook Client is used by all other components. Two main entry points:
5.**C3 polls**: `0xabc` → `fulfilled`. `discreteOrder` updated. Cached as terminal.
323
-
6. Generator stays Active until block handler checks and gets `PollNever` → `Invalid`.
330
+
5.**C2 polls**: `0xabc` → API returns order. Promoted to `discreteOrder`. Candidate deleted.
331
+
6.**C3 polls**: `0xabc` → `fulfilled`. `discreteOrder` updated. Cached as terminal.
332
+
7. Generator stays Active until block handler checks and gets `PollNever` → `Completed`.
324
333
325
334
**Note:** The Contract Poller (C1) is NOT involved. The UID was known from creation. Status tracking is pure API work.
326
335
@@ -342,23 +351,25 @@ The Orderbook Client is used by all other components. Two main entry points:
342
351
343
352
---
344
353
345
-
## 6. Open Questions
354
+
## 6. Design Decisions (Resolved)
355
+
356
+
### `allCandidatesKnown` — Semantics (Resolved)
346
357
347
-
### `allCandidatesKnown` — Exact semantics
358
+
A **boolean** is the correct type. It answers one question: "does C1 still need to poll this generator?"
348
359
349
-
**For deterministic types**: Set to `true` at creation time (all UIDs known immediately).
360
+
**For deterministic types (TWAP, StopLoss)**: Set to `true` at creation time. All UIDs are computed from `staticInput` — no RPC needed.
350
361
351
-
**For non-deterministic single-part types**: Set to `true` after the first success from the Contract Poller (the UID is now known).
362
+
**For non-deterministic single-part types (GoodAfterTime, TradeAboveThreshold)**: Set to `true` after the first success from the Contract Poller. The UID is now known; C2/C3 handle confirmation and status.
352
363
353
-
**For PerpetualSwap (repeating)**: Never set to `true` — new orders keep appearing. The Contract Poller must keep polling.
364
+
**For PerpetualSwap (repeating)**: Stays `false` — new orders keep appearing. The Contract Poller must keep polling.
354
365
355
-
**Question**: Should `allCandidatesKnown` be a boolean, or should we store the count of expected parts? For TWAP, we know `n` parts. For StopLoss, we know 1 part. For PerpetualSwap, it's unbounded.
366
+
**Why not a part count?** Part count is TWAP-specific. The polling decision is binary: either C1 needs to discover more UIDs, or it doesn't. A part count would be unused for all order types except TWAP and adds no value over the boolean.
The bootstrap discovers orders via `fetchComposableOrders(owner)`, which relies on the Orderbook API having the orders. If an order was created and expired without ever being submitted to the API (e.g., the watch-tower was down), it won't be discovered.
370
+
The bootstrap discovers orders via `fetchComposableOrders(owner)`, which relies on the Orderbook API having the orders. If an order never reached the API, the bootstrap won't discover it.
360
371
361
-
**Likely acceptable**: The watch-tower is operated by the CoW Protocol team and is highly available.
372
+
**This is correct behavior, not a gap.** All CoW Protocol orders go through the Orderbook API. An order that never reached the API is the same as an order that never existed from the protocol's perspective — it was never submitted to solvers and never had a chance to be settled. The watch-tower is the standard submission path and is operated by the CoW Protocol team.
0 commit comments