@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
@@ -68,6 +68,39 @@ function filterMessagesForUntrustedActor(messages: MessageRow[]): MessageRow[] {
68
68
  });
69
69
  }
70
70
 
71
+ /**
72
+ * Re-inject image source path annotations into message content blocks.
73
+ *
74
+ * When the desktop client attaches images from local files, the source paths
75
+ * are stored in `metadata.imageSourcePaths` (keyed by filename). The LLM-facing
76
+ * content omits these paths at persistence time, so we re-inject them when
77
+ * loading history from the DB. Only user messages are annotated.
78
+ */
79
+ export function reinjectImageSourcePaths(
80
+ content: ContentBlock[],
81
+ role: string,
82
+ metadataJson: string | null,
83
+ ): ContentBlock[] {
84
+ if (role !== "user" || !metadataJson) return content;
85
+ try {
86
+ const meta = JSON.parse(metadataJson);
87
+ if (!meta.imageSourcePaths || typeof meta.imageSourcePaths !== "object") {
88
+ return content;
89
+ }
90
+ const paths = Object.values(meta.imageSourcePaths).filter(
91
+ (v): v is string => typeof v === "string",
92
+ );
93
+ if (paths.length === 0) return content;
94
+ const annotation = paths
95
+ .map((p) => `[Attached image source: ${p}]`)
96
+ .join("\n");
97
+ return [...content, { type: "text" as const, text: annotation }];
98
+ } catch {
99
+ // metadata parse failure — skip annotation, not critical
100
+ return content;
101
+ }
102
+ }
103
+
71
104
  // ── Context Interfaces ───────────────────────────────────────────────
72
105
 
73
106
  export interface LoadFromDbContext {
@@ -78,7 +111,6 @@ export interface LoadFromDbContext {
78
111
  contextCompactedAt: number | null;
79
112
  trustContext?: { trustClass: TrustClass };
80
113
  loadedHistoryTrustClass?: TrustClass;
81
- hasAttachments?: boolean;
82
114
  }
83
115
 
84
116
  export interface AbortContext {
@@ -91,8 +123,19 @@ export interface AbortContext {
91
123
  surfaceActionRequestIds: Set<string>;
92
124
  surfaceState: Map<
93
125
  string,
94
- { surfaceType: SurfaceType; data: SurfaceData; title?: string }
126
+ {
127
+ surfaceType: SurfaceType;
128
+ data: SurfaceData;
129
+ title?: string;
130
+ actions?: Array<{
131
+ id: string;
132
+ label: string;
133
+ style?: string;
134
+ data?: Record<string, unknown>;
135
+ }>;
136
+ }
95
137
  >;
138
+ accumulatedSurfaceState: Map<string, Record<string, unknown>>;
96
139
  readonly queue: MessageQueue;
97
140
  }
98
141
 
@@ -151,6 +194,9 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
151
194
  );
152
195
  content = [{ type: "text", text: m.content }];
153
196
  }
197
+
198
+ content = reinjectImageSourcePaths(content, role, m.metadata);
199
+
154
200
  return { role, content };
155
201
  });
156
202
 
@@ -182,20 +228,6 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
182
228
 
183
229
  ctx.loadedHistoryTrustClass = trustClass;
184
230
 
185
- // Scan ALL db messages (including compacted ones) for attachments so that
186
- // asset tools remain available after context compaction.
187
- if (
188
- ctx.contextCompactedMessageCount > 0 &&
189
- dbMessages.some(
190
- (m) =>
191
- m.role === "user" &&
192
- (m.content.includes('"type":"image"') ||
193
- m.content.includes('"type":"file"')),
194
- )
195
- ) {
196
- ctx.hasAttachments = true;
197
- }
198
-
199
231
  log.info(
200
232
  { conversationId: ctx.conversationId, count: ctx.messages.length },
201
233
  "Loaded messages from DB",
@@ -216,6 +248,7 @@ export function abortConversation(ctx: AbortContext): void {
216
248
  ctx.pendingSurfaceActions.clear();
217
249
  ctx.surfaceActionRequestIds.clear();
218
250
  ctx.surfaceState.clear();
251
+ ctx.accumulatedSurfaceState.clear();
219
252
  unregisterWatchNotifiers(ctx.conversationId);
220
253
  for (const queued of ctx.queue) {
221
254
  queued.onEvent({
@@ -247,6 +280,7 @@ export function disposeConversation(ctx: DisposeContext): void {
247
280
  ctx.pendingSurfaceActions.clear();
248
281
  ctx.surfaceActionRequestIds.clear();
249
282
  ctx.surfaceState.clear();
283
+ ctx.accumulatedSurfaceState.clear();
250
284
  ctx.lastSurfaceAction.clear();
251
285
  ctx.workspaceTopLevelContext = null;
252
286
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { v4 as uuid } from "uuid";
9
9
 
10
+ import { enrichMessageWithSourcePaths } from "../agent/attachments.js";
10
11
  import { createUserMessage } from "../agent/message-types.js";
11
12
  import type {
12
13
  TurnChannelContext,
@@ -14,17 +15,23 @@ import type {
14
15
  } from "../channels/types.js";
15
16
  import { parseChannelId, parseInterfaceId } from "../channels/types.js";
16
17
  import {
18
+ attachInlineAttachmentToMessage,
19
+ attachmentExists,
17
20
  AttachmentUploadError,
18
21
  linkAttachmentToMessage,
19
- uploadAttachment,
20
22
  validateAttachmentUpload,
21
23
  } from "../memory/attachments-store.js";
22
24
  import {
23
25
  addMessage,
26
+ getConversation,
24
27
  provenanceFromTrustContext,
25
28
  setConversationOriginChannelIfUnset,
26
29
  setConversationOriginInterfaceIfUnset,
27
30
  } from "../memory/conversation-crud.js";
31
+ import {
32
+ syncMessageToDisk,
33
+ updateMetaFile,
34
+ } from "../memory/conversation-disk-view.js";
28
35
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
29
36
  import type { Message } from "../providers/types.js";
30
37
  import { getLogger } from "../util/logger.js";
@@ -276,17 +283,20 @@ export async function persistUserMessage(
276
283
  ctx.processing = true;
277
284
  ctx.abortController = new AbortController();
278
285
 
279
- const userMessage = createUserMessage(
280
- content,
281
- attachments.map((attachment) => ({
282
- id: attachment.id,
283
- filename: attachment.filename,
284
- mimeType: attachment.mimeType,
285
- data: attachment.data,
286
- extractedText: attachment.extractedText,
287
- })),
286
+ const attachmentInputs = attachments.map((attachment) => ({
287
+ id: attachment.id,
288
+ filename: attachment.filename,
289
+ mimeType: attachment.mimeType,
290
+ data: attachment.data,
291
+ extractedText: attachment.extractedText,
292
+ filePath: attachment.filePath,
293
+ }));
294
+ const cleanMessage = createUserMessage(content, attachmentInputs);
295
+ const llmMessage = enrichMessageWithSourcePaths(
296
+ cleanMessage,
297
+ attachmentInputs,
288
298
  );
289
- ctx.messages.push(userMessage);
299
+ ctx.messages.push(llmMessage);
290
300
 
291
301
  try {
292
302
  const turnCtx =
@@ -294,6 +304,14 @@ export async function persistUserMessage(
294
304
  const turnIfCtx =
295
305
  extractTurnInterfaceContext(metadata) ?? ctx.getTurnInterfaceContext();
296
306
  const provenance = provenanceFromTrustContext(ctx.trustContext);
307
+ const imageSourcePaths: Record<string, string> = {};
308
+ for (let i = 0; i < attachments.length; i++) {
309
+ const a = attachments[i];
310
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
311
+ imageSourcePaths[`${i}:${a.filename}`] = a.filePath;
312
+ }
313
+ }
314
+
297
315
  const mergedMetadata = {
298
316
  ...(metadata ?? {}),
299
317
  ...provenance,
@@ -309,6 +327,7 @@ export async function persistUserMessage(
309
327
  assistantMessageInterface: turnIfCtx.assistantMessageInterface,
310
328
  }
311
329
  : {}),
330
+ ...(Object.keys(imageSourcePaths).length > 0 ? { imageSourcePaths } : {}),
312
331
  };
313
332
 
314
333
  // When displayContent is provided (e.g. original text before recording
@@ -317,18 +336,9 @@ export async function persistUserMessage(
317
336
  // the stripped content.
318
337
  const contentToPersist = displayContent
319
338
  ? JSON.stringify(
320
- createUserMessage(
321
- displayContent,
322
- attachments.map((a) => ({
323
- id: a.id,
324
- filename: a.filename,
325
- mimeType: a.mimeType,
326
- data: a.data,
327
- extractedText: a.extractedText,
328
- })),
329
- ).content,
339
+ createUserMessage(displayContent, attachmentInputs).content,
330
340
  )
331
- : JSON.stringify(userMessage.content);
341
+ : JSON.stringify(cleanMessage.content);
332
342
  const persistedUserMessage = await addMessage(
333
343
  ctx.conversationId,
334
344
  "user",
@@ -349,15 +359,33 @@ export async function persistUserMessage(
349
359
  );
350
360
  }
351
361
 
362
+ // Rewrite meta.json so the on-disk metadata reflects the origin channel
363
+ if (turnCtx || turnIfCtx) {
364
+ const convForMeta = getConversation(ctx.conversationId);
365
+ if (convForMeta) {
366
+ updateMetaFile(convForMeta);
367
+ }
368
+ }
369
+
352
370
  if (!persistedUserMessage.id) {
353
371
  throw new Error("Failed to persist user message");
354
372
  }
355
373
 
356
- // Index user attachments in the attachments table so asset_search can find them.
374
+ // Index user attachments in the attachments table for later retrieval.
357
375
  for (let i = 0; i < attachments.length; i++) {
358
376
  const a = attachments[i];
359
- if (!a.data) continue;
360
377
  try {
378
+ // If the attachment already exists in the store (e.g. file-backed
379
+ // attachments uploaded separately), link it directly without
380
+ // re-uploading. This handles the case where data is empty because
381
+ // the attachment content lives on disk.
382
+ if (a.id && attachmentExists(a.id)) {
383
+ linkAttachmentToMessage(persistedUserMessage.id, a.id, i);
384
+ continue;
385
+ }
386
+
387
+ if (!a.data) continue;
388
+
361
389
  const validation = validateAttachmentUpload(a.filename, a.mimeType);
362
390
  if (!validation.ok) {
363
391
  log.warn(
@@ -366,8 +394,14 @@ export async function persistUserMessage(
366
394
  );
367
395
  continue;
368
396
  }
369
- const stored = uploadAttachment(a.filename, a.mimeType, a.data);
370
- linkAttachmentToMessage(persistedUserMessage.id, stored.id, i);
397
+ attachInlineAttachmentToMessage(
398
+ persistedUserMessage.id,
399
+ i,
400
+ a.filename,
401
+ a.mimeType,
402
+ a.data,
403
+ { sourcePath: a.filePath },
404
+ );
371
405
  } catch (err) {
372
406
  if (err instanceof AttachmentUploadError) {
373
407
  log.warn(
@@ -383,6 +417,16 @@ export async function persistUserMessage(
383
417
  }
384
418
  }
385
419
 
420
+ // Sync the persisted user message (with attachments) to the disk view
421
+ const conv = getConversation(ctx.conversationId);
422
+ if (conv) {
423
+ syncMessageToDisk(
424
+ ctx.conversationId,
425
+ persistedUserMessage.id,
426
+ conv.createdAt,
427
+ );
428
+ }
429
+
386
430
  return persistedUserMessage.id;
387
431
  } catch (err) {
388
432
  ctx.messages.pop();
@@ -6,6 +6,7 @@
6
6
  * used by conversation-history.ts.
7
7
  */
8
8
 
9
+ import { enrichMessageWithSourcePaths } from "../agent/attachments.js";
9
10
  import {
10
11
  createAssistantMessage,
11
12
  createUserMessage,
@@ -25,18 +26,14 @@ import {
25
26
  } from "../memory/conversation-crud.js";
26
27
  import { extractPreferences } from "../notifications/preference-extractor.js";
27
28
  import { createPreference } from "../notifications/preferences-store.js";
28
- import { getConfiguredProviders } from "../providers/provider-availability.js";
29
29
  import type { Message } from "../providers/types.js";
30
30
  import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
31
31
  import { getLogger } from "../util/logger.js";
32
32
  import type { MessageQueue } from "./conversation-queue-manager.js";
33
33
  import type { QueueDrainReason } from "./conversation-queue-manager.js";
34
34
  import type { TrustContext } from "./conversation-runtime-assembly.js";
35
- import {
36
- isProviderShortcut,
37
- resolveSlash,
38
- type SlashContext,
39
- } from "./conversation-slash.js";
35
+ import { resolveSlash, type SlashContext } from "./conversation-slash.js";
36
+ import { getModelInfo } from "./handlers/config-model.js";
40
37
  import type {
41
38
  ServerMessage,
42
39
  UsageStats,
@@ -49,23 +46,12 @@ const log = getLogger("conversation-process");
49
46
 
50
47
  /** Build a model_info event with fresh config data. */
51
48
  export async function buildModelInfoEvent(): Promise<ServerMessage> {
52
- const config = getConfig();
53
- return {
54
- type: "model_info",
55
- model: config.services.inference.model,
56
- provider: config.services.inference.provider,
57
- configuredProviders: await getConfiguredProviders(),
58
- };
49
+ return { type: "model_info", ...(await getModelInfo()) };
59
50
  }
60
51
 
61
- /** True when the trimmed content is a /model or /models slash command. */
52
+ /** True when the trimmed content is the /models slash command. */
62
53
  export function isModelSlashCommand(content: string): boolean {
63
- const trimmed = content.trim();
64
- return (
65
- trimmed === "/model" ||
66
- trimmed === "/models" ||
67
- trimmed.startsWith("/model ")
68
- );
54
+ return content.trim() === "/models";
69
55
  }
70
56
 
71
57
  // ── Context Interface ────────────────────────────────────────────────
@@ -196,6 +182,7 @@ function buildSlashContext(
196
182
  conversation: ProcessConversationContext,
197
183
  ): SlashContext {
198
184
  const config = getConfig();
185
+ const turnInterface = conversation.getTurnInterfaceContext();
199
186
  return {
200
187
  messageCount: conversation.messages.length,
201
188
  inputTokens: conversation.usageStats.inputTokens,
@@ -204,6 +191,7 @@ function buildSlashContext(
204
191
  model: config.services.inference.model,
205
192
  provider: config.services.inference.provider,
206
193
  estimatedCost: conversation.usageStats.estimatedCost,
194
+ userMessageInterface: turnInterface?.userMessageInterface,
207
195
  };
208
196
  }
209
197
 
@@ -308,6 +296,13 @@ export async function drainQueue(
308
296
  const drainProvenance = provenanceFromTrustContext(
309
297
  conversation.trustContext,
310
298
  );
299
+ const drainImageSourcePaths: Record<string, string> = {};
300
+ for (let i = 0; i < next.attachments.length; i++) {
301
+ const a = next.attachments[i];
302
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
303
+ drainImageSourcePaths[`${i}:${a.filename}`] = a.filePath;
304
+ }
305
+ }
311
306
  const drainChannelMeta = {
312
307
  ...drainProvenance,
313
308
  ...(queuedTurnCtx
@@ -324,8 +319,15 @@ export async function drainQueue(
324
319
  }
325
320
  : {}),
326
321
  ...(next.metadata?.automated ? { automated: true } : {}),
322
+ ...(Object.keys(drainImageSourcePaths).length > 0
323
+ ? { imageSourcePaths: drainImageSourcePaths }
324
+ : {}),
327
325
  };
328
- const userMsg = createUserMessage(next.content, next.attachments);
326
+ const cleanUserMsg = createUserMessage(next.content, next.attachments);
327
+ const llmUserMsg = enrichMessageWithSourcePaths(
328
+ cleanUserMsg,
329
+ next.attachments,
330
+ );
329
331
  // When displayContent is provided (e.g. original text before recording
330
332
  // intent stripping), persist that to DB so users see the full message.
331
333
  // The in-memory userMessage (sent to the LLM) still uses the stripped content.
@@ -333,14 +335,14 @@ export async function drainQueue(
333
335
  ? JSON.stringify(
334
336
  createUserMessage(next.displayContent, next.attachments).content,
335
337
  )
336
- : JSON.stringify(userMsg.content);
338
+ : JSON.stringify(cleanUserMsg.content);
337
339
  await addMessage(
338
340
  conversation.conversationId,
339
341
  "user",
340
342
  contentToPersist,
341
343
  drainChannelMeta,
342
344
  );
343
- conversation.messages.push(userMsg);
345
+ conversation.messages.push(llmUserMsg);
344
346
 
345
347
  const assistantMsg = createAssistantMessage(slashResult.message);
346
348
  await addMessage(
@@ -366,10 +368,7 @@ export async function drainQueue(
366
368
 
367
369
  // Emit fresh model info before the text delta so the client has
368
370
  // up-to-date configuredProviders when rendering /model or /models UI.
369
- if (
370
- isModelSlashCommand(next.content) ||
371
- isProviderShortcut(next.content)
372
- ) {
371
+ if (isModelSlashCommand(next.content)) {
373
372
  next.onEvent(await buildModelInfoEvent());
374
373
  }
375
374
  next.onEvent({ type: "assistant_text_delta", text: slashResult.message });
@@ -602,6 +601,13 @@ export async function processMessage(
602
601
 
603
602
  if (routerResult.consumed) {
604
603
  const guardianIfCtx = conversation.getTurnInterfaceContext();
604
+ const guardianImageSourcePaths: Record<string, string> = {};
605
+ for (let i = 0; i < attachments.length; i++) {
606
+ const a = attachments[i];
607
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
608
+ guardianImageSourcePaths[`${i}:${a.filename}`] = a.filePath;
609
+ }
610
+ }
605
611
  const routerChannelMeta = {
606
612
  userMessageChannel: "vellum" as const,
607
613
  assistantMessageChannel: "vellum" as const,
@@ -609,16 +615,23 @@ export async function processMessage(
609
615
  assistantMessageInterface:
610
616
  guardianIfCtx?.assistantMessageInterface ?? "vellum",
611
617
  provenanceTrustClass: "guardian" as const,
618
+ ...(Object.keys(guardianImageSourcePaths).length > 0
619
+ ? { imageSourcePaths: guardianImageSourcePaths }
620
+ : {}),
612
621
  };
613
622
 
614
- const userMsg = createUserMessage(content, attachments);
623
+ const cleanUserMsg = createUserMessage(content, attachments);
624
+ const llmUserMsg = enrichMessageWithSourcePaths(
625
+ cleanUserMsg,
626
+ attachments,
627
+ );
615
628
  const persisted = await addMessage(
616
629
  conversation.conversationId,
617
630
  "user",
618
- JSON.stringify(userMsg.content),
631
+ JSON.stringify(cleanUserMsg.content),
619
632
  routerChannelMeta,
620
633
  );
621
- conversation.messages.push(userMsg);
634
+ conversation.messages.push(llmUserMsg);
622
635
 
623
636
  const replyText =
624
637
  routerResult.replyText ??
@@ -666,6 +679,13 @@ export async function processMessage(
666
679
  const pmTurnCtx = conversation.getTurnChannelContext();
667
680
  const pmInterfaceCtx = conversation.getTurnInterfaceContext();
668
681
  const pmProvenance = provenanceFromTrustContext(conversation.trustContext);
682
+ const pmImageSourcePaths: Record<string, string> = {};
683
+ for (let i = 0; i < attachments.length; i++) {
684
+ const a = attachments[i];
685
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
686
+ pmImageSourcePaths[`${i}:${a.filename}`] = a.filePath;
687
+ }
688
+ }
669
689
  const pmChannelMeta = {
670
690
  ...pmProvenance,
671
691
  ...(pmTurnCtx
@@ -680,21 +700,25 @@ export async function processMessage(
680
700
  assistantMessageInterface: pmInterfaceCtx.assistantMessageInterface,
681
701
  }
682
702
  : {}),
703
+ ...(Object.keys(pmImageSourcePaths).length > 0
704
+ ? { imageSourcePaths: pmImageSourcePaths }
705
+ : {}),
683
706
  };
684
- const userMsg = createUserMessage(content, attachments);
707
+ const cleanUserMsg = createUserMessage(content, attachments);
708
+ const llmUserMsg = enrichMessageWithSourcePaths(cleanUserMsg, attachments);
685
709
  // When displayContent is provided (e.g. original text before recording
686
710
  // intent stripping), persist that to DB so users see the full message.
687
711
  // The in-memory userMessage (sent to the LLM) still uses the stripped content.
688
712
  const contentToPersist = displayContent
689
713
  ? JSON.stringify(createUserMessage(displayContent, attachments).content)
690
- : JSON.stringify(userMsg.content);
714
+ : JSON.stringify(cleanUserMsg.content);
691
715
  const persisted = await addMessage(
692
716
  conversation.conversationId,
693
717
  "user",
694
718
  contentToPersist,
695
719
  pmChannelMeta,
696
720
  );
697
- conversation.messages.push(userMsg);
721
+ conversation.messages.push(llmUserMsg);
698
722
 
699
723
  const assistantMsg = createAssistantMessage(slashResult.message);
700
724
  await addMessage(
@@ -720,7 +744,7 @@ export async function processMessage(
720
744
 
721
745
  // Emit fresh model info before the text delta so the client has
722
746
  // up-to-date configuredProviders when rendering /model or /models UI.
723
- if (isModelSlashCommand(content) || isProviderShortcut(content)) {
747
+ if (isModelSlashCommand(content)) {
724
748
  onEvent(await buildModelInfoEvent());
725
749
  }
726
750
  onEvent({ type: "assistant_text_delta", text: slashResult.message });
@@ -15,7 +15,7 @@ import {
15
15
  type TurnChannelContext,
16
16
  type TurnInterfaceContext,
17
17
  } from "../channels/types.js";
18
- import { getAppsDir, listAppFiles } from "../memory/app-store.js";
18
+ import { getAppDirPath, listAppFiles } from "../memory/app-store.js";
19
19
  import type { Message } from "../providers/types.js";
20
20
  import type { ActorTrustContext } from "../runtime/actor-trust-resolver.js";
21
21
  import { channelStatusToMemberStatus } from "../runtime/routes/inbound-stages/acl-enforcement.js";
@@ -266,6 +266,8 @@ export interface ActiveSurfaceContext {
266
266
  /** When set, the surface is backed by a persisted app. */
267
267
  appId?: string;
268
268
  appName?: string;
269
+ /** Filesystem directory/slug for the app (used to construct file paths). */
270
+ appDirName?: string;
269
271
  appSchemaJson?: string;
270
272
  /** Additional pages keyed by filename (e.g. "settings.html" → HTML content). */
271
273
  appPages?: Record<string, string>;
@@ -297,19 +299,18 @@ export function injectActiveSurfaceContext(
297
299
 
298
300
  if (ctx.appId) {
299
301
  // ── App-backed surface ──
302
+ const slug = ctx.appDirName ?? ctx.appId;
300
303
  lines.push(
301
- `The user is viewing app "${ctx.appName ?? "Untitled"}" (app_id: "${ctx.appId}") in workspace mode.`,
304
+ `The user is viewing app "${ctx.appName ?? "Untitled"}" (app_id: "${ctx.appId}", slug: "${slug}") in workspace mode.`,
302
305
  "",
303
- 'PREREQUISITE: If `app_*` tools (e.g. `app_file_edit`, `app_file_write`) are not yet available, call `skill_load` with `id: "app-builder"` first to load them.',
306
+ 'PREREQUISITE: If `app_refresh` is not yet available, call `skill_load` with `id: "app-builder"` first to load it.',
304
307
  "",
305
308
  "RULES FOR WORKSPACE MODIFICATION:",
306
- `1. Use \`app_file_edit\` with app_id "${ctx.appId}" for surgical changes.`,
307
- " Provide old_string (exact match) and new_string (replacement).",
308
- ' Include a short `status` message describing what you\'re doing (e.g. "adding dark mode styles").',
309
- "2. Use `app_file_write` to create new files or fully rewrite files. Include `status`.",
310
- "3. Use `app_file_read` to read any file with line numbers before editing.",
311
- "4. Use `app_file_list` to see all files in the app.",
312
- "5. The surface refreshes automatically after file edits — do NOT call app_update, ui_show, or ui_update.",
309
+ `1. Use \`file_edit\` to make surgical changes to app files. The file path is \`${getAppDirPath(ctx.appId)}/<path>\`.`,
310
+ "2. Use `file_write` to create new files or rewrite files.",
311
+ "3. Use `file_read` to read any file with line numbers before editing.",
312
+ "4. Use `bash ls` to see all files in the app directory.",
313
+ `5. Call \`app_refresh\` with app_id "${ctx.appId}" ONCE after all changes are complete.`,
313
314
  "6. NEVER respond with only text — the user expects a visual update.",
314
315
  "7. Make ONLY the changes the user requested. Preserve existing content/styling.",
315
316
  "8. Keep your text response to 1 brief sentence confirming what you changed.",
@@ -323,7 +324,7 @@ export function injectActiveSurfaceContext(
323
324
  for (const filePath of displayFiles) {
324
325
  let sizeLabel: string;
325
326
  try {
326
- const bytes = statSync(join(getAppsDir(), ctx.appId, filePath)).size;
327
+ const bytes = statSync(join(getAppDirPath(ctx.appId), filePath)).size;
327
328
  sizeLabel =
328
329
  bytes < 1000 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
329
330
  } catch {
@@ -634,6 +635,15 @@ export function buildTurnContextBlock(
634
635
  lines.push(`assistant_message_channel: ${assistant}`);
635
636
  lines.push(`conversation_origin_channel: ${origin}`);
636
637
  }
638
+ // Only inject response discretion for external channels (Slack, Telegram,
639
+ // etc.) where the assistant may receive thread replies not directed at it.
640
+ // The "vellum" channel is the web/desktop interface where every message is
641
+ // intentionally directed at the assistant.
642
+ if (user !== "vellum") {
643
+ lines.push(
644
+ `response_discretion: Not every message in a channel thread requires your response. If a message is clearly not directed at you (e.g. people talking among themselves, acknowledgements, reactions), output exactly <no_response/> as your entire reply to stay silent.`,
645
+ );
646
+ }
637
647
  }
638
648
 
639
649
  lines.push("</turn_context>");
@@ -951,6 +961,7 @@ const RUNTIME_INJECTION_PREFIXES = [
951
961
  "<inbound_actor_context>",
952
962
  "<interface_turn_context>",
953
963
  "<turn_context>",
964
+ "<memory_brief>",
954
965
  "<memory_context __injected>",
955
966
  "<memory_context>", // backward-compat: strip legacy blocks from pre-__injected history
956
967
  "<voice_call_control>",
@@ -1126,30 +1137,3 @@ export function applyRuntimeInjections(
1126
1137
 
1127
1138
  return result;
1128
1139
  }
1129
-
1130
- // ---------------------------------------------------------------------------
1131
- // Attachment detection
1132
- // ---------------------------------------------------------------------------
1133
-
1134
- /** Content block types that indicate user-uploaded attachments. */
1135
- const ATTACHMENT_CONTENT_TYPES = new Set(["image", "file"]);
1136
-
1137
- /**
1138
- * Scan conversation messages for user-uploaded attachment content blocks
1139
- * (image or file). Returns true as soon as any attachment is found.
1140
- *
1141
- * Used to set the one-way `hasAttachments` flag on Conversation so that asset
1142
- * tools (asset_search, asset_materialize) are included in tool definitions
1143
- * only when the conversation contains attachments.
1144
- */
1145
- export function messagesContainAttachments(messages: Message[]): boolean {
1146
- for (const message of messages) {
1147
- if (message.role !== "user") continue;
1148
- for (const block of message.content) {
1149
- if (ATTACHMENT_CONTENT_TYPES.has(block.type)) {
1150
- return true;
1151
- }
1152
- }
1153
- }
1154
- return false;
1155
- }