@vellumai/assistant 0.4.48 → 0.4.50

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 (423) hide show
  1. package/ARCHITECTURE.md +26 -35
  2. package/README.md +5 -26
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/memory.md +180 -119
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +2 -2
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/agent-loop.test.ts +3 -1
  12. package/src/__tests__/anthropic-provider.test.ts +249 -2
  13. package/src/__tests__/approval-cascade.test.ts +796 -0
  14. package/src/__tests__/approval-primitive.test.ts +0 -1
  15. package/src/__tests__/approval-routes-http.test.ts +4 -0
  16. package/src/__tests__/assistant-attachments.test.ts +12 -34
  17. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  18. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  19. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  22. package/src/__tests__/channel-guardian.test.ts +0 -2
  23. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  24. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  25. package/src/__tests__/checker.test.ts +13 -20
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-schema.test.ts +1 -68
  29. package/src/__tests__/config-watcher.test.ts +0 -1
  30. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  31. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  32. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  33. package/src/__tests__/context-token-estimator.test.ts +196 -13
  34. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  35. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  36. package/src/__tests__/conversation-routes-guardian-reply.test.ts +152 -0
  37. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  38. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  39. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  40. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  41. package/src/__tests__/credential-vault-unit.test.ts +284 -49
  42. package/src/__tests__/credential-vault.test.ts +150 -16
  43. package/src/__tests__/credentials-cli.test.ts +71 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  45. package/src/__tests__/date-context.test.ts +93 -77
  46. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  47. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  48. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  51. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  52. package/src/__tests__/guardian-routing-invariants.test.ts +93 -1
  53. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  55. package/src/__tests__/heartbeat-service.test.ts +0 -1
  56. package/src/__tests__/history-repair.test.ts +245 -0
  57. package/src/__tests__/host-cu-proxy.test.ts +791 -0
  58. package/src/__tests__/host-shell-tool.test.ts +27 -15
  59. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  60. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  61. package/src/__tests__/integration-status.test.ts +32 -51
  62. package/src/__tests__/intent-routing.test.ts +0 -1
  63. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  64. package/src/__tests__/invite-routes-http.test.ts +10 -9
  65. package/src/__tests__/keychain-broker-client.test.ts +14 -46
  66. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  68. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  69. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  70. package/src/__tests__/memory-regressions.test.ts +477 -2841
  71. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  72. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  73. package/src/__tests__/mime-builder.test.ts +28 -0
  74. package/src/__tests__/native-web-search.test.ts +1 -0
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +941 -15
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +870 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  83. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  84. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  85. package/src/__tests__/recording-handler.test.ts +3 -4
  86. package/src/__tests__/registry.test.ts +2 -3
  87. package/src/__tests__/relay-server.test.ts +46 -1
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/schedule-tools.test.ts +32 -0
  91. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  95. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  96. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  97. package/src/__tests__/secure-keys.test.ts +7 -2
  98. package/src/__tests__/send-endpoint-busy.test.ts +24 -6
  99. package/src/__tests__/sequence-store.test.ts +0 -1
  100. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  101. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  102. package/src/__tests__/session-agent-loop.test.ts +19 -15
  103. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  104. package/src/__tests__/session-error.test.ts +124 -2
  105. package/src/__tests__/session-history-web-search.test.ts +918 -0
  106. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  107. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  108. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  109. package/src/__tests__/session-queue.test.ts +37 -27
  110. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  111. package/src/__tests__/session-slash-known.test.ts +1 -15
  112. package/src/__tests__/session-slash-queue.test.ts +1 -15
  113. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  114. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  115. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  116. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  117. package/src/__tests__/skill-include-graph.test.ts +66 -0
  118. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  119. package/src/__tests__/skill-load-tool.test.ts +149 -1
  120. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  121. package/src/__tests__/skills-install-extract.test.ts +93 -0
  122. package/src/__tests__/skills-uninstall.test.ts +1 -1
  123. package/src/__tests__/skills.test.ts +3 -3
  124. package/src/__tests__/skillssh-registry.test.ts +451 -0
  125. package/src/__tests__/slack-channel-config.test.ts +67 -3
  126. package/src/__tests__/slack-share-routes.test.ts +17 -19
  127. package/src/__tests__/system-prompt.test.ts +0 -1
  128. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  129. package/src/__tests__/terminal-tools.test.ts +4 -3
  130. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  131. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  132. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  133. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  134. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  135. package/src/__tests__/tool-executor.test.ts +0 -1
  136. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  137. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  138. package/src/__tests__/trust-store.test.ts +7 -13
  139. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  140. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  141. package/src/__tests__/twilio-routes.test.ts +0 -16
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  145. package/src/agent/ax-tree-compaction.test.ts +286 -0
  146. package/src/agent/loop.ts +104 -131
  147. package/src/approvals/AGENTS.md +1 -1
  148. package/src/approvals/guardian-request-resolvers.ts +14 -2
  149. package/src/bundler/compiler-tools.ts +66 -2
  150. package/src/calls/call-domain.ts +133 -6
  151. package/src/calls/call-store.ts +6 -0
  152. package/src/calls/relay-server.ts +52 -18
  153. package/src/calls/relay-setup-router.ts +17 -1
  154. package/src/calls/twilio-config.ts +3 -8
  155. package/src/calls/twilio-routes.ts +1 -2
  156. package/src/calls/types.ts +3 -1
  157. package/src/calls/voice-ingress-preflight.ts +1 -1
  158. package/src/cli/commands/browser-relay.ts +18 -12
  159. package/src/cli/commands/completions.ts +0 -3
  160. package/src/cli/commands/credentials.ts +101 -15
  161. package/src/cli/commands/doctor.ts +4 -3
  162. package/src/cli/commands/mcp.ts +46 -59
  163. package/src/cli/commands/memory.ts +16 -165
  164. package/src/cli/commands/oauth/apps.ts +284 -0
  165. package/src/cli/commands/oauth/connections.ts +633 -0
  166. package/src/cli/commands/oauth/index.ts +52 -0
  167. package/src/cli/commands/oauth/providers.ts +256 -0
  168. package/src/cli/commands/sessions.ts +5 -2
  169. package/src/cli/commands/skills.ts +177 -339
  170. package/src/cli/http-client.ts +0 -20
  171. package/src/cli/main-screen.tsx +2 -2
  172. package/src/cli/program.ts +6 -11
  173. package/src/cli/reference.ts +1 -3
  174. package/src/cli.ts +4 -10
  175. package/src/config/assistant-feature-flags.ts +0 -3
  176. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  177. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  178. package/src/config/bundled-skills/computer-use/TOOLS.json +23 -5
  179. package/src/config/bundled-skills/computer-use/tools/{computer-use-request-control.ts → computer-use-observe.ts} +1 -5
  180. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  181. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  182. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  183. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  184. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  185. package/src/config/bundled-tool-registry.ts +2 -5
  186. package/src/config/env-registry.ts +14 -83
  187. package/src/config/env.ts +11 -50
  188. package/src/config/feature-flag-registry.json +16 -16
  189. package/src/config/loader.ts +0 -6
  190. package/src/config/schema.ts +4 -13
  191. package/src/config/schemas/memory-lifecycle.ts +0 -9
  192. package/src/config/schemas/memory-processing.ts +0 -180
  193. package/src/config/schemas/memory-retrieval.ts +32 -104
  194. package/src/config/schemas/memory.ts +0 -10
  195. package/src/config/skills.ts +21 -2
  196. package/src/config/types.ts +0 -4
  197. package/src/context/image-dimensions.ts +229 -0
  198. package/src/context/token-estimator.ts +75 -12
  199. package/src/context/window-manager.ts +53 -11
  200. package/src/daemon/assistant-attachments.ts +1 -13
  201. package/src/daemon/config-watcher.ts +61 -3
  202. package/src/daemon/daemon-control.ts +1 -1
  203. package/src/daemon/date-context.ts +114 -31
  204. package/src/daemon/handlers/config-ingress.ts +8 -33
  205. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  206. package/src/daemon/handlers/config-telegram.ts +32 -16
  207. package/src/daemon/handlers/sessions.ts +27 -36
  208. package/src/daemon/handlers/shared.ts +0 -130
  209. package/src/daemon/handlers/skills.ts +20 -1
  210. package/src/daemon/history-repair.ts +72 -8
  211. package/src/daemon/host-cu-proxy.ts +430 -0
  212. package/src/daemon/lifecycle.ts +67 -71
  213. package/src/daemon/mcp-reload-service.ts +2 -2
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/computer-use.ts +1 -129
  216. package/src/daemon/message-types/host-cu.ts +19 -0
  217. package/src/daemon/message-types/memory.ts +4 -16
  218. package/src/daemon/message-types/messages.ts +4 -0
  219. package/src/daemon/message-types/sessions.ts +4 -0
  220. package/src/daemon/server.ts +25 -21
  221. package/src/daemon/session-agent-loop-handlers.ts +40 -0
  222. package/src/daemon/session-agent-loop.ts +334 -48
  223. package/src/daemon/session-attachments.ts +1 -2
  224. package/src/daemon/session-error.ts +89 -6
  225. package/src/daemon/session-history.ts +17 -7
  226. package/src/daemon/session-media-retry.ts +6 -2
  227. package/src/daemon/session-memory.ts +69 -149
  228. package/src/daemon/session-process.ts +10 -1
  229. package/src/daemon/session-runtime-assembly.ts +49 -19
  230. package/src/daemon/session-slash.ts +1 -1
  231. package/src/daemon/session-surfaces.ts +43 -28
  232. package/src/daemon/session-tool-setup.ts +9 -10
  233. package/src/daemon/session.ts +150 -17
  234. package/src/daemon/tool-side-effects.ts +2 -8
  235. package/src/daemon/watch-handler.ts +2 -2
  236. package/src/events/tool-metrics-listener.ts +2 -2
  237. package/src/hooks/manager.ts +1 -4
  238. package/src/inbound/public-ingress-urls.ts +7 -7
  239. package/src/instrument.ts +61 -1
  240. package/src/logfire.ts +16 -5
  241. package/src/memory/admin.ts +2 -191
  242. package/src/memory/canonical-guardian-store.ts +38 -2
  243. package/src/memory/conversation-crud.ts +0 -33
  244. package/src/memory/conversation-key-store.ts +21 -0
  245. package/src/memory/conversation-queries.ts +22 -3
  246. package/src/memory/db-init.ts +32 -0
  247. package/src/memory/embedding-backend.ts +84 -8
  248. package/src/memory/embedding-types.ts +9 -1
  249. package/src/memory/indexer.ts +7 -46
  250. package/src/memory/items-extractor.ts +274 -76
  251. package/src/memory/job-handlers/backfill.ts +2 -127
  252. package/src/memory/job-handlers/cleanup.ts +2 -16
  253. package/src/memory/job-handlers/extraction.ts +2 -138
  254. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  255. package/src/memory/job-handlers/summarization.ts +3 -148
  256. package/src/memory/job-utils.ts +21 -59
  257. package/src/memory/jobs-store.ts +1 -159
  258. package/src/memory/jobs-worker.ts +9 -52
  259. package/src/memory/migrations/104-core-indexes.ts +3 -3
  260. package/src/memory/migrations/149-oauth-tables.ts +62 -0
  261. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  262. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  263. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  264. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  265. package/src/memory/migrations/154-drop-fts.ts +20 -0
  266. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  267. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  268. package/src/memory/migrations/index.ts +8 -0
  269. package/src/memory/qdrant-client.ts +148 -51
  270. package/src/memory/raw-query.ts +1 -1
  271. package/src/memory/retriever.test.ts +294 -273
  272. package/src/memory/retriever.ts +421 -645
  273. package/src/memory/schema/calls.ts +2 -0
  274. package/src/memory/schema/index.ts +1 -0
  275. package/src/memory/schema/memory-core.ts +3 -48
  276. package/src/memory/schema/oauth.ts +67 -0
  277. package/src/memory/search/formatting.ts +263 -176
  278. package/src/memory/search/lexical.ts +1 -254
  279. package/src/memory/search/ranking.ts +0 -455
  280. package/src/memory/search/semantic.ts +100 -14
  281. package/src/memory/search/staleness.ts +47 -0
  282. package/src/memory/search/tier-classifier.ts +21 -0
  283. package/src/memory/search/types.ts +15 -77
  284. package/src/memory/task-memory-cleanup.ts +4 -6
  285. package/src/messaging/provider.ts +4 -4
  286. package/src/messaging/providers/gmail/client.ts +82 -2
  287. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  288. package/src/messaging/providers/gmail/people-client.ts +10 -10
  289. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  290. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  291. package/src/messaging/registry.ts +2 -32
  292. package/src/notifications/copy-composer.ts +0 -5
  293. package/src/notifications/signal.ts +4 -5
  294. package/src/oauth/byo-connection.test.ts +133 -25
  295. package/src/oauth/byo-connection.ts +22 -6
  296. package/src/oauth/connect-orchestrator.ts +113 -57
  297. package/src/oauth/connect-types.ts +17 -23
  298. package/src/oauth/connection-resolver.ts +35 -11
  299. package/src/oauth/connection.ts +1 -1
  300. package/src/oauth/manual-token-connection.ts +104 -0
  301. package/src/oauth/oauth-store.ts +582 -0
  302. package/src/oauth/platform-connection.test.ts +29 -0
  303. package/src/oauth/platform-connection.ts +6 -5
  304. package/src/oauth/provider-behaviors.ts +124 -0
  305. package/src/oauth/scope-policy.ts +9 -2
  306. package/src/oauth/seed-providers.ts +167 -0
  307. package/src/oauth/token-persistence.ts +81 -77
  308. package/src/permissions/checker.ts +3 -3
  309. package/src/permissions/defaults.ts +1 -1
  310. package/src/permissions/prompter.ts +10 -1
  311. package/src/permissions/trust-store.ts +36 -1
  312. package/src/playbooks/playbook-compiler.ts +1 -1
  313. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  314. package/src/prompts/system-prompt.ts +46 -42
  315. package/src/providers/anthropic/client.ts +59 -20
  316. package/src/providers/retry.ts +1 -27
  317. package/src/providers/types.ts +7 -1
  318. package/src/runtime/AGENTS.md +9 -0
  319. package/src/runtime/auth/route-policy.ts +6 -6
  320. package/src/runtime/channel-reply-delivery.ts +0 -40
  321. package/src/runtime/gateway-client.ts +0 -7
  322. package/src/runtime/guardian-reply-router.ts +24 -22
  323. package/src/runtime/http-server.ts +10 -8
  324. package/src/runtime/http-types.ts +2 -2
  325. package/src/runtime/invite-redemption-service.ts +19 -1
  326. package/src/runtime/invite-service.ts +25 -0
  327. package/src/runtime/middleware/twilio-validation.ts +1 -11
  328. package/src/runtime/pending-interactions.ts +14 -12
  329. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  330. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  331. package/src/runtime/routes/conversation-routes.ts +81 -19
  332. package/src/runtime/routes/events-routes.ts +21 -11
  333. package/src/runtime/routes/host-cu-routes.ts +97 -0
  334. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  335. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  336. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  337. package/src/runtime/routes/log-export-routes.ts +126 -8
  338. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  339. package/src/runtime/routes/memory-item-routes.ts +503 -0
  340. package/src/runtime/routes/session-management-routes.ts +3 -3
  341. package/src/runtime/routes/settings-routes.ts +55 -48
  342. package/src/runtime/routes/surface-action-routes.ts +1 -1
  343. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  344. package/src/runtime/routes/watch-routes.ts +128 -0
  345. package/src/runtime/routes/workspace-routes.ts +2 -1
  346. package/src/schedule/integration-status.ts +10 -9
  347. package/src/security/credential-key.ts +0 -156
  348. package/src/security/keychain-broker-client.ts +22 -10
  349. package/src/security/oauth2.ts +1 -1
  350. package/src/security/secure-keys.ts +25 -3
  351. package/src/security/token-manager.ts +137 -64
  352. package/src/skills/catalog-install.ts +414 -0
  353. package/src/skills/include-graph.ts +32 -0
  354. package/src/skills/skillssh-registry.ts +503 -0
  355. package/src/telegram/bot-username.ts +2 -3
  356. package/src/tools/assets/search.ts +5 -1
  357. package/src/tools/browser/network-recorder.ts +1 -1
  358. package/src/tools/browser/network-recording-types.ts +1 -1
  359. package/src/tools/computer-use/definitions.ts +36 -11
  360. package/src/tools/computer-use/registry.ts +5 -6
  361. package/src/tools/credentials/broker.ts +1 -2
  362. package/src/tools/credentials/metadata-store.ts +17 -121
  363. package/src/tools/credentials/vault.ts +92 -167
  364. package/src/tools/memory/definitions.ts +4 -13
  365. package/src/tools/memory/handlers.test.ts +83 -103
  366. package/src/tools/memory/handlers.ts +50 -85
  367. package/src/tools/registry.ts +2 -7
  368. package/src/tools/schedule/create.ts +8 -1
  369. package/src/tools/schedule/update.ts +8 -1
  370. package/src/tools/skills/load.ts +85 -3
  371. package/src/tools/watch/watch-state.ts +0 -12
  372. package/src/util/logger.ts +7 -41
  373. package/src/util/platform.ts +9 -28
  374. package/src/watcher/providers/google-calendar.ts +2 -1
  375. package/src/__tests__/clarification-resolver.test.ts +0 -193
  376. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  377. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  378. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  379. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  380. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  381. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  382. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  383. package/src/__tests__/conflict-policy.test.ts +0 -269
  384. package/src/__tests__/conflict-store.test.ts +0 -372
  385. package/src/__tests__/contradiction-checker.test.ts +0 -361
  386. package/src/__tests__/entity-extractor.test.ts +0 -211
  387. package/src/__tests__/entity-search.test.ts +0 -1117
  388. package/src/__tests__/profile-compiler.test.ts +0 -392
  389. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  390. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  391. package/src/__tests__/session-profile-injection.test.ts +0 -557
  392. package/src/cli/commands/dev.ts +0 -129
  393. package/src/cli/commands/map.ts +0 -391
  394. package/src/cli/commands/oauth.ts +0 -77
  395. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  396. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  397. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  398. package/src/daemon/computer-use-session.ts +0 -1026
  399. package/src/daemon/ride-shotgun-handler.ts +0 -569
  400. package/src/daemon/session-conflict-gate.ts +0 -167
  401. package/src/daemon/session-dynamic-profile.ts +0 -77
  402. package/src/memory/clarification-resolver.ts +0 -417
  403. package/src/memory/conflict-intent.ts +0 -205
  404. package/src/memory/conflict-policy.ts +0 -127
  405. package/src/memory/conflict-store.ts +0 -410
  406. package/src/memory/contradiction-checker.ts +0 -508
  407. package/src/memory/entity-extractor.ts +0 -535
  408. package/src/memory/format-recall.ts +0 -47
  409. package/src/memory/fts-reconciler.ts +0 -165
  410. package/src/memory/job-handlers/conflict.ts +0 -200
  411. package/src/memory/profile-compiler.ts +0 -195
  412. package/src/memory/recall-cache.ts +0 -117
  413. package/src/memory/search/entity.ts +0 -535
  414. package/src/memory/search/query-expansion.test.ts +0 -70
  415. package/src/memory/search/query-expansion.ts +0 -118
  416. package/src/oauth/provider-base-urls.ts +0 -21
  417. package/src/oauth/provider-profiles.ts +0 -192
  418. package/src/prompts/computer-use-prompt.ts +0 -98
  419. package/src/runtime/routes/computer-use-routes.ts +0 -641
  420. package/src/runtime/routes/mcp-routes.ts +0 -20
  421. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  422. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  423. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -0,0 +1,796 @@
1
+ /**
2
+ * Tests for cascading approval decisions to matching pending confirmations.
3
+ *
4
+ * When a user resolves one confirmation with a broad decision (allow_10m,
5
+ * allow_thread, always_allow, always_deny), other pending confirmations in
6
+ * the same conversation that match are auto-resolved.
7
+ */
8
+ import { mkdtempSync, rmSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ import { Minimatch } from "minimatch";
14
+
15
+ import type {
16
+ AgentEvent,
17
+ CheckpointDecision,
18
+ CheckpointInfo,
19
+ } from "../agent/loop.js";
20
+ import type { ServerMessage } from "../daemon/message-protocol.js";
21
+ import type { ConfirmationStateChanged } from "../daemon/message-types/messages.js";
22
+ import type { Message, ProviderResponse } from "../providers/types.js";
23
+ import type { ConfirmationDetails } from "../runtime/pending-interactions.js";
24
+
25
+ const testDir = mkdtempSync(join(tmpdir(), "approval-cascade-test-"));
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Mocks — must precede Session import
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function makeLoggerStub(): Record<string, unknown> {
32
+ const stub: Record<string, unknown> = {};
33
+ for (const m of [
34
+ "info",
35
+ "warn",
36
+ "error",
37
+ "debug",
38
+ "trace",
39
+ "fatal",
40
+ "silent",
41
+ "child",
42
+ ]) {
43
+ stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
44
+ }
45
+ return stub;
46
+ }
47
+
48
+ mock.module("../util/logger.js", () => ({
49
+ getLogger: () => makeLoggerStub(),
50
+ }));
51
+
52
+ mock.module("../util/platform.js", () => ({
53
+ getDataDir: () => testDir,
54
+ }));
55
+
56
+ mock.module("../memory/guardian-action-store.js", () => ({
57
+ getPendingDeliveryByConversation: () => null,
58
+ getGuardianActionRequest: () => null,
59
+ resolveGuardianActionRequest: () => {},
60
+ }));
61
+
62
+ mock.module("../providers/registry.js", () => ({
63
+ getProvider: () => ({ name: "mock-provider" }),
64
+ initializeProviders: () => {},
65
+ }));
66
+
67
+ mock.module("../config/loader.js", () => ({
68
+ getConfig: () => ({
69
+ ui: {},
70
+ provider: "mock-provider",
71
+ maxTokens: 4096,
72
+ thinking: false,
73
+ contextWindow: {
74
+ maxInputTokens: 100000,
75
+ thresholdTokens: 80000,
76
+ preserveRecentMessages: 6,
77
+ summaryModel: "mock-model",
78
+ maxSummaryTokens: 512,
79
+ },
80
+ rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
81
+ timeouts: { permissionTimeoutSec: 300 },
82
+ apiKeys: {},
83
+ skills: { entries: {}, allowBundled: true },
84
+ permissions: { mode: "workspace" },
85
+ }),
86
+ loadRawConfig: () => ({}),
87
+ saveRawConfig: () => {},
88
+ invalidateConfigCache: () => {},
89
+ }));
90
+
91
+ mock.module("../prompts/system-prompt.js", () => ({
92
+ buildSystemPrompt: () => "system prompt",
93
+ }));
94
+
95
+ mock.module("../config/skills.js", () => ({
96
+ loadSkillCatalog: () => [],
97
+ loadSkillBySelector: () => ({ skill: null }),
98
+ ensureSkillIcon: async () => null,
99
+ }));
100
+
101
+ mock.module("../config/skill-state.js", () => ({
102
+ resolveSkillStates: () => [],
103
+ }));
104
+
105
+ mock.module("../skills/slash-commands.js", () => ({
106
+ buildInvocableSlashCatalog: () => new Map(),
107
+ resolveSlashSkillCommand: () => ({ kind: "not_slash" }),
108
+ rewriteKnownSlashCommandPrompt: () => "",
109
+ parseSlashCandidate: () => ({ kind: "not_slash" }),
110
+ }));
111
+
112
+ // Trust store mock — uses real minimatch for patternMatchesCandidate so the
113
+ // mock doesn't break trust-store-pattern-matches.test.ts when both files run
114
+ // in the same Bun process (mock.module leaks across test files).
115
+ mock.module("../permissions/trust-store.js", () => ({
116
+ addRule: () => {},
117
+ findHighestPriorityRule: () => null,
118
+ clearCache: () => {},
119
+ patternMatchesCandidate: (pattern: string, candidate: string): boolean => {
120
+ try {
121
+ return new Minimatch(pattern).match(candidate);
122
+ } catch {
123
+ return false;
124
+ }
125
+ },
126
+ }));
127
+
128
+ mock.module("../security/secret-allowlist.js", () => ({
129
+ resetAllowlist: () => {},
130
+ }));
131
+
132
+ mock.module("../memory/conversation-crud.js", () => ({
133
+ getConversationThreadType: () => "default",
134
+ setConversationOriginChannelIfUnset: () => {},
135
+ updateConversationContextWindow: () => {},
136
+ deleteMessageById: () => {},
137
+ provenanceFromTrustContext: () => ({
138
+ source: "user",
139
+ trustContext: undefined,
140
+ }),
141
+ getConversationOriginInterface: () => null,
142
+ getConversationOriginChannel: () => null,
143
+ getMessages: () => [],
144
+ getConversation: () => ({
145
+ id: "conv-1",
146
+ contextSummary: null,
147
+ contextCompactedMessageCount: 0,
148
+ totalInputTokens: 0,
149
+ totalOutputTokens: 0,
150
+ totalEstimatedCost: 0,
151
+ }),
152
+ createConversation: () => ({ id: "conv-1" }),
153
+ addMessage: () => ({ id: `msg-${Date.now()}` }),
154
+ updateConversationUsage: () => {},
155
+ updateConversationTitle: () => {},
156
+ }));
157
+
158
+ mock.module("../memory/conversation-queries.js", () => ({
159
+ listConversations: () => [],
160
+ }));
161
+
162
+ mock.module("../memory/attachments-store.js", () => ({
163
+ uploadAttachment: () => ({ id: `att-${Date.now()}` }),
164
+ linkAttachmentToMessage: () => {},
165
+ }));
166
+
167
+ mock.module("../memory/retriever.js", () => ({
168
+ buildMemoryRecall: async () => ({
169
+ enabled: false,
170
+ degraded: false,
171
+ injectedText: "",
172
+
173
+ semanticHits: 0,
174
+ recencyHits: 0,
175
+ injectedTokens: 0,
176
+ latencyMs: 0,
177
+ }),
178
+ stripMemoryRecallMessages: (msgs: Message[]) => msgs,
179
+ }));
180
+
181
+ mock.module("../context/window-manager.js", () => ({
182
+ ContextWindowManager: class {
183
+ constructor() {}
184
+ shouldCompact() {
185
+ return { needed: false, estimatedTokens: 0 };
186
+ }
187
+ async maybeCompact() {
188
+ return { compacted: false };
189
+ }
190
+ },
191
+ createContextSummaryMessage: () => ({
192
+ role: "user",
193
+ content: [{ type: "text", text: "summary" }],
194
+ }),
195
+ getSummaryFromContextMessage: () => null,
196
+ }));
197
+
198
+ mock.module("../memory/llm-usage-store.js", () => ({
199
+ recordUsageEvent: () => ({ id: "mock-id", createdAt: Date.now() }),
200
+ listUsageEvents: () => [],
201
+ }));
202
+
203
+ mock.module("../agent/loop.js", () => ({
204
+ AgentLoop: class {
205
+ constructor() {}
206
+ async run(
207
+ _messages: Message[],
208
+ _onEvent: (event: AgentEvent) => void,
209
+ _signal?: AbortSignal,
210
+ _requestId?: string,
211
+ _onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
212
+ ): Promise<Message[]> {
213
+ return [];
214
+ }
215
+ },
216
+ }));
217
+
218
+ mock.module("../memory/canonical-guardian-store.js", () => ({
219
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
220
+ listCanonicalGuardianRequests: () => [],
221
+ listPendingRequestsByConversationScope: () => [],
222
+ createCanonicalGuardianRequest: () => ({
223
+ id: "mock-cg-id",
224
+ code: "MOCK",
225
+ status: "pending",
226
+ }),
227
+ getCanonicalGuardianRequest: () => null,
228
+ getCanonicalGuardianRequestByCode: () => null,
229
+ updateCanonicalGuardianRequest: () => {},
230
+ resolveCanonicalGuardianRequest: () => {},
231
+ createCanonicalGuardianDelivery: () => ({ id: "mock-cgd-id" }),
232
+ listCanonicalGuardianDeliveries: () => [],
233
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
234
+ updateCanonicalGuardianDelivery: () => {},
235
+ generateCanonicalRequestCode: () => "MOCK-CODE",
236
+ }));
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Import Session and pendingInteractions AFTER mocks
240
+ // ---------------------------------------------------------------------------
241
+
242
+ import { Session } from "../daemon/session.js";
243
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Helpers
247
+ // ---------------------------------------------------------------------------
248
+
249
+ const CONV_ID = "conv-cascade-test";
250
+
251
+ function makeProvider() {
252
+ return {
253
+ name: "mock",
254
+ async sendMessage(): Promise<ProviderResponse> {
255
+ return {
256
+ content: [],
257
+ model: "mock",
258
+ usage: { inputTokens: 0, outputTokens: 0 },
259
+ stopReason: "end_turn",
260
+ };
261
+ },
262
+ };
263
+ }
264
+
265
+ function makeSession(
266
+ sendToClient?: (msg: ServerMessage) => void,
267
+ conversationId = CONV_ID,
268
+ ): Session {
269
+ return new Session(
270
+ conversationId,
271
+ makeProvider(),
272
+ "system prompt",
273
+ 4096,
274
+ sendToClient ?? (() => {}),
275
+ testDir,
276
+ );
277
+ }
278
+
279
+ /**
280
+ * Seed a pending confirmation directly in the prompter's internal map.
281
+ */
282
+ function seedPendingConfirmation(session: Session, requestId: string): void {
283
+ const prompter = session["prompter"] as unknown as {
284
+ pending: Map<
285
+ string,
286
+ {
287
+ resolve: (...args: unknown[]) => void;
288
+ reject: (...args: unknown[]) => void;
289
+ timer: ReturnType<typeof setTimeout>;
290
+ }
291
+ >;
292
+ };
293
+ prompter.pending.set(requestId, {
294
+ resolve: () => {},
295
+ reject: () => {},
296
+ timer: setTimeout(() => {}, 60_000),
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Register a pending interaction in the pending-interactions tracker with
302
+ * confirmation details.
303
+ */
304
+ function registerPendingInteraction(
305
+ session: Session,
306
+ requestId: string,
307
+ conversationId: string,
308
+ confirmationDetails?: ConfirmationDetails,
309
+ ): void {
310
+ pendingInteractions.register(requestId, {
311
+ session,
312
+ conversationId,
313
+ kind: "confirmation",
314
+ confirmationDetails,
315
+ });
316
+ }
317
+
318
+ function makeConfirmationDetails(patterns: string[]): ConfirmationDetails {
319
+ return {
320
+ toolName: "bash",
321
+ input: { command: "echo hello" },
322
+ riskLevel: "medium",
323
+ allowlistOptions: patterns.map((p) => ({
324
+ label: p,
325
+ description: `Allow ${p}`,
326
+ pattern: p,
327
+ })),
328
+ scopeOptions: [{ label: "Everywhere", scope: "everywhere" }],
329
+ };
330
+ }
331
+
332
+ afterAll(() => {
333
+ try {
334
+ rmSync(testDir, { recursive: true, force: true });
335
+ } catch {
336
+ /* best effort */
337
+ }
338
+ });
339
+
340
+ beforeEach(() => {
341
+ pendingInteractions.clear();
342
+ });
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Tests
346
+ // ---------------------------------------------------------------------------
347
+
348
+ describe("approval cascading", () => {
349
+ test("allow_10m cascades to all pending in same conversation", () => {
350
+ const emitted: ServerMessage[] = [];
351
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
352
+
353
+ // Seed 3 pending confirmations
354
+ seedPendingConfirmation(session, "req-1");
355
+ seedPendingConfirmation(session, "req-2");
356
+ seedPendingConfirmation(session, "req-3");
357
+
358
+ // Register in pending-interactions tracker
359
+ registerPendingInteraction(
360
+ session,
361
+ "req-1",
362
+ CONV_ID,
363
+ makeConfirmationDetails(["bash:echo hello"]),
364
+ );
365
+ registerPendingInteraction(
366
+ session,
367
+ "req-2",
368
+ CONV_ID,
369
+ makeConfirmationDetails(["bash:ls -la"]),
370
+ );
371
+ registerPendingInteraction(
372
+ session,
373
+ "req-3",
374
+ CONV_ID,
375
+ makeConfirmationDetails(["bash:cat file"]),
376
+ );
377
+
378
+ // Resolve the first with allow_10m
379
+ session.handleConfirmationResponse("req-1", "allow_10m");
380
+
381
+ // All 3 should be resolved (approved)
382
+ const confirmMsgs = emitted.filter(
383
+ (m) =>
384
+ m.type === "confirmation_state_changed" &&
385
+ (m as unknown as ConfirmationStateChanged).state === "approved",
386
+ ) as unknown as ConfirmationStateChanged[];
387
+
388
+ expect(confirmMsgs).toHaveLength(3);
389
+
390
+ const resolvedIds = confirmMsgs.map((m) => m.requestId).sort();
391
+ expect(resolvedIds).toEqual(["req-1", "req-2", "req-3"]);
392
+ });
393
+
394
+ test("allow_thread cascades to all pending in same conversation", () => {
395
+ const emitted: ServerMessage[] = [];
396
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
397
+
398
+ seedPendingConfirmation(session, "req-a");
399
+ seedPendingConfirmation(session, "req-b");
400
+ seedPendingConfirmation(session, "req-c");
401
+
402
+ registerPendingInteraction(
403
+ session,
404
+ "req-a",
405
+ CONV_ID,
406
+ makeConfirmationDetails(["bash:echo a"]),
407
+ );
408
+ registerPendingInteraction(
409
+ session,
410
+ "req-b",
411
+ CONV_ID,
412
+ makeConfirmationDetails(["bash:echo b"]),
413
+ );
414
+ registerPendingInteraction(
415
+ session,
416
+ "req-c",
417
+ CONV_ID,
418
+ makeConfirmationDetails(["bash:echo c"]),
419
+ );
420
+
421
+ session.handleConfirmationResponse("req-a", "allow_thread");
422
+
423
+ const confirmMsgs = emitted.filter(
424
+ (m) =>
425
+ m.type === "confirmation_state_changed" &&
426
+ (m as unknown as ConfirmationStateChanged).state === "approved",
427
+ ) as unknown as ConfirmationStateChanged[];
428
+
429
+ expect(confirmMsgs).toHaveLength(3);
430
+
431
+ const resolvedIds = confirmMsgs.map((m) => m.requestId).sort();
432
+ expect(resolvedIds).toEqual(["req-a", "req-b", "req-c"]);
433
+ });
434
+
435
+ test("temporary override does NOT cascade to different conversation", () => {
436
+ const emitted: ServerMessage[] = [];
437
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
438
+
439
+ seedPendingConfirmation(session, "req-same");
440
+ seedPendingConfirmation(session, "req-diff");
441
+
442
+ // Same conversation
443
+ registerPendingInteraction(
444
+ session,
445
+ "req-same",
446
+ CONV_ID,
447
+ makeConfirmationDetails(["bash:echo same"]),
448
+ );
449
+ // Different conversation
450
+ registerPendingInteraction(
451
+ session,
452
+ "req-diff",
453
+ "different-conv",
454
+ makeConfirmationDetails(["bash:echo diff"]),
455
+ );
456
+
457
+ // Seed a primary request
458
+ seedPendingConfirmation(session, "req-primary");
459
+ registerPendingInteraction(
460
+ session,
461
+ "req-primary",
462
+ CONV_ID,
463
+ makeConfirmationDetails(["bash:echo primary"]),
464
+ );
465
+
466
+ session.handleConfirmationResponse("req-primary", "allow_10m");
467
+
468
+ const confirmMsgs = emitted.filter(
469
+ (m) =>
470
+ m.type === "confirmation_state_changed" &&
471
+ (m as unknown as ConfirmationStateChanged).state === "approved",
472
+ ) as unknown as ConfirmationStateChanged[];
473
+
474
+ // primary + req-same should be approved, req-diff should NOT
475
+ const resolvedIds = confirmMsgs.map((m) => m.requestId).sort();
476
+ expect(resolvedIds).toContain("req-primary");
477
+ expect(resolvedIds).toContain("req-same");
478
+ expect(resolvedIds).not.toContain("req-diff");
479
+ });
480
+
481
+ test("always_allow cascades to pattern-matching pending", () => {
482
+ const emitted: ServerMessage[] = [];
483
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
484
+
485
+ // Two with matching patterns (asset_materialize:doc.pdf)
486
+ seedPendingConfirmation(session, "req-match-1");
487
+ seedPendingConfirmation(session, "req-match-2");
488
+ // One with non-overlapping pattern
489
+ seedPendingConfirmation(session, "req-nomatch");
490
+
491
+ registerPendingInteraction(
492
+ session,
493
+ "req-match-1",
494
+ CONV_ID,
495
+ makeConfirmationDetails(["asset_materialize:doc.pdf"]),
496
+ );
497
+ registerPendingInteraction(
498
+ session,
499
+ "req-match-2",
500
+ CONV_ID,
501
+ makeConfirmationDetails(["asset_materialize:report.pdf"]),
502
+ );
503
+ registerPendingInteraction(
504
+ session,
505
+ "req-nomatch",
506
+ CONV_ID,
507
+ makeConfirmationDetails(["bash:rm -rf"]),
508
+ );
509
+
510
+ // Primary request
511
+ seedPendingConfirmation(session, "req-primary");
512
+ registerPendingInteraction(
513
+ session,
514
+ "req-primary",
515
+ CONV_ID,
516
+ makeConfirmationDetails(["asset_materialize:image.png"]),
517
+ );
518
+
519
+ session.handleConfirmationResponse(
520
+ "req-primary",
521
+ "always_allow",
522
+ "asset_materialize:**",
523
+ );
524
+
525
+ const approvedMsgs = emitted.filter(
526
+ (m) =>
527
+ m.type === "confirmation_state_changed" &&
528
+ (m as unknown as ConfirmationStateChanged).state === "approved",
529
+ ) as unknown as ConfirmationStateChanged[];
530
+
531
+ const approvedIds = approvedMsgs.map((m) => m.requestId).sort();
532
+ expect(approvedIds).toContain("req-primary");
533
+ expect(approvedIds).toContain("req-match-1");
534
+ expect(approvedIds).toContain("req-match-2");
535
+ expect(approvedIds).not.toContain("req-nomatch");
536
+ });
537
+
538
+ test("always_allow does NOT cascade to high-risk pending confirmations", () => {
539
+ const emitted: ServerMessage[] = [];
540
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
541
+
542
+ // Medium-risk pending — should cascade
543
+ seedPendingConfirmation(session, "req-medium");
544
+ registerPendingInteraction(
545
+ session,
546
+ "req-medium",
547
+ CONV_ID,
548
+ makeConfirmationDetails(["asset_materialize:report.pdf"]),
549
+ );
550
+
551
+ // High-risk pending — should NOT cascade via always_allow
552
+ seedPendingConfirmation(session, "req-high");
553
+ registerPendingInteraction(session, "req-high", CONV_ID, {
554
+ toolName: "bash",
555
+ input: { command: "rm -rf /" },
556
+ riskLevel: "high",
557
+ allowlistOptions: [
558
+ {
559
+ label: "asset_materialize:dangerous.bin",
560
+ description: "Allow asset_materialize:dangerous.bin",
561
+ pattern: "asset_materialize:dangerous.bin",
562
+ },
563
+ ],
564
+ scopeOptions: [{ label: "Everywhere", scope: "everywhere" }],
565
+ });
566
+
567
+ // Primary request
568
+ seedPendingConfirmation(session, "req-primary");
569
+ registerPendingInteraction(
570
+ session,
571
+ "req-primary",
572
+ CONV_ID,
573
+ makeConfirmationDetails(["asset_materialize:image.png"]),
574
+ );
575
+
576
+ session.handleConfirmationResponse(
577
+ "req-primary",
578
+ "always_allow",
579
+ "asset_materialize:**",
580
+ );
581
+
582
+ const approvedMsgs = emitted.filter(
583
+ (m) =>
584
+ m.type === "confirmation_state_changed" &&
585
+ (m as unknown as ConfirmationStateChanged).state === "approved",
586
+ ) as unknown as ConfirmationStateChanged[];
587
+
588
+ const approvedIds = approvedMsgs.map((m) => m.requestId).sort();
589
+ expect(approvedIds).toContain("req-primary");
590
+ expect(approvedIds).toContain("req-medium");
591
+ expect(approvedIds).not.toContain("req-high");
592
+
593
+ // High-risk should still be pending (not emitted at all)
594
+ const allResolvedIds = emitted
595
+ .filter((m) => m.type === "confirmation_state_changed")
596
+ .map((m) => (m as unknown as ConfirmationStateChanged).requestId);
597
+ expect(allResolvedIds).not.toContain("req-high");
598
+ });
599
+
600
+ test("always_deny cascades deny to pattern-matching pending", () => {
601
+ const emitted: ServerMessage[] = [];
602
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
603
+
604
+ seedPendingConfirmation(session, "req-match-1");
605
+ seedPendingConfirmation(session, "req-nomatch");
606
+
607
+ registerPendingInteraction(
608
+ session,
609
+ "req-match-1",
610
+ CONV_ID,
611
+ makeConfirmationDetails(["asset_materialize:doc.pdf"]),
612
+ );
613
+ registerPendingInteraction(
614
+ session,
615
+ "req-nomatch",
616
+ CONV_ID,
617
+ makeConfirmationDetails(["bash:rm -rf"]),
618
+ );
619
+
620
+ seedPendingConfirmation(session, "req-primary");
621
+ registerPendingInteraction(
622
+ session,
623
+ "req-primary",
624
+ CONV_ID,
625
+ makeConfirmationDetails(["asset_materialize:image.png"]),
626
+ );
627
+
628
+ session.handleConfirmationResponse(
629
+ "req-primary",
630
+ "always_deny",
631
+ "asset_materialize:**",
632
+ );
633
+
634
+ const deniedMsgs = emitted.filter(
635
+ (m) =>
636
+ m.type === "confirmation_state_changed" &&
637
+ (m as unknown as ConfirmationStateChanged).state === "denied",
638
+ ) as unknown as ConfirmationStateChanged[];
639
+
640
+ const deniedIds = deniedMsgs.map((m) => m.requestId).sort();
641
+ expect(deniedIds).toContain("req-primary");
642
+ expect(deniedIds).toContain("req-match-1");
643
+ expect(deniedIds).not.toContain("req-nomatch");
644
+
645
+ // req-nomatch should still be pending (not emitted at all as approved or denied by cascade)
646
+ const allResolvedIds = emitted
647
+ .filter((m) => m.type === "confirmation_state_changed")
648
+ .map((m) => (m as unknown as ConfirmationStateChanged).requestId);
649
+ expect(allResolvedIds).not.toContain("req-nomatch");
650
+ });
651
+
652
+ test("allow (one-time) does NOT cascade", () => {
653
+ const emitted: ServerMessage[] = [];
654
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
655
+
656
+ seedPendingConfirmation(session, "req-1");
657
+ seedPendingConfirmation(session, "req-2");
658
+
659
+ registerPendingInteraction(
660
+ session,
661
+ "req-1",
662
+ CONV_ID,
663
+ makeConfirmationDetails(["bash:echo hello"]),
664
+ );
665
+ registerPendingInteraction(
666
+ session,
667
+ "req-2",
668
+ CONV_ID,
669
+ makeConfirmationDetails(["bash:echo world"]),
670
+ );
671
+
672
+ session.handleConfirmationResponse("req-1", "allow");
673
+
674
+ const confirmMsgs = emitted.filter(
675
+ (m) =>
676
+ m.type === "confirmation_state_changed" &&
677
+ (m as unknown as ConfirmationStateChanged).state === "approved",
678
+ ) as unknown as ConfirmationStateChanged[];
679
+
680
+ // Only the primary should be resolved
681
+ expect(confirmMsgs).toHaveLength(1);
682
+ expect(confirmMsgs[0].requestId).toBe("req-1");
683
+ });
684
+
685
+ test("deny (one-time) does NOT cascade", () => {
686
+ const emitted: ServerMessage[] = [];
687
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
688
+
689
+ seedPendingConfirmation(session, "req-1");
690
+ seedPendingConfirmation(session, "req-2");
691
+
692
+ registerPendingInteraction(
693
+ session,
694
+ "req-1",
695
+ CONV_ID,
696
+ makeConfirmationDetails(["bash:echo hello"]),
697
+ );
698
+ registerPendingInteraction(
699
+ session,
700
+ "req-2",
701
+ CONV_ID,
702
+ makeConfirmationDetails(["bash:echo world"]),
703
+ );
704
+
705
+ session.handleConfirmationResponse("req-1", "deny");
706
+
707
+ const confirmMsgs = emitted.filter(
708
+ (m) =>
709
+ m.type === "confirmation_state_changed" &&
710
+ (m as unknown as ConfirmationStateChanged).state === "denied",
711
+ ) as unknown as ConfirmationStateChanged[];
712
+
713
+ // Only the primary should be denied
714
+ expect(confirmMsgs).toHaveLength(1);
715
+ expect(confirmMsgs[0].requestId).toBe("req-1");
716
+ });
717
+
718
+ test("cascaded events have source 'system' and causedByRequestId", () => {
719
+ const emitted: ServerMessage[] = [];
720
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
721
+
722
+ seedPendingConfirmation(session, "req-primary");
723
+ seedPendingConfirmation(session, "req-cascaded");
724
+
725
+ registerPendingInteraction(
726
+ session,
727
+ "req-primary",
728
+ CONV_ID,
729
+ makeConfirmationDetails(["bash:echo primary"]),
730
+ );
731
+ registerPendingInteraction(
732
+ session,
733
+ "req-cascaded",
734
+ CONV_ID,
735
+ makeConfirmationDetails(["bash:echo cascaded"]),
736
+ );
737
+
738
+ session.handleConfirmationResponse("req-primary", "allow_10m");
739
+
740
+ const cascadedMsg = emitted.find(
741
+ (m) =>
742
+ m.type === "confirmation_state_changed" &&
743
+ (m as unknown as ConfirmationStateChanged).requestId === "req-cascaded",
744
+ ) as unknown as ConfirmationStateChanged;
745
+
746
+ expect(cascadedMsg).toBeDefined();
747
+ expect(cascadedMsg.source).toBe("system");
748
+ expect(cascadedMsg.causedByRequestId).toBe("req-primary");
749
+ });
750
+
751
+ test("already-resolved request handled gracefully", () => {
752
+ const emitted: ServerMessage[] = [];
753
+ const session = makeSession((msg) => emitted.push(msg), CONV_ID);
754
+
755
+ seedPendingConfirmation(session, "req-primary");
756
+ seedPendingConfirmation(session, "req-stale");
757
+
758
+ registerPendingInteraction(
759
+ session,
760
+ "req-primary",
761
+ CONV_ID,
762
+ makeConfirmationDetails(["bash:echo primary"]),
763
+ );
764
+ // Register in pending-interactions but with a request ID that exists
765
+ // in the prompter. We'll remove it from the prompter before cascading
766
+ // reaches it to simulate a stale/already-resolved request.
767
+ registerPendingInteraction(
768
+ session,
769
+ "req-stale",
770
+ CONV_ID,
771
+ makeConfirmationDetails(["bash:echo stale"]),
772
+ );
773
+
774
+ // Remove req-stale from the prompter's pending map (simulating it was
775
+ // already resolved by another path before cascade reaches it)
776
+ const prompter = session["prompter"] as unknown as {
777
+ pending: Map<string, unknown>;
778
+ };
779
+ prompter.pending.delete("req-stale");
780
+
781
+ // This should not throw — cascade should skip req-stale gracefully
782
+ expect(() => {
783
+ session.handleConfirmationResponse("req-primary", "allow_10m");
784
+ }).not.toThrow();
785
+
786
+ // Only the primary should be resolved
787
+ const confirmMsgs = emitted.filter(
788
+ (m) =>
789
+ m.type === "confirmation_state_changed" &&
790
+ (m as unknown as ConfirmationStateChanged).state === "approved",
791
+ ) as unknown as ConfirmationStateChanged[];
792
+
793
+ expect(confirmMsgs).toHaveLength(1);
794
+ expect(confirmMsgs[0].requestId).toBe("req-primary");
795
+ });
796
+ });