@vellumai/assistant 0.4.56 → 0.5.0

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 (457) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +3 -0
  3. package/README.md +11 -11
  4. package/docs/architecture/integrations.md +2 -2
  5. package/docs/architecture/memory.md +3 -4
  6. package/docs/credential-execution-service.md +13 -20
  7. package/node_modules/@vellumai/ces-contracts/src/error.ts +5 -4
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +7 -7
  10. package/src/__tests__/anthropic-provider.test.ts +172 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +15 -1
  12. package/src/__tests__/approval-cascade.test.ts +2 -2
  13. package/src/__tests__/approval-routes-http.test.ts +3 -4
  14. package/src/__tests__/asset-materialize-tool.test.ts +5 -5
  15. package/src/__tests__/asset-search-tool.test.ts +1 -1
  16. package/src/__tests__/assistant-attachments.test.ts +5 -5
  17. package/src/__tests__/assistant-events-sse-hardening.test.ts +1 -1
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +50 -38
  19. package/src/__tests__/attachments-store.test.ts +2 -2
  20. package/src/__tests__/avatar-e2e.test.ts +5 -3
  21. package/src/__tests__/browser-skill-endstate.test.ts +0 -1
  22. package/src/__tests__/call-routes-http.test.ts +2 -2
  23. package/src/__tests__/callback-handoff-copy.test.ts +1 -1
  24. package/src/__tests__/cancel-resolves-conversation-key.test.ts +158 -0
  25. package/src/__tests__/channel-readiness-routes.test.ts +0 -1
  26. package/src/__tests__/channel-readiness-service.test.ts +0 -1
  27. package/src/__tests__/checker.test.ts +31 -32
  28. package/src/__tests__/chrome-cdp.test.ts +47 -18
  29. package/src/__tests__/claude-code-skill-regression.test.ts +2 -2
  30. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  31. package/src/__tests__/config-schema.test.ts +9 -18
  32. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -4
  34. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -2
  35. package/src/__tests__/conversation-agent-loop.test.ts +11 -4
  36. package/src/__tests__/conversation-attachments.test.ts +1 -1
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -2
  38. package/src/__tests__/conversation-error.test.ts +33 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +0 -1
  40. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  41. package/src/__tests__/conversation-pairing.test.ts +1 -1
  42. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -4
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +4 -4
  44. package/src/__tests__/conversation-queue.test.ts +23 -14
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +3 -3
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +204 -185
  47. package/src/__tests__/conversation-seed-composer.test.ts +1 -1
  48. package/src/__tests__/conversation-slash-queue.test.ts +4 -4
  49. package/src/__tests__/conversation-slash-unknown.test.ts +4 -4
  50. package/src/__tests__/conversation-starter-routes.test.ts +291 -0
  51. package/src/__tests__/conversation-wipe.test.ts +438 -0
  52. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -3
  53. package/src/__tests__/conversation-workspace-injection.test.ts +4 -5
  54. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -5
  55. package/src/__tests__/credential-security-e2e.test.ts +20 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault-unit.test.ts +227 -0
  58. package/src/__tests__/credentials-cli.test.ts +3 -0
  59. package/src/__tests__/date-context.test.ts +59 -377
  60. package/src/__tests__/drop-capability-card-state-migration.test.ts +169 -0
  61. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +11 -45
  62. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -3
  63. package/src/__tests__/encrypted-store.test.ts +249 -15
  64. package/src/__tests__/ephemeral-permissions.test.ts +4 -5
  65. package/src/__tests__/event-bus.test.ts +3 -3
  66. package/src/__tests__/file-read-tool.test.ts +40 -0
  67. package/src/__tests__/gateway-only-enforcement.test.ts +2 -2
  68. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  69. package/src/__tests__/gemini-image-service.test.ts +4 -4
  70. package/src/__tests__/gemini-provider.test.ts +6 -9
  71. package/src/__tests__/guardian-binding-drift-heal.test.ts +128 -0
  72. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  73. package/src/__tests__/host-file-read-tool.test.ts +87 -0
  74. package/src/__tests__/host-shell-tool.test.ts +6 -6
  75. package/src/__tests__/http-user-message-parity.test.ts +2 -2
  76. package/src/__tests__/identity-intro-cache.test.ts +209 -0
  77. package/src/__tests__/intent-routing.test.ts +51 -99
  78. package/src/__tests__/invite-routes-http.test.ts +5 -0
  79. package/src/__tests__/list-messages-attachments.test.ts +1 -1
  80. package/src/__tests__/managed-proxy-context.test.ts +2 -5
  81. package/src/__tests__/managed-skill-lifecycle.test.ts +8 -8
  82. package/src/__tests__/media-generate-image.test.ts +32 -15
  83. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  84. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +1 -1
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +24 -18
  86. package/src/__tests__/memory-recall-quality.test.ts +4 -3
  87. package/src/__tests__/memory-regressions.test.ts +86 -90
  88. package/src/__tests__/migration-cross-version-compatibility.test.ts +32 -32
  89. package/src/__tests__/migration-export-http.test.ts +26 -27
  90. package/src/__tests__/migration-import-commit-http.test.ts +165 -37
  91. package/src/__tests__/migration-import-preflight-http.test.ts +81 -20
  92. package/src/__tests__/migration-validate-http.test.ts +16 -16
  93. package/src/__tests__/model-intents.test.ts +2 -2
  94. package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +1 -1
  95. package/src/__tests__/non-member-access-request.test.ts +3 -3
  96. package/src/__tests__/notification-broadcaster.test.ts +1 -1
  97. package/src/__tests__/notification-decision-fallback.test.ts +2 -2
  98. package/src/__tests__/notification-decision-identity.test.ts +8 -9
  99. package/src/__tests__/notification-decision-strategy.test.ts +1 -1
  100. package/src/__tests__/notification-deep-link.test.ts +1 -1
  101. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  102. package/src/__tests__/notification-schedule-dedup.test.ts +7 -7
  103. package/src/__tests__/oauth-store.test.ts +1 -3
  104. package/src/__tests__/oauth2-gateway-transport.test.ts +6 -1
  105. package/src/__tests__/onboarding-template-contract.test.ts +23 -59
  106. package/src/__tests__/provider-error-scenarios.test.ts +154 -0
  107. package/src/__tests__/provider-fail-open-selection.test.ts +2 -2
  108. package/src/__tests__/provider-managed-proxy-integration.test.ts +8 -9
  109. package/src/__tests__/provider-registry-ollama.test.ts +5 -2
  110. package/src/__tests__/qdrant-manager.test.ts +7 -7
  111. package/src/__tests__/ratelimit.test.ts +0 -74
  112. package/src/__tests__/recording-handler.test.ts +0 -1
  113. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
  115. package/src/__tests__/runtime-events-sse-parity.test.ts +1 -1
  116. package/src/__tests__/runtime-events-sse.test.ts +1 -1
  117. package/src/__tests__/scheduler-recurrence.test.ts +46 -2
  118. package/src/__tests__/schema-transforms.test.ts +114 -54
  119. package/src/__tests__/secret-onetime-send.test.ts +20 -0
  120. package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -2
  121. package/src/__tests__/secret-scanner-executor.test.ts +1 -2
  122. package/src/__tests__/send-endpoint-busy.test.ts +63 -4
  123. package/src/__tests__/send-notification-tool.test.ts +2 -2
  124. package/src/__tests__/shell-credential-ref.test.ts +0 -1
  125. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -2
  126. package/src/__tests__/skill-memory.test.ts +549 -0
  127. package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -2
  128. package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -0
  129. package/src/__tests__/slack-channel-config.test.ts +109 -94
  130. package/src/__tests__/swarm-conversation-integration.test.ts +2 -2
  131. package/src/__tests__/swarm-recursion.test.ts +2 -2
  132. package/src/__tests__/swarm-tool.test.ts +2 -2
  133. package/src/__tests__/system-prompt.test.ts +19 -66
  134. package/src/__tests__/telegram-config.test.ts +121 -0
  135. package/src/__tests__/terminal-tools.test.ts +1 -1
  136. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -2
  137. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  138. package/src/__tests__/tool-executor-shell-integration.test.ts +1 -1
  139. package/src/__tests__/tool-executor.test.ts +1 -1
  140. package/src/__tests__/trace-emitter.test.ts +8 -1
  141. package/src/__tests__/trust-store.test.ts +7 -8
  142. package/src/__tests__/twilio-routes.test.ts +1 -18
  143. package/src/__tests__/user-reference.test.ts +82 -2
  144. package/src/__tests__/vbundle-pax-and-symlink.test.ts +196 -0
  145. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  146. package/src/approvals/guardian-request-resolvers.ts +3 -3
  147. package/src/avatar/ascii-renderer.ts +2 -2
  148. package/src/avatar/png-renderer.ts +2 -2
  149. package/src/avatar/resvg-lazy.ts +21 -0
  150. package/src/calls/guardian-dispatch.ts +1 -1
  151. package/src/calls/relay-access-wait.ts +2 -2
  152. package/src/calls/twilio-rest.ts +0 -248
  153. package/src/cli/AGENTS.md +5 -8
  154. package/src/cli/__tests__/notifications.test.ts +5 -5
  155. package/src/cli/commands/avatar.ts +64 -2
  156. package/src/cli/commands/conversations.ts +131 -1
  157. package/src/cli/commands/credentials.ts +2 -0
  158. package/src/cli/commands/notifications.ts +3 -3
  159. package/src/cli.ts +10 -0
  160. package/src/config/bundled-skills/acp/SKILL.md +5 -5
  161. package/src/config/bundled-skills/acp/TOOLS.json +6 -6
  162. package/src/config/bundled-skills/app-builder/SKILL.md +42 -42
  163. package/src/config/bundled-skills/app-builder/TOOLS.json +10 -10
  164. package/src/config/bundled-skills/browser/SKILL.md +15 -15
  165. package/src/config/bundled-skills/browser/TOOLS.json +14 -14
  166. package/src/config/bundled-skills/chatgpt-import/SKILL.md +2 -2
  167. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +1 -1
  168. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  169. package/src/config/bundled-skills/claude-code/SKILL.md +5 -5
  170. package/src/config/bundled-skills/computer-use/SKILL.md +2 -2
  171. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -15
  172. package/src/config/bundled-skills/contacts/SKILL.md +3 -3
  173. package/src/config/bundled-skills/contacts/TOOLS.json +4 -4
  174. package/src/config/bundled-skills/document/SKILL.md +4 -4
  175. package/src/config/bundled-skills/document/TOOLS.json +2 -2
  176. package/src/config/bundled-skills/followups/TOOLS.json +3 -3
  177. package/src/config/bundled-skills/gmail/SKILL.md +32 -32
  178. package/src/config/bundled-skills/gmail/TOOLS.json +16 -16
  179. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +1 -1
  180. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  181. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -1
  182. package/src/config/bundled-skills/google-calendar/TOOLS.json +5 -5
  183. package/src/config/bundled-skills/google-calendar/types.ts +1 -1
  184. package/src/config/bundled-skills/heartbeat/SKILL.md +43 -0
  185. package/src/config/bundled-skills/image-studio/SKILL.md +3 -3
  186. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -3
  187. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +16 -12
  188. package/src/config/bundled-skills/media-processing/SKILL.md +40 -40
  189. package/src/config/bundled-skills/media-processing/TOOLS.json +8 -8
  190. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +2 -2
  191. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +1 -1
  192. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +5 -5
  193. package/src/config/bundled-skills/media-processing/services/gemini-video.ts +2 -2
  194. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -2
  195. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -2
  196. package/src/config/bundled-skills/media-processing/services/reduce.ts +3 -3
  197. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
  198. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +1 -1
  199. package/src/config/bundled-skills/messaging/SKILL.md +29 -25
  200. package/src/config/bundled-skills/messaging/TOOLS.json +11 -11
  201. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
  202. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  203. package/src/config/bundled-skills/notifications/SKILL.md +3 -3
  204. package/src/config/bundled-skills/notifications/TOOLS.json +2 -2
  205. package/src/config/bundled-skills/notifications/tools/send-notification.ts +3 -3
  206. package/src/config/bundled-skills/orchestration/SKILL.md +1 -1
  207. package/src/config/bundled-skills/orchestration/TOOLS.json +1 -1
  208. package/src/config/bundled-skills/phone-calls/SKILL.md +18 -14
  209. package/src/config/bundled-skills/phone-calls/TOOLS.json +3 -3
  210. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +2 -2
  211. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +2 -2
  212. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +1 -1
  213. package/src/config/bundled-skills/playbooks/TOOLS.json +4 -4
  214. package/src/config/bundled-skills/schedule/SKILL.md +26 -26
  215. package/src/config/bundled-skills/schedule/TOOLS.json +5 -5
  216. package/src/config/bundled-skills/screen-watch/SKILL.md +3 -3
  217. package/src/config/bundled-skills/screen-watch/TOOLS.json +1 -1
  218. package/src/config/bundled-skills/sequences/SKILL.md +2 -2
  219. package/src/config/bundled-skills/sequences/TOOLS.json +10 -10
  220. package/src/config/bundled-skills/sequences/tools/sequence-analytics.ts +2 -2
  221. package/src/config/bundled-skills/sequences/tools/sequence-enroll.ts +2 -2
  222. package/src/config/bundled-skills/sequences/tools/sequence-enrollment-list.ts +1 -1
  223. package/src/config/bundled-skills/sequences/tools/sequence-get.ts +1 -1
  224. package/src/config/bundled-skills/sequences/tools/sequence-import.ts +3 -3
  225. package/src/config/bundled-skills/sequences/tools/sequence-list.ts +1 -1
  226. package/src/config/bundled-skills/sequences/tools/sequence-update.ts +1 -1
  227. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  228. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -1
  229. package/src/config/bundled-skills/skill-management/TOOLS.json +5 -5
  230. package/src/config/bundled-skills/skills-catalog/SKILL.md +84 -0
  231. package/src/config/bundled-skills/slack/SKILL.md +2 -2
  232. package/src/config/bundled-skills/slack/TOOLS.json +8 -8
  233. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +3 -3
  234. package/src/config/bundled-skills/subagent/TOOLS.json +5 -5
  235. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  236. package/src/config/bundled-skills/tasks/TOOLS.json +9 -9
  237. package/src/config/bundled-skills/transcribe/SKILL.md +5 -5
  238. package/src/config/bundled-skills/transcribe/TOOLS.json +1 -1
  239. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +10 -10
  240. package/src/config/bundled-skills/watcher/SKILL.md +4 -4
  241. package/src/config/bundled-skills/watcher/TOOLS.json +5 -5
  242. package/src/config/feature-flag-registry.json +33 -17
  243. package/src/config/schemas/sandbox.ts +1 -1
  244. package/src/config/schemas/services.ts +13 -3
  245. package/src/config/schemas/timeouts.ts +0 -10
  246. package/src/contacts/contact-store.ts +63 -0
  247. package/src/contacts/contacts-write.ts +1 -1
  248. package/src/daemon/assistant-attachments.ts +2 -2
  249. package/src/daemon/conversation-agent-loop-handlers.ts +2 -2
  250. package/src/daemon/conversation-agent-loop.ts +7 -30
  251. package/src/daemon/conversation-error.ts +24 -0
  252. package/src/daemon/conversation-memory.ts +8 -7
  253. package/src/daemon/conversation-runtime-assembly.ts +141 -275
  254. package/src/daemon/conversation-slash.ts +7 -26
  255. package/src/daemon/conversation-surfaces.ts +14 -0
  256. package/src/daemon/conversation-tool-setup.ts +9 -8
  257. package/src/daemon/conversation.ts +2 -0
  258. package/src/daemon/daemon-control.ts +1 -1
  259. package/src/daemon/date-context.ts +10 -83
  260. package/src/daemon/handlers/config-channels.ts +12 -2
  261. package/src/daemon/handlers/config-slack-channel.ts +7 -1
  262. package/src/daemon/handlers/config-telegram.ts +6 -1
  263. package/src/daemon/handlers/conversations.ts +2 -2
  264. package/src/daemon/handlers/skills.ts +4 -0
  265. package/src/daemon/lifecycle.ts +28 -4
  266. package/src/daemon/providers-setup.ts +1 -1
  267. package/src/daemon/server.ts +1 -5
  268. package/src/daemon/shutdown-handlers.ts +9 -3
  269. package/src/daemon/tool-side-effects.ts +40 -0
  270. package/src/daemon/trace-emitter.ts +26 -2
  271. package/src/events/domain-events.ts +1 -1
  272. package/src/events/tool-permission-telemetry-listener.ts +46 -0
  273. package/src/inbound/platform-callback-registration.ts +0 -18
  274. package/src/media/app-icon-generator.ts +15 -8
  275. package/src/media/avatar-router.ts +15 -8
  276. package/src/media/gemini-image-service.ts +125 -21
  277. package/src/memory/attachments-store.ts +3 -3
  278. package/src/memory/channel-verification-sessions.ts +6 -6
  279. package/src/memory/conversation-crud.ts +196 -1
  280. package/src/memory/{thread-starters-cadence.ts → conversation-starters-cadence.ts} +9 -42
  281. package/src/memory/conversation-title-service.ts +2 -3
  282. package/src/memory/db-init.ts +25 -1
  283. package/src/memory/invite-store.ts +4 -4
  284. package/src/memory/items-extractor.ts +4 -4
  285. package/src/memory/job-handlers/{thread-starters.ts → conversation-starters.ts} +123 -38
  286. package/src/memory/jobs-store.ts +3 -2
  287. package/src/memory/jobs-worker.ts +7 -5
  288. package/src/memory/lifecycle-events-store.ts +63 -0
  289. package/src/memory/migrations/172-rename-created-by-session-id.ts +27 -0
  290. package/src/memory/migrations/173-rename-source-session-id.ts +16 -0
  291. package/src/memory/migrations/174-rename-thread-starters-table.ts +52 -0
  292. package/src/memory/migrations/175-create-lifecycle-events.ts +15 -0
  293. package/src/memory/migrations/176-drop-capability-card-state.ts +36 -0
  294. package/src/memory/migrations/177-create-trace-events-table.ts +40 -0
  295. package/src/memory/migrations/index.ts +6 -0
  296. package/src/memory/migrations/registry.ts +13 -0
  297. package/src/memory/retriever.test.ts +223 -96
  298. package/src/memory/retriever.ts +115 -138
  299. package/src/memory/schema/calls.ts +1 -1
  300. package/src/memory/schema/contacts.ts +1 -1
  301. package/src/memory/schema/infrastructure.ts +29 -0
  302. package/src/memory/schema/memory-core.ts +7 -17
  303. package/src/memory/schema/notifications.ts +1 -1
  304. package/src/memory/search/formatting.ts +23 -6
  305. package/src/memory/search/lexical.ts +2 -0
  306. package/src/memory/search/semantic.ts +2 -0
  307. package/src/memory/search/staleness.ts +5 -1
  308. package/src/memory/search/types.ts +4 -0
  309. package/src/memory/task-memory-cleanup.ts +96 -6
  310. package/src/memory/trace-event-store.ts +148 -0
  311. package/src/notifications/README.md +1 -1
  312. package/src/notifications/decision-engine.ts +45 -4
  313. package/src/notifications/emit-signal.ts +5 -4
  314. package/src/notifications/events-store.ts +4 -4
  315. package/src/notifications/signal.ts +1 -1
  316. package/src/oauth/manual-token-connection.ts +49 -25
  317. package/src/permissions/checker.ts +6 -5
  318. package/src/permissions/defaults.ts +4 -4
  319. package/src/prompts/__tests__/build-cli-reference-section.test.ts +9 -90
  320. package/src/prompts/cache-boundary.ts +8 -0
  321. package/src/prompts/system-prompt.ts +105 -634
  322. package/src/prompts/templates/BOOTSTRAP.md +172 -33
  323. package/src/prompts/templates/IDENTITY.md +8 -24
  324. package/src/prompts/templates/SOUL.md +20 -41
  325. package/src/prompts/templates/USER.md +3 -19
  326. package/src/prompts/user-reference.ts +14 -16
  327. package/src/providers/anthropic/client.ts +51 -19
  328. package/src/providers/gemini/client.ts +6 -9
  329. package/src/providers/managed-proxy/constants.ts +1 -7
  330. package/src/providers/managed-proxy/context.ts +0 -1
  331. package/src/providers/model-intents.ts +5 -5
  332. package/src/providers/openai/client.ts +10 -1
  333. package/src/providers/openrouter/client.ts +1 -0
  334. package/src/providers/ratelimit.ts +0 -35
  335. package/src/providers/registry.ts +3 -5
  336. package/src/providers/retry.ts +18 -1
  337. package/src/runtime/access-request-helper.ts +16 -2
  338. package/src/runtime/auth/route-policy.ts +7 -0
  339. package/src/runtime/channel-verification-service.ts +1 -1
  340. package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
  341. package/src/runtime/guardian-vellum-migration.ts +61 -1
  342. package/src/runtime/http-server.ts +8 -4
  343. package/src/runtime/migrations/vbundle-builder.ts +212 -32
  344. package/src/runtime/migrations/vbundle-import-analyzer.ts +74 -8
  345. package/src/runtime/migrations/vbundle-importer.ts +66 -1
  346. package/src/runtime/migrations/vbundle-validator.ts +17 -3
  347. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +4 -4
  348. package/src/runtime/routes/attachment-routes.ts +2 -2
  349. package/src/runtime/routes/btw-routes.ts +93 -0
  350. package/src/runtime/routes/channel-verification-routes.ts +19 -2
  351. package/src/runtime/routes/conversation-management-routes.ts +55 -1
  352. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  353. package/src/runtime/routes/conversation-routes.ts +49 -5
  354. package/src/runtime/routes/conversation-starter-routes.ts +207 -0
  355. package/src/runtime/routes/guardian-bootstrap-routes.ts +13 -9
  356. package/src/runtime/routes/identity-intro-cache.ts +105 -0
  357. package/src/runtime/routes/identity-routes.ts +51 -0
  358. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -1
  359. package/src/runtime/routes/inbound-stages/verification-intercept.ts +1 -1
  360. package/src/runtime/routes/migration-routes.ts +25 -13
  361. package/src/runtime/routes/secret-routes.ts +18 -0
  362. package/src/runtime/routes/settings-routes.ts +9 -9
  363. package/src/runtime/routes/telemetry-routes.ts +53 -0
  364. package/src/runtime/routes/trace-event-routes.ts +62 -0
  365. package/src/runtime/tool-grant-request-helper.ts +1 -1
  366. package/src/runtime/verification-outbound-actions.ts +47 -31
  367. package/src/security/encrypted-store.ts +262 -78
  368. package/src/skills/catalog-install.ts +10 -0
  369. package/src/skills/managed-store.ts +2 -0
  370. package/src/skills/skill-memory.ts +222 -0
  371. package/src/subagent/manager.ts +1 -4
  372. package/src/telemetry/types.ts +10 -1
  373. package/src/telemetry/usage-telemetry-reporter.test.ts +7 -2
  374. package/src/telemetry/usage-telemetry-reporter.ts +53 -4
  375. package/src/tools/AGENTS.md +11 -11
  376. package/src/tools/acp/spawn.ts +1 -1
  377. package/src/tools/apps/executors.ts +8 -8
  378. package/src/tools/apps/registry.ts +1 -1
  379. package/src/tools/assets/materialize.ts +6 -6
  380. package/src/tools/assets/search.ts +10 -10
  381. package/src/tools/browser/__tests__/auth-cache.test.ts +2 -2
  382. package/src/tools/browser/__tests__/auth-detector.test.ts +4 -4
  383. package/src/tools/browser/auth-detector.ts +6 -6
  384. package/src/tools/browser/browser-execution.ts +13 -13
  385. package/src/tools/browser/browser-manager.ts +3 -3
  386. package/src/tools/browser/chrome-cdp.ts +5 -5
  387. package/src/tools/browser/jit-auth.ts +2 -2
  388. package/src/tools/browser/network-recorder.test.ts +2 -2
  389. package/src/tools/browser/network-recorder.ts +3 -3
  390. package/src/tools/browser/runtime-check.ts +3 -3
  391. package/src/tools/claude-code/claude-code.ts +2 -2
  392. package/src/tools/computer-use/definitions.ts +18 -18
  393. package/src/tools/credential-execution/make-authenticated-request.ts +4 -4
  394. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -3
  395. package/src/tools/credential-execution/run-authenticated-command.ts +4 -4
  396. package/src/tools/credentials/broker-types.ts +5 -5
  397. package/src/tools/credentials/broker.ts +15 -15
  398. package/src/tools/credentials/metadata-store.ts +2 -2
  399. package/src/tools/credentials/resolve.ts +1 -1
  400. package/src/tools/credentials/selection.ts +1 -1
  401. package/src/tools/credentials/tool-policy.ts +1 -1
  402. package/src/tools/credentials/vault.ts +115 -25
  403. package/src/tools/execution-target.ts +2 -2
  404. package/src/tools/executor.ts +7 -7
  405. package/src/tools/filesystem/edit.ts +2 -2
  406. package/src/tools/filesystem/read.ts +15 -4
  407. package/src/tools/filesystem/write.ts +1 -1
  408. package/src/tools/host-filesystem/edit.ts +2 -1
  409. package/src/tools/host-filesystem/read.ts +18 -1
  410. package/src/tools/host-filesystem/write.ts +1 -1
  411. package/src/tools/host-terminal/host-shell.ts +9 -8
  412. package/src/tools/mcp/mcp-tool-factory.ts +7 -6
  413. package/src/tools/memory/definitions.ts +6 -5
  414. package/src/tools/memory/handlers.test.ts +1 -1
  415. package/src/tools/network/__tests__/web-search.test.ts +3 -3
  416. package/src/tools/network/domain-normalize.ts +2 -2
  417. package/src/tools/network/script-proxy/session-manager.ts +10 -10
  418. package/src/tools/network/web-fetch.ts +1 -1
  419. package/src/tools/network/web-search.ts +3 -3
  420. package/src/tools/permission-checker.ts +8 -8
  421. package/src/tools/registry.ts +7 -7
  422. package/src/tools/schedule/list.ts +2 -2
  423. package/src/tools/schema-transforms.ts +31 -21
  424. package/src/tools/secret-detection-handler.ts +1 -1
  425. package/src/tools/sensitive-output-placeholders.ts +1 -1
  426. package/src/tools/shared/filesystem/edit-engine.ts +1 -1
  427. package/src/tools/shared/filesystem/file-ops-service.ts +3 -3
  428. package/src/tools/shared/filesystem/image-read.ts +25 -5
  429. package/src/tools/shared/filesystem/path-policy.ts +2 -2
  430. package/src/tools/shared/shell-output.ts +1 -1
  431. package/src/tools/side-effects.ts +1 -1
  432. package/src/tools/skills/execute.ts +1 -1
  433. package/src/tools/skills/load.ts +3 -3
  434. package/src/tools/skills/sandbox-runner.ts +3 -3
  435. package/src/tools/subagent/read.ts +1 -1
  436. package/src/tools/subagent/spawn.ts +2 -2
  437. package/src/tools/swarm/delegate.ts +3 -3
  438. package/src/tools/system/request-permission.ts +5 -4
  439. package/src/tools/terminal/backends/native.ts +4 -4
  440. package/src/tools/terminal/parser.ts +6 -6
  441. package/src/tools/terminal/sandbox-diagnostics.ts +1 -1
  442. package/src/tools/terminal/shell.ts +16 -16
  443. package/src/tools/tool-approval-handler.ts +21 -12
  444. package/src/tools/tool-manifest.ts +4 -4
  445. package/src/tools/types.ts +3 -3
  446. package/src/tools/ui-surface/definitions.ts +9 -37
  447. package/src/tools/watcher/list.ts +1 -1
  448. package/src/util/logger.ts +7 -2
  449. package/src/util/pricing.ts +4 -0
  450. package/src/util/retry.ts +29 -1
  451. package/src/workspace/migrations/007-web-search-provider-rename.ts +37 -0
  452. package/src/workspace/migrations/registry.ts +2 -0
  453. package/src/__tests__/cli-help-reference-sync.test.ts +0 -26
  454. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -190
  455. package/src/cli/reference.ts +0 -38
  456. package/src/memory/job-handlers/capability-cards.ts +0 -420
  457. package/src/runtime/routes/thread-starter-routes.ts +0 -294
@@ -12,6 +12,8 @@
12
12
  * No messages are persisted. `conversation.processing` is never set or checked.
13
13
  */
14
14
 
15
+ import { existsSync, readFileSync } from "node:fs";
16
+
15
17
  import { buildToolDefinitions } from "../../daemon/conversation-tool-setup.js";
16
18
  import { getConversationByKey } from "../../memory/conversation-key-store.js";
17
19
  import { buildSystemPrompt } from "../../prompts/system-prompt.js";
@@ -21,13 +23,45 @@ import {
21
23
  } from "../../providers/provider-send-message.js";
22
24
  import { checkIngressForSecrets } from "../../security/secret-ingress.js";
23
25
  import { getLogger } from "../../util/logger.js";
26
+ import { getWorkspacePromptPath } from "../../util/platform.js";
24
27
  import type { AuthContext } from "../auth/types.js";
25
28
  import { httpError } from "../http-errors.js";
26
29
  import type { RouteDefinition } from "../http-router.js";
27
30
  import type { SendMessageDeps } from "../http-types.js";
31
+ import { getCachedIntro, setCachedIntro } from "./identity-intro-cache.js";
28
32
 
29
33
  const log = getLogger("btw-routes");
30
34
 
35
+ /** Conversation key used by the client for identity intro generation. */
36
+ const IDENTITY_INTRO_KEY = "identity-intro";
37
+
38
+ /**
39
+ * Parse the `## Identity Intro` section from SOUL.md.
40
+ * Returns the first non-empty line under that heading, or null.
41
+ */
42
+ function readSoulIdentityIntro(): string | null {
43
+ try {
44
+ const soulPath = getWorkspacePromptPath("SOUL.md");
45
+ if (!existsSync(soulPath)) return null;
46
+ const content = readFileSync(soulPath, "utf-8");
47
+
48
+ let inSection = false;
49
+ for (const line of content.split("\n")) {
50
+ const trimmed = line.trim();
51
+ if (/^#+\s/.test(trimmed)) {
52
+ inSection = trimmed.toLowerCase().includes("identity intro");
53
+ continue;
54
+ }
55
+ if (inSection && trimmed.length > 0) {
56
+ return trimmed;
57
+ }
58
+ }
59
+ } catch {
60
+ // Fall through — no SOUL.md intro available
61
+ }
62
+ return null;
63
+ }
64
+
31
65
  // ---------------------------------------------------------------------------
32
66
  // Handler
33
67
  // ---------------------------------------------------------------------------
@@ -77,6 +111,43 @@ async function handleBtw(
77
111
  );
78
112
  }
79
113
 
114
+ // ----- Identity intro fast-path -----
115
+ // When the client requests the identity intro, check SOUL.md first (persisted
116
+ // during onboarding), then the LLM-generated cache. Only fall through to a
117
+ // live LLM call when neither source has a value.
118
+ if (conversationKey === IDENTITY_INTRO_KEY) {
119
+ const soulIntro = readSoulIdentityIntro();
120
+ const fastText = soulIntro ?? getCachedIntro()?.text;
121
+ if (fastText) {
122
+ log.debug(
123
+ soulIntro
124
+ ? "Returning SOUL.md identity intro"
125
+ : "Returning cached identity intro",
126
+ );
127
+ const encoder = new TextEncoder();
128
+ const stream = new ReadableStream({
129
+ start(controller) {
130
+ controller.enqueue(
131
+ encoder.encode(
132
+ `event: btw_text_delta\ndata: ${JSON.stringify({ text: fastText })}\n\n`,
133
+ ),
134
+ );
135
+ controller.enqueue(
136
+ encoder.encode(`event: btw_complete\ndata: {}\n\n`),
137
+ );
138
+ controller.close();
139
+ },
140
+ });
141
+ return new Response(stream, {
142
+ headers: {
143
+ "Content-Type": "text/event-stream",
144
+ "Cache-Control": "no-cache",
145
+ Connection: "keep-alive",
146
+ },
147
+ });
148
+ }
149
+ }
150
+
80
151
  // Look up an existing conversation — never create one. BTW is ephemeral
81
152
  // (the file header promises "No messages are persisted"), so we must not
82
153
  // call getOrCreateConversation which would insert a DB row. When no
@@ -116,6 +187,9 @@ async function handleBtw(
116
187
  ? conversation.systemPrompt
117
188
  : buildSystemPrompt({ excludeBootstrap: true });
118
189
 
190
+ const isIntroRequest = conversationKey === IDENTITY_INTRO_KEY;
191
+ let textDeltaCount = 0;
192
+ let collectedText = "";
119
193
  await conversation.provider.sendMessage(
120
194
  messages,
121
195
  tools,
@@ -128,6 +202,8 @@ async function handleBtw(
128
202
  },
129
203
  onEvent: (event) => {
130
204
  if (event.type === "text_delta") {
205
+ textDeltaCount++;
206
+ if (isIntroRequest) collectedText += event.text;
131
207
  controller.enqueue(
132
208
  encoder.encode(
133
209
  `event: btw_text_delta\ndata: ${JSON.stringify({ text: event.text })}\n\n`,
@@ -139,6 +215,23 @@ async function handleBtw(
139
215
  },
140
216
  );
141
217
 
218
+ if (textDeltaCount === 0) {
219
+ log.warn(
220
+ { conversationKey, messageCount: messages.length },
221
+ "btw side-chain completed with no text deltas",
222
+ );
223
+ }
224
+
225
+ // Cache the generated identity intro for subsequent requests.
226
+ if (isIntroRequest && collectedText.trim()) {
227
+ try {
228
+ setCachedIntro(collectedText.trim());
229
+ log.debug("Cached identity intro text");
230
+ } catch {
231
+ // Non-fatal — next request will regenerate.
232
+ }
233
+ }
234
+
142
235
  controller.enqueue(
143
236
  encoder.encode(`event: btw_complete\ndata: {}\n\n`),
144
237
  );
@@ -21,6 +21,7 @@ import { httpError } from "../http-errors.js";
21
21
  import type { RouteDefinition } from "../http-router.js";
22
22
  import {
23
23
  cancelOutbound,
24
+ deliverVerificationSlack,
24
25
  normalizeTelegramDestination,
25
26
  resendOutbound,
26
27
  startOutbound,
@@ -118,12 +119,20 @@ export async function handleCreateVerificationSession(
118
119
  verificationRateLimiter.recordFailure(rateLimitKey);
119
120
  }
120
121
 
122
+ // Dispatch Slack DM delivery from the daemon process (not sandboxed).
123
+ if (result._pendingSlackDm) {
124
+ const { userId, text, assistantId: aid } = result._pendingSlackDm;
125
+ deliverVerificationSlack(userId, text, aid);
126
+ }
127
+
121
128
  const status = result.success
122
129
  ? 200
123
130
  : result.error === "rate_limited"
124
131
  ? 429
125
132
  : 400;
126
- return Response.json(result, { status });
133
+ // Strip internal field from the response
134
+ const { _pendingSlackDm: _, ...publicResult } = result;
135
+ return Response.json(publicResult, { status });
127
136
  }
128
137
 
129
138
  // Inbound challenge path
@@ -167,12 +176,20 @@ export async function handleResendVerificationSession(
167
176
  channel: body.channel,
168
177
  originConversationId: body.originConversationId,
169
178
  });
179
+
180
+ // Dispatch Slack DM delivery from the daemon process (not sandboxed).
181
+ if (result._pendingSlackDm) {
182
+ const { userId, text, assistantId: aid } = result._pendingSlackDm;
183
+ deliverVerificationSlack(userId, text, aid);
184
+ }
185
+
170
186
  const status = result.success
171
187
  ? 200
172
188
  : result.error === "rate_limited"
173
189
  ? 429
174
190
  : 400;
175
- return Response.json(result, { status });
191
+ const { _pendingSlackDm: _, ...publicResult } = result;
192
+ return Response.json(publicResult, { status });
176
193
  }
177
194
 
178
195
  /**
@@ -4,6 +4,7 @@
4
4
  * POST /v1/conversations/switch — switch to an existing conversation
5
5
  * PATCH /v1/conversations/:id/name — rename a conversation
6
6
  * DELETE /v1/conversations — clear all conversations
7
+ * POST /v1/conversations/:id/wipe — wipe conversation and revert memory
7
8
  * DELETE /v1/conversations/:id — delete a single conversation
8
9
  * POST /v1/conversations/:id/cancel — cancel generation
9
10
  * POST /v1/conversations/:id/undo — undo last message
@@ -14,6 +15,7 @@
14
15
  import {
15
16
  batchSetDisplayOrders,
16
17
  deleteConversation,
18
+ wipeConversation,
17
19
  } from "../../memory/conversation-crud.js";
18
20
  import {
19
21
  resolveConversationId,
@@ -121,6 +123,57 @@ export function conversationManagementRouteDefinitions(
121
123
  return new Response(null, { status: 204 });
122
124
  },
123
125
  },
126
+ {
127
+ endpoint: "conversations/:id/wipe",
128
+ method: "POST",
129
+ policyKey: "conversations/wipe",
130
+ handler: async ({ params }) => {
131
+ const resolvedId = resolveConversationId(params.id);
132
+ if (!resolvedId) {
133
+ return httpError(
134
+ "NOT_FOUND",
135
+ `Conversation ${params.id} not found`,
136
+ 404,
137
+ );
138
+ }
139
+ deps.destroyConversation(resolvedId);
140
+ const result = wipeConversation(resolvedId);
141
+ // Enqueue Qdrant vector cleanup jobs
142
+ for (const segId of result.segmentIds) {
143
+ enqueueMemoryJob("delete_qdrant_vectors", {
144
+ targetType: "segment",
145
+ targetId: segId,
146
+ });
147
+ }
148
+ for (const itemId of result.orphanedItemIds) {
149
+ enqueueMemoryJob("delete_qdrant_vectors", {
150
+ targetType: "item",
151
+ targetId: itemId,
152
+ });
153
+ }
154
+ for (const summaryId of result.deletedSummaryIds) {
155
+ enqueueMemoryJob("delete_qdrant_vectors", {
156
+ targetType: "summary",
157
+ targetId: summaryId,
158
+ });
159
+ }
160
+ log.info(
161
+ {
162
+ conversationId: resolvedId,
163
+ unsuperseded: result.unsupersededItemIds.length,
164
+ summariesDeleted: result.deletedSummaryIds.length,
165
+ jobsCancelled: result.cancelledJobCount,
166
+ },
167
+ "Wiped conversation and reverted memory changes",
168
+ );
169
+ return Response.json({
170
+ wiped: true,
171
+ unsupersededItems: result.unsupersededItemIds.length,
172
+ deletedSummaries: result.deletedSummaryIds.length,
173
+ cancelledJobs: result.cancelledJobCount,
174
+ });
175
+ },
176
+ },
124
177
  {
125
178
  endpoint: "conversations/:id",
126
179
  method: "DELETE",
@@ -163,7 +216,8 @@ export function conversationManagementRouteDefinitions(
163
216
  method: "POST",
164
217
  policyKey: "conversations/cancel",
165
218
  handler: ({ params }) => {
166
- deps.cancelGeneration(params.id);
219
+ const resolvedId = resolveConversationId(params.id) ?? params.id;
220
+ deps.cancelGeneration(resolvedId);
167
221
  return new Response(null, { status: 202 });
168
222
  },
169
223
  },
@@ -200,7 +200,7 @@ export function conversationQueryRouteDefinitions(
200
200
  requestId: params.id,
201
201
  });
202
202
  }
203
- if (result.reason === "session_not_found") {
203
+ if (result.reason === "conversation_not_found") {
204
204
  return httpError("NOT_FOUND", "Conversation not found", 404);
205
205
  }
206
206
  return httpError("NOT_FOUND", "Queued message not found", 404);
@@ -35,6 +35,7 @@ import {
35
35
  createCanonicalGuardianRequest,
36
36
  generateCanonicalRequestCode,
37
37
  listPendingRequestsByConversationScope,
38
+ resolveCanonicalGuardianRequest,
38
39
  } from "../../memory/canonical-guardian-store.js";
39
40
  import {
40
41
  addMessage,
@@ -58,6 +59,7 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
58
59
  import type { AuthContext } from "../auth/types.js";
59
60
  import { bridgeConfirmationRequestToGuardian } from "../confirmation-request-guardian-bridge.js";
60
61
  import { routeGuardianReply } from "../guardian-reply-router.js";
62
+ import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
61
63
  import { httpError } from "../http-errors.js";
62
64
  import type { RouteDefinition } from "../http-router.js";
63
65
  import type {
@@ -492,16 +494,16 @@ function makeHubPublisher(
492
494
  }
493
495
 
494
496
  // ServerMessage is a large union; conversationId exists on most but not all variants.
495
- const msgSessionId =
497
+ const msgConversationId =
496
498
  "conversationId" in msg &&
497
499
  typeof (msg as { conversationId?: unknown }).conversationId === "string"
498
500
  ? (msg as { conversationId: string }).conversationId
499
501
  : undefined;
500
- const resolvedSessionId = msgSessionId ?? conversationId;
502
+ const resolvedConversationId = msgConversationId ?? conversationId;
501
503
  const event = buildAssistantEvent(
502
504
  DAEMON_INTERNAL_ASSISTANT_ID,
503
505
  msg,
504
- resolvedSessionId,
506
+ resolvedConversationId,
505
507
  );
506
508
  hubChain = (async () => {
507
509
  await hubChain;
@@ -646,12 +648,44 @@ export async function handleSendMessage(
646
648
  // the same trust resolution pipeline that channel ingress uses.
647
649
  if (authContext.actorPrincipalId) {
648
650
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
649
- const trustCtx = resolveTrustContext({
651
+ let trustCtx = resolveTrustContext({
650
652
  assistantId,
651
653
  sourceChannel: "vellum",
652
654
  conversationExternalId: "local",
653
655
  actorExternalId: authContext.actorPrincipalId,
654
656
  });
657
+ if (trustCtx.trustClass === "unknown") {
658
+ // Attempt to heal guardian binding drift: after a DB reset the
659
+ // guardian binding gets a new vellum-principal-* UUID while the
660
+ // client still holds a valid JWT with the old one. The signing
661
+ // key survives the reset, so the JWT is authentic — just stale.
662
+ const healed = healGuardianBindingDrift(authContext.actorPrincipalId);
663
+ if (healed) {
664
+ trustCtx = resolveTrustContext({
665
+ assistantId,
666
+ sourceChannel: "vellum",
667
+ conversationExternalId: "local",
668
+ actorExternalId: authContext.actorPrincipalId,
669
+ });
670
+ log.info(
671
+ {
672
+ actorPrincipalId: authContext.actorPrincipalId,
673
+ trustClass: trustCtx.trustClass,
674
+ },
675
+ "Trust re-resolved after guardian binding drift heal",
676
+ );
677
+ } else {
678
+ log.warn(
679
+ {
680
+ actorPrincipalId: authContext.actorPrincipalId,
681
+ sourceChannel,
682
+ trustClass: trustCtx.trustClass,
683
+ principalType: authContext.principalType,
684
+ },
685
+ "JWT-verified actor resolved to unknown trust class — possible guardian binding drift (e.g. DB reset without re-bootstrap)",
686
+ );
687
+ }
688
+ }
655
689
  conversation.setTrustContext(withSourceChannel(sourceChannel, trustCtx));
656
690
  } else {
657
691
  // Service principals (svc_gateway) or tokens without an actor ID
@@ -811,6 +845,11 @@ export async function handleSendMessage(
811
845
  state: "denied" as const,
812
846
  source: "auto_deny" as const,
813
847
  });
848
+ // Sync canonical guardian request status so stale "pending" DB
849
+ // records don't get matched by later guardian reply routing.
850
+ resolveCanonicalGuardianRequest(interaction.requestId, "pending", {
851
+ status: "denied",
852
+ });
814
853
  }
815
854
  }
816
855
  conversation.denyAllPendingConfirmations();
@@ -842,6 +881,11 @@ export async function handleSendMessage(
842
881
  state: "denied" as const,
843
882
  source: "auto_deny" as const,
844
883
  });
884
+ // Sync canonical guardian request status so stale "pending" DB
885
+ // records don't get matched by later guardian reply routing.
886
+ resolveCanonicalGuardianRequest(interaction.requestId, "pending", {
887
+ status: "denied",
888
+ });
845
889
  }
846
890
  }
847
891
  conversation.denyAllPendingConfirmations();
@@ -931,7 +975,7 @@ export async function handleSendMessage(
931
975
  );
932
976
 
933
977
  // Defer event publishing to next tick so the HTTP response reaches the
934
- // client first. This ensures the client's serverToLocalSessionMap is
978
+ // client first. This ensures the client's serverToLocalConversationMap is
935
979
  // populated before SSE events arrive, preventing dropped events in new
936
980
  // desktop conversations.
937
981
  //
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Route handlers for conversation starter endpoints.
3
+ *
4
+ * GET /v1/conversation-starters — list conversation starters (chips)
5
+ */
6
+
7
+ import { and, desc, eq, inArray, like } from "drizzle-orm";
8
+
9
+ import { getDb } from "../../memory/db.js";
10
+ import { enqueueMemoryJob } from "../../memory/jobs-store.js";
11
+ import { rawGet } from "../../memory/raw-query.js";
12
+ import { conversationStarters, memoryJobs } from "../../memory/schema.js";
13
+ import type { RouteDefinition } from "../http-router.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Strongest-first ordering — maximize category diversity so the top four
17
+ // chips form a coherent, non-repetitive row.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ interface StarterItem {
21
+ id: string;
22
+ label: string;
23
+ prompt: string;
24
+ category: string | null;
25
+ batch: number;
26
+ }
27
+
28
+ /**
29
+ * Re-order starters so adjacent items have distinct categories wherever
30
+ * possible. Within each category, preserve the original (batch-desc) order.
31
+ * This is deterministic — same input always produces the same output.
32
+ */
33
+ export function orderStrongestFirst<T extends StarterItem>(items: T[]): T[] {
34
+ if (items.length <= 1) return items;
35
+
36
+ // Group by category, preserving original order within each group
37
+ const byCategory = new Map<string, T[]>();
38
+ for (const item of items) {
39
+ const cat = item.category ?? "other";
40
+ let group = byCategory.get(cat);
41
+ if (!group) {
42
+ group = [];
43
+ byCategory.set(cat, group);
44
+ }
45
+ group.push(item);
46
+ }
47
+
48
+ // Prefer categories with the most remaining items so the row stays varied
49
+ // early without burying the dominant themes entirely.
50
+ const sortedGroups = [...byCategory.entries()]
51
+ .sort((a, b) => b[1].length - a[1].length)
52
+ .map(([, group]) => ({ items: group, idx: 0 }));
53
+
54
+ const result: T[] = [];
55
+ const seenCategories = new Set<string>();
56
+ let lastCategory: string | null = null;
57
+
58
+ while (result.length < items.length) {
59
+ let picked = false;
60
+ const availableGroups = sortedGroups.filter(
61
+ (group) => group.idx < group.items.length,
62
+ );
63
+
64
+ const unseenGroups = availableGroups.filter((group) => {
65
+ const category = group.items[group.idx]?.category ?? "other";
66
+ return category !== lastCategory && !seenCategories.has(category);
67
+ });
68
+
69
+ const nextGroups =
70
+ unseenGroups.length > 0
71
+ ? unseenGroups
72
+ : availableGroups.filter((group) => {
73
+ const category = group.items[group.idx]?.category ?? "other";
74
+ return category !== lastCategory;
75
+ });
76
+
77
+ // First pass: prefer unseen categories, then avoid adjacent duplicates.
78
+ for (const group of nextGroups) {
79
+ if (group.idx >= group.items.length) continue;
80
+ const candidate = group.items[group.idx];
81
+ const cat = candidate.category ?? "other";
82
+ result.push(candidate);
83
+ group.idx++;
84
+ seenCategories.add(cat);
85
+ lastCategory = cat;
86
+ picked = true;
87
+ break;
88
+ }
89
+
90
+ // Fallback: if all remaining items share the same category, just pick next
91
+ if (!picked) {
92
+ for (const group of availableGroups) {
93
+ if (group.idx < group.items.length) {
94
+ const candidate = group.items[group.idx];
95
+ const cat = candidate.category ?? "other";
96
+ result.push(candidate);
97
+ group.idx++;
98
+ seenCategories.add(cat);
99
+ lastCategory = cat;
100
+ picked = true;
101
+ break;
102
+ }
103
+ }
104
+ }
105
+
106
+ if (!picked) break;
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // GET /v1/conversation-starters
114
+ // ---------------------------------------------------------------------------
115
+
116
+ function handleListConversationStarters(url: URL): Response {
117
+ const limitParam = Math.min(
118
+ Math.max(1, Number(url.searchParams.get("limit") ?? 4)),
119
+ 20,
120
+ );
121
+ const offsetParam = Math.max(0, Number(url.searchParams.get("offset") ?? 0));
122
+ const scopeId = url.searchParams.get("scope_id") ?? "default";
123
+
124
+ const db = getDb();
125
+
126
+ // Fetch chips (ranked by model, newest batch first)
127
+ const rawItems = db
128
+ .select({
129
+ id: conversationStarters.id,
130
+ label: conversationStarters.label,
131
+ prompt: conversationStarters.prompt,
132
+ category: conversationStarters.category,
133
+ batch: conversationStarters.generationBatch,
134
+ })
135
+ .from(conversationStarters)
136
+ .where(
137
+ and(
138
+ eq(conversationStarters.scopeId, scopeId),
139
+ eq(conversationStarters.cardType, "chip"),
140
+ ),
141
+ )
142
+ .orderBy(
143
+ desc(conversationStarters.generationBatch),
144
+ desc(conversationStarters.createdAt),
145
+ )
146
+ .limit(limitParam)
147
+ .offset(offsetParam)
148
+ .all();
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;
155
+
156
+ // If starters exist, return them immediately.
157
+ if (total > 0) {
158
+ return Response.json({
159
+ starters: orderStrongestFirst(rawItems),
160
+ total,
161
+ status: "ready",
162
+ });
163
+ }
164
+
165
+ // No starters — check whether we have memory items to generate from.
166
+ const memoryCount = rawGet<{ c: number }>(
167
+ `SELECT COUNT(*) AS c FROM memory_items WHERE status = 'active' AND scope_id = ?`,
168
+ scopeId,
169
+ );
170
+
171
+ if (!memoryCount || memoryCount.c === 0) {
172
+ return Response.json({ starters: [], total: 0, status: "empty" });
173
+ }
174
+
175
+ // Memory items exist but no starters yet — ensure a generation job is queued.
176
+ const existing = db
177
+ .select({ id: memoryJobs.id })
178
+ .from(memoryJobs)
179
+ .where(
180
+ and(
181
+ eq(memoryJobs.type, "generate_conversation_starters"),
182
+ inArray(memoryJobs.status, ["pending", "running"]),
183
+ like(memoryJobs.payload, `%"scopeId":"${scopeId}"%`),
184
+ ),
185
+ )
186
+ .get();
187
+
188
+ if (!existing) {
189
+ enqueueMemoryJob("generate_conversation_starters", { scopeId });
190
+ }
191
+
192
+ return Response.json({ starters: [], total: 0, status: "generating" });
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Route definitions
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export function conversationStarterRouteDefinitions(): RouteDefinition[] {
200
+ return [
201
+ {
202
+ endpoint: "conversation-starters",
203
+ method: "GET",
204
+ handler: (ctx) => handleListConversationStarters(ctx.url),
205
+ },
206
+ ];
207
+ }
@@ -19,6 +19,7 @@ import { getLogger } from "../../util/logger.js";
19
19
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
20
20
  import { mintCredentialPair } from "../auth/credential-service.js";
21
21
  import { httpError } from "../http-errors.js";
22
+ import { isPrivateAddress } from "../middleware/auth.js";
22
23
 
23
24
  /** Bun server shape needed for requestIP -- avoids importing the full Bun type. */
24
25
  type ServerWithRequestIP = {
@@ -71,30 +72,33 @@ function ensureGuardianPrincipal(assistantId: string): {
71
72
  return { guardianPrincipalId, isNew: true };
72
73
  }
73
74
 
74
- /** Loopback addresses — used to gate the bootstrap endpoint to local-only. */
75
- const LOOPBACK_ADDRESSES = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
76
-
77
75
  /**
78
76
  * Handle POST /v1/guardian/init
79
77
  *
80
78
  * Body: { platform: 'macos', deviceId: string }
81
79
  * Returns: { guardianPrincipalId, accessToken, isNew }
82
80
  *
83
- * This endpoint is loopback-only (macOS local use only). iOS devices
84
- * obtain actor tokens exclusively through the QR pairing flow.
81
+ * This endpoint is restricted to private-network peers (loopback, RFC 1918,
82
+ * Docker bridge, etc.). iOS devices obtain actor tokens exclusively through
83
+ * the QR pairing flow.
85
84
  */
86
85
  export async function handleGuardianBootstrap(
87
86
  req: Request,
88
87
  server: ServerWithRequestIP,
89
88
  ): Promise<Response> {
90
- // Reject proxied requests bootstrap is local-only
91
- if (req.headers.get("x-forwarded-for") && !isHttpAuthDisabled()) {
89
+ // Reject requests forwarded from public networks. The gateway sets
90
+ // x-forwarded-for to the real client IP; if that IP is on a private
91
+ // network (loopback, Docker bridge, RFC 1918) the request is still
92
+ // considered local. Only reject when the forwarded IP is public.
93
+ const forwarded = req.headers.get("x-forwarded-for");
94
+ const forwardedIp = forwarded ? forwarded.split(",")[0].trim() : null;
95
+ if (forwardedIp && !isPrivateAddress(forwardedIp) && !isHttpAuthDisabled()) {
92
96
  return httpError("FORBIDDEN", "Bootstrap endpoint is local-only", 403);
93
97
  }
94
98
 
95
- // Reject non-loopback peers
99
+ // Reject non-private-network peers (allows loopback, Docker bridge, etc.)
96
100
  const peerIp = server.requestIP(req)?.address;
97
- if ((!peerIp || !LOOPBACK_ADDRESSES.has(peerIp)) && !isHttpAuthDisabled()) {
101
+ if ((!peerIp || !isPrivateAddress(peerIp)) && !isHttpAuthDisabled()) {
98
102
  return httpError("FORBIDDEN", "Bootstrap endpoint is local-only", 403);
99
103
  }
100
104