@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
@@ -23,7 +23,7 @@ import { runJsonMode } from "./modes/json-mode.js";
23
23
  import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
24
24
  import { AgentSession } from "./core/agent-session.js";
25
25
  import { RunLifecycle } from "./core/run-lifecycle.js";
26
- import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, sessionsDirForChatAgent, switchChatAgent, } from "./chat-agents/index.js";
26
+ import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
27
27
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
28
28
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
29
29
  import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
@@ -34,6 +34,8 @@ import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-c
34
34
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
35
35
  import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
36
36
  import { AuthStorage } from "./core/auth-storage.js";
37
+ import { cleanupToolOutputs } from "./tools/overflow.js";
38
+ import { readCappedBody } from "./utils/http-body.js";
37
39
  import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
38
40
  import { loginAnthropic } from "./core/oauth/anthropic.js";
39
41
  import { loginOpenAI } from "./core/oauth/openai.js";
@@ -42,18 +44,20 @@ import { loginKimi } from "./core/oauth/kimi.js";
42
44
  import { AUTH_PROVIDERS } from "./core/auth-providers.js";
43
45
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
44
46
  import { SettingsManager } from "./core/settings-manager.js";
45
- import { getModel, getMaxThinkingLevel, getContextWindow, MODELS } from "./core/model-registry.js";
47
+ import { getModel, getDefaultThinkingLevel, getContextWindow, MODELS, } from "./core/model-registry.js";
46
48
  import { resolveStartOrFallback } from "./core/resolve-start.js";
47
- import { getGitBranch, isGitRepo } from "./utils/git.js";
49
+ import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
50
+ import { extractPlanSteps } from "./utils/plan-steps.js";
48
51
  import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
49
52
  import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
50
53
  import { loadCustomCommands } from "./core/custom-commands.js";
51
- import { discoverProjects, listRecentSessions } from "./core/project-discovery.js";
54
+ import { discoverProjects } from "./core/project-discovery.js";
55
+ import { listSidecarSessions } from "./app-sidecar-sessions.js";
52
56
  import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
53
57
  import { initLogger, log } from "./core/logger.js";
54
58
  import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
55
59
  import { enrichProcessPath } from "./core/shell-path.js";
56
- import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
60
+ import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
57
61
  import { startServeMode } from "./modes/serve-mode.js";
58
62
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
59
63
  import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
@@ -62,17 +66,22 @@ import { loadProgress, peekProgress, updateProgress } from "./core/progress/stor
62
66
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
63
67
  import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
64
68
  import { rebuildFromSessions } from "./core/progress/rebuild.js";
69
+ import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
65
70
  const ALL_PROVIDERS = [
71
+ // US
66
72
  "anthropic",
67
- "xiaomi",
68
73
  "openai",
69
74
  "gemini",
70
- "glm",
75
+ "xai",
76
+ // China
71
77
  "moonshot",
78
+ "glm",
72
79
  "minimax",
80
+ "xiaomi",
73
81
  "deepseek",
74
- "openrouter",
82
+ // Japan, then provider-agnostic gateway last
75
83
  "sakana",
84
+ "openrouter",
76
85
  ];
77
86
  function appSettingsFile() {
78
87
  return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
@@ -163,6 +172,7 @@ async function persistModelSelection(settingsFile, provider, model) {
163
172
  await sm.set("defaultModel", model);
164
173
  }
165
174
  catch (err) {
175
+ captureSidecarError(err, "app-sidecar.settings.persist-model");
166
176
  log("WARN", "app-sidecar", "failed to persist model selection", { err: String(err) });
167
177
  }
168
178
  }
@@ -179,6 +189,7 @@ async function persistThinkingLevel(settingsFile, level) {
179
189
  await sm.set("thinkingLevel", level);
180
190
  }
181
191
  catch (err) {
192
+ captureSidecarError(err, "app-sidecar.settings.persist-thinking");
182
193
  log("WARN", "app-sidecar", "failed to persist thinking level", { err: String(err) });
183
194
  }
184
195
  }
@@ -196,19 +207,28 @@ async function prepareAttachments(cwd, attachments) {
196
207
  const fileName = `${Date.now().toString(36)}-${safe}`;
197
208
  const filePath = path.join(dir, fileName);
198
209
  const buf = Buffer.from(a.data, "base64");
199
- // Validate image attachments before they become native image content blocks.
200
- // A corrupt or unsupported-format image (e.g. a malformed .ico, or a .png
201
- // with a bad IDAT) makes the provider reject the ENTIRE turn ("image data
202
- // ... not a valid image") before the agent can respond. Downgrade such files
203
- // to a plain "file" attachment so the model gets a path note and inspects
204
- // them with its tools instead of the request 400ing.
210
+ // Validate and cap image attachments before they become native image content
211
+ // blocks. Anthropic applies a 2000 px per-dimension cap once conversation
212
+ // history contains more than 20 images, so every attachment must be safe for
213
+ // later turns too. Keep the original on disk, but send the resized bytes.
214
+ // Corrupt or unsupported images become plain files so they cannot reject the
215
+ // entire provider request.
205
216
  let prepared = { ...a };
206
217
  if (a.kind === "image") {
207
- const validatedType = await validateVisionImage(buf).catch(() => null);
208
- if (validatedType)
209
- prepared = { ...a, mediaType: validatedType };
210
- else
218
+ try {
219
+ const resized = await shrinkToFit(buf, a.mediaType);
220
+ const validatedType = await validateVisionImage(resized.buffer);
221
+ prepared = validatedType
222
+ ? {
223
+ ...a,
224
+ mediaType: validatedType,
225
+ data: resized.buffer.toString("base64"),
226
+ }
227
+ : { ...a, kind: "file" };
228
+ }
229
+ catch {
211
230
  prepared = { ...a, kind: "file" };
231
+ }
212
232
  }
213
233
  try {
214
234
  await fs.writeFile(filePath, buf);
@@ -221,6 +241,9 @@ async function prepareAttachments(cwd, attachments) {
221
241
  return out;
222
242
  }
223
243
  const FILE_SEARCH_LIMIT = 20;
244
+ // Upper bound on files walked per search (baseline #8 memory cap). Far above the
245
+ // 20-result output limit, so relevance/recency ranking is unaffected in practice.
246
+ const FILE_SEARCH_SCAN_CAP = 50_000;
224
247
  /** Score a candidate path against a lowercased query. Higher is better; a
225
248
  * negative score means "no match". Basename hits beat path hits; prefix beats
226
249
  * substring; shorter paths break ties. */
@@ -273,8 +296,13 @@ async function searchProjectFiles(cwd, rawQuery) {
273
296
  }
274
297
  const ig = ignore.default().add(gitignore);
275
298
  // `stats: true` gives mtime without a second stat pass, so the empty-query
276
- // "recent files" path is a single walk.
277
- const entries = await fg.default("**/*", {
299
+ // "recent files" path is a single walk. Stream + hard scan cap (baseline #8):
300
+ // collecting the full result array retained ~0.9 MB per 1k files (18.5 MB at
301
+ // 20k), unbounded. Bail after FILE_SEARCH_SCAN_CAP entries so a giant repo
302
+ // can't balloon RSS; the newest/most-relevant matches still surface because
303
+ // the cap is far above the 20-result output limit.
304
+ const entries = [];
305
+ const scanStream = fg.default.stream("**/*", {
278
306
  cwd,
279
307
  dot: false,
280
308
  onlyFiles: true,
@@ -283,6 +311,17 @@ async function searchProjectFiles(cwd, rawQuery) {
283
311
  followSymbolicLinks: false,
284
312
  stats: true,
285
313
  });
314
+ for await (const entry of scanStream) {
315
+ entries.push(entry);
316
+ if (entries.length >= FILE_SEARCH_SCAN_CAP) {
317
+ scanStream.destroy();
318
+ log("DEBUG", "app-sidecar", "file search scan cap hit", {
319
+ cap: String(FILE_SEARCH_SCAN_CAP),
320
+ cwd,
321
+ });
322
+ break;
323
+ }
324
+ }
286
325
  const files = entries.filter((e) => !ig.ignores(e.path));
287
326
  if (!query) {
288
327
  return files
@@ -437,7 +476,11 @@ async function runJsonModeIfRequested() {
437
476
  maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
438
477
  allowedTools,
439
478
  promptCacheKey: values["prompt-cache-key"],
440
- }).catch((err) => {
479
+ }).catch(async (err) => {
480
+ captureSidecarError(err, "app-sidecar.json-mode", {
481
+ provider: String(values.provider ?? "anthropic"),
482
+ });
483
+ await flushSidecarErrors();
441
484
  process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
442
485
  process.exit(1);
443
486
  });
@@ -446,13 +489,8 @@ async function runJsonModeIfRequested() {
446
489
  // ── Daemon-level HTTP helpers (shared by the session-management routes) ─────
447
490
  // The per-session route table has its own local copies; these serve the
448
491
  // daemon's own POST /session / DELETE /session routes.
449
- function daemonReadBody(req) {
450
- return new Promise((resolve, reject) => {
451
- const chunks = [];
452
- req.on("data", (c) => chunks.push(c));
453
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
454
- req.on("error", reject);
455
- });
492
+ function daemonReadBody(req, res) {
493
+ return readCappedBody(req, res);
456
494
  }
457
495
  function daemonJson(res, status, body) {
458
496
  res.writeHead(status, {
@@ -505,6 +543,9 @@ async function main() {
505
543
  // before anything spawns (bash tool, background tasks, LSP, git helpers all
506
544
  // inherit it). Best-effort — never blocks startup beyond its internal cap.
507
545
  await enrichProcessPath();
546
+ // Sweep recoverable full tool outputs (~/.ezcoder/tool-output/) older than 48h.
547
+ // Fire-and-forget: cleanup must never delay or break startup.
548
+ void cleanupToolOutputs().catch(() => { });
508
549
  const auth = new AuthStorage(paths.authFile);
509
550
  await auth.load();
510
551
  // Every window's session lives here as an in-process object, keyed by the id
@@ -535,30 +576,63 @@ async function main() {
535
576
  });
536
577
  const usageCache = new Map();
537
578
  const usageRequests = new Map();
579
+ // 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
580
+ // provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
581
+ // the provider value so a malformed header can neither hammer the endpoint
582
+ // nor suppress usage data forever.
583
+ const usageRateLimitedUntil = new Map();
584
+ const USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS = 30 * 60_000;
585
+ const USAGE_RATE_LIMIT_MIN_BACKOFF_MS = 60_000;
586
+ const USAGE_RATE_LIMIT_MAX_BACKOFF_MS = 24 * 60 * 60_000;
538
587
  async function fetchUsageProvider(provider) {
539
- const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
540
- if (!(await auth.hasProviderAuth(provider))) {
588
+ const displayName = provider === "anthropic" ? "Anthropic" : provider === "openai" ? "Codex" : "Kimi";
589
+ // Kimi plan usage is tracked on the OAuth credential specifically — the
590
+ // Moonshot platform API key is metered per-token, not per plan window.
591
+ const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
592
+ if (!(await auth.hasProviderAuth(authKey))) {
541
593
  return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
542
594
  }
543
595
  try {
544
- let credentials = await auth.resolveCredentials(provider);
596
+ let credentials = await auth.resolveCredentials(authKey);
545
597
  try {
546
- return { ...(await fetchSubscriptionUsage(provider, credentials)), connected: true };
598
+ const snapshot = {
599
+ ...(await fetchSubscriptionUsage(provider, credentials)),
600
+ connected: true,
601
+ };
602
+ usageRateLimitedUntil.delete(provider);
603
+ return snapshot;
547
604
  }
548
605
  catch (error) {
549
606
  // A provider can revoke an access token before its stored expiry. Refresh
550
607
  // once on 401, matching inference auth recovery, then retry the usage call.
551
608
  if (error instanceof SubscriptionUsageError && error.status === 401) {
552
- credentials = await auth.resolveCredentials(provider, { forceRefresh: true });
553
- return { ...(await fetchSubscriptionUsage(provider, credentials)), connected: true };
609
+ credentials = await auth.resolveCredentials(authKey, { forceRefresh: true });
610
+ const snapshot = {
611
+ ...(await fetchSubscriptionUsage(provider, credentials)),
612
+ connected: true,
613
+ };
614
+ usageRateLimitedUntil.delete(provider);
615
+ return snapshot;
554
616
  }
555
617
  throw error;
556
618
  }
557
619
  }
558
620
  catch (error) {
559
621
  const message = error instanceof Error ? error.message : String(error);
560
- log("WARN", "app-sidecar", "subscription usage fetch failed", { provider, message });
561
- const connected = await auth.hasProviderAuth(provider);
622
+ if (shouldCaptureUsagePollingError(error)) {
623
+ captureSidecarError(error, "app-sidecar.usage.fetch", { provider });
624
+ }
625
+ let backoffMs;
626
+ if (error instanceof SubscriptionUsageError && error.status === 429) {
627
+ backoffMs = Math.min(USAGE_RATE_LIMIT_MAX_BACKOFF_MS, Math.max(USAGE_RATE_LIMIT_MIN_BACKOFF_MS, error.retryAfterMs ?? USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS));
628
+ usageRateLimitedUntil.set(provider, Date.now() + backoffMs);
629
+ }
630
+ log("WARN", "app-sidecar", "subscription usage fetch failed", {
631
+ provider,
632
+ message,
633
+ ...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
634
+ });
635
+ const connected = await auth.hasProviderAuth(authKey);
562
636
  return {
563
637
  provider,
564
638
  displayName,
@@ -586,9 +660,12 @@ async function main() {
586
660
  const missingReset = result.connected &&
587
661
  result.windows.length > 0 &&
588
662
  result.windows.some((window) => window.resetsAt === undefined);
663
+ const rateLimitedUntil = usageRateLimitedUntil.get(provider) ?? 0;
589
664
  usageCache.set(provider, {
590
665
  result,
591
- expiresAt: Date.now() + (missingReset ? 10_000 : 60_000),
666
+ expiresAt: rateLimitedUntil > Date.now()
667
+ ? rateLimitedUntil
668
+ : Date.now() + (missingReset ? 10_000 : 60_000),
592
669
  });
593
670
  return result;
594
671
  }
@@ -610,7 +687,7 @@ async function main() {
610
687
  return null;
611
688
  }
612
689
  }
613
- const server = http.createServer((req, res) => {
690
+ const server = http.createServer(wrapSidecarHandler((req, res) => {
614
691
  const url = req.url ?? "/";
615
692
  const method = req.method ?? "GET";
616
693
  // CORS preflight — the webview origin differs from 127.0.0.1.
@@ -626,7 +703,9 @@ async function main() {
626
703
  // ── Daemon-level routes (session lifecycle) ──────────────────────────
627
704
  // Create a session for a window: { mode?, cwd, sessionPath? } → { sessionId }.
628
705
  if (method === "POST" && url === "/session") {
629
- void daemonReadBody(req).then(async (raw) => {
706
+ void daemonReadBody(req, res).then(async (raw) => {
707
+ if (raw === null)
708
+ return;
630
709
  let body = {};
631
710
  try {
632
711
  body = raw ? JSON.parse(raw) : {};
@@ -654,6 +733,7 @@ async function main() {
654
733
  }
655
734
  catch (err) {
656
735
  const message = err instanceof Error ? err.message : String(err);
736
+ captureSidecarError(err, "app-sidecar.session.create");
657
737
  log("ERROR", "app-sidecar", "session create failed", { message });
658
738
  daemonJson(res, 500, { error: message });
659
739
  }
@@ -684,12 +764,13 @@ async function main() {
684
764
  if (method === "GET" && (url === "/usage" || url.startsWith("/usage?"))) {
685
765
  void (async () => {
686
766
  const provider = new URL(url, `http://${host}`).searchParams.get("provider");
687
- if (provider !== "anthropic" && provider !== "openai") {
767
+ if (provider !== "anthropic" && provider !== "openai" && provider !== "moonshot") {
688
768
  daemonJson(res, 400, { error: "unsupported usage provider" });
689
769
  return;
690
770
  }
691
771
  daemonJson(res, 200, await subscriptionUsage(provider));
692
772
  })().catch((error) => {
773
+ captureSidecarError(error, "app-sidecar.usage.request");
693
774
  log("ERROR", "app-sidecar", "subscription usage request failed", {
694
775
  message: error instanceof Error ? error.message : String(error),
695
776
  });
@@ -705,7 +786,7 @@ async function main() {
705
786
  return;
706
787
  }
707
788
  ctx.handle(req, res, url, method);
708
- });
789
+ }, "app-sidecar.http"));
709
790
  server.listen(port, host, () => {
710
791
  const addr = server.address();
711
792
  // The Rust shell reads this line to learn the daemon port.
@@ -722,6 +803,8 @@ async function main() {
722
803
  // Radio playback is app-wide (one stream across all windows), so it stops
723
804
  // at the daemon level, not per session.
724
805
  stopRadio();
806
+ // Close the ~/.ezcoder progress fs.watch handle (baseline #8 leak fix).
807
+ progress.dispose();
725
808
  await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
726
809
  server.close();
727
810
  process.exit(0);
@@ -850,6 +933,7 @@ async function createProgressManager(agentDir, broadcastAll) {
850
933
  broadcastAll(buildSnapshot(updated), originId);
851
934
  }
852
935
  catch (err) {
936
+ captureSidecarError(err, "app-sidecar.progress.award");
853
937
  log("DEBUG", "app-sidecar", "progress award failed", {
854
938
  message: err instanceof Error ? err.message : String(err),
855
939
  });
@@ -858,6 +942,7 @@ async function createProgressManager(agentDir, broadcastAll) {
858
942
  // Watch ~/.ezcoder (dir watch survives the atomic tmp+rename) for progress.json
859
943
  // writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
860
944
  let watchDebounce = null;
945
+ let progressWatcher = null;
861
946
  try {
862
947
  const watcher = fsWatch(agentDir, (_event, filename) => {
863
948
  if (filename !== "progress.json")
@@ -879,13 +964,30 @@ async function createProgressManager(agentDir, broadcastAll) {
879
964
  }, 150);
880
965
  });
881
966
  watcher.unref();
967
+ progressWatcher = watcher;
882
968
  }
883
969
  catch (err) {
970
+ captureSidecarError(err, "app-sidecar.progress.watch");
884
971
  log("DEBUG", "app-sidecar", "progress watch unavailable", {
885
972
  message: err instanceof Error ? err.message : String(err),
886
973
  });
887
974
  }
888
- return { snapshot, awardRun };
975
+ // Dispose closes the fs.watch handle (baseline #8: it was previously never
976
+ // closed — a per-daemon leak) and clears any pending debounce timer.
977
+ function dispose() {
978
+ if (watchDebounce) {
979
+ clearTimeout(watchDebounce);
980
+ watchDebounce = null;
981
+ }
982
+ try {
983
+ progressWatcher?.close();
984
+ }
985
+ catch {
986
+ // Already closed / never opened — nothing to do.
987
+ }
988
+ progressWatcher = null;
989
+ }
990
+ return { snapshot, awardRun, dispose };
889
991
  }
890
992
  /**
891
993
  * Build one in-process agent session: its AgentSession, SSE client set, event
@@ -904,6 +1006,10 @@ async function createSession(deps, opts) {
904
1006
  // parsing); the daemon owns the real listen host.
905
1007
  const host = "127.0.0.1";
906
1008
  const saved = loadSavedSettings(paths.settingsFile);
1009
+ // Native login/logout and other live sessions share auth.json. Refresh the
1010
+ // daemon-level snapshot before choosing this session's provider so a project
1011
+ // never boots against credentials that were just replaced or disconnected.
1012
+ await auth.load();
907
1013
  // Per-project model/thinking prefs win over the shared global settings.json:
908
1014
  // each window (one project cwd) restores its own selection instead of every
909
1015
  // window reading the same single global slot that the last writer clobbered
@@ -922,9 +1028,14 @@ async function createSession(deps, opts) {
922
1028
  });
923
1029
  }
924
1030
  // Per-project thinking prefs win over the global settings.json fallback.
1031
+ // With no saved level, the default follows the active credential's endpoint:
1032
+ // Kimi K3 on the OAuth coding endpoint starts at its declared default (high),
1033
+ // matching the official kimi-code CLI's plan-usage profile.
925
1034
  const thinkEnabled = projectPrefs?.thinkingEnabled ?? saved.thinkingEnabled;
926
1035
  const thinkingLevel = thinkEnabled
927
- ? (projectPrefs?.thinkingLevel ?? saved.thinkingLevel ?? getMaxThinkingLevel(model))
1036
+ ? (projectPrefs?.thinkingLevel ??
1037
+ saved.thinkingLevel ??
1038
+ getDefaultThinkingLevel(model, { baseUrl: auth.getStoredBaseUrl(provider) }))
928
1039
  : undefined;
929
1040
  // ── SSE fan-out (declared before the session so plan callbacks can use it) ─
930
1041
  const clients = new Set();
@@ -971,6 +1082,11 @@ async function createSession(deps, opts) {
971
1082
  const f = formatError(err);
972
1083
  const message = f.message ? desktopGuidance(f.message) : undefined;
973
1084
  const guidance = desktopGuidance(f.guidance);
1085
+ captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
1086
+ scope: type,
1087
+ ...(f.provider ? { provider: f.provider } : {}),
1088
+ ...(f.statusCode != null ? { status: String(f.statusCode) } : {}),
1089
+ });
974
1090
  log("ERROR", "app-sidecar", logLabel, {
975
1091
  headline: f.headline,
976
1092
  source: f.source,
@@ -1011,6 +1127,11 @@ async function createSession(deps, opts) {
1011
1127
  signal: abort.signal,
1012
1128
  // Keep MCP startup off the readiness path in both modes.
1013
1129
  backgroundMcpConnect: true,
1130
+ // Keep restore-time auto-compaction off the readiness path too: its summary
1131
+ // LLM call (30s timeout) used to freeze waitForReady — and with it the whole
1132
+ // window (project picker, session list) — whenever a resumed session was
1133
+ // over the context threshold. First prompt compacts instead, with UI events.
1134
+ deferLoadCompaction: true,
1014
1135
  };
1015
1136
  let session;
1016
1137
  if (mode === "chat") {
@@ -1023,6 +1144,7 @@ async function createSession(deps, opts) {
1023
1144
  chatAgent = nextAgent;
1024
1145
  broadcast("chat_agent_change", { chatAgent: nextAgent });
1025
1146
  await session.persistAppMarker("agent_handoff", { chatAgent: nextAgent }).catch((error) => {
1147
+ captureSidecarError(error, "app-sidecar.chat-agent.persist-handoff");
1026
1148
  log("WARN", "app-sidecar", "agent handoff marker persist failed", {
1027
1149
  message: error instanceof Error ? error.message : String(error),
1028
1150
  });
@@ -1038,7 +1160,9 @@ async function createSession(deps, opts) {
1038
1160
  // Unattended task runs cannot approve plans; implement directly instead.
1039
1161
  if (taskRunActive)
1040
1162
  return false;
1163
+ deactivateApprovedPlan();
1041
1164
  await session.setPlanMode(true);
1165
+ broadcast("plan_progress", { total: 0, completed: [] });
1042
1166
  broadcast("plan_enter", { reason: reason ?? "" });
1043
1167
  void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
1044
1168
  },
@@ -1073,11 +1197,17 @@ async function createSession(deps, opts) {
1073
1197
  }
1074
1198
  }
1075
1199
  log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
1076
- // Footer extras (context window, git branch, background tasks). The git
1077
- // branch is resolved once at startup and refreshed lazily; the context
1200
+ // Workspace extras (context window, git status, background tasks). Git state
1201
+ // is resolved once at startup and refreshed after every run; the context
1078
1202
  // window follows the active model.
1079
- let gitBranch = await getGitBranch(cwd).catch(() => null);
1080
- let gitIsRepo = await isGitRepo(cwd).catch(() => false);
1203
+ const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
1204
+ getGitBranch(cwd).catch(() => null),
1205
+ isGitRepo(cwd).catch(() => false),
1206
+ getGitDirtyFileCount(cwd).catch(() => 0),
1207
+ ]);
1208
+ let gitBranch = initialGitBranch;
1209
+ let gitIsRepo = initialGitIsRepo;
1210
+ let gitDirtyFileCount = initialDirtyFileCount;
1081
1211
  function currentContextWindow() {
1082
1212
  const st = session.getState();
1083
1213
  return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
@@ -1089,6 +1219,7 @@ async function createSession(deps, opts) {
1089
1219
  contextWindow: currentContextWindow(),
1090
1220
  gitBranch,
1091
1221
  isGitRepo: gitIsRepo,
1222
+ gitDirtyFileCount,
1092
1223
  tasks: session.listBackgroundProcesses(),
1093
1224
  };
1094
1225
  }
@@ -1098,8 +1229,94 @@ async function createSession(deps, opts) {
1098
1229
  // failures (e.g. repeated invalid-argument errors) instead of leaving the
1099
1230
  // fatal-abort path with no forensic trail.
1100
1231
  const toolCallNames = new Map();
1232
+ // Approved-plan progress belongs beside the plan file, not in the webview.
1233
+ // The implementation can rewrite/expand that file mid-run, so a step count
1234
+ // frozen at approval time becomes dishonest (the exact stale-total bug the
1235
+ // CLI already fixed). Re-read the live file after tools/markers, retain the
1236
+ // last valid step section during transient edits, and send one authoritative
1237
+ // snapshot to the app.
1238
+ let approvedPlanPath = null;
1239
+ let approvedPlanTotal = 0;
1240
+ let approvedPlanMarkers = new Set();
1241
+ let approvedPlanGeneration = 0;
1242
+ let planMarkerTail = "";
1243
+ let planProgressSync = Promise.resolve(false);
1244
+ function planProgressPayload() {
1245
+ const completed = [...approvedPlanMarkers]
1246
+ .filter((step) => step >= 1 && step <= approvedPlanTotal)
1247
+ .sort((a, b) => a - b);
1248
+ return { total: approvedPlanTotal, completed };
1249
+ }
1250
+ async function syncApprovedPlanProgress(generation) {
1251
+ const planPath = approvedPlanPath;
1252
+ if (planPath === null || generation !== approvedPlanGeneration)
1253
+ return false;
1254
+ const content = await fs.readFile(planPath, "utf-8").catch(() => null);
1255
+ if (approvedPlanPath !== planPath || generation !== approvedPlanGeneration)
1256
+ return false;
1257
+ if (content !== null) {
1258
+ const freshTotal = extractPlanSteps(content).length;
1259
+ // During an in-place rewrite the step section can briefly disappear.
1260
+ // Keep the last real total instead of flashing 0 or declaring completion.
1261
+ if (freshTotal > 0 || approvedPlanTotal === 0)
1262
+ approvedPlanTotal = freshTotal;
1263
+ }
1264
+ broadcast("plan_progress", planProgressPayload());
1265
+ return (approvedPlanTotal > 0 &&
1266
+ Array.from({ length: approvedPlanTotal }, (_, index) => index + 1).every((step) => approvedPlanMarkers.has(step)));
1267
+ }
1268
+ function queueApprovedPlanProgressSync() {
1269
+ const generation = approvedPlanGeneration;
1270
+ planProgressSync = planProgressSync
1271
+ .catch(() => false)
1272
+ .then(() => syncApprovedPlanProgress(generation))
1273
+ .catch((error) => {
1274
+ captureSidecarError(error, "app-sidecar.plan.refresh-progress");
1275
+ log("WARN", "app-sidecar", "plan progress refresh failed", {
1276
+ message: error instanceof Error ? error.message : String(error),
1277
+ });
1278
+ return false;
1279
+ });
1280
+ return planProgressSync;
1281
+ }
1282
+ async function activateApprovedPlan(planPath) {
1283
+ deactivateApprovedPlan();
1284
+ await session.setApprovedPlan(planPath);
1285
+ if (!planPath)
1286
+ return 0;
1287
+ approvedPlanPath = planPath;
1288
+ await queueApprovedPlanProgressSync();
1289
+ return approvedPlanTotal;
1290
+ }
1291
+ function deactivateApprovedPlan() {
1292
+ approvedPlanGeneration++;
1293
+ approvedPlanPath = null;
1294
+ approvedPlanTotal = 0;
1295
+ approvedPlanMarkers = new Set();
1296
+ planMarkerTail = "";
1297
+ planProgressSync = Promise.resolve(false);
1298
+ }
1299
+ function recordApprovedPlanMarkers(text) {
1300
+ if (approvedPlanPath === null || !text)
1301
+ return;
1302
+ const candidate = planMarkerTail + text;
1303
+ let changed = false;
1304
+ for (const match of candidate.matchAll(/\[DONE:(\d+)\]/gi)) {
1305
+ const step = Number.parseInt(match[1], 10);
1306
+ if (step >= 1 && !approvedPlanMarkers.has(step)) {
1307
+ approvedPlanMarkers.add(step);
1308
+ changed = true;
1309
+ }
1310
+ }
1311
+ planMarkerTail = candidate.slice(-32);
1312
+ if (changed)
1313
+ void queueApprovedPlanProgressSync();
1314
+ }
1101
1315
  // Forward every relevant bus event to the webview.
1102
- session.eventBus.on("text_delta", (d) => broadcast("text_delta", d));
1316
+ session.eventBus.on("text_delta", (d) => {
1317
+ broadcast("text_delta", d);
1318
+ recordApprovedPlanMarkers(d.text);
1319
+ });
1103
1320
  session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
1104
1321
  session.eventBus.on("tool_call_start", (d) => {
1105
1322
  toolCallNames.set(d.toolCallId, d.name);
@@ -1109,6 +1326,11 @@ async function createSession(deps, opts) {
1109
1326
  session.eventBus.on("tool_call_end", (d) => {
1110
1327
  const name = toolCallNames.get(d.toolCallId) ?? "unknown";
1111
1328
  toolCallNames.delete(d.toolCallId);
1329
+ if (d.isError && shouldCaptureToolFailure(name, d.result)) {
1330
+ // Expected model-correctable validation failures stay in the local log and
1331
+ // conversation. Unexpected failures are reported without private result data.
1332
+ captureSidecarError(new Error(`Tool ${name} failed`), `tool.${name}`, { tool: name });
1333
+ }
1112
1334
  log(d.isError ? "ERROR" : "INFO", "tool", `Tool call ended: ${name}`, {
1113
1335
  id: d.toolCallId,
1114
1336
  durationMs: String(d.durationMs),
@@ -1122,6 +1344,11 @@ async function createSession(deps, opts) {
1122
1344
  if (name === "tasks") {
1123
1345
  broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
1124
1346
  }
1347
+ // Any tool can mutate the approved plan (including bash), so refresh after
1348
+ // every completed call while tracking is active. The file is tiny and this
1349
+ // keeps the displayed total aligned before the next completion marker.
1350
+ if (approvedPlanPath !== null)
1351
+ void queueApprovedPlanProgressSync();
1125
1352
  });
1126
1353
  // Native server tools (e.g. Anthropic web_search) do NOT end the turn — text
1127
1354
  // streams before and after them in the SAME turn. The webview must reset its
@@ -1130,6 +1357,9 @@ async function createSession(deps, opts) {
1130
1357
  session.eventBus.on("server_tool_call", (d) => broadcast("server_tool_call", d));
1131
1358
  session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
1132
1359
  session.eventBus.on("agent_done", (d) => broadcast("agent_done", d));
1360
+ // Non-clean stop (max_tokens/refusal/provider error) — info-style frame so
1361
+ // the webview can warn instead of presenting truncated output as complete.
1362
+ session.eventBus.on("truncated", (d) => broadcast("truncated", d));
1133
1363
  session.eventBus.on("error", (d) => {
1134
1364
  broadcastError("error", "agent error", d.error);
1135
1365
  });
@@ -1146,15 +1376,16 @@ async function createSession(deps, opts) {
1146
1376
  });
1147
1377
  const cancelledRunEndGenerations = new Set();
1148
1378
  let pendingCancelDrain = null;
1149
- let titleGenerated = false;
1150
1379
  // Bumped by /cancel — a run whose cancel generation changed mid-flight was
1151
1380
  // canceled and earns no XP.
1152
1381
  let cancelGeneration = 0;
1153
1382
  // Autopilot (auto-review) toggle for THIS window's project. Loaded from
1154
1383
  // ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
1155
1384
  // runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
1156
- // and drives the review→prompt→review loop.
1385
+ // and drives the review→prompt→review loop. Nolan is the sole verification
1386
+ // owner in this mode, so suppress the build session's redundant Ideal hook.
1157
1387
  let autopilot = mode === "code" && (await loadAutopilot(cwd));
1388
+ session.setIdealReviewSuppressed(autopilot);
1158
1389
  // True while an autopilot review is in flight (used to defer nolanAuto model
1159
1390
  // switches, like nolanRunning does for chat Nolan, and to drive the spinner).
1160
1391
  let autopilotReviewing = false;
@@ -1351,6 +1582,9 @@ async function createSession(deps, opts) {
1351
1582
  // (often >5 min) regardless of the user's global speedProfile pick.
1352
1583
  forceLongCacheRetention: true,
1353
1584
  });
1585
+ // Nolan is already the independent autopilot reviewer; recursively running
1586
+ // his own Ideal self-review adds latency and can corrupt the verdict shape.
1587
+ ken.setIdealReviewSuppressed(true);
1354
1588
  await ken.initialize();
1355
1589
  // Deliberately no bus bridge: the review is silent. Errors surface via the
1356
1590
  // runAutopilotReview try/catch as autopilot_error frames.
@@ -1361,20 +1595,6 @@ async function createSession(deps, opts) {
1361
1595
  });
1362
1596
  return ken;
1363
1597
  }
1364
- // Resumed session: if it already has a conversation, generate its title now so
1365
- // the title bar shows it immediately on load (not just after the next prompt).
1366
- {
1367
- const hasHistory = session
1368
- .getMessages()
1369
- .some((m) => m.role === "user" || m.role === "assistant");
1370
- if (hasHistory) {
1371
- titleGenerated = true;
1372
- void session.generateTitle().then((title) => {
1373
- if (title)
1374
- broadcast("session_title", { title });
1375
- });
1376
- }
1377
- }
1378
1598
  function abortOwnedWork() {
1379
1599
  cancelGeneration++;
1380
1600
  abort.abort();
@@ -1437,10 +1657,35 @@ async function createSession(deps, opts) {
1437
1657
  // Fire-and-forget — XP must never delay or break run teardown.
1438
1658
  void progress.awardRun(cwd, runStartedAt, opts.id);
1439
1659
  }
1440
- // A run may have switched branches (git checkout) or spawned/finished
1441
- // background tasks — refresh the footer extras once it settles.
1442
- gitBranch = await getGitBranch(cwd).catch(() => gitBranch);
1443
- gitIsRepo = await isGitRepo(cwd).catch(() => gitIsRepo);
1660
+ // A run may have switched branches, changed files, or spawned/finished
1661
+ // background tasks. Refresh the workspace extras once it settles.
1662
+ [gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
1663
+ getGitBranch(cwd).catch(() => gitBranch),
1664
+ isGitRepo(cwd).catch(() => gitIsRepo),
1665
+ getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
1666
+ ]);
1667
+ // Serialize behind any marker/tool-triggered refresh so the terminal
1668
+ // progress snapshot uses the live plan file. Once every canonical step
1669
+ // is complete, remove the approved plan from future system prompts and
1670
+ // clear the widget before run_end paints the idle activity bar.
1671
+ if (runSucceeded &&
1672
+ !cancelled &&
1673
+ approvedPlanPath !== null &&
1674
+ (await queueApprovedPlanProgressSync())) {
1675
+ try {
1676
+ await session.setApprovedPlan(undefined);
1677
+ deactivateApprovedPlan();
1678
+ broadcast("plan_progress", { total: 0, completed: [] });
1679
+ }
1680
+ catch (error) {
1681
+ // Keep tracking when prompt cleanup fails; hiding the widget here
1682
+ // would claim completion while the approved-plan contract remained.
1683
+ captureSidecarError(error, "app-sidecar.plan.cleanup");
1684
+ log("WARN", "app-sidecar", "completed plan cleanup failed", {
1685
+ message: error instanceof Error ? error.message : String(error),
1686
+ });
1687
+ }
1688
+ }
1444
1689
  if (ownsGeneration)
1445
1690
  finishOwnedGeneration(generation, false);
1446
1691
  // A cancelled injected run is still owned by the surrounding autopilot
@@ -1463,13 +1708,6 @@ async function createSession(deps, opts) {
1463
1708
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
1464
1709
  broadcast("queued", { count: session.getQueuedCount() });
1465
1710
  broadcast("extras", footerExtras());
1466
- if (!titleGenerated) {
1467
- titleGenerated = true;
1468
- void session.generateTitle().then((title) => {
1469
- if (title)
1470
- broadcast("session_title", { title });
1471
- });
1472
- }
1473
1711
  }
1474
1712
  }
1475
1713
  // ── Autopilot orchestration ─────────────────────────────────
@@ -1574,6 +1812,7 @@ async function createSession(deps, opts) {
1574
1812
  const generation = runLifecycle.begin(abortOwnedWork).generation;
1575
1813
  pendingCancelDrain = null;
1576
1814
  autopilotActive = true;
1815
+ session.setIdealReviewSuppressed(true);
1577
1816
  // Generation captured by the last plan review; acceptPlan re-checks it so
1578
1817
  // a user Accept/Reject landing mid-review always wins.
1579
1818
  let planGenAtReview = -1;
@@ -1600,22 +1839,22 @@ async function createSession(deps, opts) {
1600
1839
  if (pendingPlanPath === null || planGeneration !== planGenAtReview)
1601
1840
  return false;
1602
1841
  const planPath = pendingPlanPath;
1842
+ let planTotal;
1603
1843
  try {
1604
- await session.newSession();
1844
+ await session.newSession(true);
1605
1845
  injectedAutopilotPrompts = [];
1606
- titleGenerated = false;
1607
- await session.setApprovedPlan(planPath);
1846
+ planTotal = await activateApprovedPlan(planPath);
1608
1847
  }
1609
1848
  catch (err) {
1610
1849
  broadcastError("autopilot_error", "autopilot plan accept failed", err);
1611
1850
  return false;
1612
1851
  }
1613
1852
  clearPendingPlan();
1614
- // Ordering is load-bearing: the webview reads its still-open plan
1615
- // modal state (step count) on autopilot_plan_accepted, and
1616
- // session_reset clears it — accepted must land first.
1853
+ // Keep the approval marker ahead of the reset, then seed the reset
1854
+ // with the sidecar's canonical count from the actual plan file.
1617
1855
  broadcast("autopilot_plan_accepted", {});
1618
- broadcast("session_reset", {});
1856
+ broadcast("session_reset", { planTotal });
1857
+ broadcast("plan_progress", planProgressPayload());
1619
1858
  // Persisted into the NEW session so a resume shows the marker.
1620
1859
  void session.persistAutopilotMarker("plan_approved");
1621
1860
  return true;
@@ -1687,6 +1926,7 @@ async function createSession(deps, opts) {
1687
1926
  }
1688
1927
  finally {
1689
1928
  autopilotActive = false;
1929
+ session.setIdealReviewSuppressed(autopilot);
1690
1930
  finishOwnedGeneration(generation, true);
1691
1931
  queueMicrotask(() => void runStrandedQueue());
1692
1932
  }
@@ -1786,9 +2026,9 @@ async function createSession(deps, opts) {
1786
2026
  async function runTaskByIdInner(task) {
1787
2027
  // Fresh session per task so one task's context never bleeds into the next.
1788
2028
  await session.newSession();
2029
+ deactivateApprovedPlan();
1789
2030
  injectedAutopilotPrompts = [];
1790
2031
  clearPendingPlan();
1791
- titleGenerated = false;
1792
2032
  broadcast("session_reset", {});
1793
2033
  markTaskInProgress(cwd, task.id);
1794
2034
  broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
@@ -1872,13 +2112,32 @@ async function createSession(deps, opts) {
1872
2112
  tasksPoll.unref?.();
1873
2113
  };
1874
2114
  scheduleTasksPoll(1500);
1875
- function readBody(req) {
1876
- return new Promise((resolve, reject) => {
1877
- const chunks = [];
1878
- req.on("data", (c) => chunks.push(c));
1879
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1880
- req.on("error", reject);
1881
- });
2115
+ // Files can change outside the agent (editor saves, terminal commits), so keep
2116
+ // the dirty count current while idle. Branch/repo state already refreshes after
2117
+ // agent runs; polling only the count avoids spawning three git processes per tick.
2118
+ let gitPoll;
2119
+ let gitPollStopped = false;
2120
+ const scheduleGitPoll = (delay) => {
2121
+ if (gitPollStopped)
2122
+ return;
2123
+ gitPoll = setTimeout(() => {
2124
+ void getGitDirtyFileCount(cwd)
2125
+ .catch(() => gitDirtyFileCount)
2126
+ .then((nextDirtyFileCount) => {
2127
+ if (gitPollStopped)
2128
+ return;
2129
+ if (nextDirtyFileCount !== gitDirtyFileCount) {
2130
+ gitDirtyFileCount = nextDirtyFileCount;
2131
+ broadcast("extras", footerExtras());
2132
+ }
2133
+ scheduleGitPoll(5000);
2134
+ });
2135
+ }, delay);
2136
+ gitPoll.unref?.();
2137
+ };
2138
+ scheduleGitPoll(5000);
2139
+ function readBody(req, res) {
2140
+ return readCappedBody(req, res);
1882
2141
  }
1883
2142
  function json(res, status, body) {
1884
2143
  const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
@@ -1916,7 +2175,10 @@ async function createSession(deps, opts) {
1916
2175
  void memoryStore
1917
2176
  .snapshot()
1918
2177
  .then((snapshot) => json(res, 200, snapshot))
1919
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2178
+ .catch((error) => {
2179
+ captureSidecarError(error, "app-sidecar.memory.snapshot");
2180
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2181
+ });
1920
2182
  return;
1921
2183
  }
1922
2184
  if (method === "DELETE" && url.startsWith("/memories/")) {
@@ -1929,14 +2191,20 @@ async function createSession(deps, opts) {
1929
2191
  .forget(id)
1930
2192
  .then(() => memoryStore.snapshot())
1931
2193
  .then((snapshot) => json(res, 200, snapshot))
1932
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2194
+ .catch((error) => {
2195
+ captureSidecarError(error, "app-sidecar.memory.forget");
2196
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2197
+ });
1933
2198
  return;
1934
2199
  }
1935
2200
  if (method === "GET" && url === "/jiwa") {
1936
2201
  void jiwaStore
1937
2202
  .snapshot()
1938
2203
  .then((snapshot) => json(res, 200, snapshot))
1939
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2204
+ .catch((error) => {
2205
+ captureSidecarError(error, "app-sidecar.jiwa.snapshot");
2206
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2207
+ });
1940
2208
  return;
1941
2209
  }
1942
2210
  if (method === "DELETE" && url.startsWith("/jiwa/")) {
@@ -1949,7 +2217,10 @@ async function createSession(deps, opts) {
1949
2217
  .forget(id)
1950
2218
  .then(() => jiwaStore.snapshot())
1951
2219
  .then((snapshot) => json(res, 200, snapshot))
1952
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2220
+ .catch((error) => {
2221
+ captureSidecarError(error, "app-sidecar.jiwa.forget");
2222
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2223
+ });
1953
2224
  return;
1954
2225
  }
1955
2226
  if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
@@ -2007,7 +2278,9 @@ async function createSession(deps, opts) {
2007
2278
  return;
2008
2279
  }
2009
2280
  if (method === "POST" && url === "/settings") {
2010
- void readBody(req).then(async (raw) => {
2281
+ void readBody(req, res).then(async (raw) => {
2282
+ if (raw === null)
2283
+ return;
2011
2284
  let projectsRoot;
2012
2285
  try {
2013
2286
  projectsRoot = JSON.parse(raw).projectsRoot ?? "";
@@ -2030,7 +2303,9 @@ async function createSession(deps, opts) {
2030
2303
  return;
2031
2304
  }
2032
2305
  if (method === "POST" && url === "/create-project") {
2033
- void readBody(req).then(async (raw) => {
2306
+ void readBody(req, res).then(async (raw) => {
2307
+ if (raw === null)
2308
+ return;
2034
2309
  let name;
2035
2310
  try {
2036
2311
  name = JSON.parse(raw).name ?? "";
@@ -2062,6 +2337,7 @@ async function createSession(deps, opts) {
2062
2337
  json(res, 200, { path: dir });
2063
2338
  }
2064
2339
  catch (err) {
2340
+ captureSidecarError(err, "app-sidecar.project.create");
2065
2341
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
2066
2342
  }
2067
2343
  });
@@ -2072,6 +2348,7 @@ async function createSession(deps, opts) {
2072
2348
  void discoverProjects()
2073
2349
  .then((projects) => json(res, 200, { projects }))
2074
2350
  .catch((err) => {
2351
+ captureSidecarError(err, "app-sidecar.projects.discover");
2075
2352
  log("ERROR", "app-sidecar", "discoverProjects failed", {
2076
2353
  message: err instanceof Error ? err.message : String(err),
2077
2354
  });
@@ -2086,37 +2363,14 @@ async function createSession(deps, opts) {
2086
2363
  return;
2087
2364
  }
2088
2365
  const requestedAgent = new URL(url, `http://${host}`).searchParams.get("chatAgent");
2089
- if (requestedAgent === "all") {
2090
- void Promise.all(CHAT_AGENT_IDS.map(async (agentId) => {
2091
- const agentSessions = await listRecentSessions(target, 5, chatAgentSessionsDir(paths.sessionsDir, agentId));
2092
- return agentSessions.map((item) => ({ ...item, chatAgent: agentId }));
2093
- }))
2094
- .then(async (groups) => {
2095
- const dated = await Promise.all(groups.flat().map(async (item) => ({
2096
- item,
2097
- mtime: await fs
2098
- .stat(item.path)
2099
- .then((stat) => stat.mtimeMs)
2100
- .catch(() => 0),
2101
- })));
2102
- const recent = dated
2103
- .sort((left, right) => right.mtime - left.mtime)
2104
- .slice(0, 5)
2105
- .map(({ item }) => item);
2106
- json(res, 200, { sessions: recent });
2107
- })
2108
- .catch(() => json(res, 200, { sessions: [] }));
2109
- return;
2110
- }
2111
- // The picker may be served by a sidecar currently running in Chat mode.
2112
- // An omitted chatAgent always means coding history; chat callers identify
2113
- // their namespace explicitly (one agent or "all").
2114
- const sessionsDir = requestedAgent
2115
- ? sessionsDirForChatAgent(paths.sessionsDir, requestedAgent)
2116
- : paths.sessionsDir;
2117
- void listRecentSessions(target, 5, sessionsDir)
2366
+ // An omitted chatAgent means coding history; chat callers identify one
2367
+ // agent or request the combined, recency-sorted "all" listing.
2368
+ void listSidecarSessions(target, requestedAgent, paths.sessionsDir)
2118
2369
  .then((sessions) => json(res, 200, { sessions }))
2119
- .catch(() => json(res, 200, { sessions: [] }));
2370
+ .catch((error) => {
2371
+ captureSidecarError(error, "app-sidecar.sessions.list");
2372
+ json(res, 200, { sessions: [] });
2373
+ });
2120
2374
  return;
2121
2375
  }
2122
2376
  if (method === "GET" && url.startsWith("/files")) {
@@ -2124,6 +2378,7 @@ async function createSession(deps, opts) {
2124
2378
  void searchProjectFiles(cwd, q)
2125
2379
  .then((files) => json(res, 200, { files }))
2126
2380
  .catch((err) => {
2381
+ captureSidecarError(err, "app-sidecar.files.search");
2127
2382
  log("ERROR", "app-sidecar", "searchProjectFiles failed", {
2128
2383
  message: err instanceof Error ? err.message : String(err),
2129
2384
  });
@@ -2145,6 +2400,20 @@ async function createSession(deps, opts) {
2145
2400
  void (async () => {
2146
2401
  const commandCandidates = [...PROMPT_COMMANDS, ...(await loadCustomCommands(cwd))];
2147
2402
  const messages = session.getMessages();
2403
+ // An Ideal hook is injected immediately after the assistant's candidate
2404
+ // no-tool response. That response is review scratch, not a user-visible
2405
+ // final answer. Live SSE drops it when the hook event arrives; mark the
2406
+ // same assistant message here so resumed history stays identical.
2407
+ const hiddenIdealDrafts = new Set();
2408
+ for (let i = 0; i < messages.length - 1; i++) {
2409
+ const draft = messages[i];
2410
+ const hookPrompt = messages[i + 1];
2411
+ if (draft?.role !== "assistant" || hookPrompt?.role !== "user")
2412
+ continue;
2413
+ const restored = restoreUserRow(hookPrompt.content);
2414
+ if (detectHookKind(restored.text) === "ideal")
2415
+ hiddenIdealDrafts.add(draft);
2416
+ }
2148
2417
  // Pre-index tool results by toolCallId so we can pair tool calls with
2149
2418
  // their results (for sub-agent status + image extraction).
2150
2419
  const toolResultMap = new Map();
@@ -2362,10 +2631,11 @@ async function createSession(deps, opts) {
2362
2631
  }
2363
2632
  }
2364
2633
  }
2365
- else {
2634
+ else if (!hiddenIdealDrafts.has(msg)) {
2366
2635
  // Assistant: one wire row per persisted text block — live streaming
2367
2636
  // splits bubbles at server_tool_call boundaries, and the persisted
2368
- // content keeps those blocks separate.
2637
+ // content keeps those blocks separate. Ideal-review candidate drafts
2638
+ // are intentionally omitted to match the live pre-final hook flow.
2369
2639
  for (const blockText of restoreAssistantTexts(msg.content)) {
2370
2640
  history.push({
2371
2641
  role: "assistant",
@@ -2449,7 +2719,9 @@ async function createSession(deps, opts) {
2449
2719
  return;
2450
2720
  }
2451
2721
  if (method === "POST" && url === "/prompt") {
2452
- void readBody(req).then(async (raw) => {
2722
+ void readBody(req, res).then(async (raw) => {
2723
+ if (raw === null)
2724
+ return;
2453
2725
  let text;
2454
2726
  let attachments;
2455
2727
  let meta;
@@ -2576,7 +2848,9 @@ async function createSession(deps, opts) {
2576
2848
  json(res, 404, { error: "Nolan is not available in EZ Chat." });
2577
2849
  return;
2578
2850
  }
2579
- void readBody(req).then(async (raw) => {
2851
+ void readBody(req, res).then(async (raw) => {
2852
+ if (raw === null)
2853
+ return;
2580
2854
  let text;
2581
2855
  try {
2582
2856
  text = JSON.parse(raw).text ?? "";
@@ -2635,7 +2909,9 @@ async function createSession(deps, opts) {
2635
2909
  json(res, 404, { error: "Autopilot is not available in EZ Chat." });
2636
2910
  return;
2637
2911
  }
2638
- void readBody(req).then(async (raw) => {
2912
+ void readBody(req, res).then(async (raw) => {
2913
+ if (raw === null)
2914
+ return;
2639
2915
  let enabled;
2640
2916
  try {
2641
2917
  enabled = Boolean(JSON.parse(raw).enabled);
@@ -2645,6 +2921,9 @@ async function createSession(deps, opts) {
2645
2921
  return;
2646
2922
  }
2647
2923
  autopilot = enabled;
2924
+ // A toggle-off during an active cycle takes effect after Nolan finishes;
2925
+ // until then, injected build runs must not re-enable Ideal self-review.
2926
+ session.setIdealReviewSuppressed(enabled || autopilotActive);
2648
2927
  await saveAutopilot(cwd, enabled);
2649
2928
  log("INFO", "app-sidecar", "autopilot toggled", { enabled: String(enabled) });
2650
2929
  broadcast("autopilot", { autopilot: enabled });
@@ -2653,7 +2932,9 @@ async function createSession(deps, opts) {
2653
2932
  return;
2654
2933
  }
2655
2934
  if (method === "POST" && url === "/enhance") {
2656
- void readBody(req).then(async (raw) => {
2935
+ void readBody(req, res).then(async (raw) => {
2936
+ if (raw === null)
2937
+ return;
2657
2938
  let text;
2658
2939
  try {
2659
2940
  text = JSON.parse(raw).text ?? "";
@@ -2674,6 +2955,7 @@ async function createSession(deps, opts) {
2674
2955
  }
2675
2956
  catch (err) {
2676
2957
  const message = err instanceof Error ? err.message : String(err);
2958
+ captureSidecarError(err, "app-sidecar.prompt-enhancer");
2677
2959
  log("ERROR", "app-sidecar", "enhance failed", { message });
2678
2960
  json(res, 500, { error: message });
2679
2961
  }
@@ -2701,7 +2983,9 @@ async function createSession(deps, opts) {
2701
2983
  return;
2702
2984
  }
2703
2985
  if (method === "POST" && url === "/radio/volume") {
2704
- void readBody(req).then((raw) => {
2986
+ void readBody(req, res).then((raw) => {
2987
+ if (raw === null)
2988
+ return;
2705
2989
  let volume;
2706
2990
  try {
2707
2991
  volume = Number(JSON.parse(raw).volume);
@@ -2724,7 +3008,9 @@ async function createSession(deps, opts) {
2724
3008
  return;
2725
3009
  }
2726
3010
  if (method === "POST" && url === "/radio") {
2727
- void readBody(req).then((raw) => {
3011
+ void readBody(req, res).then((raw) => {
3012
+ if (raw === null)
3013
+ return;
2728
3014
  let station;
2729
3015
  try {
2730
3016
  station = JSON.parse(raw).station ?? "";
@@ -2748,7 +3034,9 @@ async function createSession(deps, opts) {
2748
3034
  return;
2749
3035
  }
2750
3036
  if (method === "POST" && url === "/tasks/run") {
2751
- void readBody(req).then((raw) => {
3037
+ void readBody(req, res).then((raw) => {
3038
+ if (raw === null)
3039
+ return;
2752
3040
  let id;
2753
3041
  let all;
2754
3042
  try {
@@ -2770,7 +3058,9 @@ async function createSession(deps, opts) {
2770
3058
  return;
2771
3059
  }
2772
3060
  if (method === "POST" && url === "/tasks/add") {
2773
- void readBody(req).then((raw) => {
3061
+ void readBody(req, res).then((raw) => {
3062
+ if (raw === null)
3063
+ return;
2774
3064
  let title;
2775
3065
  let prompt;
2776
3066
  try {
@@ -2802,7 +3092,9 @@ async function createSession(deps, opts) {
2802
3092
  return;
2803
3093
  }
2804
3094
  if (method === "POST" && url === "/tasks/update") {
2805
- void readBody(req).then((raw) => {
3095
+ void readBody(req, res).then((raw) => {
3096
+ if (raw === null)
3097
+ return;
2806
3098
  let id;
2807
3099
  let status;
2808
3100
  let title;
@@ -2847,7 +3139,9 @@ async function createSession(deps, opts) {
2847
3139
  return;
2848
3140
  }
2849
3141
  if (method === "POST" && url === "/tasks/delete") {
2850
- void readBody(req).then((raw) => {
3142
+ void readBody(req, res).then((raw) => {
3143
+ if (raw === null)
3144
+ return;
2851
3145
  let id;
2852
3146
  try {
2853
3147
  id = JSON.parse(raw).id ?? "";
@@ -2890,7 +3184,9 @@ async function createSession(deps, opts) {
2890
3184
  return;
2891
3185
  }
2892
3186
  if (method === "POST" && url === "/model") {
2893
- void readBody(req).then(async (raw) => {
3187
+ void readBody(req, res).then(async (raw) => {
3188
+ if (raw === null)
3189
+ return;
2894
3190
  let modelId;
2895
3191
  try {
2896
3192
  modelId = JSON.parse(raw).model ?? "";
@@ -2962,7 +3258,9 @@ async function createSession(deps, opts) {
2962
3258
  // to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
2963
3259
  // either is mid-run defers via the pending-model mechanics.
2964
3260
  if (method === "POST" && url === "/ken/model") {
2965
- void readBody(req).then(async (raw) => {
3261
+ void readBody(req, res).then(async (raw) => {
3262
+ if (raw === null)
3263
+ return;
2966
3264
  let modelId;
2967
3265
  try {
2968
3266
  const parsed = JSON.parse(raw).model;
@@ -3006,7 +3304,9 @@ async function createSession(deps, opts) {
3006
3304
  return;
3007
3305
  }
3008
3306
  if (method === "POST" && url === "/kill") {
3009
- void readBody(req).then(async (raw) => {
3307
+ void readBody(req, res).then(async (raw) => {
3308
+ if (raw === null)
3309
+ return;
3010
3310
  let id;
3011
3311
  try {
3012
3312
  id = JSON.parse(raw).id ?? "";
@@ -3084,6 +3384,7 @@ async function createSession(deps, opts) {
3084
3384
  drained,
3085
3385
  });
3086
3386
  })().catch((error) => {
3387
+ captureSidecarError(error, "app-sidecar.run.cancel");
3087
3388
  broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
3088
3389
  json(res, 500, {
3089
3390
  error: "cancel_failed",
@@ -3104,12 +3405,14 @@ async function createSession(deps, opts) {
3104
3405
  if (mode === "chat") {
3105
3406
  await session.persistAppMarker("agent_handoff", { chatAgent });
3106
3407
  }
3408
+ deactivateApprovedPlan();
3107
3409
  injectedAutopilotPrompts = [];
3108
3410
  clearPendingPlan();
3109
3411
  broadcast("session_reset", {});
3110
3412
  json(res, 200, { ok: true });
3111
3413
  })
3112
3414
  .catch((err) => {
3415
+ captureSidecarError(err, "app-sidecar.session.new");
3113
3416
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
3114
3417
  });
3115
3418
  return;
@@ -3124,7 +3427,9 @@ async function createSession(deps, opts) {
3124
3427
  // 3. session_reset tells the webview to clear its transcript; it then runs
3125
3428
  // the "implement it now" prompt in the clean session.
3126
3429
  if (method === "POST" && url === "/plan/accept") {
3127
- void readBody(req).then(async (raw) => {
3430
+ void readBody(req, res).then(async (raw) => {
3431
+ if (raw === null)
3432
+ return;
3128
3433
  let planPath;
3129
3434
  try {
3130
3435
  planPath = JSON.parse(raw).planPath || undefined;
@@ -3156,14 +3461,15 @@ async function createSession(deps, opts) {
3156
3461
  broadcast("autopilot_ignored", {});
3157
3462
  }
3158
3463
  try {
3159
- await session.newSession();
3464
+ await session.newSession(true);
3160
3465
  injectedAutopilotPrompts = [];
3161
- titleGenerated = false;
3162
- await session.setApprovedPlan(planPath);
3163
- broadcast("session_reset", {});
3164
- json(res, 200, { ok: true });
3466
+ const planTotal = await activateApprovedPlan(planPath);
3467
+ broadcast("session_reset", { planTotal });
3468
+ broadcast("plan_progress", planProgressPayload());
3469
+ json(res, 200, { ok: true, planTotal });
3165
3470
  }
3166
3471
  catch (err) {
3472
+ captureSidecarError(err, "app-sidecar.plan.accept");
3167
3473
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
3168
3474
  }
3169
3475
  });
@@ -3175,7 +3481,9 @@ async function createSession(deps, opts) {
3175
3481
  return;
3176
3482
  }
3177
3483
  if (method === "POST" && url === "/auth/apikey") {
3178
- void readBody(req).then(async (raw) => {
3484
+ void readBody(req, res).then(async (raw) => {
3485
+ if (raw === null)
3486
+ return;
3179
3487
  let provider = "";
3180
3488
  let key;
3181
3489
  let variant;
@@ -3218,7 +3526,9 @@ async function createSession(deps, opts) {
3218
3526
  return;
3219
3527
  }
3220
3528
  if (method === "POST" && url === "/auth/oauth/start") {
3221
- void readBody(req).then((raw) => {
3529
+ void readBody(req, res).then((raw) => {
3530
+ if (raw === null)
3531
+ return;
3222
3532
  let provider = "";
3223
3533
  try {
3224
3534
  provider = JSON.parse(raw).provider ?? "";
@@ -3260,6 +3570,7 @@ async function createSession(deps, opts) {
3260
3570
  broadcast("auth_done", { provider });
3261
3571
  }
3262
3572
  catch (err) {
3573
+ captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
3263
3574
  broadcast("auth_error", {
3264
3575
  provider,
3265
3576
  message: err instanceof Error ? err.message : String(err),
@@ -3274,7 +3585,9 @@ async function createSession(deps, opts) {
3274
3585
  return;
3275
3586
  }
3276
3587
  if (method === "POST" && url === "/auth/oauth/code") {
3277
- void readBody(req).then((raw) => {
3588
+ void readBody(req, res).then((raw) => {
3589
+ if (raw === null)
3590
+ return;
3278
3591
  let code;
3279
3592
  try {
3280
3593
  code = JSON.parse(raw).code ?? "";
@@ -3294,7 +3607,9 @@ async function createSession(deps, opts) {
3294
3607
  return;
3295
3608
  }
3296
3609
  if (method === "POST" && url === "/auth/logout") {
3297
- void readBody(req).then(async (raw) => {
3610
+ void readBody(req, res).then(async (raw) => {
3611
+ if (raw === null)
3612
+ return;
3298
3613
  let provider;
3299
3614
  try {
3300
3615
  provider = JSON.parse(raw).provider ?? "";
@@ -3333,7 +3648,9 @@ async function createSession(deps, opts) {
3333
3648
  return;
3334
3649
  }
3335
3650
  if (method === "POST" && url === "/telegram") {
3336
- void readBody(req).then(async (raw) => {
3651
+ void readBody(req, res).then(async (raw) => {
3652
+ if (raw === null)
3653
+ return;
3337
3654
  let botTokenInput;
3338
3655
  let userIdInput;
3339
3656
  try {
@@ -3401,6 +3718,7 @@ async function createSession(deps, opts) {
3401
3718
  }
3402
3719
  catch (err) {
3403
3720
  serveController = null;
3721
+ captureSidecarError(err, "app-sidecar.serve.start", { provider: st.provider });
3404
3722
  json(res, 400, { error: err instanceof Error ? err.message : String(err) });
3405
3723
  }
3406
3724
  })();
@@ -3427,6 +3745,7 @@ async function createSession(deps, opts) {
3427
3745
  void buildMcpRows(targetCwd)
3428
3746
  .then((servers) => json(res, 200, { servers }))
3429
3747
  .catch((err) => {
3748
+ captureSidecarError(err, "app-sidecar.mcp.list");
3430
3749
  log("ERROR", "app-sidecar", "buildMcpRows failed", {
3431
3750
  message: err instanceof Error ? err.message : String(err),
3432
3751
  });
@@ -3435,7 +3754,9 @@ async function createSession(deps, opts) {
3435
3754
  return;
3436
3755
  }
3437
3756
  if (method === "POST" && url === "/mcp/add") {
3438
- void readBody(req).then(async (raw) => {
3757
+ void readBody(req, res).then(async (raw) => {
3758
+ if (raw === null)
3759
+ return;
3439
3760
  let line;
3440
3761
  let scopeValue;
3441
3762
  let bodyCwd;
@@ -3483,6 +3804,7 @@ async function createSession(deps, opts) {
3483
3804
  });
3484
3805
  }
3485
3806
  catch (err) {
3807
+ captureSidecarError(err, "app-sidecar.mcp.add", { server: config.name });
3486
3808
  json(res, 500, {
3487
3809
  error: err instanceof Error ? err.message : String(err),
3488
3810
  });
@@ -3491,7 +3813,9 @@ async function createSession(deps, opts) {
3491
3813
  return;
3492
3814
  }
3493
3815
  if (method === "POST" && url === "/mcp/remove") {
3494
- void readBody(req).then(async (raw) => {
3816
+ void readBody(req, res).then(async (raw) => {
3817
+ if (raw === null)
3818
+ return;
3495
3819
  let name;
3496
3820
  let scopeValue;
3497
3821
  let bodyCwd;
@@ -3528,7 +3852,9 @@ async function createSession(deps, opts) {
3528
3852
  // `mcp_auth_error`. Responds 202 immediately and runs the flow in the
3529
3853
  // background (the browser round-trip can take a while).
3530
3854
  if (method === "POST" && url === "/mcp/login") {
3531
- void readBody(req).then(async (raw) => {
3855
+ void readBody(req, res).then(async (raw) => {
3856
+ if (raw === null)
3857
+ return;
3532
3858
  let name;
3533
3859
  let scopeValue;
3534
3860
  let bodyCwd;
@@ -3572,6 +3898,7 @@ async function createSession(deps, opts) {
3572
3898
  }
3573
3899
  }
3574
3900
  catch (err) {
3901
+ captureSidecarError(err, "app-sidecar.mcp.login", { server: name });
3575
3902
  broadcast("mcp_auth_error", {
3576
3903
  name,
3577
3904
  message: err instanceof Error ? err.message : String(err),
@@ -3589,6 +3916,9 @@ async function createSession(deps, opts) {
3589
3916
  tasksPollStopped = true;
3590
3917
  if (tasksPoll)
3591
3918
  clearTimeout(tasksPoll);
3919
+ gitPollStopped = true;
3920
+ if (gitPoll)
3921
+ clearTimeout(gitPoll);
3592
3922
  // Stop the Telegram serve loop + dispose its per-chat sessions.
3593
3923
  if (serveController)
3594
3924
  await serveController.stop().catch(() => { });
@@ -3615,7 +3945,9 @@ async function createSession(deps, opts) {
3615
3945
  dispose,
3616
3946
  };
3617
3947
  }
3618
- main().catch((err) => {
3948
+ main().catch(async (err) => {
3949
+ captureSidecarError(err, "app-sidecar.main", { severity: "fatal" });
3950
+ await flushSidecarErrors();
3619
3951
  const message = err instanceof Error ? err.message : String(err);
3620
3952
  process.stderr.write(`GG_APP_FATAL ${message}\n`);
3621
3953
  process.exit(1);