@vellumai/assistant 0.5.1 → 0.5.3

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 (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -1,6 +1,11 @@
1
1
  import { v4 as uuid } from "uuid";
2
2
 
3
- import { getApp, getAppPreview, updateApp } from "../memory/app-store.js";
3
+ import {
4
+ getApp,
5
+ getAppPreview,
6
+ resolveAppDir,
7
+ updateApp,
8
+ } from "../memory/app-store.js";
4
9
  import type { ToolExecutionResult } from "../tools/types.js";
5
10
  import { getLogger } from "../util/logger.js";
6
11
  import { isPlainObject } from "../util/object.js";
@@ -167,6 +172,7 @@ export interface SurfaceConversationContext {
167
172
  emit(type: string, message: string, meta?: Record<string, unknown>): void;
168
173
  };
169
174
  sendToClient(msg: ServerMessage): void;
175
+ broadcastToAllClients?(msg: ServerMessage): void;
170
176
  pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
171
177
  lastSurfaceAction: Map<
172
178
  string,
@@ -187,6 +193,7 @@ export interface SurfaceConversationContext {
187
193
  }
188
194
  >;
189
195
  surfaceUndoStacks: Map<string, string[]>;
196
+ accumulatedSurfaceState: Map<string, Record<string, unknown>>;
190
197
  /** Request IDs that originated from surface action button clicks (not regular user messages). */
191
198
  surfaceActionRequestIds: Set<string>;
192
199
  currentTurnSurfaces: Array<{
@@ -346,6 +353,39 @@ function handleDocumentContentChanged(
346
353
  }
347
354
  }
348
355
 
356
+ /**
357
+ * Handle state_update action from a dynamic page.
358
+ * Accumulates state via shallow merge without triggering an LLM turn.
359
+ */
360
+ function handleStateUpdate(
361
+ ctx: SurfaceConversationContext,
362
+ surfaceId: string,
363
+ data?: Record<string, unknown>,
364
+ ): void {
365
+ if (!data) {
366
+ log.debug({ surfaceId }, "state_update action called with no data");
367
+ return;
368
+ }
369
+
370
+ const surfaceState = ctx.surfaceState.get(surfaceId);
371
+ if (!surfaceState || surfaceState.surfaceType !== "dynamic_page") {
372
+ log.warn(
373
+ { surfaceId, surfaceType: surfaceState?.surfaceType },
374
+ "state_update action received for non-dynamic_page surface",
375
+ );
376
+ return;
377
+ }
378
+
379
+ const existing = ctx.accumulatedSurfaceState.get(surfaceId) ?? {};
380
+ const merged = { ...existing, ...data };
381
+ ctx.accumulatedSurfaceState.set(surfaceId, merged);
382
+
383
+ log.debug(
384
+ { surfaceId, accumulatedState: merged },
385
+ "Accumulated surface state updated",
386
+ );
387
+ }
388
+
349
389
  export function pushUndoState(
350
390
  surfaceUndoStacks: Map<string, string[]>,
351
391
  surfaceId: string,
@@ -543,22 +583,76 @@ export function handleSurfaceAction(
543
583
  const pending = ctx.pendingSurfaceActions.get(surfaceId);
544
584
 
545
585
  // When surfaces are restored from history (e.g. onboarding cards), there is
546
- // no in-memory pendingSurfaceActions entry. For relay_prompt / agent_prompt
547
- // actions the client already sends the full payload (including { prompt }),
548
- // so we can handle them without stored state.
586
+ // no in-memory pendingSurfaceActions entry. Handle non-terminal actions
587
+ // directly, and forward custom/relay actions to the LLM.
549
588
  if (!pending) {
589
+ // Non-terminal actions don't need stored state — handle directly.
590
+ if (actionId === "selection_changed") {
591
+ log.debug(
592
+ { surfaceId, data },
593
+ "Selection changed (history-restored, not forwarding)",
594
+ );
595
+ return;
596
+ }
597
+ if (actionId === "content_changed") {
598
+ log.debug(
599
+ { surfaceId },
600
+ "Content changed (history-restored, no surface state — skipping)",
601
+ );
602
+ return;
603
+ }
604
+ if (actionId === "state_update") {
605
+ if (data) {
606
+ const existing = ctx.accumulatedSurfaceState.get(surfaceId) ?? {};
607
+ ctx.accumulatedSurfaceState.set(surfaceId, { ...existing, ...data });
608
+ }
609
+ log.debug(
610
+ { surfaceId, data },
611
+ "Silent state accumulated (history-restored)",
612
+ );
613
+ return;
614
+ }
615
+
616
+ // Determine message content from the action.
550
617
  const isRelay = actionId === "relay_prompt" || actionId === "agent_prompt";
551
618
  const prompt =
552
619
  isRelay && typeof data?.prompt === "string" ? data.prompt.trim() : "";
553
620
 
554
- if (!prompt) {
555
- log.warn({ surfaceId, actionId }, "No pending surface action found");
556
- return;
621
+ // Read accumulated state once — used by both relay and custom action paths.
622
+ const accState = ctx.accumulatedSurfaceState.get(surfaceId);
623
+ const hasAccState = accState && Object.keys(accState).length > 0;
624
+
625
+ let content: string;
626
+ let displayContent: string | undefined;
627
+ if (prompt) {
628
+ content = prompt;
629
+ // Re-append accumulated state so the LLM sees it, matching the pending path.
630
+ if (hasAccState) {
631
+ content += `\n\nAccumulated surface state: ${JSON.stringify(accState)}`;
632
+ }
633
+ } else {
634
+ // Custom action from an app (e.g. sendAction('answer_selected', {...}))
635
+ const summary = actionId
636
+ .replace(/_/g, " ")
637
+ .replace(/\b\w/g, (c) => c.toUpperCase());
638
+ content = `[User action on app: ${summary}]`;
639
+ if (data && Object.keys(data).length > 0) {
640
+ content += `\n\nAction data: ${JSON.stringify(data)}`;
641
+ }
642
+ if (hasAccState) {
643
+ content += `\n\nAccumulated surface state: ${JSON.stringify(accState)}`;
644
+ }
645
+ displayContent = summary;
557
646
  }
558
647
 
559
648
  const requestId = uuid();
560
649
  ctx.surfaceActionRequestIds.add(requestId);
561
- const onEvent = (msg: ServerMessage) => ctx.sendToClient(msg);
650
+ // Use broadcastToAllClients (publishes to the SSE event hub) instead of
651
+ // sendToClient, which is reset to a no-op between HTTP requests. Without
652
+ // this, surface action responses are persisted to DB but never reach the
653
+ // client's SSE stream.
654
+ const emit = ctx.broadcastToAllClients ?? ctx.sendToClient.bind(ctx);
655
+ const onEvent = (msg: ServerMessage) => emit(msg);
562
656
 
563
657
  ctx.traceEmitter.emit("request_received", "Surface action received", {
564
658
  requestId,
@@ -567,11 +661,15 @@ export function handleSurfaceAction(
567
661
  });
568
662
 
569
663
  const result = ctx.enqueueMessage(
570
- prompt,
664
+ content,
571
665
  [],
572
666
  onEvent,
573
667
  requestId,
574
668
  surfaceId,
669
+ undefined,
670
+ undefined,
671
+ undefined,
672
+ displayContent,
575
673
  );
576
674
 
577
675
  if (result.rejected) {
@@ -579,18 +677,26 @@ export function handleSurfaceAction(
579
677
  return;
580
678
  }
581
679
 
680
+ // One-shot: clear accumulated state now that the message has been accepted.
681
+ // Deferred until after rejection check so state is preserved for retry on rejection.
682
+ if (hasAccState) {
683
+ ctx.accumulatedSurfaceState.delete(surfaceId);
684
+ }
685
+
582
686
  // Echo the prompt to the client so it appears in the chat UI.
583
687
  // Deferred until after rejection check to avoid ghost messages.
584
- ctx.sendToClient({
585
- type: "user_message_echo",
586
- text: prompt,
587
- conversationId: ctx.conversationId,
588
- });
688
+ if (prompt) {
689
+ emit({
690
+ type: "user_message_echo",
691
+ text: prompt,
692
+ conversationId: ctx.conversationId,
693
+ });
694
+ }
589
695
 
590
696
  if (result.queued) {
591
697
  log.info(
592
698
  { surfaceId, actionId, requestId },
593
- "Relay prompt queued (conversation busy, history-restored)",
699
+ "Surface action queued (conversation busy, history-restored)",
594
700
  );
595
701
  return;
596
702
  }
@@ -598,22 +704,31 @@ export function handleSurfaceAction(
598
704
  // Conversation is idle — process the message immediately.
599
705
  log.info(
600
706
  { surfaceId, actionId, requestId },
601
- "Processing relay prompt immediately (history-restored)",
707
+ "Processing surface action immediately (history-restored)",
602
708
  );
603
709
  ctx
604
- .processMessage(prompt, [], onEvent, requestId, surfaceId)
710
+ .processMessage(
711
+ content,
712
+ [],
713
+ onEvent,
714
+ requestId,
715
+ surfaceId,
716
+ undefined,
717
+ undefined,
718
+ displayContent,
719
+ )
605
720
  .catch((err) => {
606
721
  const message = err instanceof Error ? err.message : String(err);
607
722
  log.error(
608
723
  { err, surfaceId, actionId },
609
- "Failed to process history-restored relay prompt",
724
+ "Failed to process history-restored surface action",
610
725
  );
611
726
  onEvent(
612
727
  buildConversationErrorMessage(ctx.conversationId, {
613
728
  code: "CONVERSATION_PROCESSING_FAILED",
614
729
  userMessage: `Something went wrong: ${message}`,
615
730
  retryable: false,
616
- debugDetails: `History-restored relay prompt processing failed: ${message}`,
731
+ debugDetails: `History-restored surface action processing failed: ${message}`,
617
732
  errorCategory: "processing_failed",
618
733
  }),
619
734
  );
@@ -637,6 +752,14 @@ export function handleSurfaceAction(
637
752
  handleDocumentContentChanged(ctx, surfaceId, data);
638
753
  return;
639
754
  }
755
+
756
+ // state_update is a silent accumulation action — merge data into accumulated
757
+ // state without triggering an LLM turn.
758
+ if (actionId === "state_update") {
759
+ handleStateUpdate(ctx, surfaceId, data);
760
+ return;
761
+ }
762
+
640
763
  // Merge stored action-level data (from ui_show definition) with client-sent
641
764
  // data. This is critical for relay_prompt buttons: the client only sends the
642
765
  // actionId, but the prompt payload lives in the action definition's data.
@@ -663,11 +786,16 @@ export function handleSurfaceAction(
663
786
  surfaceData,
664
787
  );
665
788
 
789
+ // Use broadcastToAllClients so events reach the SSE hub — sendToClient is
790
+ // reset to a no-op between HTTP requests (see history-restored path for
791
+ // full rationale).
792
+ const emit = ctx.broadcastToAllClients ?? ctx.sendToClient.bind(ctx);
793
+
666
794
  // Forms are one-shot surfaces — auto-complete immediately so the client
667
795
  // transitions from the "Submitting…" spinner to a completion chip without
668
796
  // requiring the LLM to call ui_dismiss.
669
797
  if (pending.surfaceType === "form") {
670
- ctx.sendToClient({
798
+ emit({
671
799
  type: "ui_surface_complete",
672
800
  conversationId: ctx.conversationId,
673
801
  surfaceId,
@@ -694,6 +822,10 @@ export function handleSurfaceAction(
694
822
  selectedIds,
695
823
  );
696
824
  }
825
+ const accumulatedState = ctx.accumulatedSurfaceState.get(surfaceId);
826
+ if (accumulatedState && Object.keys(accumulatedState).length > 0) {
827
+ fallbackContent += `\n\nAccumulated surface state: ${JSON.stringify(accumulatedState)}`;
828
+ }
697
829
  // When a relay_prompt button also carries selection data (e.g. list/table
698
830
  // surface with a canned prompt + user-selected rows), append the selection
699
831
  // context so the LLM sees both the prompt and the user's selections.
@@ -707,6 +839,11 @@ export function handleSurfaceAction(
707
839
  );
708
840
  }
709
841
  }
842
+ // When prompt is truthy, fallbackContent (which includes accumulated state)
843
+ // is discarded. Re-append accumulated state so the LLM sees it.
844
+ if (prompt && accumulatedState && Object.keys(accumulatedState).length > 0) {
845
+ content += `\n\nAccumulated surface state: ${JSON.stringify(accumulatedState)}`;
846
+ }
710
847
  // Show the user plain-text instead of raw JSON action data.
711
848
  const displayContent = prompt
712
849
  ? undefined
@@ -719,7 +856,7 @@ export function handleSurfaceAction(
719
856
 
720
857
  const requestId = uuid();
721
858
  ctx.surfaceActionRequestIds.add(requestId);
722
- const onEvent = (msg: ServerMessage) => ctx.sendToClient(msg);
859
+ const onEvent = (msg: ServerMessage) => emit(msg);
723
860
 
724
861
  ctx.traceEmitter.emit("request_received", "Surface action received", {
725
862
  requestId,
@@ -743,10 +880,16 @@ export function handleSurfaceAction(
743
880
  return;
744
881
  }
745
882
 
883
+ // One-shot: clear accumulated state now that the message has been accepted.
884
+ // Deferred until after rejection check so state is preserved for retry on rejection.
885
+ if (accumulatedState && Object.keys(accumulatedState).length > 0) {
886
+ ctx.accumulatedSurfaceState.delete(surfaceId);
887
+ }
888
+
746
889
  // Echo the user's prompt to the client so it appears in the chat UI.
747
890
  // Deferred until after rejection check to avoid ghost messages.
748
891
  if (shouldRelayPrompt && prompt) {
749
- ctx.sendToClient({
892
+ emit({
750
893
  type: "user_message_echo",
751
894
  text: prompt,
752
895
  conversationId: ctx.conversationId,
@@ -811,7 +954,7 @@ export function handleSurfaceAction(
811
954
  }
812
955
 
813
956
  /**
814
- * After an app_update, refresh any active surface that displays the updated app.
957
+ * After an app_refresh, refresh any active surface that displays the updated app.
815
958
  */
816
959
  export function refreshSurfacesForApp(
817
960
  ctx: SurfaceConversationContext,
@@ -860,7 +1003,7 @@ export function refreshSurfacesForApp(
860
1003
  refreshed = true;
861
1004
  log.info(
862
1005
  { conversationId: ctx.conversationId, surfaceId, appId },
863
- "Auto-refreshed surface after app_update",
1006
+ "Auto-refreshed surface after app_refresh",
864
1007
  );
865
1008
  }
866
1009
  return refreshed;
@@ -1201,6 +1344,7 @@ export async function surfaceProxyResolver(
1201
1344
  ctx.surfaceState.delete(surfaceId);
1202
1345
  ctx.surfaceUndoStacks.delete(surfaceId);
1203
1346
  ctx.lastSurfaceAction.delete(surfaceId);
1347
+ ctx.accumulatedSurfaceState.delete(surfaceId);
1204
1348
  return {
1205
1349
  content: lastAction ? "Surface completed" : "Surface dismissed",
1206
1350
  isError: false,
@@ -1219,9 +1363,11 @@ export async function surfaceProxyResolver(
1219
1363
  const defaultPreview = { title: app.name, subtitle: app.description };
1220
1364
 
1221
1365
  const storedPreview = getAppPreview(app.id);
1366
+ const { dirName } = resolveAppDir(app.id);
1222
1367
  const surfaceData: DynamicPageSurfaceData = {
1223
1368
  html: app.htmlDefinition,
1224
1369
  appId: app.id,
1370
+ dirName,
1225
1371
  preview: {
1226
1372
  ...defaultPreview,
1227
1373
  ...preview,
@@ -560,8 +560,6 @@ export interface SkillProjectionContext {
560
560
  };
561
561
  /** True when no client is connected (HTTP-only). */
562
562
  readonly hasNoClient?: boolean;
563
- /** True when the conversation has user-uploaded attachments. */
564
- hasAttachments?: boolean;
565
563
  }
566
564
 
567
565
  // ── Conditional tool sets ────────────────────────────────────────────
@@ -573,7 +571,6 @@ const HOST_TOOL_NAMES = new Set([
573
571
  "host_file_edit",
574
572
  "host_bash",
575
573
  ]);
576
- const ASSET_TOOL_NAMES = new Set(["asset_search", "asset_materialize"]);
577
574
  const CLIENT_CAPABILITY_TOOL_NAMES = new Set(["app_open"]);
578
575
  const PLATFORM_TOOL_NAMES = new Set(["request_system_permission"]);
579
576
 
@@ -597,9 +594,6 @@ export function isToolActiveForContext(
597
594
  // unchecked host command execution on the daemon host.
598
595
  return !ctx.hasNoClient;
599
596
  }
600
- if (ASSET_TOOL_NAMES.has(name)) {
601
- return ctx.hasAttachments ?? false;
602
- }
603
597
  if (CLIENT_CAPABILITY_TOOL_NAMES.has(name)) {
604
598
  return !ctx.hasNoClient;
605
599
  }
@@ -1,3 +1,7 @@
1
+ import { join } from "node:path";
2
+
3
+ import { getConversation } from "../memory/conversation-crud.js";
4
+ import { resolveConversationDirectoryPaths } from "../memory/conversation-directories.js";
1
5
  import { renderWorkspaceTopLevelContext } from "../workspace/top-level-renderer.js";
2
6
  import { scanTopLevelDirectories } from "../workspace/top-level-scanner.js";
3
7
 
@@ -5,6 +9,7 @@ import { scanTopLevelDirectories } from "../workspace/top-level-scanner.js";
5
9
  * Subset of Conversation state that workspace context helpers need.
6
10
  */
7
11
  export interface WorkspaceConversationContext {
12
+ conversationId: string;
8
13
  workingDir: string;
9
14
  workspaceTopLevelContext: string | null;
10
15
  workspaceTopLevelDirty: boolean;
@@ -17,6 +22,21 @@ export function refreshWorkspaceTopLevelContextIfNeeded(
17
22
  if (!ctx.workspaceTopLevelDirty && ctx.workspaceTopLevelContext != null)
18
23
  return;
19
24
  const snapshot = scanTopLevelDirectories(ctx.workingDir);
20
- ctx.workspaceTopLevelContext = renderWorkspaceTopLevelContext(snapshot);
25
+ const conversation = getConversation(ctx.conversationId);
26
+ let currentConversationPath: string | null = null;
27
+ if (conversation && typeof conversation.createdAt === "number") {
28
+ const { resolvedDirName } = resolveConversationDirectoryPaths(
29
+ conversation.id,
30
+ conversation.createdAt,
31
+ join(ctx.workingDir, "conversations"),
32
+ );
33
+ currentConversationPath = `conversations/${resolvedDirName}/`;
34
+ }
35
+ ctx.workspaceTopLevelContext = renderWorkspaceTopLevelContext(snapshot, {
36
+ currentConversationPath,
37
+ currentConversationAttachmentsPath: currentConversationPath
38
+ ? `${currentConversationPath}attachments/`
39
+ : null,
40
+ });
21
41
  ctx.workspaceTopLevelDirty = false;
22
42
  }
@@ -81,7 +81,6 @@ import type {
81
81
  ChannelCapabilities,
82
82
  TrustContext,
83
83
  } from "./conversation-runtime-assembly.js";
84
- import { messagesContainAttachments } from "./conversation-runtime-assembly.js";
85
84
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
86
85
  import {
87
86
  createSurfaceMutex,
@@ -164,7 +163,6 @@ export class Conversation {
164
163
  /** @internal */ contextCompactedAt: number | null = null;
165
164
  /** @internal */ currentRequestId?: string;
166
165
  /** @internal */ hasNoClient = false;
167
- /** @internal */ hasAttachments = false;
168
166
  /** @internal */ headlessLock = false;
169
167
  /** @internal */ taskRunId?: string;
170
168
  /** @internal */ callSessionId?: string;
@@ -197,9 +195,24 @@ export class Conversation {
197
195
  >();
198
196
  /** @internal */ surfaceState = new Map<
199
197
  string,
200
- { surfaceType: SurfaceType; data: SurfaceData; title?: string }
198
+ {
199
+ surfaceType: SurfaceType;
200
+ data: SurfaceData;
201
+ title?: string;
202
+ actions?: Array<{
203
+ id: string;
204
+ label: string;
205
+ style?: string;
206
+ data?: Record<string, unknown>;
207
+ }>;
208
+ }
201
209
  >();
202
210
  /** @internal */ surfaceUndoStacks = new Map<string, string[]>();
211
+ /** @internal */ accumulatedSurfaceState = new Map<
212
+ string,
213
+ Record<string, unknown>
214
+ >();
215
+ /** @internal */ broadcastToAllClients?: (msg: ServerMessage) => void;
203
216
  /** @internal */ withSurface = createSurfaceMutex();
204
217
  /** @internal */ currentTurnSurfaces: Array<{
205
218
  surfaceId: string;
@@ -246,6 +259,7 @@ export class Conversation {
246
259
  this.provider = provider;
247
260
  this.workingDir = workingDir;
248
261
  this.sendToClient = sendToClient;
262
+ this.broadcastToAllClients = broadcastToAllClients;
249
263
  this.memoryPolicy = memoryPolicy
250
264
  ? { ...memoryPolicy }
251
265
  : { ...DEFAULT_MEMORY_POLICY };
@@ -379,13 +393,41 @@ export class Conversation {
379
393
 
380
394
  async loadFromDb(): Promise<void> {
381
395
  await loadFromDbImpl(this);
382
- // Scan loaded history for attachment content blocks so that asset
383
- // tools are available when resuming a conversation that already had
384
- // attachments. One-way: once true it stays true for the conversation.
385
- // Also picks up the hasAttachments flag set by loadFromDbImpl which
386
- // scans compacted (sliced-off) messages that aren't in this.messages.
387
- if (!this.hasAttachments && messagesContainAttachments(this.messages)) {
388
- this.hasAttachments = true;
396
+ this.restoreSurfaceStateFromHistory();
397
+ }
398
+
399
+ /**
400
+ * Scan loaded conversation history for ui_surface content blocks and
401
+ * populate surfaceState so that findConversationBySurfaceId works for
402
+ * surfaces restored from history (e.g. after daemon restart).
403
+ *
404
+ * Only scans live (non-compacted) messages in this.messages — not all DB
405
+ * rows — because surface IDs are not globally unique and restoring stale
406
+ * compacted surfaces would let findConversationBySurfaceId route actions
407
+ * to the wrong conversation.
408
+ */
409
+ private restoreSurfaceStateFromHistory(): void {
410
+ this.surfaceState.clear();
411
+ for (const msg of this.messages) {
412
+ if (!Array.isArray(msg.content)) continue;
413
+ for (const block of msg.content) {
414
+ const b = block as unknown as Record<string, unknown>;
415
+ if (b.type === "ui_surface" && typeof b.surfaceId === "string") {
416
+ this.surfaceState.set(b.surfaceId, {
417
+ surfaceType: (b.surfaceType ?? "dynamic_page") as SurfaceType,
418
+ data: (b.data ?? {}) as SurfaceData,
419
+ title: b.title as string | undefined,
420
+ actions: Array.isArray(b.actions)
421
+ ? (b.actions as Array<{
422
+ id: string;
423
+ label: string;
424
+ style?: string;
425
+ data?: Record<string, unknown>;
426
+ }>)
427
+ : undefined,
428
+ });
429
+ }
430
+ }
389
431
  }
390
432
  }
391
433
 
@@ -606,6 +648,18 @@ export class Conversation {
606
648
  "Resuming after approval",
607
649
  );
608
650
 
651
+ // Sync the canonical guardian request status so stale "pending" DB
652
+ // records don't get matched by later guardian reply routing. Best-effort:
653
+ // CAS may harmlessly fail if the canonical decision primitive already
654
+ // resolved the request (e.g. channel approval path).
655
+ try {
656
+ resolveCanonicalGuardianRequest(requestId, "pending", {
657
+ status: resolvedState,
658
+ });
659
+ } catch {
660
+ // Canonical request tracking should not break the primary approval flow.
661
+ }
662
+
609
663
  // Cascade to other pending confirmations that match this decision
610
664
  this.cascadePendingApprovals(requestId, decision, selectedPattern);
611
665
  }
@@ -648,10 +702,11 @@ export class Conversation {
648
702
  // Consume from pending-interactions tracker
649
703
  pendingInteractions.resolve(candidateId);
650
704
 
651
- // Resolve via handleConfirmationResponse which emits events.
652
- // Use simple "allow"/"deny" so the permission-checker won't save
653
- // duplicate rules or re-activate temporary modes. Recursion
654
- // terminates because allow/deny exit cascadePendingApprovals early.
705
+ // Resolve via handleConfirmationResponse which emits events and
706
+ // syncs canonical status. Use simple "allow"/"deny" so the
707
+ // permission-checker won't save duplicate rules or re-activate
708
+ // temporary modes. Recursion terminates because allow/deny exit
709
+ // cascadePendingApprovals early.
655
710
  this.handleConfirmationResponse(
656
711
  candidateId,
657
712
  cascadeResult.allow ? "allow" : "deny",
@@ -663,17 +718,6 @@ export class Conversation {
663
718
  causedByRequestId: primaryRequestId,
664
719
  },
665
720
  );
666
-
667
- // Sync the canonical guardian request status for the cascaded request.
668
- // Best-effort: canonical request tracking should not break the cascade flow.
669
- try {
670
- const targetStatus = cascadeResult.allow ? "approved" : "denied";
671
- resolveCanonicalGuardianRequest(candidateId, "pending", {
672
- status: targetStatus,
673
- });
674
- } catch {
675
- // Ignore — canonical request tracking is best-effort
676
- }
677
721
  }
678
722
  }
679
723
 
@@ -880,11 +924,6 @@ export class Conversation {
880
924
  if (!this.processing) {
881
925
  await this.ensureActorScopedHistory();
882
926
  }
883
- // One-way flag: once an attachment arrives, asset tools stay available
884
- // for the remainder of the conversation.
885
- if (!this.hasAttachments && attachments.length > 0) {
886
- this.hasAttachments = true;
887
- }
888
927
  return persistUserMessageImpl(
889
928
  this,
890
929
  content,
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+
3
+ import { getWorkspacePromptPath } from "../util/platform.js";
4
+
5
+ /**
6
+ * The canned assistant response for the wake-up greeting on a fresh workspace.
7
+ * Warm, non-presumptuous greeting that communicates "I'm new," "I improve over
8
+ * time," "I'm ready to be useful," and "you're in control."
9
+ */
10
+ export const CANNED_FIRST_GREETING =
11
+ "Hey. I'm brand new, no name, no memories, nothing yet. The more we work together, the more context and memory I build, and the better I get. But let's not wait around. Throw a question at me, give me a task, or ask what I can do.";
12
+
13
+ /**
14
+ * Returns `true` when all of the following are true:
15
+ * - `conversationMessageCount === 0` (no prior messages in this conversation)
16
+ * - BOOTSTRAP.md exists at the workspace prompt path
17
+ * - The trimmed content matches the macOS wake-up greeting (case-insensitive)
18
+ */
19
+ export function isWakeUpGreeting(
20
+ content: string,
21
+ conversationMessageCount: number,
22
+ ): boolean {
23
+ if (conversationMessageCount !== 0) return false;
24
+ if (!existsSync(getWorkspacePromptPath("BOOTSTRAP.md"))) return false;
25
+ return content.trim().toLowerCase() === "wake up, my friend.";
26
+ }
27
+
28
+ /**
29
+ * Returns the canned first-greeting string. Simple getter that exists to keep
30
+ * the call site consistent and allow future flexibility (e.g., locale-aware
31
+ * greetings) without changing the API.
32
+ */
33
+ export function getCannedFirstGreeting(): string {
34
+ return CANNED_FIRST_GREETING;
35
+ }