@code-yeongyu/senpi 2026.8.29 → 2026.8.30

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 (200) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/core/agent-session.d.ts +45 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +312 -25
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/bash-executor.d.ts.map +1 -1
  7. package/dist/core/bash-executor.js +155 -37
  8. package/dist/core/bash-executor.js.map +1 -1
  9. package/dist/core/credential-accounts.d.ts.map +1 -1
  10. package/dist/core/credential-accounts.js +23 -9
  11. package/dist/core/credential-accounts.js.map +1 -1
  12. package/dist/core/credential-pool/env-slots.d.ts.map +1 -1
  13. package/dist/core/credential-pool/env-slots.js +2 -0
  14. package/dist/core/credential-pool/env-slots.js.map +1 -1
  15. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +8 -2
  16. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +160 -47
  18. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  19. package/dist/core/extensions/builtin/compaction/retained-message-safety.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/compaction/retained-message-safety.js +31 -8
  21. package/dist/core/extensions/builtin/compaction/retained-message-safety.js.map +1 -1
  22. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts.map +1 -1
  23. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js +8 -28
  24. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js.map +1 -1
  25. package/dist/core/extensions/builtin/permission-system/external-dir.d.ts.map +1 -1
  26. package/dist/core/extensions/builtin/permission-system/external-dir.js +15 -5
  27. package/dist/core/extensions/builtin/permission-system/external-dir.js.map +1 -1
  28. package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
  29. package/dist/core/extensions/builtin/permission-system/parsers.js +16 -1
  30. package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
  31. package/dist/core/extensions/builtin/terminal/manager.d.ts +5 -0
  32. package/dist/core/extensions/builtin/terminal/manager.d.ts.map +1 -1
  33. package/dist/core/extensions/builtin/terminal/manager.js +31 -3
  34. package/dist/core/extensions/builtin/terminal/manager.js.map +1 -1
  35. package/dist/core/extensions/builtin/terminal/monitor-permission.d.ts +8 -0
  36. package/dist/core/extensions/builtin/terminal/monitor-permission.d.ts.map +1 -0
  37. package/dist/core/extensions/builtin/terminal/monitor-permission.js +16 -0
  38. package/dist/core/extensions/builtin/terminal/monitor-permission.js.map +1 -0
  39. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +13 -0
  40. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
  41. package/dist/core/extensions/builtin/terminal/monitor-registry.js +403 -3
  42. package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
  43. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts.map +1 -1
  44. package/dist/core/extensions/builtin/terminal/session-bundle.js +1 -0
  45. package/dist/core/extensions/builtin/terminal/session-bundle.js.map +1 -1
  46. package/dist/core/extensions/builtin/terminal/tools/kill-bash.d.ts.map +1 -1
  47. package/dist/core/extensions/builtin/terminal/tools/kill-bash.js +5 -2
  48. package/dist/core/extensions/builtin/terminal/tools/kill-bash.js.map +1 -1
  49. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +4 -0
  50. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts.map +1 -1
  51. package/dist/core/extensions/builtin/terminal/tools/monitor.js +40 -2
  52. package/dist/core/extensions/builtin/terminal/tools/monitor.js.map +1 -1
  53. package/dist/core/model-resolver.js +2 -2
  54. package/dist/core/model-resolver.js.map +1 -1
  55. package/dist/core/session-manager.d.ts.map +1 -1
  56. package/dist/core/session-manager.js +10 -1
  57. package/dist/core/session-manager.js.map +1 -1
  58. package/dist/core/settings-manager.d.ts +7 -0
  59. package/dist/core/settings-manager.d.ts.map +1 -1
  60. package/dist/core/settings-manager.js +6 -0
  61. package/dist/core/settings-manager.js.map +1 -1
  62. package/dist/core/tools/bash.d.ts.map +1 -1
  63. package/dist/core/tools/bash.js +97 -15
  64. package/dist/core/tools/bash.js.map +1 -1
  65. package/dist/core/tools/index.d.ts +1 -0
  66. package/dist/core/tools/index.d.ts.map +1 -1
  67. package/dist/core/tools/index.js +8 -0
  68. package/dist/core/tools/index.js.map +1 -1
  69. package/dist/core/tools/output-accumulator.d.ts +1 -1
  70. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  71. package/dist/core/tools/output-accumulator.js +76 -34
  72. package/dist/core/tools/output-accumulator.js.map +1 -1
  73. package/dist/modes/interactive/components/footer.d.ts +7 -0
  74. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  75. package/dist/modes/interactive/components/footer.js +34 -8
  76. package/dist/modes/interactive/components/footer.js.map +1 -1
  77. package/dist/modes/interactive/grok/chrome.d.ts +2 -0
  78. package/dist/modes/interactive/grok/chrome.d.ts.map +1 -1
  79. package/dist/modes/interactive/grok/chrome.js.map +1 -1
  80. package/dist/modes/interactive/interactive-host-runtime.d.ts +51 -1
  81. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
  82. package/dist/modes/interactive/interactive-host-runtime.js +235 -20
  83. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
  84. package/dist/modes/interactive/interactive-mode.d.ts +7 -0
  85. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  86. package/dist/modes/interactive/interactive-mode.js +114 -2
  87. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  88. package/dist/modes/rpc/connection-handler.d.ts +8 -1
  89. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  90. package/dist/modes/rpc/connection-handler.js +145 -49
  91. package/dist/modes/rpc/connection-handler.js.map +1 -1
  92. package/dist/modes/rpc/host-ensure.d.ts +2 -0
  93. package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
  94. package/dist/modes/rpc/host-ensure.js +15 -2
  95. package/dist/modes/rpc/host-ensure.js.map +1 -1
  96. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  97. package/dist/modes/rpc/host-lifecycle.js +31 -12
  98. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  99. package/dist/modes/rpc/host-watchdog.d.ts +2 -0
  100. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
  101. package/dist/modes/rpc/host-watchdog.js +23 -12
  102. package/dist/modes/rpc/host-watchdog.js.map +1 -1
  103. package/dist/modes/rpc/multi-session-host.js +6 -4
  104. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  105. package/dist/modes/rpc/rpc-client.d.ts +10 -4
  106. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  107. package/dist/modes/rpc/rpc-client.js +45 -10
  108. package/dist/modes/rpc/rpc-client.js.map +1 -1
  109. package/dist/modes/rpc/rpc-input-validation.d.ts +1 -0
  110. package/dist/modes/rpc/rpc-input-validation.d.ts.map +1 -1
  111. package/dist/modes/rpc/rpc-input-validation.js +66 -0
  112. package/dist/modes/rpc/rpc-input-validation.js.map +1 -1
  113. package/dist/modes/rpc/rpc-types.d.ts +79 -1
  114. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  115. package/dist/modes/rpc/rpc-types.js.map +1 -1
  116. package/dist/modes/rpc/session-binding.d.ts +1 -0
  117. package/dist/modes/rpc/session-binding.d.ts.map +1 -1
  118. package/dist/modes/rpc/session-binding.js +21 -1
  119. package/dist/modes/rpc/session-binding.js.map +1 -1
  120. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  121. package/dist/modes/rpc/session-command-router.js +1 -0
  122. package/dist/modes/rpc/session-command-router.js.map +1 -1
  123. package/dist/modes/rpc/session-event-writer.d.ts +2 -0
  124. package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
  125. package/dist/modes/rpc/session-event-writer.js +54 -7
  126. package/dist/modes/rpc/session-event-writer.js.map +1 -1
  127. package/dist/modes/rpc/session-extension-ui-requests.d.ts +1 -0
  128. package/dist/modes/rpc/session-extension-ui-requests.d.ts.map +1 -1
  129. package/dist/modes/rpc/session-extension-ui-requests.js +5 -0
  130. package/dist/modes/rpc/session-extension-ui-requests.js.map +1 -1
  131. package/dist/modes/rpc/session-registry.d.ts +10 -2
  132. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  133. package/dist/modes/rpc/session-registry.js +58 -4
  134. package/dist/modes/rpc/session-registry.js.map +1 -1
  135. package/dist/modes/rpc/socket-event-fanout.d.ts +22 -0
  136. package/dist/modes/rpc/socket-event-fanout.d.ts.map +1 -0
  137. package/dist/modes/rpc/socket-event-fanout.js +84 -0
  138. package/dist/modes/rpc/socket-event-fanout.js.map +1 -0
  139. package/docs/rpc.md +3 -1
  140. package/docs/settings.md +34 -0
  141. package/docs/windows.md +2 -0
  142. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +38 -0
  143. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  144. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +139 -90
  145. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +4 -4
  146. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
  147. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js +40 -20
  148. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
  149. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts +3 -3
  150. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  151. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.js.map +1 -1
  152. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.d.ts +1 -1
  153. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.d.ts.map +1 -1
  154. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js +28 -9
  155. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js.map +1 -1
  156. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  157. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +58 -31
  159. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts +20 -0
  161. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts.map +1 -0
  162. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js +443 -0
  163. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js.map +1 -0
  164. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +2 -0
  165. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  166. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +2 -0
  167. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  168. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  170. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -0
  171. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  172. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  173. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  182. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
  184. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  185. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  186. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  187. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +7 -0
  188. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  189. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +5 -0
  190. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  191. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts +1 -1
  192. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts.map +1 -1
  193. package/node_modules/@earendil-works/pi-tui/dist/mux.js +2 -2
  194. package/node_modules/@earendil-works/pi-tui/dist/mux.js.map +1 -1
  195. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +1 -0
  196. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  197. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +19 -0
  198. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  199. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  200. package/package.json +7 -7
@@ -14,10 +14,11 @@
14
14
  */
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { rm } from "node:fs/promises";
17
18
  import { basename, dirname } from "node:path";
18
19
  import { isDeepStrictEqual } from "node:util";
19
20
  import { ProviderRetryWatchdogAbortError, prepareAgentToolCall } from "@earendil-works/pi-agent-core";
20
- import { contentText, SERVER_FALLBACK_ABORTED_DIAGNOSTIC } from "@earendil-works/pi-ai";
21
+ import { contentText, measureCursorHistorySerializedBytes, SERVER_FALLBACK_ABORTED_DIAGNOSTIC, } from "@earendil-works/pi-ai";
21
22
  import { cleanupSessionResources, cursorOverflowCompactionSettings, isClassifierRefusal, isContextOverflow, isCursorPayloadResourceExhausted, isCursorQuotaResourceExhausted, isCursorZeroTokenResourceExhausted, isProviderStreamStallError, isProviderTimeoutError, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, shouldRetryOverflowWithoutCompact, streamSimple, } from "@earendil-works/pi-ai/compat";
22
23
  import { extract429RetryAfterMs, parseRetryAfterMsMarker } from "@earendil-works/pi-ai/utils/retry-hint";
23
24
  import { retryBackoffDelayMs } from "@earendil-works/pi-ai/utils/retry-profile/backoff";
@@ -47,7 +48,7 @@ import { ExecuteToolError, ExtensionRunner, wrapRegisteredTools, } from "./exten
47
48
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
48
49
  import { normalizeToolExposure, RUNTIME_EXTENSION_PATH } from "./extensions/types.js";
49
50
  import { shouldWarnHighReasoning } from "./high-reasoning-warning.js";
50
- import { filterContextExcludedMessages } from "./messages.js";
51
+ import { convertToLlm, filterContextExcludedMessages, } from "./messages.js";
51
52
  import { ModelRegistry } from "./model-registry.js";
52
53
  import { getModelNarrowingPatterns, resolveModelScope } from "./model-resolver.js";
53
54
  import { PROMPT_CACHE_SAFE_WAIT_ENV, resolvePromptCacheSafeWaitSeconds } from "./prompt-cache-budget.js";
@@ -70,9 +71,21 @@ import { getSupportedThinkingLevels, supportsMax, supportsXhigh } from "./thinki
70
71
  import { resetTimings, time } from "./timings.js";
71
72
  import { createLocalBashOperations } from "./tools/bash.js";
72
73
  import { composeFilesystemPolicies } from "./tools/filesystem-policy.js";
73
- import { createAllToolDefinitions } from "./tools/index.js";
74
+ import { createAllToolDefinitions, temporarilyDisabledToolNames } from "./tools/index.js";
74
75
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
75
76
  import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
77
+ /** Built-in shell tools routed exclusively through eval when experimental.bashEvalOnly is enabled. */
78
+ const EVAL_ONLY_SHELL_TOOL_NAMES = ["bash", "powershell"];
79
+ /** Workflow tool routed exclusively through eval when experimental.workflowEvalOnly is enabled. */
80
+ const EVAL_ONLY_WORKFLOW_TOOL_NAMES = ["workflow"];
81
+ /** Sample eval-cell call for an eval-only tool, using the argument name that tool actually takes. */
82
+ function evalHelperCall(name) {
83
+ if (EVAL_ONLY_SHELL_TOOL_NAMES.includes(name))
84
+ return `tool.${name}({ command: "..." })`;
85
+ if (EVAL_ONLY_WORKFLOW_TOOL_NAMES.includes(name))
86
+ return `tool.${name}({ action: "..." })`;
87
+ return `tool.${name}({ ... })`;
88
+ }
76
89
  const TURN_RETRY_SUPPRESSION_PREFIX = "senpi:no-turn-retry:";
77
90
  const DEFERRED_RETRY_QUEUE_OWNERS = new WeakSet();
78
91
  /** Format the user-attributed payload for one or more explicit skill invocations. */
@@ -310,6 +323,121 @@ function isSameOverflowSource(message, model, upstreamModelId) {
310
323
  const THINKING_LEVELS_WITH_MAX = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
311
324
  /** Caps explicit skill expansion so one prompt cannot consume unbounded context. */
312
325
  export const MAX_SKILL_EXPANSIONS_PER_PROMPT = 5;
326
+ /** Cursor ingest rejects large verbatim tool payloads. The bound is UTF-8 bytes. */
327
+ export const CURSOR_TOOL_RESULT_MAX_CHARS = 2000;
328
+ export const CURSOR_TOOL_RESULT_MAX_BYTES = 50_000;
329
+ const CURSOR_TRUNCATION_MARKER = "\n...[truncated]";
330
+ export function truncateToolResultBodies(messages, maxChars = CURSOR_TOOL_RESULT_MAX_CHARS, maxBytes = CURSOR_TOOL_RESULT_MAX_BYTES, convert = (candidate) => convertToLlm(candidate)) {
331
+ if (!Array.isArray(messages) || messages.length === 0)
332
+ return { messages, changed: false };
333
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
334
+ const markerChars = [...segmenter.segment(CURSOR_TRUNCATION_MARKER)].length;
335
+ const result = messages.slice();
336
+ let changed = false;
337
+ // Apply the per-result character cap first, independent of the aggregate wire cap.
338
+ for (let messageIndex = result.length - 1; messageIndex >= 0; messageIndex--) {
339
+ const message = result[messageIndex];
340
+ if (message.role !== "toolResult" || !Array.isArray(message.content))
341
+ continue;
342
+ let content = message.content;
343
+ for (let partIndex = content.length - 1; partIndex >= 0; partIndex--) {
344
+ const part = content[partIndex];
345
+ if (part.type === "image" && typeof part.data === "string")
346
+ continue;
347
+ if (part.type !== "text" || typeof part.text !== "string")
348
+ continue;
349
+ const graphemes = [...segmenter.segment(part.text)].map((item) => item.segment);
350
+ if (graphemes.length <= maxChars)
351
+ continue;
352
+ const kept = graphemes.slice(0, Math.max(0, maxChars - markerChars)).join("");
353
+ const nextText = kept + CURSOR_TRUNCATION_MARKER;
354
+ content = content === message.content ? content.slice() : content;
355
+ content[partIndex] = { ...part, text: nextText };
356
+ result[messageIndex] = { ...message, content };
357
+ changed = true;
358
+ }
359
+ }
360
+ const measure = (candidate) => {
361
+ const converted = convert(candidate);
362
+ const activeUserMessageIndex = converted.at(-1)?.role === "user" ? converted.length - 1 : -1;
363
+ return measureCursorHistorySerializedBytes(converted, activeUserMessageIndex);
364
+ };
365
+ const fits = (candidate = result) => measure(candidate) <= maxBytes;
366
+ if (fits())
367
+ return { messages: changed ? result : messages, changed };
368
+ // Empty the oldest result bodies. Search the monotonic prefix of candidates
369
+ // rather than serializing once for every result (admission must stay bounded).
370
+ const emptyToolResult = (message) => {
371
+ if (message.role !== "toolResult" || !Array.isArray(message.content))
372
+ return message;
373
+ const emptiedContent = message.content.map((part) => part.type === "text" ? { ...part, text: "" } : part.type === "image" ? { ...part, data: "" } : part);
374
+ const content = emptiedContent.filter((part, index) => {
375
+ if (index === 0)
376
+ return true;
377
+ const previous = emptiedContent[index - 1];
378
+ const empty = part.type === "text" ? part.text === "" : part.type === "image" && part.data === "";
379
+ const previousEmpty = previous.type === "text" ? previous.text === "" : previous.type === "image" && previous.data === "";
380
+ return !empty || !previousEmpty;
381
+ });
382
+ return { ...message, content };
383
+ };
384
+ const toolResultIndexes = result.flatMap((message, index) => message.role === "toolResult" && Array.isArray(message.content) ? [index] : []);
385
+ const withEmptyPrefix = (count) => {
386
+ const candidate = result.slice();
387
+ for (let i = 0; i < count; i++)
388
+ candidate[toolResultIndexes[i]] = emptyToolResult(candidate[toolResultIndexes[i]]);
389
+ return candidate;
390
+ };
391
+ let low = 0;
392
+ let high = toolResultIndexes.length;
393
+ while (low < high) {
394
+ const middle = Math.floor((low + high) / 2);
395
+ if (fits(withEmptyPrefix(middle + 1)))
396
+ high = middle;
397
+ else
398
+ low = middle + 1;
399
+ }
400
+ const emptyCount = Math.min(low + 1, toolResultIndexes.length);
401
+ if (emptyCount > 0) {
402
+ result.splice(0, result.length, ...withEmptyPrefix(emptyCount));
403
+ changed = true;
404
+ }
405
+ if (fits())
406
+ return { messages: changed ? result : messages, changed };
407
+ // If metadata alone exceeds the cap, discard the oldest complete turns. This
408
+ // search is also monotonic and avoids quadratic whole-history reserialization.
409
+ const turnRanges = [];
410
+ const isConvertedUser = (message) => convert([message])[0]?.role === "user";
411
+ for (let index = 0; index < result.length; index++) {
412
+ if (!isConvertedUser(result[index]))
413
+ continue;
414
+ const nextUser = result.findIndex((message, nextIndex) => nextIndex > index && isConvertedUser(message));
415
+ turnRanges.push([index, nextUser < 0 ? result.length : nextUser]);
416
+ }
417
+ const withoutTurns = (count) => {
418
+ if (count === 0)
419
+ return result;
420
+ const start = turnRanges[0]?.[0] ?? 0;
421
+ const end = turnRanges[count - 1]?.[1] ?? start;
422
+ return [...result.slice(0, start), ...result.slice(end)];
423
+ };
424
+ low = 0;
425
+ high = turnRanges.length;
426
+ while (low < high) {
427
+ const middle = Math.floor((low + high) / 2);
428
+ if (fits(withoutTurns(middle + 1)))
429
+ high = middle;
430
+ else
431
+ low = middle + 1;
432
+ }
433
+ const turnCount = Math.min(low + 1, turnRanges.length);
434
+ if (turnCount > 0) {
435
+ const next = withoutTurns(turnCount);
436
+ result.splice(0, result.length, ...next);
437
+ changed = true;
438
+ }
439
+ return { messages: changed ? result : messages, changed };
440
+ }
313
441
  // ============================================================================
314
442
  // AgentSession Class
315
443
  // ============================================================================
@@ -395,6 +523,10 @@ export class AgentSession {
395
523
  this._pendingBashMessages = [];
396
524
  this._turnIndex = 0;
397
525
  this._baseToolDefinitions = new Map();
526
+ /** Policy tools withheld from the model, retained so disarming can restore direct access. */
527
+ this._withheldEvalOnlyToolNames = new Set();
528
+ /** Eval-only hint names this session published, so a disarm can withdraw exactly those. */
529
+ this._publishedEvalOnlyHintNames = new Set();
398
530
  this._extensionMode = "print";
399
531
  // Tool registry for extension getTools/setTools
400
532
  this._toolRegistry = new Map();
@@ -516,6 +648,8 @@ export class AgentSession {
516
648
  this._extensionRunnerRef = config.extensionRunnerRef;
517
649
  this._initialActiveToolNames = config.initialActiveToolNames;
518
650
  this._defaultToolNames = config.defaultToolNames ? new Set(config.defaultToolNames) : undefined;
651
+ this._evalOnlyToolNamesOverride = config.evalOnlyToolNames ? new Set(config.evalOnlyToolNames) : undefined;
652
+ this._evalOnlyToolNames = this._resolveEvalOnlyToolNames();
519
653
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
520
654
  this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
521
655
  this._baseToolsOverride = config.baseToolsOverride;
@@ -727,6 +861,14 @@ export class AgentSession {
727
861
  (this.agent.prepareNextTurn
728
862
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
729
863
  : undefined);
864
+ const previousTransformContext = this.agent.transformContext;
865
+ this.agent.transformContext = async (messages, signal) => {
866
+ const transformed = previousTransformContext ? await previousTransformContext(messages, signal) : messages;
867
+ if (this.model?.provider === "cursor" || this.model?.provider === "cursor-cli-oauth") {
868
+ return ((await truncateToolResultBodies(transformed, CURSOR_TOOL_RESULT_MAX_CHARS, CURSOR_TOOL_RESULT_MAX_BYTES, (candidate) => this.agent.convertToLlm(candidate))).messages ?? transformed);
869
+ }
870
+ return transformed;
871
+ };
730
872
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
731
873
  // Enforce compaction only when this prepare precedes an actual provider
732
874
  // admission: a tool continuation or queued steer/follow-up messages. A
@@ -735,7 +877,8 @@ export class AgentSession {
735
877
  const compactBeforeNextAdmission = async () => {
736
878
  const provider = this.model?.provider;
737
879
  // Cursor rebuilds the full conversation each hop. Compacting here
738
- // mutates rootPrompt mid-run and poisons conversationId.
880
+ // mutates rootPrompt mid-run and poisons conversationId (#984).
881
+ // Still truncate verbatim toolResult bodies so the skip cannot send MB-scale payloads (#1043).
739
882
  if (provider === "cursor" || provider === "cursor-cli-oauth") {
740
883
  return false;
741
884
  }
@@ -816,6 +959,19 @@ export class AgentSession {
816
959
  // Event Subscription
817
960
  // =========================================================================
818
961
  /** Emit an event to all listeners */
962
+ _emitEntryAppended(entryId) {
963
+ if (this._extensionMode !== "rpc")
964
+ return;
965
+ const entry = this.sessionManager.getEntry(entryId);
966
+ if (entry)
967
+ this._emit({ type: "entry_appended", entry });
968
+ }
969
+ /** Append a transport-provided entry and publish it on the RPC event stream. */
970
+ appendSessionEntry(entry) {
971
+ this.sessionManager.appendEntry(entry);
972
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
973
+ this._emitEntryAppended(entry.id);
974
+ }
819
975
  _emit(event) {
820
976
  this._logSessionEvent(event);
821
977
  for (const l of this._eventListeners) {
@@ -976,6 +1132,7 @@ export class AgentSession {
976
1132
  _invalidateCompactionForModelSelection() {
977
1133
  this.abortCompaction();
978
1134
  this.abortBranchSummary();
1135
+ this._delegatedCompactionKey = undefined;
979
1136
  this._incrementMessageRevision();
980
1137
  }
981
1138
  _getIdleWaitPromise() {
@@ -1367,7 +1524,7 @@ export class AgentSession {
1367
1524
  event.message.role === "assistant" ||
1368
1525
  event.message.role === "toolResult") {
1369
1526
  // Regular LLM message - persist as SessionMessageEntry
1370
- this.sessionManager.appendMessage(event.message);
1527
+ this._emitEntryAppended(this.sessionManager.appendMessage(event.message));
1371
1528
  this._incrementMessageRevision();
1372
1529
  }
1373
1530
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
@@ -2001,6 +2158,9 @@ export class AgentSession {
2001
2158
  async executeTool(toolName, params, options) {
2002
2159
  let activeTools = this.getActiveToolNames();
2003
2160
  let tool = this.agent.state.tools.find((candidate) => candidate.name === toolName);
2161
+ if (!tool && this._isEvalOnlyPolicyArmed() && this._evalOnlyToolNames?.has(toolName)) {
2162
+ tool = this._toolRegistry.get(toolName);
2163
+ }
2004
2164
  if (!tool &&
2005
2165
  options?.activateInactiveTool === true &&
2006
2166
  this._toolDefinitions.has(toolName) &&
@@ -2075,6 +2235,45 @@ export class AgentSession {
2075
2235
  return false;
2076
2236
  return this._lazyToolActivators.some((activate) => activate(toolName));
2077
2237
  }
2238
+ _isEvalOnlyPolicyArmed() {
2239
+ return this._evalOnlyToolNames !== undefined && this._toolRegistry.has("eval");
2240
+ }
2241
+ /**
2242
+ * Resolve the eval-only policy from settings so a reload picks up a flag change.
2243
+ * An explicitly configured set stays authoritative for SDK embedders and tests.
2244
+ */
2245
+ _resolveEvalOnlyToolNames() {
2246
+ if (this._evalOnlyToolNamesOverride !== undefined)
2247
+ return this._evalOnlyToolNamesOverride;
2248
+ const names = [
2249
+ ...(this.settingsManager.getExperimentalBashEvalOnly() ? EVAL_ONLY_SHELL_TOOL_NAMES : []),
2250
+ ...(this.settingsManager.getExperimentalWorkflowEvalOnly() ? EVAL_ONLY_WORKFLOW_TOOL_NAMES : []),
2251
+ ];
2252
+ return names.length > 0 ? new Set(names) : undefined;
2253
+ }
2254
+ /**
2255
+ * Publish per-tool eval-only redirect hints so a direct call names its own eval helper.
2256
+ *
2257
+ * Hints published here are tracked so a disarm - or a shrinking armed set, e.g. bash+workflow
2258
+ * down to workflow only - withdraws the stale ones. Only tracked names are ever deleted, so
2259
+ * hints owned by other publishers survive.
2260
+ */
2261
+ _publishEvalOnlyToolHints() {
2262
+ const armed = this._isEvalOnlyPolicyArmed() ? (this._evalOnlyToolNames ?? new Set()) : undefined;
2263
+ for (const name of this._publishedEvalOnlyHintNames) {
2264
+ if (armed?.has(name))
2265
+ continue;
2266
+ delete this.agent.removedToolHints[name];
2267
+ }
2268
+ this._publishedEvalOnlyHintNames.clear();
2269
+ if (!armed)
2270
+ return;
2271
+ for (const name of armed) {
2272
+ this.agent.removedToolHints[name] =
2273
+ `Run ${name} inside an eval cell via ${evalHelperCall(name)}; hooks and permissions still apply.`;
2274
+ this._publishedEvalOnlyHintNames.add(name);
2275
+ }
2276
+ }
2078
2277
  /**
2079
2278
  * Set active tools by name.
2080
2279
  * Only tools in the registry can be enabled. Unknown tool names are ignored.
@@ -2102,7 +2301,23 @@ export class AgentSession {
2102
2301
  setActiveToolsByName(toolNames) {
2103
2302
  const tools = [];
2104
2303
  const validToolNames = [];
2105
- for (const name of toolNames) {
2304
+ const policyArmed = this._isEvalOnlyPolicyArmed();
2305
+ this._publishEvalOnlyToolHints();
2306
+ const policyNames = this._evalOnlyToolNames;
2307
+ let filteredToolNames = toolNames;
2308
+ if (policyArmed && policyNames) {
2309
+ for (const name of toolNames) {
2310
+ if (policyNames.has(name))
2311
+ this._withheldEvalOnlyToolNames.add(name);
2312
+ }
2313
+ filteredToolNames = toolNames.filter((name) => !policyNames.has(name));
2314
+ }
2315
+ else {
2316
+ this._withheldEvalOnlyToolNames.clear();
2317
+ }
2318
+ // Remember the unfiltered request so a later disarm can restore withheld shell tools.
2319
+ this._requestedActiveToolNames = [...new Set([...toolNames, ...this._withheldEvalOnlyToolNames])];
2320
+ for (const name of filteredToolNames) {
2106
2321
  const tool = this._toolRegistry.get(name);
2107
2322
  if (tool) {
2108
2323
  tools.push(tool);
@@ -2259,9 +2474,26 @@ export class AgentSession {
2259
2474
  appendSystemPrompt: loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined,
2260
2475
  };
2261
2476
  const basePrompt = loaderSystemPrompt ?? buildDynamicSystemPrompt(this._baseSystemPromptOptions);
2262
- return loaderAppendSystemPrompt.length > 0
2263
- ? `${basePrompt}\n\n${loaderAppendSystemPrompt.join("\n\n")}`
2264
- : basePrompt;
2477
+ const prompt = loaderAppendSystemPrompt.length > 0 ? `${basePrompt}\n\n${loaderAppendSystemPrompt.join("\n\n")}` : basePrompt;
2478
+ if (!this._isEvalOnlyPolicyArmed())
2479
+ return prompt;
2480
+ const armed = this._evalOnlyToolNames ?? new Set();
2481
+ const sentences = [];
2482
+ const shellHelpers = EVAL_ONLY_SHELL_TOOL_NAMES.filter((name) => armed.has(name)).map(evalHelperCall);
2483
+ if (shellHelpers.length > 0) {
2484
+ sentences.push(`Shell commands run ONLY inside eval cells via ${shellHelpers.join(" or ")}; hooks and permissions still apply.`);
2485
+ }
2486
+ if (armed.has("workflow")) {
2487
+ sentences.push(`The workflow tool runs ONLY inside eval cells via ${evalHelperCall("workflow")}; hooks and permissions still apply.`);
2488
+ }
2489
+ // SDK embedders can arm names outside the built-in groups; they still need eval guidance.
2490
+ const otherHelpers = [...armed]
2491
+ .filter((name) => !EVAL_ONLY_SHELL_TOOL_NAMES.includes(name) && name !== "workflow")
2492
+ .map(evalHelperCall);
2493
+ if (otherHelpers.length > 0) {
2494
+ sentences.push(`These tools run ONLY inside eval cells via ${otherHelpers.join(" or ")}; hooks and permissions still apply.`);
2495
+ }
2496
+ return sentences.length > 0 ? `${prompt}\n\n${sentences.join("\n\n")}` : prompt;
2265
2497
  }
2266
2498
  /**
2267
2499
  * Send a prompt to the agent.
@@ -3053,7 +3285,7 @@ export class AgentSession {
3053
3285
  }
3054
3286
  _appendCustomMessage(appMessage) {
3055
3287
  this.agent.state.messages.push(appMessage);
3056
- this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
3288
+ this._emitEntryAppended(this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details));
3057
3289
  this._incrementMessageRevision();
3058
3290
  this._emit({ type: "message_start", message: appMessage });
3059
3291
  this._emit({ type: "message_end", message: appMessage });
@@ -3336,7 +3568,10 @@ export class AgentSession {
3336
3568
  }
3337
3569
  async _switchActiveModel(model, opts) {
3338
3570
  const previousModel = this.model;
3339
- if (opts.invalidateCompaction && this._modelSelectionChangesContext(previousModel, model)) {
3571
+ if (opts.invalidateCompaction &&
3572
+ (this._modelSelectionChangesContext(previousModel, model) ||
3573
+ previousModel?.provider !== model.provider ||
3574
+ previousModel?.id !== model.id)) {
3340
3575
  this._invalidateCompactionForModelSelection();
3341
3576
  }
3342
3577
  const thinking = this._getThinkingForModelSwitch(model, opts.ephemeralThinkingLevel);
@@ -3408,7 +3643,9 @@ export class AgentSession {
3408
3643
  nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
3409
3644
  }
3410
3645
  const next = favoriteModels[nextIndex];
3411
- const invalidatesCompaction = this._modelSelectionChangesContext(currentModel, next.model);
3646
+ const invalidatesCompaction = this._modelSelectionChangesContext(currentModel, next.model) ||
3647
+ currentModel?.provider !== next.model.provider ||
3648
+ currentModel?.id !== next.model.id;
3412
3649
  if (invalidatesCompaction) {
3413
3650
  this._invalidateCompactionForModelSelection();
3414
3651
  }
@@ -3610,6 +3847,7 @@ export class AgentSession {
3610
3847
  setSteeringMode(mode) {
3611
3848
  this.agent.steeringMode = mode;
3612
3849
  this.settingsManager.setSteeringMode(mode);
3850
+ this._emitSessionSettingsChanged();
3613
3851
  }
3614
3852
  /**
3615
3853
  * Set follow-up message mode.
@@ -3618,6 +3856,7 @@ export class AgentSession {
3618
3856
  setFollowUpMode(mode) {
3619
3857
  this.agent.followUpMode = mode;
3620
3858
  this.settingsManager.setFollowUpMode(mode);
3859
+ this._emitSessionSettingsChanged();
3621
3860
  }
3622
3861
  // =========================================================================
3623
3862
  // Compaction
@@ -4100,7 +4339,7 @@ export class AgentSession {
4100
4339
  if (sourceChanged) {
4101
4340
  return await this._rejectCompaction(request, requestId, operationId, "stale-revision", false);
4102
4341
  }
4103
- if (this._wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model)) {
4342
+ if (await this._wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model)) {
4104
4343
  return await this._rejectCompaction(request, requestId, operationId, "would-overflow", false);
4105
4344
  }
4106
4345
  const compactionEntryId = this.sessionManager.appendCompaction(compactionResult.summary, compactionResult.firstKeptEntryId, compactionResult.tokensBefore, compactionResult.details, fromExtension, compactionResult.usage);
@@ -4141,6 +4380,7 @@ export class AgentSession {
4141
4380
  })) {
4142
4381
  throw new CompactionCancelledError();
4143
4382
  }
4383
+ this._delegatedCompactionKey = undefined;
4144
4384
  if (request.owner === "compaction" && this._compactionAbortController === request.controller) {
4145
4385
  this._compactionAbortController = undefined;
4146
4386
  }
@@ -4191,7 +4431,7 @@ export class AgentSession {
4191
4431
  finishCompactionWork();
4192
4432
  }
4193
4433
  }
4194
- _wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model) {
4434
+ async _wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model) {
4195
4435
  const currentLeaf = pathEntries[pathEntries.length - 1];
4196
4436
  if (!currentLeaf)
4197
4437
  return false;
@@ -4206,7 +4446,12 @@ export class AgentSession {
4206
4446
  details: compactionResult.details,
4207
4447
  fromHook: fromExtension,
4208
4448
  };
4209
- const simulatedMessages = buildSessionContext([...pathEntries, simulatedCompactionEntry], simulatedCompactionEntry.id).messages;
4449
+ let simulatedMessages = buildSessionContext([...pathEntries, simulatedCompactionEntry], simulatedCompactionEntry.id).messages;
4450
+ // Size the same retained context that will be admitted to Cursor. Persisted JSONL
4451
+ // remains verbatim, but the in-memory request representation is bounded first.
4452
+ if (model.provider === "cursor" || model.provider === "cursor-cli-oauth") {
4453
+ simulatedMessages = truncateToolResultBodies(simulatedMessages).messages ?? simulatedMessages;
4454
+ }
4210
4455
  const contextTokens = estimateMessagesTokens(filterContextExcludedMessages(simulatedMessages));
4211
4456
  const settings = this.settingsManager.getCompactionSettings();
4212
4457
  return contextTokens > model.contextWindow - settings.reserveTokens;
@@ -4237,6 +4482,9 @@ export class AgentSession {
4237
4482
  // also emitted with accepted:false so the compaction extension's circuit-breaker
4238
4483
  // bookkeeping stops being dead code; other builtin session_compact handlers guard
4239
4484
  // on event.accepted.
4485
+ if (rejectionCause === "external-owner" && this.model) {
4486
+ this._delegatedCompactionKey = { provider: this.model.provider, id: this.model.id };
4487
+ }
4240
4488
  const trimmedExtensionReason = extensionReason?.trim();
4241
4489
  const detailedMessage = trimmedExtensionReason
4242
4490
  ? `Compaction rejected: ${trimmedExtensionReason}`
@@ -4573,15 +4821,15 @@ export class AgentSession {
4573
4821
  return claimant !== undefined && !claimant.signal.aborted;
4574
4822
  }
4575
4823
  _isCompactionDelegated() {
4576
- const state = this._compactionLifecycle.state;
4577
4824
  const model = this.model;
4578
- return (state.status === "failed" &&
4579
- state.rejectionCause === "external-owner" &&
4580
- state.model !== undefined &&
4825
+ return (this._delegatedCompactionKey !== undefined &&
4581
4826
  model !== undefined &&
4582
- state.model.provider === model.provider);
4827
+ this._delegatedCompactionKey.provider === model.provider &&
4828
+ this._delegatedCompactionKey.id === model.id);
4583
4829
  }
4584
4830
  async _runPrePromptCompaction(lastAssistantMessage, skipAbortedCheck, reason = "pre_prompt", willRetry = false, allowSummaryOnly = false) {
4831
+ if (this._isCompactionDelegated())
4832
+ return false;
4585
4833
  const controller = new AbortController();
4586
4834
  const requestId = randomUUID();
4587
4835
  this._claimCompactionController(controller, "compaction");
@@ -4754,6 +5002,8 @@ export class AgentSession {
4754
5002
  * @returns Whether the post-run loop should call `agent.continue()`
4755
5003
  */
4756
5004
  async _runAutoCompaction(reason, willRetry) {
5005
+ if (this._isCompactionDelegated())
5006
+ return false;
4757
5007
  const finishCompactionWork = this._sessionWorkBarrier.begin();
4758
5008
  const agentMessagesAtStart = this.agent.state.messages.slice();
4759
5009
  const autoCompactionController = new AbortController();
@@ -4887,6 +5137,15 @@ export class AgentSession {
4887
5137
  */
4888
5138
  setAutoCompactionEnabled(enabled) {
4889
5139
  this.settingsManager.setCompactionEnabled(enabled);
5140
+ this._emitSessionSettingsChanged();
5141
+ }
5142
+ _emitSessionSettingsChanged() {
5143
+ this._emit({
5144
+ type: "session_settings_changed",
5145
+ steeringMode: this.steeringMode,
5146
+ followUpMode: this.followUpMode,
5147
+ autoCompactionEnabled: this.autoCompactionEnabled,
5148
+ });
4890
5149
  }
4891
5150
  /** Whether auto-compaction is enabled */
4892
5151
  get autoCompactionEnabled() {
@@ -4984,6 +5243,7 @@ export class AgentSession {
4984
5243
  : undefined;
4985
5244
  }
4986
5245
  _refreshCurrentModelFromRegistry() {
5246
+ this._delegatedCompactionKey = undefined;
4987
5247
  const currentModel = this.model;
4988
5248
  if (!currentModel) {
4989
5249
  return;
@@ -5308,8 +5568,11 @@ export class AgentSession {
5308
5568
  }),
5309
5569
  })),
5310
5570
  ].filter((tool) => isAllowedTool(tool.definition.name));
5571
+ // Withheld tools stay in _baseToolDefinitions (and therefore in _toolRegistry, which
5572
+ // getRegisteredTool serves to the Cursor exec bridge) but are dropped from the model-facing
5573
+ // definitions so they never reach the prompt. See temporarilyDisabledToolNames.
5311
5574
  const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
5312
- .filter(([name]) => isAllowedTool(name))
5575
+ .filter(([name]) => isAllowedTool(name) && !temporarilyDisabledToolNames.has(name))
5313
5576
  .map(([name, definition]) => [
5314
5577
  name,
5315
5578
  {
@@ -5351,13 +5614,22 @@ export class AgentSession {
5351
5614
  toolRegistry.set(tool.name, tool);
5352
5615
  }
5353
5616
  this._toolRegistry = toolRegistry;
5617
+ this._publishEvalOnlyToolHints();
5354
5618
  const isDirectlyExposed = (name) => {
5355
5619
  const entry = this._toolDefinitions.get(name);
5356
5620
  return entry !== undefined && normalizeToolExposure(entry.definition).exposure === "direct";
5357
5621
  };
5358
- const nextActiveToolNames = (options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]).filter((name) => {
5622
+ // A withheld tool is dropped from the DEFAULT selection only. An explicit activeToolNames
5623
+ // request names the tool deliberately, and callers that do so (tests, SDK embedders,
5624
+ // filesystem-policy wiring) still expect it to activate.
5625
+ const hasExplicitActiveToolNames = options?.activeToolNames !== undefined;
5626
+ const nextActiveToolNames = (options?.activeToolNames
5627
+ ? [...options.activeToolNames]
5628
+ : [...(this._requestedActiveToolNames ?? previousActiveToolNames)]).filter((name) => {
5359
5629
  if (!isAllowedTool(name))
5360
5630
  return false;
5631
+ if (!hasExplicitActiveToolNames && temporarilyDisabledToolNames.has(name))
5632
+ return false;
5361
5633
  const previousRegistrationIds = options?.previousActiveToolRegistrationIds;
5362
5634
  if (!previousRegistrationIds)
5363
5635
  return true;
@@ -5388,6 +5660,7 @@ export class AgentSession {
5388
5660
  this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
5389
5661
  }
5390
5662
  _buildRuntime(options) {
5663
+ this._delegatedCompactionKey = undefined;
5391
5664
  const autoResizeImages = this.settingsManager.getImageAutoResize();
5392
5665
  const shellCommandPrefix = this.settingsManager.getShellCommandPrefix();
5393
5666
  const shellPath = this.settingsManager.getShellPath();
@@ -5439,7 +5712,9 @@ export class AgentSession {
5439
5712
  const oldExtensionIdentities = oldExtensionRunner.getExtensionIdentities();
5440
5713
  const previousFlagValues = oldExtensionRunner.getFlagValues();
5441
5714
  const previousActiveToolRegistrationIds = new Map();
5442
- for (const name of this.getActiveToolNames()) {
5715
+ // Cover withheld eval-only tools too: the rebuild drops any seeded name missing from this
5716
+ // map, which would strand shell tools when the policy disarms during this reload.
5717
+ for (const name of this._requestedActiveToolNames ?? this.getActiveToolNames()) {
5443
5718
  const entry = this._toolDefinitions.get(name);
5444
5719
  if (entry)
5445
5720
  previousActiveToolRegistrationIds.set(name, deriveExtensionRegistrationId(entry.sourceInfo, name));
@@ -5450,6 +5725,12 @@ export class AgentSession {
5450
5725
  });
5451
5726
  time("shutdown", "reload");
5452
5727
  await this.settingsManager.reload();
5728
+ this._delegatedCompactionKey = undefined;
5729
+ // Capture the unfiltered request BEFORE the rebuild reassigns it, so a policy that
5730
+ // disarms during this reload can still restore its withheld shell tools.
5731
+ const requestedActiveToolNamesBeforeRebuild = [...(this._requestedActiveToolNames ?? this.getActiveToolNames())];
5732
+ // A flag change must take effect on reload, matching documented settings hot-reload behavior.
5733
+ this._evalOnlyToolNames = this._resolveEvalOnlyToolNames();
5453
5734
  this.syncQueueModesFromSettings();
5454
5735
  resetApiProviders();
5455
5736
  time("settings", "reload");
@@ -5474,7 +5755,7 @@ export class AgentSession {
5474
5755
  time("resources", "reload");
5475
5756
  try {
5476
5757
  this._buildRuntime({
5477
- activeToolNames: this.getActiveToolNames(),
5758
+ activeToolNames: requestedActiveToolNamesBeforeRebuild,
5478
5759
  flagValues: previousFlagValues,
5479
5760
  includeAllExtensionTools: true,
5480
5761
  previousActiveToolRegistrationIds,
@@ -6179,12 +6460,13 @@ export class AgentSession {
6179
6460
  try {
6180
6461
  const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath }), {
6181
6462
  onChunk: (delta) => {
6182
- onChunk?.(delta);
6463
+ const callbackResult = onChunk?.(delta);
6183
6464
  this._emit({
6184
6465
  type: "bash_execution_update",
6185
6466
  id: options?.id,
6186
6467
  delta,
6187
6468
  });
6469
+ return callbackResult;
6188
6470
  },
6189
6471
  signal: abortController.signal,
6190
6472
  });
@@ -6232,6 +6514,10 @@ export class AgentSession {
6232
6514
  abortController.abort();
6233
6515
  }
6234
6516
  }
6517
+ /** Remove a host-owned full-output spill after a remote observer failed. */
6518
+ async cleanupBashOutput(path) {
6519
+ await rm(path, { force: true });
6520
+ }
6235
6521
  /** Whether a bash command is currently running */
6236
6522
  get isBashRunning() {
6237
6523
  return this._bashAbortControllers.size > 0;
@@ -6433,6 +6719,7 @@ export class AgentSession {
6433
6719
  }
6434
6720
  // Update agent state (preserving exact messages still awaiting persistence)
6435
6721
  this._restoreAgentMessagesFromSession();
6722
+ this._delegatedCompactionKey = undefined;
6436
6723
  this._incrementMessageRevision();
6437
6724
  // Emit session_tree event
6438
6725
  await this._extensionRunner.emit({