@vellumai/assistant 0.7.2 → 0.7.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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -30,7 +30,6 @@ import { applyCorrectionIfCalibrated } from "../anisotropy.js";
30
30
  import { embedWithBackend } from "../embedding-backend.js";
31
31
  import { clampUnitInterval } from "../validation.js";
32
32
  import { hybridQueryConceptPages } from "./qdrant.js";
33
- import { hybridQuerySkills } from "./skill-qdrant.js";
34
33
  import { generateBm25QueryEmbedding } from "./sparse-bm25.js";
35
34
 
36
35
  /**
@@ -147,6 +146,7 @@ export async function simBatch(
147
146
  text: string,
148
147
  candidateSlugs: readonly string[],
149
148
  config: AssistantConfig,
149
+ options?: { signal?: AbortSignal },
150
150
  ): Promise<Map<string, number>> {
151
151
  if (candidateSlugs.length === 0) {
152
152
  return new Map();
@@ -158,12 +158,16 @@ export async function simBatch(
158
158
  // Sparse uses BM25: the query side encodes binary occurrences per token,
159
159
  // and the stored doc vectors carry the IDF · TF-saturated weights — Qdrant
160
160
  // dot product then yields the BM25 score directly.
161
- const denseResult = await embedWithBackend(config, [text]);
161
+ throwIfAborted(options?.signal);
162
+ const denseResult = await embedWithBackend(config, [text], {
163
+ signal: options?.signal,
164
+ });
162
165
  const denseVector = await applyCorrectionIfCalibrated(
163
166
  denseResult.vectors[0],
164
167
  denseResult.provider,
165
168
  denseResult.model,
166
169
  );
170
+ throwIfAborted(options?.signal);
167
171
  const sparseVector = generateBm25QueryEmbedding(text);
168
172
 
169
173
  const hits = await hybridQueryConceptPages(
@@ -192,92 +196,14 @@ export async function simBatch(
192
196
  for (const hit of hits) {
193
197
  scores.set(hit.slug, fuseHit(hit, maxSparse, denseWeight, sparseWeight));
194
198
  }
199
+
195
200
  return scores;
196
201
  }
197
202
 
198
- /**
199
- * Compute hybrid (dense + sparse) similarity scores between a query text and
200
- * a fixed set of candidate skill ids. Mirrors `simBatch` but targets the
201
- * dedicated `memory_v2_skills` Qdrant collection via `hybridQuerySkills`.
202
- *
203
- * Differences from `simBatch`:
204
- * - Keys are skill `id` values (not concept-page slugs).
205
- * - Restricts the query to the caller's candidate ids server-side via
206
- * `hybridQuerySkills`'s `restrictToIds` parameter. Without this, when the
207
- * skills collection has more skills than `ids.length`, Qdrant would
208
- * return its global top-K and candidate ids absent from that top-K would
209
- * silently score 0 — corrupting the activation calculation.
210
- *
211
- * Returns a `Map<id, score>` of fused scores in [0, 1]. Ids that did not hit
212
- * either channel are absent from the map.
213
- *
214
- * Edge cases:
215
- * - Empty `ids` → returns an empty map without touching Qdrant or the
216
- * embedding backend.
217
- * - Empty / whitespace-only `text` → returns an empty map without touching
218
- * Qdrant or the embedding backend. Same rationale as {@link simBatch}:
219
- * Gemini rejects empty content with HTTP 400, so the activation pipeline
220
- * would otherwise fail on turn 1 (where the assistant-text channel is
221
- * `""`). Treating the channel's contribution as 0 matches a no-hit
222
- * query.
223
- */
224
- export async function simSkillBatch(
225
- text: string,
226
- ids: readonly string[],
227
- config: AssistantConfig,
228
- ): Promise<Map<string, number>> {
229
- if (ids.length === 0) {
230
- return new Map();
231
- }
232
- if (text.trim().length === 0) {
233
- return new Map();
203
+ function throwIfAborted(signal: AbortSignal | undefined): void {
204
+ if (signal?.aborted) {
205
+ throw new DOMException("Aborted", "AbortError");
234
206
  }
235
-
236
- const denseResult = await embedWithBackend(config, [text]);
237
- const denseVector = await applyCorrectionIfCalibrated(
238
- denseResult.vectors[0],
239
- denseResult.provider,
240
- denseResult.model,
241
- );
242
- const sparseVector = generateBm25QueryEmbedding(text);
243
-
244
- const hits = await hybridQuerySkills(
245
- denseVector,
246
- sparseVector,
247
- ids.length,
248
- ids,
249
- );
250
-
251
- if (hits.length === 0) {
252
- return new Map();
253
- }
254
-
255
- // Defensive post-filter — `hybridQuerySkills` restricts server-side, so
256
- // every hit should already be in `ids`, but keep this guard so a buggy
257
- // payload (e.g. a missing/typoed id index) can't silently inject
258
- // out-of-set ids into the score map.
259
- const idSet = new Set(ids);
260
- const filtered = hits.filter((h) => idSet.has(h.id));
261
- if (filtered.length === 0) {
262
- return new Map();
263
- }
264
-
265
- const maxSparse = computeMaxSparse(filtered);
266
- const { dense_weight: baseDense, sparse_weight: baseSparse } =
267
- config.memory.v2;
268
- const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
269
- filtered,
270
- maxSparse,
271
- baseDense,
272
- baseSparse,
273
- config,
274
- );
275
-
276
- const scores = new Map<string, number>();
277
- for (const hit of filtered) {
278
- scores.set(hit.id, fuseHit(hit, maxSparse, denseWeight, sparseWeight));
279
- }
280
- return scores;
281
207
  }
282
208
 
283
209
  /**
@@ -2,9 +2,10 @@ import { getConfig } from "../../config/loader.js";
2
2
  import type { SkillCapabilityInput } from "../../skills/skill-memory.js";
3
3
 
4
4
  /**
5
- * Render the prose-style capability statement embedded into the
6
- * `memory_v2_skills` Qdrant collection and rendered in
7
- * `### Skills You Can Use`. Capped at 500 chars to match v1's behavior.
5
+ * Render the prose-style capability statement embedded into the unified
6
+ * `memory_v2_concept_pages` Qdrant collection (under the `skills/<id>` slug
7
+ * prefix) and rendered in `### Skills You Can Use`. Capped at 500 chars to
8
+ * match v1's behavior.
8
9
  */
9
10
  export function buildSkillContent(input: SkillCapabilityInput): string {
10
11
  let content = `The "${input.displayName}" skill (${input.id}) is available. ${input.description}.`;
@@ -2,18 +2,22 @@
2
2
  // Memory v2 — Skill catalog → embedded skill entries
3
3
  // ---------------------------------------------------------------------------
4
4
  //
5
- // Mirrors v1's `seedSkillGraphNodes` + `seedUninstalledCatalogSkillMemories`
6
- // (capability-seed.ts) for the v2 pipeline: enumerate the enabled-skill
7
- // catalog AND uninstalled catalog skills, render each skill's prose statement
8
- // via `buildSkillContent`, embed dense + sparse, upsert into the dedicated
9
- // `memory_v2_skills` Qdrant collection, and prune stale points from prior
10
- // catalog state. Including uninstalled catalog skills ensures their activation
11
- // hints are discoverable by intent so the model can auto-install them.
5
+ // Enumerate the enabled-skill catalog AND uninstalled catalog skills, render
6
+ // each skill's prose statement via `buildSkillContent`, embed dense + sparse,
7
+ // and upsert into `memory_v2_concept_pages` under the slug `skills/<id>`.
8
+ // Including uninstalled catalog skills ensures their activation hints are
9
+ // discoverable by intent so the model can auto-install them.
12
10
  //
13
- // Unlike v1, skill entries are kept in a small in-process cache so the render
14
- // path can fetch a `SkillEntry` synchronously by id without round-tripping to
15
- // Qdrant. The cache is replaced atomically at the end of a successful seed
16
- // run; on error the prior cache stays intact (skills are best-effort).
11
+ // Skills share the concept-page collection rather than living in a dedicated
12
+ // one so the per-turn activation pipeline scores them against the same
13
+ // candidate ANN as concept pages, with the same decay and spread machinery.
14
+ // The render path branches on the `skills/` slug prefix to surface them as
15
+ // the `### Skills You Can Use` subsection.
16
+ //
17
+ // Skill entries are kept in a small in-process cache so the render path can
18
+ // fetch a `SkillEntry` synchronously by id without round-tripping to Qdrant.
19
+ // The cache is replaced atomically at the end of a successful seed run; on
20
+ // error the prior cache stays intact (skills are best-effort).
17
21
 
18
22
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
19
23
  import { getConfig } from "../../config/loader.js";
@@ -30,15 +34,31 @@ import {
30
34
  embedWithBackend,
31
35
  generateSparseEmbedding,
32
36
  } from "../embedding-backend.js";
37
+ import {
38
+ pruneSlugsWithPrefixExcept,
39
+ upsertConceptPageEmbedding,
40
+ } from "./qdrant.js";
33
41
  import {
34
42
  augmentMcpSetupDescription,
35
43
  buildSkillContent,
36
44
  } from "./skill-content.js";
37
- import { pruneSkillsExcept, upsertSkillEmbedding } from "./skill-qdrant.js";
38
45
  import type { SkillEntry } from "./types.js";
39
46
 
40
47
  const log = getLogger("memory-v2-skill-store");
41
48
 
49
+ /**
50
+ * Slug prefix under which skill embeddings are indexed in
51
+ * `memory_v2_concept_pages`. Concept-page slugs must match
52
+ * `[a-z0-9][a-z0-9-]*(/...)*`, and `skills` matches that pattern, so the
53
+ * prefix coexists with hand-authored concept pages without escape work.
54
+ */
55
+ export const SKILL_SLUG_PREFIX = "skills/";
56
+
57
+ /** Compose the unified-collection slug for a skill id. */
58
+ export function skillSlugFor(id: string): string {
59
+ return `${SKILL_SLUG_PREFIX}${id}`;
60
+ }
61
+
42
62
  /**
43
63
  * Module-level cache of rendered skill entries keyed by skill id. `null` until
44
64
  * the first successful seed run completes; replaced atomically on each
@@ -47,30 +67,27 @@ const log = getLogger("memory-v2-skill-store");
47
67
  let entries: Map<string, SkillEntry> | null = null;
48
68
 
49
69
  /**
50
- * Seed (or re-seed) the v2 skill embedding collection from the live skill
51
- * catalog. Idempotent: safe to call repeatedly. Best-effort: never throws —
52
- * any failure leaves the prior `entries` cache in place and logs a warning.
70
+ * Seed (or re-seed) skill embeddings into the unified concept-page collection.
71
+ * Idempotent: safe to call repeatedly. Best-effort: never throws — any
72
+ * failure leaves the prior `entries` cache in place and logs a warning.
53
73
  *
54
74
  * Steps:
55
- * 1. Enumerate the local skill catalog and resolve each skill's enabled state
56
- * (`resolveSkillStates`).
57
- * 2. Build a `SkillCapabilityInput` per enabled skill, applying the
58
- * mcp-setup augmentation (mirrors v1) and the prose-style content render
59
- * (`buildSkillContent`, capped at 500 chars).
75
+ * 1. Enumerate the local skill catalog and resolve each skill's enabled
76
+ * state (`resolveSkillStates`).
77
+ * 2. Build a `SkillEntry` per enabled skill, applying the mcp-setup
78
+ * augmentation and the prose-style content render (`buildSkillContent`,
79
+ * capped at 500 chars).
60
80
  * 3. Defense-in-depth feature-flag filter: drop any skill whose declared
61
- * `metadata.vellum.feature-flag` is currently disabled. `resolveSkillStates`
62
- * already enforces this, but we mirror v1's enforcement point so the v2
63
- * collection never holds an embedding for a flag-gated skill if the two
64
- * ever drift.
65
- * 3b. Fetch the full remote catalog and seed any uninstalled skills so their
66
- * activation hints are discoverable by semantic search. Best-effort: if
67
- * the catalog fetch fails, only installed skills are seeded.
81
+ * `metadata.vellum.feature-flag` is currently disabled.
82
+ * 3b. Fetch the full remote catalog and seed any uninstalled skills so
83
+ * their activation hints are discoverable by semantic search. Best-effort:
84
+ * if the catalog fetch fails, only installed skills are seeded.
68
85
  * 4. Embed all `content` strings in a single dense `embedWithBackend` call,
69
86
  * and a per-skill synchronous `generateSparseEmbedding`.
70
- * 5. Upsert one Qdrant point per skill via `upsertSkillEmbedding` (keyed
71
- * deterministically on id so re-runs replace in place).
72
- * 6. Call `pruneSkillsExcept` with the active id list to drop any stale
73
- * points from prior catalog state (e.g. uninstalled skills).
87
+ * 5. Upsert one Qdrant point per skill via `upsertConceptPageEmbedding`
88
+ * keyed deterministically on slug `skills/<id>`.
89
+ * 6. Call `pruneSlugsWithPrefixExcept(SKILL_SLUG_PREFIX, ...)` to drop any
90
+ * stale points from prior catalog state (e.g. uninstalled skills).
74
91
  * 7. Replace the module-level `entries` cache with the freshly built map.
75
92
  */
76
93
  export async function seedV2SkillEntries(): Promise<void> {
@@ -83,8 +100,7 @@ export async function seedV2SkillEntries(): Promise<void> {
83
100
  // Track every locally-installed skill id (regardless of enabled/disabled
84
101
  // state) so the catalog-seeding loop below treats them all as "installed"
85
102
  // and never re-seeds a disabled skill from `getCatalog()` as if it were
86
- // uninstalled. Mirrors v1's `seedUninstalledCatalogSkillMemories`, which
87
- // keys off `loadSkillCatalog()` (the installed set) for the same reason.
103
+ // uninstalled.
88
104
  const installedIds = new Set<string>(catalog.map((s) => s.id));
89
105
 
90
106
  // Build the input list, applying the mcp-setup description augmentation
@@ -100,8 +116,8 @@ export async function seedV2SkillEntries(): Promise<void> {
100
116
  }
101
117
 
102
118
  // Seed uninstalled catalog skills so their activation hints are
103
- // discoverable by intent (mirrors v1's seedUninstalledCatalogSkillMemories).
104
- // Track whether the catalog was available so we can guard pruning below.
119
+ // discoverable by intent. Track whether the catalog was available so we
120
+ // can guard pruning below.
105
121
  let catalogAvailable = false;
106
122
  try {
107
123
  const fullCatalog = await getCatalog();
@@ -135,24 +151,29 @@ export async function seedV2SkillEntries(): Promise<void> {
135
151
 
136
152
  const now = Date.now();
137
153
  const nextEntries = new Map<string, SkillEntry>();
138
- for (let i = 0; i < seeds.length; i++) {
139
- const seed = seeds[i];
140
- await upsertSkillEmbedding({
141
- ...seed,
142
- dense: denseVectors[i],
143
- sparse: generateSparseEmbedding(seed.content),
144
- updatedAt: now,
145
- });
154
+ await Promise.all(
155
+ seeds.map((seed, i) =>
156
+ upsertConceptPageEmbedding({
157
+ slug: skillSlugFor(seed.id),
158
+ dense: denseVectors[i],
159
+ sparse: generateSparseEmbedding(seed.content),
160
+ updatedAt: now,
161
+ }),
162
+ ),
163
+ );
164
+ for (const seed of seeds) {
146
165
  nextEntries.set(seed.id, seed);
147
166
  }
148
167
 
149
- // Prune stale points. When the catalog is unavailable (empty array from
150
- // network failure or cold cache), we cannot enumerate which uninstalled
151
- // catalog skills should exist, so skip pruning entirely to avoid
152
- // aggressively removing previously-seeded catalog skill embeddings.
153
- // Mirrors v1's safeguard in capability-seed.ts (lines 124–143).
168
+ // Prune stale skill slugs. When the catalog is unavailable (empty array
169
+ // from network failure or cold cache), we cannot enumerate which
170
+ // uninstalled catalog skills should exist, so skip pruning entirely to
171
+ // avoid aggressively removing previously-seeded catalog skill embeddings.
154
172
  if (catalogAvailable) {
155
- await pruneSkillsExcept(seeds.map((s) => s.id));
173
+ await pruneSlugsWithPrefixExcept(
174
+ SKILL_SLUG_PREFIX,
175
+ seeds.map((s) => s.id),
176
+ );
156
177
  } else {
157
178
  log.info(
158
179
  "Catalog unavailable — skipping skill pruning to preserve prior catalog embeddings",
@@ -169,20 +190,22 @@ export async function seedV2SkillEntries(): Promise<void> {
169
190
  /**
170
191
  * Synchronous lookup of a previously-seeded `SkillEntry` by skill id. Returns
171
192
  * `null` when the cache has not yet been populated, when the id is unknown,
172
- * or when a prior seed run dropped the id (e.g. the skill was disabled). Used
173
- * by the render path to attach skill-related content to outgoing prompts.
193
+ * or when a prior seed run dropped the id (e.g. the skill was disabled).
194
+ *
195
+ * Accepts either a bare skill id (`example-skill`) or its unified-collection
196
+ * slug (`skills/example-skill`) so render-side callers can pass through what
197
+ * they have without a manual prefix strip.
174
198
  */
175
- export function getSkillCapability(id: string): SkillEntry | null {
199
+ export function getSkillCapability(idOrSlug: string): SkillEntry | null {
200
+ const id = idOrSlug.startsWith(SKILL_SLUG_PREFIX)
201
+ ? idOrSlug.slice(SKILL_SLUG_PREFIX.length)
202
+ : idOrSlug;
176
203
  return entries?.get(id) ?? null;
177
204
  }
178
205
 
179
- /**
180
- * Every skill id in the cache — both installed-and-enabled skills and
181
- * uninstalled-catalog skills. Empty before the first `seedV2SkillEntries`
182
- * run completes.
183
- */
184
- export function getAllSkillIds(): string[] {
185
- return entries ? [...entries.keys()] : [];
206
+ /** True iff the slug refers to a skill entry in the unified collection. */
207
+ export function isSkillSlug(slug: string): boolean {
208
+ return slug.startsWith(SKILL_SLUG_PREFIX);
186
209
  }
187
210
 
188
211
  /** @internal Test-only: clear the module-level cache. */
@@ -17,6 +17,7 @@
17
17
  // content through when `mode === "full"` (first turn / post-compaction),
18
18
  // matching the existing PKB auto-inject pattern.
19
19
 
20
+ import type { ChannelId } from "../../channels/types.js";
20
21
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
21
22
  import { loadConfig } from "../../config/loader.js";
22
23
  import { readPromptFile } from "../../prompts/system-prompt.js";
@@ -61,3 +62,24 @@ export function readMemoryV2StaticContent(): string | null {
61
62
  }
62
63
  return sections.length > 0 ? sections.join("\n\n") : null;
63
64
  }
65
+
66
+ /**
67
+ * Static memory holds the user's aggregate personal pages
68
+ * (essentials/threads/recent/buffer). Block injection when a non-guardian
69
+ * actor reaches the assistant over a remote channel — otherwise the model
70
+ * can be prompt-injected into reciting private memory. Internal flows
71
+ * (`sourceChannel: "vellum"`) and turns with no trust context pass through
72
+ * unchanged; this gate exists only to keep remote untrusted actors out.
73
+ */
74
+ export function shouldLoadMemoryV2Static(args: {
75
+ shouldInjectNowAndPkb: boolean;
76
+ sourceChannel: ChannelId | undefined;
77
+ isTrustedActor: boolean;
78
+ }): boolean {
79
+ if (!args.shouldInjectNowAndPkb) return false;
80
+ const isRemoteUntrustedActor =
81
+ args.sourceChannel !== undefined &&
82
+ args.sourceChannel !== "vellum" &&
83
+ !args.isTrustedActor;
84
+ return !isRemoteUntrustedActor;
85
+ }
@@ -85,20 +85,20 @@ export const ActivationStateSchema = z.object({
85
85
  export type ActivationState = z.infer<typeof ActivationStateSchema>;
86
86
 
87
87
  // ---------------------------------------------------------------------------
88
- // Skill autoinjection (synthetic in-memory entries, not on-disk pages)
88
+ // Skill entries (synthetic concept-collection rows, not on-disk pages)
89
89
  // ---------------------------------------------------------------------------
90
90
 
91
91
  /**
92
- * Per-skill capability snapshot held in-process and embedded into the
93
- * `memory_v2_skills` Qdrant collection. `content` is the rendered
94
- * `buildSkillContent` string — already capped at 500 chars upstream and
95
- * already containing the skill's display name — and is what we embed and
96
- * what we render verbatim in `### Skills You Can Use`.
92
+ * Per-skill capability snapshot held in-process and embedded into the unified
93
+ * `memory_v2_concept_pages` Qdrant collection under the slug `skills/<id>`.
94
+ * `content` is the rendered `buildSkillContent` string — already capped at
95
+ * 500 chars upstream and already containing the skill's display name — and
96
+ * is what we embed and what we render verbatim in `### Skills You Can Use`.
97
97
  *
98
- * Plain interface (no Zod) because skill data does not cross a
99
- * serialization boundary: it is built in-process by `seedV2SkillEntries`
100
- * and read in-process by `renderInjectionBlock`. The Qdrant payload is
101
- * not parsed back through this type.
98
+ * Plain interface (no Zod) because skill data does not cross a serialization
99
+ * boundary: it is built in-process by `seedV2SkillEntries` and read in-process
100
+ * by `renderInjectionBlock`. The Qdrant payload is not parsed back through
101
+ * this type.
102
102
  */
103
103
  export interface SkillEntry {
104
104
  id: string;
@@ -505,6 +505,19 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
505
505
  body: str(payload.body, "A watcher event requires your attention"),
506
506
  }),
507
507
 
508
+ "heartbeat.alert": (payload) => {
509
+ const body = str(
510
+ payload.summary,
511
+ str(payload.body, "Your assistant found something worth your attention."),
512
+ );
513
+ return {
514
+ title: str(payload.title, "Heartbeat Alert"),
515
+ body,
516
+ conversationTitle: str(payload.conversationTitle, "Heartbeat"),
517
+ conversationSeedMessage: body,
518
+ };
519
+ },
520
+
508
521
  "tool_confirmation.required_action": (payload) => ({
509
522
  title: "Tool Confirmation",
510
523
  body: str(payload.toolName, "A tool") + " requires your confirmation",
@@ -101,6 +101,10 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
101
101
  description:
102
102
  "OAuth credential health issue detected (expired, revoked, missing scopes)",
103
103
  },
104
+ {
105
+ id: "heartbeat.alert",
106
+ description: "Heartbeat found something worth surfacing to the guardian",
107
+ },
104
108
  ] as const;
105
109
 
106
110
  export type NotificationSourceEventName =
@@ -54,7 +54,9 @@ Most existing logos come from [Simple Icons](https://simpleicons.org) (CC0-licen
54
54
 
55
55
  If the service is not on Simple Icons, source or create an SVG and convert it the same way. The result must be a true vector PDF (not a rasterized image wrapped in PDF) so it scales cleanly.
56
56
 
57
- The `logoUrl` field in `seed-providers.ts` still serves as the remote fallback (typically a Simple Icons CDN URL like `https://cdn.simpleicons.org/acme`). The client renders the local PDF first, then falls back to `logoUrl`, then to an initials avatar.
57
+ The `logoUrl` field in `seed-providers.ts` serves as the remote fallback (most providers use a Simple Icons CDN URL like `https://cdn.simpleicons.org/acme`). The client renders the local PDF first, then falls back to `logoUrl`, then to an initials avatar.
58
+
59
+ For brands Simple Icons doesn't host (e.g. Salesforce, which Simple Icons removed for trademark reasons), use the same `glincker/thesvg` source via jsDelivr — `https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/<key>/default.svg`. The recognised `logoUrl` prefixes are enforced by `oauth-provider-seed-logos.test.ts`; if you need a third source, extend that allowlist alongside the manifest in `clients/shared/Resources/integration-logos-manifest.json`.
58
60
 
59
61
  ### 5. Secret patterns (if applicable) — `../security/secret-patterns.ts`
60
62
 
@@ -0,0 +1,137 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ _clearAllOAuthConnectStates,
5
+ clearExpiredOAuthConnectStates,
6
+ getOAuthConnectState,
7
+ setOAuthConnectComplete,
8
+ setOAuthConnectError,
9
+ setOAuthConnectPending,
10
+ } from "../oauth-connect-state.js";
11
+
12
+ describe("oauth-connect-state", () => {
13
+ beforeEach(() => {
14
+ _clearAllOAuthConnectStates();
15
+ });
16
+
17
+ test("setOAuthConnectPending → getOAuthConnectState returns pending", () => {
18
+ setOAuthConnectPending("state-1", "google");
19
+ const result = getOAuthConnectState("state-1");
20
+ expect(result).toMatchObject({ status: "pending", service: "google" });
21
+ });
22
+
23
+ test("setOAuthConnectComplete without accountInfo → returns complete", () => {
24
+ setOAuthConnectComplete("state-1", "google");
25
+ const result = getOAuthConnectState("state-1");
26
+ expect(result).toMatchObject({ status: "complete", service: "google" });
27
+ });
28
+
29
+ test("setOAuthConnectComplete with accountInfo → returns complete with accountInfo", () => {
30
+ setOAuthConnectComplete("state-1", "google", "user@example.com");
31
+ const result = getOAuthConnectState("state-1");
32
+ expect(result).toMatchObject({
33
+ status: "complete",
34
+ service: "google",
35
+ accountInfo: "user@example.com",
36
+ });
37
+ });
38
+
39
+ test("setOAuthConnectComplete with grantedScopes → returns complete with grantedScopes", () => {
40
+ setOAuthConnectComplete("state-1", "google", "user@example.com", ["scope:read", "scope:write"]);
41
+ const result = getOAuthConnectState("state-1");
42
+ expect(result).toMatchObject({
43
+ status: "complete",
44
+ service: "google",
45
+ accountInfo: "user@example.com",
46
+ grantedScopes: ["scope:read", "scope:write"],
47
+ });
48
+ });
49
+
50
+ test("setOAuthConnectError → returns error with message", () => {
51
+ setOAuthConnectError("state-1", "google", "token exchange failed");
52
+ const result = getOAuthConnectState("state-1");
53
+ expect(result).toMatchObject({
54
+ status: "error",
55
+ service: "google",
56
+ error: "token exchange failed",
57
+ });
58
+ });
59
+
60
+ test("re-setting same state token overwrites previous", () => {
61
+ setOAuthConnectPending("state-1", "google");
62
+ setOAuthConnectComplete("state-1", "google", "user@example.com");
63
+ const result = getOAuthConnectState("state-1");
64
+ expect(result?.status).toBe("complete");
65
+ });
66
+
67
+ test("getOAuthConnectState returns null for unknown state", () => {
68
+ expect(getOAuthConnectState("nonexistent")).toBeNull();
69
+ });
70
+
71
+ test("_clearAllOAuthConnectStates removes all entries", () => {
72
+ setOAuthConnectPending("state-1", "google");
73
+ setOAuthConnectPending("state-2", "github");
74
+ _clearAllOAuthConnectStates();
75
+ expect(getOAuthConnectState("state-1")).toBeNull();
76
+ expect(getOAuthConnectState("state-2")).toBeNull();
77
+ });
78
+
79
+ test("clearExpiredOAuthConnectStates removes expired pending entries", () => {
80
+ setOAuthConnectPending("state-1", "google");
81
+ // Advance Date.now by 6 minutes past PENDING_TTL_MS (5 min)
82
+ const originalNow = Date.now;
83
+ Date.now = () => originalNow() + 6 * 60 * 1000;
84
+ clearExpiredOAuthConnectStates();
85
+ Date.now = originalNow;
86
+ expect(getOAuthConnectState("state-1")).toBeNull();
87
+ });
88
+
89
+ test("clearExpiredOAuthConnectStates removes expired complete entries (past 60s grace)", () => {
90
+ setOAuthConnectComplete("state-1", "google");
91
+ const originalNow = Date.now;
92
+ Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
93
+ clearExpiredOAuthConnectStates();
94
+ Date.now = originalNow;
95
+ expect(getOAuthConnectState("state-1")).toBeNull();
96
+ });
97
+
98
+ test("clearExpiredOAuthConnectStates removes expired error entries (past 60s grace)", () => {
99
+ setOAuthConnectError("state-1", "google", "token exchange failed");
100
+ const originalNow = Date.now;
101
+ Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
102
+ clearExpiredOAuthConnectStates();
103
+ Date.now = originalNow;
104
+ expect(getOAuthConnectState("state-1")).toBeNull();
105
+ });
106
+
107
+ test("clearExpiredOAuthConnectStates does not remove non-expired pending entries", () => {
108
+ setOAuthConnectPending("state-1", "google");
109
+ clearExpiredOAuthConnectStates(); // called without advancing time
110
+ expect(getOAuthConnectState("state-1")).not.toBeNull();
111
+ });
112
+
113
+ test("sweep-on-insert: setOAuthConnectPending purges expired entries before inserting new one", () => {
114
+ // 1. Add an entry that will expire
115
+ setOAuthConnectPending("expired-state", "google");
116
+
117
+ // 2. Advance Date.now past the PENDING_TTL_MS (5 min)
118
+ const originalNow = Date.now;
119
+ Date.now = () => originalNow() + 6 * 60 * 1000;
120
+
121
+ // 3. Insert a new entry — this should trigger clearExpiredOAuthConnectStates() internally
122
+ setOAuthConnectPending("new-state", "github");
123
+
124
+ // 4. Restore Date.now before assertions (getOAuthConnectState also calls clearExpiredOAuthConnectStates)
125
+ Date.now = originalNow;
126
+
127
+ // The expired entry must have been swept out during the insert
128
+ // Use the map directly via getOAuthConnectState — expired-state is gone
129
+ // We call _clearAllOAuthConnectStates in beforeEach so we know the map started empty.
130
+ // After the insert the map should only contain "new-state".
131
+ const expiredResult = getOAuthConnectState("expired-state");
132
+ expect(expiredResult).toBeNull();
133
+
134
+ const newResult = getOAuthConnectState("new-state");
135
+ expect(newResult).toMatchObject({ status: "pending", service: "github" });
136
+ });
137
+ });
@@ -81,6 +81,7 @@ export interface OAuthConnectOptions {
81
81
  success: boolean;
82
82
  service: string;
83
83
  accountInfo?: string;
84
+ grantedScopes?: string[];
84
85
  error?: string;
85
86
  }) => void;
86
87
  }
@@ -256,6 +257,7 @@ export async function orchestrateOAuthConnect(
256
257
  success: true,
257
258
  service: options.service,
258
259
  accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
260
+ grantedScopes: result.grantedScopes,
259
261
  });
260
262
  } catch (err) {
261
263
  log.error(