open-multi-agent-kit 0.95.0 → 0.95.2

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 (281) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -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 +169 -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 +97 -29
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +583 -199
  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-plan-cache-keys.d.ts.map +1 -1
  68. package/dist/core/context-budget-v2-plan-cache-keys.js +39 -5
  69. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  70. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  71. package/dist/core/context-budget-v2-planner.js +17 -14
  72. package/dist/core/context-budget-v2-planner.js.map +1 -1
  73. package/dist/core/context-budget-v2-types.d.ts +1 -0
  74. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  75. package/dist/core/context-budget-v2-types.js.map +1 -1
  76. package/dist/core/extensions/loader.d.ts.map +1 -1
  77. package/dist/core/extensions/loader.js +45 -22
  78. package/dist/core/extensions/loader.js.bak-sharedjiti-20260812-111246 +553 -0
  79. package/dist/core/extensions/loader.js.map +1 -1
  80. package/dist/core/extensions/types.d.ts +4 -0
  81. package/dist/core/extensions/types.d.ts.map +1 -1
  82. package/dist/core/extensions/types.js.map +1 -1
  83. package/dist/core/headroom.d.ts.map +1 -1
  84. package/dist/core/headroom.js +7 -2
  85. package/dist/core/headroom.js.map +1 -1
  86. package/dist/core/mcp/client.d.ts +105 -0
  87. package/dist/core/mcp/client.d.ts.map +1 -0
  88. package/dist/core/mcp/client.js +198 -0
  89. package/dist/core/mcp/client.js.map +1 -0
  90. package/dist/core/mcp/config.d.ts +23 -0
  91. package/dist/core/mcp/config.d.ts.map +1 -0
  92. package/dist/core/mcp/config.js +89 -0
  93. package/dist/core/mcp/config.js.map +1 -0
  94. package/dist/core/mcp/index.d.ts +15 -0
  95. package/dist/core/mcp/index.d.ts.map +1 -0
  96. package/dist/core/mcp/index.js +15 -0
  97. package/dist/core/mcp/index.js.map +1 -0
  98. package/dist/core/mcp/manager.d.ts +84 -0
  99. package/dist/core/mcp/manager.d.ts.map +1 -0
  100. package/dist/core/mcp/manager.js +173 -0
  101. package/dist/core/mcp/manager.js.map +1 -0
  102. package/dist/core/mcp/protocol.d.ts +59 -0
  103. package/dist/core/mcp/protocol.d.ts.map +1 -0
  104. package/dist/core/mcp/protocol.js +103 -0
  105. package/dist/core/mcp/protocol.js.map +1 -0
  106. package/dist/core/mcp/stdio-transport.d.ts +63 -0
  107. package/dist/core/mcp/stdio-transport.d.ts.map +1 -0
  108. package/dist/core/mcp/stdio-transport.js +132 -0
  109. package/dist/core/mcp/stdio-transport.js.map +1 -0
  110. package/dist/core/mcp/tools.d.ts +47 -0
  111. package/dist/core/mcp/tools.d.ts.map +1 -0
  112. package/dist/core/mcp/tools.js +102 -0
  113. package/dist/core/mcp/tools.js.map +1 -0
  114. package/dist/core/package-archive.d.ts +3 -0
  115. package/dist/core/package-archive.d.ts.map +1 -0
  116. package/dist/core/package-archive.js +161 -0
  117. package/dist/core/package-archive.js.map +1 -0
  118. package/dist/core/package-doctor-source-scan.d.ts +13 -0
  119. package/dist/core/package-doctor-source-scan.d.ts.map +1 -0
  120. package/dist/core/package-doctor-source-scan.js +124 -0
  121. package/dist/core/package-doctor-source-scan.js.map +1 -0
  122. package/dist/core/package-doctor.d.ts +41 -0
  123. package/dist/core/package-doctor.d.ts.map +1 -0
  124. package/dist/core/package-doctor.js +145 -0
  125. package/dist/core/package-doctor.js.map +1 -0
  126. package/dist/core/package-manager.d.ts +10 -1
  127. package/dist/core/package-manager.d.ts.map +1 -1
  128. package/dist/core/package-manager.js +175 -42
  129. package/dist/core/package-manager.js.map +1 -1
  130. package/dist/core/package-manifest.d.ts +26 -0
  131. package/dist/core/package-manifest.d.ts.map +1 -0
  132. package/dist/core/package-manifest.js +85 -0
  133. package/dist/core/package-manifest.js.map +1 -0
  134. package/dist/core/pi-compat.d.ts +5 -0
  135. package/dist/core/pi-compat.d.ts.map +1 -0
  136. package/dist/core/pi-compat.js +18 -0
  137. package/dist/core/pi-compat.js.map +1 -0
  138. package/dist/core/reasoning-router-bias.d.ts +10 -14
  139. package/dist/core/reasoning-router-bias.d.ts.map +1 -1
  140. package/dist/core/reasoning-router-bias.js +22 -15
  141. package/dist/core/reasoning-router-bias.js.map +1 -1
  142. package/dist/core/reasoning-router-v4.d.ts +9 -1
  143. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  144. package/dist/core/reasoning-router-v4.js +20 -5
  145. package/dist/core/reasoning-router-v4.js.map +1 -1
  146. package/dist/core/redaction.d.ts +2 -5
  147. package/dist/core/redaction.d.ts.map +1 -1
  148. package/dist/core/redaction.js +14 -1
  149. package/dist/core/redaction.js.map +1 -1
  150. package/dist/core/repository-learning-scope.d.ts +10 -0
  151. package/dist/core/repository-learning-scope.d.ts.map +1 -0
  152. package/dist/core/repository-learning-scope.js +42 -0
  153. package/dist/core/repository-learning-scope.js.map +1 -0
  154. package/dist/core/resource-loader.d.ts.map +1 -1
  155. package/dist/core/resource-loader.js +16 -1
  156. package/dist/core/resource-loader.js.map +1 -1
  157. package/dist/core/router-feedback-collector.d.ts +2 -1
  158. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  159. package/dist/core/router-feedback-collector.js +2 -2
  160. package/dist/core/router-feedback-collector.js.map +1 -1
  161. package/dist/core/runtime-provenance.d.ts +54 -0
  162. package/dist/core/runtime-provenance.d.ts.map +1 -0
  163. package/dist/core/runtime-provenance.js +115 -0
  164. package/dist/core/runtime-provenance.js.map +1 -0
  165. package/dist/core/sandbox/backend.d.ts.map +1 -1
  166. package/dist/core/sandbox/backend.js +39 -21
  167. package/dist/core/sandbox/backend.js.map +1 -1
  168. package/dist/core/sandbox/default-policy.d.ts +4 -5
  169. package/dist/core/sandbox/default-policy.d.ts.map +1 -1
  170. package/dist/core/sandbox/default-policy.js +9 -11
  171. package/dist/core/sandbox/default-policy.js.map +1 -1
  172. package/dist/core/session-bash-runtime.d.ts +4 -4
  173. package/dist/core/session-bash-runtime.d.ts.map +1 -1
  174. package/dist/core/session-bash-runtime.js +6 -5
  175. package/dist/core/session-bash-runtime.js.map +1 -1
  176. package/dist/core/session-compaction-service.d.ts.map +1 -1
  177. package/dist/core/session-compaction-service.js +7 -7
  178. package/dist/core/session-compaction-service.js.map +1 -1
  179. package/dist/core/session-manager.d.ts +4 -1
  180. package/dist/core/session-manager.d.ts.map +1 -1
  181. package/dist/core/session-manager.js +47 -7
  182. package/dist/core/session-manager.js.map +1 -1
  183. package/dist/core/settings-manager.d.ts +7 -4
  184. package/dist/core/settings-manager.d.ts.map +1 -1
  185. package/dist/core/settings-manager.js +11 -3
  186. package/dist/core/settings-manager.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/tool-definition-wrapper.d.ts.map +1 -1
  196. package/dist/core/tools/tool-definition-wrapper.js +15 -0
  197. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  198. package/dist/core/turn-metrics.d.ts +135 -0
  199. package/dist/core/turn-metrics.d.ts.map +1 -0
  200. package/dist/core/turn-metrics.js +261 -0
  201. package/dist/core/turn-metrics.js.map +1 -0
  202. package/dist/core/verified-bash-runtime.d.ts.map +1 -1
  203. package/dist/core/verified-bash-runtime.js +10 -4
  204. package/dist/core/verified-bash-runtime.js.map +1 -1
  205. package/dist/guardrails/replay-ledger-lock-owner.d.ts +7 -0
  206. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
  207. package/dist/guardrails/replay-ledger-lock-owner.js +39 -0
  208. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
  209. package/dist/index.d.ts +6 -1
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +6 -1
  212. package/dist/index.js.map +1 -1
  213. package/dist/main.d.ts.map +1 -1
  214. package/dist/main.js +18 -0
  215. package/dist/main.js.map +1 -1
  216. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/control-panel-layout.js +2 -2
  218. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  219. package/dist/modes/interactive/components/footer.d.ts +3 -0
  220. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  221. package/dist/modes/interactive/components/footer.js +40 -12
  222. package/dist/modes/interactive/components/footer.js.map +1 -1
  223. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  224. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  225. package/dist/modes/interactive/components/settings-selector.js +10 -0
  226. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  227. package/dist/modes/interactive/components/status-sidebar.d.ts +14 -2
  228. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  229. package/dist/modes/interactive/components/status-sidebar.js +106 -19
  230. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  231. package/dist/modes/interactive/interactive-mode.d.ts +10 -1
  232. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  233. package/dist/modes/interactive/interactive-mode.js +104 -17
  234. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  235. package/dist/modes/interactive/theme/dark.json +1 -1
  236. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  237. package/dist/modes/interactive/theme/theme.js +1 -0
  238. package/dist/modes/interactive/theme/theme.js.map +1 -1
  239. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  240. package/dist/modes/rpc/rpc-mode.js +1 -1
  241. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  242. package/docs/compaction.md +25 -3
  243. package/docs/containerization.md +14 -2
  244. package/docs/docs.json +8 -0
  245. package/docs/environment-variables.md +1 -1
  246. package/docs/extensions.md +75 -16
  247. package/docs/mcp.md +97 -0
  248. package/docs/metrics.md +87 -0
  249. package/docs/packages.md +25 -4
  250. package/docs/providers.md +1 -1
  251. package/docs/sdk.md +25 -4
  252. package/docs/settings.md +8 -7
  253. package/docs/usage.md +13 -3
  254. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  255. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  256. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  257. package/examples/extensions/gondolin/package-lock.json +2 -2
  258. package/examples/extensions/gondolin/package.json +1 -1
  259. package/examples/extensions/plannotator-approval-bridge.ts +83 -0
  260. package/examples/extensions/sandbox/package-lock.json +2 -2
  261. package/examples/extensions/sandbox/package.json +1 -1
  262. package/examples/extensions/subagent/README.md +21 -11
  263. package/examples/extensions/subagent/adaptive-agent-runtime.ts +9 -3
  264. package/examples/extensions/subagent/agents/planner.md +1 -3
  265. package/examples/extensions/subagent/agents/reviewer.md +1 -3
  266. package/examples/extensions/subagent/agents/scout.md +1 -3
  267. package/examples/extensions/subagent/agents/worker.md +0 -1
  268. package/examples/extensions/subagent/agents.test.ts +9 -0
  269. package/examples/extensions/subagent/agents.ts +11 -4
  270. package/examples/extensions/subagent/deadline-budget.test.ts +32 -0
  271. package/examples/extensions/subagent/deadline-budget.ts +25 -1
  272. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +10 -0
  273. package/examples/extensions/subagent/index.ts +234 -55
  274. package/examples/extensions/subagent/subagent-execution-policy.test.ts +27 -0
  275. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +167 -2
  276. package/examples/extensions/subagent/workflow-graph.test.ts +80 -0
  277. package/examples/extensions/subagent/workflow-graph.ts +89 -0
  278. package/examples/extensions/with-deps/package-lock.json +2 -2
  279. package/examples/extensions/with-deps/package.json +1 -1
  280. package/npm-shrinkwrap.json +253 -439
  281. package/package.json +8 -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,47 @@ 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
+ function isDisabledEnvValue(value) {
54
+ return value !== undefined && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());
55
+ }
56
+ /** Extract the first text block of a tool result, for failure classification only. */
57
+ function firstTextContent(result) {
58
+ const content = result?.content;
59
+ if (!Array.isArray(content))
60
+ return undefined;
61
+ for (const block of content) {
62
+ if (block && typeof block === "object" && block.type === "text") {
63
+ const text = block.text;
64
+ if (typeof text === "string")
65
+ return text;
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+ import { redactCredentialShapedContent } from "./compaction/transaction.js";
44
71
  import { findExactModelReferenceMatch } from "./model-resolver.js";
45
72
  import { expandPromptTemplate } from "./prompt-templates.js";
46
73
  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";
74
+ import { getBiasStepsForCell, parseRouterBiasSnapshot } from "./reasoning-router-bias.js";
75
+ import { classifyTaskV4, deriveRouterFeedbackFeaturesV4, resolveThinkingLevelV4WithUncertainty, TASK_CLASS_THINKING_LEVELS_V4, } from "./reasoning-router-v4.js";
76
+ import { redactSensitiveText, redactSensitiveTextForced } from "./redaction.js";
77
+ import { getRepositoryRouterLearningPaths } from "./repository-learning-scope.js";
78
+ import { appendRouterFeedbackRecord, ROUTER_FEEDBACK_LEVELS, } from "./router-feedback-collector.js";
51
79
  import { RunJournalStore } from "./run-journal-store.js";
52
80
  import { SessionBashRuntime } from "./session-bash-runtime.js";
53
81
  import { SessionBashService } from "./session-bash-service.js";
54
82
  import { SessionCompactionService } from "./session-compaction-service.js";
55
83
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
84
+ import { acquireSessionOwnerLeaseSync } from "./session-owner-lease.js";
56
85
  import { classifySessionTermination, } from "./session-termination.js";
57
86
  import { createSyntheticSourceInfo } from "./source-info.js";
58
- import { buildSystemPrompt } from "./system-prompt.js";
87
+ import { buildSystemPromptPlan } from "./system-prompt.js";
59
88
  import { createLocalBashOperations } from "./tools/bash.js";
60
89
  import { createAllToolDefinitions } from "./tools/index.js";
61
90
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
@@ -110,7 +139,13 @@ function pendingToolResultClass(name, args) {
110
139
  /** Standard thinking levels */
111
140
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
112
141
  function normalizeToolNames(toolNames) {
113
- return [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== ""))].sort();
142
+ return [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== ""))].sort((left, right) => {
143
+ if (left < right)
144
+ return -1;
145
+ if (left > right)
146
+ return 1;
147
+ return 0;
148
+ });
114
149
  }
115
150
  function toolNameSetsEqual(left, right) {
116
151
  const normalizedLeft = normalizeToolNames(left);
@@ -159,7 +194,8 @@ function clonePlainSnapshot(value, ancestors = new WeakSet(), copies = new WeakM
159
194
  ancestors.add(value);
160
195
  try {
161
196
  if (Array.isArray(value)) {
162
- const copy = new Array(value.length);
197
+ const copy = [];
198
+ copy.length = value.length;
163
199
  copies.set(value, copy);
164
200
  for (const key of Reflect.ownKeys(value)) {
165
201
  if (key === "length")
@@ -243,6 +279,7 @@ export class AgentSession {
243
279
  */
244
280
  _reasoningRouterBiasSnapshot = null;
245
281
  _reasoningRouterBiasSnapshotLoaded = false;
282
+ _lastAutoRouterDecision;
246
283
  /**
247
284
  * AdaptOrch advisory bridge (default-off, global-only opt-in via
248
285
  * `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily
@@ -285,7 +322,12 @@ export class AgentSession {
285
322
  _resourceLoader;
286
323
  _customTools;
287
324
  _baseToolDefinitions = new Map();
325
+ _mcpManager;
326
+ _mcpToolNames = new Set();
327
+ _turnMetricsSink;
328
+ _turnMetricsState;
288
329
  _cwd;
330
+ _repositoryRouterLearningPaths;
289
331
  _extensionRunnerRef;
290
332
  _initialActiveToolNames;
291
333
  _allowedToolNames;
@@ -302,6 +344,7 @@ export class AgentSession {
302
344
  _extensionErrorUnsubscriber;
303
345
  // Required durable run/audit chain, persisted next to the session file.
304
346
  _runJournalStore;
347
+ _ownedSessionOwnerLease;
305
348
  _activeRunId = null;
306
349
  _pendingRuntimeTerminationCause;
307
350
  _activeRunToolTermination;
@@ -326,7 +369,13 @@ export class AgentSession {
326
369
  _toolPromptGuidelines = new Map();
327
370
  // Base system prompt (without extension appends) - used to apply fresh appends each turn
328
371
  _baseSystemPrompt = "";
372
+ _baseSystemPromptCacheBoundary;
329
373
  _baseSystemPromptOptions;
374
+ _promptCacheKey;
375
+ _promptCacheKeyChanges = 0;
376
+ _promptCacheBoundaryBypasses = 0;
377
+ _promptCacheStablePrefixCharacters = 0;
378
+ _promptCacheLastBreakReason;
330
379
  constructor(config) {
331
380
  this.agent = config.agent;
332
381
  this.sessionManager = config.sessionManager;
@@ -389,31 +438,55 @@ export class AgentSession {
389
438
  recordCommit: () => this._recordCompactionCommitForHysteresis(),
390
439
  });
391
440
  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,
441
+ let ownerLease = this.sessionManager.getOwnerLease();
442
+ try {
443
+ if (sessionFile && !ownerLease) {
444
+ ownerLease = acquireSessionOwnerLeaseSync(sessionFile);
445
+ this._ownedSessionOwnerLease = ownerLease;
446
+ this.sessionManager.setOwnerLease(ownerLease);
447
+ }
448
+ this._runJournalStore = RunJournalStore.open({
449
+ ...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
450
+ sessionId: this.sessionManager.getSessionId(),
451
+ ...(ownerLease ? { ownerLease } : {}),
452
+ });
453
+ const startupRecords = this._runJournalStore.records;
454
+ const startupTerminal = startupRecords[startupRecords.length - 1];
455
+ if (startupTerminal?.event === "run_recovered") {
456
+ this._lastTermination = startupTerminal.termination;
457
+ }
458
+ // Always subscribe to agent events for internal handling
459
+ // (session persistence, extensions, auto-compaction, retry logic)
460
+ this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
461
+ this._installAgentToolHooks();
462
+ // Durable transcript-repair audit for a repair applied on open/resume.
463
+ if (this._transcriptRepair) {
464
+ this._invalidateContextBudgetCache({ type: "transcriptRepair" });
465
+ this._appendRunJournalAudit("transcript_repaired", {
466
+ insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
467
+ reason: this._transcriptRepair.reason,
468
+ });
469
+ }
470
+ this._buildRuntime({
471
+ activeToolNames: this._initialActiveToolNames,
472
+ includeAllExtensionTools: true,
411
473
  });
412
474
  }
413
- this._buildRuntime({
414
- activeToolNames: this._initialActiveToolNames,
415
- includeAllExtensionTools: true,
416
- });
475
+ catch (error) {
476
+ const ownedLease = this._ownedSessionOwnerLease;
477
+ if (ownedLease) {
478
+ try {
479
+ ownedLease.release();
480
+ if (this.sessionManager.getOwnerLease() === ownedLease)
481
+ this.sessionManager.setOwnerLease(undefined);
482
+ this._ownedSessionOwnerLease = undefined;
483
+ }
484
+ catch {
485
+ throw new Error("AgentSession initialization cleanup failed");
486
+ }
487
+ }
488
+ throw error;
489
+ }
417
490
  }
418
491
  /** Model registry for API key resolution and model discovery */
419
492
  get modelRegistry() {
@@ -564,9 +637,14 @@ export class AgentSession {
564
637
  }
565
638
  _classifyRunTermination(runId, event) {
566
639
  const timestamp = new Date().toISOString();
567
- const assistant = [...event.messages]
568
- .reverse()
569
- .find((message) => message.role === "assistant");
640
+ let assistant;
641
+ for (let index = event.messages.length - 1; index >= 0; index -= 1) {
642
+ const message = event.messages[index];
643
+ if (message?.role === "assistant") {
644
+ assistant = message;
645
+ break;
646
+ }
647
+ }
570
648
  let cause;
571
649
  let message;
572
650
  let sideEffects = this._sessionRiskLevel === "elevated" ? "possible" : "none";
@@ -595,6 +673,12 @@ export class AgentSession {
595
673
  cause = { area: "completed" };
596
674
  message = "Run completed.";
597
675
  }
676
+ let provider = this.model?.provider;
677
+ if (assistant?.provider)
678
+ provider = assistant.provider;
679
+ let model = this.model?.id;
680
+ if (assistant?.model)
681
+ model = assistant.model;
598
682
  return classifySessionTermination({
599
683
  sessionId: this.sessionId,
600
684
  runId,
@@ -603,12 +687,8 @@ export class AgentSession {
603
687
  message,
604
688
  cause,
605
689
  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 } : {}),
690
+ ...(provider ? { provider } : {}),
691
+ ...(model ? { model } : {}),
612
692
  ...(toolCallId ? { toolCallId } : {}),
613
693
  ...(toolName ? { toolName } : {}),
614
694
  });
@@ -637,11 +717,13 @@ export class AgentSession {
637
717
  const cause = this._runtimeFailureCause(error);
638
718
  const runId = this._activeRunId ?? `runtime-${randomUUID()}`;
639
719
  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.";
720
+ let message = "The AgentSession runtime failed before completing the run.";
721
+ if (cause.area === "persistence") {
722
+ message = "A required runtime persistence operation failed.";
723
+ }
724
+ else if (cause.area === "compaction") {
725
+ message = "Runtime compaction failed.";
726
+ }
645
727
  let termination = classifySessionTermination({
646
728
  sessionId: this.sessionId,
647
729
  runId,
@@ -993,9 +1075,13 @@ export class AgentSession {
993
1075
  ? "Finalized message was not found in agent state"
994
1076
  : "Finalized message is ambiguous in agent state");
995
1077
  }
1078
+ const matchingIndex = matchingIndexes[0];
1079
+ if (matchingIndex === undefined) {
1080
+ throw new Error("Finalized message was not found in agent state");
1081
+ }
996
1082
  const finalizedReplacement = createImmutableMessageSnapshot(replacement);
997
- const nextMessages = messages.slice();
998
- nextMessages[matchingIndexes[0]] = finalizedReplacement;
1083
+ const nextMessages = [...messages];
1084
+ nextMessages[matchingIndex] = finalizedReplacement;
999
1085
  this.agent.state.messages = nextMessages;
1000
1086
  return finalizedReplacement;
1001
1087
  }
@@ -1009,6 +1095,13 @@ export class AgentSession {
1009
1095
  await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
1010
1096
  }
1011
1097
  else if (event.type === "turn_start") {
1098
+ this._turnMetricsState = {
1099
+ startedAtEpochMs: Date.now(),
1100
+ toolCalls: [],
1101
+ toolStarts: new Map(),
1102
+ requestedModel: this.model ? `${this.model.provider}/${this.model.id}` : null,
1103
+ requestedThinking: this.thinkingMode === "auto" ? "auto" : this.thinkingLevel,
1104
+ };
1012
1105
  const extensionEvent = {
1013
1106
  type: "turn_start",
1014
1107
  turnIndex: this._turnIndex,
@@ -1017,6 +1110,7 @@ export class AgentSession {
1017
1110
  await this._extensionRunner.emit(extensionEvent);
1018
1111
  }
1019
1112
  else if (event.type === "turn_end") {
1113
+ this._recordTurnMetrics(event.message);
1020
1114
  const extensionEvent = {
1021
1115
  type: "turn_end",
1022
1116
  turnIndex: this._turnIndex,
@@ -1053,6 +1147,7 @@ export class AgentSession {
1053
1147
  }
1054
1148
  }
1055
1149
  else if (event.type === "tool_execution_start") {
1150
+ this._turnMetricsState?.toolStarts.set(event.toolCallId, Date.now());
1056
1151
  const extensionEvent = {
1057
1152
  type: "tool_execution_start",
1058
1153
  toolCallId: event.toolCallId,
@@ -1072,6 +1167,7 @@ export class AgentSession {
1072
1167
  await this._extensionRunner.emit(extensionEvent);
1073
1168
  }
1074
1169
  else if (event.type === "tool_execution_end") {
1170
+ this._recordToolMetric(event.toolCallId, event.toolName, event.isError === true, event.result);
1075
1171
  const extensionEvent = {
1076
1172
  type: "tool_execution_end",
1077
1173
  toolCallId: event.toolCallId,
@@ -1082,6 +1178,81 @@ export class AgentSession {
1082
1178
  await this._extensionRunner.emit(extensionEvent);
1083
1179
  }
1084
1180
  }
1181
+ /**
1182
+ * Turn metrics sink for this session, created on first use.
1183
+ *
1184
+ * Writes to `<cwd>/.omk/metrics/`. Set `OMK_TURN_METRICS=0` to disable, or
1185
+ * `OMK_TURN_METRICS_DIR` to relocate. Returns `undefined` when disabled, so
1186
+ * every call site is a no-op rather than a branch.
1187
+ */
1188
+ _metricsSink() {
1189
+ if (isDisabledEnvValue(process.env.OMK_TURN_METRICS))
1190
+ return undefined;
1191
+ if (!this._turnMetricsSink) {
1192
+ const dir = process.env.OMK_TURN_METRICS_DIR ?? joinPath(this._cwd, ".omk", "metrics");
1193
+ this._turnMetricsSink = new TurnMetricsSink({ dir });
1194
+ }
1195
+ return this._turnMetricsSink;
1196
+ }
1197
+ _recordToolMetric(toolCallId, toolName, isError, result) {
1198
+ const state = this._turnMetricsState;
1199
+ if (!state)
1200
+ return;
1201
+ const startedAt = state.toolStarts.get(toolCallId);
1202
+ state.toolStarts.delete(toolCallId);
1203
+ state.toolCalls.push({
1204
+ name: toolName,
1205
+ durationMs: startedAt === undefined ? 0 : Date.now() - startedAt,
1206
+ ok: !isError,
1207
+ ...(isError ? { error: firstTextContent(result) } : {}),
1208
+ });
1209
+ }
1210
+ /** Persist one turn. Metrics are advisory: a failure here never affects the turn. */
1211
+ _recordTurnMetrics(message) {
1212
+ const state = this._turnMetricsState;
1213
+ this._turnMetricsState = undefined;
1214
+ if (!state)
1215
+ return;
1216
+ const sink = this._metricsSink();
1217
+ if (!sink)
1218
+ return;
1219
+ const assistant = message.role === "assistant" ? message : undefined;
1220
+ const usage = assistant?.usage;
1221
+ const selectedModel = this.model ? `${this.model.provider}/${this.model.id}` : null;
1222
+ const runtimeProvenance = buildRuntimeProvenance({
1223
+ requestedModel: state.requestedModel,
1224
+ selectedModel,
1225
+ responseModel: assistant?.responseModel ?? (assistant ? `${assistant.provider}/${assistant.model}` : null),
1226
+ requestedThinking: state.requestedThinking,
1227
+ effectiveThinking: this.thinkingLevel,
1228
+ source: "session",
1229
+ });
1230
+ try {
1231
+ sink.record({
1232
+ sessionId: this.sessionId,
1233
+ turnIndex: this._turnIndex,
1234
+ provider: this.model?.provider,
1235
+ model: this.model?.id,
1236
+ startedAtEpochMs: state.startedAtEpochMs,
1237
+ endedAtEpochMs: Date.now(),
1238
+ stopReason: assistant?.stopReason,
1239
+ runtimeProvenance,
1240
+ usage: usage
1241
+ ? {
1242
+ input: usage.input,
1243
+ output: usage.output,
1244
+ cacheRead: usage.cacheRead,
1245
+ cacheWrite: usage.cacheWrite,
1246
+ costUsd: usage.cost.total,
1247
+ }
1248
+ : undefined,
1249
+ toolCalls: state.toolCalls,
1250
+ });
1251
+ }
1252
+ catch {
1253
+ // Never let observability break a turn.
1254
+ }
1255
+ }
1085
1256
  /**
1086
1257
  * Subscribe to agent events.
1087
1258
  * Session persistence is handled internally (saves messages on message_end).
@@ -1123,19 +1294,59 @@ export class AgentSession {
1123
1294
  */
1124
1295
  dispose() {
1125
1296
  try {
1126
- this.abortRetry();
1127
- this.abortCompaction();
1128
- this.abortBranchSummary();
1129
- this.abortBash();
1130
- this.agent.abort();
1297
+ try {
1298
+ this.abortRetry();
1299
+ this.abortCompaction();
1300
+ this.abortBranchSummary();
1301
+ this.abortBash();
1302
+ this.agent.abort();
1303
+ }
1304
+ catch {
1305
+ // Dispose must succeed even if an abort hook throws.
1306
+ }
1307
+ 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().");
1308
+ this._disconnectFromAgent();
1309
+ this._eventListeners = [];
1310
+ if (this._contextBudgetCacheProvider instanceof DiskContextBudgetCacheProviderV2) {
1311
+ // Persist whatever the session rendered; a failed flush is never fatal.
1312
+ this._contextBudgetCacheProvider.close();
1313
+ }
1314
+ this._mcpManager?.close();
1315
+ this._mcpManager = undefined;
1316
+ cleanupSessionResources(this.sessionId);
1317
+ }
1318
+ finally {
1319
+ const ownerLease = this._ownedSessionOwnerLease;
1320
+ if (ownerLease) {
1321
+ ownerLease.release();
1322
+ if (this.sessionManager.getOwnerLease() === ownerLease)
1323
+ this.sessionManager.setOwnerLease(undefined);
1324
+ this._ownedSessionOwnerLease = undefined;
1325
+ }
1326
+ }
1327
+ }
1328
+ /**
1329
+ * Build the context-budget cache provider for this session.
1330
+ *
1331
+ * Defaults to a workspace-layer provider persisted under `<cwd>/.omk/cache`,
1332
+ * so a second session reuses representations the first one already rendered
1333
+ * (representation keys are content-addressed; see
1334
+ * `context-budget-v2-cache-keys.ts`). Set
1335
+ * `OMK_CONTEXT_GOVERNOR_CACHE=memory` to opt out, or
1336
+ * `OMK_CONTEXT_GOVERNOR_CACHE_DIR` to relocate the snapshot.
1337
+ */
1338
+ _createContextBudgetCacheProvider() {
1339
+ if (process.env.OMK_CONTEXT_GOVERNOR_CACHE === "memory") {
1340
+ return createMemoryContextBudgetCacheProviderV2("session");
1341
+ }
1342
+ const dir = process.env.OMK_CONTEXT_GOVERNOR_CACHE_DIR ?? joinPath(this._cwd, ".omk", "cache", "context-budget-v2");
1343
+ try {
1344
+ return createDiskContextBudgetCacheProviderV2({ dir, layer: "workspace" });
1131
1345
  }
1132
1346
  catch {
1133
- // Dispose must succeed even if an abort hook throws.
1347
+ // An unusable cache directory must not take the session down.
1348
+ return createMemoryContextBudgetCacheProviderV2("session");
1134
1349
  }
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
1350
  }
1140
1351
  // =========================================================================
1141
1352
  // Read-only State Access
@@ -1220,6 +1431,9 @@ export class AgentSession {
1220
1431
  // Rebuild base system prompt with new tool set
1221
1432
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
1222
1433
  this.agent.state.systemPrompt = this._baseSystemPrompt;
1434
+ this.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;
1435
+ this.agent.state.systemPromptCacheBoundaryBypass = false;
1436
+ this._recordPromptCachePlan("tool-set");
1223
1437
  }
1224
1438
  /**
1225
1439
  * Recompute the Agent's per-name timeout map for the active tool set:
@@ -1316,7 +1530,7 @@ export class AgentSession {
1316
1530
  const recent = messages.slice(-3);
1317
1531
  for (let i = recent.length - 1; i >= 0; i--) {
1318
1532
  const msg = recent[i];
1319
- if (!("role" in msg) || msg.role !== "user") {
1533
+ if (!msg || !("role" in msg) || msg.role !== "user") {
1320
1534
  continue;
1321
1535
  }
1322
1536
  const content = msg.content;
@@ -1325,9 +1539,7 @@ export class AgentSession {
1325
1539
  return trimmed.length > 0 ? trimmed : undefined;
1326
1540
  }
1327
1541
  if (Array.isArray(content)) {
1328
- const textParts = content
1329
- .filter((c) => "type" in c && c.type === "text")
1330
- .map((c) => c.text);
1542
+ const textParts = content.flatMap((item) => (item.type === "text" ? [item.text] : []));
1331
1543
  const joined = textParts.join("\n").trim();
1332
1544
  return joined.length > 0 ? joined : undefined;
1333
1545
  }
@@ -1377,7 +1589,7 @@ export class AgentSession {
1377
1589
  responseReserveTokens =
1378
1590
  parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
1379
1591
  }
1380
- const cacheProvider = this._contextBudgetCacheProvider ?? createMemoryContextBudgetCacheProviderV2("session");
1592
+ const cacheProvider = this._contextBudgetCacheProvider ?? this._createContextBudgetCacheProvider();
1381
1593
  cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
1382
1594
  this._contextBudgetCacheProvider = cacheProvider;
1383
1595
  // Enforce floor
@@ -1411,6 +1623,32 @@ export class AgentSession {
1411
1623
  const ratio = overrideRatio ?? RESPONSE_RESERVE_RATIO;
1412
1624
  return Math.max(Math.floor(contextWindow * ratio), LEGACY_RESPONSE_RESERVE_TOKENS);
1413
1625
  }
1626
+ _recordPromptCachePlan(reason) {
1627
+ const boundary = this.agent.state.systemPromptCacheBoundary;
1628
+ const scope = this.model ? `${this.model.provider}/${this.model.id}` : "unknown";
1629
+ const nextKey = deriveContextPromptCacheKey({
1630
+ systemPrompt: this.agent.state.systemPrompt,
1631
+ systemPromptCacheBoundary: boundary,
1632
+ systemPromptCacheBoundaryBypass: this.agent.state.systemPromptCacheBoundaryBypass,
1633
+ messages: [],
1634
+ tools: this.agent.state.tools,
1635
+ }, scope);
1636
+ if (!nextKey) {
1637
+ this._promptCacheBoundaryBypasses += 1;
1638
+ this._promptCacheStablePrefixCharacters = 0;
1639
+ if (this._promptCacheKey !== undefined) {
1640
+ this._promptCacheLastBreakReason = reason;
1641
+ }
1642
+ this._promptCacheKey = undefined;
1643
+ return;
1644
+ }
1645
+ if (this._promptCacheKey !== undefined && this._promptCacheKey !== nextKey) {
1646
+ this._promptCacheKeyChanges += 1;
1647
+ this._promptCacheLastBreakReason = reason;
1648
+ }
1649
+ this._promptCacheKey = nextKey;
1650
+ this._promptCacheStablePrefixCharacters = boundary ?? 0;
1651
+ }
1414
1652
  _rebuildSystemPrompt(toolNames) {
1415
1653
  const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
1416
1654
  const toolSnippets = {};
@@ -1446,7 +1684,9 @@ export class AgentSession {
1446
1684
  promptGuidelines,
1447
1685
  contextBudget: this._getContextBudgetOptions(),
1448
1686
  };
1449
- return buildSystemPrompt(this._baseSystemPromptOptions);
1687
+ const built = buildSystemPromptPlan(this._baseSystemPromptOptions);
1688
+ this._baseSystemPromptCacheBoundary = built.cacheBoundary;
1689
+ return built.prompt;
1450
1690
  }
1451
1691
  // =========================================================================
1452
1692
  // Prompting
@@ -1625,9 +1865,9 @@ export class AgentSession {
1625
1865
  ...(promptActiveSkillNames.length > 0 ? { activeSkillNames: promptActiveSkillNames } : {}),
1626
1866
  ...(promptActiveSkillSource ? { activeSkillSource: promptActiveSkillSource } : {}),
1627
1867
  };
1628
- const turnSystemPrompt = buildSystemPrompt(turnSystemPromptOptions);
1868
+ const turnSystemPrompt = buildSystemPromptPlan(turnSystemPromptOptions);
1629
1869
  // Emit before_agent_start extension event
1630
- const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, turnSystemPrompt, turnSystemPromptOptions);
1870
+ const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, turnSystemPrompt.prompt, turnSystemPromptOptions);
1631
1871
  // Add all custom messages from extensions
1632
1872
  if (result?.messages) {
1633
1873
  for (const msg of result.messages) {
@@ -1641,14 +1881,22 @@ export class AgentSession {
1641
1881
  });
1642
1882
  }
1643
1883
  }
1644
- // Apply extension-modified system prompt, or reset to base
1884
+ // Apply extension-modified system prompt, or reset to the turn plan.
1645
1885
  if (result?.systemPrompt) {
1886
+ const preservesCacheBoundary = result.systemPrompt === turnSystemPrompt.prompt;
1646
1887
  this.agent.state.systemPrompt = result.systemPrompt;
1888
+ this.agent.state.systemPromptCacheBoundary = preservesCacheBoundary
1889
+ ? turnSystemPrompt.cacheBoundary
1890
+ : undefined;
1891
+ this.agent.state.systemPromptCacheBoundaryBypass = !preservesCacheBoundary;
1647
1892
  }
1648
1893
  else {
1649
- // Ensure we're using the base prompt (in case previous turn had modifications)
1650
- this.agent.state.systemPrompt = turnSystemPrompt;
1894
+ // Ensure we're using the turn prompt (in case the previous turn had modifications).
1895
+ this.agent.state.systemPrompt = turnSystemPrompt.prompt;
1896
+ this.agent.state.systemPromptCacheBoundary = turnSystemPrompt.cacheBoundary;
1897
+ this.agent.state.systemPromptCacheBoundaryBypass = false;
1651
1898
  }
1899
+ this._recordPromptCachePlan(result?.systemPrompt ? "extension-override" : "turn-plan");
1652
1900
  await this._checkProjectedCompaction(messages);
1653
1901
  }
1654
1902
  catch (error) {
@@ -2080,6 +2328,18 @@ export class AgentSession {
2080
2328
  });
2081
2329
  }
2082
2330
  }
2331
+ /** Apply an explicit user choice and retain it as bounded router feedback. */
2332
+ setUserThinkingLevel(level) {
2333
+ const overridesAuto = this._thinkingMode === "auto";
2334
+ this.setThinkingMode("manual");
2335
+ this.setThinkingLevel(level);
2336
+ if (overridesAuto) {
2337
+ this._recordUserThinkingOverride(this.thinkingLevel);
2338
+ }
2339
+ else {
2340
+ this._lastAutoRouterDecision = undefined;
2341
+ }
2342
+ }
2083
2343
  /**
2084
2344
  * Set the thinking mode. "auto" resolves a level per turn via the reasoning
2085
2345
  * router; "manual" keeps the explicitly selected level. The mode persists for
@@ -2107,21 +2367,20 @@ export class AgentSession {
2107
2367
  * context-pressure bucket, routed through the confidence-bearing v4 classifier
2108
2368
  * and its uncertainty-aware resolver. No
2109
2369
  * `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,
2370
+ * the default-off Adaptorch bridge is wired but its current transport returns
2371
+ * no advisory hint. `bias` stays `0` unless BOTH hold: (a) the global,
2113
2372
  * owner-only `reasoningRouterLearning.enabled` setting is `true` (default
2114
2373
  * off; a project-scope `.omk/settings.json` value for this key is never
2115
2374
  * consulted -- see settings-manager.ts), and (b) a compiled
2116
2375
  * `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
2376
+ * configured path or opaque repository/worktree-scoped default, loaded and
2377
+ * cached ("pinned") at most once per session (see
2378
+ * `_getReasoningRouterBiasSnapshot`). When learning is enabled, one bounded
2379
+ * "accepted" record (no raw prompt/path/diff/session/model/provider/tool/hook
2380
+ * content; see router-feedback-collector.ts's exact ten-key schema) is
2381
+ * appended after every v4 auto-turn. A later explicit user level records one
2382
+ * bounded `s1-override` direction for the same cell. Both remain inert until
2383
+ * a future, separate offline compile step. The
2125
2384
  * resolver's own confidence-band/fallback-reason escalation (see
2126
2385
  * reasoning-router-v4.ts) still applies on top of the base+lane+bias
2127
2386
  * target, so a low-confidence or fallback-decided verdict can still only
@@ -2129,17 +2388,17 @@ export class AgentSession {
2129
2388
  */
2130
2389
  _applyAutoThinkingLevelV4(promptText) {
2131
2390
  const availableLevels = this.getAvailableThinkingLevels();
2391
+ const features = deriveRouterFeedbackFeaturesV4(promptText);
2132
2392
  const verdict = classifyTaskV4({
2133
2393
  prompt: promptText,
2134
2394
  history: this._taskClassHistory,
2135
2395
  pressureBucket: this._computePressureBucket(),
2136
- });
2396
+ }, undefined, features);
2137
2397
  this._taskClassHistory.unshift(verdict.taskClass);
2138
2398
  if (this._taskClassHistory.length > 8)
2139
2399
  this._taskClassHistory.length = 8;
2140
2400
  const learningEnabled = this.settingsManager.getReasoningRouterLearningEnabled();
2141
2401
  const snapshot = learningEnabled ? this._getReasoningRouterBiasSnapshot() : null;
2142
- const features = this._deriveRouterFeedbackFeatures(promptText);
2143
2402
  const bias = snapshot === null
2144
2403
  ? 0
2145
2404
  : getBiasStepsForCell(snapshot, {
@@ -2150,22 +2409,23 @@ export class AgentSession {
2150
2409
  hadDiff: features.hadDiff,
2151
2410
  });
2152
2411
  const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, this._getAdaptorchHint(verdict, features));
2412
+ this._lastAutoRouterDecision = undefined;
2153
2413
  if (learningEnabled && resolved !== "off") {
2154
- const record = {
2414
+ const decision = {
2155
2415
  routerVersion: "v4",
2156
2416
  laneType: "none",
2157
2417
  predictedClass: verdict.taskClass,
2158
2418
  resolvedLevel: resolved,
2159
- acceptedLevel: resolved,
2160
- signal: "s2-accept",
2161
- outcome: "accepted",
2162
2419
  lenBucket: features.lenBucket,
2163
2420
  hadFence: features.hadFence,
2164
2421
  hadDiff: features.hadDiff,
2165
2422
  };
2166
- appendRouterFeedbackRecord(record, {
2167
- enabled: true,
2168
- ledgerPath: this.settingsManager.getReasoningRouterLearningFeedbackLedgerPath(),
2423
+ this._lastAutoRouterDecision = decision;
2424
+ this._appendRouterFeedback({
2425
+ ...decision,
2426
+ acceptedLevel: resolved,
2427
+ signal: "s2-accept",
2428
+ outcome: "accepted",
2169
2429
  });
2170
2430
  }
2171
2431
  const previousLevel = this.agent.state.thinkingLevel;
@@ -2175,21 +2435,44 @@ export class AgentSession {
2175
2435
  this.sessionManager.appendThinkingLevelChange(resolved);
2176
2436
  this._emit({ type: "thinking_level_changed", level: resolved });
2177
2437
  }
2438
+ _getRepositoryRouterLearningPaths() {
2439
+ if (this._repositoryRouterLearningPaths === undefined) {
2440
+ this._repositoryRouterLearningPaths = getRepositoryRouterLearningPaths(this._cwd);
2441
+ }
2442
+ return this._repositoryRouterLearningPaths;
2443
+ }
2444
+ _appendRouterFeedback(record) {
2445
+ appendRouterFeedbackRecord(record, {
2446
+ enabled: this.settingsManager.getReasoningRouterLearningEnabled(),
2447
+ ledgerPath: this.settingsManager.getReasoningRouterLearningFeedbackLedgerPath() ??
2448
+ this._getRepositoryRouterLearningPaths().ledgerPath,
2449
+ });
2450
+ }
2451
+ _recordUserThinkingOverride(acceptedLevel) {
2452
+ const decision = this._lastAutoRouterDecision;
2453
+ this._lastAutoRouterDecision = undefined;
2454
+ if (!this.settingsManager.getReasoningRouterLearningEnabled() || !decision || acceptedLevel === "off")
2455
+ return;
2456
+ const resolvedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(decision.resolvedLevel);
2457
+ const acceptedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(acceptedLevel);
2458
+ const outcome = acceptedIndex > resolvedIndex ? "up" : acceptedIndex < resolvedIndex ? "down" : "same";
2459
+ this._appendRouterFeedback({
2460
+ ...decision,
2461
+ acceptedLevel,
2462
+ signal: "s1-override",
2463
+ outcome,
2464
+ });
2465
+ }
2178
2466
  /**
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.
2467
+ * Strictly load the compiled bias snapshot once per session. The first call
2468
+ * pins either a valid snapshot or a null miss; later calls never touch disk.
2187
2469
  */
2188
2470
  _getReasoningRouterBiasSnapshot() {
2189
2471
  if (this._reasoningRouterBiasSnapshotLoaded)
2190
2472
  return this._reasoningRouterBiasSnapshot;
2191
2473
  this._reasoningRouterBiasSnapshotLoaded = true;
2192
- const path = this.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ?? getDefaultRouterBiasSnapshotPath();
2474
+ const path = this.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ??
2475
+ this._getRepositoryRouterLearningPaths().biasSnapshotPath;
2193
2476
  try {
2194
2477
  if (existsSync(path)) {
2195
2478
  this._reasoningRouterBiasSnapshot = parseRouterBiasSnapshot(readFileSync(path, "utf-8"));
@@ -2200,29 +2483,6 @@ export class AgentSession {
2200
2483
  }
2201
2484
  return this._reasoningRouterBiasSnapshot;
2202
2485
  }
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
2486
  /**
2227
2487
  * Context-pressure band 0..3 from the projected token estimate over the
2228
2488
  * model context window (reuses estimateProjectedContextTokens). 0..<0.5,
@@ -2298,20 +2558,7 @@ export class AgentSession {
2298
2558
  // Map confidenceBand -> numeric confidence for the resolver's threshold gate
2299
2559
  const confidenceMap = { low: 0.5, medium: 0.75, high: 0.95 };
2300
2560
  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 };
2561
+ return { level: TASK_CLASS_THINKING_LEVELS_V4[hint.taskClass], confidence };
2315
2562
  }
2316
2563
  /**
2317
2564
  * Cycle to next thinking level.
@@ -2324,7 +2571,12 @@ export class AgentSession {
2324
2571
  const currentIndex = levels.indexOf(this.thinkingLevel);
2325
2572
  const nextIndex = (currentIndex + 1) % levels.length;
2326
2573
  const nextLevel = levels[nextIndex];
2574
+ const overridesAuto = this._thinkingMode === "auto";
2327
2575
  this.setThinkingLevel(nextLevel);
2576
+ if (overridesAuto) {
2577
+ this.setThinkingMode("manual");
2578
+ this._recordUserThinkingOverride(nextLevel);
2579
+ }
2328
2580
  return nextLevel;
2329
2581
  }
2330
2582
  /**
@@ -2340,7 +2592,7 @@ export class AgentSession {
2340
2592
  * Check if current model supports thinking/reasoning.
2341
2593
  */
2342
2594
  supportsThinking() {
2343
- return !!this.model?.reasoning;
2595
+ return Boolean(this.model?.reasoning);
2344
2596
  }
2345
2597
  _getThinkingLevelForModelSwitch(explicitLevel) {
2346
2598
  if (explicitLevel !== undefined) {
@@ -2490,8 +2742,8 @@ export class AgentSession {
2490
2742
  * @param customInstructions Optional instructions for the compaction summary
2491
2743
  */
2492
2744
  async compact(customInstructions) {
2493
- this._disconnectFromAgent();
2494
2745
  await this.abort();
2746
+ this._disconnectFromAgent();
2495
2747
  this._compactionAbortController = new AbortController();
2496
2748
  this._emit({ type: "compaction_start", reason: "manual" });
2497
2749
  let committedCompaction = false;
@@ -2554,7 +2806,7 @@ export class AgentSession {
2554
2806
  throw new Error("Compaction cancelled");
2555
2807
  }
2556
2808
  const compactionResult = {
2557
- summary: redactSensitiveText(summary),
2809
+ summary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),
2558
2810
  firstKeptEntryId,
2559
2811
  tokensBefore,
2560
2812
  details,
@@ -2582,6 +2834,11 @@ export class AgentSession {
2582
2834
  const message = error instanceof Error ? error.message : String(error);
2583
2835
  const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
2584
2836
  const stale = /stale|session changed during compaction|already compacted/i.test(message);
2837
+ let compactionCode = "failed";
2838
+ if (aborted)
2839
+ compactionCode = "aborted";
2840
+ else if (stale)
2841
+ compactionCode = "stale";
2585
2842
  const timestamp = new Date().toISOString();
2586
2843
  this._publishTermination(classifySessionTermination({
2587
2844
  sessionId: this.sessionId,
@@ -2589,7 +2846,7 @@ export class AgentSession {
2589
2846
  timestamp,
2590
2847
  source: "observed",
2591
2848
  message: this._terminationMessage(message, "Manual compaction failed."),
2592
- cause: { area: "compaction", code: aborted ? "aborted" : stale ? "stale" : "failed" },
2849
+ cause: { area: "compaction", code: compactionCode },
2593
2850
  sideEffects: committedCompaction ? "confirmed" : "none",
2594
2851
  ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
2595
2852
  }));
@@ -2621,11 +2878,46 @@ export class AgentSession {
2621
2878
  abortBranchSummary() {
2622
2879
  this._branchSummaryAbortController?.abort();
2623
2880
  }
2881
+ /** True when the transcript carries image content that forces a vision-route turn. */
2882
+ _transcriptHasImages(messages) {
2883
+ return messages.some((m) => {
2884
+ const content = m.content;
2885
+ if (!Array.isArray(content))
2886
+ return false;
2887
+ return content.some((part) => {
2888
+ return typeof part === "object" && part !== null && part.type === "image";
2889
+ });
2890
+ });
2891
+ }
2892
+ /**
2893
+ * Effective context window for the upcoming turn.
2894
+ * Image-bearing turns with a text-only session model are auto-routed to the
2895
+ * vision model (gpt-5.6-luna, 400K) — that window is the real limit. Text-only
2896
+ * turns keep the session model's (typically much larger) window.
2897
+ */
2898
+ _effectiveTurnContextWindow(pendingMessages, sessionWindow) {
2899
+ // Vision routing keys off the full transcript, not just the pending turn:
2900
+ // images retained in history keep every subsequent request on the vision model.
2901
+ const hasImages = this._transcriptHasImages(this.agent.state.messages) || this._transcriptHasImages(pendingMessages);
2902
+ const model = this.model;
2903
+ if (!hasImages || !model || (model.input ?? []).includes("image")) {
2904
+ return sessionWindow;
2905
+ }
2906
+ return Math.min(sessionWindow, getVisionRouteModel(model).contextWindow);
2907
+ }
2624
2908
  async _checkProjectedCompaction(pendingMessages) {
2625
2909
  const settings = this.settingsManager.getCompactionSettings();
2626
2910
  if (!settings.enabled || pendingMessages.length === 0)
2627
2911
  return false;
2628
- const contextWindow = this.model?.contextWindow ?? 0;
2912
+ const sessionWindow = this.model?.contextWindow ?? 0;
2913
+ if (sessionWindow <= 0)
2914
+ return false;
2915
+ // Image-bearing turns are served by the auto-routed vision model, not the
2916
+ // session model. Its window (not the session model's) is the real limit the
2917
+ // provider enforces, so threshold compaction must use it or the vision
2918
+ // request overflows (context_length_exceeded) before deepseek-based
2919
+ // compaction would fire.
2920
+ const contextWindow = this._effectiveTurnContextWindow(pendingMessages, sessionWindow);
2629
2921
  if (contextWindow <= 0)
2630
2922
  return false;
2631
2923
  const messages = [...this.agent.state.messages, ...pendingMessages];
@@ -2640,7 +2932,7 @@ export class AgentSession {
2640
2932
  }
2641
2933
  const decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);
2642
2934
  if (decision.compact) {
2643
- return await this._runThresholdCompaction(decision.emergency);
2935
+ return this._runThresholdCompaction(decision.emergency);
2644
2936
  }
2645
2937
  return false;
2646
2938
  }
@@ -2668,6 +2960,13 @@ export class AgentSession {
2668
2960
  // to a larger-context model (e.g. codex) - the overflow error from the old model
2669
2961
  // shouldn't trigger compaction for the new model.
2670
2962
  const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
2963
+ // Image-bearing turns are auto-routed to a vision-capable model (gpt-5.6-luna)
2964
+ // while the session model stays text-only (deepseek). An overflow from that
2965
+ // routed model must still count as the session's own overflow: compaction is
2966
+ // the fix, and the raw context_length_exceeded error must never reach the user.
2967
+ const visionRouteOverflow = this.model != null &&
2968
+ !(this.model.input ?? []).includes("image") &&
2969
+ isVisionRouteModel({ provider: assistantMessage.provider, id: assistantMessage.model });
2671
2970
  // Skip compaction checks if this assistant message is older than the latest
2672
2971
  // compaction boundary. This prevents a stale pre-compaction usage/error
2673
2972
  // from retriggering compaction on the first prompt after compaction.
@@ -2677,7 +2976,7 @@ export class AgentSession {
2677
2976
  return false;
2678
2977
  }
2679
2978
  // Case 1: Overflow - LLM returned context overflow error
2680
- if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
2979
+ if ((sameModel || visionRouteOverflow) && isContextOverflow(assistantMessage, contextWindow)) {
2681
2980
  if (this._overflowRecoveryAttempted) {
2682
2981
  this._emit({
2683
2982
  type: "compaction_end",
@@ -2696,7 +2995,7 @@ export class AgentSession {
2696
2995
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
2697
2996
  this.agent.state.messages = messages.slice(0, -1);
2698
2997
  }
2699
- return await this._runAutoCompaction("overflow", true);
2998
+ return this._runAutoCompaction("overflow", true);
2700
2999
  }
2701
3000
  // Case 2: Threshold - context is getting large
2702
3001
  // For error messages (no usage data), estimate from last successful response.
@@ -2723,7 +3022,7 @@ export class AgentSession {
2723
3022
  }
2724
3023
  const decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);
2725
3024
  if (decision.compact) {
2726
- return await this._runThresholdCompaction(decision.emergency);
3025
+ return this._runThresholdCompaction(decision.emergency);
2727
3026
  }
2728
3027
  return false;
2729
3028
  }
@@ -2837,7 +3136,7 @@ export class AgentSession {
2837
3136
  return false;
2838
3137
  }
2839
3138
  const result = {
2840
- summary: redactSensitiveText(summary),
3139
+ summary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),
2841
3140
  firstKeptEntryId,
2842
3141
  tokensBefore,
2843
3142
  details,
@@ -2953,6 +3252,9 @@ export class AgentSession {
2953
3252
  this._resourceLoader.extendResources(extensionPaths);
2954
3253
  this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
2955
3254
  this.agent.state.systemPrompt = this._baseSystemPrompt;
3255
+ this.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;
3256
+ this.agent.state.systemPromptCacheBoundaryBypass = false;
3257
+ this._recordPromptCachePlan("resource-reload");
2956
3258
  }
2957
3259
  buildExtensionResourcePaths(entries) {
2958
3260
  return entries.map((entry) => {
@@ -3108,6 +3410,54 @@ export class AgentSession {
3108
3410
  },
3109
3411
  });
3110
3412
  }
3413
+ /**
3414
+ * Connect configured MCP servers and register their tools for this session.
3415
+ *
3416
+ * Connection is lazy by construction: nothing is spawned until this is called,
3417
+ * and a server that fails to start is reported in the returned status instead
3418
+ * of taking the session down. Calling it twice replaces the previously
3419
+ * attached MCP tools rather than duplicating them.
3420
+ *
3421
+ * Returns per-server status so a caller can surface failures; an empty
3422
+ * configuration returns an empty array without spawning anything.
3423
+ */
3424
+ async attachMcpServers(options) {
3425
+ const servers = options?.servers ?? loadMcpServerConfigs(this._cwd);
3426
+ this._mcpManager?.close();
3427
+ this._mcpManager = undefined;
3428
+ this._customTools = this._customTools.filter((definition) => !this._mcpToolNames.has(definition.name));
3429
+ this._mcpToolNames = new Set();
3430
+ if (servers.length === 0) {
3431
+ this._refreshToolRegistry();
3432
+ return [];
3433
+ }
3434
+ const manager = new McpManager({
3435
+ servers,
3436
+ cwd: this._cwd,
3437
+ clientInfo: { name: APP_NAME, version: VERSION },
3438
+ callTimeoutMs: options?.callTimeoutMs,
3439
+ });
3440
+ this._mcpManager = manager;
3441
+ const definitions = await manager.listToolDefinitions();
3442
+ // A builtin always wins a name collision; MCP must never shadow `bash`.
3443
+ const usable = definitions.filter((definition) => !this._baseToolDefinitions.has(definition.name));
3444
+ this._mcpToolNames = new Set(usable.map((definition) => definition.name));
3445
+ this._customTools = [...this._customTools, ...usable];
3446
+ this._refreshToolRegistry();
3447
+ return manager.status();
3448
+ }
3449
+ /** Status of MCP servers attached to this session. Empty when none were attached. */
3450
+ mcpServerStatus() {
3451
+ return this._mcpManager?.status() ?? [];
3452
+ }
3453
+ /**
3454
+ * Ping attached MCP servers and return fresh status, so callers (e.g. the
3455
+ * status rail) can keep displaying real connectivity instead of the last
3456
+ * handshake result. Resolves to [] when no manager is attached.
3457
+ */
3458
+ mcpCheckHealth(options) {
3459
+ return this._mcpManager?.checkHealth(options) ?? Promise.resolve([]);
3460
+ }
3111
3461
  _refreshToolRegistry(options) {
3112
3462
  const previousRegistryNames = new Set(this._toolRegistry.keys());
3113
3463
  const previousActiveToolNames = this.getActiveToolNames();
@@ -3128,15 +3478,17 @@ export class AgentSession {
3128
3478
  throw new Error(`loadout extension tool shadows builtin: ${shadowedBuiltins.join(", ")}`);
3129
3479
  }
3130
3480
  }
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
- ]));
3481
+ const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries()).flatMap(([name, definition]) => isAllowedTool(name)
3482
+ ? [
3483
+ [
3484
+ name,
3485
+ {
3486
+ definition,
3487
+ sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
3488
+ },
3489
+ ],
3490
+ ]
3491
+ : []));
3140
3492
  for (const tool of allCustomTools) {
3141
3493
  definitionRegistry.set(tool.definition.name, {
3142
3494
  definition: tool.definition,
@@ -3144,26 +3496,29 @@ export class AgentSession {
3144
3496
  });
3145
3497
  }
3146
3498
  this._toolDefinitions = definitionRegistry;
3147
- this._toolPromptSnippets = new Map(Array.from(definitionRegistry.values())
3148
- .map(({ definition }) => {
3499
+ this._toolPromptSnippets = new Map(Array.from(definitionRegistry.values()).flatMap(({ definition }) => {
3149
3500
  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 }) => {
3501
+ return snippet ? [[definition.name, snippet]] : [];
3502
+ }));
3503
+ this._toolPromptGuidelines = new Map(Array.from(definitionRegistry.values()).flatMap(({ definition }) => {
3155
3504
  const guidelines = this._normalizePromptGuidelines(definition.promptGuidelines);
3156
- return guidelines.length > 0 ? [definition.name, guidelines] : undefined;
3157
- })
3158
- .filter((entry) => entry !== undefined));
3505
+ return guidelines.length > 0 ? [[definition.name, guidelines]] : [];
3506
+ }));
3507
+ // Tools contributed by extensions the harness itself loads (`<builtin:*>`),
3508
+ // as opposed to user/project/SDK extensions.
3509
+ const builtinExtensionToolNames = new Set(allCustomTools.flatMap((tool) => (tool.sourceInfo.path.startsWith("<builtin:") ? [tool.definition.name] : [])));
3159
3510
  const runner = this._extensionRunner;
3160
3511
  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);
3512
+ const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values()).flatMap((definition) => isAllowedTool(definition.name)
3513
+ ? [
3514
+ {
3515
+ definition,
3516
+ sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, {
3517
+ source: "builtin",
3518
+ }),
3519
+ },
3520
+ ]
3521
+ : []), runner);
3167
3522
  const toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool]));
3168
3523
  for (const tool of wrappedExtensionTools) {
3169
3524
  toolRegistry.set(tool.name, tool);
@@ -3186,7 +3541,14 @@ export class AgentSession {
3186
3541
  }
3187
3542
  }
3188
3543
  else if (options?.includeAllExtensionTools) {
3544
+ // "Keep extension tools on" is about the *user's* extensions. Tools shipped
3545
+ // by a built-in extension (e.g. update_todo) are part of the harness's own
3546
+ // tool surface, so they follow the built-in policy: when the caller asked
3547
+ // for no built-in tools, they stay off too.
3548
+ const builtinToolsDisabled = options.activeToolNames?.length === 0;
3189
3549
  for (const tool of wrappedExtensionTools) {
3550
+ if (builtinToolsDisabled && builtinExtensionToolNames.has(tool.name))
3551
+ continue;
3190
3552
  nextActiveToolNames.push(tool.name);
3191
3553
  }
3192
3554
  }
@@ -3217,12 +3579,14 @@ export class AgentSession {
3217
3579
  canWritePath: (path) => loadoutAccessGuard({ operation: "write", toolName: "write", path }).allowed,
3218
3580
  }
3219
3581
  : {};
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, {
3582
+ const baseToolDefinitions = (() => {
3583
+ if (this._baseToolsOverride) {
3584
+ return Object.fromEntries(Object.entries(this._baseToolsOverride).map(([name, tool]) => [
3585
+ name,
3586
+ createToolDefinitionFromAgentTool(tool),
3587
+ ]));
3588
+ }
3589
+ return createAllToolDefinitions(this._cwd, {
3226
3590
  read: { autoResizeImages, ...loadoutReadOptions },
3227
3591
  bash: {
3228
3592
  commandPrefix: shellCommandPrefix,
@@ -3255,6 +3619,7 @@ export class AgentSession {
3255
3619
  edit: loadoutWriteOptions,
3256
3620
  write: loadoutWriteOptions,
3257
3621
  });
3622
+ })();
3258
3623
  this._baseToolDefinitions = new Map(Object.entries(baseToolDefinitions).map(([name, tool]) => [name, tool]));
3259
3624
  const extensionsResult = this._resourceLoader.getExtensions();
3260
3625
  if (options.flagValues) {
@@ -3363,7 +3728,9 @@ export class AgentSession {
3363
3728
  return;
3364
3729
  const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
3365
3730
  const next = this._modelRegistry.find(c.provider, c.id);
3366
- return !!next && this._modelRegistry.hasConfiguredAuth(next);
3731
+ if (!next)
3732
+ return false;
3733
+ return this._modelRegistry.hasConfiguredAuth(next);
3367
3734
  });
3368
3735
  if (!pick) {
3369
3736
  throw new Error(stickySafetyBlockMessage(current.id, current.provider));
@@ -3398,7 +3765,9 @@ export class AgentSession {
3398
3765
  if (this._refusedModels.has(`${c.provider}/${c.id}`))
3399
3766
  return false;
3400
3767
  const next = this._modelRegistry.find(c.provider, c.id);
3401
- return !!next && this._modelRegistry.hasConfiguredAuth(next);
3768
+ if (!next)
3769
+ return false;
3770
+ return this._modelRegistry.hasConfiguredAuth(next);
3402
3771
  });
3403
3772
  if (!pick)
3404
3773
  return undefined;
@@ -3623,6 +3992,8 @@ export class AgentSession {
3623
3992
  let summaryDetails;
3624
3993
  if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
3625
3994
  const model = this.model;
3995
+ if (!model)
3996
+ throw new Error("No model available for summarization");
3626
3997
  const { apiKey, headers } = await this._getRequiredRequestAuth(model);
3627
3998
  const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
3628
3999
  const result = await generateBranchSummary(entriesToSummarize, {
@@ -3664,13 +4035,18 @@ export class AgentSession {
3664
4035
  else if (targetEntry.type === "custom_message") {
3665
4036
  // Custom message: leaf = parent (null if root), text goes to editor
3666
4037
  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("");
4038
+ if (typeof targetEntry.content === "string") {
4039
+ editorText = targetEntry.content;
4040
+ }
4041
+ else {
4042
+ editorText = targetEntry.content
4043
+ .flatMap((item) => {
4044
+ if (item.type === "text")
4045
+ return [item.text];
4046
+ return [];
4047
+ })
4048
+ .join("");
4049
+ }
3674
4050
  }
3675
4051
  else {
3676
4052
  // Non-user message: leaf = selected node
@@ -3741,8 +4117,7 @@ export class AgentSession {
3741
4117
  return content;
3742
4118
  if (Array.isArray(content)) {
3743
4119
  return content
3744
- .filter((c) => c.type === "text")
3745
- .map((c) => c.text)
4120
+ .flatMap((item) => (item.type === "text" && typeof item.text === "string" ? [item.text] : []))
3746
4121
  .join("");
3747
4122
  }
3748
4123
  return "";
@@ -3772,6 +4147,7 @@ export class AgentSession {
3772
4147
  totalCost += assistantMsg.usage.cost.total;
3773
4148
  }
3774
4149
  }
4150
+ const providerEligibleInputTokens = totalInput + totalCacheRead + totalCacheWrite;
3775
4151
  return {
3776
4152
  sessionFile: this.sessionFile,
3777
4153
  sessionId: this.sessionId,
@@ -3787,6 +4163,14 @@ export class AgentSession {
3787
4163
  cacheWrite: totalCacheWrite,
3788
4164
  total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
3789
4165
  },
4166
+ promptCache: {
4167
+ providerEligibleInputTokens,
4168
+ providerHitRate: providerEligibleInputTokens > 0 ? totalCacheRead / providerEligibleInputTokens : 0,
4169
+ keyChanges: this._promptCacheKeyChanges,
4170
+ boundaryBypasses: this._promptCacheBoundaryBypasses,
4171
+ stablePrefixCharacters: this._promptCacheStablePrefixCharacters,
4172
+ ...(this._promptCacheLastBreakReason ? { lastBreakReason: this._promptCacheLastBreakReason } : {}),
4173
+ },
3790
4174
  cost: totalCost,
3791
4175
  contextUsage: this.getContextUsage(),
3792
4176
  };
@@ -3845,7 +4229,7 @@ export class AgentSession {
3845
4229
  theme,
3846
4230
  cwd: this.sessionManager.getCwd(),
3847
4231
  });
3848
- return await exportSessionToHtml(this.sessionManager, this.state, {
4232
+ return exportSessionToHtml(this.sessionManager, this.state, {
3849
4233
  outputPath,
3850
4234
  themeName,
3851
4235
  toolRenderer,
@@ -3891,18 +4275,18 @@ export class AgentSession {
3891
4275
  * @returns Text content, or undefined if no assistant message exists
3892
4276
  */
3893
4277
  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;
4278
+ let lastAssistant;
4279
+ for (let index = this.messages.length - 1; index >= 0; index -= 1) {
4280
+ const message = this.messages[index];
4281
+ if (message?.role !== "assistant")
4282
+ continue;
4283
+ const assistant = message;
3901
4284
  // Skip aborted messages with no content
3902
- if (msg.stopReason === "aborted" && msg.content.length === 0)
3903
- return false;
3904
- return true;
3905
- });
4285
+ if (assistant.stopReason === "aborted" && assistant.content.length === 0)
4286
+ continue;
4287
+ lastAssistant = assistant;
4288
+ break;
4289
+ }
3906
4290
  if (!lastAssistant)
3907
4291
  return undefined;
3908
4292
  let text = "";