open-multi-agent-kit 0.95.1 → 0.96.0

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 (306) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +16 -1
  3. package/dist/approvals/approval-receipt-store.d.ts +18 -0
  4. package/dist/approvals/approval-receipt-store.d.ts.map +1 -0
  5. package/dist/approvals/approval-receipt-store.js +194 -0
  6. package/dist/approvals/approval-receipt-store.js.map +1 -0
  7. package/dist/approvals/approval-receipt.d.ts +43 -0
  8. package/dist/approvals/approval-receipt.d.ts.map +1 -0
  9. package/dist/approvals/approval-receipt.js +208 -0
  10. package/dist/approvals/approval-receipt.js.map +1 -0
  11. package/dist/approvals/approval-verifier.d.ts +17 -0
  12. package/dist/approvals/approval-verifier.d.ts.map +1 -0
  13. package/dist/approvals/approval-verifier.js +23 -0
  14. package/dist/approvals/approval-verifier.js.map +1 -0
  15. package/dist/approvals/plannotator-approval-bridge.d.ts +58 -0
  16. package/dist/approvals/plannotator-approval-bridge.d.ts.map +1 -0
  17. package/dist/approvals/plannotator-approval-bridge.js +169 -0
  18. package/dist/approvals/plannotator-approval-bridge.js.map +1 -0
  19. package/dist/cli/args.d.ts.map +1 -1
  20. package/dist/cli/args.js +2 -0
  21. package/dist/cli/args.js.map +1 -1
  22. package/dist/commands/package-doctor-cli.d.ts +13 -0
  23. package/dist/commands/package-doctor-cli.d.ts.map +1 -0
  24. package/dist/commands/package-doctor-cli.js +80 -0
  25. package/dist/commands/package-doctor-cli.js.map +1 -0
  26. package/dist/commands/router-feedback-cli.d.ts +11 -0
  27. package/dist/commands/router-feedback-cli.d.ts.map +1 -0
  28. package/dist/commands/router-feedback-cli.js +99 -0
  29. package/dist/commands/router-feedback-cli.js.map +1 -0
  30. package/dist/commands/stats-cli.d.ts +20 -0
  31. package/dist/commands/stats-cli.d.ts.map +1 -0
  32. package/dist/commands/stats-cli.js +99 -0
  33. package/dist/commands/stats-cli.js.map +1 -0
  34. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  35. package/dist/core/agent-session-runtime.js +4 -1
  36. package/dist/core/agent-session-runtime.js.map +1 -1
  37. package/dist/core/agent-session.d.ts +99 -31
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +624 -216
  40. package/dist/core/agent-session.js.map +1 -1
  41. package/dist/core/bash-executor.d.ts.map +1 -1
  42. package/dist/core/bash-executor.js +8 -2
  43. package/dist/core/bash-executor.js.map +1 -1
  44. package/dist/core/compaction/transaction.d.ts +9 -0
  45. package/dist/core/compaction/transaction.d.ts.map +1 -1
  46. package/dist/core/compaction/transaction.js +17 -2
  47. package/dist/core/compaction/transaction.js.map +1 -1
  48. package/dist/core/context-budget-governor-v2.d.ts +1 -1
  49. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  50. package/dist/core/context-budget-governor-v2.js +1 -1
  51. package/dist/core/context-budget-governor-v2.js.map +1 -1
  52. package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
  53. package/dist/core/context-budget-system-prompt.js +18 -2
  54. package/dist/core/context-budget-system-prompt.js.map +1 -1
  55. package/dist/core/context-budget-v2-cache-disk.d.ts +109 -0
  56. package/dist/core/context-budget-v2-cache-disk.d.ts.map +1 -0
  57. package/dist/core/context-budget-v2-cache-disk.js +343 -0
  58. package/dist/core/context-budget-v2-cache-disk.js.map +1 -0
  59. package/dist/core/context-budget-v2-cache-keys.d.ts +22 -0
  60. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  61. package/dist/core/context-budget-v2-cache-keys.js +4 -4
  62. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  63. package/dist/core/context-budget-v2-cache.d.ts +1 -0
  64. package/dist/core/context-budget-v2-cache.d.ts.map +1 -1
  65. package/dist/core/context-budget-v2-cache.js +1 -0
  66. package/dist/core/context-budget-v2-cache.js.map +1 -1
  67. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  68. package/dist/core/context-budget-v2-planner.js +17 -14
  69. package/dist/core/context-budget-v2-planner.js.map +1 -1
  70. package/dist/core/context-budget-v2-types.d.ts +1 -0
  71. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  72. package/dist/core/context-budget-v2-types.js.map +1 -1
  73. package/dist/core/extensions/loader.d.ts.map +1 -1
  74. package/dist/core/extensions/loader.js +45 -22
  75. package/dist/core/extensions/loader.js.map +1 -1
  76. package/dist/core/extensions/types.d.ts +4 -0
  77. package/dist/core/extensions/types.d.ts.map +1 -1
  78. package/dist/core/extensions/types.js.map +1 -1
  79. package/dist/core/mcp/client.d.ts +105 -0
  80. package/dist/core/mcp/client.d.ts.map +1 -0
  81. package/dist/core/mcp/client.js +198 -0
  82. package/dist/core/mcp/client.js.map +1 -0
  83. package/dist/core/mcp/config.d.ts +23 -0
  84. package/dist/core/mcp/config.d.ts.map +1 -0
  85. package/dist/core/mcp/config.js +89 -0
  86. package/dist/core/mcp/config.js.map +1 -0
  87. package/dist/core/mcp/index.d.ts +15 -0
  88. package/dist/core/mcp/index.d.ts.map +1 -0
  89. package/dist/core/mcp/index.js +15 -0
  90. package/dist/core/mcp/index.js.map +1 -0
  91. package/dist/core/mcp/manager.d.ts +84 -0
  92. package/dist/core/mcp/manager.d.ts.map +1 -0
  93. package/dist/core/mcp/manager.js +173 -0
  94. package/dist/core/mcp/manager.js.map +1 -0
  95. package/dist/core/mcp/protocol.d.ts +59 -0
  96. package/dist/core/mcp/protocol.d.ts.map +1 -0
  97. package/dist/core/mcp/protocol.js +103 -0
  98. package/dist/core/mcp/protocol.js.map +1 -0
  99. package/dist/core/mcp/stdio-transport.d.ts +63 -0
  100. package/dist/core/mcp/stdio-transport.d.ts.map +1 -0
  101. package/dist/core/mcp/stdio-transport.js +132 -0
  102. package/dist/core/mcp/stdio-transport.js.map +1 -0
  103. package/dist/core/mcp/tools.d.ts +47 -0
  104. package/dist/core/mcp/tools.d.ts.map +1 -0
  105. package/dist/core/mcp/tools.js +102 -0
  106. package/dist/core/mcp/tools.js.map +1 -0
  107. package/dist/core/package-archive.d.ts +3 -0
  108. package/dist/core/package-archive.d.ts.map +1 -0
  109. package/dist/core/package-archive.js +161 -0
  110. package/dist/core/package-archive.js.map +1 -0
  111. package/dist/core/package-doctor-source-scan.d.ts +13 -0
  112. package/dist/core/package-doctor-source-scan.d.ts.map +1 -0
  113. package/dist/core/package-doctor-source-scan.js +124 -0
  114. package/dist/core/package-doctor-source-scan.js.map +1 -0
  115. package/dist/core/package-doctor.d.ts +41 -0
  116. package/dist/core/package-doctor.d.ts.map +1 -0
  117. package/dist/core/package-doctor.js +145 -0
  118. package/dist/core/package-doctor.js.map +1 -0
  119. package/dist/core/package-manager.d.ts +10 -1
  120. package/dist/core/package-manager.d.ts.map +1 -1
  121. package/dist/core/package-manager.js +175 -42
  122. package/dist/core/package-manager.js.map +1 -1
  123. package/dist/core/package-manifest.d.ts +26 -0
  124. package/dist/core/package-manifest.d.ts.map +1 -0
  125. package/dist/core/package-manifest.js +85 -0
  126. package/dist/core/package-manifest.js.map +1 -0
  127. package/dist/core/pi-compat.d.ts +5 -0
  128. package/dist/core/pi-compat.d.ts.map +1 -0
  129. package/dist/core/pi-compat.js +18 -0
  130. package/dist/core/pi-compat.js.map +1 -0
  131. package/dist/core/provider-resilience.d.ts +7 -0
  132. package/dist/core/provider-resilience.d.ts.map +1 -1
  133. package/dist/core/provider-resilience.js +11 -0
  134. package/dist/core/provider-resilience.js.map +1 -1
  135. package/dist/core/reasoning-router-bias.d.ts +10 -14
  136. package/dist/core/reasoning-router-bias.d.ts.map +1 -1
  137. package/dist/core/reasoning-router-bias.js +22 -15
  138. package/dist/core/reasoning-router-bias.js.map +1 -1
  139. package/dist/core/reasoning-router-v4.d.ts +9 -1
  140. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  141. package/dist/core/reasoning-router-v4.js +20 -5
  142. package/dist/core/reasoning-router-v4.js.map +1 -1
  143. package/dist/core/redaction.d.ts +2 -5
  144. package/dist/core/redaction.d.ts.map +1 -1
  145. package/dist/core/redaction.js +14 -1
  146. package/dist/core/redaction.js.map +1 -1
  147. package/dist/core/repository-learning-scope.d.ts +10 -0
  148. package/dist/core/repository-learning-scope.d.ts.map +1 -0
  149. package/dist/core/repository-learning-scope.js +42 -0
  150. package/dist/core/repository-learning-scope.js.map +1 -0
  151. package/dist/core/resource-loader.d.ts.map +1 -1
  152. package/dist/core/resource-loader.js +16 -1
  153. package/dist/core/resource-loader.js.map +1 -1
  154. package/dist/core/router-feedback-collector.d.ts +2 -1
  155. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  156. package/dist/core/router-feedback-collector.js +2 -2
  157. package/dist/core/router-feedback-collector.js.map +1 -1
  158. package/dist/core/runtime-provenance.d.ts +54 -0
  159. package/dist/core/runtime-provenance.d.ts.map +1 -0
  160. package/dist/core/runtime-provenance.js +115 -0
  161. package/dist/core/runtime-provenance.js.map +1 -0
  162. package/dist/core/sandbox/backend.d.ts.map +1 -1
  163. package/dist/core/sandbox/backend.js +39 -21
  164. package/dist/core/sandbox/backend.js.map +1 -1
  165. package/dist/core/sandbox/default-policy.d.ts +4 -5
  166. package/dist/core/sandbox/default-policy.d.ts.map +1 -1
  167. package/dist/core/sandbox/default-policy.js +9 -11
  168. package/dist/core/sandbox/default-policy.js.map +1 -1
  169. package/dist/core/session-bash-runtime.d.ts +4 -4
  170. package/dist/core/session-bash-runtime.d.ts.map +1 -1
  171. package/dist/core/session-bash-runtime.js +6 -5
  172. package/dist/core/session-bash-runtime.js.map +1 -1
  173. package/dist/core/session-compaction-service.d.ts.map +1 -1
  174. package/dist/core/session-compaction-service.js +7 -7
  175. package/dist/core/session-compaction-service.js.map +1 -1
  176. package/dist/core/session-manager.d.ts +4 -1
  177. package/dist/core/session-manager.d.ts.map +1 -1
  178. package/dist/core/session-manager.js +47 -7
  179. package/dist/core/session-manager.js.map +1 -1
  180. package/dist/core/settings-manager.d.ts +7 -4
  181. package/dist/core/settings-manager.d.ts.map +1 -1
  182. package/dist/core/settings-manager.js +11 -3
  183. package/dist/core/settings-manager.js.map +1 -1
  184. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  185. package/dist/core/subagent-orchestration.js +4 -2
  186. package/dist/core/subagent-orchestration.js.map +1 -1
  187. package/dist/core/system-prompt.d.ts +9 -1
  188. package/dist/core/system-prompt.d.ts.map +1 -1
  189. package/dist/core/system-prompt.js +10 -4
  190. package/dist/core/system-prompt.js.map +1 -1
  191. package/dist/core/tools/bash.d.ts +2 -2
  192. package/dist/core/tools/bash.d.ts.map +1 -1
  193. package/dist/core/tools/bash.js +2 -1
  194. package/dist/core/tools/bash.js.map +1 -1
  195. package/dist/core/tools/diagnostics.d.ts.map +1 -1
  196. package/dist/core/tools/diagnostics.js +8 -2
  197. package/dist/core/tools/diagnostics.js.map +1 -1
  198. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  199. package/dist/core/tools/tool-definition-wrapper.js +15 -0
  200. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  201. package/dist/core/turn-metrics.d.ts +135 -0
  202. package/dist/core/turn-metrics.d.ts.map +1 -0
  203. package/dist/core/turn-metrics.js +261 -0
  204. package/dist/core/turn-metrics.js.map +1 -0
  205. package/dist/core/verified-bash-runtime.d.ts.map +1 -1
  206. package/dist/core/verified-bash-runtime.js +10 -4
  207. package/dist/core/verified-bash-runtime.js.map +1 -1
  208. package/dist/guardrails/evidence-protocol.d.ts +10 -0
  209. package/dist/guardrails/evidence-protocol.d.ts.map +1 -0
  210. package/dist/guardrails/evidence-protocol.js +34 -0
  211. package/dist/guardrails/evidence-protocol.js.map +1 -0
  212. package/dist/guardrails/evidence-system.d.ts +3 -1
  213. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  214. package/dist/guardrails/evidence-system.js +22 -14
  215. package/dist/guardrails/evidence-system.js.map +1 -1
  216. package/dist/guardrails/replay-ledger-lock-owner.d.ts +7 -0
  217. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
  218. package/dist/guardrails/replay-ledger-lock-owner.js +42 -1
  219. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
  220. package/dist/guardrails/replay-payload-hash.d.ts +6 -0
  221. package/dist/guardrails/replay-payload-hash.d.ts.map +1 -0
  222. package/dist/guardrails/replay-payload-hash.js +24 -0
  223. package/dist/guardrails/replay-payload-hash.js.map +1 -0
  224. package/dist/index.d.ts +8 -2
  225. package/dist/index.d.ts.map +1 -1
  226. package/dist/index.js +7 -1
  227. package/dist/index.js.map +1 -1
  228. package/dist/main.d.ts.map +1 -1
  229. package/dist/main.js +18 -0
  230. package/dist/main.js.map +1 -1
  231. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  232. package/dist/modes/interactive/components/control-panel-layout.js +2 -2
  233. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  234. package/dist/modes/interactive/components/footer.d.ts +3 -0
  235. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  236. package/dist/modes/interactive/components/footer.js +40 -12
  237. package/dist/modes/interactive/components/footer.js.map +1 -1
  238. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  239. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  240. package/dist/modes/interactive/components/settings-selector.js +10 -0
  241. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  242. package/dist/modes/interactive/components/status-sidebar.d.ts +14 -2
  243. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  244. package/dist/modes/interactive/components/status-sidebar.js +106 -19
  245. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  246. package/dist/modes/interactive/interactive-mode.d.ts +10 -1
  247. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  248. package/dist/modes/interactive/interactive-mode.js +104 -17
  249. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  250. package/dist/modes/interactive/theme/dark.json +1 -1
  251. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  252. package/dist/modes/interactive/theme/theme.js +1 -0
  253. package/dist/modes/interactive/theme/theme.js.map +1 -1
  254. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  255. package/dist/modes/rpc/rpc-mode.js +1 -1
  256. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  257. package/dist/types/evidence.d.ts +11 -3
  258. package/dist/types/evidence.d.ts.map +1 -1
  259. package/dist/types/evidence.js.map +1 -1
  260. package/docs/book-to-skill.md +79 -0
  261. package/docs/compaction.md +35 -3
  262. package/docs/containerization.md +14 -2
  263. package/docs/docs.json +16 -0
  264. package/docs/environment-variables.md +1 -1
  265. package/docs/extensions.md +75 -16
  266. package/docs/index.md +3 -0
  267. package/docs/mcp.md +97 -0
  268. package/docs/metrics.md +87 -0
  269. package/docs/models.md +12 -0
  270. package/docs/packages.md +25 -4
  271. package/docs/provider-resilience.md +37 -56
  272. package/docs/providers.md +5 -1
  273. package/docs/run-protocol.md +72 -0
  274. package/docs/sdk.md +40 -6
  275. package/docs/sessions.md +6 -0
  276. package/docs/settings.md +10 -7
  277. package/docs/skills.md +9 -1
  278. package/docs/usage.md +14 -4
  279. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  280. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  281. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  282. package/examples/extensions/gondolin/package-lock.json +2 -2
  283. package/examples/extensions/gondolin/package.json +1 -1
  284. package/examples/extensions/plannotator-approval-bridge.ts +83 -0
  285. package/examples/extensions/sandbox/package-lock.json +2 -2
  286. package/examples/extensions/sandbox/package.json +1 -1
  287. package/examples/extensions/subagent/README.md +21 -11
  288. package/examples/extensions/subagent/adaptive-agent-runtime.ts +9 -3
  289. package/examples/extensions/subagent/agents/planner.md +0 -2
  290. package/examples/extensions/subagent/agents/reviewer.md +0 -2
  291. package/examples/extensions/subagent/agents/scout.md +0 -2
  292. package/examples/extensions/subagent/agents/worker.md +0 -1
  293. package/examples/extensions/subagent/agents.test.ts +9 -0
  294. package/examples/extensions/subagent/agents.ts +11 -4
  295. package/examples/extensions/subagent/deadline-budget.test.ts +32 -0
  296. package/examples/extensions/subagent/deadline-budget.ts +25 -1
  297. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +10 -0
  298. package/examples/extensions/subagent/index.ts +234 -55
  299. package/examples/extensions/subagent/subagent-execution-policy.test.ts +27 -0
  300. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +167 -2
  301. package/examples/extensions/subagent/workflow-graph.test.ts +80 -0
  302. package/examples/extensions/subagent/workflow-graph.ts +89 -0
  303. package/examples/extensions/with-deps/package-lock.json +2 -2
  304. package/examples/extensions/with-deps/package.json +1 -1
  305. package/npm-shrinkwrap.json +275 -439
  306. package/package.json +11 -8
@@ -14,9 +14,11 @@
14
14
  */
15
15
  import { createHash, randomUUID } from "node:crypto";
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
- import { basename, dirname } from "node:path";
17
+ import { basename, dirname, join as joinPath } from "node:path";
18
18
  import { isDeepStrictEqual } from "node:util";
19
- import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "omk-ai";
19
+ import { getVisionRouteModel, isVisionRouteModel } from "omk-agent-core";
20
+ import { clampThinkingLevel, cleanupSessionResources, deriveContextPromptCacheKey, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "omk-ai";
21
+ import { APP_NAME, VERSION } from "../config.js";
20
22
  import { theme } from "../modes/interactive/theme/theme.js";
21
23
  import { stripFrontmatter } from "../utils/frontmatter.js";
22
24
  import { resolvePath } from "../utils/paths.js";
@@ -31,6 +33,7 @@ import { createCompactionHysteresisConfig, createCompactionHysteresisState, step
31
33
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
32
34
  import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
33
35
  import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
36
+ import { createDiskContextBudgetCacheProviderV2, DiskContextBudgetCacheProviderV2, } from "./context-budget-v2-cache-disk.js";
34
37
  import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
35
38
  import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
36
39
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
@@ -41,21 +44,49 @@ import { emitSessionShutdownEvent } from "./extensions/runner.js";
41
44
  import { assertTextChatModelForCompletion } from "./grok-harness.js";
42
45
  import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
43
46
  import { decideLoadoutAccess } from "./loadout-access-policy.js";
47
+ import { loadMcpServerConfigs } from "./mcp/config.js";
48
+ import { McpManager } from "./mcp/manager.js";
49
+ import { buildRuntimeProvenance } from "./runtime-provenance.js";
50
+ import { TurnMetricsSink } from "./turn-metrics.js";
51
+ /** Values that disable a session-level feature through its environment variable. */
52
+ const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "disable", "disabled"]);
53
+ const MAX_OVERFLOW_RECOVERY_ATTEMPTS = 2;
54
+ const OVERFLOW_RECOVERY_EMERGENCY_TOKENS = 4_096;
55
+ function isDisabledEnvValue(value) {
56
+ return value !== undefined && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());
57
+ }
58
+ /** Extract the first text block of a tool result, for failure classification only. */
59
+ function firstTextContent(result) {
60
+ const content = result?.content;
61
+ if (!Array.isArray(content))
62
+ return undefined;
63
+ for (const block of content) {
64
+ if (block && typeof block === "object" && block.type === "text") {
65
+ const text = block.text;
66
+ if (typeof text === "string")
67
+ return text;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ import { redactCredentialShapedContent } from "./compaction/transaction.js";
44
73
  import { findExactModelReferenceMatch } from "./model-resolver.js";
45
74
  import { expandPromptTemplate } from "./prompt-templates.js";
46
- import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
47
- import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
48
- import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
49
- import { redactSensitiveText } from "./redaction.js";
50
- import { appendRouterFeedbackRecord, } from "./router-feedback-collector.js";
75
+ import { isContentSafetyStopMessage, isQuotaExhaustionMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
76
+ import { getBiasStepsForCell, parseRouterBiasSnapshot } from "./reasoning-router-bias.js";
77
+ import { classifyTaskV4, deriveRouterFeedbackFeaturesV4, resolveThinkingLevelV4WithUncertainty, TASK_CLASS_THINKING_LEVELS_V4, } from "./reasoning-router-v4.js";
78
+ import { redactSensitiveText, redactSensitiveTextForced } from "./redaction.js";
79
+ import { getRepositoryRouterLearningPaths } from "./repository-learning-scope.js";
80
+ import { appendRouterFeedbackRecord, ROUTER_FEEDBACK_LEVELS, } from "./router-feedback-collector.js";
51
81
  import { RunJournalStore } from "./run-journal-store.js";
52
82
  import { SessionBashRuntime } from "./session-bash-runtime.js";
53
83
  import { SessionBashService } from "./session-bash-service.js";
54
84
  import { SessionCompactionService } from "./session-compaction-service.js";
55
85
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
86
+ import { acquireSessionOwnerLeaseSync } from "./session-owner-lease.js";
56
87
  import { classifySessionTermination, } from "./session-termination.js";
57
88
  import { createSyntheticSourceInfo } from "./source-info.js";
58
- import { buildSystemPrompt } from "./system-prompt.js";
89
+ import { buildSystemPromptPlan } from "./system-prompt.js";
59
90
  import { createLocalBashOperations } from "./tools/bash.js";
60
91
  import { createAllToolDefinitions } from "./tools/index.js";
61
92
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
@@ -110,7 +141,13 @@ function pendingToolResultClass(name, args) {
110
141
  /** Standard thinking levels */
111
142
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
112
143
  function normalizeToolNames(toolNames) {
113
- return [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== ""))].sort();
144
+ return [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== ""))].sort((left, right) => {
145
+ if (left < right)
146
+ return -1;
147
+ if (left > right)
148
+ return 1;
149
+ return 0;
150
+ });
114
151
  }
115
152
  function toolNameSetsEqual(left, right) {
116
153
  const normalizedLeft = normalizeToolNames(left);
@@ -159,7 +196,8 @@ function clonePlainSnapshot(value, ancestors = new WeakSet(), copies = new WeakM
159
196
  ancestors.add(value);
160
197
  try {
161
198
  if (Array.isArray(value)) {
162
- const copy = new Array(value.length);
199
+ const copy = [];
200
+ copy.length = value.length;
163
201
  copies.set(value, copy);
164
202
  for (const key of Reflect.ownKeys(value)) {
165
203
  if (key === "length")
@@ -243,6 +281,7 @@ export class AgentSession {
243
281
  */
244
282
  _reasoningRouterBiasSnapshot = null;
245
283
  _reasoningRouterBiasSnapshotLoaded = false;
284
+ _lastAutoRouterDecision;
246
285
  /**
247
286
  * AdaptOrch advisory bridge (default-off, global-only opt-in via
248
287
  * `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily
@@ -265,7 +304,7 @@ export class AgentSession {
265
304
  // Compaction state
266
305
  _compactionAbortController = undefined;
267
306
  _autoCompactionAbortController = undefined;
268
- _overflowRecoveryAttempted = false;
307
+ _overflowRecoveryAttempts = 0;
269
308
  _thresholdCompactionEmergency = false;
270
309
  _compactionHysteresisState = createCompactionHysteresisState();
271
310
  // Branch summarization state
@@ -285,7 +324,12 @@ export class AgentSession {
285
324
  _resourceLoader;
286
325
  _customTools;
287
326
  _baseToolDefinitions = new Map();
327
+ _mcpManager;
328
+ _mcpToolNames = new Set();
329
+ _turnMetricsSink;
330
+ _turnMetricsState;
288
331
  _cwd;
332
+ _repositoryRouterLearningPaths;
289
333
  _extensionRunnerRef;
290
334
  _initialActiveToolNames;
291
335
  _allowedToolNames;
@@ -302,6 +346,7 @@ export class AgentSession {
302
346
  _extensionErrorUnsubscriber;
303
347
  // Required durable run/audit chain, persisted next to the session file.
304
348
  _runJournalStore;
349
+ _ownedSessionOwnerLease;
305
350
  _activeRunId = null;
306
351
  _pendingRuntimeTerminationCause;
307
352
  _activeRunToolTermination;
@@ -326,7 +371,13 @@ export class AgentSession {
326
371
  _toolPromptGuidelines = new Map();
327
372
  // Base system prompt (without extension appends) - used to apply fresh appends each turn
328
373
  _baseSystemPrompt = "";
374
+ _baseSystemPromptCacheBoundary;
329
375
  _baseSystemPromptOptions;
376
+ _promptCacheKey;
377
+ _promptCacheKeyChanges = 0;
378
+ _promptCacheBoundaryBypasses = 0;
379
+ _promptCacheStablePrefixCharacters = 0;
380
+ _promptCacheLastBreakReason;
330
381
  constructor(config) {
331
382
  this.agent = config.agent;
332
383
  this.sessionManager = config.sessionManager;
@@ -389,31 +440,55 @@ export class AgentSession {
389
440
  recordCommit: () => this._recordCompactionCommitForHysteresis(),
390
441
  });
391
442
  const sessionFile = this.sessionManager.getSessionFile();
392
- this._runJournalStore = RunJournalStore.open({
393
- ...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
394
- sessionId: this.sessionManager.getSessionId(),
395
- });
396
- const startupRecords = this._runJournalStore.records;
397
- const startupTerminal = startupRecords[startupRecords.length - 1];
398
- if (startupTerminal?.event === "run_recovered") {
399
- this._lastTermination = startupTerminal.termination;
400
- }
401
- // Always subscribe to agent events for internal handling
402
- // (session persistence, extensions, auto-compaction, retry logic)
403
- this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
404
- this._installAgentToolHooks();
405
- // Durable transcript-repair audit for a repair applied on open/resume.
406
- if (this._transcriptRepair) {
407
- this._invalidateContextBudgetCache({ type: "transcriptRepair" });
408
- this._appendRunJournalAudit("transcript_repaired", {
409
- insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
410
- reason: this._transcriptRepair.reason,
443
+ let ownerLease = this.sessionManager.getOwnerLease();
444
+ try {
445
+ if (sessionFile && !ownerLease) {
446
+ ownerLease = acquireSessionOwnerLeaseSync(sessionFile);
447
+ this._ownedSessionOwnerLease = ownerLease;
448
+ this.sessionManager.setOwnerLease(ownerLease);
449
+ }
450
+ this._runJournalStore = RunJournalStore.open({
451
+ ...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
452
+ sessionId: this.sessionManager.getSessionId(),
453
+ ...(ownerLease ? { ownerLease } : {}),
454
+ });
455
+ const startupRecords = this._runJournalStore.records;
456
+ const startupTerminal = startupRecords[startupRecords.length - 1];
457
+ if (startupTerminal?.event === "run_recovered") {
458
+ this._lastTermination = startupTerminal.termination;
459
+ }
460
+ // Always subscribe to agent events for internal handling
461
+ // (session persistence, extensions, auto-compaction, retry logic)
462
+ this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
463
+ this._installAgentToolHooks();
464
+ // Durable transcript-repair audit for a repair applied on open/resume.
465
+ if (this._transcriptRepair) {
466
+ this._invalidateContextBudgetCache({ type: "transcriptRepair" });
467
+ this._appendRunJournalAudit("transcript_repaired", {
468
+ insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
469
+ reason: this._transcriptRepair.reason,
470
+ });
471
+ }
472
+ this._buildRuntime({
473
+ activeToolNames: this._initialActiveToolNames,
474
+ includeAllExtensionTools: true,
411
475
  });
412
476
  }
413
- this._buildRuntime({
414
- activeToolNames: this._initialActiveToolNames,
415
- includeAllExtensionTools: true,
416
- });
477
+ catch (error) {
478
+ const ownedLease = this._ownedSessionOwnerLease;
479
+ if (ownedLease) {
480
+ try {
481
+ ownedLease.release();
482
+ if (this.sessionManager.getOwnerLease() === ownedLease)
483
+ this.sessionManager.setOwnerLease(undefined);
484
+ this._ownedSessionOwnerLease = undefined;
485
+ }
486
+ catch {
487
+ throw new Error("AgentSession initialization cleanup failed");
488
+ }
489
+ }
490
+ throw error;
491
+ }
417
492
  }
418
493
  /** Model registry for API key resolution and model discovery */
419
494
  get modelRegistry() {
@@ -512,12 +587,15 @@ export class AgentSession {
512
587
  if (/stop_reason\s*=\s*(refusal|sensitive)|content\/safety stop|safety stop|provider\.refusal|kind=provider_refusal/i.test(text)) {
513
588
  return { area: "provider", code: "refusal" };
514
589
  }
590
+ // Quota/billing exhaustion is checked BEFORE the generic 401/403 auth
591
+ // patterns: "403 ... usage limit for this billing cycle" is transient per
592
+ // cycle and must fail over, not terminate the turn as an auth error.
593
+ if (isQuotaExhaustionMessage(text) || /rate.?limit|too many requests|429/i.test(text)) {
594
+ return { area: "provider", code: "rate_limit" };
595
+ }
515
596
  if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
516
597
  return { area: "provider", code: "auth" };
517
598
  }
518
- if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
519
- return { area: "provider", code: "rate_limit" };
520
- }
521
599
  // Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
522
600
  // Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
523
601
  if (/tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|unknown\s+tool_call_id/i.test(text)) {
@@ -536,6 +614,9 @@ export class AgentSession {
536
614
  if (!this.model || /no model|model selected|model is required/i.test(message)) {
537
615
  return { area: "configuration", code: "invalid" };
538
616
  }
617
+ if (isQuotaExhaustionMessage(message)) {
618
+ return { area: "provider", code: "rate_limit" };
619
+ }
539
620
  if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
540
621
  return { area: "provider", code: "auth" };
541
622
  }
@@ -564,9 +645,14 @@ export class AgentSession {
564
645
  }
565
646
  _classifyRunTermination(runId, event) {
566
647
  const timestamp = new Date().toISOString();
567
- const assistant = [...event.messages]
568
- .reverse()
569
- .find((message) => message.role === "assistant");
648
+ let assistant;
649
+ for (let index = event.messages.length - 1; index >= 0; index -= 1) {
650
+ const message = event.messages[index];
651
+ if (message?.role === "assistant") {
652
+ assistant = message;
653
+ break;
654
+ }
655
+ }
570
656
  let cause;
571
657
  let message;
572
658
  let sideEffects = this._sessionRiskLevel === "elevated" ? "possible" : "none";
@@ -595,6 +681,12 @@ export class AgentSession {
595
681
  cause = { area: "completed" };
596
682
  message = "Run completed.";
597
683
  }
684
+ let provider = this.model?.provider;
685
+ if (assistant?.provider)
686
+ provider = assistant.provider;
687
+ let model = this.model?.id;
688
+ if (assistant?.model)
689
+ model = assistant.model;
598
690
  return classifySessionTermination({
599
691
  sessionId: this.sessionId,
600
692
  runId,
@@ -603,12 +695,8 @@ export class AgentSession {
603
695
  message,
604
696
  cause,
605
697
  sideEffects,
606
- ...(assistant?.provider
607
- ? { provider: assistant.provider }
608
- : this.model
609
- ? { provider: this.model.provider }
610
- : {}),
611
- ...(assistant?.model ? { model: assistant.model } : this.model ? { model: this.model.id } : {}),
698
+ ...(provider ? { provider } : {}),
699
+ ...(model ? { model } : {}),
612
700
  ...(toolCallId ? { toolCallId } : {}),
613
701
  ...(toolName ? { toolName } : {}),
614
702
  });
@@ -637,11 +725,13 @@ export class AgentSession {
637
725
  const cause = this._runtimeFailureCause(error);
638
726
  const runId = this._activeRunId ?? `runtime-${randomUUID()}`;
639
727
  const timestamp = new Date().toISOString();
640
- const message = cause.area === "persistence"
641
- ? "A required runtime persistence operation failed."
642
- : cause.area === "compaction"
643
- ? "Runtime compaction failed."
644
- : "The AgentSession runtime failed before completing the run.";
728
+ let message = "The AgentSession runtime failed before completing the run.";
729
+ if (cause.area === "persistence") {
730
+ message = "A required runtime persistence operation failed.";
731
+ }
732
+ else if (cause.area === "compaction") {
733
+ message = "Runtime compaction failed.";
734
+ }
645
735
  let termination = classifySessionTermination({
646
736
  sessionId: this.sessionId,
647
737
  runId,
@@ -884,7 +974,7 @@ export class AgentSession {
884
974
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
885
975
  // This ensures the UI sees the updated queue state
886
976
  if (event.type === "message_start" && event.message.role === "user") {
887
- this._overflowRecoveryAttempted = false;
977
+ this._overflowRecoveryAttempts = 0;
888
978
  const messageText = this._getUserMessageText(event.message);
889
979
  if (messageText) {
890
980
  // Check steering queue first
@@ -935,7 +1025,7 @@ export class AgentSession {
935
1025
  this._lastAssistantMessage = finalizedEvent.message;
936
1026
  const assistantMsg = finalizedEvent.message;
937
1027
  if (assistantMsg.stopReason !== "error") {
938
- this._overflowRecoveryAttempted = false;
1028
+ this._overflowRecoveryAttempts = 0;
939
1029
  }
940
1030
  // Reset retry counter immediately on successful assistant response
941
1031
  // This prevents accumulation across multiple LLM calls within a turn
@@ -993,9 +1083,13 @@ export class AgentSession {
993
1083
  ? "Finalized message was not found in agent state"
994
1084
  : "Finalized message is ambiguous in agent state");
995
1085
  }
1086
+ const matchingIndex = matchingIndexes[0];
1087
+ if (matchingIndex === undefined) {
1088
+ throw new Error("Finalized message was not found in agent state");
1089
+ }
996
1090
  const finalizedReplacement = createImmutableMessageSnapshot(replacement);
997
- const nextMessages = messages.slice();
998
- nextMessages[matchingIndexes[0]] = finalizedReplacement;
1091
+ const nextMessages = [...messages];
1092
+ nextMessages[matchingIndex] = finalizedReplacement;
999
1093
  this.agent.state.messages = nextMessages;
1000
1094
  return finalizedReplacement;
1001
1095
  }
@@ -1009,6 +1103,13 @@ export class AgentSession {
1009
1103
  await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
1010
1104
  }
1011
1105
  else if (event.type === "turn_start") {
1106
+ this._turnMetricsState = {
1107
+ startedAtEpochMs: Date.now(),
1108
+ toolCalls: [],
1109
+ toolStarts: new Map(),
1110
+ requestedModel: this.model ? `${this.model.provider}/${this.model.id}` : null,
1111
+ requestedThinking: this.thinkingMode === "auto" ? "auto" : this.thinkingLevel,
1112
+ };
1012
1113
  const extensionEvent = {
1013
1114
  type: "turn_start",
1014
1115
  turnIndex: this._turnIndex,
@@ -1017,6 +1118,7 @@ export class AgentSession {
1017
1118
  await this._extensionRunner.emit(extensionEvent);
1018
1119
  }
1019
1120
  else if (event.type === "turn_end") {
1121
+ this._recordTurnMetrics(event.message);
1020
1122
  const extensionEvent = {
1021
1123
  type: "turn_end",
1022
1124
  turnIndex: this._turnIndex,
@@ -1053,6 +1155,7 @@ export class AgentSession {
1053
1155
  }
1054
1156
  }
1055
1157
  else if (event.type === "tool_execution_start") {
1158
+ this._turnMetricsState?.toolStarts.set(event.toolCallId, Date.now());
1056
1159
  const extensionEvent = {
1057
1160
  type: "tool_execution_start",
1058
1161
  toolCallId: event.toolCallId,
@@ -1072,6 +1175,7 @@ export class AgentSession {
1072
1175
  await this._extensionRunner.emit(extensionEvent);
1073
1176
  }
1074
1177
  else if (event.type === "tool_execution_end") {
1178
+ this._recordToolMetric(event.toolCallId, event.toolName, event.isError === true, event.result);
1075
1179
  const extensionEvent = {
1076
1180
  type: "tool_execution_end",
1077
1181
  toolCallId: event.toolCallId,
@@ -1082,6 +1186,81 @@ export class AgentSession {
1082
1186
  await this._extensionRunner.emit(extensionEvent);
1083
1187
  }
1084
1188
  }
1189
+ /**
1190
+ * Turn metrics sink for this session, created on first use.
1191
+ *
1192
+ * Writes to `<cwd>/.omk/metrics/`. Set `OMK_TURN_METRICS=0` to disable, or
1193
+ * `OMK_TURN_METRICS_DIR` to relocate. Returns `undefined` when disabled, so
1194
+ * every call site is a no-op rather than a branch.
1195
+ */
1196
+ _metricsSink() {
1197
+ if (isDisabledEnvValue(process.env.OMK_TURN_METRICS))
1198
+ return undefined;
1199
+ if (!this._turnMetricsSink) {
1200
+ const dir = process.env.OMK_TURN_METRICS_DIR ?? joinPath(this._cwd, ".omk", "metrics");
1201
+ this._turnMetricsSink = new TurnMetricsSink({ dir });
1202
+ }
1203
+ return this._turnMetricsSink;
1204
+ }
1205
+ _recordToolMetric(toolCallId, toolName, isError, result) {
1206
+ const state = this._turnMetricsState;
1207
+ if (!state)
1208
+ return;
1209
+ const startedAt = state.toolStarts.get(toolCallId);
1210
+ state.toolStarts.delete(toolCallId);
1211
+ state.toolCalls.push({
1212
+ name: toolName,
1213
+ durationMs: startedAt === undefined ? 0 : Date.now() - startedAt,
1214
+ ok: !isError,
1215
+ ...(isError ? { error: firstTextContent(result) } : {}),
1216
+ });
1217
+ }
1218
+ /** Persist one turn. Metrics are advisory: a failure here never affects the turn. */
1219
+ _recordTurnMetrics(message) {
1220
+ const state = this._turnMetricsState;
1221
+ this._turnMetricsState = undefined;
1222
+ if (!state)
1223
+ return;
1224
+ const sink = this._metricsSink();
1225
+ if (!sink)
1226
+ return;
1227
+ const assistant = message.role === "assistant" ? message : undefined;
1228
+ const usage = assistant?.usage;
1229
+ const selectedModel = this.model ? `${this.model.provider}/${this.model.id}` : null;
1230
+ const runtimeProvenance = buildRuntimeProvenance({
1231
+ requestedModel: state.requestedModel,
1232
+ selectedModel,
1233
+ responseModel: assistant?.responseModel ?? (assistant ? `${assistant.provider}/${assistant.model}` : null),
1234
+ requestedThinking: state.requestedThinking,
1235
+ effectiveThinking: this.thinkingLevel,
1236
+ source: "session",
1237
+ });
1238
+ try {
1239
+ sink.record({
1240
+ sessionId: this.sessionId,
1241
+ turnIndex: this._turnIndex,
1242
+ provider: this.model?.provider,
1243
+ model: this.model?.id,
1244
+ startedAtEpochMs: state.startedAtEpochMs,
1245
+ endedAtEpochMs: Date.now(),
1246
+ stopReason: assistant?.stopReason,
1247
+ runtimeProvenance,
1248
+ usage: usage
1249
+ ? {
1250
+ input: usage.input,
1251
+ output: usage.output,
1252
+ cacheRead: usage.cacheRead,
1253
+ cacheWrite: usage.cacheWrite,
1254
+ costUsd: usage.cost.total,
1255
+ }
1256
+ : undefined,
1257
+ toolCalls: state.toolCalls,
1258
+ });
1259
+ }
1260
+ catch {
1261
+ // Never let observability break a turn.
1262
+ }
1263
+ }
1085
1264
  /**
1086
1265
  * Subscribe to agent events.
1087
1266
  * Session persistence is handled internally (saves messages on message_end).
@@ -1123,19 +1302,59 @@ export class AgentSession {
1123
1302
  */
1124
1303
  dispose() {
1125
1304
  try {
1126
- this.abortRetry();
1127
- this.abortCompaction();
1128
- this.abortBranchSummary();
1129
- this.abortBash();
1130
- this.agent.abort();
1305
+ try {
1306
+ this.abortRetry();
1307
+ this.abortCompaction();
1308
+ this.abortBranchSummary();
1309
+ this.abortBash();
1310
+ this.agent.abort();
1311
+ }
1312
+ catch {
1313
+ // Dispose must succeed even if an abort hook throws.
1314
+ }
1315
+ this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured extension API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
1316
+ this._disconnectFromAgent();
1317
+ this._eventListeners = [];
1318
+ if (this._contextBudgetCacheProvider instanceof DiskContextBudgetCacheProviderV2) {
1319
+ // Persist whatever the session rendered; a failed flush is never fatal.
1320
+ this._contextBudgetCacheProvider.close();
1321
+ }
1322
+ this._mcpManager?.close();
1323
+ this._mcpManager = undefined;
1324
+ cleanupSessionResources(this.sessionId);
1325
+ }
1326
+ finally {
1327
+ const ownerLease = this._ownedSessionOwnerLease;
1328
+ if (ownerLease) {
1329
+ ownerLease.release();
1330
+ if (this.sessionManager.getOwnerLease() === ownerLease)
1331
+ this.sessionManager.setOwnerLease(undefined);
1332
+ this._ownedSessionOwnerLease = undefined;
1333
+ }
1334
+ }
1335
+ }
1336
+ /**
1337
+ * Build the context-budget cache provider for this session.
1338
+ *
1339
+ * Defaults to a workspace-layer provider persisted under `<cwd>/.omk/cache`,
1340
+ * so a second session reuses representations the first one already rendered
1341
+ * (representation keys are content-addressed; see
1342
+ * `context-budget-v2-cache-keys.ts`). Set
1343
+ * `OMK_CONTEXT_GOVERNOR_CACHE=memory` to opt out, or
1344
+ * `OMK_CONTEXT_GOVERNOR_CACHE_DIR` to relocate the snapshot.
1345
+ */
1346
+ _createContextBudgetCacheProvider() {
1347
+ if (process.env.OMK_CONTEXT_GOVERNOR_CACHE === "memory") {
1348
+ return createMemoryContextBudgetCacheProviderV2("session");
1349
+ }
1350
+ const dir = process.env.OMK_CONTEXT_GOVERNOR_CACHE_DIR ?? joinPath(this._cwd, ".omk", "cache", "context-budget-v2");
1351
+ try {
1352
+ return createDiskContextBudgetCacheProviderV2({ dir, layer: "workspace" });
1131
1353
  }
1132
1354
  catch {
1133
- // Dispose must succeed even if an abort hook throws.
1355
+ // An unusable cache directory must not take the session down.
1356
+ return createMemoryContextBudgetCacheProviderV2("session");
1134
1357
  }
1135
- this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured extension API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
1136
- this._disconnectFromAgent();
1137
- this._eventListeners = [];
1138
- cleanupSessionResources(this.sessionId);
1139
1358
  }
1140
1359
  // =========================================================================
1141
1360
  // Read-only State Access
@@ -1220,6 +1439,9 @@ export class AgentSession {
1220
1439
  // Rebuild base system prompt with new tool set
1221
1440
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
1222
1441
  this.agent.state.systemPrompt = this._baseSystemPrompt;
1442
+ this.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;
1443
+ this.agent.state.systemPromptCacheBoundaryBypass = false;
1444
+ this._recordPromptCachePlan("tool-set");
1223
1445
  }
1224
1446
  /**
1225
1447
  * Recompute the Agent's per-name timeout map for the active tool set:
@@ -1316,7 +1538,7 @@ export class AgentSession {
1316
1538
  const recent = messages.slice(-3);
1317
1539
  for (let i = recent.length - 1; i >= 0; i--) {
1318
1540
  const msg = recent[i];
1319
- if (!("role" in msg) || msg.role !== "user") {
1541
+ if (!msg || !("role" in msg) || msg.role !== "user") {
1320
1542
  continue;
1321
1543
  }
1322
1544
  const content = msg.content;
@@ -1325,9 +1547,7 @@ export class AgentSession {
1325
1547
  return trimmed.length > 0 ? trimmed : undefined;
1326
1548
  }
1327
1549
  if (Array.isArray(content)) {
1328
- const textParts = content
1329
- .filter((c) => "type" in c && c.type === "text")
1330
- .map((c) => c.text);
1550
+ const textParts = content.flatMap((item) => (item.type === "text" ? [item.text] : []));
1331
1551
  const joined = textParts.join("\n").trim();
1332
1552
  return joined.length > 0 ? joined : undefined;
1333
1553
  }
@@ -1377,7 +1597,7 @@ export class AgentSession {
1377
1597
  responseReserveTokens =
1378
1598
  parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
1379
1599
  }
1380
- const cacheProvider = this._contextBudgetCacheProvider ?? createMemoryContextBudgetCacheProviderV2("session");
1600
+ const cacheProvider = this._contextBudgetCacheProvider ?? this._createContextBudgetCacheProvider();
1381
1601
  cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
1382
1602
  this._contextBudgetCacheProvider = cacheProvider;
1383
1603
  // Enforce floor
@@ -1411,6 +1631,32 @@ export class AgentSession {
1411
1631
  const ratio = overrideRatio ?? RESPONSE_RESERVE_RATIO;
1412
1632
  return Math.max(Math.floor(contextWindow * ratio), LEGACY_RESPONSE_RESERVE_TOKENS);
1413
1633
  }
1634
+ _recordPromptCachePlan(reason) {
1635
+ const boundary = this.agent.state.systemPromptCacheBoundary;
1636
+ const scope = this.model ? `${this.model.provider}/${this.model.id}` : "unknown";
1637
+ const nextKey = deriveContextPromptCacheKey({
1638
+ systemPrompt: this.agent.state.systemPrompt,
1639
+ systemPromptCacheBoundary: boundary,
1640
+ systemPromptCacheBoundaryBypass: this.agent.state.systemPromptCacheBoundaryBypass,
1641
+ messages: [],
1642
+ tools: this.agent.state.tools,
1643
+ }, scope);
1644
+ if (!nextKey) {
1645
+ this._promptCacheBoundaryBypasses += 1;
1646
+ this._promptCacheStablePrefixCharacters = 0;
1647
+ if (this._promptCacheKey !== undefined) {
1648
+ this._promptCacheLastBreakReason = reason;
1649
+ }
1650
+ this._promptCacheKey = undefined;
1651
+ return;
1652
+ }
1653
+ if (this._promptCacheKey !== undefined && this._promptCacheKey !== nextKey) {
1654
+ this._promptCacheKeyChanges += 1;
1655
+ this._promptCacheLastBreakReason = reason;
1656
+ }
1657
+ this._promptCacheKey = nextKey;
1658
+ this._promptCacheStablePrefixCharacters = boundary ?? 0;
1659
+ }
1414
1660
  _rebuildSystemPrompt(toolNames) {
1415
1661
  const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
1416
1662
  const toolSnippets = {};
@@ -1446,7 +1692,9 @@ export class AgentSession {
1446
1692
  promptGuidelines,
1447
1693
  contextBudget: this._getContextBudgetOptions(),
1448
1694
  };
1449
- return buildSystemPrompt(this._baseSystemPromptOptions);
1695
+ const built = buildSystemPromptPlan(this._baseSystemPromptOptions);
1696
+ this._baseSystemPromptCacheBoundary = built.cacheBoundary;
1697
+ return built.prompt;
1450
1698
  }
1451
1699
  // =========================================================================
1452
1700
  // Prompting
@@ -1625,9 +1873,9 @@ export class AgentSession {
1625
1873
  ...(promptActiveSkillNames.length > 0 ? { activeSkillNames: promptActiveSkillNames } : {}),
1626
1874
  ...(promptActiveSkillSource ? { activeSkillSource: promptActiveSkillSource } : {}),
1627
1875
  };
1628
- const turnSystemPrompt = buildSystemPrompt(turnSystemPromptOptions);
1876
+ const turnSystemPrompt = buildSystemPromptPlan(turnSystemPromptOptions);
1629
1877
  // Emit before_agent_start extension event
1630
- const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, turnSystemPrompt, turnSystemPromptOptions);
1878
+ const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, turnSystemPrompt.prompt, turnSystemPromptOptions);
1631
1879
  // Add all custom messages from extensions
1632
1880
  if (result?.messages) {
1633
1881
  for (const msg of result.messages) {
@@ -1641,14 +1889,22 @@ export class AgentSession {
1641
1889
  });
1642
1890
  }
1643
1891
  }
1644
- // Apply extension-modified system prompt, or reset to base
1892
+ // Apply extension-modified system prompt, or reset to the turn plan.
1645
1893
  if (result?.systemPrompt) {
1894
+ const preservesCacheBoundary = result.systemPrompt === turnSystemPrompt.prompt;
1646
1895
  this.agent.state.systemPrompt = result.systemPrompt;
1896
+ this.agent.state.systemPromptCacheBoundary = preservesCacheBoundary
1897
+ ? turnSystemPrompt.cacheBoundary
1898
+ : undefined;
1899
+ this.agent.state.systemPromptCacheBoundaryBypass = !preservesCacheBoundary;
1647
1900
  }
1648
1901
  else {
1649
- // Ensure we're using the base prompt (in case previous turn had modifications)
1650
- this.agent.state.systemPrompt = turnSystemPrompt;
1902
+ // Ensure we're using the turn prompt (in case the previous turn had modifications).
1903
+ this.agent.state.systemPrompt = turnSystemPrompt.prompt;
1904
+ this.agent.state.systemPromptCacheBoundary = turnSystemPrompt.cacheBoundary;
1905
+ this.agent.state.systemPromptCacheBoundaryBypass = false;
1651
1906
  }
1907
+ this._recordPromptCachePlan(result?.systemPrompt ? "extension-override" : "turn-plan");
1652
1908
  await this._checkProjectedCompaction(messages);
1653
1909
  }
1654
1910
  catch (error) {
@@ -2080,6 +2336,18 @@ export class AgentSession {
2080
2336
  });
2081
2337
  }
2082
2338
  }
2339
+ /** Apply an explicit user choice and retain it as bounded router feedback. */
2340
+ setUserThinkingLevel(level) {
2341
+ const overridesAuto = this._thinkingMode === "auto";
2342
+ this.setThinkingMode("manual");
2343
+ this.setThinkingLevel(level);
2344
+ if (overridesAuto) {
2345
+ this._recordUserThinkingOverride(this.thinkingLevel);
2346
+ }
2347
+ else {
2348
+ this._lastAutoRouterDecision = undefined;
2349
+ }
2350
+ }
2083
2351
  /**
2084
2352
  * Set the thinking mode. "auto" resolves a level per turn via the reasoning
2085
2353
  * router; "manual" keeps the explicitly selected level. The mode persists for
@@ -2107,21 +2375,20 @@ export class AgentSession {
2107
2375
  * context-pressure bucket, routed through the confidence-bearing v4 classifier
2108
2376
  * and its uncertainty-aware resolver. No
2109
2377
  * `laneType` applies to the main session (always "none"/`undefined`);
2110
- * `hint` is permanently `null` -- the Adaptorch advisory bridge has no
2111
- * transport wired into the session (out of this lane's scope; see
2112
- * adaptorch-bridge.ts). `bias` stays `0` unless BOTH hold: (a) the global,
2378
+ * the default-off Adaptorch bridge is wired but its current transport returns
2379
+ * no advisory hint. `bias` stays `0` unless BOTH hold: (a) the global,
2113
2380
  * owner-only `reasoningRouterLearning.enabled` setting is `true` (default
2114
2381
  * off; a project-scope `.omk/settings.json` value for this key is never
2115
2382
  * consulted -- see settings-manager.ts), and (b) a compiled
2116
2383
  * `RouterBiasSnapshot` was found and passed strict validation at the
2117
- * configured/default path, loaded and cached ("pinned") at most once per
2118
- * session (see `_getReasoningRouterBiasSnapshot`). When learning is
2119
- * enabled, exactly one bounded "accepted" feedback record (no raw prompt/
2120
- * path/diff/session/model/provider/tool/hook content; see
2121
- * router-feedback-collector.ts's exact ten-key schema) is appended to the
2122
- * local ledger after every v4 auto-turn resolution, for a future, separate
2123
- * offline compile step to learn from -- this lane never records an
2124
- * override/fail/hook-outcome signal, only the neutral "accepted" one. The
2384
+ * configured path or opaque repository/worktree-scoped default, loaded and
2385
+ * cached ("pinned") at most once per session (see
2386
+ * `_getReasoningRouterBiasSnapshot`). When learning is enabled, one bounded
2387
+ * "accepted" record (no raw prompt/path/diff/session/model/provider/tool/hook
2388
+ * content; see router-feedback-collector.ts's exact ten-key schema) is
2389
+ * appended after every v4 auto-turn. A later explicit user level records one
2390
+ * bounded `s1-override` direction for the same cell. Both remain inert until
2391
+ * a future, separate offline compile step. The
2125
2392
  * resolver's own confidence-band/fallback-reason escalation (see
2126
2393
  * reasoning-router-v4.ts) still applies on top of the base+lane+bias
2127
2394
  * target, so a low-confidence or fallback-decided verdict can still only
@@ -2129,17 +2396,17 @@ export class AgentSession {
2129
2396
  */
2130
2397
  _applyAutoThinkingLevelV4(promptText) {
2131
2398
  const availableLevels = this.getAvailableThinkingLevels();
2399
+ const features = deriveRouterFeedbackFeaturesV4(promptText);
2132
2400
  const verdict = classifyTaskV4({
2133
2401
  prompt: promptText,
2134
2402
  history: this._taskClassHistory,
2135
2403
  pressureBucket: this._computePressureBucket(),
2136
- });
2404
+ }, undefined, features);
2137
2405
  this._taskClassHistory.unshift(verdict.taskClass);
2138
2406
  if (this._taskClassHistory.length > 8)
2139
2407
  this._taskClassHistory.length = 8;
2140
2408
  const learningEnabled = this.settingsManager.getReasoningRouterLearningEnabled();
2141
2409
  const snapshot = learningEnabled ? this._getReasoningRouterBiasSnapshot() : null;
2142
- const features = this._deriveRouterFeedbackFeatures(promptText);
2143
2410
  const bias = snapshot === null
2144
2411
  ? 0
2145
2412
  : getBiasStepsForCell(snapshot, {
@@ -2150,22 +2417,23 @@ export class AgentSession {
2150
2417
  hadDiff: features.hadDiff,
2151
2418
  });
2152
2419
  const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, this._getAdaptorchHint(verdict, features));
2420
+ this._lastAutoRouterDecision = undefined;
2153
2421
  if (learningEnabled && resolved !== "off") {
2154
- const record = {
2422
+ const decision = {
2155
2423
  routerVersion: "v4",
2156
2424
  laneType: "none",
2157
2425
  predictedClass: verdict.taskClass,
2158
2426
  resolvedLevel: resolved,
2159
- acceptedLevel: resolved,
2160
- signal: "s2-accept",
2161
- outcome: "accepted",
2162
2427
  lenBucket: features.lenBucket,
2163
2428
  hadFence: features.hadFence,
2164
2429
  hadDiff: features.hadDiff,
2165
2430
  };
2166
- appendRouterFeedbackRecord(record, {
2167
- enabled: true,
2168
- ledgerPath: this.settingsManager.getReasoningRouterLearningFeedbackLedgerPath(),
2431
+ this._lastAutoRouterDecision = decision;
2432
+ this._appendRouterFeedback({
2433
+ ...decision,
2434
+ acceptedLevel: resolved,
2435
+ signal: "s2-accept",
2436
+ outcome: "accepted",
2169
2437
  });
2170
2438
  }
2171
2439
  const previousLevel = this.agent.state.thinkingLevel;
@@ -2175,21 +2443,44 @@ export class AgentSession {
2175
2443
  this.sessionManager.appendThinkingLevelChange(resolved);
2176
2444
  this._emit({ type: "thinking_level_changed", level: resolved });
2177
2445
  }
2446
+ _getRepositoryRouterLearningPaths() {
2447
+ if (this._repositoryRouterLearningPaths === undefined) {
2448
+ this._repositoryRouterLearningPaths = getRepositoryRouterLearningPaths(this._cwd);
2449
+ }
2450
+ return this._repositoryRouterLearningPaths;
2451
+ }
2452
+ _appendRouterFeedback(record) {
2453
+ appendRouterFeedbackRecord(record, {
2454
+ enabled: this.settingsManager.getReasoningRouterLearningEnabled(),
2455
+ ledgerPath: this.settingsManager.getReasoningRouterLearningFeedbackLedgerPath() ??
2456
+ this._getRepositoryRouterLearningPaths().ledgerPath,
2457
+ });
2458
+ }
2459
+ _recordUserThinkingOverride(acceptedLevel) {
2460
+ const decision = this._lastAutoRouterDecision;
2461
+ this._lastAutoRouterDecision = undefined;
2462
+ if (!this.settingsManager.getReasoningRouterLearningEnabled() || !decision || acceptedLevel === "off")
2463
+ return;
2464
+ const resolvedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(decision.resolvedLevel);
2465
+ const acceptedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(acceptedLevel);
2466
+ const outcome = acceptedIndex > resolvedIndex ? "up" : acceptedIndex < resolvedIndex ? "down" : "same";
2467
+ this._appendRouterFeedback({
2468
+ ...decision,
2469
+ acceptedLevel,
2470
+ signal: "s1-override",
2471
+ outcome,
2472
+ });
2473
+ }
2178
2474
  /**
2179
- * Loads and strictly validates the compiled reasoning-router bias snapshot
2180
- * for the opt-in v4 learning path (Goal 010 Lane I), at most once per
2181
- * session ("pinned"): the first call attempts the read and caches whatever
2182
- * it finds (including a `null` miss/failure); every later call in the same
2183
- * session reuses that cached result without touching disk again. Returns
2184
- * `null` when no file exists at the configured/default path, the file
2185
- * cannot be read, or its contents fail `parseRouterBiasSnapshot`'s schema
2186
- * validation -- never throws.
2475
+ * Strictly load the compiled bias snapshot once per session. The first call
2476
+ * pins either a valid snapshot or a null miss; later calls never touch disk.
2187
2477
  */
2188
2478
  _getReasoningRouterBiasSnapshot() {
2189
2479
  if (this._reasoningRouterBiasSnapshotLoaded)
2190
2480
  return this._reasoningRouterBiasSnapshot;
2191
2481
  this._reasoningRouterBiasSnapshotLoaded = true;
2192
- const path = this.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ?? getDefaultRouterBiasSnapshotPath();
2482
+ const path = this.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ??
2483
+ this._getRepositoryRouterLearningPaths().biasSnapshotPath;
2193
2484
  try {
2194
2485
  if (existsSync(path)) {
2195
2486
  this._reasoningRouterBiasSnapshot = parseRouterBiasSnapshot(readFileSync(path, "utf-8"));
@@ -2200,29 +2491,6 @@ export class AgentSession {
2200
2491
  }
2201
2492
  return this._reasoningRouterBiasSnapshot;
2202
2493
  }
2203
- /**
2204
- * Locally derives the same three bounded, privacy-safe feedback-ledger
2205
- * features (`lenBucket`, `hadFence`, `hadDiff`) the v4 classifier computes
2206
- * internally (reasoning-router-v4.ts keeps those helpers file-private, so
2207
- * they cannot be imported here). Mirrors reasoning-router-v4.ts's
2208
- * length-bucket, `hasCodeFence`, and `hasDiffMarkers` helpers. Operates on the same trimmed prompt text
2209
- * the classifier scores; never returns raw prompt content or its exact
2210
- * length, only the clamped [0,7] bucket and two booleans.
2211
- */
2212
- _deriveRouterFeedbackFeatures(promptText) {
2213
- const trimmed = promptText.trim();
2214
- let lenBucket = 0;
2215
- let remaining = trimmed.length + 1;
2216
- while (remaining > 1 && lenBucket < 7) {
2217
- remaining >>= 1;
2218
- lenBucket++;
2219
- }
2220
- const hadFence = trimmed.includes("```");
2221
- const hadDiff = /^@@[^\n]*@@/m.test(trimmed) ||
2222
- /^diff --git /m.test(trimmed) ||
2223
- (/^\+(?!\+)/m.test(trimmed) && /^-(?!-)/m.test(trimmed));
2224
- return { lenBucket: lenBucket, hadFence, hadDiff };
2225
- }
2226
2494
  /**
2227
2495
  * Context-pressure band 0..3 from the projected token estimate over the
2228
2496
  * model context window (reuses estimateProjectedContextTokens). 0..<0.5,
@@ -2298,20 +2566,7 @@ export class AgentSession {
2298
2566
  // Map confidenceBand -> numeric confidence for the resolver's threshold gate
2299
2567
  const confidenceMap = { low: 0.5, medium: 0.75, high: 0.95 };
2300
2568
  const confidence = confidenceMap[hint.confidenceBand] ?? 0.5;
2301
- // Map taskClass -> ThinkingLevel via the same rule table the resolver uses
2302
- const levelMap = {
2303
- trivial: "minimal",
2304
- "simple-edit": "low",
2305
- "code-gen": "medium",
2306
- debug: "high",
2307
- refactor: "high",
2308
- review: "high",
2309
- plan: "xhigh",
2310
- };
2311
- const level = levelMap[hint.taskClass];
2312
- if (level === undefined)
2313
- return null;
2314
- return { level, confidence };
2569
+ return { level: TASK_CLASS_THINKING_LEVELS_V4[hint.taskClass], confidence };
2315
2570
  }
2316
2571
  /**
2317
2572
  * Cycle to next thinking level.
@@ -2324,7 +2579,12 @@ export class AgentSession {
2324
2579
  const currentIndex = levels.indexOf(this.thinkingLevel);
2325
2580
  const nextIndex = (currentIndex + 1) % levels.length;
2326
2581
  const nextLevel = levels[nextIndex];
2582
+ const overridesAuto = this._thinkingMode === "auto";
2327
2583
  this.setThinkingLevel(nextLevel);
2584
+ if (overridesAuto) {
2585
+ this.setThinkingMode("manual");
2586
+ this._recordUserThinkingOverride(nextLevel);
2587
+ }
2328
2588
  return nextLevel;
2329
2589
  }
2330
2590
  /**
@@ -2340,7 +2600,7 @@ export class AgentSession {
2340
2600
  * Check if current model supports thinking/reasoning.
2341
2601
  */
2342
2602
  supportsThinking() {
2343
- return !!this.model?.reasoning;
2603
+ return Boolean(this.model?.reasoning);
2344
2604
  }
2345
2605
  _getThinkingLevelForModelSwitch(explicitLevel) {
2346
2606
  if (explicitLevel !== undefined) {
@@ -2490,8 +2750,8 @@ export class AgentSession {
2490
2750
  * @param customInstructions Optional instructions for the compaction summary
2491
2751
  */
2492
2752
  async compact(customInstructions) {
2493
- this._disconnectFromAgent();
2494
2753
  await this.abort();
2754
+ this._disconnectFromAgent();
2495
2755
  this._compactionAbortController = new AbortController();
2496
2756
  this._emit({ type: "compaction_start", reason: "manual" });
2497
2757
  let committedCompaction = false;
@@ -2554,7 +2814,7 @@ export class AgentSession {
2554
2814
  throw new Error("Compaction cancelled");
2555
2815
  }
2556
2816
  const compactionResult = {
2557
- summary: redactSensitiveText(summary),
2817
+ summary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),
2558
2818
  firstKeptEntryId,
2559
2819
  tokensBefore,
2560
2820
  details,
@@ -2582,6 +2842,11 @@ export class AgentSession {
2582
2842
  const message = error instanceof Error ? error.message : String(error);
2583
2843
  const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
2584
2844
  const stale = /stale|session changed during compaction|already compacted/i.test(message);
2845
+ let compactionCode = "failed";
2846
+ if (aborted)
2847
+ compactionCode = "aborted";
2848
+ else if (stale)
2849
+ compactionCode = "stale";
2585
2850
  const timestamp = new Date().toISOString();
2586
2851
  this._publishTermination(classifySessionTermination({
2587
2852
  sessionId: this.sessionId,
@@ -2589,7 +2854,7 @@ export class AgentSession {
2589
2854
  timestamp,
2590
2855
  source: "observed",
2591
2856
  message: this._terminationMessage(message, "Manual compaction failed."),
2592
- cause: { area: "compaction", code: aborted ? "aborted" : stale ? "stale" : "failed" },
2857
+ cause: { area: "compaction", code: compactionCode },
2593
2858
  sideEffects: committedCompaction ? "confirmed" : "none",
2594
2859
  ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
2595
2860
  }));
@@ -2621,11 +2886,46 @@ export class AgentSession {
2621
2886
  abortBranchSummary() {
2622
2887
  this._branchSummaryAbortController?.abort();
2623
2888
  }
2889
+ /** True when the transcript carries image content that forces a vision-route turn. */
2890
+ _transcriptHasImages(messages) {
2891
+ return messages.some((m) => {
2892
+ const content = m.content;
2893
+ if (!Array.isArray(content))
2894
+ return false;
2895
+ return content.some((part) => {
2896
+ return typeof part === "object" && part !== null && part.type === "image";
2897
+ });
2898
+ });
2899
+ }
2900
+ /**
2901
+ * Effective context window for the upcoming turn.
2902
+ * Image-bearing turns with a text-only session model are auto-routed to the
2903
+ * vision model (gpt-5.6-luna, 400K) — that window is the real limit. Text-only
2904
+ * turns keep the session model's (typically much larger) window.
2905
+ */
2906
+ _effectiveTurnContextWindow(pendingMessages, sessionWindow) {
2907
+ // Vision routing keys off the full transcript, not just the pending turn:
2908
+ // images retained in history keep every subsequent request on the vision model.
2909
+ const hasImages = this._transcriptHasImages(this.agent.state.messages) || this._transcriptHasImages(pendingMessages);
2910
+ const model = this.model;
2911
+ if (!hasImages || !model || (model.input ?? []).includes("image")) {
2912
+ return sessionWindow;
2913
+ }
2914
+ return Math.min(sessionWindow, getVisionRouteModel(model).contextWindow);
2915
+ }
2624
2916
  async _checkProjectedCompaction(pendingMessages) {
2625
2917
  const settings = this.settingsManager.getCompactionSettings();
2626
2918
  if (!settings.enabled || pendingMessages.length === 0)
2627
2919
  return false;
2628
- const contextWindow = this.model?.contextWindow ?? 0;
2920
+ const sessionWindow = this.model?.contextWindow ?? 0;
2921
+ if (sessionWindow <= 0)
2922
+ return false;
2923
+ // Image-bearing turns are served by the auto-routed vision model, not the
2924
+ // session model. Its window (not the session model's) is the real limit the
2925
+ // provider enforces, so threshold compaction must use it or the vision
2926
+ // request overflows (context_length_exceeded) before deepseek-based
2927
+ // compaction would fire.
2928
+ const contextWindow = this._effectiveTurnContextWindow(pendingMessages, sessionWindow);
2629
2929
  if (contextWindow <= 0)
2630
2930
  return false;
2631
2931
  const messages = [...this.agent.state.messages, ...pendingMessages];
@@ -2640,7 +2940,7 @@ export class AgentSession {
2640
2940
  }
2641
2941
  const decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);
2642
2942
  if (decision.compact) {
2643
- return await this._runThresholdCompaction(decision.emergency);
2943
+ return this._runThresholdCompaction(decision.emergency);
2644
2944
  }
2645
2945
  return false;
2646
2946
  }
@@ -2668,6 +2968,13 @@ export class AgentSession {
2668
2968
  // to a larger-context model (e.g. codex) - the overflow error from the old model
2669
2969
  // shouldn't trigger compaction for the new model.
2670
2970
  const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
2971
+ // Image-bearing turns are auto-routed to a vision-capable model (gpt-5.6-luna)
2972
+ // while the session model stays text-only (deepseek). An overflow from that
2973
+ // routed model must still count as the session's own overflow: compaction is
2974
+ // the fix, and the raw context_length_exceeded error must never reach the user.
2975
+ const visionRouteOverflow = this.model != null &&
2976
+ !(this.model.input ?? []).includes("image") &&
2977
+ isVisionRouteModel({ provider: assistantMessage.provider, id: assistantMessage.model });
2671
2978
  // Skip compaction checks if this assistant message is older than the latest
2672
2979
  // compaction boundary. This prevents a stale pre-compaction usage/error
2673
2980
  // from retriggering compaction on the first prompt after compaction.
@@ -2677,26 +2984,26 @@ export class AgentSession {
2677
2984
  return false;
2678
2985
  }
2679
2986
  // Case 1: Overflow - LLM returned context overflow error
2680
- if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
2681
- if (this._overflowRecoveryAttempted) {
2987
+ if ((sameModel || visionRouteOverflow) && isContextOverflow(assistantMessage, contextWindow)) {
2988
+ if (this._overflowRecoveryAttempts >= MAX_OVERFLOW_RECOVERY_ATTEMPTS) {
2682
2989
  this._emit({
2683
2990
  type: "compaction_end",
2684
2991
  reason: "overflow",
2685
2992
  result: undefined,
2686
2993
  aborted: false,
2687
2994
  willRetry: false,
2688
- errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
2995
+ errorMessage: "Context overflow recovery failed after two staged compact-and-retry attempts. Reduce the latest input or switch to a model with a larger effective context window.",
2689
2996
  });
2690
2997
  return false;
2691
2998
  }
2692
- this._overflowRecoveryAttempted = true;
2999
+ this._overflowRecoveryAttempts++;
2693
3000
  // Remove the error message from agent state (it IS saved to session for history,
2694
3001
  // but we don't want it in context for the retry)
2695
3002
  const messages = this.agent.state.messages;
2696
3003
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
2697
3004
  this.agent.state.messages = messages.slice(0, -1);
2698
3005
  }
2699
- return await this._runAutoCompaction("overflow", true);
3006
+ return this._runAutoCompaction("overflow", true);
2700
3007
  }
2701
3008
  // Case 2: Threshold - context is getting large
2702
3009
  // For error messages (no usage data), estimate from last successful response.
@@ -2723,15 +3030,28 @@ export class AgentSession {
2723
3030
  }
2724
3031
  const decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);
2725
3032
  if (decision.compact) {
2726
- return await this._runThresholdCompaction(decision.emergency);
3033
+ return this._runThresholdCompaction(decision.emergency);
2727
3034
  }
2728
3035
  return false;
2729
3036
  }
3037
+ _overflowCompactionSettings(settings, attempt) {
3038
+ if (attempt < MAX_OVERFLOW_RECOVERY_ATTEMPTS)
3039
+ return settings;
3040
+ return {
3041
+ ...settings,
3042
+ reserveTokens: Math.min(settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
3043
+ reservedOutputTokens: Math.min(settings.reservedOutputTokens ?? settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
3044
+ keepRecentTokens: Math.min(settings.keepRecentTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
3045
+ };
3046
+ }
2730
3047
  /**
2731
3048
  * Internal: Run auto-compaction with events.
2732
3049
  */
2733
3050
  async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
2734
- const settings = this.settingsManager.getCompactionSettings();
3051
+ const configuredSettings = this.settingsManager.getCompactionSettings();
3052
+ const settings = reason === "overflow"
3053
+ ? this._overflowCompactionSettings(configuredSettings, this._overflowRecoveryAttempts)
3054
+ : configuredSettings;
2735
3055
  this._emit({ type: "compaction_start", reason });
2736
3056
  this._autoCompactionAbortController = new AbortController();
2737
3057
  try {
@@ -2837,7 +3157,7 @@ export class AgentSession {
2837
3157
  return false;
2838
3158
  }
2839
3159
  const result = {
2840
- summary: redactSensitiveText(summary),
3160
+ summary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),
2841
3161
  firstKeptEntryId,
2842
3162
  tokensBefore,
2843
3163
  details,
@@ -2953,6 +3273,9 @@ export class AgentSession {
2953
3273
  this._resourceLoader.extendResources(extensionPaths);
2954
3274
  this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
2955
3275
  this.agent.state.systemPrompt = this._baseSystemPrompt;
3276
+ this.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;
3277
+ this.agent.state.systemPromptCacheBoundaryBypass = false;
3278
+ this._recordPromptCachePlan("resource-reload");
2956
3279
  }
2957
3280
  buildExtensionResourcePaths(entries) {
2958
3281
  return entries.map((entry) => {
@@ -3108,6 +3431,54 @@ export class AgentSession {
3108
3431
  },
3109
3432
  });
3110
3433
  }
3434
+ /**
3435
+ * Connect configured MCP servers and register their tools for this session.
3436
+ *
3437
+ * Connection is lazy by construction: nothing is spawned until this is called,
3438
+ * and a server that fails to start is reported in the returned status instead
3439
+ * of taking the session down. Calling it twice replaces the previously
3440
+ * attached MCP tools rather than duplicating them.
3441
+ *
3442
+ * Returns per-server status so a caller can surface failures; an empty
3443
+ * configuration returns an empty array without spawning anything.
3444
+ */
3445
+ async attachMcpServers(options) {
3446
+ const servers = options?.servers ?? loadMcpServerConfigs(this._cwd);
3447
+ this._mcpManager?.close();
3448
+ this._mcpManager = undefined;
3449
+ this._customTools = this._customTools.filter((definition) => !this._mcpToolNames.has(definition.name));
3450
+ this._mcpToolNames = new Set();
3451
+ if (servers.length === 0) {
3452
+ this._refreshToolRegistry();
3453
+ return [];
3454
+ }
3455
+ const manager = new McpManager({
3456
+ servers,
3457
+ cwd: this._cwd,
3458
+ clientInfo: { name: APP_NAME, version: VERSION },
3459
+ callTimeoutMs: options?.callTimeoutMs,
3460
+ });
3461
+ this._mcpManager = manager;
3462
+ const definitions = await manager.listToolDefinitions();
3463
+ // A builtin always wins a name collision; MCP must never shadow `bash`.
3464
+ const usable = definitions.filter((definition) => !this._baseToolDefinitions.has(definition.name));
3465
+ this._mcpToolNames = new Set(usable.map((definition) => definition.name));
3466
+ this._customTools = [...this._customTools, ...usable];
3467
+ this._refreshToolRegistry();
3468
+ return manager.status();
3469
+ }
3470
+ /** Status of MCP servers attached to this session. Empty when none were attached. */
3471
+ mcpServerStatus() {
3472
+ return this._mcpManager?.status() ?? [];
3473
+ }
3474
+ /**
3475
+ * Ping attached MCP servers and return fresh status, so callers (e.g. the
3476
+ * status rail) can keep displaying real connectivity instead of the last
3477
+ * handshake result. Resolves to [] when no manager is attached.
3478
+ */
3479
+ mcpCheckHealth(options) {
3480
+ return this._mcpManager?.checkHealth(options) ?? Promise.resolve([]);
3481
+ }
3111
3482
  _refreshToolRegistry(options) {
3112
3483
  const previousRegistryNames = new Set(this._toolRegistry.keys());
3113
3484
  const previousActiveToolNames = this.getActiveToolNames();
@@ -3128,15 +3499,17 @@ export class AgentSession {
3128
3499
  throw new Error(`loadout extension tool shadows builtin: ${shadowedBuiltins.join(", ")}`);
3129
3500
  }
3130
3501
  }
3131
- const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
3132
- .filter(([name]) => isAllowedTool(name))
3133
- .map(([name, definition]) => [
3134
- name,
3135
- {
3136
- definition,
3137
- sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
3138
- },
3139
- ]));
3502
+ const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries()).flatMap(([name, definition]) => isAllowedTool(name)
3503
+ ? [
3504
+ [
3505
+ name,
3506
+ {
3507
+ definition,
3508
+ sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
3509
+ },
3510
+ ],
3511
+ ]
3512
+ : []));
3140
3513
  for (const tool of allCustomTools) {
3141
3514
  definitionRegistry.set(tool.definition.name, {
3142
3515
  definition: tool.definition,
@@ -3144,26 +3517,29 @@ export class AgentSession {
3144
3517
  });
3145
3518
  }
3146
3519
  this._toolDefinitions = definitionRegistry;
3147
- this._toolPromptSnippets = new Map(Array.from(definitionRegistry.values())
3148
- .map(({ definition }) => {
3520
+ this._toolPromptSnippets = new Map(Array.from(definitionRegistry.values()).flatMap(({ definition }) => {
3149
3521
  const snippet = this._normalizePromptSnippet(definition.promptSnippet);
3150
- return snippet ? [definition.name, snippet] : undefined;
3151
- })
3152
- .filter((entry) => entry !== undefined));
3153
- this._toolPromptGuidelines = new Map(Array.from(definitionRegistry.values())
3154
- .map(({ definition }) => {
3522
+ return snippet ? [[definition.name, snippet]] : [];
3523
+ }));
3524
+ this._toolPromptGuidelines = new Map(Array.from(definitionRegistry.values()).flatMap(({ definition }) => {
3155
3525
  const guidelines = this._normalizePromptGuidelines(definition.promptGuidelines);
3156
- return guidelines.length > 0 ? [definition.name, guidelines] : undefined;
3157
- })
3158
- .filter((entry) => entry !== undefined));
3526
+ return guidelines.length > 0 ? [[definition.name, guidelines]] : [];
3527
+ }));
3528
+ // Tools contributed by extensions the harness itself loads (`<builtin:*>`),
3529
+ // as opposed to user/project/SDK extensions.
3530
+ const builtinExtensionToolNames = new Set(allCustomTools.flatMap((tool) => (tool.sourceInfo.path.startsWith("<builtin:") ? [tool.definition.name] : [])));
3159
3531
  const runner = this._extensionRunner;
3160
3532
  const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);
3161
- const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values())
3162
- .filter((definition) => isAllowedTool(definition.name))
3163
- .map((definition) => ({
3164
- definition,
3165
- sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, { source: "builtin" }),
3166
- })), runner);
3533
+ const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values()).flatMap((definition) => isAllowedTool(definition.name)
3534
+ ? [
3535
+ {
3536
+ definition,
3537
+ sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, {
3538
+ source: "builtin",
3539
+ }),
3540
+ },
3541
+ ]
3542
+ : []), runner);
3167
3543
  const toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool]));
3168
3544
  for (const tool of wrappedExtensionTools) {
3169
3545
  toolRegistry.set(tool.name, tool);
@@ -3186,7 +3562,14 @@ export class AgentSession {
3186
3562
  }
3187
3563
  }
3188
3564
  else if (options?.includeAllExtensionTools) {
3565
+ // "Keep extension tools on" is about the *user's* extensions. Tools shipped
3566
+ // by a built-in extension (e.g. update_todo) are part of the harness's own
3567
+ // tool surface, so they follow the built-in policy: when the caller asked
3568
+ // for no built-in tools, they stay off too.
3569
+ const builtinToolsDisabled = options.activeToolNames?.length === 0;
3189
3570
  for (const tool of wrappedExtensionTools) {
3571
+ if (builtinToolsDisabled && builtinExtensionToolNames.has(tool.name))
3572
+ continue;
3190
3573
  nextActiveToolNames.push(tool.name);
3191
3574
  }
3192
3575
  }
@@ -3217,12 +3600,14 @@ export class AgentSession {
3217
3600
  canWritePath: (path) => loadoutAccessGuard({ operation: "write", toolName: "write", path }).allowed,
3218
3601
  }
3219
3602
  : {};
3220
- const baseToolDefinitions = this._baseToolsOverride
3221
- ? Object.fromEntries(Object.entries(this._baseToolsOverride).map(([name, tool]) => [
3222
- name,
3223
- createToolDefinitionFromAgentTool(tool),
3224
- ]))
3225
- : createAllToolDefinitions(this._cwd, {
3603
+ const baseToolDefinitions = (() => {
3604
+ if (this._baseToolsOverride) {
3605
+ return Object.fromEntries(Object.entries(this._baseToolsOverride).map(([name, tool]) => [
3606
+ name,
3607
+ createToolDefinitionFromAgentTool(tool),
3608
+ ]));
3609
+ }
3610
+ return createAllToolDefinitions(this._cwd, {
3226
3611
  read: { autoResizeImages, ...loadoutReadOptions },
3227
3612
  bash: {
3228
3613
  commandPrefix: shellCommandPrefix,
@@ -3255,6 +3640,7 @@ export class AgentSession {
3255
3640
  edit: loadoutWriteOptions,
3256
3641
  write: loadoutWriteOptions,
3257
3642
  });
3643
+ })();
3258
3644
  this._baseToolDefinitions = new Map(Object.entries(baseToolDefinitions).map(([name, tool]) => [name, tool]));
3259
3645
  const extensionsResult = this._resourceLoader.getExtensions();
3260
3646
  if (options.flagValues) {
@@ -3301,9 +3687,6 @@ export class AgentSession {
3301
3687
  // =========================================================================
3302
3688
  // Auto-Retry
3303
3689
  // =========================================================================
3304
- _isNonRetryableProviderLimitError(errorMessage) {
3305
- return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(errorMessage);
3306
- }
3307
3690
  /**
3308
3691
  * Check if an error is retryable (overloaded, rate limit, server errors).
3309
3692
  * Context overflow errors are NOT retryable (handled by compaction instead).
@@ -3316,8 +3699,11 @@ export class AgentSession {
3316
3699
  if (isContextOverflow(message, contextWindow))
3317
3700
  return false;
3318
3701
  const err = message.errorMessage;
3319
- if (this._isNonRetryableProviderLimitError(err))
3320
- return false;
3702
+ // Billing/quota exhaustion: useless to retry the same model until the
3703
+ // cycle resets, but the turn may still be saved by failing over to the
3704
+ // next candidate, so let _prepareRetry run its failover path.
3705
+ if (isQuotaExhaustionMessage(err))
3706
+ return true;
3321
3707
  // Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
3322
3708
  return isTransientProviderErrorMessage(err);
3323
3709
  }
@@ -3363,7 +3749,9 @@ export class AgentSession {
3363
3749
  return;
3364
3750
  const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
3365
3751
  const next = this._modelRegistry.find(c.provider, c.id);
3366
- return !!next && this._modelRegistry.hasConfiguredAuth(next);
3752
+ if (!next)
3753
+ return false;
3754
+ return this._modelRegistry.hasConfiguredAuth(next);
3367
3755
  });
3368
3756
  if (!pick) {
3369
3757
  throw new Error(stickySafetyBlockMessage(current.id, current.provider));
@@ -3385,8 +3773,11 @@ export class AgentSession {
3385
3773
  const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3386
3774
  if (!resilience.autoFailoverOnSafetyStop)
3387
3775
  return undefined;
3388
- if (!isContentSafetyStopMessage(message.errorMessage))
3776
+ // Fire on safety-stop FPs AND on billing/quota exhaustion: both mean the
3777
+ // current model cannot finish this turn and same-model retry is useless.
3778
+ if (!isContentSafetyStopMessage(message.errorMessage) && !isQuotaExhaustionMessage(message.errorMessage)) {
3389
3779
  return undefined;
3780
+ }
3390
3781
  const current = this.model;
3391
3782
  // v10.0-Ω: do NOT gate on isStickySafetyModel(source).
3392
3783
  // claude-opus-5 emits the same stop_reason=refusal FP; same-model retry is useless.
@@ -3398,7 +3789,9 @@ export class AgentSession {
3398
3789
  if (this._refusedModels.has(`${c.provider}/${c.id}`))
3399
3790
  return false;
3400
3791
  const next = this._modelRegistry.find(c.provider, c.id);
3401
- return !!next && this._modelRegistry.hasConfiguredAuth(next);
3792
+ if (!next)
3793
+ return false;
3794
+ return this._modelRegistry.hasConfiguredAuth(next);
3402
3795
  });
3403
3796
  if (!pick)
3404
3797
  return undefined;
@@ -3623,6 +4016,8 @@ export class AgentSession {
3623
4016
  let summaryDetails;
3624
4017
  if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
3625
4018
  const model = this.model;
4019
+ if (!model)
4020
+ throw new Error("No model available for summarization");
3626
4021
  const { apiKey, headers } = await this._getRequiredRequestAuth(model);
3627
4022
  const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
3628
4023
  const result = await generateBranchSummary(entriesToSummarize, {
@@ -3664,13 +4059,18 @@ export class AgentSession {
3664
4059
  else if (targetEntry.type === "custom_message") {
3665
4060
  // Custom message: leaf = parent (null if root), text goes to editor
3666
4061
  newLeafId = targetEntry.parentId;
3667
- editorText =
3668
- typeof targetEntry.content === "string"
3669
- ? targetEntry.content
3670
- : targetEntry.content
3671
- .filter((c) => c.type === "text")
3672
- .map((c) => c.text)
3673
- .join("");
4062
+ if (typeof targetEntry.content === "string") {
4063
+ editorText = targetEntry.content;
4064
+ }
4065
+ else {
4066
+ editorText = targetEntry.content
4067
+ .flatMap((item) => {
4068
+ if (item.type === "text")
4069
+ return [item.text];
4070
+ return [];
4071
+ })
4072
+ .join("");
4073
+ }
3674
4074
  }
3675
4075
  else {
3676
4076
  // Non-user message: leaf = selected node
@@ -3741,8 +4141,7 @@ export class AgentSession {
3741
4141
  return content;
3742
4142
  if (Array.isArray(content)) {
3743
4143
  return content
3744
- .filter((c) => c.type === "text")
3745
- .map((c) => c.text)
4144
+ .flatMap((item) => (item.type === "text" && typeof item.text === "string" ? [item.text] : []))
3746
4145
  .join("");
3747
4146
  }
3748
4147
  return "";
@@ -3772,6 +4171,7 @@ export class AgentSession {
3772
4171
  totalCost += assistantMsg.usage.cost.total;
3773
4172
  }
3774
4173
  }
4174
+ const providerEligibleInputTokens = totalInput + totalCacheRead + totalCacheWrite;
3775
4175
  return {
3776
4176
  sessionFile: this.sessionFile,
3777
4177
  sessionId: this.sessionId,
@@ -3787,6 +4187,14 @@ export class AgentSession {
3787
4187
  cacheWrite: totalCacheWrite,
3788
4188
  total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
3789
4189
  },
4190
+ promptCache: {
4191
+ providerEligibleInputTokens,
4192
+ providerHitRate: providerEligibleInputTokens > 0 ? totalCacheRead / providerEligibleInputTokens : 0,
4193
+ keyChanges: this._promptCacheKeyChanges,
4194
+ boundaryBypasses: this._promptCacheBoundaryBypasses,
4195
+ stablePrefixCharacters: this._promptCacheStablePrefixCharacters,
4196
+ ...(this._promptCacheLastBreakReason ? { lastBreakReason: this._promptCacheLastBreakReason } : {}),
4197
+ },
3790
4198
  cost: totalCost,
3791
4199
  contextUsage: this.getContextUsage(),
3792
4200
  };
@@ -3845,7 +4253,7 @@ export class AgentSession {
3845
4253
  theme,
3846
4254
  cwd: this.sessionManager.getCwd(),
3847
4255
  });
3848
- return await exportSessionToHtml(this.sessionManager, this.state, {
4256
+ return exportSessionToHtml(this.sessionManager, this.state, {
3849
4257
  outputPath,
3850
4258
  themeName,
3851
4259
  toolRenderer,
@@ -3891,18 +4299,18 @@ export class AgentSession {
3891
4299
  * @returns Text content, or undefined if no assistant message exists
3892
4300
  */
3893
4301
  getLastAssistantText() {
3894
- const lastAssistant = this.messages
3895
- .slice()
3896
- .reverse()
3897
- .find((m) => {
3898
- if (m.role !== "assistant")
3899
- return false;
3900
- const msg = m;
4302
+ let lastAssistant;
4303
+ for (let index = this.messages.length - 1; index >= 0; index -= 1) {
4304
+ const message = this.messages[index];
4305
+ if (message?.role !== "assistant")
4306
+ continue;
4307
+ const assistant = message;
3901
4308
  // Skip aborted messages with no content
3902
- if (msg.stopReason === "aborted" && msg.content.length === 0)
3903
- return false;
3904
- return true;
3905
- });
4309
+ if (assistant.stopReason === "aborted" && assistant.content.length === 0)
4310
+ continue;
4311
+ lastAssistant = assistant;
4312
+ break;
4313
+ }
3906
4314
  if (!lastAssistant)
3907
4315
  return undefined;
3908
4316
  let text = "";