dexbot 1.4.24 → 1.4.25

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 (131) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +1 -0
  3. package/analysis/README.md +49 -0
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/grid_correction_check.ts +896 -0
  6. package/analysis/trend_detection/package.json +1 -1
  7. package/claw/package.json +1 -1
  8. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  9. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  10. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  11. package/dist/analysis/grid_correction_check.d.ts +90 -0
  12. package/dist/analysis/grid_correction_check.d.ts.map +1 -0
  13. package/dist/analysis/grid_correction_check.js +861 -0
  14. package/dist/analysis/grid_correction_check.js.map +1 -0
  15. package/dist/dexbot.d.ts.map +1 -1
  16. package/dist/dexbot.js +62 -6
  17. package/dist/dexbot.js.map +1 -1
  18. package/dist/modules/account_bots.d.ts.map +1 -1
  19. package/dist/modules/account_bots.js +53 -5
  20. package/dist/modules/account_bots.js.map +1 -1
  21. package/dist/modules/account_orders.d.ts +7 -10
  22. package/dist/modules/account_orders.d.ts.map +1 -1
  23. package/dist/modules/account_orders.js +26 -2
  24. package/dist/modules/account_orders.js.map +1 -1
  25. package/dist/modules/chain_orders.d.ts.map +1 -1
  26. package/dist/modules/chain_orders.js +21 -2
  27. package/dist/modules/chain_orders.js.map +1 -1
  28. package/dist/modules/config.d.ts.map +1 -1
  29. package/dist/modules/config.js +27 -0
  30. package/dist/modules/config.js.map +1 -1
  31. package/dist/modules/constants.d.ts +14 -1
  32. package/dist/modules/constants.d.ts.map +1 -1
  33. package/dist/modules/constants.js +57 -6
  34. package/dist/modules/constants.js.map +1 -1
  35. package/dist/modules/dexbot_class.d.ts +24 -0
  36. package/dist/modules/dexbot_class.d.ts.map +1 -1
  37. package/dist/modules/dexbot_class.js +66 -12
  38. package/dist/modules/dexbot_class.js.map +1 -1
  39. package/dist/modules/dexbot_cow_runtime.d.ts +22 -17
  40. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  41. package/dist/modules/dexbot_cow_runtime.js +646 -128
  42. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  43. package/dist/modules/dexbot_fill_runtime.d.ts +11 -1
  44. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  45. package/dist/modules/dexbot_fill_runtime.js +108 -1
  46. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  47. package/dist/modules/dexbot_maintenance_runtime.d.ts +17 -1
  48. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  49. package/dist/modules/dexbot_maintenance_runtime.js +215 -26
  50. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  51. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  52. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  53. package/dist/modules/dexbot_startup_runtime.js +58 -7
  54. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  55. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  56. package/dist/modules/dexbot_state_recovery.js +55 -3
  57. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  58. package/dist/modules/graceful_shutdown.d.ts +12 -1
  59. package/dist/modules/graceful_shutdown.d.ts.map +1 -1
  60. package/dist/modules/graceful_shutdown.js +39 -1
  61. package/dist/modules/graceful_shutdown.js.map +1 -1
  62. package/dist/modules/key_store.d.ts.map +1 -1
  63. package/dist/modules/key_store.js +38 -1
  64. package/dist/modules/key_store.js.map +1 -1
  65. package/dist/modules/order/accounting.d.ts.map +1 -1
  66. package/dist/modules/order/accounting.js +20 -3
  67. package/dist/modules/order/accounting.js.map +1 -1
  68. package/dist/modules/order/grid.d.ts +1 -1
  69. package/dist/modules/order/grid.d.ts.map +1 -1
  70. package/dist/modules/order/grid.js +288 -31
  71. package/dist/modules/order/grid.js.map +1 -1
  72. package/dist/modules/order/grid_reconcile.d.ts +4 -2
  73. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  74. package/dist/modules/order/grid_reconcile.js +242 -222
  75. package/dist/modules/order/grid_reconcile.js.map +1 -1
  76. package/dist/modules/order/grid_reconcile_internal.d.ts +11 -1
  77. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  78. package/dist/modules/order/grid_reconcile_internal.js +132 -6
  79. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  80. package/dist/modules/order/manager.d.ts +68 -14
  81. package/dist/modules/order/manager.d.ts.map +1 -1
  82. package/dist/modules/order/manager.js +241 -52
  83. package/dist/modules/order/manager.js.map +1 -1
  84. package/dist/modules/order/strategy.d.ts.map +1 -1
  85. package/dist/modules/order/strategy.js +25 -8
  86. package/dist/modules/order/strategy.js.map +1 -1
  87. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  88. package/dist/modules/order/sync_engine.js +261 -166
  89. package/dist/modules/order/sync_engine.js.map +1 -1
  90. package/dist/modules/order/utils/math.d.ts +47 -5
  91. package/dist/modules/order/utils/math.d.ts.map +1 -1
  92. package/dist/modules/order/utils/math.js +175 -11
  93. package/dist/modules/order/utils/math.js.map +1 -1
  94. package/dist/modules/order/utils/order.d.ts +16 -15
  95. package/dist/modules/order/utils/order.d.ts.map +1 -1
  96. package/dist/modules/order/utils/order.js +199 -32
  97. package/dist/modules/order/utils/order.js.map +1 -1
  98. package/dist/modules/order/utils/slot.d.ts +8 -0
  99. package/dist/modules/order/utils/slot.d.ts.map +1 -0
  100. package/dist/modules/order/utils/slot.js +16 -0
  101. package/dist/modules/order/utils/slot.js.map +1 -0
  102. package/dist/modules/order/utils/system.d.ts.map +1 -1
  103. package/dist/modules/order/utils/system.js +3 -2
  104. package/dist/modules/order/utils/system.js.map +1 -1
  105. package/dist/modules/order/utils/validate.d.ts +5 -8
  106. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  107. package/dist/modules/order/utils/validate.js +64 -82
  108. package/dist/modules/order/utils/validate.js.map +1 -1
  109. package/dist/modules/order/working_grid.d.ts +5 -0
  110. package/dist/modules/order/working_grid.d.ts.map +1 -1
  111. package/dist/modules/order/working_grid.js +21 -0
  112. package/dist/modules/order/working_grid.js.map +1 -1
  113. package/dist/modules/paths.d.ts +4 -2
  114. package/dist/modules/paths.d.ts.map +1 -1
  115. package/dist/modules/paths.js +79 -24
  116. package/dist/modules/paths.js.map +1 -1
  117. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  118. package/dist/scripts/analyze-orders.js +23 -2
  119. package/dist/scripts/analyze-orders.js.map +1 -1
  120. package/dist/scripts/generate_market_adapter_whitelist.d.ts +2 -5
  121. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  122. package/dist/scripts/generate_market_adapter_whitelist.js +83 -1
  123. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  124. package/docs/DEXBOT_COMPARISON.md +11 -11
  125. package/docs/EVOLUTION.md +9 -8
  126. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  127. package/docs/GRID_RECALCULATION.md +6 -0
  128. package/docs/README.md +2 -2
  129. package/market_adapter/README.md +10 -0
  130. package/package.json +2 -1
  131. package/scripts/README.md +4 -0
@@ -51,6 +51,9 @@ function calculateSwapInAmount(...args) { return require('./order/utils/math').c
51
51
  function floatToBlockchainInt(...args) { return require('./order/utils/math').floatToBlockchainInt(...args); }
52
52
  function blockchainToFloat(...args) { return require('./order/utils/math').blockchainToFloat(...args); }
53
53
  function updateDynamicGridSnapshotSync(...args) { return require('../market_adapter/utils/dynamic_grid_snapshot').updateDynamicGridSnapshotSync(...args); }
54
+ // Lazy require: dexbot_fill_runtime imports this module, so a static import
55
+ // would be circular; at call time both modules are fully loaded.
56
+ function scheduleFillConsumerRestartFn(...args) { return require('./dexbot_fill_runtime').scheduleFillConsumerRestart(...args); }
54
57
  function reconcileGridOrders(...args) { return require('./order/grid_reconcile').reconcileGridOrders(...args); }
55
58
  function formatUnmatchedChainOrder(...args) { return require('./order/utils/order').formatUnmatchedChainOrder(...args); }
56
59
  function getSideBudget(...args) { return require('./order/utils/order').getSideBudget(...args); }
@@ -94,7 +97,7 @@ const GRID_RESYNC_REASONS = Object.freeze({
94
97
  ...MARKET_ADAPTER_TRIGGER_RESETS,
95
98
  manual_grid_resync: MANUAL_TRIGGER_METADATA,
96
99
  rms_structural_grid_resync: {
97
- shouldRefreshCenterPrice: true,
100
+ shouldRefreshCenterPrice: false,
98
101
  centerRefreshContext: 'RMS structural grid resync',
99
102
  centerRefreshLabel: 'RMS structural grid resync',
100
103
  },
@@ -782,14 +785,38 @@ function performGridResync(bot, options = {}) {
782
785
  // of virtualizing live ACTIVE slots; the trigger file is retained
783
786
  // so the resync retries on a clean read.
784
787
  let resyncReadAmbiguous = false;
788
+ const readOpenOrdersForResync = (label) => readOpenOrdersGuarded(chainOrders, self.accountId, {
789
+ log: (message, level) => self._log(message, level),
790
+ label,
791
+ detail: 'trigger-file resync',
792
+ });
793
+ // Empty-read confirm guard: an EMPTY read during a trigger reset is
794
+ // only accepted after one confirming re-read. A single 0-order
795
+ // snapshot from a lagging/partial node must never wipe a live grid
796
+ // and rebuild over existing orders (phantom reset — duplicated
797
+ // price levels and mis-tracked orders). A contradicted re-read
798
+ // (non-empty) feeds the fresh snapshot to the resync so the rebuild
799
+ // reconciles against the real chain state instead of overwriting it.
785
800
  const readFn = async () => {
786
- const orders = await readOpenOrdersGuarded(chainOrders, self.accountId, {
787
- log: (message, level) => self._log(message, level),
788
- label: 'GRID-RESYNC',
789
- detail: 'trigger-file resync',
790
- });
791
- if (orders === null)
801
+ const orders = await readOpenOrdersForResync('GRID-RESYNC');
802
+ if (orders === null) {
792
803
  resyncReadAmbiguous = true;
804
+ return orders;
805
+ }
806
+ if (orders.length > 0)
807
+ return orders;
808
+ await sleep(TIMING.SYNC_EMPTY_READ_CONFIRM_DELAY_MS);
809
+ const confirmed = await readOpenOrdersForResync('GRID-RESYNC-CONFIRM');
810
+ if (confirmed === null) {
811
+ resyncReadAmbiguous = true;
812
+ return orders;
813
+ }
814
+ if (confirmed.length > 0) {
815
+ self._log(`[GRID-RESYNC] Empty open-order read contradicted by confirm re-read ` +
816
+ `(${confirmed.length} order(s) present) — using fresh non-empty snapshot`, 'warn');
817
+ return confirmed;
818
+ }
819
+ self._log('[GRID-RESYNC] Empty open-order read confirmed by re-read — accepting empty account', 'info');
793
820
  return orders;
794
821
  };
795
822
  await recalculateGrid(self.manager, {
@@ -861,8 +888,11 @@ async function handlePendingTriggerReset(bot) {
861
888
  bot._log('Pending trigger file detected. Processing reset before startup...');
862
889
  const triggerInfo = readTriggerMetadata(bot.triggerFile);
863
890
  let resetSucceeded = false;
891
+ // skipIdle: a pending trigger is an explicit reset command that must run
892
+ // before the startup sequence. The idle gate can block it indefinitely
893
+ // when the fill queue never drains, which would silently drop the reset.
864
894
  await bot.manager._fillProcessingLock.acquire(async () => {
865
- resetSucceeded = await performGridResync(bot, buildGridResyncOptions(triggerInfo));
895
+ resetSucceeded = await performGridResync(bot, { ...buildGridResyncOptions(triggerInfo), skipIdle: true });
866
896
  });
867
897
  if (!resetSucceeded) {
868
898
  bot._warn('Pending trigger reset failed. Continuing with normal startup path.');
@@ -907,7 +937,13 @@ async function setupTriggerFileDetection(bot) {
907
937
  bot.manager._fillProcessingLock.acquire(async () => {
908
938
  if (bot._shuttingDown)
909
939
  return;
910
- const ok = await performGridResync(bot, buildGridResyncOptions(triggerInfo));
940
+ // skipIdle: a trigger file is an explicit reset command.
941
+ // Deferring it behind the idle cooldown can loop forever
942
+ // when grid activity is continuous (fills arriving during
943
+ // placement batches refresh the settle window); the fill
944
+ // lock below already serializes the reset with in-flight
945
+ // batches.
946
+ const ok = await performGridResync(bot, { ...buildGridResyncOptions(triggerInfo), skipIdle: true });
911
947
  if (!ok) {
912
948
  bot._warn('Runtime trigger reset failed; retaining existing grid state.');
913
949
  }
@@ -1003,7 +1039,7 @@ function startOpenOrdersSyncLoop(bot) {
1003
1039
  if (syncResult?.filledOrders && syncResult.filledOrders.length > 0) {
1004
1040
  bot._log(`Open-orders sync loop: ${syncResult.filledOrders.length} grid order(s) found filled on-chain. Triggering rebalance.`, 'info');
1005
1041
  bot._markGridActivity?.('open-orders sync fill');
1006
- const batchResult = await bot._processFillsWithBatching(syncResult.filledOrders, new Set(), 'open-orders sync fill rebalance');
1042
+ const batchResult = await bot._processFillsWithBatching(syncResult.filledOrders, new Set(), 'open-orders sync fill rebalance', { isReplay: true });
1007
1043
  if (!batchResult?.aborted) {
1008
1044
  await bot.manager.persistGrid();
1009
1045
  }
@@ -1134,7 +1170,7 @@ function setupBlockchainFetchInterval(bot) {
1134
1170
  if (syncResult.filledOrders && syncResult.filledOrders.length > 0) {
1135
1171
  bot._log(`Periodic sync: ${syncResult.filledOrders.length} grid order(s) found filled on-chain. Triggering rebalance.`, 'info');
1136
1172
  bot._markGridActivity?.('periodic sync fill rebalance');
1137
- const batchResult = await bot._processFillsWithBatching(syncResult.filledOrders, new Set(), 'periodic sync fill rebalance');
1173
+ const batchResult = await bot._processFillsWithBatching(syncResult.filledOrders, new Set(), 'periodic sync fill rebalance', { isReplay: true });
1138
1174
  if (!batchResult?.aborted) {
1139
1175
  await bot.manager.persistGrid();
1140
1176
  }
@@ -1180,6 +1216,72 @@ function stopBlockchainFetchInterval(bot) {
1180
1216
  bot._log('Stopped periodic blockchain fetch interval');
1181
1217
  }
1182
1218
  }
1219
+ /**
1220
+ * Set up the periodic bots.json fingerprint poll interval.
1221
+ * Decoupled from the heavy blockchain fetch interval (default 240min) so
1222
+ * config changes are visible within BOTS_CONFIG_POLL_INTERVAL_MS (default 5min).
1223
+ * Uses the shared fingerprint via syncMarketAdapterOnPeriodicConfigCheck so
1224
+ * the market adapter start/stop logic stays in one place.
1225
+ * @param {import('./dexbot_class.js').DEXBot} bot
1226
+ */
1227
+ function setupBotsConfigPollInterval(bot) {
1228
+ // Allow per-bot timing override (e.g. tests or per-bot tuning)
1229
+ let intervalMs = bot.config?.timing?.BOTS_CONFIG_POLL_INTERVAL_MS;
1230
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
1231
+ intervalMs = Number(TIMING.BOTS_CONFIG_POLL_INTERVAL_MS);
1232
+ }
1233
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
1234
+ bot._log(`Bots-config poll interval disabled (value: ${intervalMs}). Fingerprint changes will only be seen on blockchain fetch ticks.`);
1235
+ return;
1236
+ }
1237
+ // Ensure the fingerprint exists before the first interval fires.
1238
+ // setupBlockchainFetchInterval already fires syncMarketAdapterOnPeriodicConfigCheck
1239
+ // at startup, so this is a fallback for bots that never run the blockchain interval
1240
+ // (e.g. dryRun or accountId missing).
1241
+ if (!bot._marketAdapterWatchdogFingerprint) {
1242
+ syncMarketAdapterOnPeriodicConfigCheck(bot, 'startup bots-config poll setup').catch((err) => {
1243
+ bot._warn(`Bots-config poll setup failed: ${getErrorMessage(err)}`);
1244
+ });
1245
+ }
1246
+ if (bot._botsConfigPollInterval !== null && bot._botsConfigPollInterval !== undefined) {
1247
+ stopBotsConfigPollInterval(bot);
1248
+ }
1249
+ bot._botsConfigPollInterval = setInterval(async () => {
1250
+ if (bot._shuttingDown)
1251
+ return;
1252
+ // Coalesce with the blockchain-fetch tick: if its tick is already
1253
+ // driving the watchdog, skip to avoid double PM2 queries.
1254
+ if (bot._marketAdapterWatchdogInFlight)
1255
+ return;
1256
+ if (bot._botsConfigPollInFlight)
1257
+ return;
1258
+ bot._botsConfigPollInFlight = true;
1259
+ try {
1260
+ await syncMarketAdapterOnPeriodicConfigCheck(bot, 'bots-config poll');
1261
+ }
1262
+ catch (err) {
1263
+ bot._warn(`Bots-config poll failed: ${getErrorMessage(err)}`);
1264
+ }
1265
+ finally {
1266
+ bot._botsConfigPollInFlight = false;
1267
+ }
1268
+ }, intervalMs);
1269
+ if (typeof bot._botsConfigPollInterval.unref === 'function') {
1270
+ bot._botsConfigPollInterval.unref();
1271
+ }
1272
+ bot._log(`Started bots-config poll interval: every ${Math.round(intervalMs / 1000)}s (fingerprint check)`);
1273
+ }
1274
+ /**
1275
+ * Stop the periodic bots.json fingerprint poll interval.
1276
+ * @param {import('./dexbot_class.js').DEXBot} bot
1277
+ */
1278
+ function stopBotsConfigPollInterval(bot) {
1279
+ if (bot._botsConfigPollInterval !== null && bot._botsConfigPollInterval !== undefined) {
1280
+ clearInterval(bot._botsConfigPollInterval);
1281
+ bot._botsConfigPollInterval = null;
1282
+ bot._log('Stopped bots-config poll interval');
1283
+ }
1284
+ }
1183
1285
  /**
1184
1286
  * Release the market adapter runtime for a bot.
1185
1287
  * In PM2 mode this is a no-op; in direct mode it calls the shared runtime's releaseBot.
@@ -1417,7 +1519,13 @@ async function executeMaintenanceLogic(bot, context) {
1417
1519
  if (pendingCorrections > 0) {
1418
1520
  const correctionResult = await correctAllPriceMismatches(bot.manager, bot.account, bot.privateKey, chainOrders);
1419
1521
  if (correctionResult.failed > 0) {
1420
- bot._warn(`[MAINT] ${correctionResult.failed}/${pendingCorrections} price correction(s) failed`);
1522
+ const failedDetails = (correctionResult.results || [])
1523
+ .filter((r) => !(r.result && r.result.success))
1524
+ .slice(0, 3)
1525
+ .map((r) => `${r.chainOrderId ?? '?'}: ${r.result?.error ?? 'unknown'}`)
1526
+ .join(' | ');
1527
+ bot._warn(`[MAINT] ${correctionResult.failed}/${pendingCorrections} price correction(s) failed` +
1528
+ (failedDetails ? ` — ${failedDetails}` : ''));
1421
1529
  }
1422
1530
  }
1423
1531
  // Dust detection runs before the pipeline gate. Cancellation is immediate
@@ -1652,7 +1760,7 @@ async function cancelDustOrders(bot, { buy: buyDust = [], sell: sellDust = [] }
1652
1760
  }
1653
1761
  if (syntheticFills.length === 0)
1654
1762
  return { cancelledCount: 0, batchResult: null };
1655
- const result = await bot._processFillsWithBatching(syntheticFills, new Set(), `dust cancel [${syntheticFills.map((o) => o.id).join(', ')}]`);
1763
+ const result = await bot._processFillsWithBatching(syntheticFills, new Set(), `dust cancel [${syntheticFills.map((o) => o.id).join(', ')}]`, { skipAnchorUpdate: true });
1656
1764
  if (!result.aborted) {
1657
1765
  await bot.manager.persistGrid();
1658
1766
  }
@@ -1896,6 +2004,32 @@ function setupDustHealthCheckInterval(bot) {
1896
2004
  bot._dustHealthCheckTimer.unref();
1897
2005
  }
1898
2006
  }
2007
+ /**
2008
+ * Retrigger the fill consumer once the recovery/pipeline window has fully
2009
+ * cleared. The consumer's defer branch (batchInFlight / recoverySyncInFlight /
2010
+ * broadcasting) returns WITHOUT rescheduling, so fills enqueued during a long
2011
+ * window would otherwise sit in the queue forever — which also keeps the
2012
+ * maintenance idle gate shut (queue-length check returns the full settle
2013
+ * delay) and blocks deferred grid resyncs and periodic maintenance.
2014
+ * @param {import('./dexbot_class.js').DEXBot} bot
2015
+ */
2016
+ function drainFillQueueAfterPipelineClear(bot) {
2017
+ try {
2018
+ if (!bot || bot._shuttingDown)
2019
+ return;
2020
+ if ((bot._recoverySyncInFlight || 0) > 0)
2021
+ return;
2022
+ if ((bot._batchInFlight || 0) > 0)
2023
+ return;
2024
+ if (bot.manager?.isBroadcastingActive?.())
2025
+ return;
2026
+ if (!bot._incomingFillQueue || bot._incomingFillQueue.length === 0)
2027
+ return;
2028
+ bot._log(`[FILL-QUEUE] Pipeline cleared; draining ${bot._incomingFillQueue.length} deferred fill(s).`, 'info');
2029
+ scheduleFillConsumerRestartFn(bot, chainOrders);
2030
+ }
2031
+ catch { }
2032
+ }
1899
2033
  /**
1900
2034
  * Request a full grid reset from fresh on-chain state.
1901
2035
  * @param {import('./dexbot_class.js').DEXBot} bot
@@ -1930,6 +2064,8 @@ async function requestGridReset(bot, reason = 'structural change', options = {})
1930
2064
  }
1931
2065
  finally {
1932
2066
  bot._recoverySyncInFlight = Math.max(0, (bot._recoverySyncInFlight || 0) - 1);
2067
+ if ((bot._recoverySyncInFlight || 0) === 0)
2068
+ drainFillQueueAfterPipelineClear(bot);
1933
2069
  }
1934
2070
  }
1935
2071
  return bot.manager._fillProcessingLock.acquire(async () => performGridResync(bot, resetOptions));
@@ -1943,14 +2079,21 @@ function wireStructuralGridResyncRequest(bot) {
1943
2079
  return;
1944
2080
  bot.manager.requestStructuralGridResync = async (reason = 'structural recovery', details = {}) => {
1945
2081
  if (bot._shuttingDown) {
2082
+ bot._warn(`[RECOVERY] Structural resync skip (shutting down): ${reason}`);
1946
2083
  return { skipped: true, reason: 'shutting down' };
1947
2084
  }
1948
2085
  if (bot._structuralGridResyncRunning || bot._structuralGridResyncTimer) {
1949
- return { skipped: true, reason: 'structural grid resync already scheduled' };
2086
+ const why = bot._structuralGridResyncRunning ? 'already running' : 'already scheduled';
2087
+ bot._warn(`[RECOVERY] Structural resync skip (${why}): ${reason}`);
2088
+ return { skipped: true, reason: `structural grid resync ${why}` };
1950
2089
  }
1951
2090
  const unmatchedCount = Array.isArray(details?.unmatchedChainOrders)
1952
2091
  ? details.unmatchedChainOrders.length
1953
2092
  : 0;
2093
+ // P5 max-defer: cap batchInFlight re-arm so a fill storm cannot starve
2094
+ // the resync forever (a fill-storm defer loop once kept the flag armed
2095
+ // indefinitely). After the cap, the resync forces instead of re-arming.
2096
+ const STRUCTURAL_RESYNC_MAX_DEFER_MS = 30000;
1954
2097
  const runStructuralResync = async () => {
1955
2098
  bot._structuralGridResyncTimer = null;
1956
2099
  if (bot._shuttingDown)
@@ -1962,8 +2105,36 @@ function wireStructuralGridResyncRequest(bot) {
1962
2105
  // completes; the fill consumer is already gated on _batchInFlight,
1963
2106
  // so this only blocks the recovery itself, never new fills.
1964
2107
  if (bot._batchInFlight > 0) {
1965
- bot._structuralGridResyncTimer = setTimeout(runStructuralResync, TIMING.LOCK_REFRESH_MIN_MS);
1966
- return;
2108
+ const now = Date.now();
2109
+ if (!bot._structuralGridResyncDeferStartedAt) {
2110
+ bot._structuralGridResyncDeferStartedAt = now;
2111
+ bot._structuralGridResyncDeferCount = 0;
2112
+ }
2113
+ bot._structuralGridResyncDeferCount = (bot._structuralGridResyncDeferCount || 0) + 1;
2114
+ const deferredMs = now - bot._structuralGridResyncDeferStartedAt;
2115
+ if (deferredMs >= STRUCTURAL_RESYNC_MAX_DEFER_MS) {
2116
+ bot._warn(`[RECOVERY] Structural resync max-defer reached (${deferredMs}ms, cap ${STRUCTURAL_RESYNC_MAX_DEFER_MS}ms, deferred ${bot._structuralGridResyncDeferCount}x) — forcing despite _batchInFlight=${bot._batchInFlight}: ${reason}`);
2117
+ bot._structuralGridResyncDeferStartedAt = null;
2118
+ bot._structuralGridResyncDeferCount = 0;
2119
+ }
2120
+ else {
2121
+ // Fix #7 (docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md §2): throttle the defer log to the
2122
+ // first occurrence per cap window (a fill storm can emit hundreds of
2123
+ // defer lines in seconds); count the rest and surface them only in
2124
+ // the final forced message.
2125
+ if (bot._structuralGridResyncDeferCount === 1) {
2126
+ bot._warn(`[RECOVERY] Structural resync defer (batchInFlight=${bot._batchInFlight}, deferred ${deferredMs}ms/${STRUCTURAL_RESYNC_MAX_DEFER_MS}ms): ${reason}`);
2127
+ }
2128
+ else {
2129
+ bot.manager?.logger?.log?.(`[RECOVERY] Structural resync defer (suppressed; count=${bot._structuralGridResyncDeferCount}, deferred ${deferredMs}ms/${STRUCTURAL_RESYNC_MAX_DEFER_MS}ms): ${reason}`, 'debug');
2130
+ }
2131
+ bot._structuralGridResyncTimer = setTimeout(runStructuralResync, TIMING.LOCK_REFRESH_MIN_MS);
2132
+ return;
2133
+ }
2134
+ }
2135
+ else {
2136
+ bot._structuralGridResyncDeferStartedAt = null;
2137
+ bot._structuralGridResyncDeferCount = 0;
1967
2138
  }
1968
2139
  bot._structuralGridResyncRunning++;
1969
2140
  try {
@@ -1989,14 +2160,30 @@ function wireStructuralGridResyncRequest(bot) {
1989
2160
  }
1990
2161
  const suffix = unmatchedCount > 0 ? ` (${unmatchedCount} unmatched chain order(s))` : '';
1991
2162
  bot._warn(`[RECOVERY] Running structural full grid resync for ${reason}${suffix}`);
1992
- const resetResult = await bot.requestGridReset('rms_structural_grid_resync', {
1993
- refreshCenterPrice: true,
1994
- // Structural resync is a chain-read/rebuild; do not let a
1995
- // fill storm starve it behind the idle cooldown or the
1996
- // fill-processing lock.
1997
- skipIdle: true,
1998
- skipFillLock: true,
1999
- });
2163
+ // Same protection as the persisted-grid reload above: the
2164
+ // reset's reconcile Phase-2 placement runs for minutes; a
2165
+ // fill arriving mid-reset must not start a COW batch whose
2166
+ // broadcast overlaps it (commit refusal + duplicate
2167
+ // generation orphans). _recoverFromPersistedGrid already
2168
+ // incremented and released the counter; increment again
2169
+ // for the full-resync branch.
2170
+ bot._recoverySyncInFlight = (bot._recoverySyncInFlight || 0) + 1;
2171
+ let resetResult;
2172
+ try {
2173
+ resetResult = await bot.requestGridReset('rms_structural_grid_resync', {
2174
+ refreshCenterPrice: false,
2175
+ // Structural resync is a chain-read/rebuild; do not let a
2176
+ // fill storm starve it behind the idle cooldown or the
2177
+ // fill-processing lock.
2178
+ skipIdle: true,
2179
+ skipFillLock: true,
2180
+ });
2181
+ }
2182
+ finally {
2183
+ bot._recoverySyncInFlight = Math.max(0, (bot._recoverySyncInFlight || 0) - 1);
2184
+ if ((bot._recoverySyncInFlight || 0) === 0)
2185
+ drainFillQueueAfterPipelineClear(bot);
2186
+ }
2000
2187
  if (resetResult && bot.manager?._recoveryState) {
2001
2188
  bot.manager._recoveryState = { ...bot.manager._recoveryState, attemptCount: 0, lastAttemptAt: 0, lastFailureAt: 0 };
2002
2189
  }
@@ -2101,7 +2288,7 @@ async function syncOpenOrdersAndProcessFillsImpl(bot, tag) {
2101
2288
  if (syncResult?.filledOrders?.length > 0) {
2102
2289
  bot._refreshDynamicWeightDistribution(`${tag} sync-fill`);
2103
2290
  bot._log(`[SYNC-CHAIN] ${syncResult.filledOrders.length} filled order(s) found during ${tag}`, 'info');
2104
- const batchResult = await bot._processFillsWithBatching(syncResult.filledOrders, new Set(), `${tag} sync-fill`);
2291
+ const batchResult = await bot._processFillsWithBatching(syncResult.filledOrders, new Set(), `${tag} sync-fill`, { isReplay: true });
2105
2292
  if (!batchResult?.aborted) {
2106
2293
  // Reassign the returned snapshot to the post-fill re-read: the
2107
2294
  // caller feeds openOrders into reconcileGridOrders, which must
@@ -2128,7 +2315,7 @@ async function syncOpenOrdersAndProcessFillsImpl(bot, tag) {
2128
2315
  return { syncResult: null, aborted: true, hasUnmatched: -1, openOrders: null };
2129
2316
  }
2130
2317
  }
2131
- export { loadBotsConfigSnapshot, refreshDynamicWeightDistribution, performGridResync, updateBotGridResetMetadata, handlePendingTriggerReset, setupTriggerFileDetection, performPeriodicGridChecks, isOpenOrdersSyncLoopEnabled, startOpenOrdersSyncLoop, stopOpenOrdersSyncLoop, setupBlockchainFetchInterval, stopBlockchainFetchInterval, executeMaintenanceLogic, cancelDustOrders, isOrderDoesNotExistError, runGridMaintenance, stopMarketAdapterPm2, releaseMarketAdapterRuntime, syncMarketAdapterOnPeriodicConfigCheck, findSnapshotBotForRuntimeConfig, runtimeConfigNeedsMarketAdapter, usesAmaGridPrice, checkBtsBalanceAndAcquire, acquireBts, runDustHealthCheck, setupDustHealthCheckInterval, requestGridReset, wireStructuralGridResyncRequest, getPipelineSignals, markGridActivity, getMetrics, syncOpenOrdersAndProcessFills };
2318
+ export { loadBotsConfigSnapshot, refreshDynamicWeightDistribution, performGridResync, updateBotGridResetMetadata, handlePendingTriggerReset, setupTriggerFileDetection, performPeriodicGridChecks, isOpenOrdersSyncLoopEnabled, startOpenOrdersSyncLoop, stopOpenOrdersSyncLoop, setupBlockchainFetchInterval, stopBlockchainFetchInterval, setupBotsConfigPollInterval, stopBotsConfigPollInterval, executeMaintenanceLogic, cancelDustOrders, isOrderDoesNotExistError, runGridMaintenance, stopMarketAdapterPm2, releaseMarketAdapterRuntime, syncMarketAdapterOnPeriodicConfigCheck, findSnapshotBotForRuntimeConfig, runtimeConfigNeedsMarketAdapter, usesAmaGridPrice, checkBtsBalanceAndAcquire, acquireBts, runDustHealthCheck, setupDustHealthCheckInterval, requestGridReset, wireStructuralGridResyncRequest, getPipelineSignals, markGridActivity, getMetrics, syncOpenOrdersAndProcessFills };
2132
2319
  export default {
2133
2320
  loadBotsConfigSnapshot,
2134
2321
  refreshDynamicWeightDistribution,
@@ -2142,6 +2329,8 @@ export default {
2142
2329
  stopOpenOrdersSyncLoop,
2143
2330
  setupBlockchainFetchInterval,
2144
2331
  stopBlockchainFetchInterval,
2332
+ setupBotsConfigPollInterval,
2333
+ stopBotsConfigPollInterval,
2145
2334
  executeMaintenanceLogic,
2146
2335
  cancelDustOrders,
2147
2336
  isOrderDoesNotExistError,