apex-code 0.0.1-alpha.8 → 0.0.4

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 (634) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +9 -8
  3. package/dist/cli/args.d.ts +26 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +72 -10
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +2 -1
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/cli/mcp-command.d.ts +9 -0
  11. package/dist/cli/mcp-command.d.ts.map +1 -0
  12. package/dist/cli/mcp-command.js +53 -0
  13. package/dist/cli/mcp-command.js.map +1 -0
  14. package/dist/cli/project-trust.d.ts.map +1 -1
  15. package/dist/cli/project-trust.js +3 -1
  16. package/dist/cli/project-trust.js.map +1 -1
  17. package/dist/cli/startup-ui.d.ts.map +1 -1
  18. package/dist/cli/startup-ui.js +2 -1
  19. package/dist/cli/startup-ui.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +94 -5
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +3 -2
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +45 -18
  26. package/dist/config.js.map +1 -1
  27. package/dist/core/agent-session-services.d.ts +3 -0
  28. package/dist/core/agent-session-services.d.ts.map +1 -1
  29. package/dist/core/agent-session-services.js +1 -0
  30. package/dist/core/agent-session-services.js.map +1 -1
  31. package/dist/core/agent-session.d.ts +183 -15
  32. package/dist/core/agent-session.d.ts.map +1 -1
  33. package/dist/core/agent-session.js +728 -117
  34. package/dist/core/agent-session.js.map +1 -1
  35. package/dist/core/auth-storage.d.ts.map +1 -1
  36. package/dist/core/auth-storage.js +6 -7
  37. package/dist/core/auth-storage.js.map +1 -1
  38. package/dist/core/bash-executor.d.ts.map +1 -1
  39. package/dist/core/bash-executor.js +7 -0
  40. package/dist/core/bash-executor.js.map +1 -1
  41. package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
  42. package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
  43. package/dist/core/checkpoints/git-checkpoints.js +251 -0
  44. package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
  45. package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
  46. package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
  47. package/dist/core/checkpoints/session-checkpoints.js +36 -0
  48. package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
  49. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  50. package/dist/core/compaction/branch-summarization.js +7 -3
  51. package/dist/core/compaction/branch-summarization.js.map +1 -1
  52. package/dist/core/compaction/compaction.d.ts +9 -3
  53. package/dist/core/compaction/compaction.d.ts.map +1 -1
  54. package/dist/core/compaction/compaction.js +59 -36
  55. package/dist/core/compaction/compaction.js.map +1 -1
  56. package/dist/core/defaults.d.ts +1 -0
  57. package/dist/core/defaults.d.ts.map +1 -1
  58. package/dist/core/defaults.js +9 -0
  59. package/dist/core/defaults.js.map +1 -1
  60. package/dist/core/delegation/runtime.d.ts +6 -5
  61. package/dist/core/delegation/runtime.d.ts.map +1 -1
  62. package/dist/core/delegation/runtime.js +4 -3
  63. package/dist/core/delegation/runtime.js.map +1 -1
  64. package/dist/core/durable-state/sqlite.d.ts.map +1 -1
  65. package/dist/core/durable-state/sqlite.js +26 -2
  66. package/dist/core/durable-state/sqlite.js.map +1 -1
  67. package/dist/core/exec.d.ts.map +1 -1
  68. package/dist/core/exec.js +5 -2
  69. package/dist/core/exec.js.map +1 -1
  70. package/dist/core/extensions/index.d.ts +2 -2
  71. package/dist/core/extensions/index.d.ts.map +1 -1
  72. package/dist/core/extensions/index.js +1 -1
  73. package/dist/core/extensions/index.js.map +1 -1
  74. package/dist/core/extensions/loader.d.ts.map +1 -1
  75. package/dist/core/extensions/loader.js +123 -50
  76. package/dist/core/extensions/loader.js.map +1 -1
  77. package/dist/core/extensions/runner.d.ts +5 -0
  78. package/dist/core/extensions/runner.d.ts.map +1 -1
  79. package/dist/core/extensions/runner.js +45 -1
  80. package/dist/core/extensions/runner.js.map +1 -1
  81. package/dist/core/extensions/source-runtime.d.ts +14 -0
  82. package/dist/core/extensions/source-runtime.d.ts.map +1 -0
  83. package/dist/core/extensions/source-runtime.js +56 -0
  84. package/dist/core/extensions/source-runtime.js.map +1 -0
  85. package/dist/core/extensions/types.d.ts +60 -11
  86. package/dist/core/extensions/types.d.ts.map +1 -1
  87. package/dist/core/extensions/types.js +3 -0
  88. package/dist/core/extensions/types.js.map +1 -1
  89. package/dist/core/footer-data-provider.d.ts +1 -1
  90. package/dist/core/footer-data-provider.d.ts.map +1 -1
  91. package/dist/core/footer-data-provider.js +1 -1
  92. package/dist/core/footer-data-provider.js.map +1 -1
  93. package/dist/core/formatter-lifecycle.d.ts +69 -0
  94. package/dist/core/formatter-lifecycle.d.ts.map +1 -0
  95. package/dist/core/formatter-lifecycle.js +360 -0
  96. package/dist/core/formatter-lifecycle.js.map +1 -0
  97. package/dist/core/hooks/command-handler.d.ts +19 -0
  98. package/dist/core/hooks/command-handler.d.ts.map +1 -0
  99. package/dist/core/hooks/command-handler.js +112 -0
  100. package/dist/core/hooks/command-handler.js.map +1 -0
  101. package/dist/core/hooks/http-handler.d.ts +10 -0
  102. package/dist/core/hooks/http-handler.d.ts.map +1 -0
  103. package/dist/core/hooks/http-handler.js +36 -0
  104. package/dist/core/hooks/http-handler.js.map +1 -0
  105. package/dist/core/hooks/loader.d.ts +29 -0
  106. package/dist/core/hooks/loader.d.ts.map +1 -0
  107. package/dist/core/hooks/loader.js +139 -0
  108. package/dist/core/hooks/loader.js.map +1 -0
  109. package/dist/core/hooks/runtime.d.ts +13 -0
  110. package/dist/core/hooks/runtime.d.ts.map +1 -0
  111. package/dist/core/hooks/runtime.js +63 -0
  112. package/dist/core/hooks/runtime.js.map +1 -0
  113. package/dist/core/hooks/types.d.ts +83 -0
  114. package/dist/core/hooks/types.d.ts.map +1 -0
  115. package/dist/core/hooks/types.js +24 -0
  116. package/dist/core/hooks/types.js.map +1 -0
  117. package/dist/core/keybindings.d.ts +20 -19
  118. package/dist/core/keybindings.d.ts.map +1 -1
  119. package/dist/core/keybindings.js +26 -5
  120. package/dist/core/keybindings.js.map +1 -1
  121. package/dist/core/mcp/config.d.ts +16 -0
  122. package/dist/core/mcp/config.d.ts.map +1 -0
  123. package/dist/core/mcp/config.js +166 -0
  124. package/dist/core/mcp/config.js.map +1 -0
  125. package/dist/core/mcp/connector.d.ts +35 -0
  126. package/dist/core/mcp/connector.d.ts.map +1 -0
  127. package/dist/core/mcp/connector.js +105 -0
  128. package/dist/core/mcp/connector.js.map +1 -0
  129. package/dist/core/mcp/contract.d.ts +19 -0
  130. package/dist/core/mcp/contract.d.ts.map +1 -0
  131. package/dist/core/mcp/contract.js +89 -0
  132. package/dist/core/mcp/contract.js.map +1 -0
  133. package/dist/core/mcp/mcp-tool.d.ts +32 -0
  134. package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
  135. package/dist/core/mcp/mcp-tool.js +171 -0
  136. package/dist/core/mcp/mcp-tool.js.map +1 -0
  137. package/dist/core/mcp/metadata-cache.d.ts +29 -0
  138. package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
  139. package/dist/core/mcp/metadata-cache.js +99 -0
  140. package/dist/core/mcp/metadata-cache.js.map +1 -0
  141. package/dist/core/mcp/oauth/authorize.d.ts +21 -0
  142. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
  143. package/dist/core/mcp/oauth/authorize.js +32 -0
  144. package/dist/core/mcp/oauth/authorize.js.map +1 -0
  145. package/dist/core/mcp/oauth/discover.d.ts +27 -0
  146. package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
  147. package/dist/core/mcp/oauth/discover.js +116 -0
  148. package/dist/core/mcp/oauth/discover.js.map +1 -0
  149. package/dist/core/mcp/oauth/flow.d.ts +31 -0
  150. package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
  151. package/dist/core/mcp/oauth/flow.js +175 -0
  152. package/dist/core/mcp/oauth/flow.js.map +1 -0
  153. package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
  154. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
  155. package/dist/core/mcp/oauth/mcp-token.js +129 -0
  156. package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
  157. package/dist/core/mcp/oauth/pkce.d.ts +12 -0
  158. package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
  159. package/dist/core/mcp/oauth/pkce.js +11 -0
  160. package/dist/core/mcp/oauth/pkce.js.map +1 -0
  161. package/dist/core/mcp/oauth/register.d.ts +16 -0
  162. package/dist/core/mcp/oauth/register.d.ts.map +1 -0
  163. package/dist/core/mcp/oauth/register.js +42 -0
  164. package/dist/core/mcp/oauth/register.js.map +1 -0
  165. package/dist/core/mcp/oauth/token-client.d.ts +33 -0
  166. package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
  167. package/dist/core/mcp/oauth/token-client.js +65 -0
  168. package/dist/core/mcp/oauth/token-client.js.map +1 -0
  169. package/dist/core/mcp/runtime.d.ts +20 -0
  170. package/dist/core/mcp/runtime.d.ts.map +1 -0
  171. package/dist/core/mcp/runtime.js +37 -0
  172. package/dist/core/mcp/runtime.js.map +1 -0
  173. package/dist/core/mcp/schema.d.ts +22 -0
  174. package/dist/core/mcp/schema.d.ts.map +1 -0
  175. package/dist/core/mcp/schema.js +20 -0
  176. package/dist/core/mcp/schema.js.map +1 -0
  177. package/dist/core/mcp/server-manager.d.ts +43 -0
  178. package/dist/core/mcp/server-manager.d.ts.map +1 -0
  179. package/dist/core/mcp/server-manager.js +142 -0
  180. package/dist/core/mcp/server-manager.js.map +1 -0
  181. package/dist/core/mcp/types.d.ts +100 -0
  182. package/dist/core/mcp/types.d.ts.map +1 -0
  183. package/dist/core/mcp/types.js +8 -0
  184. package/dist/core/mcp/types.js.map +1 -0
  185. package/dist/core/model-config.d.ts +5 -0
  186. package/dist/core/model-config.d.ts.map +1 -1
  187. package/dist/core/model-config.js +3 -1
  188. package/dist/core/model-config.js.map +1 -1
  189. package/dist/core/model-resolver.d.ts +1 -0
  190. package/dist/core/model-resolver.d.ts.map +1 -1
  191. package/dist/core/model-resolver.js +14 -8
  192. package/dist/core/model-resolver.js.map +1 -1
  193. package/dist/core/models-store.d.ts.map +1 -1
  194. package/dist/core/models-store.js +2 -1
  195. package/dist/core/models-store.js.map +1 -1
  196. package/dist/core/package-manager.d.ts.map +1 -1
  197. package/dist/core/package-manager.js +22 -14
  198. package/dist/core/package-manager.js.map +1 -1
  199. package/dist/core/permissions/gate.d.ts.map +1 -1
  200. package/dist/core/permissions/gate.js +45 -4
  201. package/dist/core/permissions/gate.js.map +1 -1
  202. package/dist/core/permissions/operations.d.ts +20 -0
  203. package/dist/core/permissions/operations.d.ts.map +1 -0
  204. package/dist/core/permissions/operations.js +9 -0
  205. package/dist/core/permissions/operations.js.map +1 -0
  206. package/dist/core/permissions/policy-command.d.ts +42 -0
  207. package/dist/core/permissions/policy-command.d.ts.map +1 -0
  208. package/dist/core/permissions/policy-command.js +44 -0
  209. package/dist/core/permissions/policy-command.js.map +1 -0
  210. package/dist/core/permissions/responder.d.ts +49 -2
  211. package/dist/core/permissions/responder.d.ts.map +1 -1
  212. package/dist/core/permissions/responder.js +20 -8
  213. package/dist/core/permissions/responder.js.map +1 -1
  214. package/dist/core/permissions/rules.d.ts.map +1 -1
  215. package/dist/core/permissions/rules.js +11 -1
  216. package/dist/core/permissions/rules.js.map +1 -1
  217. package/dist/core/permissions/store.d.ts +18 -0
  218. package/dist/core/permissions/store.d.ts.map +1 -1
  219. package/dist/core/permissions/store.js +89 -8
  220. package/dist/core/permissions/store.js.map +1 -1
  221. package/dist/core/pi-manifest.d.ts.map +1 -1
  222. package/dist/core/pi-manifest.js +2 -1
  223. package/dist/core/pi-manifest.js.map +1 -1
  224. package/dist/core/policy-executor.d.ts +47 -0
  225. package/dist/core/policy-executor.d.ts.map +1 -0
  226. package/dist/core/policy-executor.js +228 -0
  227. package/dist/core/policy-executor.js.map +1 -0
  228. package/dist/core/policy-loader.d.ts +72 -0
  229. package/dist/core/policy-loader.d.ts.map +1 -0
  230. package/dist/core/policy-loader.js +195 -0
  231. package/dist/core/policy-loader.js.map +1 -0
  232. package/dist/core/project-trust.d.ts +1 -1
  233. package/dist/core/project-trust.d.ts.map +1 -1
  234. package/dist/core/project-trust.js.map +1 -1
  235. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  236. package/dist/core/remote-catalog-provider.js +2 -1
  237. package/dist/core/remote-catalog-provider.js.map +1 -1
  238. package/dist/core/resource-loader.d.ts.map +1 -1
  239. package/dist/core/resource-loader.js +3 -2
  240. package/dist/core/resource-loader.js.map +1 -1
  241. package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
  242. package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
  243. package/dist/core/sandbox/bwrap-arguments.js +136 -0
  244. package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
  245. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  246. package/dist/core/sandbox/child-entry.js +3 -1
  247. package/dist/core/sandbox/child-entry.js.map +1 -1
  248. package/dist/core/sandbox/cli-launch.d.ts +84 -4
  249. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  250. package/dist/core/sandbox/cli-launch.js +115 -16
  251. package/dist/core/sandbox/cli-launch.js.map +1 -1
  252. package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
  253. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  254. package/dist/core/sandbox/cli-supervisor.js +50 -2
  255. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  256. package/dist/core/sandbox/full-access.d.ts +28 -0
  257. package/dist/core/sandbox/full-access.d.ts.map +1 -0
  258. package/dist/core/sandbox/full-access.js +63 -0
  259. package/dist/core/sandbox/full-access.js.map +1 -0
  260. package/dist/core/sandbox/git-identity.d.ts +57 -0
  261. package/dist/core/sandbox/git-identity.d.ts.map +1 -0
  262. package/dist/core/sandbox/git-identity.js +78 -0
  263. package/dist/core/sandbox/git-identity.js.map +1 -0
  264. package/dist/core/sandbox/host-approval.d.ts +42 -0
  265. package/dist/core/sandbox/host-approval.d.ts.map +1 -0
  266. package/dist/core/sandbox/host-approval.js +99 -0
  267. package/dist/core/sandbox/host-approval.js.map +1 -0
  268. package/dist/core/sandbox/linux-backend.d.ts +14 -0
  269. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  270. package/dist/core/sandbox/linux-backend.js +158 -80
  271. package/dist/core/sandbox/linux-backend.js.map +1 -1
  272. package/dist/core/sandbox/macos-backend.d.ts +5 -0
  273. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  274. package/dist/core/sandbox/macos-backend.js +164 -5
  275. package/dist/core/sandbox/macos-backend.js.map +1 -1
  276. package/dist/core/sandbox/network-proxy.d.ts +19 -0
  277. package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
  278. package/dist/core/sandbox/network-proxy.js +93 -19
  279. package/dist/core/sandbox/network-proxy.js.map +1 -1
  280. package/dist/core/sandbox/policy.d.ts +8 -0
  281. package/dist/core/sandbox/policy.d.ts.map +1 -1
  282. package/dist/core/sandbox/policy.js +29 -12
  283. package/dist/core/sandbox/policy.js.map +1 -1
  284. package/dist/core/sandbox/profiles.d.ts +25 -0
  285. package/dist/core/sandbox/profiles.d.ts.map +1 -0
  286. package/dist/core/sandbox/profiles.js +23 -0
  287. package/dist/core/sandbox/profiles.js.map +1 -0
  288. package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
  289. package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
  290. package/dist/core/sandbox/rpc/command-client.js +86 -0
  291. package/dist/core/sandbox/rpc/command-client.js.map +1 -0
  292. package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
  293. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
  294. package/dist/core/sandbox/rpc/command-proxy.js +120 -0
  295. package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
  296. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
  297. package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
  298. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
  299. package/dist/core/sandbox/rpc/framing.d.ts +35 -0
  300. package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
  301. package/dist/core/sandbox/rpc/framing.js +114 -0
  302. package/dist/core/sandbox/rpc/framing.js.map +1 -0
  303. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
  304. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
  305. package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
  306. package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
  307. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
  308. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
  309. package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
  310. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
  311. package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
  312. package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
  313. package/dist/core/sandbox/supervisor-temp.js +21 -0
  314. package/dist/core/sandbox/supervisor-temp.js.map +1 -0
  315. package/dist/core/sandbox/supervisor.d.ts +12 -1
  316. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  317. package/dist/core/sandbox/supervisor.js +15 -1
  318. package/dist/core/sandbox/supervisor.js.map +1 -1
  319. package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
  320. package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
  321. package/dist/core/sandbox/terminal-handoff.js +221 -0
  322. package/dist/core/sandbox/terminal-handoff.js.map +1 -0
  323. package/dist/core/sandbox/terminal-size.d.ts +13 -0
  324. package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
  325. package/dist/core/sandbox/terminal-size.js +23 -2
  326. package/dist/core/sandbox/terminal-size.js.map +1 -1
  327. package/dist/core/sdk.d.ts +24 -2
  328. package/dist/core/sdk.d.ts.map +1 -1
  329. package/dist/core/sdk.js +63 -4
  330. package/dist/core/sdk.js.map +1 -1
  331. package/dist/core/session-export.d.ts +4 -0
  332. package/dist/core/session-export.d.ts.map +1 -0
  333. package/dist/core/session-export.js +32 -0
  334. package/dist/core/session-export.js.map +1 -0
  335. package/dist/core/session-manager.d.ts +13 -1
  336. package/dist/core/session-manager.d.ts.map +1 -1
  337. package/dist/core/session-manager.js +34 -6
  338. package/dist/core/session-manager.js.map +1 -1
  339. package/dist/core/settings-diagnostics.d.ts +9 -0
  340. package/dist/core/settings-diagnostics.d.ts.map +1 -0
  341. package/dist/core/settings-diagnostics.js +21 -0
  342. package/dist/core/settings-diagnostics.js.map +1 -0
  343. package/dist/core/settings-manager.d.ts +172 -1
  344. package/dist/core/settings-manager.d.ts.map +1 -1
  345. package/dist/core/settings-manager.js +160 -11
  346. package/dist/core/settings-manager.js.map +1 -1
  347. package/dist/core/skills.d.ts.map +1 -1
  348. package/dist/core/skills.js +51 -33
  349. package/dist/core/skills.js.map +1 -1
  350. package/dist/core/slash-commands.d.ts.map +1 -1
  351. package/dist/core/slash-commands.js +4 -1
  352. package/dist/core/slash-commands.js.map +1 -1
  353. package/dist/core/system-prompt.d.ts.map +1 -1
  354. package/dist/core/system-prompt.js +8 -2
  355. package/dist/core/system-prompt.js.map +1 -1
  356. package/dist/core/tools/background-shell.d.ts +39 -0
  357. package/dist/core/tools/background-shell.d.ts.map +1 -0
  358. package/dist/core/tools/background-shell.js +81 -0
  359. package/dist/core/tools/background-shell.js.map +1 -0
  360. package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
  361. package/dist/core/tools/bash-command-segments.js +2 -0
  362. package/dist/core/tools/bash-command-segments.js.map +1 -1
  363. package/dist/core/tools/bash.d.ts +44 -16
  364. package/dist/core/tools/bash.d.ts.map +1 -1
  365. package/dist/core/tools/bash.js +264 -32
  366. package/dist/core/tools/bash.js.map +1 -1
  367. package/dist/core/tools/contract-snapshot.d.ts +58 -0
  368. package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
  369. package/dist/core/tools/contract-snapshot.js +51 -0
  370. package/dist/core/tools/contract-snapshot.js.map +1 -0
  371. package/dist/core/tools/contract.d.ts +53 -5
  372. package/dist/core/tools/contract.d.ts.map +1 -1
  373. package/dist/core/tools/contract.js +22 -2
  374. package/dist/core/tools/contract.js.map +1 -1
  375. package/dist/core/tools/delegate.d.ts +6 -4
  376. package/dist/core/tools/delegate.d.ts.map +1 -1
  377. package/dist/core/tools/delegate.js +21 -11
  378. package/dist/core/tools/delegate.js.map +1 -1
  379. package/dist/core/tools/edit-diff.d.ts +48 -5
  380. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  381. package/dist/core/tools/edit-diff.js +217 -8
  382. package/dist/core/tools/edit-diff.js.map +1 -1
  383. package/dist/core/tools/edit.d.ts +8 -0
  384. package/dist/core/tools/edit.d.ts.map +1 -1
  385. package/dist/core/tools/edit.js +72 -10
  386. package/dist/core/tools/edit.js.map +1 -1
  387. package/dist/core/tools/find.d.ts.map +1 -1
  388. package/dist/core/tools/find.js +5 -2
  389. package/dist/core/tools/find.js.map +1 -1
  390. package/dist/core/tools/grep.d.ts.map +1 -1
  391. package/dist/core/tools/grep.js +5 -2
  392. package/dist/core/tools/grep.js.map +1 -1
  393. package/dist/core/tools/index.d.ts +14 -2
  394. package/dist/core/tools/index.d.ts.map +1 -1
  395. package/dist/core/tools/index.js +12 -0
  396. package/dist/core/tools/index.js.map +1 -1
  397. package/dist/core/tools/ls.d.ts.map +1 -1
  398. package/dist/core/tools/ls.js +5 -2
  399. package/dist/core/tools/ls.js.map +1 -1
  400. package/dist/core/tools/lsp.d.ts +19 -1
  401. package/dist/core/tools/lsp.d.ts.map +1 -1
  402. package/dist/core/tools/lsp.js +163 -10
  403. package/dist/core/tools/lsp.js.map +1 -1
  404. package/dist/core/tools/path-permission.d.ts +3 -0
  405. package/dist/core/tools/path-permission.d.ts.map +1 -1
  406. package/dist/core/tools/path-permission.js +16 -4
  407. package/dist/core/tools/path-permission.js.map +1 -1
  408. package/dist/core/tools/path-utils.d.ts +27 -2
  409. package/dist/core/tools/path-utils.d.ts.map +1 -1
  410. package/dist/core/tools/path-utils.js +221 -4
  411. package/dist/core/tools/path-utils.js.map +1 -1
  412. package/dist/core/tools/powershell.d.ts +16 -0
  413. package/dist/core/tools/powershell.d.ts.map +1 -0
  414. package/dist/core/tools/powershell.js +39 -0
  415. package/dist/core/tools/powershell.js.map +1 -0
  416. package/dist/core/tools/read.d.ts.map +1 -1
  417. package/dist/core/tools/read.js +20 -11
  418. package/dist/core/tools/read.js.map +1 -1
  419. package/dist/core/tools/test.d.ts +54 -9
  420. package/dist/core/tools/test.d.ts.map +1 -1
  421. package/dist/core/tools/test.js +239 -19
  422. package/dist/core/tools/test.js.map +1 -1
  423. package/dist/core/tools/write.d.ts +7 -0
  424. package/dist/core/tools/write.d.ts.map +1 -1
  425. package/dist/core/tools/write.js +64 -9
  426. package/dist/core/tools/write.js.map +1 -1
  427. package/dist/core/trust-manager.d.ts.map +1 -1
  428. package/dist/core/trust-manager.js +2 -1
  429. package/dist/core/trust-manager.js.map +1 -1
  430. package/dist/core/verification-lifecycle.d.ts +77 -0
  431. package/dist/core/verification-lifecycle.d.ts.map +1 -0
  432. package/dist/core/verification-lifecycle.js +146 -0
  433. package/dist/core/verification-lifecycle.js.map +1 -0
  434. package/dist/core/workspace/artifacts.d.ts +45 -0
  435. package/dist/core/workspace/artifacts.d.ts.map +1 -0
  436. package/dist/core/workspace/artifacts.js +165 -0
  437. package/dist/core/workspace/artifacts.js.map +1 -0
  438. package/dist/core/workspace/comparison.d.ts +27 -0
  439. package/dist/core/workspace/comparison.d.ts.map +1 -0
  440. package/dist/core/workspace/comparison.js +94 -0
  441. package/dist/core/workspace/comparison.js.map +1 -0
  442. package/dist/core/workspace/git-observer.d.ts +48 -0
  443. package/dist/core/workspace/git-observer.d.ts.map +1 -0
  444. package/dist/core/workspace/git-observer.js +346 -0
  445. package/dist/core/workspace/git-observer.js.map +1 -0
  446. package/dist/core/workspace/projection.d.ts +3 -0
  447. package/dist/core/workspace/projection.d.ts.map +1 -0
  448. package/dist/core/workspace/projection.js +56 -0
  449. package/dist/core/workspace/projection.js.map +1 -0
  450. package/dist/core/workspace/state.d.ts +133 -0
  451. package/dist/core/workspace/state.d.ts.map +1 -0
  452. package/dist/core/workspace/state.js +96 -0
  453. package/dist/core/workspace/state.js.map +1 -0
  454. package/dist/extensions/llama/client.d.ts +6 -0
  455. package/dist/extensions/llama/client.d.ts.map +1 -1
  456. package/dist/extensions/llama/client.js +7 -0
  457. package/dist/extensions/llama/client.js.map +1 -1
  458. package/dist/extensions/llama/index.d.ts.map +1 -1
  459. package/dist/extensions/llama/index.js +2 -0
  460. package/dist/extensions/llama/index.js.map +1 -1
  461. package/dist/extensions/llama/provider.d.ts +3 -1
  462. package/dist/extensions/llama/provider.d.ts.map +1 -1
  463. package/dist/extensions/llama/provider.js +29 -4
  464. package/dist/extensions/llama/provider.js.map +1 -1
  465. package/dist/index.d.ts +8 -5
  466. package/dist/index.d.ts.map +1 -1
  467. package/dist/index.js +7 -4
  468. package/dist/index.js.map +1 -1
  469. package/dist/main.d.ts +6 -0
  470. package/dist/main.d.ts.map +1 -1
  471. package/dist/main.js +58 -23
  472. package/dist/main.js.map +1 -1
  473. package/dist/migrations.d.ts.map +1 -1
  474. package/dist/migrations.js +4 -3
  475. package/dist/migrations.js.map +1 -1
  476. package/dist/modes/acp/server.d.ts +55 -0
  477. package/dist/modes/acp/server.d.ts.map +1 -0
  478. package/dist/modes/acp/server.js +244 -0
  479. package/dist/modes/acp/server.js.map +1 -0
  480. package/dist/modes/acp/translate.d.ts +37 -0
  481. package/dist/modes/acp/translate.d.ts.map +1 -0
  482. package/dist/modes/acp/translate.js +50 -0
  483. package/dist/modes/acp/translate.js.map +1 -0
  484. package/dist/modes/index.d.ts +1 -0
  485. package/dist/modes/index.d.ts.map +1 -1
  486. package/dist/modes/index.js +1 -0
  487. package/dist/modes/index.js.map +1 -1
  488. package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
  489. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  490. package/dist/modes/interactive/components/assistant-message.js +182 -23
  491. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  492. package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
  493. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  494. package/dist/modes/interactive/components/bash-execution.js +20 -0
  495. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  496. package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
  497. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  498. package/dist/modes/interactive/components/custom-editor.js +20 -9
  499. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  500. package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
  501. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  502. package/dist/modes/interactive/components/extension-selector.js +7 -3
  503. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  504. package/dist/modes/interactive/components/footer.d.ts +19 -0
  505. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  506. package/dist/modes/interactive/components/footer.js +70 -10
  507. package/dist/modes/interactive/components/footer.js.map +1 -1
  508. package/dist/modes/interactive/components/model-selector.d.ts +10 -3
  509. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  510. package/dist/modes/interactive/components/model-selector.js +56 -14
  511. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  512. package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
  513. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
  514. package/dist/modes/interactive/components/permission-preview.js +33 -0
  515. package/dist/modes/interactive/components/permission-preview.js.map +1 -0
  516. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  517. package/dist/modes/interactive/components/session-selector.js +2 -2
  518. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  519. package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
  520. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  521. package/dist/modes/interactive/components/settings-selector.js +128 -56
  522. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  523. package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
  524. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
  525. package/dist/modes/interactive/components/settings-submenu.js +165 -0
  526. package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
  527. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  528. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  529. package/dist/modes/interactive/components/thinking-selector.js +74 -17
  530. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  531. package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
  532. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  533. package/dist/modes/interactive/components/tool-execution.js +64 -10
  534. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  535. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  536. package/dist/modes/interactive/external-editor.js +2 -1
  537. package/dist/modes/interactive/external-editor.js.map +1 -1
  538. package/dist/modes/interactive/interactive-mode.d.ts +45 -0
  539. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  540. package/dist/modes/interactive/interactive-mode.js +395 -43
  541. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  542. package/dist/modes/interactive/theme/theme.d.ts +12 -0
  543. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  544. package/dist/modes/interactive/theme/theme.js +20 -9
  545. package/dist/modes/interactive/theme/theme.js.map +1 -1
  546. package/dist/modes/json-event.d.ts +16 -13
  547. package/dist/modes/json-event.d.ts.map +1 -1
  548. package/dist/modes/json-event.js +20 -6
  549. package/dist/modes/json-event.js.map +1 -1
  550. package/dist/modes/print-mode.d.ts.map +1 -1
  551. package/dist/modes/print-mode.js +27 -12
  552. package/dist/modes/print-mode.js.map +1 -1
  553. package/dist/modes/rpc/rpc-client.d.ts +7 -0
  554. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  555. package/dist/modes/rpc/rpc-client.js +7 -0
  556. package/dist/modes/rpc/rpc-client.js.map +1 -1
  557. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  558. package/dist/modes/rpc/rpc-mode.js +3 -0
  559. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  560. package/dist/modes/rpc/rpc-types.d.ts +12 -0
  561. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  562. package/dist/modes/rpc/rpc-types.js.map +1 -1
  563. package/dist/package-manager-cli.d.ts +1 -0
  564. package/dist/package-manager-cli.d.ts.map +1 -1
  565. package/dist/package-manager-cli.js +196 -10
  566. package/dist/package-manager-cli.js.map +1 -1
  567. package/dist/utils/frontmatter.d.ts.map +1 -1
  568. package/dist/utils/frontmatter.js +2 -1
  569. package/dist/utils/frontmatter.js.map +1 -1
  570. package/dist/utils/management-http.d.ts +5 -3
  571. package/dist/utils/management-http.d.ts.map +1 -1
  572. package/dist/utils/management-http.js +14 -10
  573. package/dist/utils/management-http.js.map +1 -1
  574. package/dist/utils/shell.d.ts +3 -0
  575. package/dist/utils/shell.d.ts.map +1 -1
  576. package/dist/utils/shell.js +23 -9
  577. package/dist/utils/shell.js.map +1 -1
  578. package/dist/utils/syntax-highlight.d.ts +1 -0
  579. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  580. package/dist/utils/syntax-highlight.js +60 -1
  581. package/dist/utils/syntax-highlight.js.map +1 -1
  582. package/dist/utils/text.d.ts +8 -0
  583. package/dist/utils/text.d.ts.map +1 -0
  584. package/dist/utils/text.js +9 -0
  585. package/dist/utils/text.js.map +1 -0
  586. package/dist/utils/version-check.d.ts.map +1 -1
  587. package/dist/utils/version-check.js +6 -4
  588. package/dist/utils/version-check.js.map +1 -1
  589. package/docs/compaction.md +19 -0
  590. package/docs/custom-provider.md +12 -9
  591. package/docs/environment-variables.md +1 -1
  592. package/docs/extensions.md +35 -7
  593. package/docs/json.md +7 -2
  594. package/docs/keybindings.md +9 -9
  595. package/docs/llama-cpp.md +2 -0
  596. package/docs/models.md +8 -2
  597. package/docs/packages.md +2 -2
  598. package/docs/quickstart.md +1 -1
  599. package/docs/rpc.md +32 -3
  600. package/docs/sdk.md +32 -2
  601. package/docs/settings.md +23 -7
  602. package/docs/skills.md +4 -3
  603. package/docs/terminal-setup.md +23 -7
  604. package/docs/usage.md +10 -5
  605. package/docs/windows.md +24 -2
  606. package/examples/extensions/border-status-editor.ts +1 -1
  607. package/examples/extensions/built-in-tool-renderer.ts +8 -4
  608. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  609. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  610. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  611. package/examples/extensions/git-checkpoint.ts +24 -36
  612. package/examples/extensions/gondolin/package-lock.json +2 -2
  613. package/examples/extensions/gondolin/package.json +1 -1
  614. package/examples/extensions/minimal-mode.ts +2 -2
  615. package/examples/extensions/notify.ts +3 -1
  616. package/examples/extensions/permission-gate.ts +1 -1
  617. package/examples/extensions/plan-mode/index.ts +1 -1
  618. package/examples/extensions/rpc-demo.ts +1 -1
  619. package/examples/extensions/sandbox/package-lock.json +2 -2
  620. package/examples/extensions/sandbox/package.json +1 -1
  621. package/examples/extensions/subagent/README.md +1 -1
  622. package/examples/extensions/subagent/index.ts +6 -1
  623. package/examples/extensions/titlebar-spinner.ts +1 -1
  624. package/examples/extensions/with-deps/package-lock.json +2 -2
  625. package/examples/extensions/with-deps/package.json +1 -1
  626. package/examples/rpc-extension-ui.ts +1 -1
  627. package/examples/sdk/06-extensions.ts +1 -1
  628. package/examples/sdk/README.md +1 -1
  629. package/npm-shrinkwrap.json +439 -405
  630. package/package.json +17 -17
  631. package/dist/server/create-harness.d.ts +0 -26
  632. package/dist/server/create-harness.d.ts.map +0 -1
  633. package/dist/server/create-harness.js +0 -107
  634. package/dist/server/create-harness.js.map +0 -1
@@ -12,41 +12,51 @@
12
12
  *
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
- import { basename, dirname } from "node:path";
15
+ import { readFileSync } from "node:fs";
16
+ import { basename, dirname, isAbsolute, relative } from "node:path";
17
17
  import { contentText } from "@earendil-works/pi-ai";
18
18
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, } from "@earendil-works/pi-ai/compat";
19
19
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
20
20
  import { stripFrontmatter } from "../utils/frontmatter.js";
21
- import { resolvePath } from "../utils/paths.js";
22
21
  import { sleep } from "../utils/sleep.js";
23
22
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
24
23
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
25
24
  import { executeBashWithOperations } from "./bash-executor.js";
25
+ import { createSessionCheckpoints } from "./checkpoints/session-checkpoints.js";
26
26
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
27
27
  import { evictionBudget, installContextPipeline, isDefaultStreamFunction } from "./context/pipeline.js";
28
- import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
28
+ import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
29
29
  import { SessionEvidenceSink } from "./evidence.js";
30
30
  import { exportSessionToHtml } from "./export-html/index.js";
31
31
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
32
32
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
33
33
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
34
+ import { runFormatterCommand } from "./formatter-lifecycle.js";
34
35
  import { ModelRegistry } from "./model-registry.js";
35
36
  import { evaluateToolCall } from "./permissions/gate.js";
37
+ import { authorizeConfiguredCommand } from "./permissions/policy-command.js";
36
38
  import { createInteractiveResponder } from "./permissions/responder.js";
37
39
  import { resolveEffectiveModeWithOrigin } from "./permissions/startup.js";
40
+ import { loadPolicyConfiguration } from "./policy-loader.js";
38
41
  import { expandPromptTemplate } from "./prompt-templates.js";
39
- import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, TODO_CUSTOM_ENTRY_TYPE, } from "./session-manager.js";
42
+ import { exportSessionToJsonl } from "./session-export.js";
43
+ import { getLatestCompactionEntry, TODO_CUSTOM_ENTRY_TYPE } from "./session-manager.js";
40
44
  import { slugifySkillCommandName } from "./skills.js";
41
45
  import { createSyntheticSourceInfo } from "./source-info.js";
42
46
  import { buildSystemPrompt } from "./system-prompt.js";
43
47
  import { createLocalBashOperations } from "./tools/bash.js";
48
+ import { buildToolContractSnapshot, isUnclassifiedTool, } from "./tools/contract-snapshot.js";
44
49
  import { createAllToolDefinitions } from "./tools/index.js";
45
50
  import { createSkillSearchToolDefinition } from "./tools/skill-search.js";
46
51
  import { createTodoWriteToolDefinition } from "./tools/todo-write.js";
47
52
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
48
53
  import { createToolSchemaToolDefinition } from "./tools/tool-schema.js";
49
54
  import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
55
+ import { VerificationTracker, } from "./verification-lifecycle.js";
56
+ import { buildWorkspaceComparison, compareWorkspaceObservations } from "./workspace/comparison.js";
57
+ import { observeWorkspaceGit } from "./workspace/git-observer.js";
58
+ import { formatWorkspaceProjection } from "./workspace/projection.js";
59
+ import { appendWorkspaceComparison, appendWorkspaceObservation, findLatestWorkspaceObservation, hasWorkspaceObservationAwaitingComparison, } from "./workspace/state.js";
50
60
  /**
51
61
  * Parse a skill block from message text.
52
62
  * Returns null if the text doesn't contain a skill block.
@@ -77,14 +87,6 @@ function estimateMessagesTokens(messages) {
77
87
  }
78
88
  return tokens;
79
89
  }
80
- // ============================================================================
81
- // Constants
82
- // ============================================================================
83
- /** Standard thinking levels */
84
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
85
- // ============================================================================
86
- // AgentSession Class
87
- // ============================================================================
88
90
  export class AgentSession {
89
91
  agent;
90
92
  sessionManager;
@@ -102,10 +104,13 @@ export class AgentSession {
102
104
  _followUpMessages = [];
103
105
  /** Messages queued to be included with the next user prompt as context ("asides"). */
104
106
  _pendingNextTurnMessages = [];
107
+ /** Context-only custom messages queued during a run, flushed once the current turn's tool results are in. */
108
+ _pendingCustomMessages = [];
105
109
  // Compaction state
106
110
  _compactionAbortController = undefined;
107
111
  _autoCompactionAbortController = undefined;
108
112
  _overflowRecoveryAttempted = false;
113
+ _midRunCompactionNeeded = false;
109
114
  // Branch summarization state
110
115
  _branchSummaryAbortController = undefined;
111
116
  // Retry state
@@ -121,6 +126,21 @@ export class AgentSession {
121
126
  _customTools;
122
127
  _baseToolDefinitions = new Map();
123
128
  _cwd;
129
+ /**
130
+ * Workspace observation adapter for compaction capture (WS.4). Injectable
131
+ * so tests can force capture outcomes without a real repository.
132
+ */
133
+ _workspaceObserver = observeWorkspaceGit;
134
+ /**
135
+ * WS.5: one workspace comparison is due at the next model-turn boundary.
136
+ * Set after a compaction stored an observation, or on construction when
137
+ * the session's latest observation has no later comparison (resume).
138
+ */
139
+ _workspaceComparePending = false;
140
+ /** VF.4: verification lifecycle state, built lazily from policy settings. */
141
+ _verificationTracker;
142
+ /** The workspace observation a verification result described, for staleness. */
143
+ _verificationBaseline;
124
144
  _extensionRunnerRef;
125
145
  _initialActiveToolNames;
126
146
  _allowedToolNames;
@@ -131,6 +151,19 @@ export class AgentSession {
131
151
  _evidenceSink;
132
152
  _diagnosticsOperations;
133
153
  _lspOperations;
154
+ _mcpRuntime;
155
+ /** Configured MCP subsystem, or undefined when no server is configured. */
156
+ get mcpRuntime() {
157
+ return this._mcpRuntime;
158
+ }
159
+ _checkpoints;
160
+ _hookRuntime;
161
+ _backgroundShellRegistry;
162
+ _permissionResponderFactory;
163
+ /** Worktree checkpoints for this session. Inert unless `checkpoints.enabled` is set. */
164
+ get checkpoints() {
165
+ return this._checkpoints;
166
+ }
134
167
  _webSearchOperations;
135
168
  _extensionUIContext;
136
169
  _extensionMode = "print";
@@ -169,13 +202,33 @@ export class AgentSession {
169
202
  this._evidenceSink = config.evidenceSink ?? new SessionEvidenceSink(this.sessionManager);
170
203
  this._diagnosticsOperations = config.diagnosticsOperations;
171
204
  this._lspOperations = config.lspOperations;
205
+ this._mcpRuntime = config.mcpRuntime;
172
206
  this._webSearchOperations = config.webSearchOperations;
207
+ this._checkpoints = createSessionCheckpoints({
208
+ workspace: this._cwd,
209
+ sessionId: this.sessionManager.getSessionId(),
210
+ settings: config.checkpointSettings,
211
+ });
212
+ this._hookRuntime = config.hookRuntime;
213
+ this._backgroundShellRegistry = config.backgroundShellRegistry;
214
+ this._permissionResponderFactory = config.permissionResponderFactory;
215
+ this._workspaceComparePending = hasWorkspaceObservationAwaitingComparison(this.sessionManager);
173
216
  // Always subscribe to agent events for internal handling
174
217
  // (session persistence, extensions, auto-compaction, retry logic)
175
218
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
176
219
  this._installAgentToolHooks();
177
220
  this._installAgentNextTurnRefresh();
221
+ this._installMidRunCompactionHook();
178
222
  this._installContextPipeline();
223
+ // Declarative hooks: session_start is observe-only and must neither delay
224
+ // nor fail construction, so it fires detached (emitObserve swallows
225
+ // handler failures already).
226
+ if (this._hookRuntime?.hasHandlers("session_start")) {
227
+ void this._hookRuntime.emitObserve("session_start", {
228
+ type: "session_start",
229
+ reason: this._sessionStartEvent.reason,
230
+ });
231
+ }
179
232
  this._buildRuntime({
180
233
  activeToolNames: this._initialActiveToolNames,
181
234
  includeAllExtensionTools: true,
@@ -266,6 +319,20 @@ export class AgentSession {
266
319
  if (extensionResult?.block === true)
267
320
  return extensionResult;
268
321
  }
322
+ // Declarative hooks (spec 2026-08-31-declarative-hooks) ride the same
323
+ // seam, after extensions and before the gate. Restriction-only: a block
324
+ // short-circuits, allow and ask fall through, and the gate below still
325
+ // evaluates the call, so a hook can narrow authority but never widen it.
326
+ if (this._hookRuntime?.hasHandlers("tool_call")) {
327
+ const hookBlock = await this._hookRuntime.decideToolCall(toolCall.name, {
328
+ type: "tool_call",
329
+ toolName: toolCall.name,
330
+ toolCallId: toolCall.id,
331
+ input: args,
332
+ });
333
+ if (hookBlock)
334
+ return { block: true, reason: hookBlock.reason };
335
+ }
269
336
  // Permission gate (roadmap Phase 2a, ADR 0004). Off by default — see
270
337
  // AgentSessionConfig.permissionGate. When present, every registered tool
271
338
  // call passes through it (contracts.md invariant 2): this is the seam,
@@ -274,9 +341,10 @@ export class AgentSession {
274
341
  const decision = await evaluateToolCall(toolCall.name, args, {
275
342
  ...this._permissionGate,
276
343
  getContract: (name) => this.getToolDefinition(name)?.contract,
277
- getResponder: () => this._extensionMode === "tui" && this._extensionUIContext
278
- ? createInteractiveResponder(this._extensionUIContext)
279
- : undefined,
344
+ getResponder: () => this._permissionResponderFactory?.() ??
345
+ (this._extensionMode === "tui" && this._extensionUIContext
346
+ ? createInteractiveResponder(this._extensionUIContext)
347
+ : undefined),
280
348
  });
281
349
  if (decision.block)
282
350
  return { block: true, reason: decision.reason };
@@ -314,6 +382,16 @@ export class AgentSession {
314
382
  usage: result.usage,
315
383
  })
316
384
  : undefined;
385
+ // Declarative hooks observe tool results after extension rewrites and
386
+ // before image normalization; no decision is read from them (spec).
387
+ if (this._hookRuntime?.hasHandlers("tool_result")) {
388
+ await this._hookRuntime.emitObserve("tool_result", {
389
+ type: "tool_result",
390
+ toolName: toolCall.name,
391
+ toolCallId: toolCall.id,
392
+ input: args,
393
+ });
394
+ }
317
395
  const content = hookResult?.content ?? result.content ?? [];
318
396
  // Runs after the extension hook so images injected or replaced by extensions are normalized too.
319
397
  const normalizedContent = await normalizeToolResultImages(content, {
@@ -330,18 +408,33 @@ export class AgentSession {
330
408
  };
331
409
  };
332
410
  }
411
+ async _compactBeforeNextAssistantResponse(context) {
412
+ const model = this.model;
413
+ const settings = this.settingsManager.getCompactionSettings();
414
+ if (!model ||
415
+ model.contextWindow <= 0 ||
416
+ !shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
417
+ return context;
418
+ }
419
+ await this._runAutoCompaction("threshold", false);
420
+ return {
421
+ ...context,
422
+ messages: this.agent.state.messages.slice(),
423
+ };
424
+ }
333
425
  _installAgentNextTurnRefresh() {
334
426
  const previousPrepareNextTurnWithContext = this.agent.prepareNextTurnWithContext ??
335
427
  (this.agent.prepareNextTurn
336
428
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
337
429
  : undefined);
338
430
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
339
- const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
340
- const previousContext = previousSnapshot?.context ?? turn.context;
431
+ const context = await this._compactBeforeNextAssistantResponse(turn.context);
432
+ const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
433
+ const nextContext = previousSnapshot?.context ?? context;
341
434
  return {
342
435
  ...previousSnapshot,
343
436
  context: {
344
- ...previousContext,
437
+ ...nextContext,
345
438
  systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
346
439
  tools: this.agent.state.tools.slice(),
347
440
  },
@@ -350,6 +443,35 @@ export class AgentSession {
350
443
  };
351
444
  };
352
445
  }
446
+ /** Stop an over-threshold tool loop before it sends another provider request. */
447
+ _installMidRunCompactionHook() {
448
+ const previousShouldStopAfterTurn = this.agent.shouldStopAfterTurn;
449
+ this.agent.shouldStopAfterTurn = async (turn, signal) => {
450
+ if (await previousShouldStopAfterTurn?.(turn, signal)) {
451
+ return true;
452
+ }
453
+ return this._shouldStopForMidRunCompaction(turn);
454
+ };
455
+ }
456
+ _shouldStopForMidRunCompaction(turn) {
457
+ if (!turn.hasMoreToolCalls || turn.toolResults.length === 0) {
458
+ return false;
459
+ }
460
+ const settings = this.settingsManager.getCompactionSettings();
461
+ if (!settings.enabled) {
462
+ return false;
463
+ }
464
+ const contextWindow = this.model?.contextWindow ?? 0;
465
+ if (contextWindow <= 0) {
466
+ return false;
467
+ }
468
+ const contextTokens = estimateContextTokens(turn.context.messages).tokens;
469
+ if (!shouldCompact(contextTokens, contextWindow, settings)) {
470
+ return false;
471
+ }
472
+ this._midRunCompactionNeeded = true;
473
+ return true;
474
+ }
353
475
  /**
354
476
  * Installs the context pipeline settled in `docs/architecture/contracts.md` § 2:
355
477
  * deferred-schema resolution → tool-result eviction → compaction. Compaction
@@ -406,6 +528,11 @@ export class AgentSession {
406
528
  followUp: [...this._followUpMessages],
407
529
  });
408
530
  }
531
+ async _emitSessionCompactFailed(event) {
532
+ if (this._extensionRunner.hasHandlers("session_compact_failed")) {
533
+ await this._extensionRunner.emit({ type: "session_compact_failed", ...event });
534
+ }
535
+ }
409
536
  _getIdleWaitPromise() {
410
537
  if (!this._idleWaitPromise) {
411
538
  this._idleWaitPromise = new Promise((resolve) => {
@@ -496,6 +623,14 @@ export class AgentSession {
496
623
  }
497
624
  }
498
625
  }
626
+ // A turn ends after its assistant message and every tool result has been appended,
627
+ // so this is the first point in the run where a context-only custom message can be
628
+ // inserted without landing between a tool call and its result. Flushing after the
629
+ // extension and listener dispatch above also picks up messages that turn_end
630
+ // handlers queued.
631
+ if (event.type === "turn_end") {
632
+ this._flushPendingCustomMessages();
633
+ }
499
634
  };
500
635
  _willRetryAfterAgentEnd(event) {
501
636
  const settings = this.settingsManager.getRetrySettings();
@@ -545,6 +680,18 @@ export class AgentSession {
545
680
  await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
546
681
  }
547
682
  else if (event.type === "turn_start") {
683
+ // Snapshot before the model gets to act, keyed to the entry `/tree` and `/fork`
684
+ // already navigate to, so the two agree about what an entry means.
685
+ //
686
+ // Awaited, despite the turn latency. Detached, the snapshot races the turn it is
687
+ // supposed to precede: a tool call can edit the worktree while `add -A` is still
688
+ // walking it, and the ref then holds mid-turn state that no `/fork` should ever
689
+ // restore. "The state before the model acted" is the whole guarantee, so paying
690
+ // for it here is the point. The engine's own timeout bounds the wait and a failed
691
+ // capture is never fatal.
692
+ const leafEntryId = this.sessionManager.getLeafEntry()?.id;
693
+ if (leafEntryId)
694
+ await this._checkpoints.capture(leafEntryId);
548
695
  const extensionEvent = {
549
696
  type: "turn_start",
550
697
  turnIndex: this._turnIndex,
@@ -560,6 +707,9 @@ export class AgentSession {
560
707
  toolResults: event.toolResults,
561
708
  };
562
709
  await this._extensionRunner.emit(extensionEvent);
710
+ if (this._hookRuntime?.hasHandlers("turn_end")) {
711
+ await this._hookRuntime.emitObserve("turn_end", { type: "turn_end" });
712
+ }
563
713
  this._turnIndex++;
564
714
  }
565
715
  else if (event.type === "message_start") {
@@ -659,6 +809,9 @@ export class AgentSession {
659
809
  this.abortBranchSummary();
660
810
  this.abortBash();
661
811
  this.agent.abort();
812
+ // Background shell children (spec 2026-08-31-background-shell.md) must
813
+ // never outlive the session that launched them.
814
+ this._backgroundShellRegistry?.dispose();
662
815
  }
663
816
  catch {
664
817
  // Dispose must succeed even if an abort hook throws.
@@ -666,6 +819,9 @@ export class AgentSession {
666
819
  this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
667
820
  this._disconnectFromAgent();
668
821
  this._eventListeners = [];
822
+ // Every connected MCP server is a child process this session started. Without
823
+ // this, one leaks per server per session, because nothing else owns them.
824
+ this._mcpRuntime?.close().catch(() => undefined);
669
825
  cleanupSessionResources(this.sessionId);
670
826
  }
671
827
  // =========================================================================
@@ -709,6 +865,13 @@ export class AgentSession {
709
865
  /**
710
866
  * Get all configured tools with name, description, parameter schema, prompt guidelines, and source metadata.
711
867
  */
868
+ /**
869
+ * The ADR 0010 projection over this session's registry: the one read every surface
870
+ * that *describes* the tool set consumes. Never an authorization input.
871
+ */
872
+ getToolContractSnapshot() {
873
+ return buildToolContractSnapshot(Array.from(this._toolDefinitions.values()).map(({ definition }) => definition));
874
+ }
712
875
  getAllTools() {
713
876
  const schemaToolActive = this.getActiveToolNames().includes("tool_schema");
714
877
  return Array.from(this._toolDefinitions.values())
@@ -719,7 +882,7 @@ export class AgentSession {
719
882
  parameters: definition.parameters,
720
883
  promptGuidelines: definition.promptGuidelines,
721
884
  sourceInfo,
722
- unclassified: !("contract" in definition),
885
+ unclassified: isUnclassifiedTool(definition),
723
886
  }));
724
887
  }
725
888
  getToolDefinition(name) {
@@ -855,10 +1018,15 @@ export class AgentSession {
855
1018
  finally {
856
1019
  this._systemPromptOverride = undefined;
857
1020
  this._flushPendingBashMessages();
1021
+ this._flushPendingCustomMessages();
858
1022
  await this._emitAgentSettled();
859
1023
  }
860
1024
  }
861
1025
  async _handlePostAgentRun() {
1026
+ if (this._midRunCompactionNeeded) {
1027
+ this._midRunCompactionNeeded = false;
1028
+ return await this._runAutoCompaction("threshold", true);
1029
+ }
862
1030
  const msg = this._lastAssistantMessage;
863
1031
  this._lastAssistantMessage = undefined;
864
1032
  if (!msg) {
@@ -944,8 +1112,9 @@ export class AgentSession {
944
1112
  preflightResult?.(true);
945
1113
  return;
946
1114
  }
947
- // Flush any pending bash messages before the new prompt
1115
+ // Flush any pending bash and custom messages before the new prompt
948
1116
  this._flushPendingBashMessages();
1117
+ this._flushPendingCustomMessages();
949
1118
  // Validate model
950
1119
  if (!this.model) {
951
1120
  throw new Error(formatNoModelSelectedMessage());
@@ -1019,7 +1188,9 @@ export class AgentSession {
1019
1188
  return;
1020
1189
  }
1021
1190
  preflightResult?.(true);
1191
+ await this._runWorkspaceComparisonBoundary();
1022
1192
  await this._runAgentPrompt(messages);
1193
+ await this._runVerificationTurnBoundary();
1023
1194
  }
1024
1195
  /**
1025
1196
  * Try to execute an extension command. Returns true if command was found and executed.
@@ -1166,8 +1337,9 @@ export class AgentSession {
1166
1337
  /**
1167
1338
  * Send a custom message to the session. Creates a CustomMessageEntry.
1168
1339
  *
1169
- * Handles three cases:
1340
+ * Handles four cases:
1170
1341
  * - Streaming: queues message, processed when loop pulls from queue
1342
+ * - Streaming + triggerTurn false: appended to state/session once the current turn ends
1171
1343
  * - Not streaming + triggerTurn: appends to state/session, starts new turn
1172
1344
  * - Not streaming + no trigger: appends to state/session, no turn
1173
1345
  *
@@ -1199,11 +1371,34 @@ export class AgentSession {
1199
1371
  else if (options?.triggerTurn) {
1200
1372
  await this._runAgentPrompt(appMessage);
1201
1373
  }
1374
+ else if (this.isStreaming) {
1375
+ // Appending now would put the message between an assistant tool call and its
1376
+ // result, which providers that validate message order reject on replay. Defer
1377
+ // to the end of the turn. Nothing is emitted yet: message events must not
1378
+ // describe messages the session tree does not contain.
1379
+ this._pendingCustomMessages.push(appMessage);
1380
+ }
1202
1381
  else {
1203
- this.agent.state.messages.push(appMessage);
1204
- this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
1205
- this._emit({ type: "message_start", message: appMessage });
1206
- this._emit({ type: "message_end", message: appMessage });
1382
+ this._appendCustomMessage(appMessage);
1383
+ }
1384
+ }
1385
+ _appendCustomMessage(appMessage) {
1386
+ this.agent.state.messages.push(appMessage);
1387
+ this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
1388
+ this._emit({ type: "message_start", message: appMessage });
1389
+ this._emit({ type: "message_end", message: appMessage });
1390
+ }
1391
+ /**
1392
+ * Append custom messages queued while the agent was running.
1393
+ * Called once the current turn's tool results are in agent state and session history.
1394
+ */
1395
+ _flushPendingCustomMessages() {
1396
+ if (this._pendingCustomMessages.length === 0)
1397
+ return;
1398
+ const pending = this._pendingCustomMessages;
1399
+ this._pendingCustomMessages = [];
1400
+ for (const appMessage of pending) {
1401
+ this._appendCustomMessage(appMessage);
1207
1402
  }
1208
1403
  }
1209
1404
  /**
@@ -1328,35 +1523,55 @@ export class AgentSession {
1328
1523
  }
1329
1524
  /**
1330
1525
  * Set model directly.
1331
- * Validates that auth is configured, saves to session and settings.
1526
+ * Validates that auth is configured and saves to the session transcript.
1527
+ * Persists to global defaults only when options.persist is true.
1332
1528
  * @throws Error if no auth is configured for the model
1333
1529
  */
1334
- async setModel(model) {
1530
+ async setModel(model, options = {}) {
1335
1531
  if (!(await this._modelRuntime.checkAuth(model.provider))) {
1336
1532
  throw new Error(`No API key for ${model.provider}/${model.id}`);
1337
1533
  }
1338
1534
  const previousModel = this.model;
1339
- const thinkingLevel = this._getThinkingLevelForModelSwitch();
1535
+ const thinkingLevel = this._getThinkingLevelForModelSwitch(model);
1340
1536
  this.agent.state.model = model;
1341
1537
  this.sessionManager.appendModelChange(model.provider, model.id);
1342
- this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1343
- // Re-clamp thinking level for new model's capabilities
1538
+ if (options.persist) {
1539
+ this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1540
+ this._addPersistedDefaultToNonEmptyScope(model);
1541
+ }
1542
+ // Apply thinking level for the new model.
1543
+ // Per-model thinking level overrides take priority over the global default.
1544
+ // Model persistence does not implicitly rewrite the global thinking default.
1344
1545
  this.setThinkingLevel(thinkingLevel);
1345
1546
  await this._emitModelSelect(model, previousModel, "set");
1346
1547
  }
1548
+ _addPersistedDefaultToNonEmptyScope(model) {
1549
+ if (this._scopedModels.length === 0)
1550
+ return;
1551
+ if (this._scopedModels.some((scoped) => modelsAreEqual(scoped.model, model)))
1552
+ return;
1553
+ this._scopedModels = [...this._scopedModels, { model }];
1554
+ const enabledModels = this.settingsManager.getEnabledModels();
1555
+ if (!enabledModels?.length)
1556
+ return;
1557
+ const modelReference = `${model.provider}/${model.id}`;
1558
+ if (enabledModels.some((pattern) => pattern.toLowerCase() === modelReference.toLowerCase()))
1559
+ return;
1560
+ this.settingsManager.setEnabledModels([...enabledModels, modelReference]);
1561
+ }
1347
1562
  /**
1348
1563
  * Cycle to next/previous model.
1349
1564
  * Uses scoped models (from --models flag) if available, otherwise all available models.
1350
1565
  * @param direction - "forward" (default) or "backward"
1351
1566
  * @returns The new model info, or undefined if only one model available
1352
1567
  */
1353
- async cycleModel(direction = "forward") {
1568
+ async cycleModel(direction = "forward", options = {}) {
1354
1569
  if (this._scopedModels.length > 0) {
1355
- return this._cycleScopedModel(direction);
1570
+ return this._cycleScopedModel(direction, options);
1356
1571
  }
1357
- return this._cycleAvailableModel(direction);
1572
+ return this._cycleAvailableModel(direction, options);
1358
1573
  }
1359
- async _cycleScopedModel(direction) {
1574
+ async _cycleScopedModel(direction, options) {
1360
1575
  const availableIds = new Set(this._modelRuntime.getAvailableSnapshot().map((model) => `${model.provider}\0${model.id}`));
1361
1576
  const scopedModels = this._scopedModels.filter((scoped) => availableIds.has(`${scoped.model.provider}\0${scoped.model.id}`));
1362
1577
  if (scopedModels.length <= 1)
@@ -1368,20 +1583,24 @@ export class AgentSession {
1368
1583
  const len = scopedModels.length;
1369
1584
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1370
1585
  const next = scopedModels[nextIndex];
1371
- const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
1586
+ const thinkingLevel = this._getThinkingLevelForModelSwitch(next.model, next.thinkingLevel);
1372
1587
  // Apply model
1373
1588
  this.agent.state.model = next.model;
1374
1589
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
1375
- this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1376
- // Apply thinking level.
1377
- // - Explicit scoped model thinking level overrides current session level
1378
- // - Undefined scoped model thinking level inherits the current session preference
1590
+ if (options.persist) {
1591
+ this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1592
+ this._addPersistedDefaultToNonEmptyScope(next.model);
1593
+ }
1594
+ // Apply thinking level for the new model.
1595
+ // - Explicit scoped model thinking level overrides defaults
1596
+ // - Per-model thinking level overrides take priority over the global default
1379
1597
  // setThinkingLevel clamps to model capabilities.
1598
+ // Model persistence does not implicitly rewrite the global thinking default.
1380
1599
  this.setThinkingLevel(thinkingLevel);
1381
1600
  await this._emitModelSelect(next.model, currentModel, "cycle");
1382
1601
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
1383
1602
  }
1384
- async _cycleAvailableModel(direction) {
1603
+ async _cycleAvailableModel(direction, options) {
1385
1604
  const availableModels = this._modelRuntime.getAvailableSnapshot();
1386
1605
  if (availableModels.length <= 1)
1387
1606
  return undefined;
@@ -1392,11 +1611,15 @@ export class AgentSession {
1392
1611
  const len = availableModels.length;
1393
1612
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1394
1613
  const nextModel = availableModels[nextIndex];
1395
- const thinkingLevel = this._getThinkingLevelForModelSwitch();
1614
+ const thinkingLevel = this._getThinkingLevelForModelSwitch(nextModel);
1396
1615
  this.agent.state.model = nextModel;
1397
1616
  this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1398
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1399
- // Re-clamp thinking level for new model's capabilities
1617
+ if (options.persist) {
1618
+ this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1619
+ this._addPersistedDefaultToNonEmptyScope(nextModel);
1620
+ }
1621
+ // Apply thinking level for the new model.
1622
+ // Model persistence does not implicitly rewrite the global thinking default.
1400
1623
  this.setThinkingLevel(thinkingLevel);
1401
1624
  await this._emitModelSelect(nextModel, currentModel, "cycle");
1402
1625
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
@@ -1407,20 +1630,21 @@ export class AgentSession {
1407
1630
  /**
1408
1631
  * Set thinking level.
1409
1632
  * Clamps to model capabilities based on available thinking levels.
1410
- * Saves to session and settings only if the level actually changes.
1633
+ * Saves the clamped level to the session transcript only if the level actually changes.
1634
+ * Persists the requested level to global defaults only when options.persist is true.
1411
1635
  */
1412
- setThinkingLevel(level) {
1636
+ setThinkingLevel(level, options = {}) {
1413
1637
  const availableLevels = this.getAvailableThinkingLevels();
1414
1638
  const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
1415
1639
  // Only persist if actually changing
1416
1640
  const previousLevel = this.agent.state.thinkingLevel;
1417
1641
  const isChanging = effectiveLevel !== previousLevel;
1418
1642
  this.agent.state.thinkingLevel = effectiveLevel;
1643
+ if (options.persist) {
1644
+ this.settingsManager.setDefaultThinkingLevel(level);
1645
+ }
1419
1646
  if (isChanging) {
1420
1647
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
1421
- if (this.supportsThinking() || effectiveLevel !== "off") {
1422
- this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
1423
- }
1424
1648
  this._emit({ type: "thinking_level_changed", level: effectiveLevel });
1425
1649
  void this._extensionRunner.emit({
1426
1650
  type: "thinking_level_select",
@@ -1433,14 +1657,14 @@ export class AgentSession {
1433
1657
  * Cycle to next thinking level.
1434
1658
  * @returns New level, or undefined if model doesn't support thinking
1435
1659
  */
1436
- cycleThinkingLevel() {
1660
+ cycleThinkingLevel(options = {}) {
1437
1661
  if (!this.supportsThinking())
1438
1662
  return undefined;
1439
1663
  const levels = this.getAvailableThinkingLevels();
1440
1664
  const currentIndex = levels.indexOf(this.thinkingLevel);
1441
1665
  const nextIndex = (currentIndex + 1) % levels.length;
1442
1666
  const nextLevel = levels[nextIndex];
1443
- this.setThinkingLevel(nextLevel);
1667
+ this.setThinkingLevel(nextLevel, options);
1444
1668
  return nextLevel;
1445
1669
  }
1446
1670
  /**
@@ -1449,7 +1673,7 @@ export class AgentSession {
1449
1673
  */
1450
1674
  getAvailableThinkingLevels() {
1451
1675
  if (!this.model)
1452
- return THINKING_LEVELS;
1676
+ return [...THINKING_LEVEL_OPTIONS];
1453
1677
  return getSupportedThinkingLevels(this.model);
1454
1678
  }
1455
1679
  /**
@@ -1458,14 +1682,18 @@ export class AgentSession {
1458
1682
  supportsThinking() {
1459
1683
  return !!this.model?.reasoning;
1460
1684
  }
1461
- _getThinkingLevelForModelSwitch(explicitLevel) {
1685
+ _getThinkingLevelForModelSwitch(targetModel, explicitLevel) {
1462
1686
  if (explicitLevel !== undefined) {
1463
1687
  return explicitLevel;
1464
1688
  }
1465
- if (!this.supportsThinking()) {
1466
- return this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
1689
+ // Per-model default takes priority when switching to a model that has one
1690
+ if (targetModel) {
1691
+ const perModel = this.settingsManager.getModelThinkingLevel(targetModel.provider, targetModel.id);
1692
+ if (perModel !== undefined) {
1693
+ return perModel;
1694
+ }
1467
1695
  }
1468
- return this.thinkingLevel;
1696
+ return this.settingsManager.getDefaultThinkingLevel() ?? this.thinkingLevel ?? DEFAULT_THINKING_LEVEL;
1469
1697
  }
1470
1698
  _clampThinkingLevel(level, _availableLevels) {
1471
1699
  return this.model ? clampThinkingLevel(this.model, level) : "off";
@@ -1496,15 +1724,232 @@ export class AgentSession {
1496
1724
  // =========================================================================
1497
1725
  // Compaction
1498
1726
  // =========================================================================
1727
+ /** Generate Pi's built-in compaction summary for manual and automatic compaction. */
1728
+ /**
1729
+ * Attempt exactly one workspace observation for a compaction (spec
1730
+ * 2026-09-01-harness-correctness-and-workspace-state.md § 3 step 1).
1731
+ * Returns undefined when observation is impossible; never throws and
1732
+ * never fails the surrounding compaction.
1733
+ */
1734
+ async _captureWorkspaceObservation(signal) {
1735
+ try {
1736
+ return await this._workspaceObserver(this._cwd, {
1737
+ signal,
1738
+ // The session's own state directory is harness bookkeeping, not
1739
+ // workspace content; observing it would report every turn as drift.
1740
+ excludePaths: this._sessionExclusionPaths(),
1741
+ });
1742
+ }
1743
+ catch {
1744
+ return undefined;
1745
+ }
1746
+ }
1747
+ /** Session/artifact directories inside the workspace, relative paths. */
1748
+ _sessionExclusionPaths() {
1749
+ const exclusions = [];
1750
+ const sessionDir = this.sessionManager.getSessionDir();
1751
+ if (sessionDir) {
1752
+ const rel = relative(this._cwd, sessionDir);
1753
+ if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel))
1754
+ exclusions.push(rel);
1755
+ }
1756
+ // Harness runtime bookkeeping (auth, model store, durable state) is
1757
+ // not workspace content either: when the agent directory sits inside
1758
+ // the workspace, its runtime files would report every turn as drift.
1759
+ // Same rule and same name list the fork/workspace fingerprints use.
1760
+ const agentDir = this.settingsManager.getAgentDir();
1761
+ if (agentDir !== undefined) {
1762
+ const rel = relative(this._cwd, agentDir);
1763
+ if (rel === "") {
1764
+ exclusions.push("auth.json", "models-store.json", "state.sqlite");
1765
+ }
1766
+ else if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel)) {
1767
+ exclusions.push(rel);
1768
+ }
1769
+ }
1770
+ return exclusions;
1771
+ }
1772
+ // ── VF.4: verification lifecycle (spec
1773
+ // 2026-09-01-configured-verification-and-formatting.md § 4) ──────────
1774
+ /**
1775
+ * Build (once) and refresh the tracker from the raw per-source policy
1776
+ * settings. ADR 0028: the loader consumes the sources separately and the
1777
+ * trust gate decides whether the project source exists at all.
1778
+ */
1779
+ /**
1780
+ * PS.1. Configured commands reach the same permission authority tool calls
1781
+ * use. Undefined only when the session has no gate at all, which is the
1782
+ * documented SDK default; a gated session always authorizes.
1783
+ */
1784
+ _authorizeConfiguredCommand() {
1785
+ const gate = this._permissionGate;
1786
+ if (gate === undefined)
1787
+ return undefined;
1788
+ return (operation) => authorizeConfiguredCommand(operation, {
1789
+ getMode: gate.getMode,
1790
+ getResponder: () => this._permissionResponderFactory?.() ??
1791
+ (this._extensionMode === "tui" && this._extensionUIContext
1792
+ ? createInteractiveResponder(this._extensionUIContext)
1793
+ : undefined),
1794
+ });
1795
+ }
1796
+ _ensureVerificationTracker() {
1797
+ if (this._verificationTracker === undefined) {
1798
+ this._verificationTracker = new VerificationTracker({
1799
+ workspaceRoot: this._cwd,
1800
+ ...(this._authorizeConfiguredCommand() !== undefined
1801
+ ? { authorize: this._authorizeConfiguredCommand() }
1802
+ : {}),
1803
+ });
1804
+ }
1805
+ const snapshot = loadPolicyConfiguration({
1806
+ globalSettings: this.settingsManager.getGlobalSettings().policies,
1807
+ projectSettings: this.settingsManager.getProjectSettings().policies,
1808
+ projectTrusted: this.settingsManager.isProjectTrusted(),
1809
+ });
1810
+ const boundary = this.settingsManager.getPolicySettings()?.boundary === "post-turn" ? "post-turn" : "explicit";
1811
+ this._verificationTracker.configure(snapshot.verification, boundary);
1812
+ return this._verificationTracker;
1813
+ }
1814
+ /**
1815
+ * Run one configured verification policy now and report the resulting
1816
+ * completion status. Unavailable when nothing is configured; the
1817
+ * workspace observation after the run becomes the staleness baseline.
1818
+ */
1819
+ async requestVerification(options = {}) {
1820
+ const tracker = this._ensureVerificationTracker();
1821
+ if (tracker.policyIds().length === 0) {
1822
+ return "unavailable";
1823
+ }
1824
+ const record = await tracker.runExplicit(options.policyId, { signal: options.signal });
1825
+ if (record !== undefined) {
1826
+ this._verificationBaseline = await this._captureWorkspaceObservation(options.signal);
1827
+ }
1828
+ return this.verificationStatus();
1829
+ }
1830
+ /**
1831
+ * VF.5: run one configured formatter policy now and report what it
1832
+ * actually mutated. Undefined when no formatter policy matches. Any real
1833
+ * mutation retires a prior verification record: the workspace the
1834
+ * verification described no longer exists.
1835
+ */
1836
+ async runConfiguredFormatter(options = {}) {
1837
+ const snapshot = loadPolicyConfiguration({
1838
+ globalSettings: this.settingsManager.getGlobalSettings().policies,
1839
+ projectSettings: this.settingsManager.getProjectSettings().policies,
1840
+ projectTrusted: this.settingsManager.isProjectTrusted(),
1841
+ });
1842
+ const policy = options.policyId === undefined
1843
+ ? snapshot.formatter[0]
1844
+ : snapshot.formatter.find((entry) => entry.id === options.policyId);
1845
+ if (policy === undefined)
1846
+ return undefined;
1847
+ const authorize = this._authorizeConfiguredCommand();
1848
+ const outcome = await runFormatterCommand(policy, {
1849
+ workspaceRoot: this._cwd,
1850
+ signal: options.signal,
1851
+ ...(authorize !== undefined ? { authorize } : {}),
1852
+ });
1853
+ if (outcome.mutations.changedPaths.length > 0) {
1854
+ this._verificationTracker?.noteWorkspaceChange(new Set(outcome.mutations.changedPaths));
1855
+ }
1856
+ return outcome;
1857
+ }
1858
+ /** The current completion status: verified, failed, unavailable, interrupted, or continued-unverified. */
1859
+ verificationStatus() {
1860
+ return this._verificationTracker?.completionStatus() ?? "unavailable";
1861
+ }
1862
+ /** The live verification record with its bounded evidence, if any. */
1863
+ verificationRecord() {
1864
+ return this._verificationTracker?.latest();
1865
+ }
1866
+ /** Record that the user chose to finish without verification. */
1867
+ continueWithoutVerification() {
1868
+ this._ensureVerificationTracker().recordContinuedWithoutVerification();
1869
+ }
1870
+ /**
1871
+ * Turn boundary: retire a verification result the workspace has moved
1872
+ * past (conservatively — anything not provably "same" retires it), then
1873
+ * run the configured post-turn boundary. Never throws: verification is
1874
+ * advisory and the turn already completed.
1875
+ */
1876
+ async _runVerificationTurnBoundary() {
1877
+ let tracker = this._verificationTracker;
1878
+ if (tracker === undefined) {
1879
+ tracker = this._ensureVerificationTracker();
1880
+ }
1881
+ try {
1882
+ if (tracker.latest() !== undefined) {
1883
+ const fresh = await this._captureWorkspaceObservation(undefined);
1884
+ if (this._verificationBaseline === undefined) {
1885
+ tracker.noteWorkspaceChange(new Set(["unbaseline"]));
1886
+ }
1887
+ else {
1888
+ const comparison = compareWorkspaceObservations(this._verificationBaseline, fresh);
1889
+ tracker.noteWorkspaceChange(comparison.result === "same" ? new Set() : new Set(["workspace-changed"]));
1890
+ }
1891
+ }
1892
+ if (tracker.boundary === "post-turn" && tracker.policyIds().length > 0) {
1893
+ await tracker.runExplicit();
1894
+ this._verificationBaseline = await this._captureWorkspaceObservation(undefined);
1895
+ }
1896
+ }
1897
+ catch {
1898
+ // Verification is advisory; the turn already completed.
1899
+ }
1900
+ }
1901
+ /**
1902
+ * WS.5 (spec 2026-09-01-harness-correctness-and-workspace-state.md § 3):
1903
+ * at the first model turn after a compaction, or after resume onto a
1904
+ * session with an uncompared observation, compare fresh workspace state
1905
+ * against the stored observation once and persist the outcome. The
1906
+ * observation stays historical. Never throws: comparison problems must
1907
+ * not fail the user's turn.
1908
+ */
1909
+ async _runWorkspaceComparisonBoundary() {
1910
+ if (!this._workspaceComparePending)
1911
+ return;
1912
+ this._workspaceComparePending = false;
1913
+ try {
1914
+ const baseline = findLatestWorkspaceObservation(this.sessionManager);
1915
+ if (!baseline)
1916
+ return;
1917
+ const fresh = await this._captureWorkspaceObservation(undefined);
1918
+ const outcome = compareWorkspaceObservations(baseline.record, fresh);
1919
+ appendWorkspaceComparison(this.sessionManager, buildWorkspaceComparison(baseline.record.observationId, outcome));
1920
+ }
1921
+ catch {
1922
+ // Comparison is advisory; the turn proceeds.
1923
+ }
1924
+ }
1925
+ async _runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, signal, env, reason) {
1926
+ // Maintenance budget (spec 2026-09-01-tool-reliability-and-execution-budgets.md):
1927
+ // a compaction summarization is context maintenance, not task progress. It is
1928
+ // recorded on the run's budget for observability and never consumes
1929
+ // maxProviderRequests.
1930
+ this.agent.recordMaintenanceRequest();
1931
+ return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
1932
+ }
1499
1933
  /**
1500
1934
  * Manually compact the session context.
1501
- * Aborts current agent operation first.
1935
+ *
1936
+ * This is the manual entry point used by `/compact`, RPC, and extensions. It is
1937
+ * separate from automatic threshold/overflow compaction, which enters through
1938
+ * `_checkCompaction()` and `_runAutoCompaction()`. After preparation and the
1939
+ * `session_before_compact` hook, both paths call the lower-level `compact()`
1940
+ * function imported from `./compaction/index.ts`, unless the hook cancels or
1941
+ * supplies a custom result.
1942
+ *
1943
+ * Aborts the current agent operation first. Manual compaction never retries or
1944
+ * continues the interrupted agent turn.
1945
+ *
1502
1946
  * @param customInstructions Optional instructions for the compaction summary
1503
1947
  */
1504
1948
  async compact(customInstructions) {
1505
1949
  await this.abort();
1506
1950
  this._compactionAbortController = new AbortController();
1507
1951
  this._emit({ type: "compaction_start", reason: "manual" });
1952
+ let fromExtension = false;
1508
1953
  try {
1509
1954
  if (!this.model) {
1510
1955
  throw new Error(formatNoModelSelectedMessage());
@@ -1522,7 +1967,12 @@ export class AgentSession {
1522
1967
  throw new Error("Nothing to compact (session too small)");
1523
1968
  }
1524
1969
  let extensionCompaction;
1525
- let fromExtension = false;
1970
+ if (this._hookRuntime?.hasHandlers("session_before_compact")) {
1971
+ await this._hookRuntime.emitObserve("session_before_compact", {
1972
+ type: "session_before_compact",
1973
+ reason: "manual",
1974
+ });
1975
+ }
1526
1976
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1527
1977
  const result = (await this._extensionRunner.emit({
1528
1978
  type: "session_before_compact",
@@ -1546,6 +1996,12 @@ export class AgentSession {
1546
1996
  let tokensBefore;
1547
1997
  let usage;
1548
1998
  let details;
1999
+ // One workspace observation attempt per compaction (spec
2000
+ // 2026-09-01-harness-correctness-and-workspace-state.md § 3),
2001
+ // before summary generation so the bounded projection can join
2002
+ // the model-facing summary. Capture problems never fail the
2003
+ // compaction.
2004
+ const workspaceRecord = await this._captureWorkspaceObservation(this._compactionAbortController?.signal ?? this._autoCompactionAbortController?.signal);
1549
2005
  if (extensionCompaction) {
1550
2006
  // Extension provided compaction content
1551
2007
  summary = extensionCompaction.summary;
@@ -1555,18 +2011,35 @@ export class AgentSession {
1555
2011
  details = extensionCompaction.details;
1556
2012
  }
1557
2013
  else {
1558
- // Generate compaction result
1559
- const result = await compact(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
2014
+ // Shared default summary generator, also used by automatic compaction.
2015
+ const result = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, env, "manual");
1560
2016
  summary = result.summary;
1561
2017
  firstKeptEntryId = result.firstKeptEntryId;
1562
2018
  tokensBefore = result.tokensBefore;
1563
2019
  usage = result.usage;
1564
2020
  details = result.details;
2021
+ if (workspaceRecord) {
2022
+ summary += `\n\n${formatWorkspaceProjection(workspaceRecord)}`;
2023
+ }
1565
2024
  }
1566
2025
  if (this._compactionAbortController.signal.aborted) {
1567
2026
  throw new Error("Compaction cancelled");
1568
2027
  }
1569
2028
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2029
+ if (workspaceRecord) {
2030
+ try {
2031
+ // Historical record, child of the compaction entry; never in
2032
+ // the model context and never a rewrite of history.
2033
+ appendWorkspaceObservation(this.sessionManager, workspaceRecord);
2034
+ // WS.5: the next model turn compares fresh state against
2035
+ // this observation and records the drift outcome.
2036
+ this._workspaceComparePending = true;
2037
+ }
2038
+ catch {
2039
+ // Artifact/entry persistence problems downgrade the
2040
+ // workspace status; they never fail transcript compaction.
2041
+ }
2042
+ }
1570
2043
  const newEntries = this.sessionManager.getEntries();
1571
2044
  const sessionContext = this.sessionManager.buildSessionContext();
1572
2045
  this.agent.state.messages = sessionContext.messages;
@@ -1604,6 +2077,7 @@ export class AgentSession {
1604
2077
  catch (error) {
1605
2078
  const message = error instanceof Error ? error.message : String(error);
1606
2079
  const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
2080
+ const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
1607
2081
  this._compactionAbortController = undefined;
1608
2082
  this._emit({
1609
2083
  type: "compaction_end",
@@ -1611,7 +2085,14 @@ export class AgentSession {
1611
2085
  result: undefined,
1612
2086
  aborted,
1613
2087
  willRetry: false,
1614
- errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
2088
+ errorMessage,
2089
+ });
2090
+ await this._emitSessionCompactFailed({
2091
+ reason: "manual",
2092
+ errorMessage,
2093
+ aborted,
2094
+ willRetry: false,
2095
+ fromExtension,
1615
2096
  });
1616
2097
  throw error;
1617
2098
  }
@@ -1633,16 +2114,25 @@ export class AgentSession {
1633
2114
  this._branchSummaryAbortController?.abort();
1634
2115
  }
1635
2116
  /**
1636
- * Check if compaction is needed and run it.
1637
- * Called after agent_end and before prompt submission.
2117
+ * Dispatch automatic compaction after `agent_end` or before prompt submission.
2118
+ * Manual compaction does not call this method; it enters through `compact()`.
2119
+ *
2120
+ * Automatic cases:
2121
+ * 1. Overflow with retry: a context-overflow error or recoverable length stop;
2122
+ * remove the failed assistant message, compact, and retry the turn once.
2123
+ * 2. Overflow without retry: a successful response exceeded the configured
2124
+ * context window; compact but preserve the completed response.
2125
+ * 3. Threshold without retry: valid or estimated context usage crossed the
2126
+ * configured threshold; compact without retrying the completed response.
1638
2127
  *
1639
- * Two cases:
1640
- * 1. Recoverable failure: LLM returned context overflow or stopped below its desired output limit;
1641
- * remove the assistant message, compact, and auto-retry once
1642
- * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
2128
+ * Each case calls `_runAutoCompaction()`. After preparation and the
2129
+ * `session_before_compact` hook, that method calls the lower-level `compact()`
2130
+ * function imported from `./compaction/index.ts`, unless the hook cancels or
2131
+ * supplies a custom result.
1643
2132
  *
1644
2133
  * @param assistantMessage The assistant message to check
1645
2134
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
2135
+ * @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
1646
2136
  */
1647
2137
  async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
1648
2138
  const settings = this.settingsManager.getCompactionSettings();
@@ -1665,38 +2155,49 @@ export class AgentSession {
1665
2155
  if (assistantIsFromBeforeCompaction) {
1666
2156
  return false;
1667
2157
  }
1668
- // Case 1: Recoverable failure. Explicit/silent context overflow still uses context metadata.
2158
+ // Automatic cases 1 and 2: context overflow.
1669
2159
  // A length stop is recoverable when output ended below the model's original desired limit,
1670
2160
  // independent of the configured context size or any context-clamped provider request limit.
1671
- // A successful response over the configured window should compact but must not retry: the
1672
- // assistant answer already completed and agent.continue() cannot continue from an assistant.
2161
+ const contextOverflow = sameModel && isContextOverflow(assistantMessage, contextWindow);
1673
2162
  const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
1674
- if (sameModel && (isContextOverflow(assistantMessage, contextWindow) || recoverableLength)) {
2163
+ if (contextOverflow || recoverableLength) {
1675
2164
  const willRetry = assistantMessage.stopReason !== "stop";
2165
+ // Case 2: the response completed successfully. Compact, but do not retry because
2166
+ // agent.continue() cannot continue from a completed assistant response.
1676
2167
  if (!willRetry) {
1677
2168
  return await this._runAutoCompaction("overflow", false);
1678
2169
  }
1679
2170
  if (this._overflowRecoveryAttempted) {
2171
+ const errorMessage = contextOverflow
2172
+ ? "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."
2173
+ : "Truncated response recovery failed after one compact-and-retry attempt.";
1680
2174
  this._emit({
1681
2175
  type: "compaction_end",
1682
2176
  reason: "overflow",
1683
2177
  result: undefined,
1684
2178
  aborted: false,
1685
2179
  willRetry: false,
1686
- errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
2180
+ errorMessage,
2181
+ });
2182
+ await this._emitSessionCompactFailed({
2183
+ reason: "overflow",
2184
+ errorMessage,
2185
+ aborted: false,
2186
+ willRetry: false,
2187
+ fromExtension: false,
1687
2188
  });
1688
2189
  return false;
1689
2190
  }
2191
+ // Case 1: remove the failed or truncated message from agent state, compact, and
2192
+ // retry once. The message remains in session history but is excluded from retry context.
1690
2193
  this._overflowRecoveryAttempted = true;
1691
- // Remove the failed or truncated message from agent state. It remains in session history,
1692
- // but must not be included in the compact-and-retry context.
1693
2194
  const messages = this.agent.state.messages;
1694
2195
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
1695
2196
  this.agent.state.messages = messages.slice(0, -1);
1696
2197
  }
1697
2198
  return await this._runAutoCompaction("overflow", willRetry);
1698
2199
  }
1699
- // Case 2: Threshold - context is getting large
2200
+ // Case 3: threshold compaction without retry.
1700
2201
  // For error messages or all-zero usage messages, estimate from the last valid response.
1701
2202
  // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
1702
2203
  // responses can still compact and do not reset context accounting.
@@ -1705,16 +2206,18 @@ export class AgentSession {
1705
2206
  if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
1706
2207
  const messages = this.agent.state.messages;
1707
2208
  const estimate = estimateContextTokens(messages);
1708
- if (estimate.lastUsageIndex === null)
1709
- return false; // No usage data at all
1710
- // Verify the usage source is post-compaction. Kept pre-compaction messages
1711
- // have stale usage reflecting the old (larger) context and would falsely
1712
- // trigger compaction right after one just finished.
1713
- const usageMsg = messages[estimate.lastUsageIndex];
1714
- if (compactionEntry &&
1715
- usageMsg.role === "assistant" &&
1716
- usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1717
- return false;
2209
+ // Without provider usage, estimate.tokens is the pure message-size estimate.
2210
+ // Only usage-backed estimates need the stale pre-compaction check.
2211
+ if (estimate.lastUsageIndex !== null) {
2212
+ // Verify the usage source is post-compaction. Kept pre-compaction messages
2213
+ // have stale usage reflecting the old (larger) context and would falsely
2214
+ // trigger compaction right after one just finished.
2215
+ const usageMsg = messages[estimate.lastUsageIndex];
2216
+ if (compactionEntry &&
2217
+ usageMsg.role === "assistant" &&
2218
+ usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
2219
+ return false;
2220
+ }
1718
2221
  }
1719
2222
  contextTokens = estimate.tokens;
1720
2223
  }
@@ -1727,11 +2230,19 @@ export class AgentSession {
1727
2230
  return false;
1728
2231
  }
1729
2232
  /**
1730
- * Internal: Run auto-compaction with events.
2233
+ * Execute threshold or overflow compaction. Manual compaction uses
2234
+ * `AgentSession.compact()` instead. Both paths call the lower-level `compact()`
2235
+ * function imported from `./compaction/index.ts` after preparation and extension
2236
+ * interception.
2237
+ *
2238
+ * @param reason Automatic trigger selected by `_checkCompaction()`
2239
+ * @param willRetry Whether to continue the interrupted turn after overflow compaction
2240
+ * @returns Whether the post-run loop should call `agent.continue()`
1731
2241
  */
1732
2242
  async _runAutoCompaction(reason, willRetry) {
1733
2243
  const settings = this.settingsManager.getCompactionSettings();
1734
2244
  let started = false;
2245
+ let fromExtension = false;
1735
2246
  try {
1736
2247
  if (!this.model) {
1737
2248
  return false;
@@ -1746,7 +2257,9 @@ export class AgentSession {
1746
2257
  this._autoCompactionAbortController = new AbortController();
1747
2258
  started = true;
1748
2259
  let extensionCompaction;
1749
- let fromExtension = false;
2260
+ if (this._hookRuntime?.hasHandlers("session_before_compact")) {
2261
+ await this._hookRuntime.emitObserve("session_before_compact", { type: "session_before_compact", reason });
2262
+ }
1750
2263
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1751
2264
  const extensionResult = (await this._extensionRunner.emit({
1752
2265
  type: "session_before_compact",
@@ -1765,6 +2278,12 @@ export class AgentSession {
1765
2278
  aborted: true,
1766
2279
  willRetry: false,
1767
2280
  });
2281
+ await this._emitSessionCompactFailed({
2282
+ reason,
2283
+ aborted: true,
2284
+ willRetry: false,
2285
+ fromExtension: false,
2286
+ });
1768
2287
  return false;
1769
2288
  }
1770
2289
  if (extensionResult?.compaction) {
@@ -1777,6 +2296,12 @@ export class AgentSession {
1777
2296
  let tokensBefore;
1778
2297
  let usage;
1779
2298
  let details;
2299
+ // One workspace observation attempt per compaction (spec
2300
+ // 2026-09-01-harness-correctness-and-workspace-state.md § 3),
2301
+ // before summary generation so the bounded projection can join
2302
+ // the model-facing summary. Capture problems never fail the
2303
+ // compaction.
2304
+ const workspaceRecord = await this._captureWorkspaceObservation(this._compactionAbortController?.signal ?? this._autoCompactionAbortController?.signal);
1780
2305
  if (extensionCompaction) {
1781
2306
  // Extension provided compaction content
1782
2307
  summary = extensionCompaction.summary;
@@ -1786,13 +2311,16 @@ export class AgentSession {
1786
2311
  details = extensionCompaction.details;
1787
2312
  }
1788
2313
  else {
1789
- // Generate compaction result
1790
- const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
2314
+ // Shared default summary generator, also used by manual compaction.
2315
+ const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, env, reason);
1791
2316
  summary = compactResult.summary;
1792
2317
  firstKeptEntryId = compactResult.firstKeptEntryId;
1793
2318
  tokensBefore = compactResult.tokensBefore;
1794
2319
  usage = compactResult.usage;
1795
2320
  details = compactResult.details;
2321
+ if (workspaceRecord) {
2322
+ summary += `\n\n${formatWorkspaceProjection(workspaceRecord)}`;
2323
+ }
1796
2324
  }
1797
2325
  if (this._autoCompactionAbortController.signal.aborted) {
1798
2326
  this._emit({
@@ -1802,9 +2330,29 @@ export class AgentSession {
1802
2330
  aborted: true,
1803
2331
  willRetry: false,
1804
2332
  });
2333
+ await this._emitSessionCompactFailed({
2334
+ reason,
2335
+ aborted: true,
2336
+ willRetry: false,
2337
+ fromExtension,
2338
+ });
1805
2339
  return false;
1806
2340
  }
1807
2341
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2342
+ if (workspaceRecord) {
2343
+ try {
2344
+ // Historical record, child of the compaction entry; never in
2345
+ // the model context and never a rewrite of history.
2346
+ appendWorkspaceObservation(this.sessionManager, workspaceRecord);
2347
+ // WS.5: the next model turn compares fresh state against
2348
+ // this observation and records the drift outcome.
2349
+ this._workspaceComparePending = true;
2350
+ }
2351
+ catch {
2352
+ // Artifact/entry persistence problems downgrade the
2353
+ // workspace status; they never fail transcript compaction.
2354
+ }
2355
+ }
1808
2356
  const newEntries = this.sessionManager.getEntries();
1809
2357
  const sessionContext = this.sessionManager.buildSessionContext();
1810
2358
  this.agent.state.messages = sessionContext.messages;
@@ -1848,15 +2396,23 @@ export class AgentSession {
1848
2396
  catch (error) {
1849
2397
  const errorMessage = error instanceof Error ? error.message : "compaction failed";
1850
2398
  if (started) {
2399
+ const formattedErrorMessage = reason === "overflow"
2400
+ ? `Context overflow recovery failed: ${errorMessage}`
2401
+ : `Auto-compaction failed: ${errorMessage}`;
1851
2402
  this._emit({
1852
2403
  type: "compaction_end",
1853
2404
  reason,
1854
2405
  result: undefined,
1855
2406
  aborted: false,
1856
2407
  willRetry: false,
1857
- errorMessage: reason === "overflow"
1858
- ? `Context overflow recovery failed: ${errorMessage}`
1859
- : `Auto-compaction failed: ${errorMessage}`,
2408
+ errorMessage: formattedErrorMessage,
2409
+ });
2410
+ await this._emitSessionCompactFailed({
2411
+ reason,
2412
+ errorMessage: formattedErrorMessage,
2413
+ aborted: false,
2414
+ willRetry: false,
2415
+ fromExtension,
1860
2416
  });
1861
2417
  }
1862
2418
  return false;
@@ -2170,10 +2726,15 @@ export class AgentSession {
2170
2726
  ]))
2171
2727
  : createAllToolDefinitions(this._cwd, {
2172
2728
  read: { autoResizeImages },
2173
- bash: { commandPrefix: shellCommandPrefix, shellPath },
2729
+ bash: {
2730
+ commandPrefix: shellCommandPrefix,
2731
+ shellPath,
2732
+ backgroundRegistry: this._backgroundShellRegistry,
2733
+ },
2174
2734
  edit: { diagnosticsOperations: this._diagnosticsOperations },
2175
2735
  write: { diagnosticsOperations: this._diagnosticsOperations },
2176
2736
  ...(this._lspOperations ? { lsp: { operations: this._lspOperations } } : {}),
2737
+ ...(this._mcpRuntime ? { mcp: this._mcpRuntime } : {}),
2177
2738
  ...(this._webSearchOperations ? { web_search: { operations: this._webSearchOperations } } : {}),
2178
2739
  });
2179
2740
  baseToolDefinitions.tool_schema = createToolSchemaToolDefinition({
@@ -2485,6 +3046,13 @@ export class AgentSession {
2485
3046
  if (targetId === oldLeafId) {
2486
3047
  return { cancelled: false };
2487
3048
  }
3049
+ // WS.6 (spec 2026-09-01-harness-correctness-and-workspace-state.md § 4):
3050
+ // files move only under an explicit policy, and a refused or cancelled
3051
+ // restore leaves the conversation untouched too.
3052
+ const workspaceStep = await this._resolveTreeWorkspaceStep(targetId, options.workspacePolicy ?? "keep");
3053
+ if (workspaceStep.cancelled) {
3054
+ return { cancelled: true, workspace: workspaceStep.outcome };
3055
+ }
2488
3056
  // Model required for summarization
2489
3057
  if (options.summarize && !this.model) {
2490
3058
  throw new Error("No model available for summarization");
@@ -2631,12 +3199,77 @@ export class AgentSession {
2631
3199
  fromExtension: summaryText ? fromExtension : undefined,
2632
3200
  });
2633
3201
  // Emit to custom tools
2634
- return { editorText, cancelled: false, summaryEntry };
3202
+ return { editorText, cancelled: false, summaryEntry, workspace: workspaceStep.outcome };
2635
3203
  }
2636
3204
  finally {
2637
3205
  this._branchSummaryAbortController = undefined;
2638
3206
  }
2639
3207
  }
3208
+ /**
3209
+ * WS.6: resolve what navigation may do to the workspace before the
3210
+ * conversation moves. `keep` never touches files; `restore` overwrites
3211
+ * only through the engine's pinned pre-restore checkpoint; `fail-if-
3212
+ * drifted` and `cancel` refuse when the workspace moved; a missing
3213
+ * checkpoint leaves the workspace unchanged and says so. Never throws.
3214
+ */
3215
+ async _resolveTreeWorkspaceStep(targetId, policy) {
3216
+ const unchanged = { policy, outcome: "unchanged", warnings: [] };
3217
+ if (policy === "keep")
3218
+ return { outcome: unchanged };
3219
+ try {
3220
+ const engine = await this._checkpoints.engine();
3221
+ if (!engine) {
3222
+ return {
3223
+ outcome: {
3224
+ policy,
3225
+ outcome: "missing-checkpoint",
3226
+ warnings: ["checkpoints are unavailable; workspace left unchanged"],
3227
+ },
3228
+ };
3229
+ }
3230
+ const checkpoint = await engine.lookup(targetId);
3231
+ if (!checkpoint)
3232
+ return { outcome: { policy, outcome: "missing-checkpoint", warnings: [] } };
3233
+ const matches = await engine.matchesWorktree(checkpoint);
3234
+ if (matches === undefined) {
3235
+ return {
3236
+ outcome: {
3237
+ policy,
3238
+ outcome: "failed",
3239
+ warnings: ["could not compare the workspace against the checkpoint"],
3240
+ },
3241
+ };
3242
+ }
3243
+ if (matches)
3244
+ return { outcome: unchanged };
3245
+ if (policy === "fail-if-drifted" || policy === "cancel") {
3246
+ return { cancelled: true, outcome: { policy, outcome: "refused-drifted", warnings: [] } };
3247
+ }
3248
+ const preRestore = await engine.restore(checkpoint);
3249
+ if (!preRestore) {
3250
+ return {
3251
+ outcome: { policy, outcome: "failed", warnings: ["restore failed; workspace left unchanged"] },
3252
+ };
3253
+ }
3254
+ return {
3255
+ outcome: {
3256
+ policy,
3257
+ outcome: "restored",
3258
+ preRestoreCheckpoint: { entryId: preRestore.entryId, commit: preRestore.commit },
3259
+ warnings: [],
3260
+ },
3261
+ };
3262
+ }
3263
+ catch (error) {
3264
+ return {
3265
+ outcome: {
3266
+ policy,
3267
+ outcome: "failed",
3268
+ warnings: [`workspace policy check failed: ${error instanceof Error ? error.message : String(error)}`],
3269
+ },
3270
+ };
3271
+ }
3272
+ }
2640
3273
  /**
2641
3274
  * Get all user messages from session for fork selector.
2642
3275
  */
@@ -2780,29 +3413,7 @@ export class AgentSession {
2780
3413
  * @returns The resolved output file path.
2781
3414
  */
2782
3415
  exportToJsonl(outputPath) {
2783
- const filePath = resolvePath(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`, process.cwd());
2784
- const dir = dirname(filePath);
2785
- if (!existsSync(dir)) {
2786
- mkdirSync(dir, { recursive: true });
2787
- }
2788
- const header = {
2789
- type: "session",
2790
- version: CURRENT_SESSION_VERSION,
2791
- id: this.sessionManager.getSessionId(),
2792
- timestamp: new Date().toISOString(),
2793
- cwd: this.sessionManager.getCwd(),
2794
- };
2795
- const branchEntries = this.sessionManager.getBranch();
2796
- const lines = [JSON.stringify(header)];
2797
- // Re-chain parentIds to form a linear sequence
2798
- let prevId = null;
2799
- for (const entry of branchEntries) {
2800
- const linear = { ...entry, parentId: prevId };
2801
- lines.push(JSON.stringify(linear));
2802
- prevId = entry.id;
2803
- }
2804
- writeFileSync(filePath, `${lines.join("\n")}\n`);
2805
- return filePath;
3416
+ return exportSessionToJsonl(this.sessionManager, outputPath);
2806
3417
  }
2807
3418
  // =========================================================================
2808
3419
  // Utilities