@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
@@ -23,7 +23,9 @@ import { getLogger } from "../util/logger.js";
23
23
  import { getSandboxWorkingDir } from "../util/platform.js";
24
24
  import {
25
25
  SUBAGENT_LIMITS,
26
+ SUBAGENT_ROLE_REGISTRY,
26
27
  type SubagentConfig,
28
+ type SubagentRole,
27
29
  type SubagentState,
28
30
  type SubagentStatus,
29
31
  TERMINAL_STATUSES,
@@ -31,35 +33,76 @@ import {
31
33
 
32
34
  const log = getLogger("subagent-manager");
33
35
 
36
+ /** How long to keep terminal subagent metadata after the live conversation is released (ms). */
37
+ const TERMINAL_RETENTION_MS = 30 * 60 * 1000; // 30 minutes
38
+ /** How often to sweep expired terminal entries (ms). */
39
+ const SWEEP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
40
+
41
+ // ── Skill ID merge helper ──────────────────────────────────────────────
42
+
43
+ /**
44
+ * Merge role-defined skill IDs with caller-provided skill IDs, deduplicating.
45
+ * Exported for direct unit testing.
46
+ */
47
+ export function mergeSkillIds(
48
+ roleSkillIds: string[],
49
+ configSkillIds?: string[],
50
+ ): string[] {
51
+ return [...new Set([...roleSkillIds, ...(configSkillIds ?? [])])];
52
+ }
53
+
34
54
  // ── Default subagent system prompt ──────────────────────────────────────
35
55
 
36
- function buildSubagentSystemPrompt(config: SubagentConfig): string {
56
+ function buildSubagentSystemPrompt(
57
+ config: SubagentConfig,
58
+ role: SubagentRole,
59
+ ): string {
60
+ const roleConfig = SUBAGENT_ROLE_REGISTRY[role];
37
61
  const sections: string[] = [
38
- "You are a focused subagent working on a specific task delegated by a parent assistant.",
39
- "Complete the task thoroughly and concisely.",
62
+ roleConfig.systemPromptPreamble,
40
63
  "",
41
- `## Your Task`,
64
+ "## Your Task",
42
65
  config.objective,
43
66
  ];
44
67
  if (config.context) {
45
68
  sections.push("", "## Context from Parent", config.context);
46
69
  }
70
+ sections.push(
71
+ "",
72
+ "## Constraints",
73
+ `- Role: ${role}`,
74
+ "- You cannot spawn nested subagents.",
75
+ "- Use notify_parent to report important findings or if you are blocked.",
76
+ );
47
77
  return sections.join("\n");
48
78
  }
49
79
 
50
80
  // ── Manager ─────────────────────────────────────────────────────────────
51
81
 
52
82
  interface ManagedSubagent {
53
- conversation: Conversation;
83
+ /** Live conversation — null after the subagent reaches a terminal state and is released. */
84
+ conversation: Conversation | null;
54
85
  state: SubagentState;
55
86
  /** Mutable reference to the parent's current sendToClient. Updated on reconnect. */
56
87
  parentSendToClient: (msg: ServerMessage) => void;
88
+ /** Epoch ms after which this terminal entry can be removed by the TTL sweep. */
89
+ retainedUntil?: number;
90
+ /**
91
+ * Set to true when sendMessage enqueues a follow-up message while the
92
+ * initial objective loop is running. runAgentLoop fires drainQueue without
93
+ * awaiting it, and drainQueue shift()s the item synchronously — so both
94
+ * hasQueuedMessages() and isProcessing() can return false while the drain
95
+ * is still active. This flag lets the finally block in runSubagent defer
96
+ * the release to the TTL sweep rather than tearing down the conversation
97
+ * mid-drain.
98
+ */
99
+ hadEnqueuedMessages?: boolean;
57
100
  }
58
101
 
59
102
  export interface SubagentNotificationInfo {
60
103
  subagentId: string;
61
104
  label: string;
62
- status: "completed" | "failed" | "aborted";
105
+ status: "running" | "completed" | "failed" | "aborted";
63
106
  error?: string;
64
107
  conversationId?: string;
65
108
  }
@@ -76,6 +119,8 @@ export class SubagentManager {
76
119
  private subagents = new Map<string, ManagedSubagent>();
77
120
  /** parentConversationId → Set<subagentId> */
78
121
  private parentToChildren = new Map<string, Set<string>>();
122
+ /** `${parentConversationId}:${normalizedLabel}` → subagentId */
123
+ private labelIndex = new Map<string, string>();
79
124
 
80
125
  /**
81
126
  * Optional callback to inject a completion/failure message into the parent
@@ -119,10 +164,20 @@ export class SubagentManager {
119
164
  );
120
165
  }
121
166
 
167
+ // ── Resolve role ─────────────────────────────────────────────────
168
+ const role: SubagentRole = (config.role as SubagentRole) ?? "general";
169
+ if (!SUBAGENT_ROLE_REGISTRY[role]) {
170
+ throw new Error(
171
+ `Invalid subagent role "${config.role}". Must be one of: ${Object.keys(SUBAGENT_ROLE_REGISTRY).join(", ")}`,
172
+ );
173
+ }
174
+ const roleConfig = SUBAGENT_ROLE_REGISTRY[role];
175
+
122
176
  // ── Create conversation ─────────────────────────────────────────
123
177
  const subagentId = uuid();
124
178
  const conversationRecord = bootstrapConversation({
125
179
  conversationType: "background",
180
+ source: "subagent",
126
181
  origin: "subagent",
127
182
  systemHint: `Subagent: ${config.label}`,
128
183
  });
@@ -141,7 +196,7 @@ export class SubagentManager {
141
196
 
142
197
  const systemPrompt =
143
198
  config.systemPromptOverride ??
144
- buildSubagentSystemPrompt({ ...config, id: subagentId });
199
+ buildSubagentSystemPrompt({ ...config, id: subagentId }, role);
145
200
  const maxTokens = appConfig.maxTokens;
146
201
  const workingDir = getSandboxWorkingDir();
147
202
 
@@ -190,14 +245,45 @@ export class SubagentManager {
190
245
  workingDir,
191
246
  this.broadcastToAllClients, // forward parent's broadcast so tool side-effects (e.g. app_files_changed) reach all clients
192
247
  memoryPolicy,
248
+ undefined, // sharedCesClient
249
+ undefined, // speedOverride
250
+ "5m", // cacheTtl — subagents run tight tool-use loops, 5m is always hot
193
251
  );
194
252
 
195
253
  // Mark conversation as having no direct client — it routes through parent.
196
254
  // This ensures interactive prompts (host attachment reads) fail fast.
197
255
  conversation.updateClient(wrappedSendToClient, true);
256
+ conversation.setIsSubagent(true);
257
+
258
+ // Apply role-based tool filter if the role defines one.
259
+ if (roleConfig.allowedTools) {
260
+ conversation.setSubagentAllowedTools(new Set(roleConfig.allowedTools));
261
+ }
262
+
263
+ // Pre-activate skills defined by the role config, merged with any caller-provided skill IDs.
264
+ const mergedSkillIds = mergeSkillIds(
265
+ roleConfig.skillIds,
266
+ config.preactivatedSkillIds,
267
+ );
268
+ if (mergedSkillIds.length > 0) {
269
+ conversation.setPreactivatedSkillIds(mergedSkillIds);
270
+ }
198
271
 
199
272
  managed.conversation = conversation;
200
273
  this.subagents.set(subagentId, managed);
274
+ const labelKey = `${config.parentConversationId}:${config.label.toLowerCase().trim()}`;
275
+ if (this.labelIndex.has(labelKey)) {
276
+ log.warn(
277
+ {
278
+ label: config.label,
279
+ parentConversationId: config.parentConversationId,
280
+ existingSubagentId: this.labelIndex.get(labelKey),
281
+ newSubagentId: subagentId,
282
+ },
283
+ "Label collision: new subagent overwrites label index entry (previous subagent still accessible by UUID)",
284
+ );
285
+ }
286
+ this.labelIndex.set(labelKey, subagentId);
201
287
 
202
288
  // Track parent → child relationship.
203
289
  if (!this.parentToChildren.has(config.parentConversationId)) {
@@ -240,25 +326,26 @@ export class SubagentManager {
240
326
  const managed = this.subagents.get(subagentId);
241
327
  if (!managed) return;
242
328
 
329
+ // Capture the live conversation — it is non-null at this point because
330
+ // spawn() sets it before firing runSubagent.
331
+ const conversation = managed.conversation!;
332
+
243
333
  // Read the current parent sender so reconnects are picked up.
244
334
  const getSender = () => managed.parentSendToClient;
245
335
 
246
336
  this.setStatus(subagentId, "running", getSender());
247
337
  managed.state.startedAt = Date.now();
248
338
 
249
- const onEvent = managed.conversation.sendToClient;
339
+ const onEvent = conversation.sendToClient;
250
340
 
251
341
  try {
252
342
  // Send the objective as the first user message and process it.
253
- const messageId = await managed.conversation.persistUserMessage(
254
- objective,
255
- [],
256
- );
257
- await managed.conversation.runAgentLoop(objective, messageId, onEvent);
343
+ const messageId = await conversation.persistUserMessage(objective, []);
344
+ await conversation.runAgentLoop(objective, messageId, onEvent);
258
345
 
259
346
  // Agent loop completed successfully.
260
347
  // Copy usage stats from the conversation before sending status (which includes usage).
261
- managed.state.usage = { ...managed.conversation.usageStats };
348
+ managed.state.usage = { ...conversation.usageStats };
262
349
  // Only update state + notify if still non-terminal (guards against abort race).
263
350
  if (!TERMINAL_STATUSES.has(managed.state.status)) {
264
351
  managed.state.completedAt = Date.now();
@@ -267,21 +354,40 @@ export class SubagentManager {
267
354
  log.info({ subagentId }, "Subagent completed");
268
355
 
269
356
  // Notify the parent conversation so the LLM can call subagent_read.
270
- this.notifyParent(managed, "completed", getSender());
357
+ this.notifyParentTerminal(managed, "completed", getSender());
271
358
  }
272
359
  } catch (err) {
273
360
  const errorMsg = err instanceof Error ? err.message : String(err);
274
361
  managed.state.error = errorMsg;
275
362
  managed.state.completedAt = Date.now();
276
- managed.state.usage = { ...managed.conversation.usageStats };
363
+ // Copy usage from the captured conversation reference — managed.conversation
364
+ // may have been nulled by an external dispose() before catch runs.
365
+ managed.state.usage = { ...conversation.usageStats };
277
366
 
278
367
  // Only update status if not already terminal (e.g. aborted).
279
368
  if (!TERMINAL_STATUSES.has(managed.state.status)) {
280
369
  this.setStatus(subagentId, "failed", getSender(), errorMsg);
281
- this.notifyParent(managed, "failed", getSender());
370
+ this.notifyParentTerminal(managed, "failed", getSender());
282
371
  }
283
372
 
284
373
  log.error({ subagentId, err }, "Subagent failed");
374
+ } finally {
375
+ // Release the heavyweight Conversation — output is already persisted in DB.
376
+ // runAgentLoop fires drainQueue without awaiting it, and drainQueue shift()s
377
+ // the next item synchronously — so both hasQueuedMessages() and
378
+ // isProcessing() can return false while a drain is still active. Use the
379
+ // hadEnqueuedMessages flag (set in sendMessage) to detect this case and
380
+ // defer the release to the TTL sweep rather than tearing down mid-drain.
381
+ if (managed.hadEnqueuedMessages) {
382
+ log.debug(
383
+ { subagentId },
384
+ "Deferring conversation release — messages were enqueued during run",
385
+ );
386
+ managed.retainedUntil = Date.now() + TERMINAL_RETENTION_MS;
387
+ this.ensureSweepRunning();
388
+ } else {
389
+ this.releaseConversation(managed);
390
+ }
285
391
  }
286
392
  }
287
393
 
@@ -312,7 +418,7 @@ export class SubagentManager {
312
418
  return false;
313
419
  }
314
420
 
315
- managed.conversation.abort();
421
+ managed.conversation?.abort();
316
422
  managed.state.completedAt = Date.now();
317
423
  if (parentSendToClient) {
318
424
  // Route the status update through the stored parent sender so the
@@ -393,7 +499,8 @@ export class SubagentManager {
393
499
 
394
500
  const managed = this.subagents.get(subagentId);
395
501
  if (!managed) return "not_found";
396
- if (TERMINAL_STATUSES.has(managed.state.status)) return "terminal";
502
+ if (TERMINAL_STATUSES.has(managed.state.status) || !managed.conversation)
503
+ return "terminal";
397
504
 
398
505
  const onEvent = managed.conversation.sendToClient;
399
506
  const requestId = uuid();
@@ -408,13 +515,15 @@ export class SubagentManager {
408
515
  if (result.rejected) {
409
516
  return "sent"; // error event already delivered via onEvent
410
517
  }
518
+ if (result.queued) {
519
+ managed.hadEnqueuedMessages = true;
520
+ }
411
521
  if (!result.queued) {
412
- // Conversation is idlesend directly. Fire-and-forget so we don't block.
413
- const messageId = await managed.conversation.persistUserMessage(
414
- trimmed,
415
- [],
416
- );
417
- managed.conversation
522
+ // Capture conversation before the await managed.conversation may be
523
+ // nulled by an external dispose() while persistUserMessage is awaited.
524
+ const conversation = managed.conversation;
525
+ const messageId = await conversation.persistUserMessage(trimmed, []);
526
+ conversation
418
527
  .runAgentLoop(trimmed, messageId, onEvent)
419
528
  .catch((err) => {
420
529
  log.error({ subagentId, err }, "Subagent message processing failed");
@@ -429,6 +538,15 @@ export class SubagentManager {
429
538
  return this.subagents.get(subagentId)?.state;
430
539
  }
431
540
 
541
+ getByLabel(
542
+ label: string,
543
+ parentConversationId: string,
544
+ ): SubagentState | undefined {
545
+ const key = `${parentConversationId}:${label.toLowerCase().trim()}`;
546
+ const id = this.labelIndex.get(key);
547
+ return id ? this.getState(id) : undefined;
548
+ }
549
+
432
550
  getChildrenOf(parentConversationId: string): SubagentState[] {
433
551
  const children = this.parentToChildren.get(parentConversationId);
434
552
  if (!children) return [];
@@ -465,6 +583,24 @@ export class SubagentManager {
465
583
 
466
584
  // ── Cleanup ───────────────────────────────────────────────────────────
467
585
 
586
+ /**
587
+ * Release the live Conversation from a terminal subagent, keeping only
588
+ * lightweight metadata (state, config, usage) for later queries.
589
+ * The conversation's output is already persisted in the database.
590
+ */
591
+ private releaseConversation(managed: ManagedSubagent): void {
592
+ if (!managed.conversation) return;
593
+ managed.conversation.dispose();
594
+ managed.conversation = null;
595
+ managed.retainedUntil = Date.now() + TERMINAL_RETENTION_MS;
596
+ this.ensureSweepRunning();
597
+
598
+ log.debug(
599
+ { subagentId: managed.state.config.id },
600
+ "Released live conversation for terminal subagent",
601
+ );
602
+ }
603
+
468
604
  /**
469
605
  * Dispose a subagent and remove it from tracking.
470
606
  * Should be called after the subagent reaches a terminal state
@@ -474,12 +610,24 @@ export class SubagentManager {
474
610
  const managed = this.subagents.get(subagentId);
475
611
  if (!managed) return;
476
612
 
477
- if (!TERMINAL_STATUSES.has(managed.state.status)) {
478
- managed.conversation.abort();
613
+ if (managed.conversation) {
614
+ if (!TERMINAL_STATUSES.has(managed.state.status)) {
615
+ managed.conversation.abort();
616
+ }
617
+ managed.conversation.dispose();
618
+ managed.conversation = null;
479
619
  }
480
- managed.conversation.dispose();
481
620
  this.subagents.delete(subagentId);
482
621
 
622
+ // Remove from label index only if it still maps to this subagent
623
+ // (guards against stale delete when a newer subagent reused the label).
624
+ const label = managed.state.config.label;
625
+ const parentConvId = managed.state.config.parentConversationId;
626
+ const labelKey = `${parentConvId}:${label.toLowerCase().trim()}`;
627
+ if (this.labelIndex.get(labelKey) === subagentId) {
628
+ this.labelIndex.delete(labelKey);
629
+ }
630
+
483
631
  // Remove from parent tracking.
484
632
  const parentId = managed.state.config.parentConversationId;
485
633
  const siblings = this.parentToChildren.get(parentId);
@@ -493,11 +641,71 @@ export class SubagentManager {
493
641
 
494
642
  /** Dispose all subagents. Called on daemon shutdown. */
495
643
  disposeAll(): void {
644
+ this.stopSweep();
496
645
  for (const id of [...this.subagents.keys()]) {
497
646
  this.dispose(id);
498
647
  }
499
648
  }
500
649
 
650
+ // ── TTL sweep for terminal metadata ──────────────────────────────────
651
+
652
+ private sweepTimer?: ReturnType<typeof setInterval>;
653
+
654
+ private ensureSweepRunning(): void {
655
+ if (this.sweepTimer) return;
656
+ this.sweepTimer = setInterval(
657
+ () => this.sweepTerminal(),
658
+ SWEEP_INTERVAL_MS,
659
+ );
660
+ // Don't let the sweep timer keep the process alive.
661
+ if (
662
+ this.sweepTimer &&
663
+ typeof this.sweepTimer === "object" &&
664
+ "unref" in this.sweepTimer
665
+ ) {
666
+ (this.sweepTimer as { unref: () => void }).unref();
667
+ }
668
+ }
669
+
670
+ private stopSweep(): void {
671
+ if (this.sweepTimer) {
672
+ clearInterval(this.sweepTimer);
673
+ this.sweepTimer = undefined;
674
+ }
675
+ }
676
+
677
+ /** Remove terminal entries whose retention period has expired. */
678
+ private sweepTerminal(): void {
679
+ const now = Date.now();
680
+ const expired: string[] = [];
681
+ for (const [id, managed] of this.subagents) {
682
+ if (!managed.retainedUntil || now < managed.retainedUntil) continue;
683
+ // If the retention window has expired and the conversation is still live,
684
+ // release it now — the drain has had ample time to complete.
685
+ if (managed.conversation) {
686
+ this.releaseConversation(managed);
687
+ // releaseConversation resets retainedUntil to keep metadata around for
688
+ // another window; the entry will be swept on the next pass.
689
+ continue;
690
+ }
691
+ expired.push(id);
692
+ }
693
+ for (const id of expired) {
694
+ log.debug(
695
+ { subagentId: id },
696
+ "Sweeping expired terminal subagent metadata",
697
+ );
698
+ this.dispose(id);
699
+ }
700
+ // Stop the timer if there are no more entries to sweep.
701
+ const hasTerminal = [...this.subagents.values()].some(
702
+ (s) => s.retainedUntil !== undefined,
703
+ );
704
+ if (!hasTerminal) {
705
+ this.stopSweep();
706
+ }
707
+ }
708
+
501
709
  // ── Internals ─────────────────────────────────────────────────────────
502
710
 
503
711
  private setStatus(
@@ -529,11 +737,82 @@ export class SubagentManager {
529
737
  } as ServerMessage);
530
738
  }
531
739
 
740
+ // ── Child → Parent notification ────────────────────────────────────
741
+
742
+ /**
743
+ * Look up the parent info for a child conversation.
744
+ * Returns undefined if the conversationId doesn't belong to a subagent.
745
+ */
746
+ getParentInfo(childConversationId: string):
747
+ | {
748
+ parentConversationId: string;
749
+ subagentId: string;
750
+ label: string;
751
+ parentSendToClient: (msg: ServerMessage) => void;
752
+ }
753
+ | undefined {
754
+ for (const [subagentId, managed] of this.subagents) {
755
+ if (managed.state.conversationId === childConversationId) {
756
+ return {
757
+ parentConversationId: managed.state.config.parentConversationId,
758
+ subagentId,
759
+ label: managed.state.config.label,
760
+ parentSendToClient: managed.parentSendToClient,
761
+ };
762
+ }
763
+ }
764
+ return undefined;
765
+ }
766
+
767
+ /**
768
+ * Send a notification from a running subagent to its parent conversation.
769
+ * Returns true if the notification was sent, false if the child is not a
770
+ * subagent, is in a terminal state, or the parent callback is not wired.
771
+ */
772
+ notifyParent(
773
+ childConversationId: string,
774
+ message: string,
775
+ urgency: string,
776
+ ): boolean {
777
+ const info = this.getParentInfo(childConversationId);
778
+ if (!info) return false;
779
+
780
+ const managed = this.subagents.get(info.subagentId);
781
+ if (!managed || TERMINAL_STATUSES.has(managed.state.status)) return false;
782
+ if (!this.onSubagentFinished) return false;
783
+
784
+ let notificationString = `[Subagent "${info.label}" — ${urgency}] ${message}`;
785
+ if (urgency === "blocked") {
786
+ notificationString +=
787
+ "\nUse subagent_message to send guidance to this subagent.";
788
+ }
789
+
790
+ try {
791
+ this.onSubagentFinished(
792
+ info.parentConversationId,
793
+ notificationString,
794
+ info.parentSendToClient,
795
+ {
796
+ subagentId: info.subagentId,
797
+ label: info.label,
798
+ status: "running",
799
+ },
800
+ );
801
+ return true;
802
+ } catch (err) {
803
+ log.error(
804
+ { subagentId: info.subagentId, err },
805
+ "Failed to notify parent from subagent",
806
+ );
807
+ return false;
808
+ }
809
+ }
810
+
532
811
  /**
533
812
  * Inject a completion/failure notification into the parent conversation
534
813
  * so the LLM automatically informs the user.
535
814
  */
536
- private notifyParent(
815
+ private notifyParentTerminal(
537
816
  managed: ManagedSubagent,
538
817
  outcome: "completed" | "failed",
539
818
  parentSendToClient: (msg: ServerMessage) => void,
@@ -41,6 +41,8 @@ export interface SubagentConfig {
41
41
  preactivatedSkillIds?: string[];
42
42
  /** Whether the parent should present the result to the user. Defaults to true. */
43
43
  sendResultToUser?: boolean;
44
+ /** Optional role for the subagent. Defaults handled by consumers. */
45
+ role?: SubagentRole;
44
46
  }
45
47
 
46
48
  // ── State (runtime) ─────────────────────────────────────────────────────
@@ -66,3 +68,69 @@ export const SUBAGENT_LIMITS = {
66
68
  /** Max nesting depth (1 = no nested subagents). */
67
69
  maxDepth: 1,
68
70
  } as const;
71
+
72
+ // ── Roles ───────────────────────────────────────────────────────────────
73
+
74
+ export type SubagentRole = "general" | "researcher" | "coder" | "planner";
75
+
76
+ export interface SubagentRoleConfig {
77
+ /**
78
+ * When defined, only these tools are visible to the subagent.
79
+ * `undefined` means no filter (all tools available).
80
+ */
81
+ allowedTools?: string[];
82
+ /** Skill IDs to pre-activate on the subagent conversation. */
83
+ skillIds: string[];
84
+ /** Role-specific text prepended to the subagent system prompt. */
85
+ systemPromptPreamble: string;
86
+ }
87
+
88
+ export const SUBAGENT_ROLE_REGISTRY: Record<SubagentRole, SubagentRoleConfig> =
89
+ {
90
+ general: {
91
+ allowedTools: undefined,
92
+ skillIds: [],
93
+ systemPromptPreamble:
94
+ "You are a general-purpose subagent. Complete the delegated task thoroughly and concisely.",
95
+ },
96
+ researcher: {
97
+ allowedTools: [
98
+ "web_search",
99
+ "web_fetch",
100
+ "file_read",
101
+ "file_list",
102
+ "recall",
103
+ "notify_parent",
104
+ ],
105
+ skillIds: [],
106
+ systemPromptPreamble:
107
+ "You are a research-focused subagent with read-only access. Search the web, read files, and recall memories. You cannot write files or run shell commands.",
108
+ },
109
+ coder: {
110
+ allowedTools: [
111
+ "bash",
112
+ "file_read",
113
+ "file_write",
114
+ "file_edit",
115
+ "web_search",
116
+ "recall",
117
+ "notify_parent",
118
+ ],
119
+ skillIds: [],
120
+ systemPromptPreamble:
121
+ "You are a code-focused subagent with file and shell access. Read, write, and edit files, and run shell commands.",
122
+ },
123
+ planner: {
124
+ allowedTools: [
125
+ "file_read",
126
+ "file_list",
127
+ "web_search",
128
+ "web_fetch",
129
+ "recall",
130
+ "notify_parent",
131
+ ],
132
+ skillIds: [],
133
+ systemPromptPreamble:
134
+ "You are an analysis-focused subagent with read-only access. Read files, search the web, and synthesize findings. You cannot write files or run shell commands.",
135
+ },
136
+ };
@@ -60,10 +60,10 @@ export async function runTask(
60
60
 
61
61
  const run = createTaskRun(task.id);
62
62
  const conversation = bootstrapConversation({
63
- // Schedule-triggered tasks use "standard" so they appear in the sidebar's
64
- // Scheduled section; non-schedule tasks use "background" to stay out of
65
- // the main conversation list.
66
- conversationType: opts.source === "schedule" ? undefined : "background",
63
+ // Schedule-triggered tasks use "scheduled" so they don't crowd out
64
+ // interactive conversations in the main list; non-schedule tasks use
65
+ // "background" to stay out of the list entirely.
66
+ conversationType: opts.source === "schedule" ? "scheduled" : "background",
67
67
  source: opts.source === "schedule" ? "schedule" : "task",
68
68
  scheduleJobId: opts.scheduleJobId,
69
69
  groupId:
@@ -277,10 +277,10 @@ export interface AppRefreshInput {
277
277
  app_id: string;
278
278
  }
279
279
 
280
- export function executeAppRefresh(
280
+ export async function executeAppRefresh(
281
281
  input: AppRefreshInput,
282
282
  store: AppStore,
283
- ): ExecutorResult {
283
+ ): Promise<ExecutorResult> {
284
284
  const app = store.getApp(input.app_id);
285
285
  if (!app) {
286
286
  return {
@@ -289,9 +289,34 @@ export function executeAppRefresh(
289
289
  };
290
290
  }
291
291
 
292
- // Empty update bumps updatedAt timestamp, triggering recompilation and
293
- // surface refresh on the client side.
292
+ // Empty update bumps updatedAt timestamp, triggering surface refresh on
293
+ // the client side.
294
294
  const updated = store.updateApp(input.app_id, {});
295
+
296
+ // Multifile apps need an explicit compile so the LLM sees any errors
297
+ // (bad imports, syntax issues, etc.) instead of silently serving the
298
+ // stale scaffold placeholder from the initial app_create.
299
+ if (app.formatVersion === 2) {
300
+ const appDir = getAppDirPath(input.app_id);
301
+ const compileResult = await compileApp(appDir);
302
+ return {
303
+ content: JSON.stringify({
304
+ refreshed: true,
305
+ appId: updated.id,
306
+ name: updated.name,
307
+ compiled: compileResult.ok,
308
+ ...(compileResult.ok
309
+ ? { compile_duration_ms: compileResult.durationMs }
310
+ : {
311
+ compile_errors: compileResult.errors,
312
+ compile_warnings: compileResult.warnings,
313
+ compile_duration_ms: compileResult.durationMs,
314
+ }),
315
+ }),
316
+ isError: false,
317
+ };
318
+ }
319
+
295
320
  return {
296
321
  content: JSON.stringify({
297
322
  refreshed: true,