The lockfile gained patchedDependencies (libsodium-wrappers-sumo) in
2aa1688, and the package manager hashes the patch file during install —
every image build since then fails with ENOENT on /repo/patches. No
image had been rebuilt since, so it surfaced only on the next deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Caught the indexer stuck for 3.5h at slot 23463870 today while the
chain tip had advanced to 23477134. Ogmios TCP socket was alive but
the server had stopped pushing nextBlock responses; /healthz reported
ogmios_status: "connected" because that flag only tracked tip_slot > 0,
not actual block flow.
OgmiosSource now arms a 90s response watchdog on every nextBlock /
findIntersection send and clears it on any well-formed protocol reply.
On timeout it ws.close()s, re-entering the existing onclose →
scheduleReconnect → findIntersection path that resumes from the saved
cursor. Errors and malformed messages intentionally leave the watchdog
armed so a stuck Ogmios error path also recycles instead of wedging.
OGMIOS_RESPONSE_TIMEOUT_MS env var tunes the window (0 disables).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mainnet marketplace-indexer was stuck in an event-replay loop since
2026-05-14. Diagnosis: OgmiosSource.intersectAt is captured ONCE in
start() from the cursor at process startup. On every WebSocket reconnect
(Cloudflare-fronted WS times out at ~100s idle, plus periodic Ogmios
server restarts), scheduleReconnect calls sendFindIntersection with the
stale start-time intersect, never the latest persisted cursor. Ogmios
seeks to that old point, sends nextBlock forward from there, the worker
re-processes the same blocks and writes saveCursor over the advanced
cursor with the old slot — DB cursor effectively never moves past the
startup point, and events get duplicated each iteration.
Fix:
- Add ChainSyncSource.updateIntersect({ slot, id }) to the interface.
- OgmiosSource.updateIntersect mutates this.intersectAt, so the next
reconnect's findIntersection picks up from the latest saved cursor.
- worker.handleBlock calls source.updateIntersect after every saveCursor.
- worker.handleRollback also calls it so a WS drop right after a rollback
doesn't resume past the rollback point.
- MockChainSyncSource implements as no-op (no reconnect path in tests).
Verified: same shape as the testnet projection-gap bug (memory 95fe6946),
manifesting differently. After this fix the cursor will only ever advance,
and reconnects resume from the freshest persisted point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes 2/4 builders from the M1-F-D-defer list (postAdvert already live in
7ea7328; updateAdvert and release remain).
Reclaim:
- buildLiveTxForReclaim in liveCbor.ts (mirrors buildLiveTxForAccept):
spends the escrow UTxO with REDEEMER_RECLAIM (Constr3), pays
payment + buyer_bond + supplier_bond to buyer, delegates UPLC eval
to Ogmios via localUPLCEval:false. Validity range = tipMs ± 60s.
- reclaim.ts: replace the unconditional mockSlotToWallclockMs tipMs
computation with `isLive ? Date.now() : mockSlotToWallclockMs(tipSlot)`,
matching accept.ts:77. Adds the live-CBOR dispatch branch.
Funds-at-risk before this fix: with the broken tipMs path, every live
Reclaim was rejected client-side ("tip < deliver_by" using a Nov-1970
tip). If a supplier never Claimed, buyer's 6-7 AP3X stayed locked
indefinitely. Verified recovery of 4 stuck escrows on Vector L2 testnet
(txs f6660e52, 769f16fd, 79970d86, 9f4db14b — 14 AP3X total recovered).
RetireAdvert:
- buildLiveTxForRetireAdvert in liveCbor.ts: spends advert UTxO with
REDEEMER_RETIRE_ADVERT (Constr2), advert script address.
- retireAdvert.ts: live-CBOR dispatch.
- supplier CLI: retire-advert.ts + retireAdvertFlow.ts.
- supplier/package.json: tx:retire-advert script.
Indexer:
- follower/blockProcessor + worker: handle RetireAdvert events,
transition advert row state to Retired.
- db/cache.ts: markAdvertTerminal helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>