@vellumai/assistant 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -390,7 +390,8 @@ function classifyByMessage(
390
390
  if (isStreamingError(message)) {
391
391
  return {
392
392
  code: "PROVIDER_API",
393
- userMessage: "The AI provider's response was interrupted. Please try again.",
393
+ userMessage:
394
+ "The AI provider's response was interrupted. Please try again.",
394
395
  retryable: true,
395
396
  errorCategory: "stream_corruption",
396
397
  };
@@ -29,6 +29,16 @@ function isToolResultBlock(
29
29
  );
30
30
  }
31
31
 
32
+ function isSystemNoticeBlock(
33
+ block: ContentBlock | Record<string, unknown>,
34
+ ): boolean {
35
+ if (block.type !== "text") return false;
36
+ const text = (block as { text?: string }).text ?? "";
37
+ return (
38
+ text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
39
+ );
40
+ }
41
+
32
42
  function isUndoableUserMessage(message: Message): boolean {
33
43
  if (message.role !== "user") return false;
34
44
  if (getSummaryFromContextMessage(message) != null) return false;
@@ -37,8 +47,9 @@ function isUndoableUserMessage(message: Message): boolean {
37
47
  // responses) are not undoable. Messages that have both tool_result and text blocks
38
48
  // (e.g. after repairHistory merges a tool_result turn with a user prompt) are still
39
49
  // undoable because they contain real user content.
50
+ // System notice text blocks (retry nudges, progress checks) are not user content.
40
51
  const hasNonToolResultContent = message.content.some(
41
- (block) => !isToolResultBlock(block),
52
+ (block) => !isToolResultBlock(block) && !isSystemNoticeBlock(block),
42
53
  );
43
54
  if (!hasNonToolResultContent) return false;
44
55
  return true;
@@ -131,10 +142,10 @@ export async function cleanupQdrantVectors(
131
142
  export function consolidateAssistantMessages(
132
143
  conversationId: string,
133
144
  userMessageId: string,
134
- ): void {
145
+ ): boolean {
135
146
  const allMessages = getMessages(conversationId);
136
147
  const userMsgIndex = allMessages.findIndex((m) => m.id === userMessageId);
137
- if (userMsgIndex === -1) return;
148
+ if (userMsgIndex === -1) return false;
138
149
 
139
150
  const messagesToConsolidate: typeof allMessages = [];
140
151
  const internalToolResultMessages: typeof allMessages = [];
@@ -171,12 +182,14 @@ export function consolidateAssistantMessages(
171
182
 
172
183
  // Only consolidate if there are multiple assistant messages
173
184
  if (messagesToConsolidate.length <= 1) {
185
+ let didMutate = false;
174
186
  // Still delete internal tool_result messages even if only one assistant message,
175
187
  // and collect IDs for vector cleanup
176
188
  const allSegmentIds: string[] = [];
177
189
  const allOrphanedItemIds: string[] = [];
178
190
  for (const id of messagesToDelete) {
179
191
  const deleted = deleteMessageById(id);
192
+ didMutate = true;
180
193
  allSegmentIds.push(...deleted.segmentIds);
181
194
  allOrphanedItemIds.push(...deleted.orphanedItemIds);
182
195
  }
@@ -194,7 +207,7 @@ export function consolidateAssistantMessages(
194
207
  );
195
208
  });
196
209
  }
197
- return;
210
+ return didMutate;
198
211
  }
199
212
 
200
213
  log.info(
@@ -339,6 +352,7 @@ export function consolidateAssistantMessages(
339
352
  },
340
353
  "Assistant messages consolidated",
341
354
  );
355
+ return true;
342
356
  }
343
357
 
344
358
  // ── Undo ─────────────────────────────────────────────────────────────
@@ -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 {
@@ -93,6 +125,7 @@ export interface AbortContext {
93
125
  string,
94
126
  { surfaceType: SurfaceType; data: SurfaceData; title?: string }
95
127
  >;
128
+ accumulatedSurfaceState: Map<string, Record<string, unknown>>;
96
129
  readonly queue: MessageQueue;
97
130
  }
98
131
 
@@ -151,6 +184,9 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
151
184
  );
152
185
  content = [{ type: "text", text: m.content }];
153
186
  }
187
+
188
+ content = reinjectImageSourcePaths(content, role, m.metadata);
189
+
154
190
  return { role, content };
155
191
  });
156
192
 
@@ -182,20 +218,6 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
182
218
 
183
219
  ctx.loadedHistoryTrustClass = trustClass;
184
220
 
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
221
  log.info(
200
222
  { conversationId: ctx.conversationId, count: ctx.messages.length },
201
223
  "Loaded messages from DB",
@@ -216,6 +238,7 @@ export function abortConversation(ctx: AbortContext): void {
216
238
  ctx.pendingSurfaceActions.clear();
217
239
  ctx.surfaceActionRequestIds.clear();
218
240
  ctx.surfaceState.clear();
241
+ ctx.accumulatedSurfaceState.clear();
219
242
  unregisterWatchNotifiers(ctx.conversationId);
220
243
  for (const queued of ctx.queue) {
221
244
  queued.onEvent({
@@ -247,6 +270,7 @@ export function disposeConversation(ctx: DisposeContext): void {
247
270
  ctx.pendingSurfaceActions.clear();
248
271
  ctx.surfaceActionRequestIds.clear();
249
272
  ctx.surfaceState.clear();
273
+ ctx.accumulatedSurfaceState.clear();
250
274
  ctx.lastSurfaceAction.clear();
251
275
  ctx.workspaceTopLevelContext = null;
252
276
  }
@@ -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 \`~/.vellum/workspace/data/apps/${slug}/<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>");
@@ -1126,30 +1136,3 @@ export function applyRuntimeInjections(
1126
1136
 
1127
1137
  return result;
1128
1138
  }
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
- }