mixdog 0.9.51 → 0.9.53

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 (178) 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/bench-run.mjs +2 -2
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/channel-daemon-smoke.mjs +630 -10
  10. package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
  11. package/scripts/code-graph-disk-hit-test.mjs +11 -0
  12. package/scripts/code-graph-root-federation-test.mjs +273 -0
  13. package/scripts/compact-pressure-test.mjs +159 -1
  14. package/scripts/compact-smoke.mjs +80 -0
  15. package/scripts/context-mcp-metering-test.mjs +1350 -19
  16. package/scripts/deferred-tool-loading-test.mjs +17 -0
  17. package/scripts/desktop-session-bridge-test.mjs +704 -0
  18. package/scripts/freevar-smoke.mjs +7 -4
  19. package/scripts/gemini-provider-test.mjs +1053 -0
  20. package/scripts/interrupted-turn-history-test.mjs +371 -0
  21. package/scripts/lifecycle-api-test.mjs +137 -0
  22. package/scripts/max-output-recovery-test.mjs +86 -0
  23. package/scripts/mcp-grace-deferred-test.mjs +89 -13
  24. package/scripts/memory-core-input-test.mjs +10 -0
  25. package/scripts/memory-pg-recovery-test.mjs +59 -0
  26. package/scripts/openai-oauth-ws-1006-retry-test.mjs +387 -6
  27. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  28. package/scripts/parent-abort-link-test.mjs +24 -0
  29. package/scripts/process-lifecycle-test.mjs +447 -0
  30. package/scripts/provider-admission-scheduler-test.mjs +582 -0
  31. package/scripts/provider-contract-test.mjs +525 -0
  32. package/scripts/provider-toolcall-test.mjs +492 -11
  33. package/scripts/reactive-compact-persist-smoke.mjs +59 -0
  34. package/scripts/resource-admission-test.mjs +789 -0
  35. package/scripts/session-orphan-sweep-test.mjs +27 -1
  36. package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
  37. package/scripts/steering-drain-buckets-test.mjs +18 -0
  38. package/scripts/toolcall-args-test.mjs +14 -6
  39. package/scripts/tui-transcript-perf-test.mjs +5 -7
  40. package/src/cli.mjs +15 -2
  41. package/src/lib/keychain-cjs.cjs +36 -23
  42. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +27 -13
  43. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  45. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  46. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
  47. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  48. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +114 -308
  49. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +31 -21
  50. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -286
  51. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +42 -5
  52. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
  53. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
  54. package/src/runtime/agent/orchestrator/providers/gemini.mjs +84 -150
  55. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -119
  56. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  57. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  58. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  59. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  60. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  61. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
  62. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +28 -74
  64. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
  65. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +136 -20
  66. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +94 -33
  67. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +201 -123
  68. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +37 -28
  69. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  70. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  71. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
  72. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +50 -45
  73. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
  74. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
  75. package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
  76. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +229 -111
  77. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +130 -63
  78. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
  79. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +37 -224
  81. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  82. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +81 -42
  83. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
  84. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  85. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
  86. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  87. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +30 -7
  88. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  89. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
  90. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
  91. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
  92. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
  94. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -1
  96. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  97. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  98. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
  99. package/src/runtime/agent/orchestrator/session/store.mjs +353 -108
  100. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
  101. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
  102. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  109. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +317 -250
  112. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
  113. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
  114. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
  115. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
  116. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
  117. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  118. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +125 -85
  119. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  120. package/src/runtime/channels/backends/discord.mjs +6 -6
  121. package/src/runtime/channels/lib/config.mjs +15 -2
  122. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  123. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  124. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  125. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  126. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  127. package/src/runtime/memory/index.mjs +46 -202
  128. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  129. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  130. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  131. package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
  132. package/src/runtime/memory/lib/pg/process.mjs +91 -47
  133. package/src/runtime/memory/lib/pg/supervisor.mjs +50 -17
  134. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  135. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  136. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  137. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  138. package/src/runtime/shared/atomic-file.mjs +28 -150
  139. package/src/runtime/shared/config.mjs +58 -13
  140. package/src/runtime/shared/llm/cost.mjs +14 -4
  141. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  142. package/src/runtime/shared/process-lifecycle.mjs +436 -0
  143. package/src/runtime/shared/process-shutdown.mjs +33 -4
  144. package/src/runtime/shared/resource-admission.mjs +364 -0
  145. package/src/runtime/shared/staged-child-result.mjs +19 -0
  146. package/src/session-runtime/channel-config-api.mjs +7 -7
  147. package/src/session-runtime/config-lifecycle.mjs +20 -17
  148. package/src/session-runtime/context-status.mjs +38 -27
  149. package/src/session-runtime/cwd-plugins.mjs +9 -7
  150. package/src/session-runtime/env.mjs +1 -2
  151. package/src/session-runtime/hitch-profile.mjs +45 -0
  152. package/src/session-runtime/lifecycle-api.mjs +60 -13
  153. package/src/session-runtime/mcp-glue.mjs +6 -11
  154. package/src/session-runtime/provider-init-key.mjs +17 -0
  155. package/src/session-runtime/provider-request-tools.mjs +72 -0
  156. package/src/session-runtime/runtime-core.mjs +49 -105
  157. package/src/session-runtime/runtime-paths.mjs +20 -0
  158. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  159. package/src/session-runtime/session-turn-api.mjs +5 -4
  160. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  161. package/src/session-runtime/tool-catalog.mjs +390 -104
  162. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  163. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  164. package/src/standalone/agent-tool.mjs +29 -200
  165. package/src/standalone/channel-admin.mjs +29 -0
  166. package/src/standalone/channel-daemon-client.mjs +200 -38
  167. package/src/standalone/channel-daemon-transport.mjs +136 -9
  168. package/src/standalone/channel-worker.mjs +79 -12
  169. package/src/tui/App.jsx +11 -5
  170. package/src/tui/dist/index.mjs +275 -343
  171. package/src/tui/engine/session-api-ext.mjs +50 -6
  172. package/src/tui/engine/session-api.mjs +1 -1
  173. package/src/tui/engine/turn.mjs +10 -6
  174. package/src/tui/engine.mjs +13 -4
  175. package/src/tui/index.jsx +5 -1
  176. package/src/tui/lib/voice-setup.mjs +21 -5
  177. package/scripts/_devtools-stub.mjs +0 -1
  178. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -3,34 +3,41 @@
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';
20
+ import {
21
+ DEFERRED_DEFAULT_FULL_TOOLS,
22
+ DEFERRED_DEFAULT_LEAD_TOOLS,
23
+ DEFERRED_DEFAULT_READONLY_TOOLS,
24
+ DEFERRED_SELECT_ALIASES,
25
+ MEASURED_TOOL_ORDER,
26
+ MEASURED_TOOL_USAGE,
27
+ READONLY_TOOL_NAMES,
28
+ } from './tool-catalog-data.mjs';
29
+ export {
30
+ DEFERRED_DEFAULT_FULL_TOOLS,
31
+ DEFERRED_DEFAULT_LEAD_TOOLS,
32
+ DEFERRED_DEFAULT_READONLY_TOOLS,
33
+ MEASURED_TOOL_USAGE,
34
+ } from './tool-catalog-data.mjs';
12
35
 
13
- export const MEASURED_TOOL_USAGE = Object.freeze({
14
- read: 710,
15
- code_graph: 520,
16
- grep: 500,
17
- find: 480,
18
- glob: 460,
19
- list: 430,
20
- apply_patch: 400,
21
- explore: 360,
22
- agent: 330,
23
- shell: 81,
24
- cwd: 2,
25
- recall: 2,
26
- search: 2,
27
- web_fetch: 2,
28
- });
29
- const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
30
36
  const toolSchemaBreakdownMemo = new WeakMap();
31
37
 
32
38
  function sameToolSchemaEntries(cached, tools) {
33
39
  if (!cached || cached.entries.length !== tools.length) return false;
40
+ const nativePrefixCount = providerNativeToolPrefixCount(tools);
34
41
  for (let index = 0; index < tools.length; index += 1) {
35
42
  const entry = cached.entries[index];
36
43
  const tool = tools[index];
@@ -45,12 +52,17 @@ function sameToolSchemaEntries(cached, tools) {
45
52
  || entry.defer_loading !== tool?.defer_loading
46
53
  || entry.annotationsMixdogKind !== tool?.annotations?.mixdogKind
47
54
  || entry.annotationsAgentHidden !== tool?.annotations?.agentHidden
48
- || entry.wireSignature !== toolSchemaSignature([tool])) return false;
55
+ || entry.native !== (index < nativePrefixCount)
56
+ || entry.wireSignature !== toolSchemaSignature(toolMeteringList(tool, index < nativePrefixCount))) return false;
49
57
  }
50
58
  return true;
51
59
  }
52
60
 
53
- function toolSchemaEntry(tool) {
61
+ function toolMeteringList(tool, native) {
62
+ return native ? finalizeProviderRequestTools([tool], 1) : [tool];
63
+ }
64
+
65
+ function toolSchemaEntry(tool, native = false) {
54
66
  return {
55
67
  tool,
56
68
  name: tool?.name,
@@ -63,81 +75,10 @@ function toolSchemaEntry(tool) {
63
75
  defer_loading: tool?.defer_loading,
64
76
  annotationsMixdogKind: tool?.annotations?.mixdogKind,
65
77
  annotationsAgentHidden: tool?.annotations?.agentHidden,
66
- wireSignature: toolSchemaSignature([tool]),
78
+ native,
79
+ wireSignature: toolSchemaSignature(toolMeteringList(tool, native)),
67
80
  };
68
81
  }
69
- export const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
70
- 'read',
71
- 'code_graph',
72
- 'grep',
73
- 'find',
74
- 'glob',
75
- 'list',
76
- 'explore',
77
- 'apply_patch',
78
- 'Skill',
79
- 'load_tool',
80
- ]);
81
- export const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
82
- 'read',
83
- 'code_graph',
84
- 'grep',
85
- 'find',
86
- 'glob',
87
- 'list',
88
- 'explore',
89
- 'Skill',
90
- 'load_tool',
91
- ]);
92
- export const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
93
- 'read',
94
- 'code_graph',
95
- 'grep',
96
- 'find',
97
- 'glob',
98
- 'list',
99
- 'shell',
100
- 'task',
101
- 'explore',
102
- 'apply_patch',
103
- 'agent',
104
- 'recall',
105
- 'search',
106
- 'web_fetch',
107
- 'cwd',
108
- 'session_manage',
109
- 'Skill',
110
- 'load_tool',
111
- ]);
112
- const READONLY_TOOL_NAMES = new Set([
113
- 'read',
114
- 'list',
115
- 'grep',
116
- 'find',
117
- 'glob',
118
- 'code_graph',
119
- 'search',
120
- 'web_fetch',
121
- 'recall',
122
- 'memory',
123
- 'fetch',
124
- 'Skill',
125
- ]);
126
- const DEFERRED_SELECT_ALIASES = {
127
- filesystem: ['read', 'list', 'grep', 'find', 'glob'],
128
- search: ['search', 'web_fetch'],
129
- web: ['web_fetch', 'search'],
130
- memory: ['memory', 'recall'],
131
- channels: ['reply', 'fetch'],
132
- discord: ['reply', 'fetch'],
133
- explore: ['explore'],
134
- discovery: ['explore'],
135
- agent: ['agent'],
136
- graph: ['code_graph'],
137
- code: ['code_graph'],
138
- shell: ['shell', 'task'],
139
- };
140
-
141
82
  export function toolKind(tool) {
142
83
  const name = clean(tool?.name);
143
84
  if (name.startsWith('mcp__')) return 'mcp';
@@ -170,14 +111,22 @@ export function estimateToolSchemaBreakdown(tools) {
170
111
  if (sameToolSchemaEntries(cached, tools)) return cached.value;
171
112
  }
172
113
  const out = {};
173
- for (const tool of Array.isArray(tools) ? tools : []) {
114
+ const list = Array.isArray(tools) ? tools : [];
115
+ const nativePrefixCount = providerNativeToolPrefixCount(list);
116
+ for (let index = 0; index < list.length; index += 1) {
117
+ const tool = list[index];
174
118
  const bucket = toolSchemaBucket(tool);
175
119
  const row = out[bucket] || { count: 0, tokens: 0 };
176
120
  row.count += 1;
177
- row.tokens += estimateToolSchemaTokens([tool]);
121
+ row.tokens += estimateToolSchemaTokens(toolMeteringList(tool, index < nativePrefixCount));
178
122
  out[bucket] = row;
179
123
  }
180
- if (Array.isArray(tools)) toolSchemaBreakdownMemo.set(tools, { entries: tools.map(toolSchemaEntry), value: out });
124
+ if (Array.isArray(tools)) {
125
+ toolSchemaBreakdownMemo.set(tools, {
126
+ entries: tools.map((tool, index) => toolSchemaEntry(tool, index < nativePrefixCount)),
127
+ value: out,
128
+ });
129
+ }
181
130
  return out;
182
131
  }
183
132
 
@@ -246,6 +195,324 @@ function nativeProviderFamily(provider) {
246
195
  return '';
247
196
  }
248
197
 
198
+ const ANTHROPIC_NATIVE_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
199
+
200
+ // Pure projection of the tool definitions that the next provider request will
201
+ // serialize. Anthropic's native deferred surface sends the base active tools
202
+ // plus only definitions that have actually been discovered through the
203
+ // session/native tool-search history. Every other provider already receives
204
+ // its canonical/native surface in `tools`, so preserve that array verbatim.
205
+ export function resolveProviderRequestTools({
206
+ provider,
207
+ tools,
208
+ messages,
209
+ session,
210
+ } = {}) {
211
+ const activeTools = Array.isArray(tools) ? tools : [];
212
+ const normalizedProvider = clean(provider || session?.provider).toLowerCase();
213
+ if (!ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider)
214
+ || session?.deferredNativeTools !== true
215
+ || activeTools.length === 0) {
216
+ return activeTools;
217
+ }
218
+ const discovered = new Set(
219
+ parseToolSelection(session?.deferredDiscoveredTools),
220
+ );
221
+ for (const message of Array.isArray(messages) ? messages : []) {
222
+ const native = message?.nativeToolSearch;
223
+ const source = clean(native?.provider).toLowerCase();
224
+ if (source && source !== normalizedProvider
225
+ && !(ANTHROPIC_NATIVE_PROVIDERS.has(source)
226
+ && ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider))) continue;
227
+ for (const name of parseToolSelection(native?.toolReferences)) discovered.add(name);
228
+ }
229
+ if (discovered.size === 0) return activeTools;
230
+ const activeNames = new Set(activeTools.map((tool) => clean(tool?.name)).filter(Boolean));
231
+ const catalog = Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : [];
232
+ const deferredTools = catalog
233
+ .filter((tool) => {
234
+ const name = clean(tool?.name);
235
+ return name && discovered.has(name) && !activeNames.has(name);
236
+ })
237
+ .map((tool) => ({ ...tool, deferLoading: true }));
238
+ return deferredTools.length ? [...activeTools, ...deferredTools] : activeTools;
239
+ }
240
+
241
+ const OMIT_REQUEST_TOOL_VALUE = Symbol('omit-request-tool-value');
242
+ const MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH = 1_000_000;
243
+
244
+ function defineEnumerableDataProperty(target, key, value) {
245
+ Object.defineProperty(target, key, {
246
+ value,
247
+ enumerable: true,
248
+ configurable: false,
249
+ writable: false,
250
+ });
251
+ }
252
+
253
+ function boxedJsonPrimitive(value) {
254
+ try { return { matched: true, value: Number.prototype.valueOf.call(value) }; } catch {}
255
+ try { return { matched: true, value: String.prototype.valueOf.call(value) }; } catch {}
256
+ try { return { matched: true, value: Boolean.prototype.valueOf.call(value) }; } catch {}
257
+ try { return { matched: true, value: BigInt.prototype.valueOf.call(value) }; } catch {}
258
+ return { matched: false, value: null };
259
+ }
260
+
261
+ function providerSnapshotLengthPrimitive(value) {
262
+ if ((typeof value !== 'object' || value === null) && typeof value !== 'function') return value;
263
+ const exotic = value[Symbol.toPrimitive];
264
+ if (exotic !== undefined && exotic !== null) {
265
+ if (typeof exotic !== 'function') throw new TypeError('invalid length primitive');
266
+ const primitive = exotic.call(value, 'number');
267
+ if ((typeof primitive === 'object' && primitive !== null) || typeof primitive === 'function') {
268
+ throw new TypeError('invalid length primitive');
269
+ }
270
+ return primitive;
271
+ }
272
+ for (const methodName of ['valueOf', 'toString']) {
273
+ const method = value[methodName];
274
+ if (typeof method !== 'function') continue;
275
+ const primitive = method.call(value);
276
+ if ((typeof primitive !== 'object' || primitive === null) && typeof primitive !== 'function') {
277
+ return primitive;
278
+ }
279
+ }
280
+ throw new TypeError('invalid length primitive');
281
+ }
282
+
283
+ function providerSnapshotArrayLength(rawLength) {
284
+ let primitive;
285
+ let numeric;
286
+ try {
287
+ primitive = providerSnapshotLengthPrimitive(rawLength);
288
+ if (typeof primitive === 'bigint' || typeof primitive === 'symbol') {
289
+ throw new TypeError('invalid length primitive');
290
+ }
291
+ numeric = Number(primitive);
292
+ } catch {
293
+ throw new TypeError('provider tool snapshot: invalid array length');
294
+ }
295
+ if (Number.isNaN(numeric) || numeric <= 0) return 0;
296
+ if (!Number.isFinite(numeric)) {
297
+ throw new RangeError(
298
+ `provider tool snapshot: array length exceeds safe limit ${MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH}`,
299
+ );
300
+ }
301
+ const effectiveLength = Math.floor(numeric);
302
+ if (effectiveLength > MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH) {
303
+ throw new RangeError(
304
+ `provider tool snapshot: array length exceeds safe limit ${MAX_PROVIDER_SNAPSHOT_ARRAY_LENGTH}`,
305
+ );
306
+ }
307
+ return effectiveLength;
308
+ }
309
+
310
+ function normalizeRequestToolJson(value, state, {
311
+ arrayEntry = false,
312
+ key = '',
313
+ applyToJSON = true,
314
+ seededProperties = null,
315
+ } = {}) {
316
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
317
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
318
+ if (typeof value === 'bigint') {
319
+ throw new TypeError('provider tool snapshot: BigInt is not JSON-serializable');
320
+ }
321
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
322
+ return arrayEntry ? null : OMIT_REQUEST_TOOL_VALUE;
323
+ }
324
+ if (typeof value !== 'object') {
325
+ throw new TypeError(`provider tool snapshot: unsupported JSON value type ${typeof value}`);
326
+ }
327
+ if (state.active.has(value)) {
328
+ throw new TypeError('provider tool snapshot: cyclic value is not JSON-serializable');
329
+ }
330
+ if (state.memo.has(value)) return state.memo.get(value);
331
+
332
+ let capturedProperties = seededProperties;
333
+ if (applyToJSON) {
334
+ // JSON reads `toJSON` once before serializing an object. Preserve that
335
+ // single observation for an own-enumerable non-function (and for a
336
+ // callable that returns `this`) so property traversal cannot invoke a
337
+ // stateful accessor a second time.
338
+ const toJSONDescriptor = Object.getOwnPropertyDescriptor(value, 'toJSON');
339
+ const toJSON = value.toJSON;
340
+ if (typeof toJSON === 'function') {
341
+ // Keep the source guarded through both hook execution and replacement
342
+ // normalization. A hook returning itself, or any replacement graph that
343
+ // points back to its source, is a JSON cycle rather than another hook
344
+ // invocation.
345
+ state.active.add(value);
346
+ try {
347
+ const replacement = toJSON.call(value, key);
348
+ const normalized = normalizeRequestToolJson(replacement, state, {
349
+ arrayEntry,
350
+ key,
351
+ applyToJSON: false,
352
+ seededProperties: null,
353
+ });
354
+ state.memo.set(value, normalized);
355
+ return normalized;
356
+ } finally {
357
+ state.active.delete(value);
358
+ }
359
+ }
360
+ if (toJSONDescriptor?.enumerable) {
361
+ capturedProperties = { ...(seededProperties || {}), toJSON };
362
+ }
363
+ }
364
+ const boxed = boxedJsonPrimitive(value);
365
+ if (boxed.matched) {
366
+ return normalizeRequestToolJson(boxed.value, state, { arrayEntry, key });
367
+ }
368
+
369
+ const isArray = Array.isArray(value);
370
+ const normalized = isArray ? [] : {};
371
+ state.active.add(value);
372
+ try {
373
+ if (isArray) {
374
+ // JSON.stringify captures array length once. Accessors may mutate the
375
+ // source array, but they cannot extend or shorten this iteration bound.
376
+ const rawLength = value.length;
377
+ const length = providerSnapshotArrayLength(rawLength);
378
+ for (let index = 0; index < length; index += 1) {
379
+ const entry = value[index];
380
+ normalized.push(normalizeRequestToolJson(entry, state, {
381
+ arrayEntry: true,
382
+ key: String(index),
383
+ }));
384
+ }
385
+ } else {
386
+ // JSON-compatible request schemas are own-enumerable data. Normalize class
387
+ // instances/accessors to a plain record, ignore inherited mutable fields,
388
+ // and define keys explicitly so an own "__proto__" remains ordinary data.
389
+ for (const key of Reflect.ownKeys(value)) {
390
+ if (typeof key !== 'string') continue;
391
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
392
+ if (!descriptor?.enumerable) continue;
393
+ // Deferred catalog selection captures `name` once before touching the
394
+ // candidate schema. Seed that captured value here so a stateful/throwing
395
+ // getter is never observed a second time during JSON normalization.
396
+ const entry = capturedProperties && Object.hasOwn(capturedProperties, key)
397
+ ? capturedProperties[key]
398
+ : value[key];
399
+ const child = normalizeRequestToolJson(entry, state, { key });
400
+ if (child !== OMIT_REQUEST_TOOL_VALUE) defineEnumerableDataProperty(normalized, key, child);
401
+ }
402
+ }
403
+ } finally {
404
+ state.active.delete(value);
405
+ }
406
+ Object.freeze(normalized);
407
+ state.memo.set(value, normalized);
408
+ return normalized;
409
+ }
410
+
411
+ // Establish one immutable request-attempt snapshot. Nested schema records are
412
+ // cloned before freezing, so a catalog refresh or in-place schema mutation
413
+ // after this boundary cannot change either provider bytes or their signature.
414
+ export function snapshotProviderRequestTools(options = {}) {
415
+ const {
416
+ provider,
417
+ tools,
418
+ nativeTools,
419
+ messages,
420
+ session,
421
+ } = options;
422
+ const activeTools = Array.isArray(tools) ? tools : [];
423
+ const state = { active: new WeakSet(), memo: new WeakMap() };
424
+ const snapshots = [];
425
+ const names = new Set();
426
+ const activeCandidateRefs = new WeakSet();
427
+ // Anthropic native definitions are already provider-wire objects. Preserve
428
+ // their prior prepend order and duplicate behavior, but freeze the exact
429
+ // bytes into the same request snapshot used for accounting and retries.
430
+ if (ANTHROPIC_NATIVE_PROVIDERS.has(clean(provider || session?.provider).toLowerCase())) {
431
+ for (const nativeTool of Array.isArray(nativeTools) ? nativeTools : []) {
432
+ if (!nativeTool || typeof nativeTool !== 'object') continue;
433
+ const normalized = normalizeRequestToolJson(nativeTool, state);
434
+ if (normalized && typeof normalized === 'object' && !Array.isArray(normalized)) {
435
+ snapshots.push(normalized);
436
+ }
437
+ }
438
+ }
439
+ const nativePrefixCount = snapshots.length;
440
+ const finish = () => finalizeProviderRequestTools(snapshots, nativePrefixCount);
441
+ const appendSnapshot = (candidate, selectedName = null, deferred = false) => {
442
+ const normalized = normalizeRequestToolJson(candidate, state, {
443
+ seededProperties: selectedName === null ? null : { name: selectedName },
444
+ });
445
+ if (deferred && (
446
+ !normalized
447
+ || typeof normalized !== 'object'
448
+ || Array.isArray(normalized)
449
+ || typeof normalized.name !== 'string'
450
+ || !clean(normalized.name)
451
+ || normalized.name !== selectedName
452
+ )) {
453
+ throw new TypeError(`provider tool snapshot: selected tool identity mismatch for ${JSON.stringify(selectedName)}`);
454
+ }
455
+ if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) return;
456
+ const name = clean(normalized.name);
457
+ if (!name || names.has(name)) return;
458
+ names.add(name);
459
+ if (!deferred) {
460
+ snapshots.push(normalized);
461
+ return;
462
+ }
463
+ const deferredSnapshot = {};
464
+ for (const key of Object.keys(normalized)) {
465
+ if (key === 'deferLoading' || key === 'defer_loading') continue;
466
+ defineEnumerableDataProperty(deferredSnapshot, key, normalized[key]);
467
+ }
468
+ defineEnumerableDataProperty(deferredSnapshot, 'deferLoading', true);
469
+ snapshots.push(Object.freeze(deferredSnapshot));
470
+ };
471
+
472
+ // Active candidates are provider-visible by definition: normalize each once,
473
+ // then perform all validation/dedupe from the plain snapshot only.
474
+ for (const tool of activeTools) {
475
+ if (tool && typeof tool === 'object') activeCandidateRefs.add(tool);
476
+ appendSnapshot(tool);
477
+ }
478
+
479
+ const normalizedProvider = clean(provider || session?.provider).toLowerCase();
480
+ if (!ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider)
481
+ || session?.deferredNativeTools !== true
482
+ // Native definitions preserve their historical prepend behavior, but they
483
+ // do not make an otherwise all-deferred catalog eligible for expansion.
484
+ || names.size === 0) {
485
+ return finish();
486
+ }
487
+ const discovered = new Set(parseToolSelection(session?.deferredDiscoveredTools));
488
+ for (const message of Array.isArray(messages) ? messages : []) {
489
+ const native = message?.nativeToolSearch;
490
+ const source = clean(native?.provider).toLowerCase();
491
+ if (source && source !== normalizedProvider
492
+ && !(ANTHROPIC_NATIVE_PROVIDERS.has(source)
493
+ && ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider))) continue;
494
+ for (const name of parseToolSelection(native?.toolReferences)) discovered.add(name);
495
+ }
496
+ if (discovered.size === 0) return finish();
497
+
498
+ // Catalog arrays have no separate key map, so `name` is their explicit
499
+ // selection key contract: capture it once, skip undiscovered/duplicate
500
+ // entries without touching schemas, and seed selected normalization with the
501
+ // captured value to avoid a second getter evaluation.
502
+ const seenCatalogRefs = new WeakSet();
503
+ for (const tool of Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : []) {
504
+ if (tool && typeof tool === 'object') {
505
+ if (activeCandidateRefs.has(tool) || seenCatalogRefs.has(tool)) continue;
506
+ seenCatalogRefs.add(tool);
507
+ }
508
+ const capturedName = tool?.name;
509
+ const selectionName = typeof capturedName === 'string' ? clean(capturedName) : '';
510
+ if (!selectionName || !discovered.has(selectionName) || names.has(selectionName)) continue;
511
+ appendSnapshot(tool, capturedName, true);
512
+ }
513
+ return finish();
514
+ }
515
+
249
516
  export function filterDisallowedTools(tools, disallowed = []) {
250
517
  if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
251
518
  const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
@@ -510,6 +777,7 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
510
777
 
511
778
  export function rebuildDeferredToolSurfaceForProvider(session, provider) {
512
779
  if (!session || !Array.isArray(session.tools)) return session;
780
+ const previousMode = session.deferredProviderMode;
513
781
  const previousFamily = nativeProviderFamily(session.provider);
514
782
  const nextFamily = nativeProviderFamily(provider);
515
783
  const preserveNativeState = previousFamily && previousFamily === nextFamily;
@@ -532,21 +800,36 @@ export function rebuildDeferredToolSurfaceForProvider(session, provider) {
532
800
  ]));
533
801
  session.deferredSelectedTools = session.deferredCallableTools.slice();
534
802
  }
803
+ if (previousMode && previousMode !== session.deferredProviderMode) {
804
+ if (session.deferredProviderMode === 'native') {
805
+ session.mcpServerInstructions = getMcpServerInstructionsMap();
806
+ applyInitialDeferredToolManifestToBp1(session, deferredPoolToolNames(session), { rebuild: true });
807
+ const rendered = session.messages?.find((message) => message?.role === 'system')?.content;
808
+ session.deferredAnnouncedTools = deferredPoolToolNames(session)
809
+ .filter((name) => typeof rendered === 'string' && rendered.includes(name));
810
+ } else if (previousMode === 'native') {
811
+ const system = session.messages?.find((message) => message?.role === 'system');
812
+ if (system && typeof system.content === 'string') {
813
+ system.content = stripDeferredToolManifestBlock(system.content);
814
+ }
815
+ session.deferredAnnouncedTools = [];
816
+ session.deferredToolBp1Applied = true;
817
+ }
818
+ }
535
819
  return session;
536
820
  }
537
821
 
538
822
  // FIRST-TURN deferred-surface refresh (claude-code turn-time deferred manifest).
539
823
  // An MCP server may finish its handshake BETWEEN session-create and the first
540
824
  // 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).
825
+ // provider-visible first-turn surface. Native providers rebuild the initial BP1
826
+ // <available-deferred-tools> manifest IN PLACE and pre-mark names announced.
827
+ // Manifest/canonical providers update their active fixed surface directly; the
828
+ // canonical path never emits a deferred manifest or late reminder. Fully sync
829
+ // and idempotent: no genuinely-new MCP name => no-op / no mutation.
547
830
  export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
548
831
  if (!session || !Array.isArray(session.messages)) return false;
549
- if (session.deferredProviderMode === 'full' || session.deferredProviderMode === 'canonical') return false;
832
+ if (session.deferredProviderMode === 'full') return false;
550
833
  const isMcp = (name) => typeof name === 'string' && name.startsWith('mcp__');
551
834
  const byName = new Map();
552
835
  for (const tool of Array.isArray(session.deferredToolCatalog) ? session.deferredToolCatalog : []) {
@@ -562,12 +845,15 @@ export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
562
845
  }
563
846
  if (!added) return false;
564
847
  session.deferredToolCatalog = sortedCatalogByMeasuredUsage([...byName.values()]);
565
- if (session.deferredProviderMode === 'manifest') {
848
+ if (session.deferredProviderMode === 'manifest' || session.deferredProviderMode === 'canonical') {
566
849
  const next = session.deferredToolCatalog.filter((tool) => (
567
850
  session.deferredSurfaceMode !== 'readonly' || isReadonlySelectable(tool)
568
851
  ));
569
852
  session.tools.splice(0, session.tools.length, ...next);
570
853
  session.deferredCallableTools = next.map((tool) => clean(tool?.name)).filter(Boolean);
854
+ if (session.deferredProviderMode === 'canonical') {
855
+ setDeferredToolState(session, session.deferredCallableTools);
856
+ }
571
857
  session.updatedAt = Date.now();
572
858
  return true;
573
859
  }
@@ -0,0 +1,73 @@
1
+ import { isKnownProvider } from '../provider-admin.mjs';
2
+ import {
3
+ DEFAULT_AGENT_PRESETS,
4
+ DEFAULT_PROVIDER,
5
+ } from './tool-def.mjs';
6
+ import {
7
+ agentPresetName,
8
+ clean,
9
+ findPreset,
10
+ normalizeAgentName,
11
+ normalizeAgentRoute,
12
+ synthesizePreset,
13
+ } from './helpers.mjs';
14
+
15
+ export function resolveAgentSpawnPreset(config, args = {}) {
16
+ if (args.provider && args.model) {
17
+ return {
18
+ presetName: args.preset || '__direct__',
19
+ preset: {
20
+ id: '__direct__',
21
+ name: '__DIRECT__',
22
+ type: 'agent',
23
+ provider: clean(args.provider),
24
+ model: clean(args.model),
25
+ effort: clean(args.effort) || undefined,
26
+ fast: args.fast === true,
27
+ tools: 'full',
28
+ },
29
+ };
30
+ }
31
+
32
+ const agentName = normalizeAgentName(args.agent);
33
+ const configuredDefault = clean(config?.defaultProvider);
34
+ const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
35
+ ? configuredDefault
36
+ : DEFAULT_PROVIDER;
37
+ const workflowSlot = agentName === 'explore' ? 'explorer'
38
+ : (agentName === 'maintainer' ? 'memory' : '');
39
+ const maintenanceSlot = agentName === 'explore' ? 'explore'
40
+ : (agentName === 'maintainer' ? 'memory' : '');
41
+ const agentRoute = !clean(args.preset)
42
+ ? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
43
+ || (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null)
44
+ || normalizeAgentRoute(config?.workflowRoutes?.[workflowSlot], fallbackProvider)
45
+ || normalizeAgentRoute(config?.maintenance?.[maintenanceSlot], fallbackProvider))
46
+ : null;
47
+ if (agentRoute) {
48
+ return {
49
+ presetName: agentPresetName(agentName),
50
+ preset: {
51
+ id: `agent-${agentName}`,
52
+ name: agentPresetName(agentName),
53
+ type: 'agent',
54
+ provider: agentRoute.provider,
55
+ model: agentRoute.model,
56
+ effort: agentRoute.effort,
57
+ fast: agentRoute.fast === true,
58
+ tools: 'full',
59
+ },
60
+ };
61
+ }
62
+
63
+ const mainPreset = !clean(args.preset) && (agentName === 'explore' || agentName === 'maintainer')
64
+ ? findPreset(config, config?.default)
65
+ : null;
66
+ if (mainPreset) return { presetName: mainPreset.id || mainPreset.name, preset: mainPreset };
67
+
68
+ const presetName = clean(args.preset) || DEFAULT_AGENT_PRESETS[agentName];
69
+ if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
70
+ const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
71
+ if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
72
+ return { presetName, preset };
73
+ }