@vellumai/assistant 0.11.8 → 0.11.9-staging.2

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 (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -8,6 +8,7 @@
8
8
  * stays bound to the oldest upload.
9
9
  */
10
10
  import { readFileSync } from "node:fs";
11
+ import { basename } from "node:path";
11
12
  import { beforeEach, describe, expect, test } from "bun:test";
12
13
 
13
14
  import { reinjectAttachmentPathAnnotations } from "../daemon/conversation-lifecycle.js";
@@ -119,6 +120,85 @@ describe("persistQueuedMessageBody stored path annotations", () => {
119
120
  expect(rebuiltBlock.text).toBe(annotation.text);
120
121
  });
121
122
 
123
+ test("offloaded oversized text records its stored path for history reload", async () => {
124
+ const conv = createConversation();
125
+ const ctx = makeCtx(conv.id);
126
+ const original = "x".repeat(8_000_001);
127
+
128
+ await persistQueuedMessageBody(ctx, { content: original });
129
+
130
+ const row = rawGet<{ metadata: string; content: string }>(
131
+ "test:offloadMessage",
132
+ "SELECT metadata, content FROM messages ORDER BY created_at DESC LIMIT 1",
133
+ );
134
+ const meta = JSON.parse(row!.metadata) as {
135
+ attachmentStoredPaths?: Record<string, string>;
136
+ };
137
+ const storedPaths = Object.values(meta.attachmentStoredPaths ?? {});
138
+ expect(storedPaths).toHaveLength(1);
139
+ const filename = basename(storedPaths[0]!);
140
+ expect(filename.startsWith("oversized-content-")).toBe(true);
141
+ expect(filename.endsWith(".txt")).toBe(true);
142
+ expect(readFileSync(storedPaths[0]!).toString("utf8")).toBe(original);
143
+
144
+ const annotation = lastAnnotationBlock(ctx);
145
+ expect(annotation.text).toBe(
146
+ `[Attachment "${filename}" is stored at: ${storedPaths[0]}]`,
147
+ );
148
+
149
+ const persisted = JSON.parse(row!.content) as ContentBlock[];
150
+ const rebuilt = reinjectAttachmentPathAnnotations(
151
+ persisted,
152
+ "user",
153
+ row!.metadata,
154
+ );
155
+ const rebuiltBlock = rebuilt.at(-1) as { type: "text"; text: string };
156
+ expect(rebuiltBlock.text).toBe(annotation.text);
157
+ });
158
+
159
+ test("two oversized messages in one conversation get distinct filenames", async () => {
160
+ const conv = createConversation();
161
+ const ctx = makeCtx(conv.id);
162
+
163
+ await persistQueuedMessageBody(ctx, { content: "a".repeat(8_000_001) });
164
+ await persistQueuedMessageBody(ctx, { content: "b".repeat(8_000_001) });
165
+
166
+ const first = (ctx.messages[0].content as ContentBlock[]).at(-1) as {
167
+ text: string;
168
+ };
169
+ const second = (ctx.messages[1].content as ContentBlock[]).at(-1) as {
170
+ text: string;
171
+ };
172
+ expect(first.text).toContain("oversized-content-");
173
+ expect(second.text).toContain("oversized-content-");
174
+ expect(first.text).not.toBe(second.text);
175
+ });
176
+
177
+ test("display-only offload is not named on the model-facing list", async () => {
178
+ const conv = createConversation();
179
+ const ctx = makeCtx(conv.id);
180
+ const display = "y".repeat(8_000_001);
181
+
182
+ await persistQueuedMessageBody(ctx, {
183
+ content: "fenced model copy",
184
+ displayContent: display,
185
+ });
186
+
187
+ const llmContent = ctx.messages[0].content as ContentBlock[];
188
+ expect(JSON.stringify(llmContent)).not.toContain("oversized-content-");
189
+ expect(JSON.stringify(llmContent)).toContain("fenced model copy");
190
+ expect(JSON.stringify(llmContent)).not.toContain(display);
191
+
192
+ const row = rawGet<{ metadata: string }>(
193
+ "test:displayOnlyOffloadMetadata",
194
+ "SELECT metadata FROM messages ORDER BY created_at DESC LIMIT 1",
195
+ );
196
+ const meta = JSON.parse(row!.metadata) as {
197
+ attachmentStoredPaths?: Record<string, string>;
198
+ };
199
+ expect(meta.attachmentStoredPaths).toBeUndefined();
200
+ });
201
+
122
202
  test("messages without attachments get no annotation block", async () => {
123
203
  const conv = createConversation();
124
204
  const ctx = makeCtx(conv.id);
@@ -2,6 +2,10 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { beforeEach, describe, expect, test } from "bun:test";
4
4
 
5
+ import {
6
+ offloadOversizedText,
7
+ OVERSIZED_CONTENT_FILENAME_PREFIX,
8
+ } from "../daemon/port-oversized-content.js";
5
9
  import {
6
10
  attachInlineAttachmentToMessage,
7
11
  AttachmentUploadError,
@@ -456,6 +460,29 @@ describe("createInlineAttachment (workspace_ref persistence)", () => {
456
460
  stored.id,
457
461
  ]);
458
462
  });
463
+
464
+ test("offloadOversizedText writes the original bytes next to the conversation", async () => {
465
+ const conv = createConversation();
466
+ const original = "x".repeat(32);
467
+ const result = await offloadOversizedText(
468
+ original,
469
+ {
470
+ conversationId: conv.id,
471
+ conversationCreatedAt: conv.createdAt,
472
+ },
473
+ 16,
474
+ );
475
+
476
+ expect(result.attachmentId).toBeDefined();
477
+ const filePath = getFilePathForAttachment(result.attachmentId!);
478
+ expect(filePath).toBeTruthy();
479
+ expect(result.filename).toBeDefined();
480
+ expect(filePath!.endsWith(result.filename!)).toBe(true);
481
+ expect(result.filename!.startsWith(`${OVERSIZED_CONTENT_FILENAME_PREFIX}-`)).toBe(
482
+ true,
483
+ );
484
+ expect(readFileSync(filePath!).toString("utf8")).toBe(original);
485
+ });
459
486
  });
460
487
 
461
488
  describe("attachInlineAttachmentToMessage filename collisions", () => {
@@ -127,6 +127,32 @@ describe("attachmentsToReferenceBlocks", () => {
127
127
  expect(JSON.stringify(blocks)).not.toContain("data");
128
128
  });
129
129
 
130
+ test("omits extracted_text on a video workspace_ref", () => {
131
+ const blocks = attachmentsToReferenceBlocks([
132
+ {
133
+ attachmentId: "att-vid",
134
+ filename: "clip.mp4",
135
+ mimeType: "video/mp4",
136
+ sizeBytes: 40_000_000,
137
+ extractedText: "this must not dump into the prompt",
138
+ },
139
+ ]);
140
+
141
+ expect(blocks).toEqual([
142
+ {
143
+ type: "file",
144
+ source: {
145
+ type: "workspace_ref",
146
+ media_type: "video/mp4",
147
+ attachmentId: "att-vid",
148
+ sizeBytes: 40_000_000,
149
+ filename: "clip.mp4",
150
+ },
151
+ },
152
+ ]);
153
+ expect(JSON.stringify(blocks)).not.toContain("extracted");
154
+ });
155
+
130
156
  test("builds a file workspace_ref with filename and extracted text", () => {
131
157
  const blocks = attachmentsToReferenceBlocks([
132
158
  {
@@ -208,6 +234,23 @@ describe("attachmentsToContentBlocks", () => {
208
234
  expect(blocks[0].type).toBe("image");
209
235
  });
210
236
 
237
+ test("omits extracted_text on an inline video file block", async () => {
238
+ const blocks = await attachmentsToContentBlocks([
239
+ {
240
+ filename: "clip.mp4",
241
+ mimeType: "video/mp4",
242
+ data: "AAAA",
243
+ extractedText: "must not dump",
244
+ },
245
+ ]);
246
+ expect(blocks).toHaveLength(1);
247
+ expect(blocks[0]).toMatchObject({
248
+ type: "file",
249
+ source: { type: "base64", media_type: "video/mp4", filename: "clip.mp4" },
250
+ });
251
+ expect(blocks[0]).not.toHaveProperty("extracted_text");
252
+ });
253
+
211
254
  test("creates file content block for non-image mime types", async () => {
212
255
  const blocks = await attachmentsToContentBlocks([
213
256
  { filename: "doc.pdf", mimeType: "application/pdf", data: "pdfdata" },
@@ -5,21 +5,35 @@
5
5
  * turns get: no agent loop runs, so a throw escaping the burst would latch the
6
6
  * conversation "processing" for the life of the daemon and queue every later
7
7
  * send behind an idle conversation.
8
+ *
9
+ * It releases the claim its scheduler took, and only that one: the timer fires
10
+ * a tick after the route returned, so a Stop and a fresh acquire can land in
11
+ * between, and clearing there would free a turn that is running.
8
12
  */
9
13
  import { describe, expect, test } from "bun:test";
10
14
 
11
15
  import type { QueueDrainReason } from "../daemon/conversation-queue-manager.js";
12
16
  import { scheduleCannedReplyRelease } from "../runtime/routes/canned-reply-release.js";
13
17
 
14
- function makeConversation() {
18
+ function makeConversation(owner = 1) {
15
19
  let processing = true;
20
+ let holder = owner;
16
21
  const drainOrigins: string[] = [];
17
22
  return {
18
23
  isProcessing: () => processing,
19
24
  drainOrigins,
25
+ /** The force-clear-then-reacquire a Stop and a new request perform. */
26
+ claimAway: (nextOwner: number) => {
27
+ processing = true;
28
+ holder = nextOwner;
29
+ },
20
30
  target: {
21
- setProcessing: (value: boolean) => {
22
- processing = value;
31
+ releaseProcessing: (claim: number) => {
32
+ if (claim !== holder) {
33
+ return false;
34
+ }
35
+ processing = false;
36
+ return true;
23
37
  },
24
38
  kickDrainQueue: (_reason?: QueueDrainReason, origin?: string) => {
25
39
  drainOrigins.push(origin ?? "");
@@ -41,12 +55,13 @@ describe("scheduleCannedReplyRelease", () => {
41
55
 
42
56
  scheduleCannedReplyRelease({
43
57
  conversation: {
44
- setProcessing: (value: boolean) => {
58
+ releaseProcessing: (claim: number) => {
45
59
  order.push("release");
46
- conversation.target.setProcessing(value);
60
+ return conversation.target.releaseProcessing(claim);
47
61
  },
48
62
  kickDrainQueue: conversation.target.kickDrainQueue,
49
63
  },
64
+ owner: 1,
50
65
  origin: "canned_greeting",
51
66
  emit: () => {
52
67
  order.push("emit");
@@ -72,6 +87,7 @@ describe("scheduleCannedReplyRelease", () => {
72
87
 
73
88
  scheduleCannedReplyRelease({
74
89
  conversation: conversation.target,
90
+ owner: 1,
75
91
  origin: "slash_command",
76
92
  emit: () => {
77
93
  throw new Error("broadcast exploded");
@@ -83,4 +99,31 @@ describe("scheduleCannedReplyRelease", () => {
83
99
  expect(conversation.isProcessing()).toBe(false);
84
100
  expect(conversation.drainOrigins).toEqual(["slash_command"]);
85
101
  });
102
+
103
+ test("leaves a hold claimed away since it was scheduled alone", async () => {
104
+ // Stop force-clears the flag and the next request acquires, all before
105
+ // this timer runs. Clearing here would release a turn that is running,
106
+ // and kicking the drain would start the next message inside it.
107
+ const conversation = makeConversation();
108
+ const after: string[] = [];
109
+
110
+ scheduleCannedReplyRelease({
111
+ conversation: conversation.target,
112
+ owner: 1,
113
+ origin: "canned_greeting",
114
+ emit: () => {},
115
+ afterRelease: () => {
116
+ after.push("after");
117
+ },
118
+ });
119
+
120
+ conversation.claimAway(2);
121
+ await nextTick();
122
+
123
+ // The newer hold survives, nothing drains into the turn that owns it, and
124
+ // the follow-up work waits for whoever does.
125
+ expect(conversation.isProcessing()).toBe(true);
126
+ expect(conversation.drainOrigins).toEqual([]);
127
+ expect(after).toEqual([]);
128
+ });
86
129
  });
@@ -10,10 +10,11 @@ import { getDb } from "../persistence/db-connection.js";
10
10
  import { initializeDb } from "../persistence/db-init.js";
11
11
  import {
12
12
  clearPayload,
13
- findConversationByProviderMessageId,
13
+ findMessageByProviderMessageId,
14
14
  findMessageBySourceId,
15
15
  linkMessage,
16
16
  recordInbound,
17
+ recordOutboundPost,
17
18
  storePayload,
18
19
  } from "../persistence/delivery-crud.js";
19
20
  import {
@@ -52,6 +53,7 @@ await initializeDb();
52
53
  function resetTables() {
53
54
  const db = getDb();
54
55
  db.run("DELETE FROM channel_inbound_events");
56
+ db.run("DELETE FROM channel_outbound_posts");
55
57
  db.run("DELETE FROM messages");
56
58
  db.run("DELETE FROM conversation_keys");
57
59
  db.run("DELETE FROM external_conversation_bindings");
@@ -147,35 +149,144 @@ describe("channel-delivery-store", () => {
147
149
  .run();
148
150
 
149
151
  expect(
150
- findConversationByProviderMessageId(
151
- "discord",
152
- chatId,
153
- "1234567890123456789",
154
- ),
155
- ).toBe(minted.conversationId);
152
+ findMessageByProviderMessageId("discord", chatId, "1234567890123456789"),
153
+ ).toEqual({
154
+ messageId: "assistant-post-1",
155
+ conversationId: minted.conversationId,
156
+ });
156
157
  // A split reply posts several provider messages from one row; any of
157
158
  // their ids resolves it.
158
159
  expect(
159
- findConversationByProviderMessageId(
160
- "discord",
161
- chatId,
162
- "2234567890123456789",
163
- ),
164
- ).toBe(minted.conversationId);
160
+ findMessageByProviderMessageId("discord", chatId, "2234567890123456789"),
161
+ ).toEqual({
162
+ messageId: "assistant-post-1",
163
+ conversationId: minted.conversationId,
164
+ });
165
165
  // The scan is scoped to the reaction's own channel address: the same id
166
166
  // is invisible from another channel's or another chat's key space.
167
167
  expect(
168
- findConversationByProviderMessageId(
169
- "telegram",
170
- chatId,
168
+ findMessageByProviderMessageId("telegram", chatId, "1234567890123456789"),
169
+ ).toBeNull();
170
+ expect(
171
+ findMessageByProviderMessageId(
172
+ "discord",
173
+ "other-chat",
171
174
  "1234567890123456789",
172
175
  ),
173
176
  ).toBeNull();
177
+ });
178
+
179
+ test("linked inbound rows are excluded from the provider-id scan; crash-window rows resolve", () => {
180
+ const chatId = "chat-scan-budget";
181
+ const minted = recordInbound("telegram", chatId, "evt-linked", {
182
+ sourceMessageId: "tg-linked-1",
183
+ });
184
+ const envelope = (messageId: string) =>
185
+ JSON.stringify({
186
+ providerMeta: JSON.stringify({
187
+ source: "telegram",
188
+ conversationExternalId: chatId,
189
+ messageId,
190
+ eventKind: "message",
191
+ }),
192
+ });
193
+
194
+ // A normally-ingested inbound row: linked to its event, so the
195
+ // inbound-event index owns its resolution and the fallback scan must
196
+ // not spend budget on it.
197
+ insertMessage("linked-user-row", minted.conversationId, {});
198
+ getDb()
199
+ .update(messages)
200
+ .set({ metadata: envelope("tg-linked-1") })
201
+ .where(eq(messages.id, "linked-user-row"))
202
+ .run();
203
+ linkMessage(minted.eventId, "linked-user-row");
204
+ expect(
205
+ findMessageByProviderMessageId("telegram", chatId, "tg-linked-1"),
206
+ ).toBeNull();
207
+
208
+ // A crash-window row: persisted with its envelope but its event link
209
+ // never landed. The fallback is the only path that can resolve it.
210
+ insertMessage("orphan-user-row", minted.conversationId, {});
211
+ getDb()
212
+ .update(messages)
213
+ .set({ metadata: envelope("tg-orphan-1") })
214
+ .where(eq(messages.id, "orphan-user-row"))
215
+ .run();
216
+ expect(
217
+ findMessageByProviderMessageId("telegram", chatId, "tg-orphan-1"),
218
+ ).toEqual({
219
+ messageId: "orphan-user-row",
220
+ conversationId: minted.conversationId,
221
+ });
222
+
223
+ // A legacy linked row whose event carries no source_message_id:
224
+ // findMessageBySourceId matches only that column, so the fallback must
225
+ // still admit the row or reactions to it become unresolvable.
226
+ const legacy = recordInbound("telegram", chatId, "evt-legacy");
227
+ insertMessage("legacy-user-row", legacy.conversationId, {});
228
+ getDb()
229
+ .update(messages)
230
+ .set({ metadata: envelope("tg-legacy-1") })
231
+ .where(eq(messages.id, "legacy-user-row"))
232
+ .run();
233
+ linkMessage(legacy.eventId, "legacy-user-row");
234
+ expect(
235
+ findMessageByProviderMessageId("telegram", chatId, "tg-legacy-1"),
236
+ ).toEqual({
237
+ messageId: "legacy-user-row",
238
+ conversationId: legacy.conversationId,
239
+ });
240
+ });
241
+
242
+ test("the outbound-posts index resolves a post exactly, at any age", () => {
243
+ const chatId = "999000111222333444";
244
+ const minted = recordInbound("discord", chatId, "evt-index-seed");
245
+ const db = getDb();
246
+ // The row's envelope names nothing: resolution comes from the index
247
+ // alone, proving the table is the contract rather than a cache in
248
+ // front of the scan.
249
+ db.insert(messages)
250
+ .values({
251
+ id: "assistant-indexed-post",
252
+ conversationId: minted.conversationId,
253
+ role: "assistant",
254
+ content: "indexed reply",
255
+ createdAt: Date.now(),
256
+ metadata: null,
257
+ })
258
+ .run();
259
+ recordOutboundPost({
260
+ sourceChannel: "discord",
261
+ externalChatId: chatId,
262
+ providerMessageId: "555666777888999000",
263
+ messageId: "assistant-indexed-post",
264
+ conversationId: minted.conversationId,
265
+ });
266
+ // Idempotent on redelivery.
267
+ recordOutboundPost({
268
+ sourceChannel: "discord",
269
+ externalChatId: chatId,
270
+ providerMessageId: "555666777888999000",
271
+ messageId: "assistant-indexed-post",
272
+ conversationId: minted.conversationId,
273
+ });
274
+
275
+ expect(
276
+ findMessageByProviderMessageId("discord", chatId, "555666777888999000"),
277
+ ).toEqual({
278
+ messageId: "assistant-indexed-post",
279
+ conversationId: minted.conversationId,
280
+ });
281
+ // Channel and chat scoping hold on the index path too.
282
+ expect(
283
+ findMessageByProviderMessageId("telegram", chatId, "555666777888999000"),
284
+ ).toBeNull();
174
285
  expect(
175
- findConversationByProviderMessageId(
286
+ findMessageByProviderMessageId(
176
287
  "discord",
177
288
  "other-chat",
178
- "1234567890123456789",
289
+ "555666777888999000",
179
290
  ),
180
291
  ).toBeNull();
181
292
  });