@prestyj/cli 5.7.0 → 5.9.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 (364) hide show
  1. package/README.md +2 -2
  2. package/assets/skills/evidence-led-ui/LICENSES.md +12 -0
  3. package/assets/skills/evidence-led-ui/SKILL.md +14 -6
  4. package/assets/skills/evidence-led-ui/references/anti-defaults.md +9 -1
  5. package/assets/skills/evidence-led-ui/references/craft-rulings.md +33 -0
  6. package/assets/skills/evidence-led-ui/references/production-contract.md +41 -21
  7. package/assets/skills/evidence-led-ui/references/provenance.md +7 -5
  8. package/assets/skills/evidence-led-ui/references/quality-rubric.md +10 -10
  9. package/dist/app-sidecar-sessions.d.ts +8 -0
  10. package/dist/app-sidecar-sessions.d.ts.map +1 -0
  11. package/dist/app-sidecar-sessions.js +31 -0
  12. package/dist/app-sidecar-sessions.js.map +1 -0
  13. package/dist/app-sidecar-sessions.test.d.ts +2 -0
  14. package/dist/app-sidecar-sessions.test.d.ts.map +1 -0
  15. package/dist/app-sidecar-sessions.test.js +83 -0
  16. package/dist/app-sidecar-sessions.test.js.map +1 -0
  17. package/dist/app-sidecar.js +488 -156
  18. package/dist/app-sidecar.js.map +1 -1
  19. package/dist/cli/auth.d.ts.map +1 -1
  20. package/dist/cli/auth.js +5 -2
  21. package/dist/cli/auth.js.map +1 -1
  22. package/dist/cli/shared.d.ts.map +1 -1
  23. package/dist/cli/shared.js +2 -0
  24. package/dist/cli/shared.js.map +1 -1
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +61 -29
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config.d.ts +4 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +10 -0
  31. package/dist/config.js.map +1 -1
  32. package/dist/config.test.js +7 -0
  33. package/dist/config.test.js.map +1 -1
  34. package/dist/core/agent-session-compaction.test.js +540 -10
  35. package/dist/core/agent-session-compaction.test.js.map +1 -1
  36. package/dist/core/agent-session-memory-tail.test.js +1 -1
  37. package/dist/core/agent-session-memory-tail.test.js.map +1 -1
  38. package/dist/core/agent-session-queue.test.js +1 -1
  39. package/dist/core/agent-session-queue.test.js.map +1 -1
  40. package/dist/core/agent-session-review-coverage.test.js +26 -0
  41. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  42. package/dist/core/agent-session-tool-result-policy.test.d.ts +2 -0
  43. package/dist/core/agent-session-tool-result-policy.test.d.ts.map +1 -0
  44. package/dist/core/agent-session-tool-result-policy.test.js +25 -0
  45. package/dist/core/agent-session-tool-result-policy.test.js.map +1 -0
  46. package/dist/core/agent-session.d.ts +44 -8
  47. package/dist/core/agent-session.d.ts.map +1 -1
  48. package/dist/core/agent-session.js +369 -99
  49. package/dist/core/agent-session.js.map +1 -1
  50. package/dist/core/agents.d.ts +1 -1
  51. package/dist/core/agents.d.ts.map +1 -1
  52. package/dist/core/agents.js +4 -1
  53. package/dist/core/agents.js.map +1 -1
  54. package/dist/core/auth-providers.d.ts.map +1 -1
  55. package/dist/core/auth-providers.js +15 -8
  56. package/dist/core/auth-providers.js.map +1 -1
  57. package/dist/core/auth-storage.d.ts +1 -1
  58. package/dist/core/auth-storage.d.ts.map +1 -1
  59. package/dist/core/auth-storage.js +1 -1
  60. package/dist/core/auth-storage.js.map +1 -1
  61. package/dist/core/compaction/active-context.d.ts +17 -0
  62. package/dist/core/compaction/active-context.d.ts.map +1 -0
  63. package/dist/core/compaction/active-context.js +20 -0
  64. package/dist/core/compaction/active-context.js.map +1 -0
  65. package/dist/core/compaction/active-context.test.d.ts +2 -0
  66. package/dist/core/compaction/active-context.test.d.ts.map +1 -0
  67. package/dist/core/compaction/active-context.test.js +36 -0
  68. package/dist/core/compaction/active-context.test.js.map +1 -0
  69. package/dist/core/compaction/compactor.d.ts +19 -18
  70. package/dist/core/compaction/compactor.d.ts.map +1 -1
  71. package/dist/core/compaction/compactor.js +84 -53
  72. package/dist/core/compaction/compactor.js.map +1 -1
  73. package/dist/core/compaction/compactor.test.js +135 -65
  74. package/dist/core/compaction/compactor.test.js.map +1 -1
  75. package/dist/core/compaction/token-estimator.d.ts +25 -1
  76. package/dist/core/compaction/token-estimator.d.ts.map +1 -1
  77. package/dist/core/compaction/token-estimator.js +86 -1
  78. package/dist/core/compaction/token-estimator.js.map +1 -1
  79. package/dist/core/compaction/token-estimator.test.js +147 -1
  80. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  81. package/dist/core/compaction/tool-result-pruner.d.ts +37 -0
  82. package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -0
  83. package/dist/core/compaction/tool-result-pruner.js +103 -0
  84. package/dist/core/compaction/tool-result-pruner.js.map +1 -0
  85. package/dist/core/compaction/tool-result-pruner.test.d.ts +2 -0
  86. package/dist/core/compaction/tool-result-pruner.test.d.ts.map +1 -0
  87. package/dist/core/compaction/tool-result-pruner.test.js +148 -0
  88. package/dist/core/compaction/tool-result-pruner.test.js.map +1 -0
  89. package/dist/core/event-bus.d.ts +5 -0
  90. package/dist/core/event-bus.d.ts.map +1 -1
  91. package/dist/core/event-bus.js +6 -0
  92. package/dist/core/event-bus.js.map +1 -1
  93. package/dist/core/goal-controller.test.js +3 -3
  94. package/dist/core/goal-controller.test.js.map +1 -1
  95. package/dist/core/ideal-review.d.ts +6 -0
  96. package/dist/core/ideal-review.d.ts.map +1 -1
  97. package/dist/core/ideal-review.js +14 -0
  98. package/dist/core/ideal-review.js.map +1 -1
  99. package/dist/core/ideal-review.test.js +10 -1
  100. package/dist/core/ideal-review.test.js.map +1 -1
  101. package/dist/core/index.d.ts +3 -3
  102. package/dist/core/index.d.ts.map +1 -1
  103. package/dist/core/index.js +3 -3
  104. package/dist/core/index.js.map +1 -1
  105. package/dist/core/input-normalization.test.d.ts +2 -0
  106. package/dist/core/input-normalization.test.d.ts.map +1 -0
  107. package/dist/core/input-normalization.test.js +75 -0
  108. package/dist/core/input-normalization.test.js.map +1 -0
  109. package/dist/core/loop-breaker.d.ts +38 -1
  110. package/dist/core/loop-breaker.d.ts.map +1 -1
  111. package/dist/core/loop-breaker.js +84 -2
  112. package/dist/core/loop-breaker.js.map +1 -1
  113. package/dist/core/loop-breaker.test.js +108 -1
  114. package/dist/core/loop-breaker.test.js.map +1 -1
  115. package/dist/core/message-images.d.ts +12 -0
  116. package/dist/core/message-images.d.ts.map +1 -0
  117. package/dist/core/message-images.js +52 -0
  118. package/dist/core/message-images.js.map +1 -0
  119. package/dist/core/message-images.test.d.ts +2 -0
  120. package/dist/core/message-images.test.d.ts.map +1 -0
  121. package/dist/core/message-images.test.js +62 -0
  122. package/dist/core/message-images.test.js.map +1 -0
  123. package/dist/core/project-discovery.d.ts +7 -10
  124. package/dist/core/project-discovery.d.ts.map +1 -1
  125. package/dist/core/project-discovery.js +125 -66
  126. package/dist/core/project-discovery.js.map +1 -1
  127. package/dist/core/project-discovery.test.js +177 -0
  128. package/dist/core/project-discovery.test.js.map +1 -1
  129. package/dist/core/resolve-start.test.js +1 -0
  130. package/dist/core/resolve-start.test.js.map +1 -1
  131. package/dist/core/session-compaction.d.ts +3 -0
  132. package/dist/core/session-compaction.d.ts.map +1 -1
  133. package/dist/core/session-compaction.js +14 -1
  134. package/dist/core/session-compaction.js.map +1 -1
  135. package/dist/core/session-compaction.test.js +5 -0
  136. package/dist/core/session-compaction.test.js.map +1 -1
  137. package/dist/core/session-manager.d.ts +35 -8
  138. package/dist/core/session-manager.d.ts.map +1 -1
  139. package/dist/core/session-manager.js +264 -125
  140. package/dist/core/session-manager.js.map +1 -1
  141. package/dist/core/session-manager.test.js +17 -0
  142. package/dist/core/session-manager.test.js.map +1 -1
  143. package/dist/core/session-preview.d.ts +11 -0
  144. package/dist/core/session-preview.d.ts.map +1 -0
  145. package/dist/core/session-preview.js +50 -0
  146. package/dist/core/session-preview.js.map +1 -0
  147. package/dist/core/session-storage.d.ts +57 -0
  148. package/dist/core/session-storage.d.ts.map +1 -0
  149. package/dist/core/session-storage.js +568 -0
  150. package/dist/core/session-storage.js.map +1 -0
  151. package/dist/core/session-storage.test.d.ts +2 -0
  152. package/dist/core/session-storage.test.d.ts.map +1 -0
  153. package/dist/core/session-storage.test.js +269 -0
  154. package/dist/core/session-storage.test.js.map +1 -0
  155. package/dist/core/settings-manager.d.ts +3 -0
  156. package/dist/core/settings-manager.d.ts.map +1 -1
  157. package/dist/core/settings-manager.js +10 -2
  158. package/dist/core/settings-manager.js.map +1 -1
  159. package/dist/core/sidecar-error-reporter.d.ts +12 -0
  160. package/dist/core/sidecar-error-reporter.d.ts.map +1 -0
  161. package/dist/core/sidecar-error-reporter.js +57 -0
  162. package/dist/core/sidecar-error-reporter.js.map +1 -0
  163. package/dist/core/sidecar-error-reporter.test.d.ts +2 -0
  164. package/dist/core/sidecar-error-reporter.test.d.ts.map +1 -0
  165. package/dist/core/sidecar-error-reporter.test.js +61 -0
  166. package/dist/core/sidecar-error-reporter.test.js.map +1 -0
  167. package/dist/core/skills.d.ts +1 -1
  168. package/dist/core/skills.d.ts.map +1 -1
  169. package/dist/core/skills.js +4 -1
  170. package/dist/core/skills.js.map +1 -1
  171. package/dist/core/subagent-manager.d.ts +9 -4
  172. package/dist/core/subagent-manager.d.ts.map +1 -1
  173. package/dist/core/subagent-manager.js +30 -2
  174. package/dist/core/subagent-manager.js.map +1 -1
  175. package/dist/core/subagent-manager.test.js +47 -3
  176. package/dist/core/subagent-manager.test.js.map +1 -1
  177. package/dist/core/verify-commands.d.ts.map +1 -1
  178. package/dist/core/verify-commands.js +1 -6
  179. package/dist/core/verify-commands.js.map +1 -1
  180. package/dist/core/verify-commands.test.js +1 -5
  181. package/dist/core/verify-commands.test.js.map +1 -1
  182. package/dist/core/workspace-guard.d.ts +38 -0
  183. package/dist/core/workspace-guard.d.ts.map +1 -0
  184. package/dist/core/workspace-guard.js +112 -0
  185. package/dist/core/workspace-guard.js.map +1 -0
  186. package/dist/core/workspace-guard.test.d.ts +2 -0
  187. package/dist/core/workspace-guard.test.d.ts.map +1 -0
  188. package/dist/core/workspace-guard.test.js +86 -0
  189. package/dist/core/workspace-guard.test.js.map +1 -0
  190. package/dist/index.d.ts +1 -1
  191. package/dist/index.d.ts.map +1 -1
  192. package/dist/index.js +1 -1
  193. package/dist/index.js.map +1 -1
  194. package/dist/modes/json-mode.d.ts.map +1 -1
  195. package/dist/modes/json-mode.js +9 -0
  196. package/dist/modes/json-mode.js.map +1 -1
  197. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  198. package/dist/modes/subagent-worker-mode.js +11 -0
  199. package/dist/modes/subagent-worker-mode.js.map +1 -1
  200. package/dist/session.d.ts +4 -4
  201. package/dist/session.d.ts.map +1 -1
  202. package/dist/session.js +38 -101
  203. package/dist/session.js.map +1 -1
  204. package/dist/session.test.d.ts +2 -0
  205. package/dist/session.test.d.ts.map +1 -0
  206. package/dist/session.test.js +35 -0
  207. package/dist/session.test.js.map +1 -0
  208. package/dist/system-prompt.d.ts +12 -0
  209. package/dist/system-prompt.d.ts.map +1 -1
  210. package/dist/system-prompt.js +86 -19
  211. package/dist/system-prompt.js.map +1 -1
  212. package/dist/system-prompt.test.js +132 -8
  213. package/dist/system-prompt.test.js.map +1 -1
  214. package/dist/tools/bash.d.ts +12 -2
  215. package/dist/tools/bash.d.ts.map +1 -1
  216. package/dist/tools/bash.js +65 -24
  217. package/dist/tools/bash.js.map +1 -1
  218. package/dist/tools/bash.test.d.ts +2 -0
  219. package/dist/tools/bash.test.d.ts.map +1 -0
  220. package/dist/tools/bash.test.js +89 -0
  221. package/dist/tools/bash.test.js.map +1 -0
  222. package/dist/tools/edit.d.ts +2 -1
  223. package/dist/tools/edit.d.ts.map +1 -1
  224. package/dist/tools/edit.js +7 -1
  225. package/dist/tools/edit.js.map +1 -1
  226. package/dist/tools/edit.test.js +7 -0
  227. package/dist/tools/edit.test.js.map +1 -1
  228. package/dist/tools/exit-plan.d.ts.map +1 -1
  229. package/dist/tools/exit-plan.js +15 -4
  230. package/dist/tools/exit-plan.js.map +1 -1
  231. package/dist/tools/exit-plan.test.d.ts +2 -0
  232. package/dist/tools/exit-plan.test.d.ts.map +1 -0
  233. package/dist/tools/exit-plan.test.js +72 -0
  234. package/dist/tools/exit-plan.test.js.map +1 -0
  235. package/dist/tools/grep.d.ts.map +1 -1
  236. package/dist/tools/grep.js +8 -3
  237. package/dist/tools/grep.js.map +1 -1
  238. package/dist/tools/grep.test.js +6 -0
  239. package/dist/tools/grep.test.js.map +1 -1
  240. package/dist/tools/index.d.ts +8 -0
  241. package/dist/tools/index.d.ts.map +1 -1
  242. package/dist/tools/index.js +3 -2
  243. package/dist/tools/index.js.map +1 -1
  244. package/dist/tools/overflow.d.ts +12 -2
  245. package/dist/tools/overflow.d.ts.map +1 -1
  246. package/dist/tools/overflow.js +49 -4
  247. package/dist/tools/overflow.js.map +1 -1
  248. package/dist/tools/overflow.test.d.ts +2 -0
  249. package/dist/tools/overflow.test.d.ts.map +1 -0
  250. package/dist/tools/overflow.test.js +62 -0
  251. package/dist/tools/overflow.test.js.map +1 -0
  252. package/dist/tools/screenshot.js +1 -1
  253. package/dist/tools/screenshot.js.map +1 -1
  254. package/dist/tools/skill.d.ts.map +1 -1
  255. package/dist/tools/skill.js +4 -2
  256. package/dist/tools/skill.js.map +1 -1
  257. package/dist/tools/source-path.d.ts.map +1 -1
  258. package/dist/tools/source-path.js +9 -0
  259. package/dist/tools/source-path.js.map +1 -1
  260. package/dist/tools/subagent-shared.d.ts +9 -1
  261. package/dist/tools/subagent-shared.d.ts.map +1 -1
  262. package/dist/tools/subagent-shared.js +4 -1
  263. package/dist/tools/subagent-shared.js.map +1 -1
  264. package/dist/tools/subagent-shared.test.js +21 -1
  265. package/dist/tools/subagent-shared.test.js.map +1 -1
  266. package/dist/tools/subagent.d.ts +3 -8
  267. package/dist/tools/subagent.d.ts.map +1 -1
  268. package/dist/tools/subagent.js +10 -8
  269. package/dist/tools/subagent.js.map +1 -1
  270. package/dist/tools/subagent.test.js +17 -1
  271. package/dist/tools/subagent.test.js.map +1 -1
  272. package/dist/tools/web-search.d.ts.map +1 -1
  273. package/dist/tools/web-search.js +13 -2
  274. package/dist/tools/web-search.js.map +1 -1
  275. package/dist/tools/write.d.ts +2 -1
  276. package/dist/tools/write.d.ts.map +1 -1
  277. package/dist/tools/write.js +7 -1
  278. package/dist/tools/write.js.map +1 -1
  279. package/dist/tools/write.test.js +12 -0
  280. package/dist/tools/write.test.js.map +1 -1
  281. package/dist/ui/App.d.ts +0 -2
  282. package/dist/ui/App.d.ts.map +1 -1
  283. package/dist/ui/App.js +23 -87
  284. package/dist/ui/App.js.map +1 -1
  285. package/dist/ui/app-items.d.ts +10 -0
  286. package/dist/ui/app-items.d.ts.map +1 -1
  287. package/dist/ui/app-items.js +10 -0
  288. package/dist/ui/app-items.js.map +1 -1
  289. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  290. package/dist/ui/components/ActivityIndicator.js +1 -0
  291. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  292. package/dist/ui/components/Footer.test.js +1 -1
  293. package/dist/ui/components/ModelSelector.d.ts.map +1 -1
  294. package/dist/ui/components/ModelSelector.js +1 -0
  295. package/dist/ui/components/ModelSelector.js.map +1 -1
  296. package/dist/ui/components/SubAgentPanel.d.ts +2 -0
  297. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  298. package/dist/ui/components/SubAgentPanel.js +4 -2
  299. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  300. package/dist/ui/hooks/useAgentLoop.d.ts +7 -6
  301. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  302. package/dist/ui/hooks/useAgentLoop.js +56 -11
  303. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  304. package/dist/ui/hooks/useAgentLoop.test.js +31 -0
  305. package/dist/ui/hooks/useAgentLoop.test.js.map +1 -1
  306. package/dist/ui/hooks/useContextCompaction.d.ts +5 -8
  307. package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -1
  308. package/dist/ui/hooks/useContextCompaction.js +77 -16
  309. package/dist/ui/hooks/useContextCompaction.js.map +1 -1
  310. package/dist/ui/hooks/useContextCompaction.test.d.ts +2 -0
  311. package/dist/ui/hooks/useContextCompaction.test.d.ts.map +1 -0
  312. package/dist/ui/hooks/useContextCompaction.test.js +135 -0
  313. package/dist/ui/hooks/useContextCompaction.test.js.map +1 -0
  314. package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -1
  315. package/dist/ui/hooks/useSessionPersistence.js +3 -0
  316. package/dist/ui/hooks/useSessionPersistence.js.map +1 -1
  317. package/dist/ui/hooks/useTerminalTitle.d.ts +3 -3
  318. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  319. package/dist/ui/hooks/useTerminalTitle.js +5 -9
  320. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  321. package/dist/ui/hooks/useTranscriptHistory.d.ts +8 -5
  322. package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -1
  323. package/dist/ui/hooks/useTranscriptHistory.js +2 -30
  324. package/dist/ui/hooks/useTranscriptHistory.js.map +1 -1
  325. package/dist/ui/login.d.ts.map +1 -1
  326. package/dist/ui/login.js +7 -2
  327. package/dist/ui/login.js.map +1 -1
  328. package/dist/ui/render.d.ts +0 -2
  329. package/dist/ui/render.d.ts.map +1 -1
  330. package/dist/ui/render.js +0 -4
  331. package/dist/ui/render.js.map +1 -1
  332. package/dist/ui/render.test.js +10 -1
  333. package/dist/ui/render.test.js.map +1 -1
  334. package/dist/ui/terminal-history.js +6 -2
  335. package/dist/ui/terminal-history.js.map +1 -1
  336. package/dist/ui/terminal-history.test.js +2 -2
  337. package/dist/ui/terminal-history.test.js.map +1 -1
  338. package/dist/ui/tui-history-parity.test.js +8 -1
  339. package/dist/ui/tui-history-parity.test.js.map +1 -1
  340. package/dist/utils/git.d.ts +2 -0
  341. package/dist/utils/git.d.ts.map +1 -1
  342. package/dist/utils/git.js +12 -0
  343. package/dist/utils/git.js.map +1 -1
  344. package/dist/utils/git.test.d.ts +2 -0
  345. package/dist/utils/git.test.d.ts.map +1 -0
  346. package/dist/utils/git.test.js +36 -0
  347. package/dist/utils/git.test.js.map +1 -0
  348. package/dist/utils/http-body.d.ts +19 -0
  349. package/dist/utils/http-body.d.ts.map +1 -0
  350. package/dist/utils/http-body.js +58 -0
  351. package/dist/utils/http-body.js.map +1 -0
  352. package/dist/utils/http-body.test.d.ts +2 -0
  353. package/dist/utils/http-body.test.d.ts.map +1 -0
  354. package/dist/utils/http-body.test.js +68 -0
  355. package/dist/utils/http-body.test.js.map +1 -0
  356. package/dist/utils/text.d.ts +9 -0
  357. package/dist/utils/text.d.ts.map +1 -0
  358. package/dist/utils/text.js +11 -0
  359. package/dist/utils/text.js.map +1 -0
  360. package/package.json +5 -5
  361. package/dist/utils/session-title.d.ts +0 -19
  362. package/dist/utils/session-title.d.ts.map +0 -1
  363. package/dist/utils/session-title.js +0 -82
  364. package/dist/utils/session-title.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { agentLoop, isAbortError, } from "@prestyj/agent";
1
+ import { agentLoop, isAbortError, isUsageLimitError, } from "@prestyj/agent";
2
2
  import { ProviderError, } from "@prestyj/ai";
3
3
  import { EventBus } from "./event-bus.js";
4
4
  import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
@@ -6,12 +6,13 @@ 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
10
  import { getClaudeCliUserAgent } from "./claude-code-version.js";
10
11
  import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
11
12
  import { SessionManager, NOLAN_TURN_CUSTOM_KIND, AUTOPILOT_MARKER_CUSTOM_KIND, APP_MARKER_CUSTOM_KIND, } from "./session-manager.js";
12
13
  import { ExtensionLoader } from "./extensions/loader.js";
13
- import { shouldCompact, compact, getCompactionReserveTokens } from "./compaction/compactor.js";
14
- import { getAuthStorageKeys, getContextWindow, getModel, MODELS } from "./model-registry.js";
14
+ import { shouldCompact, compact } from "./compaction/compactor.js";
15
+ import { getAuthStorageKeys, getContextWindow, getModel, getToolResultCharLimit, MODELS, } from "./model-registry.js";
15
16
  import { discoverSkills } from "./skills.js";
16
17
  import { ensureAppDirs } from "../config.js";
17
18
  import { buildSystemPrompt } from "../system-prompt.js";
@@ -22,18 +23,39 @@ import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
22
23
  import { DeferredToolCatalog } from "./mcp/deferred-catalog.js";
23
24
  import { createToolSearchTool } from "../tools/tool-search.js";
24
25
  import { log } from "./logger.js";
25
- import { setEstimatorModel } from "./compaction/token-estimator.js";
26
+ import { setEstimatorModel, calibrateEstimatorFromUsage } from "./compaction/token-estimator.js";
27
+ import { calculateActiveContextTokens } from "./compaction/active-context.js";
28
+ import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
26
29
  import { discoverAgents } from "./agents.js";
27
- import { generateSessionTitle } from "../utils/session-title.js";
28
30
  import { enhancePrompt } from "../utils/prompt-enhancer.js";
29
31
  import { detectProjectStack } from "./language-detector.js";
30
- import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
31
- import { evaluateLoopBreak, buildLoopBreakMessage, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
32
+ import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
33
+ import { evaluateLoopBreak, buildLoopBreakMessage, CycleDetector, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
32
34
  import { buildRegroundingMessage } from "./regrounding.js";
33
35
  import { wrapSteeringText, STEERING_PREFIX } from "./steering.js";
36
+ import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
37
+ import { normalizeMessageImages } from "./message-images.js";
34
38
  import crypto from "node:crypto";
35
39
  import fs from "node:fs/promises";
36
40
  import path from "node:path";
41
+ // ── Tool-result policy ─────────────────────────────────────
42
+ /** Resolve the per-result cap passed to the agent loop for the active transport. */
43
+ export function resolveSessionToolResultCharLimit(model, provider, accountId) {
44
+ return (getToolResultCharLimit(model, { provider, accountId }) ??
45
+ Math.floor(getContextWindow(model, { provider, accountId }) * 3.5 * 0.3));
46
+ }
47
+ /**
48
+ * Aggregate budget for ALL tool results produced in one assistant turn.
49
+ * Individual results are already capped, but wide parallel fan-outs (GPT-5.6's
50
+ * signature behavior) were observed injecting 100k+ uncached tokens in a single
51
+ * turn. ~15% of the context window in chars (1 token ≈ 3.5 chars), floored at
52
+ * 100KB so small windows still fit two full-size reads, ceilinged at 240KB so
53
+ * 1M-context models don't waive the budget entirely.
54
+ */
55
+ export function resolveSessionTurnToolResultCharLimit(model, provider, accountId) {
56
+ const contextChars = getContextWindow(model, { provider, accountId }) * 3.5;
57
+ return Math.max(100_000, Math.min(Math.floor(contextChars * 0.15), 240_000));
58
+ }
37
59
  // ── Agent Session ──────────────────────────────────────────
38
60
  export class AgentSession {
39
61
  eventBus = new EventBus();
@@ -84,13 +106,22 @@ export class AgentSession {
84
106
  hookConsecutiveFailures = 0;
85
107
  hookRepeatedNoProgressCalls = 0;
86
108
  hookProgressTracker = new ToolCallProgressTracker();
109
+ hookCycleDetector = new CycleDetector();
110
+ hookCyclicPattern = null;
87
111
  hookFileEditCounts = new Map();
88
112
  hookToolCalls = new Map();
89
113
  idealReviewPhase = "idle";
114
+ /** Runtime-only suppression while Nolan owns verification in autopilot mode. */
115
+ idealReviewSuppressed = false;
90
116
  reviewCoverage;
91
- loopBreakInjected = false;
117
+ /** 0 = none; 1 = first nudge sent; 2 = final stop-and-report injected. */
118
+ loopBreakInjected = 0;
92
119
  regroundingInjected = false;
93
120
  compactionOccurred = false;
121
+ lastCompactionCompacted = false;
122
+ compactionRetryAfter = 0;
123
+ /** Latest provider count, anchored to the assistant response it measured. */
124
+ providerContext = null;
94
125
  originalRequest = "";
95
126
  // Messages queued by the user while a run is in flight. Drained at the
96
127
  // mid-loop steering boundary (user steering wins over the hooks), mirroring
@@ -128,6 +159,10 @@ export class AgentSession {
128
159
  * model emits step-completion markers the UI's plan-progress widget reads. */
129
160
  approvedPlanPath;
130
161
  sessionId = "";
162
+ /** Stable identity shared by compaction and approved-plan checkpoint files. */
163
+ conversationId = "";
164
+ /** Original user-authored prompt, retained when internal messages replace history. */
165
+ sessionPreview = "";
131
166
  /** Runtime conversation identity for provider transport headers. Transient
132
167
  * children need one even though they intentionally have no persisted session. */
133
168
  transportSessionId = crypto.randomUUID();
@@ -206,6 +241,9 @@ export class AgentSession {
206
241
  provider: this.provider,
207
242
  model: this.model,
208
243
  lspDiagnostics: this.settingsManager.get("lspDiagnostics"),
244
+ getWriteGuardSettings: () => ({
245
+ allowOutsideWorkspaceWrites: this.settingsManager.get("allowOutsideWorkspaceWrites"),
246
+ }),
209
247
  authStorage: this.authStorage,
210
248
  onFileRead: (filePath) => this.reviewCoverage.recordRead(filePath),
211
249
  onFileMutated: (filePath) => {
@@ -220,6 +258,7 @@ export class AgentSession {
220
258
  getThinkingLevel: () => this.thinkingLevel,
221
259
  getBaseUrl: () => this.baseUrl,
222
260
  getCacheKey: () => this.getPromptCacheKey(),
261
+ getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
223
262
  disableAsyncSubagents: this.opts.subagentWorker,
224
263
  onSubAgentState: (snapshot) => this.eventBus.emit("subagent_state", snapshot),
225
264
  // Plan mode: only wired when the host supplies callbacks. The ref is
@@ -294,6 +333,32 @@ export class AgentSession {
294
333
  }
295
334
  if (this.sessionId)
296
335
  await this.subAgentManager?.hydrate(this.sessionId);
336
+ // Maintenance is deliberately queued after initialization work and never
337
+ // awaited, so retention/compression cannot delay sidecar or CLI readiness.
338
+ if (!this.opts.transient) {
339
+ const retentionDays = this.settingsManager.get("sessionRetentionDays");
340
+ void Promise.resolve()
341
+ .then(() => this.sessionManager.runMaintenance({
342
+ retentionDays,
343
+ keepPaths: this.sessionPath ? [this.sessionPath] : [],
344
+ }))
345
+ .then((metrics) => {
346
+ if (metrics.deletedFiles > 0 || metrics.archivedFiles > 0 || metrics.failures > 0) {
347
+ log("INFO", "session", "Session maintenance complete", {
348
+ deletedFiles: String(metrics.deletedFiles),
349
+ deletedMB: (metrics.deletedBytes / 1024 / 1024).toFixed(1),
350
+ archivedFiles: String(metrics.archivedFiles),
351
+ savedMB: (metrics.bytesSaved / 1024 / 1024).toFixed(1),
352
+ failures: String(metrics.failures),
353
+ });
354
+ }
355
+ })
356
+ .catch((error) => {
357
+ log("WARN", "session", "Session maintenance failed", {
358
+ message: error instanceof Error ? error.message : String(error),
359
+ });
360
+ });
361
+ }
297
362
  // EZ Coder owns its command registry. Other agents start with an isolated
298
363
  // empty registry and can register their own commands in their own file.
299
364
  if (this.opts.coderSlashCommands !== false) {
@@ -586,11 +651,13 @@ export class AgentSession {
586
651
  this.hookConsecutiveFailures = 0;
587
652
  this.hookRepeatedNoProgressCalls = 0;
588
653
  this.hookProgressTracker.reset();
654
+ this.hookCycleDetector.reset();
655
+ this.hookCyclicPattern = null;
589
656
  this.hookFileEditCounts.clear();
590
657
  this.hookToolCalls.clear();
591
658
  this.reviewCoverage.reset();
592
659
  this.idealReviewPhase = "idle";
593
- this.loopBreakInjected = false;
660
+ this.loopBreakInjected = 0;
594
661
  this.regroundingInjected = false;
595
662
  this.compactionOccurred = false;
596
663
  this.originalRequest = originalRequest;
@@ -623,6 +690,7 @@ export class AgentSession {
623
690
  this.hookStats.bashCalls += 1;
624
691
  this.hookConsecutiveFailures = event.isError ? this.hookConsecutiveFailures + 1 : 0;
625
692
  this.hookRepeatedNoProgressCalls = this.hookProgressTracker.record(name, args, event.result, event.isError);
693
+ this.hookCyclicPattern = this.hookCycleDetector.record(name, args, event.result, event.isError);
626
694
  if (name === "edit" && !event.isError) {
627
695
  const diff = event.details?.diff ?? event.result;
628
696
  const added = (diff.match(/^\+[^+]/gm) ?? []).length;
@@ -633,6 +701,13 @@ export class AgentSession {
633
701
  }
634
702
  case "turn_end":
635
703
  this.hookStats.turns = event.turn;
704
+ for (let index = this.messages.length - 1; index >= 0; index--) {
705
+ const anchor = this.messages[index];
706
+ if (anchor?.role === "assistant") {
707
+ this.providerContext = { usage: { ...event.usage }, anchor };
708
+ break;
709
+ }
710
+ }
636
711
  await this.persistTurnMetric(event);
637
712
  break;
638
713
  }
@@ -670,19 +745,34 @@ export class AgentSession {
670
745
  return null;
671
746
  if (!this.settingsManager.get("idealReviewEnabled"))
672
747
  return null;
673
- if (!this.loopBreakInjected) {
748
+ // Two-stage loop-breaker: stage 1 nudges; a FRESH detection after that
749
+ // injects the harsher final stop-and-report prompt. Signals reset after
750
+ // each injection so stage 2 only fires on new evidence.
751
+ if (this.loopBreakInjected < 2) {
674
752
  const decision = evaluateLoopBreak({
675
753
  consecutiveFailures: this.hookConsecutiveFailures,
676
754
  repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
677
755
  textRepetitionDetected: detectTextRepetition(this.hookText),
756
+ ...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
678
757
  });
679
758
  if (decision.shouldBreak) {
680
- this.loopBreakInjected = true;
759
+ const stage = this.loopBreakInjected === 0 ? 1 : 2;
760
+ this.loopBreakInjected = stage;
761
+ this.hookProgressTracker.reset();
762
+ this.hookCycleDetector.reset();
763
+ this.hookCyclicPattern = null;
764
+ this.hookConsecutiveFailures = 0;
765
+ this.hookRepeatedNoProgressCalls = 0;
766
+ // Clear the text buffer too — otherwise a stage-1 text-repetition
767
+ // trigger still sees the same repeated tail on the next check and
768
+ // escalates to stage 2 on stale evidence.
769
+ this.hookText = "";
681
770
  log("INFO", "loop-break", "Injecting loop-break nudge", {
771
+ stage: String(stage),
682
772
  reasons: decision.reasons.join(", "),
683
773
  });
684
774
  this.eventBus.emit("hook", { kind: "loop_break" });
685
- return [buildLoopBreakMessage(decision.reasons)];
775
+ return [buildLoopBreakMessage(decision.reasons, stage === 2)];
686
776
  }
687
777
  }
688
778
  if (!this.regroundingInjected && this.compactionOccurred) {
@@ -700,7 +790,7 @@ export class AgentSession {
700
790
  const childCompletionFollowUp = buildSubAgentCompletionFollowUp(this.subAgentManager);
701
791
  if (childCompletionFollowUp)
702
792
  return childCompletionFollowUp;
703
- if (this.opts.selfCorrectionHooks === false)
793
+ if (this.opts.selfCorrectionHooks === false || this.idealReviewSuppressed)
704
794
  return null;
705
795
  if (this.idealReviewPhase === "reviewing") {
706
796
  const coverage = this.reviewCoverage.evidence();
@@ -745,7 +835,7 @@ export class AgentSession {
745
835
  lspMissing: lspEvidence.missing,
746
836
  });
747
837
  return [
748
- this.withReviewLspEvidence(buildIdealReviewMessage(decision.reasons, driftedFiles), lspEvidence),
838
+ this.withReviewLspEvidence(withReviewCoverageRequirements(buildIdealReviewMessage(decision.reasons, driftedFiles), coverage.missing), lspEvidence),
749
839
  ];
750
840
  }
751
841
  reviewLspEvidence(files) {
@@ -805,24 +895,45 @@ export class AgentSession {
805
895
  this.lastAccountId = creds.accountId;
806
896
  // Auto-compact if needed. This must happen after credential resolution so
807
897
  // OpenAI OAuth/Codex sessions use the Codex product context window instead
808
- // of the public API model window.
809
- if (this.settingsManager.get("autoCompact")) {
898
+ // of the public API model window. Failed/no-op attempts cool down across
899
+ // prompts; provider overflow recovery still bypasses this path entirely.
900
+ if (this.settingsManager.get("autoCompact") && Date.now() >= this.compactionRetryAfter) {
810
901
  const contextWindow = getContextWindow(this.model, {
811
902
  provider: this.provider,
812
903
  accountId: creds.accountId,
813
904
  });
814
905
  const threshold = this.settingsManager.get("compactThreshold");
815
- // Reserve headroom for this model's real output budget (e.g. GPT-5.5 over
816
- // Codex OAuth: 272K window but up to 128K max output) — without this the
817
- // default 16K reserve lets compaction skip until input alone is near the
818
- // window, then `input + max_tokens` exceeds it and the provider rejects
819
- // the turn outright with "exceeds the context window". Mirrors the TUI's
820
- // useContextCompaction hook.
821
- const reserveTokens = getCompactionReserveTokens(this.maxTokens);
822
- if (shouldCompact(this.messages, contextWindow, threshold, undefined, reserveTokens)) {
823
- await this.compact(creds);
824
- // Re-grounding hook keys off this — the context was just summarized.
825
- this.compactionOccurred = true;
906
+ let activeTokens;
907
+ if (this.providerContext) {
908
+ const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
909
+ if (anchorIndex >= 0) {
910
+ activeTokens = calculateActiveContextTokens(this.messages, {
911
+ usage: this.providerContext.usage,
912
+ pendingMessages: this.messages.slice(anchorIndex + 1),
913
+ });
914
+ }
915
+ else {
916
+ this.providerContext = null;
917
+ }
918
+ }
919
+ if (shouldCompact(this.messages, contextWindow, threshold, activeTokens)) {
920
+ try {
921
+ await this.compact(creds);
922
+ if (this.lastCompactionCompacted) {
923
+ // Re-grounding hook keys off this — the context was just summarized.
924
+ this.compactionOccurred = true;
925
+ this.compactionRetryAfter = 0;
926
+ }
927
+ else {
928
+ this.compactionRetryAfter = Date.now() + 30_000;
929
+ }
930
+ }
931
+ catch (error) {
932
+ this.compactionRetryAfter = Date.now() + 30_000;
933
+ if (isAbortError(error) || this.opts.signal?.aborted)
934
+ throw error;
935
+ log("WARN", "compaction", `Pre-run compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
936
+ }
826
937
  }
827
938
  }
828
939
  const userAgent = this.provider === "anthropic" ? await getClaudeCliUserAgent() : undefined;
@@ -856,29 +967,99 @@ export class AgentSession {
856
967
  supportsImages: modelInfo?.supportsImages,
857
968
  supportsVideo: modelInfo?.supportsVideo,
858
969
  userAgent,
859
- // clearToolUses disabled — causes model to output unsolicited context summaries
860
- // Single tool result shouldn't exceed 30% of context window (in chars)
861
- maxToolResultChars: Math.floor(getContextWindow(this.model, { provider: this.provider, accountId }) * 3.5 * 0.3),
970
+ // Codex caps each tool output at 10K tokens. Other transports retain the
971
+ // generic 30%-of-context allowance used before this provider policy.
972
+ maxToolResultChars: resolveSessionToolResultCharLimit(this.model, this.provider, accountId),
973
+ // Aggregate per-turn budget across parallel tool results (fan-out guard).
974
+ maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
862
975
  // Self-correction hooks (same as the TUI): loop-break + re-grounding are
863
976
  // polled mid-loop; the ideal review is polled when the agent would stop.
864
977
  getSteeringMessages: () => this.getHookSteeringMessages(),
865
978
  getFollowUpMessages: () => this.getHookFollowUpMessages(),
866
- // Overflow recovery: the loop calls this with { force: true } when the
867
- // provider rejects a turn as too large (request_too_large / context
868
- // overflow). Force-compact the in-flight history and hand it back so the
869
- // loop retries with a smaller request, instead of surfacing the error.
870
- // Without this the desktop app (which drives the loop through
871
- // AgentSession, not the TUI's useContextCompaction hook) had NO auto
872
- // recovery on 413 — the error went straight to the user. The non-force
873
- // pre-call invocations pass through untouched: pre-turn compaction is
874
- // already handled above, so we only act on the overflow force path.
875
- // `loopMessages === this.messages` (prepareDynamicContext returns it by
876
- // reference) and the post-loop `this.messages = loopMessages` re-sync
877
- // keeps persistence correct after compact() swaps the array.
979
+ // Check authoritative provider usage before every model/tool step.
980
+ // Forced overflow recovery bypasses settings and cooldown; proactive
981
+ // checks honor both and estimate only messages unseen by the provider.
878
982
  transformContext: async (messages, transformOpts) => {
879
- if (!transformOpts?.force)
983
+ if (transformOpts.usage) {
984
+ const anchorIndex = messages.length - transformOpts.pendingMessages.length - 1;
985
+ const anchor = messages[anchorIndex];
986
+ if (anchor?.role === "assistant") {
987
+ this.providerContext = { usage: { ...transformOpts.usage }, anchor };
988
+ // Feed the authoritative usage back into the token estimator so
989
+ // char-based estimates track this session's real tokenizer.
990
+ calibrateEstimatorFromUsage(messages.slice(0, anchorIndex), transformOpts.usage);
991
+ }
992
+ }
993
+ const force = transformOpts.force === true;
994
+ if (!force) {
995
+ if (!this.settingsManager.get("autoCompact"))
996
+ return messages;
997
+ // Cheap stale-tool-output pruning before the expensive LLM
998
+ // compaction check. In-place mutation preserves anchors; drop the
999
+ // retained usage afterwards since it counted the pruned content.
1000
+ const pruneResult = pruneStaleToolResults(messages);
1001
+ if (pruneResult.pruned) {
1002
+ this.providerContext = null;
1003
+ log("INFO", "compaction", "Pruned stale tool outputs", {
1004
+ prunedResults: String(pruneResult.prunedResults),
1005
+ freedTokens: String(pruneResult.freedTokens),
1006
+ });
1007
+ }
1008
+ if (Date.now() < this.compactionRetryAfter)
1009
+ return messages;
1010
+ // The turn's own usage also counted the pruned content — after a
1011
+ // prune, fall back to estimating the (now smaller) history so the
1012
+ // freed tokens actually defer the LLM compaction.
1013
+ let usage = pruneResult.pruned ? undefined : transformOpts.usage;
1014
+ let pendingMessages = transformOpts.pendingMessages;
1015
+ if (!usage && this.providerContext) {
1016
+ const anchorIndex = messages.lastIndexOf(this.providerContext.anchor);
1017
+ if (anchorIndex >= 0) {
1018
+ usage = this.providerContext.usage;
1019
+ pendingMessages = messages.slice(anchorIndex + 1);
1020
+ }
1021
+ else {
1022
+ this.providerContext = null;
1023
+ }
1024
+ }
1025
+ const contextWindow = getContextWindow(this.model, {
1026
+ provider: this.provider,
1027
+ accountId,
1028
+ });
1029
+ const threshold = this.settingsManager.get("compactThreshold");
1030
+ const activeTokens = calculateActiveContextTokens(messages, {
1031
+ usage,
1032
+ pendingMessages,
1033
+ });
1034
+ if (!shouldCompact(messages, contextWindow, threshold, activeTokens))
1035
+ return messages;
1036
+ }
1037
+ // compact() operates on this.messages, while an earlier transform may
1038
+ // have replaced the loop's in-flight array. Rebind before every attempt
1039
+ // so the current tool results are included in the summary.
1040
+ this.messages = messages;
1041
+ try {
1042
+ await this.compact({
1043
+ accessToken: apiKey,
1044
+ accountId,
1045
+ projectId,
1046
+ baseUrl: effectiveBaseUrl,
1047
+ });
1048
+ }
1049
+ catch (error) {
1050
+ this.messages = messages;
1051
+ this.compactionRetryAfter = Date.now() + 30_000;
1052
+ if (force || isAbortError(error) || this.opts.signal?.aborted)
1053
+ throw error;
1054
+ log("WARN", "compaction", `In-flight compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
1055
+ return messages;
1056
+ }
1057
+ if (!this.lastCompactionCompacted) {
1058
+ this.messages = messages;
1059
+ this.compactionRetryAfter = Date.now() + 30_000;
880
1060
  return messages;
881
- await this.compact();
1061
+ }
1062
+ this.compactionRetryAfter = 0;
882
1063
  this.compactionOccurred = true;
883
1064
  return this.messages;
884
1065
  },
@@ -888,6 +1069,19 @@ export class AgentSession {
888
1069
  this.eventBus.forwardAgentEvent(event);
889
1070
  }
890
1071
  };
1072
+ const clearInvalidStaticApiKey = async (error) => {
1073
+ if (!(error instanceof ProviderError) || error.statusCode !== 401)
1074
+ return false;
1075
+ if (!(await this.authStorage.isStaticApiKey(this.provider)))
1076
+ return false;
1077
+ // Clear whichever key actually resolved (the request may have used a
1078
+ // fallback key, not the model's first preference).
1079
+ const badKey = (await this.authStorage.pickStorageKey(this.currentAuthStorageKeys())) ??
1080
+ this.currentAuthStorageKeys()[0];
1081
+ log("WARN", "auth", `Got 401 for ${this.provider} (${badKey}) — API key is invalid or revoked`);
1082
+ await this.authStorage.clearCredentials(badKey);
1083
+ return true;
1084
+ };
891
1085
  try {
892
1086
  await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
893
1087
  }
@@ -896,21 +1090,48 @@ export class AgentSession {
896
1090
  if (isAbortError(err) || this.opts.signal?.aborted) {
897
1091
  return;
898
1092
  }
899
- if (err instanceof ProviderError && err.statusCode === 401) {
1093
+ // Kimi OAuth plan ran out of usage (hard usage-limit stop, or an HTTP 402
1094
+ // billing stop). If the user ALSO configured a Moonshot API key, mark
1095
+ // the OAuth credential usage-exhausted (honoring the provider-stated
1096
+ // reset time when present) and retry this turn on the API key — OAuth
1097
+ // stays the preferred credential and resumes automatically once the mark
1098
+ // lapses. A generic 429 is deliberately excluded: it may be a transient
1099
+ // rate limit and must not silently switch the user to a billed API key.
1100
+ // Guarded on the Kimi managed endpoint actually being in use: if the API
1101
+ // key was already active, the same error means BOTH are out and must surface.
1102
+ if (this.provider === "moonshot" &&
1103
+ !this.baseUrl &&
1104
+ isKimiCodingEndpoint(creds.baseUrl) &&
1105
+ (isUsageLimitError(err) || (err instanceof ProviderError && err.statusCode === 402)) &&
1106
+ (await this.authStorage.hasCredentials("moonshot"))) {
1107
+ const resetsAt = err instanceof ProviderError ? err.resetsAt : undefined;
1108
+ await this.authStorage.markUsageExhausted(MOONSHOT_OAUTH_KEY, resetsAt);
1109
+ log("WARN", "auth", "Kimi OAuth usage limit reached — retrying this turn on the Moonshot API key", { resetsAt: resetsAt !== undefined ? String(resetsAt) : "unknown" });
1110
+ creds = await this.authStorage.resolveCredentials(this.provider, {
1111
+ storageKeys: this.currentAuthStorageKeys(),
1112
+ });
1113
+ this.lastAccountId = creds.accountId;
1114
+ // The runAgentLoop closure re-reads `creds`, so the retry picks up the
1115
+ // API key's baseUrl (api.moonshot.ai) and drops the Kimi coding headers.
1116
+ try {
1117
+ await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
1118
+ }
1119
+ catch (fallbackErr) {
1120
+ // The fallback is inside this catch branch, so its errors do not pass
1121
+ // through the outer 401 handler. Clear a rejected API key explicitly
1122
+ // before surfacing the error and prompting the user to log in again.
1123
+ await clearInvalidStaticApiKey(fallbackErr);
1124
+ throw fallbackErr;
1125
+ }
1126
+ }
1127
+ else if (err instanceof ProviderError && err.statusCode === 401) {
900
1128
  // Static API-key providers (GLM, Moonshot API key, etc.) have no refresh
901
1129
  // mechanism — retrying with the same key is pointless. Clear the
902
1130
  // credential and surface the error so the user re-logins. Kimi OAuth
903
1131
  // (active for `moonshot` when present) is refreshable, so it falls
904
1132
  // through to the force-refresh path below.
905
- if (await this.authStorage.isStaticApiKey(this.provider)) {
906
- // Clear whichever key actually resolved (the request may have used
907
- // a fallback key, not the model's first preference).
908
- const badKey = (await this.authStorage.pickStorageKey(this.currentAuthStorageKeys())) ??
909
- this.currentAuthStorageKeys()[0];
910
- log("WARN", "auth", `Got 401 for ${this.provider} (${badKey}) — API key is invalid or revoked`);
911
- await this.authStorage.clearCredentials(badKey);
1133
+ if (await clearInvalidStaticApiKey(err))
912
1134
  throw err;
913
- }
914
1135
  log("INFO", "auth", "Got 401, force-refreshing token and retrying");
915
1136
  creds = await this.authStorage.resolveCredentials(this.provider, {
916
1137
  forceRefresh: true,
@@ -935,6 +1156,7 @@ export class AgentSession {
935
1156
  if (provider)
936
1157
  this.provider = provider;
937
1158
  this.model = model;
1159
+ this.providerContext = null;
938
1160
  // Keep host-provided option closures (notably chat delegation) aligned with
939
1161
  // the live selection after an in-session model switch.
940
1162
  this.opts.provider = this.provider;
@@ -1012,6 +1234,7 @@ export class AgentSession {
1012
1234
  }
1013
1235
  }
1014
1236
  async compact(existingCredentials) {
1237
+ this.lastCompactionCompacted = false;
1015
1238
  const creds = existingCredentials ??
1016
1239
  (await this.authStorage.resolveCredentials(this.provider, {
1017
1240
  storageKeys: this.currentAuthStorageKeys(),
@@ -1032,6 +1255,16 @@ export class AgentSession {
1032
1255
  signal: this.opts.signal,
1033
1256
  });
1034
1257
  this.messages = result.messages;
1258
+ this.lastCompactionCompacted = result.result.compacted;
1259
+ if (!result.result.compacted) {
1260
+ this.eventBus.emit("compaction_end", {
1261
+ compacted: false,
1262
+ originalCount: result.result.originalCount,
1263
+ newCount: result.result.newCount,
1264
+ });
1265
+ return;
1266
+ }
1267
+ this.providerContext = null;
1035
1268
  // Transient sessions (Nolan chat/autopilot, subagent spawns) must NEVER touch
1036
1269
  // the session store: without this guard, the first auto-compaction called
1037
1270
  // sessionManager.create() and assigned a real sessionPath, silently turning
@@ -1044,9 +1277,13 @@ export class AgentSession {
1044
1277
  else {
1045
1278
  // Persist compacted messages to a new session file so `ezcoder continue`
1046
1279
  // picks up the compacted state instead of the full original history.
1047
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
1280
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1281
+ conversationId: this.conversationId || undefined,
1282
+ preview: this.sessionPreview || undefined,
1283
+ });
1048
1284
  this.sessionId = session.id;
1049
- this.sessionPath = session.path;
1285
+ this.conversationId = session.header.conversationId ?? session.id;
1286
+ this.setSessionPath(session.path);
1050
1287
  await this.subAgentManager?.rebindParentSession(this.sessionId);
1051
1288
  // Write compacted messages (skip system — it's rebuilt on load)
1052
1289
  for (const msg of this.messages) {
@@ -1068,11 +1305,18 @@ export class AgentSession {
1068
1305
  });
1069
1306
  }
1070
1307
  this.eventBus.emit("compaction_end", {
1308
+ compacted: true,
1071
1309
  originalCount: result.result.originalCount,
1072
1310
  newCount: result.result.newCount,
1073
1311
  });
1074
1312
  }
1075
- async newSession() {
1313
+ async newSession(preserveConversation = false) {
1314
+ // Approved-plan execution is a clean checkpoint of the same conversation;
1315
+ // explicit new sessions reset the conversation identity.
1316
+ if (!preserveConversation) {
1317
+ this.conversationId = "";
1318
+ this.sessionPreview = "";
1319
+ }
1076
1320
  // A fresh session drops any in-flight plan state so its prompt is clean.
1077
1321
  this.planModeRef.current = false;
1078
1322
  this.approvedPlanPath = undefined;
@@ -1098,7 +1342,9 @@ export class AgentSession {
1098
1342
  // polluted the project's session list.
1099
1343
  if (this.opts.transient) {
1100
1344
  this.sessionId = "";
1101
- this.sessionPath = "";
1345
+ this.conversationId = "";
1346
+ this.sessionPreview = "";
1347
+ this.setSessionPath("");
1102
1348
  this.lastPersistedIndex = this.messages.length;
1103
1349
  }
1104
1350
  else {
@@ -1123,6 +1369,7 @@ export class AgentSession {
1123
1369
  async branch(stepsBack = 2) {
1124
1370
  // Load the full session to access the DAG
1125
1371
  const loaded = await this.sessionManager.load(this.sessionPath);
1372
+ this.setSessionPath(loaded.path);
1126
1373
  const branch = this.sessionManager.getBranch(loaded.entries, this.currentLeafId);
1127
1374
  // Walk back stepsBack message entries
1128
1375
  const messageEntries = branch.filter((e) => e.type === "message");
@@ -1153,6 +1400,7 @@ export class AgentSession {
1153
1400
  */
1154
1401
  async listBranches() {
1155
1402
  const loaded = await this.sessionManager.load(this.sessionPath);
1403
+ this.setSessionPath(loaded.path);
1156
1404
  return this.sessionManager.listBranches(loaded.entries);
1157
1405
  }
1158
1406
  getState() {
@@ -1170,6 +1418,18 @@ export class AgentSession {
1170
1418
  getPlanMode() {
1171
1419
  return this.planModeRef.current;
1172
1420
  }
1421
+ /**
1422
+ * Suppress only the pre-final Ideal self-review for this live session.
1423
+ * Autopilot uses this while Nolan independently owns verification; loop-break
1424
+ * and post-compaction re-grounding remain active.
1425
+ */
1426
+ setIdealReviewSuppressed(suppressed) {
1427
+ this.idealReviewSuppressed = suppressed;
1428
+ if (suppressed) {
1429
+ this.idealReviewPhase = "idle";
1430
+ this.reviewCoverage.reset();
1431
+ }
1432
+ }
1173
1433
  /** Queue a user message (optionally with attachments) to be injected mid-run
1174
1434
  * as steering. Returns the new queue length. No-op semantics are the caller's
1175
1435
  * concern. */
@@ -1467,46 +1727,12 @@ export class AgentSession {
1467
1727
  await this.sessionManager.appendEntry(this.sessionPath, entry);
1468
1728
  }
1469
1729
  }
1470
- /**
1471
- * Generate a short LLM session title from the conversation so far (first user
1472
- * message + first assistant reply). Best-effort; returns null on failure or
1473
- * when there's no user message yet. Uses the cheapest model for the provider.
1474
- */
1475
- async generateTitle() {
1476
- const extractText = (content) => typeof content === "string"
1477
- ? content
1478
- : content
1479
- .map((c) => c.type === "text" && "text" in c && typeof c.text === "string" ? c.text : "")
1480
- .join(" ");
1481
- const userMsg = this.messages.find((m) => m.role === "user");
1482
- const assistantMsg = this.messages.find((m) => m.role === "assistant");
1483
- const userText = userMsg ? extractText(userMsg.content) : "";
1484
- if (!userText.trim())
1485
- return null;
1486
- try {
1487
- const creds = await this.authStorage.resolveCredentials(this.provider, {
1488
- storageKeys: this.currentAuthStorageKeys(),
1489
- });
1490
- const title = await generateSessionTitle({
1491
- provider: this.provider,
1492
- userMessage: userText,
1493
- assistantPreview: assistantMsg ? extractText(assistantMsg.content).slice(0, 200) : "",
1494
- apiKey: creds.accessToken,
1495
- baseUrl: this.baseUrl ?? creds.baseUrl,
1496
- accountId: creds.accountId,
1497
- });
1498
- return title || null;
1499
- }
1500
- catch {
1501
- return null;
1502
- }
1503
- }
1504
1730
  /**
1505
1731
  * Rewrite a draft prompt into a tighter, terminology-correct version using
1506
1732
  * the ACTIVE provider/model. A stateless one-off LLM call (no agent loop, no
1507
1733
  * tools, no session mutation) — safe to run even mid-run. Returns the plain
1508
1734
  * enhanced text plus typed segments marking each corrected term. Errors throw
1509
- * so the caller can surface them (unlike best-effort title generation).
1735
+ * so the caller can surface them.
1510
1736
  */
1511
1737
  async enhancePrompt(text) {
1512
1738
  if (!text.trim())
@@ -1598,21 +1824,44 @@ export class AgentSession {
1598
1824
  this.lspManager?.shutdownAll();
1599
1825
  await Promise.all([this.subAgentManager?.shutdownAll(), this.mcpManager?.dispose()]);
1600
1826
  await this.extensionLoader.deactivateAll();
1827
+ this.setSessionPath("");
1601
1828
  this.eventBus.removeAllListeners();
1602
1829
  this.messages = [];
1603
1830
  this.tools = [];
1604
1831
  }
1605
1832
  // ── Private ────────────────────────────────────────────
1833
+ setSessionPath(nextPath) {
1834
+ if (this.sessionPath === nextPath)
1835
+ return;
1836
+ if (this.sessionPath)
1837
+ this.sessionManager.unregisterActivePath(this.sessionPath);
1838
+ this.sessionPath = nextPath;
1839
+ if (nextPath)
1840
+ this.sessionManager.registerActivePath(nextPath);
1841
+ }
1606
1842
  async createNewSession() {
1607
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
1843
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1844
+ conversationId: this.conversationId || undefined,
1845
+ preview: this.sessionPreview || undefined,
1846
+ });
1608
1847
  this.sessionId = session.id;
1609
- this.sessionPath = session.path;
1848
+ this.conversationId = session.header.conversationId ?? session.id;
1849
+ this.setSessionPath(session.path);
1610
1850
  this.lastPersistedIndex = this.messages.length;
1611
1851
  }
1612
1852
  async loadExistingSession(sessionPath) {
1613
1853
  const loaded = await this.sessionManager.load(sessionPath);
1614
1854
  // Use the leaf from the header to walk the correct branch
1615
1855
  const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
1856
+ this.conversationId = loaded.header.conversationId ?? loaded.header.id;
1857
+ const legacyLabel = [...loaded.entries]
1858
+ .reverse()
1859
+ .find((entry) => entry.type === "label")
1860
+ ?.label.replace(/\s+/g, " ")
1861
+ .trim()
1862
+ .slice(0, 80);
1863
+ this.sessionPreview =
1864
+ legacyLabel || loaded.header.preview || findUserSessionPrompt(loadedMessages);
1616
1865
  // Restore Nolan's advisory turns (custom entries, not on the message branch) so
1617
1866
  // they reappear in the transcript and survive into the continuation file.
1618
1867
  this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries);
@@ -1623,9 +1872,15 @@ export class AgentSession {
1623
1872
  this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
1624
1873
  // Track the current leaf for subsequent entries
1625
1874
  this.currentLeafId = loaded.header.leafId;
1626
- // Rebuild messages: keep system, add loaded
1875
+ // Rebuild messages: keep system, add loaded. Older ezcoder-app sessions may
1876
+ // contain full-resolution attachments; repair them once on load so they do
1877
+ // not fail when Anthropic's stricter many-image limit activates later.
1627
1878
  const systemMsg = this.messages[0]; // Already built
1628
1879
  this.messages = [systemMsg, ...loadedMessages];
1880
+ const normalizedImageCount = await normalizeMessageImages(this.messages);
1881
+ if (normalizedImageCount > 0) {
1882
+ log("INFO", "session", `Resized ${normalizedImageCount} restored session image(s)`);
1883
+ }
1629
1884
  // Auto-compact on load if the restored session exceeds the context window.
1630
1885
  // Without this, huge sessions (1M+ tokens) get loaded into memory and OOM.
1631
1886
  const creds = await this.authStorage.resolveCredentials(this.provider, {
@@ -1639,7 +1894,15 @@ export class AgentSession {
1639
1894
  provider: this.provider,
1640
1895
  accountId: creds.accountId,
1641
1896
  });
1642
- if (shouldCompact(this.messages, contextWindow, 0.8, undefined, getCompactionReserveTokens(this.maxTokens))) {
1897
+ const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
1898
+ shouldCompact(this.messages, contextWindow, this.settingsManager.get("compactThreshold"));
1899
+ if (needsLoadCompaction && this.opts.deferLoadCompaction) {
1900
+ // Host readiness is gated on initialize() — don't block it on a summary
1901
+ // LLM call (up to 30s). runLoop()'s pre-run auto-compaction picks this
1902
+ // up on the first prompt and emits compaction_start/_end for the UI.
1903
+ log("INFO", "session", "Restored session exceeds context — deferring compaction to first prompt");
1904
+ }
1905
+ else if (needsLoadCompaction) {
1643
1906
  await this.subAgentManager?.hydrate(loaded.header.id);
1644
1907
  log("INFO", "session", `Restored session exceeds context — auto-compacting`);
1645
1908
  const compacted = await compact(this.messages, {
@@ -1661,9 +1924,13 @@ export class AgentSession {
1661
1924
  // what's in memory — fork a fresh session file for the compacted state
1662
1925
  // (mirrors compact()'s own persistence) so `ezcoder continue` picks up
1663
1926
  // the summary instead of the full original transcript.
1664
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
1927
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1928
+ conversationId: this.conversationId || undefined,
1929
+ preview: this.sessionPreview || undefined,
1930
+ });
1665
1931
  this.sessionId = session.id;
1666
- this.sessionPath = session.path;
1932
+ this.conversationId = session.header.conversationId ?? session.id;
1933
+ this.setSessionPath(session.path);
1667
1934
  await this.subAgentManager?.rebindParentSession(this.sessionId);
1668
1935
  this.currentLeafId = null;
1669
1936
  // Re-persist (compacted) messages — skip system, it's rebuilt on load
@@ -1691,13 +1958,16 @@ export class AgentSession {
1691
1958
  // session was merely reopened (e.g. app/window restart) with zero new
1692
1959
  // messages in between — the duplicate entries seen in the session list.
1693
1960
  this.sessionId = loaded.header.id;
1694
- this.sessionPath = sessionPath;
1961
+ this.setSessionPath(loaded.path);
1695
1962
  this.lastPersistedIndex = this.messages.length;
1696
1963
  }
1697
1964
  async prepareDynamicContext(_latestUserPrompt) {
1698
1965
  return this.messages;
1699
1966
  }
1700
1967
  async persistMessage(message) {
1968
+ if (!this.sessionPreview && message.role === "user") {
1969
+ this.sessionPreview = getUserSessionPrompt(message.content) ?? "";
1970
+ }
1701
1971
  // Transient sessions (subagent spawns) have no session file — skip.
1702
1972
  if (!this.sessionPath)
1703
1973
  return;