open-multi-agent-kit 0.90.7 → 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 (331) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +17 -10
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  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 +116 -2
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +1015 -71
  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-headroom.d.ts.map +1 -1
  63. package/dist/core/context-budget-headroom.js +5 -1
  64. package/dist/core/context-budget-headroom.js.map +1 -1
  65. package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
  66. package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
  67. package/dist/core/context-budget-reserved-tokens.js +188 -0
  68. package/dist/core/context-budget-reserved-tokens.js.map +1 -0
  69. package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
  70. package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
  71. package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
  72. package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
  73. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  74. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  75. package/dist/core/context-budget-v2-cache-keys.js +6 -0
  76. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  77. package/dist/core/context-budget-v2-cache-provider.d.ts +8 -2
  78. package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
  79. package/dist/core/context-budget-v2-cache-provider.js +28 -5
  80. package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
  81. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  82. package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
  83. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  84. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  85. package/dist/core/context-budget-v2-planner.js +1 -0
  86. package/dist/core/context-budget-v2-planner.js.map +1 -1
  87. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  88. package/dist/core/context-budget-v2-selection.js +9 -0
  89. package/dist/core/context-budget-v2-selection.js.map +1 -1
  90. package/dist/core/context-budget-v2-types.d.ts +4 -0
  91. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  92. package/dist/core/context-budget-v2-types.js.map +1 -1
  93. package/dist/core/durable-file-directory.d.ts +4 -0
  94. package/dist/core/durable-file-directory.d.ts.map +1 -0
  95. package/dist/core/durable-file-directory.js +76 -0
  96. package/dist/core/durable-file-directory.js.map +1 -0
  97. package/dist/core/durable-file-identity.d.ts +26 -0
  98. package/dist/core/durable-file-identity.d.ts.map +1 -0
  99. package/dist/core/durable-file-identity.js +118 -0
  100. package/dist/core/durable-file-identity.js.map +1 -0
  101. package/dist/core/durable-file-io.d.ts +29 -0
  102. package/dist/core/durable-file-io.d.ts.map +1 -0
  103. package/dist/core/durable-file-io.js +179 -0
  104. package/dist/core/durable-file-io.js.map +1 -0
  105. package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
  106. package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
  107. package/dist/core/durable-file-lock-cleanup.js +39 -0
  108. package/dist/core/durable-file-lock-cleanup.js.map +1 -0
  109. package/dist/core/durable-file-lock-owner.d.ts +18 -0
  110. package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
  111. package/dist/core/durable-file-lock-owner.js +78 -0
  112. package/dist/core/durable-file-lock-owner.js.map +1 -0
  113. package/dist/core/durable-file-lock-storage.d.ts +35 -0
  114. package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
  115. package/dist/core/durable-file-lock-storage.js +263 -0
  116. package/dist/core/durable-file-lock-storage.js.map +1 -0
  117. package/dist/core/durable-file-mode.d.ts +8 -0
  118. package/dist/core/durable-file-mode.d.ts.map +1 -0
  119. package/dist/core/durable-file-mode.js +41 -0
  120. package/dist/core/durable-file-mode.js.map +1 -0
  121. package/dist/core/extensions/loader.d.ts +12 -0
  122. package/dist/core/extensions/loader.d.ts.map +1 -1
  123. package/dist/core/extensions/loader.js +50 -37
  124. package/dist/core/extensions/loader.js.map +1 -1
  125. package/dist/core/extensions/types.d.ts +2 -0
  126. package/dist/core/extensions/types.d.ts.map +1 -1
  127. package/dist/core/extensions/types.js.map +1 -1
  128. package/dist/core/index.d.ts +6 -2
  129. package/dist/core/index.d.ts.map +1 -1
  130. package/dist/core/index.js +5 -0
  131. package/dist/core/index.js.map +1 -1
  132. package/dist/core/loop-gates.d.ts.map +1 -1
  133. package/dist/core/loop-gates.js +25 -1
  134. package/dist/core/loop-gates.js.map +1 -1
  135. package/dist/core/model-resolver.d.ts.map +1 -1
  136. package/dist/core/model-resolver.js +1 -1
  137. package/dist/core/model-resolver.js.map +1 -1
  138. package/dist/core/pi-package-intake.d.ts +1 -0
  139. package/dist/core/pi-package-intake.d.ts.map +1 -1
  140. package/dist/core/pi-package-intake.js +7 -0
  141. package/dist/core/pi-package-intake.js.map +1 -1
  142. package/dist/core/redaction.d.ts +8 -0
  143. package/dist/core/redaction.d.ts.map +1 -0
  144. package/dist/core/redaction.js +31 -0
  145. package/dist/core/redaction.js.map +1 -0
  146. package/dist/core/resource-loader.d.ts.map +1 -1
  147. package/dist/core/resource-loader.js +36 -0
  148. package/dist/core/resource-loader.js.map +1 -1
  149. package/dist/core/run-journal-store.d.ts +53 -0
  150. package/dist/core/run-journal-store.d.ts.map +1 -0
  151. package/dist/core/run-journal-store.js +278 -0
  152. package/dist/core/run-journal-store.js.map +1 -0
  153. package/dist/core/run-journal.d.ts +172 -0
  154. package/dist/core/run-journal.d.ts.map +1 -0
  155. package/dist/core/run-journal.js +1053 -0
  156. package/dist/core/run-journal.js.map +1 -0
  157. package/dist/core/sdk.d.ts +7 -0
  158. package/dist/core/sdk.d.ts.map +1 -1
  159. package/dist/core/sdk.js +94 -3
  160. package/dist/core/sdk.js.map +1 -1
  161. package/dist/core/session-doctor-plan.d.ts +128 -0
  162. package/dist/core/session-doctor-plan.d.ts.map +1 -0
  163. package/dist/core/session-doctor-plan.js +833 -0
  164. package/dist/core/session-doctor-plan.js.map +1 -0
  165. package/dist/core/session-file-compaction.d.ts +6 -0
  166. package/dist/core/session-file-compaction.d.ts.map +1 -0
  167. package/dist/core/session-file-compaction.js +169 -0
  168. package/dist/core/session-file-compaction.js.map +1 -0
  169. package/dist/core/session-file-head.d.ts +10 -0
  170. package/dist/core/session-file-head.d.ts.map +1 -0
  171. package/dist/core/session-file-head.js +78 -0
  172. package/dist/core/session-file-head.js.map +1 -0
  173. package/dist/core/session-file-persistence.d.ts +30 -0
  174. package/dist/core/session-file-persistence.d.ts.map +1 -0
  175. package/dist/core/session-file-persistence.js +106 -0
  176. package/dist/core/session-file-persistence.js.map +1 -0
  177. package/dist/core/session-integrity.d.ts +43 -0
  178. package/dist/core/session-integrity.d.ts.map +1 -0
  179. package/dist/core/session-integrity.js +420 -0
  180. package/dist/core/session-integrity.js.map +1 -0
  181. package/dist/core/session-manager.d.ts +24 -4
  182. package/dist/core/session-manager.d.ts.map +1 -1
  183. package/dist/core/session-manager.js +242 -125
  184. package/dist/core/session-manager.js.map +1 -1
  185. package/dist/core/session-owner-lease.d.ts +22 -0
  186. package/dist/core/session-owner-lease.d.ts.map +1 -0
  187. package/dist/core/session-owner-lease.js +176 -0
  188. package/dist/core/session-owner-lease.js.map +1 -0
  189. package/dist/core/session-path-policy.d.ts +132 -0
  190. package/dist/core/session-path-policy.d.ts.map +1 -0
  191. package/dist/core/session-path-policy.js +780 -0
  192. package/dist/core/session-path-policy.js.map +1 -0
  193. package/dist/core/session-repair-plan.d.ts +49 -0
  194. package/dist/core/session-repair-plan.d.ts.map +1 -0
  195. package/dist/core/session-repair-plan.js +202 -0
  196. package/dist/core/session-repair-plan.js.map +1 -0
  197. package/dist/core/session-termination.d.ts +100 -0
  198. package/dist/core/session-termination.d.ts.map +1 -0
  199. package/dist/core/session-termination.js +403 -0
  200. package/dist/core/session-termination.js.map +1 -0
  201. package/dist/core/settings-manager.d.ts +32 -0
  202. package/dist/core/settings-manager.d.ts.map +1 -1
  203. package/dist/core/settings-manager.js +52 -5
  204. package/dist/core/settings-manager.js.map +1 -1
  205. package/dist/core/system-prompt.d.ts.map +1 -1
  206. package/dist/core/system-prompt.js +1 -1
  207. package/dist/core/system-prompt.js.map +1 -1
  208. package/dist/core/verified-bash-adapter.d.ts +36 -0
  209. package/dist/core/verified-bash-adapter.d.ts.map +1 -0
  210. package/dist/core/verified-bash-adapter.js +90 -0
  211. package/dist/core/verified-bash-adapter.js.map +1 -0
  212. package/dist/guardrails/command-redaction.d.ts +32 -0
  213. package/dist/guardrails/command-redaction.d.ts.map +1 -0
  214. package/dist/guardrails/command-redaction.js +776 -0
  215. package/dist/guardrails/command-redaction.js.map +1 -0
  216. package/dist/guardrails/evidence-attestation.d.ts +31 -0
  217. package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
  218. package/dist/guardrails/evidence-attestation.js +178 -0
  219. package/dist/guardrails/evidence-attestation.js.map +1 -0
  220. package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
  221. package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
  222. package/dist/guardrails/evidence-receipt-store.js +446 -0
  223. package/dist/guardrails/evidence-receipt-store.js.map +1 -0
  224. package/dist/guardrails/evidence-receipt.d.ts +58 -0
  225. package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
  226. package/dist/guardrails/evidence-receipt.js +389 -0
  227. package/dist/guardrails/evidence-receipt.js.map +1 -0
  228. package/dist/guardrails/evidence-system.d.ts +40 -9
  229. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  230. package/dist/guardrails/evidence-system.js +379 -83
  231. package/dist/guardrails/evidence-system.js.map +1 -1
  232. package/dist/guardrails/replay-ledger-io.d.ts +6 -0
  233. package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
  234. package/dist/guardrails/replay-ledger-io.js +42 -0
  235. package/dist/guardrails/replay-ledger-io.js.map +1 -0
  236. package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
  237. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
  238. package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
  239. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
  240. package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
  241. package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
  242. package/dist/guardrails/replay-ledger-lock.js +135 -0
  243. package/dist/guardrails/replay-ledger-lock.js.map +1 -0
  244. package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
  245. package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
  246. package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
  247. package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
  248. package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
  249. package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
  250. package/dist/guardrails/replay-ledger-quarantine.js +19 -0
  251. package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
  252. package/dist/guardrails/replay-ledger-store.d.ts +32 -0
  253. package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
  254. package/dist/guardrails/replay-ledger-store.js +211 -0
  255. package/dist/guardrails/replay-ledger-store.js.map +1 -0
  256. package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
  257. package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
  258. package/dist/guardrails/shell-command-name-glob.js +233 -0
  259. package/dist/guardrails/shell-command-name-glob.js.map +1 -0
  260. package/dist/guardrails/shell-command-option.d.ts +7 -0
  261. package/dist/guardrails/shell-command-option.d.ts.map +1 -0
  262. package/dist/guardrails/shell-command-option.js +24 -0
  263. package/dist/guardrails/shell-command-option.js.map +1 -0
  264. package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
  265. package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
  266. package/dist/guardrails/shell-command-tokenizer.js +145 -0
  267. package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
  268. package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
  269. package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
  270. package/dist/guardrails/shell-command-wrapper.js +42 -0
  271. package/dist/guardrails/shell-command-wrapper.js.map +1 -0
  272. package/dist/guardrails/verified-executor.d.ts +65 -0
  273. package/dist/guardrails/verified-executor.d.ts.map +1 -0
  274. package/dist/guardrails/verified-executor.js +171 -0
  275. package/dist/guardrails/verified-executor.js.map +1 -0
  276. package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
  277. package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
  278. package/dist/guardrails/workspace-fingerprint.js +634 -0
  279. package/dist/guardrails/workspace-fingerprint.js.map +1 -0
  280. package/dist/index.d.ts +15 -3
  281. package/dist/index.d.ts.map +1 -1
  282. package/dist/index.js +13 -1
  283. package/dist/index.js.map +1 -1
  284. package/dist/main.d.ts.map +1 -1
  285. package/dist/main.js +12 -0
  286. package/dist/main.js.map +1 -1
  287. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  288. package/dist/modes/interactive/components/control-panel-layout.js +1 -1
  289. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  290. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  291. package/dist/modes/interactive/components/footer.js +1 -1
  292. package/dist/modes/interactive/components/footer.js.map +1 -1
  293. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  294. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  295. package/dist/modes/interactive/interactive-mode.js +59 -9
  296. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  297. package/dist/modes/print-mode.d.ts.map +1 -1
  298. package/dist/modes/print-mode.js +43 -2
  299. package/dist/modes/print-mode.js.map +1 -1
  300. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  301. package/dist/modes/rpc/rpc-mode.js +23 -5
  302. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  303. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  304. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  305. package/dist/modes/rpc/rpc-types.js.map +1 -1
  306. package/dist/types/evidence.d.ts +215 -1
  307. package/dist/types/evidence.d.ts.map +1 -1
  308. package/dist/types/evidence.js.map +1 -1
  309. package/dist/verify-ci.d.ts +3 -0
  310. package/dist/verify-ci.d.ts.map +1 -0
  311. package/dist/verify-ci.js +30 -0
  312. package/dist/verify-ci.js.map +1 -0
  313. package/docs/compaction.md +44 -4
  314. package/docs/extensions.md +2 -0
  315. package/docs/providers.md +6 -0
  316. package/docs/sdk.md +238 -1
  317. package/docs/settings.md +67 -4
  318. package/docs/usage.md +38 -0
  319. package/examples/extensions/correctness-wall/README.md +6 -6
  320. package/examples/extensions/correctness-wall/index.ts +37 -11
  321. package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
  322. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  323. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  324. package/examples/extensions/gondolin/package-lock.json +2 -2
  325. package/examples/extensions/gondolin/package.json +1 -1
  326. package/examples/extensions/sandbox/package-lock.json +2 -2
  327. package/examples/extensions/sandbox/package.json +1 -1
  328. package/examples/extensions/with-deps/package-lock.json +4 -4
  329. package/examples/extensions/with-deps/package.json +1 -1
  330. package/npm-shrinkwrap.json +12 -12
  331. package/package.json +4 -4
@@ -12,19 +12,30 @@
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";
38
+ import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
28
39
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
29
40
  import { exportSessionToHtml } from "./export-html/index.js";
30
41
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -34,12 +45,17 @@ import { emitSessionShutdownEvent } from "./extensions/runner.js";
34
45
  import { assertTextChatModelForCompletion } from "./grok-harness.js";
35
46
  import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
36
47
  import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
48
+ import { findExactModelReferenceMatch } from "./model-resolver.js";
37
49
  import { expandPromptTemplate } from "./prompt-templates.js";
38
50
  import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
39
51
  import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
52
+ import { redactSensitiveText } from "./redaction.js";
40
53
  import { appendRouterFeedbackRecord, } from "./router-feedback-collector.js";
54
+ import { RunJournalStore } from "./run-journal-store.js";
41
55
  import { detectSandboxBackend } from "./sandbox/backend.js";
56
+ import { inspectSessionIntegrity } from "./session-integrity.js";
42
57
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
58
+ import { classifySessionTermination, } from "./session-termination.js";
43
59
  import { createSyntheticSourceInfo } from "./source-info.js";
44
60
  import { buildSystemPrompt } from "./system-prompt.js";
45
61
  import { createLocalBashOperations } from "./tools/bash.js";
@@ -71,6 +87,23 @@ function mergePromptActiveSkillNames(first, second) {
71
87
  }
72
88
  return names;
73
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
+ }
74
107
  // ============================================================================
75
108
  // Constants
76
109
  // ============================================================================
@@ -96,6 +129,102 @@ function buildSandboxDeniedBashResult(reason) {
96
129
  truncated: false,
97
130
  };
98
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
+ }
99
228
  // ============================================================================
100
229
  // AgentSession Class
101
230
  // ============================================================================
@@ -128,6 +257,7 @@ export class AgentSession {
128
257
  // Event subscription state
129
258
  _unsubscribeAgent;
130
259
  _eventListeners = [];
260
+ _messageEndReplacements = new WeakMap();
131
261
  /** Tracks pending steering messages for UI display. Removed when delivered. */
132
262
  _steeringMessages = [];
133
263
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -138,6 +268,8 @@ export class AgentSession {
138
268
  _compactionAbortController = undefined;
139
269
  _autoCompactionAbortController = undefined;
140
270
  _overflowRecoveryAttempted = false;
271
+ _thresholdCompactionEmergency = false;
272
+ _compactionHysteresisState = createCompactionHysteresisState();
141
273
  // Branch summarization state
142
274
  _branchSummaryAbortController = undefined;
143
275
  // Retry state
@@ -169,8 +301,24 @@ export class AgentSession {
169
301
  _extensionShutdownHandler;
170
302
  _extensionErrorListener;
171
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;
172
317
  // Model registry for API key resolution
173
318
  _modelRegistry;
319
+ /** Lazy, in-memory only; never shared across sessions or persisted. */
320
+ _contextBudgetCacheProvider;
321
+ _contextCacheInvalidationSnapshot;
174
322
  // Tool registry for extension getTools/setTools
175
323
  _toolRegistry = new Map();
176
324
  _toolDefinitions = new Map();
@@ -188,6 +336,13 @@ export class AgentSession {
188
336
  this._customTools = config.customTools ?? [];
189
337
  this._cwd = config.cwd;
190
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
+ });
191
346
  this._extensionRunnerRef = config.extensionRunnerRef;
192
347
  this._initialActiveToolNames = config.initialActiveToolNames;
193
348
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -196,10 +351,34 @@ export class AgentSession {
196
351
  this._loadoutAccessPolicy = config.loadoutAccessPolicy;
197
352
  this._configuredBashSandboxPreflight = config.bashSandboxPreflight;
198
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
+ }
199
370
  // Always subscribe to agent events for internal handling
200
371
  // (session persistence, extensions, auto-compaction, retry logic)
201
372
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
202
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
+ }
203
382
  this._buildRuntime({
204
383
  activeToolNames: this._initialActiveToolNames,
205
384
  includeAllExtensionTools: true,
@@ -209,6 +388,347 @@ export class AgentSession {
209
388
  get modelRegistry() {
210
389
  return this._modelRegistry;
211
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
+ }
212
732
  _getBashSandboxPreflight(override) {
213
733
  const preflight = override ?? this._configuredBashSandboxPreflight;
214
734
  if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
@@ -319,6 +839,8 @@ export class AgentSession {
319
839
  _lastAssistantMessage = undefined;
320
840
  /** Internal handler for agent events - shared by subscribe and reconnect */
321
841
  _handleAgentEvent = async (event) => {
842
+ // Required run lifecycle persistence executes before extension/user listeners.
843
+ this._handleRunLifecycleEvent(event);
322
844
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
323
845
  // This ensures the UI sees the updated queue state
324
846
  if (event.type === "message_start" && event.message.role === "user") {
@@ -341,28 +863,37 @@ export class AgentSession {
341
863
  }
342
864
  }
343
865
  }
866
+ // Durable tool timeout / late-settlement audits (ALG004-A/B).
867
+ this._handleToolAuditEvent(event);
344
868
  // Emit to extensions first
345
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 });
346
875
  // Notify all listeners
347
- 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);
348
879
  // Handle session persistence
349
- if (event.type === "message_end") {
880
+ if (finalizedEvent.type === "message_end") {
350
881
  // Check if this is a custom message from extensions
351
- if (event.message.role === "custom") {
882
+ if (finalizedEvent.message.role === "custom") {
352
883
  // Persist as CustomMessageEntry
353
- 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);
354
885
  }
355
- else if (event.message.role === "user" ||
356
- event.message.role === "assistant" ||
357
- event.message.role === "toolResult") {
886
+ else if (finalizedEvent.message.role === "user" ||
887
+ finalizedEvent.message.role === "assistant" ||
888
+ finalizedEvent.message.role === "toolResult") {
358
889
  // Regular LLM message - persist as SessionMessageEntry
359
- this.sessionManager.appendMessage(event.message);
890
+ this.sessionManager.appendMessage(finalizedEvent.message);
360
891
  }
361
892
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
362
893
  // Track assistant message for auto-compaction (checked on agent_end)
363
- if (event.message.role === "assistant") {
364
- this._lastAssistantMessage = event.message;
365
- const assistantMsg = event.message;
894
+ if (finalizedEvent.message.role === "assistant") {
895
+ this._lastAssistantMessage = finalizedEvent.message;
896
+ const assistantMsg = finalizedEvent.message;
366
897
  if (assistantMsg.stopReason !== "error") {
367
898
  this._overflowRecoveryAttempted = false;
368
899
  }
@@ -413,19 +944,19 @@ export class AgentSession {
413
944
  }
414
945
  return undefined;
415
946
  }
416
- _replaceMessageInPlace(target, replacement) {
417
- // Agent-core stores the finalized message object in its state before emitting message_end.
418
- // SessionManager persistence happens later in _handleAgentEvent() with event.message.
419
- // Mutating this object in place keeps agent state, later turn/agent events, listeners,
420
- // and the eventual SessionManager.appendMessage(event.message) persistence in sync.
421
- if (target === replacement) {
422
- return;
423
- }
424
- const targetRecord = target;
425
- for (const key of Object.keys(targetRecord)) {
426
- delete targetRecord[key];
427
- }
428
- 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;
429
960
  }
430
961
  /** Emit extension events based on agent events */
431
962
  async _emitExtensionEvent(event) {
@@ -476,7 +1007,8 @@ export class AgentSession {
476
1007
  };
477
1008
  const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
478
1009
  if (replacement) {
479
- this._replaceMessageInPlace(event.message, replacement);
1010
+ const finalizedReplacement = this._replaceFinalizedMessage(event.message, replacement);
1011
+ this._messageEndReplacements.set(event, finalizedReplacement);
480
1012
  }
481
1013
  }
482
1014
  else if (event.type === "tool_execution_start") {
@@ -643,10 +1175,28 @@ export class AgentSession {
643
1175
  }
644
1176
  }
645
1177
  this.agent.state.tools = tools;
1178
+ this._applyToolTimeoutCategoryDefaults(validToolNames);
646
1179
  // Rebuild base system prompt with new tool set
647
1180
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
648
1181
  this.agent.state.systemPrompt = this._baseSystemPrompt;
649
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
+ }
650
1200
  /** Whether compaction or branch summarization is currently running */
651
1201
  get isCompacting() {
652
1202
  return (this._autoCompactionAbortController !== undefined ||
@@ -744,7 +1294,11 @@ export class AgentSession {
744
1294
  return undefined;
745
1295
  }
746
1296
  _getContextBudgetOptions() {
747
- if (process.env.OMK_CONTEXT_GOVERNOR !== "1") {
1297
+ const contextGovernorOverride = process.env.OMK_CONTEXT_GOVERNOR;
1298
+ if (contextGovernorOverride === "0") {
1299
+ return undefined;
1300
+ }
1301
+ if (contextGovernorOverride !== "1" && !this.settingsManager.getContextBudgetEnabled()) {
748
1302
  return undefined;
749
1303
  }
750
1304
  const contextWindow = this.model?.contextWindow ?? 0;
@@ -782,6 +1336,9 @@ export class AgentSession {
782
1336
  responseReserveTokens =
783
1337
  parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
784
1338
  }
1339
+ const cacheProvider = this._contextBudgetCacheProvider ?? createMemoryContextBudgetCacheProviderV2("session");
1340
+ cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
1341
+ this._contextBudgetCacheProvider = cacheProvider;
785
1342
  // Enforce floor
786
1343
  if (maxPromptTokens < MIN_PROMPT_TOKENS) {
787
1344
  maxPromptTokens = MIN_PROMPT_TOKENS;
@@ -797,6 +1354,7 @@ export class AgentSession {
797
1354
  tokenizerMode: parseTokenizerModeEnv(process.env.OMK_CONTEXT_GOVERNOR_TOKENIZER),
798
1355
  activeSkillNames: parseCommaSeparatedEnv(process.env.OMK_CONTEXT_GOVERNOR_ACTIVE_SKILLS),
799
1356
  queryContext: this._extractCurrentQuery(),
1357
+ cacheProvider,
800
1358
  };
801
1359
  }
802
1360
  /**
@@ -859,6 +1417,10 @@ export class AgentSession {
859
1417
  await this.agent.continue();
860
1418
  }
861
1419
  }
1420
+ catch (error) {
1421
+ this._publishRuntimeFailure(error);
1422
+ throw error;
1423
+ }
862
1424
  finally {
863
1425
  this._flushPendingBashMessages();
864
1426
  }
@@ -900,7 +1462,7 @@ export class AgentSession {
900
1462
  async prompt(text, options) {
901
1463
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
902
1464
  const preflightResult = options?.preflightResult;
903
- let currentText = text;
1465
+ let currentText = redactSensitiveText(text);
904
1466
  let promptActiveSkillNames = [...(options?.activeSkillNames ?? [])];
905
1467
  let promptActiveSkillSource = options?.activeSkillSource;
906
1468
  let isBangSkillInvocation = false;
@@ -948,6 +1510,7 @@ export class AgentSession {
948
1510
  expandedText = this._expandSkillCommand(expandedText);
949
1511
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
950
1512
  }
1513
+ expandedText = redactSensitiveText(expandedText);
951
1514
  // If streaming, queue via steer() or followUp() based on option
952
1515
  if (this.isStreaming) {
953
1516
  if (!options?.streamingBehavior) {
@@ -1047,6 +1610,19 @@ export class AgentSession {
1047
1610
  }
1048
1611
  catch (error) {
1049
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
+ }));
1050
1626
  throw error;
1051
1627
  }
1052
1628
  if (!messages) {
@@ -1121,14 +1697,15 @@ export class AgentSession {
1121
1697
  * @throws Error if text is an extension command
1122
1698
  */
1123
1699
  async steer(text, images) {
1700
+ const sanitizedText = redactSensitiveText(text);
1124
1701
  // Check for extension commands (cannot be queued)
1125
- if (text.startsWith("/")) {
1126
- this._throwIfExtensionCommand(text);
1702
+ if (sanitizedText.startsWith("/")) {
1703
+ this._throwIfExtensionCommand(sanitizedText);
1127
1704
  }
1128
1705
  // Expand skill commands and prompt templates
1129
- let expandedText = this._expandSkillCommand(text);
1706
+ let expandedText = this._expandSkillCommand(sanitizedText);
1130
1707
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1131
- await this._queueSteer(expandedText, images);
1708
+ await this._queueSteer(redactSensitiveText(expandedText), images);
1132
1709
  }
1133
1710
  /**
1134
1711
  * Queue a follow-up message to be processed after the agent finishes.
@@ -1138,19 +1715,21 @@ export class AgentSession {
1138
1715
  * @throws Error if text is an extension command
1139
1716
  */
1140
1717
  async followUp(text, images) {
1718
+ const sanitizedText = redactSensitiveText(text);
1141
1719
  // Check for extension commands (cannot be queued)
1142
- if (text.startsWith("/")) {
1143
- this._throwIfExtensionCommand(text);
1720
+ if (sanitizedText.startsWith("/")) {
1721
+ this._throwIfExtensionCommand(sanitizedText);
1144
1722
  }
1145
1723
  // Expand skill commands and prompt templates
1146
- let expandedText = this._expandSkillCommand(text);
1724
+ let expandedText = this._expandSkillCommand(sanitizedText);
1147
1725
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1148
- await this._queueFollowUp(expandedText, images);
1726
+ await this._queueFollowUp(redactSensitiveText(expandedText), images);
1149
1727
  }
1150
1728
  /**
1151
1729
  * Internal: Queue a steering message (already expanded, no extension command check).
1152
1730
  */
1153
1731
  async _queueSteer(text, images) {
1732
+ this._invalidateContextBudgetCache({ type: "userSteering" });
1154
1733
  this._steeringMessages.push(text);
1155
1734
  this._emitQueueUpdate();
1156
1735
  const content = [{ type: "text", text }];
@@ -1211,6 +1790,7 @@ export class AgentSession {
1211
1790
  details: message.details,
1212
1791
  timestamp: Date.now(),
1213
1792
  };
1793
+ this._recordEvidenceReceiptInvalidation(message.customType);
1214
1794
  if (options?.deliverAs === "nextTurn") {
1215
1795
  this._pendingNextTurnMessages.push(appMessage);
1216
1796
  }
@@ -1302,16 +1882,43 @@ export class AgentSession {
1302
1882
  * Abort current operation and wait for agent to become idle.
1303
1883
  */
1304
1884
  async abort() {
1885
+ this._userAbortRequested = this.isStreaming || this.isRetrying;
1305
1886
  this.abortRetry();
1306
1887
  this.agent.abort();
1307
1888
  await this.agent.waitForIdle();
1308
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
+ }
1309
1915
  // =========================================================================
1310
1916
  // Model Management
1311
1917
  // =========================================================================
1312
1918
  async _emitModelSelect(nextModel, previousModel, source) {
1313
1919
  if (modelsAreEqual(previousModel, nextModel))
1314
1920
  return;
1921
+ this._invalidateContextBudgetCache({ type: "activeModelId", value: this._contextCacheModelId(nextModel) });
1315
1922
  await this._extensionRunner.emit({
1316
1923
  type: "model_select",
1317
1924
  model: nextModel,
@@ -1649,6 +2256,331 @@ export class AgentSession {
1649
2256
  // =========================================================================
1650
2257
  // Compaction
1651
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
+ }
2564
+ _resolveCompactionModel(sessionModel) {
2565
+ const configuredModel = this.settingsManager.getCompactionModel();
2566
+ const availableModels = this._modelRegistry.getAvailable();
2567
+ let model;
2568
+ if (configuredModel) {
2569
+ const configured = findExactModelReferenceMatch(configuredModel, availableModels);
2570
+ if (!configured) {
2571
+ throw new Error(`Configured compaction model "${configuredModel}" is unavailable or unauthenticated.`);
2572
+ }
2573
+ model = configured;
2574
+ }
2575
+ else {
2576
+ model = resolveCompactionModel(sessionModel, availableModels);
2577
+ }
2578
+ this._invalidateContextBudgetCache({
2579
+ type: "compactionModelId",
2580
+ value: this._contextCacheModelId(model),
2581
+ });
2582
+ return model;
2583
+ }
1652
2584
  /**
1653
2585
  * Manually compact the session context.
1654
2586
  * Aborts current agent operation first.
@@ -1659,14 +2591,16 @@ export class AgentSession {
1659
2591
  await this.abort();
1660
2592
  this._compactionAbortController = new AbortController();
1661
2593
  this._emit({ type: "compaction_start", reason: "manual" });
2594
+ let committedCompaction = false;
1662
2595
  try {
1663
2596
  if (!this.model) {
1664
2597
  throw new Error(formatNoModelSelectedMessage());
1665
2598
  }
1666
- const compactionModel = resolveCompactionModel(this.model, this._modelRegistry.getAvailable());
2599
+ const compactionModel = this._resolveCompactionModel(this.model);
1667
2600
  const { apiKey, headers } = await this._getCompactionRequestAuth(compactionModel);
1668
- const pathEntries = this.sessionManager.getBranch();
1669
2601
  const settings = this.settingsManager.getCompactionSettings();
2602
+ const begun = this._beginCompactionTransaction(compactionModel, false);
2603
+ const pathEntries = [...begun.capture.branchEntries];
1670
2604
  const preparation = prepareCompaction(pathEntries, settings);
1671
2605
  if (!preparation) {
1672
2606
  // Check why we can't compact
@@ -1716,25 +2650,22 @@ export class AgentSession {
1716
2650
  if (this._compactionAbortController.signal.aborted) {
1717
2651
  throw new Error("Compaction cancelled");
1718
2652
  }
1719
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1720
- const newEntries = this.sessionManager.getEntries();
1721
- const sessionContext = this.sessionManager.buildSessionContext();
1722
- this.agent.state.messages = sessionContext.messages;
1723
- // Get the saved compaction entry for the extension event
1724
- const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1725
- if (this._extensionRunner && savedCompactionEntry) {
1726
- await this._extensionRunner.emit({
1727
- type: "session_compact",
1728
- compactionEntry: savedCompactionEntry,
1729
- fromExtension,
1730
- });
1731
- }
1732
2653
  const compactionResult = {
1733
2654
  summary,
1734
2655
  firstKeptEntryId,
1735
2656
  tokensBefore,
1736
2657
  details,
1737
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
+ }
1738
2669
  this._emit({
1739
2670
  type: "compaction_end",
1740
2671
  reason: "manual",
@@ -1747,6 +2678,18 @@ export class AgentSession {
1747
2678
  catch (error) {
1748
2679
  const message = error instanceof Error ? error.message : String(error);
1749
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
+ }));
1750
2693
  this._emit({
1751
2694
  type: "compaction_end",
1752
2695
  reason: "manual",
@@ -1792,8 +2735,9 @@ export class AgentSession {
1792
2735
  return false;
1793
2736
  }
1794
2737
  }
1795
- if (shouldCompact(estimate.tokens, contextWindow, settings)) {
1796
- 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);
1797
2741
  }
1798
2742
  return false;
1799
2743
  }
@@ -1874,15 +2818,16 @@ export class AgentSession {
1874
2818
  else {
1875
2819
  contextTokens = calculateContextTokens(assistantMessage.usage);
1876
2820
  }
1877
- if (shouldCompact(contextTokens, contextWindow, settings)) {
1878
- 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);
1879
2824
  }
1880
2825
  return false;
1881
2826
  }
1882
2827
  /**
1883
2828
  * Internal: Run auto-compaction with events.
1884
2829
  */
1885
- async _runAutoCompaction(reason, willRetry) {
2830
+ async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
1886
2831
  const settings = this.settingsManager.getCompactionSettings();
1887
2832
  this._emit({ type: "compaction_start", reason });
1888
2833
  this._autoCompactionAbortController = new AbortController();
@@ -1897,7 +2842,7 @@ export class AgentSession {
1897
2842
  });
1898
2843
  return false;
1899
2844
  }
1900
- const compactionModel = resolveCompactionModel(this.model, this._modelRegistry.getAvailable());
2845
+ const compactionModel = this._resolveCompactionModel(this.model);
1901
2846
  let apiKey;
1902
2847
  let headers;
1903
2848
  if (this.agent.streamFn === streamSimple) {
@@ -1921,7 +2866,8 @@ export class AgentSession {
1921
2866
  else {
1922
2867
  ({ apiKey, headers } = await this._getCompactionRequestAuth(compactionModel));
1923
2868
  }
1924
- const pathEntries = this.sessionManager.getBranch();
2869
+ const begun = this._beginCompactionTransaction(compactionModel, emergency);
2870
+ const pathEntries = [...begun.capture.branchEntries];
1925
2871
  const preparation = prepareCompaction(pathEntries, settings);
1926
2872
  if (!preparation) {
1927
2873
  this._emit({
@@ -1987,25 +2933,21 @@ export class AgentSession {
1987
2933
  });
1988
2934
  return false;
1989
2935
  }
1990
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1991
- const newEntries = this.sessionManager.getEntries();
1992
- const sessionContext = this.sessionManager.buildSessionContext();
1993
- this.agent.state.messages = sessionContext.messages;
1994
- // Get the saved compaction entry for the extension event
1995
- const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1996
- if (this._extensionRunner && savedCompactionEntry) {
1997
- await this._extensionRunner.emit({
1998
- type: "session_compact",
1999
- compactionEntry: savedCompactionEntry,
2000
- fromExtension,
2001
- });
2002
- }
2003
2936
  const result = {
2004
2937
  summary,
2005
2938
  firstKeptEntryId,
2006
2939
  tokensBefore,
2007
2940
  details,
2008
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
+ }
2009
2951
  const emitWillRetry = compactionEmitWillRetry(willRetry, this.agent.hasQueuedMessages());
2010
2952
  this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry: emitWillRetry });
2011
2953
  if (willRetry) {
@@ -2173,6 +3115,7 @@ export class AgentSession {
2173
3115
  },
2174
3116
  appendEntry: (customType, data) => {
2175
3117
  this.sessionManager.appendCustomEntry(customType, data);
3118
+ this._recordEvidenceReceiptInvalidation(customType);
2176
3119
  },
2177
3120
  setSessionName: (name) => {
2178
3121
  this.setSessionName(name);
@@ -2391,6 +3334,7 @@ export class AgentSession {
2391
3334
  const previousFlagValues = this._extensionRunner.getFlagValues();
2392
3335
  await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
2393
3336
  await this.settingsManager.reload();
3337
+ this._invalidateContextBudgetCache({ type: "settings" });
2394
3338
  resetApiProviders();
2395
3339
  await this._resourceLoader.reload();
2396
3340
  this._buildRuntime({