dexbot 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/CHANGELOG.md +53 -3
  2. package/README.md +11 -7
  3. package/analysis/README.md +20 -14
  4. package/analysis/account_resolver.ts +171 -0
  5. package/analysis/ama_fitting/analyze_lambda_vs_slow.ts +2 -5
  6. package/analysis/ama_fitting/package.json +1 -1
  7. package/analysis/bot_usage/discover_bot_accounts.ts +20 -27
  8. package/analysis/chain_pool.ts +47 -0
  9. package/analysis/chart_utils.ts +33 -20
  10. package/analysis/derivative_chart_generator.ts +2 -3
  11. package/analysis/fills_source.ts +234 -0
  12. package/analysis/grid_correction_check.ts +104 -244
  13. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
  14. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
  15. package/analysis/trade_profitability.ts +169 -304
  16. package/analysis/tradingview/README.md +2 -2
  17. package/analysis/tradingview/h-bts_tradingview.html +1570 -0
  18. package/analysis/tradingview/t-bts_tradingview.html +1570 -0
  19. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +3 -4
  20. package/analysis/trend_detection/dynamic_weight_chart_generator.ts +2 -3
  21. package/analysis/trend_detection/kalman_chart_generator.ts +2 -3
  22. package/analysis/trend_detection/package.json +1 -1
  23. package/analysis/trend_detection/regime_chart_generator.ts +2 -3
  24. package/analysis/trend_detection/volatility_chart_generator.ts +2 -3
  25. package/claw/package.json +1 -1
  26. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  27. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  28. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  29. package/dist/analysis/account_resolver.d.ts +43 -0
  30. package/dist/analysis/account_resolver.d.ts.map +1 -0
  31. package/dist/analysis/account_resolver.js +137 -0
  32. package/dist/analysis/account_resolver.js.map +1 -0
  33. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +1 -1
  34. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +2 -4
  35. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +1 -1
  36. package/dist/analysis/bot_usage/discover_bot_accounts.js +20 -28
  37. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  38. package/dist/analysis/chain_pool.d.ts +10 -0
  39. package/dist/analysis/chain_pool.d.ts.map +1 -0
  40. package/dist/analysis/chain_pool.js +47 -0
  41. package/dist/analysis/chain_pool.js.map +1 -0
  42. package/dist/analysis/chart_utils.d.ts +14 -1
  43. package/dist/analysis/chart_utils.d.ts.map +1 -1
  44. package/dist/analysis/chart_utils.js +32 -22
  45. package/dist/analysis/chart_utils.js.map +1 -1
  46. package/dist/analysis/derivative_chart_generator.d.ts.map +1 -1
  47. package/dist/analysis/derivative_chart_generator.js +2 -3
  48. package/dist/analysis/derivative_chart_generator.js.map +1 -1
  49. package/dist/analysis/fills_source.d.ts +79 -0
  50. package/dist/analysis/fills_source.d.ts.map +1 -0
  51. package/dist/analysis/fills_source.js +186 -0
  52. package/dist/analysis/fills_source.js.map +1 -0
  53. package/dist/analysis/grid_correction_check.d.ts +4 -20
  54. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  55. package/dist/analysis/grid_correction_check.js +95 -248
  56. package/dist/analysis/grid_correction_check.js.map +1 -1
  57. package/dist/analysis/trade_profitability.d.ts +18 -19
  58. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  59. package/dist/analysis/trade_profitability.js +136 -280
  60. package/dist/analysis/trade_profitability.js.map +1 -1
  61. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  62. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +3 -4
  63. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  64. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
  65. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +2 -3
  66. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
  67. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +1 -1
  68. package/dist/analysis/trend_detection/kalman_chart_generator.js +2 -3
  69. package/dist/analysis/trend_detection/kalman_chart_generator.js.map +1 -1
  70. package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +1 -1
  71. package/dist/analysis/trend_detection/regime_chart_generator.js +2 -3
  72. package/dist/analysis/trend_detection/regime_chart_generator.js.map +1 -1
  73. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +1 -1
  74. package/dist/analysis/trend_detection/volatility_chart_generator.js +2 -3
  75. package/dist/analysis/trend_detection/volatility_chart_generator.js.map +1 -1
  76. package/dist/market_adapter/lp_chart_core.d.ts.map +1 -1
  77. package/dist/market_adapter/lp_chart_core.js +2 -3
  78. package/dist/market_adapter/lp_chart_core.js.map +1 -1
  79. package/dist/modules/account_bots.d.ts +1 -18
  80. package/dist/modules/account_bots.d.ts.map +1 -1
  81. package/dist/modules/account_bots.js +1 -1
  82. package/dist/modules/account_bots.js.map +1 -1
  83. package/dist/modules/bitshares-native/serial/operations.d.ts +1 -84
  84. package/dist/modules/bitshares-native/serial/operations.d.ts.map +1 -1
  85. package/dist/modules/bitshares-native/serial/operations.js +1 -38
  86. package/dist/modules/bitshares-native/serial/operations.js.map +1 -1
  87. package/dist/modules/cli_colors.d.ts +0 -1
  88. package/dist/modules/cli_colors.d.ts.map +1 -1
  89. package/dist/modules/constants.js +5 -5
  90. package/dist/modules/constants.js.map +1 -1
  91. package/dist/modules/credential_policy.d.ts +1 -25
  92. package/dist/modules/credential_policy.d.ts.map +1 -1
  93. package/dist/modules/credential_policy.js +1 -1
  94. package/dist/modules/credential_policy.js.map +1 -1
  95. package/dist/modules/credential_runtime.d.ts +1 -6
  96. package/dist/modules/credential_runtime.d.ts.map +1 -1
  97. package/dist/modules/credential_runtime.js +1 -1
  98. package/dist/modules/credential_runtime.js.map +1 -1
  99. package/dist/modules/credential_session_cache.d.ts +1 -5
  100. package/dist/modules/credential_session_cache.d.ts.map +1 -1
  101. package/dist/modules/credential_session_cache.js +1 -1
  102. package/dist/modules/credential_session_cache.js.map +1 -1
  103. package/dist/modules/crypto/pure_secp256k1.d.ts +1 -2
  104. package/dist/modules/crypto/pure_secp256k1.d.ts.map +1 -1
  105. package/dist/modules/crypto/pure_secp256k1.js +1 -1
  106. package/dist/modules/crypto/pure_secp256k1.js.map +1 -1
  107. package/dist/modules/dexbot_class.d.ts +9 -8
  108. package/dist/modules/dexbot_class.d.ts.map +1 -1
  109. package/dist/modules/dexbot_class.js +9 -8
  110. package/dist/modules/dexbot_class.js.map +1 -1
  111. package/dist/modules/dexbot_cow_runtime.d.ts +87 -7
  112. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  113. package/dist/modules/dexbot_cow_runtime.js +489 -14
  114. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  115. package/dist/modules/dexbot_fill_runtime.d.ts +1 -17
  116. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  117. package/dist/modules/dexbot_fill_runtime.js +1 -1
  118. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  119. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -1
  120. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  121. package/dist/modules/dexbot_maintenance_runtime.js +21 -5
  122. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  123. package/dist/modules/dexbot_state_recovery.d.ts +1 -7
  124. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  125. package/dist/modules/dexbot_state_recovery.js +1 -1
  126. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  127. package/dist/modules/launcher/adapter_requirement.d.ts +1 -5
  128. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -1
  129. package/dist/modules/launcher/adapter_requirement.js +1 -1
  130. package/dist/modules/launcher/adapter_requirement.js.map +1 -1
  131. package/dist/modules/launcher/bot_supervisor.d.ts +1 -6
  132. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  133. package/dist/modules/launcher/bot_supervisor.js +1 -1
  134. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  135. package/dist/modules/launcher/foreign_cred_daemon.d.ts +1 -3
  136. package/dist/modules/launcher/foreign_cred_daemon.d.ts.map +1 -1
  137. package/dist/modules/launcher/foreign_cred_daemon.js +1 -1
  138. package/dist/modules/launcher/foreign_cred_daemon.js.map +1 -1
  139. package/dist/modules/launcher/launch_modes.d.ts +1 -2
  140. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  141. package/dist/modules/launcher/launch_modes.js +1 -1
  142. package/dist/modules/launcher/launch_modes.js.map +1 -1
  143. package/dist/modules/launcher/market_adapter_runtime.d.ts +1 -3
  144. package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
  145. package/dist/modules/launcher/market_adapter_runtime.js +1 -1
  146. package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
  147. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -5
  148. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  149. package/dist/modules/launcher/monolithic_runtime.js +1 -1
  150. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  151. package/dist/modules/launcher/status_reporting.d.ts +1 -2
  152. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  153. package/dist/modules/launcher/status_reporting.js +1 -1
  154. package/dist/modules/launcher/status_reporting.js.map +1 -1
  155. package/dist/modules/order/export.d.ts +1 -8
  156. package/dist/modules/order/export.d.ts.map +1 -1
  157. package/dist/modules/order/export.js +1 -1
  158. package/dist/modules/order/export.js.map +1 -1
  159. package/dist/modules/order/grid.d.ts +7 -0
  160. package/dist/modules/order/grid.d.ts.map +1 -1
  161. package/dist/modules/order/grid.js +52 -98
  162. package/dist/modules/order/grid.js.map +1 -1
  163. package/dist/modules/order/grid_reconcile_internal.d.ts +1 -10
  164. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  165. package/dist/modules/order/grid_reconcile_internal.js +2 -2
  166. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  167. package/dist/modules/order/manager.d.ts +10 -2
  168. package/dist/modules/order/manager.d.ts.map +1 -1
  169. package/dist/modules/order/manager.js +20 -7
  170. package/dist/modules/order/manager.js.map +1 -1
  171. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  172. package/dist/modules/order/sync_engine.js +25 -20
  173. package/dist/modules/order/sync_engine.js.map +1 -1
  174. package/dist/modules/order/utils/math.d.ts +1 -10
  175. package/dist/modules/order/utils/math.d.ts.map +1 -1
  176. package/dist/modules/order/utils/math.js +1 -1
  177. package/dist/modules/order/utils/math.js.map +1 -1
  178. package/dist/modules/order/utils/order.d.ts +45 -7
  179. package/dist/modules/order/utils/order.d.ts.map +1 -1
  180. package/dist/modules/order/utils/order.js +160 -15
  181. package/dist/modules/order/utils/order.js.map +1 -1
  182. package/dist/modules/order/utils/system.d.ts +0 -15
  183. package/dist/modules/order/utils/system.d.ts.map +1 -1
  184. package/dist/modules/order/utils/system.js +2 -2
  185. package/dist/modules/order/utils/system.js.map +1 -1
  186. package/dist/modules/process_discovery.d.ts +0 -3
  187. package/dist/modules/process_discovery.d.ts.map +1 -1
  188. package/dist/modules/process_discovery.js +1 -1
  189. package/dist/modules/process_discovery.js.map +1 -1
  190. package/dist/modules/utils/chain_logs.d.ts +1 -2
  191. package/dist/modules/utils/chain_logs.d.ts.map +1 -1
  192. package/dist/modules/utils/chain_logs.js +1 -1
  193. package/dist/modules/utils/chain_logs.js.map +1 -1
  194. package/dist/scripts/analyze-git.js +2 -2
  195. package/dist/scripts/analyze-git.js.map +1 -1
  196. package/dist/scripts/update.js +46 -31
  197. package/dist/scripts/update.js.map +1 -1
  198. package/docs/BITSHARES_ONBOARDING.md +110 -40
  199. package/docs/COPY_ON_WRITE_MASTER_PLAN.md +10 -207
  200. package/docs/COW_INVARIANTS.md +16 -10
  201. package/docs/DEXBOT_COMPARISON.md +3 -3
  202. package/docs/EVOLUTION.md +75 -29
  203. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +6 -6
  204. package/docs/GRID_RECALCULATION.md +14 -6
  205. package/docs/GRID_RECONCILE.md +28 -25
  206. package/docs/LIFECYCLE.md +12 -11
  207. package/docs/README.md +48 -42
  208. package/docs/architecture.md +19 -30
  209. package/docs/developer_guide.md +3 -3
  210. package/package.json +1 -1
  211. package/scripts/git-viewer.sh +1 -1
  212. package/tests/README.md +10 -1
@@ -323,17 +323,23 @@ function formatUnmatchedChainOrderForLog(order) {
323
323
  * Record a pending CREATE broadcast on the manager.
324
324
  * @param {import('./dexbot_class.js').DEXBot} bot
325
325
  * @param {Object} entry
326
+ * @returns {string|null} The fingerprint the entry was stored under (null
327
+ * when recording was skipped). Callers that must later remap the entry's
328
+ * stored opIndex/ctxIndex (the final pivot gate's compaction) collect
329
+ * these to identify exactly which pending entries belong to THIS batch —
330
+ * entry.batchId is unreliable because _currentBatchId is never populated
331
+ * in production (always null), so batchId scoping cannot discriminate.
326
332
  */
327
333
  function recordPendingBroadcast(bot, entry) {
328
334
  if (!bot.manager || !entry || !entry.order)
329
- return;
335
+ return null;
330
336
  if (!bot.manager._pendingBroadcasts || !(bot.manager._pendingBroadcasts instanceof Map)) {
331
337
  bot.manager._pendingBroadcasts = new Map();
332
338
  }
333
339
  const fingerprint = createOpFingerprintForSlot(bot, entry.order, entry.finalInts, entry.order.id);
334
340
  if (!fingerprint) {
335
341
  bot.manager.logger.log?.(`[COW] Skipped pending-broadcast record: could not build fingerprint for ${entry.order?.id || 'unknown'}`, 'warn');
336
- return;
342
+ return null;
337
343
  }
338
344
  bot.manager._pendingBroadcasts.set(fingerprint, {
339
345
  fingerprint,
@@ -347,6 +353,7 @@ function recordPendingBroadcast(bot, entry) {
347
353
  batchId: bot._currentBatchId || null,
348
354
  recordedAt: Date.now()
349
355
  });
356
+ return fingerprint;
350
357
  }
351
358
  /**
352
359
  * Clear the pending-broadcast cache.
@@ -452,7 +459,7 @@ function popPushedWorkingGrid(bot, cowResult) {
452
459
  */
453
460
  async function deferUncertainBroadcastRead(bot, detail, suffix, resyncReason, resyncOptions = {}) {
454
461
  bot.manager.logger.log(`[COW][UNCERTAIN] ${detail}; keeping pending-broadcast protection ${suffix}`, 'warn');
455
- // Fix #6 (docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md §2): an ambiguous/truncated chain read is
462
+ // Fix #6 (docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md §2): an ambiguous/truncated chain read is
456
463
  // node lag, not a missing order — the broadcast already succeeded. Previously every
457
464
  // such read requested a structural resync, piling pending broadcasts (up to 14) and
458
465
  // forcing a resync mid-broadcast (the T-BTS 06:43Z thrash). The pending-broadcast
@@ -1287,11 +1294,12 @@ function formatPartialBroadcastSummary(err) {
1287
1294
  }
1288
1295
  /**
1289
1296
  * Execute a batch with retry-on-uncertain semantics, enforcing a gap-slot
1290
- * per-broadcast operation cap (_getGapSlotBatchSize). When the batch carries
1291
- * more operations than the cap, it is split into sequential broadcast chunks
1292
- * of at most `maxOps` operations each, so a single on-chain transaction never
1293
- * holds more than gapSlots order operations (the original "N fills
1294
- * per broadcast" intent, applied at the op level rather than the fill level).
1297
+ * per-broadcast operation cap (_getGapSlotBatchSize = gapSlots + 1). When the
1298
+ * batch carries more operations than the cap, it is split into sequential
1299
+ * broadcast chunks of at most `maxOps` operations each, so a single on-chain
1300
+ * transaction never holds more than gapSlots + 1 order operations (the
1301
+ * original "N fills per broadcast" intent, applied at the op level rather
1302
+ * than the fill level).
1295
1303
  *
1296
1304
  * Failure isolation — no swallowed orders: if one chunk's broadcast is
1297
1305
  * uncertain (BroadcastUncertainError), the remaining chunks are STILL
@@ -3028,7 +3036,12 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3028
3036
  bot.manager.logger.log(`[COW] Draining ${pendingCorrectionCount} pending correction(s) before batch`, 'info');
3029
3037
  const drainResult = await orderUtils.correctAllPriceMismatches(bot.manager, bot.account, bot.privateKey, chainOrders);
3030
3038
  if (drainResult?.failed > 0) {
3031
- bot.manager.logger.log(`[COW] ${drainResult.failed} correction(s) failed pre-batch; remaining entries retry on next sync/maintenance tick`, 'warn');
3039
+ bot.manager.logger.log(`[COW] ${drainResult.failed} correction(s) failed pre-batch` +
3040
+ (drainResult.staleDropped > 0 ? `, ${drainResult.staleDropped} stale dropped` : '') +
3041
+ `; remaining entries retry on next sync/maintenance tick`, 'warn');
3042
+ }
3043
+ else if (drainResult?.staleDropped > 0) {
3044
+ bot.manager.logger.log(`[COW] Pre-batch drain resolved, ${drainResult.staleDropped} stale correction(s) dropped`, 'info');
3032
3045
  }
3033
3046
  }
3034
3047
  catch (drainErr) {
@@ -3055,6 +3068,12 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3055
3068
  // would spam big batches, so the guard emits one batch summary instead
3056
3069
  // (see the summary after the action loop below).
3057
3070
  const lastFillGuardStats = { checked: 0, passed: 0, skipped: 0, bypassed: 0, pivotOffGrid: 0 };
3071
+ // Fingerprints of the pending-broadcast entries recorded by THIS batch's
3072
+ // op-building (both CREATE paths). The final pivot gate's compaction
3073
+ // remaps these entries' stored opIndex/ctxIndex to their post-drop
3074
+ // positions; entry.batchId cannot discriminate batches (always null in
3075
+ // production), so the fingerprint set is the ownership marker.
3076
+ const batchPendingFps = new Set();
3058
3077
  // Per-batch GRID-PRICE-INVARIANT counters (BLOCKING). Tracks emitted prices
3059
3078
  // that are not the genesis level for their slot — such emissions are
3060
3079
  // rejected, not placed. One batch summary.
@@ -3108,12 +3127,33 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3108
3127
  // mutated the pivot mid-batch (02:03 pivots drifted 0.001523→0.001529
3109
3128
  // across 20 checks), so early actions were judged against a different
3110
3129
  // pivot than later ones. The batch summary still reports whether this
3111
- // freeze moved the pivot under the plan.
3130
+ // freeze moved the pivot under the plan. The frozen value feeds the
3131
+ // final pre-broadcast gate below (runFinalPivotGate), which re-checks
3132
+ // built ops when a fill queued AFTER the freeze moved the pivot.
3133
+ // freezeQueueDepth is the Step-2 observability half: queue depth at
3134
+ // freeze time, paired with the gate's own queue readout.
3135
+ let freezeQueueDepth = null;
3136
+ try {
3137
+ freezeQueueDepth = Array.isArray(bot?._incomingFillQueue)
3138
+ ? bot._incomingFillQueue.length
3139
+ : null;
3140
+ }
3141
+ catch {
3142
+ freezeQueueDepth = null;
3143
+ }
3112
3144
  try {
3113
3145
  if (refreshLastFillPivotFromQueue(bot))
3114
3146
  lastFillGuardPivotRefreshed = true;
3115
3147
  }
3116
3148
  catch { /* best-effort */ }
3149
+ // Captured AFTER the freeze refresh, not before: the refresh is part
3150
+ // of the freeze, so the baseline must be the pivot the ops are about
3151
+ // to be judged against. Capturing pre-refresh would make every batch
3152
+ // whose freeze picked up a pre-freeze queued fill look "moved" at the
3153
+ // gate — a spurious warn plus a redundant full re-check against the
3154
+ // identical pivot.
3155
+ const frozenPivotAtBatchStart = bot.manager?._lastFilledPrice;
3156
+ const frozenTypeAtBatchStart = bot.manager?._lastFilledType;
3117
3157
  for (const action of actions) {
3118
3158
  if (action.type === COW_ACTIONS.CANCEL) {
3119
3159
  try {
@@ -3248,12 +3288,14 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3248
3288
  operations.push(buildResult.op);
3249
3289
  opContexts.push({ kind: 'create', id: order.id, order: effectiveOrder, args, finalInts: buildResult.finalInts });
3250
3290
  intraBatchCandidates.push(effectiveOrder);
3251
- recordPendingBroadcast(bot, {
3291
+ const recordedFp = recordPendingBroadcast(bot, {
3252
3292
  opIndex: operations.length - 1,
3253
3293
  ctxIndex: opContexts.length - 1,
3254
3294
  order: effectiveOrder,
3255
3295
  finalInts: buildResult.finalInts
3256
3296
  });
3297
+ if (recordedFp)
3298
+ batchPendingFps.add(recordedFp);
3257
3299
  }
3258
3300
  catch (err) {
3259
3301
  bot.manager.logger.log(`Failed to prepare create op for ${action.id}: ${getErrorMessage(err)}`, 'error');
@@ -3652,12 +3694,14 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3652
3694
  args: { amountToSell: fbArgs.amountToSell, minToReceive: fbArgs.minToReceive },
3653
3695
  finalInts: fbResult.finalInts
3654
3696
  });
3655
- recordPendingBroadcast(bot, {
3697
+ const fbRecordedFp = recordPendingBroadcast(bot, {
3656
3698
  opIndex: operations.length - 1,
3657
3699
  ctxIndex: opContexts.length - 1,
3658
3700
  order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
3659
3701
  finalInts: fbResult.finalInts
3660
3702
  });
3703
+ if (fbRecordedFp)
3704
+ batchPendingFps.add(fbRecordedFp);
3661
3705
  bot.manager.logger.log(`[COW] Recovered "not found" for ${action.id}: converted UPDATE to CREATE for slot ${targetSlotId}`, 'warn');
3662
3706
  continue;
3663
3707
  }
@@ -3671,6 +3715,73 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3671
3715
  }
3672
3716
  }
3673
3717
  }
3718
+ // FINAL PRE-BROADCAST PIVOT GATE (2026-09-13 incident on a live
3719
+ // market-pair bot): a
3720
+ // fill queued AFTER the batch-start freeze but BEFORE broadcast
3721
+ // passes every per-action check on a stale pivot (freeze .745, fill
3722
+ // queued .765, broadcast .910). Re-check the BUILT ops against a
3723
+ // re-refreshed pivot here — after op-building, before the batch
3724
+ // summary (drops count as skipped, not passed), fund validation
3725
+ // (snapshot reflects filtered ops) and single-flight claim.
3726
+ // op indexes recorded during op-building are rebuilt from kept
3727
+ // contexts below, so any future reader sees live indexes.
3728
+ // The gate runs on its OWN stats object: its re-checks would
3729
+ // otherwise double-count the build loop's checked/passed/skipped
3730
+ // totals in the batch summary below. Gate contributions are
3731
+ // reported separately (gateChecked=... fields).
3732
+ const skippedUpdateCountRef = { count: 0 };
3733
+ const finalGateStats = { checked: 0, passed: 0, skipped: 0, bypassed: 0, pivotOffGrid: 0 };
3734
+ let finalGate = null;
3735
+ try {
3736
+ finalGate = runFinalPivotGate(bot, operations, opContexts, {
3737
+ actions,
3738
+ cowResult,
3739
+ frozenPivot: frozenPivotAtBatchStart,
3740
+ frozenType: frozenTypeAtBatchStart,
3741
+ lastFillGuardStats: finalGateStats,
3742
+ skippedUpdateSlotIds,
3743
+ skippedCreateSlotIds,
3744
+ skippedUpdateCountRef,
3745
+ freezeQueueDepth,
3746
+ batchPendingFps,
3747
+ });
3748
+ if (finalGate.refreshed)
3749
+ lastFillGuardPivotRefreshed = true;
3750
+ if (finalGate.pivotChanged) {
3751
+ try {
3752
+ const fmtP = (v) => (v == null || !Number.isFinite(Number(v)) ? 'none' : Format.formatPrice6(Number(v)));
3753
+ bot.manager?.logger?.log?.(`[LAST-FILL-GUARD] Final gate: pivot moved under batch ` +
3754
+ `${fmtP(frozenPivotAtBatchStart)}(${frozenTypeAtBatchStart ?? 'cold'})` +
3755
+ `->${fmtP(bot.manager?._lastFilledPrice)}(${bot.manager?._lastFilledType ?? 'cold'}) ` +
3756
+ `(freezeQueue=${freezeQueueDepth ?? '?'}) ` +
3757
+ `dropped=${finalGate.dropped.length}`, 'warn');
3758
+ }
3759
+ catch { /* logging is best-effort */ }
3760
+ }
3761
+ // Rebuild cancelOpIndexByOrderId from the KEPT contexts: the
3762
+ // gate compacted operations/opContexts in lockstep, so indexes
3763
+ // recorded during op-building are stale for every op after the
3764
+ // first drop. cancelOpIndexByOrderId has NO readers past this
3765
+ // point (verified: the op-building loop is its last use —
3766
+ // pair-mode/chunk grouping is computed lazily at broadcast from
3767
+ // opContexts with no stored indexes), so this rebuild is
3768
+ // defence-in-depth for future readers, not a live fix.
3769
+ cancelOpIndexByOrderId.clear();
3770
+ for (let ci = 0; ci < opContexts.length; ci++) {
3771
+ const cctx = opContexts[ci];
3772
+ const cord = cctx?.order;
3773
+ const cOrderId = cctx?.kind === 'cancel'
3774
+ ? cctx?.order?.orderId || cctx?.orderId
3775
+ : cord?.orderId;
3776
+ if (cOrderId)
3777
+ cancelOpIndexByOrderId.set(cOrderId, ci);
3778
+ }
3779
+ // skippedUpdateCount is threaded via countRef so the restore
3780
+ // below covers gate-dropped rotations too.
3781
+ if (skippedUpdateCountRef.count > 0)
3782
+ skippedUpdateCount += skippedUpdateCountRef.count;
3783
+ }
3784
+ catch (_gateErr) { /* gate is fail-open: keep the built ops */ }
3674
3785
  // Batch-level LAST-FILL-GUARD summary: per-action pass lines would spam
3675
3786
  // big batches, so one line per batch records the mode, pivot, resolved
3676
3787
  // increment, and pass/skip/bypass counts — the guard's pass decisions
@@ -3682,7 +3793,16 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3682
3793
  // for the whole batch, so every action was checked against the same
3683
3794
  // pivot printed here.
3684
3795
  try {
3685
- const totalGuarded = lastFillGuardStats.checked + lastFillGuardStats.bypassed;
3796
+ // The final gate reports on its OWN counters (finalGateStats), so
3797
+ // checked/passed/skipped/bypassed here are the build loop's
3798
+ // verdicts only — the gate's re-checks never inflate them. The
3799
+ // gate's contributions ride along as gate* fields, omitted when
3800
+ // the gate did not re-check anything (same convention as
3801
+ // pivotOffGrid above). totalGuarded includes the gate so a
3802
+ // batch that was ONLY gate-checked (e.g. cold freeze armed
3803
+ // mid-batch) still prints.
3804
+ const gateGuarded = finalGateStats.checked + finalGateStats.bypassed;
3805
+ const totalGuarded = lastFillGuardStats.checked + lastFillGuardStats.bypassed + gateGuarded;
3686
3806
  if (totalGuarded > 0) {
3687
3807
  const sumPivotRaw = bot.manager?._lastFilledPrice;
3688
3808
  const sumType = bot.manager?._lastFilledType;
@@ -3720,6 +3840,10 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
3720
3840
  // "counter unavailable".
3721
3841
  (Number(lastFillGuardStats.pivotOffGrid) > 0
3722
3842
  ? ` pivotOffGrid=${lastFillGuardStats.pivotOffGrid}`
3843
+ : '') +
3844
+ (gateGuarded > 0
3845
+ ? ` gateChecked=${finalGateStats.checked} gatePassed=${finalGateStats.passed} ` +
3846
+ `gateSkipped=${finalGateStats.skipped} gateBypassed=${finalGateStats.bypassed}`
3723
3847
  : ''), cold || (batchPivot && batchPivot.idx == null) ? 'warn' : 'info');
3724
3848
  }
3725
3849
  }
@@ -4410,6 +4534,13 @@ function resolveOnGridPivot(manager, rawPrice) {
4410
4534
  * skip logging stay at the call sites, which differ per action kind).
4411
4535
  * Batch callers pass skipRefresh=true: the batch-start freeze owns refreshes
4412
4536
  * so every action in a batch is judged against the same pivot.
4537
+ *
4538
+ * FINAL-GATE CONTRACT (see runFinalPivotGate): the gate re-checks BUILT ops
4539
+ * against a re-refreshed pivot AFTER the op-building loop. It must run BEFORE
4540
+ * any later mutation of operations/opContexts (fund validation snapshot,
4541
+ * pair-mode/chunk grouping) — those stages index op positions and read stale
4542
+ * indexes after a filter. Call sites after the gate must treat
4543
+ * operations/opContexts as the filtered arrays.
4413
4544
  * @param {Object} bot
4414
4545
  * @param {number} price - Target order price
4415
4546
  * @param {number} size - Order size
@@ -4467,6 +4598,349 @@ function runLastFillGuardCheck(bot, price, size, type, stats, skipRefresh = fals
4467
4598
  stats.checked++;
4468
4599
  return { check, refreshed };
4469
4600
  }
4601
+ /**
4602
+ * Final pre-broadcast pivot gate: re-check BUILT ops against a re-refreshed
4603
+ * pivot RIGHT BEFORE broadcast.
4604
+ *
4605
+ * Why this exists (2026-09-13 incident on a live market-pair bot): the batch-start freeze
4606
+ * refreshes the pivot once, then every CREATE / rotation-UPDATE /
4607
+ * fallback-CREATE is judged against that frozen value (skipRefresh=true).
4608
+ * A fill that is queued AFTER the freeze but BEFORE broadcast (in the
4609
+ * incident: freeze at .745, fill queued at .765, broadcast at .910) passes
4610
+ * every per-action check on a stale pivot and ships. The batch summary
4611
+ * prints pivotRefreshed=false — the freeze honestly found nothing — and
4612
+ * the violating ops broadcast anyway.
4613
+ *
4614
+ * Placement (see FINAL-GATE CONTRACT on runLastFillGuardCheck):
4615
+ * 1. Called after the op-building action loop, BEFORE the LAST-FILL-GUARD
4616
+ * batch summary line (so dropped ops are visible as skipped, not
4617
+ * passed) and BEFORE fund validation (so the VALIDATION snapshot
4618
+ * reflects the filtered ops).
4619
+ * 2. Must run before pair-mode/chunk grouping, which indexes op positions.
4620
+ * executeOperationsWithStrategy groups lazily at broadcast time from
4621
+ * the (filtered) opContexts it receives, so filtering here is safe —
4622
+ * but any future grouping computed between op-building and broadcast
4623
+ * must be rebuilt after the gate (same rule as the contract).
4624
+ *
4625
+ * Semantics:
4626
+ * - Peek-only refresh (never drains the fill queue — same as the freeze).
4627
+ * - Pivot UNCHANGED since the freeze => pure no-op: returns the input
4628
+ * arrays untouched, no extra log lines beyond queue-depth debug.
4629
+ * - Pivot CHANGED => re-run isLastFillGuardBlocked against each built op's
4630
+ * final price with the SAME bypass rules as the build loop
4631
+ * (spread-correction CREATEs, stamped gap-evacuation UPDATEs). Violating
4632
+ * ops + their contexts are dropped; their slots feed the existing
4633
+ * skippedUpdateSlotIds/skippedCreateSlotIds restore paths so the working
4634
+ * grid stays consistent (dropped rotations restore from master, dropped
4635
+ * creates count toward the boundary-hold intersect).
4636
+ * - fail-open on everything unjudgeable: unresolvable price/type, cold
4637
+ * pivot (null), or a refresh/inference throw => the op is KEPT. A gate
4638
+ * that cannot prove a violation must not invent one — dropping a healthy
4639
+ * op strands its slot, while a missed violation is still caught by the
4640
+ * next cycle's guard + commit chain adoption.
4641
+ * - size-update ops are NEVER gated (same-price, no repricing).
4642
+ * - cancel ops are NEVER gated or dropped.
4643
+ *
4644
+ * Stale-index hygiene: recordPendingBroadcast stores opIndex/ctxIndex
4645
+ * against the build-time arrays. Dropped CREATEs must have their pending
4646
+ * entries removed (else the reconcile path adopts a broadcast that never
4647
+ * shipped), and KEPT entries must have their stored indexes REMAPPED: a
4648
+ * lockstep compaction keeps operations/opContexts aligned with each other,
4649
+ * but the absolute indexes stored inside the pending entries are not
4650
+ * rewritten by it — after the first drop, an unremapped ctxIndex resolves
4651
+ * to a shifted position (undefined at best, a DIFFERENT create's context at
4652
+ * worst, which would let adoptMatchedEntries synchronize the wrong slot
4653
+ * with a matched chain order). The gate therefore builds an old→new index
4654
+ * map during compaction and remaps/removes entries identified by
4655
+ * opts.batchPendingFps (the fingerprints THIS batch recorded — entry.batchId
4656
+ * is always null in production, so it cannot discriminate). Sibling
4657
+ * batches' entries are never touched (their indexes refer to their own
4658
+ * build-time arrays).
4659
+ *
4660
+ * @param {import('./dexbot_class.js').DEXBot} bot
4661
+ * @param {Array} operations - Built chain ops (mutated in place on drop)
4662
+ * @param {Array} opContexts - Built op contexts (mutated in place on drop)
4663
+ * @param {Object} opts - { actions, cowResult, frozenPivot, frozenType,
4664
+ * lastFillGuardStats, skippedUpdateSlotIds, skippedCreateSlotIds,
4665
+ * skippedUpdateCountRef: { count }, freezeQueueDepth, batchPendingFps }
4666
+ * @returns {{ dropped: Array, pivotChanged: boolean, refreshed: boolean }}
4667
+ */
4668
+ function runFinalPivotGate(bot, operations, opContexts, opts = {}) {
4669
+ const empty = { dropped: [], pivotChanged: false, refreshed: false };
4670
+ try {
4671
+ if (!Array.isArray(operations) || !Array.isArray(opContexts) || operations.length === 0)
4672
+ return empty;
4673
+ const stats = opts?.lastFillGuardStats;
4674
+ const frozenPivot = Number(opts?.frozenPivot);
4675
+ const frozenType = opts?.frozenType;
4676
+ const frozenCold = !Number.isFinite(frozenPivot) || frozenType == null;
4677
+ const queueDepthBefore = Array.isArray(bot?._incomingFillQueue)
4678
+ ? bot._incomingFillQueue.length
4679
+ : null;
4680
+ // Peek-only re-refresh: never drains the queue (same as the freeze).
4681
+ let refreshed = false;
4682
+ try {
4683
+ refreshed = !!refreshLastFillPivotFromQueue(bot);
4684
+ }
4685
+ catch {
4686
+ refreshed = false;
4687
+ }
4688
+ const queueDepthAfter = Array.isArray(bot?._incomingFillQueue)
4689
+ ? bot._incomingFillQueue.length
4690
+ : null;
4691
+ const livePivot = Number(bot.manager?._lastFilledPrice);
4692
+ const liveType = bot.manager?._lastFilledType;
4693
+ const liveCold = !Number.isFinite(livePivot) || liveType == null;
4694
+ // No-op fast path: pivot unchanged (or uncomparable) since the freeze.
4695
+ // frozenCold + liveCold: guard stayed disabled — nothing to re-check.
4696
+ // frozenCold + liveArmed: the freeze ran cold but a fill arrived
4697
+ // mid-batch. The built ops were NEVER guarded; treat as changed so
4698
+ // they are checked below (fail-open keeps whatever is unjudgeable).
4699
+ let pivotChanged = refreshed;
4700
+ if (frozenCold && liveCold)
4701
+ pivotChanged = false;
4702
+ else if (!frozenCold && !liveCold)
4703
+ pivotChanged = livePivot !== frozenPivot || liveType !== frozenType;
4704
+ else
4705
+ pivotChanged = true;
4706
+ try {
4707
+ bot.manager?.logger?.log?.(`[LAST-FILL-GUARD] Final gate: queue ${queueDepthBefore ?? '?'}->${queueDepthAfter ?? '?'} ` +
4708
+ `pivot ${Number.isFinite(frozenPivot) ? Format.formatPrice6(frozenPivot) : 'none'}(${frozenType ?? 'cold'})` +
4709
+ `->${Number.isFinite(livePivot) ? Format.formatPrice6(livePivot) : 'none'}(${liveType ?? 'cold'}) ` +
4710
+ `changed=${pivotChanged} refreshed=${refreshed}`, 'debug');
4711
+ }
4712
+ catch { /* logging is best-effort */ }
4713
+ if (!pivotChanged)
4714
+ return { ...empty, refreshed };
4715
+ // Pivot moved (or armed mid-batch): re-check every built op's FINAL
4716
+ // price. Same bypass rules as the build loop; unjudgeable => KEEP.
4717
+ const actions = Array.isArray(opts?.actions) ? opts.actions : [];
4718
+ const batchOrigin = opts?.cowResult?.origin;
4719
+ const actionBySlot = new Map();
4720
+ for (const a of actions) {
4721
+ if (!a)
4722
+ continue;
4723
+ // Rotation UPDATEs are keyed by DESTINATION slot (the emitted
4724
+ // price is the destination's level); plain CREATEs by slot id.
4725
+ const rotDest = a?.newGridId;
4726
+ const key = (a?.type === COW_ACTIONS.UPDATE && rotDest && rotDest !== a?.id) ? rotDest : a?.id;
4727
+ if (key && !actionBySlot.has(key))
4728
+ actionBySlot.set(key, a);
4729
+ }
4730
+ const dropIdx = new Set();
4731
+ const dropped = [];
4732
+ const inc = resolveLastFillGuardIncrement(bot);
4733
+ const onGrid = resolveOnGridPivot(bot.manager, bot.manager?._lastFilledPrice);
4734
+ for (let i = 0; i < opContexts.length; i++) {
4735
+ const ctx = opContexts[i];
4736
+ if (!ctx || ctx.kind === 'cancel' || ctx.kind === 'size-update')
4737
+ continue;
4738
+ let price = null;
4739
+ let type = null;
4740
+ let size = null;
4741
+ let slotId = null;
4742
+ let action = null;
4743
+ if (ctx.kind === 'create') {
4744
+ slotId = ctx.id || ctx.order?.id || null;
4745
+ price = Number(ctx.order?.price);
4746
+ type = ctx.order?.type || null;
4747
+ size = Number(ctx.order?.size);
4748
+ action = (slotId && actionBySlot.get(slotId)) || null;
4749
+ // Spread-correction CREATE bypass (mirrors the build loop:
4750
+ // per-action origin, batch origin as back-compat fallback).
4751
+ const actionOrigin = action?.origin;
4752
+ if (actionOrigin === 'spread-correction'
4753
+ || (actionOrigin == null && batchOrigin === 'spread-correction')) {
4754
+ if (stats)
4755
+ stats.bypassed = (Number(stats.bypassed) || 0) + 1;
4756
+ continue;
4757
+ }
4758
+ }
4759
+ else if (ctx.kind === 'rotation') {
4760
+ const rot = ctx.rotation || {};
4761
+ slotId = rot.newGridId || rot.oldOrder?.id || null;
4762
+ price = Number(rot.newPrice);
4763
+ type = rot.type || null;
4764
+ size = Number(rot.newSize);
4765
+ // Rotation UPDATEs are keyed by DESTINATION slot (the
4766
+ // emitted price is the destination's level) — EXCEPT the
4767
+ // same-slot size-only form (no newGridId, or newGridId ===
4768
+ // source id), which buildActionsFromPlan emits for
4769
+ // ordersToUpdate and which must resolve to the source
4770
+ // action. A same-slot UPDATE carries no repricing, so a
4771
+ // dest-keyed lookup that misses it would ALSO miss its
4772
+ // origin stamp — fall back to the source id before
4773
+ // judging the bypass.
4774
+ action = (slotId && actionBySlot.get(slotId)) || null;
4775
+ if (!action) {
4776
+ const srcId = rot.oldOrder?.id || null;
4777
+ if (srcId)
4778
+ action = actionBySlot.get(srcId) || null;
4779
+ }
4780
+ // Gap-evacuation UPDATE bypass mirrors the build loop's
4781
+ // stamped path ONLY — with one deliberate asymmetry (see
4782
+ // below): the build loop re-proves UNSTAMPED evacuations
4783
+ // live from the master grid; the gate guards them normally.
4784
+ // An unstamped rotation reaching the final gate was either
4785
+ // (a) probed-and-allowed at build time — in which case its
4786
+ // price already survived an evacuation proof and the guard
4787
+ // re-check here is harmless duplication, or (b) probe-
4788
+ // rejected/failed-closed — in which case it was SKIPPED at
4789
+ // build time and never reached op-building, so the gate
4790
+ // cannot see it either. Either way there is no live
4791
+ // unstamped evacuation in the built ops that needs
4792
+ // re-proving: re-proving here would need the master-grid
4793
+ // source read the build loop does, and the source may have
4794
+ // been pre-applied since. Stamped rotations carry origin +
4795
+ // evacBoundary/evacGapSlots.
4796
+ //
4797
+ // ASYMMETRY (fail-open, not fail-closed): the build loop's
4798
+ // unstamped path FAILS CLOSED (unresolvable source => skip
4799
+ // the op). The gate FAILS OPEN (unjudgeable => keep). A
4800
+ // dropped op strands its slot until the next cycle; a kept
4801
+ // op is still subject to the commit guard + chain adoption.
4802
+ // The gate must not invent a block it cannot prove —
4803
+ // especially not on an op the build loop already allowed.
4804
+ const rotOrigin = action?.origin;
4805
+ if (rotOrigin === 'gap-evacuation'
4806
+ && Number.isFinite(Number(action?.evacBoundary))
4807
+ && Number.isFinite(Number(action?.evacGapSlots))) {
4808
+ if (stats)
4809
+ stats.bypassed = (Number(stats.bypassed) || 0) + 1;
4810
+ continue;
4811
+ }
4812
+ }
4813
+ else {
4814
+ continue;
4815
+ }
4816
+ // Fail-open: unresolvable price/type => KEEP (never invent a
4817
+ // violation the gate cannot prove).
4818
+ if (!Number.isFinite(price) || price <= 0)
4819
+ continue;
4820
+ if (type !== ORDER_TYPES.BUY && type !== ORDER_TYPES.SELL)
4821
+ continue;
4822
+ const check = isLastFillGuardBlocked(price, size, type, onGrid.price, liveType, inc);
4823
+ if (stats)
4824
+ stats.checked = (Number(stats.checked) || 0) + 1;
4825
+ if (!check.blocked) {
4826
+ if (stats)
4827
+ stats.passed = (Number(stats.passed) || 0) + 1;
4828
+ continue;
4829
+ }
4830
+ // Blocked: drop the op + context, restore the slot below.
4831
+ dropIdx.add(i);
4832
+ if (stats)
4833
+ stats.skipped = (Number(stats.skipped) || 0) + 1;
4834
+ const dir = type === ORDER_TYPES.BUY ? 'above' : 'below';
4835
+ dropped.push({ index: i, kind: ctx.kind, slotId, price, type });
4836
+ try {
4837
+ bot.manager?.logger?.log?.(`[LAST-FILL-GUARD] Final gate dropping ${type} ${ctx.kind} for ${slotId ?? 'unknown'} at ` +
4838
+ `${Format.formatPrice6(price)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} ` +
4839
+ `(halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)}); re-planned after market moves`, 'warn');
4840
+ }
4841
+ catch { /* logging is best-effort */ }
4842
+ }
4843
+ if (dropIdx.size === 0)
4844
+ return { dropped, pivotChanged, refreshed };
4845
+ // Compact operations/opContexts in lockstep so every surviving index
4846
+ // still lines up. The caller rebuilds cancelOpIndexByOrderId from the
4847
+ // kept contexts (it was built during op-building and goes stale for
4848
+ // every op after the first drop), and the remap pass below re-points
4849
+ // THIS batch's kept pending-broadcast entries at their new positions
4850
+ // — lockstep keeps the two arrays aligned with each other, but the
4851
+ // absolute indexes stored INSIDE pending entries are not rewritten
4852
+ // by a compaction, so they must be remapped explicitly.
4853
+ const keptOps = [];
4854
+ const keptCtxs = [];
4855
+ const oldToNew = new Map();
4856
+ for (let i = 0, ni = 0; i < opContexts.length; i++) {
4857
+ if (dropIdx.has(i))
4858
+ continue;
4859
+ oldToNew.set(i, ni);
4860
+ ni++;
4861
+ keptOps.push(operations[i]);
4862
+ keptCtxs.push(opContexts[i]);
4863
+ }
4864
+ operations.length = 0;
4865
+ operations.push(...keptOps);
4866
+ opContexts.length = 0;
4867
+ opContexts.push(...keptCtxs);
4868
+ // Slot restore: dropped rotations restore source+dest from master
4869
+ // (same sets the build loop feeds to restoreSkippedUpdateSlots...);
4870
+ // dropped creates count toward the refill-hold intersect. Pending
4871
+ // entries for dropped CREATEs are removed (never shipped); kept
4872
+ // CREATEs get their stored opIndex/ctxIndex remapped to the
4873
+ // compacted positions (see the remap pass below — an unremapped
4874
+ // absolute index would resolve to a SHIFTED context after the first
4875
+ // drop: undefined at best, a DIFFERENT create's context at worst,
4876
+ // which would let the uncertain-broadcast reconcile adopt a matched
4877
+ // chain order into the wrong slot).
4878
+ const skippedUpdateSlotIds = opts?.skippedUpdateSlotIds;
4879
+ const skippedCreateSlotIds = opts?.skippedCreateSlotIds;
4880
+ const countRef = opts?.skippedUpdateCountRef;
4881
+ try {
4882
+ const pending = bot.manager?._pendingBroadcasts;
4883
+ for (const d of dropped) {
4884
+ if (d.kind === 'rotation') {
4885
+ const act = (d.slotId && actionBySlot.get(d.slotId)) || null;
4886
+ const srcId = act?.id || null;
4887
+ if (srcId && skippedUpdateSlotIds instanceof Set)
4888
+ skippedUpdateSlotIds.add(srcId);
4889
+ if (d.slotId && skippedUpdateSlotIds instanceof Set)
4890
+ skippedUpdateSlotIds.add(d.slotId);
4891
+ if (countRef && typeof countRef === 'object')
4892
+ countRef.count = (Number(countRef.count) || 0) + 1;
4893
+ }
4894
+ else if (d.kind === 'create') {
4895
+ if (d.slotId && skippedCreateSlotIds instanceof Set)
4896
+ skippedCreateSlotIds.add(d.slotId);
4897
+ if (pending instanceof Map) {
4898
+ for (const [fp, entry] of pending) {
4899
+ // Match by slot only: the fingerprint embeds
4900
+ // side/amounts/slot (no op indexes), but entry.slotId
4901
+ // is the narrowest predicate that cannot touch a
4902
+ // sibling batch's entry for a different slot.
4903
+ if (entry?.slotId && entry.slotId === d.slotId) {
4904
+ pending.delete(fp);
4905
+ }
4906
+ }
4907
+ }
4908
+ }
4909
+ }
4910
+ // Remap THIS batch's kept pending entries (identified by the
4911
+ // fingerprint set the build loop collected — entry.batchId is
4912
+ // always null in production and cannot discriminate). A stored
4913
+ // ctxIndex whose old position was dropped means the entry was
4914
+ // not slot-matched above; it could never resolve post-compaction,
4915
+ // so it is removed. Everything else is re-pointed at the same
4916
+ // context object it was recorded with. Sibling batches' entries
4917
+ // are never touched: their indexes refer to their own long-gone
4918
+ // build-time arrays.
4919
+ if (pending instanceof Map && opts?.batchPendingFps instanceof Set) {
4920
+ for (const fp of opts.batchPendingFps) {
4921
+ const entry = pending.get(fp);
4922
+ if (!entry)
4923
+ continue; // dropped create: already removed by slot
4924
+ const newCtx = oldToNew.get(Number(entry.ctxIndex));
4925
+ if (newCtx == null) {
4926
+ // Referenced op was dropped (or the entry predates
4927
+ // lockstep indexing) — the index cannot be healed.
4928
+ pending.delete(fp);
4929
+ continue;
4930
+ }
4931
+ entry.ctxIndex = newCtx;
4932
+ const newOp = oldToNew.get(Number(entry.opIndex));
4933
+ entry.opIndex = newOp == null ? entry.opIndex : newOp;
4934
+ }
4935
+ }
4936
+ }
4937
+ catch { /* restore bookkeeping is best-effort */ }
4938
+ return { dropped, pivotChanged, refreshed };
4939
+ }
4940
+ catch {
4941
+ return empty;
4942
+ }
4943
+ }
4470
4944
  /**
4471
4945
  * Apply BTS create-fee accounting for a batch that bypassed the normal
4472
4946
  * processBatchResults pipeline (commit refused after broadcast, or
@@ -4709,7 +5183,7 @@ async function processBatchResults(bot, result, opContexts) {
4709
5183
  updateOperationCount
4710
5184
  };
4711
5185
  }
4712
- export { isLastFillGuardBlocked, resolveOnGridPivot, checkGridPriceInvariant, deriveRotationPrice, refreshLastFillPivotFromQueue, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, extractOperationResults, findMissingCreateResultContexts, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, clearPendingBroadcastsForSlots, popPushedWorkingGrid, buildChainOrderFingerprint, normalizeChainOrderForPendingMatch, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, shouldExecuteCreatePairMode, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, validateOperationFunds, resolveIdealSizeForValidation, validateOrderSizeForExecution, buildActionsFromPlan, buildCowResultFromPlan, restoreSkippedUpdateSlotsInWorkingGrid, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain, resolveRefillBoundaryHold, toRefillSlotIdSet, trackBoundaryHold };
5186
+ export { isLastFillGuardBlocked, resolveOnGridPivot, checkGridPriceInvariant, deriveRotationPrice, refreshLastFillPivotFromQueue, runFinalPivotGate, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, popPushedWorkingGrid, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, buildActionsFromPlan, buildCowResultFromPlan, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain, resolveRefillBoundaryHold, toRefillSlotIdSet, trackBoundaryHold };
4713
5187
  // Exported for regression tests (issue #23 sibling): the uncertain-broadcast
4714
5188
  // discard path must never drop a placement silently when master lost the slot.
4715
5189
  export { restoreDiscardedCreates };
@@ -4748,5 +5222,6 @@ export default {
4748
5222
  pollChainForConfirmation,
4749
5223
  updateOrdersOnChainBatchCOW,
4750
5224
  processBatchResults,
5225
+ runFinalPivotGate,
4751
5226
  };
4752
5227
  //# sourceMappingURL=dexbot_cow_runtime.js.map