@vellumai/assistant 0.11.7 → 0.11.8-staging.1

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 (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -1,7 +1,7 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
- const completeSurfaceAndNotify = mock(() => {});
4
- const markSurfaceCompleted = mock(() => {});
3
+ const completeSurfaceAndNotify = mock(() => true);
4
+ const markSurfaceCompleted = mock(() => true);
5
5
  mock.module("../daemon/conversation-surfaces.js", () => ({
6
6
  completeSurfaceAndNotify,
7
7
  markSurfaceCompleted,
@@ -20,7 +20,7 @@ mock.module("../messaging/providers/discord/withdraw.js", () => ({
20
20
  }));
21
21
 
22
22
  const withdrawTelegramApprovalCard = mock(
23
- async (_params: Record<string, unknown>) => {},
23
+ async (_params: Record<string, unknown>) => ({ complete: true }),
24
24
  );
25
25
  mock.module("../messaging/providers/telegram-bot/withdraw.js", () => ({
26
26
  withdrawTelegramApprovalCard,
@@ -355,12 +355,104 @@ describe("withdrawGuardianRequestCards", () => {
355
355
  status: "approved",
356
356
  originChannel: "telegram",
357
357
  }),
358
- ).resolves.toBeUndefined();
358
+ ).resolves.toEqual({ complete: false });
359
359
 
360
360
  // The in-app card was still withdrawn despite the Slack failure.
361
361
  expect(completeSurfaceAndNotify).toHaveBeenCalledTimes(1);
362
362
  });
363
363
 
364
+ test("completes the in-app projection of a channel card's paired conversation", async () => {
365
+ // A channel delivery is paired with a conversation whose in-app rendering
366
+ // carries the same actionable card; the channel edit alone leaves that
367
+ // projection clickable after the decision (LUM-3489).
368
+ const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
369
+ const slack = bridgeState.seedDelivery({
370
+ requestId: req.id,
371
+ destinationChannel: "slack",
372
+ destinationChatId: "C1",
373
+ destinationMessageId: "1.0",
374
+ destinationConversationId: "conv-dm",
375
+ });
376
+
377
+ await withdrawGuardianRequestCards({
378
+ request: req,
379
+ status: "approved",
380
+ originChannel: "slack",
381
+ });
382
+
383
+ expect(withdrawSlackApprovalCard).toHaveBeenCalledTimes(1);
384
+ expect(completeSurfaceAndNotify).toHaveBeenCalledWith(
385
+ "conv-dm",
386
+ `tool-approval-${req.id}`,
387
+ "Approved",
388
+ );
389
+ const byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
390
+ expect(byId.get(slack.id)).toBe("withdrawn");
391
+ });
392
+
393
+ test("suppresses only the acting conversation's broadcast for an in-app decision", async () => {
394
+ // Same request, two in-app projections: the vellum card and a channel
395
+ // card's paired conversation. The acting client's optimistic completion
396
+ // covers only the conversation it acted in; the sibling still needs its
397
+ // live ui_surface_complete.
398
+ const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
399
+ bridgeState.seedDelivery({
400
+ requestId: req.id,
401
+ destinationChannel: "vellum",
402
+ destinationConversationId: "conv-acting",
403
+ });
404
+ bridgeState.seedDelivery({
405
+ requestId: req.id,
406
+ destinationChannel: "slack",
407
+ destinationChatId: "C1",
408
+ destinationMessageId: "1.0",
409
+ destinationConversationId: "conv-sibling",
410
+ });
411
+
412
+ await withdrawGuardianRequestCards({
413
+ request: req,
414
+ status: "approved",
415
+ originChannel: "vellum",
416
+ originConversationId: "conv-acting",
417
+ });
418
+
419
+ expect(markSurfaceCompleted).toHaveBeenCalledWith(
420
+ { conversationId: "conv-acting" },
421
+ `tool-approval-${req.id}`,
422
+ "Approved",
423
+ );
424
+ expect(completeSurfaceAndNotify).toHaveBeenCalledWith(
425
+ "conv-sibling",
426
+ `tool-approval-${req.id}`,
427
+ "Approved",
428
+ );
429
+ expect(completeSurfaceAndNotify).toHaveBeenCalledTimes(1);
430
+ });
431
+
432
+ test("a failed in-app persist on a channel-paired projection holds the receipt back", async () => {
433
+ const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
434
+ const slack = bridgeState.seedDelivery({
435
+ requestId: req.id,
436
+ destinationChannel: "slack",
437
+ destinationChatId: "C1",
438
+ destinationMessageId: "1.0",
439
+ destinationConversationId: "conv-dm",
440
+ });
441
+ completeSurfaceAndNotify.mockReturnValueOnce(false);
442
+
443
+ const result = await withdrawGuardianRequestCards({
444
+ request: req,
445
+ status: "approved",
446
+ originChannel: "telegram",
447
+ });
448
+
449
+ // The Slack edit landed, but the paired conversation's block would revert
450
+ // to a clickable card on reload, so the surface stays unreceipted.
451
+ expect(result.complete).toBe(false);
452
+ const byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
453
+ expect(byId.get(slack.id)).not.toBe("withdrawn");
454
+ });
455
+
364
456
  test("tool-approval cards resolve to the tool-approval surface id", async () => {
365
457
  const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
366
458
  bridgeState.seedDelivery({
@@ -438,8 +530,131 @@ describe("withdrawGuardianRequestCards", () => {
438
530
  "Denied",
439
531
  );
440
532
  });
441
- });
442
533
 
534
+ test("a Telegram keyboard that stayed live holds the completeness back", async () => {
535
+ // The nested boolean is the whole receipt: clearInlineKeyboard already
536
+ // reports the failed edit, and dropping it once made the sweep confirm
537
+ // an expiry while the card's buttons stayed actionable.
538
+ const req = makeRequest({ sourceChannel: "telegram" });
539
+ bridgeState.seedDelivery({
540
+ requestId: req.id,
541
+ destinationChannel: "telegram",
542
+ destinationChatId: "T1",
543
+ destinationMessageId: "9",
544
+ });
545
+ withdrawTelegramApprovalCard.mockResolvedValueOnce({ complete: false });
546
+
547
+ const result = await withdrawGuardianRequestCards({
548
+ request: req,
549
+ status: "expired",
550
+ });
551
+
552
+ expect(result.complete).toBe(false);
553
+ });
554
+
555
+ test("a failed in-app persistence write holds the completeness back", async () => {
556
+ // markSurfaceCompleted's false means the persisted block reverts to a
557
+ // pending, clickable card on the next reload; the broadcast still goes
558
+ // out, but the caller's receipt must wait for a durable write.
559
+ const req = makeRequest();
560
+ bridgeState.seedDelivery({
561
+ requestId: req.id,
562
+ destinationChannel: "vellum",
563
+ destinationConversationId: "conv-1",
564
+ });
565
+ completeSurfaceAndNotify.mockReturnValueOnce(false);
566
+
567
+ const result = await withdrawGuardianRequestCards({
568
+ request: req,
569
+ status: "expired",
570
+ });
571
+
572
+ expect(result.complete).toBe(false);
573
+ });
574
+
575
+ test("a surface that succeeded on retry reports complete", async () => {
576
+ const req = makeRequest({ sourceChannel: "telegram" });
577
+ bridgeState.seedDelivery({
578
+ requestId: req.id,
579
+ destinationChannel: "telegram",
580
+ destinationChatId: "T1",
581
+ destinationMessageId: "9",
582
+ });
583
+
584
+ const result = await withdrawGuardianRequestCards({
585
+ request: req,
586
+ status: "expired",
587
+ });
588
+
589
+ expect(result.complete).toBe(true);
590
+ });
591
+
592
+ test("each durably withdrawn surface marks its delivery row as the receipt", async () => {
593
+ const req = makeRequest();
594
+ const slack = bridgeState.seedDelivery({
595
+ requestId: req.id,
596
+ destinationChannel: "slack",
597
+ destinationChatId: "C1",
598
+ destinationMessageId: "1.0",
599
+ });
600
+ const vellum = bridgeState.seedDelivery({
601
+ requestId: req.id,
602
+ destinationChannel: "vellum",
603
+ destinationConversationId: "conv-1",
604
+ });
605
+
606
+ const result = await withdrawGuardianRequestCards({
607
+ request: req,
608
+ status: "expired",
609
+ });
610
+
611
+ expect(result.complete).toBe(true);
612
+ const byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
613
+ expect(byId.get(slack.id)).toBe("withdrawn");
614
+ expect(byId.get(vellum.id)).toBe("withdrawn");
615
+ });
616
+
617
+ test("a retry skips rows already withdrawn: no re-edits, no re-broadcasts", async () => {
618
+ // The delivery row is the per-surface receipt. When one surface fails,
619
+ // the next round must retry only that surface; re-editing the others
620
+ // would re-broadcast in-app completions and re-run channel edits that
621
+ // already landed.
622
+ const req = makeRequest({ sourceChannel: "telegram" });
623
+ const slack = bridgeState.seedDelivery({
624
+ requestId: req.id,
625
+ destinationChannel: "slack",
626
+ destinationChatId: "C1",
627
+ destinationMessageId: "1.0",
628
+ });
629
+ const telegram = bridgeState.seedDelivery({
630
+ requestId: req.id,
631
+ destinationChannel: "telegram",
632
+ destinationChatId: "T1",
633
+ destinationMessageId: "9",
634
+ });
635
+ withdrawTelegramApprovalCard.mockResolvedValueOnce({ complete: false });
636
+
637
+ const first = await withdrawGuardianRequestCards({
638
+ request: req,
639
+ status: "expired",
640
+ });
641
+ expect(first.complete).toBe(false);
642
+ let byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
643
+ expect(byId.get(slack.id)).toBe("withdrawn");
644
+ expect(byId.get(telegram.id)).not.toBe("withdrawn");
645
+
646
+ const second = await withdrawGuardianRequestCards({
647
+ request: req,
648
+ status: "expired",
649
+ });
650
+ expect(second.complete).toBe(true);
651
+ byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
652
+ expect(byId.get(telegram.id)).toBe("withdrawn");
653
+ // The already-receipted Slack surface was not re-edited.
654
+ expect(withdrawSlackApprovalCard).toHaveBeenCalledTimes(1);
655
+ expect(withdrawTelegramApprovalCard).toHaveBeenCalledTimes(2);
656
+ });
657
+ });
443
658
  describe("recordApprovalCardDelivery", () => {
444
659
  beforeEach(() => {
445
660
  bridgeState.reset();
@@ -582,6 +797,85 @@ describe("recordGuardianRequestDeliveries", () => {
582
797
  expect(delivery.destinationChatId).toBeNull();
583
798
  });
584
799
 
800
+ test("re-runs withdrawal when the request resolved before its rows were recorded", async () => {
801
+ // Recording happens after the notification pipeline settles, so a
802
+ // guardian acting the moment the card lands can resolve the request
803
+ // before any delivery row exists; that decision's withdrawal pass finds
804
+ // nothing. The recorder reconciles by withdrawing what it just recorded.
805
+ const req = makeRequest({
806
+ kind: "tool_approval",
807
+ toolName: "shell",
808
+ status: "approved",
809
+ });
810
+
811
+ await recordGuardianRequestDeliveries({
812
+ requestId: req.id,
813
+ deliveryResults: [
814
+ {
815
+ channel: "slack",
816
+ destination: "C999",
817
+ status: "sent",
818
+ messageId: "1700000000.1234",
819
+ conversationId: "conv-dm",
820
+ },
821
+ ],
822
+ });
823
+
824
+ expect(withdrawSlackApprovalCard).toHaveBeenCalledTimes(1);
825
+ const [delivery] = deliveriesFor(req.id);
826
+ expect(delivery.status).toBe("withdrawn");
827
+ });
828
+
829
+ test("leaves rows of a still-pending request alone after recording", async () => {
830
+ const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
831
+ await recordGuardianRequestDeliveries({
832
+ requestId: req.id,
833
+ deliveryResults: [
834
+ {
835
+ channel: "slack",
836
+ destination: "C999",
837
+ status: "sent",
838
+ messageId: "1700000000.1234",
839
+ },
840
+ ],
841
+ });
842
+
843
+ expect(withdrawSlackApprovalCard).not.toHaveBeenCalled();
844
+ const [delivery] = deliveriesFor(req.id);
845
+ expect(delivery.status).toBe("sent");
846
+ });
847
+
848
+ test("a late status patch cannot resurrect a withdrawn vellum row", async () => {
849
+ // The pre-created vellum row can be withdrawn by a fast decision while
850
+ // the broadcast is still settling; the recorder's sent patch must not
851
+ // overwrite that receipt (mirrors the gateway store's sticky rule).
852
+ const req = makeRequest({ status: "approved" });
853
+ const pre = await recordApprovalCardDelivery({
854
+ requestId: req.id,
855
+ channel: "vellum",
856
+ conversationId: "conv-1",
857
+ });
858
+ await bridgeState.module.updateGuardianRequestDelivery(pre!.id, {
859
+ status: "withdrawn",
860
+ });
861
+
862
+ await recordGuardianRequestDeliveries({
863
+ requestId: req.id,
864
+ deliveryResults: [
865
+ {
866
+ channel: "vellum",
867
+ destination: "",
868
+ status: "sent",
869
+ conversationId: "conv-1",
870
+ },
871
+ ],
872
+ vellumDeliveryId: pre?.id,
873
+ });
874
+
875
+ const [delivery] = deliveriesFor(req.id);
876
+ expect(delivery.status).toBe("withdrawn");
877
+ });
878
+
585
879
  test("records a Slack delivery the withdrawal path can then edit in place", async () => {
586
880
  const req = makeRequest();
587
881
  await recordGuardianRequestDeliveries({
@@ -38,35 +38,49 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
38
38
  },
39
39
  }));
40
40
 
41
- // The sweep withdraws cards via this module; we only assert it is invoked, and
42
- // mocking it keeps the sweep import light (no surface/slack transitive deps).
41
+ // The sweep withdraws cards via this module; mocking it keeps the sweep
42
+ // import light (no surface/slack transitive deps). The completeness result
43
+ // gates the sweep's expire confirmation.
43
44
  let withdrawCalls = 0;
45
+ let withdrawIncomplete = false;
44
46
  mock.module("../approvals/guardian-card-withdrawal.js", () => ({
45
47
  withdrawGuardianRequestCards: async () => {
46
48
  withdrawCalls++;
49
+ return { complete: !withdrawIncomplete };
47
50
  },
48
51
  }));
49
52
 
50
- // Gateway guardian-request client — in-memory rows driven by tests. The sweep
51
- // asks the gateway to CAS-expire past-deadline pending rows and fans out
52
- // notifications from the returned rows.
53
+ // Gateway guardian-request client, in-memory rows driven by tests. The
54
+ // sweep lists past-deadline pending rows read-only and confirms each with
55
+ // the per-request expire CAS after its side effects run.
53
56
  const gatewayRequests = new Map<string, GuardianRequestWire>();
57
+ let expireCasFails = false;
58
+ const actualGatewayGuardianRequests =
59
+ await import("../channels/gateway-guardian-requests.js");
54
60
  mock.module("../channels/gateway-guardian-requests.js", () => ({
55
- sweepExpiredGuardianRequests: async () => {
61
+ ...actualGatewayGuardianRequests,
62
+ listExpiredPendingGuardianRequests: async () => {
56
63
  const now = Date.now();
57
- const expired: GuardianRequestWire[] = [];
64
+ const stale: GuardianRequestWire[] = [];
58
65
  for (const row of gatewayRequests.values()) {
59
66
  if (
60
67
  row.status === "pending" &&
61
68
  row.expiresAt !== null &&
62
69
  row.expiresAt <= now
63
70
  ) {
64
- const flipped = { ...row, status: "expired" as const };
65
- gatewayRequests.set(row.id, flipped);
66
- expired.push(flipped);
71
+ stale.push(row);
67
72
  }
68
73
  }
69
- return expired;
74
+ return stale;
75
+ },
76
+ expireGuardianRequest: async (id: string) => {
77
+ if (expireCasFails) {
78
+ throw new Error("simulated IPC timeout");
79
+ }
80
+ const row = gatewayRequests.get(id);
81
+ if (row?.status === "pending") {
82
+ gatewayRequests.set(id, { ...row, status: "expired" as const });
83
+ }
70
84
  },
71
85
  }));
72
86
 
@@ -117,6 +131,9 @@ beforeEach(() => {
117
131
  broadcasts.length = 0;
118
132
  deliveryError = null;
119
133
  withdrawCalls = 0;
134
+ withdrawIncomplete = false;
135
+ expireCasFails = false;
136
+ gatewayRequests.clear();
120
137
  pendingInteractions.clear();
121
138
  });
122
139
 
@@ -229,7 +246,7 @@ describe("notifyExpiredGuardianRequest", () => {
229
246
  expect(deliveredReplies).toHaveLength(0);
230
247
  });
231
248
 
232
- test("delivery failure is swallowed (best-effort)", async () => {
249
+ test("delivery failure never throws, and reports incomplete", async () => {
233
250
  deliveryError = new Error("gateway down");
234
251
 
235
252
  await expect(
@@ -240,7 +257,7 @@ describe("notifyExpiredGuardianRequest", () => {
240
257
  requesterChatId: "tg-chat",
241
258
  }),
242
259
  ),
243
- ).resolves.toBeUndefined();
260
+ ).resolves.toEqual({ complete: false });
244
261
  });
245
262
  });
246
263
 
@@ -291,4 +308,91 @@ describe("sweep integration", () => {
291
308
  expect(gatewayRequests.get("req-fresh")?.status).toBe("pending");
292
309
  expect(deliveredReplies).toHaveLength(0);
293
310
  });
311
+
312
+ test("an incomplete withdrawal defers the request and holds the notice back", async () => {
313
+ // A card edit that failed leaves an actionable control somewhere, so the
314
+ // request must stay pending and retryable. The notice is held back too:
315
+ // sending it only after withdrawal completes means the retry round can
316
+ // never duplicate a delivered notice.
317
+ gatewayRequests.set(
318
+ "req-withdraw-fail",
319
+ makeRequest({
320
+ id: "req-withdraw-fail",
321
+ kind: "access_request",
322
+ status: "pending",
323
+ requesterChatId: "tg-chat",
324
+ requesterExternalUserId: "tg-user",
325
+ expiresAt: Date.now() - 1000,
326
+ }),
327
+ );
328
+
329
+ withdrawIncomplete = true;
330
+ expect(await runGuardianExpirySweep()).toBe(0);
331
+ expect(gatewayRequests.get("req-withdraw-fail")?.status).toBe("pending");
332
+ expect(deliveredReplies).toHaveLength(0);
333
+
334
+ withdrawIncomplete = false;
335
+ expect(await runGuardianExpirySweep()).toBe(1);
336
+ expect(gatewayRequests.get("req-withdraw-fail")?.status).toBe("expired");
337
+ expect(deliveredReplies).toHaveLength(1);
338
+ });
339
+
340
+ test("a failed requester notice defers the request until a retry delivers it", async () => {
341
+ gatewayRequests.set(
342
+ "req-notice-fail",
343
+ makeRequest({
344
+ id: "req-notice-fail",
345
+ kind: "access_request",
346
+ status: "pending",
347
+ requesterChatId: "tg-chat",
348
+ requesterExternalUserId: "tg-user",
349
+ expiresAt: Date.now() - 1000,
350
+ }),
351
+ );
352
+
353
+ deliveryError = new Error("channel briefly unreachable");
354
+ expect(await runGuardianExpirySweep()).toBe(0);
355
+ expect(gatewayRequests.get("req-notice-fail")?.status).toBe("pending");
356
+ expect(deliveredReplies).toHaveLength(0);
357
+
358
+ deliveryError = null;
359
+ expect(await runGuardianExpirySweep()).toBe(1);
360
+ expect(gatewayRequests.get("req-notice-fail")?.status).toBe("expired");
361
+ // The notice lands exactly once: the failed attempt delivered nothing.
362
+ expect(deliveredReplies).toHaveLength(1);
363
+ });
364
+
365
+ test("a lost expire confirmation leaves the row pending, and the next round recovers it", async () => {
366
+ // The status flip is the receipt that side effects ran, not the
367
+ // announcement that they should. When the confirmation is lost, the row
368
+ // must stay in the pending set so the next round re-runs its fan-out;
369
+ // the cost is at-least-once side effects, never their silent loss.
370
+ gatewayRequests.set(
371
+ "req-lost",
372
+ makeRequest({
373
+ id: "req-lost",
374
+ kind: "access_request",
375
+ status: "pending",
376
+ requesterChatId: "tg-chat",
377
+ requesterExternalUserId: "tg-user",
378
+ expiresAt: Date.now() - 1000,
379
+ }),
380
+ );
381
+
382
+ expireCasFails = true;
383
+ expect(await runGuardianExpirySweep()).toBe(0);
384
+ expect(gatewayRequests.get("req-lost")?.status).toBe("pending");
385
+ expect(withdrawCalls).toBe(1);
386
+ expect(deliveredReplies).toHaveLength(1);
387
+
388
+ expireCasFails = false;
389
+ expect(await runGuardianExpirySweep()).toBe(1);
390
+ expect(gatewayRequests.get("req-lost")?.status).toBe("expired");
391
+ // The re-run repeats the side effects (at-least-once); the third round
392
+ // finds nothing.
393
+ expect(withdrawCalls).toBe(2);
394
+ expect(deliveredReplies).toHaveLength(2);
395
+ expect(await runGuardianExpirySweep()).toBe(0);
396
+ expect(withdrawCalls).toBe(2);
397
+ });
294
398
  });
@@ -0,0 +1,119 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { createGuardianGatewaySim } from "./guardian-gateway-sim.js";
4
+
5
+ import "./test-preload.js";
6
+
7
+ /**
8
+ * Pins the simulator to the gateway's authorization-lifecycle contract.
9
+ * Fifteen assistant suites prove behavior against this sim, so a sim whose
10
+ * semantics drift from the gateway's lets them pass against a production
11
+ * shape that does not exist.
12
+ */
13
+ describe("guardian gateway sim: boot expiry contract", () => {
14
+ test("expires only interaction-bound kinds, never persistent kinds", async () => {
15
+ const sim = createGuardianGatewaySim();
16
+ const past = Date.now() - 60_000;
17
+ sim.seedRequest({ id: "ta", kind: "tool_approval", status: "pending" });
18
+ sim.seedRequest({
19
+ id: "pq",
20
+ kind: "pending_question",
21
+ status: "pending",
22
+ });
23
+ // Past-deadline persistent rows stay pending for the sweep, which owns
24
+ // the card-withdrawal and requester-notice fan-out; boot must not
25
+ // pre-empt it (the gateway contract this sim mirrors).
26
+ sim.seedRequest({
27
+ id: "ar",
28
+ kind: "access_request",
29
+ status: "pending",
30
+ expiresAt: past,
31
+ });
32
+ sim.seedRequest({
33
+ id: "tg",
34
+ kind: "tool_grant_request",
35
+ status: "pending",
36
+ expiresAt: past,
37
+ });
38
+
39
+ const expired = await sim.module.expireInteractionBoundGuardianRequests();
40
+
41
+ expect(expired).toBe(2);
42
+ expect(sim.getRequest("ta")?.status).toBe("expired");
43
+ expect(sim.getRequest("pq")?.status).toBe("expired");
44
+ expect(sim.getRequest("ar")?.status).toBe("pending");
45
+ expect(sim.getRequest("tg")?.status).toBe("pending");
46
+ });
47
+ });
48
+
49
+ describe("guardian gateway sim: decision arbitration contract", () => {
50
+ test("a decision past the deadline loses atomically", async () => {
51
+ const sim = createGuardianGatewaySim();
52
+ sim.seedRequest({
53
+ id: "late",
54
+ kind: "access_request",
55
+ status: "pending",
56
+ expiresAt: Date.now() - 1000,
57
+ });
58
+
59
+ const result = await sim.module.decideGuardianRequest({
60
+ id: "late",
61
+ expectedStatus: "pending",
62
+ status: "approved",
63
+ });
64
+
65
+ expect(result.applied).toBe(false);
66
+ expect(sim.getRequest("late")?.status).toBe("pending");
67
+ });
68
+
69
+ test("a no-op expire never restamps a decided request's deliveries", async () => {
70
+ const sim = createGuardianGatewaySim();
71
+ sim.seedRequest({ id: "done", kind: "access_request", status: "pending" });
72
+ sim.seedDelivery({
73
+ id: "d1",
74
+ requestId: "done",
75
+ destinationChannel: "telegram",
76
+ destinationChatId: "chat-1",
77
+ status: "sent",
78
+ });
79
+ await sim.module.decideGuardianRequest({
80
+ id: "done",
81
+ expectedStatus: "pending",
82
+ status: "approved",
83
+ });
84
+
85
+ await sim.module.expireGuardianRequest("done");
86
+
87
+ expect(sim.getRequest("done")?.status).toBe("approved");
88
+ expect(sim.deliveries.find((d) => d.id === "d1")?.status).toBe("sent");
89
+ });
90
+
91
+ test("a withdrawn delivery keeps its receipt through the expire flip", async () => {
92
+ const sim = createGuardianGatewaySim();
93
+ sim.seedRequest({ id: "exp", kind: "access_request", status: "pending" });
94
+ sim.seedDelivery({
95
+ id: "kept",
96
+ requestId: "exp",
97
+ destinationChannel: "telegram",
98
+ destinationChatId: "chat-1",
99
+ status: "withdrawn",
100
+ });
101
+ sim.seedDelivery({
102
+ id: "flipped",
103
+ requestId: "exp",
104
+ destinationChannel: "vellum",
105
+ destinationConversationId: "conv-1",
106
+ status: "sent",
107
+ });
108
+
109
+ await sim.module.expireGuardianRequest("exp");
110
+
111
+ expect(sim.getRequest("exp")?.status).toBe("expired");
112
+ expect(sim.deliveries.find((d) => d.id === "kept")?.status).toBe(
113
+ "withdrawn",
114
+ );
115
+ expect(sim.deliveries.find((d) => d.id === "flipped")?.status).toBe(
116
+ "expired",
117
+ );
118
+ });
119
+ });