@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -6,11 +6,17 @@ describe("HostCuProxy", () => {
6
6
  let proxy: InstanceType<typeof HostCuProxy>;
7
7
  let sentMessages: unknown[];
8
8
  let sendToClient: (msg: unknown) => void;
9
+ let resolvedRequestIds: string[];
9
10
 
10
11
  function setup(maxSteps?: number) {
11
12
  sentMessages = [];
13
+ resolvedRequestIds = [];
12
14
  sendToClient = (msg: unknown) => sentMessages.push(msg);
13
- proxy = new HostCuProxy(sendToClient as never, maxSteps);
15
+ proxy = new HostCuProxy(
16
+ sendToClient as never,
17
+ (requestId: string) => resolvedRequestIds.push(requestId),
18
+ maxSteps,
19
+ );
14
20
  }
15
21
 
16
22
  afterEach(() => {
@@ -364,6 +370,60 @@ describe("HostCuProxy", () => {
364
370
  );
365
371
  });
366
372
 
373
+ test("does not emit spurious warning on first observation", async () => {
374
+ setup();
375
+
376
+ // First ever request — no previous AX tree exists
377
+ proxy.recordAction("computer_use_click", { element_id: 1 });
378
+ const p1 = proxy.request(
379
+ "computer_use_click",
380
+ { element_id: 1 },
381
+ "session-1",
382
+ 1,
383
+ );
384
+ const sent1 = sentMessages[0] as Record<string, unknown>;
385
+ proxy.resolve(sent1.requestId as string, {
386
+ axTree: "Button [1]",
387
+ // No axDiff on first observation — this is normal, not unchanged
388
+ });
389
+ const result1 = await p1;
390
+ expect(result1.content).not.toContain("NO VISIBLE EFFECT");
391
+ });
392
+
393
+ test("skips unchanged warning after computer_use_wait", async () => {
394
+ setup();
395
+
396
+ // Establish previous AX tree
397
+ const p1 = proxy.request(
398
+ "computer_use_click",
399
+ { element_id: 1 },
400
+ "session-1",
401
+ 1,
402
+ );
403
+ proxy.recordAction("computer_use_click", { element_id: 1 });
404
+ const sent1 = sentMessages[0] as Record<string, unknown>;
405
+ proxy.resolve(sent1.requestId as string, {
406
+ axTree: "Button [1]",
407
+ });
408
+ await p1;
409
+
410
+ // Wait action with unchanged screen — should NOT warn
411
+ const p2 = proxy.request(
412
+ "computer_use_wait",
413
+ { duration_ms: 2000 },
414
+ "session-1",
415
+ 2,
416
+ );
417
+ proxy.recordAction("computer_use_wait", { duration_ms: 2000 });
418
+ const sent2 = sentMessages[1] as Record<string, unknown>;
419
+ proxy.resolve(sent2.requestId as string, {
420
+ axTree: "Button [1]",
421
+ // No axDiff — screen unchanged, but that's expected after wait
422
+ });
423
+ const result2 = await p2;
424
+ expect(result2.content).not.toContain("NO VISIBLE EFFECT");
425
+ });
426
+
367
427
  test("resets consecutive count when diff is present", async () => {
368
428
  setup();
369
429
 
@@ -507,6 +567,35 @@ describe("HostCuProxy", () => {
507
567
  expect(result.content).toMatch(/<\/ax-tree>$/m);
508
568
  });
509
569
 
570
+ test("includes secondaryWindows after AX tree with cross-window note", () => {
571
+ setup();
572
+
573
+ const result = proxy.formatObservation({
574
+ axTree: "Button [1]\nLabel [2]",
575
+ secondaryWindows: "Safari — Window [10]\n Link [11]",
576
+ });
577
+
578
+ expect(result.content).toContain("Safari — Window [10]");
579
+ expect(result.content).toContain("Link [11]");
580
+ expect(result.content).toContain(
581
+ "Note: The element [ID]s above are from other windows",
582
+ );
583
+ // secondaryWindows should appear after the AX tree
584
+ const axTreeEnd = result.content.indexOf("</ax-tree>");
585
+ const secondaryIdx = result.content.indexOf("Safari — Window [10]");
586
+ expect(axTreeEnd).toBeLessThan(secondaryIdx);
587
+ });
588
+
589
+ test("omits secondaryWindows section when field is absent", () => {
590
+ setup();
591
+
592
+ const result = proxy.formatObservation({
593
+ axTree: "Button [1]",
594
+ });
595
+
596
+ expect(result.content).not.toContain("other windows");
597
+ });
598
+
510
599
  test("includes diff when present", () => {
511
600
  setup();
512
601
 
@@ -576,7 +665,7 @@ describe("HostCuProxy", () => {
576
665
  // -------------------------------------------------------------------------
577
666
 
578
667
  describe("dispose", () => {
579
- test("rejects all pending requests", () => {
668
+ test("rejects all pending requests", async () => {
580
669
  setup();
581
670
 
582
671
  const resultPromise = proxy.request(
@@ -593,7 +682,80 @@ describe("HostCuProxy", () => {
593
682
  proxy.dispose();
594
683
 
595
684
  expect(proxy.hasPendingRequest(requestId)).toBe(false);
596
- expect(resultPromise).rejects.toThrow("Host CU proxy disposed");
685
+ await expect(resultPromise).rejects.toThrow("Host CU proxy disposed");
686
+ });
687
+ });
688
+
689
+ // -------------------------------------------------------------------------
690
+ // onInternalResolve callback
691
+ // -------------------------------------------------------------------------
692
+
693
+ describe("onInternalResolve", () => {
694
+ test("calls onInternalResolve when abort signal fires", async () => {
695
+ setup();
696
+
697
+ const controller = new AbortController();
698
+ const resultPromise = proxy.request(
699
+ "computer_use_click",
700
+ { element_id: 1 },
701
+ "session-1",
702
+ 1,
703
+ undefined,
704
+ controller.signal,
705
+ );
706
+
707
+ const sent = sentMessages[0] as Record<string, unknown>;
708
+ const requestId = sent.requestId as string;
709
+
710
+ controller.abort();
711
+
712
+ await resultPromise;
713
+ expect(resolvedRequestIds).toContain(requestId);
714
+ });
715
+
716
+ test("calls onInternalResolve on dispose", async () => {
717
+ setup();
718
+
719
+ const resultPromise = proxy.request(
720
+ "computer_use_click",
721
+ { element_id: 1 },
722
+ "session-1",
723
+ 1,
724
+ );
725
+
726
+ const sent = sentMessages[0] as Record<string, unknown>;
727
+ const requestId = sent.requestId as string;
728
+
729
+ proxy.dispose();
730
+
731
+ // dispose rejects pending requests — catch to avoid unhandled rejection
732
+ await resultPromise.catch(() => {});
733
+
734
+ expect(resolvedRequestIds).toContain(requestId);
735
+ });
736
+ });
737
+
738
+ // -------------------------------------------------------------------------
739
+ // isAvailable
740
+ // -------------------------------------------------------------------------
741
+
742
+ describe("isAvailable", () => {
743
+ test("returns false by default", () => {
744
+ setup();
745
+ expect(proxy.isAvailable()).toBe(false);
746
+ });
747
+
748
+ test("returns true after updateSender with clientConnected=true", () => {
749
+ setup();
750
+ proxy.updateSender(sendToClient as never, true);
751
+ expect(proxy.isAvailable()).toBe(true);
752
+ });
753
+
754
+ test("returns false after updateSender with clientConnected=false", () => {
755
+ setup();
756
+ proxy.updateSender(sendToClient as never, true);
757
+ proxy.updateSender(sendToClient as never, false);
758
+ expect(proxy.isAvailable()).toBe(false);
597
759
  });
598
760
  });
599
761
 
@@ -170,6 +170,7 @@ function makeSession(overrides: Record<string, unknown> = {}) {
170
170
  setHostBashProxy: () => {},
171
171
  setHostFileProxy: () => {},
172
172
  setHostCuProxy: () => {},
173
+ addPreactivatedSkillId: () => {},
173
174
  emitConfirmationStateChanged: () => {},
174
175
  emitActivityState: () => {},
175
176
  setTurnChannelContext: () => {},
@@ -85,7 +85,10 @@ mock.module("../runtime/approval-message-composer.js", () => ({
85
85
  composeApprovalMessageGenerative: async () => "mock generative message",
86
86
  }));
87
87
 
88
- import { findContactChannel } from "../contacts/contact-store.js";
88
+ import {
89
+ findContactChannel,
90
+ upsertContact,
91
+ } from "../contacts/contact-store.js";
89
92
  import { upsertContactChannel } from "../contacts/contacts-write.js";
90
93
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
91
94
  import { createInvite, revokeInvite } from "../memory/invite-store.js";
@@ -106,6 +109,11 @@ afterAll(() => {
106
109
  // Helpers
107
110
  // ---------------------------------------------------------------------------
108
111
 
112
+ /** Create a throwaway contact and return its ID, for use as the invite's contactId. */
113
+ function createTargetContact(displayName = "Test Contact"): string {
114
+ return upsertContact({ displayName, role: "contact" }).id;
115
+ }
116
+
109
117
  const TEST_BEARER_TOKEN = "test-token";
110
118
  let msgCounter = 0;
111
119
 
@@ -178,6 +186,7 @@ describe("inbound invite redemption intercept", () => {
178
186
  test("non-member with valid invite token becomes active member without guardian approval", async () => {
179
187
  const { rawToken } = createInvite({
180
188
  sourceChannel: "telegram",
189
+ contactId: createTargetContact(),
181
190
  maxUses: 5,
182
191
  });
183
192
 
@@ -231,6 +240,7 @@ describe("inbound invite redemption intercept", () => {
231
240
  test("non-member with expired token gets appropriate message", async () => {
232
241
  const { rawToken } = createInvite({
233
242
  sourceChannel: "telegram",
243
+ contactId: createTargetContact(),
234
244
  maxUses: 1,
235
245
  expiresInMs: -1, // already expired
236
246
  });
@@ -252,6 +262,7 @@ describe("inbound invite redemption intercept", () => {
252
262
  test("non-member with revoked token gets refusal text", async () => {
253
263
  const { rawToken, invite } = createInvite({
254
264
  sourceChannel: "telegram",
265
+ contactId: createTargetContact(),
255
266
  maxUses: 5,
256
267
  });
257
268
  revokeInvite(invite.id);
@@ -292,6 +303,7 @@ describe("inbound invite redemption intercept", () => {
292
303
  test("duplicate Telegram webhook deliveries do not double-redeem", async () => {
293
304
  const { rawToken } = createInvite({
294
305
  sourceChannel: "telegram",
306
+ contactId: createTargetContact(),
295
307
  maxUses: 5,
296
308
  });
297
309
 
@@ -353,7 +365,11 @@ describe("inbound invite redemption intercept", () => {
353
365
 
354
366
  test("channel mismatch returns appropriate message", async () => {
355
367
  // Create an invite for voice, but try to redeem via Telegram
356
- const { rawToken } = createInvite({ sourceChannel: "phone", maxUses: 5 });
368
+ const { rawToken } = createInvite({
369
+ sourceChannel: "phone",
370
+ contactId: createTargetContact(),
371
+ maxUses: 5,
372
+ });
357
373
 
358
374
  const req = buildInviteRequest(rawToken);
359
375
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
@@ -372,6 +388,7 @@ describe("inbound invite redemption intercept", () => {
372
388
  test("already-active member with invite token gets acknowledgement", async () => {
373
389
  const { rawToken } = createInvite({
374
390
  sourceChannel: "telegram",
391
+ contactId: createTargetContact(),
375
392
  maxUses: 5,
376
393
  });
377
394
 
@@ -397,11 +414,8 @@ describe("inbound invite redemption intercept", () => {
397
414
  });
398
415
 
399
416
  test("reactivation via invite preserves existing guardian-managed member display name", async () => {
400
- const { rawToken } = createInvite({
401
- sourceChannel: "telegram",
402
- maxUses: 5,
403
- });
404
-
417
+ // Pre-create a revoked member named "Jeff" — the invite should preserve
418
+ // that guardian-assigned name rather than overwriting with the Telegram name.
405
419
  upsertContactChannel({
406
420
  sourceChannel: "telegram",
407
421
  externalUserId: "user-invite-123",
@@ -411,6 +425,21 @@ describe("inbound invite redemption intercept", () => {
411
425
  displayName: "Jeff",
412
426
  });
413
427
 
428
+ // Look up the contact that upsertContactChannel created so we can use
429
+ // its ID as the invite's contactId (satisfies the FK constraint).
430
+ const existing = findContactChannel({
431
+ channelType: "telegram",
432
+ externalUserId: "user-invite-123",
433
+ externalChatId: "chat-invite-test",
434
+ });
435
+ const targetContactId = existing!.contact.id;
436
+
437
+ const { rawToken } = createInvite({
438
+ sourceChannel: "telegram",
439
+ contactId: targetContactId,
440
+ maxUses: 5,
441
+ });
442
+
414
443
  const req = buildInviteRequest(rawToken, {
415
444
  actorDisplayName: "Noa Flaherty",
416
445
  });
@@ -10,6 +10,7 @@ const connectedProviders = new Set<string>();
10
10
 
11
11
  mock.module("../security/secure-keys.js", () => ({
12
12
  getSecureKey: (account: string) => secureKeyValues.get(account),
13
+ getSecureKeyAsync: async (account: string) => secureKeyValues.get(account),
13
14
  }));
14
15
 
15
16
  mock.module("../config/loader.js", () => ({
@@ -45,8 +46,8 @@ describe("integration-status", () => {
45
46
  });
46
47
 
47
48
  describe("getIntegrationSummary", () => {
48
- test("returns all disconnected when no keys are set", () => {
49
- const summary = getIntegrationSummary();
49
+ test("returns all disconnected when no keys are set", async () => {
50
+ const summary = await getIntegrationSummary();
50
51
  expect(summary).toEqual([
51
52
  { name: "Gmail", category: "email", connected: false },
52
53
  { name: "Slack", category: "messaging", connected: false },
@@ -55,25 +56,25 @@ describe("integration-status", () => {
55
56
  ]);
56
57
  });
57
58
 
58
- test("returns all connected when all keys are set", () => {
59
- setOAuthConnected("integration:gmail");
59
+ test("returns all connected when all keys are set", async () => {
60
+ setOAuthConnected("integration:google");
60
61
  setOAuthConnected("integration:slack");
61
62
  mockTwilioAccountSid = "sid";
62
63
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
63
64
  setOAuthConnected("telegram");
64
65
 
65
- const summary = getIntegrationSummary();
66
+ const summary = await getIntegrationSummary();
66
67
  expect(summary.every((s: { connected: boolean }) => s.connected)).toBe(
67
68
  true,
68
69
  );
69
70
  });
70
71
 
71
- test("returns mixed status", () => {
72
+ test("returns mixed status", async () => {
72
73
  mockTwilioAccountSid = "sid";
73
74
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
74
75
  setOAuthConnected("telegram");
75
76
 
76
- const summary = getIntegrationSummary();
77
+ const summary = await getIntegrationSummary();
77
78
  const connected = summary.filter(
78
79
  (s: { connected: boolean }) => s.connected,
79
80
  );
@@ -91,17 +92,17 @@ describe("integration-status", () => {
91
92
  ]);
92
93
  });
93
94
 
94
- test("Twilio disconnected when only account_sid is set (missing auth_token)", () => {
95
+ test("Twilio disconnected when only account_sid is set (missing auth_token)", async () => {
95
96
  mockTwilioAccountSid = "sid";
96
97
 
97
- const summary = getIntegrationSummary();
98
+ const summary = await getIntegrationSummary();
98
99
  const twilio = summary.find((s: { name: string }) => s.name === "Twilio");
99
100
  expect(twilio?.connected).toBe(false);
100
101
  });
101
102
 
102
- test("Telegram disconnected when no connection record exists", () => {
103
+ test("Telegram disconnected when no connection record exists", async () => {
103
104
  // No oauth_connection record for telegram — should be disconnected
104
- const summary = getIntegrationSummary();
105
+ const summary = await getIntegrationSummary();
105
106
  const telegram = summary.find(
106
107
  (s: { name: string }) => s.name === "Telegram",
107
108
  );
@@ -110,32 +111,32 @@ describe("integration-status", () => {
110
111
  });
111
112
 
112
113
  describe("formatIntegrationSummary", () => {
113
- test("shows checkmarks and crosses", () => {
114
+ test("shows checkmarks and crosses", async () => {
114
115
  mockTwilioAccountSid = "sid";
115
116
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
116
117
  setOAuthConnected("telegram");
117
118
 
118
- const result = formatIntegrationSummary();
119
+ const result = await formatIntegrationSummary();
119
120
  expect(result).toBe(
120
121
  "Gmail \u2717 | Slack \u2717 | Twilio \u2713 | Telegram \u2713",
121
122
  );
122
123
  });
123
124
 
124
- test("all disconnected", () => {
125
- const result = formatIntegrationSummary();
125
+ test("all disconnected", async () => {
126
+ const result = await formatIntegrationSummary();
126
127
  expect(result).toBe(
127
128
  "Gmail \u2717 | Slack \u2717 | Twilio \u2717 | Telegram \u2717",
128
129
  );
129
130
  });
130
131
 
131
- test("all connected", () => {
132
- setOAuthConnected("integration:gmail");
132
+ test("all connected", async () => {
133
+ setOAuthConnected("integration:google");
133
134
  setOAuthConnected("integration:slack");
134
135
  mockTwilioAccountSid = "sid";
135
136
  secureKeyValues.set(credentialKey("twilio", "auth_token"), "auth");
136
137
  setOAuthConnected("telegram");
137
138
 
138
- const result = formatIntegrationSummary();
139
+ const result = await formatIntegrationSummary();
139
140
  expect(result).toBe(
140
141
  "Gmail \u2713 | Slack \u2713 | Twilio \u2713 | Telegram \u2713",
141
142
  );
@@ -143,28 +144,28 @@ describe("integration-status", () => {
143
144
  });
144
145
 
145
146
  describe("hasCapability", () => {
146
- test("returns false when no integrations in category are connected", () => {
147
- expect(hasCapability("email")).toBe(false);
148
- expect(hasCapability("messaging")).toBe(false);
147
+ test("returns false when no integrations in category are connected", async () => {
148
+ expect(await hasCapability("email")).toBe(false);
149
+ expect(await hasCapability("messaging")).toBe(false);
149
150
  });
150
151
 
151
- test("returns true when any integration in category is connected", () => {
152
+ test("returns true when any integration in category is connected", async () => {
152
153
  setOAuthConnected("telegram");
153
- expect(hasCapability("messaging")).toBe(true);
154
+ expect(await hasCapability("messaging")).toBe(true);
154
155
  });
155
156
 
156
- test("returns false when no connection record exists for category integrations", () => {
157
+ test("returns false when no connection record exists for category integrations", async () => {
157
158
  // No oauth_connection record for telegram — should not count as connected
158
- expect(hasCapability("messaging")).toBe(false);
159
+ expect(await hasCapability("messaging")).toBe(false);
159
160
  });
160
161
 
161
- test("returns false for unknown categories", () => {
162
- expect(hasCapability("nonexistent")).toBe(false);
162
+ test("returns false for unknown categories", async () => {
163
+ expect(await hasCapability("nonexistent")).toBe(false);
163
164
  });
164
165
 
165
- test("email category checks Gmail", () => {
166
- setOAuthConnected("integration:gmail");
167
- expect(hasCapability("email")).toBe(true);
166
+ test("email category checks Gmail", async () => {
167
+ setOAuthConnected("integration:google");
168
+ expect(await hasCapability("email")).toBe(true);
168
169
  });
169
170
  });
170
171
  });