mixdog 0.9.50 → 0.9.52

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 (134) hide show
  1. package/package.json +5 -3
  2. package/scripts/abort-recovery-test.mjs +17 -1
  3. package/scripts/agent-model-liveness-test.mjs +79 -2
  4. package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
  5. package/scripts/anthropic-transport-policy-test.mjs +466 -0
  6. package/scripts/atomic-lock-tryonce-test.mjs +60 -1
  7. package/scripts/build-tui.mjs +13 -1
  8. package/scripts/channel-daemon-smoke.mjs +630 -10
  9. package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
  10. package/scripts/code-graph-disk-hit-test.mjs +11 -0
  11. package/scripts/code-graph-root-federation-test.mjs +273 -0
  12. package/scripts/compact-pressure-test.mjs +55 -1
  13. package/scripts/compact-smoke.mjs +80 -0
  14. package/scripts/context-mcp-metering-test.mjs +1350 -19
  15. package/scripts/deferred-tool-loading-test.mjs +17 -0
  16. package/scripts/gemini-provider-test.mjs +1053 -0
  17. package/scripts/hook-bus-test.mjs +23 -0
  18. package/scripts/internal-tools-normalization-test.mjs +10 -0
  19. package/scripts/interrupted-turn-history-test.mjs +371 -0
  20. package/scripts/lifecycle-api-test.mjs +76 -0
  21. package/scripts/max-output-recovery-test.mjs +55 -0
  22. package/scripts/mcp-grace-deferred-test.mjs +89 -13
  23. package/scripts/memory-pg-recovery-test.mjs +59 -0
  24. package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
  25. package/scripts/process-lifecycle-test.mjs +389 -0
  26. package/scripts/provider-admission-scheduler-test.mjs +582 -0
  27. package/scripts/provider-contract-test.mjs +268 -0
  28. package/scripts/provider-toolcall-test.mjs +520 -3
  29. package/scripts/reactive-compact-persist-smoke.mjs +59 -0
  30. package/scripts/resource-admission-test.mjs +789 -0
  31. package/scripts/session-bench-cache-break-test.mjs +102 -0
  32. package/scripts/session-bench.mjs +101 -42
  33. package/scripts/shell-failure-diagnostics-test.mjs +73 -4
  34. package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
  35. package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
  36. package/scripts/smoke-loop-failure-summary.mjs +16 -0
  37. package/scripts/smoke-loop.mjs +2 -7
  38. package/scripts/steering-drain-buckets-test.mjs +18 -0
  39. package/scripts/tool-failures.mjs +15 -1
  40. package/scripts/toolcall-args-test.mjs +14 -6
  41. package/scripts/tui-transcript-perf-test.mjs +43 -7
  42. package/scripts/web-fetch-routing-test.mjs +158 -0
  43. package/src/cli.mjs +15 -2
  44. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
  45. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
  46. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
  47. package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
  48. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
  49. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
  50. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
  51. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
  52. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
  53. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
  54. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
  55. package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
  56. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
  57. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
  61. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
  62. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
  63. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
  64. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
  66. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
  68. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
  69. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
  71. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
  72. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
  73. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
  74. package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
  75. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
  76. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
  77. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
  78. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
  79. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
  80. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
  81. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
  84. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
  85. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
  86. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
  87. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
  88. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
  89. package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
  90. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
  91. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
  92. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
  93. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
  94. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
  95. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
  96. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
  97. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
  98. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
  99. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
  100. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
  101. package/src/runtime/memory/index.mjs +22 -4
  102. package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
  103. package/src/runtime/memory/lib/pg/process.mjs +91 -47
  104. package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
  105. package/src/runtime/search/index.mjs +41 -0
  106. package/src/runtime/search/lib/http-fetch.mjs +154 -0
  107. package/src/runtime/search/tool-defs.mjs +23 -0
  108. package/src/runtime/shared/atomic-file.mjs +28 -150
  109. package/src/runtime/shared/process-lifecycle.mjs +363 -0
  110. package/src/runtime/shared/process-shutdown.mjs +27 -4
  111. package/src/runtime/shared/resource-admission.mjs +359 -0
  112. package/src/runtime/shared/staged-child-result.mjs +19 -0
  113. package/src/session-runtime/context-status.mjs +38 -27
  114. package/src/session-runtime/lifecycle-api.mjs +26 -6
  115. package/src/session-runtime/provider-request-tools.mjs +72 -0
  116. package/src/session-runtime/runtime-core.mjs +43 -18
  117. package/src/session-runtime/session-turn-api.mjs +5 -4
  118. package/src/session-runtime/tool-catalog.mjs +375 -15
  119. package/src/standalone/agent-tool.mjs +17 -38
  120. package/src/standalone/channel-daemon-client.mjs +200 -38
  121. package/src/standalone/channel-daemon-transport.mjs +136 -9
  122. package/src/standalone/channel-worker.mjs +79 -12
  123. package/src/standalone/hook-bus/handlers.mjs +4 -0
  124. package/src/standalone/hook-bus/rules.mjs +7 -1
  125. package/src/standalone/hook-bus.mjs +10 -2
  126. package/src/tui/App.jsx +17 -11
  127. package/src/tui/app/live-spinner-visibility.mjs +20 -0
  128. package/src/tui/dist/index.mjs +101 -281
  129. package/src/tui/engine/session-api-ext.mjs +13 -2
  130. package/src/tui/engine/session-api.mjs +1 -1
  131. package/src/tui/engine/turn.mjs +10 -6
  132. package/src/tui/engine.mjs +13 -4
  133. package/src/tui/index.jsx +4 -1
  134. package/src/tui/lib/voice-setup.mjs +16 -0
@@ -331,12 +331,39 @@ export function shouldMirrorCompletionToPendingQueue({
331
331
  return shouldPersistModelVisibleToolCompletion(text, meta);
332
332
  }
333
333
 
334
+ export async function dispatchSearchRuntimeTool(name, args, callerCtx = {}, {
335
+ getSearchModule,
336
+ getCurrentCwd,
337
+ getSession,
338
+ notifyFnForSession,
339
+ runNativeWebSearch,
340
+ } = {}) {
341
+ const currentSession = typeof getSession === 'function' ? getSession() : null;
342
+ const callerCwd = callerCtx?.callerCwd || (typeof getCurrentCwd === 'function' ? getCurrentCwd() : process.cwd());
343
+ const callerSessionId = callerCtx?.callerSessionId || currentSession?.id || null;
344
+ const callerSignal = callerCtx?.signal || currentSession?.controller?.signal;
345
+ const searchMod = await getSearchModule();
346
+ if (!searchMod?.handleToolCall) throw new Error('search runtime is not available');
347
+ return await searchMod.handleToolCall(name, args || {}, {
348
+ callerCwd,
349
+ callerSessionId,
350
+ routingSessionId: callerSessionId,
351
+ clientHostPid: callerCtx?.clientHostPid || currentSession?.clientHostPid || process.pid,
352
+ notifyFn: notifyFnForSession(callerSessionId),
353
+ signal: callerSignal,
354
+ nativeSearch: name === 'search'
355
+ ? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerSignal })
356
+ : undefined,
357
+ });
358
+ }
359
+
334
360
  export async function createMixdogSessionRuntime({
335
361
  provider,
336
362
  model,
337
363
  cwd = process.cwd(),
338
364
  toolMode = 'full',
339
365
  remote = false,
366
+ desktopSession = null,
340
367
  } = {}) {
341
368
  bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
342
369
  let remoteEnabled = remote === true;
@@ -1049,13 +1076,15 @@ export async function createMixdogSessionRuntime({
1049
1076
  });
1050
1077
  bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
1051
1078
  const toolsStartedAt = performance.now();
1079
+ const searchRuntimeTools = (searchToolDefs?.TOOL_DEFS || [])
1080
+ .filter((tool) => ['search', 'web_fetch', 'local_fetch', 'image_fetch'].includes(tool?.name));
1052
1081
  const standaloneTools = [
1053
1082
  TOOL_SEARCH_TOOL,
1054
1083
  ...(envFlag('MIXDOG_DISABLE_SKILLS') ? [] : [SKILL_TOOL]),
1055
1084
  CWD_TOOL,
1056
1085
  SESSION_MANAGE_TOOL,
1057
1086
  EXPLORE_TOOL,
1058
- ...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
1087
+ ...searchRuntimeTools.filter((tool) => tool?.public !== false),
1059
1088
  ...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
1060
1089
  ...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
1061
1090
  ...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
@@ -1102,22 +1131,16 @@ export async function createMixdogSessionRuntime({
1102
1131
  if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
1103
1132
  }
1104
1133
  internalTools.setInternalToolsProvider({
1105
- tools: standaloneTools,
1134
+ tools: [...standaloneTools, ...searchRuntimeTools.filter((tool) => tool?.public === false)],
1106
1135
  executor: async (name, args, callerCtx = {}) => {
1107
1136
  const callerCwd = callerCtx?.callerCwd || currentCwd;
1108
- if (name === 'search' || name === 'web_fetch') {
1109
- const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
1110
- const searchMod = await getSearchModule();
1111
- if (!searchMod?.handleToolCall) throw new Error('search runtime is not available');
1112
- return await searchMod.handleToolCall(name, args || {}, {
1113
- callerCwd,
1114
- callerSessionId,
1115
- routingSessionId: callerSessionId,
1116
- clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
1117
- notifyFn: notifyFnForSession(callerSessionId),
1118
- nativeSearch: name === 'search'
1119
- ? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerCtx?.signal || session?.controller?.signal })
1120
- : undefined,
1137
+ if (name === 'search' || name === 'web_fetch' || name === 'local_fetch' || name === 'image_fetch') {
1138
+ return dispatchSearchRuntimeTool(name, args, callerCtx, {
1139
+ getSearchModule,
1140
+ getCurrentCwd: () => currentCwd,
1141
+ getSession: () => session,
1142
+ notifyFnForSession,
1143
+ runNativeWebSearch,
1121
1144
  });
1122
1145
  }
1123
1146
  if (name === 'recall' || name === 'memory' || name === 'search_memories') {
@@ -1533,6 +1556,7 @@ export async function createMixdogSessionRuntime({
1533
1556
  clientHostPid: process.pid,
1534
1557
  disallowedTools: LEAD_DISALLOWED_TOOLS,
1535
1558
  cwd: currentCwd,
1559
+ ...(desktopSession && typeof desktopSession === 'object' ? { desktopSession } : {}),
1536
1560
  coreMemoryContext,
1537
1561
  workflow,
1538
1562
  workflowContext,
@@ -1547,11 +1571,11 @@ export async function createMixdogSessionRuntime({
1547
1571
  session = mgr.createSession(sessionOpts);
1548
1572
  sessionNeedsCwdRefresh = false;
1549
1573
  attachSessionHooks(session, { hooks, hookCommonPayload, getCwd: () => currentCwd });
1550
- // Every-create MCP fold (NO blocking): seed the INITIAL deferred surface +
1551
- // BP1 manifest from whatever MCP servers are ALREADY connected at create
1574
+ // Every-create MCP fold (NO blocking): seed the INITIAL provider-visible
1575
+ // surface (and native BP1 manifest) from MCP servers connected at create
1552
1576
  // time. There is no await — a boot connect still mid-handshake is caught on
1553
1577
  // the first user turn by refreshInitialDeferredMcpSurface (session-turn-api),
1554
- // which re-folds the live registry into the initial manifest before the
1578
+ // which re-folds the live registry into the first-turn surface before the
1555
1579
  // prompt renders. This fold keeps recreate paths (cwd change with MCP
1556
1580
  // already connected) seeding their manifest instead of re-announcing late.
1557
1581
  let connectedMcpTools = [];
@@ -2133,6 +2157,7 @@ export async function createMixdogSessionRuntime({
2133
2157
  resolveRoute,
2134
2158
  applyDeferredToolSurface,
2135
2159
  standaloneTools,
2160
+ desktopSession,
2136
2161
  });
2137
2162
  const resourceApi = createResourceApi({
2138
2163
  getConfig: () => config,
@@ -92,10 +92,11 @@ export function createSessionTurnApi(deps) {
92
92
  // FIRST TURN of a FRESH session (session-local gate, NOT the
93
93
  // process-wide firstTurnCompleted): an MCP server may have finished its
94
94
  // handshake BETWEEN session-create and this first send. Re-fold the
95
- // LIVE registry into the INITIAL deferred surface + BP1
96
- // <available-deferred-tools> manifest (sync, in-place, idempotent) and
97
- // pre-mark those names announced, so they ship in the initial manifest
98
- // instead of a late-tool <system-reminder>. One-shot: cleared before
95
+ // LIVE registry into the INITIAL provider-visible surface (sync,
96
+ // in-place, idempotent). Native providers rebuild BP1 and pre-mark the
97
+ // names announced; canonical providers extend their fixed active tool
98
+ // snapshot without introducing manifest/reminder semantics. One-shot:
99
+ // cleared before
99
100
  // the fold so a throw still never re-runs it, and a resumed session
100
101
  // (flag unset) skips straight to the late path below.
101
102
  session0.deferredInitialRefreshPending = false;
@@ -3,12 +3,20 @@
3
3
  // application/selection logic. Pure module (session objects passed in).
4
4
  import { clean, LATE_TOOL_ANNOUNCEMENT_SENTINEL } from './session-text.mjs';
5
5
  import { estimateToolSchemaTokens, toolSchemaSignature } from '../runtime/agent/orchestrator/session/context-utils.mjs';
6
- import { applyInitialDeferredToolManifestToBp1, buildDeferredToolManifest } from '../runtime/agent/orchestrator/context/collect.mjs';
6
+ import {
7
+ applyInitialDeferredToolManifestToBp1,
8
+ buildDeferredToolManifest,
9
+ stripDeferredToolManifestBlock,
10
+ } from '../runtime/agent/orchestrator/context/collect.mjs';
7
11
  import { getMcpServerInstructionsMap } from '../runtime/agent/orchestrator/mcp/client.mjs';
8
12
  import {
9
13
  isResponsesFreeformTool,
10
14
  toResponsesCustomTool,
11
15
  } from '../runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
16
+ import {
17
+ finalizeProviderRequestTools,
18
+ providerNativeToolPrefixCount,
19
+ } from './provider-request-tools.mjs';
12
20
 
13
21
  export const MEASURED_TOOL_USAGE = Object.freeze({
14
22
  read: 710,
@@ -31,6 +39,7 @@ const toolSchemaBreakdownMemo = new WeakMap();
31
39
 
32
40
  function sameToolSchemaEntries(cached, tools) {
33
41
  if (!cached || cached.entries.length !== tools.length) return false;
42
+ const nativePrefixCount = providerNativeToolPrefixCount(tools);
34
43
  for (let index = 0; index < tools.length; index += 1) {
35
44
  const entry = cached.entries[index];
36
45
  const tool = tools[index];
@@ -45,12 +54,17 @@ function sameToolSchemaEntries(cached, tools) {
45
54
  || entry.defer_loading !== tool?.defer_loading
46
55
  || entry.annotationsMixdogKind !== tool?.annotations?.mixdogKind
47
56
  || entry.annotationsAgentHidden !== tool?.annotations?.agentHidden
48
- || entry.wireSignature !== toolSchemaSignature([tool])) return false;
57
+ || entry.native !== (index < nativePrefixCount)
58
+ || entry.wireSignature !== toolSchemaSignature(toolMeteringList(tool, index < nativePrefixCount))) return false;
49
59
  }
50
60
  return true;
51
61
  }
52
62
 
53
- function toolSchemaEntry(tool) {
63
+ function toolMeteringList(tool, native) {
64
+ return native ? finalizeProviderRequestTools([tool], 1) : [tool];
65
+ }
66
+
67
+ function toolSchemaEntry(tool, native = false) {
54
68
  return {
55
69
  tool,
56
70
  name: tool?.name,
@@ -63,7 +77,8 @@ function toolSchemaEntry(tool) {
63
77
  defer_loading: tool?.defer_loading,
64
78
  annotationsMixdogKind: tool?.annotations?.mixdogKind,
65
79
  annotationsAgentHidden: tool?.annotations?.agentHidden,
66
- wireSignature: toolSchemaSignature([tool]),
80
+ native,
81
+ wireSignature: toolSchemaSignature(toolMeteringList(tool, native)),
67
82
  };
68
83
  }
69
84
  export const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
@@ -170,14 +185,22 @@ export function estimateToolSchemaBreakdown(tools) {
170
185
  if (sameToolSchemaEntries(cached, tools)) return cached.value;
171
186
  }
172
187
  const out = {};
173
- for (const tool of Array.isArray(tools) ? tools : []) {
188
+ const list = Array.isArray(tools) ? tools : [];
189
+ const nativePrefixCount = providerNativeToolPrefixCount(list);
190
+ for (let index = 0; index < list.length; index += 1) {
191
+ const tool = list[index];
174
192
  const bucket = toolSchemaBucket(tool);
175
193
  const row = out[bucket] || { count: 0, tokens: 0 };
176
194
  row.count += 1;
177
- row.tokens += estimateToolSchemaTokens([tool]);
195
+ row.tokens += estimateToolSchemaTokens(toolMeteringList(tool, index < nativePrefixCount));
178
196
  out[bucket] = row;
179
197
  }
180
- if (Array.isArray(tools)) toolSchemaBreakdownMemo.set(tools, { entries: tools.map(toolSchemaEntry), value: out });
198
+ if (Array.isArray(tools)) {
199
+ toolSchemaBreakdownMemo.set(tools, {
200
+ entries: tools.map((tool, index) => toolSchemaEntry(tool, index < nativePrefixCount)),
201
+ value: out,
202
+ });
203
+ }
181
204
  return out;
182
205
  }
183
206
 
@@ -246,6 +269,324 @@ function nativeProviderFamily(provider) {
246
269
  return '';
247
270
  }
248
271
 
272
+ const ANTHROPIC_NATIVE_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
273
+
274
+ // Pure projection of the tool definitions that the next provider request will
275
+ // serialize. Anthropic's native deferred surface sends the base active tools
276
+ // plus only definitions that have actually been discovered through the
277
+ // session/native tool-search history. Every other provider already receives
278
+ // its canonical/native surface in `tools`, so preserve that array verbatim.
279
+ export function resolveProviderRequestTools({
280
+ provider,
281
+ tools,
282
+ messages,
283
+ session,
284
+ } = {}) {
285
+ const activeTools = Array.isArray(tools) ? tools : [];
286
+ const normalizedProvider = clean(provider || session?.provider).toLowerCase();
287
+ if (!ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider)
288
+ || session?.deferredNativeTools !== true
289
+ || activeTools.length === 0) {
290
+ return activeTools;
291
+ }
292
+ const discovered = new Set(
293
+ parseToolSelection(session?.deferredDiscoveredTools),
294
+ );
295
+ for (const message of Array.isArray(messages) ? messages : []) {
296
+ const native = message?.nativeToolSearch;
297
+ const source = clean(native?.provider).toLowerCase();
298
+ if (source && source !== normalizedProvider
299
+ && !(ANTHROPIC_NATIVE_PROVIDERS.has(source)
300
+ && ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider))) continue;
301
+ for (const name of parseToolSelection(native?.toolReferences)) discovered.add(name);
302
+ }
303
+ if (discovered.size === 0) return activeTools;
304
+ const activeNames = new Set(activeTools.map((tool) => clean(tool?.name)).filter(Boolean));
305
+ const catalog = Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : [];
306
+ const deferredTools = catalog
307
+ .filter((tool) => {
308
+ const name = clean(tool?.name);
309
+ return name && discovered.has(name) && !activeNames.has(name);
310
+ })
311
+ .map((tool) => ({ ...tool, deferLoading: true }));
312
+ return deferredTools.length ? [...activeTools, ...deferredTools] : activeTools;
313
+ }
314
+
315
+ const OMIT_REQUEST_TOOL_VALUE = Symbol('omit-request-tool-value');
316
+ const MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH = 1_000_000;
317
+
318
+ function defineEnumerableDataProperty(target, key, value) {
319
+ Object.defineProperty(target, key, {
320
+ value,
321
+ enumerable: true,
322
+ configurable: false,
323
+ writable: false,
324
+ });
325
+ }
326
+
327
+ function boxedJsonPrimitive(value) {
328
+ try { return { matched: true, value: Number.prototype.valueOf.call(value) }; } catch {}
329
+ try { return { matched: true, value: String.prototype.valueOf.call(value) }; } catch {}
330
+ try { return { matched: true, value: Boolean.prototype.valueOf.call(value) }; } catch {}
331
+ try { return { matched: true, value: BigInt.prototype.valueOf.call(value) }; } catch {}
332
+ return { matched: false, value: null };
333
+ }
334
+
335
+ function providerSnapshotLengthPrimitive(value) {
336
+ if ((typeof value !== 'object' || value === null) && typeof value !== 'function') return value;
337
+ const exotic = value[Symbol.toPrimitive];
338
+ if (exotic !== undefined && exotic !== null) {
339
+ if (typeof exotic !== 'function') throw new TypeError('invalid length primitive');
340
+ const primitive = exotic.call(value, 'number');
341
+ if ((typeof primitive === 'object' && primitive !== null) || typeof primitive === 'function') {
342
+ throw new TypeError('invalid length primitive');
343
+ }
344
+ return primitive;
345
+ }
346
+ for (const methodName of ['valueOf', 'toString']) {
347
+ const method = value[methodName];
348
+ if (typeof method !== 'function') continue;
349
+ const primitive = method.call(value);
350
+ if ((typeof primitive !== 'object' || primitive === null) && typeof primitive !== 'function') {
351
+ return primitive;
352
+ }
353
+ }
354
+ throw new TypeError('invalid length primitive');
355
+ }
356
+
357
+ function providerSnapshotArrayLength(rawLength) {
358
+ let primitive;
359
+ let numeric;
360
+ try {
361
+ primitive = providerSnapshotLengthPrimitive(rawLength);
362
+ if (typeof primitive === 'bigint' || typeof primitive === 'symbol') {
363
+ throw new TypeError('invalid length primitive');
364
+ }
365
+ numeric = Number(primitive);
366
+ } catch {
367
+ throw new TypeError('provider tool snapshot: invalid array length');
368
+ }
369
+ if (Number.isNaN(numeric) || numeric <= 0) return 0;
370
+ if (!Number.isFinite(numeric)) {
371
+ throw new RangeError(
372
+ `provider tool snapshot: array length exceeds safe limit ${MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH}`,
373
+ );
374
+ }
375
+ const effectiveLength = Math.floor(numeric);
376
+ if (effectiveLength > MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH) {
377
+ throw new RangeError(
378
+ `provider tool snapshot: array length exceeds safe limit ${MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH}`,
379
+ );
380
+ }
381
+ return effectiveLength;
382
+ }
383
+
384
+ function normalizeRequestToolJson(value, state, {
385
+ arrayEntry = false,
386
+ key = '',
387
+ applyToJSON = true,
388
+ seededProperties = null,
389
+ } = {}) {
390
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
391
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
392
+ if (typeof value === 'bigint') {
393
+ throw new TypeError('provider tool snapshot: BigInt is not JSON-serializable');
394
+ }
395
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
396
+ return arrayEntry ? null : OMIT_REQUEST_TOOL_VALUE;
397
+ }
398
+ if (typeof value !== 'object') {
399
+ throw new TypeError(`provider tool snapshot: unsupported JSON value type ${typeof value}`);
400
+ }
401
+ if (state.active.has(value)) {
402
+ throw new TypeError('provider tool snapshot: cyclic value is not JSON-serializable');
403
+ }
404
+ if (state.memo.has(value)) return state.memo.get(value);
405
+
406
+ let capturedProperties = seededProperties;
407
+ if (applyToJSON) {
408
+ // JSON reads `toJSON` once before serializing an object. Preserve that
409
+ // single observation for an own-enumerable non-function (and for a
410
+ // callable that returns `this`) so property traversal cannot invoke a
411
+ // stateful accessor a second time.
412
+ const toJSONDescriptor = Object.getOwnPropertyDescriptor(value, 'toJSON');
413
+ const toJSON = value.toJSON;
414
+ if (typeof toJSON === 'function') {
415
+ // Keep the source guarded through both hook execution and replacement
416
+ // normalization. A hook returning itself, or any replacement graph that
417
+ // points back to its source, is a JSON cycle rather than another hook
418
+ // invocation.
419
+ state.active.add(value);
420
+ try {
421
+ const replacement = toJSON.call(value, key);
422
+ const normalized = normalizeRequestToolJson(replacement, state, {
423
+ arrayEntry,
424
+ key,
425
+ applyToJSON: false,
426
+ seededProperties: null,
427
+ });
428
+ state.memo.set(value, normalized);
429
+ return normalized;
430
+ } finally {
431
+ state.active.delete(value);
432
+ }
433
+ }
434
+ if (toJSONDescriptor?.enumerable) {
435
+ capturedProperties = { ...(seededProperties || {}), toJSON };
436
+ }
437
+ }
438
+ const boxed = boxedJsonPrimitive(value);
439
+ if (boxed.matched) {
440
+ return normalizeRequestToolJson(boxed.value, state, { arrayEntry, key });
441
+ }
442
+
443
+ const isArray = Array.isArray(value);
444
+ const normalized = isArray ? [] : {};
445
+ state.active.add(value);
446
+ try {
447
+ if (isArray) {
448
+ // JSON.stringify captures array length once. Accessors may mutate the
449
+ // source array, but they cannot extend or shorten this iteration bound.
450
+ const rawLength = value.length;
451
+ const length = providerSnapshotArrayLength(rawLength);
452
+ for (let index = 0; index < length; index += 1) {
453
+ const entry = value[index];
454
+ normalized.push(normalizeRequestToolJson(entry, state, {
455
+ arrayEntry: true,
456
+ key: String(index),
457
+ }));
458
+ }
459
+ } else {
460
+ // JSON-compatible request schemas are own-enumerable data. Normalize class
461
+ // instances/accessors to a plain record, ignore inherited mutable fields,
462
+ // and define keys explicitly so an own "__proto__" remains ordinary data.
463
+ for (const key of Reflect.ownKeys(value)) {
464
+ if (typeof key !== 'string') continue;
465
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
466
+ if (!descriptor?.enumerable) continue;
467
+ // Deferred catalog selection captures `name` once before touching the
468
+ // candidate schema. Seed that captured value here so a stateful/throwing
469
+ // getter is never observed a second time during JSON normalization.
470
+ const entry = capturedProperties && Object.hasOwn(capturedProperties, key)
471
+ ? capturedProperties[key]
472
+ : value[key];
473
+ const child = normalizeRequestToolJson(entry, state, { key });
474
+ if (child !== OMIT_REQUEST_TOOL_VALUE) defineEnumerableDataProperty(normalized, key, child);
475
+ }
476
+ }
477
+ } finally {
478
+ state.active.delete(value);
479
+ }
480
+ Object.freeze(normalized);
481
+ state.memo.set(value, normalized);
482
+ return normalized;
483
+ }
484
+
485
+ // Establish one immutable request-attempt snapshot. Nested schema records are
486
+ // cloned before freezing, so a catalog refresh or in-place schema mutation
487
+ // after this boundary cannot change either provider bytes or their signature.
488
+ export function snapshotProviderRequestTools(options = {}) {
489
+ const {
490
+ provider,
491
+ tools,
492
+ nativeTools,
493
+ messages,
494
+ session,
495
+ } = options;
496
+ const activeTools = Array.isArray(tools) ? tools : [];
497
+ const state = { active: new WeakSet(), memo: new WeakMap() };
498
+ const snapshots = [];
499
+ const names = new Set();
500
+ const activeCandidateRefs = new WeakSet();
501
+ // Anthropic native definitions are already provider-wire objects. Preserve
502
+ // their prior prepend order and duplicate behavior, but freeze the exact
503
+ // bytes into the same request snapshot used for accounting and retries.
504
+ if (ANTHROPIC_NATIVE_PROVIDERS.has(clean(provider || session?.provider).toLowerCase())) {
505
+ for (const nativeTool of Array.isArray(nativeTools) ? nativeTools : []) {
506
+ if (!nativeTool || typeof nativeTool !== 'object') continue;
507
+ const normalized = normalizeRequestToolJson(nativeTool, state);
508
+ if (normalized && typeof normalized === 'object' && !Array.isArray(normalized)) {
509
+ snapshots.push(normalized);
510
+ }
511
+ }
512
+ }
513
+ const nativePrefixCount = snapshots.length;
514
+ const finish = () => finalizeProviderRequestTools(snapshots, nativePrefixCount);
515
+ const appendSnapshot = (candidate, selectedName = null, deferred = false) => {
516
+ const normalized = normalizeRequestToolJson(candidate, state, {
517
+ seededProperties: selectedName === null ? null : { name: selectedName },
518
+ });
519
+ if (deferred && (
520
+ !normalized
521
+ || typeof normalized !== 'object'
522
+ || Array.isArray(normalized)
523
+ || typeof normalized.name !== 'string'
524
+ || !clean(normalized.name)
525
+ || normalized.name !== selectedName
526
+ )) {
527
+ throw new TypeError(`provider tool snapshot: selected tool identity mismatch for ${JSON.stringify(selectedName)}`);
528
+ }
529
+ if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) return;
530
+ const name = clean(normalized.name);
531
+ if (!name || names.has(name)) return;
532
+ names.add(name);
533
+ if (!deferred) {
534
+ snapshots.push(normalized);
535
+ return;
536
+ }
537
+ const deferredSnapshot = {};
538
+ for (const key of Object.keys(normalized)) {
539
+ if (key === 'deferLoading' || key === 'defer_loading') continue;
540
+ defineEnumerableDataProperty(deferredSnapshot, key, normalized[key]);
541
+ }
542
+ defineEnumerableDataProperty(deferredSnapshot, 'deferLoading', true);
543
+ snapshots.push(Object.freeze(deferredSnapshot));
544
+ };
545
+
546
+ // Active candidates are provider-visible by definition: normalize each once,
547
+ // then perform all validation/dedupe from the plain snapshot only.
548
+ for (const tool of activeTools) {
549
+ if (tool && typeof tool === 'object') activeCandidateRefs.add(tool);
550
+ appendSnapshot(tool);
551
+ }
552
+
553
+ const normalizedProvider = clean(provider || session?.provider).toLowerCase();
554
+ if (!ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider)
555
+ || session?.deferredNativeTools !== true
556
+ // Native definitions preserve their historical prepend behavior, but they
557
+ // do not make an otherwise all-deferred catalog eligible for expansion.
558
+ || names.size === 0) {
559
+ return finish();
560
+ }
561
+ const discovered = new Set(parseToolSelection(session?.deferredDiscoveredTools));
562
+ for (const message of Array.isArray(messages) ? messages : []) {
563
+ const native = message?.nativeToolSearch;
564
+ const source = clean(native?.provider).toLowerCase();
565
+ if (source && source !== normalizedProvider
566
+ && !(ANTHROPIC_NATIVE_PROVIDERS.has(source)
567
+ && ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider))) continue;
568
+ for (const name of parseToolSelection(native?.toolReferences)) discovered.add(name);
569
+ }
570
+ if (discovered.size === 0) return finish();
571
+
572
+ // Catalog arrays have no separate key map, so `name` is their explicit
573
+ // selection key contract: capture it once, skip undiscovered/duplicate
574
+ // entries without touching schemas, and seed selected normalization with the
575
+ // captured value to avoid a second getter evaluation.
576
+ const seenCatalogRefs = new WeakSet();
577
+ for (const tool of Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : []) {
578
+ if (tool && typeof tool === 'object') {
579
+ if (activeCandidateRefs.has(tool) || seenCatalogRefs.has(tool)) continue;
580
+ seenCatalogRefs.add(tool);
581
+ }
582
+ const capturedName = tool?.name;
583
+ const selectionName = typeof capturedName === 'string' ? clean(capturedName) : '';
584
+ if (!selectionName || !discovered.has(selectionName) || names.has(selectionName)) continue;
585
+ appendSnapshot(tool, capturedName, true);
586
+ }
587
+ return finish();
588
+ }
589
+
249
590
  export function filterDisallowedTools(tools, disallowed = []) {
250
591
  if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
251
592
  const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
@@ -510,6 +851,7 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
510
851
 
511
852
  export function rebuildDeferredToolSurfaceForProvider(session, provider) {
512
853
  if (!session || !Array.isArray(session.tools)) return session;
854
+ const previousMode = session.deferredProviderMode;
513
855
  const previousFamily = nativeProviderFamily(session.provider);
514
856
  const nextFamily = nativeProviderFamily(provider);
515
857
  const preserveNativeState = previousFamily && previousFamily === nextFamily;
@@ -532,21 +874,36 @@ export function rebuildDeferredToolSurfaceForProvider(session, provider) {
532
874
  ]));
533
875
  session.deferredSelectedTools = session.deferredCallableTools.slice();
534
876
  }
877
+ if (previousMode && previousMode !== session.deferredProviderMode) {
878
+ if (session.deferredProviderMode === 'native') {
879
+ session.mcpServerInstructions = getMcpServerInstructionsMap();
880
+ applyInitialDeferredToolManifestToBp1(session, deferredPoolToolNames(session), { rebuild: true });
881
+ const rendered = session.messages?.find((message) => message?.role === 'system')?.content;
882
+ session.deferredAnnouncedTools = deferredPoolToolNames(session)
883
+ .filter((name) => typeof rendered === 'string' && rendered.includes(name));
884
+ } else if (previousMode === 'native') {
885
+ const system = session.messages?.find((message) => message?.role === 'system');
886
+ if (system && typeof system.content === 'string') {
887
+ system.content = stripDeferredToolManifestBlock(system.content);
888
+ }
889
+ session.deferredAnnouncedTools = [];
890
+ session.deferredToolBp1Applied = true;
891
+ }
892
+ }
535
893
  return session;
536
894
  }
537
895
 
538
896
  // FIRST-TURN deferred-surface refresh (claude-code turn-time deferred manifest).
539
897
  // An MCP server may finish its handshake BETWEEN session-create and the first
540
898
  // user send. Fold those LIVE MCP tools into the boot deferred catalog + the
541
- // initial BP1 <available-deferred-tools> manifest (rebuilt IN PLACE strip +
542
- // reappend, never duplicated) and pre-mark them announced, so they appear in the
543
- // INITIAL manifest instead of arriving as a late-tool <system-reminder>. Fully
544
- // sync (registry read is sync, no await) and idempotent: no genuinely-new MCP
545
- // name => no-op / no mutation. Skipped in 'full' provider mode (all tools ship
546
- // active — there is no deferred manifest to refresh).
899
+ // provider-visible first-turn surface. Native providers rebuild the initial BP1
900
+ // <available-deferred-tools> manifest IN PLACE and pre-mark names announced.
901
+ // Manifest/canonical providers update their active fixed surface directly; the
902
+ // canonical path never emits a deferred manifest or late reminder. Fully sync
903
+ // and idempotent: no genuinely-new MCP name => no-op / no mutation.
547
904
  export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
548
905
  if (!session || !Array.isArray(session.messages)) return false;
549
- if (session.deferredProviderMode === 'full' || session.deferredProviderMode === 'canonical') return false;
906
+ if (session.deferredProviderMode === 'full') return false;
550
907
  const isMcp = (name) => typeof name === 'string' && name.startsWith('mcp__');
551
908
  const byName = new Map();
552
909
  for (const tool of Array.isArray(session.deferredToolCatalog) ? session.deferredToolCatalog : []) {
@@ -562,12 +919,15 @@ export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
562
919
  }
563
920
  if (!added) return false;
564
921
  session.deferredToolCatalog = sortedCatalogByMeasuredUsage([...byName.values()]);
565
- if (session.deferredProviderMode === 'manifest') {
922
+ if (session.deferredProviderMode === 'manifest' || session.deferredProviderMode === 'canonical') {
566
923
  const next = session.deferredToolCatalog.filter((tool) => (
567
924
  session.deferredSurfaceMode !== 'readonly' || isReadonlySelectable(tool)
568
925
  ));
569
926
  session.tools.splice(0, session.tools.length, ...next);
570
927
  session.deferredCallableTools = next.map((tool) => clean(tool?.name)).filter(Boolean);
928
+ if (session.deferredProviderMode === 'canonical') {
929
+ setDeferredToolState(session, session.deferredCallableTools);
930
+ }
571
931
  session.updatedAt = Date.now();
572
932
  return true;
573
933
  }