@vellumai/assistant 0.5.0 → 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 (347) 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__/assistant-feature-flags-integration.test.ts +7 -9
  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 +149 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +290 -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-queue.test.ts +36 -1
  37. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  38. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  39. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  41. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  42. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  43. package/src/__tests__/conversation-store.test.ts +24 -21
  44. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  45. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  46. package/src/__tests__/conversation-title-service.test.ts +137 -0
  47. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  48. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  49. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  51. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  52. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  53. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  54. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  55. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  56. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  57. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  58. package/src/__tests__/diagnostics-export.test.ts +70 -1
  59. package/src/__tests__/filesystem-tools.test.ts +4 -2
  60. package/src/__tests__/first-greeting.test.ts +80 -0
  61. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  63. package/src/__tests__/history-repair.test.ts +103 -10
  64. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  65. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  66. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  67. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  68. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  69. package/src/__tests__/media-generate-image.test.ts +47 -94
  70. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  71. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  72. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  73. package/src/__tests__/migration-export-http.test.ts +3 -1
  74. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  75. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  76. package/src/__tests__/mime-builder.test.ts +3 -2
  77. package/src/__tests__/non-member-access-request.test.ts +12 -1
  78. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  80. package/src/__tests__/oauth-store.test.ts +115 -0
  81. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  82. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  83. package/src/__tests__/recording-handler.test.ts +17 -0
  84. package/src/__tests__/registry.test.ts +3 -8
  85. package/src/__tests__/relay-server.test.ts +1 -1
  86. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  87. package/src/__tests__/schema-transforms.test.ts +165 -5
  88. package/src/__tests__/server-history-render.test.ts +2 -2
  89. package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
  90. package/src/__tests__/skill-feature-flags.test.ts +13 -13
  91. package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
  92. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  93. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  94. package/src/__tests__/starter-task-flow.test.ts +1 -0
  95. package/src/__tests__/suggestion-routes.test.ts +443 -0
  96. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  97. package/src/__tests__/swarm-recursion.test.ts +1 -0
  98. package/src/__tests__/swarm-tool.test.ts +1 -0
  99. package/src/__tests__/system-prompt.test.ts +8 -0
  100. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  101. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  102. package/src/__tests__/top-level-renderer.test.ts +22 -0
  103. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  104. package/src/__tests__/web-fetch.test.ts +6 -2
  105. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  106. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  107. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  108. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  109. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  110. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  111. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  112. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  113. package/src/agent/attachments.ts +27 -1
  114. package/src/agent/loop.ts +29 -1
  115. package/src/avatar/traits-png-sync.ts +80 -25
  116. package/src/bundler/app-bundler.ts +4 -4
  117. package/src/calls/call-domain.ts +1 -0
  118. package/src/calls/voice-session-bridge.ts +1 -0
  119. package/src/cli/commands/auth.ts +92 -0
  120. package/src/cli/commands/avatar.ts +7 -6
  121. package/src/cli/commands/config.ts +2 -0
  122. package/src/cli/commands/oauth/providers.ts +29 -0
  123. package/src/cli/program.ts +12 -0
  124. package/src/cli.ts +15 -48
  125. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  126. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  127. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  128. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  129. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  130. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  131. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  135. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  136. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  137. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  139. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  140. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  141. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  142. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  143. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  144. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  145. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  146. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  148. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  149. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  150. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  151. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  152. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  153. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  154. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  155. package/src/config/bundled-tool-registry.ts +2 -14
  156. package/src/config/feature-flag-registry.json +16 -0
  157. package/src/config/loader.ts +64 -0
  158. package/src/config/raw-config-utils.ts +30 -0
  159. package/src/config/schema-utils.ts +28 -7
  160. package/src/config/schema.ts +8 -0
  161. package/src/config/schemas/elevenlabs.ts +18 -0
  162. package/src/config/schemas/memory-lifecycle.ts +4 -2
  163. package/src/config/schemas/memory-storage.ts +1 -1
  164. package/src/config/schemas/services.ts +8 -6
  165. package/src/contacts/contact-store.ts +13 -6
  166. package/src/contacts/contacts-write.ts +0 -1
  167. package/src/context/window-manager.ts +13 -2
  168. package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
  169. package/src/daemon/conversation-agent-loop.ts +56 -19
  170. package/src/daemon/conversation-attachments.ts +18 -36
  171. package/src/daemon/conversation-error.ts +2 -1
  172. package/src/daemon/conversation-history.ts +18 -4
  173. package/src/daemon/conversation-lifecycle.ts +39 -15
  174. package/src/daemon/conversation-messaging.ts +70 -26
  175. package/src/daemon/conversation-process.ts +58 -34
  176. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  177. package/src/daemon/conversation-slash.ts +121 -256
  178. package/src/daemon/conversation-surfaces.ts +143 -20
  179. package/src/daemon/conversation-tool-setup.ts +0 -6
  180. package/src/daemon/conversation-workspace.ts +21 -1
  181. package/src/daemon/conversation.ts +51 -29
  182. package/src/daemon/first-greeting.ts +35 -0
  183. package/src/daemon/handlers/config-embeddings.ts +148 -0
  184. package/src/daemon/handlers/config-model.ts +71 -26
  185. package/src/daemon/handlers/conversations.ts +0 -23
  186. package/src/daemon/handlers/recording.ts +26 -21
  187. package/src/daemon/history-repair.ts +28 -8
  188. package/src/daemon/host-cu-proxy.ts +2 -2
  189. package/src/daemon/lifecycle.ts +106 -64
  190. package/src/daemon/message-protocol.ts +3 -0
  191. package/src/daemon/message-types/conversations.ts +19 -0
  192. package/src/daemon/message-types/messages.ts +1 -0
  193. package/src/daemon/message-types/shared.ts +2 -0
  194. package/src/daemon/message-types/surfaces.ts +2 -0
  195. package/src/daemon/message-types/upgrades.ts +23 -0
  196. package/src/daemon/server.ts +83 -12
  197. package/src/daemon/shutdown-handlers.ts +8 -5
  198. package/src/daemon/startup-error.ts +9 -0
  199. package/src/daemon/tool-side-effects.ts +11 -28
  200. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  201. package/src/instrument.ts +0 -4
  202. package/src/media/app-icon-generator.ts +2 -2
  203. package/src/memory/app-git-service.ts +28 -16
  204. package/src/memory/app-store.ts +230 -41
  205. package/src/memory/attachments-store.ts +558 -130
  206. package/src/memory/conversation-attention-store.ts +70 -0
  207. package/src/memory/conversation-crud.ts +442 -3
  208. package/src/memory/conversation-directories.ts +125 -0
  209. package/src/memory/conversation-disk-view.ts +390 -0
  210. package/src/memory/conversation-key-store.ts +17 -5
  211. package/src/memory/conversation-queries.ts +5 -1
  212. package/src/memory/conversation-title-service.ts +21 -49
  213. package/src/memory/db-init.ts +28 -0
  214. package/src/memory/embedding-backend.ts +42 -53
  215. package/src/memory/embedding-gemini.test.ts +4 -4
  216. package/src/memory/embedding-local.ts +1 -3
  217. package/src/memory/embedding-ollama.ts +1 -3
  218. package/src/memory/embedding-openai.ts +1 -3
  219. package/src/memory/indexer.ts +9 -7
  220. package/src/memory/items-extractor.ts +42 -13
  221. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  222. package/src/memory/job-handlers/embedding.test.ts +1 -4
  223. package/src/memory/llm-request-log-store.ts +100 -1
  224. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  225. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  226. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  227. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  228. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  229. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  230. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  231. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  232. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  233. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  234. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  235. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  236. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  237. package/src/memory/migrations/index.ts +7 -0
  238. package/src/memory/migrations/registry.ts +13 -0
  239. package/src/memory/retriever.test.ts +601 -2
  240. package/src/memory/retriever.ts +85 -9
  241. package/src/memory/schema/conversations.ts +6 -0
  242. package/src/memory/schema/infrastructure.ts +13 -7
  243. package/src/memory/schema/oauth.ts +6 -0
  244. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  245. package/src/notifications/copy-composer.ts +26 -0
  246. package/src/notifications/decision-engine.ts +14 -1
  247. package/src/notifications/emit-signal.ts +1 -1
  248. package/src/notifications/signal.ts +36 -0
  249. package/src/oauth/byo-connection.test.ts +1 -45
  250. package/src/oauth/byo-connection.ts +2 -8
  251. package/src/oauth/connect-orchestrator.ts +15 -11
  252. package/src/oauth/connection-resolver.test.ts +191 -0
  253. package/src/oauth/connection-resolver.ts +66 -38
  254. package/src/oauth/connection.ts +0 -1
  255. package/src/oauth/oauth-store.ts +97 -47
  256. package/src/oauth/platform-connection.test.ts +0 -1
  257. package/src/oauth/platform-connection.ts +11 -3
  258. package/src/oauth/seed-providers.ts +78 -3
  259. package/src/oauth/token-persistence.ts +16 -10
  260. package/src/permissions/checker.ts +62 -19
  261. package/src/prompts/system-prompt.ts +2 -0
  262. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  263. package/src/providers/anthropic/client.ts +8 -1
  264. package/src/providers/failover.ts +4 -1
  265. package/src/providers/gemini/client.ts +50 -0
  266. package/src/providers/model-catalog.ts +92 -0
  267. package/src/providers/model-intents.ts +29 -20
  268. package/src/providers/openai/client.ts +49 -0
  269. package/src/providers/types.ts +2 -0
  270. package/src/runtime/access-request-helper.ts +16 -7
  271. package/src/runtime/auth/credential-service.ts +3 -1
  272. package/src/runtime/auth/route-policy.ts +14 -1
  273. package/src/runtime/btw-sidechain.ts +101 -0
  274. package/src/runtime/channel-reply-delivery.ts +17 -1
  275. package/src/runtime/http-router.ts +3 -1
  276. package/src/runtime/http-server.ts +196 -141
  277. package/src/runtime/http-types.ts +1 -0
  278. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  279. package/src/runtime/routes/access-request-decision.ts +41 -0
  280. package/src/runtime/routes/app-management-routes.ts +6 -3
  281. package/src/runtime/routes/app-routes.ts +7 -3
  282. package/src/runtime/routes/approval-routes.ts +1 -0
  283. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  284. package/src/runtime/routes/attachment-routes.ts +45 -15
  285. package/src/runtime/routes/btw-routes.ts +21 -61
  286. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  287. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  288. package/src/runtime/routes/conversation-routes.ts +222 -28
  289. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  290. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  291. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  292. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  293. package/src/runtime/routes/log-export-routes.ts +3 -0
  294. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  295. package/src/runtime/routes/memory-item-routes.ts +4 -0
  296. package/src/runtime/routes/migration-routes.ts +4 -1
  297. package/src/runtime/routes/oauth-apps.ts +291 -0
  298. package/src/runtime/routes/secret-routes.ts +28 -1
  299. package/src/runtime/routes/settings-routes.ts +14 -0
  300. package/src/runtime/routes/trace-event-routes.ts +4 -1
  301. package/src/schedule/schedule-store.ts +9 -21
  302. package/src/security/secure-keys.ts +21 -0
  303. package/src/signals/bash.ts +1 -1
  304. package/src/swarm/backend-claude-code.ts +3 -6
  305. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  306. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  307. package/src/tools/AGENTS.md +6 -10
  308. package/src/tools/apps/executors.ts +17 -232
  309. package/src/tools/claude-code/claude-code.ts +2 -3
  310. package/src/tools/credentials/vault.ts +7 -12
  311. package/src/tools/host-filesystem/read.ts +13 -10
  312. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  313. package/src/tools/schedule/list.ts +2 -7
  314. package/src/tools/schema-transforms.ts +5 -0
  315. package/src/tools/shared/filesystem/format-diff.ts +4 -21
  316. package/src/tools/skills/execute.ts +1 -1
  317. package/src/tools/tool-manifest.ts +0 -6
  318. package/src/tools/ui-surface/definitions.ts +2 -2
  319. package/src/util/device-id.ts +28 -5
  320. package/src/util/platform.ts +6 -0
  321. package/src/util/pricing.ts +1 -0
  322. package/src/util/retry.ts +1 -3
  323. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  324. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  325. package/src/workspace/migrations/006-services-config.ts +5 -0
  326. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  327. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  328. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  329. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  330. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  331. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  332. package/src/workspace/migrations/registry.ts +10 -0
  333. package/src/workspace/top-level-renderer.ts +12 -0
  334. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  335. package/src/__tests__/asset-search-tool.test.ts +0 -536
  336. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  337. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  338. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  339. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  340. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  341. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  342. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  343. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  344. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  345. package/src/daemon/media-visibility-policy.ts +0 -59
  346. package/src/tools/assets/materialize.ts +0 -248
  347. package/src/tools/assets/search.ts +0 -400
@@ -163,6 +163,76 @@ export function projectAssistantMessage(params: {
163
163
  .run();
164
164
  }
165
165
 
166
+ /**
167
+ * Seed a forked conversation's assistant-attention projection so copied
168
+ * assistant history is treated as already seen from the outset.
169
+ */
170
+ export function seedForkedConversationAttention(params: {
171
+ conversationId: string;
172
+ latestAssistantMessageId: string | null;
173
+ latestAssistantMessageAt: number | null;
174
+ }): void {
175
+ const {
176
+ conversationId,
177
+ latestAssistantMessageId,
178
+ latestAssistantMessageAt,
179
+ } = params;
180
+
181
+ if (!latestAssistantMessageId || latestAssistantMessageAt == null) {
182
+ return;
183
+ }
184
+
185
+ const db = getDb();
186
+ const now = Date.now();
187
+ const existing = db
188
+ .select()
189
+ .from(conversationAssistantAttentionState)
190
+ .where(
191
+ eq(conversationAssistantAttentionState.conversationId, conversationId),
192
+ )
193
+ .get();
194
+
195
+ if (!existing) {
196
+ db.insert(conversationAssistantAttentionState)
197
+ .values({
198
+ conversationId,
199
+ latestAssistantMessageId,
200
+ latestAssistantMessageAt,
201
+ lastSeenAssistantMessageId: latestAssistantMessageId,
202
+ lastSeenAssistantMessageAt: latestAssistantMessageAt,
203
+ lastSeenEventAt: null,
204
+ lastSeenConfidence: null,
205
+ lastSeenSignalType: null,
206
+ lastSeenSourceChannel: null,
207
+ lastSeenSource: null,
208
+ lastSeenEvidenceText: null,
209
+ createdAt: now,
210
+ updatedAt: now,
211
+ })
212
+ .run();
213
+ return;
214
+ }
215
+
216
+ db.update(conversationAssistantAttentionState)
217
+ .set({
218
+ latestAssistantMessageId,
219
+ latestAssistantMessageAt,
220
+ lastSeenAssistantMessageId: latestAssistantMessageId,
221
+ lastSeenAssistantMessageAt: latestAssistantMessageAt,
222
+ lastSeenEventAt: null,
223
+ lastSeenConfidence: null,
224
+ lastSeenSignalType: null,
225
+ lastSeenSourceChannel: null,
226
+ lastSeenSource: null,
227
+ lastSeenEvidenceText: null,
228
+ updatedAt: now,
229
+ })
230
+ .where(
231
+ eq(conversationAssistantAttentionState.conversationId, conversationId),
232
+ )
233
+ .run();
234
+ }
235
+
166
236
  // ── recordConversationSeenSignal ─────────────────────────────────────
167
237
 
168
238
  /**
@@ -1,4 +1,17 @@
1
- import { and, asc, count, eq, inArray, isNull, sql } from "drizzle-orm";
1
+ import { mkdirSync, rmSync } from "node:fs";
2
+
3
+ import {
4
+ and,
5
+ asc,
6
+ count,
7
+ desc,
8
+ eq,
9
+ gt,
10
+ inArray,
11
+ isNull,
12
+ lte,
13
+ sql,
14
+ } from "drizzle-orm";
2
15
  import { v4 as uuid } from "uuid";
3
16
  import { z } from "zod";
4
17
 
@@ -7,10 +20,24 @@ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
7
20
  import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from "../channels/types.js";
8
21
  import { getConfig } from "../config/loader.js";
9
22
  import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
23
+ import { UserError } from "../util/errors.js";
10
24
  import { getLogger } from "../util/logger.js";
25
+ import { getConversationsDir } from "../util/platform.js";
11
26
  import { createRowMapper } from "../util/row-mapper.js";
12
- import { deleteOrphanAttachments } from "./attachments-store.js";
13
- import { projectAssistantMessage } from "./conversation-attention-store.js";
27
+ import {
28
+ deleteOrphanAttachments,
29
+ linkAttachmentToMessage,
30
+ } from "./attachments-store.js";
31
+ import {
32
+ projectAssistantMessage,
33
+ seedForkedConversationAttention,
34
+ } from "./conversation-attention-store.js";
35
+ import {
36
+ initConversationDir,
37
+ removeConversationDir,
38
+ syncMessageToDisk,
39
+ updateMetaFile,
40
+ } from "./conversation-disk-view.js";
14
41
  import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
15
42
  import { getDb, rawAll, rawExec, rawGet, rawRun } from "./db.js";
16
43
  import { indexMessageNow } from "./indexer.js";
@@ -47,6 +74,9 @@ const subagentNotificationSchema = z.object({
47
74
  conversationId: z.string().optional(),
48
75
  });
49
76
 
77
+ export const PRIVATE_CONVERSATION_FORK_ERROR =
78
+ "Private conversations cannot be forked";
79
+
50
80
  export const messageMetadataSchema = z
51
81
  .object({
52
82
  userMessageChannel: channelIdSchema.optional(),
@@ -67,11 +97,42 @@ export const messageMetadataSchema = z
67
97
  provenanceGuardianExternalUserId: z.string().optional(),
68
98
  provenanceRequesterIdentifier: z.string().optional(),
69
99
  automated: z.boolean().optional(),
100
+ forkSourceMessageId: z.string().optional(),
101
+ /** Image source paths from desktop attachments, keyed by filename. */
102
+ imageSourcePaths: z.record(z.string(), z.string()).optional(),
70
103
  })
71
104
  .passthrough();
72
105
 
73
106
  export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
74
107
 
108
+ function cloneForkMessageMetadata(
109
+ metadata: string | null,
110
+ sourceMessageId: string,
111
+ ): string {
112
+ if (!metadata) {
113
+ return JSON.stringify({ forkSourceMessageId: sourceMessageId });
114
+ }
115
+
116
+ try {
117
+ const parsed = JSON.parse(metadata);
118
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
119
+ const sourceRecord = parsed as Record<string, unknown>;
120
+ const forkSourceMessageId =
121
+ typeof sourceRecord.forkSourceMessageId === "string"
122
+ ? sourceRecord.forkSourceMessageId
123
+ : sourceMessageId;
124
+ return JSON.stringify({
125
+ ...sourceRecord,
126
+ forkSourceMessageId,
127
+ });
128
+ }
129
+ } catch {
130
+ // Fall through to source-only metadata.
131
+ }
132
+
133
+ return JSON.stringify({ forkSourceMessageId: sourceMessageId });
134
+ }
135
+
75
136
  /**
76
137
  * Extract provenance metadata fields from a TrustContext.
77
138
  * When no guardian context is provided, defaults to 'unknown' because the
@@ -106,6 +167,8 @@ export interface ConversationRow {
106
167
  memoryScopeId: string;
107
168
  originChannel: string | null;
108
169
  originInterface: string | null;
170
+ forkParentConversationId: string | null;
171
+ forkParentMessageId: string | null;
109
172
  isAutoTitle: number;
110
173
  scheduleJobId: string | null;
111
174
  }
@@ -129,6 +192,8 @@ export const parseConversation = createRowMapper<
129
192
  memoryScopeId: "memoryScopeId",
130
193
  originChannel: "originChannel",
131
194
  originInterface: "originInterface",
195
+ forkParentConversationId: "forkParentConversationId",
196
+ forkParentMessageId: "forkParentMessageId",
132
197
  isAutoTitle: "isAutoTitle",
133
198
  scheduleJobId: "scheduleJobId",
134
199
  });
@@ -232,6 +297,8 @@ export function createConversation(
232
297
  }
233
298
  }
234
299
 
300
+ initConversationDir({ ...conversation, originChannel: null });
301
+
235
302
  return conversation;
236
303
  }
237
304
 
@@ -258,6 +325,187 @@ export function getConversationMemoryScopeId(conversationId: string): string {
258
325
  return conv?.memoryScopeId ?? "default";
259
326
  }
260
327
 
328
+ export function forkConversation(params: {
329
+ conversationId: string;
330
+ throughMessageId?: string;
331
+ }): ConversationRow {
332
+ const { conversationId, throughMessageId } = params;
333
+ const db = getDb();
334
+ const sourceConversation = getConversation(conversationId);
335
+
336
+ if (!sourceConversation) {
337
+ throw new UserError(`Conversation ${conversationId} not found`);
338
+ }
339
+ if (sourceConversation.conversationType === "private") {
340
+ throw new UserError(PRIVATE_CONVERSATION_FORK_ERROR);
341
+ }
342
+
343
+ const sourceMessages = getMessages(conversationId);
344
+
345
+ if (sourceMessages.length === 0) {
346
+ throw new UserError(
347
+ `Conversation ${conversationId} has no persisted messages to fork`,
348
+ );
349
+ }
350
+
351
+ const copyBoundaryIndex =
352
+ throughMessageId == null
353
+ ? sourceMessages.length - 1
354
+ : sourceMessages.findIndex((message) => message.id === throughMessageId);
355
+
356
+ if (throughMessageId != null && copyBoundaryIndex === -1) {
357
+ throw new UserError(
358
+ `Message ${throughMessageId} does not belong to conversation ${conversationId}`,
359
+ );
360
+ }
361
+
362
+ const visibleWindowStartIndex = Math.max(
363
+ 0,
364
+ Math.min(
365
+ sourceConversation.contextCompactedMessageCount,
366
+ sourceMessages.length,
367
+ ),
368
+ );
369
+ const preserveSourceCompactionState =
370
+ copyBoundaryIndex >= visibleWindowStartIndex;
371
+
372
+ const messagesToCopy =
373
+ copyBoundaryIndex >= 0
374
+ ? sourceMessages.slice(0, copyBoundaryIndex + 1)
375
+ : ([] as MessageRow[]);
376
+ const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
377
+ const forkTitle = `${sourceConversation.title ?? "Untitled"} (Fork)`;
378
+ const forkedConversation = createConversation({
379
+ title: forkTitle,
380
+ conversationType: "standard",
381
+ });
382
+
383
+ db.update(conversations)
384
+ .set({
385
+ forkParentConversationId: sourceConversation.id,
386
+ forkParentMessageId,
387
+ contextSummary: preserveSourceCompactionState
388
+ ? sourceConversation.contextSummary
389
+ : null,
390
+ contextCompactedMessageCount: preserveSourceCompactionState
391
+ ? sourceConversation.contextCompactedMessageCount
392
+ : 0,
393
+ contextCompactedAt: preserveSourceCompactionState
394
+ ? sourceConversation.contextCompactedAt
395
+ : null,
396
+ })
397
+ .where(eq(conversations.id, forkedConversation.id))
398
+ .run();
399
+
400
+ const forkedMessageIds = new Map<string, string>();
401
+ let latestForkedAssistant: { messageId: string; messageAt: number } | null =
402
+ null;
403
+
404
+ for (const message of messagesToCopy) {
405
+ const forkedMessageId = uuid();
406
+ db.insert(messages)
407
+ .values({
408
+ id: forkedMessageId,
409
+ conversationId: forkedConversation.id,
410
+ role: message.role,
411
+ content: message.content,
412
+ createdAt: message.createdAt,
413
+ metadata: cloneForkMessageMetadata(message.metadata, message.id),
414
+ })
415
+ .run();
416
+ forkedMessageIds.set(message.id, forkedMessageId);
417
+
418
+ if (message.role === "assistant") {
419
+ latestForkedAssistant = {
420
+ messageId: forkedMessageId,
421
+ messageAt: message.createdAt,
422
+ };
423
+ }
424
+ }
425
+
426
+ const attachmentIdMap = new Map<string, string>();
427
+ for (const message of messagesToCopy) {
428
+ const forkedMessageId = forkedMessageIds.get(message.id);
429
+ if (!forkedMessageId) continue;
430
+
431
+ const attachmentLinks = db
432
+ .select({
433
+ attachmentId: messageAttachments.attachmentId,
434
+ position: messageAttachments.position,
435
+ })
436
+ .from(messageAttachments)
437
+ .where(eq(messageAttachments.messageId, message.id))
438
+ .orderBy(messageAttachments.position)
439
+ .all();
440
+ const uncachedAttachmentLinks = attachmentLinks.filter(
441
+ (link) => !attachmentIdMap.has(link.attachmentId),
442
+ );
443
+ const stagingMessageId = uncachedAttachmentLinks.length > 0 ? uuid() : null;
444
+
445
+ if (stagingMessageId) {
446
+ db.insert(messages)
447
+ .values({
448
+ id: stagingMessageId,
449
+ conversationId: forkedConversation.id,
450
+ role: message.role,
451
+ content: "",
452
+ createdAt: message.createdAt,
453
+ metadata: null,
454
+ })
455
+ .run();
456
+ }
457
+
458
+ for (const link of attachmentLinks) {
459
+ const cachedAttachmentId = attachmentIdMap.get(link.attachmentId);
460
+ if (cachedAttachmentId) {
461
+ db.insert(messageAttachments)
462
+ .values({
463
+ id: uuid(),
464
+ messageId: forkedMessageId,
465
+ attachmentId: cachedAttachmentId,
466
+ position: link.position,
467
+ createdAt: Date.now(),
468
+ })
469
+ .run();
470
+ continue;
471
+ }
472
+
473
+ const scopedAttachmentId = linkAttachmentToMessage(
474
+ stagingMessageId ?? forkedMessageId,
475
+ link.attachmentId,
476
+ link.position,
477
+ );
478
+ attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
479
+ }
480
+
481
+ if (stagingMessageId) {
482
+ relinkAttachments([stagingMessageId], forkedMessageId);
483
+ db.delete(messages).where(eq(messages.id, stagingMessageId)).run();
484
+ }
485
+
486
+ syncMessageToDisk(
487
+ forkedConversation.id,
488
+ forkedMessageId,
489
+ forkedConversation.createdAt,
490
+ );
491
+ }
492
+
493
+ seedForkedConversationAttention({
494
+ conversationId: forkedConversation.id,
495
+ latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
496
+ latestAssistantMessageAt: latestForkedAssistant?.messageAt ?? null,
497
+ });
498
+
499
+ const persistedFork = getConversation(forkedConversation.id);
500
+ if (!persistedFork) {
501
+ throw new Error(
502
+ `Failed to load forked conversation ${forkedConversation.id} after creation`,
503
+ );
504
+ }
505
+
506
+ return persistedFork;
507
+ }
508
+
261
509
  /**
262
510
  * Delete a conversation and all its messages, cleaning up orphaned memory
263
511
  * artifacts (items, embeddings). Returns segment and orphaned item IDs so
@@ -267,6 +515,11 @@ export function deleteConversation(id: string): DeletedMemoryIds {
267
515
  const db = getDb();
268
516
  const result: DeletedMemoryIds = { segmentIds: [], orphanedItemIds: [] };
269
517
 
518
+ // Capture createdAt before the transaction deletes the row — needed to
519
+ // resolve the conversation's disk-view directory path after deletion.
520
+ const convBeforeDelete = getConversation(id);
521
+ const createdAtForDiskCleanup = convBeforeDelete?.createdAt;
522
+
270
523
  db.transaction((tx) => {
271
524
  // Collect all message IDs for this conversation.
272
525
  const messageRows = tx
@@ -357,6 +610,11 @@ export function deleteConversation(id: string): DeletedMemoryIds {
357
610
  tx.delete(conversations).where(eq(conversations.id, id)).run();
358
611
  });
359
612
 
613
+ // Remove the conversation's disk-view directory after the DB transaction
614
+ if (createdAtForDiskCleanup != null) {
615
+ removeConversationDir(id, createdAtForDiskCleanup);
616
+ }
617
+
360
618
  return result;
361
619
  }
362
620
 
@@ -759,6 +1017,12 @@ export function updateConversationTitle(
759
1017
  const set: Record<string, unknown> = { title, updatedAt: Date.now() };
760
1018
  if (isAutoTitle !== undefined) set.isAutoTitle = isAutoTitle;
761
1019
  db.update(conversations).set(set).where(eq(conversations.id, id)).run();
1020
+
1021
+ // Update disk view meta.json with the new title
1022
+ const conv = getConversation(id);
1023
+ if (conv) {
1024
+ updateMetaFile(conv);
1025
+ }
762
1026
  }
763
1027
 
764
1028
  export function updateConversationUsage(
@@ -864,6 +1128,14 @@ export function clearAll(): { conversations: number; messages: number } {
864
1128
  );
865
1129
  }
866
1130
 
1131
+ // Clear the disk-view conversations directory and recreate it empty
1132
+ try {
1133
+ rmSync(getConversationsDir(), { recursive: true, force: true });
1134
+ mkdirSync(getConversationsDir(), { recursive: true });
1135
+ } catch (err) {
1136
+ log.warn({ err }, "clearAll: failed to reset conversations directory");
1137
+ }
1138
+
867
1139
  return { conversations: convCount, messages: msgCount };
868
1140
  }
869
1141
 
@@ -1232,3 +1504,170 @@ export function getDisplayMetaForConversations(
1232
1504
  }
1233
1505
  return result;
1234
1506
  }
1507
+
1508
+ // ── Turn boundary resolution ─────────────────────────────────────────
1509
+
1510
+ /**
1511
+ * Returns `true` if a message is a tool-result user message — i.e. its
1512
+ * role is "user" and its content is a JSON array where every block has
1513
+ * `type === "tool_result"`. These synthetic user messages are injected
1514
+ * between assistant messages within a single agent turn and should NOT
1515
+ * be treated as turn boundaries.
1516
+ */
1517
+ function isToolResultMessage(role: string, content: string): boolean {
1518
+ if (role !== "user") return false;
1519
+ try {
1520
+ const parsed = JSON.parse(content);
1521
+ if (!Array.isArray(parsed) || parsed.length === 0) return false;
1522
+ return parsed.every(
1523
+ (block: unknown) =>
1524
+ block != null &&
1525
+ typeof block === "object" &&
1526
+ (block as Record<string, unknown>).type === "tool_result",
1527
+ );
1528
+ } catch {
1529
+ return false;
1530
+ }
1531
+ }
1532
+
1533
+ /**
1534
+ * Resolve all assistant message IDs that belong to the same agent turn
1535
+ * as the given `messageId`. A "turn" is bounded by:
1536
+ * - The start of the conversation, or
1537
+ * - A user message whose content is NOT a tool_result array.
1538
+ *
1539
+ * Within a multi-step agent loop, the pattern is:
1540
+ * user msg → assistant A1 → user (tool_result) → assistant A2 → ...
1541
+ * All assistant messages from A1 through the queried message (and beyond,
1542
+ * up to the next real user message) are part of the same turn.
1543
+ *
1544
+ * Returns `[messageId]` as a fallback if the message is not found,
1545
+ * preserving backward compatibility for callers.
1546
+ */
1547
+ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1548
+ const db = getDb();
1549
+
1550
+ // Look up the target message to get its conversationId and createdAt.
1551
+ const target = getMessageById(messageId);
1552
+ if (!target) return [messageId];
1553
+
1554
+ // Walk backward from the target message to find the turn boundary.
1555
+ // Limit to 50 rows — sufficient for even aggressive tool-use loops.
1556
+ const backwardRows = db
1557
+ .select({
1558
+ id: messages.id,
1559
+ role: messages.role,
1560
+ content: messages.content,
1561
+ createdAt: messages.createdAt,
1562
+ })
1563
+ .from(messages)
1564
+ .where(
1565
+ and(
1566
+ eq(messages.conversationId, target.conversationId),
1567
+ lte(messages.createdAt, target.createdAt),
1568
+ ),
1569
+ )
1570
+ .orderBy(desc(messages.createdAt))
1571
+ .limit(50)
1572
+ .all();
1573
+
1574
+ const assistantIds: string[] = [];
1575
+ let boundaryCreatedAt: number | null = null;
1576
+
1577
+ for (const row of backwardRows) {
1578
+ if (row.role === "assistant") {
1579
+ assistantIds.push(row.id);
1580
+ } else if (row.role === "user") {
1581
+ if (isToolResultMessage(row.role, row.content)) {
1582
+ // Tool-result user message — still within the same turn, continue.
1583
+ continue;
1584
+ }
1585
+ // Real user message — this is the turn boundary.
1586
+ boundaryCreatedAt = row.createdAt;
1587
+ break;
1588
+ }
1589
+ }
1590
+
1591
+ // Walk forward from the target to collect any later assistant messages
1592
+ // still within the same turn (e.g. when querying an intermediate
1593
+ // message like A1 in a multi-step turn A1 → tool_result → A2).
1594
+ const forwardRows = db
1595
+ .select({
1596
+ id: messages.id,
1597
+ role: messages.role,
1598
+ content: messages.content,
1599
+ createdAt: messages.createdAt,
1600
+ })
1601
+ .from(messages)
1602
+ .where(
1603
+ and(
1604
+ eq(messages.conversationId, target.conversationId),
1605
+ gt(messages.createdAt, target.createdAt),
1606
+ ),
1607
+ )
1608
+ .orderBy(asc(messages.createdAt))
1609
+ .limit(50)
1610
+ .all();
1611
+
1612
+ for (const row of forwardRows) {
1613
+ if (row.role === "assistant") {
1614
+ if (!assistantIds.includes(row.id)) {
1615
+ assistantIds.push(row.id);
1616
+ }
1617
+ } else if (row.role === "user") {
1618
+ if (isToolResultMessage(row.role, row.content)) {
1619
+ // Tool-result user message — still within the same turn.
1620
+ continue;
1621
+ }
1622
+ // Real user message — end of the turn.
1623
+ break;
1624
+ }
1625
+ }
1626
+
1627
+ // Also query forward from the backward-walk boundary to pick up any
1628
+ // assistant messages between the boundary and the target that may have
1629
+ // been missed (e.g. due to the 50-row limit in the backward walk).
1630
+ if (boundaryCreatedAt != null) {
1631
+ const gapRows = db
1632
+ .select({
1633
+ id: messages.id,
1634
+ role: messages.role,
1635
+ createdAt: messages.createdAt,
1636
+ })
1637
+ .from(messages)
1638
+ .where(
1639
+ and(
1640
+ eq(messages.conversationId, target.conversationId),
1641
+ gt(messages.createdAt, boundaryCreatedAt),
1642
+ lte(messages.createdAt, target.createdAt),
1643
+ ),
1644
+ )
1645
+ .orderBy(asc(messages.createdAt))
1646
+ .all();
1647
+
1648
+ for (const row of gapRows) {
1649
+ if (row.role === "assistant" && !assistantIds.includes(row.id)) {
1650
+ assistantIds.push(row.id);
1651
+ }
1652
+ }
1653
+ }
1654
+
1655
+ // Sort by createdAt to ensure stable ordering.
1656
+ // Re-fetch createdAt for all collected IDs so the sort is accurate.
1657
+ if (assistantIds.length <= 1) return assistantIds;
1658
+
1659
+ const idSet = new Set(assistantIds);
1660
+ const sorted = db
1661
+ .select({ id: messages.id, createdAt: messages.createdAt })
1662
+ .from(messages)
1663
+ .where(
1664
+ and(
1665
+ eq(messages.conversationId, target.conversationId),
1666
+ inArray(messages.id, [...idSet]),
1667
+ ),
1668
+ )
1669
+ .orderBy(asc(messages.createdAt))
1670
+ .all();
1671
+
1672
+ return sorted.map((r) => r.id);
1673
+ }