@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -17,6 +17,7 @@ import {
17
17
  isInteractiveInterface,
18
18
  parseChannelId,
19
19
  parseInterfaceId,
20
+ supportsHostProxy,
20
21
  } from "../../channels/types.js";
21
22
  import { isHttpAuthDisabled } from "../../config/env.js";
22
23
  import { getConfig } from "../../config/loader.js";
@@ -38,6 +39,10 @@ import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
38
39
  import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
39
40
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
40
41
  import type { ServerMessage } from "../../daemon/message-protocol.js";
42
+ import type {
43
+ MacosTransportMetadata,
44
+ NonMacosTransportMetadata,
45
+ } from "../../daemon/message-types/conversations.js";
41
46
  import type { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
42
47
  import * as attachmentsStore from "../../memory/attachments-store.js";
43
48
  import {
@@ -410,8 +415,15 @@ export function handleListMessages(
410
415
  rawMessages = getMessages(resolvedConversationId);
411
416
  }
412
417
 
418
+ // During streaming, tool_use (assistant) and tool_result (user) events are
419
+ // assembled client-side into a single assistant ChatMessage. On reload, they
420
+ // are separate DB rows. Merge tool_result blocks from user messages into the
421
+ // preceding assistant message so renderHistoryContent can pair them via its
422
+ // pendingToolUses map — otherwise they render as "Unknown" tool calls.
423
+ const mergedMessages = mergeToolResultsIntoAssistantMessages(rawMessages);
424
+
413
425
  // Parse content blocks and extract text + tool calls
414
- const parsed = rawMessages.map((msg) => {
426
+ const parsed = mergedMessages.map((msg) => {
415
427
  let content: unknown;
416
428
  try {
417
429
  content = JSON.parse(msg.content);
@@ -424,10 +436,33 @@ export function handleListMessages(
424
436
  // was queued or its persistence was delayed (long assistant generation),
425
437
  // sentAt captures the actual event time. Falls back to createdAt.
426
438
  let sentAt: number | undefined;
439
+ let subagentNotification:
440
+ | {
441
+ subagentId: string;
442
+ label: string;
443
+ status: string;
444
+ error?: string;
445
+ conversationId?: string;
446
+ }
447
+ | undefined;
427
448
  if (msg.metadata) {
428
449
  try {
429
450
  const meta = JSON.parse(msg.metadata);
430
451
  if (typeof meta.sentAt === "number") sentAt = meta.sentAt;
452
+ if (meta.subagentNotification) {
453
+ const n = meta.subagentNotification;
454
+ if (typeof n.subagentId === "string" && typeof n.label === "string") {
455
+ subagentNotification = {
456
+ subagentId: n.subagentId,
457
+ label: n.label,
458
+ status: typeof n.status === "string" ? n.status : "completed",
459
+ ...(typeof n.error === "string" ? { error: n.error } : {}),
460
+ ...(typeof n.conversationId === "string"
461
+ ? { conversationId: n.conversationId }
462
+ : {}),
463
+ };
464
+ }
465
+ }
431
466
  } catch {
432
467
  // Ignore malformed metadata
433
468
  }
@@ -475,6 +510,7 @@ export function handleListMessages(
475
510
  ? { thinkingSegments: rendered.thinkingSegments }
476
511
  : {}),
477
512
  id: msg.id,
513
+ subagentNotification,
478
514
  };
479
515
  }
480
516
 
@@ -492,6 +528,7 @@ export function handleListMessages(
492
528
  ? { thinkingSegments: rendered.thinkingSegments }
493
529
  : {}),
494
530
  id: msg.id,
531
+ subagentNotification,
495
532
  };
496
533
  });
497
534
 
@@ -580,6 +617,9 @@ export function handleListMessages(
580
617
  ? { thinkingSegments: m.thinkingSegments }
581
618
  : {}),
582
619
  ...(m.contentOrder.length > 0 ? { contentOrder: m.contentOrder } : {}),
620
+ ...(m.subagentNotification
621
+ ? { subagentNotification: m.subagentNotification }
622
+ : {}),
583
623
  };
584
624
  });
585
625
 
@@ -599,6 +639,160 @@ export function handleListMessages(
599
639
  return Response.json({ messages });
600
640
  }
601
641
 
642
+ // ── Tool-result merging ─────────────────────────────────────────────
643
+
644
+ function isToolResultType(type: string): boolean {
645
+ return type === "tool_result" || type === "web_search_tool_result";
646
+ }
647
+
648
+ function isSystemNoticeText(block: Record<string, unknown>): boolean {
649
+ if (block.type !== "text") return false;
650
+ const text = typeof block.text === "string" ? block.text : "";
651
+ return (
652
+ text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
653
+ );
654
+ }
655
+
656
+ /**
657
+ * Merge tool_result blocks from user messages into the preceding assistant
658
+ * message's content array. This lets renderHistoryContent's pendingToolUses
659
+ * map pair tool_use and tool_result blocks, preventing "unknown" tool names.
660
+ *
661
+ * User messages that consist entirely of tool_result blocks (and optional
662
+ * system_notice text) are removed from the output. Mixed messages (tool_result
663
+ * + real user text) keep only the non-tool-result blocks.
664
+ */
665
+ function mergeToolResultsIntoAssistantMessages(
666
+ messages: MessageRow[],
667
+ ): MessageRow[] {
668
+ // Index of the most recent assistant message in the output array.
669
+ let lastAssistantIdx = -1;
670
+ // Parsed content caches — lazily populated per assistant message.
671
+ const parsedAssistantContent = new Map<number, unknown[]>();
672
+
673
+ const result: MessageRow[] = [];
674
+
675
+ for (const msg of messages) {
676
+ if (msg.role === "assistant") {
677
+ lastAssistantIdx = result.length;
678
+ result.push(msg);
679
+ continue;
680
+ }
681
+
682
+ // Only process user messages — other roles pass through.
683
+ if (msg.role !== "user") {
684
+ result.push(msg);
685
+ continue;
686
+ }
687
+
688
+ let blocks: unknown[];
689
+ try {
690
+ const parsed = JSON.parse(msg.content);
691
+ if (!Array.isArray(parsed)) {
692
+ result.push(msg);
693
+ continue;
694
+ }
695
+ blocks = parsed;
696
+ } catch {
697
+ result.push(msg);
698
+ continue;
699
+ }
700
+
701
+ // Separate tool-result blocks from real user content.
702
+ const toolResultBlocks: unknown[] = [];
703
+ const otherBlocks: unknown[] = [];
704
+ for (const block of blocks) {
705
+ if (
706
+ typeof block === "object" &&
707
+ block !== null &&
708
+ typeof (block as Record<string, unknown>).type === "string"
709
+ ) {
710
+ const rec = block as Record<string, unknown>;
711
+ if (isToolResultType(rec.type as string)) {
712
+ toolResultBlocks.push(block);
713
+ } else if (isSystemNoticeText(rec)) {
714
+ // System notices don't count as user content — drop them when
715
+ // the message is otherwise tool-result-only.
716
+ otherBlocks.push(block);
717
+ } else {
718
+ otherBlocks.push(block);
719
+ }
720
+ } else {
721
+ otherBlocks.push(block);
722
+ }
723
+ }
724
+
725
+ // No tool results → pass through unchanged. System notices are only
726
+ // injected alongside tool results in the agent loop, so a pure user
727
+ // message (no tool_result blocks) should never be filtered — even if
728
+ // the user's text happens to look like a system_notice tag.
729
+ if (toolResultBlocks.length === 0) {
730
+ result.push(msg);
731
+ continue;
732
+ }
733
+
734
+ // Append tool_result blocks to the preceding assistant message's content.
735
+ if (lastAssistantIdx >= 0) {
736
+ const assistant = result[lastAssistantIdx];
737
+ let assistantContent = parsedAssistantContent.get(lastAssistantIdx);
738
+ if (!assistantContent) {
739
+ try {
740
+ const parsed = JSON.parse(assistant.content);
741
+ assistantContent = Array.isArray(parsed) ? parsed : [parsed];
742
+ } catch {
743
+ assistantContent = [];
744
+ }
745
+ parsedAssistantContent.set(lastAssistantIdx, assistantContent);
746
+ }
747
+ assistantContent.push(...toolResultBlocks);
748
+ } else {
749
+ // No preceding assistant message (pagination boundary) — keep the
750
+ // original message as-is to avoid permanent data loss. The preceding
751
+ // assistant tool_use lives in the previous page; dropping the result
752
+ // here would be unrecoverable.
753
+ // Still strip system notices so internal prompt text isn't exposed.
754
+ const filteredBlocks = blocks.filter(
755
+ (b) =>
756
+ !(
757
+ typeof b === "object" &&
758
+ b !== null &&
759
+ isSystemNoticeText(b as Record<string, unknown>)
760
+ ),
761
+ );
762
+ result.push({
763
+ ...msg,
764
+ content:
765
+ filteredBlocks.length === blocks.length
766
+ ? msg.content
767
+ : JSON.stringify(filteredBlocks),
768
+ });
769
+ continue;
770
+ }
771
+
772
+ // If the user message had only tool_result (+ system_notice) blocks,
773
+ // suppress it entirely. Otherwise keep the non-tool-result content.
774
+ const realUserContent = otherBlocks.filter(
775
+ (b) =>
776
+ !(
777
+ typeof b === "object" &&
778
+ b !== null &&
779
+ isSystemNoticeText(b as Record<string, unknown>)
780
+ ),
781
+ );
782
+ if (realUserContent.length > 0) {
783
+ result.push({ ...msg, content: JSON.stringify(otherBlocks) });
784
+ }
785
+ // else: tool-result-only → suppressed (results already merged above)
786
+ }
787
+
788
+ // Write back any modified assistant message content.
789
+ for (const [idx, content] of parsedAssistantContent) {
790
+ result[idx] = { ...result[idx], content: JSON.stringify(content) };
791
+ }
792
+
793
+ return result;
794
+ }
795
+
602
796
  /**
603
797
  * Build an `onEvent` callback that publishes every outbound event to the
604
798
  * assistant event hub, maintaining ordered delivery through a serial chain.
@@ -658,13 +852,10 @@ function makeHubPublisher(
658
852
  guardianPrincipalId: trustContext?.guardianPrincipalId ?? undefined,
659
853
  toolName: msg.toolName,
660
854
  commandPreview:
661
- redactSecrets(
662
- summarizeToolInput(msg.toolName, inputRecord),
663
- ) || undefined,
855
+ redactSecrets(summarizeToolInput(msg.toolName, inputRecord)) ||
856
+ undefined,
664
857
  riskLevel: msg.riskLevel,
665
- activityText: activityRaw
666
- ? redactSecrets(activityRaw)
667
- : undefined,
858
+ activityText: activityRaw ? redactSecrets(activityRaw) : undefined,
668
859
  executionTarget: msg.executionTarget,
669
860
  status: "pending",
670
861
  requestCode: generateCanonicalRequestCode(),
@@ -759,6 +950,8 @@ export async function handleSendMessage(
759
950
  conversationType?: string;
760
951
  automated?: boolean;
761
952
  bypassSecretCheck?: boolean;
953
+ hostHomeDir?: string;
954
+ hostUsername?: string;
762
955
  };
763
956
 
764
957
  const { conversationKey, content, attachmentIds } = body;
@@ -791,9 +984,11 @@ export async function handleSendMessage(
791
984
  );
792
985
  }
793
986
 
794
- if (!conversationKey) {
795
- return httpError("BAD_REQUEST", "conversationKey is required", 400);
796
- }
987
+ // When conversationKey is omitted, derive a stable default from
988
+ // sourceChannel + sourceInterface so that repeated calls from the same
989
+ // channel/interface pair share a single conversation thread.
990
+ const resolvedConversationKey =
991
+ conversationKey ?? `default:${sourceChannel}:${sourceInterface}`;
797
992
 
798
993
  // Reject non-string content values (numbers, objects, etc.)
799
994
  if (content != null && typeof content !== "string") {
@@ -856,12 +1051,29 @@ export async function handleSendMessage(
856
1051
 
857
1052
  const conversationType =
858
1053
  body.conversationType === "private" ? ("private" as const) : undefined;
859
- const mapping = getOrCreateConversation(conversationKey, {
1054
+ const mapping = getOrCreateConversation(resolvedConversationKey, {
860
1055
  conversationType,
861
1056
  });
862
1057
  const smDeps = deps.sendMessageDeps;
1058
+
1059
+ // Build transport metadata from the request so the daemon can inject
1060
+ // host environment hints (home directory, username) into the LLM context.
1061
+ const transport =
1062
+ sourceInterface === "macos"
1063
+ ? ({
1064
+ channelId: sourceChannel,
1065
+ interfaceId: "macos" as const,
1066
+ hostHomeDir: body.hostHomeDir,
1067
+ hostUsername: body.hostUsername,
1068
+ } satisfies MacosTransportMetadata)
1069
+ : ({
1070
+ channelId: sourceChannel,
1071
+ interfaceId: sourceInterface,
1072
+ } satisfies NonMacosTransportMetadata);
1073
+
863
1074
  const conversation = await smDeps.getOrCreateConversation(
864
1075
  mapping.conversationId,
1076
+ { transport },
865
1077
  );
866
1078
 
867
1079
  // Resolve guardian context from the AuthContext's actorPrincipalId.
@@ -932,7 +1144,7 @@ export async function handleSendMessage(
932
1144
  // channels, headless) fall back to local execution.
933
1145
  // Set the proxy BEFORE updateClient so updateClient's call to
934
1146
  // hostBashProxy.updateSender targets the correct (new) proxy.
935
- if (sourceInterface === "macos" || sourceInterface === "ios") {
1147
+ if (supportsHostProxy(sourceInterface)) {
936
1148
  // Reuse the existing proxy if the conversation is actively processing a
937
1149
  // host bash request to avoid orphaning in-flight requests.
938
1150
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
@@ -969,9 +1181,7 @@ export async function handleSendMessage(
969
1181
  // When proxies are preserved during an active turn (non-desktop request while
970
1182
  // processing), skip updating proxy senders to avoid degrading them.
971
1183
  const preservingProxies =
972
- conversation.isProcessing() &&
973
- sourceInterface !== "macos" &&
974
- sourceInterface !== "ios";
1184
+ conversation.isProcessing() && !supportsHostProxy(sourceInterface);
975
1185
  conversation.updateClient(onEvent, !isInteractive, {
976
1186
  skipProxySenderUpdate: preservingProxies,
977
1187
  });
@@ -1132,6 +1342,8 @@ export async function handleSendMessage(
1132
1342
  ...(body.automated === true ? { automated: true } : {}),
1133
1343
  },
1134
1344
  { isInteractive },
1345
+ undefined, // displayContent
1346
+ transport,
1135
1347
  );
1136
1348
  if (enqueueResult.rejected) {
1137
1349
  return Response.json(
@@ -1143,36 +1355,47 @@ export async function handleSendMessage(
1143
1355
  // Auto-deny pending confirmations only after enqueue succeeds, so we
1144
1356
  // don't cancel approval-gated workflows when the replacement message
1145
1357
  // is itself rejected by the queue budget.
1146
- if (conversation.hasAnyPendingConfirmation()) {
1147
- // Emit authoritative denial state for each pending request.
1148
- // sendToClient (wired to the SSE hub) delivers these to the client.
1149
- for (const interaction of pendingInteractions.getByConversation(
1150
- mapping.conversationId,
1151
- )) {
1152
- if (
1153
- interaction.conversation === conversation &&
1154
- interaction.kind === "confirmation"
1155
- ) {
1156
- conversation.emitConfirmationStateChanged({
1157
- conversationId: mapping.conversationId,
1158
- requestId: interaction.requestId,
1159
- state: "denied" as const,
1160
- source: "auto_deny" as const,
1161
- });
1162
- // Sync canonical guardian request status so stale "pending" DB
1163
- // records don't get matched by later guardian reply routing.
1164
- resolveCanonicalGuardianRequest(interaction.requestId, "pending", {
1165
- status: "denied",
1166
- });
1358
+ // Wrapped in try-catch: the message is already enqueued, so a failure
1359
+ // here must not turn the 202 response into a 500 — that would leave
1360
+ // the client showing "Failed to send" for a message the daemon will
1361
+ // process from the queue.
1362
+ try {
1363
+ if (conversation.hasAnyPendingConfirmation()) {
1364
+ // Emit authoritative denial state for each pending request.
1365
+ // sendToClient (wired to the SSE hub) delivers these to the client.
1366
+ for (const interaction of pendingInteractions.getByConversation(
1367
+ mapping.conversationId,
1368
+ )) {
1369
+ if (
1370
+ interaction.conversation === conversation &&
1371
+ interaction.kind === "confirmation"
1372
+ ) {
1373
+ conversation.emitConfirmationStateChanged({
1374
+ conversationId: mapping.conversationId,
1375
+ requestId: interaction.requestId,
1376
+ state: "denied" as const,
1377
+ source: "auto_deny" as const,
1378
+ });
1379
+ // Sync canonical guardian request status so stale "pending" DB
1380
+ // records don't get matched by later guardian reply routing.
1381
+ resolveCanonicalGuardianRequest(interaction.requestId, "pending", {
1382
+ status: "denied",
1383
+ });
1384
+ }
1167
1385
  }
1386
+ conversation.denyAllPendingConfirmations();
1387
+ pendingInteractions.removeByConversation(conversation);
1168
1388
  }
1169
- conversation.denyAllPendingConfirmations();
1170
- pendingInteractions.removeByConversation(conversation);
1171
- }
1172
1389
 
1173
- // Expire any orphaned canonical requests that survived without a
1174
- // matching in-memory pending interaction (e.g. prompter timeouts).
1175
- expireOrphanedCanonicalRequests(mapping.conversationId);
1390
+ // Expire any orphaned canonical requests that survived without a
1391
+ // matching in-memory pending interaction (e.g. prompter timeouts).
1392
+ expireOrphanedCanonicalRequests(mapping.conversationId);
1393
+ } catch (err) {
1394
+ log.warn(
1395
+ { err, conversationId: mapping.conversationId },
1396
+ "Post-enqueue auto-deny failed — queued message unaffected",
1397
+ );
1398
+ }
1176
1399
 
1177
1400
  return Response.json(
1178
1401
  { accepted: true, queued: true, conversationId: mapping.conversationId },
@@ -1397,7 +1620,10 @@ export async function handleSendMessage(
1397
1620
  conversationId,
1398
1621
  });
1399
1622
  conversation.processing = false;
1400
- silentlyWithLog(conversation.drainQueue(), "compact-command queue drain");
1623
+ silentlyWithLog(
1624
+ conversation.drainQueue(),
1625
+ "compact-command queue drain",
1626
+ );
1401
1627
  }, 0);
1402
1628
 
1403
1629
  cleanupDeferred = true;
@@ -1713,7 +1939,7 @@ export function conversationRouteDefinitions(deps: {
1713
1939
  "Send a user message to a conversation and trigger the assistant response.",
1714
1940
  tags: ["messages"],
1715
1941
  requestBody: z.object({
1716
- conversationKey: z.string(),
1942
+ conversationKey: z.string().optional(),
1717
1943
  content: z.string().describe("Message text content"),
1718
1944
  attachments: z
1719
1945
  .array(z.unknown())
@@ -63,8 +63,22 @@ export function groupRouteDefinitions(): RouteDefinition[] {
63
63
  if (!body.name || typeof body.name !== "string") {
64
64
  return httpError("BAD_REQUEST", "Missing or invalid name", 400);
65
65
  }
66
- const group = createGroup(body.name);
67
- return Response.json(serializeGroup(group), { status: 201 });
66
+ try {
67
+ const group = createGroup(body.name);
68
+ return Response.json(serializeGroup(group), { status: 201 });
69
+ } catch (err) {
70
+ if (
71
+ err instanceof Error &&
72
+ err.message.includes("sort_position must be >= 4")
73
+ ) {
74
+ return httpError(
75
+ "BAD_REQUEST",
76
+ "Too many custom groups — sort_position ceiling reached",
77
+ 400,
78
+ );
79
+ }
80
+ throw err;
81
+ }
68
82
  },
69
83
  },
70
84
  {
@@ -105,16 +119,16 @@ export function groupRouteDefinitions(): RouteDefinition[] {
105
119
  403,
106
120
  );
107
121
  }
108
- // Custom group sort_position must be >= 3
122
+ // Custom group sort_position must be >= 4 (0–3 reserved for system groups)
109
123
  if (
110
124
  body.sortPosition !== undefined &&
111
125
  (typeof body.sortPosition !== "number" ||
112
126
  !isFinite(body.sortPosition) ||
113
- body.sortPosition < 3)
127
+ body.sortPosition < 4)
114
128
  ) {
115
129
  return httpError(
116
130
  "BAD_REQUEST",
117
- "Custom group sort_position must be >= 3",
131
+ "Custom group sort_position must be >= 4",
118
132
  400,
119
133
  );
120
134
  }
@@ -176,7 +190,7 @@ export function groupRouteDefinitions(): RouteDefinition[] {
176
190
  if (!Array.isArray(body.updates)) {
177
191
  return httpError("BAD_REQUEST", "Missing updates array", 400);
178
192
  }
179
- // Validate: no system group reordering, no sort_position < 3 for custom groups
193
+ // Validate: no system group reordering, sort_position >= 4 for custom groups
180
194
  for (const update of body.updates) {
181
195
  const group = getGroup(update.groupId);
182
196
  if (!group) continue;
@@ -190,11 +204,11 @@ export function groupRouteDefinitions(): RouteDefinition[] {
190
204
  if (
191
205
  typeof update.sortPosition !== "number" ||
192
206
  !isFinite(update.sortPosition) ||
193
- update.sortPosition < 3
207
+ update.sortPosition < 4
194
208
  ) {
195
209
  return httpError(
196
210
  "BAD_REQUEST",
197
- `Custom group sort_position must be >= 3 (got ${update.sortPosition} for ${update.groupId})`,
211
+ `Custom group sort_position must be >= 4 (got ${update.sortPosition} for ${update.groupId})`,
198
212
  400,
199
213
  );
200
214
  }
@@ -47,16 +47,10 @@ function handleUpdateConfig(
47
47
  if (typeof body.enabled === "boolean") heartbeat.enabled = body.enabled;
48
48
  if (typeof body.intervalMs === "number")
49
49
  heartbeat.intervalMs = body.intervalMs;
50
- if ("activeHoursStart" in body) {
51
- heartbeat.activeHoursStart =
52
- typeof body.activeHoursStart === "number"
53
- ? body.activeHoursStart
54
- : undefined;
55
- }
56
- if ("activeHoursEnd" in body) {
57
- heartbeat.activeHoursEnd =
58
- typeof body.activeHoursEnd === "number" ? body.activeHoursEnd : undefined;
59
- }
50
+ if (typeof body.activeHoursStart === "number")
51
+ heartbeat.activeHoursStart = body.activeHoursStart;
52
+ if (typeof body.activeHoursEnd === "number")
53
+ heartbeat.activeHoursEnd = body.activeHoursEnd;
60
54
 
61
55
  try {
62
56
  saveConfig({ ...config, heartbeat });
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  import { z } from "zod";
11
11
 
12
12
  import { parseIdentityFields } from "../../daemon/handlers/identity.js";
13
+ import { getProfilerRuntimeStatus } from "../../daemon/profiler-run-store.js";
13
14
  import { getMaxMigrationVersion } from "../../memory/migrations/registry.js";
14
15
  import {
15
16
  getWorkspaceDir,
@@ -144,6 +145,13 @@ export function handleHealth(): Response {
144
145
  }
145
146
 
146
147
  export function handleDetailedHealth(): Response {
148
+ let profiler: ReturnType<typeof getProfilerRuntimeStatus> | undefined;
149
+ try {
150
+ profiler = getProfilerRuntimeStatus();
151
+ } catch {
152
+ // Profiler status is non-critical — omit on error
153
+ }
154
+
147
155
  return Response.json({
148
156
  status: "healthy",
149
157
  timestamp: new Date().toISOString(),
@@ -156,6 +164,7 @@ export function handleDetailedHealth(): Response {
156
164
  lastWorkspaceMigrationId:
157
165
  getLastWorkspaceMigrationId(WORKSPACE_MIGRATIONS),
158
166
  },
167
+ ...(profiler ? { profiler } : {}),
159
168
  });
160
169
  }
161
170
 
@@ -239,6 +248,48 @@ export function handleGetIdentityIntro(): Response {
239
248
  return Response.json({ text: cached.text });
240
249
  }
241
250
 
251
+ // ---------------------------------------------------------------------------
252
+ // Zod schemas for profiler health metadata
253
+ // ---------------------------------------------------------------------------
254
+
255
+ const profilerBudgetSchema = z.object({
256
+ maxBytes: z.number(),
257
+ remainingBytes: z.number(),
258
+ minFreeMb: z.number(),
259
+ freeMb: z.number(),
260
+ overBudget: z.boolean(),
261
+ });
262
+
263
+ const profilerLastCompletedRunSchema = z.object({
264
+ runId: z.string(),
265
+ totalBytes: z.number(),
266
+ artifactCount: z.number(),
267
+ hasSummaries: z.boolean(),
268
+ completedAt: z.string(),
269
+ });
270
+
271
+ const profilerStatusSchema = z.object({
272
+ enabled: z.boolean(),
273
+ mode: z.string().nullable(),
274
+ runId: z.string().nullable(),
275
+ runDir: z.string().nullable(),
276
+ totalBytes: z.number(),
277
+ artifactCount: z.number(),
278
+ budget: profilerBudgetSchema.nullable(),
279
+ lastCompletedRun: profilerLastCompletedRunSchema.nullable(),
280
+ });
281
+
282
+ const detailedHealthSchema = z.object({
283
+ status: z.string(),
284
+ timestamp: z.string(),
285
+ version: z.string(),
286
+ disk: z.object({}).passthrough(),
287
+ memory: z.object({}).passthrough(),
288
+ cpu: z.object({}).passthrough(),
289
+ migrations: z.object({}).passthrough(),
290
+ profiler: profilerStatusSchema.optional(),
291
+ });
292
+
242
293
  // ---------------------------------------------------------------------------
243
294
  // Route definitions
244
295
  // ---------------------------------------------------------------------------
@@ -253,15 +304,7 @@ export function identityRouteDefinitions(): RouteDefinition[] {
253
304
  description:
254
305
  "Returns runtime health including version, disk, memory, CPU, and migration status.",
255
306
  tags: ["system"],
256
- responseBody: z.object({
257
- status: z.string(),
258
- timestamp: z.string(),
259
- version: z.string(),
260
- disk: z.object({}).passthrough(),
261
- memory: z.object({}).passthrough(),
262
- cpu: z.object({}).passthrough(),
263
- migrations: z.object({}).passthrough(),
264
- }),
307
+ responseBody: detailedHealthSchema,
265
308
  },
266
309
  {
267
310
  endpoint: "healthz",
@@ -272,15 +315,7 @@ export function identityRouteDefinitions(): RouteDefinition[] {
272
315
  description:
273
316
  "Alias for /v1/health. Returns runtime health including version, disk, memory, CPU, and migration status.",
274
317
  tags: ["system"],
275
- responseBody: z.object({
276
- status: z.string(),
277
- timestamp: z.string(),
278
- version: z.string(),
279
- disk: z.object({}).passthrough(),
280
- memory: z.object({}).passthrough(),
281
- cpu: z.object({}).passthrough(),
282
- migrations: z.object({}).passthrough(),
283
- }),
318
+ responseBody: detailedHealthSchema,
284
319
  },
285
320
  {
286
321
  endpoint: "identity",