@vellumai/assistant 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -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";
@@ -76,6 +81,9 @@ import {
76
81
 
77
82
  const log = getLogger("conversation-routes");
78
83
 
84
+ /** Matches the `<no_response/>` sentinel used by channel delivery suppression. */
85
+ const NO_RESPONSE_INLINE_RE = /<no_response\s*\/?>/g;
86
+
79
87
  const SUGGESTION_CACHE_MAX = 100;
80
88
 
81
89
  function collectCanonicalGuardianRequestHintIds(
@@ -97,6 +105,43 @@ function collectCanonicalGuardianRequestHintIds(
97
105
  .map((req) => req.id);
98
106
  }
99
107
 
108
+ /**
109
+ * Expire orphaned canonical guardian requests for a conversation.
110
+ *
111
+ * After the in-memory auto-deny loop runs, there may still be "pending"
112
+ * canonical requests in the DB that have no corresponding in-memory
113
+ * pending interaction (e.g. the prompter timed out and resolved the
114
+ * confirmation directly without syncing canonical status). This sweep
115
+ * catches those stragglers so they don't get falsely matched by the
116
+ * guardian reply router on subsequent messages.
117
+ *
118
+ * Only expires requests *sourced from* (not merely delivered to) this
119
+ * conversation. Delivered requests may still have live pending interactions
120
+ * in their source conversation. Additionally skips requests that still
121
+ * have a live in-memory pending interaction.
122
+ *
123
+ * Uses `listCanonicalGuardianRequests` (not `listPendingRequestsByConversationScope`)
124
+ * so that time-expired requests (past their `expiresAt`) are also caught
125
+ * instead of being silently filtered out.
126
+ */
127
+ function expireOrphanedCanonicalRequests(conversationId: string): void {
128
+ const sourceScoped = listCanonicalGuardianRequests({
129
+ conversationId,
130
+ status: "pending",
131
+ kind: "tool_approval",
132
+ });
133
+
134
+ for (const req of sourceScoped) {
135
+ // Skip requests that still have a live in-memory pending interaction —
136
+ // they are not orphaned.
137
+ if (pendingInteractions.get(req.id)) continue;
138
+
139
+ resolveCanonicalGuardianRequest(req.id, "pending", {
140
+ status: "expired",
141
+ });
142
+ }
143
+ }
144
+
100
145
  async function tryConsumeCanonicalGuardianReply(params: {
101
146
  conversationId: string;
102
147
  sourceChannel: string;
@@ -107,6 +152,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
107
152
  filename: string;
108
153
  mimeType: string;
109
154
  data: string;
155
+ filePath?: string;
110
156
  }>;
111
157
  conversation: import("../../daemon/conversation.js").Conversation;
112
158
  onEvent: (msg: ServerMessage) => void;
@@ -139,8 +185,12 @@ async function tryConsumeCanonicalGuardianReply(params: {
139
185
  sourceChannel,
140
186
  conversation,
141
187
  );
142
- const pendingRequestIds =
143
- pendingRequestHintIds.length > 0 ? pendingRequestHintIds : undefined;
188
+ // Always pass the hints array (even when empty) so
189
+ // findPendingCanonicalRequests respects the in-memory staleness filter
190
+ // applied by collectCanonicalGuardianRequestHintIds. Converting empty
191
+ // hints to `undefined` caused the router to fall through to raw DB
192
+ // queries that rediscovered stale canonical requests.
193
+ const pendingRequestIds = pendingRequestHintIds;
144
194
 
145
195
  const routerResult = await routeGuardianReply({
146
196
  messageText: trimmedContent,
@@ -183,19 +233,33 @@ async function tryConsumeCanonicalGuardianReply(params: {
183
233
  // is not re-processed as a new user turn.
184
234
  let messageId: string | undefined;
185
235
  try {
236
+ const guardianImageSourcePaths: Record<string, string> = {};
237
+ for (let i = 0; i < attachments.length; i++) {
238
+ const a = attachments[i];
239
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
240
+ guardianImageSourcePaths[`${i}:${a.filename}`] = a.filePath;
241
+ }
242
+ }
186
243
  const channelMeta = {
187
244
  userMessageChannel: sourceChannel,
188
245
  assistantMessageChannel: sourceChannel,
189
246
  userMessageInterface: sourceInterface,
190
247
  assistantMessageInterface: sourceInterface,
191
248
  provenanceTrustClass: "guardian" as const,
249
+ ...(Object.keys(guardianImageSourcePaths).length > 0
250
+ ? { imageSourcePaths: guardianImageSourcePaths }
251
+ : {}),
192
252
  };
193
253
 
194
- const userMessage = createUserMessage(content, attachments);
254
+ const cleanUserMessage = createUserMessage(content, attachments);
255
+ const llmUserMessage = enrichMessageWithSourcePaths(
256
+ cleanUserMessage,
257
+ attachments,
258
+ );
195
259
  const persistedUser = await addMessage(
196
260
  conversationId,
197
261
  "user",
198
- JSON.stringify(userMessage.content),
262
+ JSON.stringify(cleanUserMessage.content),
199
263
  channelMeta,
200
264
  );
201
265
  messageId = persistedUser.id;
@@ -215,7 +279,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
215
279
 
216
280
  // Avoid mutating in-memory history / emitting stream deltas while a run is active.
217
281
  if (!conversation.isProcessing()) {
218
- conversation.getMessages().push(userMessage, assistantMessage);
282
+ conversation.getMessages().push(llmUserMessage, assistantMessage);
219
283
  onEvent({
220
284
  type: "assistant_text_delta",
221
285
  text: replyText,
@@ -302,6 +366,48 @@ export function handleListMessages(
302
366
  content = msg.content;
303
367
  }
304
368
  const rendered = renderHistoryContent(content);
369
+
370
+ // Strip <no_response/> markers from assistant messages so web/API
371
+ // clients never see the raw sentinel. Only assistant messages produce
372
+ // this marker; user messages are left untouched.
373
+ if (msg.role === "assistant") {
374
+ const originalSegments = rendered.textSegments;
375
+ const keepIndices: number[] = [];
376
+ const filteredSegments: string[] = [];
377
+ for (let i = 0; i < originalSegments.length; i++) {
378
+ const cleaned = originalSegments[i]
379
+ .replace(NO_RESPONSE_INLINE_RE, "")
380
+ .trim();
381
+ if (cleaned.length > 0) {
382
+ keepIndices.push(i);
383
+ filteredSegments.push(cleaned);
384
+ }
385
+ }
386
+ // Remap contentOrder text:N indices to account for removed segments
387
+ const indexMap = new Map<number, number>();
388
+ keepIndices.forEach((oldIdx, newIdx) => indexMap.set(oldIdx, newIdx));
389
+ const filteredContentOrder = rendered.contentOrder
390
+ .map((entry) => {
391
+ const m = entry.match(/^text:(\d+)$/);
392
+ if (!m) return entry;
393
+ const newIdx = indexMap.get(Number(m[1]));
394
+ return newIdx !== undefined ? `text:${newIdx}` : undefined;
395
+ })
396
+ .filter((e): e is string => e !== undefined);
397
+
398
+ return {
399
+ role: msg.role,
400
+ text: rendered.text.replace(NO_RESPONSE_INLINE_RE, "").trim(),
401
+ timestamp: msg.createdAt,
402
+ toolCalls: rendered.toolCalls,
403
+ toolCallsBeforeText: rendered.toolCallsBeforeText,
404
+ textSegments: filteredSegments,
405
+ contentOrder: filteredContentOrder,
406
+ surfaces: rendered.surfaces,
407
+ id: msg.id,
408
+ };
409
+ }
410
+
305
411
  return {
306
412
  role: msg.role,
307
413
  text: rendered.text,
@@ -327,15 +433,11 @@ export function handleListMessages(
327
433
  // generate thumbnails for inline display on history restore.
328
434
  const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
329
435
  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
436
  msgAttachments = linked.map((a) => {
336
- const isFileBacked = fileBackedIds.has(a.id);
337
437
  if (a.mimeType.startsWith("image/")) {
338
- const full = attachmentsStore.getAttachmentById(a.id);
438
+ const full = attachmentsStore.getAttachmentById(a.id, {
439
+ hydrateFileData: true,
440
+ });
339
441
  return {
340
442
  id: a.id,
341
443
  filename: a.originalFilename,
@@ -346,7 +448,7 @@ export function handleListMessages(
346
448
  ...(a.thumbnailBase64
347
449
  ? { thumbnailData: a.thumbnailBase64 }
348
450
  : {}),
349
- ...(isFileBacked ? { fileBacked: true } : {}),
451
+ fileBacked: true,
350
452
  };
351
453
  }
352
454
  return {
@@ -356,7 +458,7 @@ export function handleListMessages(
356
458
  sizeBytes: a.sizeBytes,
357
459
  kind: a.kind,
358
460
  ...(a.thumbnailBase64 ? { thumbnailData: a.thumbnailBase64 } : {}),
359
- ...(isFileBacked ? { fileBacked: true } : {}),
461
+ fileBacked: true,
360
462
  };
361
463
  });
362
464
  }
@@ -748,6 +850,92 @@ export async function handleSendMessage(
748
850
  skipProxySenderUpdate: preservingProxies,
749
851
  });
750
852
 
853
+ // ── Canned first-greeting fast path ──
854
+ // On a completely fresh workspace, skip LLM inference for the macOS
855
+ // wake-up greeting and return a pre-written response. This eliminates
856
+ // 10-30s of inference latency on first boot.
857
+ if (isWakeUpGreeting(trimmedContent, conversation.getMessages().length)) {
858
+ const cannedGreeting = getCannedFirstGreeting();
859
+ if (cannedGreeting) {
860
+ conversation.processing = true;
861
+ let cleanupDeferred = false;
862
+ try {
863
+ const provenance = provenanceFromTrustContext(
864
+ conversation.trustContext,
865
+ );
866
+ const channelMeta = {
867
+ ...provenance,
868
+ userMessageChannel: sourceChannel,
869
+ assistantMessageChannel: sourceChannel,
870
+ userMessageInterface: sourceInterface,
871
+ assistantMessageInterface: sourceInterface,
872
+ };
873
+
874
+ const rawContent = content ?? "";
875
+ const attachments = hasAttachments
876
+ ? smDeps.resolveAttachments(attachmentIds)
877
+ : [];
878
+ const userMsg = createUserMessage(rawContent, attachments);
879
+ const persisted = await addMessage(
880
+ mapping.conversationId,
881
+ "user",
882
+ JSON.stringify(userMsg.content),
883
+ channelMeta,
884
+ );
885
+ conversation.getMessages().push(userMsg);
886
+
887
+ setConversationOriginChannelIfUnset(
888
+ mapping.conversationId,
889
+ sourceChannel,
890
+ );
891
+ setConversationOriginInterfaceIfUnset(
892
+ mapping.conversationId,
893
+ sourceInterface,
894
+ );
895
+
896
+ const assistantMsg = createAssistantMessage(cannedGreeting);
897
+ await addMessage(
898
+ mapping.conversationId,
899
+ "assistant",
900
+ JSON.stringify(assistantMsg.content),
901
+ channelMeta,
902
+ );
903
+ conversation.getMessages().push(assistantMsg);
904
+
905
+ const conversationId = mapping.conversationId;
906
+ const response = Response.json(
907
+ { accepted: true, messageId: persisted.id, conversationId },
908
+ { status: 202 },
909
+ );
910
+
911
+ // Defer event publishing to next tick (same pattern as unknown-slash
912
+ // fast path) so the HTTP response reaches the client before SSE
913
+ // events arrive.
914
+ setTimeout(() => {
915
+ onEvent({ type: "assistant_text_delta", text: cannedGreeting });
916
+ onEvent({ type: "message_complete", conversationId });
917
+ conversation.processing = false;
918
+ silentlyWithLog(
919
+ conversation.drainQueue(),
920
+ "canned-greeting queue drain",
921
+ );
922
+ }, 0);
923
+
924
+ log.info(
925
+ { conversationId },
926
+ "Served canned first greeting — skipped LLM inference",
927
+ );
928
+ cleanupDeferred = true;
929
+ return response;
930
+ } finally {
931
+ if (!cleanupDeferred && conversation.processing) {
932
+ conversation.processing = false;
933
+ silentlyWithLog(conversation.drainQueue(), "error-path queue drain");
934
+ }
935
+ }
936
+ }
937
+ }
938
+
751
939
  const attachments = hasAttachments
752
940
  ? smDeps.resolveAttachments(attachmentIds)
753
941
  : [];
@@ -856,6 +1044,10 @@ export async function handleSendMessage(
856
1044
  pendingInteractions.removeByConversation(conversation);
857
1045
  }
858
1046
 
1047
+ // Expire any orphaned canonical requests that survived without a
1048
+ // matching in-memory pending interaction (e.g. prompter timeouts).
1049
+ expireOrphanedCanonicalRequests(mapping.conversationId);
1050
+
859
1051
  return Response.json(
860
1052
  { accepted: true, queued: true, conversationId: mapping.conversationId },
861
1053
  { status: 202 },
@@ -892,6 +1084,10 @@ export async function handleSendMessage(
892
1084
  pendingInteractions.removeByConversation(conversation);
893
1085
  }
894
1086
 
1087
+ // Expire any orphaned canonical requests that survived without a
1088
+ // matching in-memory pending interaction (e.g. prompter timeouts).
1089
+ expireOrphanedCanonicalRequests(mapping.conversationId);
1090
+
895
1091
  // Conversation is idle — persist and fire agent loop immediately
896
1092
  conversation.setTurnChannelContext({
897
1093
  userMessageChannel: sourceChannel,
@@ -915,6 +1111,7 @@ export async function handleSendMessage(
915
1111
  model: config.services.inference.model,
916
1112
  provider: config.services.inference.provider,
917
1113
  estimatedCost: conversation.usageStats.estimatedCost,
1114
+ userMessageInterface: sourceInterface,
918
1115
  };
919
1116
  const slashResult = await resolveSlash(rawContent, slashContext);
920
1117
 
@@ -923,6 +1120,13 @@ export async function handleSendMessage(
923
1120
  let cleanupDeferred = false;
924
1121
  try {
925
1122
  const provenance = provenanceFromTrustContext(conversation.trustContext);
1123
+ const imageSourcePaths: Record<string, string> = {};
1124
+ for (let i = 0; i < attachments.length; i++) {
1125
+ const a = attachments[i];
1126
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
1127
+ imageSourcePaths[`${i}:${a.filename}`] = a.filePath;
1128
+ }
1129
+ }
926
1130
  const channelMeta = {
927
1131
  ...provenance,
928
1132
  userMessageChannel: sourceChannel,
@@ -930,15 +1134,19 @@ export async function handleSendMessage(
930
1134
  userMessageInterface: sourceInterface,
931
1135
  assistantMessageInterface: sourceInterface,
932
1136
  ...(body.automated === true ? { automated: true } : {}),
1137
+ ...(Object.keys(imageSourcePaths).length > 0
1138
+ ? { imageSourcePaths }
1139
+ : {}),
933
1140
  };
934
- const userMsg = createUserMessage(rawContent, attachments);
1141
+ const cleanMsg = createUserMessage(rawContent, attachments);
1142
+ const llmMsg = enrichMessageWithSourcePaths(cleanMsg, attachments);
935
1143
  const persisted = await addMessage(
936
1144
  mapping.conversationId,
937
1145
  "user",
938
- JSON.stringify(userMsg.content),
1146
+ JSON.stringify(cleanMsg.content),
939
1147
  channelMeta,
940
1148
  );
941
- conversation.getMessages().push(userMsg);
1149
+ conversation.getMessages().push(llmMsg);
942
1150
 
943
1151
  const assistantMsg = createAssistantMessage(slashResult.message);
944
1152
  await addMessage(
@@ -960,10 +1168,9 @@ export async function handleSendMessage(
960
1168
 
961
1169
  // Snapshot model info now so the deferred callback cannot observe
962
1170
  // a config change from a concurrent request.
963
- const modelInfoEvent =
964
- isModelSlashCommand(rawContent) || isProviderShortcut(rawContent)
965
- ? await buildModelInfoEvent()
966
- : null;
1171
+ const modelInfoEvent = isModelSlashCommand(rawContent)
1172
+ ? await buildModelInfoEvent()
1173
+ : null;
967
1174
 
968
1175
  const response = Response.json(
969
1176
  {
@@ -1047,6 +1254,7 @@ async function generateLlmSuggestion(
1047
1254
  provider: Provider,
1048
1255
  assistantText: string,
1049
1256
  ): Promise<string | null> {
1257
+ const log = (await import("../../util/logger.js")).getLogger("runtime-http");
1050
1258
  const truncated =
1051
1259
  assistantText.length > 2000 ? assistantText.slice(-2000) : assistantText;
1052
1260
 
@@ -1055,18 +1263,43 @@ async function generateLlmSuggestion(
1055
1263
  [{ role: "user", content: [{ type: "text", text: prompt }] }],
1056
1264
  [], // no tools
1057
1265
  undefined, // no system prompt
1058
- { config: { max_tokens: 30 } },
1266
+ { config: { max_tokens: 40, modelIntent: "latency-optimized" } },
1059
1267
  );
1060
1268
 
1061
1269
  const textBlock = response.content.find((b) => b.type === "text");
1062
1270
  const raw = textBlock && "text" in textBlock ? textBlock.text.trim() : "";
1271
+ const stripped = raw.replace(/^["']+|["']+$/g, "");
1063
1272
 
1064
- if (!raw) return null;
1273
+ if (!stripped) {
1274
+ log.debug("Suggestion rejected: empty LLM response");
1275
+ return null;
1276
+ }
1065
1277
 
1066
1278
  // 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;
1279
+ const firstLine = stripped.split("\n")[0].trim();
1280
+ if (!firstLine) {
1281
+ log.debug(
1282
+ { rawLength: stripped.length },
1283
+ "Suggestion rejected: empty after first-line extraction",
1284
+ );
1285
+ return null;
1286
+ }
1287
+ if (firstLine.length <= 50) return firstLine;
1288
+ // Truncate at last word boundary within 50 chars.
1289
+ // Only strip the trailing partial word if the slice actually cut mid-word;
1290
+ // if the character right after the cut is whitespace, the slice is already clean.
1291
+ const sliced = firstLine.slice(0, 50);
1292
+ const wordTruncated = (
1293
+ /\s/.test(firstLine[50]) ? sliced : sliced.replace(/\s+\S*$/, "")
1294
+ ).trim();
1295
+ if (wordTruncated.length < 15) {
1296
+ log.debug(
1297
+ { rawLength: firstLine.length, truncatedLength: wordTruncated.length },
1298
+ "Suggestion rejected: too short after word-boundary truncation",
1299
+ );
1300
+ return null;
1301
+ }
1302
+ return wordTruncated;
1070
1303
  }
1071
1304
 
1072
1305
  export async function handleGetSuggestion(
@@ -1193,8 +1426,16 @@ export async function handleGetSuggestion(
1193
1426
  }
1194
1427
  } catch (err) {
1195
1428
  suggestionInFlight.delete(msg.id);
1196
- log.warn({ err }, "LLM suggestion failed");
1429
+ log.warn(
1430
+ { err, conversationKey, messageId: msg.id },
1431
+ "LLM suggestion failed",
1432
+ );
1197
1433
  }
1434
+ } else {
1435
+ log.debug(
1436
+ { conversationKey, messageId: msg.id },
1437
+ "Suggestion skipped: no provider available",
1438
+ );
1198
1439
  }
1199
1440
 
1200
1441
  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,
@@ -8,6 +8,7 @@ import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
10
  import { getBaseDataDir } from "../../config/env-registry.js";
11
+ import { parseIdentityFields } from "../../daemon/handlers/identity.js";
11
12
  import { getWorkspacePromptPath, readLockfile } from "../../util/platform.js";
12
13
  import { httpError } from "../http-errors.js";
13
14
  import type { RouteDefinition } from "../http-router.js";
@@ -149,41 +150,7 @@ export function handleGetIdentity(): Response {
149
150
  }
150
151
 
151
152
  const content = readFileSync(identityPath, "utf-8");
152
- const fields: Record<string, string> = {};
153
- for (const line of content.split("\n")) {
154
- const trimmed = line.trim();
155
- const lower = trimmed.toLowerCase();
156
- const extract = (prefix: string): string | null => {
157
- if (!lower.startsWith(prefix)) return null;
158
- return trimmed.split(":**").pop()?.trim() ?? null;
159
- };
160
-
161
- const name = extract("- **name:**");
162
- if (name) {
163
- fields.name = name;
164
- continue;
165
- }
166
- const role = extract("- **role:**");
167
- if (role) {
168
- fields.role = role;
169
- continue;
170
- }
171
- const personality = extract("- **personality:**") ?? extract("- **vibe:**");
172
- if (personality) {
173
- fields.personality = personality;
174
- continue;
175
- }
176
- const emoji = extract("- **emoji:**");
177
- if (emoji) {
178
- fields.emoji = emoji;
179
- continue;
180
- }
181
- const home = extract("- **home:**");
182
- if (home) {
183
- fields.home = home;
184
- continue;
185
- }
186
- }
153
+ const fields = parseIdentityFields(content);
187
154
 
188
155
  const version = getPackageVersion();
189
156
 
@@ -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(),