@prestyj/cli 5.10.0 → 5.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (703) hide show
  1. package/README.md +3 -4
  2. package/assets/skills/compliance-guard/SKILL.md +251 -0
  3. package/assets/skills/compliance-guard/references/artifacts.md +186 -0
  4. package/assets/skills/compliance-guard/references/eu-uk.md +151 -0
  5. package/assets/skills/compliance-guard/references/exposure-triage.md +161 -0
  6. package/assets/skills/compliance-guard/references/lawsuit-vectors.md +141 -0
  7. package/assets/skills/compliance-guard/references/provenance.md +42 -0
  8. package/assets/skills/compliance-guard/references/sector-gates.md +173 -0
  9. package/assets/skills/compliance-guard/references/security-baseline.md +130 -0
  10. package/assets/skills/compliance-guard/references/trigger-map.md +163 -0
  11. package/assets/skills/compliance-guard/references/us.md +164 -0
  12. package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
  13. package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
  14. package/dist/app-sidecar-auth-broadcast.test.js +348 -0
  15. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
  16. package/dist/app-sidecar-sessions.d.ts +5 -2
  17. package/dist/app-sidecar-sessions.d.ts.map +1 -1
  18. package/dist/app-sidecar-sessions.js +34 -8
  19. package/dist/app-sidecar-sessions.js.map +1 -1
  20. package/dist/app-sidecar-sessions.test.js +96 -1
  21. package/dist/app-sidecar-sessions.test.js.map +1 -1
  22. package/dist/app-sidecar.js +616 -173
  23. package/dist/app-sidecar.js.map +1 -1
  24. package/dist/chat-agents/jiwa.test.js +1 -1
  25. package/dist/cli/auth.d.ts.map +1 -1
  26. package/dist/cli/auth.js +33 -36
  27. package/dist/cli/auth.js.map +1 -1
  28. package/dist/cli/command-routing.d.ts +1 -1
  29. package/dist/cli/command-routing.d.ts.map +1 -1
  30. package/dist/cli/command-routing.js +1 -0
  31. package/dist/cli/command-routing.js.map +1 -1
  32. package/dist/cli/command-routing.test.js +1 -0
  33. package/dist/cli/command-routing.test.js.map +1 -1
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +179 -48
  36. package/dist/cli.js.map +1 -1
  37. package/dist/config.d.ts +18 -5
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +45 -76
  40. package/dist/config.js.map +1 -1
  41. package/dist/config.test.js +30 -13
  42. package/dist/config.test.js.map +1 -1
  43. package/dist/core/agent-notifications.d.ts +62 -0
  44. package/dist/core/agent-notifications.d.ts.map +1 -0
  45. package/dist/core/agent-notifications.js +92 -0
  46. package/dist/core/agent-notifications.js.map +1 -0
  47. package/dist/core/agent-notifications.test.d.ts +2 -0
  48. package/dist/core/agent-notifications.test.d.ts.map +1 -0
  49. package/dist/core/agent-notifications.test.js +97 -0
  50. package/dist/core/agent-notifications.test.js.map +1 -0
  51. package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
  52. package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
  53. package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
  54. package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
  55. package/dist/core/agent-session-compaction.test.js +323 -6
  56. package/dist/core/agent-session-compaction.test.js.map +1 -1
  57. package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
  58. package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
  59. package/dist/core/agent-session-crash-durability.test.js +102 -0
  60. package/dist/core/agent-session-crash-durability.test.js.map +1 -0
  61. package/dist/core/agent-session-marker-anchors.test.js +181 -2
  62. package/dist/core/agent-session-marker-anchors.test.js.map +1 -1
  63. package/dist/core/agent-session-model-switch.test.d.ts +2 -0
  64. package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  65. package/dist/core/agent-session-model-switch.test.js +161 -0
  66. package/dist/core/agent-session-model-switch.test.js.map +1 -0
  67. package/dist/core/agent-session-notifications.test.d.ts +2 -0
  68. package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
  69. package/dist/core/agent-session-notifications.test.js +114 -0
  70. package/dist/core/agent-session-notifications.test.js.map +1 -0
  71. package/dist/core/agent-session-process-gate.test.d.ts +2 -0
  72. package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
  73. package/dist/core/agent-session-process-gate.test.js +98 -0
  74. package/dist/core/agent-session-process-gate.test.js.map +1 -0
  75. package/dist/core/agent-session-queue.test.js +65 -0
  76. package/dist/core/agent-session-queue.test.js.map +1 -1
  77. package/dist/core/agent-session-review-coverage.test.js +80 -23
  78. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  79. package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
  80. package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
  81. package/dist/core/agent-session-slash-restore.test.js +121 -0
  82. package/dist/core/agent-session-slash-restore.test.js.map +1 -0
  83. package/dist/core/agent-session-tool-tiers.test.d.ts +2 -0
  84. package/dist/core/agent-session-tool-tiers.test.d.ts.map +1 -0
  85. package/dist/core/agent-session-tool-tiers.test.js +138 -0
  86. package/dist/core/agent-session-tool-tiers.test.js.map +1 -0
  87. package/dist/core/agent-session.d.ts +257 -13
  88. package/dist/core/agent-session.d.ts.map +1 -1
  89. package/dist/core/agent-session.js +1118 -241
  90. package/dist/core/agent-session.js.map +1 -1
  91. package/dist/core/agents.d.ts +29 -1
  92. package/dist/core/agents.d.ts.map +1 -1
  93. package/dist/core/agents.js +47 -97
  94. package/dist/core/agents.js.map +1 -1
  95. package/dist/core/agents.test.js +87 -7
  96. package/dist/core/agents.test.js.map +1 -1
  97. package/dist/core/api-benchmark.d.ts +3 -3
  98. package/dist/core/api-benchmark.js +7 -7
  99. package/dist/core/auth-providers.d.ts +37 -1
  100. package/dist/core/auth-providers.d.ts.map +1 -1
  101. package/dist/core/auth-providers.js +83 -3
  102. package/dist/core/auth-providers.js.map +1 -1
  103. package/dist/core/auto-update.test.js +1 -1
  104. package/dist/core/autopilot-gate.d.ts +2 -2
  105. package/dist/core/autopilot-gate.js +1 -1
  106. package/dist/core/bash-spawn-benchmark.js +6 -6
  107. package/dist/core/bundled-agents.d.ts +3 -0
  108. package/dist/core/bundled-agents.d.ts.map +1 -0
  109. package/dist/core/bundled-agents.js +252 -0
  110. package/dist/core/bundled-agents.js.map +1 -0
  111. package/dist/core/cache-warm-benchmark.d.ts +2 -2
  112. package/dist/core/cache-warm-benchmark.js +5 -5
  113. package/dist/core/code-retrieval-chunkers.d.ts +27 -0
  114. package/dist/core/code-retrieval-chunkers.d.ts.map +1 -0
  115. package/dist/core/code-retrieval-chunkers.js +264 -0
  116. package/dist/core/code-retrieval-chunkers.js.map +1 -0
  117. package/dist/core/code-retrieval-chunkers.test.d.ts +2 -0
  118. package/dist/core/code-retrieval-chunkers.test.d.ts.map +1 -0
  119. package/dist/core/code-retrieval-chunkers.test.js +203 -0
  120. package/dist/core/code-retrieval-chunkers.test.js.map +1 -0
  121. package/dist/core/code-retrieval.d.ts +6 -0
  122. package/dist/core/code-retrieval.d.ts.map +1 -1
  123. package/dist/core/code-retrieval.js +38 -1
  124. package/dist/core/code-retrieval.js.map +1 -1
  125. package/dist/core/compaction/active-context.test.js +21 -0
  126. package/dist/core/compaction/active-context.test.js.map +1 -1
  127. package/dist/core/compaction/compactor.d.ts +108 -8
  128. package/dist/core/compaction/compactor.d.ts.map +1 -1
  129. package/dist/core/compaction/compactor.js +423 -119
  130. package/dist/core/compaction/compactor.js.map +1 -1
  131. package/dist/core/compaction/compactor.test.js +360 -8
  132. package/dist/core/compaction/compactor.test.js.map +1 -1
  133. package/dist/core/compaction/policy.d.ts +17 -0
  134. package/dist/core/compaction/policy.d.ts.map +1 -0
  135. package/dist/core/compaction/policy.js +22 -0
  136. package/dist/core/compaction/policy.js.map +1 -0
  137. package/dist/core/compaction/policy.test.d.ts +2 -0
  138. package/dist/core/compaction/policy.test.d.ts.map +1 -0
  139. package/dist/core/compaction/policy.test.js +35 -0
  140. package/dist/core/compaction/policy.test.js.map +1 -0
  141. package/dist/core/compaction/query-aware-selector.d.ts +33 -0
  142. package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
  143. package/dist/core/compaction/query-aware-selector.js +202 -0
  144. package/dist/core/compaction/query-aware-selector.js.map +1 -0
  145. package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
  146. package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
  147. package/dist/core/compaction/query-aware-selector.test.js +96 -0
  148. package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
  149. package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
  150. package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
  151. package/dist/core/compaction/tool-result-pruner.js +98 -26
  152. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  153. package/dist/core/compaction/tool-result-pruner.test.js +178 -6
  154. package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
  155. package/dist/core/encode-cwd.test.js +1 -1
  156. package/dist/core/event-bus.d.ts +23 -0
  157. package/dist/core/event-bus.d.ts.map +1 -1
  158. package/dist/core/event-bus.js +10 -0
  159. package/dist/core/event-bus.js.map +1 -1
  160. package/dist/core/extensions/loader.d.ts.map +1 -1
  161. package/dist/core/extensions/loader.js +28 -8
  162. package/dist/core/extensions/loader.js.map +1 -1
  163. package/dist/core/extensions/plugin-bundles.d.ts +23 -0
  164. package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
  165. package/dist/core/extensions/plugin-bundles.js +228 -0
  166. package/dist/core/extensions/plugin-bundles.js.map +1 -0
  167. package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
  168. package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
  169. package/dist/core/extensions/plugin-bundles.test.js +110 -0
  170. package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
  171. package/dist/core/fast-apply-benchmark.d.ts +2 -2
  172. package/dist/core/fast-apply-benchmark.js +6 -6
  173. package/dist/core/foreign-session-import.d.ts +120 -0
  174. package/dist/core/foreign-session-import.d.ts.map +1 -0
  175. package/dist/core/foreign-session-import.js +598 -0
  176. package/dist/core/foreign-session-import.js.map +1 -0
  177. package/dist/core/foreign-session-import.test.d.ts +2 -0
  178. package/dist/core/foreign-session-import.test.d.ts.map +1 -0
  179. package/dist/core/foreign-session-import.test.js +220 -0
  180. package/dist/core/foreign-session-import.test.js.map +1 -0
  181. package/dist/core/foreign-transcript-blocks.d.ts +51 -0
  182. package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
  183. package/dist/core/foreign-transcript-blocks.js +151 -0
  184. package/dist/core/foreign-transcript-blocks.js.map +1 -0
  185. package/dist/core/goal-lifecycle-smoke.test.js +2 -2
  186. package/dist/core/goal-store.js +1 -1
  187. package/dist/core/goal-store.test.js +2 -2
  188. package/dist/core/goal-verifier.test.js +2 -2
  189. package/dist/core/goal-worker-dev-server-lifecycle.test.js +1 -1
  190. package/dist/core/goal-worker.test.js +2 -2
  191. package/dist/core/hashline-edit-benchmark.d.ts +2 -2
  192. package/dist/core/hashline-edit-benchmark.js +7 -7
  193. package/dist/core/ideal-review.d.ts +20 -1
  194. package/dist/core/ideal-review.d.ts.map +1 -1
  195. package/dist/core/ideal-review.js +37 -2
  196. package/dist/core/ideal-review.js.map +1 -1
  197. package/dist/core/ideal-review.test.js +99 -3
  198. package/dist/core/ideal-review.test.js.map +1 -1
  199. package/dist/core/local-endpoint-store.test.js +1 -1
  200. package/dist/core/logger.d.ts +1 -1
  201. package/dist/core/logger.js +2 -2
  202. package/dist/core/logger.js.map +1 -1
  203. package/dist/core/loop-breaker.d.ts.map +1 -1
  204. package/dist/core/loop-breaker.js +1 -0
  205. package/dist/core/loop-breaker.js.map +1 -1
  206. package/dist/core/lsp/client-navigation.test.d.ts +2 -0
  207. package/dist/core/lsp/client-navigation.test.d.ts.map +1 -0
  208. package/dist/core/lsp/client-navigation.test.js +135 -0
  209. package/dist/core/lsp/client-navigation.test.js.map +1 -0
  210. package/dist/core/lsp/client.d.ts +84 -0
  211. package/dist/core/lsp/client.d.ts.map +1 -1
  212. package/dist/core/lsp/client.js +190 -3
  213. package/dist/core/lsp/client.js.map +1 -1
  214. package/dist/core/lsp/integration.test.js +2 -2
  215. package/dist/core/lsp/manager-navigation.test.d.ts +2 -0
  216. package/dist/core/lsp/manager-navigation.test.d.ts.map +1 -0
  217. package/dist/core/lsp/manager-navigation.test.js +121 -0
  218. package/dist/core/lsp/manager-navigation.test.js.map +1 -0
  219. package/dist/core/lsp/manager.d.ts +81 -9
  220. package/dist/core/lsp/manager.d.ts.map +1 -1
  221. package/dist/core/lsp/manager.js +183 -64
  222. package/dist/core/lsp/manager.js.map +1 -1
  223. package/dist/core/lsp/manager.test.js +18 -0
  224. package/dist/core/lsp/manager.test.js.map +1 -1
  225. package/dist/core/lsp/pool.d.ts +113 -0
  226. package/dist/core/lsp/pool.d.ts.map +1 -0
  227. package/dist/core/lsp/pool.js +255 -0
  228. package/dist/core/lsp/pool.js.map +1 -0
  229. package/dist/core/lsp/pool.test.d.ts +2 -0
  230. package/dist/core/lsp/pool.test.d.ts.map +1 -0
  231. package/dist/core/lsp/pool.test.js +286 -0
  232. package/dist/core/lsp/pool.test.js.map +1 -0
  233. package/dist/core/lsp/servers.d.ts.map +1 -1
  234. package/dist/core/lsp/servers.js +43 -3
  235. package/dist/core/lsp/servers.js.map +1 -1
  236. package/dist/core/lsp/servers.test.d.ts +2 -0
  237. package/dist/core/lsp/servers.test.d.ts.map +1 -0
  238. package/dist/core/lsp/servers.test.js +67 -0
  239. package/dist/core/lsp/servers.test.js.map +1 -0
  240. package/dist/core/lsp/windows.test.js +6 -18
  241. package/dist/core/lsp/windows.test.js.map +1 -1
  242. package/dist/core/mcp/catalog-cache.d.ts +63 -0
  243. package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
  244. package/dist/core/mcp/catalog-cache.js +163 -0
  245. package/dist/core/mcp/catalog-cache.js.map +1 -0
  246. package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
  247. package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
  248. package/dist/core/mcp/catalog-cache.test.js +99 -0
  249. package/dist/core/mcp/catalog-cache.test.js.map +1 -0
  250. package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
  251. package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
  252. package/dist/core/mcp/client-session-recovery.test.js +231 -0
  253. package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
  254. package/dist/core/mcp/client.d.ts +158 -0
  255. package/dist/core/mcp/client.d.ts.map +1 -1
  256. package/dist/core/mcp/client.js +478 -45
  257. package/dist/core/mcp/client.js.map +1 -1
  258. package/dist/core/mcp/content.d.ts +19 -0
  259. package/dist/core/mcp/content.d.ts.map +1 -0
  260. package/dist/core/mcp/content.js +172 -0
  261. package/dist/core/mcp/content.js.map +1 -0
  262. package/dist/core/mcp/content.test.d.ts +2 -0
  263. package/dist/core/mcp/content.test.d.ts.map +1 -0
  264. package/dist/core/mcp/content.test.js +135 -0
  265. package/dist/core/mcp/content.test.js.map +1 -0
  266. package/dist/core/mcp/deferred-catalog.d.ts +1 -1
  267. package/dist/core/mcp/deferred-catalog.js +1 -1
  268. package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
  269. package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
  270. package/dist/core/mcp/elicitation-bridge.js +52 -0
  271. package/dist/core/mcp/elicitation-bridge.js.map +1 -0
  272. package/dist/core/mcp/elicitation.test.d.ts +2 -0
  273. package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
  274. package/dist/core/mcp/elicitation.test.js +158 -0
  275. package/dist/core/mcp/elicitation.test.js.map +1 -0
  276. package/dist/core/mcp/index.d.ts +5 -1
  277. package/dist/core/mcp/index.d.ts.map +1 -1
  278. package/dist/core/mcp/index.js +2 -0
  279. package/dist/core/mcp/index.js.map +1 -1
  280. package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
  281. package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
  282. package/dist/core/mcp/legacy-negotiation.test.js +80 -0
  283. package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
  284. package/dist/core/mcp/oauth-provider.d.ts +1 -2
  285. package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
  286. package/dist/core/mcp/oauth-provider.js +6 -2
  287. package/dist/core/mcp/oauth-provider.js.map +1 -1
  288. package/dist/core/mcp/oauth-store.d.ts +1 -1
  289. package/dist/core/mcp/oauth-store.d.ts.map +1 -1
  290. package/dist/core/mcp/oauth-store.js.map +1 -1
  291. package/dist/core/mcp/oauth-store.test.js +2 -2
  292. package/dist/core/mcp/resolve-stdio.test.js +5 -5
  293. package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
  294. package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
  295. package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
  296. package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
  297. package/dist/core/mcp/shared-pool.d.ts +158 -0
  298. package/dist/core/mcp/shared-pool.d.ts.map +1 -0
  299. package/dist/core/mcp/shared-pool.js +235 -0
  300. package/dist/core/mcp/shared-pool.js.map +1 -0
  301. package/dist/core/mcp/shared-pool.test.d.ts +2 -0
  302. package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
  303. package/dist/core/mcp/shared-pool.test.js +425 -0
  304. package/dist/core/mcp/shared-pool.test.js.map +1 -0
  305. package/dist/core/mcp/store.test.js +5 -5
  306. package/dist/core/mcp/types.d.ts +12 -0
  307. package/dist/core/mcp/types.d.ts.map +1 -1
  308. package/dist/core/mcp/when-connected.test.d.ts +2 -0
  309. package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
  310. package/dist/core/mcp/when-connected.test.js +46 -0
  311. package/dist/core/mcp/when-connected.test.js.map +1 -0
  312. package/dist/core/nolan-context.d.ts.map +1 -1
  313. package/dist/core/nolan-context.js +8 -0
  314. package/dist/core/nolan-context.js.map +1 -1
  315. package/dist/core/nolan-context.test.js +40 -0
  316. package/dist/core/nolan-context.test.js.map +1 -1
  317. package/dist/core/nolan-model.test.js +1 -1
  318. package/dist/core/nolan-prompt.js +8 -5
  319. package/dist/core/nolan-prompt.js.map +1 -1
  320. package/dist/core/nolan-prompt.test.js +5 -0
  321. package/dist/core/nolan-prompt.test.js.map +1 -1
  322. package/dist/core/oauth/xai.d.ts +2 -0
  323. package/dist/core/oauth/xai.d.ts.map +1 -0
  324. package/dist/core/oauth/xai.js +4 -0
  325. package/dist/core/oauth/xai.js.map +1 -0
  326. package/dist/core/persistent-shell.d.ts +3 -2
  327. package/dist/core/persistent-shell.d.ts.map +1 -1
  328. package/dist/core/persistent-shell.js +11 -4
  329. package/dist/core/persistent-shell.js.map +1 -1
  330. package/dist/core/persistent-shell.test.js +6 -6
  331. package/dist/core/process-gate.d.ts +45 -0
  332. package/dist/core/process-gate.d.ts.map +1 -0
  333. package/dist/core/process-gate.js +79 -0
  334. package/dist/core/process-gate.js.map +1 -0
  335. package/dist/core/process-gate.test.d.ts +2 -0
  336. package/dist/core/process-gate.test.d.ts.map +1 -0
  337. package/dist/core/process-gate.test.js +87 -0
  338. package/dist/core/process-gate.test.js.map +1 -0
  339. package/dist/core/process-manager-dev-server-repro.test.js +51 -12
  340. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  341. package/dist/core/process-manager-notifications.test.d.ts +2 -0
  342. package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
  343. package/dist/core/process-manager-notifications.test.js +227 -0
  344. package/dist/core/process-manager-notifications.test.js.map +1 -0
  345. package/dist/core/process-manager.d.ts +72 -1
  346. package/dist/core/process-manager.d.ts.map +1 -1
  347. package/dist/core/process-manager.js +247 -4
  348. package/dist/core/process-manager.js.map +1 -1
  349. package/dist/core/progress/git-xp.test.js +2 -2
  350. package/dist/core/progress/ranks.d.ts +14 -3
  351. package/dist/core/progress/ranks.d.ts.map +1 -1
  352. package/dist/core/progress/ranks.js +181 -12
  353. package/dist/core/progress/ranks.js.map +1 -1
  354. package/dist/core/progress/ranks.test.js +48 -7
  355. package/dist/core/progress/ranks.test.js.map +1 -1
  356. package/dist/core/progress/rebuild.test.js +1 -1
  357. package/dist/core/progress/store.test.js +1 -1
  358. package/dist/core/progress/types.d.ts +2 -1
  359. package/dist/core/progress/types.d.ts.map +1 -1
  360. package/dist/core/project-discovery.d.ts +31 -6
  361. package/dist/core/project-discovery.d.ts.map +1 -1
  362. package/dist/core/project-discovery.js +349 -79
  363. package/dist/core/project-discovery.js.map +1 -1
  364. package/dist/core/project-discovery.test.js +128 -4
  365. package/dist/core/project-discovery.test.js.map +1 -1
  366. package/dist/core/prompt-commands.js +3 -3
  367. package/dist/core/radio.d.ts +1 -1
  368. package/dist/core/radio.d.ts.map +1 -1
  369. package/dist/core/radio.js +90 -6
  370. package/dist/core/radio.js.map +1 -1
  371. package/dist/core/radio.test.js +15 -2
  372. package/dist/core/radio.test.js.map +1 -1
  373. package/dist/core/regrounding.d.ts.map +1 -1
  374. package/dist/core/regrounding.js +1 -0
  375. package/dist/core/regrounding.js.map +1 -1
  376. package/dist/core/run-claim.d.ts +37 -0
  377. package/dist/core/run-claim.d.ts.map +1 -0
  378. package/dist/core/run-claim.js +46 -0
  379. package/dist/core/run-claim.js.map +1 -0
  380. package/dist/core/run-claim.test.d.ts +2 -0
  381. package/dist/core/run-claim.test.d.ts.map +1 -0
  382. package/dist/core/run-claim.test.js +107 -0
  383. package/dist/core/run-claim.test.js.map +1 -0
  384. package/dist/core/run-journal.test.d.ts +2 -0
  385. package/dist/core/run-journal.test.d.ts.map +1 -0
  386. package/dist/core/run-journal.test.js +195 -0
  387. package/dist/core/run-journal.test.js.map +1 -0
  388. package/dist/core/run-lifecycle.d.ts +22 -3
  389. package/dist/core/run-lifecycle.d.ts.map +1 -1
  390. package/dist/core/run-lifecycle.js +12 -3
  391. package/dist/core/run-lifecycle.js.map +1 -1
  392. package/dist/core/sandbox-domains.d.ts +14 -0
  393. package/dist/core/sandbox-domains.d.ts.map +1 -0
  394. package/dist/core/sandbox-domains.js +76 -0
  395. package/dist/core/sandbox-domains.js.map +1 -0
  396. package/dist/core/sandbox-feedback.d.ts +22 -0
  397. package/dist/core/sandbox-feedback.d.ts.map +1 -0
  398. package/dist/core/sandbox-feedback.js +69 -0
  399. package/dist/core/sandbox-feedback.js.map +1 -0
  400. package/dist/core/sandbox-feedback.test.d.ts +2 -0
  401. package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
  402. package/dist/core/sandbox-feedback.test.js +32 -0
  403. package/dist/core/sandbox-feedback.test.js.map +1 -0
  404. package/dist/core/sandbox.d.ts +91 -0
  405. package/dist/core/sandbox.d.ts.map +1 -0
  406. package/dist/core/sandbox.js +254 -0
  407. package/dist/core/sandbox.js.map +1 -0
  408. package/dist/core/sandbox.test.d.ts +2 -0
  409. package/dist/core/sandbox.test.d.ts.map +1 -0
  410. package/dist/core/sandbox.test.js +157 -0
  411. package/dist/core/sandbox.test.js.map +1 -0
  412. package/dist/core/semantic-search-benchmark.d.ts +2 -2
  413. package/dist/core/semantic-search-benchmark.js +5 -5
  414. package/dist/core/session-compaction.d.ts +6 -0
  415. package/dist/core/session-compaction.d.ts.map +1 -1
  416. package/dist/core/session-compaction.js +11 -1
  417. package/dist/core/session-compaction.js.map +1 -1
  418. package/dist/core/session-compaction.test.js +19 -2
  419. package/dist/core/session-compaction.test.js.map +1 -1
  420. package/dist/core/session-export.d.ts.map +1 -1
  421. package/dist/core/session-export.js +12 -4
  422. package/dist/core/session-export.js.map +1 -1
  423. package/dist/core/session-history.d.ts +95 -6
  424. package/dist/core/session-history.d.ts.map +1 -1
  425. package/dist/core/session-history.js +261 -17
  426. package/dist/core/session-history.js.map +1 -1
  427. package/dist/core/session-history.test.js +173 -2
  428. package/dist/core/session-history.test.js.map +1 -1
  429. package/dist/core/session-manager.d.ts +223 -6
  430. package/dist/core/session-manager.d.ts.map +1 -1
  431. package/dist/core/session-manager.js +579 -74
  432. package/dist/core/session-manager.js.map +1 -1
  433. package/dist/core/session-manager.test.js +154 -10
  434. package/dist/core/session-manager.test.js.map +1 -1
  435. package/dist/core/session-notification-visibility.test.d.ts +2 -0
  436. package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
  437. package/dist/core/session-notification-visibility.test.js +113 -0
  438. package/dist/core/session-notification-visibility.test.js.map +1 -0
  439. package/dist/core/session-preview.d.ts +5 -4
  440. package/dist/core/session-preview.d.ts.map +1 -1
  441. package/dist/core/session-preview.js +12 -5
  442. package/dist/core/session-preview.js.map +1 -1
  443. package/dist/core/session-restore-display.test.js +25 -0
  444. package/dist/core/session-restore-display.test.js.map +1 -1
  445. package/dist/core/session-storage-fd-leak.test.d.ts +2 -0
  446. package/dist/core/session-storage-fd-leak.test.d.ts.map +1 -0
  447. package/dist/core/session-storage-fd-leak.test.js +116 -0
  448. package/dist/core/session-storage-fd-leak.test.js.map +1 -0
  449. package/dist/core/session-storage.d.ts +22 -2
  450. package/dist/core/session-storage.d.ts.map +1 -1
  451. package/dist/core/session-storage.js +72 -9
  452. package/dist/core/session-storage.js.map +1 -1
  453. package/dist/core/session-storage.test.js +8 -3
  454. package/dist/core/session-storage.test.js.map +1 -1
  455. package/dist/core/settings-manager.d.ts +10 -2
  456. package/dist/core/settings-manager.d.ts.map +1 -1
  457. package/dist/core/settings-manager.js +36 -1
  458. package/dist/core/settings-manager.js.map +1 -1
  459. package/dist/core/shell-path.js +1 -1
  460. package/dist/core/shell.d.ts +1 -1
  461. package/dist/core/shell.js +2 -2
  462. package/dist/core/shell.test.js +2 -2
  463. package/dist/core/shutdown.d.ts +71 -0
  464. package/dist/core/shutdown.d.ts.map +1 -0
  465. package/dist/core/shutdown.js +144 -0
  466. package/dist/core/shutdown.js.map +1 -0
  467. package/dist/core/shutdown.test.d.ts +2 -0
  468. package/dist/core/shutdown.test.d.ts.map +1 -0
  469. package/dist/core/shutdown.test.js +160 -0
  470. package/dist/core/shutdown.test.js.map +1 -0
  471. package/dist/core/sidecar-error-reporter.js +1 -1
  472. package/dist/core/sidecar-error-reporter.test.js +2 -2
  473. package/dist/core/skills-routing.test.js +96 -0
  474. package/dist/core/skills-routing.test.js.map +1 -1
  475. package/dist/core/skills.d.ts.map +1 -1
  476. package/dist/core/skills.js +3 -0
  477. package/dist/core/skills.js.map +1 -1
  478. package/dist/core/steering.d.ts +12 -0
  479. package/dist/core/steering.d.ts.map +1 -1
  480. package/dist/core/steering.js +16 -0
  481. package/dist/core/steering.js.map +1 -1
  482. package/dist/core/subagent-manager.d.ts +22 -0
  483. package/dist/core/subagent-manager.d.ts.map +1 -1
  484. package/dist/core/subagent-manager.js +84 -13
  485. package/dist/core/subagent-manager.js.map +1 -1
  486. package/dist/core/subagent-manager.test.js +57 -15
  487. package/dist/core/subagent-manager.test.js.map +1 -1
  488. package/dist/core/subagent-store.test.js +3 -3
  489. package/dist/core/task-store.js +1 -1
  490. package/dist/core/verification-evidence.d.ts +17 -0
  491. package/dist/core/verification-evidence.d.ts.map +1 -0
  492. package/dist/core/verification-evidence.js +283 -0
  493. package/dist/core/verification-evidence.js.map +1 -0
  494. package/dist/core/verification-evidence.test.d.ts +2 -0
  495. package/dist/core/verification-evidence.test.d.ts.map +1 -0
  496. package/dist/core/verification-evidence.test.js +103 -0
  497. package/dist/core/verification-evidence.test.js.map +1 -0
  498. package/dist/index.js +1 -1
  499. package/dist/index.js.map +1 -1
  500. package/dist/interactive.d.ts.map +1 -1
  501. package/dist/interactive.js +17 -3
  502. package/dist/interactive.js.map +1 -1
  503. package/dist/modes/acp-mode.d.ts +122 -0
  504. package/dist/modes/acp-mode.d.ts.map +1 -0
  505. package/dist/modes/acp-mode.js +1366 -0
  506. package/dist/modes/acp-mode.js.map +1 -0
  507. package/dist/modes/acp-mode.test.d.ts +2 -0
  508. package/dist/modes/acp-mode.test.d.ts.map +1 -0
  509. package/dist/modes/acp-mode.test.js +1065 -0
  510. package/dist/modes/acp-mode.test.js.map +1 -0
  511. package/dist/modes/agent-home-mode.d.ts.map +1 -1
  512. package/dist/modes/agent-home-mode.js +15 -11
  513. package/dist/modes/agent-home-mode.js.map +1 -1
  514. package/dist/modes/index.d.ts +1 -0
  515. package/dist/modes/index.d.ts.map +1 -1
  516. package/dist/modes/index.js +1 -0
  517. package/dist/modes/index.js.map +1 -1
  518. package/dist/modes/json-mode.d.ts +36 -0
  519. package/dist/modes/json-mode.d.ts.map +1 -1
  520. package/dist/modes/json-mode.js +39 -0
  521. package/dist/modes/json-mode.js.map +1 -1
  522. package/dist/modes/json-mode.test.d.ts +2 -0
  523. package/dist/modes/json-mode.test.d.ts.map +1 -0
  524. package/dist/modes/json-mode.test.js +84 -0
  525. package/dist/modes/json-mode.test.js.map +1 -0
  526. package/dist/modes/serve-mode.d.ts +1 -1
  527. package/dist/modes/serve-mode.d.ts.map +1 -1
  528. package/dist/modes/serve-mode.js +14 -9
  529. package/dist/modes/serve-mode.js.map +1 -1
  530. package/dist/modes/subagent-worker-mode.d.ts +8 -0
  531. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  532. package/dist/modes/subagent-worker-mode.js +91 -11
  533. package/dist/modes/subagent-worker-mode.js.map +1 -1
  534. package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
  535. package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
  536. package/dist/modes/subagent-worker-mode.test.js +52 -0
  537. package/dist/modes/subagent-worker-mode.test.js.map +1 -0
  538. package/dist/session.d.ts +29 -0
  539. package/dist/session.d.ts.map +1 -1
  540. package/dist/session.js +41 -0
  541. package/dist/session.js.map +1 -1
  542. package/dist/session.test.js +1 -1
  543. package/dist/system-prompt.d.ts +37 -1
  544. package/dist/system-prompt.d.ts.map +1 -1
  545. package/dist/system-prompt.js +123 -11
  546. package/dist/system-prompt.js.map +1 -1
  547. package/dist/system-prompt.test.js +90 -11
  548. package/dist/system-prompt.test.js.map +1 -1
  549. package/dist/system-prompt.tiering.test.d.ts +2 -0
  550. package/dist/system-prompt.tiering.test.d.ts.map +1 -0
  551. package/dist/system-prompt.tiering.test.js +105 -0
  552. package/dist/system-prompt.tiering.test.js.map +1 -0
  553. package/dist/tools/bash.d.ts +2 -1
  554. package/dist/tools/bash.d.ts.map +1 -1
  555. package/dist/tools/bash.js +55 -16
  556. package/dist/tools/bash.js.map +1 -1
  557. package/dist/tools/code-nav.d.ts +30 -0
  558. package/dist/tools/code-nav.d.ts.map +1 -0
  559. package/dist/tools/code-nav.js +320 -0
  560. package/dist/tools/code-nav.js.map +1 -0
  561. package/dist/tools/code-nav.test.d.ts +2 -0
  562. package/dist/tools/code-nav.test.d.ts.map +1 -0
  563. package/dist/tools/code-nav.test.js +136 -0
  564. package/dist/tools/code-nav.test.js.map +1 -0
  565. package/dist/tools/edit.test.js +1 -1
  566. package/dist/tools/generate-image.d.ts +1 -1
  567. package/dist/tools/goals.d.ts +4 -4
  568. package/dist/tools/goals.test.js +2 -2
  569. package/dist/tools/grep-parity.test.d.ts +2 -0
  570. package/dist/tools/grep-parity.test.d.ts.map +1 -0
  571. package/dist/tools/grep-parity.test.js +92 -0
  572. package/dist/tools/grep-parity.test.js.map +1 -0
  573. package/dist/tools/grep-recall.test.d.ts +2 -0
  574. package/dist/tools/grep-recall.test.d.ts.map +1 -0
  575. package/dist/tools/grep-recall.test.js +114 -0
  576. package/dist/tools/grep-recall.test.js.map +1 -0
  577. package/dist/tools/grep-spawn.test.d.ts +2 -0
  578. package/dist/tools/grep-spawn.test.d.ts.map +1 -0
  579. package/dist/tools/grep-spawn.test.js +66 -0
  580. package/dist/tools/grep-spawn.test.js.map +1 -0
  581. package/dist/tools/grep.d.ts +13 -3
  582. package/dist/tools/grep.d.ts.map +1 -1
  583. package/dist/tools/grep.js +342 -41
  584. package/dist/tools/grep.js.map +1 -1
  585. package/dist/tools/grep.test.js +17 -14
  586. package/dist/tools/grep.test.js.map +1 -1
  587. package/dist/tools/index.d.ts +20 -2
  588. package/dist/tools/index.d.ts.map +1 -1
  589. package/dist/tools/index.js +28 -18
  590. package/dist/tools/index.js.map +1 -1
  591. package/dist/tools/index.test.d.ts +2 -0
  592. package/dist/tools/index.test.d.ts.map +1 -0
  593. package/dist/tools/index.test.js +55 -0
  594. package/dist/tools/index.test.js.map +1 -0
  595. package/dist/tools/plan-mode.test.js +1 -1
  596. package/dist/tools/prompt-hints.d.ts +9 -0
  597. package/dist/tools/prompt-hints.d.ts.map +1 -1
  598. package/dist/tools/prompt-hints.js +61 -8
  599. package/dist/tools/prompt-hints.js.map +1 -1
  600. package/dist/tools/read-only-bash.d.ts +3 -0
  601. package/dist/tools/read-only-bash.d.ts.map +1 -1
  602. package/dist/tools/read-only-bash.js +7 -9
  603. package/dist/tools/read-only-bash.js.map +1 -1
  604. package/dist/tools/safe-env.js +1 -1
  605. package/dist/tools/search-code.d.ts.map +1 -1
  606. package/dist/tools/search-code.js +7 -6
  607. package/dist/tools/search-code.js.map +1 -1
  608. package/dist/tools/search-code.test.js +8 -2
  609. package/dist/tools/search-code.test.js.map +1 -1
  610. package/dist/tools/skill.js +2 -1
  611. package/dist/tools/skill.js.map +1 -1
  612. package/dist/tools/subagent-control.d.ts.map +1 -1
  613. package/dist/tools/subagent-control.js +25 -6
  614. package/dist/tools/subagent-control.js.map +1 -1
  615. package/dist/tools/subagent-control.test.js +34 -1
  616. package/dist/tools/subagent-control.test.js.map +1 -1
  617. package/dist/tools/subagent-shared.d.ts +26 -1
  618. package/dist/tools/subagent-shared.d.ts.map +1 -1
  619. package/dist/tools/subagent-shared.js +40 -7
  620. package/dist/tools/subagent-shared.js.map +1 -1
  621. package/dist/tools/subagent-shared.test.js +43 -9
  622. package/dist/tools/subagent-shared.test.js.map +1 -1
  623. package/dist/tools/subagent.d.ts.map +1 -1
  624. package/dist/tools/subagent.js +54 -16
  625. package/dist/tools/subagent.js.map +1 -1
  626. package/dist/tools/subagent.test.js +68 -3
  627. package/dist/tools/subagent.test.js.map +1 -1
  628. package/dist/tools/task-output.d.ts.map +1 -1
  629. package/dist/tools/task-output.js +3 -1
  630. package/dist/tools/task-output.js.map +1 -1
  631. package/dist/tools/task-send.test.js +19 -10
  632. package/dist/tools/task-send.test.js.map +1 -1
  633. package/dist/tools/tool-search.d.ts +12 -1
  634. package/dist/tools/tool-search.d.ts.map +1 -1
  635. package/dist/tools/tool-search.js +24 -4
  636. package/dist/tools/tool-search.js.map +1 -1
  637. package/dist/tools/tool-search.test.d.ts +2 -0
  638. package/dist/tools/tool-search.test.d.ts.map +1 -0
  639. package/dist/tools/tool-search.test.js +69 -0
  640. package/dist/tools/tool-search.test.js.map +1 -0
  641. package/dist/tools/tool-tiers.d.ts +42 -0
  642. package/dist/tools/tool-tiers.d.ts.map +1 -0
  643. package/dist/tools/tool-tiers.js +78 -0
  644. package/dist/tools/tool-tiers.js.map +1 -0
  645. package/dist/tools/tool-tiers.test.d.ts +2 -0
  646. package/dist/tools/tool-tiers.test.d.ts.map +1 -0
  647. package/dist/tools/tool-tiers.test.js +48 -0
  648. package/dist/tools/tool-tiers.test.js.map +1 -0
  649. package/dist/tools/web-fetch.d.ts +1 -1
  650. package/dist/tools/web-fetch.d.ts.map +1 -1
  651. package/dist/tools/web-fetch.js +5 -2
  652. package/dist/tools/web-fetch.js.map +1 -1
  653. package/dist/tools/write.test.js +1 -1
  654. package/dist/types.d.ts +7 -0
  655. package/dist/types.d.ts.map +1 -1
  656. package/dist/ui/App.d.ts.map +1 -1
  657. package/dist/ui/App.js +27 -1
  658. package/dist/ui/App.js.map +1 -1
  659. package/dist/ui/components/Banner.d.ts.map +1 -1
  660. package/dist/ui/components/Banner.js +24 -16
  661. package/dist/ui/components/Banner.js.map +1 -1
  662. package/dist/ui/components/ToolExecution.js +42 -34
  663. package/dist/ui/components/ToolExecution.js.map +1 -1
  664. package/dist/ui/components/force-color.d.ts +2 -0
  665. package/dist/ui/components/force-color.d.ts.map +1 -0
  666. package/dist/ui/components/force-color.js +11 -0
  667. package/dist/ui/components/force-color.js.map +1 -0
  668. package/dist/ui/components/themed-output.test.d.ts +2 -0
  669. package/dist/ui/components/themed-output.test.d.ts.map +1 -0
  670. package/dist/ui/components/themed-output.test.js +111 -0
  671. package/dist/ui/components/themed-output.test.js.map +1 -0
  672. package/dist/ui/error-item.d.ts +1 -1
  673. package/dist/ui/error-item.js +3 -3
  674. package/dist/ui/error-item.js.map +1 -1
  675. package/dist/ui/hooks/useAgentLoop.d.ts +3 -0
  676. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  677. package/dist/ui/hooks/useAgentLoop.js +45 -6
  678. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  679. package/dist/ui/login.js +1 -1
  680. package/dist/ui/login.js.map +1 -1
  681. package/dist/ui/render.d.ts +1 -1
  682. package/dist/ui/render.js +6 -6
  683. package/dist/ui/theme/dark-ansi.json +6 -0
  684. package/dist/ui/theme/dark-daltonized.json +6 -0
  685. package/dist/ui/theme/dark.json +6 -0
  686. package/dist/ui/theme/light-ansi.json +6 -0
  687. package/dist/ui/theme/light-daltonized.json +6 -0
  688. package/dist/ui/theme/light.json +6 -0
  689. package/dist/ui/theme/theme.d.ts +6 -0
  690. package/dist/ui/theme/theme.d.ts.map +1 -1
  691. package/dist/ui/tui-terminal-recorder.test.js +1 -1
  692. package/dist/utils/git.test.js +3 -3
  693. package/dist/utils/github.test.js +3 -3
  694. package/dist/utils/github.test.js.map +1 -1
  695. package/dist/utils/image.d.ts +26 -3
  696. package/dist/utils/image.d.ts.map +1 -1
  697. package/dist/utils/image.js +83 -14
  698. package/dist/utils/image.js.map +1 -1
  699. package/dist/utils/image.test.d.ts +2 -0
  700. package/dist/utils/image.test.d.ts.map +1 -0
  701. package/dist/utils/image.test.js +139 -0
  702. package/dist/utils/image.test.js.map +1 -0
  703. package/package.json +7 -6
@@ -6,39 +6,54 @@ import { PROMPT_COMMANDS, getPromptCommand } from "./prompt-commands.js";
6
6
  import { loadCustomCommands } from "./custom-commands.js";
7
7
  import { SettingsManager } from "./settings-manager.js";
8
8
  import { AuthStorage } from "./auth-storage.js";
9
- import { MOONSHOT_OAUTH_KEY } from "@prestyj/core";
9
+ import { dualAuthProvider } from "@prestyj/core";
10
10
  import { getClaudeCliUserAgent } from "./claude-code-version.js";
11
11
  import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
12
+ import { isGrokCliEndpoint } from "./oauth/xai.js";
12
13
  import { SessionManager, NOLAN_TURN_CUSTOM_KIND, AUTOPILOT_MARKER_CUSTOM_KIND, APP_MARKER_CUSTOM_KIND, } from "./session-manager.js";
13
14
  import { ExtensionLoader } from "./extensions/loader.js";
14
- import { shouldCompact, compact } from "./compaction/compactor.js";
15
+ import { shouldCompact, compact, } from "./compaction/compactor.js";
16
+ import { getHistoryMessageVisibility, remapAnchorForCompaction, stripRecordedPosition, } from "./session-history.js";
17
+ import { sourceFingerprint as computeSourceFingerprint } from "./session-compaction.js";
15
18
  import { getAuthStorageKeys, getContextWindow, getModel, getToolResultCharLimit, MODELS, } from "./model-registry.js";
16
19
  import { discoverSkills } from "./skills.js";
17
20
  import { ensureAppDirs } from "../config.js";
18
- import { buildSystemPrompt } from "../system-prompt.js";
21
+ import { buildSubAgentSystemPrompt, buildSystemPrompt, } from "../system-prompt.js";
19
22
  import { createTools, createWebSearchTool, } from "../tools/index.js";
23
+ import { partitionToolsByTier } from "../tools/tool-tiers.js";
24
+ import { buildProcessCompletionFollowUp } from "./process-gate.js";
20
25
  import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
21
26
  import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
27
+ import { z } from "zod";
22
28
  import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
23
29
  import { DeferredToolCatalog } from "./mcp/deferred-catalog.js";
30
+ import { McpCatalogCache } from "./mcp/catalog-cache.js";
31
+ import { describeDropped, importForeignSession, } from "./foreign-session-import.js";
24
32
  import { createToolSearchTool } from "../tools/tool-search.js";
25
33
  import { log } from "./logger.js";
26
34
  import { setEstimatorModel, calibrateEstimatorFromUsage } from "./compaction/token-estimator.js";
27
35
  import { calculateActiveContextTokens } from "./compaction/active-context.js";
36
+ import { resolveCompactionPolicy } from "./compaction/policy.js";
28
37
  import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
29
38
  import { discoverAgents } from "./agents.js";
30
39
  import { enhancePrompt } from "../utils/prompt-enhancer.js";
31
40
  import { detectProjectStack } from "./language-detector.js";
32
- import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
41
+ import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageEscalationMessage, buildReviewCoverageMessage, MAX_REVIEW_COVERAGE_INJECTIONS, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
33
42
  import { evaluateLoopBreak, buildLoopBreakMessage, CycleDetector, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
34
43
  import { buildRegroundingMessage } from "./regrounding.js";
35
- import { wrapSteeringText, STEERING_PREFIX } from "./steering.js";
44
+ import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
45
+ import { AgentNotificationQueue } from "./agent-notifications.js";
36
46
  import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
37
47
  import { normalizeMessageImages } from "./message-images.js";
38
48
  import crypto from "node:crypto";
39
49
  import fs from "node:fs/promises";
40
50
  import os from "node:os";
41
51
  import path from "node:path";
52
+ /**
53
+ * A run whose tool calls fail more often than this is thrashing, not
54
+ * progressing — refuse to extend its turn budget.
55
+ */
56
+ const TURN_EXTENSION_MAX_FAILURE_RATIO = 0.5;
42
57
  // ── Tool-result policy ─────────────────────────────────────
43
58
  /** Resolve the per-result cap passed to the agent loop for the active transport. */
44
59
  export function resolveSessionToolResultCharLimit(model, provider, accountId) {
@@ -57,6 +72,17 @@ export function resolveSessionTurnToolResultCharLimit(model, provider, accountId
57
72
  const contextChars = getContextWindow(model, { provider, accountId }) * 3.5;
58
73
  return Math.max(100_000, Math.min(Math.floor(contextChars * 0.15), 240_000));
59
74
  }
75
+ /** Marker the compactor prepends to the summary message it injects. */
76
+ /**
77
+ * True when an assistant message ends a turn with tool calls still awaiting
78
+ * their results. Inserting a user message there would orphan the tool_use
79
+ * blocks and the provider rejects the next request.
80
+ */
81
+ function hasUnresolvedToolCalls(message) {
82
+ if (typeof message.content === "string" || !Array.isArray(message.content))
83
+ return false;
84
+ return message.content.some((part) => part.type === "tool_call");
85
+ }
60
86
  // ── Agent Session ──────────────────────────────────────────
61
87
  export class AgentSession {
62
88
  eventBus = new EventBus();
@@ -115,12 +141,20 @@ export class AgentSession {
115
141
  /** Runtime-only suppression while Nolan owns verification in autopilot mode. */
116
142
  idealReviewSuppressed = false;
117
143
  reviewCoverage;
144
+ /** Coverage follow-ups spent this run, capped by MAX_REVIEW_COVERAGE_INJECTIONS. */
145
+ reviewCoverageInjected = 0;
118
146
  /** 0 = none; 1 = first nudge sent; 2 = final stop-and-report injected. */
119
147
  loopBreakInjected = 0;
120
148
  regroundingInjected = false;
149
+ /** Wall-clock start of the current run; scopes the background-process gate. */
150
+ runStartedAt = 0;
151
+ /** Gate injections spent this run, capped by MAX_PROCESS_GATE_INJECTIONS. */
152
+ processGateInjected = 0;
121
153
  compactionOccurred = false;
122
154
  lastCompactionCompacted = false;
123
155
  compactionRetryAfter = 0;
156
+ /** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
157
+ deferredCompactionPending = false;
124
158
  /** Latest provider count, anchored to the assistant response it measured. */
125
159
  providerContext = null;
126
160
  originalRequest = "";
@@ -128,17 +162,39 @@ export class AgentSession {
128
162
  // mid-loop steering boundary (user steering wins over the hooks), mirroring
129
163
  // the TUI's getSteeringMessages. Each entry carries its own attachments so a
130
164
  // user can queue media (images/video/files) mid-run, not just plain text.
165
+ // Each entry carries a stable id so a client can cancel one specific pending
166
+ // message by identity. Index-based removal would race: the queue drains at
167
+ // every turn boundary, so an index captured by the UI can point at a
168
+ // different message (or past the end) by the time the cancel arrives.
131
169
  userQueue = [];
170
+ queueSeq = 0;
132
171
  processManager;
133
172
  lspManager;
134
173
  subAgentManager;
174
+ /**
175
+ * Out-of-band push notifications (finished children, background-process
176
+ * progress). Producers enqueue; `getHookSteeringMessages` drains into the
177
+ * live turn so the agent never has to spend a turn asking.
178
+ */
179
+ notifications = new AgentNotificationQueue();
135
180
  managerAbortSignal;
136
181
  managerAbortHandler = () => {
137
182
  void this.subAgentManager?.interruptAll();
138
183
  };
139
184
  mcpManager;
140
- /** Deferred MCP tools awaiting discovery via tool_search (bench A win). */
185
+ /** Deferred MCP tools awaiting discovery via tool_search. */
141
186
  mcpCatalog;
187
+ /**
188
+ * Built-in tools held in the catalog instead of the live toolset. Their names
189
+ * still render as one-line hints in the prompt's Tools section, so the model
190
+ * can discover and promote them; a promoted name drops out of this list.
191
+ */
192
+ deferredBuiltinToolNames = [];
193
+ /** Live (connected) MCP tools by name — the reconcile target for cached stubs. */
194
+ liveMcpTools = new Map();
195
+ /** Server name for each cached-only tool, so a stub knows what to wait on. */
196
+ cachedMcpToolServers = new Map();
197
+ mcpCatalogCache = new McpCatalogCache();
142
198
  provider;
143
199
  model;
144
200
  cwd;
@@ -151,6 +207,8 @@ export class AgentSession {
151
207
  maxTokens;
152
208
  thinkingLevel;
153
209
  customSystemPrompt;
210
+ /** Sub-agent definition body composed into the standard prompt scaffolding. */
211
+ agentPrompt;
154
212
  /** Stable prompt prefix retained separately from the volatile uncached tail. */
155
213
  baseSystemPrompt = "";
156
214
  /** Shared with the tool layer so plan-mode restrictions read live state. */
@@ -162,6 +220,7 @@ export class AgentSession {
162
220
  /** Extra workspace roots added with `/add-dir` (resolved, de-duplicated). */
163
221
  additionalRoots = [];
164
222
  sessionId = "";
223
+ checkpointGeneration = 0;
165
224
  /** Stable identity shared by compaction and approved-plan checkpoint files. */
166
225
  conversationId = "";
167
226
  /** Original user-authored prompt, retained when internal messages replace history. */
@@ -171,6 +230,15 @@ export class AgentSession {
171
230
  transportSessionId = crypto.randomUUID();
172
231
  sessionPath = "";
173
232
  lastPersistedIndex = 0;
233
+ /**
234
+ * The array `agentLoop` is currently mutating, while a run is in flight.
235
+ *
236
+ * Normally identical to `this.messages`, but a mid-loop compaction rebinds
237
+ * `this.messages` to the compacted result while the loop keeps appending to
238
+ * its own array. Step-boundary flushes must follow the loop's array or they
239
+ * would silently persist nothing for the rest of the run.
240
+ */
241
+ activeLoopMessages = null;
174
242
  /**
175
243
  * Number of non-system messages guaranteed to be in the session file — the
176
244
  * anchor base for transcript markers (Nolan turns, autopilot verdicts, app
@@ -200,6 +268,7 @@ export class AgentSession {
200
268
  this.maxTokens = this.resolveMaxTokens(options.model);
201
269
  this.thinkingLevel = options.thinkingLevel;
202
270
  this.customSystemPrompt = options.systemPrompt;
271
+ this.agentPrompt = options.agentPrompt;
203
272
  }
204
273
  /**
205
274
  * Derive the output-token cap for a model. Follows the active model's
@@ -268,6 +337,19 @@ export class AgentSession {
268
337
  mode: this.settingsManager.get("networkMode"),
269
338
  allow: this.settingsManager.get("networkAllow"),
270
339
  }),
340
+ getSandboxPolicy: () => ({
341
+ mode: this.settingsManager.get("sandboxMode"),
342
+ // networkAllow always applies. Choosing "allowlist" is the user taking
343
+ // over network policy, so the built-in developer defaults drop out and
344
+ // only their hosts remain reachable.
345
+ allowedDomains: this.settingsManager.get("networkAllow"),
346
+ strictDomains: this.settingsManager.get("networkMode") === "allowlist",
347
+ // Same source of truth as getWriteGuardSettings, so bash and the write
348
+ // tool agree on which roots are writable.
349
+ additionalRoots: this.additionalRoots,
350
+ allowOutsideWorkspaceWrites: this.settingsManager.get("allowOutsideWorkspaceWrites"),
351
+ }),
352
+ getUseExternalGrep: () => this.settingsManager.get("grepUseRipgrep"),
271
353
  authStorage: this.authStorage,
272
354
  onFileRead: (filePath) => this.reviewCoverage.recordRead(filePath),
273
355
  onFileMutated: (filePath) => {
@@ -283,8 +365,12 @@ export class AgentSession {
283
365
  getBaseUrl: () => this.baseUrl,
284
366
  getCacheKey: () => this.getPromptCacheKey(),
285
367
  getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
286
- disableAsyncSubagents: this.opts.subagentWorker,
368
+ // A persistent child is already the single allowed fan-out level. Blocking
369
+ // `subagent` here created a timeout sandwich: the nested call could consume
370
+ // the child's entire 10-minute turn budget, discarding all nested results.
371
+ disableSubagents: this.opts.subagentWorker,
287
372
  onSubAgentState: (snapshot) => this.eventBus.emit("subagent_state", snapshot),
373
+ notifications: this.notifications,
288
374
  // Plan mode: only wired when the host supplies callbacks. The ref is
289
375
  // shared so bash/edit/write enforce read-only restrictions live.
290
376
  ...(this.opts.onEnterPlan || this.opts.onExitPlan
@@ -301,39 +387,45 @@ export class AgentSession {
301
387
  // a hallucinated call can't mutate the repo — and buildSystemPrompt below is
302
388
  // fed the same filtered names so the Tools section matches exactly.
303
389
  this.tools = this.opts.allowedTools ? tools.filter((t) => this.isToolAllowed(t.name)) : tools;
390
+ // Tier the built-ins: rarely reached schemas move into the tool_search
391
+ // catalog and cost one hint line each instead of a full parameter schema on
392
+ // every request. Allow-listed sessions keep the eager path — their fixed
393
+ // tool expectations predate the catalog, and tool_search isn't allow-listed.
394
+ if (!this.opts.allowedTools && this.settingsManager.get("deferredBuiltinTools")) {
395
+ const { core, deferred } = partitionToolsByTier(this.tools);
396
+ if (deferred.length > 0) {
397
+ // Append-only: `core` preserves the original relative order and
398
+ // tool_search is pushed after it, so the serialized tool block that
399
+ // sits inside the cached prefix stays byte-stable across turns.
400
+ this.tools = core;
401
+ this.deferredBuiltinToolNames = deferred.map((t) => t.name);
402
+ this.mcpCatalog ??= new DeferredToolCatalog();
403
+ this.mcpCatalog.add(deferred);
404
+ this.ensureToolSearchTool();
405
+ }
406
+ }
304
407
  this.rebuildReadTool = rebuildReadTool;
305
408
  this.processManager = processManager;
306
409
  this.lspManager = lspManager;
307
410
  this.subAgentManager = subAgentManager;
308
411
  this.bindManagerCancellation(this.opts.signal);
309
- // Connect MCP servers (non-blocking — failures are logged and skipped).
310
- // Child sessions (subagents and goal workers run via `--json` mode) get the
311
- // provider defaults only — NOT user-configured servers. The defaults now
312
- // resolve kencode-search to the locally installed bin instead of a flaky
313
- // `npx` cold-spawn, so research tools are reliable here. We deliberately
314
- // skip user servers: a single `/expand` fans out 5 parallel subagents, and
315
- // cold-spawning every user MCP server (e.g. npx-based remotes) per
316
- // short-lived child would be slow and could hammer rate-limited endpoints.
317
- //
318
- // The connect attempt itself can block for up to the per-server connect
319
- // timeout (~30s) — a slow stdio server such as a first-run
320
- // `npx -y @playwright/mcp` download stalls here. When the host gates its own
321
- // readiness on initialize() (the ezcoder-app sidecar can't emit its
322
- // listening handshake until this resolves), `backgroundMcpConnect` moves the
323
- // connect off the critical path so the session becomes usable immediately
324
- // and tools are appended whenever the servers come up.
325
- this.mcpManager = new MCPClientManager();
412
+ // Connect MCP servers. Child sessions skip user-configured servers to avoid
413
+ // cold-spawning every server per short-lived worker. Background connection
414
+ // keeps slow first-run stdio servers off the host readiness path.
415
+ this.mcpManager = new MCPClientManager({
416
+ catalogCache: this.mcpCatalogCache,
417
+ modernProtocol: this.settingsManager.get("mcpModernProtocol"),
418
+ onElicit: this.opts.onMcpElicit,
419
+ });
326
420
  if (this.opts.backgroundMcpConnect) {
327
421
  void this.connectMcpServers();
328
422
  }
329
423
  else {
330
424
  await this.connectMcpServers();
331
425
  }
332
- const basePrompt = this.customSystemPrompt ??
333
- (await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment()));
426
+ const basePrompt = await this.buildBasePrompt(false, undefined);
334
427
  this.baseSystemPrompt = basePrompt;
335
428
  this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
336
- this.syncUltraOrchestrationPrompt();
337
429
  // Load or create session. Transient sessions (subagent spawns) never
338
430
  // touch the session store — sessionPath stays empty and persistMessage
339
431
  // is a no-op so their transcripts can't pollute `ezcoder continue`.
@@ -486,6 +578,11 @@ export class AgentSession {
486
578
  if (this.opts.allowedTools && mcpWhitelist) {
487
579
  servers = servers.filter((s) => mcpWhitelist.includes(s.name));
488
580
  }
581
+ // Seed the catalog from the on-disk cache BEFORE connecting. With
582
+ // `backgroundMcpConnect` the first turns would otherwise run against an
583
+ // empty catalog and tool_search would answer "the catalog is empty" for
584
+ // capabilities that genuinely exist — a wrong answer, not a slow one.
585
+ await this.seedMcpCatalogFromCache(servers);
489
586
  const connected = await this.mcpManager.connectAll(servers);
490
587
  // Defense-in-depth: even from a whitelisted server, only push tools that
491
588
  // pass the allow-list (no-op when there's no allow-list).
@@ -511,8 +608,8 @@ export class AgentSession {
511
608
  }
512
609
  /**
513
610
  * Route freshly connected MCP tools: deferred into the tool_search catalog
514
- * (default — keeps ~8k tokens of schema out of every cache-miss turn, see
515
- * bench/RESULTS.md bench A) or pushed eagerly when the user opted out.
611
+ * (default — keeps ~8k tokens of schema out of every cache-miss turn) or
612
+ * pushed eagerly when the user opted out.
516
613
  * Allow-listed sessions (Nolan) always get the eager path — their fixed tool
517
614
  * expectations predate the catalog, and tool_search isn't allow-listed.
518
615
  * Promotion pushes onto the live `this.tools` array the running agent loop
@@ -521,23 +618,149 @@ export class AgentSession {
521
618
  addMcpTools(mcpTools) {
522
619
  if (mcpTools.length === 0)
523
620
  return;
621
+ for (const tool of mcpTools) {
622
+ this.liveMcpTools.set(tool.name, tool);
623
+ this.cachedMcpToolServers.delete(tool.name);
624
+ }
524
625
  const defer = !this.opts.allowedTools && this.settingsManager.get("deferredMcpTools");
525
626
  if (!defer) {
526
- this.tools.push(...mcpTools);
627
+ this.replaceOrPushTools(mcpTools);
527
628
  return;
528
629
  }
529
630
  this.mcpCatalog ??= new DeferredToolCatalog();
631
+ // `add` is name-keyed, so live definitions replace cached stubs in place.
530
632
  this.mcpCatalog.add(mcpTools);
531
- if (!this.tools.some((t) => t.name === "tool_search")) {
532
- this.tools.push(createToolSearchTool(this.mcpCatalog, (promoted) => {
533
- this.tools.push(...promoted);
534
- }));
633
+ // A stub the model already promoted lives in `this.tools`; swap it for the
634
+ // live tool so later calls dispatch directly instead of through the stub.
635
+ this.replaceLivePromotedTools(mcpTools);
636
+ this.ensureToolSearchTool();
637
+ }
638
+ /**
639
+ * Register `tool_search` once. Promotion of a cached-only entry waits for its
640
+ * server so the model is told immediately when that capability turns out to
641
+ * be unreachable, instead of promoting a tool that fails on first call.
642
+ *
643
+ * The catalog is created on demand rather than required up front: deferred
644
+ * built-in tools populate it with zero MCP servers connected, so gating
645
+ * registration on an existing catalog would leave those tools unreachable.
646
+ */
647
+ ensureToolSearchTool() {
648
+ this.mcpCatalog ??= new DeferredToolCatalog();
649
+ if (this.tools.some((t) => t.name === "tool_search"))
650
+ return;
651
+ this.tools.push(createToolSearchTool(this.mcpCatalog, (promoted) => {
652
+ this.tools.push(...promoted);
653
+ }, async (toolName) => {
654
+ if (this.liveMcpTools.has(toolName))
655
+ return undefined;
656
+ const serverName = this.cachedMcpToolServers.get(toolName);
657
+ if (!serverName)
658
+ return undefined;
659
+ const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
660
+ ok: false,
661
+ error: "MCP is disabled for this session",
662
+ };
663
+ return outcome.ok
664
+ ? { serverName, ok: true }
665
+ : { serverName, ok: false, error: outcome.error };
666
+ }));
667
+ }
668
+ /** Append tools, replacing any same-named entry (cached stub → live tool). */
669
+ replaceOrPushTools(tools) {
670
+ for (const tool of tools) {
671
+ const index = this.tools.findIndex((t) => t.name === tool.name);
672
+ if (index >= 0)
673
+ this.tools[index] = tool;
674
+ else
675
+ this.tools.push(tool);
676
+ }
677
+ }
678
+ /** Swap already-promoted cached stubs for their live equivalents, in place. */
679
+ replaceLivePromotedTools(tools) {
680
+ for (const tool of tools) {
681
+ const index = this.tools.findIndex((t) => t.name === tool.name);
682
+ if (index >= 0)
683
+ this.tools[index] = tool;
535
684
  }
536
685
  }
537
686
  /**
538
- * Process user input. Handles slash commands or runs agent loop.
687
+ * Publish cached tool definitions into the deferred catalog so `tool_search`
688
+ * answers correctly on turn 1. A cached stub carries the real name, one-line
689
+ * description and input schema; calling it waits for the live connection and
690
+ * then dispatches against the real client, or returns a clear error when that
691
+ * server ultimately failed. Live tools replace stubs on connect.
539
692
  */
540
- async prompt(content) {
693
+ async seedMcpCatalogFromCache(servers) {
694
+ if (!this.opts.backgroundMcpConnect)
695
+ return;
696
+ if (this.opts.allowedTools || !this.settingsManager.get("deferredMcpTools"))
697
+ return;
698
+ let entries;
699
+ try {
700
+ entries = await this.mcpCatalogCache.entriesFor(servers);
701
+ }
702
+ catch {
703
+ return;
704
+ }
705
+ const stubs = [];
706
+ for (const [serverName, entry] of entries) {
707
+ for (const cached of entry.tools) {
708
+ if (this.liveMcpTools.has(cached.name))
709
+ continue;
710
+ this.cachedMcpToolServers.set(cached.name, serverName);
711
+ stubs.push(this.buildCachedMcpTool(serverName, cached));
712
+ }
713
+ }
714
+ if (stubs.length === 0)
715
+ return;
716
+ log("INFO", "mcp", "Seeded deferred tool catalog from cache", {
717
+ tools: String(stubs.length),
718
+ servers: String(entries.size),
719
+ });
720
+ this.addCachedMcpTools(stubs);
721
+ }
722
+ /** Catalog-only registration for cached stubs — never marks them live. */
723
+ addCachedMcpTools(stubs) {
724
+ this.mcpCatalog ??= new DeferredToolCatalog();
725
+ this.mcpCatalog.add(stubs);
726
+ this.ensureToolSearchTool();
727
+ }
728
+ buildCachedMcpTool(serverName, cached) {
729
+ return {
730
+ name: cached.name,
731
+ description: cached.description,
732
+ parameters: z.record(z.string(), z.unknown()),
733
+ rawInputSchema: cached.rawInputSchema,
734
+ execute: async (args, context) => {
735
+ const live = this.liveMcpTools.get(cached.name);
736
+ if (live)
737
+ return live.execute(args, context);
738
+ const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
739
+ ok: false,
740
+ error: "MCP is disabled for this session",
741
+ };
742
+ if (!outcome.ok) {
743
+ return (`MCP tool ${cached.name} is unavailable: server "${serverName}" did not connect ` +
744
+ `(${outcome.error}). This tool was offered from a cached catalog. ` +
745
+ `Use a different approach or ask the user to check their MCP configuration.`);
746
+ }
747
+ const connected = this.liveMcpTools.get(cached.name);
748
+ if (!connected) {
749
+ return (`MCP tool ${cached.name} no longer exists: server "${serverName}" connected but ` +
750
+ `does not expose it. The cached catalog entry was stale.`);
751
+ }
752
+ return connected.execute(args, context);
753
+ },
754
+ };
755
+ }
756
+ /**
757
+ * Resolve a `/name [args]` input to the prompt template it expands into, or
758
+ * null when it isn't a prompt-template command for THIS session (an ordinary
759
+ * message, a registry/action command, or any slash input on a non-coder
760
+ * agent). Shared by {@link prompt} and {@link willExpandPromptTemplate} so
761
+ * callers can't drift from the expansion that actually happens.
762
+ */
763
+ async resolveSlashInput(content) {
541
764
  const parsedInput = this.slashCommands.parse(content);
542
765
  const coderCommands = this.opts.coderSlashCommands !== false;
543
766
  // Non-coder agents only intercept commands registered in their own registry.
@@ -545,27 +768,53 @@ export class AgentSession {
545
768
  const parsed = parsedInput && (coderCommands || this.slashCommands.get(parsedInput.name))
546
769
  ? parsedInput
547
770
  : null;
548
- if (parsed) {
549
- // EZ Coder alone can resolve its prompt-template and project commands.
550
- const builtinPromptCmd = coderCommands ? getPromptCommand(parsed.name) : undefined;
551
- const customCmds = coderCommands ? await loadCustomCommands(this.cwd) : [];
552
- const customPromptCmd = !builtinPromptCmd
553
- ? customCmds.find((c) => c.name === parsed.name)
554
- : undefined;
555
- const promptText = builtinPromptCmd?.prompt ?? customPromptCmd?.prompt;
556
- if (promptText) {
557
- // Inject the prompt-template command as a user message to the agent
558
- const fullPrompt = parsed.args
559
- ? `${promptText}\n\n## User Instructions\n\n${parsed.args}`
560
- : promptText;
561
- // Run as a normal prompt (push message + agent loop)
562
- const userMessage = { role: "user", content: fullPrompt };
563
- this.messages.push(userMessage);
564
- await this.persistMessage(userMessage);
565
- this.lastPersistedIndex = this.messages.length;
566
- await this.runLoop();
567
- return;
568
- }
771
+ if (!parsed)
772
+ return null;
773
+ // EZ Coder alone can resolve its prompt-template and project commands.
774
+ const builtinPromptCmd = coderCommands ? getPromptCommand(parsed.name) : undefined;
775
+ const customCmds = coderCommands ? await loadCustomCommands(this.cwd) : [];
776
+ const customPromptCmd = !builtinPromptCmd
777
+ ? customCmds.find((c) => c.name === parsed.name)
778
+ : undefined;
779
+ const promptText = builtinPromptCmd?.prompt ?? customPromptCmd?.prompt;
780
+ if (!promptText)
781
+ return { kind: "command" };
782
+ return {
783
+ kind: "template",
784
+ fullPrompt: parsed.args
785
+ ? `${promptText}\n\n## User Instructions\n\n${parsed.args}`
786
+ : promptText,
787
+ };
788
+ }
789
+ /**
790
+ * Whether {@link prompt} would expand this input into a template body and
791
+ * persist it as a user message. Hosts use it to record the typed `/name` for
792
+ * transcript restore — gating on anything looser risks tagging an unrelated
793
+ * message when the command turns out NOT to expand.
794
+ */
795
+ async willExpandPromptTemplate(content) {
796
+ return (await this.resolveSlashInput(content))?.kind === "template";
797
+ }
798
+ /**
799
+ * Process user input. Handles slash commands or runs agent loop.
800
+ */
801
+ async prompt(content, provenance = {
802
+ source: "human",
803
+ kind: "prompt",
804
+ visibility: "transcript",
805
+ }, options = {}) {
806
+ await this.adoptDeferredCheckpointBeforePrompt();
807
+ const slash = await this.resolveSlashInput(content);
808
+ if (slash?.kind === "template") {
809
+ // Prompt templates remain human-originated: the user invoked the command.
810
+ const userMessage = { role: "user", content: slash.fullPrompt, provenance };
811
+ this.messages.push(userMessage);
812
+ await this.persistMessage(userMessage);
813
+ this.lastPersistedIndex = this.messages.length;
814
+ await this.runLoop(options);
815
+ return;
816
+ }
817
+ if (slash?.kind === "command") {
569
818
  const cmdContext = this.createSlashCommandContext();
570
819
  const result = await this.slashCommands.execute(content, cmdContext);
571
820
  if (result) {
@@ -574,11 +823,11 @@ export class AgentSession {
574
823
  return;
575
824
  }
576
825
  // Push user message
577
- const userMessage = { role: "user", content };
826
+ const userMessage = { role: "user", content, provenance };
578
827
  this.messages.push(userMessage);
579
828
  await this.persistMessage(userMessage);
580
829
  this.lastPersistedIndex = this.messages.length;
581
- await this.runLoop();
830
+ await this.runLoop(options);
582
831
  }
583
832
  /**
584
833
  * Prompt with multimodal attachments (images / videos) alongside optional
@@ -588,10 +837,15 @@ export class AgentSession {
588
837
  * attachments are always a direct conversational turn.
589
838
  */
590
839
  async promptWithAttachments(text, attachments) {
840
+ await this.adoptDeferredCheckpointBeforePrompt();
591
841
  const parts = this.buildAttachmentParts(text, attachments);
592
842
  if (parts.length === 0)
593
843
  return;
594
- const userMessage = { role: "user", content: parts };
844
+ const userMessage = {
845
+ role: "user",
846
+ content: parts,
847
+ provenance: { source: "human", kind: "prompt", visibility: "transcript" },
848
+ };
595
849
  this.messages.push(userMessage);
596
850
  await this.persistMessage(userMessage);
597
851
  this.lastPersistedIndex = this.messages.length;
@@ -680,9 +934,12 @@ export class AgentSession {
680
934
  this.hookFileEditCounts.clear();
681
935
  this.hookToolCalls.clear();
682
936
  this.reviewCoverage.reset();
937
+ this.reviewCoverageInjected = 0;
683
938
  this.idealReviewPhase = "idle";
684
939
  this.loopBreakInjected = 0;
685
940
  this.regroundingInjected = false;
941
+ this.runStartedAt = Date.now();
942
+ this.processGateInjected = 0;
686
943
  this.compactionOccurred = false;
687
944
  this.originalRequest = originalRequest;
688
945
  }
@@ -733,28 +990,94 @@ export class AgentSession {
733
990
  }
734
991
  }
735
992
  await this.persistTurnMetric(event);
993
+ // The assistant message for this turn is now in the array.
994
+ await this.flushPendingMessages();
995
+ break;
996
+ case "checkpoint":
997
+ // Tool results for this step are in the array and their side effects
998
+ // already hit the filesystem. Flushing here is what makes a crash lose
999
+ // at most the in-flight step instead of the entire turn.
1000
+ await this.flushPendingMessages();
736
1001
  break;
737
1002
  }
738
1003
  }
739
1004
  /**
740
- * Mid-loop steering hook: fires the loop-breaker when the agent looks stuck,
741
- * then post-compaction re-grounding. At most one of each per run. Mirrors the
742
- * TUI's getSteeringMessages ordering (minus user steering, which the app
743
- * delivers as normal prompts).
1005
+ * Append every message added since the last flush to the session file.
1006
+ *
1007
+ * Safe to call mid-run: `agentLoop` mutates its message array in place, so
1008
+ * the slice from `lastPersistedIndex` is always exactly the new tail.
1009
+ * Transient sessions (subagent spawns) have no session file and
1010
+ * `persistMessage` no-ops for them.
1011
+ *
1012
+ * Persistence failures must never take down a live run — the post-loop flush
1013
+ * retries the same range.
1014
+ */
1015
+ async flushPendingMessages() {
1016
+ const messages = this.activeLoopMessages ?? this.messages;
1017
+ if (this.lastPersistedIndex >= messages.length)
1018
+ return;
1019
+ const target = messages.length;
1020
+ try {
1021
+ for (let i = this.lastPersistedIndex; i < target; i++) {
1022
+ await this.persistMessage(messages[i]);
1023
+ this.lastPersistedIndex = i + 1;
1024
+ }
1025
+ }
1026
+ catch (err) {
1027
+ log("WARN", "session", "Failed to flush messages at a step boundary", {
1028
+ error: err instanceof Error ? err.message : String(err),
1029
+ });
1030
+ }
1031
+ }
1032
+ /**
1033
+ * Mid-loop steering hook: delivers pushed notifications and queued user
1034
+ * steering, fires the loop-breaker when the agent looks stuck, then
1035
+ * post-compaction re-grounding. At most one loop-break/re-grounding per run.
1036
+ * Mirrors the TUI's getSteeringMessages ordering.
744
1037
  */
745
1038
  getHookSteeringMessages() {
1039
+ // Push notifications: a child that finished or a background build that
1040
+ // exited is new *fact*, not a correction, and it is cheap (bounded to 1 KiB
1041
+ // per drain). Drained above the loop-break checks so the agent can act on
1042
+ // it in the very next turn instead of discovering it at the pre-stop
1043
+ // completion gate — but it never displaces user steering, which rides out
1044
+ // in the same batch when both are pending.
1045
+ const notified = this.notifications.drain();
1046
+ const notificationMessage = notified.length > 0
1047
+ ? {
1048
+ role: "user",
1049
+ content: buildNotificationSteeringText(notified.map((entry) => entry.text)),
1050
+ provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
1051
+ }
1052
+ : null;
1053
+ if (notificationMessage) {
1054
+ log("INFO", "notifications", "Injecting pushed notifications", {
1055
+ count: String(notified.length),
1056
+ chars: String(notificationMessage.content.length),
1057
+ });
1058
+ }
746
1059
  // User steering wins: drain any messages queued during this run first so the
747
1060
  // agent sees them mid-loop instead of after it stops.
748
1061
  if (this.userQueue.length > 0) {
749
1062
  const queued = this.userQueue.splice(0);
1063
+ // The agent has now consumed these. Announce the new depth immediately so
1064
+ // clients can drop the "queued" affordance at the turn boundary rather
1065
+ // than holding it until the whole run ends — the message is already in
1066
+ // the loop, and showing it as still-pending for minutes is a lie.
1067
+ this.eventBus.emit("queue_drained", { count: this.userQueue.length });
750
1068
  // Frame each queued item as concurrent steering — without this wrapper
751
1069
  // the model treats a mid-run message as a fresh request that supersedes
752
1070
  // the original task and silently drops it. ONE message per queued item
753
1071
  // (not merged): each persists as its own user message, so a resumed
754
1072
  // session shows the same number of bubbles the live run did.
755
- return queued.map((m) => {
1073
+ const steeringMessages = queued.map((m) => {
1074
+ const provenance = {
1075
+ source: "human",
1076
+ kind: "steering",
1077
+ visibility: "transcript",
1078
+ };
756
1079
  if (m.attachments.length === 0) {
757
- return { role: "user", content: wrapSteeringText(m.text) };
1080
+ return { role: "user", content: wrapSteeringText(m.text), provenance };
758
1081
  }
759
1082
  // Queued attachments ride the same native-block path as a non-queued
760
1083
  // attachment prompt, prefixed with the steering framing.
@@ -762,9 +1085,12 @@ export class AgentSession {
762
1085
  { type: "text", text: STEERING_PREFIX },
763
1086
  ...this.buildAttachmentParts(m.text, m.attachments),
764
1087
  ];
765
- return { role: "user", content: parts };
1088
+ return { role: "user", content: parts, provenance };
766
1089
  });
1090
+ return notificationMessage ? [...steeringMessages, notificationMessage] : steeringMessages;
767
1091
  }
1092
+ if (notificationMessage)
1093
+ return [notificationMessage];
768
1094
  if (this.opts.selfCorrectionHooks === false)
769
1095
  return null;
770
1096
  if (!this.settingsManager.get("idealReviewEnabled"))
@@ -806,6 +1132,39 @@ export class AgentSession {
806
1132
  }
807
1133
  return null;
808
1134
  }
1135
+ /**
1136
+ * Turn-budget extension gate. The loop consults this instead of stopping
1137
+ * mid-task when it exhausts `maxTurns`. Grant ONLY on evidence of progress —
1138
+ * handing more turns to a spinning agent just buys it more tokens to spin
1139
+ * with — so reuse the same stuck signals that drive the loop-breaker.
1140
+ */
1141
+ shouldExtendTurnBudget(ctx) {
1142
+ const refusals = [];
1143
+ const stuck = evaluateLoopBreak({
1144
+ consecutiveFailures: this.hookConsecutiveFailures,
1145
+ repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
1146
+ textRepetitionDetected: detectTextRepetition(this.hookText),
1147
+ ...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
1148
+ });
1149
+ if (stuck.shouldBreak)
1150
+ refusals.push(...stuck.reasons);
1151
+ // Stage 2 means the loop-breaker already detected spinning twice and told
1152
+ // the agent to stop and report. Do not overrule that with more turns.
1153
+ if (this.loopBreakInjected >= 2)
1154
+ refusals.push("loop-breaker already escalated");
1155
+ const { toolCalls, toolFailures } = this.hookStats;
1156
+ if (toolCalls > 0 && toolFailures / toolCalls > TURN_EXTENSION_MAX_FAILURE_RATIO) {
1157
+ refusals.push(`${toolFailures}/${toolCalls} tool calls failed`);
1158
+ }
1159
+ const granted = refusals.length === 0;
1160
+ log("INFO", "turn-budget", granted ? "Extending turn budget" : "Refusing turn-budget extension", {
1161
+ turn: String(ctx.turn),
1162
+ maxTurns: String(ctx.maxTurns),
1163
+ extension: String(ctx.extension),
1164
+ ...(granted ? {} : { reasons: refusals.join(", ") }),
1165
+ });
1166
+ return granted;
1167
+ }
809
1168
  /**
810
1169
  * Pre-stop Ideal review phase machine. Once review starts, completion is
811
1170
  * blocked until harness-owned post-injection reads cover every changed file.
@@ -814,6 +1173,17 @@ export class AgentSession {
814
1173
  const childCompletionFollowUp = buildSubAgentCompletionFollowUp(this.subAgentManager);
815
1174
  if (childCompletionFollowUp)
816
1175
  return childCompletionFollowUp;
1176
+ // Background processes started this run and never read block completion:
1177
+ // their progress/exit checkpoints only land on the steering path, which an
1178
+ // agent about to stop never reaches.
1179
+ const processFollowUp = buildProcessCompletionFollowUp(this.processManager?.list() ?? [], this.runStartedAt, this.processGateInjected);
1180
+ if (processFollowUp) {
1181
+ this.processGateInjected += 1;
1182
+ log("INFO", "process-gate", "Injecting background-process completion gate", {
1183
+ injected: String(this.processGateInjected),
1184
+ });
1185
+ return processFollowUp;
1186
+ }
817
1187
  if (this.opts.selfCorrectionHooks === false || this.idealReviewSuppressed)
818
1188
  return null;
819
1189
  if (this.idealReviewPhase === "reviewing") {
@@ -826,9 +1196,20 @@ export class AgentSession {
826
1196
  lspMissing: lspEvidence.missing,
827
1197
  });
828
1198
  if (coverage.missing.length > 0) {
829
- return [
830
- this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
831
- ];
1199
+ if (this.reviewCoverageInjected < MAX_REVIEW_COVERAGE_INJECTIONS) {
1200
+ this.reviewCoverageInjected += 1;
1201
+ return [
1202
+ this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
1203
+ ];
1204
+ }
1205
+ // Budget spent: close the gate so the run cannot spin on a file that
1206
+ // never becomes readable, and require the gap be reported to the user.
1207
+ this.idealReviewPhase = "complete";
1208
+ log("INFO", "ideal", "Ideal review coverage escalated after retry budget", {
1209
+ injected: String(this.reviewCoverageInjected),
1210
+ missing: coverage.missing,
1211
+ });
1212
+ return [buildReviewCoverageEscalationMessage(coverage.missing)];
832
1213
  }
833
1214
  this.idealReviewPhase = "complete";
834
1215
  return null;
@@ -886,10 +1267,14 @@ export class AgentSession {
886
1267
  : []),
887
1268
  "Do not describe those files as compiler-clean without other evidence.",
888
1269
  ];
889
- return { role: "user", content: `${String(message.content)}\n\n${notes.join(" ")}` };
1270
+ return {
1271
+ role: "user",
1272
+ provenance: message.provenance,
1273
+ content: `${String(message.content)}\n\n${notes.join(" ")}`,
1274
+ };
890
1275
  }
891
1276
  /** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
892
- async runLoop() {
1277
+ async runLoop(options = {}) {
893
1278
  this.refreshSystemPromptTail();
894
1279
  // One-shot cache-key marker per session so turn_end cacheRead numbers
895
1280
  // in the log can be traced back to a specific routing namespace —
@@ -917,6 +1302,10 @@ export class AgentSession {
917
1302
  // Cache for sync callers (see field doc) — kept in step with `creds`
918
1303
  // through the 401 force-refresh retry below.
919
1304
  this.lastAccountId = creds.accountId;
1305
+ // The access token most recently handed to the provider. Tracked separately
1306
+ // from `creds` because the per-turn resolver can rotate it mid-run, and the
1307
+ // 401 handler must name the token that was actually rejected.
1308
+ let lastResolvedAccessToken = creds.accessToken;
920
1309
  // Auto-compact if needed. This must happen after credential resolution so
921
1310
  // OpenAI OAuth/Codex sessions use the Codex product context window instead
922
1311
  // of the public API model window. Failed/no-op attempts cool down across
@@ -926,7 +1315,14 @@ export class AgentSession {
926
1315
  provider: this.provider,
927
1316
  accountId: creds.accountId,
928
1317
  });
929
- const threshold = this.settingsManager.get("compactThreshold");
1318
+ const policy = resolveCompactionPolicy({
1319
+ provider: this.provider,
1320
+ model: this.model,
1321
+ contextWindow,
1322
+ threshold: this.settingsManager.get("compactThreshold"),
1323
+ accountId: creds.accountId,
1324
+ approvedPlanPath: this.approvedPlanPath,
1325
+ });
930
1326
  let activeTokens;
931
1327
  if (this.providerContext) {
932
1328
  const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
@@ -940,9 +1336,17 @@ export class AgentSession {
940
1336
  this.providerContext = null;
941
1337
  }
942
1338
  }
943
- if (shouldCompact(this.messages, contextWindow, threshold, activeTokens)) {
1339
+ log("INFO", "compaction", "Pre-run compaction decision", {
1340
+ provider: this.provider,
1341
+ model: this.model,
1342
+ transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
1343
+ contextWindow: String(contextWindow),
1344
+ activeTokens: activeTokens === undefined ? "estimated" : String(activeTokens),
1345
+ triggerLimit: String(policy.targetTokens),
1346
+ });
1347
+ if (shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens)) {
944
1348
  try {
945
- await this.compact(creds);
1349
+ await this.compact(creds, "automatic");
946
1350
  if (this.lastCompactionCompacted) {
947
1351
  // Re-grounding hook keys off this — the context was just summarized.
948
1352
  this.compactionOccurred = true;
@@ -963,17 +1367,36 @@ export class AgentSession {
963
1367
  const userAgent = this.provider === "anthropic" ? await getClaudeCliUserAgent() : undefined;
964
1368
  const loopMessages = await this.prepareDynamicContext();
965
1369
  const runAgentLoop = async (apiKey, accountId, projectId) => {
1370
+ lastResolvedAccessToken = apiKey;
966
1371
  const modelInfo = getModel(this.model);
967
1372
  const effectiveBaseUrl = this.baseUrl ?? creds.baseUrl;
968
1373
  const generator = agentLoop(loopMessages, {
969
1374
  provider: this.provider,
970
1375
  model: this.model,
971
- tools: this.tools,
972
- webSearch: true,
1376
+ tools: options.disableTools ? [] : this.tools,
1377
+ webSearch: !options.disableTools,
973
1378
  maxTokens: this.maxTokens,
974
1379
  maxTurns: this.opts.maxTurns,
1380
+ maxTurnExtensions: this.opts.maxTurnExtensions,
975
1381
  thinking: this.thinkingLevel,
976
1382
  apiKey,
1383
+ // Per-turn credential resolution. A run can span many minutes; if any
1384
+ // process sharing auth.json refreshes this grant meanwhile, the token
1385
+ // captured above is invalidated server-side and every remaining turn
1386
+ // would fail with an authentication error until the user restarted.
1387
+ // Static API keys resolve to the same value, so this is a no-op for them.
1388
+ resolveCredentials: async () => {
1389
+ const live = await this.authStorage.resolveCredentials(this.provider, {
1390
+ storageKeys: this.currentAuthStorageKeys(),
1391
+ });
1392
+ this.lastAccountId = live.accountId;
1393
+ lastResolvedAccessToken = live.accessToken;
1394
+ return {
1395
+ apiKey: live.accessToken,
1396
+ ...(live.accountId !== undefined ? { accountId: live.accountId } : {}),
1397
+ ...(live.projectId !== undefined ? { projectId: live.projectId } : {}),
1398
+ };
1399
+ },
977
1400
  baseUrl: effectiveBaseUrl,
978
1401
  signal: this.opts.signal,
979
1402
  accountId,
@@ -1000,6 +1423,7 @@ export class AgentSession {
1000
1423
  // polled mid-loop; the ideal review is polled when the agent would stop.
1001
1424
  getSteeringMessages: () => this.getHookSteeringMessages(),
1002
1425
  getFollowUpMessages: () => this.getHookFollowUpMessages(),
1426
+ onTurnBudgetExhausted: (ctx) => this.shouldExtendTurnBudget(ctx),
1003
1427
  // Check authoritative provider usage before every model/tool step.
1004
1428
  // Forced overflow recovery bypasses settings and cooldown; proactive
1005
1429
  // checks honor both and estimate only messages unseen by the provider.
@@ -1026,6 +1450,7 @@ export class AgentSession {
1026
1450
  this.providerContext = null;
1027
1451
  log("INFO", "compaction", "Pruned stale tool outputs", {
1028
1452
  prunedResults: String(pruneResult.prunedResults),
1453
+ compactedToolCalls: String(pruneResult.compactedToolCalls),
1029
1454
  freedTokens: String(pruneResult.freedTokens),
1030
1455
  });
1031
1456
  }
@@ -1050,12 +1475,27 @@ export class AgentSession {
1050
1475
  provider: this.provider,
1051
1476
  accountId,
1052
1477
  });
1053
- const threshold = this.settingsManager.get("compactThreshold");
1478
+ const policy = resolveCompactionPolicy({
1479
+ provider: this.provider,
1480
+ model: this.model,
1481
+ contextWindow,
1482
+ threshold: this.settingsManager.get("compactThreshold"),
1483
+ accountId,
1484
+ approvedPlanPath: this.approvedPlanPath,
1485
+ });
1054
1486
  const activeTokens = calculateActiveContextTokens(messages, {
1055
1487
  usage,
1056
1488
  pendingMessages,
1057
1489
  });
1058
- if (!shouldCompact(messages, contextWindow, threshold, activeTokens))
1490
+ log("INFO", "compaction", "In-flight compaction decision", {
1491
+ provider: this.provider,
1492
+ model: this.model,
1493
+ transport: this.provider === "openai" && accountId ? "codex_oauth" : "public_api",
1494
+ contextWindow: String(contextWindow),
1495
+ activeTokens: String(activeTokens),
1496
+ triggerLimit: String(policy.targetTokens),
1497
+ });
1498
+ if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens))
1059
1499
  return messages;
1060
1500
  }
1061
1501
  // compact() operates on this.messages, while an earlier transform may
@@ -1068,7 +1508,7 @@ export class AgentSession {
1068
1508
  accountId,
1069
1509
  projectId,
1070
1510
  baseUrl: effectiveBaseUrl,
1071
- });
1511
+ }, force ? "forced" : "automatic");
1072
1512
  }
1073
1513
  catch (error) {
1074
1514
  this.messages = messages;
@@ -1088,9 +1528,15 @@ export class AgentSession {
1088
1528
  return this.messages;
1089
1529
  },
1090
1530
  });
1091
- for await (const event of generator) {
1092
- await this.trackHookEvent(event);
1093
- this.eventBus.forwardAgentEvent(event);
1531
+ this.activeLoopMessages = loopMessages;
1532
+ try {
1533
+ for await (const event of generator) {
1534
+ await this.trackHookEvent(event);
1535
+ this.eventBus.forwardAgentEvent(event);
1536
+ }
1537
+ }
1538
+ finally {
1539
+ this.activeLoopMessages = null;
1094
1540
  }
1095
1541
  };
1096
1542
  const clearInvalidStaticApiKey = async (error) => {
@@ -1114,29 +1560,45 @@ export class AgentSession {
1114
1560
  if (isAbortError(err) || this.opts.signal?.aborted) {
1115
1561
  return;
1116
1562
  }
1117
- // Kimi OAuth plan ran out of usage (hard usage-limit stop, or an HTTP 402
1118
- // billing stop). If the user ALSO configured a Moonshot API key, mark
1119
- // the OAuth credential usage-exhausted (honoring the provider-stated
1120
- // reset time when present) and retry this turn on the API key — OAuth
1121
- // stays the preferred credential and resumes automatically once the mark
1122
- // lapses. A generic 429 is deliberately excluded: it may be a transient
1123
- // rate limit and must not silently switch the user to a billed API key.
1124
- // Guarded on the Kimi managed endpoint actually being in use: if the API
1125
- // key was already active, the same error means BOTH are out and must surface.
1126
- if (this.provider === "moonshot" &&
1563
+ // A subscription OAuth plan ran out of usage (hard usage-limit stop, or an
1564
+ // HTTP 402 billing stop). If the user ALSO configured that provider's API
1565
+ // key, mark the OAuth credential usage-exhausted (honoring the
1566
+ // provider-stated reset time when present) and retry this turn on the API
1567
+ // key — OAuth stays the preferred credential and resumes automatically once
1568
+ // the mark lapses. A generic 429 is deliberately excluded: it may be a
1569
+ // transient rate limit and must not silently switch the user to a billed
1570
+ // API key.
1571
+ //
1572
+ // Grok adds one case Kimi doesn't have: xAI gates its CLI chat proxy by
1573
+ // subscription tier, so an entitled-looking account can be refused outright
1574
+ // with a 403. That means "OAuth cannot serve this", not a transient error,
1575
+ // so it counts as exhausted too — otherwise a dual-configured user would
1576
+ // hard-fail while holding a perfectly good API key.
1577
+ //
1578
+ // Guarded on the subscription endpoint actually being in use: if the API key
1579
+ // was already active, the same error means BOTH are out and must surface.
1580
+ const dualAuth = dualAuthProvider(this.provider);
1581
+ const onSubscriptionEndpoint = (this.provider === "moonshot" && isKimiCodingEndpoint(creds.baseUrl)) ||
1582
+ (this.provider === "xai" && isGrokCliEndpoint(creds.baseUrl));
1583
+ const tierRefusal = this.provider === "xai" && err instanceof ProviderError && err.statusCode === 403;
1584
+ const oauthIsOut = isUsageLimitError(err) ||
1585
+ (err instanceof ProviderError && err.statusCode === 402) ||
1586
+ tierRefusal;
1587
+ if (dualAuth &&
1127
1588
  !this.baseUrl &&
1128
- isKimiCodingEndpoint(creds.baseUrl) &&
1129
- (isUsageLimitError(err) || (err instanceof ProviderError && err.statusCode === 402)) &&
1130
- (await this.authStorage.hasCredentials("moonshot"))) {
1589
+ onSubscriptionEndpoint &&
1590
+ oauthIsOut &&
1591
+ (await this.authStorage.hasCredentials(dualAuth.provider))) {
1131
1592
  const resetsAt = err instanceof ProviderError ? err.resetsAt : undefined;
1132
- await this.authStorage.markUsageExhausted(MOONSHOT_OAUTH_KEY, resetsAt);
1133
- log("WARN", "auth", "Kimi OAuth usage limit reached — retrying this turn on the Moonshot API key", { resetsAt: resetsAt !== undefined ? String(resetsAt) : "unknown" });
1593
+ await this.authStorage.markUsageExhausted(dualAuth.oauthKey, resetsAt);
1594
+ log("WARN", "auth", `${dualAuth.oauthLabel} usage limit reached — retrying this turn on the ${dualAuth.apiKeyLabel}`, { resetsAt: resetsAt !== undefined ? String(resetsAt) : "unknown" });
1134
1595
  creds = await this.authStorage.resolveCredentials(this.provider, {
1135
1596
  storageKeys: this.currentAuthStorageKeys(),
1136
1597
  });
1137
1598
  this.lastAccountId = creds.accountId;
1138
1599
  // The runAgentLoop closure re-reads `creds`, so the retry picks up the
1139
- // API key's baseUrl (api.moonshot.ai) and drops the Kimi coding headers.
1600
+ // API key's public baseUrl (api.moonshot.ai / api.x.ai) and drops the
1601
+ // subscription endpoint's client-identity headers.
1140
1602
  try {
1141
1603
  await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
1142
1604
  }
@@ -1151,15 +1613,21 @@ export class AgentSession {
1151
1613
  else if (err instanceof ProviderError && err.statusCode === 401) {
1152
1614
  // Static API-key providers (GLM, Moonshot API key, etc.) have no refresh
1153
1615
  // mechanism — retrying with the same key is pointless. Clear the
1154
- // credential and surface the error so the user re-logins. Kimi OAuth
1155
- // (active for `moonshot` when present) is refreshable, so it falls
1156
- // through to the force-refresh path below.
1616
+ // credential and surface the error so the user re-logins. Subscription
1617
+ // OAuth (active for `moonshot`/`xai` when present) is refreshable, so it
1618
+ // falls through to the force-refresh path below.
1157
1619
  if (await clearInvalidStaticApiKey(err))
1158
1620
  throw err;
1159
1621
  log("INFO", "auth", "Got 401, force-refreshing token and retrying");
1160
1622
  creds = await this.authStorage.resolveCredentials(this.provider, {
1161
1623
  forceRefresh: true,
1162
1624
  storageKeys: this.currentAuthStorageKeys(),
1625
+ // Name the token the provider actually rejected (which the per-turn
1626
+ // resolver may have rotated since the run started). If disk already
1627
+ // holds a newer one, adopt it instead of minting another — a fresh
1628
+ // refresh would invalidate the token every sibling process is using
1629
+ // and turn one 401 into a cascade of them.
1630
+ rejectedToken: lastResolvedAccessToken,
1163
1631
  });
1164
1632
  this.lastAccountId = creds.accountId;
1165
1633
  await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
@@ -1169,7 +1637,9 @@ export class AgentSession {
1169
1637
  }
1170
1638
  }
1171
1639
  this.messages = loopMessages;
1172
- // Persist new messages
1640
+ // Backstop. Step-boundary flushes (checkpoint / turn_end) normally leave
1641
+ // nothing here, but this still catches messages appended after the last
1642
+ // checkpoint — an aborted final step, or a flush that failed mid-run.
1173
1643
  for (let i = this.lastPersistedIndex; i < this.messages.length; i++) {
1174
1644
  await this.persistMessage(this.messages[i]);
1175
1645
  }
@@ -1177,6 +1647,11 @@ export class AgentSession {
1177
1647
  }
1178
1648
  async switchModel(provider, model) {
1179
1649
  const prevProvider = this.provider;
1650
+ const prevModel = this.model;
1651
+ // Diff gate: a "switch" to the model already in use is not state change.
1652
+ // Recording it anyway would write a redundant marker and a redundant
1653
+ // history note on every no-op re-selection from the UI.
1654
+ const changed = model !== prevModel || (Boolean(provider) && provider !== prevProvider);
1180
1655
  if (provider)
1181
1656
  this.provider = provider;
1182
1657
  this.model = model;
@@ -1185,7 +1660,6 @@ export class AgentSession {
1185
1660
  // the live selection after an in-session model switch.
1186
1661
  this.opts.provider = this.provider;
1187
1662
  this.opts.model = this.model;
1188
- this.syncUltraOrchestrationPrompt();
1189
1663
  setEstimatorModel(model);
1190
1664
  // maxTokens must follow the active model — it was frozen at the boot
1191
1665
  // model's `maxOutputTokens` in the constructor, so without this a session
@@ -1206,6 +1680,27 @@ export class AgentSession {
1206
1680
  const newReadTool = this.rebuildReadTool(model);
1207
1681
  this.tools = this.tools.map((t) => (t.name === "read" ? newReadTool : t));
1208
1682
  }
1683
+ // Model-dependent guidance lives in the uncached tail, so this rewrites
1684
+ // only the bytes after the cache marker — the cached prefix survives the
1685
+ // switch intact and the next turn still reads from cache.
1686
+ this.refreshSystemPromptTail();
1687
+ if (changed) {
1688
+ // Durable, replayable record of which model produced which segment. Rides
1689
+ // the existing app-marker path (parentId null, anchored by message count)
1690
+ // so a resumed session reconstructs the switch without the LLM seeing an
1691
+ // extra transcript row.
1692
+ await this.persistAppMarker("model_switch", {
1693
+ from: prevModel,
1694
+ to: this.model,
1695
+ provider: this.provider,
1696
+ fromProvider: prevProvider,
1697
+ }).catch((error) => {
1698
+ log("WARN", "session", "Failed to persist model-switch marker", {
1699
+ error: error instanceof Error ? error.message : String(error),
1700
+ });
1701
+ });
1702
+ this.appendModelSwitchNote(prevModel, prevProvider);
1703
+ }
1209
1704
  // Update provider-specific tools when provider changes
1210
1705
  if (provider && provider !== prevProvider) {
1211
1706
  // Add/remove client-side web_search tool based on provider.
@@ -1252,6 +1747,8 @@ export class AgentSession {
1252
1747
  // re-adding. Some tools may already have been promoted out of the catalog.
1253
1748
  this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
1254
1749
  this.mcpCatalog?.removeWhere((name) => name.startsWith("mcp__"));
1750
+ this.liveMcpTools.clear();
1751
+ this.cachedMcpToolServers.clear();
1255
1752
  this.addMcpTools(mcpTools);
1256
1753
  }
1257
1754
  catch (err) {
@@ -1260,7 +1757,93 @@ export class AgentSession {
1260
1757
  }
1261
1758
  }
1262
1759
  }
1263
- async compact(existingCredentials) {
1760
+ /**
1761
+ * Record the switch as its own trailing message at the point it happened,
1762
+ * rather than by rewriting the system prompt. Two reasons: the system prompt
1763
+ * is the cached prefix and rewriting it costs a full cache write, and a
1764
+ * resumed transcript can only attribute output to the right model if the
1765
+ * switch sits in message order.
1766
+ *
1767
+ * Skipped before the conversation starts (nothing to disambiguate) and while
1768
+ * an assistant turn has unresolved tool calls, where inserting a user message
1769
+ * would break tool_use/tool_result pairing.
1770
+ */
1771
+ appendModelSwitchNote(prevModel, prevProvider) {
1772
+ const last = this.messages[this.messages.length - 1];
1773
+ if (!last || last.role === "system")
1774
+ return;
1775
+ if (last.role === "assistant" && hasUnresolvedToolCalls(last))
1776
+ return;
1777
+ const from = prevProvider === this.provider ? prevModel : `${prevProvider}/${prevModel}`;
1778
+ const to = prevProvider === this.provider ? this.model : `${this.provider}/${this.model}`;
1779
+ this.messages.push({
1780
+ role: "user",
1781
+ content: `[Model switched from ${from} to ${to}. Everything above was produced under the previous model; continue the current task from here.]`,
1782
+ provenance: { source: "runtime", kind: "model_switch", visibility: "hidden" },
1783
+ });
1784
+ }
1785
+ async adoptCompactionCheckpoint(loaded) {
1786
+ const systemMessage = this.messages[0];
1787
+ const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
1788
+ this.messages = [systemMessage, ...loadedMessages];
1789
+ this.sessionId = loaded.header.id;
1790
+ this.conversationId = loaded.header.conversationId ?? loaded.header.id;
1791
+ this.checkpointGeneration = loaded.header.generation ?? 0;
1792
+ this.currentLeafId = loaded.header.leafId;
1793
+ this.setSessionPath(loaded.path);
1794
+ this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries, loaded.header.leafId);
1795
+ this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
1796
+ this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
1797
+ this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
1798
+ this.lastPersistedIndex = this.messages.length;
1799
+ this.providerContext = null;
1800
+ await this.subAgentManager?.rebindParentSession(this.sessionId);
1801
+ }
1802
+ /** Canonicalize a deferred restore before a new prompt can fork stale history. */
1803
+ async adoptDeferredCheckpointBeforePrompt() {
1804
+ if (!this.deferredCompactionPending || !this.conversationId)
1805
+ return;
1806
+ this.deferredCompactionPending = false;
1807
+ const canonicalPath = await this.sessionManager.resolveCanonicalSession(this.conversationId, this.cwd);
1808
+ if (!canonicalPath || canonicalPath === this.sessionPath)
1809
+ return;
1810
+ await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath));
1811
+ }
1812
+ async persistCompactionCheckpoint(sourceFingerprint, result) {
1813
+ const parentSessionId = this.sessionId || undefined;
1814
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1815
+ conversationId: this.conversationId || undefined,
1816
+ generation: this.checkpointGeneration + 1,
1817
+ parentSessionId,
1818
+ sourceFingerprint,
1819
+ retainedMessageCount: result.retainedCount === 0
1820
+ ? 0
1821
+ : this.messages
1822
+ .slice(-result.retainedCount)
1823
+ .filter((message) => getHistoryMessageVisibility(message) !== "hidden").length,
1824
+ preview: this.sessionPreview || undefined,
1825
+ });
1826
+ this.sessionId = session.id;
1827
+ this.checkpointGeneration = session.header.generation ?? 0;
1828
+ this.conversationId = session.header.conversationId ?? session.id;
1829
+ this.currentLeafId = null;
1830
+ this.setSessionPath(session.path);
1831
+ await this.subAgentManager?.rebindParentSession(this.sessionId);
1832
+ for (const message of this.messages) {
1833
+ if (message.role !== "system")
1834
+ await this.persistMessage(message);
1835
+ }
1836
+ this.lastPersistedIndex = this.messages.length;
1837
+ await this.rePersistTurnMetrics();
1838
+ await this.rePersistNolanTurns();
1839
+ await this.rePersistAutopilotMarkers();
1840
+ await this.rePersistAppMarkers();
1841
+ await this.persistAppMarker("compaction", {
1842
+ originalCount: result.originalCount,
1843
+ newCount: result.newCount,
1844
+ });
1845
+ }
1846
+ async compact(existingCredentials, mode = "manual") {
1264
1847
  this.lastCompactionCompacted = false;
1265
1848
  const creds = existingCredentials ??
1266
1849
  (await this.authStorage.resolveCredentials(this.provider, {
@@ -1270,71 +1853,141 @@ export class AgentSession {
1270
1853
  provider: this.provider,
1271
1854
  accountId: creds.accountId,
1272
1855
  });
1273
- this.eventBus.emit("compaction_start", { messageCount: this.messages.length });
1274
- const result = await compact(this.messages, {
1856
+ const policy = resolveCompactionPolicy({
1275
1857
  provider: this.provider,
1276
1858
  model: this.model,
1277
- apiKey: creds.accessToken,
1278
- accountId: creds.accountId,
1279
- projectId: creds.projectId,
1280
- baseUrl: this.baseUrl ?? creds.baseUrl,
1281
1859
  contextWindow,
1282
- signal: this.opts.signal,
1860
+ threshold: this.settingsManager.get("compactThreshold"),
1861
+ accountId: creds.accountId,
1862
+ approvedPlanPath: this.approvedPlanPath,
1283
1863
  });
1284
- this.messages = result.messages;
1285
- this.lastCompactionCompacted = result.result.compacted;
1286
- if (!result.result.compacted) {
1287
- this.eventBus.emit("compaction_end", {
1288
- compacted: false,
1289
- originalCount: result.result.originalCount,
1290
- newCount: result.result.newCount,
1864
+ const originalCount = this.messages.length;
1865
+ this.eventBus.emit("compaction_start", { messageCount: originalCount });
1866
+ let contextSelection;
1867
+ const runCompactor = async () => {
1868
+ const output = await compact(this.messages, {
1869
+ provider: this.provider,
1870
+ model: this.model,
1871
+ apiKey: creds.accessToken,
1872
+ accountId: creds.accountId,
1873
+ projectId: creds.projectId,
1874
+ baseUrl: this.baseUrl ?? creds.baseUrl,
1875
+ contextWindow,
1876
+ targetTokens: policy.targetTokens,
1877
+ signal: this.opts.signal,
1878
+ approvedPlanPath: this.approvedPlanPath,
1291
1879
  });
1292
- return;
1293
- }
1294
- this.providerContext = null;
1295
- // Transient sessions (Nolan chat/autopilot, subagent spawns) must NEVER touch
1296
- // the session store: without this guard, the first auto-compaction called
1297
- // sessionManager.create() and assigned a real sessionPath, silently turning
1298
- // the "in-memory only" session into a persisted one — every later turn (and
1299
- // every further compaction) then leaked a Nolan transcript file into the
1300
- // project's session list. Compact in memory only and keep sessionPath empty.
1301
- if (this.opts.transient) {
1302
- this.lastPersistedIndex = this.messages.length;
1880
+ contextSelection = output.result.contextSelection;
1881
+ return output;
1882
+ };
1883
+ let finalCount = originalCount;
1884
+ if (this.opts.transient || !this.conversationId) {
1885
+ const result = await runCompactor();
1886
+ finalCount = result.result.newCount;
1887
+ this.messages = result.messages;
1888
+ this.lastCompactionCompacted = result.result.compacted;
1889
+ if (result.result.compacted) {
1890
+ this.providerContext = null;
1891
+ this.remapMarkerAnchors(result.result.anchorRemap);
1892
+ this.lastPersistedIndex = this.messages.length;
1893
+ }
1303
1894
  }
1304
1895
  else {
1305
- // Persist compacted messages to a new session file so `ezcoder continue`
1306
- // picks up the compacted state instead of the full original history.
1307
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1308
- conversationId: this.conversationId || undefined,
1309
- preview: this.sessionPreview || undefined,
1310
- });
1311
- this.sessionId = session.id;
1312
- this.conversationId = session.header.conversationId ?? session.id;
1313
- this.setSessionPath(session.path);
1314
- await this.subAgentManager?.rebindParentSession(this.sessionId);
1315
- // Write compacted messages (skip system — it's rebuilt on load)
1316
- for (const msg of this.messages) {
1317
- if (msg.role === "system")
1318
- continue;
1319
- await this.persistMessage(msg);
1320
- }
1321
- this.lastPersistedIndex = this.messages.length;
1322
- // Carry evidence and Nolan's advisory turns into the new file so they survive compaction.
1323
- await this.rePersistTurnMetrics();
1324
- await this.rePersistNolanTurns();
1325
- await this.rePersistAutopilotMarkers();
1326
- await this.rePersistAppMarkers();
1327
- // Persist the compaction counts so a resumed session's quiet notice can
1328
- // show the same "N → M messages" summary the live run did.
1329
- await this.persistAppMarker("compaction", {
1330
- originalCount: result.result.originalCount,
1331
- newCount: result.result.newCount,
1896
+ const conversationId = this.conversationId;
1897
+ await this.sessionManager.withCompactionLease(conversationId, this.opts.signal, async () => {
1898
+ let sourceFingerprint = computeSourceFingerprint(this.messages);
1899
+ const canonicalPath = await this.sessionManager.resolveCanonicalSession(conversationId, this.cwd);
1900
+ if (canonicalPath && canonicalPath !== this.sessionPath) {
1901
+ const newest = await this.sessionManager.load(canonicalPath);
1902
+ if (newest.header.sourceFingerprint === sourceFingerprint) {
1903
+ await this.adoptCompactionCheckpoint(newest);
1904
+ this.lastCompactionCompacted = true;
1905
+ finalCount = this.messages.length;
1906
+ return;
1907
+ }
1908
+ // The canonical branch contains different progress. Rebase onto it before
1909
+ // compacting so a stale caller cannot supersede newer history by generation.
1910
+ await this.adoptCompactionCheckpoint(newest);
1911
+ sourceFingerprint = computeSourceFingerprint(this.messages);
1912
+ }
1913
+ const attempt = await this.sessionManager.readCompactionAttemptState(conversationId);
1914
+ const attemptActive = !attempt?.expiresAt || Date.parse(attempt.expiresAt) > Date.now();
1915
+ if (mode === "automatic" &&
1916
+ attempt?.fingerprint === sourceFingerprint &&
1917
+ attempt.policyKey === policy.policyKey &&
1918
+ attemptActive) {
1919
+ if (attempt.outcome === "success" && attempt.checkpointId) {
1920
+ const checkpointPath = await this.sessionManager.findById(this.cwd, attempt.checkpointId);
1921
+ if (checkpointPath) {
1922
+ const checkpoint = await this.sessionManager.load(checkpointPath);
1923
+ if (checkpoint.header.sourceFingerprint === sourceFingerprint) {
1924
+ await this.adoptCompactionCheckpoint(checkpoint);
1925
+ this.lastCompactionCompacted = true;
1926
+ finalCount = this.messages.length;
1927
+ return;
1928
+ }
1929
+ }
1930
+ }
1931
+ else if (attempt.outcome === "failed" || attempt.outcome === "noop") {
1932
+ return;
1933
+ }
1934
+ }
1935
+ try {
1936
+ const result = await runCompactor();
1937
+ finalCount = result.result.newCount;
1938
+ this.messages = result.messages;
1939
+ this.lastCompactionCompacted = result.result.compacted;
1940
+ if (!result.result.compacted) {
1941
+ await this.sessionManager.writeCompactionAttemptState(conversationId, {
1942
+ fingerprint: sourceFingerprint,
1943
+ policyKey: policy.policyKey,
1944
+ outcome: "noop",
1945
+ updatedAt: new Date().toISOString(),
1946
+ expiresAt: new Date(Date.now() + 30_000).toISOString(),
1947
+ });
1948
+ return;
1949
+ }
1950
+ this.providerContext = null;
1951
+ this.remapMarkerAnchors(result.result.anchorRemap);
1952
+ await this.persistCompactionCheckpoint(sourceFingerprint, result.result);
1953
+ await this.sessionManager.writeCompactionAttemptState(conversationId, {
1954
+ fingerprint: sourceFingerprint,
1955
+ policyKey: policy.policyKey,
1956
+ outcome: "success",
1957
+ checkpointId: this.sessionId,
1958
+ updatedAt: new Date().toISOString(),
1959
+ });
1960
+ }
1961
+ catch (error) {
1962
+ await this.sessionManager
1963
+ .writeCompactionAttemptState(conversationId, {
1964
+ fingerprint: sourceFingerprint,
1965
+ policyKey: policy.policyKey,
1966
+ outcome: "failed",
1967
+ updatedAt: new Date().toISOString(),
1968
+ expiresAt: new Date(Date.now() + 30_000).toISOString(),
1969
+ })
1970
+ .catch(() => { });
1971
+ throw error;
1972
+ }
1332
1973
  });
1333
1974
  }
1334
1975
  this.eventBus.emit("compaction_end", {
1335
- compacted: true,
1336
- originalCount: result.result.originalCount,
1337
- newCount: result.result.newCount,
1976
+ compacted: this.lastCompactionCompacted,
1977
+ originalCount,
1978
+ newCount: finalCount,
1979
+ ...(contextSelection
1980
+ ? {
1981
+ selectionStrategy: contextSelection.strategy,
1982
+ selectedMessages: contextSelection.selectedMessages,
1983
+ selectedTokens: contextSelection.selectedTokens,
1984
+ droppedMessages: contextSelection.droppedMessages,
1985
+ queryTerms: contextSelection.queryTerms,
1986
+ ...(contextSelection.fallbackReason
1987
+ ? { selectionFallback: contextSelection.fallbackReason }
1988
+ : {}),
1989
+ }
1990
+ : {}),
1338
1991
  });
1339
1992
  }
1340
1993
  async newSession(preserveConversation = false) {
@@ -1342,6 +1995,7 @@ export class AgentSession {
1342
1995
  // explicit new sessions reset the conversation identity.
1343
1996
  if (!preserveConversation) {
1344
1997
  this.conversationId = "";
1998
+ this.checkpointGeneration = 0;
1345
1999
  this.sessionPreview = "";
1346
2000
  }
1347
2001
  // A fresh session drops any in-flight plan state so its prompt is clean.
@@ -1355,11 +2009,9 @@ export class AgentSession {
1355
2009
  this.autopilotMarkers = [];
1356
2010
  this.appMarkers = [];
1357
2011
  this.turnMetrics = [];
1358
- const basePrompt = this.customSystemPrompt ??
1359
- (await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment()));
2012
+ const basePrompt = await this.buildBasePrompt(false, undefined);
1360
2013
  this.baseSystemPrompt = basePrompt;
1361
2014
  this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
1362
- this.syncUltraOrchestrationPrompt();
1363
2015
  // Fresh conversation — new entries must not chain onto the old DAG's leaf.
1364
2016
  this.currentLeafId = null;
1365
2017
  // Transient sessions (Nolan chat/autopilot, subagent spawns) never touch the
@@ -1370,6 +2022,7 @@ export class AgentSession {
1370
2022
  if (this.opts.transient) {
1371
2023
  this.sessionId = "";
1372
2024
  this.conversationId = "";
2025
+ this.checkpointGeneration = 0;
1373
2026
  this.sessionPreview = "";
1374
2027
  this.setSessionPath("");
1375
2028
  this.lastPersistedIndex = this.messages.length;
@@ -1442,6 +2095,41 @@ export class AgentSession {
1442
2095
  accountId: this.lastAccountId,
1443
2096
  };
1444
2097
  }
2098
+ /**
2099
+ * Tokens currently in context and the window they are measured against.
2100
+ *
2101
+ * Uses the same accounting as the compaction decision: authoritative provider
2102
+ * usage when we have it (it includes the system prompt and tool schemas),
2103
+ * plus a local estimate of anything appended after that sample. A client
2104
+ * reading this right after a compaction sees the drop, because compaction
2105
+ * clears the retained provider sample along with the messages it measured.
2106
+ *
2107
+ * `costUsd` is present only when EVERY recorded turn has an authoritative
2108
+ * price; a partial sum would read as a full session cost and understate it.
2109
+ */
2110
+ getContextUsage() {
2111
+ const size = getContextWindow(this.model, {
2112
+ provider: this.provider,
2113
+ accountId: this.lastAccountId,
2114
+ });
2115
+ let used;
2116
+ const anchorIndex = this.providerContext
2117
+ ? this.messages.lastIndexOf(this.providerContext.anchor)
2118
+ : -1;
2119
+ if (this.providerContext && anchorIndex >= 0) {
2120
+ used = calculateActiveContextTokens(this.messages, {
2121
+ usage: this.providerContext.usage,
2122
+ pendingMessages: this.messages.slice(anchorIndex + 1),
2123
+ });
2124
+ }
2125
+ else {
2126
+ used = calculateActiveContextTokens(this.messages);
2127
+ }
2128
+ const costUsd = this.turnMetrics.length > 0 && this.turnMetrics.every((m) => m.cost.status === "known")
2129
+ ? this.turnMetrics.reduce((sum, m) => sum + (m.cost.status === "known" ? m.cost.usd : 0), 0)
2130
+ : undefined;
2131
+ return costUsd === undefined ? { used, size } : { used, size, costUsd };
2132
+ }
1445
2133
  getPlanMode() {
1446
2134
  return this.planModeRef.current;
1447
2135
  }
@@ -1461,9 +2149,25 @@ export class AgentSession {
1461
2149
  * as steering. Returns the new queue length. No-op semantics are the caller's
1462
2150
  * concern. */
1463
2151
  queueMessage(text, attachments = []) {
1464
- this.userQueue.push({ text, attachments });
2152
+ this.queueSeq += 1;
2153
+ this.userQueue.push({ id: `q${this.queueSeq}`, text, attachments });
1465
2154
  return this.userQueue.length;
1466
2155
  }
2156
+ /** Pending queued messages (id + text), oldest first, for client display. */
2157
+ listQueuedMessages() {
2158
+ return this.userQueue.map((m) => ({ id: m.id, text: m.text }));
2159
+ }
2160
+ /** Cancel one pending message by id. Returns true if it was still queued.
2161
+ * A false return is the normal race rather than an error: the message drained
2162
+ * into the run between the client rendering the cancel affordance and the
2163
+ * click arriving. */
2164
+ cancelQueuedMessage(id) {
2165
+ const index = this.userQueue.findIndex((m) => m.id === id);
2166
+ if (index === -1)
2167
+ return false;
2168
+ this.userQueue.splice(index, 1);
2169
+ return true;
2170
+ }
1467
2171
  /** Number of messages currently queued. */
1468
2172
  getQueuedCount() {
1469
2173
  return this.userQueue.length;
@@ -1473,7 +2177,13 @@ export class AgentSession {
1473
2177
  * reviewing (no run in flight to steer it into) — unlike {@link drainQueue},
1474
2178
  * attachments survive so queued media isn't silently dropped. */
1475
2179
  takeNextQueuedMessage() {
1476
- return this.userQueue.shift() ?? null;
2180
+ const next = this.userQueue.shift();
2181
+ if (next === undefined)
2182
+ return null;
2183
+ // Strip the internal queue id: it exists only so clients can cancel a
2184
+ // specific pending message, and callers here feed the result straight into
2185
+ // a run.
2186
+ return { text: next.text, attachments: next.attachments };
1477
2187
  }
1478
2188
  /** Clear the queue, returning the combined text (to restore to the composer).
1479
2189
  * Queued attachments are dropped on cancel — the composer only restores text. */
@@ -1507,7 +2217,6 @@ export class AgentSession {
1507
2217
  else {
1508
2218
  this.messages.unshift({ role: "system", content });
1509
2219
  }
1510
- this.syncUltraOrchestrationPrompt();
1511
2220
  }
1512
2221
  /**
1513
2222
  * Toggle plan mode: flips the shared ref (so tools enforce read-only
@@ -1582,6 +2291,17 @@ export class AgentSession {
1582
2291
  await this.rebuildSystemPromptInPlace();
1583
2292
  return { ok: true, root: resolved };
1584
2293
  }
2294
+ /**
2295
+ * Names to advertise as available-on-demand. A tool the model already
2296
+ * promoted lives in `this.tools` and carries its own schema, so it drops out
2297
+ * of the index rather than being listed twice.
2298
+ */
2299
+ deferredToolNamesForPrompt(liveNames) {
2300
+ if (this.deferredBuiltinToolNames.length === 0)
2301
+ return [];
2302
+ const live = new Set(liveNames);
2303
+ return this.deferredBuiltinToolNames.filter((name) => !live.has(name));
2304
+ }
1585
2305
  /** Environment facts that vary per session rather than per host. */
1586
2306
  promptEnvironment() {
1587
2307
  const networkAllow = this.settingsManager.get("networkMode") === "allowlist"
@@ -1589,11 +2309,37 @@ export class AgentSession {
1589
2309
  : [];
1590
2310
  return { additionalRoots: this.additionalRoots, networkAllow };
1591
2311
  }
2312
+ /**
2313
+ * Build the stable system-prompt prefix for the current tool set and state.
2314
+ *
2315
+ * Three modes, in precedence order: a full replacement (`systemPrompt`), a
2316
+ * composed sub-agent prompt (`agentPrompt` — agent body plus Tools, project
2317
+ * context, return contract and Environment), or the standard prompt.
2318
+ */
2319
+ async buildBasePrompt(planMode, approvedPlanPath) {
2320
+ if (this.customSystemPrompt)
2321
+ return this.customSystemPrompt;
2322
+ const toolNames = this.tools.map((tool) => tool.name);
2323
+ const deferredToolNames = this.deferredToolNamesForPrompt(toolNames);
2324
+ if (this.agentPrompt !== undefined) {
2325
+ return buildSubAgentSystemPrompt(this.agentPrompt, {
2326
+ cwd: this.cwd,
2327
+ toolNames,
2328
+ deferredToolNames,
2329
+ context: this.opts.agentContext,
2330
+ environment: this.promptEnvironment(),
2331
+ });
2332
+ }
2333
+ return buildSystemPrompt(this.cwd, this.skills, planMode, approvedPlanPath, toolNames, undefined, this.provider, this.promptEnvironment(), deferredToolNames);
2334
+ }
1592
2335
  /** Rebuild messages[0] from current plan-mode + approved-plan state. */
1593
2336
  async rebuildSystemPromptInPlace() {
2337
+ // A full replacement is verbatim by contract — there is nothing to rebuild.
2338
+ // A composed agent prompt still must be: its Tools section has to follow
2339
+ // late-arriving MCP tools, or a compacted child loses tools it can call.
1594
2340
  if (this.customSystemPrompt)
1595
2341
  return;
1596
- const rebuilt = await buildSystemPrompt(this.cwd, this.skills, this.planModeRef.current, this.approvedPlanPath, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment());
2342
+ const rebuilt = await this.buildBasePrompt(this.planModeRef.current, this.approvedPlanPath);
1597
2343
  this.baseSystemPrompt = rebuilt;
1598
2344
  const content = this.withSystemPromptTail(rebuilt);
1599
2345
  if (this.messages[0]?.role === "system") {
@@ -1602,18 +2348,47 @@ export class AgentSession {
1602
2348
  else {
1603
2349
  this.messages.unshift({ role: "system", content });
1604
2350
  }
1605
- this.syncUltraOrchestrationPrompt();
1606
2351
  }
2352
+ /**
2353
+ * Compose the system message: stable prefix, then everything volatile behind
2354
+ * the `<!-- uncached -->` marker that providers split on for cache control.
2355
+ *
2356
+ * Anything that varies with the live model/provider/thinking level belongs in
2357
+ * the tail. Putting it in the prefix means every model switch rewrites cached
2358
+ * bytes and the next turn pays a full cache write instead of a read.
2359
+ */
1607
2360
  withSystemPromptTail(basePrompt) {
1608
- if (!this.opts.getSystemPromptTail)
2361
+ const tailParts = [];
2362
+ const orchestration = this.orchestrationPolicyTail();
2363
+ if (orchestration)
2364
+ tailParts.push(orchestration);
2365
+ const hostTail = this.opts.getSystemPromptTail?.();
2366
+ if (hostTail)
2367
+ tailParts.push(hostTail);
2368
+ if (tailParts.length === 0)
1609
2369
  return basePrompt;
1610
- return `${basePrompt}\n\n<!-- uncached -->\n${this.opts.getSystemPromptTail()}`;
2370
+ return `${basePrompt}\n\n<!-- uncached -->\n${tailParts.join("\n\n")}`;
2371
+ }
2372
+ /**
2373
+ * Sol/Terra async-orchestration guidance for the CURRENT model and thinking
2374
+ * level. Per-switch volatile by definition, so it is rendered as a tail part
2375
+ * rather than spliced into the cached prefix.
2376
+ */
2377
+ orchestrationPolicyTail() {
2378
+ if (this.opts.orchestrationPrompt === false)
2379
+ return "";
2380
+ return applyAsyncSubagentPolicy("", this.provider, this.model, this.thinkingLevel, this.tools.map((tool) => tool.name)).trim();
1611
2381
  }
2382
+ /**
2383
+ * Re-render the uncached tail of messages[0] in place. The cached prefix is
2384
+ * byte-identical afterwards, so this is safe to call on every model /
2385
+ * thinking-level change.
2386
+ */
1612
2387
  refreshSystemPromptTail() {
1613
- if (!this.opts.getSystemPromptTail)
1614
- return;
1615
2388
  const content = this.withSystemPromptTail(this.baseSystemPrompt);
1616
2389
  if (this.messages[0]?.role === "system") {
2390
+ if (this.messages[0].content === content)
2391
+ return;
1617
2392
  this.messages[0] = { role: "system", content };
1618
2393
  }
1619
2394
  else {
@@ -1668,6 +2443,29 @@ export class AgentSession {
1668
2443
  getAutopilotMarkers() {
1669
2444
  return this.autopilotMarkers;
1670
2445
  }
2446
+ /** Non-system messages that are actually on disk. Transcript markers anchor
2447
+ * against this (not the in-memory list, which can run ahead after a failed
2448
+ * run), so hosts computing marker-derived values must use the same base. */
2449
+ getPersistedTranscriptCount() {
2450
+ return this.persistedTranscriptCount();
2451
+ }
2452
+ /**
2453
+ * Rebase every transcript anchor (Nolan turns, autopilot verdicts, app markers)
2454
+ * onto a freshly compacted message list. Called right after `this.messages`
2455
+ * is replaced and before the markers are re-persisted into the continuation
2456
+ * file, so the new file carries positions that match its own transcript.
2457
+ */
2458
+ remapMarkerAnchors(remap) {
2459
+ if (!remap)
2460
+ return;
2461
+ const move = (payload) => ({
2462
+ ...payload,
2463
+ afterMessageCount: remapAnchorForCompaction(payload.afterMessageCount, remap),
2464
+ });
2465
+ this.nolanTurns = this.nolanTurns.map(move);
2466
+ this.autopilotMarkers = this.autopilotMarkers.map(move);
2467
+ this.appMarkers = this.appMarkers.map(move);
2468
+ }
1671
2469
  /**
1672
2470
  * Record one Nolan Grout (mentor agent) turn against this build session: the
1673
2471
  * user's question + Nolan's reply. Kept in memory for the live transcript and
@@ -1708,7 +2506,7 @@ export class AgentSession {
1708
2506
  id: crypto.randomUUID(),
1709
2507
  parentId: null,
1710
2508
  timestamp: new Date().toISOString(),
1711
- data: payload,
2509
+ data: stripRecordedPosition(payload),
1712
2510
  };
1713
2511
  await this.sessionManager.appendEntry(this.sessionPath, entry);
1714
2512
  }
@@ -1756,7 +2554,7 @@ export class AgentSession {
1756
2554
  id: crypto.randomUUID(),
1757
2555
  parentId: null,
1758
2556
  timestamp: new Date().toISOString(),
1759
- data: payload,
2557
+ data: stripRecordedPosition(payload),
1760
2558
  };
1761
2559
  await this.sessionManager.appendEntry(this.sessionPath, entry);
1762
2560
  }
@@ -1792,6 +2590,33 @@ export class AgentSession {
1792
2590
  };
1793
2591
  await this.sessionManager.appendEntry(this.sessionPath, entry);
1794
2592
  }
2593
+ /**
2594
+ * Open the run journal for one `RunLifecycle` generation.
2595
+ *
2596
+ * Never throws and never blocks the run: a session with no file (transient
2597
+ * children) writes nothing, and a write failure just means the run isn't
2598
+ * journalled — which is strictly better than failing the run over it.
2599
+ */
2600
+ async persistRunStarted(generation) {
2601
+ if (!this.sessionPath)
2602
+ return;
2603
+ await this.sessionManager.appendRunStarted(this.sessionPath, {
2604
+ version: 1,
2605
+ generation,
2606
+ startedAt: new Date().toISOString(),
2607
+ afterMessageCount: this.persistedTranscriptCount(),
2608
+ });
2609
+ }
2610
+ /** Close the run journal. Its absence is what marks a run as crashed. */
2611
+ async persistRunFinished(generation, outcome) {
2612
+ if (!this.sessionPath)
2613
+ return;
2614
+ await this.sessionManager.appendRunFinished(this.sessionPath, {
2615
+ version: 1,
2616
+ generation,
2617
+ outcome,
2618
+ });
2619
+ }
1795
2620
  /** Re-append the in-memory app markers to the current session file. Mirrors
1796
2621
  * `rePersistNolanTurns` — called after a continuation/compaction file is
1797
2622
  * created so display-only rows survive the rewrite. */
@@ -1805,7 +2630,7 @@ export class AgentSession {
1805
2630
  id: crypto.randomUUID(),
1806
2631
  parentId: null,
1807
2632
  timestamp: new Date().toISOString(),
1808
- data: payload,
2633
+ data: stripRecordedPosition(payload),
1809
2634
  };
1810
2635
  await this.sessionManager.appendEntry(this.sessionPath, entry);
1811
2636
  }
@@ -1851,16 +2676,7 @@ export class AgentSession {
1851
2676
  * effect on the next prompt, since the in-flight loop reads it at start. */
1852
2677
  setThinkingLevel(level) {
1853
2678
  this.thinkingLevel = level;
1854
- this.syncUltraOrchestrationPrompt();
1855
- }
1856
- /** Sol/Terra Ultra delegates proactively; lower levels require an explicit request. */
1857
- syncUltraOrchestrationPrompt() {
1858
- if (this.opts.orchestrationPrompt === false)
1859
- return;
1860
- const systemMessage = this.messages[0];
1861
- if (systemMessage?.role !== "system" || typeof systemMessage.content !== "string")
1862
- return;
1863
- systemMessage.content = applyAsyncSubagentPolicy(systemMessage.content, this.provider, this.model, this.thinkingLevel, this.tools.map((tool) => tool.name));
2679
+ this.refreshSystemPromptTail();
1864
2680
  }
1865
2681
  /** Replace the abort signal (e.g. after cancellation). */
1866
2682
  setSignal(signal) {
@@ -1923,19 +2739,28 @@ export class AgentSession {
1923
2739
  this.sessionManager.registerActivePath(nextPath);
1924
2740
  }
1925
2741
  async createNewSession() {
2742
+ const continuingConversation = Boolean(this.conversationId && this.sessionId);
1926
2743
  const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1927
2744
  conversationId: this.conversationId || undefined,
2745
+ generation: continuingConversation ? this.checkpointGeneration + 1 : 0,
2746
+ parentSessionId: continuingConversation ? this.sessionId : undefined,
1928
2747
  preview: this.sessionPreview || undefined,
1929
2748
  });
1930
2749
  this.sessionId = session.id;
2750
+ this.checkpointGeneration = session.header.generation ?? 0;
1931
2751
  this.conversationId = session.header.conversationId ?? session.id;
1932
2752
  this.setSessionPath(session.path);
1933
2753
  this.lastPersistedIndex = this.messages.length;
1934
2754
  }
1935
2755
  async loadExistingSession(sessionPath) {
1936
- const loaded = await this.sessionManager.load(sessionPath);
2756
+ // A stale physical checkpoint is only an address, not the conversation tip.
2757
+ // Resolve every resume—not just over-threshold/deferred compaction resumes—
2758
+ // before reading history so the next prompt cannot continue an old branch.
2759
+ const canonicalPath = (await this.sessionManager.resolveCanonicalSession(sessionPath, this.cwd)) ?? sessionPath;
2760
+ const loaded = await this.sessionManager.load(canonicalPath);
1937
2761
  // Use the leaf from the header to walk the correct branch
1938
2762
  const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
2763
+ this.checkpointGeneration = loaded.header.generation ?? 0;
1939
2764
  this.conversationId = loaded.header.conversationId ?? loaded.header.id;
1940
2765
  const legacyLabel = [...loaded.entries]
1941
2766
  .reverse()
@@ -1947,12 +2772,17 @@ export class AgentSession {
1947
2772
  legacyLabel || loaded.header.preview || findUserSessionPrompt(loadedMessages);
1948
2773
  // Restore Nolan's advisory turns (custom entries, not on the message branch) so
1949
2774
  // they reappear in the transcript and survive into the continuation file.
1950
- this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries);
2775
+ // The leaf lets marker replay retain its independent file-order position.
2776
+ this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries, loaded.header.leafId);
1951
2777
  // Restore autopilot verdict markers the same way (not on the message DAG).
1952
- this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries);
2778
+ this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
1953
2779
  // Restore app transcript markers (plan banner / task header / errors / hints).
1954
- this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries);
2780
+ this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
1955
2781
  this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
2782
+ // A run that opened the journal and never closed it died mid-flight. Read
2783
+ // it here, before anything rewrites the file, and report it once the
2784
+ // transcript is in place.
2785
+ const interruptedRuns = this.sessionManager.getUnfinishedRuns(loaded.entries);
1956
2786
  // Track the current leaf for subsequent entries
1957
2787
  this.currentLeafId = loaded.header.leafId;
1958
2788
  // Rebuild messages: keep system, add loaded. Older ezcoder-app sessions may
@@ -1977,63 +2807,41 @@ export class AgentSession {
1977
2807
  provider: this.provider,
1978
2808
  accountId: creds.accountId,
1979
2809
  });
2810
+ this.sessionId = loaded.header.id;
2811
+ this.setSessionPath(loaded.path);
2812
+ this.lastPersistedIndex = this.messages.length;
2813
+ const loadPolicy = resolveCompactionPolicy({
2814
+ provider: this.provider,
2815
+ model: this.model,
2816
+ contextWindow,
2817
+ threshold: this.settingsManager.get("compactThreshold"),
2818
+ accountId: creds.accountId,
2819
+ approvedPlanPath: this.approvedPlanPath,
2820
+ });
2821
+ log("INFO", "compaction", "Restore compaction decision", {
2822
+ provider: this.provider,
2823
+ model: this.model,
2824
+ transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
2825
+ contextWindow: String(contextWindow),
2826
+ activeTokens: "estimated",
2827
+ triggerLimit: String(loadPolicy.targetTokens),
2828
+ });
1980
2829
  const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
1981
- shouldCompact(this.messages, contextWindow, this.settingsManager.get("compactThreshold"));
2830
+ shouldCompact(this.messages, contextWindow, loadPolicy.threshold);
1982
2831
  if (needsLoadCompaction && this.opts.deferLoadCompaction) {
1983
- // Host readiness is gated on initialize() — don't block it on a summary
1984
- // LLM call (up to 30s). runLoop()'s pre-run auto-compaction picks this
1985
- // up on the first prompt and emits compaction_start/_end for the UI.
2832
+ // Canonicalize again immediately before the first prompt is persisted:
2833
+ // another process may create the shared checkpoint after this load.
2834
+ this.deferredCompactionPending = true;
1986
2835
  log("INFO", "session", "Restored session exceeds context — deferring compaction to first prompt");
1987
2836
  }
1988
2837
  else if (needsLoadCompaction) {
1989
2838
  await this.subAgentManager?.hydrate(loaded.header.id);
1990
2839
  log("INFO", "session", `Restored session exceeds context — auto-compacting`);
1991
- const compacted = await compact(this.messages, {
1992
- provider: this.provider,
1993
- model: this.model,
1994
- apiKey: creds.accessToken,
1995
- accountId: creds.accountId,
1996
- projectId: creds.projectId,
1997
- baseUrl: this.baseUrl ?? creds.baseUrl,
1998
- contextWindow,
1999
- signal: this.opts.signal,
2000
- });
2001
- this.messages = compacted.messages;
2002
- log("INFO", "session", `Auto-compaction complete`, {
2003
- before: String(compacted.result.originalCount),
2004
- after: String(compacted.result.newCount),
2005
- });
2006
- // Compaction rewrote history, so the on-disk file no longer reflects
2007
- // what's in memory — fork a fresh session file for the compacted state
2008
- // (mirrors compact()'s own persistence) so `ezcoder continue` picks up
2009
- // the summary instead of the full original transcript.
2010
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
2011
- conversationId: this.conversationId || undefined,
2012
- preview: this.sessionPreview || undefined,
2013
- });
2014
- this.sessionId = session.id;
2015
- this.conversationId = session.header.conversationId ?? session.id;
2016
- this.setSessionPath(session.path);
2017
- await this.subAgentManager?.rebindParentSession(this.sessionId);
2018
- this.currentLeafId = null;
2019
- // Re-persist (compacted) messages — skip system, it's rebuilt on load
2020
- for (const msg of this.messages) {
2021
- if (msg.role === "system")
2022
- continue;
2023
- await this.persistMessage(msg);
2840
+ await this.compact(creds, "automatic");
2841
+ if (this.lastCompactionCompacted) {
2842
+ await this.recordInterruptedRuns(interruptedRuns);
2843
+ return;
2024
2844
  }
2025
- this.lastPersistedIndex = this.messages.length;
2026
- // Carry restored evidence and Nolan turns into the continuation file.
2027
- await this.rePersistTurnMetrics();
2028
- await this.rePersistNolanTurns();
2029
- await this.rePersistAutopilotMarkers();
2030
- await this.rePersistAppMarkers();
2031
- // Record this load-time auto-compaction's counts for the resumed notice.
2032
- await this.persistAppMarker("compaction", {
2033
- originalCount: compacted.result.originalCount,
2034
- newCount: compacted.result.newCount,
2035
- });
2036
- return;
2037
2845
  }
2038
2846
  // Plain resume (no compaction needed): keep using the original session
2039
2847
  // file/id and append future turns to it in place. Forking a new file here
@@ -2043,13 +2851,39 @@ export class AgentSession {
2043
2851
  this.sessionId = loaded.header.id;
2044
2852
  this.setSessionPath(loaded.path);
2045
2853
  this.lastPersistedIndex = this.messages.length;
2854
+ await this.recordInterruptedRuns(interruptedRuns);
2855
+ }
2856
+ /**
2857
+ * Surface runs that died mid-flight, without resuming them.
2858
+ *
2859
+ * Deliberately NOT auto-resumed: the dead run's tools already wrote files,
2860
+ * ran commands and made commits. Replaying it would duplicate those effects.
2861
+ * The user gets a transcript row and decides.
2862
+ *
2863
+ * Each detected run is also closed as `aborted`, so reopening the session
2864
+ * reports it once rather than on every load.
2865
+ */
2866
+ async recordInterruptedRuns(runs) {
2867
+ for (const run of runs) {
2868
+ log("WARN", "session", "Restored a session with an unfinished run", {
2869
+ generation: String(run.generation),
2870
+ startedAt: run.startedAt,
2871
+ });
2872
+ await this.persistAppMarker("interrupted_run", {
2873
+ generation: run.generation,
2874
+ startedAt: run.startedAt,
2875
+ });
2876
+ await this.persistRunFinished(run.generation, "aborted");
2877
+ }
2046
2878
  }
2047
2879
  async prepareDynamicContext(_latestUserPrompt) {
2048
2880
  return this.messages;
2049
2881
  }
2050
2882
  async persistMessage(message) {
2051
- if (!this.sessionPreview && message.role === "user") {
2052
- this.sessionPreview = getUserSessionPrompt(message.content) ?? "";
2883
+ if (!this.sessionPreview &&
2884
+ message.role === "user" &&
2885
+ (message.provenance?.source === "human" || !message.provenance)) {
2886
+ this.sessionPreview = getUserSessionPrompt(message.content, message.provenance) ?? "";
2053
2887
  }
2054
2888
  // Transient sessions (subagent spawns) have no session file — skip.
2055
2889
  if (!this.sessionPath)
@@ -2069,7 +2903,7 @@ export class AgentSession {
2069
2903
  createSlashCommandContext() {
2070
2904
  return {
2071
2905
  switchModel: (provider, model) => this.switchModel(provider, model),
2072
- compact: () => this.compact(),
2906
+ compact: () => this.compact(undefined, "manual"),
2073
2907
  newSession: () => this.newSession(),
2074
2908
  listSessions: async () => {
2075
2909
  const sessions = await this.sessionManager.list(this.cwd);
@@ -2107,5 +2941,48 @@ export class AgentSession {
2107
2941
  getAdditionalRoots: () => this.getAdditionalRoots(),
2108
2942
  };
2109
2943
  }
2944
+ /**
2945
+ * Import a Claude Code / Codex / Cursor transcript as a resumable EZ Coder
2946
+ * session in this session's sessions directory. Never throws — a bad path or
2947
+ * an unrecognized format comes back as `{ ok: false, error }` so both the CLI
2948
+ * and the desktop app can show it verbatim.
2949
+ */
2950
+ async importForeignTranscript(filePath, opts = {}) {
2951
+ try {
2952
+ const imported = await importForeignSession({
2953
+ filePath: resolveHomePath(filePath),
2954
+ sessionManager: this.sessionManager,
2955
+ provider: this.provider,
2956
+ model: this.model,
2957
+ cwd: opts.cwd ?? undefined,
2958
+ });
2959
+ log("INFO", "import", "Imported foreign transcript", {
2960
+ format: imported.format,
2961
+ messages: String(imported.messageCount),
2962
+ });
2963
+ return {
2964
+ ok: true,
2965
+ sessionId: imported.sessionId,
2966
+ sessionPath: imported.sessionPath,
2967
+ cwd: imported.cwd,
2968
+ format: imported.format,
2969
+ messageCount: imported.messageCount,
2970
+ dropped: describeDropped(imported.dropped),
2971
+ ...(imported.preview ? { preview: imported.preview } : {}),
2972
+ };
2973
+ }
2974
+ catch (err) {
2975
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2976
+ }
2977
+ }
2978
+ }
2979
+ /** Expand a leading `~` so `/import ~/.codex/...` works from any shell. */
2980
+ function resolveHomePath(filePath) {
2981
+ if (filePath === "~")
2982
+ return os.homedir();
2983
+ if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
2984
+ return path.join(os.homedir(), filePath.slice(2));
2985
+ }
2986
+ return filePath;
2110
2987
  }
2111
2988
  //# sourceMappingURL=agent-session.js.map