@vellumai/assistant 0.8.7 → 0.8.8

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 (387) hide show
  1. package/Dockerfile +20 -4
  2. package/docker-entrypoint.sh +4 -2
  3. package/docker-init-apt-root.sh +3 -1
  4. package/docker-kata-apt-env.sh +3 -1
  5. package/docker-kata-runtime-family.sh +12 -0
  6. package/docs/architecture/memory.md +1 -1
  7. package/docs/plugins.md +75 -79
  8. package/examples/plugins/echo/README.md +6 -12
  9. package/examples/plugins/echo/register.ts +0 -41
  10. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  11. package/openapi.yaml +3381 -348
  12. package/package.json +1 -1
  13. package/scripts/generate-openapi.ts +68 -41
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
  15. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  16. package/src/__tests__/agent-loop.test.ts +37 -87
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  18. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  19. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  20. package/src/__tests__/anthropic-provider.test.ts +95 -2
  21. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  22. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  23. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  24. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  25. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  26. package/src/__tests__/btw-routes.test.ts +62 -3
  27. package/src/__tests__/build-persisted-content.test.ts +184 -0
  28. package/src/__tests__/catalog-files.test.ts +1 -1
  29. package/src/__tests__/clawhub-files.test.ts +1 -1
  30. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  32. package/src/__tests__/config-watcher.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
  37. package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
  38. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  39. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  40. package/src/__tests__/conversation-pairing.test.ts +4 -31
  41. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
  43. package/src/__tests__/conversation-queue.test.ts +2 -0
  44. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
  48. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  49. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  50. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  51. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  52. package/src/__tests__/conversation-title-service.test.ts +135 -2
  53. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  54. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  55. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  56. package/src/__tests__/dm-persistence.test.ts +5 -1
  57. package/src/__tests__/empty-response-hook.test.ts +304 -0
  58. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  59. package/src/__tests__/gemini-image-service.test.ts +13 -0
  60. package/src/__tests__/helpers/mock-provider.ts +110 -0
  61. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  62. package/src/__tests__/history-repair-hook.test.ts +1 -0
  63. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  64. package/src/__tests__/identity-routes.test.ts +248 -7
  65. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  66. package/src/__tests__/injector-background-turn.test.ts +2 -8
  67. package/src/__tests__/injector-chain.test.ts +106 -270
  68. package/src/__tests__/injector-disk-pressure.test.ts +3 -12
  69. package/src/__tests__/injector-document-comments.test.ts +2 -2
  70. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  71. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  72. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  73. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  74. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  75. package/src/__tests__/llm-usage-store.test.ts +223 -1
  76. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  77. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  78. package/src/__tests__/native-web-search.test.ts +191 -0
  79. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  80. package/src/__tests__/openai-image-service.test.ts +17 -0
  81. package/src/__tests__/openai-provider.test.ts +31 -1
  82. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  83. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  84. package/src/__tests__/pipeline-runner.test.ts +29 -39
  85. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  86. package/src/__tests__/plugin-bootstrap.test.ts +13 -28
  87. package/src/__tests__/plugin-registry.test.ts +0 -27
  88. package/src/__tests__/plugin-types.test.ts +2 -125
  89. package/src/__tests__/process-message-display-content.test.ts +6 -2
  90. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  91. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  92. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  93. package/src/__tests__/schedule-routes.test.ts +603 -2
  94. package/src/__tests__/schedule-store.test.ts +41 -0
  95. package/src/__tests__/schedule-tools.test.ts +35 -0
  96. package/src/__tests__/server-history-render.test.ts +314 -1
  97. package/src/__tests__/skillssh-files.test.ts +1 -1
  98. package/src/__tests__/system-prompt.test.ts +20 -0
  99. package/src/__tests__/task-scheduler.test.ts +162 -1
  100. package/src/__tests__/terminal-tools.test.ts +6 -1
  101. package/src/__tests__/title-generate-hook.test.ts +319 -0
  102. package/src/__tests__/tool-error-hook.test.ts +278 -0
  103. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  104. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  106. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  107. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  108. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  109. package/src/__tests__/usage-routes.test.ts +285 -1
  110. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  111. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  112. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  113. package/src/agent/loop.ts +346 -442
  114. package/src/api/events/assistant-thinking-delta.ts +33 -0
  115. package/src/api/events/tool-output-chunk.ts +45 -0
  116. package/src/api/events/tool-use-preview-start.ts +32 -0
  117. package/src/api/events/trace-event.ts +69 -0
  118. package/src/api/index.ts +48 -13
  119. package/src/api/responses/conversation-message.ts +368 -0
  120. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  121. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  122. package/src/cli/commands/notifications.ts +112 -60
  123. package/src/config/assistant-feature-flags.ts +22 -11
  124. package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
  125. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  126. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  127. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  128. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  129. package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
  130. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  131. package/src/config/feature-flag-cache.ts +3 -3
  132. package/src/config/feature-flag-registry.json +35 -3
  133. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  134. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  135. package/src/config/schemas/llm.ts +1 -0
  136. package/src/config/schemas/memory-v2.ts +8 -0
  137. package/src/config/schemas/memory-v3.ts +8 -0
  138. package/src/config/schemas/platform.ts +8 -0
  139. package/src/config/seed-inference-profiles.ts +2 -2
  140. package/src/config/skills.ts +13 -0
  141. package/src/context/compactor.ts +1 -1
  142. package/src/context/strip-injections.ts +122 -0
  143. package/src/context/token-estimator.ts +23 -0
  144. package/src/context/tool-result-truncation.ts +0 -23
  145. package/src/context/window-manager.ts +3 -6
  146. package/src/credential-execution/executable-discovery.ts +16 -0
  147. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  148. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  149. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  150. package/src/daemon/assistant-attachments.ts +1 -1
  151. package/src/daemon/config-watcher.ts +2 -2
  152. package/src/daemon/context-overflow-reducer.ts +0 -1
  153. package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
  154. package/src/daemon/conversation-agent-loop.ts +281 -760
  155. package/src/daemon/conversation-history.ts +5 -4
  156. package/src/daemon/conversation-lifecycle.ts +3 -4
  157. package/src/daemon/conversation-messaging.ts +7 -6
  158. package/src/daemon/conversation-process.ts +11 -16
  159. package/src/daemon/conversation-runtime-assembly.ts +130 -347
  160. package/src/daemon/conversation-slash.ts +6 -25
  161. package/src/daemon/conversation-surfaces.ts +222 -4
  162. package/src/daemon/conversation-tool-setup.ts +2 -29
  163. package/src/daemon/conversation.ts +32 -14
  164. package/src/daemon/external-plugins-bootstrap.ts +9 -10
  165. package/src/daemon/handlers/config-a2a.ts +51 -36
  166. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  167. package/src/daemon/handlers/config-telegram.ts +16 -2
  168. package/src/daemon/handlers/shared.ts +156 -84
  169. package/src/daemon/handlers/skills.ts +39 -10
  170. package/src/daemon/lifecycle.ts +4 -0
  171. package/src/daemon/message-types/apps.ts +1 -29
  172. package/src/daemon/message-types/messages.ts +9 -57
  173. package/src/daemon/message-types/skills.ts +2 -0
  174. package/src/daemon/message-types/surfaces.ts +136 -3
  175. package/src/daemon/now-scratchpad.ts +21 -0
  176. package/src/daemon/orphan-reaper.test.ts +210 -0
  177. package/src/daemon/orphan-reaper.ts +240 -0
  178. package/src/daemon/persist-unsendable-image.ts +117 -0
  179. package/src/daemon/process-message.ts +1 -3
  180. package/src/daemon/trace-emitter.ts +6 -4
  181. package/src/daemon/trust-context.ts +19 -0
  182. package/src/daemon/wake-target-adapter.ts +3 -1
  183. package/src/home/home-greeting-cache.ts +24 -1
  184. package/src/ipc/gateway-client.test.ts +2 -2
  185. package/src/ipc/gateway-client.ts +3 -3
  186. package/src/media/gemini-image-service.ts +15 -0
  187. package/src/media/openai-image-service.ts +14 -0
  188. package/src/media/types.ts +34 -0
  189. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  190. package/src/memory/auth-fallback-events-store.ts +94 -0
  191. package/src/memory/conversation-title-service.ts +65 -41
  192. package/src/memory/db-init.ts +4 -0
  193. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  194. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  195. package/src/memory/jobs-store.ts +33 -0
  196. package/src/memory/jobs-worker.ts +31 -4
  197. package/src/memory/llm-usage-store.ts +224 -50
  198. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  199. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  200. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  201. package/src/memory/migrations/index.ts +2 -0
  202. package/src/memory/pkb/autoinject.ts +61 -0
  203. package/src/memory/pkb/context.ts +50 -0
  204. package/src/memory/pkb/types.ts +14 -0
  205. package/src/memory/schedule-attribution-sql.ts +104 -0
  206. package/src/memory/schema/infrastructure.ts +16 -0
  207. package/src/memory/usage-grouped-buckets.ts +6 -1
  208. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
  209. package/src/memory/v2/consolidation-job.ts +1 -1
  210. package/src/memory/v3/__tests__/health.test.ts +16 -0
  211. package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
  212. package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
  213. package/src/memory/v3/__tests__/router.test.ts +101 -29
  214. package/src/memory/v3/__tests__/selector.test.ts +93 -27
  215. package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
  216. package/src/memory/v3/health.ts +0 -0
  217. package/src/memory/v3/llm-retry.ts +32 -0
  218. package/src/memory/v3/orchestrate.ts +26 -14
  219. package/src/memory/v3/provider-blocks.ts +15 -5
  220. package/src/memory/v3/router.ts +48 -42
  221. package/src/memory/v3/selector.ts +57 -42
  222. package/src/memory/v3/shadow-plugin.ts +47 -15
  223. package/src/memory/v3/types.ts +8 -0
  224. package/src/notifications/conversation-pairing.ts +8 -15
  225. package/src/notifications/decision-engine.ts +6 -3
  226. package/src/notifications/home-feed-side-effect.ts +12 -1
  227. package/src/permissions/prompter.ts +4 -0
  228. package/src/plugin-api/constants.ts +4 -0
  229. package/src/plugin-api/index.ts +8 -1
  230. package/src/plugin-api/types.ts +151 -1
  231. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  232. package/src/plugins/defaults/empty-response/register.ts +8 -13
  233. package/src/plugins/defaults/index.ts +1 -15
  234. package/src/plugins/defaults/injectors/register.ts +243 -74
  235. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
  236. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  237. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  238. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  239. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  240. package/src/plugins/defaults/title-generate/package.json +1 -1
  241. package/src/plugins/defaults/title-generate/register.ts +18 -18
  242. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  243. package/src/plugins/defaults/tool-error/package.json +1 -1
  244. package/src/plugins/defaults/tool-error/register.ts +9 -21
  245. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  246. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  247. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  248. package/src/plugins/pipeline.ts +6 -18
  249. package/src/plugins/registry.ts +8 -25
  250. package/src/plugins/types.ts +43 -474
  251. package/src/proactive-artifact/aux-message-injector.ts +3 -3
  252. package/src/proactive-artifact/job.test.ts +7 -12
  253. package/src/prompts/__tests__/system-prompt.test.ts +36 -0
  254. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
  255. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  256. package/src/prompts/templates/system-sections.ts +15 -0
  257. package/src/providers/anthropic/client.ts +37 -29
  258. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  259. package/src/providers/openai/chat-completions-provider.ts +44 -0
  260. package/src/providers/openrouter/client.ts +1 -0
  261. package/src/providers/placeholder-sentinels.ts +35 -0
  262. package/src/runtime/__tests__/agent-wake.test.ts +5 -1
  263. package/src/runtime/agent-wake.ts +2 -2
  264. package/src/runtime/assistant-event-hub.ts +36 -6
  265. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  266. package/src/runtime/http-router.ts +16 -21
  267. package/src/runtime/http-types.ts +16 -70
  268. package/src/runtime/pending-interactions.ts +1 -0
  269. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  270. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  271. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  272. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  273. package/src/runtime/routes/app-management-routes.ts +6 -117
  274. package/src/runtime/routes/app-routes.ts +13 -15
  275. package/src/runtime/routes/attachment-routes.ts +26 -15
  276. package/src/runtime/routes/avatar-routes.ts +26 -0
  277. package/src/runtime/routes/btw-routes.ts +29 -23
  278. package/src/runtime/routes/consolidation-routes.ts +120 -20
  279. package/src/runtime/routes/conversation-query-routes.ts +2 -0
  280. package/src/runtime/routes/conversation-routes.ts +358 -184
  281. package/src/runtime/routes/documents-routes.ts +4 -0
  282. package/src/runtime/routes/domain-routes.ts +51 -37
  283. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  284. package/src/runtime/routes/events-routes.ts +28 -34
  285. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  286. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  287. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  288. package/src/runtime/routes/identity-routes.ts +208 -17
  289. package/src/runtime/routes/image-generation-routes.ts +40 -2
  290. package/src/runtime/routes/index.ts +2 -0
  291. package/src/runtime/routes/integrations/a2a.ts +12 -10
  292. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  293. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  294. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  295. package/src/runtime/routes/integrations/telegram.ts +6 -0
  296. package/src/runtime/routes/integrations/twilio.ts +42 -0
  297. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  298. package/src/runtime/routes/log-export-routes.ts +8 -0
  299. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  300. package/src/runtime/routes/memory-v3-routes.ts +50 -28
  301. package/src/runtime/routes/oauth-apps.ts +66 -12
  302. package/src/runtime/routes/oauth-providers.ts +44 -5
  303. package/src/runtime/routes/platform-routes.ts +81 -5
  304. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  305. package/src/runtime/routes/playground/force-compact.ts +1 -1
  306. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  307. package/src/runtime/routes/schedule-routes.ts +152 -42
  308. package/src/runtime/routes/secret-routes.ts +14 -2
  309. package/src/runtime/routes/skills-routes.ts +43 -14
  310. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  311. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  312. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  313. package/src/runtime/routes/tts-routes.ts +35 -0
  314. package/src/runtime/routes/types.ts +66 -8
  315. package/src/runtime/routes/usage-routes.ts +47 -39
  316. package/src/runtime/routes/webhook-routes.ts +41 -2
  317. package/src/runtime/routes/workspace-routes.ts +4 -0
  318. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  319. package/src/runtime/services/analyze-conversation.ts +2 -2
  320. package/src/schedule/schedule-store.ts +20 -1
  321. package/src/schedule/schedule-usage-store.ts +83 -0
  322. package/src/schedule/scheduler.ts +12 -5
  323. package/src/skills/catalog-files.ts +2 -2
  324. package/src/skills/catalog-install.ts +3 -0
  325. package/src/skills/categories-cache.ts +118 -0
  326. package/src/skills/clawhub-files.ts +1 -2
  327. package/src/skills/skillssh-files.ts +1 -2
  328. package/src/telemetry/types.ts +29 -1
  329. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  330. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  331. package/src/tools/executor.ts +1 -53
  332. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  333. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  334. package/src/tools/network/web-search-error.test.ts +248 -0
  335. package/src/tools/network/web-search-error.ts +267 -0
  336. package/src/tools/network/web-search.ts +207 -48
  337. package/src/tools/schedule/create.ts +2 -0
  338. package/src/tools/terminal/safe-env.ts +10 -1
  339. package/src/tools/ui-surface/definitions.ts +9 -1
  340. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  341. package/src/tts/provider-catalog.ts +76 -1
  342. package/src/util/mutex.ts +47 -0
  343. package/src/workspace/git-service.ts +1 -42
  344. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  345. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  346. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
  347. package/src/workspace/migrations/registry.ts +6 -0
  348. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  349. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  350. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  351. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  352. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  353. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  354. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  355. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  356. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  357. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  358. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  359. package/src/gallery/default-gallery.ts +0 -1359
  360. package/src/gallery/gallery-manifest.ts +0 -28
  361. package/src/home/feature-gate.ts +0 -22
  362. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  363. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  364. package/src/plugins/defaults/injectors/package.json +0 -15
  365. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  366. package/src/plugins/defaults/llm-call/package.json +0 -15
  367. package/src/plugins/defaults/llm-call/register.ts +0 -45
  368. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  369. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  370. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  371. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  372. package/src/plugins/defaults/persistence/package.json +0 -15
  373. package/src/plugins/defaults/persistence/register.ts +0 -38
  374. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  375. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  376. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  377. package/src/plugins/defaults/token-estimate/package.json +0 -15
  378. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  379. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  380. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  381. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  382. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  383. package/src/plugins/defaults/tool-execute/package.json +0 -15
  384. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  385. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  386. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  387. package/src/skills/category-inference.ts +0 -111
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Periodic reaper for orphaned subprocesses that reparent to the daemon.
3
+ *
4
+ * Tools run commands in their own process group (`detached: true`) and, on
5
+ * timeout/abort, group-kill with `process.kill(-pgid, SIGKILL)` (the bash and
6
+ * host_bash tools, the skill sandbox runner, and the debug-bash route). The
7
+ * immediate child is reaped by Bun/libuv, but its descendants — e.g. git's
8
+ * transport helpers or a skill runner's `bun` process — were never spawned by
9
+ * the daemon, so when the group dies they reparent to PID 1. When the daemon
10
+ * runs as PID 1 in a container, Bun is not an init: it never calls `waitpid()`
11
+ * on those reparented orphans, so they accumulate as `<defunct>` entries that
12
+ * consume PID slots until the container is recycled.
13
+ *
14
+ * This reaper scans `/proc` for zombie children of the daemon and reaps each
15
+ * by **specific PID** with `WNOHANG`. It deliberately does NOT use
16
+ * `waitpid(-1)`: libuv reaps the children it spawned by specific PID on
17
+ * `SIGCHLD`, and a blanket `waitpid(-1)` would race libuv and could swallow a
18
+ * tracked child's exit status — libuv's own source handles the lost race by
19
+ * dropping the exit callback ("someone else stole the waitpid from us. Handle
20
+ * this by not handling it at all."). To stay clear of that race we only reap a
21
+ * zombie after it has survived at least one scan interval: libuv reaps its own
22
+ * within milliseconds of `SIGCHLD`, so anything still defunct a full interval
23
+ * later is a genuine orphan libuv is not tracking.
24
+ *
25
+ * The reaper is a no-op unless the daemon is PID 1 on Linux. Off PID 1 (local
26
+ * macOS dev, or if an init such as tini is ever placed above the daemon),
27
+ * orphans reparent to that init and are reaped there, so there is nothing for
28
+ * this to do. Because the daemon is PID 1, orphans already reparent to it and
29
+ * `PR_SET_CHILD_SUBREAPER` is unnecessary. It is additionally gated behind the
30
+ * `daemon.reapOrphanedSubprocesses` config flag (default off) so the behavior
31
+ * can be enabled per workspace for validation before becoming the default.
32
+ *
33
+ * References:
34
+ * - libuv reaps its own children by pid on SIGCHLD (`uv__wait_children`):
35
+ * https://github.com/nodejs/node/blob/main/deps/uv/src/unix/process.c
36
+ * - Subreaper reaping pattern for runtimes embedding libuv (specific-pid +
37
+ * WNOHANG, never `waitpid(-1)`, grace window for libuv co-existence):
38
+ * https://github.com/coopergwrenn/prctl-subreaper
39
+ * - waitpid(2): https://man7.org/linux/man-pages/man2/waitpid.2.html
40
+ */
41
+
42
+ import { readdirSync, readFileSync } from "node:fs";
43
+ import { dlopen, FFIType, ptr } from "bun:ffi";
44
+
45
+ import { getConfigReadOnly } from "../config/loader.js";
46
+ import { getLogger } from "../util/logger.js";
47
+
48
+ const log = getLogger("orphan-reaper");
49
+
50
+ /** Linux `WNOHANG` — return immediately if no child has changed state. */
51
+ const WNOHANG = 1;
52
+
53
+ const SCAN_INTERVAL_MS = 60_000;
54
+
55
+ let scanTimer: ReturnType<typeof setInterval> | null = null;
56
+
57
+ /** Zombie child PIDs observed on the previous scan (the grace set). */
58
+ let seenLastScan: Set<number> = new Set();
59
+
60
+ type WaitpidFn = (pid: number, statusPtr: unknown, options: number) => number;
61
+
62
+ let waitpid: WaitpidFn | null = null;
63
+ // Held at module scope so the backing buffer is not GC'd while `waitStatusPtr`
64
+ // keeps only a raw pointer into it (waitpid writes the exit status here).
65
+ let waitStatusBuf: Int32Array | null = null;
66
+ let waitStatusPtr: unknown = null;
67
+
68
+ /**
69
+ * Bind libc `waitpid` via FFI. Returns false (and disables the reaper) if FFI
70
+ * is unavailable so daemon startup never fails on this subsystem.
71
+ */
72
+ function initWaitpid(): boolean {
73
+ if (waitpid) return true;
74
+ try {
75
+ const lib = dlopen("libc.so.6", {
76
+ waitpid: {
77
+ args: [FFIType.i32, FFIType.ptr, FFIType.i32],
78
+ returns: FFIType.i32,
79
+ },
80
+ });
81
+ // Reusable out-param buffer for the wstatus we don't inspect.
82
+ waitStatusBuf = new Int32Array(1);
83
+ waitStatusPtr = ptr(waitStatusBuf);
84
+ waitpid = lib.symbols.waitpid as unknown as WaitpidFn;
85
+ return true;
86
+ } catch (err) {
87
+ log.warn(
88
+ { err },
89
+ "Orphan reaper unavailable: failed to bind libc waitpid via FFI",
90
+ );
91
+ return false;
92
+ }
93
+ }
94
+
95
+ export interface ZombieChild {
96
+ pid: number;
97
+ comm: string;
98
+ }
99
+
100
+ /**
101
+ * Parse a `/proc/<pid>/stat` line into its leading fields. `comm` (the
102
+ * executable name) may itself contain spaces and parentheses, so the fixed
103
+ * fields are read relative to the final `)` rather than by naive splitting.
104
+ * Returns null if the line is malformed.
105
+ */
106
+ export function parseProcStat(
107
+ content: string,
108
+ ): { comm: string; state: string; ppid: number } | null {
109
+ const lparen = content.indexOf("(");
110
+ const rparen = content.lastIndexOf(")");
111
+ if (lparen === -1 || rparen === -1 || rparen < lparen) return null;
112
+ const comm = content.slice(lparen + 1, rparen);
113
+ const rest = content.slice(rparen + 2).split(" ");
114
+ const state = rest[0];
115
+ const ppid = Number(rest[1]);
116
+ if (!state || !Number.isInteger(ppid)) return null;
117
+ return { comm, state, ppid };
118
+ }
119
+
120
+ /**
121
+ * Given the zombie child PIDs seen this scan and those seen on the previous
122
+ * scan, decide which to reap now. A zombie is only reaped once it has
123
+ * survived a full interval (present in `seenLast`), leaving newly-defunct
124
+ * children for libuv to reap first. Returns the PIDs to reap and the set to
125
+ * carry into the next scan.
126
+ */
127
+ export function selectReapable(
128
+ current: number[],
129
+ seenLast: Set<number>,
130
+ ): { reap: number[]; nextSeen: Set<number> } {
131
+ const reap = current.filter((pid) => seenLast.has(pid));
132
+ return { reap, nextSeen: new Set(current) };
133
+ }
134
+
135
+ /**
136
+ * Scan `/proc` for zombie (`Z`) processes whose parent is this daemon.
137
+ * Reparented orphans keep their original process group but their parent
138
+ * becomes PID 1 (the daemon), so they appear here once defunct.
139
+ */
140
+ function findZombieChildren(): ZombieChild[] {
141
+ const selfPid = process.pid;
142
+ const zombies: ZombieChild[] = [];
143
+ let entries: string[];
144
+ try {
145
+ entries = readdirSync("/proc");
146
+ } catch {
147
+ return zombies;
148
+ }
149
+ for (const entry of entries) {
150
+ const pid = Number(entry);
151
+ if (!Number.isInteger(pid) || pid <= 1) continue;
152
+ let stat: string;
153
+ try {
154
+ stat = readFileSync(`/proc/${pid}/stat`, "utf8");
155
+ } catch {
156
+ // Process exited between readdir and read — skip.
157
+ continue;
158
+ }
159
+ const parsed = parseProcStat(stat);
160
+ if (parsed && parsed.state === "Z" && parsed.ppid === selfPid) {
161
+ zombies.push({ pid, comm: parsed.comm });
162
+ }
163
+ }
164
+ return zombies;
165
+ }
166
+
167
+ /**
168
+ * Reap zombie children that have persisted for at least one scan interval,
169
+ * leaving newly-defunct children for libuv to reap first.
170
+ */
171
+ function reapScan(): void {
172
+ if (!waitpid) return;
173
+ const zombies = findZombieChildren();
174
+ const byPid = new Map(zombies.map((z) => [z.pid, z]));
175
+ const { reap, nextSeen } = selectReapable([...byPid.keys()], seenLastScan);
176
+ const reaped: ZombieChild[] = [];
177
+ for (const pid of reap) {
178
+ const rc = waitpid(pid, waitStatusPtr, WNOHANG);
179
+ // rc > 0: reaped. rc <= 0 (0 = not yet, -1 = ECHILD/raced): leave it.
180
+ if (rc > 0) reaped.push(byPid.get(pid)!);
181
+ }
182
+ seenLastScan = nextSeen;
183
+ if (reaped.length > 0) {
184
+ log.info(
185
+ {
186
+ count: reaped.length,
187
+ pids: reaped.map((z) => z.pid),
188
+ comms: reaped.map((z) => z.comm),
189
+ },
190
+ "Reaped orphaned subprocesses reparented to the daemon (PID 1)",
191
+ );
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Read the opt-in gate from workspace config (`daemon.reapOrphanedSubprocesses`),
197
+ * tolerating a missing or malformed config so startup never fails on it.
198
+ */
199
+ function isReaperEnabled(): boolean {
200
+ try {
201
+ return getConfigReadOnly().daemon.reapOrphanedSubprocesses;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Start the periodic orphan reaper. No-op unless the daemon is PID 1 on Linux
209
+ * (otherwise reparented orphans are reaped by the real init) and the
210
+ * `daemon.reapOrphanedSubprocesses` config gate is enabled.
211
+ */
212
+ export function startOrphanReaper(): void {
213
+ if (scanTimer) return;
214
+ if (process.platform !== "linux" || process.pid !== 1) {
215
+ log.info(
216
+ { platform: process.platform, pid: process.pid },
217
+ "Orphan reaper not started: daemon is not PID 1 on Linux",
218
+ );
219
+ return;
220
+ }
221
+ if (!isReaperEnabled()) {
222
+ log.info(
223
+ "Orphan reaper not started: daemon.reapOrphanedSubprocesses is disabled",
224
+ );
225
+ return;
226
+ }
227
+ if (!initWaitpid()) return;
228
+ seenLastScan = new Set();
229
+ scanTimer = setInterval(reapScan, SCAN_INTERVAL_MS);
230
+ scanTimer.unref?.();
231
+ log.info({ intervalMs: SCAN_INTERVAL_MS }, "Orphan reaper started");
232
+ }
233
+
234
+ export function stopOrphanReaper(): void {
235
+ if (scanTimer) {
236
+ clearInterval(scanTimer);
237
+ scanTimer = null;
238
+ }
239
+ seenLastScan = new Set();
240
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Persistence for the image-too-large recovery path.
3
+ *
4
+ * When the provider rejects a turn because an attached image exceeds its
5
+ * limits, the agent loop downgrades the offending image blocks in memory and
6
+ * retries. That transformation is transient — the stored message row keeps the
7
+ * full-size image block, so the rejected image is rehydrated on every later
8
+ * turn and keeps re-entering the model's context. This module makes the
9
+ * downgrade durable for images that can *never* be transmitted, so a rejected
10
+ * upload cannot resurface after the user re-uploads a smaller version.
11
+ */
12
+
13
+ import { optimizeImageForTransport } from "../agent/image-optimize.js";
14
+ import { parseImageDimensions } from "../context/image-dimensions.js";
15
+ import {
16
+ getMessages,
17
+ updateMessageContent,
18
+ } from "../memory/conversation-crud.js";
19
+ import type { ContentBlock } from "../providers/types.js";
20
+ import { getLogger } from "../util/logger.js";
21
+
22
+ const log = getLogger("persist-unsendable-image");
23
+
24
+ // Anthropic rejects any image whose longest side exceeds this many pixels,
25
+ // regardless of payload size. Mirrors the user-facing message surfaced by
26
+ // `classifyConversationError` for the IMAGE_TOO_LARGE code.
27
+ // https://docs.anthropic.com/en/docs/build-with-claude/vision#image-size
28
+ const PROVIDER_MAX_IMAGE_DIMENSION = 8000;
29
+
30
+ // Anthropic rejects any single image whose base64 payload exceeds 5 MB.
31
+ // https://docs.anthropic.com/en/docs/build-with-claude/vision#image-size
32
+ const PROVIDER_MAX_IMAGE_PAYLOAD_BYTES = 5 * 1024 * 1024;
33
+
34
+ /**
35
+ * Note left in place of an image that cannot be sent to the provider. Shared
36
+ * with the in-memory recovery path so the persisted history matches what the
37
+ * model saw on the turn the image was rejected.
38
+ */
39
+ export const UNSENDABLE_IMAGE_NOTE =
40
+ "(An image was attached but could not be sent — its dimensions exceed the provider limit and automatic resize was not available. Please resize the image and try again.)";
41
+
42
+ /**
43
+ * True when a stored image block can never be transmitted to the provider: it
44
+ * violates a provider hard limit (per-side pixel cap or payload size) and
45
+ * cannot be shrunk to fit (re-optimization is a no-op).
46
+ *
47
+ * Stored blocks are already post-optimization, so a block that is still
48
+ * oversized here only stays oversized because resizing is unavailable on this
49
+ * host (e.g. `sips` is absent off macOS, or the format is unsupported). A
50
+ * downscalable image would have been reduced at upload time and would not reach
51
+ * this predicate, so it is left untouched.
52
+ */
53
+ function isImagePermanentlyUnsendable(
54
+ block: Extract<ContentBlock, { type: "image" }>,
55
+ ): boolean {
56
+ const payloadBytes = block.source.data.length;
57
+ const dims = parseImageDimensions(block.source.data, block.source.media_type);
58
+ const exceedsDimensionCap =
59
+ dims != null &&
60
+ (dims.width > PROVIDER_MAX_IMAGE_DIMENSION ||
61
+ dims.height > PROVIDER_MAX_IMAGE_DIMENSION);
62
+ const exceedsPayloadCap = payloadBytes > PROVIDER_MAX_IMAGE_PAYLOAD_BYTES;
63
+ if (!exceedsDimensionCap && !exceedsPayloadCap) return false;
64
+
65
+ const optimized = optimizeImageForTransport(
66
+ block.source.data,
67
+ block.source.media_type,
68
+ );
69
+ return optimized.data === block.source.data;
70
+ }
71
+
72
+ /**
73
+ * Rewrite every stored message in a conversation that holds a permanently
74
+ * unsendable image, replacing those image blocks with {@link
75
+ * UNSENDABLE_IMAGE_NOTE}. Reads stored content directly (not the in-memory,
76
+ * injection-enriched copy) so injected prefixes and hydrated source paths are
77
+ * never written back.
78
+ *
79
+ * Idempotent: once an image is replaced by the note there is no image block
80
+ * left to match, so re-running is a no-op. Returns the number of rewritten
81
+ * messages.
82
+ */
83
+ export function persistUnsendableImageDowngrades(
84
+ conversationId: string,
85
+ ): number {
86
+ let rewritten = 0;
87
+ for (const row of getMessages(conversationId)) {
88
+ // Cheap prefilter — JSON.stringify emits no spaces, so an image block
89
+ // always serializes with this exact substring.
90
+ if (!row.content.includes('"type":"image"')) continue;
91
+
92
+ let parsed: unknown;
93
+ try {
94
+ parsed = JSON.parse(row.content);
95
+ } catch {
96
+ continue;
97
+ }
98
+ if (!Array.isArray(parsed)) continue;
99
+
100
+ let changed = false;
101
+ const next = (parsed as ContentBlock[]).map((block): ContentBlock => {
102
+ if (block.type !== "image") return block;
103
+ if (!isImagePermanentlyUnsendable(block)) return block;
104
+ changed = true;
105
+ return { type: "text", text: UNSENDABLE_IMAGE_NOTE };
106
+ });
107
+ if (!changed) continue;
108
+
109
+ updateMessageContent(row.id, JSON.stringify(next));
110
+ rewritten++;
111
+ log.info(
112
+ { conversationId, messageId: row.id },
113
+ "Persisted unsendable-image downgrade so it cannot resurface on later turns",
114
+ );
115
+ }
116
+ return rewritten;
117
+ }
@@ -437,9 +437,7 @@ export async function processMessage(
437
437
  conversation.getMessages().push(cleanMsg);
438
438
 
439
439
  conversation.emitActivityState("thinking", "context_compacting");
440
- const result = await conversation.forceCompact({
441
- targetInputTokensOverride: slashResult.targetInputTokensOverride,
442
- });
440
+ const result = await conversation.forceCompact();
443
441
  const responseText = formatCompactResult(result);
444
442
  const assistantMsg = createAssistantMessage(responseText);
445
443
  const persistedAssistant = await addMessage(
@@ -1,14 +1,16 @@
1
1
  import { v4 as uuid } from "uuid";
2
2
 
3
+ import type {
4
+ TraceEvent,
5
+ TraceEventKind,
6
+ TraceEventStatus,
7
+ } from "../api/events/trace-event.js";
3
8
  import {
4
9
  getMaxSequence,
5
10
  persistTraceEvent,
6
11
  } from "../memory/trace-event-store.js";
7
12
  import { getLogger } from "../util/logger.js";
8
- import type { ServerMessage, TraceEventKind } from "./message-protocol.js";
9
- import type { TraceEvent } from "./message-types/messages.js";
10
-
11
- export type TraceEventStatus = "info" | "success" | "warning" | "error";
13
+ import type { ServerMessage } from "./message-protocol.js";
12
14
 
13
15
  const log = getLogger("trace-emitter");
14
16
 
@@ -5,6 +5,8 @@
5
5
  * imports (memory/conversation-crud → daemon/conversation-runtime-assembly).
6
6
  */
7
7
  import type { ChannelId } from "../channels/types.js";
8
+ import { isHttpAuthDisabled } from "../config/env.js";
9
+ import type { TrustClass } from "../runtime/actor-trust-resolver.js";
8
10
 
9
11
  export interface TrustContext {
10
12
  /** Channel through which the inbound message arrived. */
@@ -62,3 +64,20 @@ export const FALLBACK_TURN_TRUST: TrustContext = {
62
64
  sourceChannel: "vellum",
63
65
  trustClass: "unknown",
64
66
  };
67
+
68
+ /**
69
+ * Resolve the effective trust class for an actor.
70
+ *
71
+ * When HTTP auth is disabled (dev bypass), always returns `'guardian'`
72
+ * so that control-plane gates don't block local development.
73
+ *
74
+ * When no trust context is available (e.g. desktop-only conversations that
75
+ * don't go through channel trust resolution), defaults to `'unknown'`
76
+ * to fail-closed.
77
+ */
78
+ export function resolveTrustClass(
79
+ trustContext: TrustContext | undefined,
80
+ ): TrustClass {
81
+ if (isHttpAuthDisabled()) return "guardian";
82
+ return trustContext?.trustClass ?? "unknown";
83
+ }
@@ -140,6 +140,8 @@ function translateAgentEventToServerMessage(
140
140
  case "context_compacting":
141
141
  case "compaction_circuit_open":
142
142
  case "compaction_circuit_closed":
143
+ case "compaction_completed":
144
+ case "history_stripped":
143
145
  case "agent_loop_exit":
144
146
  return null;
145
147
  case "llm_call_started":
@@ -192,7 +194,7 @@ export function conversationToWakeTarget(
192
194
  },
193
195
  isProcessing: () => conversation.isProcessing(),
194
196
  markProcessing: (on) => {
195
- conversation.processing = on;
197
+ conversation.setProcessing(on);
196
198
  },
197
199
  setTrustContext: (ctx) => conversation.setTrustContext(ctx),
198
200
  setWakeAllowedTools: (tools) => {
@@ -10,11 +10,15 @@
10
10
  * Storage uses the existing `memory_checkpoints` table (simple key-value store).
11
11
  */
12
12
 
13
+ import { createHash } from "node:crypto";
14
+ import { existsSync, readFileSync } from "node:fs";
15
+
13
16
  import {
14
17
  getMemoryCheckpoint,
15
18
  setMemoryCheckpoint,
16
19
  } from "../memory/checkpoints.js";
17
- import { computeIdentityContentHash } from "../runtime/routes/identity-intro-cache.js";
20
+ import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
21
+ import { getWorkspacePromptPath } from "../util/platform.js";
18
22
 
19
23
  // ---------------------------------------------------------------------------
20
24
  // Constants
@@ -26,6 +30,25 @@ const CHECKPOINT_KEY_TEXT = "home:greeting:text";
26
30
  const CHECKPOINT_KEY_HASH = "home:greeting:content_hash";
27
31
  const CHECKPOINT_KEY_TIMESTAMP = "home:greeting:cached_at";
28
32
 
33
+ const IDENTITY_FILES = ["IDENTITY.md", "SOUL.md"] as const;
34
+
35
+ function readWorkspaceFile(name: string): string {
36
+ try {
37
+ const path = getWorkspacePromptPath(name);
38
+ if (!existsSync(path)) return "";
39
+ return readFileSync(path, "utf-8");
40
+ } catch {
41
+ return "";
42
+ }
43
+ }
44
+
45
+ function computeIdentityContentHash(): string {
46
+ const staticFiles = IDENTITY_FILES.map(readWorkspaceFile).join("\n---\n");
47
+ const guardianPersona = resolveGuardianPersona() ?? "";
48
+ const combined = staticFiles + "\n---\n" + guardianPersona;
49
+ return createHash("sha256").update(combined).digest("hex");
50
+ }
51
+
29
52
  // ---------------------------------------------------------------------------
30
53
  // Public API
31
54
  // ---------------------------------------------------------------------------
@@ -91,7 +91,7 @@ describe("ipcGetFeatureFlags", () => {
91
91
  expect(flags).toEqual({ "flag-a": true, "flag-b": false });
92
92
  });
93
93
 
94
- test("filters non-boolean values from response", async () => {
94
+ test("accepts boolean and string values, filters other types from response", async () => {
95
95
  mockGatewayIpc(null, {
96
96
  results: {
97
97
  get_feature_flags: {
@@ -104,7 +104,7 @@ describe("ipcGetFeatureFlags", () => {
104
104
  });
105
105
 
106
106
  const flags = await ipcGetFeatureFlags();
107
- expect(flags).toEqual({ valid: true });
107
+ expect(flags).toEqual({ valid: true, string: "yes" });
108
108
  });
109
109
 
110
110
  test("returns empty record when IPC returns undefined", async () => {
@@ -98,12 +98,12 @@ export function resetPersistentClient(): void {
98
98
  */
99
99
  export async function ipcGetFeatureFlags(
100
100
  timeoutMs?: number,
101
- ): Promise<Record<string, boolean>> {
101
+ ): Promise<Record<string, boolean | string>> {
102
102
  const result = await ipcCall("get_feature_flags", undefined, timeoutMs);
103
103
  if (result && typeof result === "object" && !Array.isArray(result)) {
104
- const filtered: Record<string, boolean> = {};
104
+ const filtered: Record<string, boolean | string> = {};
105
105
  for (const [k, v] of Object.entries(result as Record<string, unknown>)) {
106
- if (typeof v === "boolean") filtered[k] = v;
106
+ if (typeof v === "boolean" || typeof v === "string") filtered[k] = v;
107
107
  }
108
108
  return filtered;
109
109
  }
@@ -5,6 +5,7 @@ import {
5
5
  type ImageGenCredentials,
6
6
  type ImageGenerationRequest,
7
7
  type ImageGenerationResult,
8
+ isImageProviderBillingError,
8
9
  type ManagedProxyCredentials,
9
10
  MAX_VARIANTS,
10
11
  } from "./types.js";
@@ -19,9 +20,18 @@ const ALLOWED_MODELS = new Set([
19
20
 
20
21
  // --- Error mapping ---
21
22
 
23
+ const GEMINI_BILLING_MESSAGE =
24
+ "Image generation is unavailable because the Gemini account or API key is out of credits. " +
25
+ "Add funds with the provider or update the key in Settings — retrying won't help until credits are added.";
26
+
22
27
  export function mapGeminiError(error: unknown): string {
23
28
  if (error instanceof ApiError) {
24
29
  const status = error.status;
30
+ // Billing failures are non-retryable, so check them before the rate-limit
31
+ // branch to avoid telling the user to "wait and try again".
32
+ if (isImageProviderBillingError({ status, message: error.message })) {
33
+ return GEMINI_BILLING_MESSAGE;
34
+ }
25
35
  if (status === 400) {
26
36
  return "The image request was invalid. Please check your prompt and try again.";
27
37
  }
@@ -37,6 +47,11 @@ export function mapGeminiError(error: unknown): string {
37
47
  return `Gemini API error (status ${status}). Please try again.`;
38
48
  }
39
49
  if (error instanceof Error) {
50
+ // The managed proxy surfaces failures as plain Errors whose message embeds
51
+ // the upstream status (e.g. "Managed proxy request failed (402): ...").
52
+ if (isImageProviderBillingError({ message: error.message })) {
53
+ return GEMINI_BILLING_MESSAGE;
54
+ }
40
55
  return `Image generation failed: ${error.message}`;
41
56
  }
42
57
  return "An unexpected error occurred during image generation.";
@@ -6,6 +6,7 @@ import {
6
6
  type ImageGenCredentials,
7
7
  type ImageGenerationRequest,
8
8
  type ImageGenerationResult,
9
+ isImageProviderBillingError,
9
10
  MAX_VARIANTS,
10
11
  } from "./types.js";
11
12
 
@@ -16,6 +17,10 @@ const ALLOWED_MODELS = new Set(["gpt-image-2"]);
16
17
 
17
18
  // --- Error mapping ---
18
19
 
20
+ const OPENAI_BILLING_MESSAGE =
21
+ "Image generation is unavailable because the OpenAI account or API key is out of credits. " +
22
+ "Add funds with the provider or update the key in Settings — retrying won't help until credits are added.";
23
+
19
24
  /**
20
25
  * Map an error raised by the OpenAI Images API to a user-friendly string.
21
26
  * Mirrors the status-code branches of `mapGeminiError` in
@@ -24,6 +29,12 @@ const ALLOWED_MODELS = new Set(["gpt-image-2"]);
24
29
  export function mapOpenAIError(error: unknown): string {
25
30
  if (error instanceof OpenAI.APIError) {
26
31
  const status = error.status;
32
+ // Billing failures are non-retryable and can surface as a 402 or as a 429
33
+ // with an `insufficient_quota` body, so check them before the rate-limit
34
+ // branch to avoid telling the user to "wait and try again".
35
+ if (isImageProviderBillingError({ status, message: error.message })) {
36
+ return OPENAI_BILLING_MESSAGE;
37
+ }
27
38
  if (status === 400) {
28
39
  return "The image request was invalid. Please check your prompt and try again.";
29
40
  }
@@ -39,6 +50,9 @@ export function mapOpenAIError(error: unknown): string {
39
50
  return `OpenAI API error (status ${status}). Please try again.`;
40
51
  }
41
52
  if (error instanceof Error) {
53
+ if (isImageProviderBillingError({ message: error.message })) {
54
+ return OPENAI_BILLING_MESSAGE;
55
+ }
42
56
  return `Image generation failed: ${error.message}`;
43
57
  }
44
58
  return "An unexpected error occurred during image generation.";
@@ -44,3 +44,37 @@ export function providerForImageModelPrefix(model: string): ImageGenProvider {
44
44
  }
45
45
  return "gemini";
46
46
  }
47
+
48
+ /**
49
+ * Message fragments that indicate a provider billing / insufficient-credits
50
+ * failure. Used by the per-provider error mappers to detect a non-retryable
51
+ * out-of-credits condition that no number of retries will resolve.
52
+ */
53
+ const BILLING_MESSAGE_PATTERNS: readonly RegExp[] = [
54
+ /credit balance is too low/i,
55
+ /insufficient[\s_-]*credits?/i,
56
+ /insufficient_quota/i,
57
+ /exceeded your current quota/i,
58
+ /out of credits/i,
59
+ /requires more credits/i,
60
+ /billing/i,
61
+ /request failed \(402\)/i,
62
+ ];
63
+
64
+ /**
65
+ * Detect a provider billing / insufficient-credits failure from an HTTP status
66
+ * and/or error message. A 402 status is billing by definition; otherwise the
67
+ * message is matched against known billing phrasings (OpenAI's
68
+ * `insufficient_quota` is reported as a 429, so status alone is insufficient).
69
+ *
70
+ * Billing failures are non-retryable: the user must add funds or update the API
71
+ * key. Callers surface a distinct message instead of a generic "try again".
72
+ */
73
+ export function isImageProviderBillingError(args: {
74
+ status?: number;
75
+ message?: string;
76
+ }): boolean {
77
+ if (args.status === 402) return true;
78
+ const message = args.message ?? "";
79
+ return BILLING_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
80
+ }