@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
@@ -53,6 +53,7 @@ import type { GraphMemoryResult } from "../graph/conversation-graph-memory.js";
53
53
  import { recordMemoryRecallLog } from "../memory-recall-log-store.js";
54
54
  import { stripTailInjectionsForReinjection } from "../tail-reinjection-strip.js";
55
55
  import { MEMORY_V3_INJECTED_BLOCK_METADATA_KEY } from "../v3/ever-injected-store.js";
56
+ import { MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY } from "../v3/types.js";
56
57
 
57
58
  /**
58
59
  * Whether to run legacy graph-memory retrieval this turn. It gates BOTH
@@ -220,6 +221,10 @@ async function recordRecallSideEffects(
220
221
  * - `blocks.memoryV3InjectedBlock` (the frozen net-new card block, unwrapped)
221
222
  * persists under `MEMORY_V3_INJECTED_BLOCK_METADATA_KEY`; `loadFromDb`
222
223
  * re-wraps and splices it on load, freezing the cards into history.
224
+ * - `blocks.memoryV3SpotlightBlock` (the wrapped `<memory_spotlight>` that
225
+ * was sent) persists under `MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY`;
226
+ * `loadFromDb` splices it back onto the same row so historical turns
227
+ * keep the spotlight they were sent with.
223
228
  */
224
229
  async function persistInjectionBlocks(
225
230
  blocks: RuntimeInjectionResult["blocks"],
@@ -235,6 +240,7 @@ async function persistInjectionBlocks(
235
240
  !blocks.pkbContextBlock &&
236
241
  !blocks.memoryV2StaticBlock &&
237
242
  !blocks.memoryV3InjectedBlock &&
243
+ !blocks.memoryV3SpotlightBlock &&
238
244
  !blocks.backgroundTurnBlock &&
239
245
  !blocks.channelCapabilitiesBlock &&
240
246
  !blocks.nonInteractiveContextBlock &&
@@ -255,6 +261,10 @@ async function persistInjectionBlocks(
255
261
  metadataUpdates[MEMORY_V3_INJECTED_BLOCK_METADATA_KEY] =
256
262
  blocks.memoryV3InjectedBlock;
257
263
  }
264
+ if (blocks.memoryV3SpotlightBlock) {
265
+ metadataUpdates[MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY] =
266
+ blocks.memoryV3SpotlightBlock;
267
+ }
258
268
  if (blocks.unifiedTurnContext) {
259
269
  metadataUpdates.turnContextBlock = blocks.unifiedTurnContext;
260
270
  }
@@ -428,7 +428,7 @@ export const memoryInjectors: Injector[] = [
428
428
  // `memory-v2-static` is FROZEN: `daemon/conversation-runtime-assembly.ts`
429
429
  // switches on it to capture the block into persisted message metadata.
430
430
  memoryV2StaticInjector,
431
- // V3 — delete with v3. The frozen net-new card block and its ephemeral
431
+ // V3: delete with v3. The frozen net-new card block and its per-turn
432
432
  // spotlight companion; both self-gate on `memory.v3.live`.
433
433
  memoryV3Injector,
434
434
  memoryV3SpotlightInjector,
@@ -30,13 +30,11 @@ export function unwrapMemoryBlock(block: string): string {
30
30
  }
31
31
 
32
32
  /**
33
- * The memory-v3 ephemeral spotlight wrapper. Unlike `<memory>` card blocks
34
- * (frozen into history), the spotlight block is re-rendered onto the current
35
- * user tail every turn: the per-turn scoped strip in
36
- * `context/strip-injections.ts` (`stripSpotlightInjections`) and the
37
- * compaction matcher in `RUNTIME_INJECTION_PREFIXES` both key off this exact
38
- * prefix/suffix pair, so the producer wrapper lives here beside the `<memory>`
39
- * marker it parallels.
33
+ * The memory-v3 per-turn spotlight wrapper. Each turn's `<memory_spotlight>`
34
+ * stays on the user message that was sent, matching frozen `<memory>` cards.
35
+ * Compaction still strips these blocks via `RUNTIME_INJECTION_PREFIXES`, and
36
+ * `stripSpotlightInjections` is the full-history leftover cleanup used there.
37
+ * The producer wrapper lives here beside the `<memory>` marker it parallels.
40
38
  */
41
39
  export const MEMORY_SPOTLIGHT_PREFIX = "<memory_spotlight>\n";
42
40
  export const MEMORY_SPOTLIGHT_SUFFIX = "\n</memory_spotlight>";
@@ -44,3 +42,15 @@ export const MEMORY_SPOTLIGHT_SUFFIX = "\n</memory_spotlight>";
44
42
  export function wrapMemorySpotlightBlock(text: string): string {
45
43
  return `${MEMORY_SPOTLIGHT_PREFIX}${text}${MEMORY_SPOTLIGHT_SUFFIX}`;
46
44
  }
45
+
46
+ /**
47
+ * Whether `text` is a complete `<memory_spotlight>…</memory_spotlight>`
48
+ * wrapper. Requires both ends so user-authored text that merely opens with
49
+ * the tag is not treated as an injected spotlight.
50
+ */
51
+ export function isMemorySpotlightText(text: string): boolean {
52
+ return (
53
+ text.startsWith(MEMORY_SPOTLIGHT_PREFIX) &&
54
+ text.endsWith(MEMORY_SPOTLIGHT_SUFFIX)
55
+ );
56
+ }
@@ -103,12 +103,8 @@ function loadDefaultDisabledAssistantFlagKeys(): string[] {
103
103
  .sort();
104
104
  }
105
105
 
106
- function escapeRegExp(value: string): string {
107
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
- }
109
-
110
106
  function featureFlagKeyPattern(key: string): RegExp {
111
- const escaped = escapeRegExp(key);
107
+ const escaped = RegExp.escape(key);
112
108
  if (key.includes("-")) {
113
109
  return new RegExp(`(?:^|[^a-z0-9-])${escaped}(?:$|[^a-z0-9-])`, "i");
114
110
  }
@@ -119,3 +119,20 @@ describe("augmentMcpSetupDescription", () => {
119
119
  expect(out.id).toBe("mcp-setup");
120
120
  });
121
121
  });
122
+
123
+ describe("SKILLS_INJECTION_CATALOG_HINT", () => {
124
+ test("points a missing product at plugin and skill search", async () => {
125
+ const { SKILLS_INJECTION_CATALOG_HINT } = await import(
126
+ "../skill-content.js"
127
+ );
128
+ expect(SKILLS_INJECTION_CATALOG_HINT).toContain(
129
+ "assistant plugins search <name>",
130
+ );
131
+ expect(SKILLS_INJECTION_CATALOG_HINT).toContain(
132
+ "assistant skills search <name>",
133
+ );
134
+ expect(SKILLS_INJECTION_CATALOG_HINT.toLowerCase()).toContain(
135
+ "currently in the workspace",
136
+ );
137
+ });
138
+ });
@@ -29,6 +29,13 @@ export const ALWAYS_CANDIDATE_CARD_CHARS = 900;
29
29
  */
30
30
  export const DEFAULT_CARD_CHARS = 500;
31
31
 
32
+ /**
33
+ * One-shot line under injected skill cards. The cards list workspace skills
34
+ * only, so a missing product is not evidence it is unsupported.
35
+ */
36
+ export const SKILLS_INJECTION_CATALOG_HINT =
37
+ "Injected skills are only ones currently in the workspace. Run `assistant plugins search <name>` and `assistant skills search <name>` before concluding a given integration or skill is unsupported";
38
+
32
39
  /**
33
40
  * Render the prose-style capability statement embedded into the unified
34
41
  * `memory_v2_concept_pages` Qdrant collection (under the `skills/<id>` slug
@@ -12,11 +12,11 @@
12
12
  * → selectPool (real two-segment render + cache breakpoint; the PROVIDER
13
13
  * is stubbed to return deterministic ids per scripted turn)
14
14
  * → the real injectors (`memoryV3Injector` net-new cards + recordInjected
15
- * + schedulePruneValve; `memoryV3SpotlightInjector` ephemeral window)
16
- * → simulated runtime assembly (splice the card block onto the current
17
- * user message; scoped spotlight strip-and-replace via the real
18
- * `stripSpotlightInjections`) and metadata persistence (the
19
- * user-prompt-submit hook's `memoryV3InjectedBlock` write)
15
+ * + schedulePruneValve; `memoryV3SpotlightInjector` per-turn window)
16
+ * → simulated runtime assembly (splice the card block and the
17
+ * spotlight onto the current user message; do not rewrite historical
18
+ * spotlights) and metadata persistence (the user-prompt-submit
19
+ * hook's `memoryV3InjectedBlock` and `memoryV3SpotlightBlock` writes)
20
20
  * → the real prune valve against the live history (conversation-registry
21
21
  * stubbed to the simulated message arrays)
22
22
  * → rehydration from the temp DB (mirroring `daemon/conversation.ts`'s
@@ -57,7 +57,6 @@ import type {
57
57
  import { drizzle } from "drizzle-orm/bun-sqlite";
58
58
 
59
59
  import { setConfig } from "../../../../../__tests__/helpers/set-config.js";
60
- import { stripSpotlightInjections } from "../../../../../context/strip-injections.js";
61
60
  import { ensureMemoryV3SelectionsSchema } from "../../../../../persistence/migrations/338-move-memory-v3-selections-to-memory-db.js";
62
61
  import { ensureMemoryV3EverInjectedSchema } from "../../../../../persistence/migrations/345-move-memory-v3-ever-injected-to-memory-db.js";
63
62
  import * as schema from "../../../../../persistence/schema/index.js";
@@ -71,6 +70,7 @@ import { buildSectionNeedle } from "../section-needle.js";
71
70
  import { buildSectionIndex } from "../sections.js";
72
71
  import {
73
72
  MEMORY_V3_COMMIT_META_KEY,
73
+ MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY,
74
74
  type SectionIndex,
75
75
  type Slug,
76
76
  } from "../types.js";
@@ -561,7 +561,7 @@ interface TurnRecord {
561
561
  spotlightPlacement: string;
562
562
  residentBytes: number;
563
563
  prunedSlugs: Set<string>;
564
- /** JSON of the spotlight-stripped (persistent-layer) history after the turn. */
564
+ /** JSON of the live history after the turn (cards + spotlight persist). */
565
565
  snapshot: string;
566
566
  }
567
567
 
@@ -583,7 +583,7 @@ function insertMessageRow(
583
583
  }
584
584
 
585
585
  function persistentView(history: Message[]): string {
586
- return JSON.stringify(stripSpotlightInjections(history));
586
+ return JSON.stringify(history);
587
587
  }
588
588
 
589
589
  async function runTurn(
@@ -646,29 +646,34 @@ async function runTurn(
646
646
  if (cards.text.length > 0) {
647
647
  const tail = history[history.length - 1]!;
648
648
  tail.content = [{ type: "text", text: cards.text }, ...tail.content];
649
- testSqlite
650
- .query(/*sql*/ `UPDATE messages SET metadata = ? WHERE id = ?`)
651
- .run(
652
- JSON.stringify({
653
- [MEMORY_V3_INJECTED_BLOCK_METADATA_KEY]: unwrapMemoryBlock(
654
- cards.text,
655
- ),
656
- }),
657
- userRowId,
658
- );
659
649
  }
660
650
 
661
- // Spotlight: scoped strip of the stale block (real assembly helper), then
662
- // re-attach the fresh one. Real assembly splices it after the memory cards
663
- // (after-memory-prefix); this sim appends to the tail because only the
664
- // block's presence, content, placement value, and strip-and-replace are
665
- // asserted here — not its exact position within the message.
651
+ // Spotlight splices after the frozen cards (after-memory-prefix). Historical
652
+ // user messages keep the spotlight they already carry.
666
653
  const spotlight = await memoryV3SpotlightInjector.produce(ctx);
667
- const stripped = stripSpotlightInjections(history);
668
- history.splice(0, history.length, ...stripped);
669
- if (spotlight) {
654
+ if (spotlight && spotlight.text.length > 0) {
670
655
  const tail = history[history.length - 1]!;
671
- tail.content = [...tail.content, { type: "text", text: spotlight.text }];
656
+ const prefixCount = cards.text.length > 0 ? 1 : 0;
657
+ tail.content = [
658
+ ...tail.content.slice(0, prefixCount),
659
+ { type: "text", text: spotlight.text },
660
+ ...tail.content.slice(prefixCount),
661
+ ];
662
+ }
663
+
664
+ const metadata: Record<string, string> = {};
665
+ if (cards.text.length > 0) {
666
+ metadata[MEMORY_V3_INJECTED_BLOCK_METADATA_KEY] = unwrapMemoryBlock(
667
+ cards.text,
668
+ );
669
+ }
670
+ if (spotlight?.text) {
671
+ metadata[MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY] = spotlight.text;
672
+ }
673
+ if (Object.keys(metadata).length > 0) {
674
+ testSqlite
675
+ .query(/*sql*/ `UPDATE messages SET metadata = ? WHERE id = ?`)
676
+ .run(JSON.stringify(metadata), userRowId);
672
677
  }
673
678
 
674
679
  const replyContent: ContentBlock[] = [
@@ -726,6 +731,10 @@ function rehydrateFromDb(convId: string): Message[] {
726
731
  let content = JSON.parse(row.content) as ContentBlock[];
727
732
  if (row.role === "user" && row.metadata) {
728
733
  const meta = JSON.parse(row.metadata) as Record<string, unknown>;
734
+ const spotlight = meta[MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY];
735
+ if (typeof spotlight === "string") {
736
+ content = [{ type: "text", text: spotlight }, ...content];
737
+ }
729
738
  const block = meta[MEMORY_V3_INJECTED_BLOCK_METADATA_KEY];
730
739
  if (typeof block === "string") {
731
740
  const resident = filterPrunedCardSections(
@@ -1014,7 +1023,7 @@ describe("memory-v3 carry integration — cache contract", () => {
1014
1023
  });
1015
1024
 
1016
1025
  describe("memory-v3 carry integration — spotlight contract", () => {
1017
- test("spotlight is present every turn, after the memory cards, bounded by n × (window + 1)", () => {
1026
+ test("spotlight is produced every turn, spliced after cards, bounded by n × (window + 1)", () => {
1018
1027
  for (const record of records) {
1019
1028
  expect(record.spotlightText.startsWith("<memory_spotlight>\n")).toBe(
1020
1029
  true,
@@ -1040,12 +1049,9 @@ describe("memory-v3 carry integration — spotlight contract", () => {
1040
1049
  expect(records[5]!.spotlightText).not.toContain("page-a.md");
1041
1050
  });
1042
1051
 
1043
- test("the spotlight never reaches the persistent layer (blocks, metadata, history)", () => {
1052
+ test("each turn persists its spotlight on that user message and keeps historical copies", () => {
1044
1053
  for (const record of records) {
1045
1054
  expect(record.blockText).not.toContain("<memory_spotlight>");
1046
- // The persistent-layer snapshot is spotlight-stripped by construction;
1047
- // the raw live history must carry exactly ONE spotlight block (the
1048
- // current turn's).
1049
1055
  }
1050
1056
  const metadataRows = testSqlite
1051
1057
  .query(
@@ -1056,9 +1062,10 @@ describe("memory-v3 carry integration — spotlight contract", () => {
1056
1062
  )
1057
1063
  .all(CONV) as Array<{ metadata: string }>;
1058
1064
  expect(metadataRows.length).toBeGreaterThan(0);
1059
- for (const row of metadataRows) {
1060
- expect(row.metadata).not.toContain("memory_spotlight");
1061
- }
1065
+ const persistedSpotlights = metadataRows.filter((row) =>
1066
+ row.metadata.includes(MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY),
1067
+ );
1068
+ expect(persistedSpotlights.length).toBeGreaterThan(1);
1062
1069
  const liveSpotlights = histories
1063
1070
  .get(CONV)!
1064
1071
  .flatMap((m) => m.content)
@@ -1066,7 +1073,7 @@ describe("memory-v3 carry integration — spotlight contract", () => {
1066
1073
  (b): b is { type: "text"; text: string } =>
1067
1074
  b.type === "text" && b.text.startsWith("<memory_spotlight>\n"),
1068
1075
  );
1069
- expect(liveSpotlights).toHaveLength(1);
1076
+ expect(liveSpotlights.length).toBeGreaterThan(1);
1070
1077
  });
1071
1078
  });
1072
1079
 
@@ -1106,15 +1113,21 @@ describe("memory-v3 carry integration — restart contract", () => {
1106
1113
 
1107
1114
  test("pruned sections are absent from the rehydrated history (metadata stays intact)", () => {
1108
1115
  const rehydrated = JSON.parse(restartRehydratedJson) as Message[];
1109
- const allText = rehydrated
1116
+ const cardText = rehydrated
1110
1117
  .flatMap((m) => m.content)
1111
1118
  .filter((b): b is { type: "text"; text: string } => b.type === "text")
1119
+ .filter(
1120
+ (b) =>
1121
+ b.text.startsWith("<memory>\n") && b.text.endsWith("\n</memory>"),
1122
+ )
1112
1123
  .map((b) => b.text)
1113
1124
  .join("\n");
1114
- expect(allText).not.toContain("# memory/concepts/page-a.md");
1115
- expect(allText).not.toContain("# memory/concepts/page-b.md");
1116
- // Resident cards survive the round trip…
1117
- expect(allText).toContain(card("page-c"));
1125
+ // Prune filters card sections on rehydrate. Historical spotlights stay
1126
+ // as sent, so a pruned slug can still appear under `##` in those blocks.
1127
+ expect(cardText).not.toContain("# memory/concepts/page-a.md");
1128
+ expect(cardText).not.toContain("# memory/concepts/page-b.md");
1129
+ // Resident cards survive the round trip.
1130
+ expect(cardText).toContain(card("page-c"));
1118
1131
  // …and the persisted metadata still carries the pruned cards (the filter
1119
1132
  // is rehydration-time, never a metadata rewrite).
1120
1133
  const metadata = testSqlite
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tests for the memory-v3 injection layer (`injector.ts`): frozen net-new
3
- * cards + ephemeral spotlight.
3
+ * cards + per-turn spotlight.
4
4
  *
5
5
  * - net-new dedup: a turn re-selecting already-injected pages renders zero
6
6
  * new cards (empty-text block — still produced, so v2 suppression holds);
@@ -613,14 +613,14 @@ describe("memoryV3Injector — frozen net-new cards", () => {
613
613
  });
614
614
  });
615
615
 
616
- // ─── ephemeral spotlight ────────────────────────────────────────────────────
616
+ // ─── per-turn spotlight ─────────────────────────────────────────────────────
617
617
 
618
618
  describe("memoryV3SpotlightInjector — ephemeral section spotlight", () => {
619
619
  const sectionA = section("page-a", "Alpha", "alpha section text");
620
620
  const sectionB = section("page-b", "Beta", "beta section text");
621
621
  const sectionC = section("page-c", "Gamma", "gamma section text");
622
622
 
623
- test("renders selected finder hits' matched sections right after the memory cards", async () => {
623
+ test("renders selected finder hits' matched sections as an after-memory-prefix spotlight", async () => {
624
624
  liveEnabled = true;
625
625
  turnResults.set(0, result(["page-a", "page-b"], [["page-a", sectionA]]));
626
626
 
@@ -36,6 +36,28 @@ describe("renderCardsBlockInner", () => {
36
36
  test("empty card list renders the empty string (no header-only block)", () => {
37
37
  expect(renderCardsBlockInner([])).toBe("");
38
38
  });
39
+
40
+ test("skill cards get a one-shot catalog hint that is not a concept card", () => {
41
+ const inner = renderCardsBlockInner([
42
+ "# Skill: telegram-setup\nSet up Telegram.",
43
+ "# memory/concepts/page-a.md\nhead a",
44
+ ]);
45
+ expect(inner).toContain(V3_CARDS_INJECTION_HEADER);
46
+ expect(inner).toContain("# Skills\n");
47
+ expect(inner).toContain("assistant plugins search <name>");
48
+ expect(inner).toContain("assistant skills search <name>");
49
+ expect(inner).toContain("currently in the workspace");
50
+ expect(inner.indexOf("# Skills")).toBeLessThan(inner.indexOf("# Skill:"));
51
+ expect(inner.startsWith(`${V3_CARDS_INJECTION_HEADER}\n\n# Skills\n`)).toBe(
52
+ true,
53
+ );
54
+ });
55
+
56
+ test("concept-only cards omit the skill catalog hint", () => {
57
+ const inner = renderCardsBlockInner(["# memory/concepts/page-a.md\nhead a"]);
58
+ expect(inner).not.toContain("# Skills\n");
59
+ expect(inner).not.toContain("assistant plugins search");
60
+ });
39
61
  });
40
62
 
41
63
  describe("renderSpotlightInner", () => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The memory-v3 {@link Injector}s: frozen net-new cards + ephemeral spotlight.
2
+ * The memory-v3 {@link Injector}s: frozen net-new cards + per-turn spotlight.
3
3
  *
4
4
  * Two injectors share one orchestration result per turn (memoized via
5
5
  * {@link observeTurnOnce} so re-entry assemblies — overflow convergence,
@@ -30,15 +30,18 @@
30
30
  * frozen cards still ride history).
31
31
  *
32
32
  * - {@link memoryV3SpotlightInjector} (id `memory-v3-spotlight`,
33
- * `after-memory-prefix`): the EPHEMERAL layer. Renders the top `spotlight.n`
34
- * selected finder hits' matched sections, plus the previous
33
+ * `after-memory-prefix`): the per-turn spotlight layer. Renders the top
34
+ * `spotlight.n` selected finder hits' matched sections, plus the previous
35
35
  * `spotlight.windowTurns` turns' entries from an in-memory per-conversation
36
36
  * ring (a daemon restart simply re-warms it), as a `<memory_spotlight>`
37
- * block spliced immediately after the `<memory>` cards block (so the two
38
- * memory layers sit adjacent in the prefix, ahead of the user's message
39
- * text). Assembly strip-and-replaces this block every turn (scoped to this
40
- * block id only); it is never persisted to metadata, so the frozen card
41
- * prefix it follows stays byte-stable and cached regardless.
37
+ * block. Runtime assembly splices the block onto the current user message
38
+ * immediately after any frozen `<memory>` cards. The user-prompt-submit
39
+ * hook persists the wrapped text under `metadata.memoryV3SpotlightBlock`;
40
+ * `conversation.ts` rehydrates it on load. Historical user messages keep
41
+ * the spotlight they were sent with, so the provider prefix through those
42
+ * messages stays byte-identical. Mid-turn re-entry and post-compact
43
+ * tail-strip the current tail before splicing a fresh spotlight so the
44
+ * block does not double-stack.
42
45
  *
43
46
  * Gating: `memory.v3.live` (config) runs orchestration and attaches blocks;
44
47
  * with it off, no orchestration runs and nothing is attached.
@@ -442,13 +445,9 @@ export const memoryV3SpotlightInjector: Injector = {
442
445
  return {
443
446
  id: MEMORY_V3_SPOTLIGHT_BLOCK_ID,
444
447
  text: wrapMemorySpotlightBlock(renderSpotlightInner(window)),
445
- // Splices right after the `<memory>` cards block (this injector's
446
- // order 1001 runs after the cards' 1000, and `countMemoryPrefixBlocks`
447
- // counts the cards `<memory>` block but not `<memory_spotlight>`, so the
448
- // spotlight lands immediately after the cards rather than at the user
449
- // tail). Cache-neutral: the block is strip-and-replaced from prior
450
- // messages by block id every turn regardless of placement, so the
451
- // frozen card prefix stays byte-stable and cached.
448
+ // Immediately after frozen `<memory>` cards. `countMemoryPrefixBlocks`
449
+ // counts `<memory>` but not `<memory_spotlight>`, so this lands
450
+ // between the cards and NOW.md / user text.
452
451
  placement: "after-memory-prefix",
453
452
  };
454
453
  } catch (err) {
@@ -190,6 +190,18 @@ describe("parseCardSections / filterPrunedCardSections", () => {
190
190
  expect(parsed.sections[1]!.text).toBe(card("page-b"));
191
191
  });
192
192
 
193
+ test("skill catalog hint is a non-card piece and leaves the read-affordance preamble intact", () => {
194
+ const mixed = renderCardsBlockInner([
195
+ "# Skill: telegram-setup\nSet up Telegram.",
196
+ card("page-a"),
197
+ ]);
198
+ const parsed = parseCardSections(mixed);
199
+ expect(parsed.preamble).toBe(V3_CARDS_INJECTION_HEADER);
200
+ expect(parsed.sections.map((s) => s.slug)).toEqual(["page-a"]);
201
+ expect(parsed.pieces.some((piece) => piece.kind === "other")).toBe(true);
202
+ expect(mixed).toContain("assistant plugins search <name>");
203
+ });
204
+
193
205
  test("no pruned slug present → returns the SAME reference (no-op)", () => {
194
206
  expect(filterPrunedCardSections(inner, new Set(["page-z"]))).toBe(inner);
195
207
  expect(filterPrunedCardSections(inner, new Set())).toBe(inner);
@@ -228,8 +240,14 @@ describe("parseCardSections / filterPrunedCardSections", () => {
228
240
  expect(parsed.sections.map((s) => s.slug)).toEqual(["page-a", "page-b"]);
229
241
  // page-a's section stops AT the capability header — it must not absorb it.
230
242
  expect(parsed.sections[0]!.text).toBe(card("page-a"));
231
- expect(parsed.pieces.map((p) => p.kind)).toEqual(["card", "other", "card"]);
232
- expect(parsed.pieces[1]!.text).toBe(CAPABILITY_CHUNK);
243
+ expect(parsed.pieces.map((p) => p.kind)).toEqual([
244
+ "other",
245
+ "card",
246
+ "other",
247
+ "card",
248
+ ]);
249
+ expect(parsed.pieces[0]!.text).toContain("assistant plugins search <name>");
250
+ expect(parsed.pieces[2]!.text).toBe(CAPABILITY_CHUNK);
233
251
  });
234
252
 
235
253
  test("pruning a concept card never swallows a trailing capability chunk", () => {
@@ -1,6 +1,10 @@
1
1
  import { wrapMemoryBlock } from "../memory-marker.js";
2
+ import { SKILLS_INJECTION_CATALOG_HINT } from "../substrate/skill-content.js";
2
3
  import { Section, Slug } from "./types.js";
3
4
 
5
+ /** Own `# ` chunk so prune treats the catalog hint as a non-card piece. */
6
+ const SKILLS_CATALOG_HINT_CHUNK = `# Skills\n${SKILLS_INJECTION_CATALOG_HINT}`;
7
+
4
8
  /**
5
9
  * Leading instruction line of the frozen card block — byte-identical to v2's
6
10
  * `INJECTION_HEADER` (`memory/v2/injection.ts`) so the read affordance the
@@ -23,7 +27,12 @@ export function renderCardsBlockInner(cards: string[]): string {
23
27
  if (cards.length === 0) {
24
28
  return "";
25
29
  }
26
- return [V3_CARDS_INJECTION_HEADER, ...cards].join("\n\n");
30
+ const parts = [V3_CARDS_INJECTION_HEADER];
31
+ if (cards.some((card) => card.startsWith("# Skill:"))) {
32
+ parts.push(SKILLS_CATALOG_HINT_CHUNK);
33
+ }
34
+ parts.push(...cards);
35
+ return parts.join("\n\n");
27
36
  }
28
37
 
29
38
  /**
@@ -23,13 +23,24 @@ export const MEMORY_V3_BLOCK_ID = "memory-v3" as const;
23
23
  export const MEMORY_V3_COMMIT_META_KEY = "memoryV3Commit" as const;
24
24
 
25
25
  /**
26
- * Injection-block id for the v3 ephemeral `<memory_spotlight>` block (the
27
- * current window's matched sections, re-rendered at the user tail each turn).
28
- * Distinct from {@link MEMORY_V3_BLOCK_ID}: the spotlight never participates
29
- * in v2 suppression and is never persisted to message metadata.
26
+ * Injection-block id for the v3 per-turn `<memory_spotlight>` block (the
27
+ * current window's matched sections). Distinct from {@link MEMORY_V3_BLOCK_ID}:
28
+ * the spotlight never participates in v2 suppression. Each turn's block stays
29
+ * on the user message that was sent with it.
30
30
  */
31
31
  export const MEMORY_V3_SPOTLIGHT_BLOCK_ID = "memory-v3-spotlight" as const;
32
32
 
33
+ /**
34
+ * Message-metadata key for the wrapped `<memory_spotlight>` block persisted
35
+ * on the user row that received it. `loadFromDb` rehydrates from this key so
36
+ * historical turns keep the spotlight they were sent with. Kept as a
37
+ * literal in `messageMetadataSchema` (same pattern as
38
+ * `memoryV3InjectedBlock`) so the storage schema does not import the
39
+ * memory feature.
40
+ */
41
+ export const MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY =
42
+ "memoryV3SpotlightBlock" as const;
43
+
33
44
  /**
34
45
  * A single section of a page: the lead (text before the first `## heading`,
35
46
  * ordinal 0) or a heading-delimited block. Over-long sections are split into
@@ -37,6 +37,9 @@ const unifiedTurnContextInjector: Injector = {
37
37
  clientOs: ctx.clientOs,
38
38
  visibleApp: ctx.visibleApp,
39
39
  channelName: ctx.channelName,
40
+ // The chat and thread this turn arrived through, from its trust snapshot.
41
+ chatId: ctx.trust.requesterChatId,
42
+ threadId: ctx.trust.sourceThreadId,
40
43
  actorContext: ctx.actorContext,
41
44
  configuredUserTimezone: ctx.configuredUserTimezone,
42
45
  clientTimezone: ctx.clientTimezone,
@@ -33,6 +33,19 @@ export interface UnifiedTurnContextOptions {
33
33
  pluginName?: string;
34
34
  } | null;
35
35
  channelName?: string;
36
+ /**
37
+ * Channel-native id of the chat this turn arrived through (a Slack `D…` or
38
+ * `C…` id, a Telegram chat id). Rendered as the `chat_id:` line for channel
39
+ * turns only, so a request that needs the chat's own history starts from
40
+ * the id the turn already carries instead of discovering it.
41
+ */
42
+ chatId?: string | null;
43
+ /**
44
+ * Channel-native thread id of the message that started this turn, when it
45
+ * arrived in a thread (a Slack `thread_ts`). Rendered as the `thread_id:`
46
+ * line beside `chat_id:`; absent for an un-threaded turn.
47
+ */
48
+ threadId?: string | null;
36
49
  actorContext?: InboundActorContext | null;
37
50
  configuredUserTimezone?: string | null;
38
51
  clientTimezone?: string | null;
@@ -130,6 +143,17 @@ export function buildUnifiedTurnContextBlock(
130
143
  );
131
144
  }
132
145
 
146
+ // Where the turn is: the chat, and the thread within it when there is one.
147
+ // Channel turns only; an app turn has no external chat.
148
+ if (options.channelName && options.channelName !== "vellum") {
149
+ if (options.chatId) {
150
+ lines.push(`chat_id: ${sanitizeInlineContextValue(options.chatId)}`);
151
+ }
152
+ if (options.threadId) {
153
+ lines.push(`thread_id: ${sanitizeInlineContextValue(options.threadId)}`);
154
+ }
155
+ }
156
+
133
157
  // Actor identity and trust fields — only for non-guardian turns.
134
158
  if (options.actorContext) {
135
159
  const ctx = options.actorContext;
@@ -93,13 +93,25 @@ const PluginStdioServerSchema = z.object({
93
93
  cwd: z.string().optional(),
94
94
  });
95
95
 
96
+ /**
97
+ * Remote MCP entry. Agent Plugins 1.0.0 requires `type` and names no
98
+ * default. MCP's current remote transport is Streamable HTTP, so this
99
+ * host fills omitted `type` and the Claude-style `http` alias as
100
+ * `streamable-http`. `sse` stays explicit.
101
+ */
96
102
  const PluginHttpServerSchema = z.object({
97
- type: z.enum(["streamable-http", "sse"]),
103
+ type: z
104
+ .union([
105
+ z.literal("streamable-http"),
106
+ z.literal("sse"),
107
+ z.literal("http").transform(() => "streamable-http" as const),
108
+ ])
109
+ .default("streamable-http"),
98
110
  url: z.string().min(1),
99
111
  headers: z.record(z.string(), z.string()).optional(),
100
112
  });
101
113
 
102
- const PluginMcpServerSchema = z.discriminatedUnion("type", [
114
+ const PluginMcpServerSchema = z.union([
103
115
  PluginStdioServerSchema,
104
116
  PluginHttpServerSchema,
105
117
  ]);