@prestyj/cli 5.8.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 (231) hide show
  1. package/assets/skills/evidence-led-ui/LICENSES.md +12 -0
  2. package/assets/skills/evidence-led-ui/SKILL.md +14 -6
  3. package/assets/skills/evidence-led-ui/references/anti-defaults.md +9 -1
  4. package/assets/skills/evidence-led-ui/references/craft-rulings.md +33 -0
  5. package/assets/skills/evidence-led-ui/references/production-contract.md +41 -21
  6. package/assets/skills/evidence-led-ui/references/provenance.md +7 -5
  7. package/assets/skills/evidence-led-ui/references/quality-rubric.md +10 -10
  8. package/dist/app-sidecar-sessions.d.ts +8 -0
  9. package/dist/app-sidecar-sessions.d.ts.map +1 -0
  10. package/dist/app-sidecar-sessions.js +31 -0
  11. package/dist/app-sidecar-sessions.js.map +1 -0
  12. package/dist/app-sidecar-sessions.test.d.ts +2 -0
  13. package/dist/app-sidecar-sessions.test.d.ts.map +1 -0
  14. package/dist/app-sidecar-sessions.test.js +83 -0
  15. package/dist/app-sidecar-sessions.test.js.map +1 -0
  16. package/dist/app-sidecar.js +250 -107
  17. package/dist/app-sidecar.js.map +1 -1
  18. package/dist/cli.d.ts.map +1 -1
  19. package/dist/cli.js +43 -26
  20. package/dist/cli.js.map +1 -1
  21. package/dist/config.d.ts +4 -0
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +9 -0
  24. package/dist/config.js.map +1 -1
  25. package/dist/core/agent-session-compaction.test.js +19 -0
  26. package/dist/core/agent-session-compaction.test.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +4 -0
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +84 -12
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/agents.d.ts +1 -1
  32. package/dist/core/agents.d.ts.map +1 -1
  33. package/dist/core/agents.js +4 -1
  34. package/dist/core/agents.js.map +1 -1
  35. package/dist/core/auth-storage.d.ts +1 -1
  36. package/dist/core/auth-storage.d.ts.map +1 -1
  37. package/dist/core/auth-storage.js +1 -1
  38. package/dist/core/auth-storage.js.map +1 -1
  39. package/dist/core/compaction/compactor.d.ts.map +1 -1
  40. package/dist/core/compaction/compactor.js +20 -11
  41. package/dist/core/compaction/compactor.js.map +1 -1
  42. package/dist/core/compaction/compactor.test.js +16 -3
  43. package/dist/core/compaction/compactor.test.js.map +1 -1
  44. package/dist/core/event-bus.d.ts +5 -0
  45. package/dist/core/event-bus.d.ts.map +1 -1
  46. package/dist/core/event-bus.js +6 -0
  47. package/dist/core/event-bus.js.map +1 -1
  48. package/dist/core/goal-controller.test.js +3 -3
  49. package/dist/core/goal-controller.test.js.map +1 -1
  50. package/dist/core/index.d.ts +2 -2
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/core/index.js +2 -2
  53. package/dist/core/index.js.map +1 -1
  54. package/dist/core/input-normalization.test.d.ts +2 -0
  55. package/dist/core/input-normalization.test.d.ts.map +1 -0
  56. package/dist/core/input-normalization.test.js +75 -0
  57. package/dist/core/input-normalization.test.js.map +1 -0
  58. package/dist/core/loop-breaker.d.ts +38 -1
  59. package/dist/core/loop-breaker.d.ts.map +1 -1
  60. package/dist/core/loop-breaker.js +84 -2
  61. package/dist/core/loop-breaker.js.map +1 -1
  62. package/dist/core/loop-breaker.test.js +108 -1
  63. package/dist/core/loop-breaker.test.js.map +1 -1
  64. package/dist/core/message-images.d.ts +12 -0
  65. package/dist/core/message-images.d.ts.map +1 -0
  66. package/dist/core/message-images.js +52 -0
  67. package/dist/core/message-images.js.map +1 -0
  68. package/dist/core/message-images.test.d.ts +2 -0
  69. package/dist/core/message-images.test.d.ts.map +1 -0
  70. package/dist/core/message-images.test.js +62 -0
  71. package/dist/core/message-images.test.js.map +1 -0
  72. package/dist/core/project-discovery.d.ts +1 -1
  73. package/dist/core/project-discovery.d.ts.map +1 -1
  74. package/dist/core/project-discovery.js +105 -53
  75. package/dist/core/project-discovery.js.map +1 -1
  76. package/dist/core/project-discovery.test.js +29 -0
  77. package/dist/core/project-discovery.test.js.map +1 -1
  78. package/dist/core/session-manager.d.ts +27 -7
  79. package/dist/core/session-manager.d.ts.map +1 -1
  80. package/dist/core/session-manager.js +257 -124
  81. package/dist/core/session-manager.js.map +1 -1
  82. package/dist/core/session-storage.d.ts +57 -0
  83. package/dist/core/session-storage.d.ts.map +1 -0
  84. package/dist/core/session-storage.js +568 -0
  85. package/dist/core/session-storage.js.map +1 -0
  86. package/dist/core/session-storage.test.d.ts +2 -0
  87. package/dist/core/session-storage.test.d.ts.map +1 -0
  88. package/dist/core/session-storage.test.js +269 -0
  89. package/dist/core/session-storage.test.js.map +1 -0
  90. package/dist/core/settings-manager.d.ts +2 -0
  91. package/dist/core/settings-manager.d.ts.map +1 -1
  92. package/dist/core/settings-manager.js +7 -0
  93. package/dist/core/settings-manager.js.map +1 -1
  94. package/dist/core/sidecar-error-reporter.d.ts +12 -0
  95. package/dist/core/sidecar-error-reporter.d.ts.map +1 -0
  96. package/dist/core/sidecar-error-reporter.js +57 -0
  97. package/dist/core/sidecar-error-reporter.js.map +1 -0
  98. package/dist/core/sidecar-error-reporter.test.d.ts +2 -0
  99. package/dist/core/sidecar-error-reporter.test.d.ts.map +1 -0
  100. package/dist/core/sidecar-error-reporter.test.js +61 -0
  101. package/dist/core/sidecar-error-reporter.test.js.map +1 -0
  102. package/dist/core/skills.d.ts +1 -1
  103. package/dist/core/skills.d.ts.map +1 -1
  104. package/dist/core/skills.js +4 -1
  105. package/dist/core/skills.js.map +1 -1
  106. package/dist/core/subagent-manager.d.ts +7 -0
  107. package/dist/core/subagent-manager.d.ts.map +1 -1
  108. package/dist/core/subagent-manager.js +26 -2
  109. package/dist/core/subagent-manager.js.map +1 -1
  110. package/dist/core/subagent-manager.test.js +34 -0
  111. package/dist/core/subagent-manager.test.js.map +1 -1
  112. package/dist/core/verify-commands.d.ts.map +1 -1
  113. package/dist/core/verify-commands.js +1 -6
  114. package/dist/core/verify-commands.js.map +1 -1
  115. package/dist/core/verify-commands.test.js +1 -5
  116. package/dist/core/verify-commands.test.js.map +1 -1
  117. package/dist/core/workspace-guard.d.ts +38 -0
  118. package/dist/core/workspace-guard.d.ts.map +1 -0
  119. package/dist/core/workspace-guard.js +112 -0
  120. package/dist/core/workspace-guard.js.map +1 -0
  121. package/dist/core/workspace-guard.test.d.ts +2 -0
  122. package/dist/core/workspace-guard.test.d.ts.map +1 -0
  123. package/dist/core/workspace-guard.test.js +86 -0
  124. package/dist/core/workspace-guard.test.js.map +1 -0
  125. package/dist/index.d.ts +1 -1
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +1 -1
  128. package/dist/index.js.map +1 -1
  129. package/dist/modes/json-mode.d.ts.map +1 -1
  130. package/dist/modes/json-mode.js +9 -0
  131. package/dist/modes/json-mode.js.map +1 -1
  132. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  133. package/dist/modes/subagent-worker-mode.js +11 -0
  134. package/dist/modes/subagent-worker-mode.js.map +1 -1
  135. package/dist/session.d.ts +4 -4
  136. package/dist/session.d.ts.map +1 -1
  137. package/dist/session.js +38 -101
  138. package/dist/session.js.map +1 -1
  139. package/dist/session.test.d.ts +2 -0
  140. package/dist/session.test.d.ts.map +1 -0
  141. package/dist/session.test.js +35 -0
  142. package/dist/session.test.js.map +1 -0
  143. package/dist/system-prompt.d.ts +12 -0
  144. package/dist/system-prompt.d.ts.map +1 -1
  145. package/dist/system-prompt.js +86 -19
  146. package/dist/system-prompt.js.map +1 -1
  147. package/dist/system-prompt.test.js +132 -8
  148. package/dist/system-prompt.test.js.map +1 -1
  149. package/dist/tools/bash.d.ts +3 -2
  150. package/dist/tools/bash.d.ts.map +1 -1
  151. package/dist/tools/bash.js +37 -6
  152. package/dist/tools/bash.js.map +1 -1
  153. package/dist/tools/bash.test.js +34 -1
  154. package/dist/tools/bash.test.js.map +1 -1
  155. package/dist/tools/edit.d.ts +2 -1
  156. package/dist/tools/edit.d.ts.map +1 -1
  157. package/dist/tools/edit.js +7 -1
  158. package/dist/tools/edit.js.map +1 -1
  159. package/dist/tools/edit.test.js +7 -0
  160. package/dist/tools/edit.test.js.map +1 -1
  161. package/dist/tools/exit-plan.d.ts.map +1 -1
  162. package/dist/tools/exit-plan.js +15 -4
  163. package/dist/tools/exit-plan.js.map +1 -1
  164. package/dist/tools/exit-plan.test.d.ts +2 -0
  165. package/dist/tools/exit-plan.test.d.ts.map +1 -0
  166. package/dist/tools/exit-plan.test.js +72 -0
  167. package/dist/tools/exit-plan.test.js.map +1 -0
  168. package/dist/tools/grep.d.ts.map +1 -1
  169. package/dist/tools/grep.js +8 -3
  170. package/dist/tools/grep.js.map +1 -1
  171. package/dist/tools/grep.test.js +6 -0
  172. package/dist/tools/grep.test.js.map +1 -1
  173. package/dist/tools/index.d.ts +8 -0
  174. package/dist/tools/index.d.ts.map +1 -1
  175. package/dist/tools/index.js +3 -2
  176. package/dist/tools/index.js.map +1 -1
  177. package/dist/tools/screenshot.js +1 -1
  178. package/dist/tools/screenshot.js.map +1 -1
  179. package/dist/tools/skill.d.ts.map +1 -1
  180. package/dist/tools/skill.js +4 -2
  181. package/dist/tools/skill.js.map +1 -1
  182. package/dist/tools/source-path.d.ts.map +1 -1
  183. package/dist/tools/source-path.js +9 -0
  184. package/dist/tools/source-path.js.map +1 -1
  185. package/dist/tools/subagent-shared.d.ts +1 -1
  186. package/dist/tools/subagent-shared.d.ts.map +1 -1
  187. package/dist/tools/subagent-shared.js +4 -1
  188. package/dist/tools/subagent-shared.js.map +1 -1
  189. package/dist/tools/subagent-shared.test.js +21 -1
  190. package/dist/tools/subagent-shared.test.js.map +1 -1
  191. package/dist/tools/web-search.d.ts.map +1 -1
  192. package/dist/tools/web-search.js +13 -2
  193. package/dist/tools/web-search.js.map +1 -1
  194. package/dist/tools/write.d.ts +2 -1
  195. package/dist/tools/write.d.ts.map +1 -1
  196. package/dist/tools/write.js +7 -1
  197. package/dist/tools/write.js.map +1 -1
  198. package/dist/tools/write.test.js +12 -0
  199. package/dist/tools/write.test.js.map +1 -1
  200. package/dist/ui/App.d.ts.map +1 -1
  201. package/dist/ui/App.js +17 -5
  202. package/dist/ui/App.js.map +1 -1
  203. package/dist/ui/app-items.d.ts +10 -0
  204. package/dist/ui/app-items.d.ts.map +1 -1
  205. package/dist/ui/app-items.js +10 -0
  206. package/dist/ui/app-items.js.map +1 -1
  207. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  208. package/dist/ui/components/ActivityIndicator.js +1 -0
  209. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  210. package/dist/ui/components/Footer.test.js +1 -1
  211. package/dist/ui/hooks/useAgentLoop.d.ts +5 -2
  212. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  213. package/dist/ui/hooks/useAgentLoop.js +49 -8
  214. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  215. package/dist/ui/hooks/useTranscriptHistory.d.ts +8 -5
  216. package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -1
  217. package/dist/ui/hooks/useTranscriptHistory.js +2 -30
  218. package/dist/ui/hooks/useTranscriptHistory.js.map +1 -1
  219. package/dist/utils/http-body.d.ts +19 -0
  220. package/dist/utils/http-body.d.ts.map +1 -0
  221. package/dist/utils/http-body.js +58 -0
  222. package/dist/utils/http-body.js.map +1 -0
  223. package/dist/utils/http-body.test.d.ts +2 -0
  224. package/dist/utils/http-body.test.d.ts.map +1 -0
  225. package/dist/utils/http-body.test.js +68 -0
  226. package/dist/utils/http-body.test.js.map +1 -0
  227. package/dist/utils/text.d.ts +9 -0
  228. package/dist/utils/text.d.ts.map +1 -0
  229. package/dist/utils/text.js +11 -0
  230. package/dist/utils/text.js.map +1 -0
  231. package/package.json +5 -5
@@ -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";
@@ -35,6 +35,7 @@ import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model
35
35
  import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
36
36
  import { AuthStorage } from "./core/auth-storage.js";
37
37
  import { cleanupToolOutputs } from "./tools/overflow.js";
38
+ import { readCappedBody } from "./utils/http-body.js";
38
39
  import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
39
40
  import { loginAnthropic } from "./core/oauth/anthropic.js";
40
41
  import { loginOpenAI } from "./core/oauth/openai.js";
@@ -43,19 +44,20 @@ import { loginKimi } from "./core/oauth/kimi.js";
43
44
  import { AUTH_PROVIDERS } from "./core/auth-providers.js";
44
45
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
45
46
  import { SettingsManager } from "./core/settings-manager.js";
46
- import { getModel, getMaxThinkingLevel, getContextWindow, MODELS } from "./core/model-registry.js";
47
+ import { getModel, getDefaultThinkingLevel, getContextWindow, MODELS, } from "./core/model-registry.js";
47
48
  import { resolveStartOrFallback } from "./core/resolve-start.js";
48
49
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
49
50
  import { extractPlanSteps } from "./utils/plan-steps.js";
50
51
  import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
51
52
  import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
52
53
  import { loadCustomCommands } from "./core/custom-commands.js";
53
- import { discoverProjects, listRecentSessions } from "./core/project-discovery.js";
54
+ import { discoverProjects } from "./core/project-discovery.js";
55
+ import { listSidecarSessions } from "./app-sidecar-sessions.js";
54
56
  import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
55
57
  import { initLogger, log } from "./core/logger.js";
56
58
  import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
57
59
  import { enrichProcessPath } from "./core/shell-path.js";
58
- import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
60
+ import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
59
61
  import { startServeMode } from "./modes/serve-mode.js";
60
62
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
61
63
  import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
@@ -64,6 +66,7 @@ import { loadProgress, peekProgress, updateProgress } from "./core/progress/stor
64
66
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
65
67
  import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
66
68
  import { rebuildFromSessions } from "./core/progress/rebuild.js";
69
+ import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
67
70
  const ALL_PROVIDERS = [
68
71
  // US
69
72
  "anthropic",
@@ -169,6 +172,7 @@ async function persistModelSelection(settingsFile, provider, model) {
169
172
  await sm.set("defaultModel", model);
170
173
  }
171
174
  catch (err) {
175
+ captureSidecarError(err, "app-sidecar.settings.persist-model");
172
176
  log("WARN", "app-sidecar", "failed to persist model selection", { err: String(err) });
173
177
  }
174
178
  }
@@ -185,6 +189,7 @@ async function persistThinkingLevel(settingsFile, level) {
185
189
  await sm.set("thinkingLevel", level);
186
190
  }
187
191
  catch (err) {
192
+ captureSidecarError(err, "app-sidecar.settings.persist-thinking");
188
193
  log("WARN", "app-sidecar", "failed to persist thinking level", { err: String(err) });
189
194
  }
190
195
  }
@@ -202,19 +207,28 @@ async function prepareAttachments(cwd, attachments) {
202
207
  const fileName = `${Date.now().toString(36)}-${safe}`;
203
208
  const filePath = path.join(dir, fileName);
204
209
  const buf = Buffer.from(a.data, "base64");
205
- // Validate image attachments before they become native image content blocks.
206
- // A corrupt or unsupported-format image (e.g. a malformed .ico, or a .png
207
- // with a bad IDAT) makes the provider reject the ENTIRE turn ("image data
208
- // ... not a valid image") before the agent can respond. Downgrade such files
209
- // to a plain "file" attachment so the model gets a path note and inspects
210
- // 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.
211
216
  let prepared = { ...a };
212
217
  if (a.kind === "image") {
213
- const validatedType = await validateVisionImage(buf).catch(() => null);
214
- if (validatedType)
215
- prepared = { ...a, mediaType: validatedType };
216
- 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 {
217
230
  prepared = { ...a, kind: "file" };
231
+ }
218
232
  }
219
233
  try {
220
234
  await fs.writeFile(filePath, buf);
@@ -227,6 +241,9 @@ async function prepareAttachments(cwd, attachments) {
227
241
  return out;
228
242
  }
229
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;
230
247
  /** Score a candidate path against a lowercased query. Higher is better; a
231
248
  * negative score means "no match". Basename hits beat path hits; prefix beats
232
249
  * substring; shorter paths break ties. */
@@ -279,8 +296,13 @@ async function searchProjectFiles(cwd, rawQuery) {
279
296
  }
280
297
  const ig = ignore.default().add(gitignore);
281
298
  // `stats: true` gives mtime without a second stat pass, so the empty-query
282
- // "recent files" path is a single walk.
283
- 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("**/*", {
284
306
  cwd,
285
307
  dot: false,
286
308
  onlyFiles: true,
@@ -289,6 +311,17 @@ async function searchProjectFiles(cwd, rawQuery) {
289
311
  followSymbolicLinks: false,
290
312
  stats: true,
291
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
+ }
292
325
  const files = entries.filter((e) => !ig.ignores(e.path));
293
326
  if (!query) {
294
327
  return files
@@ -443,7 +476,11 @@ async function runJsonModeIfRequested() {
443
476
  maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
444
477
  allowedTools,
445
478
  promptCacheKey: values["prompt-cache-key"],
446
- }).catch((err) => {
479
+ }).catch(async (err) => {
480
+ captureSidecarError(err, "app-sidecar.json-mode", {
481
+ provider: String(values.provider ?? "anthropic"),
482
+ });
483
+ await flushSidecarErrors();
447
484
  process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
448
485
  process.exit(1);
449
486
  });
@@ -452,13 +489,8 @@ async function runJsonModeIfRequested() {
452
489
  // ── Daemon-level HTTP helpers (shared by the session-management routes) ─────
453
490
  // The per-session route table has its own local copies; these serve the
454
491
  // daemon's own POST /session / DELETE /session routes.
455
- function daemonReadBody(req) {
456
- return new Promise((resolve, reject) => {
457
- const chunks = [];
458
- req.on("data", (c) => chunks.push(c));
459
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
460
- req.on("error", reject);
461
- });
492
+ function daemonReadBody(req, res) {
493
+ return readCappedBody(req, res);
462
494
  }
463
495
  function daemonJson(res, status, body) {
464
496
  res.writeHead(status, {
@@ -544,18 +576,24 @@ async function main() {
544
576
  });
545
577
  const usageCache = new Map();
546
578
  const usageRequests = new Map();
547
- // 429 backoff: when the provider rate-limits the usage endpoint, hold the
548
- // error result until this timestamp instead of re-polling (and re-logging a
549
- // WARN) every 60s — the old cadence hammered a limited endpoint for hours.
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.
550
583
  const usageRateLimitedUntil = new Map();
551
- const USAGE_RATE_LIMIT_BACKOFF_MS = 5 * 60_000;
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;
552
587
  async function fetchUsageProvider(provider) {
553
- const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
554
- 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))) {
555
593
  return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
556
594
  }
557
595
  try {
558
- let credentials = await auth.resolveCredentials(provider);
596
+ let credentials = await auth.resolveCredentials(authKey);
559
597
  try {
560
598
  const snapshot = {
561
599
  ...(await fetchSubscriptionUsage(provider, credentials)),
@@ -568,7 +606,7 @@ async function main() {
568
606
  // A provider can revoke an access token before its stored expiry. Refresh
569
607
  // once on 401, matching inference auth recovery, then retry the usage call.
570
608
  if (error instanceof SubscriptionUsageError && error.status === 401) {
571
- credentials = await auth.resolveCredentials(provider, { forceRefresh: true });
609
+ credentials = await auth.resolveCredentials(authKey, { forceRefresh: true });
572
610
  const snapshot = {
573
611
  ...(await fetchSubscriptionUsage(provider, credentials)),
574
612
  connected: true,
@@ -581,11 +619,20 @@ async function main() {
581
619
  }
582
620
  catch (error) {
583
621
  const message = error instanceof Error ? error.message : String(error);
584
- log("WARN", "app-sidecar", "subscription usage fetch failed", { provider, message });
622
+ if (shouldCaptureUsagePollingError(error)) {
623
+ captureSidecarError(error, "app-sidecar.usage.fetch", { provider });
624
+ }
625
+ let backoffMs;
585
626
  if (error instanceof SubscriptionUsageError && error.status === 429) {
586
- usageRateLimitedUntil.set(provider, Date.now() + USAGE_RATE_LIMIT_BACKOFF_MS);
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);
587
629
  }
588
- const connected = await auth.hasProviderAuth(provider);
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);
589
636
  return {
590
637
  provider,
591
638
  displayName,
@@ -640,7 +687,7 @@ async function main() {
640
687
  return null;
641
688
  }
642
689
  }
643
- const server = http.createServer((req, res) => {
690
+ const server = http.createServer(wrapSidecarHandler((req, res) => {
644
691
  const url = req.url ?? "/";
645
692
  const method = req.method ?? "GET";
646
693
  // CORS preflight — the webview origin differs from 127.0.0.1.
@@ -656,7 +703,9 @@ async function main() {
656
703
  // ── Daemon-level routes (session lifecycle) ──────────────────────────
657
704
  // Create a session for a window: { mode?, cwd, sessionPath? } → { sessionId }.
658
705
  if (method === "POST" && url === "/session") {
659
- void daemonReadBody(req).then(async (raw) => {
706
+ void daemonReadBody(req, res).then(async (raw) => {
707
+ if (raw === null)
708
+ return;
660
709
  let body = {};
661
710
  try {
662
711
  body = raw ? JSON.parse(raw) : {};
@@ -684,6 +733,7 @@ async function main() {
684
733
  }
685
734
  catch (err) {
686
735
  const message = err instanceof Error ? err.message : String(err);
736
+ captureSidecarError(err, "app-sidecar.session.create");
687
737
  log("ERROR", "app-sidecar", "session create failed", { message });
688
738
  daemonJson(res, 500, { error: message });
689
739
  }
@@ -714,12 +764,13 @@ async function main() {
714
764
  if (method === "GET" && (url === "/usage" || url.startsWith("/usage?"))) {
715
765
  void (async () => {
716
766
  const provider = new URL(url, `http://${host}`).searchParams.get("provider");
717
- if (provider !== "anthropic" && provider !== "openai") {
767
+ if (provider !== "anthropic" && provider !== "openai" && provider !== "moonshot") {
718
768
  daemonJson(res, 400, { error: "unsupported usage provider" });
719
769
  return;
720
770
  }
721
771
  daemonJson(res, 200, await subscriptionUsage(provider));
722
772
  })().catch((error) => {
773
+ captureSidecarError(error, "app-sidecar.usage.request");
723
774
  log("ERROR", "app-sidecar", "subscription usage request failed", {
724
775
  message: error instanceof Error ? error.message : String(error),
725
776
  });
@@ -735,7 +786,7 @@ async function main() {
735
786
  return;
736
787
  }
737
788
  ctx.handle(req, res, url, method);
738
- });
789
+ }, "app-sidecar.http"));
739
790
  server.listen(port, host, () => {
740
791
  const addr = server.address();
741
792
  // The Rust shell reads this line to learn the daemon port.
@@ -752,6 +803,8 @@ async function main() {
752
803
  // Radio playback is app-wide (one stream across all windows), so it stops
753
804
  // at the daemon level, not per session.
754
805
  stopRadio();
806
+ // Close the ~/.ezcoder progress fs.watch handle (baseline #8 leak fix).
807
+ progress.dispose();
755
808
  await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
756
809
  server.close();
757
810
  process.exit(0);
@@ -880,6 +933,7 @@ async function createProgressManager(agentDir, broadcastAll) {
880
933
  broadcastAll(buildSnapshot(updated), originId);
881
934
  }
882
935
  catch (err) {
936
+ captureSidecarError(err, "app-sidecar.progress.award");
883
937
  log("DEBUG", "app-sidecar", "progress award failed", {
884
938
  message: err instanceof Error ? err.message : String(err),
885
939
  });
@@ -888,6 +942,7 @@ async function createProgressManager(agentDir, broadcastAll) {
888
942
  // Watch ~/.ezcoder (dir watch survives the atomic tmp+rename) for progress.json
889
943
  // writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
890
944
  let watchDebounce = null;
945
+ let progressWatcher = null;
891
946
  try {
892
947
  const watcher = fsWatch(agentDir, (_event, filename) => {
893
948
  if (filename !== "progress.json")
@@ -909,13 +964,30 @@ async function createProgressManager(agentDir, broadcastAll) {
909
964
  }, 150);
910
965
  });
911
966
  watcher.unref();
967
+ progressWatcher = watcher;
912
968
  }
913
969
  catch (err) {
970
+ captureSidecarError(err, "app-sidecar.progress.watch");
914
971
  log("DEBUG", "app-sidecar", "progress watch unavailable", {
915
972
  message: err instanceof Error ? err.message : String(err),
916
973
  });
917
974
  }
918
- 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 };
919
991
  }
920
992
  /**
921
993
  * Build one in-process agent session: its AgentSession, SSE client set, event
@@ -934,6 +1006,10 @@ async function createSession(deps, opts) {
934
1006
  // parsing); the daemon owns the real listen host.
935
1007
  const host = "127.0.0.1";
936
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();
937
1013
  // Per-project model/thinking prefs win over the shared global settings.json:
938
1014
  // each window (one project cwd) restores its own selection instead of every
939
1015
  // window reading the same single global slot that the last writer clobbered
@@ -952,9 +1028,14 @@ async function createSession(deps, opts) {
952
1028
  });
953
1029
  }
954
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.
955
1034
  const thinkEnabled = projectPrefs?.thinkingEnabled ?? saved.thinkingEnabled;
956
1035
  const thinkingLevel = thinkEnabled
957
- ? (projectPrefs?.thinkingLevel ?? saved.thinkingLevel ?? getMaxThinkingLevel(model))
1036
+ ? (projectPrefs?.thinkingLevel ??
1037
+ saved.thinkingLevel ??
1038
+ getDefaultThinkingLevel(model, { baseUrl: auth.getStoredBaseUrl(provider) }))
958
1039
  : undefined;
959
1040
  // ── SSE fan-out (declared before the session so plan callbacks can use it) ─
960
1041
  const clients = new Set();
@@ -1001,6 +1082,11 @@ async function createSession(deps, opts) {
1001
1082
  const f = formatError(err);
1002
1083
  const message = f.message ? desktopGuidance(f.message) : undefined;
1003
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
+ });
1004
1090
  log("ERROR", "app-sidecar", logLabel, {
1005
1091
  headline: f.headline,
1006
1092
  source: f.source,
@@ -1058,6 +1144,7 @@ async function createSession(deps, opts) {
1058
1144
  chatAgent = nextAgent;
1059
1145
  broadcast("chat_agent_change", { chatAgent: nextAgent });
1060
1146
  await session.persistAppMarker("agent_handoff", { chatAgent: nextAgent }).catch((error) => {
1147
+ captureSidecarError(error, "app-sidecar.chat-agent.persist-handoff");
1061
1148
  log("WARN", "app-sidecar", "agent handoff marker persist failed", {
1062
1149
  message: error instanceof Error ? error.message : String(error),
1063
1150
  });
@@ -1184,6 +1271,7 @@ async function createSession(deps, opts) {
1184
1271
  .catch(() => false)
1185
1272
  .then(() => syncApprovedPlanProgress(generation))
1186
1273
  .catch((error) => {
1274
+ captureSidecarError(error, "app-sidecar.plan.refresh-progress");
1187
1275
  log("WARN", "app-sidecar", "plan progress refresh failed", {
1188
1276
  message: error instanceof Error ? error.message : String(error),
1189
1277
  });
@@ -1238,6 +1326,11 @@ async function createSession(deps, opts) {
1238
1326
  session.eventBus.on("tool_call_end", (d) => {
1239
1327
  const name = toolCallNames.get(d.toolCallId) ?? "unknown";
1240
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
+ }
1241
1334
  log(d.isError ? "ERROR" : "INFO", "tool", `Tool call ended: ${name}`, {
1242
1335
  id: d.toolCallId,
1243
1336
  durationMs: String(d.durationMs),
@@ -1264,6 +1357,9 @@ async function createSession(deps, opts) {
1264
1357
  session.eventBus.on("server_tool_call", (d) => broadcast("server_tool_call", d));
1265
1358
  session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
1266
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));
1267
1363
  session.eventBus.on("error", (d) => {
1268
1364
  broadcastError("error", "agent error", d.error);
1269
1365
  });
@@ -1584,6 +1680,7 @@ async function createSession(deps, opts) {
1584
1680
  catch (error) {
1585
1681
  // Keep tracking when prompt cleanup fails; hiding the widget here
1586
1682
  // would claim completion while the approved-plan contract remained.
1683
+ captureSidecarError(error, "app-sidecar.plan.cleanup");
1587
1684
  log("WARN", "app-sidecar", "completed plan cleanup failed", {
1588
1685
  message: error instanceof Error ? error.message : String(error),
1589
1686
  });
@@ -2039,13 +2136,8 @@ async function createSession(deps, opts) {
2039
2136
  gitPoll.unref?.();
2040
2137
  };
2041
2138
  scheduleGitPoll(5000);
2042
- function readBody(req) {
2043
- return new Promise((resolve, reject) => {
2044
- const chunks = [];
2045
- req.on("data", (c) => chunks.push(c));
2046
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
2047
- req.on("error", reject);
2048
- });
2139
+ function readBody(req, res) {
2140
+ return readCappedBody(req, res);
2049
2141
  }
2050
2142
  function json(res, status, body) {
2051
2143
  const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
@@ -2083,7 +2175,10 @@ async function createSession(deps, opts) {
2083
2175
  void memoryStore
2084
2176
  .snapshot()
2085
2177
  .then((snapshot) => json(res, 200, snapshot))
2086
- .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
+ });
2087
2182
  return;
2088
2183
  }
2089
2184
  if (method === "DELETE" && url.startsWith("/memories/")) {
@@ -2096,14 +2191,20 @@ async function createSession(deps, opts) {
2096
2191
  .forget(id)
2097
2192
  .then(() => memoryStore.snapshot())
2098
2193
  .then((snapshot) => json(res, 200, snapshot))
2099
- .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
+ });
2100
2198
  return;
2101
2199
  }
2102
2200
  if (method === "GET" && url === "/jiwa") {
2103
2201
  void jiwaStore
2104
2202
  .snapshot()
2105
2203
  .then((snapshot) => json(res, 200, snapshot))
2106
- .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
+ });
2107
2208
  return;
2108
2209
  }
2109
2210
  if (method === "DELETE" && url.startsWith("/jiwa/")) {
@@ -2116,7 +2217,10 @@ async function createSession(deps, opts) {
2116
2217
  .forget(id)
2117
2218
  .then(() => jiwaStore.snapshot())
2118
2219
  .then((snapshot) => json(res, 200, snapshot))
2119
- .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
+ });
2120
2224
  return;
2121
2225
  }
2122
2226
  if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
@@ -2174,7 +2278,9 @@ async function createSession(deps, opts) {
2174
2278
  return;
2175
2279
  }
2176
2280
  if (method === "POST" && url === "/settings") {
2177
- void readBody(req).then(async (raw) => {
2281
+ void readBody(req, res).then(async (raw) => {
2282
+ if (raw === null)
2283
+ return;
2178
2284
  let projectsRoot;
2179
2285
  try {
2180
2286
  projectsRoot = JSON.parse(raw).projectsRoot ?? "";
@@ -2197,7 +2303,9 @@ async function createSession(deps, opts) {
2197
2303
  return;
2198
2304
  }
2199
2305
  if (method === "POST" && url === "/create-project") {
2200
- void readBody(req).then(async (raw) => {
2306
+ void readBody(req, res).then(async (raw) => {
2307
+ if (raw === null)
2308
+ return;
2201
2309
  let name;
2202
2310
  try {
2203
2311
  name = JSON.parse(raw).name ?? "";
@@ -2229,6 +2337,7 @@ async function createSession(deps, opts) {
2229
2337
  json(res, 200, { path: dir });
2230
2338
  }
2231
2339
  catch (err) {
2340
+ captureSidecarError(err, "app-sidecar.project.create");
2232
2341
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
2233
2342
  }
2234
2343
  });
@@ -2239,6 +2348,7 @@ async function createSession(deps, opts) {
2239
2348
  void discoverProjects()
2240
2349
  .then((projects) => json(res, 200, { projects }))
2241
2350
  .catch((err) => {
2351
+ captureSidecarError(err, "app-sidecar.projects.discover");
2242
2352
  log("ERROR", "app-sidecar", "discoverProjects failed", {
2243
2353
  message: err instanceof Error ? err.message : String(err),
2244
2354
  });
@@ -2253,37 +2363,14 @@ async function createSession(deps, opts) {
2253
2363
  return;
2254
2364
  }
2255
2365
  const requestedAgent = new URL(url, `http://${host}`).searchParams.get("chatAgent");
2256
- if (requestedAgent === "all") {
2257
- void Promise.all(CHAT_AGENT_IDS.map(async (agentId) => {
2258
- const agentSessions = await listRecentSessions(target, 5, chatAgentSessionsDir(paths.sessionsDir, agentId));
2259
- return agentSessions.map((item) => ({ ...item, chatAgent: agentId }));
2260
- }))
2261
- .then(async (groups) => {
2262
- const dated = await Promise.all(groups.flat().map(async (item) => ({
2263
- item,
2264
- mtime: await fs
2265
- .stat(item.path)
2266
- .then((stat) => stat.mtimeMs)
2267
- .catch(() => 0),
2268
- })));
2269
- const recent = dated
2270
- .sort((left, right) => right.mtime - left.mtime)
2271
- .slice(0, 5)
2272
- .map(({ item }) => item);
2273
- json(res, 200, { sessions: recent });
2274
- })
2275
- .catch(() => json(res, 200, { sessions: [] }));
2276
- return;
2277
- }
2278
- // The picker may be served by a sidecar currently running in Chat mode.
2279
- // An omitted chatAgent always means coding history; chat callers identify
2280
- // their namespace explicitly (one agent or "all").
2281
- const sessionsDir = requestedAgent
2282
- ? sessionsDirForChatAgent(paths.sessionsDir, requestedAgent)
2283
- : paths.sessionsDir;
2284
- 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)
2285
2369
  .then((sessions) => json(res, 200, { sessions }))
2286
- .catch(() => json(res, 200, { sessions: [] }));
2370
+ .catch((error) => {
2371
+ captureSidecarError(error, "app-sidecar.sessions.list");
2372
+ json(res, 200, { sessions: [] });
2373
+ });
2287
2374
  return;
2288
2375
  }
2289
2376
  if (method === "GET" && url.startsWith("/files")) {
@@ -2291,6 +2378,7 @@ async function createSession(deps, opts) {
2291
2378
  void searchProjectFiles(cwd, q)
2292
2379
  .then((files) => json(res, 200, { files }))
2293
2380
  .catch((err) => {
2381
+ captureSidecarError(err, "app-sidecar.files.search");
2294
2382
  log("ERROR", "app-sidecar", "searchProjectFiles failed", {
2295
2383
  message: err instanceof Error ? err.message : String(err),
2296
2384
  });
@@ -2631,7 +2719,9 @@ async function createSession(deps, opts) {
2631
2719
  return;
2632
2720
  }
2633
2721
  if (method === "POST" && url === "/prompt") {
2634
- void readBody(req).then(async (raw) => {
2722
+ void readBody(req, res).then(async (raw) => {
2723
+ if (raw === null)
2724
+ return;
2635
2725
  let text;
2636
2726
  let attachments;
2637
2727
  let meta;
@@ -2758,7 +2848,9 @@ async function createSession(deps, opts) {
2758
2848
  json(res, 404, { error: "Nolan is not available in EZ Chat." });
2759
2849
  return;
2760
2850
  }
2761
- void readBody(req).then(async (raw) => {
2851
+ void readBody(req, res).then(async (raw) => {
2852
+ if (raw === null)
2853
+ return;
2762
2854
  let text;
2763
2855
  try {
2764
2856
  text = JSON.parse(raw).text ?? "";
@@ -2817,7 +2909,9 @@ async function createSession(deps, opts) {
2817
2909
  json(res, 404, { error: "Autopilot is not available in EZ Chat." });
2818
2910
  return;
2819
2911
  }
2820
- void readBody(req).then(async (raw) => {
2912
+ void readBody(req, res).then(async (raw) => {
2913
+ if (raw === null)
2914
+ return;
2821
2915
  let enabled;
2822
2916
  try {
2823
2917
  enabled = Boolean(JSON.parse(raw).enabled);
@@ -2838,7 +2932,9 @@ async function createSession(deps, opts) {
2838
2932
  return;
2839
2933
  }
2840
2934
  if (method === "POST" && url === "/enhance") {
2841
- void readBody(req).then(async (raw) => {
2935
+ void readBody(req, res).then(async (raw) => {
2936
+ if (raw === null)
2937
+ return;
2842
2938
  let text;
2843
2939
  try {
2844
2940
  text = JSON.parse(raw).text ?? "";
@@ -2859,6 +2955,7 @@ async function createSession(deps, opts) {
2859
2955
  }
2860
2956
  catch (err) {
2861
2957
  const message = err instanceof Error ? err.message : String(err);
2958
+ captureSidecarError(err, "app-sidecar.prompt-enhancer");
2862
2959
  log("ERROR", "app-sidecar", "enhance failed", { message });
2863
2960
  json(res, 500, { error: message });
2864
2961
  }
@@ -2886,7 +2983,9 @@ async function createSession(deps, opts) {
2886
2983
  return;
2887
2984
  }
2888
2985
  if (method === "POST" && url === "/radio/volume") {
2889
- void readBody(req).then((raw) => {
2986
+ void readBody(req, res).then((raw) => {
2987
+ if (raw === null)
2988
+ return;
2890
2989
  let volume;
2891
2990
  try {
2892
2991
  volume = Number(JSON.parse(raw).volume);
@@ -2909,7 +3008,9 @@ async function createSession(deps, opts) {
2909
3008
  return;
2910
3009
  }
2911
3010
  if (method === "POST" && url === "/radio") {
2912
- void readBody(req).then((raw) => {
3011
+ void readBody(req, res).then((raw) => {
3012
+ if (raw === null)
3013
+ return;
2913
3014
  let station;
2914
3015
  try {
2915
3016
  station = JSON.parse(raw).station ?? "";
@@ -2933,7 +3034,9 @@ async function createSession(deps, opts) {
2933
3034
  return;
2934
3035
  }
2935
3036
  if (method === "POST" && url === "/tasks/run") {
2936
- void readBody(req).then((raw) => {
3037
+ void readBody(req, res).then((raw) => {
3038
+ if (raw === null)
3039
+ return;
2937
3040
  let id;
2938
3041
  let all;
2939
3042
  try {
@@ -2955,7 +3058,9 @@ async function createSession(deps, opts) {
2955
3058
  return;
2956
3059
  }
2957
3060
  if (method === "POST" && url === "/tasks/add") {
2958
- void readBody(req).then((raw) => {
3061
+ void readBody(req, res).then((raw) => {
3062
+ if (raw === null)
3063
+ return;
2959
3064
  let title;
2960
3065
  let prompt;
2961
3066
  try {
@@ -2987,7 +3092,9 @@ async function createSession(deps, opts) {
2987
3092
  return;
2988
3093
  }
2989
3094
  if (method === "POST" && url === "/tasks/update") {
2990
- void readBody(req).then((raw) => {
3095
+ void readBody(req, res).then((raw) => {
3096
+ if (raw === null)
3097
+ return;
2991
3098
  let id;
2992
3099
  let status;
2993
3100
  let title;
@@ -3032,7 +3139,9 @@ async function createSession(deps, opts) {
3032
3139
  return;
3033
3140
  }
3034
3141
  if (method === "POST" && url === "/tasks/delete") {
3035
- void readBody(req).then((raw) => {
3142
+ void readBody(req, res).then((raw) => {
3143
+ if (raw === null)
3144
+ return;
3036
3145
  let id;
3037
3146
  try {
3038
3147
  id = JSON.parse(raw).id ?? "";
@@ -3075,7 +3184,9 @@ async function createSession(deps, opts) {
3075
3184
  return;
3076
3185
  }
3077
3186
  if (method === "POST" && url === "/model") {
3078
- void readBody(req).then(async (raw) => {
3187
+ void readBody(req, res).then(async (raw) => {
3188
+ if (raw === null)
3189
+ return;
3079
3190
  let modelId;
3080
3191
  try {
3081
3192
  modelId = JSON.parse(raw).model ?? "";
@@ -3147,7 +3258,9 @@ async function createSession(deps, opts) {
3147
3258
  // to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
3148
3259
  // either is mid-run defers via the pending-model mechanics.
3149
3260
  if (method === "POST" && url === "/ken/model") {
3150
- void readBody(req).then(async (raw) => {
3261
+ void readBody(req, res).then(async (raw) => {
3262
+ if (raw === null)
3263
+ return;
3151
3264
  let modelId;
3152
3265
  try {
3153
3266
  const parsed = JSON.parse(raw).model;
@@ -3191,7 +3304,9 @@ async function createSession(deps, opts) {
3191
3304
  return;
3192
3305
  }
3193
3306
  if (method === "POST" && url === "/kill") {
3194
- void readBody(req).then(async (raw) => {
3307
+ void readBody(req, res).then(async (raw) => {
3308
+ if (raw === null)
3309
+ return;
3195
3310
  let id;
3196
3311
  try {
3197
3312
  id = JSON.parse(raw).id ?? "";
@@ -3269,6 +3384,7 @@ async function createSession(deps, opts) {
3269
3384
  drained,
3270
3385
  });
3271
3386
  })().catch((error) => {
3387
+ captureSidecarError(error, "app-sidecar.run.cancel");
3272
3388
  broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
3273
3389
  json(res, 500, {
3274
3390
  error: "cancel_failed",
@@ -3296,6 +3412,7 @@ async function createSession(deps, opts) {
3296
3412
  json(res, 200, { ok: true });
3297
3413
  })
3298
3414
  .catch((err) => {
3415
+ captureSidecarError(err, "app-sidecar.session.new");
3299
3416
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
3300
3417
  });
3301
3418
  return;
@@ -3310,7 +3427,9 @@ async function createSession(deps, opts) {
3310
3427
  // 3. session_reset tells the webview to clear its transcript; it then runs
3311
3428
  // the "implement it now" prompt in the clean session.
3312
3429
  if (method === "POST" && url === "/plan/accept") {
3313
- void readBody(req).then(async (raw) => {
3430
+ void readBody(req, res).then(async (raw) => {
3431
+ if (raw === null)
3432
+ return;
3314
3433
  let planPath;
3315
3434
  try {
3316
3435
  planPath = JSON.parse(raw).planPath || undefined;
@@ -3350,6 +3469,7 @@ async function createSession(deps, opts) {
3350
3469
  json(res, 200, { ok: true, planTotal });
3351
3470
  }
3352
3471
  catch (err) {
3472
+ captureSidecarError(err, "app-sidecar.plan.accept");
3353
3473
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
3354
3474
  }
3355
3475
  });
@@ -3361,7 +3481,9 @@ async function createSession(deps, opts) {
3361
3481
  return;
3362
3482
  }
3363
3483
  if (method === "POST" && url === "/auth/apikey") {
3364
- void readBody(req).then(async (raw) => {
3484
+ void readBody(req, res).then(async (raw) => {
3485
+ if (raw === null)
3486
+ return;
3365
3487
  let provider = "";
3366
3488
  let key;
3367
3489
  let variant;
@@ -3404,7 +3526,9 @@ async function createSession(deps, opts) {
3404
3526
  return;
3405
3527
  }
3406
3528
  if (method === "POST" && url === "/auth/oauth/start") {
3407
- void readBody(req).then((raw) => {
3529
+ void readBody(req, res).then((raw) => {
3530
+ if (raw === null)
3531
+ return;
3408
3532
  let provider = "";
3409
3533
  try {
3410
3534
  provider = JSON.parse(raw).provider ?? "";
@@ -3446,6 +3570,7 @@ async function createSession(deps, opts) {
3446
3570
  broadcast("auth_done", { provider });
3447
3571
  }
3448
3572
  catch (err) {
3573
+ captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
3449
3574
  broadcast("auth_error", {
3450
3575
  provider,
3451
3576
  message: err instanceof Error ? err.message : String(err),
@@ -3460,7 +3585,9 @@ async function createSession(deps, opts) {
3460
3585
  return;
3461
3586
  }
3462
3587
  if (method === "POST" && url === "/auth/oauth/code") {
3463
- void readBody(req).then((raw) => {
3588
+ void readBody(req, res).then((raw) => {
3589
+ if (raw === null)
3590
+ return;
3464
3591
  let code;
3465
3592
  try {
3466
3593
  code = JSON.parse(raw).code ?? "";
@@ -3480,7 +3607,9 @@ async function createSession(deps, opts) {
3480
3607
  return;
3481
3608
  }
3482
3609
  if (method === "POST" && url === "/auth/logout") {
3483
- void readBody(req).then(async (raw) => {
3610
+ void readBody(req, res).then(async (raw) => {
3611
+ if (raw === null)
3612
+ return;
3484
3613
  let provider;
3485
3614
  try {
3486
3615
  provider = JSON.parse(raw).provider ?? "";
@@ -3519,7 +3648,9 @@ async function createSession(deps, opts) {
3519
3648
  return;
3520
3649
  }
3521
3650
  if (method === "POST" && url === "/telegram") {
3522
- void readBody(req).then(async (raw) => {
3651
+ void readBody(req, res).then(async (raw) => {
3652
+ if (raw === null)
3653
+ return;
3523
3654
  let botTokenInput;
3524
3655
  let userIdInput;
3525
3656
  try {
@@ -3587,6 +3718,7 @@ async function createSession(deps, opts) {
3587
3718
  }
3588
3719
  catch (err) {
3589
3720
  serveController = null;
3721
+ captureSidecarError(err, "app-sidecar.serve.start", { provider: st.provider });
3590
3722
  json(res, 400, { error: err instanceof Error ? err.message : String(err) });
3591
3723
  }
3592
3724
  })();
@@ -3613,6 +3745,7 @@ async function createSession(deps, opts) {
3613
3745
  void buildMcpRows(targetCwd)
3614
3746
  .then((servers) => json(res, 200, { servers }))
3615
3747
  .catch((err) => {
3748
+ captureSidecarError(err, "app-sidecar.mcp.list");
3616
3749
  log("ERROR", "app-sidecar", "buildMcpRows failed", {
3617
3750
  message: err instanceof Error ? err.message : String(err),
3618
3751
  });
@@ -3621,7 +3754,9 @@ async function createSession(deps, opts) {
3621
3754
  return;
3622
3755
  }
3623
3756
  if (method === "POST" && url === "/mcp/add") {
3624
- void readBody(req).then(async (raw) => {
3757
+ void readBody(req, res).then(async (raw) => {
3758
+ if (raw === null)
3759
+ return;
3625
3760
  let line;
3626
3761
  let scopeValue;
3627
3762
  let bodyCwd;
@@ -3669,6 +3804,7 @@ async function createSession(deps, opts) {
3669
3804
  });
3670
3805
  }
3671
3806
  catch (err) {
3807
+ captureSidecarError(err, "app-sidecar.mcp.add", { server: config.name });
3672
3808
  json(res, 500, {
3673
3809
  error: err instanceof Error ? err.message : String(err),
3674
3810
  });
@@ -3677,7 +3813,9 @@ async function createSession(deps, opts) {
3677
3813
  return;
3678
3814
  }
3679
3815
  if (method === "POST" && url === "/mcp/remove") {
3680
- void readBody(req).then(async (raw) => {
3816
+ void readBody(req, res).then(async (raw) => {
3817
+ if (raw === null)
3818
+ return;
3681
3819
  let name;
3682
3820
  let scopeValue;
3683
3821
  let bodyCwd;
@@ -3714,7 +3852,9 @@ async function createSession(deps, opts) {
3714
3852
  // `mcp_auth_error`. Responds 202 immediately and runs the flow in the
3715
3853
  // background (the browser round-trip can take a while).
3716
3854
  if (method === "POST" && url === "/mcp/login") {
3717
- void readBody(req).then(async (raw) => {
3855
+ void readBody(req, res).then(async (raw) => {
3856
+ if (raw === null)
3857
+ return;
3718
3858
  let name;
3719
3859
  let scopeValue;
3720
3860
  let bodyCwd;
@@ -3758,6 +3898,7 @@ async function createSession(deps, opts) {
3758
3898
  }
3759
3899
  }
3760
3900
  catch (err) {
3901
+ captureSidecarError(err, "app-sidecar.mcp.login", { server: name });
3761
3902
  broadcast("mcp_auth_error", {
3762
3903
  name,
3763
3904
  message: err instanceof Error ? err.message : String(err),
@@ -3804,7 +3945,9 @@ async function createSession(deps, opts) {
3804
3945
  dispose,
3805
3946
  };
3806
3947
  }
3807
- main().catch((err) => {
3948
+ main().catch(async (err) => {
3949
+ captureSidecarError(err, "app-sidecar.main", { severity: "fatal" });
3950
+ await flushSidecarErrors();
3808
3951
  const message = err instanceof Error ? err.message : String(err);
3809
3952
  process.stderr.write(`GG_APP_FATAL ${message}\n`);
3810
3953
  process.exit(1);