open-multi-agent-kit 0.90.8 → 0.90.9

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 +22 -0
  2. package/README.md +7 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/doctor-provider-cli.d.ts +33 -0
  7. package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
  8. package/dist/commands/doctor-provider-cli.js +215 -0
  9. package/dist/commands/doctor-provider-cli.js.map +1 -0
  10. package/dist/commands/doctor-provider-transport.d.ts +35 -0
  11. package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
  12. package/dist/commands/doctor-provider-transport.js +185 -0
  13. package/dist/commands/doctor-provider-transport.js.map +1 -0
  14. package/dist/commands/doctor-provider.d.ts +121 -7
  15. package/dist/commands/doctor-provider.d.ts.map +1 -1
  16. package/dist/commands/doctor-provider.js +1295 -102
  17. package/dist/commands/doctor-provider.js.map +1 -1
  18. package/dist/commands/session-doctor-cli.d.ts +43 -0
  19. package/dist/commands/session-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/session-doctor-cli.js +494 -0
  21. package/dist/commands/session-doctor-cli.js.map +1 -0
  22. package/dist/commands/verify-ci.d.ts +21 -0
  23. package/dist/commands/verify-ci.d.ts.map +1 -0
  24. package/dist/commands/verify-ci.js +66 -0
  25. package/dist/commands/verify-ci.js.map +1 -0
  26. package/dist/core/agent-session-services.d.ts +3 -0
  27. package/dist/core/agent-session-services.d.ts.map +1 -1
  28. package/dist/core/agent-session-services.js +3 -0
  29. package/dist/core/agent-session-services.js.map +1 -1
  30. package/dist/core/agent-session.d.ts +113 -2
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +993 -72
  33. package/dist/core/agent-session.js.map +1 -1
  34. package/dist/core/agent-tool-settings.d.ts +35 -0
  35. package/dist/core/agent-tool-settings.d.ts.map +1 -0
  36. package/dist/core/agent-tool-settings.js +155 -0
  37. package/dist/core/agent-tool-settings.js.map +1 -0
  38. package/dist/core/atomic-session-file.d.ts +14 -0
  39. package/dist/core/atomic-session-file.d.ts.map +1 -0
  40. package/dist/core/atomic-session-file.js +104 -0
  41. package/dist/core/atomic-session-file.js.map +1 -0
  42. package/dist/core/compaction/compaction.d.ts +6 -0
  43. package/dist/core/compaction/compaction.d.ts.map +1 -1
  44. package/dist/core/compaction/compaction.js +12 -1
  45. package/dist/core/compaction/compaction.js.map +1 -1
  46. package/dist/core/compaction/hysteresis.d.ts +27 -0
  47. package/dist/core/compaction/hysteresis.d.ts.map +1 -0
  48. package/dist/core/compaction/hysteresis.js +84 -0
  49. package/dist/core/compaction/hysteresis.js.map +1 -0
  50. package/dist/core/compaction/index.d.ts +2 -0
  51. package/dist/core/compaction/index.d.ts.map +1 -1
  52. package/dist/core/compaction/index.js +2 -0
  53. package/dist/core/compaction/index.js.map +1 -1
  54. package/dist/core/compaction/transaction.d.ts +172 -0
  55. package/dist/core/compaction/transaction.d.ts.map +1 -0
  56. package/dist/core/compaction/transaction.js +824 -0
  57. package/dist/core/compaction/transaction.js.map +1 -0
  58. package/dist/core/context-budget-governor-v2.d.ts +2 -1
  59. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  60. package/dist/core/context-budget-governor-v2.js +2 -1
  61. package/dist/core/context-budget-governor-v2.js.map +1 -1
  62. package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
  63. package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
  64. package/dist/core/context-budget-reserved-tokens.js +188 -0
  65. package/dist/core/context-budget-reserved-tokens.js.map +1 -0
  66. package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
  67. package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
  68. package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
  69. package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
  70. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  71. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  72. package/dist/core/context-budget-v2-cache-keys.js +6 -0
  73. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  74. package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
  75. package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
  76. package/dist/core/context-budget-v2-cache-provider.js +19 -0
  77. package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
  78. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  79. package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
  80. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  81. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  82. package/dist/core/context-budget-v2-planner.js +1 -0
  83. package/dist/core/context-budget-v2-planner.js.map +1 -1
  84. package/dist/core/context-budget-v2-types.d.ts +4 -0
  85. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  86. package/dist/core/context-budget-v2-types.js.map +1 -1
  87. package/dist/core/durable-file-directory.d.ts +4 -0
  88. package/dist/core/durable-file-directory.d.ts.map +1 -0
  89. package/dist/core/durable-file-directory.js +76 -0
  90. package/dist/core/durable-file-directory.js.map +1 -0
  91. package/dist/core/durable-file-identity.d.ts +26 -0
  92. package/dist/core/durable-file-identity.d.ts.map +1 -0
  93. package/dist/core/durable-file-identity.js +118 -0
  94. package/dist/core/durable-file-identity.js.map +1 -0
  95. package/dist/core/durable-file-io.d.ts +29 -0
  96. package/dist/core/durable-file-io.d.ts.map +1 -0
  97. package/dist/core/durable-file-io.js +179 -0
  98. package/dist/core/durable-file-io.js.map +1 -0
  99. package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
  100. package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
  101. package/dist/core/durable-file-lock-cleanup.js +39 -0
  102. package/dist/core/durable-file-lock-cleanup.js.map +1 -0
  103. package/dist/core/durable-file-lock-owner.d.ts +18 -0
  104. package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
  105. package/dist/core/durable-file-lock-owner.js +78 -0
  106. package/dist/core/durable-file-lock-owner.js.map +1 -0
  107. package/dist/core/durable-file-lock-storage.d.ts +35 -0
  108. package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
  109. package/dist/core/durable-file-lock-storage.js +263 -0
  110. package/dist/core/durable-file-lock-storage.js.map +1 -0
  111. package/dist/core/durable-file-mode.d.ts +8 -0
  112. package/dist/core/durable-file-mode.d.ts.map +1 -0
  113. package/dist/core/durable-file-mode.js +41 -0
  114. package/dist/core/durable-file-mode.js.map +1 -0
  115. package/dist/core/extensions/loader.d.ts +12 -0
  116. package/dist/core/extensions/loader.d.ts.map +1 -1
  117. package/dist/core/extensions/loader.js +46 -37
  118. package/dist/core/extensions/loader.js.map +1 -1
  119. package/dist/core/index.d.ts +6 -2
  120. package/dist/core/index.d.ts.map +1 -1
  121. package/dist/core/index.js +5 -0
  122. package/dist/core/index.js.map +1 -1
  123. package/dist/core/model-resolver.d.ts.map +1 -1
  124. package/dist/core/model-resolver.js +1 -1
  125. package/dist/core/model-resolver.js.map +1 -1
  126. package/dist/core/redaction.d.ts +8 -0
  127. package/dist/core/redaction.d.ts.map +1 -0
  128. package/dist/core/redaction.js +31 -0
  129. package/dist/core/redaction.js.map +1 -0
  130. package/dist/core/resource-loader.d.ts.map +1 -1
  131. package/dist/core/resource-loader.js +36 -0
  132. package/dist/core/resource-loader.js.map +1 -1
  133. package/dist/core/run-journal-store.d.ts +53 -0
  134. package/dist/core/run-journal-store.d.ts.map +1 -0
  135. package/dist/core/run-journal-store.js +278 -0
  136. package/dist/core/run-journal-store.js.map +1 -0
  137. package/dist/core/run-journal.d.ts +172 -0
  138. package/dist/core/run-journal.d.ts.map +1 -0
  139. package/dist/core/run-journal.js +1053 -0
  140. package/dist/core/run-journal.js.map +1 -0
  141. package/dist/core/sdk.d.ts +7 -0
  142. package/dist/core/sdk.d.ts.map +1 -1
  143. package/dist/core/sdk.js +94 -3
  144. package/dist/core/sdk.js.map +1 -1
  145. package/dist/core/session-doctor-plan.d.ts +128 -0
  146. package/dist/core/session-doctor-plan.d.ts.map +1 -0
  147. package/dist/core/session-doctor-plan.js +833 -0
  148. package/dist/core/session-doctor-plan.js.map +1 -0
  149. package/dist/core/session-file-compaction.d.ts +6 -0
  150. package/dist/core/session-file-compaction.d.ts.map +1 -0
  151. package/dist/core/session-file-compaction.js +169 -0
  152. package/dist/core/session-file-compaction.js.map +1 -0
  153. package/dist/core/session-file-head.d.ts +10 -0
  154. package/dist/core/session-file-head.d.ts.map +1 -0
  155. package/dist/core/session-file-head.js +78 -0
  156. package/dist/core/session-file-head.js.map +1 -0
  157. package/dist/core/session-file-persistence.d.ts +30 -0
  158. package/dist/core/session-file-persistence.d.ts.map +1 -0
  159. package/dist/core/session-file-persistence.js +106 -0
  160. package/dist/core/session-file-persistence.js.map +1 -0
  161. package/dist/core/session-integrity.d.ts +43 -0
  162. package/dist/core/session-integrity.d.ts.map +1 -0
  163. package/dist/core/session-integrity.js +420 -0
  164. package/dist/core/session-integrity.js.map +1 -0
  165. package/dist/core/session-manager.d.ts +24 -4
  166. package/dist/core/session-manager.d.ts.map +1 -1
  167. package/dist/core/session-manager.js +242 -125
  168. package/dist/core/session-manager.js.map +1 -1
  169. package/dist/core/session-owner-lease.d.ts +22 -0
  170. package/dist/core/session-owner-lease.d.ts.map +1 -0
  171. package/dist/core/session-owner-lease.js +176 -0
  172. package/dist/core/session-owner-lease.js.map +1 -0
  173. package/dist/core/session-path-policy.d.ts +132 -0
  174. package/dist/core/session-path-policy.d.ts.map +1 -0
  175. package/dist/core/session-path-policy.js +780 -0
  176. package/dist/core/session-path-policy.js.map +1 -0
  177. package/dist/core/session-repair-plan.d.ts +49 -0
  178. package/dist/core/session-repair-plan.d.ts.map +1 -0
  179. package/dist/core/session-repair-plan.js +202 -0
  180. package/dist/core/session-repair-plan.js.map +1 -0
  181. package/dist/core/session-termination.d.ts +100 -0
  182. package/dist/core/session-termination.d.ts.map +1 -0
  183. package/dist/core/session-termination.js +403 -0
  184. package/dist/core/session-termination.js.map +1 -0
  185. package/dist/core/settings-manager.d.ts +22 -0
  186. package/dist/core/settings-manager.d.ts.map +1 -1
  187. package/dist/core/settings-manager.js +37 -5
  188. package/dist/core/settings-manager.js.map +1 -1
  189. package/dist/core/system-prompt.d.ts.map +1 -1
  190. package/dist/core/system-prompt.js +1 -1
  191. package/dist/core/system-prompt.js.map +1 -1
  192. package/dist/core/verified-bash-adapter.d.ts +36 -0
  193. package/dist/core/verified-bash-adapter.d.ts.map +1 -0
  194. package/dist/core/verified-bash-adapter.js +90 -0
  195. package/dist/core/verified-bash-adapter.js.map +1 -0
  196. package/dist/guardrails/command-redaction.d.ts +32 -0
  197. package/dist/guardrails/command-redaction.d.ts.map +1 -0
  198. package/dist/guardrails/command-redaction.js +776 -0
  199. package/dist/guardrails/command-redaction.js.map +1 -0
  200. package/dist/guardrails/evidence-attestation.d.ts +31 -0
  201. package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
  202. package/dist/guardrails/evidence-attestation.js +178 -0
  203. package/dist/guardrails/evidence-attestation.js.map +1 -0
  204. package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
  205. package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
  206. package/dist/guardrails/evidence-receipt-store.js +446 -0
  207. package/dist/guardrails/evidence-receipt-store.js.map +1 -0
  208. package/dist/guardrails/evidence-receipt.d.ts +58 -0
  209. package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
  210. package/dist/guardrails/evidence-receipt.js +389 -0
  211. package/dist/guardrails/evidence-receipt.js.map +1 -0
  212. package/dist/guardrails/evidence-system.d.ts +39 -15
  213. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  214. package/dist/guardrails/evidence-system.js +255 -97
  215. package/dist/guardrails/evidence-system.js.map +1 -1
  216. package/dist/guardrails/replay-ledger-io.d.ts +6 -0
  217. package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
  218. package/dist/guardrails/replay-ledger-io.js +42 -0
  219. package/dist/guardrails/replay-ledger-io.js.map +1 -0
  220. package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
  221. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
  222. package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
  223. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
  224. package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
  225. package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
  226. package/dist/guardrails/replay-ledger-lock.js +135 -0
  227. package/dist/guardrails/replay-ledger-lock.js.map +1 -0
  228. package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
  229. package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
  230. package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
  231. package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
  232. package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
  233. package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
  234. package/dist/guardrails/replay-ledger-quarantine.js +19 -0
  235. package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
  236. package/dist/guardrails/replay-ledger-store.d.ts +32 -0
  237. package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
  238. package/dist/guardrails/replay-ledger-store.js +211 -0
  239. package/dist/guardrails/replay-ledger-store.js.map +1 -0
  240. package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
  241. package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
  242. package/dist/guardrails/shell-command-name-glob.js +233 -0
  243. package/dist/guardrails/shell-command-name-glob.js.map +1 -0
  244. package/dist/guardrails/shell-command-option.d.ts +7 -0
  245. package/dist/guardrails/shell-command-option.d.ts.map +1 -0
  246. package/dist/guardrails/shell-command-option.js +24 -0
  247. package/dist/guardrails/shell-command-option.js.map +1 -0
  248. package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
  249. package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
  250. package/dist/guardrails/shell-command-tokenizer.js +145 -0
  251. package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
  252. package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
  253. package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
  254. package/dist/guardrails/shell-command-wrapper.js +42 -0
  255. package/dist/guardrails/shell-command-wrapper.js.map +1 -0
  256. package/dist/guardrails/verified-executor.d.ts +65 -0
  257. package/dist/guardrails/verified-executor.d.ts.map +1 -0
  258. package/dist/guardrails/verified-executor.js +171 -0
  259. package/dist/guardrails/verified-executor.js.map +1 -0
  260. package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
  261. package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
  262. package/dist/guardrails/workspace-fingerprint.js +634 -0
  263. package/dist/guardrails/workspace-fingerprint.js.map +1 -0
  264. package/dist/index.d.ts +15 -3
  265. package/dist/index.d.ts.map +1 -1
  266. package/dist/index.js +13 -1
  267. package/dist/index.js.map +1 -1
  268. package/dist/main.d.ts.map +1 -1
  269. package/dist/main.js +12 -0
  270. package/dist/main.js.map +1 -1
  271. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  272. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  273. package/dist/modes/interactive/interactive-mode.js +59 -9
  274. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  275. package/dist/modes/print-mode.d.ts.map +1 -1
  276. package/dist/modes/print-mode.js +43 -2
  277. package/dist/modes/print-mode.js.map +1 -1
  278. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  279. package/dist/modes/rpc/rpc-mode.js +23 -5
  280. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  281. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  282. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  283. package/dist/modes/rpc/rpc-types.js.map +1 -1
  284. package/dist/types/evidence.d.ts +211 -1
  285. package/dist/types/evidence.d.ts.map +1 -1
  286. package/dist/types/evidence.js.map +1 -1
  287. package/dist/verify-ci.d.ts +3 -0
  288. package/dist/verify-ci.d.ts.map +1 -0
  289. package/dist/verify-ci.js +30 -0
  290. package/dist/verify-ci.js.map +1 -0
  291. package/docs/compaction.md +38 -4
  292. package/docs/extensions.md +2 -0
  293. package/docs/sdk.md +238 -1
  294. package/docs/settings.md +50 -2
  295. package/docs/usage.md +38 -0
  296. package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
  297. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  298. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  299. package/examples/extensions/gondolin/package-lock.json +2 -2
  300. package/examples/extensions/gondolin/package.json +1 -1
  301. package/examples/extensions/sandbox/package-lock.json +2 -2
  302. package/examples/extensions/sandbox/package.json +1 -1
  303. package/examples/extensions/with-deps/package-lock.json +4 -4
  304. package/examples/extensions/with-deps/package.json +1 -1
  305. package/npm-shrinkwrap.json +12 -12
  306. package/package.json +4 -4
@@ -12,19 +12,29 @@
12
12
  *
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
+ import { createHash, randomUUID } from "node:crypto";
15
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
17
  import { basename, dirname } from "node:path";
18
+ import { isDeepStrictEqual } from "node:util";
19
+ import { repairTranscriptIntegrity, } from "omk-agent-core";
17
20
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "omk-ai";
18
21
  import { theme } from "../modes/interactive/theme/theme.js";
19
22
  import { stripFrontmatter } from "../utils/frontmatter.js";
20
23
  import { resolvePath } from "../utils/paths.js";
24
+ import { sanitizeBinaryOutput } from "../utils/shell.js";
21
25
  import { sleep } from "../utils/sleep.js";
26
+ import { applyCategoryTimeoutDefaults, resolveAgentToolSettings, resolveToolTimeoutCategory, } from "./agent-tool-settings.js";
22
27
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
23
28
  import { parseBangInvocation } from "./bang-skill-invocation.js";
24
29
  import { executeBashWithOperations } from "./bash-executor.js";
25
30
  import { classifyShellCommand } from "./command-safety.js";
26
- import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, shouldCompact, } from "./compaction/index.js";
31
+ import { getCompactionHeadroomThreshold } from "./compaction/compaction.js";
32
+ import { createCompactionHysteresisConfig, createCompactionHysteresisState, stepCompactionHysteresis, } from "./compaction/hysteresis.js";
33
+ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
27
34
  import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
35
+ import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, decideCompactionCommit, evaluateCompactionBarrier, validateCompactionEnvelope, } from "./compaction/transaction.js";
36
+ import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
37
+ import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
28
38
  import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
29
39
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
30
40
  import { exportSessionToHtml } from "./export-html/index.js";
@@ -39,9 +49,13 @@ import { findExactModelReferenceMatch } from "./model-resolver.js";
39
49
  import { expandPromptTemplate } from "./prompt-templates.js";
40
50
  import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
41
51
  import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
52
+ import { redactSensitiveText } from "./redaction.js";
42
53
  import { appendRouterFeedbackRecord, } from "./router-feedback-collector.js";
54
+ import { RunJournalStore } from "./run-journal-store.js";
43
55
  import { detectSandboxBackend } from "./sandbox/backend.js";
56
+ import { inspectSessionIntegrity } from "./session-integrity.js";
44
57
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
58
+ import { classifySessionTermination, } from "./session-termination.js";
45
59
  import { createSyntheticSourceInfo } from "./source-info.js";
46
60
  import { buildSystemPrompt } from "./system-prompt.js";
47
61
  import { createLocalBashOperations } from "./tools/bash.js";
@@ -73,6 +87,23 @@ function mergePromptActiveSkillNames(first, second) {
73
87
  }
74
88
  return names;
75
89
  }
90
+ const PENDING_TOOL_RESULT_TOKENS = {
91
+ text: 1024,
92
+ image: 4096,
93
+ "large-output": 16_384,
94
+ };
95
+ const TEXT_RESULT_TOOLS = new Set(["edit", "find", "grep", "ls", "read", "write"]);
96
+ const IMAGE_PATH_PATTERN = /\.(?:avif|gif|jpe?g|png|webp)$/iu;
97
+ function pendingToolResultClass(name, args) {
98
+ if (name === "read" &&
99
+ typeof args === "object" &&
100
+ args !== null &&
101
+ typeof Reflect.get(args, "path") === "string" &&
102
+ IMAGE_PATH_PATTERN.test(Reflect.get(args, "path"))) {
103
+ return "image";
104
+ }
105
+ return TEXT_RESULT_TOOLS.has(name) ? "text" : "large-output";
106
+ }
76
107
  // ============================================================================
77
108
  // Constants
78
109
  // ============================================================================
@@ -98,6 +129,102 @@ function buildSandboxDeniedBashResult(reason) {
98
129
  truncated: false,
99
130
  };
100
131
  }
132
+ function snapshotContractError(reason) {
133
+ return new Error(`Finalized message replacement must be a plain serializable snapshot: ${reason}`);
134
+ }
135
+ /**
136
+ * Clone a finalized message replacement into the JSON-compatible snapshot
137
+ * persisted by SessionManager. Undefined remains allowed as ordinary optional
138
+ * message data: JSON omits object properties with undefined values and writes
139
+ * undefined array elements as null.
140
+ */
141
+ function clonePlainSnapshot(value, ancestors = new WeakSet(), copies = new WeakMap()) {
142
+ if (value === null)
143
+ return value;
144
+ switch (typeof value) {
145
+ case "string":
146
+ case "boolean":
147
+ case "undefined":
148
+ return value;
149
+ case "number":
150
+ if (!Number.isFinite(value)) {
151
+ throw snapshotContractError("non-finite number values are not allowed");
152
+ }
153
+ return value;
154
+ case "bigint":
155
+ throw snapshotContractError("bigint values are not allowed");
156
+ case "function":
157
+ case "symbol":
158
+ throw snapshotContractError(`${typeof value} values are not allowed`);
159
+ case "object":
160
+ break;
161
+ default:
162
+ throw snapshotContractError(`${typeof value} values are not allowed`);
163
+ }
164
+ if (ancestors.has(value)) {
165
+ throw snapshotContractError("cyclic values are not allowed");
166
+ }
167
+ if (copies.has(value)) {
168
+ return copies.get(value);
169
+ }
170
+ ancestors.add(value);
171
+ try {
172
+ if (Array.isArray(value)) {
173
+ const copy = new Array(value.length);
174
+ copies.set(value, copy);
175
+ for (const key of Reflect.ownKeys(value)) {
176
+ if (key === "length")
177
+ continue;
178
+ if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
179
+ throw snapshotContractError("arrays may only contain indexed values");
180
+ }
181
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
182
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
183
+ throw snapshotContractError("accessor or non-enumerable properties are not allowed");
184
+ }
185
+ copy[Number(key)] = clonePlainSnapshot(descriptor.value, ancestors, copies);
186
+ }
187
+ return copy;
188
+ }
189
+ const prototype = Object.getPrototypeOf(value);
190
+ if (prototype !== Object.prototype && prototype !== null) {
191
+ throw snapshotContractError("non-plain objects are not allowed");
192
+ }
193
+ const copy = Object.create(prototype);
194
+ copies.set(value, copy);
195
+ for (const key of Reflect.ownKeys(value)) {
196
+ if (typeof key !== "string") {
197
+ throw snapshotContractError("symbol-keyed properties are not allowed");
198
+ }
199
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
200
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
201
+ throw snapshotContractError("accessor or non-enumerable properties are not allowed");
202
+ }
203
+ Object.defineProperty(copy, key, {
204
+ value: clonePlainSnapshot(descriptor.value, ancestors, copies),
205
+ enumerable: true,
206
+ configurable: true,
207
+ writable: true,
208
+ });
209
+ }
210
+ return copy;
211
+ }
212
+ finally {
213
+ ancestors.delete(value);
214
+ }
215
+ }
216
+ function freezeSnapshot(value, seen = new WeakSet()) {
217
+ if (value === null || typeof value !== "object" || seen.has(value))
218
+ return value;
219
+ seen.add(value);
220
+ for (const child of Object.values(value)) {
221
+ freezeSnapshot(child, seen);
222
+ }
223
+ return Object.freeze(value);
224
+ }
225
+ function createImmutableMessageSnapshot(message) {
226
+ return freezeSnapshot(clonePlainSnapshot(message));
227
+ }
101
228
  // ============================================================================
102
229
  // AgentSession Class
103
230
  // ============================================================================
@@ -130,6 +257,7 @@ export class AgentSession {
130
257
  // Event subscription state
131
258
  _unsubscribeAgent;
132
259
  _eventListeners = [];
260
+ _messageEndReplacements = new WeakMap();
133
261
  /** Tracks pending steering messages for UI display. Removed when delivered. */
134
262
  _steeringMessages = [];
135
263
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -140,6 +268,8 @@ export class AgentSession {
140
268
  _compactionAbortController = undefined;
141
269
  _autoCompactionAbortController = undefined;
142
270
  _overflowRecoveryAttempted = false;
271
+ _thresholdCompactionEmergency = false;
272
+ _compactionHysteresisState = createCompactionHysteresisState();
143
273
  // Branch summarization state
144
274
  _branchSummaryAbortController = undefined;
145
275
  // Retry state
@@ -171,10 +301,24 @@ export class AgentSession {
171
301
  _extensionShutdownHandler;
172
302
  _extensionErrorListener;
173
303
  _extensionErrorUnsubscriber;
304
+ // Required durable run/audit chain, persisted next to the session file.
305
+ _runJournalStore;
306
+ _activeRunId = null;
307
+ _pendingRuntimeTerminationCause;
308
+ _activeRunToolTermination;
309
+ _lastTermination;
310
+ _userAbortRequested = false;
311
+ _replayLedger;
312
+ _replayGoalId;
313
+ _replayLaneId;
314
+ _transcriptRepair;
315
+ _sessionRiskLevel = "normal";
316
+ _workspaceMutationCount = 0;
174
317
  // Model registry for API key resolution
175
318
  _modelRegistry;
176
319
  /** Lazy, in-memory only; never shared across sessions or persisted. */
177
320
  _contextBudgetCacheProvider;
321
+ _contextCacheInvalidationSnapshot;
178
322
  // Tool registry for extension getTools/setTools
179
323
  _toolRegistry = new Map();
180
324
  _toolDefinitions = new Map();
@@ -192,6 +336,13 @@ export class AgentSession {
192
336
  this._customTools = config.customTools ?? [];
193
337
  this._cwd = config.cwd;
194
338
  this._modelRegistry = config.modelRegistry;
339
+ const initialModelId = this._contextCacheModelId(this.agent.state.model);
340
+ this._contextCacheInvalidationSnapshot = createContextCacheInvalidationSnapshot({
341
+ forkId: this.sessionManager.getSessionId(),
342
+ worktreeFingerprint: this._contextCacheWorktreeFingerprint(),
343
+ activeModelId: initialModelId,
344
+ compactionModelId: initialModelId,
345
+ });
195
346
  this._extensionRunnerRef = config.extensionRunnerRef;
196
347
  this._initialActiveToolNames = config.initialActiveToolNames;
197
348
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -200,10 +351,34 @@ export class AgentSession {
200
351
  this._loadoutAccessPolicy = config.loadoutAccessPolicy;
201
352
  this._configuredBashSandboxPreflight = config.bashSandboxPreflight;
202
353
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
354
+ this._transcriptRepair = config.transcriptRepair;
355
+ this._replayLedger = config.replayLedger;
356
+ this._replayGoalId = config.replayGoalId ?? config.replayLedger?.getLedger().goalId;
357
+ this._replayLaneId = config.replayLaneId;
358
+ if (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {
359
+ throw new Error("AgentSession replayGoalId does not match the replay ledger goal id");
360
+ }
361
+ const sessionFile = this.sessionManager.getSessionFile();
362
+ this._runJournalStore = RunJournalStore.open({
363
+ ...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
364
+ sessionId: this.sessionManager.getSessionId(),
365
+ });
366
+ const startupTerminal = this._runJournalStore.records.at(-1);
367
+ if (startupTerminal?.event === "run_recovered") {
368
+ this._lastTermination = startupTerminal.termination;
369
+ }
203
370
  // Always subscribe to agent events for internal handling
204
371
  // (session persistence, extensions, auto-compaction, retry logic)
205
372
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
206
373
  this._installAgentToolHooks();
374
+ // Durable transcript-repair audit for a repair applied on open/resume.
375
+ if (this._transcriptRepair) {
376
+ this._invalidateContextBudgetCache({ type: "transcriptRepair" });
377
+ this._appendRunJournalAudit("transcript_repaired", {
378
+ insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
379
+ reason: this._transcriptRepair.reason,
380
+ });
381
+ }
207
382
  this._buildRuntime({
208
383
  activeToolNames: this._initialActiveToolNames,
209
384
  includeAllExtensionTools: true,
@@ -213,6 +388,347 @@ export class AgentSession {
213
388
  get modelRegistry() {
214
389
  return this._modelRegistry;
215
390
  }
391
+ /** Transcript repair applied while this session was opened/resumed, if any. */
392
+ get transcriptRepair() {
393
+ return this._transcriptRepair;
394
+ }
395
+ /** Durable lifecycle and audit records appended by this session's run journal. */
396
+ get runJournalRecords() {
397
+ return this._runJournalStore.records;
398
+ }
399
+ /** Most recently observed or inferred termination for this session. */
400
+ get lastTermination() {
401
+ return this._lastTermination;
402
+ }
403
+ /** Exact trailing journal fragment quarantine performed during startup, if any. */
404
+ get runJournalQuarantineReport() {
405
+ return this._runJournalStore.quarantineReport;
406
+ }
407
+ /** Elevated once a late-settling potentially-writing tool may have mutated the workspace. */
408
+ get sessionRiskLevel() {
409
+ return this._sessionRiskLevel;
410
+ }
411
+ /** Monotonic count of workspace mutation/invalidation signals emitted by this session. */
412
+ get workspaceMutationCount() {
413
+ return this._workspaceMutationCount;
414
+ }
415
+ get contextCacheInvalidationSnapshot() {
416
+ return this._contextCacheInvalidationSnapshot;
417
+ }
418
+ _contextCacheModelId(model) {
419
+ return model
420
+ ? `model-${createHash("sha256").update(`${model.provider}\0${model.id}`, "utf8").digest("hex")}`
421
+ : "unknown";
422
+ }
423
+ _contextCacheWorktreeFingerprint() {
424
+ return createHash("sha256").update(`${this._cwd}\0${this._workspaceMutationCount}`, "utf8").digest("hex");
425
+ }
426
+ _invalidateContextBudgetCache(event) {
427
+ const result = applyContextCacheInvalidation(this._contextCacheInvalidationSnapshot, event);
428
+ this._contextCacheInvalidationSnapshot = result.snapshot;
429
+ if (result.status === "overflow") {
430
+ this._contextBudgetCacheProvider = undefined;
431
+ return;
432
+ }
433
+ this._contextBudgetCacheProvider?.setInvalidationSnapshot?.(result.snapshot);
434
+ }
435
+ _recordEvidenceReceiptInvalidation(customType) {
436
+ if (customType === "evidence_receipt" || customType === "evidence-receipt") {
437
+ this._invalidateContextBudgetCache({ type: "evidenceReceipt" });
438
+ }
439
+ }
440
+ _sessionRevision() {
441
+ return this.sessionManager.getEntries().length;
442
+ }
443
+ _appendReplayEvent(type, payload) {
444
+ if (!this._replayLedger || !this._replayGoalId)
445
+ return;
446
+ this._replayLedger.append({
447
+ type,
448
+ goalId: this._replayGoalId,
449
+ ...(this._replayLaneId ? { laneId: this._replayLaneId } : {}),
450
+ payload,
451
+ });
452
+ this._replayLedger.persist();
453
+ }
454
+ /** Append one required durable audit record. Persistence failures propagate. */
455
+ _appendRunJournalAudit(event, details) {
456
+ this._runJournalStore.audit({
457
+ event,
458
+ details,
459
+ sessionRevision: this._sessionRevision(),
460
+ timestamp: new Date().toISOString(),
461
+ });
462
+ this._appendReplayEvent(event, details);
463
+ }
464
+ _terminationMessage(value, fallback) {
465
+ const redacted = redactSensitiveText(value?.trim() || fallback)
466
+ .replace(/\0/g, "")
467
+ .slice(0, 512);
468
+ return redacted || fallback;
469
+ }
470
+ _providerFailureCause(message) {
471
+ const text = message.errorMessage ?? "";
472
+ if (isContextOverflow(message, this.model?.contextWindow ?? 0)) {
473
+ return { area: "provider", code: "context_overflow" };
474
+ }
475
+ if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
476
+ return { area: "provider", code: "auth" };
477
+ }
478
+ if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
479
+ return { area: "provider", code: "rate_limit" };
480
+ }
481
+ if (/tool.+timed? out|tool.+timeout/i.test(text))
482
+ return { area: "tool", code: "timeout" };
483
+ if (/tool/i.test(text))
484
+ return { area: "tool", code: "fatal" };
485
+ if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn/i.test(text)) {
486
+ return { area: "provider", code: "network" };
487
+ }
488
+ return { area: "provider", code: "protocol" };
489
+ }
490
+ _classifyPreflightCause(message) {
491
+ if (!this.model || /no model|model selected|model is required/i.test(message)) {
492
+ return { area: "configuration", code: "invalid" };
493
+ }
494
+ if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
495
+ return { area: "provider", code: "auth" };
496
+ }
497
+ if (/context.+overflow|context window|too many tokens/i.test(message)) {
498
+ return { area: "provider", code: "context_overflow" };
499
+ }
500
+ if (/duplicate.?result/i.test(message))
501
+ return { area: "transcript", code: "duplicate_result" };
502
+ if (/orphan.?result/i.test(message))
503
+ return { area: "transcript", code: "orphan_result" };
504
+ if (/duplicate.?call/i.test(message))
505
+ return { area: "transcript", code: "duplicate_call_id" };
506
+ if (/transcript|missing.?result/i.test(message))
507
+ return { area: "transcript", code: "missing_result" };
508
+ if (/compaction/i.test(message))
509
+ return { area: "compaction", code: "failed" };
510
+ if (/fsync/i.test(message))
511
+ return { area: "persistence", code: "fsync_failed" };
512
+ if (/lock/i.test(message))
513
+ return { area: "persistence", code: "lock_failed" };
514
+ if (/append|persist|write/i.test(message))
515
+ return { area: "persistence", code: "append_failed" };
516
+ if (/tool/i.test(message))
517
+ return { area: "tool", code: "fatal" };
518
+ return { area: "internal", code: "unclassified" };
519
+ }
520
+ _classifyRunTermination(runId, event) {
521
+ const timestamp = new Date().toISOString();
522
+ const assistant = [...event.messages]
523
+ .reverse()
524
+ .find((message) => message.role === "assistant");
525
+ let cause;
526
+ let message;
527
+ let sideEffects = this._sessionRiskLevel === "elevated" ? "possible" : "none";
528
+ let toolCallId;
529
+ let toolName;
530
+ if (this._activeRunToolTermination) {
531
+ cause = { area: "tool", code: "timeout" };
532
+ message = `Tool ${this._activeRunToolTermination.toolName} timed out.`;
533
+ toolCallId = this._activeRunToolTermination.toolCallId;
534
+ toolName = this._activeRunToolTermination.toolName;
535
+ sideEffects = this._activeRunToolTermination.executionStarted ? "possible" : sideEffects;
536
+ }
537
+ else if (!assistant) {
538
+ cause = { area: "internal", code: "unclassified" };
539
+ message = "Agent run ended without an assistant result.";
540
+ }
541
+ else if (assistant.stopReason === "aborted") {
542
+ cause = this._userAbortRequested ? { area: "user", code: "abort" } : { area: "provider", code: "abort" };
543
+ message = this._terminationMessage(assistant.errorMessage, this._userAbortRequested ? "The user aborted the run." : "The provider aborted the run.");
544
+ }
545
+ else if (assistant.stopReason === "error") {
546
+ cause = this._providerFailureCause(assistant);
547
+ message = this._terminationMessage(assistant.errorMessage, "The provider request failed.");
548
+ }
549
+ else {
550
+ cause = { area: "completed" };
551
+ message = "Run completed.";
552
+ }
553
+ return classifySessionTermination({
554
+ sessionId: this.sessionId,
555
+ runId,
556
+ timestamp,
557
+ source: "observed",
558
+ message,
559
+ cause,
560
+ sideEffects,
561
+ ...(assistant?.provider
562
+ ? { provider: assistant.provider }
563
+ : this.model
564
+ ? { provider: this.model.provider }
565
+ : {}),
566
+ ...(assistant?.model ? { model: assistant.model } : this.model ? { model: this.model.id } : {}),
567
+ ...(toolCallId ? { toolCallId } : {}),
568
+ ...(toolName ? { toolName } : {}),
569
+ });
570
+ }
571
+ _publishTermination(termination) {
572
+ this._lastTermination = termination;
573
+ this._emit({ type: "session_termination", termination });
574
+ }
575
+ _runtimeFailureCause(error) {
576
+ if (this._pendingRuntimeTerminationCause)
577
+ return this._pendingRuntimeTerminationCause;
578
+ const code = typeof error === "object" && error !== null && "code" in error ? Reflect.get(error, "code") : undefined;
579
+ if (typeof code === "string" &&
580
+ new Set(["EACCES", "EDQUOT", "EFBIG", "EIO", "EISDIR", "EMFILE", "ENFILE", "ENOSPC", "EPERM", "EROFS"]).has(code)) {
581
+ return { area: "persistence", code: "append_failed" };
582
+ }
583
+ const message = error instanceof Error ? error.message : String(error);
584
+ if (/compaction.+stale|session changed during compaction/i.test(message)) {
585
+ return { area: "compaction", code: "stale" };
586
+ }
587
+ if (/compaction/i.test(message))
588
+ return { area: "compaction", code: "failed" };
589
+ return { area: "internal", code: "unclassified" };
590
+ }
591
+ _publishRuntimeFailure(error) {
592
+ const cause = this._runtimeFailureCause(error);
593
+ const runId = this._activeRunId ?? `runtime-${randomUUID()}`;
594
+ const timestamp = new Date().toISOString();
595
+ const message = cause.area === "persistence"
596
+ ? "A required runtime persistence operation failed."
597
+ : cause.area === "compaction"
598
+ ? "Runtime compaction failed."
599
+ : "The AgentSession runtime failed before completing the run.";
600
+ let termination = classifySessionTermination({
601
+ sessionId: this.sessionId,
602
+ runId,
603
+ timestamp,
604
+ source: "observed",
605
+ message,
606
+ cause,
607
+ sideEffects: this._activeRunId === null ? "none" : "possible",
608
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
609
+ });
610
+ if (this._activeRunId !== null && this._runJournalStore.openRunId === this._activeRunId) {
611
+ try {
612
+ this._runJournalStore.finish({
613
+ termination,
614
+ sessionRevision: this._sessionRevision(),
615
+ timestamp,
616
+ });
617
+ }
618
+ catch {
619
+ termination = classifySessionTermination({
620
+ sessionId: this.sessionId,
621
+ runId,
622
+ timestamp,
623
+ source: "observed",
624
+ message: "The run journal could not persist the runtime termination.",
625
+ cause: { area: "persistence", code: "append_failed" },
626
+ sideEffects: "possible",
627
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
628
+ });
629
+ }
630
+ }
631
+ this._activeRunId = null;
632
+ this._activeRunToolTermination = undefined;
633
+ this._pendingRuntimeTerminationCause = undefined;
634
+ this._userAbortRequested = false;
635
+ this._publishTermination(termination);
636
+ }
637
+ _handleRunLifecycleEvent(event) {
638
+ if (event.type === "agent_start") {
639
+ if (this._activeRunId !== null)
640
+ throw new Error("run journal already has an active AgentSession run");
641
+ const runId = randomUUID();
642
+ this._activeRunId = runId;
643
+ this._activeRunToolTermination = undefined;
644
+ this._pendingRuntimeTerminationCause = undefined;
645
+ this._userAbortRequested = false;
646
+ try {
647
+ this._runJournalStore.start({
648
+ runId,
649
+ sessionRevision: this._sessionRevision(),
650
+ timestamp: new Date().toISOString(),
651
+ });
652
+ }
653
+ catch (error) {
654
+ this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
655
+ throw error;
656
+ }
657
+ return;
658
+ }
659
+ if (event.type !== "agent_end")
660
+ return;
661
+ if (this._activeRunId === null)
662
+ throw new Error("run journal received agent_end without run_started");
663
+ const termination = this._classifyRunTermination(this._activeRunId, event);
664
+ try {
665
+ this._runJournalStore.finish({
666
+ termination,
667
+ sessionRevision: this._sessionRevision(),
668
+ timestamp: termination.timestamp,
669
+ });
670
+ }
671
+ catch (error) {
672
+ this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
673
+ throw error;
674
+ }
675
+ this._activeRunId = null;
676
+ this._activeRunToolTermination = undefined;
677
+ this._pendingRuntimeTerminationCause = undefined;
678
+ this._userAbortRequested = false;
679
+ this._publishTermination(termination);
680
+ }
681
+ /**
682
+ * Handle tool timeout / late-settlement audit signals (ALG004-A/B). A late
683
+ * settlement of a potentially-writing tool raises session risk and emits a
684
+ * workspace mutation/invalidation signal for evidence freshness consumers.
685
+ */
686
+ _handleToolAuditEvent(event) {
687
+ if (event.type === "tool_execution_end") {
688
+ this._invalidateContextBudgetCache({ type: "toolResultDisposition" });
689
+ const envelope = event.result?.details?.omk;
690
+ if (envelope && envelope.schema === "tool-result/v2" && envelope.disposition === "timeout") {
691
+ const timeout = {
692
+ toolCallId: event.toolCallId,
693
+ toolName: event.toolName,
694
+ ...(typeof envelope.timeoutMs === "number" ? { timeoutMs: envelope.timeoutMs } : {}),
695
+ executionStarted: envelope.executionStarted === true,
696
+ };
697
+ this._activeRunToolTermination = timeout;
698
+ this._appendRunJournalAudit("tool_timeout", timeout);
699
+ }
700
+ return;
701
+ }
702
+ if (event.type !== "tool_execution_late_settlement") {
703
+ return;
704
+ }
705
+ // Fail closed: anything not classified as a read-category tool may write.
706
+ const potentiallyWriting = resolveToolTimeoutCategory(event.toolName) !== "read";
707
+ this._appendRunJournalAudit("tool_late_settlement", {
708
+ toolCallId: event.toolCallId,
709
+ toolName: event.toolName,
710
+ disposition: event.disposition,
711
+ outcome: event.outcome,
712
+ ...(potentiallyWriting ? { sessionRisk: "elevated" } : {}),
713
+ });
714
+ if (potentiallyWriting) {
715
+ this._sessionRiskLevel = "elevated";
716
+ this._workspaceMutationCount += 1;
717
+ this._invalidateContextBudgetCache({
718
+ type: "worktreeFingerprint",
719
+ value: this._contextCacheWorktreeFingerprint(),
720
+ });
721
+ const payload = { root: this._cwd, paths: [] };
722
+ this._appendReplayEvent("workspace_mutation", payload);
723
+ this._emit({
724
+ type: "workspace_mutation",
725
+ source: "tool_late_settlement",
726
+ toolCallId: event.toolCallId,
727
+ toolName: event.toolName,
728
+ payload,
729
+ });
730
+ }
731
+ }
216
732
  _getBashSandboxPreflight(override) {
217
733
  const preflight = override ?? this._configuredBashSandboxPreflight;
218
734
  if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
@@ -323,6 +839,8 @@ export class AgentSession {
323
839
  _lastAssistantMessage = undefined;
324
840
  /** Internal handler for agent events - shared by subscribe and reconnect */
325
841
  _handleAgentEvent = async (event) => {
842
+ // Required run lifecycle persistence executes before extension/user listeners.
843
+ this._handleRunLifecycleEvent(event);
326
844
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
327
845
  // This ensures the UI sees the updated queue state
328
846
  if (event.type === "message_start" && event.message.role === "user") {
@@ -345,28 +863,37 @@ export class AgentSession {
345
863
  }
346
864
  }
347
865
  }
866
+ // Durable tool timeout / late-settlement audits (ALG004-A/B).
867
+ this._handleToolAuditEvent(event);
348
868
  // Emit to extensions first
349
869
  await this._emitExtensionEvent(event);
870
+ const replacement = event.type === "message_end" ? this._messageEndReplacements.get(event) : undefined;
871
+ if (event.type === "message_end") {
872
+ this._messageEndReplacements.delete(event);
873
+ }
874
+ const finalizedEvent = replacement === undefined ? event : Object.freeze({ ...event, message: replacement });
350
875
  // Notify all listeners
351
- this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
876
+ this._emit(finalizedEvent.type === "agent_end"
877
+ ? { ...finalizedEvent, willRetry: this._willRetryAfterAgentEnd(finalizedEvent) }
878
+ : finalizedEvent);
352
879
  // Handle session persistence
353
- if (event.type === "message_end") {
880
+ if (finalizedEvent.type === "message_end") {
354
881
  // Check if this is a custom message from extensions
355
- if (event.message.role === "custom") {
882
+ if (finalizedEvent.message.role === "custom") {
356
883
  // Persist as CustomMessageEntry
357
- this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
884
+ this.sessionManager.appendCustomMessageEntry(finalizedEvent.message.customType, finalizedEvent.message.content, finalizedEvent.message.display, finalizedEvent.message.details);
358
885
  }
359
- else if (event.message.role === "user" ||
360
- event.message.role === "assistant" ||
361
- event.message.role === "toolResult") {
886
+ else if (finalizedEvent.message.role === "user" ||
887
+ finalizedEvent.message.role === "assistant" ||
888
+ finalizedEvent.message.role === "toolResult") {
362
889
  // Regular LLM message - persist as SessionMessageEntry
363
- this.sessionManager.appendMessage(event.message);
890
+ this.sessionManager.appendMessage(finalizedEvent.message);
364
891
  }
365
892
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
366
893
  // Track assistant message for auto-compaction (checked on agent_end)
367
- if (event.message.role === "assistant") {
368
- this._lastAssistantMessage = event.message;
369
- const assistantMsg = event.message;
894
+ if (finalizedEvent.message.role === "assistant") {
895
+ this._lastAssistantMessage = finalizedEvent.message;
896
+ const assistantMsg = finalizedEvent.message;
370
897
  if (assistantMsg.stopReason !== "error") {
371
898
  this._overflowRecoveryAttempted = false;
372
899
  }
@@ -417,19 +944,19 @@ export class AgentSession {
417
944
  }
418
945
  return undefined;
419
946
  }
420
- _replaceMessageInPlace(target, replacement) {
421
- // Agent-core stores the finalized message object in its state before emitting message_end.
422
- // SessionManager persistence happens later in _handleAgentEvent() with event.message.
423
- // Mutating this object in place keeps agent state, later turn/agent events, listeners,
424
- // and the eventual SessionManager.appendMessage(event.message) persistence in sync.
425
- if (target === replacement) {
426
- return;
427
- }
428
- const targetRecord = target;
429
- for (const key of Object.keys(targetRecord)) {
430
- delete targetRecord[key];
431
- }
432
- Object.assign(targetRecord, replacement);
947
+ _replaceFinalizedMessage(target, replacement) {
948
+ const messages = this.agent.state.messages;
949
+ const matchingIndexes = messages.flatMap((message, index) => (isDeepStrictEqual(message, target) ? [index] : []));
950
+ if (matchingIndexes.length !== 1) {
951
+ throw new Error(matchingIndexes.length === 0
952
+ ? "Finalized message was not found in agent state"
953
+ : "Finalized message is ambiguous in agent state");
954
+ }
955
+ const finalizedReplacement = createImmutableMessageSnapshot(replacement);
956
+ const nextMessages = messages.slice();
957
+ nextMessages[matchingIndexes[0]] = finalizedReplacement;
958
+ this.agent.state.messages = nextMessages;
959
+ return finalizedReplacement;
433
960
  }
434
961
  /** Emit extension events based on agent events */
435
962
  async _emitExtensionEvent(event) {
@@ -480,7 +1007,8 @@ export class AgentSession {
480
1007
  };
481
1008
  const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
482
1009
  if (replacement) {
483
- this._replaceMessageInPlace(event.message, replacement);
1010
+ const finalizedReplacement = this._replaceFinalizedMessage(event.message, replacement);
1011
+ this._messageEndReplacements.set(event, finalizedReplacement);
484
1012
  }
485
1013
  }
486
1014
  else if (event.type === "tool_execution_start") {
@@ -647,10 +1175,28 @@ export class AgentSession {
647
1175
  }
648
1176
  }
649
1177
  this.agent.state.tools = tools;
1178
+ this._applyToolTimeoutCategoryDefaults(validToolNames);
650
1179
  // Rebuild base system prompt with new tool set
651
1180
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
652
1181
  this.agent.state.systemPrompt = this._baseSystemPrompt;
653
1182
  }
1183
+ /**
1184
+ * Recompute the Agent's per-name timeout map for the active tool set:
1185
+ * explicit user/settings entries always win; active tools without an entry
1186
+ * receive their §6.3 category default (ALG004-C); uncategorized tools fall
1187
+ * through to the global `toolTimeoutMs`.
1188
+ */
1189
+ _applyToolTimeoutCategoryDefaults(activeToolNames) {
1190
+ try {
1191
+ const resolved = resolveAgentToolSettings(this.settingsManager);
1192
+ this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, resolved.toolTimeouts);
1193
+ }
1194
+ catch {
1195
+ // Invalid settings fail closed at session creation; keep current
1196
+ // explicit entries and still fill category defaults for active tools.
1197
+ this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, this.agent.toolTimeouts ?? {});
1198
+ }
1199
+ }
654
1200
  /** Whether compaction or branch summarization is currently running */
655
1201
  get isCompacting() {
656
1202
  return (this._autoCompactionAbortController !== undefined ||
@@ -791,6 +1337,7 @@ export class AgentSession {
791
1337
  parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
792
1338
  }
793
1339
  const cacheProvider = this._contextBudgetCacheProvider ?? createMemoryContextBudgetCacheProviderV2("session");
1340
+ cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
794
1341
  this._contextBudgetCacheProvider = cacheProvider;
795
1342
  // Enforce floor
796
1343
  if (maxPromptTokens < MIN_PROMPT_TOKENS) {
@@ -870,6 +1417,10 @@ export class AgentSession {
870
1417
  await this.agent.continue();
871
1418
  }
872
1419
  }
1420
+ catch (error) {
1421
+ this._publishRuntimeFailure(error);
1422
+ throw error;
1423
+ }
873
1424
  finally {
874
1425
  this._flushPendingBashMessages();
875
1426
  }
@@ -911,7 +1462,7 @@ export class AgentSession {
911
1462
  async prompt(text, options) {
912
1463
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
913
1464
  const preflightResult = options?.preflightResult;
914
- let currentText = text;
1465
+ let currentText = redactSensitiveText(text);
915
1466
  let promptActiveSkillNames = [...(options?.activeSkillNames ?? [])];
916
1467
  let promptActiveSkillSource = options?.activeSkillSource;
917
1468
  let isBangSkillInvocation = false;
@@ -959,6 +1510,7 @@ export class AgentSession {
959
1510
  expandedText = this._expandSkillCommand(expandedText);
960
1511
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
961
1512
  }
1513
+ expandedText = redactSensitiveText(expandedText);
962
1514
  // If streaming, queue via steer() or followUp() based on option
963
1515
  if (this.isStreaming) {
964
1516
  if (!options?.streamingBehavior) {
@@ -1058,6 +1610,19 @@ export class AgentSession {
1058
1610
  }
1059
1611
  catch (error) {
1060
1612
  preflightResult?.(false);
1613
+ const rawMessage = error instanceof Error ? error.message : String(error);
1614
+ const cause = this._classifyPreflightCause(rawMessage);
1615
+ const timestamp = new Date().toISOString();
1616
+ this._publishTermination(classifySessionTermination({
1617
+ sessionId: this.sessionId,
1618
+ runId: `preflight-${randomUUID()}`,
1619
+ timestamp,
1620
+ source: "observed",
1621
+ message: this._terminationMessage(rawMessage, "Prompt preflight failed."),
1622
+ cause,
1623
+ sideEffects: "none",
1624
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
1625
+ }));
1061
1626
  throw error;
1062
1627
  }
1063
1628
  if (!messages) {
@@ -1132,14 +1697,15 @@ export class AgentSession {
1132
1697
  * @throws Error if text is an extension command
1133
1698
  */
1134
1699
  async steer(text, images) {
1700
+ const sanitizedText = redactSensitiveText(text);
1135
1701
  // Check for extension commands (cannot be queued)
1136
- if (text.startsWith("/")) {
1137
- this._throwIfExtensionCommand(text);
1702
+ if (sanitizedText.startsWith("/")) {
1703
+ this._throwIfExtensionCommand(sanitizedText);
1138
1704
  }
1139
1705
  // Expand skill commands and prompt templates
1140
- let expandedText = this._expandSkillCommand(text);
1706
+ let expandedText = this._expandSkillCommand(sanitizedText);
1141
1707
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1142
- await this._queueSteer(expandedText, images);
1708
+ await this._queueSteer(redactSensitiveText(expandedText), images);
1143
1709
  }
1144
1710
  /**
1145
1711
  * Queue a follow-up message to be processed after the agent finishes.
@@ -1149,19 +1715,21 @@ export class AgentSession {
1149
1715
  * @throws Error if text is an extension command
1150
1716
  */
1151
1717
  async followUp(text, images) {
1718
+ const sanitizedText = redactSensitiveText(text);
1152
1719
  // Check for extension commands (cannot be queued)
1153
- if (text.startsWith("/")) {
1154
- this._throwIfExtensionCommand(text);
1720
+ if (sanitizedText.startsWith("/")) {
1721
+ this._throwIfExtensionCommand(sanitizedText);
1155
1722
  }
1156
1723
  // Expand skill commands and prompt templates
1157
- let expandedText = this._expandSkillCommand(text);
1724
+ let expandedText = this._expandSkillCommand(sanitizedText);
1158
1725
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1159
- await this._queueFollowUp(expandedText, images);
1726
+ await this._queueFollowUp(redactSensitiveText(expandedText), images);
1160
1727
  }
1161
1728
  /**
1162
1729
  * Internal: Queue a steering message (already expanded, no extension command check).
1163
1730
  */
1164
1731
  async _queueSteer(text, images) {
1732
+ this._invalidateContextBudgetCache({ type: "userSteering" });
1165
1733
  this._steeringMessages.push(text);
1166
1734
  this._emitQueueUpdate();
1167
1735
  const content = [{ type: "text", text }];
@@ -1222,6 +1790,7 @@ export class AgentSession {
1222
1790
  details: message.details,
1223
1791
  timestamp: Date.now(),
1224
1792
  };
1793
+ this._recordEvidenceReceiptInvalidation(message.customType);
1225
1794
  if (options?.deliverAs === "nextTurn") {
1226
1795
  this._pendingNextTurnMessages.push(appMessage);
1227
1796
  }
@@ -1313,16 +1882,43 @@ export class AgentSession {
1313
1882
  * Abort current operation and wait for agent to become idle.
1314
1883
  */
1315
1884
  async abort() {
1885
+ this._userAbortRequested = this.isStreaming || this.isRetrying;
1316
1886
  this.abortRetry();
1317
1887
  this.agent.abort();
1318
1888
  await this.agent.waitForIdle();
1319
1889
  }
1890
+ /** Record an observed process signal before a mode begins shutdown. */
1891
+ recordProcessSignal(signal) {
1892
+ const timestamp = new Date().toISOString();
1893
+ const runId = this._activeRunId ?? `signal-${randomUUID()}`;
1894
+ const termination = classifySessionTermination({
1895
+ sessionId: this.sessionId,
1896
+ runId,
1897
+ timestamp,
1898
+ source: "observed",
1899
+ message: `Process received ${signal}.`,
1900
+ cause: { area: "process", code: "signal", signal },
1901
+ sideEffects: this._activeRunId === null ? "none" : "possible",
1902
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
1903
+ });
1904
+ if (this._activeRunId !== null) {
1905
+ this._runJournalStore.finish({
1906
+ termination,
1907
+ sessionRevision: this._sessionRevision(),
1908
+ timestamp,
1909
+ });
1910
+ this._activeRunId = null;
1911
+ }
1912
+ this._publishTermination(termination);
1913
+ return termination;
1914
+ }
1320
1915
  // =========================================================================
1321
1916
  // Model Management
1322
1917
  // =========================================================================
1323
1918
  async _emitModelSelect(nextModel, previousModel, source) {
1324
1919
  if (modelsAreEqual(previousModel, nextModel))
1325
1920
  return;
1921
+ this._invalidateContextBudgetCache({ type: "activeModelId", value: this._contextCacheModelId(nextModel) });
1326
1922
  await this._extensionRunner.emit({
1327
1923
  type: "model_select",
1328
1924
  model: nextModel,
@@ -1660,17 +2256,330 @@ export class AgentSession {
1660
2256
  // =========================================================================
1661
2257
  // Compaction
1662
2258
  // =========================================================================
2259
+ _captureCompactionState() {
2260
+ return this.sessionManager.withCompactionCommitLock(() => this._captureCompactionStateLocked());
2261
+ }
2262
+ _captureCompactionStateLocked() {
2263
+ const sessionFile = this.sessionManager.getSessionFile();
2264
+ const bytes = sessionFile && existsSync(sessionFile)
2265
+ ? new Uint8Array(readFileSync(sessionFile))
2266
+ : new TextEncoder().encode(`${[this.sessionManager.getHeader(), ...this.sessionManager.getEntries()]
2267
+ .filter((entry) => entry !== null)
2268
+ .map((entry) => JSON.stringify(entry))
2269
+ .join("\n")}\n`);
2270
+ const report = inspectSessionIntegrity(bytes, { activeLeafId: this.sessionManager.getLeafId() });
2271
+ const branchEntries = report.activeBranch;
2272
+ let latestCompactionIndex = -1;
2273
+ for (let index = branchEntries.length - 1; index >= 0; index -= 1) {
2274
+ if (branchEntries[index]?.type === "compaction") {
2275
+ latestCompactionIndex = index;
2276
+ break;
2277
+ }
2278
+ }
2279
+ const latestCompaction = branchEntries[latestCompactionIndex];
2280
+ const firstKeptIndex = latestCompaction?.type === "compaction"
2281
+ ? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)
2282
+ : -1;
2283
+ const sourceEntries = branchEntries.slice(latestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);
2284
+ const firstEntry = sourceEntries[0];
2285
+ const lastEntry = sourceEntries.at(-1);
2286
+ if (!firstEntry || !lastEntry || report.activeLeafId === null) {
2287
+ const barrier = evaluateCompactionBarrier(report, [...this.agent.state.pendingToolCalls]);
2288
+ if (barrier.status !== "ready")
2289
+ throw this._barrierError(barrier);
2290
+ throw new Error("Nothing to compact: the active session branch is empty");
2291
+ }
2292
+ const revision = this.sessionManager.getDurableHeadToken();
2293
+ const source = createCompactionSourceIdentity({
2294
+ sessionId: revision.sessionId,
2295
+ entryIds: sourceEntries.map((entry) => entry.id),
2296
+ firstEntryId: firstEntry.id,
2297
+ lastEntryId: lastEntry.id,
2298
+ sourceSha256: createHash("sha256")
2299
+ .update(sourceEntries.map((entry) => JSON.stringify(entry)).join("\n"), "utf8")
2300
+ .digest("hex"),
2301
+ activeLeafId: report.activeLeafId,
2302
+ messageCount: report.activeMessages.length,
2303
+ });
2304
+ return { report, branchEntries, revision, source };
2305
+ }
2306
+ _captureCompactionProvenance(capture) {
2307
+ let latestIntent = "Continue the current session";
2308
+ for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
2309
+ const message = capture.report.activeMessages[index];
2310
+ if (message?.role !== "user")
2311
+ continue;
2312
+ const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim()).slice(0, 16_384);
2313
+ if (candidate.length > 0)
2314
+ latestIntent = candidate;
2315
+ break;
2316
+ }
2317
+ const modelHistory = capture.branchEntries
2318
+ .flatMap((entry) => {
2319
+ if (entry.type === "model_change") {
2320
+ return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
2321
+ }
2322
+ if (entry.type === "message" && entry.message.role === "assistant") {
2323
+ return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
2324
+ }
2325
+ return [];
2326
+ })
2327
+ .slice(-256);
2328
+ const customEntryIds = (customType) => capture.branchEntries
2329
+ .filter((entry) => entry.type === "custom" && entry.customType === customType)
2330
+ .map((entry) => entry.id);
2331
+ return {
2332
+ latestIntent,
2333
+ openTasks: [],
2334
+ laneIds: customEntryIds("lane"),
2335
+ acceptancePredicateIds: customEntryIds("acceptance_predicate"),
2336
+ evidenceReceiptIds: customEntryIds("evidence_receipt"),
2337
+ blockerReasons: [],
2338
+ repairEventIds: [
2339
+ ...customEntryIds("transcript_repaired"),
2340
+ ...customEntryIds("compaction_transcript_repaired"),
2341
+ ],
2342
+ branch: null,
2343
+ worktree: this._cwd,
2344
+ modelHistory,
2345
+ nextAction: latestIntent,
2346
+ };
2347
+ }
2348
+ _barrierError(barrier) {
2349
+ if (barrier.status === "defer") {
2350
+ return new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);
2351
+ }
2352
+ return new Error(`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`);
2353
+ }
2354
+ _evaluateCompactionBarrier(capture, includeMissingTailAsPending, excludedPendingIds = new Set()) {
2355
+ const pending = new Set([...this.agent.state.pendingToolCalls].filter((toolCallId) => !excludedPendingIds.has(toolCallId)));
2356
+ if (includeMissingTailAsPending) {
2357
+ for (const issue of capture.report.transcript?.issues ?? []) {
2358
+ if (issue.kind === "missing_result")
2359
+ pending.add(issue.toolCallId);
2360
+ }
2361
+ }
2362
+ return evaluateCompactionBarrier(capture.report, [...pending]);
2363
+ }
2364
+ _repairEmergencyCompactionTail(capture) {
2365
+ const barrier = this._evaluateCompactionBarrier(capture, true);
2366
+ if (barrier.status !== "defer" || barrier.reason !== "missing_active_tail_results") {
2367
+ if (barrier.status !== "ready")
2368
+ throw this._barrierError(barrier);
2369
+ return { capture, repairedToolCallIds: new Set() };
2370
+ }
2371
+ const repairedMessages = repairTranscriptIntegrity([...capture.report.activeMessages], "Tool result missing; synthesized to close an emergency compaction barrier");
2372
+ const inserted = repairedMessages.slice(capture.report.activeMessages.length);
2373
+ const repairedToolCallIds = new Set();
2374
+ for (const message of inserted) {
2375
+ if (message.role !== "toolResult") {
2376
+ throw new Error("Emergency compaction repair produced a non-tool result");
2377
+ }
2378
+ const toolResult = message;
2379
+ repairedToolCallIds.add(toolResult.toolCallId);
2380
+ this.sessionManager.appendMessage(toolResult);
2381
+ }
2382
+ this.sessionManager.appendCustomEntry("compaction_transcript_repaired", {
2383
+ insertedToolCallIds: [...repairedToolCallIds],
2384
+ reason: "emergency_compaction",
2385
+ });
2386
+ this._invalidateContextBudgetCache({ type: "transcriptRepair" });
2387
+ const closedCapture = this._captureCompactionState();
2388
+ const closedBarrier = this._evaluateCompactionBarrier(closedCapture, false, repairedToolCallIds);
2389
+ if (closedBarrier.status !== "ready")
2390
+ throw this._barrierError(closedBarrier);
2391
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
2392
+ return { capture: closedCapture, repairedToolCallIds };
2393
+ }
2394
+ _priorCommittedCompactionSourceDigests() {
2395
+ const digests = [];
2396
+ for (const entry of this.sessionManager.getEntries()) {
2397
+ if (entry.type !== "compaction" || typeof entry.details !== "object" || entry.details === null)
2398
+ continue;
2399
+ if (!Object.hasOwn(entry.details, "compactionEnvelope"))
2400
+ continue;
2401
+ const envelope = validateCompactionEnvelope(Reflect.get(entry.details, "compactionEnvelope"));
2402
+ if (envelope.summary !== entry.summary) {
2403
+ throw new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);
2404
+ }
2405
+ digests.push(envelope.source.sourceSha256);
2406
+ }
2407
+ return digests;
2408
+ }
2409
+ _beginCompactionTransaction(compactionModel, emergency) {
2410
+ let capture = this._captureCompactionState();
2411
+ if (emergency) {
2412
+ capture = this._repairEmergencyCompactionTail(capture).capture;
2413
+ }
2414
+ else {
2415
+ const barrier = this._evaluateCompactionBarrier(capture, false);
2416
+ if (barrier.status !== "ready")
2417
+ throw this._barrierError(barrier);
2418
+ }
2419
+ const transaction = createCompactionTransaction({
2420
+ transactionId: randomUUID(),
2421
+ baseRevision: capture.revision,
2422
+ source: capture.source,
2423
+ createdAt: new Date().toISOString(),
2424
+ model: { provider: compactionModel.provider, id: compactionModel.id },
2425
+ preserved: this._captureCompactionProvenance(capture),
2426
+ });
2427
+ if (this._priorCommittedCompactionSourceDigests().includes(transaction.source.sourceSha256)) {
2428
+ throw new Error("This exact compaction source was already compacted");
2429
+ }
2430
+ return { capture, transaction };
2431
+ }
2432
+ _detailsWithCompactionEnvelope(details, envelope) {
2433
+ if (typeof details === "object" && details !== null && !Array.isArray(details)) {
2434
+ return { ...details, compactionEnvelope: envelope };
2435
+ }
2436
+ return {
2437
+ compactionEnvelope: envelope,
2438
+ ...(details === undefined ? {} : { resultDetails: details }),
2439
+ };
2440
+ }
2441
+ _commitCompaction(begun, result, fromExtension) {
2442
+ if (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {
2443
+ throw new Error("Compaction first-kept entry is outside the captured source");
2444
+ }
2445
+ const committed = this.sessionManager.withCompactionCommitLock(() => {
2446
+ const current = this._captureCompactionState();
2447
+ const barrier = this._evaluateCompactionBarrier(current, false);
2448
+ const decision = decideCompactionCommit({
2449
+ transaction: begun.transaction,
2450
+ currentRevision: current.revision,
2451
+ currentSource: current.source,
2452
+ barrier,
2453
+ priorCommittedSourceDigests: this._priorCommittedCompactionSourceDigests(),
2454
+ });
2455
+ switch (decision.decision) {
2456
+ case "duplicate":
2457
+ throw new Error("This exact compaction source was already compacted");
2458
+ case "stale":
2459
+ throw new Error(`Session changed during compaction (${decision.reason}); generated summary was discarded`);
2460
+ case "defer":
2461
+ case "fail_closed":
2462
+ throw this._barrierError(barrier);
2463
+ case "commit": {
2464
+ const envelope = createCompactionEnvelope({
2465
+ transaction: begun.transaction,
2466
+ decision,
2467
+ summary: result.summary,
2468
+ summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
2469
+ });
2470
+ const entryId = this.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, this._detailsWithCompactionEnvelope(result.details, envelope), fromExtension);
2471
+ const entry = this.sessionManager.getEntry(entryId);
2472
+ if (!entry || entry.type !== "compaction") {
2473
+ throw new Error("Compaction commit did not produce a compaction entry");
2474
+ }
2475
+ return { entry, envelope };
2476
+ }
2477
+ }
2478
+ });
2479
+ this._recordCompactionCommitForHysteresis();
2480
+ return committed;
2481
+ }
2482
+ _pendingToolResultReserve(settings) {
2483
+ const pendingIds = this.agent.state.pendingToolCalls;
2484
+ const calls = new Map();
2485
+ const streaming = this.agent.state.streamingMessage;
2486
+ const messages = streaming ? [...this.agent.state.messages, streaming] : this.agent.state.messages;
2487
+ for (const message of messages) {
2488
+ if (message.role !== "assistant" || !Array.isArray(message.content))
2489
+ continue;
2490
+ for (const part of message.content) {
2491
+ if (part.type === "toolCall" && pendingIds.has(part.id)) {
2492
+ calls.set(part.id, { name: part.name, args: part.arguments });
2493
+ }
2494
+ }
2495
+ }
2496
+ const counts = { text: 0, image: 0, "large-output": 0 };
2497
+ for (const id of pendingIds) {
2498
+ const call = calls.get(id);
2499
+ const resultClass = call ? pendingToolResultClass(call.name, call.args) : "large-output";
2500
+ counts[resultClass] += 1;
2501
+ }
2502
+ const requests = [];
2503
+ for (const resultClass of ["text", "image", "large-output"]) {
2504
+ if (counts[resultClass] > 0) {
2505
+ requests.push({
2506
+ class: resultClass,
2507
+ count: counts[resultClass],
2508
+ tokensPerResult: PENDING_TOOL_RESULT_TOKENS[resultClass],
2509
+ });
2510
+ }
2511
+ }
2512
+ const configured = settings.reservedToolResultTokens ?? 0;
2513
+ requests.push({ class: "large-output", count: 1, tokensPerResult: configured });
2514
+ return estimateToolResultReserve(requests);
2515
+ }
2516
+ _compactionHysteresisConfig(contextWindow, settings) {
2517
+ const threshold = getCompactionHeadroomThreshold(contextWindow, {
2518
+ ...settings,
2519
+ reservedToolResultTokens: this._pendingToolResultReserve(settings),
2520
+ });
2521
+ if (!threshold)
2522
+ return undefined;
2523
+ const triggerRatio = Math.min(1, Math.max(1 / Math.floor(contextWindow), threshold.triggerTokens / contextWindow));
2524
+ const configuredRearm = settings.rearmRatio ?? triggerRatio * 0.75;
2525
+ const rearmRatio = Math.min(configuredRearm, triggerRatio * 0.999);
2526
+ const emergencyRatio = Math.max(triggerRatio, settings.emergencyRatio ?? 0.98);
2527
+ return createCompactionHysteresisConfig({ rearmRatio, triggerRatio, emergencyRatio });
2528
+ }
2529
+ _runtimeCompactionDecision(contextTokens, contextWindow, settings) {
2530
+ if (!Number.isFinite(contextTokens) || contextTokens < 0)
2531
+ return { compact: false, emergency: false };
2532
+ const config = this._compactionHysteresisConfig(contextWindow, settings);
2533
+ if (!config)
2534
+ return { compact: false, emergency: false };
2535
+ const result = stepCompactionHysteresis({
2536
+ config,
2537
+ state: this._compactionHysteresisState,
2538
+ ratio: Math.min(1, contextTokens / contextWindow),
2539
+ });
2540
+ this._compactionHysteresisState = result.nextState;
2541
+ return { compact: result.action === "compact", emergency: result.reason === "emergency_threshold_reached" };
2542
+ }
2543
+ async _runThresholdCompaction(emergency) {
2544
+ this._thresholdCompactionEmergency = emergency;
2545
+ try {
2546
+ return await this._runAutoCompaction("threshold", false);
2547
+ }
2548
+ finally {
2549
+ this._thresholdCompactionEmergency = false;
2550
+ }
2551
+ }
2552
+ _recordCompactionCommitForHysteresis() {
2553
+ const contextWindow = this.model?.contextWindow ?? 0;
2554
+ const config = this._compactionHysteresisConfig(contextWindow, this.settingsManager.getCompactionSettings());
2555
+ if (!config)
2556
+ return;
2557
+ this._compactionHysteresisState = stepCompactionHysteresis({
2558
+ config,
2559
+ state: this._compactionHysteresisState,
2560
+ ratio: 0,
2561
+ outcome: "commit",
2562
+ }).nextState;
2563
+ }
1663
2564
  _resolveCompactionModel(sessionModel) {
1664
2565
  const configuredModel = this.settingsManager.getCompactionModel();
1665
2566
  const availableModels = this._modelRegistry.getAvailable();
2567
+ let model;
1666
2568
  if (configuredModel) {
1667
- const model = findExactModelReferenceMatch(configuredModel, availableModels);
1668
- if (!model) {
2569
+ const configured = findExactModelReferenceMatch(configuredModel, availableModels);
2570
+ if (!configured) {
1669
2571
  throw new Error(`Configured compaction model "${configuredModel}" is unavailable or unauthenticated.`);
1670
2572
  }
1671
- return model;
2573
+ model = configured;
2574
+ }
2575
+ else {
2576
+ model = resolveCompactionModel(sessionModel, availableModels);
1672
2577
  }
1673
- return resolveCompactionModel(sessionModel, availableModels);
2578
+ this._invalidateContextBudgetCache({
2579
+ type: "compactionModelId",
2580
+ value: this._contextCacheModelId(model),
2581
+ });
2582
+ return model;
1674
2583
  }
1675
2584
  /**
1676
2585
  * Manually compact the session context.
@@ -1682,14 +2591,16 @@ export class AgentSession {
1682
2591
  await this.abort();
1683
2592
  this._compactionAbortController = new AbortController();
1684
2593
  this._emit({ type: "compaction_start", reason: "manual" });
2594
+ let committedCompaction = false;
1685
2595
  try {
1686
2596
  if (!this.model) {
1687
2597
  throw new Error(formatNoModelSelectedMessage());
1688
2598
  }
1689
2599
  const compactionModel = this._resolveCompactionModel(this.model);
1690
2600
  const { apiKey, headers } = await this._getCompactionRequestAuth(compactionModel);
1691
- const pathEntries = this.sessionManager.getBranch();
1692
2601
  const settings = this.settingsManager.getCompactionSettings();
2602
+ const begun = this._beginCompactionTransaction(compactionModel, false);
2603
+ const pathEntries = [...begun.capture.branchEntries];
1693
2604
  const preparation = prepareCompaction(pathEntries, settings);
1694
2605
  if (!preparation) {
1695
2606
  // Check why we can't compact
@@ -1739,25 +2650,22 @@ export class AgentSession {
1739
2650
  if (this._compactionAbortController.signal.aborted) {
1740
2651
  throw new Error("Compaction cancelled");
1741
2652
  }
1742
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1743
- const newEntries = this.sessionManager.getEntries();
1744
- const sessionContext = this.sessionManager.buildSessionContext();
1745
- this.agent.state.messages = sessionContext.messages;
1746
- // Get the saved compaction entry for the extension event
1747
- const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1748
- if (this._extensionRunner && savedCompactionEntry) {
1749
- await this._extensionRunner.emit({
1750
- type: "session_compact",
1751
- compactionEntry: savedCompactionEntry,
1752
- fromExtension,
1753
- });
1754
- }
1755
2653
  const compactionResult = {
1756
2654
  summary,
1757
2655
  firstKeptEntryId,
1758
2656
  tokensBefore,
1759
2657
  details,
1760
2658
  };
2659
+ const committed = this._commitCompaction(begun, compactionResult, fromExtension);
2660
+ committedCompaction = true;
2661
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
2662
+ if (this._extensionRunner) {
2663
+ await this._extensionRunner.emit({
2664
+ type: "session_compact",
2665
+ compactionEntry: committed.entry,
2666
+ fromExtension,
2667
+ });
2668
+ }
1761
2669
  this._emit({
1762
2670
  type: "compaction_end",
1763
2671
  reason: "manual",
@@ -1770,6 +2678,18 @@ export class AgentSession {
1770
2678
  catch (error) {
1771
2679
  const message = error instanceof Error ? error.message : String(error);
1772
2680
  const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
2681
+ const stale = /stale|session changed during compaction|already compacted/i.test(message);
2682
+ const timestamp = new Date().toISOString();
2683
+ this._publishTermination(classifySessionTermination({
2684
+ sessionId: this.sessionId,
2685
+ runId: `compaction-${randomUUID()}`,
2686
+ timestamp,
2687
+ source: "observed",
2688
+ message: this._terminationMessage(message, "Manual compaction failed."),
2689
+ cause: { area: "compaction", code: aborted ? "aborted" : stale ? "stale" : "failed" },
2690
+ sideEffects: committedCompaction ? "confirmed" : "none",
2691
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
2692
+ }));
1773
2693
  this._emit({
1774
2694
  type: "compaction_end",
1775
2695
  reason: "manual",
@@ -1815,8 +2735,9 @@ export class AgentSession {
1815
2735
  return false;
1816
2736
  }
1817
2737
  }
1818
- if (shouldCompact(estimate.tokens, contextWindow, settings)) {
1819
- return await this._runAutoCompaction("threshold", false);
2738
+ const decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);
2739
+ if (decision.compact) {
2740
+ return await this._runThresholdCompaction(decision.emergency);
1820
2741
  }
1821
2742
  return false;
1822
2743
  }
@@ -1897,15 +2818,16 @@ export class AgentSession {
1897
2818
  else {
1898
2819
  contextTokens = calculateContextTokens(assistantMessage.usage);
1899
2820
  }
1900
- if (shouldCompact(contextTokens, contextWindow, settings)) {
1901
- return await this._runAutoCompaction("threshold", false);
2821
+ const decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);
2822
+ if (decision.compact) {
2823
+ return await this._runThresholdCompaction(decision.emergency);
1902
2824
  }
1903
2825
  return false;
1904
2826
  }
1905
2827
  /**
1906
2828
  * Internal: Run auto-compaction with events.
1907
2829
  */
1908
- async _runAutoCompaction(reason, willRetry) {
2830
+ async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
1909
2831
  const settings = this.settingsManager.getCompactionSettings();
1910
2832
  this._emit({ type: "compaction_start", reason });
1911
2833
  this._autoCompactionAbortController = new AbortController();
@@ -1944,7 +2866,8 @@ export class AgentSession {
1944
2866
  else {
1945
2867
  ({ apiKey, headers } = await this._getCompactionRequestAuth(compactionModel));
1946
2868
  }
1947
- const pathEntries = this.sessionManager.getBranch();
2869
+ const begun = this._beginCompactionTransaction(compactionModel, emergency);
2870
+ const pathEntries = [...begun.capture.branchEntries];
1948
2871
  const preparation = prepareCompaction(pathEntries, settings);
1949
2872
  if (!preparation) {
1950
2873
  this._emit({
@@ -2010,25 +2933,21 @@ export class AgentSession {
2010
2933
  });
2011
2934
  return false;
2012
2935
  }
2013
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
2014
- const newEntries = this.sessionManager.getEntries();
2015
- const sessionContext = this.sessionManager.buildSessionContext();
2016
- this.agent.state.messages = sessionContext.messages;
2017
- // Get the saved compaction entry for the extension event
2018
- const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
2019
- if (this._extensionRunner && savedCompactionEntry) {
2020
- await this._extensionRunner.emit({
2021
- type: "session_compact",
2022
- compactionEntry: savedCompactionEntry,
2023
- fromExtension,
2024
- });
2025
- }
2026
2936
  const result = {
2027
2937
  summary,
2028
2938
  firstKeptEntryId,
2029
2939
  tokensBefore,
2030
2940
  details,
2031
2941
  };
2942
+ const committed = this._commitCompaction(begun, result, fromExtension);
2943
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
2944
+ if (this._extensionRunner) {
2945
+ await this._extensionRunner.emit({
2946
+ type: "session_compact",
2947
+ compactionEntry: committed.entry,
2948
+ fromExtension,
2949
+ });
2950
+ }
2032
2951
  const emitWillRetry = compactionEmitWillRetry(willRetry, this.agent.hasQueuedMessages());
2033
2952
  this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry: emitWillRetry });
2034
2953
  if (willRetry) {
@@ -2196,6 +3115,7 @@ export class AgentSession {
2196
3115
  },
2197
3116
  appendEntry: (customType, data) => {
2198
3117
  this.sessionManager.appendCustomEntry(customType, data);
3118
+ this._recordEvidenceReceiptInvalidation(customType);
2199
3119
  },
2200
3120
  setSessionName: (name) => {
2201
3121
  this.setSessionName(name);
@@ -2414,6 +3334,7 @@ export class AgentSession {
2414
3334
  const previousFlagValues = this._extensionRunner.getFlagValues();
2415
3335
  await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
2416
3336
  await this.settingsManager.reload();
3337
+ this._invalidateContextBudgetCache({ type: "settings" });
2417
3338
  resetApiProviders();
2418
3339
  await this._resourceLoader.reload();
2419
3340
  this._buildRuntime({