@code-yeongyu/senpi 2026.8.28 → 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 (272) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session-services.d.ts.map +1 -1
  7. package/dist/core/agent-session-services.js +2 -0
  8. package/dist/core/agent-session-services.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +50 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +313 -25
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/auth-storage.d.ts +1 -0
  14. package/dist/core/auth-storage.d.ts.map +1 -1
  15. package/dist/core/auth-storage.js +3 -0
  16. package/dist/core/auth-storage.js.map +1 -1
  17. package/dist/core/bash-executor.d.ts.map +1 -1
  18. package/dist/core/bash-executor.js +155 -37
  19. package/dist/core/bash-executor.js.map +1 -1
  20. package/dist/core/credential-accounts.d.ts.map +1 -1
  21. package/dist/core/credential-accounts.js +45 -21
  22. package/dist/core/credential-accounts.js.map +1 -1
  23. package/dist/core/credential-pool/classify.d.ts +6 -1
  24. package/dist/core/credential-pool/classify.d.ts.map +1 -1
  25. package/dist/core/credential-pool/classify.js +22 -5
  26. package/dist/core/credential-pool/classify.js.map +1 -1
  27. package/dist/core/credential-pool/env-slots.d.ts.map +1 -1
  28. package/dist/core/credential-pool/env-slots.js +2 -0
  29. package/dist/core/credential-pool/env-slots.js.map +1 -1
  30. package/dist/core/credential-pool/failover.d.ts +8 -0
  31. package/dist/core/credential-pool/failover.d.ts.map +1 -1
  32. package/dist/core/credential-pool/failover.js +24 -6
  33. package/dist/core/credential-pool/failover.js.map +1 -1
  34. package/dist/core/credential-pool/rotation-stream.d.ts +13 -2
  35. package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -1
  36. package/dist/core/credential-pool/rotation-stream.js +97 -10
  37. package/dist/core/credential-pool/rotation-stream.js.map +1 -1
  38. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +8 -2
  39. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  40. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +160 -47
  41. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  42. package/dist/core/extensions/builtin/compaction/retained-message-safety.d.ts.map +1 -1
  43. package/dist/core/extensions/builtin/compaction/retained-message-safety.js +31 -8
  44. package/dist/core/extensions/builtin/compaction/retained-message-safety.js.map +1 -1
  45. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts.map +1 -1
  46. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js +8 -28
  47. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js.map +1 -1
  48. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  49. package/dist/core/extensions/builtin/goal/index.js +7 -0
  50. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  51. package/dist/core/extensions/builtin/permission-system/external-dir.d.ts.map +1 -1
  52. package/dist/core/extensions/builtin/permission-system/external-dir.js +15 -5
  53. package/dist/core/extensions/builtin/permission-system/external-dir.js.map +1 -1
  54. package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
  55. package/dist/core/extensions/builtin/permission-system/parsers.js +16 -1
  56. package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
  57. package/dist/core/extensions/builtin/service-tier.d.ts.map +1 -1
  58. package/dist/core/extensions/builtin/service-tier.js +13 -0
  59. package/dist/core/extensions/builtin/service-tier.js.map +1 -1
  60. package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
  61. package/dist/core/extensions/builtin/terminal/extension.js +4 -2
  62. package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
  63. package/dist/core/extensions/builtin/terminal/manager.d.ts +5 -0
  64. package/dist/core/extensions/builtin/terminal/manager.d.ts.map +1 -1
  65. package/dist/core/extensions/builtin/terminal/manager.js +31 -3
  66. package/dist/core/extensions/builtin/terminal/manager.js.map +1 -1
  67. package/dist/core/extensions/builtin/terminal/monitor-permission.d.ts +8 -0
  68. package/dist/core/extensions/builtin/terminal/monitor-permission.d.ts.map +1 -0
  69. package/dist/core/extensions/builtin/terminal/monitor-permission.js +16 -0
  70. package/dist/core/extensions/builtin/terminal/monitor-permission.js.map +1 -0
  71. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +13 -0
  72. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
  73. package/dist/core/extensions/builtin/terminal/monitor-registry.js +403 -3
  74. package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
  75. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts.map +1 -1
  76. package/dist/core/extensions/builtin/terminal/session-bundle.js +1 -0
  77. package/dist/core/extensions/builtin/terminal/session-bundle.js.map +1 -1
  78. package/dist/core/extensions/builtin/terminal/tools/kill-bash.d.ts.map +1 -1
  79. package/dist/core/extensions/builtin/terminal/tools/kill-bash.js +5 -2
  80. package/dist/core/extensions/builtin/terminal/tools/kill-bash.js.map +1 -1
  81. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +4 -0
  82. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts.map +1 -1
  83. package/dist/core/extensions/builtin/terminal/tools/monitor.js +40 -2
  84. package/dist/core/extensions/builtin/terminal/tools/monitor.js.map +1 -1
  85. package/dist/core/model-config-schema.d.ts +4 -4
  86. package/dist/core/model-resolver.js +2 -2
  87. package/dist/core/model-resolver.js.map +1 -1
  88. package/dist/core/model-runtime.d.ts +2 -0
  89. package/dist/core/model-runtime.d.ts.map +1 -1
  90. package/dist/core/model-runtime.js +74 -37
  91. package/dist/core/model-runtime.js.map +1 -1
  92. package/dist/core/retry-fallback/chains.d.ts +23 -1
  93. package/dist/core/retry-fallback/chains.d.ts.map +1 -1
  94. package/dist/core/retry-fallback/chains.js +59 -3
  95. package/dist/core/retry-fallback/chains.js.map +1 -1
  96. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  97. package/dist/core/retry-fallback/controller.js +9 -2
  98. package/dist/core/retry-fallback/controller.js.map +1 -1
  99. package/dist/core/retry-fallback/settings.d.ts.map +1 -1
  100. package/dist/core/retry-fallback/settings.js +9 -0
  101. package/dist/core/retry-fallback/settings.js.map +1 -1
  102. package/dist/core/retry-fallback/validate.js +1 -1
  103. package/dist/core/retry-fallback/validate.js.map +1 -1
  104. package/dist/core/sdk.js +1 -1
  105. package/dist/core/sdk.js.map +1 -1
  106. package/dist/core/session-manager.d.ts +9 -0
  107. package/dist/core/session-manager.d.ts.map +1 -1
  108. package/dist/core/session-manager.js +40 -0
  109. package/dist/core/session-manager.js.map +1 -1
  110. package/dist/core/settings-manager.d.ts +7 -0
  111. package/dist/core/settings-manager.d.ts.map +1 -1
  112. package/dist/core/settings-manager.js +6 -0
  113. package/dist/core/settings-manager.js.map +1 -1
  114. package/dist/core/tools/bash.d.ts.map +1 -1
  115. package/dist/core/tools/bash.js +97 -15
  116. package/dist/core/tools/bash.js.map +1 -1
  117. package/dist/core/tools/index.d.ts +1 -0
  118. package/dist/core/tools/index.d.ts.map +1 -1
  119. package/dist/core/tools/index.js +8 -0
  120. package/dist/core/tools/index.js.map +1 -1
  121. package/dist/core/tools/output-accumulator.d.ts +1 -1
  122. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  123. package/dist/core/tools/output-accumulator.js +76 -34
  124. package/dist/core/tools/output-accumulator.js.map +1 -1
  125. package/dist/main.d.ts +7 -0
  126. package/dist/main.d.ts.map +1 -1
  127. package/dist/main.js +10 -1
  128. package/dist/main.js.map +1 -1
  129. package/dist/modes/interactive/components/favorite-models-selector.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/favorite-models-selector.js +2 -2
  131. package/dist/modes/interactive/components/favorite-models-selector.js.map +1 -1
  132. package/dist/modes/interactive/components/footer.d.ts +7 -0
  133. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  134. package/dist/modes/interactive/components/footer.js +34 -8
  135. package/dist/modes/interactive/components/footer.js.map +1 -1
  136. package/dist/modes/interactive/grok/chrome.d.ts +2 -0
  137. package/dist/modes/interactive/grok/chrome.d.ts.map +1 -1
  138. package/dist/modes/interactive/grok/chrome.js.map +1 -1
  139. package/dist/modes/interactive/interactive-host-runtime.d.ts +51 -1
  140. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
  141. package/dist/modes/interactive/interactive-host-runtime.js +545 -40
  142. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
  143. package/dist/modes/interactive/interactive-mode.d.ts +7 -0
  144. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  145. package/dist/modes/interactive/interactive-mode.js +390 -239
  146. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  147. package/dist/modes/rpc/connection-handler.d.ts +8 -1
  148. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  149. package/dist/modes/rpc/connection-handler.js +268 -63
  150. package/dist/modes/rpc/connection-handler.js.map +1 -1
  151. package/dist/modes/rpc/host-ensure.d.ts +2 -0
  152. package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
  153. package/dist/modes/rpc/host-ensure.js +15 -2
  154. package/dist/modes/rpc/host-ensure.js.map +1 -1
  155. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  156. package/dist/modes/rpc/host-lifecycle.js +31 -12
  157. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  158. package/dist/modes/rpc/host-watchdog.d.ts +2 -0
  159. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
  160. package/dist/modes/rpc/host-watchdog.js +23 -12
  161. package/dist/modes/rpc/host-watchdog.js.map +1 -1
  162. package/dist/modes/rpc/multi-session-host.js +20 -8
  163. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  164. package/dist/modes/rpc/rpc-client.d.ts +91 -16
  165. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  166. package/dist/modes/rpc/rpc-client.js +129 -37
  167. package/dist/modes/rpc/rpc-client.js.map +1 -1
  168. package/dist/modes/rpc/rpc-input-validation.d.ts +1 -0
  169. package/dist/modes/rpc/rpc-input-validation.d.ts.map +1 -1
  170. package/dist/modes/rpc/rpc-input-validation.js +66 -0
  171. package/dist/modes/rpc/rpc-input-validation.js.map +1 -1
  172. package/dist/modes/rpc/rpc-mode.d.ts +3 -1
  173. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  174. package/dist/modes/rpc/rpc-mode.js +3 -1
  175. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  176. package/dist/modes/rpc/rpc-types.d.ts +241 -3
  177. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  178. package/dist/modes/rpc/rpc-types.js.map +1 -1
  179. package/dist/modes/rpc/session-binding.d.ts +1 -0
  180. package/dist/modes/rpc/session-binding.d.ts.map +1 -1
  181. package/dist/modes/rpc/session-binding.js +21 -1
  182. package/dist/modes/rpc/session-binding.js.map +1 -1
  183. package/dist/modes/rpc/session-command-router.d.ts +22 -0
  184. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  185. package/dist/modes/rpc/session-command-router.js +148 -16
  186. package/dist/modes/rpc/session-command-router.js.map +1 -1
  187. package/dist/modes/rpc/session-event-writer.d.ts +4 -0
  188. package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
  189. package/dist/modes/rpc/session-event-writer.js +58 -7
  190. package/dist/modes/rpc/session-event-writer.js.map +1 -1
  191. package/dist/modes/rpc/session-extension-ui-requests.d.ts +1 -0
  192. package/dist/modes/rpc/session-extension-ui-requests.d.ts.map +1 -1
  193. package/dist/modes/rpc/session-extension-ui-requests.js +5 -0
  194. package/dist/modes/rpc/session-extension-ui-requests.js.map +1 -1
  195. package/dist/modes/rpc/session-registry.d.ts +17 -2
  196. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  197. package/dist/modes/rpc/session-registry.js +67 -4
  198. package/dist/modes/rpc/session-registry.js.map +1 -1
  199. package/dist/modes/rpc/socket-event-fanout.d.ts +22 -0
  200. package/dist/modes/rpc/socket-event-fanout.d.ts.map +1 -0
  201. package/dist/modes/rpc/socket-event-fanout.js +84 -0
  202. package/dist/modes/rpc/socket-event-fanout.js.map +1 -0
  203. package/docs/rpc.md +40 -1
  204. package/docs/settings.md +34 -0
  205. package/docs/windows.md +2 -0
  206. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +62 -0
  207. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  208. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +139 -90
  209. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +4 -4
  210. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  211. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +6 -3
  212. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  213. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
  214. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js +40 -20
  215. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
  216. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts +3 -3
  217. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  218. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.js.map +1 -1
  219. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.d.ts +1 -1
  220. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.d.ts.map +1 -1
  221. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js +28 -9
  222. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js.map +1 -1
  223. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  224. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  225. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +58 -31
  226. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  227. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts +20 -0
  228. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts.map +1 -0
  229. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js +443 -0
  230. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js.map +1 -0
  231. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +2 -0
  232. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  233. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +2 -0
  234. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  235. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +1 -1
  236. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  237. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -0
  238. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  239. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  240. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  241. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  242. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  243. package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
  244. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  245. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  246. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  247. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  248. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  249. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  250. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  251. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  252. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  253. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  254. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
  255. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
  256. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  257. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  258. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  259. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +7 -0
  260. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  261. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +5 -0
  262. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  263. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts +1 -1
  264. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts.map +1 -1
  265. package/node_modules/@earendil-works/pi-tui/dist/mux.js +2 -2
  266. package/node_modules/@earendil-works/pi-tui/dist/mux.js.map +1 -1
  267. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +1 -0
  268. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  269. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +19 -0
  270. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  271. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  272. 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) {
@@ -934,6 +1090,7 @@ export class AgentSession {
934
1090
  type: "queue_update",
935
1091
  steering: [...this._steeringMessages],
936
1092
  followUp: [...this._followUpMessages],
1093
+ ordered: [...this._queuedInputOrder].sort((a, b) => a.enqueueOrder - b.enqueueOrder),
937
1094
  });
938
1095
  }
939
1096
  _incrementMessageRevision() {
@@ -975,6 +1132,7 @@ export class AgentSession {
975
1132
  _invalidateCompactionForModelSelection() {
976
1133
  this.abortCompaction();
977
1134
  this.abortBranchSummary();
1135
+ this._delegatedCompactionKey = undefined;
978
1136
  this._incrementMessageRevision();
979
1137
  }
980
1138
  _getIdleWaitPromise() {
@@ -1366,7 +1524,7 @@ export class AgentSession {
1366
1524
  event.message.role === "assistant" ||
1367
1525
  event.message.role === "toolResult") {
1368
1526
  // Regular LLM message - persist as SessionMessageEntry
1369
- this.sessionManager.appendMessage(event.message);
1527
+ this._emitEntryAppended(this.sessionManager.appendMessage(event.message));
1370
1528
  this._incrementMessageRevision();
1371
1529
  }
1372
1530
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
@@ -2000,6 +2158,9 @@ export class AgentSession {
2000
2158
  async executeTool(toolName, params, options) {
2001
2159
  let activeTools = this.getActiveToolNames();
2002
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
+ }
2003
2164
  if (!tool &&
2004
2165
  options?.activateInactiveTool === true &&
2005
2166
  this._toolDefinitions.has(toolName) &&
@@ -2074,6 +2235,45 @@ export class AgentSession {
2074
2235
  return false;
2075
2236
  return this._lazyToolActivators.some((activate) => activate(toolName));
2076
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
+ }
2077
2277
  /**
2078
2278
  * Set active tools by name.
2079
2279
  * Only tools in the registry can be enabled. Unknown tool names are ignored.
@@ -2101,7 +2301,23 @@ export class AgentSession {
2101
2301
  setActiveToolsByName(toolNames) {
2102
2302
  const tools = [];
2103
2303
  const validToolNames = [];
2104
- 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) {
2105
2321
  const tool = this._toolRegistry.get(name);
2106
2322
  if (tool) {
2107
2323
  tools.push(tool);
@@ -2258,9 +2474,26 @@ export class AgentSession {
2258
2474
  appendSystemPrompt: loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined,
2259
2475
  };
2260
2476
  const basePrompt = loaderSystemPrompt ?? buildDynamicSystemPrompt(this._baseSystemPromptOptions);
2261
- return loaderAppendSystemPrompt.length > 0
2262
- ? `${basePrompt}\n\n${loaderAppendSystemPrompt.join("\n\n")}`
2263
- : 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;
2264
2497
  }
2265
2498
  /**
2266
2499
  * Send a prompt to the agent.
@@ -3052,7 +3285,7 @@ export class AgentSession {
3052
3285
  }
3053
3286
  _appendCustomMessage(appMessage) {
3054
3287
  this.agent.state.messages.push(appMessage);
3055
- 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));
3056
3289
  this._incrementMessageRevision();
3057
3290
  this._emit({ type: "message_start", message: appMessage });
3058
3291
  this._emit({ type: "message_end", message: appMessage });
@@ -3335,7 +3568,10 @@ export class AgentSession {
3335
3568
  }
3336
3569
  async _switchActiveModel(model, opts) {
3337
3570
  const previousModel = this.model;
3338
- 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)) {
3339
3575
  this._invalidateCompactionForModelSelection();
3340
3576
  }
3341
3577
  const thinking = this._getThinkingForModelSwitch(model, opts.ephemeralThinkingLevel);
@@ -3407,7 +3643,9 @@ export class AgentSession {
3407
3643
  nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
3408
3644
  }
3409
3645
  const next = favoriteModels[nextIndex];
3410
- 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;
3411
3649
  if (invalidatesCompaction) {
3412
3650
  this._invalidateCompactionForModelSelection();
3413
3651
  }
@@ -3609,6 +3847,7 @@ export class AgentSession {
3609
3847
  setSteeringMode(mode) {
3610
3848
  this.agent.steeringMode = mode;
3611
3849
  this.settingsManager.setSteeringMode(mode);
3850
+ this._emitSessionSettingsChanged();
3612
3851
  }
3613
3852
  /**
3614
3853
  * Set follow-up message mode.
@@ -3617,6 +3856,7 @@ export class AgentSession {
3617
3856
  setFollowUpMode(mode) {
3618
3857
  this.agent.followUpMode = mode;
3619
3858
  this.settingsManager.setFollowUpMode(mode);
3859
+ this._emitSessionSettingsChanged();
3620
3860
  }
3621
3861
  // =========================================================================
3622
3862
  // Compaction
@@ -4099,7 +4339,7 @@ export class AgentSession {
4099
4339
  if (sourceChanged) {
4100
4340
  return await this._rejectCompaction(request, requestId, operationId, "stale-revision", false);
4101
4341
  }
4102
- if (this._wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model)) {
4342
+ if (await this._wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model)) {
4103
4343
  return await this._rejectCompaction(request, requestId, operationId, "would-overflow", false);
4104
4344
  }
4105
4345
  const compactionEntryId = this.sessionManager.appendCompaction(compactionResult.summary, compactionResult.firstKeptEntryId, compactionResult.tokensBefore, compactionResult.details, fromExtension, compactionResult.usage);
@@ -4140,6 +4380,7 @@ export class AgentSession {
4140
4380
  })) {
4141
4381
  throw new CompactionCancelledError();
4142
4382
  }
4383
+ this._delegatedCompactionKey = undefined;
4143
4384
  if (request.owner === "compaction" && this._compactionAbortController === request.controller) {
4144
4385
  this._compactionAbortController = undefined;
4145
4386
  }
@@ -4190,7 +4431,7 @@ export class AgentSession {
4190
4431
  finishCompactionWork();
4191
4432
  }
4192
4433
  }
4193
- _wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model) {
4434
+ async _wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model) {
4194
4435
  const currentLeaf = pathEntries[pathEntries.length - 1];
4195
4436
  if (!currentLeaf)
4196
4437
  return false;
@@ -4205,7 +4446,12 @@ export class AgentSession {
4205
4446
  details: compactionResult.details,
4206
4447
  fromHook: fromExtension,
4207
4448
  };
4208
- 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
+ }
4209
4455
  const contextTokens = estimateMessagesTokens(filterContextExcludedMessages(simulatedMessages));
4210
4456
  const settings = this.settingsManager.getCompactionSettings();
4211
4457
  return contextTokens > model.contextWindow - settings.reserveTokens;
@@ -4236,6 +4482,9 @@ export class AgentSession {
4236
4482
  // also emitted with accepted:false so the compaction extension's circuit-breaker
4237
4483
  // bookkeeping stops being dead code; other builtin session_compact handlers guard
4238
4484
  // on event.accepted.
4485
+ if (rejectionCause === "external-owner" && this.model) {
4486
+ this._delegatedCompactionKey = { provider: this.model.provider, id: this.model.id };
4487
+ }
4239
4488
  const trimmedExtensionReason = extensionReason?.trim();
4240
4489
  const detailedMessage = trimmedExtensionReason
4241
4490
  ? `Compaction rejected: ${trimmedExtensionReason}`
@@ -4572,15 +4821,15 @@ export class AgentSession {
4572
4821
  return claimant !== undefined && !claimant.signal.aborted;
4573
4822
  }
4574
4823
  _isCompactionDelegated() {
4575
- const state = this._compactionLifecycle.state;
4576
4824
  const model = this.model;
4577
- return (state.status === "failed" &&
4578
- state.rejectionCause === "external-owner" &&
4579
- state.model !== undefined &&
4825
+ return (this._delegatedCompactionKey !== undefined &&
4580
4826
  model !== undefined &&
4581
- state.model.provider === model.provider);
4827
+ this._delegatedCompactionKey.provider === model.provider &&
4828
+ this._delegatedCompactionKey.id === model.id);
4582
4829
  }
4583
4830
  async _runPrePromptCompaction(lastAssistantMessage, skipAbortedCheck, reason = "pre_prompt", willRetry = false, allowSummaryOnly = false) {
4831
+ if (this._isCompactionDelegated())
4832
+ return false;
4584
4833
  const controller = new AbortController();
4585
4834
  const requestId = randomUUID();
4586
4835
  this._claimCompactionController(controller, "compaction");
@@ -4753,6 +5002,8 @@ export class AgentSession {
4753
5002
  * @returns Whether the post-run loop should call `agent.continue()`
4754
5003
  */
4755
5004
  async _runAutoCompaction(reason, willRetry) {
5005
+ if (this._isCompactionDelegated())
5006
+ return false;
4756
5007
  const finishCompactionWork = this._sessionWorkBarrier.begin();
4757
5008
  const agentMessagesAtStart = this.agent.state.messages.slice();
4758
5009
  const autoCompactionController = new AbortController();
@@ -4886,6 +5137,15 @@ export class AgentSession {
4886
5137
  */
4887
5138
  setAutoCompactionEnabled(enabled) {
4888
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
+ });
4889
5149
  }
4890
5150
  /** Whether auto-compaction is enabled */
4891
5151
  get autoCompactionEnabled() {
@@ -4983,6 +5243,7 @@ export class AgentSession {
4983
5243
  : undefined;
4984
5244
  }
4985
5245
  _refreshCurrentModelFromRegistry() {
5246
+ this._delegatedCompactionKey = undefined;
4986
5247
  const currentModel = this.model;
4987
5248
  if (!currentModel) {
4988
5249
  return;
@@ -5307,8 +5568,11 @@ export class AgentSession {
5307
5568
  }),
5308
5569
  })),
5309
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.
5310
5574
  const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
5311
- .filter(([name]) => isAllowedTool(name))
5575
+ .filter(([name]) => isAllowedTool(name) && !temporarilyDisabledToolNames.has(name))
5312
5576
  .map(([name, definition]) => [
5313
5577
  name,
5314
5578
  {
@@ -5350,13 +5614,22 @@ export class AgentSession {
5350
5614
  toolRegistry.set(tool.name, tool);
5351
5615
  }
5352
5616
  this._toolRegistry = toolRegistry;
5617
+ this._publishEvalOnlyToolHints();
5353
5618
  const isDirectlyExposed = (name) => {
5354
5619
  const entry = this._toolDefinitions.get(name);
5355
5620
  return entry !== undefined && normalizeToolExposure(entry.definition).exposure === "direct";
5356
5621
  };
5357
- 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) => {
5358
5629
  if (!isAllowedTool(name))
5359
5630
  return false;
5631
+ if (!hasExplicitActiveToolNames && temporarilyDisabledToolNames.has(name))
5632
+ return false;
5360
5633
  const previousRegistrationIds = options?.previousActiveToolRegistrationIds;
5361
5634
  if (!previousRegistrationIds)
5362
5635
  return true;
@@ -5387,6 +5660,7 @@ export class AgentSession {
5387
5660
  this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
5388
5661
  }
5389
5662
  _buildRuntime(options) {
5663
+ this._delegatedCompactionKey = undefined;
5390
5664
  const autoResizeImages = this.settingsManager.getImageAutoResize();
5391
5665
  const shellCommandPrefix = this.settingsManager.getShellCommandPrefix();
5392
5666
  const shellPath = this.settingsManager.getShellPath();
@@ -5438,7 +5712,9 @@ export class AgentSession {
5438
5712
  const oldExtensionIdentities = oldExtensionRunner.getExtensionIdentities();
5439
5713
  const previousFlagValues = oldExtensionRunner.getFlagValues();
5440
5714
  const previousActiveToolRegistrationIds = new Map();
5441
- 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()) {
5442
5718
  const entry = this._toolDefinitions.get(name);
5443
5719
  if (entry)
5444
5720
  previousActiveToolRegistrationIds.set(name, deriveExtensionRegistrationId(entry.sourceInfo, name));
@@ -5449,6 +5725,12 @@ export class AgentSession {
5449
5725
  });
5450
5726
  time("shutdown", "reload");
5451
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();
5452
5734
  this.syncQueueModesFromSettings();
5453
5735
  resetApiProviders();
5454
5736
  time("settings", "reload");
@@ -5473,7 +5755,7 @@ export class AgentSession {
5473
5755
  time("resources", "reload");
5474
5756
  try {
5475
5757
  this._buildRuntime({
5476
- activeToolNames: this.getActiveToolNames(),
5758
+ activeToolNames: requestedActiveToolNamesBeforeRebuild,
5477
5759
  flagValues: previousFlagValues,
5478
5760
  includeAllExtensionTools: true,
5479
5761
  previousActiveToolRegistrationIds,
@@ -6178,12 +6460,13 @@ export class AgentSession {
6178
6460
  try {
6179
6461
  const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath }), {
6180
6462
  onChunk: (delta) => {
6181
- onChunk?.(delta);
6463
+ const callbackResult = onChunk?.(delta);
6182
6464
  this._emit({
6183
6465
  type: "bash_execution_update",
6184
6466
  id: options?.id,
6185
6467
  delta,
6186
6468
  });
6469
+ return callbackResult;
6187
6470
  },
6188
6471
  signal: abortController.signal,
6189
6472
  });
@@ -6231,6 +6514,10 @@ export class AgentSession {
6231
6514
  abortController.abort();
6232
6515
  }
6233
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
+ }
6234
6521
  /** Whether a bash command is currently running */
6235
6522
  get isBashRunning() {
6236
6523
  return this._bashAbortControllers.size > 0;
@@ -6432,6 +6719,7 @@ export class AgentSession {
6432
6719
  }
6433
6720
  // Update agent state (preserving exact messages still awaiting persistence)
6434
6721
  this._restoreAgentMessagesFromSession();
6722
+ this._delegatedCompactionKey = undefined;
6435
6723
  this._incrementMessageRevision();
6436
6724
  // Emit session_tree event
6437
6725
  await this._extensionRunner.emit({