@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
@@ -4,6 +4,7 @@
4
4
  import { existsSync, readdirSync, statSync } from "node:fs";
5
5
  import { join, relative } from "node:path";
6
6
 
7
+ import { enrichMessageWithSourcePaths } from "../../agent/attachments.js";
7
8
  import {
8
9
  createAssistantMessage,
9
10
  createUserMessage,
@@ -21,10 +22,13 @@ import {
21
22
  isModelSlashCommand,
22
23
  } from "../../daemon/conversation-process.js";
23
24
  import {
24
- isProviderShortcut,
25
25
  resolveSlash,
26
26
  type SlashContext,
27
27
  } from "../../daemon/conversation-slash.js";
28
+ import {
29
+ getCannedFirstGreeting,
30
+ isWakeUpGreeting,
31
+ } from "../../daemon/first-greeting.js";
28
32
  import { renderHistoryContent } from "../../daemon/handlers/shared.js";
29
33
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
30
34
  import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
@@ -34,6 +38,7 @@ import * as attachmentsStore from "../../memory/attachments-store.js";
34
38
  import {
35
39
  createCanonicalGuardianRequest,
36
40
  generateCanonicalRequestCode,
41
+ listCanonicalGuardianRequests,
37
42
  listPendingRequestsByConversationScope,
38
43
  resolveCanonicalGuardianRequest,
39
44
  } from "../../memory/canonical-guardian-store.js";
@@ -97,6 +102,43 @@ function collectCanonicalGuardianRequestHintIds(
97
102
  .map((req) => req.id);
98
103
  }
99
104
 
105
+ /**
106
+ * Expire orphaned canonical guardian requests for a conversation.
107
+ *
108
+ * After the in-memory auto-deny loop runs, there may still be "pending"
109
+ * canonical requests in the DB that have no corresponding in-memory
110
+ * pending interaction (e.g. the prompter timed out and resolved the
111
+ * confirmation directly without syncing canonical status). This sweep
112
+ * catches those stragglers so they don't get falsely matched by the
113
+ * guardian reply router on subsequent messages.
114
+ *
115
+ * Only expires requests *sourced from* (not merely delivered to) this
116
+ * conversation. Delivered requests may still have live pending interactions
117
+ * in their source conversation. Additionally skips requests that still
118
+ * have a live in-memory pending interaction.
119
+ *
120
+ * Uses `listCanonicalGuardianRequests` (not `listPendingRequestsByConversationScope`)
121
+ * so that time-expired requests (past their `expiresAt`) are also caught
122
+ * instead of being silently filtered out.
123
+ */
124
+ function expireOrphanedCanonicalRequests(conversationId: string): void {
125
+ const sourceScoped = listCanonicalGuardianRequests({
126
+ conversationId,
127
+ status: "pending",
128
+ kind: "tool_approval",
129
+ });
130
+
131
+ for (const req of sourceScoped) {
132
+ // Skip requests that still have a live in-memory pending interaction —
133
+ // they are not orphaned.
134
+ if (pendingInteractions.get(req.id)) continue;
135
+
136
+ resolveCanonicalGuardianRequest(req.id, "pending", {
137
+ status: "expired",
138
+ });
139
+ }
140
+ }
141
+
100
142
  async function tryConsumeCanonicalGuardianReply(params: {
101
143
  conversationId: string;
102
144
  sourceChannel: string;
@@ -107,6 +149,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
107
149
  filename: string;
108
150
  mimeType: string;
109
151
  data: string;
152
+ filePath?: string;
110
153
  }>;
111
154
  conversation: import("../../daemon/conversation.js").Conversation;
112
155
  onEvent: (msg: ServerMessage) => void;
@@ -139,8 +182,12 @@ async function tryConsumeCanonicalGuardianReply(params: {
139
182
  sourceChannel,
140
183
  conversation,
141
184
  );
142
- const pendingRequestIds =
143
- pendingRequestHintIds.length > 0 ? pendingRequestHintIds : undefined;
185
+ // Always pass the hints array (even when empty) so
186
+ // findPendingCanonicalRequests respects the in-memory staleness filter
187
+ // applied by collectCanonicalGuardianRequestHintIds. Converting empty
188
+ // hints to `undefined` caused the router to fall through to raw DB
189
+ // queries that rediscovered stale canonical requests.
190
+ const pendingRequestIds = pendingRequestHintIds;
144
191
 
145
192
  const routerResult = await routeGuardianReply({
146
193
  messageText: trimmedContent,
@@ -183,19 +230,33 @@ async function tryConsumeCanonicalGuardianReply(params: {
183
230
  // is not re-processed as a new user turn.
184
231
  let messageId: string | undefined;
185
232
  try {
233
+ const guardianImageSourcePaths: Record<string, string> = {};
234
+ for (let i = 0; i < attachments.length; i++) {
235
+ const a = attachments[i];
236
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
237
+ guardianImageSourcePaths[`${i}:${a.filename}`] = a.filePath;
238
+ }
239
+ }
186
240
  const channelMeta = {
187
241
  userMessageChannel: sourceChannel,
188
242
  assistantMessageChannel: sourceChannel,
189
243
  userMessageInterface: sourceInterface,
190
244
  assistantMessageInterface: sourceInterface,
191
245
  provenanceTrustClass: "guardian" as const,
246
+ ...(Object.keys(guardianImageSourcePaths).length > 0
247
+ ? { imageSourcePaths: guardianImageSourcePaths }
248
+ : {}),
192
249
  };
193
250
 
194
- const userMessage = createUserMessage(content, attachments);
251
+ const cleanUserMessage = createUserMessage(content, attachments);
252
+ const llmUserMessage = enrichMessageWithSourcePaths(
253
+ cleanUserMessage,
254
+ attachments,
255
+ );
195
256
  const persistedUser = await addMessage(
196
257
  conversationId,
197
258
  "user",
198
- JSON.stringify(userMessage.content),
259
+ JSON.stringify(cleanUserMessage.content),
199
260
  channelMeta,
200
261
  );
201
262
  messageId = persistedUser.id;
@@ -215,7 +276,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
215
276
 
216
277
  // Avoid mutating in-memory history / emitting stream deltas while a run is active.
217
278
  if (!conversation.isProcessing()) {
218
- conversation.getMessages().push(userMessage, assistantMessage);
279
+ conversation.getMessages().push(llmUserMessage, assistantMessage);
219
280
  onEvent({
220
281
  type: "assistant_text_delta",
221
282
  text: replyText,
@@ -327,15 +388,11 @@ export function handleListMessages(
327
388
  // generate thumbnails for inline display on history restore.
328
389
  const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
329
390
  if (linked.length > 0) {
330
- // Batch-fetch file-backed status for all attachments in one query
331
- // instead of issuing a separate query per attachment.
332
- const fileBackedIds = attachmentsStore.getFileBackedAttachmentIds(
333
- linked.map((a) => a.id),
334
- );
335
391
  msgAttachments = linked.map((a) => {
336
- const isFileBacked = fileBackedIds.has(a.id);
337
392
  if (a.mimeType.startsWith("image/")) {
338
- const full = attachmentsStore.getAttachmentById(a.id);
393
+ const full = attachmentsStore.getAttachmentById(a.id, {
394
+ hydrateFileData: true,
395
+ });
339
396
  return {
340
397
  id: a.id,
341
398
  filename: a.originalFilename,
@@ -346,7 +403,7 @@ export function handleListMessages(
346
403
  ...(a.thumbnailBase64
347
404
  ? { thumbnailData: a.thumbnailBase64 }
348
405
  : {}),
349
- ...(isFileBacked ? { fileBacked: true } : {}),
406
+ fileBacked: true,
350
407
  };
351
408
  }
352
409
  return {
@@ -356,7 +413,7 @@ export function handleListMessages(
356
413
  sizeBytes: a.sizeBytes,
357
414
  kind: a.kind,
358
415
  ...(a.thumbnailBase64 ? { thumbnailData: a.thumbnailBase64 } : {}),
359
- ...(isFileBacked ? { fileBacked: true } : {}),
416
+ fileBacked: true,
360
417
  };
361
418
  });
362
419
  }
@@ -748,6 +805,92 @@ export async function handleSendMessage(
748
805
  skipProxySenderUpdate: preservingProxies,
749
806
  });
750
807
 
808
+ // ── Canned first-greeting fast path ──
809
+ // On a completely fresh workspace, skip LLM inference for the macOS
810
+ // wake-up greeting and return a pre-written response. This eliminates
811
+ // 10-30s of inference latency on first boot.
812
+ if (isWakeUpGreeting(trimmedContent, conversation.getMessages().length)) {
813
+ const cannedGreeting = getCannedFirstGreeting();
814
+ if (cannedGreeting) {
815
+ conversation.processing = true;
816
+ let cleanupDeferred = false;
817
+ try {
818
+ const provenance = provenanceFromTrustContext(
819
+ conversation.trustContext,
820
+ );
821
+ const channelMeta = {
822
+ ...provenance,
823
+ userMessageChannel: sourceChannel,
824
+ assistantMessageChannel: sourceChannel,
825
+ userMessageInterface: sourceInterface,
826
+ assistantMessageInterface: sourceInterface,
827
+ };
828
+
829
+ const rawContent = content ?? "";
830
+ const attachments = hasAttachments
831
+ ? smDeps.resolveAttachments(attachmentIds)
832
+ : [];
833
+ const userMsg = createUserMessage(rawContent, attachments);
834
+ const persisted = await addMessage(
835
+ mapping.conversationId,
836
+ "user",
837
+ JSON.stringify(userMsg.content),
838
+ channelMeta,
839
+ );
840
+ conversation.getMessages().push(userMsg);
841
+
842
+ setConversationOriginChannelIfUnset(
843
+ mapping.conversationId,
844
+ sourceChannel,
845
+ );
846
+ setConversationOriginInterfaceIfUnset(
847
+ mapping.conversationId,
848
+ sourceInterface,
849
+ );
850
+
851
+ const assistantMsg = createAssistantMessage(cannedGreeting);
852
+ await addMessage(
853
+ mapping.conversationId,
854
+ "assistant",
855
+ JSON.stringify(assistantMsg.content),
856
+ channelMeta,
857
+ );
858
+ conversation.getMessages().push(assistantMsg);
859
+
860
+ const conversationId = mapping.conversationId;
861
+ const response = Response.json(
862
+ { accepted: true, messageId: persisted.id, conversationId },
863
+ { status: 202 },
864
+ );
865
+
866
+ // Defer event publishing to next tick (same pattern as unknown-slash
867
+ // fast path) so the HTTP response reaches the client before SSE
868
+ // events arrive.
869
+ setTimeout(() => {
870
+ onEvent({ type: "assistant_text_delta", text: cannedGreeting });
871
+ onEvent({ type: "message_complete", conversationId });
872
+ conversation.processing = false;
873
+ silentlyWithLog(
874
+ conversation.drainQueue(),
875
+ "canned-greeting queue drain",
876
+ );
877
+ }, 0);
878
+
879
+ log.info(
880
+ { conversationId },
881
+ "Served canned first greeting — skipped LLM inference",
882
+ );
883
+ cleanupDeferred = true;
884
+ return response;
885
+ } finally {
886
+ if (!cleanupDeferred && conversation.processing) {
887
+ conversation.processing = false;
888
+ silentlyWithLog(conversation.drainQueue(), "error-path queue drain");
889
+ }
890
+ }
891
+ }
892
+ }
893
+
751
894
  const attachments = hasAttachments
752
895
  ? smDeps.resolveAttachments(attachmentIds)
753
896
  : [];
@@ -856,6 +999,10 @@ export async function handleSendMessage(
856
999
  pendingInteractions.removeByConversation(conversation);
857
1000
  }
858
1001
 
1002
+ // Expire any orphaned canonical requests that survived without a
1003
+ // matching in-memory pending interaction (e.g. prompter timeouts).
1004
+ expireOrphanedCanonicalRequests(mapping.conversationId);
1005
+
859
1006
  return Response.json(
860
1007
  { accepted: true, queued: true, conversationId: mapping.conversationId },
861
1008
  { status: 202 },
@@ -892,6 +1039,10 @@ export async function handleSendMessage(
892
1039
  pendingInteractions.removeByConversation(conversation);
893
1040
  }
894
1041
 
1042
+ // Expire any orphaned canonical requests that survived without a
1043
+ // matching in-memory pending interaction (e.g. prompter timeouts).
1044
+ expireOrphanedCanonicalRequests(mapping.conversationId);
1045
+
895
1046
  // Conversation is idle — persist and fire agent loop immediately
896
1047
  conversation.setTurnChannelContext({
897
1048
  userMessageChannel: sourceChannel,
@@ -915,6 +1066,7 @@ export async function handleSendMessage(
915
1066
  model: config.services.inference.model,
916
1067
  provider: config.services.inference.provider,
917
1068
  estimatedCost: conversation.usageStats.estimatedCost,
1069
+ userMessageInterface: sourceInterface,
918
1070
  };
919
1071
  const slashResult = await resolveSlash(rawContent, slashContext);
920
1072
 
@@ -923,6 +1075,13 @@ export async function handleSendMessage(
923
1075
  let cleanupDeferred = false;
924
1076
  try {
925
1077
  const provenance = provenanceFromTrustContext(conversation.trustContext);
1078
+ const imageSourcePaths: Record<string, string> = {};
1079
+ for (let i = 0; i < attachments.length; i++) {
1080
+ const a = attachments[i];
1081
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
1082
+ imageSourcePaths[`${i}:${a.filename}`] = a.filePath;
1083
+ }
1084
+ }
926
1085
  const channelMeta = {
927
1086
  ...provenance,
928
1087
  userMessageChannel: sourceChannel,
@@ -930,15 +1089,19 @@ export async function handleSendMessage(
930
1089
  userMessageInterface: sourceInterface,
931
1090
  assistantMessageInterface: sourceInterface,
932
1091
  ...(body.automated === true ? { automated: true } : {}),
1092
+ ...(Object.keys(imageSourcePaths).length > 0
1093
+ ? { imageSourcePaths }
1094
+ : {}),
933
1095
  };
934
- const userMsg = createUserMessage(rawContent, attachments);
1096
+ const cleanMsg = createUserMessage(rawContent, attachments);
1097
+ const llmMsg = enrichMessageWithSourcePaths(cleanMsg, attachments);
935
1098
  const persisted = await addMessage(
936
1099
  mapping.conversationId,
937
1100
  "user",
938
- JSON.stringify(userMsg.content),
1101
+ JSON.stringify(cleanMsg.content),
939
1102
  channelMeta,
940
1103
  );
941
- conversation.getMessages().push(userMsg);
1104
+ conversation.getMessages().push(llmMsg);
942
1105
 
943
1106
  const assistantMsg = createAssistantMessage(slashResult.message);
944
1107
  await addMessage(
@@ -960,10 +1123,9 @@ export async function handleSendMessage(
960
1123
 
961
1124
  // Snapshot model info now so the deferred callback cannot observe
962
1125
  // a config change from a concurrent request.
963
- const modelInfoEvent =
964
- isModelSlashCommand(rawContent) || isProviderShortcut(rawContent)
965
- ? await buildModelInfoEvent()
966
- : null;
1126
+ const modelInfoEvent = isModelSlashCommand(rawContent)
1127
+ ? await buildModelInfoEvent()
1128
+ : null;
967
1129
 
968
1130
  const response = Response.json(
969
1131
  {
@@ -1047,6 +1209,7 @@ async function generateLlmSuggestion(
1047
1209
  provider: Provider,
1048
1210
  assistantText: string,
1049
1211
  ): Promise<string | null> {
1212
+ const log = (await import("../../util/logger.js")).getLogger("runtime-http");
1050
1213
  const truncated =
1051
1214
  assistantText.length > 2000 ? assistantText.slice(-2000) : assistantText;
1052
1215
 
@@ -1055,18 +1218,41 @@ async function generateLlmSuggestion(
1055
1218
  [{ role: "user", content: [{ type: "text", text: prompt }] }],
1056
1219
  [], // no tools
1057
1220
  undefined, // no system prompt
1058
- { config: { max_tokens: 30 } },
1221
+ { config: { max_tokens: 40, modelIntent: "latency-optimized" } },
1059
1222
  );
1060
1223
 
1061
1224
  const textBlock = response.content.find((b) => b.type === "text");
1062
1225
  const raw = textBlock && "text" in textBlock ? textBlock.text.trim() : "";
1226
+ const stripped = raw.replace(/^["']+|["']+$/g, "");
1063
1227
 
1064
- if (!raw) return null;
1228
+ if (!stripped) {
1229
+ log.debug("Suggestion rejected: empty LLM response");
1230
+ return null;
1231
+ }
1065
1232
 
1066
1233
  // Take first line only, then enforce the length cap
1067
- const firstLine = raw.split("\n")[0].trim();
1068
- if (!firstLine || firstLine.length > 50) return null;
1069
- return firstLine;
1234
+ const firstLine = stripped.split("\n")[0].trim();
1235
+ if (!firstLine) {
1236
+ log.debug(
1237
+ { rawLength: stripped.length },
1238
+ "Suggestion rejected: empty after first-line extraction",
1239
+ );
1240
+ return null;
1241
+ }
1242
+ if (firstLine.length <= 50) return firstLine;
1243
+ // Truncate at last word boundary within 50 chars
1244
+ const wordTruncated = firstLine
1245
+ .slice(0, 50)
1246
+ .replace(/\s+\S*$/, "")
1247
+ .trim();
1248
+ if (wordTruncated.length < 15) {
1249
+ log.debug(
1250
+ { rawLength: firstLine.length, truncatedLength: wordTruncated.length },
1251
+ "Suggestion rejected: too short after word-boundary truncation",
1252
+ );
1253
+ return null;
1254
+ }
1255
+ return wordTruncated;
1070
1256
  }
1071
1257
 
1072
1258
  export async function handleGetSuggestion(
@@ -1193,8 +1379,16 @@ export async function handleGetSuggestion(
1193
1379
  }
1194
1380
  } catch (err) {
1195
1381
  suggestionInFlight.delete(msg.id);
1196
- log.warn({ err }, "LLM suggestion failed");
1382
+ log.warn(
1383
+ { err, conversationKey, messageId: msg.id },
1384
+ "LLM suggestion failed",
1385
+ );
1197
1386
  }
1387
+ } else {
1388
+ log.debug(
1389
+ { conversationKey, messageId: msg.id },
1390
+ "Suggestion skipped: no provider available",
1391
+ );
1198
1392
  }
1199
1393
 
1200
1394
  return Response.json({
@@ -123,8 +123,10 @@ function handleListConversationStarters(url: URL): Response {
123
123
 
124
124
  const db = getDb();
125
125
 
126
- // Fetch chips (ranked by model, newest batch first)
127
- const rawItems = db
126
+ // Fetch all chips (ranked by model, newest batch first), apply diversity
127
+ // reordering, then paginate. Reordering must happen before offset/limit so
128
+ // that paginated results are stable across pages.
129
+ const allItems = db
128
130
  .select({
129
131
  id: conversationStarters.id,
130
132
  label: conversationStarters.label,
@@ -143,20 +145,16 @@ function handleListConversationStarters(url: URL): Response {
143
145
  desc(conversationStarters.generationBatch),
144
146
  desc(conversationStarters.createdAt),
145
147
  )
146
- .limit(limitParam)
147
- .offset(offsetParam)
148
148
  .all();
149
149
 
150
- const countRow = rawGet<{ c: number }>(
151
- `SELECT COUNT(*) AS c FROM conversation_starters WHERE scope_id = ? AND card_type = 'chip'`,
152
- scopeId,
153
- );
154
- const total = countRow?.c ?? 0;
150
+ const total = allItems.length;
155
151
 
156
- // If starters exist, return them immediately.
152
+ // If starters exist, reorder for category diversity then paginate.
157
153
  if (total > 0) {
154
+ const ordered = orderStrongestFirst(allItems);
155
+ const page = ordered.slice(offsetParam, offsetParam + limitParam);
158
156
  return Response.json({
159
- starters: orderStrongestFirst(rawItems),
157
+ starters: page,
160
158
  total,
161
159
  status: "ready",
162
160
  });
@@ -410,6 +410,7 @@ async function handleDiagnosticsExport(body: {
410
410
  return JSON.stringify({
411
411
  id: r.id,
412
412
  conversationId: r.conversationId,
413
+ provider: r.provider,
413
414
  request: redactDeep(request),
414
415
  response: redactDeep(response),
415
416
  createdAt: r.createdAt,
@@ -334,7 +334,7 @@ export async function enforceIngressAcl(
334
334
  dmCallbackUrl,
335
335
  {
336
336
  chatId: senderUserId,
337
- text: "I've notified the owner that you'd like to chat with me. If they approve your request, they'll share a 6-digit verification code with you. You can reply with the code here.",
337
+ text: "I don't recognize you yet! I've let my owner know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.",
338
338
  assistantId,
339
339
  },
340
340
  mintBearerToken(),
@@ -588,7 +588,7 @@ export async function enforceIngressAcl(
588
588
  dmCallbackUrl,
589
589
  {
590
590
  chatId: senderUserId,
591
- text: "I've notified the owner that you'd like to chat with me. If they approve your request, they'll share a 6-digit verification code with you. You can reply with the code here.",
591
+ text: "I don't recognize you yet! I've let my owner know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.",
592
592
  assistantId,
593
593
  },
594
594
  mintBearerToken(),