open-multi-agent-kit 0.90.8 → 0.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +21 -2
  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 +140 -2
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +1179 -95
  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/command-safety.d.ts.map +1 -1
  43. package/dist/core/command-safety.js +0 -8
  44. package/dist/core/command-safety.js.map +1 -1
  45. package/dist/core/compaction/compaction.d.ts +6 -0
  46. package/dist/core/compaction/compaction.d.ts.map +1 -1
  47. package/dist/core/compaction/compaction.js +12 -1
  48. package/dist/core/compaction/compaction.js.map +1 -1
  49. package/dist/core/compaction/hysteresis.d.ts +27 -0
  50. package/dist/core/compaction/hysteresis.d.ts.map +1 -0
  51. package/dist/core/compaction/hysteresis.js +84 -0
  52. package/dist/core/compaction/hysteresis.js.map +1 -0
  53. package/dist/core/compaction/index.d.ts +2 -0
  54. package/dist/core/compaction/index.d.ts.map +1 -1
  55. package/dist/core/compaction/index.js +2 -0
  56. package/dist/core/compaction/index.js.map +1 -1
  57. package/dist/core/compaction/transaction.d.ts +172 -0
  58. package/dist/core/compaction/transaction.d.ts.map +1 -0
  59. package/dist/core/compaction/transaction.js +824 -0
  60. package/dist/core/compaction/transaction.js.map +1 -0
  61. package/dist/core/context-budget-governor-v2.d.ts +2 -1
  62. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  63. package/dist/core/context-budget-governor-v2.js +2 -1
  64. package/dist/core/context-budget-governor-v2.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 +4 -0
  78. package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
  79. package/dist/core/context-budget-v2-cache-provider.js +19 -0
  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-types.d.ts +4 -0
  88. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  89. package/dist/core/context-budget-v2-types.js.map +1 -1
  90. package/dist/core/durable-file-directory.d.ts +4 -0
  91. package/dist/core/durable-file-directory.d.ts.map +1 -0
  92. package/dist/core/durable-file-directory.js +76 -0
  93. package/dist/core/durable-file-directory.js.map +1 -0
  94. package/dist/core/durable-file-identity.d.ts +26 -0
  95. package/dist/core/durable-file-identity.d.ts.map +1 -0
  96. package/dist/core/durable-file-identity.js +118 -0
  97. package/dist/core/durable-file-identity.js.map +1 -0
  98. package/dist/core/durable-file-io.d.ts +29 -0
  99. package/dist/core/durable-file-io.d.ts.map +1 -0
  100. package/dist/core/durable-file-io.js +179 -0
  101. package/dist/core/durable-file-io.js.map +1 -0
  102. package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
  103. package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
  104. package/dist/core/durable-file-lock-cleanup.js +39 -0
  105. package/dist/core/durable-file-lock-cleanup.js.map +1 -0
  106. package/dist/core/durable-file-lock-owner.d.ts +18 -0
  107. package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
  108. package/dist/core/durable-file-lock-owner.js +78 -0
  109. package/dist/core/durable-file-lock-owner.js.map +1 -0
  110. package/dist/core/durable-file-lock-storage.d.ts +35 -0
  111. package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
  112. package/dist/core/durable-file-lock-storage.js +263 -0
  113. package/dist/core/durable-file-lock-storage.js.map +1 -0
  114. package/dist/core/durable-file-mode.d.ts +8 -0
  115. package/dist/core/durable-file-mode.d.ts.map +1 -0
  116. package/dist/core/durable-file-mode.js +41 -0
  117. package/dist/core/durable-file-mode.js.map +1 -0
  118. package/dist/core/extensions/loader.d.ts +12 -0
  119. package/dist/core/extensions/loader.d.ts.map +1 -1
  120. package/dist/core/extensions/loader.js +46 -37
  121. package/dist/core/extensions/loader.js.map +1 -1
  122. package/dist/core/footer-data-provider.d.ts +7 -1
  123. package/dist/core/footer-data-provider.d.ts.map +1 -1
  124. package/dist/core/footer-data-provider.js +12 -0
  125. package/dist/core/footer-data-provider.js.map +1 -1
  126. package/dist/core/index.d.ts +7 -2
  127. package/dist/core/index.d.ts.map +1 -1
  128. package/dist/core/index.js +6 -0
  129. package/dist/core/index.js.map +1 -1
  130. package/dist/core/metrics-sampler.d.ts +20 -0
  131. package/dist/core/metrics-sampler.d.ts.map +1 -1
  132. package/dist/core/metrics-sampler.js +52 -0
  133. package/dist/core/metrics-sampler.js.map +1 -1
  134. package/dist/core/model-registry.d.ts.map +1 -1
  135. package/dist/core/model-registry.js +4 -1
  136. package/dist/core/model-registry.js.map +1 -1
  137. package/dist/core/model-resolver.d.ts.map +1 -1
  138. package/dist/core/model-resolver.js +16 -7
  139. package/dist/core/model-resolver.js.map +1 -1
  140. package/dist/core/provider-resilience.d.ts +51 -0
  141. package/dist/core/provider-resilience.d.ts.map +1 -0
  142. package/dist/core/provider-resilience.js +86 -0
  143. package/dist/core/provider-resilience.js.map +1 -0
  144. package/dist/core/redaction.d.ts +8 -0
  145. package/dist/core/redaction.d.ts.map +1 -0
  146. package/dist/core/redaction.js +31 -0
  147. package/dist/core/redaction.js.map +1 -0
  148. package/dist/core/resource-loader.d.ts.map +1 -1
  149. package/dist/core/resource-loader.js +36 -0
  150. package/dist/core/resource-loader.js.map +1 -1
  151. package/dist/core/run-journal-store.d.ts +53 -0
  152. package/dist/core/run-journal-store.d.ts.map +1 -0
  153. package/dist/core/run-journal-store.js +278 -0
  154. package/dist/core/run-journal-store.js.map +1 -0
  155. package/dist/core/run-journal.d.ts +172 -0
  156. package/dist/core/run-journal.d.ts.map +1 -0
  157. package/dist/core/run-journal.js +1045 -0
  158. package/dist/core/run-journal.js.map +1 -0
  159. package/dist/core/sdk.d.ts +7 -0
  160. package/dist/core/sdk.d.ts.map +1 -1
  161. package/dist/core/sdk.js +94 -3
  162. package/dist/core/sdk.js.map +1 -1
  163. package/dist/core/session-doctor-plan.d.ts +128 -0
  164. package/dist/core/session-doctor-plan.d.ts.map +1 -0
  165. package/dist/core/session-doctor-plan.js +833 -0
  166. package/dist/core/session-doctor-plan.js.map +1 -0
  167. package/dist/core/session-file-compaction.d.ts +6 -0
  168. package/dist/core/session-file-compaction.d.ts.map +1 -0
  169. package/dist/core/session-file-compaction.js +169 -0
  170. package/dist/core/session-file-compaction.js.map +1 -0
  171. package/dist/core/session-file-head.d.ts +10 -0
  172. package/dist/core/session-file-head.d.ts.map +1 -0
  173. package/dist/core/session-file-head.js +78 -0
  174. package/dist/core/session-file-head.js.map +1 -0
  175. package/dist/core/session-file-persistence.d.ts +30 -0
  176. package/dist/core/session-file-persistence.d.ts.map +1 -0
  177. package/dist/core/session-file-persistence.js +106 -0
  178. package/dist/core/session-file-persistence.js.map +1 -0
  179. package/dist/core/session-integrity.d.ts +43 -0
  180. package/dist/core/session-integrity.d.ts.map +1 -0
  181. package/dist/core/session-integrity.js +420 -0
  182. package/dist/core/session-integrity.js.map +1 -0
  183. package/dist/core/session-manager.d.ts +24 -4
  184. package/dist/core/session-manager.d.ts.map +1 -1
  185. package/dist/core/session-manager.js +242 -125
  186. package/dist/core/session-manager.js.map +1 -1
  187. package/dist/core/session-owner-lease.d.ts +22 -0
  188. package/dist/core/session-owner-lease.d.ts.map +1 -0
  189. package/dist/core/session-owner-lease.js +176 -0
  190. package/dist/core/session-owner-lease.js.map +1 -0
  191. package/dist/core/session-path-policy.d.ts +132 -0
  192. package/dist/core/session-path-policy.d.ts.map +1 -0
  193. package/dist/core/session-path-policy.js +780 -0
  194. package/dist/core/session-path-policy.js.map +1 -0
  195. package/dist/core/session-repair-plan.d.ts +49 -0
  196. package/dist/core/session-repair-plan.d.ts.map +1 -0
  197. package/dist/core/session-repair-plan.js +202 -0
  198. package/dist/core/session-repair-plan.js.map +1 -0
  199. package/dist/core/session-termination.d.ts +106 -0
  200. package/dist/core/session-termination.d.ts.map +1 -0
  201. package/dist/core/session-termination.js +445 -0
  202. package/dist/core/session-termination.js.map +1 -0
  203. package/dist/core/settings-manager.d.ts +52 -0
  204. package/dist/core/settings-manager.d.ts.map +1 -1
  205. package/dist/core/settings-manager.js +54 -5
  206. package/dist/core/settings-manager.js.map +1 -1
  207. package/dist/core/system-prompt.d.ts.map +1 -1
  208. package/dist/core/system-prompt.js +1 -1
  209. package/dist/core/system-prompt.js.map +1 -1
  210. package/dist/core/tools/grep.d.ts.map +1 -1
  211. package/dist/core/tools/grep.js +47 -0
  212. package/dist/core/tools/grep.js.map +1 -1
  213. package/dist/core/tools/omp-pure-seams.d.ts +41 -0
  214. package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
  215. package/dist/core/tools/omp-pure-seams.js +142 -0
  216. package/dist/core/tools/omp-pure-seams.js.map +1 -0
  217. package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
  218. package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
  219. package/dist/core/tools/omp-seam-runtime.js +40 -0
  220. package/dist/core/tools/omp-seam-runtime.js.map +1 -0
  221. package/dist/core/tools/read.d.ts.map +1 -1
  222. package/dist/core/tools/read.js +34 -1
  223. package/dist/core/tools/read.js.map +1 -1
  224. package/dist/core/verified-bash-adapter.d.ts +36 -0
  225. package/dist/core/verified-bash-adapter.d.ts.map +1 -0
  226. package/dist/core/verified-bash-adapter.js +90 -0
  227. package/dist/core/verified-bash-adapter.js.map +1 -0
  228. package/dist/guardrails/command-redaction.d.ts +32 -0
  229. package/dist/guardrails/command-redaction.d.ts.map +1 -0
  230. package/dist/guardrails/command-redaction.js +776 -0
  231. package/dist/guardrails/command-redaction.js.map +1 -0
  232. package/dist/guardrails/evidence-attestation.d.ts +31 -0
  233. package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
  234. package/dist/guardrails/evidence-attestation.js +178 -0
  235. package/dist/guardrails/evidence-attestation.js.map +1 -0
  236. package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
  237. package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
  238. package/dist/guardrails/evidence-receipt-store.js +446 -0
  239. package/dist/guardrails/evidence-receipt-store.js.map +1 -0
  240. package/dist/guardrails/evidence-receipt.d.ts +58 -0
  241. package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
  242. package/dist/guardrails/evidence-receipt.js +389 -0
  243. package/dist/guardrails/evidence-receipt.js.map +1 -0
  244. package/dist/guardrails/evidence-system.d.ts +39 -15
  245. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  246. package/dist/guardrails/evidence-system.js +255 -97
  247. package/dist/guardrails/evidence-system.js.map +1 -1
  248. package/dist/guardrails/replay-ledger-io.d.ts +6 -0
  249. package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
  250. package/dist/guardrails/replay-ledger-io.js +42 -0
  251. package/dist/guardrails/replay-ledger-io.js.map +1 -0
  252. package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
  253. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
  254. package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
  255. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
  256. package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
  257. package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
  258. package/dist/guardrails/replay-ledger-lock.js +135 -0
  259. package/dist/guardrails/replay-ledger-lock.js.map +1 -0
  260. package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
  261. package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
  262. package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
  263. package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
  264. package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
  265. package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
  266. package/dist/guardrails/replay-ledger-quarantine.js +19 -0
  267. package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
  268. package/dist/guardrails/replay-ledger-store.d.ts +32 -0
  269. package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
  270. package/dist/guardrails/replay-ledger-store.js +211 -0
  271. package/dist/guardrails/replay-ledger-store.js.map +1 -0
  272. package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
  273. package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
  274. package/dist/guardrails/shell-command-name-glob.js +233 -0
  275. package/dist/guardrails/shell-command-name-glob.js.map +1 -0
  276. package/dist/guardrails/shell-command-option.d.ts +7 -0
  277. package/dist/guardrails/shell-command-option.d.ts.map +1 -0
  278. package/dist/guardrails/shell-command-option.js +24 -0
  279. package/dist/guardrails/shell-command-option.js.map +1 -0
  280. package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
  281. package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
  282. package/dist/guardrails/shell-command-tokenizer.js +145 -0
  283. package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
  284. package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
  285. package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
  286. package/dist/guardrails/shell-command-wrapper.js +42 -0
  287. package/dist/guardrails/shell-command-wrapper.js.map +1 -0
  288. package/dist/guardrails/verified-executor.d.ts +65 -0
  289. package/dist/guardrails/verified-executor.d.ts.map +1 -0
  290. package/dist/guardrails/verified-executor.js +171 -0
  291. package/dist/guardrails/verified-executor.js.map +1 -0
  292. package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
  293. package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
  294. package/dist/guardrails/workspace-fingerprint.js +634 -0
  295. package/dist/guardrails/workspace-fingerprint.js.map +1 -0
  296. package/dist/index.d.ts +15 -3
  297. package/dist/index.d.ts.map +1 -1
  298. package/dist/index.js +13 -1
  299. package/dist/index.js.map +1 -1
  300. package/dist/main.d.ts.map +1 -1
  301. package/dist/main.js +12 -0
  302. package/dist/main.js.map +1 -1
  303. package/dist/modes/interactive/components/footer.d.ts +1 -1
  304. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  305. package/dist/modes/interactive/components/footer.js +11 -8
  306. package/dist/modes/interactive/components/footer.js.map +1 -1
  307. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  308. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  309. package/dist/modes/interactive/interactive-mode.js +59 -9
  310. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  311. package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
  312. package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
  313. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  314. package/dist/modes/interactive/theme/theme.js +5 -0
  315. package/dist/modes/interactive/theme/theme.js.map +1 -1
  316. package/dist/modes/print-mode.d.ts.map +1 -1
  317. package/dist/modes/print-mode.js +43 -2
  318. package/dist/modes/print-mode.js.map +1 -1
  319. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  320. package/dist/modes/rpc/rpc-mode.js +23 -5
  321. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  322. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  323. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  324. package/dist/modes/rpc/rpc-types.js.map +1 -1
  325. package/dist/types/evidence.d.ts +211 -1
  326. package/dist/types/evidence.d.ts.map +1 -1
  327. package/dist/types/evidence.js.map +1 -1
  328. package/dist/verify-ci.d.ts +3 -0
  329. package/dist/verify-ci.d.ts.map +1 -0
  330. package/dist/verify-ci.js +30 -0
  331. package/dist/verify-ci.js.map +1 -0
  332. package/docs/adaptorch-preview.md +1 -1
  333. package/docs/compaction.md +38 -4
  334. package/docs/extensions.md +2 -0
  335. package/docs/provider-resilience.md +87 -0
  336. package/docs/sdk.md +238 -1
  337. package/docs/settings.md +80 -2
  338. package/docs/themes.md +2 -0
  339. package/docs/usage.md +38 -0
  340. package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
  341. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  342. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  343. package/examples/extensions/gondolin/package-lock.json +2 -2
  344. package/examples/extensions/gondolin/package.json +1 -1
  345. package/examples/extensions/sandbox/package-lock.json +2 -2
  346. package/examples/extensions/sandbox/package.json +1 -1
  347. package/examples/extensions/with-deps/package-lock.json +4 -4
  348. package/examples/extensions/with-deps/package.json +1 -1
  349. package/npm-shrinkwrap.json +22 -13
  350. package/package.json +6 -5
@@ -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 { createAdaptorchBridge } from "./adaptorch-bridge.js";
27
+ import { applyCategoryTimeoutDefaults, resolveAgentToolSettings, resolveToolTimeoutCategory, } from "./agent-tool-settings.js";
22
28
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
23
29
  import { parseBangInvocation } from "./bang-skill-invocation.js";
24
30
  import { executeBashWithOperations } from "./bash-executor.js";
25
31
  import { classifyShellCommand } from "./command-safety.js";
26
- import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, shouldCompact, } from "./compaction/index.js";
32
+ import { getCompactionHeadroomThreshold } from "./compaction/compaction.js";
33
+ import { createCompactionHysteresisConfig, createCompactionHysteresisState, stepCompactionHysteresis, } from "./compaction/hysteresis.js";
34
+ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
27
35
  import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
36
+ import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, decideCompactionCommit, evaluateCompactionBarrier, validateCompactionEnvelope, } from "./compaction/transaction.js";
37
+ import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
38
+ import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
28
39
  import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
29
40
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
30
41
  import { exportSessionToHtml } from "./export-html/index.js";
@@ -37,11 +48,16 @@ import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
37
48
  import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
38
49
  import { findExactModelReferenceMatch } from "./model-resolver.js";
39
50
  import { expandPromptTemplate } from "./prompt-templates.js";
51
+ import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
40
52
  import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
41
53
  import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
54
+ import { redactSensitiveText } from "./redaction.js";
42
55
  import { appendRouterFeedbackRecord, } from "./router-feedback-collector.js";
56
+ import { RunJournalStore } from "./run-journal-store.js";
43
57
  import { detectSandboxBackend } from "./sandbox/backend.js";
58
+ import { inspectSessionIntegrity } from "./session-integrity.js";
44
59
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
60
+ import { classifySessionTermination, } from "./session-termination.js";
45
61
  import { createSyntheticSourceInfo } from "./source-info.js";
46
62
  import { buildSystemPrompt } from "./system-prompt.js";
47
63
  import { createLocalBashOperations } from "./tools/bash.js";
@@ -73,6 +89,23 @@ function mergePromptActiveSkillNames(first, second) {
73
89
  }
74
90
  return names;
75
91
  }
92
+ const PENDING_TOOL_RESULT_TOKENS = {
93
+ text: 1024,
94
+ image: 4096,
95
+ "large-output": 16_384,
96
+ };
97
+ const TEXT_RESULT_TOOLS = new Set(["edit", "find", "grep", "ls", "read", "write"]);
98
+ const IMAGE_PATH_PATTERN = /\.(?:avif|gif|jpe?g|png|webp)$/iu;
99
+ function pendingToolResultClass(name, args) {
100
+ if (name === "read" &&
101
+ typeof args === "object" &&
102
+ args !== null &&
103
+ typeof Reflect.get(args, "path") === "string" &&
104
+ IMAGE_PATH_PATTERN.test(Reflect.get(args, "path"))) {
105
+ return "image";
106
+ }
107
+ return TEXT_RESULT_TOOLS.has(name) ? "text" : "large-output";
108
+ }
76
109
  // ============================================================================
77
110
  // Constants
78
111
  // ============================================================================
@@ -98,6 +131,102 @@ function buildSandboxDeniedBashResult(reason) {
98
131
  truncated: false,
99
132
  };
100
133
  }
134
+ function snapshotContractError(reason) {
135
+ return new Error(`Finalized message replacement must be a plain serializable snapshot: ${reason}`);
136
+ }
137
+ /**
138
+ * Clone a finalized message replacement into the JSON-compatible snapshot
139
+ * persisted by SessionManager. Undefined remains allowed as ordinary optional
140
+ * message data: JSON omits object properties with undefined values and writes
141
+ * undefined array elements as null.
142
+ */
143
+ function clonePlainSnapshot(value, ancestors = new WeakSet(), copies = new WeakMap()) {
144
+ if (value === null)
145
+ return value;
146
+ switch (typeof value) {
147
+ case "string":
148
+ case "boolean":
149
+ case "undefined":
150
+ return value;
151
+ case "number":
152
+ if (!Number.isFinite(value)) {
153
+ throw snapshotContractError("non-finite number values are not allowed");
154
+ }
155
+ return value;
156
+ case "bigint":
157
+ throw snapshotContractError("bigint values are not allowed");
158
+ case "function":
159
+ case "symbol":
160
+ throw snapshotContractError(`${typeof value} values are not allowed`);
161
+ case "object":
162
+ break;
163
+ default:
164
+ throw snapshotContractError(`${typeof value} values are not allowed`);
165
+ }
166
+ if (ancestors.has(value)) {
167
+ throw snapshotContractError("cyclic values are not allowed");
168
+ }
169
+ if (copies.has(value)) {
170
+ return copies.get(value);
171
+ }
172
+ ancestors.add(value);
173
+ try {
174
+ if (Array.isArray(value)) {
175
+ const copy = new Array(value.length);
176
+ copies.set(value, copy);
177
+ for (const key of Reflect.ownKeys(value)) {
178
+ if (key === "length")
179
+ continue;
180
+ if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
181
+ throw snapshotContractError("arrays may only contain indexed values");
182
+ }
183
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
184
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
185
+ throw snapshotContractError("accessor or non-enumerable properties are not allowed");
186
+ }
187
+ copy[Number(key)] = clonePlainSnapshot(descriptor.value, ancestors, copies);
188
+ }
189
+ return copy;
190
+ }
191
+ const prototype = Object.getPrototypeOf(value);
192
+ if (prototype !== Object.prototype && prototype !== null) {
193
+ throw snapshotContractError("non-plain objects are not allowed");
194
+ }
195
+ const copy = Object.create(prototype);
196
+ copies.set(value, copy);
197
+ for (const key of Reflect.ownKeys(value)) {
198
+ if (typeof key !== "string") {
199
+ throw snapshotContractError("symbol-keyed properties are not allowed");
200
+ }
201
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
202
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
203
+ throw snapshotContractError("accessor or non-enumerable properties are not allowed");
204
+ }
205
+ Object.defineProperty(copy, key, {
206
+ value: clonePlainSnapshot(descriptor.value, ancestors, copies),
207
+ enumerable: true,
208
+ configurable: true,
209
+ writable: true,
210
+ });
211
+ }
212
+ return copy;
213
+ }
214
+ finally {
215
+ ancestors.delete(value);
216
+ }
217
+ }
218
+ function freezeSnapshot(value, seen = new WeakSet()) {
219
+ if (value === null || typeof value !== "object" || seen.has(value))
220
+ return value;
221
+ seen.add(value);
222
+ for (const child of Object.values(value)) {
223
+ freezeSnapshot(child, seen);
224
+ }
225
+ return Object.freeze(value);
226
+ }
227
+ function createImmutableMessageSnapshot(message) {
228
+ return freezeSnapshot(clonePlainSnapshot(message));
229
+ }
101
230
  // ============================================================================
102
231
  // AgentSession Class
103
232
  // ============================================================================
@@ -127,9 +256,19 @@ export class AgentSession {
127
256
  */
128
257
  _reasoningRouterBiasSnapshot = null;
129
258
  _reasoningRouterBiasSnapshotLoaded = false;
259
+ /**
260
+ * AdaptOrch advisory bridge (default-off, global-only opt-in via
261
+ * `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily
262
+ * constructed on the first v4 auto-turn when enabled; stays `null`
263
+ * otherwise. The bridge is advisory-only: its hint is fused into the
264
+ * resolver as a bounded ±2 step nudge, never as an override.
265
+ */
266
+ _adaptorchBridge = null;
267
+ _adaptorchBridgeInitAttempted = false;
130
268
  // Event subscription state
131
269
  _unsubscribeAgent;
132
270
  _eventListeners = [];
271
+ _messageEndReplacements = new WeakMap();
133
272
  /** Tracks pending steering messages for UI display. Removed when delivered. */
134
273
  _steeringMessages = [];
135
274
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -140,6 +279,8 @@ export class AgentSession {
140
279
  _compactionAbortController = undefined;
141
280
  _autoCompactionAbortController = undefined;
142
281
  _overflowRecoveryAttempted = false;
282
+ _thresholdCompactionEmergency = false;
283
+ _compactionHysteresisState = createCompactionHysteresisState();
143
284
  // Branch summarization state
144
285
  _branchSummaryAbortController = undefined;
145
286
  // Retry state
@@ -171,10 +312,24 @@ export class AgentSession {
171
312
  _extensionShutdownHandler;
172
313
  _extensionErrorListener;
173
314
  _extensionErrorUnsubscriber;
315
+ // Required durable run/audit chain, persisted next to the session file.
316
+ _runJournalStore;
317
+ _activeRunId = null;
318
+ _pendingRuntimeTerminationCause;
319
+ _activeRunToolTermination;
320
+ _lastTermination;
321
+ _userAbortRequested = false;
322
+ _replayLedger;
323
+ _replayGoalId;
324
+ _replayLaneId;
325
+ _transcriptRepair;
326
+ _sessionRiskLevel = "normal";
327
+ _workspaceMutationCount = 0;
174
328
  // Model registry for API key resolution
175
329
  _modelRegistry;
176
330
  /** Lazy, in-memory only; never shared across sessions or persisted. */
177
331
  _contextBudgetCacheProvider;
332
+ _contextCacheInvalidationSnapshot;
178
333
  // Tool registry for extension getTools/setTools
179
334
  _toolRegistry = new Map();
180
335
  _toolDefinitions = new Map();
@@ -192,6 +347,13 @@ export class AgentSession {
192
347
  this._customTools = config.customTools ?? [];
193
348
  this._cwd = config.cwd;
194
349
  this._modelRegistry = config.modelRegistry;
350
+ const initialModelId = this._contextCacheModelId(this.agent.state.model);
351
+ this._contextCacheInvalidationSnapshot = createContextCacheInvalidationSnapshot({
352
+ forkId: this.sessionManager.getSessionId(),
353
+ worktreeFingerprint: this._contextCacheWorktreeFingerprint(),
354
+ activeModelId: initialModelId,
355
+ compactionModelId: initialModelId,
356
+ });
195
357
  this._extensionRunnerRef = config.extensionRunnerRef;
196
358
  this._initialActiveToolNames = config.initialActiveToolNames;
197
359
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -200,10 +362,34 @@ export class AgentSession {
200
362
  this._loadoutAccessPolicy = config.loadoutAccessPolicy;
201
363
  this._configuredBashSandboxPreflight = config.bashSandboxPreflight;
202
364
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
365
+ this._transcriptRepair = config.transcriptRepair;
366
+ this._replayLedger = config.replayLedger;
367
+ this._replayGoalId = config.replayGoalId ?? config.replayLedger?.getLedger().goalId;
368
+ this._replayLaneId = config.replayLaneId;
369
+ if (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {
370
+ throw new Error("AgentSession replayGoalId does not match the replay ledger goal id");
371
+ }
372
+ const sessionFile = this.sessionManager.getSessionFile();
373
+ this._runJournalStore = RunJournalStore.open({
374
+ ...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
375
+ sessionId: this.sessionManager.getSessionId(),
376
+ });
377
+ const startupTerminal = this._runJournalStore.records.at(-1);
378
+ if (startupTerminal?.event === "run_recovered") {
379
+ this._lastTermination = startupTerminal.termination;
380
+ }
203
381
  // Always subscribe to agent events for internal handling
204
382
  // (session persistence, extensions, auto-compaction, retry logic)
205
383
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
206
384
  this._installAgentToolHooks();
385
+ // Durable transcript-repair audit for a repair applied on open/resume.
386
+ if (this._transcriptRepair) {
387
+ this._invalidateContextBudgetCache({ type: "transcriptRepair" });
388
+ this._appendRunJournalAudit("transcript_repaired", {
389
+ insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
390
+ reason: this._transcriptRepair.reason,
391
+ });
392
+ }
207
393
  this._buildRuntime({
208
394
  activeToolNames: this._initialActiveToolNames,
209
395
  includeAllExtensionTools: true,
@@ -213,6 +399,357 @@ export class AgentSession {
213
399
  get modelRegistry() {
214
400
  return this._modelRegistry;
215
401
  }
402
+ /** Transcript repair applied while this session was opened/resumed, if any. */
403
+ get transcriptRepair() {
404
+ return this._transcriptRepair;
405
+ }
406
+ /** Durable lifecycle and audit records appended by this session's run journal. */
407
+ get runJournalRecords() {
408
+ return this._runJournalStore.records;
409
+ }
410
+ /** Most recently observed or inferred termination for this session. */
411
+ get lastTermination() {
412
+ return this._lastTermination;
413
+ }
414
+ /** Exact trailing journal fragment quarantine performed during startup, if any. */
415
+ get runJournalQuarantineReport() {
416
+ return this._runJournalStore.quarantineReport;
417
+ }
418
+ /** Elevated once a late-settling potentially-writing tool may have mutated the workspace. */
419
+ get sessionRiskLevel() {
420
+ return this._sessionRiskLevel;
421
+ }
422
+ /** Monotonic count of workspace mutation/invalidation signals emitted by this session. */
423
+ get workspaceMutationCount() {
424
+ return this._workspaceMutationCount;
425
+ }
426
+ get contextCacheInvalidationSnapshot() {
427
+ return this._contextCacheInvalidationSnapshot;
428
+ }
429
+ _contextCacheModelId(model) {
430
+ return model
431
+ ? `model-${createHash("sha256").update(`${model.provider}\0${model.id}`, "utf8").digest("hex")}`
432
+ : "unknown";
433
+ }
434
+ _contextCacheWorktreeFingerprint() {
435
+ return createHash("sha256").update(`${this._cwd}\0${this._workspaceMutationCount}`, "utf8").digest("hex");
436
+ }
437
+ _invalidateContextBudgetCache(event) {
438
+ const result = applyContextCacheInvalidation(this._contextCacheInvalidationSnapshot, event);
439
+ this._contextCacheInvalidationSnapshot = result.snapshot;
440
+ if (result.status === "overflow") {
441
+ this._contextBudgetCacheProvider = undefined;
442
+ return;
443
+ }
444
+ this._contextBudgetCacheProvider?.setInvalidationSnapshot?.(result.snapshot);
445
+ }
446
+ _recordEvidenceReceiptInvalidation(customType) {
447
+ if (customType === "evidence_receipt" || customType === "evidence-receipt") {
448
+ this._invalidateContextBudgetCache({ type: "evidenceReceipt" });
449
+ }
450
+ }
451
+ _sessionRevision() {
452
+ return this.sessionManager.getEntries().length;
453
+ }
454
+ _appendReplayEvent(type, payload) {
455
+ if (!this._replayLedger || !this._replayGoalId)
456
+ return;
457
+ this._replayLedger.append({
458
+ type,
459
+ goalId: this._replayGoalId,
460
+ ...(this._replayLaneId ? { laneId: this._replayLaneId } : {}),
461
+ payload,
462
+ });
463
+ this._replayLedger.persist();
464
+ }
465
+ /** Append one required durable audit record. Persistence failures propagate. */
466
+ _appendRunJournalAudit(event, details) {
467
+ this._runJournalStore.audit({
468
+ event,
469
+ details,
470
+ sessionRevision: this._sessionRevision(),
471
+ timestamp: new Date().toISOString(),
472
+ });
473
+ this._appendReplayEvent(event, details);
474
+ }
475
+ _terminationMessage(value, fallback) {
476
+ const redacted = redactSensitiveText(value?.trim() || fallback)
477
+ .replace(/\0/g, "")
478
+ .slice(0, 512);
479
+ return redacted || fallback;
480
+ }
481
+ _providerFailureCause(message) {
482
+ const text = message.errorMessage ?? "";
483
+ if (isContextOverflow(message, this.model?.contextWindow ?? 0)) {
484
+ return { area: "provider", code: "context_overflow" };
485
+ }
486
+ // Fable/Claude often emit stop_reason=refusal on benign turns (false positive).
487
+ // Match broadly so we never mislabel these as provider.protocol.
488
+ if (/stop_reason\s*=\s*(refusal|sensitive)|content\/safety stop|safety stop|provider\.refusal|kind=provider_refusal/i.test(text)) {
489
+ return { area: "provider", code: "refusal" };
490
+ }
491
+ if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
492
+ return { area: "provider", code: "auth" };
493
+ }
494
+ if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
495
+ return { area: "provider", code: "rate_limit" };
496
+ }
497
+ // Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
498
+ // Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
499
+ if (/tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|unknown\s+tool_call_id/i.test(text)) {
500
+ return { area: "provider", code: "protocol" };
501
+ }
502
+ if (/tool.+timed? out|tool.+timeout/i.test(text))
503
+ return { area: "tool", code: "timeout" };
504
+ if (/\btool\b/i.test(text) && !/tool_call_id/i.test(text))
505
+ return { area: "tool", code: "fatal" };
506
+ if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn|^terminated$/i.test(text)) {
507
+ return { area: "provider", code: "network" };
508
+ }
509
+ return { area: "provider", code: "protocol" };
510
+ }
511
+ _classifyPreflightCause(message) {
512
+ if (!this.model || /no model|model selected|model is required/i.test(message)) {
513
+ return { area: "configuration", code: "invalid" };
514
+ }
515
+ if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
516
+ return { area: "provider", code: "auth" };
517
+ }
518
+ if (/context.+overflow|context window|too many tokens/i.test(message)) {
519
+ return { area: "provider", code: "context_overflow" };
520
+ }
521
+ if (/duplicate.?result/i.test(message))
522
+ return { area: "transcript", code: "duplicate_result" };
523
+ if (/orphan.?result/i.test(message))
524
+ return { area: "transcript", code: "orphan_result" };
525
+ if (/duplicate.?call/i.test(message))
526
+ return { area: "transcript", code: "duplicate_call_id" };
527
+ if (/transcript|missing.?result/i.test(message))
528
+ return { area: "transcript", code: "missing_result" };
529
+ if (/compaction/i.test(message))
530
+ return { area: "compaction", code: "failed" };
531
+ if (/fsync/i.test(message))
532
+ return { area: "persistence", code: "fsync_failed" };
533
+ if (/lock/i.test(message))
534
+ return { area: "persistence", code: "lock_failed" };
535
+ if (/append|persist|write/i.test(message))
536
+ return { area: "persistence", code: "append_failed" };
537
+ if (/tool/i.test(message))
538
+ return { area: "tool", code: "fatal" };
539
+ return { area: "internal", code: "unclassified" };
540
+ }
541
+ _classifyRunTermination(runId, event) {
542
+ const timestamp = new Date().toISOString();
543
+ const assistant = [...event.messages]
544
+ .reverse()
545
+ .find((message) => message.role === "assistant");
546
+ let cause;
547
+ let message;
548
+ let sideEffects = this._sessionRiskLevel === "elevated" ? "possible" : "none";
549
+ let toolCallId;
550
+ let toolName;
551
+ if (this._activeRunToolTermination) {
552
+ cause = { area: "tool", code: "timeout" };
553
+ message = `Tool ${this._activeRunToolTermination.toolName} timed out.`;
554
+ toolCallId = this._activeRunToolTermination.toolCallId;
555
+ toolName = this._activeRunToolTermination.toolName;
556
+ sideEffects = this._activeRunToolTermination.executionStarted ? "possible" : sideEffects;
557
+ }
558
+ else if (!assistant) {
559
+ cause = { area: "internal", code: "unclassified" };
560
+ message = "Agent run ended without an assistant result.";
561
+ }
562
+ else if (assistant.stopReason === "aborted") {
563
+ cause = this._userAbortRequested ? { area: "user", code: "abort" } : { area: "provider", code: "abort" };
564
+ message = this._terminationMessage(assistant.errorMessage, this._userAbortRequested ? "The user aborted the run." : "The provider aborted the run.");
565
+ }
566
+ else if (assistant.stopReason === "error") {
567
+ cause = this._providerFailureCause(assistant);
568
+ message = this._terminationMessage(assistant.errorMessage, "The provider request failed.");
569
+ }
570
+ else {
571
+ cause = { area: "completed" };
572
+ message = "Run completed.";
573
+ }
574
+ return classifySessionTermination({
575
+ sessionId: this.sessionId,
576
+ runId,
577
+ timestamp,
578
+ source: "observed",
579
+ message,
580
+ cause,
581
+ sideEffects,
582
+ ...(assistant?.provider
583
+ ? { provider: assistant.provider }
584
+ : this.model
585
+ ? { provider: this.model.provider }
586
+ : {}),
587
+ ...(assistant?.model ? { model: assistant.model } : this.model ? { model: this.model.id } : {}),
588
+ ...(toolCallId ? { toolCallId } : {}),
589
+ ...(toolName ? { toolName } : {}),
590
+ });
591
+ }
592
+ _publishTermination(termination) {
593
+ this._lastTermination = termination;
594
+ this._emit({ type: "session_termination", termination });
595
+ }
596
+ _runtimeFailureCause(error) {
597
+ if (this._pendingRuntimeTerminationCause)
598
+ return this._pendingRuntimeTerminationCause;
599
+ const code = typeof error === "object" && error !== null && "code" in error ? Reflect.get(error, "code") : undefined;
600
+ if (typeof code === "string" &&
601
+ new Set(["EACCES", "EDQUOT", "EFBIG", "EIO", "EISDIR", "EMFILE", "ENFILE", "ENOSPC", "EPERM", "EROFS"]).has(code)) {
602
+ return { area: "persistence", code: "append_failed" };
603
+ }
604
+ const message = error instanceof Error ? error.message : String(error);
605
+ if (/compaction.+stale|session changed during compaction/i.test(message)) {
606
+ return { area: "compaction", code: "stale" };
607
+ }
608
+ if (/compaction/i.test(message))
609
+ return { area: "compaction", code: "failed" };
610
+ return { area: "internal", code: "unclassified" };
611
+ }
612
+ _publishRuntimeFailure(error) {
613
+ const cause = this._runtimeFailureCause(error);
614
+ const runId = this._activeRunId ?? `runtime-${randomUUID()}`;
615
+ const timestamp = new Date().toISOString();
616
+ const message = cause.area === "persistence"
617
+ ? "A required runtime persistence operation failed."
618
+ : cause.area === "compaction"
619
+ ? "Runtime compaction failed."
620
+ : "The AgentSession runtime failed before completing the run.";
621
+ let termination = classifySessionTermination({
622
+ sessionId: this.sessionId,
623
+ runId,
624
+ timestamp,
625
+ source: "observed",
626
+ message,
627
+ cause,
628
+ sideEffects: this._activeRunId === null ? "none" : "possible",
629
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
630
+ });
631
+ if (this._activeRunId !== null && this._runJournalStore.openRunId === this._activeRunId) {
632
+ try {
633
+ this._runJournalStore.finish({
634
+ termination,
635
+ sessionRevision: this._sessionRevision(),
636
+ timestamp,
637
+ });
638
+ }
639
+ catch {
640
+ termination = classifySessionTermination({
641
+ sessionId: this.sessionId,
642
+ runId,
643
+ timestamp,
644
+ source: "observed",
645
+ message: "The run journal could not persist the runtime termination.",
646
+ cause: { area: "persistence", code: "append_failed" },
647
+ sideEffects: "possible",
648
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
649
+ });
650
+ }
651
+ }
652
+ this._activeRunId = null;
653
+ this._activeRunToolTermination = undefined;
654
+ this._pendingRuntimeTerminationCause = undefined;
655
+ this._userAbortRequested = false;
656
+ this._publishTermination(termination);
657
+ }
658
+ _handleRunLifecycleEvent(event) {
659
+ if (event.type === "agent_start") {
660
+ if (this._activeRunId !== null)
661
+ throw new Error("run journal already has an active AgentSession run");
662
+ const runId = randomUUID();
663
+ this._activeRunId = runId;
664
+ this._activeRunToolTermination = undefined;
665
+ this._pendingRuntimeTerminationCause = undefined;
666
+ this._userAbortRequested = false;
667
+ try {
668
+ this._runJournalStore.start({
669
+ runId,
670
+ sessionRevision: this._sessionRevision(),
671
+ timestamp: new Date().toISOString(),
672
+ });
673
+ }
674
+ catch (error) {
675
+ this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
676
+ throw error;
677
+ }
678
+ return;
679
+ }
680
+ if (event.type !== "agent_end")
681
+ return;
682
+ if (this._activeRunId === null)
683
+ throw new Error("run journal received agent_end without run_started");
684
+ const termination = this._classifyRunTermination(this._activeRunId, event);
685
+ try {
686
+ this._runJournalStore.finish({
687
+ termination,
688
+ sessionRevision: this._sessionRevision(),
689
+ timestamp: termination.timestamp,
690
+ });
691
+ }
692
+ catch (error) {
693
+ this._pendingRuntimeTerminationCause = { area: "persistence", code: "append_failed" };
694
+ throw error;
695
+ }
696
+ this._activeRunId = null;
697
+ this._activeRunToolTermination = undefined;
698
+ this._pendingRuntimeTerminationCause = undefined;
699
+ this._userAbortRequested = false;
700
+ this._publishTermination(termination);
701
+ }
702
+ /**
703
+ * Handle tool timeout / late-settlement audit signals (ALG004-A/B). A late
704
+ * settlement of a potentially-writing tool raises session risk and emits a
705
+ * workspace mutation/invalidation signal for evidence freshness consumers.
706
+ */
707
+ _handleToolAuditEvent(event) {
708
+ if (event.type === "tool_execution_end") {
709
+ this._invalidateContextBudgetCache({ type: "toolResultDisposition" });
710
+ const envelope = event.result?.details?.omk;
711
+ if (envelope && envelope.schema === "tool-result/v2" && envelope.disposition === "timeout") {
712
+ const timeout = {
713
+ toolCallId: event.toolCallId,
714
+ toolName: event.toolName,
715
+ ...(typeof envelope.timeoutMs === "number" ? { timeoutMs: envelope.timeoutMs } : {}),
716
+ executionStarted: envelope.executionStarted === true,
717
+ };
718
+ this._activeRunToolTermination = timeout;
719
+ this._appendRunJournalAudit("tool_timeout", timeout);
720
+ }
721
+ return;
722
+ }
723
+ if (event.type !== "tool_execution_late_settlement") {
724
+ return;
725
+ }
726
+ // Fail closed: anything not classified as a read-category tool may write.
727
+ const potentiallyWriting = resolveToolTimeoutCategory(event.toolName) !== "read";
728
+ this._appendRunJournalAudit("tool_late_settlement", {
729
+ toolCallId: event.toolCallId,
730
+ toolName: event.toolName,
731
+ disposition: event.disposition,
732
+ outcome: event.outcome,
733
+ ...(potentiallyWriting ? { sessionRisk: "elevated" } : {}),
734
+ });
735
+ if (potentiallyWriting) {
736
+ this._sessionRiskLevel = "elevated";
737
+ this._workspaceMutationCount += 1;
738
+ this._invalidateContextBudgetCache({
739
+ type: "worktreeFingerprint",
740
+ value: this._contextCacheWorktreeFingerprint(),
741
+ });
742
+ const payload = { root: this._cwd, paths: [] };
743
+ this._appendReplayEvent("workspace_mutation", payload);
744
+ this._emit({
745
+ type: "workspace_mutation",
746
+ source: "tool_late_settlement",
747
+ toolCallId: event.toolCallId,
748
+ toolName: event.toolName,
749
+ payload,
750
+ });
751
+ }
752
+ }
216
753
  _getBashSandboxPreflight(override) {
217
754
  const preflight = override ?? this._configuredBashSandboxPreflight;
218
755
  if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
@@ -323,6 +860,8 @@ export class AgentSession {
323
860
  _lastAssistantMessage = undefined;
324
861
  /** Internal handler for agent events - shared by subscribe and reconnect */
325
862
  _handleAgentEvent = async (event) => {
863
+ // Required run lifecycle persistence executes before extension/user listeners.
864
+ this._handleRunLifecycleEvent(event);
326
865
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
327
866
  // This ensures the UI sees the updated queue state
328
867
  if (event.type === "message_start" && event.message.role === "user") {
@@ -345,28 +884,37 @@ export class AgentSession {
345
884
  }
346
885
  }
347
886
  }
887
+ // Durable tool timeout / late-settlement audits (ALG004-A/B).
888
+ this._handleToolAuditEvent(event);
348
889
  // Emit to extensions first
349
890
  await this._emitExtensionEvent(event);
891
+ const replacement = event.type === "message_end" ? this._messageEndReplacements.get(event) : undefined;
892
+ if (event.type === "message_end") {
893
+ this._messageEndReplacements.delete(event);
894
+ }
895
+ const finalizedEvent = replacement === undefined ? event : Object.freeze({ ...event, message: replacement });
350
896
  // Notify all listeners
351
- this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
897
+ this._emit(finalizedEvent.type === "agent_end"
898
+ ? { ...finalizedEvent, willRetry: this._willRetryAfterAgentEnd(finalizedEvent) }
899
+ : finalizedEvent);
352
900
  // Handle session persistence
353
- if (event.type === "message_end") {
901
+ if (finalizedEvent.type === "message_end") {
354
902
  // Check if this is a custom message from extensions
355
- if (event.message.role === "custom") {
903
+ if (finalizedEvent.message.role === "custom") {
356
904
  // Persist as CustomMessageEntry
357
- this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
905
+ this.sessionManager.appendCustomMessageEntry(finalizedEvent.message.customType, finalizedEvent.message.content, finalizedEvent.message.display, finalizedEvent.message.details);
358
906
  }
359
- else if (event.message.role === "user" ||
360
- event.message.role === "assistant" ||
361
- event.message.role === "toolResult") {
907
+ else if (finalizedEvent.message.role === "user" ||
908
+ finalizedEvent.message.role === "assistant" ||
909
+ finalizedEvent.message.role === "toolResult") {
362
910
  // Regular LLM message - persist as SessionMessageEntry
363
- this.sessionManager.appendMessage(event.message);
911
+ this.sessionManager.appendMessage(finalizedEvent.message);
364
912
  }
365
913
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
366
914
  // 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;
915
+ if (finalizedEvent.message.role === "assistant") {
916
+ this._lastAssistantMessage = finalizedEvent.message;
917
+ const assistantMsg = finalizedEvent.message;
370
918
  if (assistantMsg.stopReason !== "error") {
371
919
  this._overflowRecoveryAttempted = false;
372
920
  }
@@ -417,19 +965,19 @@ export class AgentSession {
417
965
  }
418
966
  return undefined;
419
967
  }
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);
968
+ _replaceFinalizedMessage(target, replacement) {
969
+ const messages = this.agent.state.messages;
970
+ const matchingIndexes = messages.flatMap((message, index) => (isDeepStrictEqual(message, target) ? [index] : []));
971
+ if (matchingIndexes.length !== 1) {
972
+ throw new Error(matchingIndexes.length === 0
973
+ ? "Finalized message was not found in agent state"
974
+ : "Finalized message is ambiguous in agent state");
975
+ }
976
+ const finalizedReplacement = createImmutableMessageSnapshot(replacement);
977
+ const nextMessages = messages.slice();
978
+ nextMessages[matchingIndexes[0]] = finalizedReplacement;
979
+ this.agent.state.messages = nextMessages;
980
+ return finalizedReplacement;
433
981
  }
434
982
  /** Emit extension events based on agent events */
435
983
  async _emitExtensionEvent(event) {
@@ -480,7 +1028,8 @@ export class AgentSession {
480
1028
  };
481
1029
  const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
482
1030
  if (replacement) {
483
- this._replaceMessageInPlace(event.message, replacement);
1031
+ const finalizedReplacement = this._replaceFinalizedMessage(event.message, replacement);
1032
+ this._messageEndReplacements.set(event, finalizedReplacement);
484
1033
  }
485
1034
  }
486
1035
  else if (event.type === "tool_execution_start") {
@@ -647,10 +1196,28 @@ export class AgentSession {
647
1196
  }
648
1197
  }
649
1198
  this.agent.state.tools = tools;
1199
+ this._applyToolTimeoutCategoryDefaults(validToolNames);
650
1200
  // Rebuild base system prompt with new tool set
651
1201
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
652
1202
  this.agent.state.systemPrompt = this._baseSystemPrompt;
653
1203
  }
1204
+ /**
1205
+ * Recompute the Agent's per-name timeout map for the active tool set:
1206
+ * explicit user/settings entries always win; active tools without an entry
1207
+ * receive their §6.3 category default (ALG004-C); uncategorized tools fall
1208
+ * through to the global `toolTimeoutMs`.
1209
+ */
1210
+ _applyToolTimeoutCategoryDefaults(activeToolNames) {
1211
+ try {
1212
+ const resolved = resolveAgentToolSettings(this.settingsManager);
1213
+ this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, resolved.toolTimeouts);
1214
+ }
1215
+ catch {
1216
+ // Invalid settings fail closed at session creation; keep current
1217
+ // explicit entries and still fill category defaults for active tools.
1218
+ this.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, this.agent.toolTimeouts ?? {});
1219
+ }
1220
+ }
654
1221
  /** Whether compaction or branch summarization is currently running */
655
1222
  get isCompacting() {
656
1223
  return (this._autoCompactionAbortController !== undefined ||
@@ -791,6 +1358,7 @@ export class AgentSession {
791
1358
  parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
792
1359
  }
793
1360
  const cacheProvider = this._contextBudgetCacheProvider ?? createMemoryContextBudgetCacheProviderV2("session");
1361
+ cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
794
1362
  this._contextBudgetCacheProvider = cacheProvider;
795
1363
  // Enforce floor
796
1364
  if (maxPromptTokens < MIN_PROMPT_TOKENS) {
@@ -870,6 +1438,10 @@ export class AgentSession {
870
1438
  await this.agent.continue();
871
1439
  }
872
1440
  }
1441
+ catch (error) {
1442
+ this._publishRuntimeFailure(error);
1443
+ throw error;
1444
+ }
873
1445
  finally {
874
1446
  this._flushPendingBashMessages();
875
1447
  }
@@ -911,7 +1483,7 @@ export class AgentSession {
911
1483
  async prompt(text, options) {
912
1484
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
913
1485
  const preflightResult = options?.preflightResult;
914
- let currentText = text;
1486
+ let currentText = redactSensitiveText(text);
915
1487
  let promptActiveSkillNames = [...(options?.activeSkillNames ?? [])];
916
1488
  let promptActiveSkillSource = options?.activeSkillSource;
917
1489
  let isBangSkillInvocation = false;
@@ -959,6 +1531,7 @@ export class AgentSession {
959
1531
  expandedText = this._expandSkillCommand(expandedText);
960
1532
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
961
1533
  }
1534
+ expandedText = redactSensitiveText(expandedText);
962
1535
  // If streaming, queue via steer() or followUp() based on option
963
1536
  if (this.isStreaming) {
964
1537
  if (!options?.streamingBehavior) {
@@ -979,6 +1552,8 @@ export class AgentSession {
979
1552
  if (!this.model) {
980
1553
  throw new Error(formatNoModelSelectedMessage());
981
1554
  }
1555
+ // Root-level: eject sticky safety models (Fable) before the turn leaves the machine.
1556
+ await this._ejectStickySafetyModelIfNeeded();
982
1557
  // Grok OAuth: refuse Imagine ids on the chat/completions path (tool-only).
983
1558
  assertTextChatModelForCompletion(this.model.id, this.model.provider);
984
1559
  if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
@@ -1058,6 +1633,19 @@ export class AgentSession {
1058
1633
  }
1059
1634
  catch (error) {
1060
1635
  preflightResult?.(false);
1636
+ const rawMessage = error instanceof Error ? error.message : String(error);
1637
+ const cause = this._classifyPreflightCause(rawMessage);
1638
+ const timestamp = new Date().toISOString();
1639
+ this._publishTermination(classifySessionTermination({
1640
+ sessionId: this.sessionId,
1641
+ runId: `preflight-${randomUUID()}`,
1642
+ timestamp,
1643
+ source: "observed",
1644
+ message: this._terminationMessage(rawMessage, "Prompt preflight failed."),
1645
+ cause,
1646
+ sideEffects: "none",
1647
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
1648
+ }));
1061
1649
  throw error;
1062
1650
  }
1063
1651
  if (!messages) {
@@ -1132,14 +1720,15 @@ export class AgentSession {
1132
1720
  * @throws Error if text is an extension command
1133
1721
  */
1134
1722
  async steer(text, images) {
1723
+ const sanitizedText = redactSensitiveText(text);
1135
1724
  // Check for extension commands (cannot be queued)
1136
- if (text.startsWith("/")) {
1137
- this._throwIfExtensionCommand(text);
1725
+ if (sanitizedText.startsWith("/")) {
1726
+ this._throwIfExtensionCommand(sanitizedText);
1138
1727
  }
1139
1728
  // Expand skill commands and prompt templates
1140
- let expandedText = this._expandSkillCommand(text);
1729
+ let expandedText = this._expandSkillCommand(sanitizedText);
1141
1730
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1142
- await this._queueSteer(expandedText, images);
1731
+ await this._queueSteer(redactSensitiveText(expandedText), images);
1143
1732
  }
1144
1733
  /**
1145
1734
  * Queue a follow-up message to be processed after the agent finishes.
@@ -1149,19 +1738,21 @@ export class AgentSession {
1149
1738
  * @throws Error if text is an extension command
1150
1739
  */
1151
1740
  async followUp(text, images) {
1741
+ const sanitizedText = redactSensitiveText(text);
1152
1742
  // Check for extension commands (cannot be queued)
1153
- if (text.startsWith("/")) {
1154
- this._throwIfExtensionCommand(text);
1743
+ if (sanitizedText.startsWith("/")) {
1744
+ this._throwIfExtensionCommand(sanitizedText);
1155
1745
  }
1156
1746
  // Expand skill commands and prompt templates
1157
- let expandedText = this._expandSkillCommand(text);
1747
+ let expandedText = this._expandSkillCommand(sanitizedText);
1158
1748
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1159
- await this._queueFollowUp(expandedText, images);
1749
+ await this._queueFollowUp(redactSensitiveText(expandedText), images);
1160
1750
  }
1161
1751
  /**
1162
1752
  * Internal: Queue a steering message (already expanded, no extension command check).
1163
1753
  */
1164
1754
  async _queueSteer(text, images) {
1755
+ this._invalidateContextBudgetCache({ type: "userSteering" });
1165
1756
  this._steeringMessages.push(text);
1166
1757
  this._emitQueueUpdate();
1167
1758
  const content = [{ type: "text", text }];
@@ -1222,6 +1813,7 @@ export class AgentSession {
1222
1813
  details: message.details,
1223
1814
  timestamp: Date.now(),
1224
1815
  };
1816
+ this._recordEvidenceReceiptInvalidation(message.customType);
1225
1817
  if (options?.deliverAs === "nextTurn") {
1226
1818
  this._pendingNextTurnMessages.push(appMessage);
1227
1819
  }
@@ -1313,16 +1905,43 @@ export class AgentSession {
1313
1905
  * Abort current operation and wait for agent to become idle.
1314
1906
  */
1315
1907
  async abort() {
1908
+ this._userAbortRequested = this.isStreaming || this.isRetrying;
1316
1909
  this.abortRetry();
1317
1910
  this.agent.abort();
1318
1911
  await this.agent.waitForIdle();
1319
1912
  }
1913
+ /** Record an observed process signal before a mode begins shutdown. */
1914
+ recordProcessSignal(signal) {
1915
+ const timestamp = new Date().toISOString();
1916
+ const runId = this._activeRunId ?? `signal-${randomUUID()}`;
1917
+ const termination = classifySessionTermination({
1918
+ sessionId: this.sessionId,
1919
+ runId,
1920
+ timestamp,
1921
+ source: "observed",
1922
+ message: `Process received ${signal}.`,
1923
+ cause: { area: "process", code: "signal", signal },
1924
+ sideEffects: this._activeRunId === null ? "none" : "possible",
1925
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
1926
+ });
1927
+ if (this._activeRunId !== null) {
1928
+ this._runJournalStore.finish({
1929
+ termination,
1930
+ sessionRevision: this._sessionRevision(),
1931
+ timestamp,
1932
+ });
1933
+ this._activeRunId = null;
1934
+ }
1935
+ this._publishTermination(termination);
1936
+ return termination;
1937
+ }
1320
1938
  // =========================================================================
1321
1939
  // Model Management
1322
1940
  // =========================================================================
1323
1941
  async _emitModelSelect(nextModel, previousModel, source) {
1324
1942
  if (modelsAreEqual(previousModel, nextModel))
1325
1943
  return;
1944
+ this._invalidateContextBudgetCache({ type: "activeModelId", value: this._contextCacheModelId(nextModel) });
1326
1945
  await this._extensionRunner.emit({
1327
1946
  type: "model_select",
1328
1947
  model: nextModel,
@@ -1339,6 +1958,11 @@ export class AgentSession {
1339
1958
  if (!this._modelRegistry.hasConfiguredAuth(model)) {
1340
1959
  throw new Error(`No API key for ${model.provider}/${model.id}`);
1341
1960
  }
1961
+ // Root-level: block sticky safety models (Fable) unless explicitly allowed in settings.
1962
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
1963
+ if (resilience.blockStickySafetyModels && isStickySafetyModel(model.id, model.provider)) {
1964
+ throw new Error(stickySafetyBlockMessage(model.id, model.provider));
1965
+ }
1342
1966
  // Grok OAuth: block selecting Imagine models as the session chat model.
1343
1967
  assertTextChatModelForCompletion(model.id, model.provider);
1344
1968
  const previousModel = this.model;
@@ -1363,7 +1987,15 @@ export class AgentSession {
1363
1987
  return this._cycleAvailableModel(direction);
1364
1988
  }
1365
1989
  async _cycleScopedModel(direction) {
1366
- const scopedModels = this._scopedModels.filter((scoped) => this._modelRegistry.hasConfiguredAuth(scoped.model));
1990
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
1991
+ const scopedModels = this._scopedModels.filter((scoped) => {
1992
+ if (!this._modelRegistry.hasConfiguredAuth(scoped.model))
1993
+ return false;
1994
+ if (resilience.blockStickySafetyModels && isStickySafetyModel(scoped.model.id, scoped.model.provider)) {
1995
+ return false;
1996
+ }
1997
+ return true;
1998
+ });
1367
1999
  if (scopedModels.length <= 1)
1368
2000
  return undefined;
1369
2001
  const currentModel = this.model;
@@ -1374,20 +2006,19 @@ export class AgentSession {
1374
2006
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1375
2007
  const next = scopedModels[nextIndex];
1376
2008
  const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
1377
- // Apply model
1378
- this.agent.state.model = next.model;
1379
- this.sessionManager.appendModelChange(next.model.provider, next.model.id);
1380
- this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1381
- // Apply thinking level.
1382
- // - Explicit scoped model thinking level overrides current session level
1383
- // - Undefined scoped model thinking level inherits the current session preference
1384
- // setThinkingLevel clamps to model capabilities.
2009
+ // Single path through setModel (sticky block + auth + Imagine guards).
2010
+ await this.setModel(next.model);
2011
+ // Explicit scoped thinking level still applies after switch.
1385
2012
  this.setThinkingLevel(thinkingLevel);
1386
- await this._emitModelSelect(next.model, currentModel, "cycle");
1387
2013
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
1388
2014
  }
1389
2015
  async _cycleAvailableModel(direction) {
1390
- const availableModels = await this._modelRegistry.getAvailable();
2016
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
2017
+ const raw = await this._modelRegistry.getAvailable();
2018
+ // Hard-kill sticky safety models from the cycle list (Fable must not re-enter via hotkey).
2019
+ const availableModels = resilience.blockStickySafetyModels
2020
+ ? raw.filter((m) => !isStickySafetyModel(m.id, m.provider))
2021
+ : raw;
1391
2022
  if (availableModels.length <= 1)
1392
2023
  return undefined;
1393
2024
  const currentModel = this.model;
@@ -1397,13 +2028,8 @@ export class AgentSession {
1397
2028
  const len = availableModels.length;
1398
2029
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1399
2030
  const nextModel = availableModels[nextIndex];
1400
- const thinkingLevel = this._getThinkingLevelForModelSwitch();
1401
- this.agent.state.model = nextModel;
1402
- this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1403
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1404
- // Re-clamp thinking level for new model's capabilities
1405
- this.setThinkingLevel(thinkingLevel);
1406
- await this._emitModelSelect(nextModel, currentModel, "cycle");
2031
+ // Always go through setModel so sticky blocks / auth / Imagine guards stay single-path.
2032
+ await this.setModel(nextModel);
1407
2033
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
1408
2034
  }
1409
2035
  // =========================================================================
@@ -1503,7 +2129,7 @@ export class AgentSession {
1503
2129
  hadFence: features.hadFence,
1504
2130
  hadDiff: features.hadDiff,
1505
2131
  });
1506
- const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, null);
2132
+ const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, this._getAdaptorchHint(verdict, features));
1507
2133
  if (learningEnabled && resolved !== "off") {
1508
2134
  const record = {
1509
2135
  routerVersion: "v4",
@@ -1597,6 +2223,76 @@ export class AgentSession {
1597
2223
  return 1;
1598
2224
  return 0;
1599
2225
  }
2226
+ /**
2227
+ * AdaptOrch advisory bridge hint accessor (default-off, global-only).
2228
+ * Lazily constructs the bridge on first call when enabled; returns `null`
2229
+ * (no hint) otherwise. The bridge's `getFreshHint` is synchronous and
2230
+ * cache-read-only, so this never blocks the turn-start path. A fire-and-
2231
+ * forget `requestRefresh` is issued for the NEXT turn's benefit.
2232
+ *
2233
+ * The returned hint is shaped as `{ level, confidence }` for the resolver's
2234
+ * hint-fusion slot: confidence is mapped from the bridge's closed
2235
+ * `confidenceBand` enum (low=0.5, medium=0.75, high=0.95) and the level is
2236
+ * derived from the bridge's `taskClass` via the same static rule table the
2237
+ * resolver uses. The resolver's own HINT_CONFIDENCE_THRESHOLD (0.7) gates
2238
+ * whether the hint actually fuses, so a "low" band hint is structurally
2239
+ * inert.
2240
+ */
2241
+ _getAdaptorchHint(verdict, features) {
2242
+ if (!this.settingsManager.getAdaptorchBridgeEnabled())
2243
+ return null;
2244
+ // Lazy init (at most once per session)
2245
+ if (!this._adaptorchBridgeInitAttempted) {
2246
+ this._adaptorchBridgeInitAttempted = true;
2247
+ const bridgeSettings = this.settingsManager.getAdaptorchBridgeSettings();
2248
+ this._adaptorchBridge = createAdaptorchBridge({
2249
+ // The advisory function is a no-op stub until a real MCP transport
2250
+ // is wired by a future lane. The bridge's circuit breaker and budget
2251
+ // counter ensure this stub can never cause harm.
2252
+ advisoryFn: async () => null,
2253
+ ttlMs: bridgeSettings?.ttlMs,
2254
+ timeoutMs: bridgeSettings?.timeoutMs,
2255
+ maxConsultsPerSession: bridgeSettings?.maxConsultsPerSession,
2256
+ failureThreshold: bridgeSettings?.failureThreshold,
2257
+ });
2258
+ }
2259
+ const bridge = this._adaptorchBridge;
2260
+ if (bridge === null)
2261
+ return null;
2262
+ const payload = {
2263
+ schemaVersion: 1,
2264
+ taskClass: verdict.taskClass,
2265
+ runnerUp: verdict.taskClass,
2266
+ marginBucket: "high",
2267
+ lenBucket: features.lenBucket,
2268
+ hadFence: features.hadFence,
2269
+ hadDiff: features.hadDiff,
2270
+ pressureBucket: this._computePressureBucket(),
2271
+ };
2272
+ // Fire-and-forget refresh for next turn
2273
+ bridge.requestRefresh(payload);
2274
+ // Synchronous cache read for this turn
2275
+ const hint = bridge.getFreshHint(payload);
2276
+ if (hint === null)
2277
+ return null;
2278
+ // Map confidenceBand -> numeric confidence for the resolver's threshold gate
2279
+ const confidenceMap = { low: 0.5, medium: 0.75, high: 0.95 };
2280
+ const confidence = confidenceMap[hint.confidenceBand] ?? 0.5;
2281
+ // Map taskClass -> ThinkingLevel via the same rule table the resolver uses
2282
+ const levelMap = {
2283
+ trivial: "minimal",
2284
+ "simple-edit": "low",
2285
+ "code-gen": "medium",
2286
+ debug: "high",
2287
+ refactor: "high",
2288
+ review: "high",
2289
+ plan: "xhigh",
2290
+ };
2291
+ const level = levelMap[hint.taskClass];
2292
+ if (level === undefined)
2293
+ return null;
2294
+ return { level, confidence };
2295
+ }
1600
2296
  /**
1601
2297
  * Cycle to next thinking level.
1602
2298
  * @returns New level, or undefined if model doesn't support thinking
@@ -1660,17 +2356,330 @@ export class AgentSession {
1660
2356
  // =========================================================================
1661
2357
  // Compaction
1662
2358
  // =========================================================================
2359
+ _captureCompactionState() {
2360
+ return this.sessionManager.withCompactionCommitLock(() => this._captureCompactionStateLocked());
2361
+ }
2362
+ _captureCompactionStateLocked() {
2363
+ const sessionFile = this.sessionManager.getSessionFile();
2364
+ const bytes = sessionFile && existsSync(sessionFile)
2365
+ ? new Uint8Array(readFileSync(sessionFile))
2366
+ : new TextEncoder().encode(`${[this.sessionManager.getHeader(), ...this.sessionManager.getEntries()]
2367
+ .filter((entry) => entry !== null)
2368
+ .map((entry) => JSON.stringify(entry))
2369
+ .join("\n")}\n`);
2370
+ const report = inspectSessionIntegrity(bytes, { activeLeafId: this.sessionManager.getLeafId() });
2371
+ const branchEntries = report.activeBranch;
2372
+ let latestCompactionIndex = -1;
2373
+ for (let index = branchEntries.length - 1; index >= 0; index -= 1) {
2374
+ if (branchEntries[index]?.type === "compaction") {
2375
+ latestCompactionIndex = index;
2376
+ break;
2377
+ }
2378
+ }
2379
+ const latestCompaction = branchEntries[latestCompactionIndex];
2380
+ const firstKeptIndex = latestCompaction?.type === "compaction"
2381
+ ? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)
2382
+ : -1;
2383
+ const sourceEntries = branchEntries.slice(latestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);
2384
+ const firstEntry = sourceEntries[0];
2385
+ const lastEntry = sourceEntries.at(-1);
2386
+ if (!firstEntry || !lastEntry || report.activeLeafId === null) {
2387
+ const barrier = evaluateCompactionBarrier(report, [...this.agent.state.pendingToolCalls]);
2388
+ if (barrier.status !== "ready")
2389
+ throw this._barrierError(barrier);
2390
+ throw new Error("Nothing to compact: the active session branch is empty");
2391
+ }
2392
+ const revision = this.sessionManager.getDurableHeadToken();
2393
+ const source = createCompactionSourceIdentity({
2394
+ sessionId: revision.sessionId,
2395
+ entryIds: sourceEntries.map((entry) => entry.id),
2396
+ firstEntryId: firstEntry.id,
2397
+ lastEntryId: lastEntry.id,
2398
+ sourceSha256: createHash("sha256")
2399
+ .update(sourceEntries.map((entry) => JSON.stringify(entry)).join("\n"), "utf8")
2400
+ .digest("hex"),
2401
+ activeLeafId: report.activeLeafId,
2402
+ messageCount: report.activeMessages.length,
2403
+ });
2404
+ return { report, branchEntries, revision, source };
2405
+ }
2406
+ _captureCompactionProvenance(capture) {
2407
+ let latestIntent = "Continue the current session";
2408
+ for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
2409
+ const message = capture.report.activeMessages[index];
2410
+ if (message?.role !== "user")
2411
+ continue;
2412
+ const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim()).slice(0, 16_384);
2413
+ if (candidate.length > 0)
2414
+ latestIntent = candidate;
2415
+ break;
2416
+ }
2417
+ const modelHistory = capture.branchEntries
2418
+ .flatMap((entry) => {
2419
+ if (entry.type === "model_change") {
2420
+ return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
2421
+ }
2422
+ if (entry.type === "message" && entry.message.role === "assistant") {
2423
+ return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
2424
+ }
2425
+ return [];
2426
+ })
2427
+ .slice(-256);
2428
+ const customEntryIds = (customType) => capture.branchEntries
2429
+ .filter((entry) => entry.type === "custom" && entry.customType === customType)
2430
+ .map((entry) => entry.id);
2431
+ return {
2432
+ latestIntent,
2433
+ openTasks: [],
2434
+ laneIds: customEntryIds("lane"),
2435
+ acceptancePredicateIds: customEntryIds("acceptance_predicate"),
2436
+ evidenceReceiptIds: customEntryIds("evidence_receipt"),
2437
+ blockerReasons: [],
2438
+ repairEventIds: [
2439
+ ...customEntryIds("transcript_repaired"),
2440
+ ...customEntryIds("compaction_transcript_repaired"),
2441
+ ],
2442
+ branch: null,
2443
+ worktree: this._cwd,
2444
+ modelHistory,
2445
+ nextAction: latestIntent,
2446
+ };
2447
+ }
2448
+ _barrierError(barrier) {
2449
+ if (barrier.status === "defer") {
2450
+ return new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);
2451
+ }
2452
+ return new Error(`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`);
2453
+ }
2454
+ _evaluateCompactionBarrier(capture, includeMissingTailAsPending, excludedPendingIds = new Set()) {
2455
+ const pending = new Set([...this.agent.state.pendingToolCalls].filter((toolCallId) => !excludedPendingIds.has(toolCallId)));
2456
+ if (includeMissingTailAsPending) {
2457
+ for (const issue of capture.report.transcript?.issues ?? []) {
2458
+ if (issue.kind === "missing_result")
2459
+ pending.add(issue.toolCallId);
2460
+ }
2461
+ }
2462
+ return evaluateCompactionBarrier(capture.report, [...pending]);
2463
+ }
2464
+ _repairEmergencyCompactionTail(capture) {
2465
+ const barrier = this._evaluateCompactionBarrier(capture, true);
2466
+ if (barrier.status !== "defer" || barrier.reason !== "missing_active_tail_results") {
2467
+ if (barrier.status !== "ready")
2468
+ throw this._barrierError(barrier);
2469
+ return { capture, repairedToolCallIds: new Set() };
2470
+ }
2471
+ const repairedMessages = repairTranscriptIntegrity([...capture.report.activeMessages], "Tool result missing; synthesized to close an emergency compaction barrier");
2472
+ const inserted = repairedMessages.slice(capture.report.activeMessages.length);
2473
+ const repairedToolCallIds = new Set();
2474
+ for (const message of inserted) {
2475
+ if (message.role !== "toolResult") {
2476
+ throw new Error("Emergency compaction repair produced a non-tool result");
2477
+ }
2478
+ const toolResult = message;
2479
+ repairedToolCallIds.add(toolResult.toolCallId);
2480
+ this.sessionManager.appendMessage(toolResult);
2481
+ }
2482
+ this.sessionManager.appendCustomEntry("compaction_transcript_repaired", {
2483
+ insertedToolCallIds: [...repairedToolCallIds],
2484
+ reason: "emergency_compaction",
2485
+ });
2486
+ this._invalidateContextBudgetCache({ type: "transcriptRepair" });
2487
+ const closedCapture = this._captureCompactionState();
2488
+ const closedBarrier = this._evaluateCompactionBarrier(closedCapture, false, repairedToolCallIds);
2489
+ if (closedBarrier.status !== "ready")
2490
+ throw this._barrierError(closedBarrier);
2491
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
2492
+ return { capture: closedCapture, repairedToolCallIds };
2493
+ }
2494
+ _priorCommittedCompactionSourceDigests() {
2495
+ const digests = [];
2496
+ for (const entry of this.sessionManager.getEntries()) {
2497
+ if (entry.type !== "compaction" || typeof entry.details !== "object" || entry.details === null)
2498
+ continue;
2499
+ if (!Object.hasOwn(entry.details, "compactionEnvelope"))
2500
+ continue;
2501
+ const envelope = validateCompactionEnvelope(Reflect.get(entry.details, "compactionEnvelope"));
2502
+ if (envelope.summary !== entry.summary) {
2503
+ throw new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);
2504
+ }
2505
+ digests.push(envelope.source.sourceSha256);
2506
+ }
2507
+ return digests;
2508
+ }
2509
+ _beginCompactionTransaction(compactionModel, emergency) {
2510
+ let capture = this._captureCompactionState();
2511
+ if (emergency) {
2512
+ capture = this._repairEmergencyCompactionTail(capture).capture;
2513
+ }
2514
+ else {
2515
+ const barrier = this._evaluateCompactionBarrier(capture, false);
2516
+ if (barrier.status !== "ready")
2517
+ throw this._barrierError(barrier);
2518
+ }
2519
+ const transaction = createCompactionTransaction({
2520
+ transactionId: randomUUID(),
2521
+ baseRevision: capture.revision,
2522
+ source: capture.source,
2523
+ createdAt: new Date().toISOString(),
2524
+ model: { provider: compactionModel.provider, id: compactionModel.id },
2525
+ preserved: this._captureCompactionProvenance(capture),
2526
+ });
2527
+ if (this._priorCommittedCompactionSourceDigests().includes(transaction.source.sourceSha256)) {
2528
+ throw new Error("This exact compaction source was already compacted");
2529
+ }
2530
+ return { capture, transaction };
2531
+ }
2532
+ _detailsWithCompactionEnvelope(details, envelope) {
2533
+ if (typeof details === "object" && details !== null && !Array.isArray(details)) {
2534
+ return { ...details, compactionEnvelope: envelope };
2535
+ }
2536
+ return {
2537
+ compactionEnvelope: envelope,
2538
+ ...(details === undefined ? {} : { resultDetails: details }),
2539
+ };
2540
+ }
2541
+ _commitCompaction(begun, result, fromExtension) {
2542
+ if (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {
2543
+ throw new Error("Compaction first-kept entry is outside the captured source");
2544
+ }
2545
+ const committed = this.sessionManager.withCompactionCommitLock(() => {
2546
+ const current = this._captureCompactionState();
2547
+ const barrier = this._evaluateCompactionBarrier(current, false);
2548
+ const decision = decideCompactionCommit({
2549
+ transaction: begun.transaction,
2550
+ currentRevision: current.revision,
2551
+ currentSource: current.source,
2552
+ barrier,
2553
+ priorCommittedSourceDigests: this._priorCommittedCompactionSourceDigests(),
2554
+ });
2555
+ switch (decision.decision) {
2556
+ case "duplicate":
2557
+ throw new Error("This exact compaction source was already compacted");
2558
+ case "stale":
2559
+ throw new Error(`Session changed during compaction (${decision.reason}); generated summary was discarded`);
2560
+ case "defer":
2561
+ case "fail_closed":
2562
+ throw this._barrierError(barrier);
2563
+ case "commit": {
2564
+ const envelope = createCompactionEnvelope({
2565
+ transaction: begun.transaction,
2566
+ decision,
2567
+ summary: result.summary,
2568
+ summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
2569
+ });
2570
+ const entryId = this.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, this._detailsWithCompactionEnvelope(result.details, envelope), fromExtension);
2571
+ const entry = this.sessionManager.getEntry(entryId);
2572
+ if (!entry || entry.type !== "compaction") {
2573
+ throw new Error("Compaction commit did not produce a compaction entry");
2574
+ }
2575
+ return { entry, envelope };
2576
+ }
2577
+ }
2578
+ });
2579
+ this._recordCompactionCommitForHysteresis();
2580
+ return committed;
2581
+ }
2582
+ _pendingToolResultReserve(settings) {
2583
+ const pendingIds = this.agent.state.pendingToolCalls;
2584
+ const calls = new Map();
2585
+ const streaming = this.agent.state.streamingMessage;
2586
+ const messages = streaming ? [...this.agent.state.messages, streaming] : this.agent.state.messages;
2587
+ for (const message of messages) {
2588
+ if (message.role !== "assistant" || !Array.isArray(message.content))
2589
+ continue;
2590
+ for (const part of message.content) {
2591
+ if (part.type === "toolCall" && pendingIds.has(part.id)) {
2592
+ calls.set(part.id, { name: part.name, args: part.arguments });
2593
+ }
2594
+ }
2595
+ }
2596
+ const counts = { text: 0, image: 0, "large-output": 0 };
2597
+ for (const id of pendingIds) {
2598
+ const call = calls.get(id);
2599
+ const resultClass = call ? pendingToolResultClass(call.name, call.args) : "large-output";
2600
+ counts[resultClass] += 1;
2601
+ }
2602
+ const requests = [];
2603
+ for (const resultClass of ["text", "image", "large-output"]) {
2604
+ if (counts[resultClass] > 0) {
2605
+ requests.push({
2606
+ class: resultClass,
2607
+ count: counts[resultClass],
2608
+ tokensPerResult: PENDING_TOOL_RESULT_TOKENS[resultClass],
2609
+ });
2610
+ }
2611
+ }
2612
+ const configured = settings.reservedToolResultTokens ?? 0;
2613
+ requests.push({ class: "large-output", count: 1, tokensPerResult: configured });
2614
+ return estimateToolResultReserve(requests);
2615
+ }
2616
+ _compactionHysteresisConfig(contextWindow, settings) {
2617
+ const threshold = getCompactionHeadroomThreshold(contextWindow, {
2618
+ ...settings,
2619
+ reservedToolResultTokens: this._pendingToolResultReserve(settings),
2620
+ });
2621
+ if (!threshold)
2622
+ return undefined;
2623
+ const triggerRatio = Math.min(1, Math.max(1 / Math.floor(contextWindow), threshold.triggerTokens / contextWindow));
2624
+ const configuredRearm = settings.rearmRatio ?? triggerRatio * 0.75;
2625
+ const rearmRatio = Math.min(configuredRearm, triggerRatio * 0.999);
2626
+ const emergencyRatio = Math.max(triggerRatio, settings.emergencyRatio ?? 0.98);
2627
+ return createCompactionHysteresisConfig({ rearmRatio, triggerRatio, emergencyRatio });
2628
+ }
2629
+ _runtimeCompactionDecision(contextTokens, contextWindow, settings) {
2630
+ if (!Number.isFinite(contextTokens) || contextTokens < 0)
2631
+ return { compact: false, emergency: false };
2632
+ const config = this._compactionHysteresisConfig(contextWindow, settings);
2633
+ if (!config)
2634
+ return { compact: false, emergency: false };
2635
+ const result = stepCompactionHysteresis({
2636
+ config,
2637
+ state: this._compactionHysteresisState,
2638
+ ratio: Math.min(1, contextTokens / contextWindow),
2639
+ });
2640
+ this._compactionHysteresisState = result.nextState;
2641
+ return { compact: result.action === "compact", emergency: result.reason === "emergency_threshold_reached" };
2642
+ }
2643
+ async _runThresholdCompaction(emergency) {
2644
+ this._thresholdCompactionEmergency = emergency;
2645
+ try {
2646
+ return await this._runAutoCompaction("threshold", false);
2647
+ }
2648
+ finally {
2649
+ this._thresholdCompactionEmergency = false;
2650
+ }
2651
+ }
2652
+ _recordCompactionCommitForHysteresis() {
2653
+ const contextWindow = this.model?.contextWindow ?? 0;
2654
+ const config = this._compactionHysteresisConfig(contextWindow, this.settingsManager.getCompactionSettings());
2655
+ if (!config)
2656
+ return;
2657
+ this._compactionHysteresisState = stepCompactionHysteresis({
2658
+ config,
2659
+ state: this._compactionHysteresisState,
2660
+ ratio: 0,
2661
+ outcome: "commit",
2662
+ }).nextState;
2663
+ }
1663
2664
  _resolveCompactionModel(sessionModel) {
1664
2665
  const configuredModel = this.settingsManager.getCompactionModel();
1665
2666
  const availableModels = this._modelRegistry.getAvailable();
2667
+ let model;
1666
2668
  if (configuredModel) {
1667
- const model = findExactModelReferenceMatch(configuredModel, availableModels);
1668
- if (!model) {
2669
+ const configured = findExactModelReferenceMatch(configuredModel, availableModels);
2670
+ if (!configured) {
1669
2671
  throw new Error(`Configured compaction model "${configuredModel}" is unavailable or unauthenticated.`);
1670
2672
  }
1671
- return model;
2673
+ model = configured;
2674
+ }
2675
+ else {
2676
+ model = resolveCompactionModel(sessionModel, availableModels);
1672
2677
  }
1673
- return resolveCompactionModel(sessionModel, availableModels);
2678
+ this._invalidateContextBudgetCache({
2679
+ type: "compactionModelId",
2680
+ value: this._contextCacheModelId(model),
2681
+ });
2682
+ return model;
1674
2683
  }
1675
2684
  /**
1676
2685
  * Manually compact the session context.
@@ -1682,14 +2691,16 @@ export class AgentSession {
1682
2691
  await this.abort();
1683
2692
  this._compactionAbortController = new AbortController();
1684
2693
  this._emit({ type: "compaction_start", reason: "manual" });
2694
+ let committedCompaction = false;
1685
2695
  try {
1686
2696
  if (!this.model) {
1687
2697
  throw new Error(formatNoModelSelectedMessage());
1688
2698
  }
1689
2699
  const compactionModel = this._resolveCompactionModel(this.model);
1690
2700
  const { apiKey, headers } = await this._getCompactionRequestAuth(compactionModel);
1691
- const pathEntries = this.sessionManager.getBranch();
1692
2701
  const settings = this.settingsManager.getCompactionSettings();
2702
+ const begun = this._beginCompactionTransaction(compactionModel, false);
2703
+ const pathEntries = [...begun.capture.branchEntries];
1693
2704
  const preparation = prepareCompaction(pathEntries, settings);
1694
2705
  if (!preparation) {
1695
2706
  // Check why we can't compact
@@ -1739,25 +2750,22 @@ export class AgentSession {
1739
2750
  if (this._compactionAbortController.signal.aborted) {
1740
2751
  throw new Error("Compaction cancelled");
1741
2752
  }
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
2753
  const compactionResult = {
1756
2754
  summary,
1757
2755
  firstKeptEntryId,
1758
2756
  tokensBefore,
1759
2757
  details,
1760
2758
  };
2759
+ const committed = this._commitCompaction(begun, compactionResult, fromExtension);
2760
+ committedCompaction = true;
2761
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
2762
+ if (this._extensionRunner) {
2763
+ await this._extensionRunner.emit({
2764
+ type: "session_compact",
2765
+ compactionEntry: committed.entry,
2766
+ fromExtension,
2767
+ });
2768
+ }
1761
2769
  this._emit({
1762
2770
  type: "compaction_end",
1763
2771
  reason: "manual",
@@ -1770,6 +2778,18 @@ export class AgentSession {
1770
2778
  catch (error) {
1771
2779
  const message = error instanceof Error ? error.message : String(error);
1772
2780
  const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
2781
+ const stale = /stale|session changed during compaction|already compacted/i.test(message);
2782
+ const timestamp = new Date().toISOString();
2783
+ this._publishTermination(classifySessionTermination({
2784
+ sessionId: this.sessionId,
2785
+ runId: `compaction-${randomUUID()}`,
2786
+ timestamp,
2787
+ source: "observed",
2788
+ message: this._terminationMessage(message, "Manual compaction failed."),
2789
+ cause: { area: "compaction", code: aborted ? "aborted" : stale ? "stale" : "failed" },
2790
+ sideEffects: committedCompaction ? "confirmed" : "none",
2791
+ ...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
2792
+ }));
1773
2793
  this._emit({
1774
2794
  type: "compaction_end",
1775
2795
  reason: "manual",
@@ -1815,8 +2835,9 @@ export class AgentSession {
1815
2835
  return false;
1816
2836
  }
1817
2837
  }
1818
- if (shouldCompact(estimate.tokens, contextWindow, settings)) {
1819
- return await this._runAutoCompaction("threshold", false);
2838
+ const decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);
2839
+ if (decision.compact) {
2840
+ return await this._runThresholdCompaction(decision.emergency);
1820
2841
  }
1821
2842
  return false;
1822
2843
  }
@@ -1897,15 +2918,16 @@ export class AgentSession {
1897
2918
  else {
1898
2919
  contextTokens = calculateContextTokens(assistantMessage.usage);
1899
2920
  }
1900
- if (shouldCompact(contextTokens, contextWindow, settings)) {
1901
- return await this._runAutoCompaction("threshold", false);
2921
+ const decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);
2922
+ if (decision.compact) {
2923
+ return await this._runThresholdCompaction(decision.emergency);
1902
2924
  }
1903
2925
  return false;
1904
2926
  }
1905
2927
  /**
1906
2928
  * Internal: Run auto-compaction with events.
1907
2929
  */
1908
- async _runAutoCompaction(reason, willRetry) {
2930
+ async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
1909
2931
  const settings = this.settingsManager.getCompactionSettings();
1910
2932
  this._emit({ type: "compaction_start", reason });
1911
2933
  this._autoCompactionAbortController = new AbortController();
@@ -1944,7 +2966,8 @@ export class AgentSession {
1944
2966
  else {
1945
2967
  ({ apiKey, headers } = await this._getCompactionRequestAuth(compactionModel));
1946
2968
  }
1947
- const pathEntries = this.sessionManager.getBranch();
2969
+ const begun = this._beginCompactionTransaction(compactionModel, emergency);
2970
+ const pathEntries = [...begun.capture.branchEntries];
1948
2971
  const preparation = prepareCompaction(pathEntries, settings);
1949
2972
  if (!preparation) {
1950
2973
  this._emit({
@@ -2010,25 +3033,21 @@ export class AgentSession {
2010
3033
  });
2011
3034
  return false;
2012
3035
  }
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
3036
  const result = {
2027
3037
  summary,
2028
3038
  firstKeptEntryId,
2029
3039
  tokensBefore,
2030
3040
  details,
2031
3041
  };
3042
+ const committed = this._commitCompaction(begun, result, fromExtension);
3043
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
3044
+ if (this._extensionRunner) {
3045
+ await this._extensionRunner.emit({
3046
+ type: "session_compact",
3047
+ compactionEntry: committed.entry,
3048
+ fromExtension,
3049
+ });
3050
+ }
2032
3051
  const emitWillRetry = compactionEmitWillRetry(willRetry, this.agent.hasQueuedMessages());
2033
3052
  this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry: emitWillRetry });
2034
3053
  if (willRetry) {
@@ -2196,6 +3215,7 @@ export class AgentSession {
2196
3215
  },
2197
3216
  appendEntry: (customType, data) => {
2198
3217
  this.sessionManager.appendCustomEntry(customType, data);
3218
+ this._recordEvidenceReceiptInvalidation(customType);
2199
3219
  },
2200
3220
  setSessionName: (name) => {
2201
3221
  this.setSessionName(name);
@@ -2414,6 +3434,7 @@ export class AgentSession {
2414
3434
  const previousFlagValues = this._extensionRunner.getFlagValues();
2415
3435
  await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
2416
3436
  await this.settingsManager.reload();
3437
+ this._invalidateContextBudgetCache({ type: "settings" });
2417
3438
  resetApiProviders();
2418
3439
  await this._resourceLoader.reload();
2419
3440
  this._buildRuntime({
@@ -2450,13 +3471,68 @@ export class AgentSession {
2450
3471
  const err = message.errorMessage;
2451
3472
  if (this._isNonRetryableProviderLimitError(err))
2452
3473
  return false;
2453
- // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded
2454
- return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(err);
3474
+ // Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
3475
+ return isTransientProviderErrorMessage(err);
2455
3476
  }
2456
3477
  /**
2457
3478
  * Prepare a retryable error for continuation with exponential backoff.
2458
3479
  * @returns true if the caller should continue the agent, false otherwise
2459
3480
  */
3481
+ /** Eject sticky safety model at prompt boundary (session resume / leftover default). */
3482
+ async _ejectStickySafetyModelIfNeeded() {
3483
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3484
+ if (!resilience.blockStickySafetyModels)
3485
+ return;
3486
+ const current = this.model;
3487
+ if (!current || !isStickySafetyModel(current.id, current.provider))
3488
+ return;
3489
+ const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
3490
+ const next = this._modelRegistry.find(c.provider, c.id);
3491
+ return !!next && this._modelRegistry.hasConfiguredAuth(next);
3492
+ });
3493
+ if (!pick) {
3494
+ throw new Error(stickySafetyBlockMessage(current.id, current.provider));
3495
+ }
3496
+ const next = this._modelRegistry.find(pick.provider, pick.id);
3497
+ if (!next) {
3498
+ throw new Error(stickySafetyBlockMessage(current.id, current.provider));
3499
+ }
3500
+ // Bypass setModel sticky check by temporarily allowing via direct state switch path:
3501
+ // setModel itself blocks sticky targets, not sources — safe.
3502
+ await this.setModel(next);
3503
+ }
3504
+ /**
3505
+ * Sticky safety models (e.g. claude-fable-5) often false-positive on coding turns.
3506
+ * Retrying the same model repeats the refusal — switch via provider-resilience chain first.
3507
+ */
3508
+ async _maybeFailoverFromSafetyStop(message) {
3509
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3510
+ if (!resilience.autoFailoverOnSafetyStop)
3511
+ return undefined;
3512
+ if (!isContentSafetyStopMessage(message.errorMessage))
3513
+ return undefined;
3514
+ const current = this.model;
3515
+ const currentId = message.model || current?.id || "";
3516
+ const currentProvider = message.provider || current?.provider || "";
3517
+ if (!isStickySafetyModel(currentId, currentProvider))
3518
+ return undefined;
3519
+ const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
3520
+ const next = this._modelRegistry.find(c.provider, c.id);
3521
+ return !!next && this._modelRegistry.hasConfiguredAuth(next);
3522
+ });
3523
+ if (!pick)
3524
+ return undefined;
3525
+ const next = this._modelRegistry.find(pick.provider, pick.id);
3526
+ if (!next)
3527
+ return undefined;
3528
+ try {
3529
+ await this.setModel(next);
3530
+ return `${next.provider}/${next.id}`;
3531
+ }
3532
+ catch {
3533
+ return undefined;
3534
+ }
3535
+ }
2460
3536
  async _prepareRetry(message) {
2461
3537
  const settings = this.settingsManager.getRetrySettings();
2462
3538
  if (!settings.enabled) {
@@ -2468,13 +3544,21 @@ export class AgentSession {
2468
3544
  this._retryAttempt--;
2469
3545
  return false;
2470
3546
  }
2471
- const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3547
+ // Content/safety stop on Fable: switch model BEFORE delay so the retry is not another Fable refusal.
3548
+ const failoverTo = await this._maybeFailoverFromSafetyStop(message);
3549
+ // Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
3550
+ const delayMs = failoverTo
3551
+ ? Math.min(400, settings.baseDelayMs)
3552
+ : settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3553
+ const errorMessage = failoverTo
3554
+ ? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
3555
+ : message.errorMessage || "Unknown error";
2472
3556
  this._emit({
2473
3557
  type: "auto_retry_start",
2474
3558
  attempt: this._retryAttempt,
2475
3559
  maxAttempts: settings.maxRetries,
2476
3560
  delayMs,
2477
- errorMessage: message.errorMessage || "Unknown error",
3561
+ errorMessage,
2478
3562
  });
2479
3563
  // Remove error message from agent state (keep in session for history)
2480
3564
  const messages = this.agent.state.messages;