@sayknow-cli/coding-agent 0.5.2 → 0.5.8

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 (94) hide show
  1. package/CHANGELOG.md +66 -16
  2. package/dist/types/cli/update-cli.d.ts +8 -1
  3. package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
  4. package/dist/types/config/keybindings.d.ts +10 -0
  5. package/dist/types/config/model-registry.d.ts +1 -1
  6. package/dist/types/config/models-config-schema.d.ts +4 -0
  7. package/dist/types/config/settings-schema.d.ts +73 -0
  8. package/dist/types/dap/client.d.ts +1 -0
  9. package/dist/types/lsp/client.d.ts +5 -0
  10. package/dist/types/modes/components/welcome.d.ts +1 -0
  11. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  12. package/dist/types/modes/interactive-mode.d.ts +13 -1
  13. package/dist/types/modes/types.d.ts +7 -1
  14. package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
  15. package/dist/types/session/agent-session.d.ts +8 -0
  16. package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
  17. package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
  18. package/dist/types/session/session-manager.d.ts +9 -0
  19. package/dist/types/session/session-storage.d.ts +17 -0
  20. package/dist/types/session-import/claude.d.ts +26 -0
  21. package/dist/types/session-import/codex.d.ts +22 -0
  22. package/dist/types/session-import/command.d.ts +20 -0
  23. package/dist/types/session-import/detect.d.ts +22 -0
  24. package/dist/types/session-import/index.d.ts +7 -0
  25. package/dist/types/session-import/redact.d.ts +23 -0
  26. package/dist/types/session-import/service.d.ts +28 -0
  27. package/dist/types/session-import/types.d.ts +125 -0
  28. package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
  29. package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
  30. package/dist/types/slash-commands/types.d.ts +5 -0
  31. package/dist/types/system-prompt.d.ts +5 -0
  32. package/dist/types/tools/ask.d.ts +5 -0
  33. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  34. package/package.json +13 -7
  35. package/scripts/compile-args.ts +8 -4
  36. package/scripts/generate-sdk-operation-inventory.ts +5 -6
  37. package/src/cli/config-cli.ts +23 -8
  38. package/src/cli/update-cli.ts +265 -21
  39. package/src/config/atomic-yaml-patch.ts +167 -30
  40. package/src/config/keybindings.ts +10 -0
  41. package/src/config/model-profiles.ts +84 -16
  42. package/src/config/model-registry.ts +60 -38
  43. package/src/config/models-config-schema.ts +1 -1
  44. package/src/config/settings-schema.ts +79 -0
  45. package/src/config/settings.ts +13 -2
  46. package/src/dap/client.ts +78 -30
  47. package/src/internal-urls/docs-index.generated.ts +5 -4
  48. package/src/lsp/client.ts +17 -29
  49. package/src/modes/action-registry.ts +2 -0
  50. package/src/modes/components/welcome.ts +5 -0
  51. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  52. package/src/modes/controllers/input-controller.ts +71 -6
  53. package/src/modes/controllers/plan-mode-controller.ts +8 -1
  54. package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
  55. package/src/modes/controllers/selector-controller.ts +47 -19
  56. package/src/modes/interactive-mode.ts +54 -4
  57. package/src/modes/types.ts +5 -1
  58. package/src/prompts/system/system-prompt.md +8 -0
  59. package/src/runtime-mcp/transports/stdio.ts +96 -33
  60. package/src/sdk/broker/lifecycle.ts +6 -5
  61. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  62. package/src/sdk/session.ts +27 -18
  63. package/src/session/agent-session.ts +155 -15
  64. package/src/session/internal/managed-session-scope.ts +41 -6
  65. package/src/session/internal/managed-session-storage.ts +14 -0
  66. package/src/session/session-manager.ts +113 -11
  67. package/src/session/session-storage.ts +70 -0
  68. package/src/session-import/claude.ts +382 -0
  69. package/src/session-import/codex.ts +458 -0
  70. package/src/session-import/command.ts +61 -0
  71. package/src/session-import/detect.ts +133 -0
  72. package/src/session-import/index.ts +27 -0
  73. package/src/session-import/redact.ts +125 -0
  74. package/src/session-import/service.ts +749 -0
  75. package/src/session-import/types.ts +163 -0
  76. package/src/skc-runtime/launch-worktree.ts +277 -4
  77. package/src/slash-commands/acp-builtins.ts +5 -1
  78. package/src/slash-commands/builtin-registry.ts +100 -1
  79. package/src/slash-commands/types.ts +5 -0
  80. package/src/system-prompt.ts +7 -0
  81. package/src/tools/ask.ts +5 -0
  82. package/src/tools/fetch.ts +7 -2
  83. package/src/utils/markit.ts +24 -2
  84. package/src/utils/mupdf-wasm.ts +26 -0
  85. package/src/export/html/template.css +0 -1060
  86. package/src/export/html/template.html +0 -47
  87. package/src/export/html/template.js +0 -2348
  88. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  89. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  90. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  91. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  92. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  93. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  94. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -24,6 +24,12 @@ import { MCPExpectedFailure, toJsonRpcError } from "../../runtime-mcp/types";
24
24
  */
25
25
  const CLOSE_WAIT_MS = 1_000;
26
26
 
27
+ /** Write one JSONL frame while honoring Bun FileSink backpressure and flush failures. */
28
+ export async function writeStdioFrame(stdin: Bun.FileSink, message: string): Promise<void> {
29
+ await Promise.resolve(stdin.write(message));
30
+ await stdin.flush();
31
+ }
32
+
27
33
  /**
28
34
  * Build a minimal environment for a no-inherit stdio MCP child. Only OS-level
29
35
  * keys needed to locate/run an interpreter (PATH, HOME, temp, locale, and the
@@ -69,6 +75,9 @@ export class StdioTransport implements MCPTransport {
69
75
  #readLoop: Promise<void> | null = null;
70
76
  #stderrLoop: Promise<void> | null = null;
71
77
  #closePromise: Promise<void> | null = null;
78
+ #writeQueue: Promise<void> = Promise.resolve();
79
+ #connectionEpoch = 0;
80
+ #terminalFailure: { epoch: number; error: MCPExpectedFailure } | undefined;
72
81
 
73
82
  onClose?: () => void;
74
83
  onError?: (error: Error) => void;
@@ -115,6 +124,8 @@ export class StdioTransport implements MCPTransport {
115
124
  throw new MCPExpectedFailure(error);
116
125
  }
117
126
 
127
+ this.#connectionEpoch++;
128
+ this.#terminalFailure = undefined;
118
129
  this.#connected = true;
119
130
 
120
131
  // Start reading stdout
@@ -126,6 +137,7 @@ export class StdioTransport implements MCPTransport {
126
137
 
127
138
  async #startReadLoop(): Promise<void> {
128
139
  if (!this.#process?.child.stdout) return;
140
+ const epoch = this.#connectionEpoch;
129
141
  let failure: MCPExpectedFailure | undefined;
130
142
  try {
131
143
  for await (const line of readJsonl(this.#process.child.stdout)) {
@@ -138,11 +150,12 @@ export class StdioTransport implements MCPTransport {
138
150
  }
139
151
  } catch (error) {
140
152
  failure = new MCPExpectedFailure(error);
141
- if (this.#connected) {
142
- this.onError?.(failure);
143
- }
144
153
  } finally {
145
- this.#handleClose(failure);
154
+ if (failure) {
155
+ this.#terminalizeFailure(failure, epoch);
156
+ } else {
157
+ this.#handleClose();
158
+ }
146
159
  }
147
160
  }
148
161
 
@@ -177,7 +190,10 @@ export class StdioTransport implements MCPTransport {
177
190
  }
178
191
  // Server-to-client request: has both method and id
179
192
  if ("method" in message && "id" in message && message.id != null) {
180
- void this.#handleServerRequest(message as JsonRpcRequest);
193
+ const stdin = this.#getStdin();
194
+ if (stdin) {
195
+ void this.#handleServerRequest(message as JsonRpcRequest, this.#connectionEpoch, stdin);
196
+ }
181
197
  return;
182
198
  }
183
199
 
@@ -211,21 +227,23 @@ export class StdioTransport implements MCPTransport {
211
227
  }
212
228
  }
213
229
 
214
- async #handleServerRequest(request: JsonRpcRequest): Promise<void> {
230
+ async #handleServerRequest(request: JsonRpcRequest, epoch: number, stdin: Bun.FileSink): Promise<void> {
231
+ if (!this.onRequest) {
232
+ await this.#sendResponseOrClose(request.id, epoch, stdin, undefined, {
233
+ code: -32601,
234
+ message: "Method not found",
235
+ });
236
+ return;
237
+ }
238
+
239
+ let result: unknown;
215
240
  try {
216
- if (!this.onRequest) {
217
- this.#sendResponse(request.id, undefined, { code: -32601, message: "Method not found" });
218
- return;
219
- }
220
- const result = await this.onRequest(request.method, request.params);
221
- this.#sendResponse(request.id, result);
241
+ result = await this.onRequest(request.method, request.params);
222
242
  } catch (error) {
223
- try {
224
- this.#sendResponse(request.id, undefined, toJsonRpcError(error));
225
- } catch {
226
- // Best-effort — process may have exited
227
- }
243
+ await this.#sendResponseOrClose(request.id, epoch, stdin, undefined, toJsonRpcError(error));
244
+ return;
228
245
  }
246
+ await this.#sendResponseOrClose(request.id, epoch, stdin, result);
229
247
  }
230
248
 
231
249
  #getStdin(): Bun.FileSink | null {
@@ -233,28 +251,70 @@ export class StdioTransport implements MCPTransport {
233
251
  return typeof stdin === "object" && stdin !== null ? stdin : null;
234
252
  }
235
253
 
236
- #sendResponse(id: string | number, result?: unknown, error?: JsonRpcError): void {
237
- const stdin = this.#getStdin();
238
- if (!this.#connected || !stdin) return;
254
+ async #sendResponse(
255
+ id: string | number,
256
+ epoch: number,
257
+ stdin: Bun.FileSink,
258
+ result?: unknown,
259
+ error?: JsonRpcError,
260
+ ): Promise<void> {
239
261
  const response = error
240
262
  ? { jsonrpc: "2.0" as const, id, error }
241
263
  : { jsonrpc: "2.0" as const, id, result: result ?? {} };
242
- stdin.write(`${JSON.stringify(response)}\n`);
243
- stdin.flush();
264
+ await this.#queueWriteFrame(stdin, `${JSON.stringify(response)}\n`, epoch);
265
+ }
266
+
267
+ #queueWriteFrame(stdin: Bun.FileSink, message: string, epoch: number): Promise<void> {
268
+ const write = this.#writeQueue
269
+ .catch(() => {})
270
+ .then(() => {
271
+ if (!this.#connected || this.#connectionEpoch !== epoch || this.#getStdin() !== stdin) {
272
+ throw this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : new MCPExpectedFailure();
273
+ }
274
+ return writeStdioFrame(stdin, message);
275
+ });
276
+ this.#writeQueue = write.catch(() => {});
277
+ return write;
278
+ }
279
+
280
+ async #sendResponseOrClose(
281
+ id: string | number,
282
+ epoch: number,
283
+ stdin: Bun.FileSink,
284
+ result?: unknown,
285
+ error?: JsonRpcError,
286
+ ): Promise<void> {
287
+ try {
288
+ await this.#sendResponse(id, epoch, stdin, result, error);
289
+ } catch (writeError) {
290
+ this.#terminalizeFailure(writeError, epoch);
291
+ }
244
292
  }
245
293
 
246
294
  #handleClose(failure?: MCPExpectedFailure): void {
247
295
  void this.#closeInternal(true, failure);
248
296
  }
249
297
 
298
+ #terminalizeFailure(error: unknown, epoch: number): MCPExpectedFailure {
299
+ const existing = this.#terminalFailure;
300
+ if (existing?.epoch === epoch) return existing.error;
301
+ const failure = error instanceof MCPExpectedFailure ? error : new MCPExpectedFailure(error);
302
+ if (epoch !== this.#connectionEpoch) return failure;
303
+ this.#terminalFailure = { epoch, error: failure };
304
+ this.onError?.(failure);
305
+ this.#handleClose(failure);
306
+ return failure;
307
+ }
308
+
250
309
  async request<T = unknown>(
251
310
  method: string,
252
311
  params?: Record<string, unknown>,
253
312
  options?: MCPRequestOptions,
254
313
  ): Promise<T> {
255
314
  const stdin = this.#getStdin();
315
+ const epoch = this.#connectionEpoch;
256
316
  if (!this.#connected || !stdin) {
257
- throw new MCPExpectedFailure();
317
+ throw this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : new MCPExpectedFailure();
258
318
  }
259
319
 
260
320
  const id = Snowflake.next();
@@ -318,12 +378,10 @@ export class StdioTransport implements MCPTransport {
318
378
 
319
379
  const message = `${JSON.stringify(request)}\n`;
320
380
  try {
321
- // Bun's FileSink has write() method directly
322
- stdin.write(message);
323
- stdin.flush();
381
+ await this.#queueWriteFrame(stdin, message, epoch);
324
382
  } catch (error: unknown) {
325
383
  cleanup();
326
- reject(new MCPExpectedFailure(error));
384
+ reject(this.#terminalizeFailure(error, epoch));
327
385
  }
328
386
 
329
387
  return promise;
@@ -331,8 +389,9 @@ export class StdioTransport implements MCPTransport {
331
389
 
332
390
  async notify(method: string, params?: Record<string, unknown>): Promise<void> {
333
391
  const stdin = this.#getStdin();
392
+ const epoch = this.#connectionEpoch;
334
393
  if (!this.#connected || !stdin) {
335
- throw new MCPExpectedFailure();
394
+ throw this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : new MCPExpectedFailure();
336
395
  }
337
396
 
338
397
  const notification = {
@@ -343,16 +402,20 @@ export class StdioTransport implements MCPTransport {
343
402
 
344
403
  const message = `${JSON.stringify(notification)}\n`;
345
404
  try {
346
- // Bun's FileSink has write() method directly
347
- stdin.write(message);
348
- stdin.flush();
405
+ await this.#queueWriteFrame(stdin, message, epoch);
349
406
  } catch (error) {
350
- throw new MCPExpectedFailure(error);
407
+ throw this.#terminalizeFailure(error, epoch);
351
408
  }
352
409
  }
353
410
 
354
411
  async close(): Promise<void> {
355
- await this.#closeInternal(false);
412
+ const epoch = this.#connectionEpoch;
413
+ let failure = this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : undefined;
414
+ if (!failure) {
415
+ failure = new MCPExpectedFailure();
416
+ this.#terminalFailure = { epoch, error: failure };
417
+ }
418
+ await this.#closeInternal(false, failure);
356
419
  }
357
420
 
358
421
  #closeInternal(fromReadLoop: boolean, failure?: MCPExpectedFailure): Promise<void> {
@@ -25,7 +25,8 @@ import {
25
25
  } from "../../session/session-storage";
26
26
  import {
27
27
  ensureLaunchWorktree,
28
- ensureReusableNodeModules,
28
+ ensureLaunchWorktreeDependencies,
29
+ launchWorktreeCwd,
29
30
  planLaunchWorktree,
30
31
  type SkcLaunchWorktreePlan,
31
32
  } from "../../skc-runtime/launch-worktree";
@@ -656,7 +657,7 @@ async function validateLiveResumeScope(
656
657
  : { enabled: true, detached: true, name: null },
657
658
  );
658
659
  if (!planned.enabled) return fail("invalid_input", "Lifecycle worktree target is invalid.");
659
- cwd = path.resolve(planned.worktreePath);
660
+ cwd = launchWorktreeCwd(planned);
660
661
  } catch (error) {
661
662
  return fail(
662
663
  "invalid_input",
@@ -2338,7 +2339,7 @@ function preparePlannedWorktree(plan: SkcLaunchWorktreePlan): SessionLifecycleWo
2338
2339
  throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
2339
2340
  return {
2340
2341
  enabled: true,
2341
- cwd: path.resolve(prepared.worktreePath),
2342
+ cwd: launchWorktreeCwd(prepared),
2342
2343
  created: prepared.created,
2343
2344
  reused: prepared.reused,
2344
2345
  ...(prepared.branchName ? { branch: prepared.branchName } : {}),
@@ -2377,7 +2378,7 @@ async function launchInput(
2377
2378
  );
2378
2379
  if (!planned.enabled) return fail("invalid_input", "Lifecycle worktree target is invalid.");
2379
2380
  worktreePlan = planned;
2380
- cwd = path.resolve(planned.worktreePath);
2381
+ cwd = launchWorktreeCwd(planned);
2381
2382
  } catch (error) {
2382
2383
  return fail(
2383
2384
  "invalid_input",
@@ -2867,7 +2868,7 @@ async function executeLifecycleResponse(
2867
2868
  digest: createHash("sha256").update(canonicalJson({ worktree })).digest("hex"),
2868
2869
  };
2869
2870
  await broker.ledger.transition(identity, "effect_started", { durableEffects });
2870
- ensureReusableNodeModules(launch.worktreePlan.repoRoot, launch.worktreePlan.worktreePath);
2871
+ ensureLaunchWorktreeDependencies(launch.worktreePlan);
2871
2872
  }
2872
2873
  } catch (error) {
2873
2874
  return fail(
@@ -1825,6 +1825,17 @@
1825
1825
  "packages/coding-agent/test/sdk-operation-inventory.test.ts"
1826
1826
  ]
1827
1827
  },
1828
+ {
1829
+ "sourceId": "slash_command:import-session",
1830
+ "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
1831
+ "sourceKind": "slash_command",
1832
+ "decision": "exclude",
1833
+ "rationale": "local transcript-file import into a new session; ACP is explicitly disabled and no SDK operation reads caller-selected host paths",
1834
+ "exclusionMetadata": {
1835
+ "adapterMappings": "not_applicable",
1836
+ "testIds": "not_applicable"
1837
+ }
1838
+ },
1828
1839
  {
1829
1840
  "sourceId": "slash_command:copy",
1830
1841
  "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
@@ -2870,6 +2881,17 @@
2870
2881
  "testIds": "not_applicable"
2871
2882
  }
2872
2883
  },
2884
+ {
2885
+ "sourceId": "agent_session:hasExplicitlyDisabledTools",
2886
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2887
+ "sourceKind": "agent_session",
2888
+ "decision": "exclude",
2889
+ "rationale": "internal automatic-tool-selection predicate, not a user-facing SDK control seam",
2890
+ "exclusionMetadata": {
2891
+ "adapterMappings": "not_applicable",
2892
+ "testIds": "not_applicable"
2893
+ }
2894
+ },
2873
2895
  {
2874
2896
  "sourceId": "agent_session:setActiveToolsByName",
2875
2897
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -4317,6 +4339,17 @@
4317
4339
  "testIds": "not_applicable"
4318
4340
  }
4319
4341
  },
4342
+ {
4343
+ "sourceId": "agent_session:runPrePromptContextCheckForTests",
4344
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
4345
+ "sourceKind": "agent_session",
4346
+ "decision": "exclude",
4347
+ "rationale": "test-only context check seam, not a user-facing SDK control seam",
4348
+ "exclusionMetadata": {
4349
+ "adapterMappings": "not_applicable",
4350
+ "testIds": "not_applicable"
4351
+ }
4352
+ },
4320
4353
  {
4321
4354
  "sourceId": "agent_session:setRetainedMemorySampler",
4322
4355
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -2197,6 +2197,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2197
2197
 
2198
2198
  const repeatToolDescriptions = settings.get("repeatToolDescriptions");
2199
2199
  const eagerTasks = settings.get("task.eager");
2200
+ const reasoningLanguage = settings.get("reasoningLanguage");
2200
2201
  const intentTracingEnabled = resolveIntentTracingEnabled(
2201
2202
  settings.get("tools.intentTracing"),
2202
2203
  options.hasUI ?? false,
@@ -2257,6 +2258,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2257
2258
  toolDiscoveryActive: effectiveDiscoveryMode === "all" || mcpDiscoveryEnabled,
2258
2259
  eagerTasks,
2259
2260
  secretsEnabled,
2261
+ reasoningLanguage,
2260
2262
  workspaceTree: workspaceTreePromise,
2261
2263
  subagent: options.parentTaskPrefix !== undefined,
2262
2264
  });
@@ -2272,13 +2274,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2272
2274
  };
2273
2275
  };
2274
2276
 
2277
+ const explicitlyDisabledTools = options.toolNames?.length === 0;
2275
2278
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
2276
2279
  const hasExplicitToolNames = options.toolNames !== undefined;
2277
2280
  const requestedToolNames = hasExplicitToolNames
2278
2281
  ? [
2279
2282
  ...new Set([
2280
2283
  ...options.toolNames!.map(name => name.toLowerCase()),
2281
- ...(settings.get("goal.enabled") ? ["goal"] : []),
2284
+ ...(settings.get("goal.enabled") && !explicitlyDisabledTools ? ["goal"] : []),
2282
2285
  ]),
2283
2286
  ]
2284
2287
  : toolNamesFromRegistry;
@@ -2298,7 +2301,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2298
2301
  registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
2299
2302
  );
2300
2303
  const requestedActiveToolNames = normalizedRequested;
2301
- const initialRequestedActiveToolNames = options.toolNames
2304
+ const initialRequestedActiveToolNames = hasExplicitToolNames
2302
2305
  ? requestedActiveToolNames
2303
2306
  : requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
2304
2307
  const discoverableMCPToolNames = new Set<string>();
@@ -2348,11 +2351,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2348
2351
  ];
2349
2352
  hasExplicitMCPToolSelection =
2350
2353
  hasExplicitToolNames && (options.toolNames!.length === 0 || selectableExplicitMCPToolNames.length > 0);
2351
- initialSelectedMCPToolNames = existingSession.hasPersistedMCPToolSelection
2352
- ? restoredSelectedMCPToolNames
2353
- : hasExplicitMCPToolSelection
2354
- ? selectableExplicitMCPToolNames
2355
- : defaultSelectedMCPToolNames;
2354
+ initialSelectedMCPToolNames = explicitlyDisabledTools
2355
+ ? []
2356
+ : existingSession.hasPersistedMCPToolSelection
2357
+ ? restoredSelectedMCPToolNames
2358
+ : hasExplicitMCPToolSelection
2359
+ ? selectableExplicitMCPToolNames
2360
+ : defaultSelectedMCPToolNames;
2356
2361
  initialToolNames = [
2357
2362
  ...new Set([
2358
2363
  ...initialRequestedActiveToolNames.filter(name => !discoverableMCPToolNames.has(name)),
@@ -2361,14 +2366,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2361
2366
  ];
2362
2367
  }
2363
2368
 
2364
- // Custom, extension-registered, and plugin-bundle MCP tools are always
2365
- // included regardless of the caller's built-in tool filter. Plugin MCPs
2366
- // remain always-on even when generic MCP discovery is disabled.
2367
- const alwaysInclude: string[] = [
2368
- ...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
2369
- ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
2370
- ...pluginMcpToolNames,
2371
- ];
2369
+ // Custom, extension-registered, and plugin-bundle MCP tools bypass
2370
+ // built-in filtering, but not an explicit zero-tool selection.
2371
+ const alwaysInclude: string[] = explicitlyDisabledTools
2372
+ ? []
2373
+ : [
2374
+ ...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
2375
+ ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
2376
+ ...pluginMcpToolNames,
2377
+ ];
2372
2378
  const pluginMcpToolNameSet = new Set(pluginMcpToolNames);
2373
2379
  for (const name of alwaysInclude) {
2374
2380
  if (mcpDiscoveryEnabled && discoverableMCPToolNames.has(name) && !pluginMcpToolNameSet.has(name)) {
@@ -2414,9 +2420,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2414
2420
  allowedDiscoveredBuiltinNames,
2415
2421
  essentialBuiltinNames,
2416
2422
  );
2417
- initialSelectedDiscoveredBuiltinToolNames = existingSession.hasPersistedDiscoveredBuiltinToolSelection
2418
- ? restoredDiscoveredNames
2419
- : explicitlyRequestedDiscoveredBuiltinToolNames;
2423
+ initialSelectedDiscoveredBuiltinToolNames = explicitlyDisabledTools
2424
+ ? []
2425
+ : existingSession.hasPersistedDiscoveredBuiltinToolSelection
2426
+ ? restoredDiscoveredNames
2427
+ : explicitlyRequestedDiscoveredBuiltinToolNames;
2420
2428
  initialToolNames = [...new Set([...baselineInitialToolNames, ...initialSelectedDiscoveredBuiltinToolNames])];
2421
2429
  hasExplicitDiscoveredBuiltinToolSelection =
2422
2430
  hasExplicitToolNames &&
@@ -2717,6 +2725,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2717
2725
  workspaceTree: resolvedWorkspaceTree,
2718
2726
  reloadSshTool,
2719
2727
  requestedToolNames: requestedToolNameSet,
2728
+ explicitlyDisabledTools,
2720
2729
  discoverableToolAllowedNames: options.discoverableToolAllowedNames,
2721
2730
  mcpDiscoveryEnabled,
2722
2731
  discoveryMode: effectiveDiscoveryMode,