@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
@@ -441,7 +441,7 @@ const NAMESPACE_IMPORT = "*";
441
441
  * namespace import yields the {@link NAMESPACE_IMPORT} sentinel — it reaches
442
442
  * the whole surface, so it must not slip past the anti-backslide check. */
443
443
  function symbolsImportedFrom(source: string, hostPathSuffix: string): string[] {
444
- const escaped = hostPathSuffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
444
+ const escaped = RegExp.escape(hostPathSuffix);
445
445
  const from = String.raw`\s*from\s*['"]([^'"]*` + escaped + String.raw`)['"]`;
446
446
  const namedRegex = new RegExp(
447
447
  String.raw`import\s+(?:type\s+)?\{([^}]*)\}` + from,
@@ -224,6 +224,17 @@ describe("resolvePricing", () => {
224
224
  );
225
225
  });
226
226
 
227
+ test("returns priced for gemini-3.8-flash", () => {
228
+ const result = resolvePricing(
229
+ "gemini",
230
+ "gemini-3.8-flash",
231
+ 1_000_000,
232
+ 1_000_000,
233
+ );
234
+ expect(result.pricingStatus).toBe("priced");
235
+ expect(result.estimatedCostUsd).toBe(1.5 + 7.5);
236
+ });
237
+
227
238
  test("returns priced for gemini-3-flash-preview", () => {
228
239
  const result = resolvePricing(
229
240
  "gemini",
@@ -160,6 +160,7 @@ function makeTestConversation() {
160
160
  size: () => 0,
161
161
  } as unknown as MessageQueue;
162
162
  let processing = false;
163
+ let owner = 0;
163
164
  const messagingCtx: MessagingConversationContext = {
164
165
  conversationId: "conv-display-content",
165
166
  messages,
@@ -167,6 +168,21 @@ function makeTestConversation() {
167
168
  setProcessing: (value: boolean) => {
168
169
  processing = value;
169
170
  },
171
+ acquireProcessingFenced: async () => {
172
+ if (processing) {
173
+ return null;
174
+ }
175
+ processing = true;
176
+ owner += 1;
177
+ return owner;
178
+ },
179
+ releaseProcessing: (claim: number) => {
180
+ if (claim !== owner) {
181
+ return false;
182
+ }
183
+ processing = false;
184
+ return true;
185
+ },
170
186
  abortController: null,
171
187
  queue: queueStub,
172
188
  getTurnChannelContext: () => turnChannelContext,
@@ -398,20 +414,15 @@ describe("processMessage displayContent", () => {
398
414
  type: "text",
399
415
  text: modelContent,
400
416
  });
401
- const inMemoryFileBlock = inMemoryMessage.content[1] as unknown as Record<
402
- string,
403
- unknown
404
- >;
405
- expect(inMemoryFileBlock._attachmentId).toBe("att-1");
406
- expect(inMemoryFileBlock).toMatchObject({
417
+ expect(inMemoryMessage.content[1]).toEqual({
407
418
  type: "file",
408
419
  source: {
409
- type: "base64",
420
+ type: "workspace_ref",
410
421
  media_type: "application/pdf",
411
- data: Buffer.from("pdf bytes").toString("base64"),
422
+ attachmentId: "att-stored",
423
+ sizeBytes: 9,
412
424
  filename: "attachment.pdf",
413
425
  },
414
- extracted_text: undefined,
415
426
  });
416
427
  });
417
428
 
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Every acquisition of the processing flag goes through the fenced helper.
3
+ *
4
+ * `acquireProcessing` hands back a live claim, and the marker fence that has
5
+ * to follow it can fail. A site that calls the two itself owns the ordering,
6
+ * and getting it wrong (fencing outside the try whose finally releases) leaves
7
+ * the conversation processing for good with every later send queued behind a
8
+ * dead hold. `acquireProcessingFenced` owns that ordering once, so the rule is
9
+ * simply that nothing else calls the primitive.
10
+ */
11
+ import { readdirSync, readFileSync, statSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { describe, expect, test } from "bun:test";
14
+
15
+ /** Where the primitive and its fenced wrapper legitimately live together. */
16
+ const OWNER_FILE = join("src", "daemon", "conversation.ts");
17
+
18
+ function sourceFiles(dir: string, found: string[] = []): string[] {
19
+ for (const entry of readdirSync(dir)) {
20
+ const full = join(dir, entry);
21
+ if (statSync(full).isDirectory()) {
22
+ if (entry !== "node_modules" && entry !== "__tests__") {
23
+ sourceFiles(full, found);
24
+ }
25
+ continue;
26
+ }
27
+ if (entry.endsWith(".ts") && !entry.endsWith(".test.ts")) {
28
+ found.push(full);
29
+ }
30
+ }
31
+ return found;
32
+ }
33
+
34
+ describe("processing flag acquisition", () => {
35
+ test("only the fenced helper calls the raw acquire", () => {
36
+ const offenders = sourceFiles("src").filter(
37
+ (file) =>
38
+ file !== OWNER_FILE &&
39
+ /\bacquireProcessing\s*\(\s*\)/.test(readFileSync(file, "utf8")),
40
+ );
41
+
42
+ expect(offenders).toEqual([]);
43
+ });
44
+
45
+ test("only the fenced helper awaits the marker fence", () => {
46
+ // Same rule for the other half: a site that awaits the fence itself is a
47
+ // site that can await it in the wrong place.
48
+ const offenders = sourceFiles("src").filter(
49
+ (file) =>
50
+ file !== OWNER_FILE &&
51
+ /\bensureProcessingMarker\s*\(/.test(readFileSync(file, "utf8")),
52
+ );
53
+
54
+ expect(offenders).toEqual([]);
55
+ });
56
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildProviderMetaForPersistence } from "../daemon/conversation-messaging.js";
4
+ import type { ProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
5
+ import { readProviderMetadata } from "../messaging/read-provider-metadata.js";
6
+
7
+ const telegramInbound: ProviderMessageMetadata = {
8
+ source: "telegram",
9
+ conversationExternalId: "chat-42",
10
+ messageId: "msg-100",
11
+ threadId: "topic-7",
12
+ actorExternalId: "user-9",
13
+ displayName: "Alice",
14
+ eventKind: "message",
15
+ };
16
+
17
+ describe("buildProviderMetaForPersistence", () => {
18
+ test("a matching non-Slack turn persists an envelope the neutral reader round-trips", () => {
19
+ const serialized = buildProviderMetaForPersistence({
20
+ channelInbound: telegramInbound,
21
+ turnChannel: "telegram",
22
+ });
23
+ expect(serialized).not.toBeNull();
24
+
25
+ // The stored row's metadata JSON carries the envelope under the
26
+ // `providerMeta` key; the canonical reader must resolve it.
27
+ const rowMetadata = JSON.stringify({
28
+ userMessageChannel: "telegram",
29
+ providerMeta: serialized,
30
+ });
31
+ const read = readProviderMetadata(rowMetadata);
32
+ expect(read).toEqual(telegramInbound);
33
+ });
34
+
35
+ test("a Slack turn never gains a providerMeta key", () => {
36
+ expect(
37
+ buildProviderMetaForPersistence({
38
+ channelInbound: { ...telegramInbound, source: "slack" },
39
+ turnChannel: "slack",
40
+ }),
41
+ ).toBeNull();
42
+ });
43
+
44
+ test("a turn channel that does not match the envelope's source is refused", () => {
45
+ expect(
46
+ buildProviderMetaForPersistence({
47
+ channelInbound: telegramInbound,
48
+ turnChannel: "discord",
49
+ }),
50
+ ).toBeNull();
51
+ expect(
52
+ buildProviderMetaForPersistence({
53
+ channelInbound: telegramInbound,
54
+ turnChannel: undefined,
55
+ }),
56
+ ).toBeNull();
57
+ });
58
+
59
+ test("an absent envelope persists nothing", () => {
60
+ expect(
61
+ buildProviderMetaForPersistence({
62
+ channelInbound: undefined,
63
+ turnChannel: "telegram",
64
+ }),
65
+ ).toBeNull();
66
+ });
67
+ });
@@ -20,6 +20,15 @@ mock.module("../config/env.js", () => ({
20
20
  }));
21
21
 
22
22
  const _conversationMocks = new Map<string, unknown>();
23
+ // Wake dispatches are captured, not run: this suite pins persistence, and
24
+ // the real background machinery would race the assertions.
25
+ const dispatchedWakes: Array<Record<string, unknown>> = [];
26
+ mock.module("../runtime/routes/inbound-stages/background-dispatch.js", () => ({
27
+ processChannelMessageInBackground: (params: Record<string, unknown>) => {
28
+ dispatchedWakes.push(params);
29
+ },
30
+ }));
31
+
23
32
  mock.module("../daemon/conversation-registry.js", () => ({
24
33
  findConversation: (id: string) => _conversationMocks.get(id),
25
34
  }));
@@ -78,14 +87,12 @@ import { getDb } from "../persistence/db-connection.js";
78
87
  import { initializeDb } from "../persistence/db-init.js";
79
88
  import { linkMessage, recordInbound } from "../persistence/delivery-crud.js";
80
89
  import { messages } from "../persistence/schema/conversations.js";
81
- import * as pendingInteractions from "../runtime/pending-interactions.js";
82
90
  import { isReactionEvent } from "../runtime/routes/inbound-stages/reaction-intercept.js";
83
91
  import {
84
92
  handleChannelInbound,
85
93
  seedContactChannel,
86
94
  } from "./helpers/channel-test-adapter.js";
87
95
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
88
- import { bridgeState } from "./helpers/gateway-guardian-requests-store-bridge.js";
89
96
 
90
97
  await initializeDb();
91
98
 
@@ -286,6 +293,8 @@ describe("Slack reaction event persistence", () => {
286
293
  expect(slackMeta!.actorExternalUserId).toBe(SLACK_USER_ID);
287
294
  expect(slackMeta!.reaction).toEqual({
288
295
  emoji: "thumbsup",
296
+ emojiKind: "shortcode",
297
+ emojiName: "thumbsup",
289
298
  actorDisplayName: SLACK_DISPLAY_NAME,
290
299
  targetChannelTs: "1700000000.111111",
291
300
  op: "added",
@@ -490,13 +499,21 @@ describe("Slack reaction event persistence", () => {
490
499
  TEST_BEARER_TOKEN,
491
500
  );
492
501
  expect(resp.status).toBe(200);
493
-
502
+ const body = (await resp.json()) as Record<string, unknown>;
503
+
504
+ // An admitted actor adding a reaction to the assistant's own post wakes
505
+ // a discretion turn instead of writing the passive row here: the turn's
506
+ // own persisted user row is the reaction row.
507
+ expect(body.reaction).toBe("wake_dispatched");
508
+ expect(dispatchedWakes).toHaveLength(1);
509
+ expect(dispatchedWakes[0].conversationId).toBe(conversationId);
510
+ expect(typeof dispatchedWakes[0].slackReactionRowMeta).toBe("string");
494
511
  const reactionRow = db.$client
495
512
  .prepare(
496
513
  "SELECT conversation_id AS conversationId FROM messages WHERE content = '[reaction]'",
497
514
  )
498
515
  .get() as { conversationId: string } | null;
499
- expect(reactionRow?.conversationId).toBe(conversationId);
516
+ expect(reactionRow).toBeNull();
500
517
  });
501
518
 
502
519
  test("a reaction leaves the reacted message resolvable by its own id", async () => {
@@ -606,200 +623,6 @@ describe("Slack reaction event persistence", () => {
606
623
  });
607
624
  });
608
625
 
609
- // ---------------------------------------------------------------------------
610
- // Guardian approval-by-reaction integration test
611
- // ---------------------------------------------------------------------------
612
- //
613
- // Verifies that approval interception runs before reaction persistence so a
614
- // guardian's `reaction:` event on a pending approval prompt applies the
615
- // decision and no transcript-line row is written for the reaction itself.
616
-
617
- const GUARDIAN_USER_ID = "U_GUARDIAN_REACT";
618
- const GUARDIAN_DISPLAY_NAME = "Guardian Reactor";
619
- const GUARDIAN_REACTION_TOOL = "execute_shell";
620
- const GUARDIAN_REACTION_INPUT = { command: "rm -rf /tmp/test" };
621
-
622
- function seedGuardianForChannel(): void {
623
- seedGatewayGuardian({
624
- channelType: "slack",
625
- address: GUARDIAN_USER_ID,
626
- principalId: GUARDIAN_USER_ID,
627
- externalChatId: SLACK_CHANNEL_ID,
628
- });
629
- createGuardianBinding({
630
- channel: "slack",
631
- guardianExternalUserId: GUARDIAN_USER_ID,
632
- guardianDeliveryChatId: SLACK_CHANNEL_ID,
633
- guardianPrincipalId: GUARDIAN_USER_ID,
634
- });
635
- }
636
-
637
- function seedPendingGuardianApprovalForReaction(
638
- requestId: string,
639
- conversationId: string,
640
- reactedTs: string,
641
- ): void {
642
- // Canonical tool_approval request keyed by the confirmation requestId — the
643
- // same record assistant-event-hub creates for every confirmation.
644
- bridgeState.seedRequest({
645
- id: requestId,
646
- kind: "tool_approval",
647
- sourceType: "channel",
648
- sourceChannel: "slack",
649
- sourceConversationId: conversationId,
650
- requesterExternalUserId: "requester-user-1",
651
- requesterChatId: SLACK_CHANNEL_ID,
652
- guardianExternalUserId: GUARDIAN_USER_ID,
653
- guardianPrincipalId: GUARDIAN_USER_ID,
654
- toolName: GUARDIAN_REACTION_TOOL,
655
- status: "pending",
656
- expiresAt: Date.now() + 300_000,
657
- });
658
-
659
- // The delivered approval card → request mapping the reaction resolves
660
- // against (destination message id = the reacted Slack ts).
661
- bridgeState.seedDelivery({
662
- requestId,
663
- destinationChannel: "slack",
664
- destinationChatId: SLACK_CHANNEL_ID,
665
- destinationMessageId: reactedTs,
666
- });
667
-
668
- // Register a pending interaction so the tool_approval resolver finds the
669
- // requester-side hook to drive `allow`.
670
- const handleConfirmationResponse = mock(() => {});
671
- const _mockSession = {
672
- handleConfirmationResponse,
673
- ensureActorScopedHistory: async () => {},
674
- } as unknown as Conversation;
675
- _conversationMocks.set(conversationId, _mockSession);
676
- pendingInteractions.register(requestId, {
677
- conversationId,
678
- kind: "confirmation",
679
- confirmationDetails: {
680
- toolName: GUARDIAN_REACTION_TOOL,
681
- input: GUARDIAN_REACTION_INPUT,
682
- riskLevel: "high",
683
- allowlistOptions: [
684
- { label: "test", description: "test", pattern: "test" },
685
- ],
686
- scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
687
- },
688
- });
689
- }
690
-
691
- describe("guardian approval-by-reaction integration via handleChannelInbound", () => {
692
- beforeEach(() => {
693
- const db = getDb();
694
- db.run("DELETE FROM messages");
695
- db.run("DELETE FROM channel_inbound_events");
696
- db.run("DELETE FROM conversations");
697
- db.run("DELETE FROM contact_channels");
698
- db.run("DELETE FROM contacts");
699
- bridgeState.reset();
700
- gatewayGuardians = [];
701
- pendingInteractions.clear();
702
- msgCounter = 0;
703
- });
704
-
705
- test("guardian reaction on pending approval applies decision and persists no transcript row", async () => {
706
- seedGuardianForChannel();
707
- const requestId = "req-guardian-react-1";
708
- // Back the guardian request and its pending interaction with a real
709
- // conversation row, inserted directly via the DB layer.
710
- const db = getDb();
711
- const conversationId = "conv-react-test-1";
712
- const now = Date.now();
713
- db.$client
714
- .prepare(
715
- `INSERT INTO conversations (
716
- id, title, created_at, updated_at, total_input_tokens, total_output_tokens,
717
- total_estimated_cost, context_compacted_message_count, conversation_type,
718
- source, memory_scope_id, host_access, is_auto_title
719
- ) VALUES (?, NULL, ?, ?, 0, 0, 0, 0, 'standard', 'user', 'default', 0, 1)`,
720
- )
721
- .run(conversationId, now, now);
722
-
723
- const reactedTs = "1700000099.000001";
724
- // The approval card was delivered on this ts; its canonical delivery row
725
- // is how the guardian reaction is scoped to a known approval message.
726
- seedPendingGuardianApprovalForReaction(
727
- requestId,
728
- conversationId,
729
- reactedTs,
730
- );
731
-
732
- const body = {
733
- sourceChannel: "slack",
734
- interface: "slack",
735
- conversationExternalId: SLACK_CHANNEL_ID,
736
- externalMessageId: `${SLACK_CHANNEL_ID}:${reactedTs}:guardian-react`,
737
- content: "reaction:white_check_mark",
738
- callbackData: "reaction:white_check_mark",
739
- actorExternalId: GUARDIAN_USER_ID,
740
- actorDisplayName: GUARDIAN_DISPLAY_NAME,
741
- replyCallbackUrl: "http://localhost:7830/deliver/slack",
742
- sourceMetadata: {
743
- messageId: reactedTs,
744
- chatType: "channel",
745
- },
746
- };
747
- const req = new Request("http://localhost:8080/channels/inbound", {
748
- method: "POST",
749
- headers: {
750
- "Content-Type": "application/json",
751
- "X-Gateway-Origin": TEST_BEARER_TOKEN,
752
- },
753
- body: JSON.stringify(body),
754
- });
755
-
756
- // Wire a non-undefined `approvalConversationGenerator` to mirror the
757
- // production configuration. The deterministic reaction handler must
758
- // short-circuit before the generator is consulted — if it runs, it
759
- // returns `keep_pending` so the assertions below would fail loudly.
760
- const approvalConversationGenerator = mock(async () => ({
761
- disposition: "keep_pending" as const,
762
- replyText: "mock conversational turn should not be invoked for reactions",
763
- }));
764
-
765
- const resp = await handleChannelInbound(
766
- req,
767
- undefined,
768
- TEST_BEARER_TOKEN,
769
- undefined,
770
- approvalConversationGenerator,
771
- );
772
- const json = (await resp.json()) as Record<string, unknown>;
773
-
774
- expect(resp.status).toBe(200);
775
- expect(json.accepted).toBe(true);
776
- expect(json.canonicalRouter).toBe("canonical_decision_applied");
777
- expect(approvalConversationGenerator).not.toHaveBeenCalled();
778
-
779
- // The guardian request is resolved (no longer pending).
780
- expect(bridgeState.getRequest(requestId)?.status).toBe("approved");
781
-
782
- // No transcript row was written for the reaction itself — resolved
783
- // guardian approval reactions have no transcript representation.
784
- const reactionRows = readPersistedMessages().filter((row) => {
785
- if (!row.metadata) {
786
- return false;
787
- }
788
- try {
789
- const env = JSON.parse(row.metadata) as Record<string, unknown>;
790
- if (typeof env.slackMeta !== "string") {
791
- return false;
792
- }
793
- const meta = readSlackMetadata(env.slackMeta);
794
- return meta?.eventKind === "reaction";
795
- } catch {
796
- return false;
797
- }
798
- });
799
- expect(reactionRows.length).toBe(0);
800
- });
801
- });
802
-
803
626
  // ---------------------------------------------------------------------------
804
627
  // Reaction access-control regression (LUM-2489)
805
628
  // ---------------------------------------------------------------------------
@@ -813,6 +636,7 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
813
636
  describe("reaction access control (no verification handshake)", () => {
814
637
  const STRANGER_USER_ID = "U_REACTION_STRANGER";
815
638
  const CONTACT_USER_ID = "U_REACTION_CONTACT";
639
+ const GUARDIAN_USER_ID = "U_GUARDIAN_REACT";
816
640
  // Guardian's approval channel is a DM, distinct from the public channel the
817
641
  // reaction lands in (mirroring production). Reusing the public channel id
818
642
  // would let a reactor match the guardian's channel via findContactChannel's
@@ -196,8 +196,11 @@ describe("runConversationTurn persistence", () => {
196
196
  // send-message route.
197
197
  expect(listInvalidations()).toEqual([creationTags(result.conversationId)]);
198
198
  expect(
199
- broadcasts.filter((msg) => msg.type === "conversation_list_invalidated"),
200
- ).toEqual([{ type: "conversation_list_invalidated", reason: "created" }]);
199
+ broadcasts.filter(
200
+ (msg) =>
201
+ (msg as { type: string }).type === "conversation_list_invalidated",
202
+ ),
203
+ ).toEqual([]);
201
204
  });
202
205
 
203
206
  test("adopts a caller-supplied conversation id verbatim when no row exists", async () => {
@@ -89,6 +89,7 @@ function createContext(): MessagingConversationContext {
89
89
  size: () => 0,
90
90
  } as unknown as MessageQueue;
91
91
  let processing = false;
92
+ let owner = 0;
92
93
  return {
93
94
  conversationId: "conv-scripted-test",
94
95
  messages: [],
@@ -96,6 +97,21 @@ function createContext(): MessagingConversationContext {
96
97
  setProcessing: (value: boolean) => {
97
98
  processing = value;
98
99
  },
100
+ acquireProcessingFenced: async () => {
101
+ if (processing) {
102
+ return null;
103
+ }
104
+ processing = true;
105
+ owner += 1;
106
+ return owner;
107
+ },
108
+ releaseProcessing: (claim: number) => {
109
+ if (claim !== owner) {
110
+ return false;
111
+ }
112
+ processing = false;
113
+ return true;
114
+ },
99
115
  abortController: null,
100
116
  queue: queueStub,
101
117
  clientOs: undefined,
@@ -127,6 +127,20 @@ describe("skill_load tool", () => {
127
127
  );
128
128
  });
129
129
 
130
+ test("description routes Claude Code and Codex mentions to acp", () => {
131
+ // skill_load is always in the tool list. Naming the agents here is what
132
+ // makes a mention load ACP instead of treating Claude Code as a competitor.
133
+ expect(skillLoadTool.description).toContain("Claude Code");
134
+ expect(skillLoadTool.description).toContain("Codex");
135
+ expect(skillLoadTool.description).toContain('skill: "acp"');
136
+ });
137
+
138
+ test("routes recurring monitoring and reminders to the schedule skill", () => {
139
+ const description = skillLoadTool.description;
140
+ expect(description).toContain('skill: "schedule"');
141
+ expect(description.toLowerCase()).toContain("recurring monitoring");
142
+ });
143
+
130
144
  test("loads a skill by exact id", async () => {
131
145
  writeSkill(
132
146
  "release-checklist",
@@ -393,6 +407,19 @@ describe("skill_load tool", () => {
393
407
  expect(result.content).toContain('<loaded_skill id="app-builder"');
394
408
  });
395
409
 
410
+ test("bundled acp skill offers a one-time connect when the user names an agent", async () => {
411
+ const result = await executeSkillLoad({ skill: "acp" });
412
+
413
+ expect(result.isError).toBe(false);
414
+ expect(result.content).toContain("Skill: ACP");
415
+ expect(result.content).toContain(
416
+ "When the user names Claude Code or Codex",
417
+ );
418
+ expect(result.content).toContain("offer once, in one short sentence");
419
+ expect(result.content).toContain("Do not spawn unless they accept");
420
+ expect(result.content).toContain('<loaded_skill id="acp"');
421
+ });
422
+
396
423
  test("bundled phone-calls loads when setup includes are unavailable", async () => {
397
424
  const result = await executeSkillLoad({ skill: "phone-calls" });
398
425
 
@@ -869,16 +869,49 @@ describe("always-candidate frontmatter parsing", () => {
869
869
  expect(card).toContain("Avoid when:");
870
870
  });
871
871
 
872
+ test("the bundled schedule skill's card fits the default budget", async () => {
873
+ // Recurring-monitoring turns only reach this skill if the card still
874
+ // carries that vocabulary after the default-budget truncation. An overrun
875
+ // drops trailing hints and the whole avoid-when list.
876
+ const { DEFAULT_CARD_CHARS, buildSkillContent } =
877
+ await import("../plugins/defaults/memory/substrate/skill-content.js");
878
+ const schedule = loadSkillCatalog().find((skill) => skill.id === "schedule");
879
+ expect(schedule).toBeDefined();
880
+
881
+ const card = buildSkillContent(schedule!);
882
+ expect(card.length).toBeLessThan(DEFAULT_CARD_CHARS);
883
+ expect(card).toContain("Avoid when:");
884
+ });
885
+
886
+ test("the bundled schedule card carries its monitoring vocabulary", async () => {
887
+ const { buildSkillContent } =
888
+ await import("../plugins/defaults/memory/substrate/skill-content.js");
889
+ const schedule = loadSkillCatalog().find((skill) => skill.id === "schedule");
890
+ const card = buildSkillContent(schedule!).toLowerCase();
891
+
892
+ for (const term of ["monitor", "page", "dashboard", "status", "alert"]) {
893
+ expect(card).toContain(term);
894
+ }
895
+ });
896
+
872
897
  test("the bundled acp card carries its setup and sign-in vocabulary", async () => {
873
898
  // A "configure Claude Code" or "let's do the auth" turn only reaches this
874
899
  // skill if the card actually says it covers setup and authentication;
875
900
  // delegation-only wording routes those turns to a terminal skill instead.
901
+ // "names" keeps a mention ("claude code will do it") from falling through
902
+ // as a competitor reference.
876
903
  const { buildSkillContent } =
877
904
  await import("../plugins/defaults/memory/substrate/skill-content.js");
878
905
  const acp = loadSkillCatalog().find((skill) => skill.id === "acp");
879
906
  const card = buildSkillContent(acp!).toLowerCase();
880
907
 
881
- for (const term of ["set up", "install", "authenticate", "connect"]) {
908
+ for (const term of [
909
+ "names",
910
+ "set up",
911
+ "install",
912
+ "authenticate",
913
+ "connect",
914
+ ]) {
882
915
  expect(card).toContain(term);
883
916
  }
884
917
  });
@@ -220,10 +220,9 @@ describe("stripExistingMemoryInjections", () => {
220
220
  });
221
221
 
222
222
  // ---------------------------------------------------------------------------
223
- // stripSpotlightInjections — the memory-v3 ephemeral spotlight is the ONLY
224
- // block strip-and-replaced across every user message each turn. Frozen
225
- // `<memory>` card blocks must never be touched by it (the cache contract);
226
- // the old whole-layer stripAllMemoryInjections replace is gone.
223
+ // stripSpotlightInjections: leftover memory-v3 spotlight blocks are removed
224
+ // from every user message (compaction leftover cleanup). Frozen `<memory>`
225
+ // card blocks must never be touched (the cache contract).
227
226
  // ---------------------------------------------------------------------------
228
227
 
229
228
  const spotlightBlock = (inner: string): ContentBlock => ({
@@ -146,6 +146,15 @@ describe("buildSanitizedEnv", () => {
146
146
  expect(env.VELLUM_APT_DATA_ROOT).toBe("/data/system");
147
147
  expect(env.PATH.split(":")).toContain("/data/system/usr/bin");
148
148
  expect(env.PATH.split(":")).toContain("/data/system/usr/local/bin");
149
+ // Shims must shadow the chroot bin dirs so a shim beats a wrapper script
150
+ // whose hardcoded absolute path only resolves inside the chroot.
151
+ const kataPathParts = env.PATH.split(":");
152
+ expect(
153
+ kataPathParts.indexOf("/data/system/.host-shims"),
154
+ ).toBeGreaterThanOrEqual(0);
155
+ expect(kataPathParts.indexOf("/data/system/.host-shims")).toBeLessThan(
156
+ kataPathParts.indexOf("/data/system/bin"),
157
+ );
149
158
  expect(env.LD_LIBRARY_PATH.split(":")).toContain("/data/system/usr/lib");
150
159
  expect(env.LD_LIBRARY_PATH.split(":")).toContain(
151
160
  "/data/system/usr/local/lib",
@@ -104,6 +104,7 @@ import * as slackBackfill from "../messaging/providers/slack/backfill.js";
104
104
  import {
105
105
  buildSlackTimezoneMetadata,
106
106
  readSlackMetadata,
107
+ readSlackMetadataFromMessageMetadata,
107
108
  writeSlackMetadata,
108
109
  } from "../messaging/providers/slack/message-metadata.js";
109
110
  import type { MessageRow } from "../persistence/conversation-crud.js";
@@ -360,12 +361,13 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
360
361
  out.push(blank);
361
362
  continue;
362
363
  }
363
- const slackMetaRaw = envelope.slackMeta;
364
- if (typeof slackMetaRaw !== "string") {
364
+ // Either envelope: a person's row still writes `slackMeta`, a
365
+ // bot-authored row the neutral envelope with Slack's fields riding it.
366
+ const slackMeta = readSlackMetadataFromMessageMetadata(row.metadata);
367
+ if (slackMeta === null) {
365
368
  out.push(blank);
366
369
  continue;
367
370
  }
368
- const slackMeta = readSlackMetadata(slackMetaRaw);
369
371
  out.push({
370
372
  role: row.role,
371
373
  content: unwrapExternalContent(row.content),