@jsonstudio/llms 0.6.1172 → 0.6.1354

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 (160) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
  2. package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
  4. package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
  5. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
  7. package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
  8. package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
  9. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  10. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
  12. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  13. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  42. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  43. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  47. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  52. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  53. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  54. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  57. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  58. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  59. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  61. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  62. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  63. package/dist/conversion/hub/process/chat-process.js +256 -16
  64. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  65. package/dist/conversion/hub/response/provider-response.js +85 -27
  66. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  67. package/dist/conversion/hub/response/response-mappers.js +30 -6
  68. package/dist/conversion/hub/response/response-runtime.js +4 -38
  69. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  70. package/dist/conversion/hub/standardized-bridge.js +23 -15
  71. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  72. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  73. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  74. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  75. package/dist/conversion/shared/jsonish.js +1 -1
  76. package/dist/conversion/shared/mcp-injection.js +67 -33
  77. package/dist/conversion/shared/openai-finalizer.js +2 -1
  78. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  79. package/dist/conversion/shared/responses-output-builder.js +6 -0
  80. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  81. package/dist/conversion/shared/runtime-metadata.js +23 -0
  82. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  83. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  84. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  85. package/dist/conversion/shared/tool-governor.js +3 -3
  86. package/dist/filters/engine.js +5 -5
  87. package/dist/filters/special/request-tool-list-filter.js +194 -60
  88. package/dist/filters/special/request-tools-normalize.js +1 -1
  89. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  90. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  91. package/dist/filters/special/tool-filter-hooks.js +58 -62
  92. package/dist/guidance/index.js +5 -1
  93. package/dist/http/sse-response.js +6 -6
  94. package/dist/router/virtual-router/bootstrap.js +48 -4
  95. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  96. package/dist/router/virtual-router/engine-health.js +11 -110
  97. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
  98. package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
  99. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  100. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  101. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  102. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  103. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  104. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  105. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  106. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  107. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  108. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  109. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  110. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  111. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  112. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  113. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  114. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  115. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  116. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  117. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  118. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
  119. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  120. package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
  121. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  122. package/dist/router/virtual-router/engine-selection.js +10 -962
  123. package/dist/router/virtual-router/engine.d.ts +1 -0
  124. package/dist/router/virtual-router/engine.js +55 -10
  125. package/dist/router/virtual-router/routing-instructions.js +6 -1
  126. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  127. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  128. package/dist/router/virtual-router/types.d.ts +25 -1
  129. package/dist/servertool/clock/config.d.ts +8 -0
  130. package/dist/servertool/clock/config.js +22 -0
  131. package/dist/servertool/clock/log.d.ts +3 -0
  132. package/dist/servertool/clock/log.js +13 -0
  133. package/dist/servertool/clock/task-store.d.ts +1 -1
  134. package/dist/servertool/clock/task-store.js +1 -1
  135. package/dist/servertool/clock/tasks.js +1 -1
  136. package/dist/servertool/engine.js +146 -21
  137. package/dist/servertool/handlers/clock-auto.js +11 -6
  138. package/dist/servertool/handlers/clock.js +36 -10
  139. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  140. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  141. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  142. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  143. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  144. package/dist/servertool/handlers/vision.js +4 -1
  145. package/dist/servertool/handlers/web-search.js +3 -1
  146. package/dist/servertool/pending-session.d.ts +19 -0
  147. package/dist/servertool/pending-session.js +97 -0
  148. package/dist/servertool/reenter-backend.js +5 -3
  149. package/dist/servertool/server-side-tools.js +235 -6
  150. package/dist/servertool/types.d.ts +13 -0
  151. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  152. package/dist/sse/shared/chat-serializer.js +2 -2
  153. package/dist/sse/shared/constants.js +1 -1
  154. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  155. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  156. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  157. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  158. package/dist/tools/exec-command/normalize.js +4 -0
  159. package/dist/tools/exec-command/regression-capturer.js +1 -1
  160. package/package.json +10 -5
@@ -17,6 +17,7 @@ export declare class VirtualRouterEngine {
17
17
  private readonly contextAdvisor;
18
18
  private contextRouting;
19
19
  private routeStats;
20
+ private readonly aliasQueueStore;
20
21
  private readonly debug;
21
22
  private healthConfig;
22
23
  private readonly statsCenter;
@@ -10,7 +10,7 @@ import { parseRoutingInstructions, applyRoutingInstructions, cleanMessagesFromRo
10
10
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from './sticky-session-store.js';
11
11
  import { buildHitReason, formatVirtualRouterHit } from './engine-logging.js';
12
12
  import { selectDirectProviderModel, selectFromStickyPool, selectProviderImpl } from './engine-selection.js';
13
- import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
13
+ import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
14
14
  import { mergeStopMessageFromPersisted } from './stop-message-state-sync.js';
15
15
  export class VirtualRouterEngine {
16
16
  routing = {};
@@ -22,6 +22,9 @@ export class VirtualRouterEngine {
22
22
  contextAdvisor = new ContextAdvisor();
23
23
  contextRouting;
24
24
  routeStats = new Map();
25
+ // Alias selection state (global within this VirtualRouterEngine instance).
26
+ // Used by alias-selection strategies to avoid rapid cross-alias switching.
27
+ aliasQueueStore = new Map();
25
28
  debug = console; // thin hook; host may monkey-patch for colored logging
26
29
  healthConfig = null;
27
30
  statsCenter = getStatsCenter();
@@ -161,7 +164,13 @@ export class VirtualRouterEngine {
161
164
  : undefined;
162
165
  return Boolean(entryText) && entryText === sessionText && entryMax === sessionMax;
163
166
  });
164
- if (allSame) {
167
+ const used = typeof sessionState.stopMessageUsed === 'number' && Number.isFinite(sessionState.stopMessageUsed)
168
+ ? Math.max(0, Math.floor(sessionState.stopMessageUsed))
169
+ : 0;
170
+ const hasLastUsedAt = typeof sessionState.stopMessageLastUsedAt === 'number' &&
171
+ Number.isFinite(sessionState.stopMessageLastUsedAt);
172
+ const alreadyArmed = used === 0 && !hasLastUsedAt;
173
+ if (allSame && alreadyArmed) {
165
174
  instructions = parsedInstructions.filter((entry) => entry.type !== 'stopMessageSet');
166
175
  }
167
176
  }
@@ -213,9 +222,16 @@ export class VirtualRouterEngine {
213
222
  typeof maxRepeats === 'number' &&
214
223
  Math.floor(sessionState.stopMessageMaxRepeats) === Math.floor(maxRepeats);
215
224
  const isSameInstruction = Boolean(text) && sameText && sameMax;
225
+ const used = typeof sessionState.stopMessageUsed === 'number' && Number.isFinite(sessionState.stopMessageUsed)
226
+ ? Math.max(0, Math.floor(sessionState.stopMessageUsed))
227
+ : 0;
228
+ const hasLastUsedAt = typeof sessionState.stopMessageLastUsedAt === 'number' &&
229
+ Number.isFinite(sessionState.stopMessageLastUsedAt);
230
+ const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
216
231
  nextSessionState.stopMessageText = text || undefined;
217
232
  nextSessionState.stopMessageMaxRepeats = maxRepeats;
218
- if (!isSameInstruction) {
233
+ nextSessionState.stopMessageSource = 'explicit';
234
+ if (shouldRearm) {
219
235
  nextSessionState.stopMessageUsed = 0;
220
236
  nextSessionState.stopMessageUpdatedAt =
221
237
  typeof routingState.stopMessageUpdatedAt === 'number'
@@ -426,7 +442,26 @@ export class VirtualRouterEngine {
426
442
  else if (routingState.preferTarget) {
427
443
  // Auto-clear only when the target becomes invalid or blocked by explicit routing instructions.
428
444
  // Do NOT clear for temporary unavailability (e.g. 429 cooldown, quota cooldown, transient health).
429
- const shouldAutoClear = candidateKeys.length === 0 || eligibleKeys.length > 0;
445
+ const shouldAutoClear = (() => {
446
+ if (candidateKeys.length === 0) {
447
+ return true;
448
+ }
449
+ // Prefer selection failed despite eligible keys existing: treat as a hard block (e.g. routing rules).
450
+ if (eligibleKeys.length > 0) {
451
+ return true;
452
+ }
453
+ // If quota explicitly marks the preferred target as out-of-pool, clear the prefer instruction so
454
+ // the router can fall back to other targets without repeatedly retrying an impossible preference.
455
+ if (selectionDeps.quotaView) {
456
+ for (const key of candidateKeys) {
457
+ const entry = selectionDeps.quotaView(key);
458
+ if (entry && entry.inPool === false) {
459
+ return true;
460
+ }
461
+ }
462
+ }
463
+ return false;
464
+ })();
430
465
  if (shouldAutoClear) {
431
466
  routingState = {
432
467
  ...routingState,
@@ -588,10 +623,18 @@ export class VirtualRouterEngine {
588
623
  // ignore persistence errors
589
624
  }
590
625
  }
591
- // Host 注入 quotaView 时,VirtualRouter 的入池/优先级决策应以 quota 为准;
592
- // 此时不再在 engine-health 内部进行 429/backoff/series cooldown 等健康决策,
593
- // 以避免与 daemon/quota-center 的长期熔断策略重复维护并导致日志噪声。
626
+ // When Host injects quotaView, pool decisions primarily follow quota;
627
+ // however explicit host-provided health signals (quota recovery/depleted/series cooldown)
628
+ // must still be applied so retry selection can avoid obviously blocked runtimes.
594
629
  if (this.quotaView) {
630
+ const handledByQuota = applyQuotaRecoveryImpl(event, this.healthManager, (key) => this.clearProviderCooldown(key), this.debug);
631
+ if (handledByQuota) {
632
+ return;
633
+ }
634
+ const handledByQuotaDepleted = applyQuotaDepletedImpl(event, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
635
+ if (handledByQuotaDepleted) {
636
+ return;
637
+ }
595
638
  return;
596
639
  }
597
640
  // 配额恢复事件优先处理:一旦识别到 virtualRouterQuotaRecovery,
@@ -604,7 +647,6 @@ export class VirtualRouterEngine {
604
647
  if (handledByQuotaDepleted) {
605
648
  return;
606
649
  }
607
- applySeriesCooldownImpl(event, this.providerRegistry, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
608
650
  const derived = mapProviderErrorImpl(event, this.providerHealthConfig());
609
651
  if (!derived) {
610
652
  return;
@@ -678,7 +720,8 @@ export class VirtualRouterEngine {
678
720
  loadBalancer: this.loadBalancer,
679
721
  isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
680
722
  resolveStickyKey: (m) => this.resolveStickyKey(m),
681
- quotaView: this.quotaView
723
+ quotaView: this.quotaView,
724
+ aliasQueueStore: this.aliasQueueStore
682
725
  }, { routingState });
683
726
  }
684
727
  incrementRouteStat(routeName, providerKey) {
@@ -1015,7 +1058,9 @@ export class VirtualRouterEngine {
1015
1058
  contextAdvisor: this.contextAdvisor,
1016
1059
  loadBalancer: this.loadBalancer,
1017
1060
  isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
1018
- resolveStickyKey: (m) => this.resolveStickyKey(m)
1061
+ resolveStickyKey: (m) => this.resolveStickyKey(m),
1062
+ quotaView: this.quotaView,
1063
+ aliasQueueStore: this.aliasQueueStore
1019
1064
  }, { routingState: state });
1020
1065
  }
1021
1066
  extractProviderId(providerKey) {
@@ -504,10 +504,15 @@ export function applyRoutingInstructions(instructions, currentState) {
504
504
  const sameMax = typeof newState.stopMessageMaxRepeats === 'number' &&
505
505
  Math.floor(newState.stopMessageMaxRepeats) === maxRepeats;
506
506
  const isSameInstruction = sameText && sameMax;
507
+ const used = typeof newState.stopMessageUsed === 'number' && Number.isFinite(newState.stopMessageUsed)
508
+ ? Math.max(0, Math.floor(newState.stopMessageUsed))
509
+ : 0;
510
+ const hasLastUsedAt = typeof newState.stopMessageLastUsedAt === 'number' && Number.isFinite(newState.stopMessageLastUsedAt);
511
+ const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
507
512
  newState.stopMessageText = text;
508
513
  newState.stopMessageMaxRepeats = maxRepeats;
509
514
  newState.stopMessageSource = 'explicit';
510
- if (!isSameInstruction) {
515
+ if (shouldRearm) {
511
516
  newState.stopMessageUsed = 0;
512
517
  newState.stopMessageUpdatedAt = Date.now();
513
518
  newState.stopMessageLastUsedAt = undefined;
@@ -10,6 +10,11 @@ type StopMessageSubset = Pick<RoutingInstructionState, 'stopMessageSource' | 'st
10
10
  * Strategy:
11
11
  * - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config.
12
12
  * - Otherwise → adopt persisted fully.
13
+ *
14
+ * Note:
15
+ * - We intentionally do NOT merge counters from an older persisted config into a newer in-memory config.
16
+ * A stopMessage "set" is expected to re-arm/reset counters; allowing older lastUsedAt to overwrite
17
+ * would make re-arming flaky until the async persistence catches up.
13
18
  */
14
19
  export declare function mergeStopMessageFromPersisted(existing: StopMessageSubset, persisted: StopMessageSubset | null): StopMessageSubset;
15
20
  export {};
@@ -21,6 +21,11 @@ function lastUsedAtOf(state) {
21
21
  * Strategy:
22
22
  * - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config.
23
23
  * - Otherwise → adopt persisted fully.
24
+ *
25
+ * Note:
26
+ * - We intentionally do NOT merge counters from an older persisted config into a newer in-memory config.
27
+ * A stopMessage "set" is expected to re-arm/reset counters; allowing older lastUsedAt to overwrite
28
+ * would make re-arming flaky until the async persistence catches up.
24
29
  */
25
30
  export function mergeStopMessageFromPersisted(existing, persisted) {
26
31
  if (!persisted) {
@@ -40,18 +45,5 @@ export function mergeStopMessageFromPersisted(existing, persisted) {
40
45
  stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
41
46
  };
42
47
  }
43
- // Keep existing config, but still allow persisted usage counters to move forward if they are newer.
44
- const existingLastUsedAt = lastUsedAtOf(existing);
45
- const persistedLastUsedAt = lastUsedAtOf(persisted);
46
- const countersAreNewer = persistedLastUsedAt !== null &&
47
- (existingLastUsedAt === null || persistedLastUsedAt > existingLastUsedAt);
48
- return {
49
- ...existing,
50
- ...(countersAreNewer
51
- ? {
52
- stopMessageUsed: persisted.stopMessageUsed,
53
- stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
54
- }
55
- : {})
56
- };
48
+ return { ...existing };
57
49
  }
@@ -22,7 +22,7 @@ export interface RoutePoolTier {
22
22
  * Optional force flag for this route pool.
23
23
  * Currently interpreted for:
24
24
  * - routing.vision: force dedicated vision backend handling.
25
- * - routing.web_search / routing.search: force server-side web_search flow.
25
+ * - routing.web_search: force server-side web_search flow.
26
26
  */
27
27
  force?: boolean;
28
28
  }
@@ -100,6 +100,15 @@ export interface VirtualRouterClassifierConfig {
100
100
  export interface LoadBalancingPolicy {
101
101
  strategy: 'round-robin' | 'weighted' | 'sticky';
102
102
  weights?: Record<string, number>;
103
+ /**
104
+ * Alias-level selection strategy (provider auth aliases).
105
+ *
106
+ * Use this when a provider exposes multiple auth aliases for the same model, and the upstream
107
+ * gateway behaves poorly when requests rapidly switch across keys (e.g. repeated 429 "no capacity"
108
+ * despite quota). Strategies are applied inside VirtualRouter selection only; providers remain
109
+ * transport-only.
110
+ */
111
+ aliasSelection?: AliasSelectionConfig;
103
112
  /**
104
113
  * AWRR: health-weighted selection.
105
114
  * - Deterministic (no randomness)
@@ -143,6 +152,21 @@ export interface HealthWeightedLoadBalancingConfig {
143
152
  */
144
153
  recoverToBestOnRetry?: boolean;
145
154
  }
155
+ export type AliasSelectionStrategy = 'none' | 'sticky-queue';
156
+ export interface AliasSelectionConfig {
157
+ /**
158
+ * Global on/off switch. When false, no alias-level selection is applied.
159
+ */
160
+ enabled?: boolean;
161
+ /**
162
+ * Default strategy used when a provider has no explicit override.
163
+ */
164
+ defaultStrategy?: AliasSelectionStrategy;
165
+ /**
166
+ * Per-provider overrides keyed by providerId (e.g. "antigravity").
167
+ */
168
+ providers?: Record<string, AliasSelectionStrategy>;
169
+ }
146
170
  export interface ContextWeightedLoadBalancingConfig {
147
171
  /**
148
172
  * When false, context-weighted logic is disabled.
@@ -5,3 +5,11 @@ export declare const CLOCK_CONFIG_DEFAULTS: {
5
5
  readonly tickMs: 60000;
6
6
  };
7
7
  export declare function normalizeClockConfig(raw: unknown): ClockConfigSnapshot | null;
8
+ /**
9
+ * Resolve the effective clock config for a request/session.
10
+ *
11
+ * - If a config object exists and enabled=true -> return normalized config.
12
+ * - If the config is explicitly present but disabled/invalid -> return null.
13
+ * - If the config is absent (undefined) -> return null (opt-in only).
14
+ */
15
+ export declare function resolveClockConfig(raw: unknown): ClockConfigSnapshot | null;
@@ -3,6 +3,11 @@ export const CLOCK_CONFIG_DEFAULTS = {
3
3
  dueWindowMs: 60_000,
4
4
  tickMs: 60_000
5
5
  };
6
+ function isClockDisabledByEnv() {
7
+ const raw = process.env.ROUTECODEX_DISABLE_CLOCK ?? process.env.LLMSWITCH_DISABLE_CLOCK ?? '';
8
+ const v = String(raw).trim().toLowerCase();
9
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
10
+ }
6
11
  export function normalizeClockConfig(raw) {
7
12
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
8
13
  return null;
@@ -25,3 +30,20 @@ export function normalizeClockConfig(raw) {
25
30
  : CLOCK_CONFIG_DEFAULTS.tickMs;
26
31
  return { enabled: true, retentionMs, dueWindowMs, tickMs };
27
32
  }
33
+ /**
34
+ * Resolve the effective clock config for a request/session.
35
+ *
36
+ * - If a config object exists and enabled=true -> return normalized config.
37
+ * - If the config is explicitly present but disabled/invalid -> return null.
38
+ * - If the config is absent (undefined) -> return null (opt-in only).
39
+ */
40
+ export function resolveClockConfig(raw) {
41
+ if (isClockDisabledByEnv()) {
42
+ return null;
43
+ }
44
+ const normalized = normalizeClockConfig(raw);
45
+ if (normalized) {
46
+ return normalized;
47
+ }
48
+ return null;
49
+ }
@@ -0,0 +1,3 @@
1
+ export declare const CLOCK_LOG_GOLD = "\u001B[38;5;220m";
2
+ export declare const CLOCK_LOG_RESET = "\u001B[0m";
3
+ export declare function logClock(message: string, extra?: Record<string, unknown>): void;
@@ -0,0 +1,13 @@
1
+ export const CLOCK_LOG_GOLD = '\x1b[38;5;220m';
2
+ export const CLOCK_LOG_RESET = '\x1b[0m';
3
+ export function logClock(message, extra) {
4
+ try {
5
+ // eslint-disable-next-line no-console
6
+ console.log(`${CLOCK_LOG_GOLD}[servertool][clock] ${message}` +
7
+ (extra ? ` ${JSON.stringify(extra)}` : '') +
8
+ CLOCK_LOG_RESET);
9
+ }
10
+ catch {
11
+ // best-effort logging
12
+ }
13
+ }
@@ -1,5 +1,5 @@
1
1
  export type { ClockConfigSnapshot, ClockReservation, ClockScheduleItem, ClockSessionState, ClockTask } from './types.js';
2
- export { normalizeClockConfig } from './config.js';
2
+ export { normalizeClockConfig, resolveClockConfig } from './config.js';
3
3
  export { startClockDaemonIfNeeded, stopClockDaemonForTests } from './daemon.js';
4
4
  export { loadClockSessionState, clearClockSession } from './session-store.js';
5
5
  export { cancelClockTask, clearClockTasks, commitClockReservation, findNextUndeliveredDueAtMs, listClockTasks, parseDueAtMs, reserveDueTasksForRequest, scheduleClockTasks, selectDueUndeliveredTasks } from './tasks.js';
@@ -1,4 +1,4 @@
1
- export { normalizeClockConfig } from './config.js';
1
+ export { normalizeClockConfig, resolveClockConfig } from './config.js';
2
2
  export { startClockDaemonIfNeeded, stopClockDaemonForTests } from './daemon.js';
3
3
  export { loadClockSessionState, clearClockSession } from './session-store.js';
4
4
  export { cancelClockTask, clearClockTasks, commitClockReservation, findNextUndeliveredDueAtMs, listClockTasks, parseDueAtMs, reserveDueTasksForRequest, scheduleClockTasks, selectDueUndeliveredTasks } from './tasks.js';
@@ -15,7 +15,7 @@ function safeJson(value) {
15
15
  }
16
16
  function safeQuoted(text) {
17
17
  const normalized = String(text ?? '');
18
- const escaped = normalized.replace(/\"/g, '\\"');
18
+ const escaped = normalized.replace(/"/g, '\\"');
19
19
  return `"${escaped}"`;
20
20
  }
21
21
  function buildTaskId() {
@@ -1,11 +1,39 @@
1
1
  import { runServerSideToolEngine } from './server-side-tools.js';
2
2
  import { ProviderProtocolError } from '../conversion/shared/errors.js';
3
+ import { ensureRuntimeMetadata, readRuntimeMetadata } from '../conversion/shared/runtime-metadata.js';
3
4
  import { createHash } from 'node:crypto';
4
5
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateSync } from '../router/virtual-router/sticky-session-store.js';
5
6
  import { deserializeRoutingInstructionState, serializeRoutingInstructionState } from '../router/virtual-router/routing-instructions.js';
6
7
  import { applyHubFollowupPolicyShadow } from './followup-shadow.js';
7
8
  import { buildServerToolFollowupChatPayloadFromInjection } from './handlers/followup-request-builder.js';
8
- import { findNextUndeliveredDueAtMs, listClockTasks, normalizeClockConfig } from './clock/task-store.js';
9
+ import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig } from './clock/task-store.js';
10
+ import { savePendingServerToolInjection } from './pending-session.js';
11
+ function stripToolHistoryFromFollowupMessages(raw) {
12
+ const messages = Array.isArray(raw) ? raw : null;
13
+ if (!messages) {
14
+ return raw;
15
+ }
16
+ const out = [];
17
+ for (const msg of messages) {
18
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {
19
+ out.push(msg);
20
+ continue;
21
+ }
22
+ const record = msg;
23
+ const role = typeof record.role === 'string' ? record.role.trim().toLowerCase() : '';
24
+ // Drop tool-role messages entirely for a "no-tools" recovery followup.
25
+ if (role === 'tool') {
26
+ continue;
27
+ }
28
+ // Remove OpenAI tool call fields that could trigger Gemini strict validation.
29
+ const cloned = { ...record };
30
+ delete cloned.tool_calls;
31
+ delete cloned.tool_call_id;
32
+ delete cloned.name;
33
+ out.push(cloned);
34
+ }
35
+ return out;
36
+ }
9
37
  function parseTimeoutMs(raw, fallback) {
10
38
  const n = typeof raw === 'string' ? Number(raw.trim()) : typeof raw === 'number' ? raw : NaN;
11
39
  if (!Number.isFinite(n) || n <= 0) {
@@ -207,14 +235,15 @@ async function shouldDisableServerToolTimeoutForClockHold(args) {
207
235
  return false;
208
236
  }
209
237
  const record = args.adapterContext;
210
- const clockConfig = normalizeClockConfig(record.clock);
211
- if (!clockConfig) {
212
- return false;
213
- }
238
+ const rt = readRuntimeMetadata(record);
214
239
  const sessionId = typeof record.sessionId === 'string' ? record.sessionId.trim() : '';
215
240
  if (!sessionId) {
216
241
  return false;
217
242
  }
243
+ const clockConfig = resolveClockConfig(rt?.clock);
244
+ if (!clockConfig) {
245
+ return false;
246
+ }
218
247
  // If already within due window, clock_auto won't need long hold.
219
248
  try {
220
249
  const tasks = await listClockTasks(sessionId, clockConfig);
@@ -282,6 +311,29 @@ export async function runServerToolOrchestration(options) {
282
311
  const flowId = engineResult.execution.flowId ?? 'unknown';
283
312
  const totalSteps = 5;
284
313
  logProgress(1, totalSteps, 'matched', { flowId });
314
+ // Mixed tools: persist servertool outputs for next request, but return remaining tool_calls to client.
315
+ if (engineResult.pendingInjection) {
316
+ const sessionId = engineResult.pendingInjection.sessionId;
317
+ if (sessionId && sessionId.trim()) {
318
+ try {
319
+ await savePendingServerToolInjection(sessionId.trim(), {
320
+ createdAtMs: Date.now(),
321
+ afterToolCallIds: engineResult.pendingInjection.afterToolCallIds,
322
+ messages: engineResult.pendingInjection.messages,
323
+ sourceRequestId: options.requestId
324
+ });
325
+ }
326
+ catch {
327
+ // best-effort: do not fail the response conversion just because persistence failed
328
+ }
329
+ }
330
+ logProgress(5, totalSteps, 'completed (mixed tools; no reenter)', { flowId });
331
+ return {
332
+ chat: engineResult.finalChatResponse,
333
+ executed: true,
334
+ flowId: engineResult.execution.flowId
335
+ };
336
+ }
285
337
  if (!engineResult.execution.followup || !options.reenterPipeline) {
286
338
  logProgress(5, totalSteps, 'completed (no followup)', { flowId });
287
339
  return {
@@ -328,7 +380,41 @@ export async function runServerToolOrchestration(options) {
328
380
  }
329
381
  return null;
330
382
  })();
331
- if (!followupPayloadRaw) {
383
+ // Prevent nested followup execution on serverToolFollowup hops.
384
+ // Followup responses should still be eligible for servertool triggers (e.g. clock/web_search parsing),
385
+ // but they must not start a new followup flow inside an existing followup hop.
386
+ //
387
+ // Exception: allow continuing the same flow when serverToolLoopState.flowId matches.
388
+ const followupSeedPayload = (() => {
389
+ if (!followupPayloadRaw) {
390
+ return null;
391
+ }
392
+ try {
393
+ const rt = readRuntimeMetadata(options.adapterContext);
394
+ const followupFlagRaw = rt?.serverToolFollowup;
395
+ const isFollowup = followupFlagRaw === true ||
396
+ (typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true');
397
+ if (!isFollowup) {
398
+ return followupPayloadRaw;
399
+ }
400
+ const loopState = rt?.serverToolLoopState;
401
+ const loopFlowId = loopState && typeof loopState === 'object' && !Array.isArray(loopState)
402
+ ? String(loopState.flowId || '').trim()
403
+ : '';
404
+ const flowId = typeof engineResult.execution?.flowId === 'string' && engineResult.execution.flowId.trim().length
405
+ ? engineResult.execution.flowId.trim()
406
+ : '';
407
+ if (loopFlowId && flowId && loopFlowId === flowId) {
408
+ return followupPayloadRaw;
409
+ }
410
+ return null;
411
+ }
412
+ catch {
413
+ // best-effort: if metadata is malformed, avoid nested followups
414
+ return null;
415
+ }
416
+ })();
417
+ if (!followupSeedPayload) {
332
418
  logProgress(5, totalSteps, 'completed (missing followup payload)', { flowId });
333
419
  return {
334
420
  chat: engineResult.finalChatResponse,
@@ -336,7 +422,7 @@ export async function runServerToolOrchestration(options) {
336
422
  flowId: engineResult.execution.flowId
337
423
  };
338
424
  }
339
- const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, followupPayloadRaw);
425
+ const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, followupSeedPayload);
340
426
  if (applyAutoLimit && loopState && typeof loopState.repeatCount === 'number' && loopState.repeatCount >= 3) {
341
427
  logProgress(5, totalSteps, 'completed (auto limit hit)', { flowId });
342
428
  return {
@@ -354,11 +440,14 @@ export async function runServerToolOrchestration(options) {
354
440
  };
355
441
  }
356
442
  const metadata = {
357
- serverToolFollowup: true,
358
443
  stream: false,
359
- ...(loopState ? { serverToolLoopState: loopState } : {}),
360
444
  ...(engineResult.execution.followup.metadata ?? {})
361
445
  };
446
+ const rt = ensureRuntimeMetadata(metadata);
447
+ rt.serverToolFollowup = true;
448
+ if (loopState) {
449
+ rt.serverToolLoopState = loopState;
450
+ }
362
451
  // Followup re-enters HubPipeline at chat-process entry with a canonical "chat-like" body.
363
452
  // This avoids re-running per-protocol inbound parse/semantic-map for each client protocol.
364
453
  metadata.__hubEntry = 'chat_process';
@@ -366,18 +455,44 @@ export async function runServerToolOrchestration(options) {
366
455
  // - clear any inherited routeHint
367
456
  // - do not inherit sticky target
368
457
  // - record original entry endpoint for downstream formatting/debug
369
- metadata.preserveRouteHint = preserveRouteHint;
458
+ rt.preserveRouteHint = preserveRouteHint;
370
459
  // Use empty string (falsy) to avoid VirtualRouter calling `.trim()` on non-string values.
371
460
  metadata.routeHint = '';
372
- metadata.disableStickyRoutes = true;
373
- metadata.serverToolOriginalEntryEndpoint =
461
+ rt.disableStickyRoutes = true;
462
+ rt.serverToolOriginalEntryEndpoint =
374
463
  (typeof options.entryEndpoint === 'string' && options.entryEndpoint.trim().length
375
464
  ? options.entryEndpoint
376
465
  : followupEntryEndpoint);
466
+ // For stateful auto-followups, keep the same providerKey/alias.
467
+ // Otherwise the followup requestId suffix could cause round-robin alias switching or
468
+ // route re-evaluation (e.g. "continue" prompt being treated as a new intent).
469
+ if (isStopMessageFlow || isGeminiEmptyReplyContinue) {
470
+ const providerKeyRaw = options.adapterContext.providerKey;
471
+ const providerKey = typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length ? providerKeyRaw.trim() : '';
472
+ if (providerKey) {
473
+ metadata.__shadowCompareForcedProviderKey = providerKey;
474
+ }
475
+ }
377
476
  const retryEmptyFollowupOnce = isStopMessageFlow || isGeminiEmptyReplyContinue;
378
477
  const maxAttempts = retryEmptyFollowupOnce ? 2 : 1;
379
478
  const followupRequestId = buildFollowupRequestId(options.requestId, engineResult.execution.followup.requestIdSuffix);
380
- let followupPayload = coerceFollowupPayloadStream(followupPayloadRaw, metadata.stream === true);
479
+ let followupPayload = coerceFollowupPayloadStream(followupSeedPayload, metadata.stream === true);
480
+ if (isGeminiEmptyReplyContinue) {
481
+ // For gemini_empty_reply_continue, the goal is to recover text output from an empty/malformed reply.
482
+ // Force the followup to be non-tool-calling to avoid repeated MALFORMED_FUNCTION_CALL loops.
483
+ const paramsRaw = followupPayload.parameters;
484
+ const params = paramsRaw && typeof paramsRaw === 'object' && !Array.isArray(paramsRaw) ? { ...paramsRaw } : {};
485
+ params.tool_choice = 'none';
486
+ params.parallel_tool_calls = false;
487
+ // Ensure we don't override the tool_choice->toolConfig mapping with an inherited tool_config.
488
+ delete params.tool_config;
489
+ delete params.toolConfig;
490
+ followupPayload.parameters = params;
491
+ // Additionally, strip tool-call history. Gemini/CloudCode can strict-validate
492
+ // (history tool calls) ↔ (current tool declarations). We keep tools declared (so the
493
+ // session can continue), but remove history tool artifacts to avoid malformed loops.
494
+ followupPayload.messages = stripToolHistoryFromFollowupMessages(followupPayload.messages);
495
+ }
381
496
  followupPayload = applyHubFollowupPolicyShadow({
382
497
  requestId: followupRequestId,
383
498
  entryEndpoint: followupEntryEndpoint,
@@ -499,7 +614,8 @@ function reserveStopMessageUsage(adapterContext) {
499
614
  }
500
615
  let state = loadRoutingInstructionStateSync(stickyKey);
501
616
  if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
502
- const fallback = resolveStopMessageSnapshot(adapterContext.stopMessageState);
617
+ const rt = readRuntimeMetadata(adapterContext);
618
+ const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
503
619
  if (!fallback) {
504
620
  return null;
505
621
  }
@@ -518,13 +634,18 @@ function reserveStopMessageUsage(adapterContext) {
518
634
  : 0;
519
635
  const nextUsed = used + 1;
520
636
  state.stopMessageUsed = nextUsed;
521
- state.stopMessageLastUsedAt = Date.now();
637
+ const now = Date.now();
638
+ state.stopMessageLastUsedAt = now;
522
639
  if (nextUsed >= maxRepeats) {
640
+ // Auto-clear after reaching max repeats. This avoids leaving an "exhausted" stopMessage
641
+ // stuck in sticky state and ensures a fresh `<**stopMessage:...**>` can re-arm cleanly.
523
642
  state.stopMessageText = undefined;
524
643
  state.stopMessageMaxRepeats = undefined;
525
644
  state.stopMessageUsed = undefined;
526
- state.stopMessageUpdatedAt = undefined;
527
- state.stopMessageLastUsedAt = undefined;
645
+ state.stopMessageSource = undefined;
646
+ // Keep monotonic timestamps as a tombstone to prevent accidental re-application from replayed history.
647
+ state.stopMessageUpdatedAt = now;
648
+ state.stopMessageLastUsedAt = now;
528
649
  }
529
650
  saveRoutingInstructionStateSync(stickyKey, state);
530
651
  return { stickyKey, previousState };
@@ -544,11 +665,13 @@ function disableStopMessageAfterFailedFollowup(adapterContext, reservation) {
544
665
  if (!state) {
545
666
  return;
546
667
  }
668
+ const now = Date.now();
547
669
  state.stopMessageText = undefined;
548
670
  state.stopMessageMaxRepeats = undefined;
549
671
  state.stopMessageUsed = undefined;
550
- state.stopMessageUpdatedAt = undefined;
551
- state.stopMessageLastUsedAt = undefined;
672
+ state.stopMessageSource = undefined;
673
+ state.stopMessageUpdatedAt = now;
674
+ state.stopMessageLastUsedAt = now;
552
675
  saveRoutingInstructionStateSync(key, state);
553
676
  }
554
677
  catch {
@@ -715,7 +838,8 @@ function readServerToolLoopState(adapterContext) {
715
838
  if (!adapterContext || typeof adapterContext !== 'object') {
716
839
  return null;
717
840
  }
718
- const raw = adapterContext.serverToolLoopState;
841
+ const rt = readRuntimeMetadata(adapterContext);
842
+ const raw = rt?.serverToolLoopState;
719
843
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
720
844
  return null;
721
845
  }
@@ -774,7 +898,8 @@ function getStopMessageSource(adapterContext) {
774
898
  if (!adapterContext || typeof adapterContext !== 'object') {
775
899
  return undefined;
776
900
  }
777
- const raw = adapterContext.stopMessageState;
901
+ const rt = readRuntimeMetadata(adapterContext);
902
+ const raw = rt?.stopMessageState;
778
903
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
779
904
  return undefined;
780
905
  }
@@ -1,6 +1,8 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { extractCapturedChatSeed } from './followup-request-builder.js';
3
- import { findNextUndeliveredDueAtMs, listClockTasks, normalizeClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
3
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
+ import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
5
+ import { logClock } from '../clock/log.js';
4
6
  const FLOW_ID = 'clock_hold_flow';
5
7
  function resolveClientConnectionState(value) {
6
8
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -96,15 +98,17 @@ const handler = async (ctx) => {
96
98
  (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
97
99
  return null;
98
100
  }
99
- const clockConfig = normalizeClockConfig(record.clock);
100
- if (!clockConfig) {
101
- return null;
102
- }
103
- await startClockDaemonIfNeeded(clockConfig);
101
+ const rt = readRuntimeMetadata(ctx.adapterContext);
104
102
  const sessionId = resolveSessionId(ctx.adapterContext);
105
103
  if (!sessionId) {
106
104
  return null;
107
105
  }
106
+ // Default-enable clock when config is absent, but keep "explicitly disabled" honored.
107
+ const clockConfig = resolveClockConfig(rt?.clock);
108
+ if (!clockConfig) {
109
+ return null;
110
+ }
111
+ await startClockDaemonIfNeeded(clockConfig);
108
112
  const seed = extractCapturedChatSeed(record.capturedChatRequest);
109
113
  if (!seed) {
110
114
  return null;
@@ -117,6 +121,7 @@ const handler = async (ctx) => {
117
121
  }
118
122
  // Wait until the "due window" is reached (now >= dueAt - dueWindowMs).
119
123
  const thresholdMs = nextDueAtMs - clockConfig.dueWindowMs;
124
+ logClock('hold_start', { sessionId, nextDueAtMs, thresholdMs });
120
125
  while (Date.now() < thresholdMs) {
121
126
  const state = resolveClientConnectionState(ctx.adapterContext.clientConnectionState);
122
127
  if (state?.disconnected === true) {