@vellumai/assistant 0.11.8 → 0.11.9-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) 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__/guardian-card-withdrawal.test.ts +0 -22
  64. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  65. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  66. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  67. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  68. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  69. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  70. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  71. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  72. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  73. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  74. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  75. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  76. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  77. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  78. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  79. package/src/__tests__/openai-provider.test.ts +5 -5
  80. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  81. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  82. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  83. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  84. package/src/__tests__/process-message-display-content.test.ts +20 -9
  85. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  86. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  87. package/src/__tests__/reaction-persistence.test.ts +22 -198
  88. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  89. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  90. package/src/__tests__/skill-load-tool.test.ts +27 -0
  91. package/src/__tests__/skills.test.ts +34 -1
  92. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  93. package/src/__tests__/terminal-tools.test.ts +9 -0
  94. package/src/__tests__/thread-backfill.test.ts +5 -3
  95. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  96. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  97. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  98. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  99. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  100. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  101. package/src/agent/attachments.ts +13 -2
  102. package/src/agent/loop.ts +3 -19
  103. package/src/api/README.md +9 -5
  104. package/src/api/index.ts +9 -8
  105. package/src/api/package.json +1 -0
  106. package/src/api/responses/conversation-message.ts +8 -0
  107. package/src/api/responses/home.ts +7 -18
  108. package/src/api/surfaces.ts +114 -7
  109. package/src/approvals/AGENTS.md +1 -1
  110. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  111. package/src/channels/__tests__/types.test.ts +22 -1
  112. package/src/channels/gateway-guardian-requests.ts +0 -22
  113. package/src/channels/types.ts +8 -6
  114. package/src/cli/__tests__/catalog-search-help.test.ts +12 -0
  115. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  116. package/src/cli/commands/channels/index.help.ts +15 -1
  117. package/src/cli/commands/channels/index.ts +6 -2
  118. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  119. package/src/cli/commands/db/index.help.ts +1 -1
  120. package/src/cli/commands/db/status.ts +172 -1
  121. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  122. package/src/cli/commands/platform/connect.ts +14 -5
  123. package/src/cli/commands/plugins.help.ts +6 -5
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  125. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  126. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  127. package/src/cli/lib/bundled-marketplace.json +14 -0
  128. package/src/cli/lib/install-from-github.ts +9 -5
  129. package/src/cli/lib/install-from-platform.ts +12 -1
  130. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  131. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  132. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  133. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  134. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  135. package/src/config/call-site-defaults.ts +4 -1
  136. package/src/config/feature-flag-registry.json +21 -4
  137. package/src/config/schemas/memory-v3.ts +4 -3
  138. package/src/context/strip-injections.ts +17 -56
  139. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  140. package/src/conversations/message-consolidation.ts +4 -3
  141. package/src/credential-health/credential-health-service.ts +130 -0
  142. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  143. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  144. package/src/daemon/conversation-error.ts +2 -0
  145. package/src/daemon/conversation-messaging.ts +195 -47
  146. package/src/daemon/conversation-process.ts +20 -5
  147. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  148. package/src/daemon/conversation-store.ts +159 -11
  149. package/src/daemon/conversation-tool-setup.ts +9 -2
  150. package/src/daemon/conversation.ts +290 -19
  151. package/src/daemon/dictation-text-processing.ts +2 -7
  152. package/src/daemon/handlers/config-channels.ts +33 -3
  153. package/src/daemon/handlers/shared.ts +9 -0
  154. package/src/daemon/port-oversized-content.test.ts +117 -0
  155. package/src/daemon/port-oversized-content.ts +110 -0
  156. package/src/daemon/process-message.ts +87 -16
  157. package/src/daemon/reaction-record.test.ts +23 -11
  158. package/src/daemon/reaction-record.ts +6 -9
  159. package/src/documents/document-store.ts +1 -5
  160. package/src/home/conversation-starter-validation.ts +1 -5
  161. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  162. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  163. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  164. package/src/live-voice/live-voice-photo.ts +544 -61
  165. package/src/live-voice/live-voice-session.ts +6 -2
  166. package/src/live-voice/protocol.ts +20 -1
  167. package/src/messaging/provider-message-metadata.ts +40 -1
  168. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  169. package/src/messaging/providers/channel-transport.ts +28 -0
  170. package/src/messaging/providers/discord/send.ts +3 -2
  171. package/src/messaging/providers/slack/api.ts +11 -1
  172. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  173. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  174. package/src/messaging/providers/slack/send.test.ts +58 -0
  175. package/src/messaging/providers/slack/send.ts +4 -4
  176. package/src/messaging/providers/slack/transport.ts +28 -2
  177. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  178. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  179. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  180. package/src/messaging/reaction-envelopes.test.ts +73 -0
  181. package/src/messaging/reaction-envelopes.ts +33 -5
  182. package/src/messaging/read-provider-metadata.ts +4 -3
  183. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  184. package/src/monitoring/recovery/db.ts +35 -0
  185. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  186. package/src/monitoring/recovery/run-recovery.ts +6 -2
  187. package/src/monitoring/recovery/stale-processing.ts +3 -20
  188. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  189. package/src/notifications/AGENTS.md +2 -2
  190. package/src/notifications/README.md +2 -2
  191. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  192. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  193. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  194. package/src/notifications/access-request-copy.ts +126 -114
  195. package/src/notifications/adapters/discord.ts +9 -5
  196. package/src/notifications/adapters/shared.ts +17 -4
  197. package/src/notifications/adapters/slack.ts +14 -4
  198. package/src/notifications/adapters/telegram.ts +8 -4
  199. package/src/notifications/approval-card-data.ts +5 -2
  200. package/src/notifications/assistant-reply-producer.ts +7 -7
  201. package/src/notifications/broadcaster.ts +57 -25
  202. package/src/notifications/conversation-pairing.ts +68 -2
  203. package/src/notifications/copy-composer.ts +13 -32
  204. package/src/notifications/decision-engine.ts +56 -237
  205. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  206. package/src/notifications/guardian-feed-projection.ts +0 -15
  207. package/src/notifications/guardian-question-mode.ts +147 -6
  208. package/src/notifications/notification-utils.ts +117 -1
  209. package/src/permissions/confirmation-guardian-request.ts +2 -2
  210. package/src/permissions/prompter.ts +1 -1
  211. package/src/persistence/conversation-crud.ts +93 -14
  212. package/src/persistence/conversation-queries.ts +144 -17
  213. package/src/persistence/conversation-types.ts +39 -5
  214. package/src/persistence/delivery-crud.ts +224 -28
  215. package/src/persistence/delivery-status.ts +21 -2
  216. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  217. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  218. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  219. package/src/persistence/schema/conversations.ts +71 -2
  220. package/src/persistence/schema-contract.test.ts +78 -0
  221. package/src/persistence/schema-contract.ts +96 -0
  222. package/src/persistence/steps.ts +4 -0
  223. package/src/platform/client.ts +55 -0
  224. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  225. package/src/plugins/defaults/injector-order.ts +2 -1
  226. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  227. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  228. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  229. package/src/plugins/defaults/memory/injectors.ts +1 -1
  230. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  231. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  232. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  233. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  234. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  235. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  236. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  237. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  238. package/src/plugins/mcp-servers.ts +14 -2
  239. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  240. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  241. package/src/providers/anthropic/client.ts +21 -24
  242. package/src/providers/call-site-routing.ts +5 -4
  243. package/src/providers/connection-resolution.ts +29 -1
  244. package/src/providers/content-block-size.test.ts +82 -0
  245. package/src/providers/content-block-size.ts +99 -0
  246. package/src/providers/file-block-text.test.ts +64 -0
  247. package/src/providers/file-block-text.ts +28 -0
  248. package/src/providers/gemini/client.ts +12 -7
  249. package/src/providers/inference/auth.ts +6 -6
  250. package/src/providers/media-resolve.ts +14 -0
  251. package/src/providers/model-catalog.ts +31 -34
  252. package/src/providers/openai/chat-completions-provider.ts +9 -25
  253. package/src/providers/openai/responses-provider.ts +11 -18
  254. package/src/providers/registry.ts +10 -7
  255. package/src/providers/routing-identity.ts +2 -1
  256. package/src/providers/types.ts +1 -4
  257. package/src/providers/vellum-model-routing.ts +3 -2
  258. package/src/runtime/AGENTS.md +40 -15
  259. package/src/runtime/approval-message-composer.ts +1 -6
  260. package/src/runtime/assistant-event-hub.ts +2 -39
  261. package/src/runtime/channel-approval-types.ts +1 -5
  262. package/src/runtime/channel-reply-delivery.ts +182 -115
  263. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  264. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  265. package/src/runtime/channel-retry-sweep.ts +102 -1
  266. package/src/runtime/finalize-event-delivery.ts +4 -4
  267. package/src/runtime/guardian-action-message-composer.ts +1 -4
  268. package/src/runtime/guardian-reply-router.ts +4 -75
  269. package/src/runtime/http-router.ts +1 -5
  270. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  271. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  272. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  273. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  274. package/src/runtime/routes/acp-routes.ts +1 -1
  275. package/src/runtime/routes/canned-reply-release.ts +19 -9
  276. package/src/runtime/routes/channel-route-shared.ts +0 -39
  277. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  278. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  279. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  280. package/src/runtime/routes/conversation-routes.ts +138 -59
  281. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  282. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  283. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  284. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  286. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  287. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  288. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  289. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  290. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  291. package/src/runtime/routes/index.ts +2 -0
  292. package/src/runtime/routes/platform-routes.ts +99 -10
  293. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  294. package/src/runtime/sync/resource-sync-events.ts +14 -37
  295. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  296. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  297. package/src/tools/client-os.ts +11 -1
  298. package/src/tools/host-filesystem/edit.ts +2 -2
  299. package/src/tools/host-filesystem/read.ts +2 -2
  300. package/src/tools/host-filesystem/transfer.ts +2 -2
  301. package/src/tools/host-filesystem/write.ts +2 -2
  302. package/src/tools/host-terminal/host-shell.ts +2 -2
  303. package/src/tools/skills/load.ts +1 -1
  304. package/src/tools/terminal/safe-env.ts +4 -0
  305. package/src/tools/tool-input-schemas.ts +2 -0
  306. package/src/tools/tool-manifest.ts +2 -0
  307. package/src/tools/ui-surface/definitions.ts +4 -3
  308. package/src/tools/watch/watch-retro-report.ts +205 -0
  309. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  310. package/src/watch/watch-retro.ts +190 -77
  311. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  312. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  313. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  314. package/src/workspace/migrations/registry.ts +4 -0
  315. package/docker-kata-pip-chroot.sh +0 -22
  316. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  317. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  318. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -15,7 +15,6 @@ import {
15
15
  type MessageAttachmentInput,
16
16
  } from "../agent/attachments.js";
17
17
  import { optimizeImageForTransport } from "../agent/image-optimize.js";
18
- import { createUserMessage } from "../agent/message-types.js";
19
18
  import type { AssistantEvent } from "../api/index.js";
20
19
  import type {
21
20
  TurnChannelContext,
@@ -27,6 +26,10 @@ import {
27
26
  parseInterfaceId,
28
27
  } from "../channels/types.js";
29
28
  import { parseImageDimensions } from "../context/image-dimensions.js";
29
+ import {
30
+ type ProviderMessageMetadata,
31
+ providerMessageMetadataSchema,
32
+ } from "../messaging/provider-message-metadata.js";
30
33
  import {
31
34
  buildSlackTimezoneMetadata,
32
35
  type SlackMessageMetadata,
@@ -68,11 +71,16 @@ import {
68
71
  } from "../providers/types.js";
69
72
  import type { AuthContext } from "../runtime/auth/types.js";
70
73
  import { getLogger } from "../util/logger.js";
71
- import { withSqliteRetry } from "../util/sqlite-retry.js";
72
74
  import type { MessageQueue } from "./conversation-queue-manager.js";
73
75
  import type { SlackInboundMessageMetadata } from "./handlers/shared.js";
74
76
  import type { UserMessageAttachment } from "./message-protocol.js";
75
77
  import type { ConversationTransportMetadata } from "./message-types/conversations.js";
78
+ import {
79
+ assembleUserContentBlocks,
80
+ offloadLinkPlan,
81
+ offloadOversizedText,
82
+ type PortOversizedContext,
83
+ } from "./port-oversized-content.js";
76
84
  import type { TrustContext } from "./trust-context-types.js";
77
85
  import { restingTrust } from "./trust-context-types.js";
78
86
  import { postUnsendableImageNotice } from "./unsendable-image-notice.js";
@@ -217,6 +225,8 @@ export interface MessagingConversationContext {
217
225
  messages: Message[];
218
226
  isProcessing(): boolean;
219
227
  setProcessing(value: boolean): void;
228
+ acquireProcessingFenced(): Promise<number | null>;
229
+ releaseProcessing(owner: number): boolean;
220
230
  abortController: AbortController | null;
221
231
  currentRequestId?: string;
222
232
  readonly queue: MessageQueue;
@@ -267,11 +277,8 @@ export async function serializePersistedUserMessageContent(
267
277
  displayContent: string | undefined,
268
278
  attachments: MessageAttachmentInput[],
269
279
  ): Promise<string> {
270
- return serializeUserContentBlocks(
271
- content,
272
- displayContent,
273
- await attachmentsToContentBlocks(attachments),
274
- );
280
+ const attachmentBlocks = await attachmentsToContentBlocks(attachments);
281
+ return serializeUserContentBlocks(content, displayContent, attachmentBlocks);
275
282
  }
276
283
 
277
284
  /**
@@ -501,17 +508,19 @@ async function prepareUserAttachmentReferences(
501
508
  * backstop, and swallowed on failure: cleaning up must never mask the error
502
509
  * that caused it.
503
510
  */
504
- function discardAttemptAttachments(attachmentIds: string[]): void {
511
+ function discardAttemptAttachments(attachmentIds: string[]): string[] {
505
512
  if (attachmentIds.length === 0) {
506
- return;
513
+ return [];
507
514
  }
508
515
  try {
509
516
  deleteOrphanAttachments(attachmentIds);
517
+ return [];
510
518
  } catch (err) {
511
519
  log.warn(
512
520
  { err, attachmentIds },
513
521
  "Could not discard the attachments of a failed persist attempt",
514
522
  );
523
+ return attachmentIds;
515
524
  }
516
525
  }
517
526
 
@@ -748,6 +757,37 @@ export function buildSlackMetaForPersistence(params: {
748
757
  return writeSlackMetadata(slackMeta);
749
758
  }
750
759
 
760
+ /**
761
+ * Build the neutral channel envelope persisted under the `providerMeta` key
762
+ * on a user message's `metadata` JSON, the non-Slack counterpart of
763
+ * {@link buildSlackMetaForPersistence}. Returns `null` (do not include the
764
+ * key) when the turn channel does not match the envelope's own `source`, so
765
+ * a stale plumbing field can never tag a row with another channel's
766
+ * identity. Slack turns also return `null`: Slack still writes `slackMeta`,
767
+ * which `readProviderMetadata` maps onto the neutral shape on read, and a
768
+ * `providerMeta` key on a Slack row would shadow that richer envelope.
769
+ *
770
+ * TRANSITIONAL: the Slack exclusion exists only while Slack writes its own
771
+ * envelope; do not extend it.
772
+ */
773
+ export function buildProviderMetaForPersistence(params: {
774
+ channelInbound: ProviderMessageMetadata | undefined;
775
+ turnChannel: string | undefined;
776
+ }): string | null {
777
+ const inbound = params.channelInbound;
778
+ if (!inbound) {
779
+ return null;
780
+ }
781
+ if (params.turnChannel !== inbound.source || inbound.source === "slack") {
782
+ return null;
783
+ }
784
+ const parsed = providerMessageMetadataSchema.safeParse(inbound);
785
+ if (!parsed.success) {
786
+ return null;
787
+ }
788
+ return JSON.stringify(parsed.data);
789
+ }
790
+
751
791
  // ── EnqueueMessageOptions ────────────────────────────────────────────
752
792
 
753
793
  /** Options for `enqueueMessage`. Only `content` is required; everything
@@ -961,6 +1001,30 @@ export interface PersistMessageOptions {
961
1001
  * exactly the one retention has to be able to stub.
962
1002
  */
963
1003
  sightFrameAttachmentIds?: readonly string[];
1004
+ /**
1005
+ * Answered synchronously at the top of every insert attempt, immediately
1006
+ * before that attempt's statement. False aborts the persist with a
1007
+ * `MessageInsertPreconditionError` and inserts nothing.
1008
+ *
1009
+ * For a caller whose right to write can lapse while this persist runs.
1010
+ * Everything ahead of the insert (attachment materialization, content
1011
+ * building) is awaited, and the insert retries itself on contention across
1012
+ * an awaited backoff, so a caller that answered before calling has only
1013
+ * answered for the moment it called. Threaded down to the insert rather
1014
+ * than checked here, so each attempt asks for itself.
1015
+ */
1016
+ insertPrecondition?: () => boolean;
1017
+ /**
1018
+ * Told the ids this attempt materialized for itself and then could not
1019
+ * delete, so a caller that owns the cleanup can come back to them.
1020
+ *
1021
+ * An attachment already linked to another conversation is CLONED into this
1022
+ * one under a fresh id, and only this function knows that id. A caller
1023
+ * retrying the delete under the id it handed in would reclaim nothing: that
1024
+ * row is still linked where it came from, and the clone nobody names
1025
+ * survives every pass.
1026
+ */
1027
+ onUndiscardedAttachments?: (attachmentIds: readonly string[]) => void;
964
1028
  }
965
1029
 
966
1030
  // ── persistUserMessage ───────────────────────────────────────────────
@@ -1002,38 +1066,46 @@ export async function persistUserMessage(
1002
1066
  ctx.currentRequestId = reqId;
1003
1067
  ctx.abortController = new AbortController();
1004
1068
 
1069
+ let owner: number | null = null;
1005
1070
  try {
1006
- // `setProcessing(true)` persists the flag and can throw (e.g.
1007
- // SQLITE_BUSY). Keeping it inside the try ensures a failure here unwinds
1008
- // the request-id/abort bookkeeping below rather than stranding it.
1009
- //
1010
- // This is the first DB write on the message-send path — it precedes the
1011
- // (already retried) message insert — so under transient WAL contention it
1012
- // must retry too, or the turn dies here before the insert is ever reached.
1013
- // `setProcessing` reverts its in-memory flag when its persist throws, so
1014
- // each attempt is safe to re-run.
1015
- await withSqliteRetry(() => ctx.setProcessing(true), {
1016
- op: "conversation:setProcessing",
1017
- context: { conversationId: ctx.conversationId },
1018
- });
1071
+ // Taking the flag rather than setting it: the read and the take are one
1072
+ // step, so nothing can claim it in between, and the hold is released only
1073
+ // by the claim that took it. Its advisory mirror write is detached, which
1074
+ // is what closes the window a retry around a strict set used to open. That
1075
+ // retry re-ran the set after an awaited backoff, and the set reverted the
1076
+ // in-memory flag before rethrowing, so the conversation read idle for the
1077
+ // length of the sleep and anything waiting for idle could take the flag
1078
+ // while this turn was still starting.
1079
+ // Fenced: the flag is taken and its marker is durable before anything
1080
+ // this turn writes, because a reconnecting client and the out-of-process
1081
+ // retrospective worker read that marker to decide a turn is live. Null is
1082
+ // a conversation that belongs to someone else, whether it was already held
1083
+ // or was claimed away while the marker landed. A throw is the marker
1084
+ // refusing to persist, with the claim already given back.
1085
+ owner = await ctx.acquireProcessingFenced();
1086
+ if (owner === null) {
1087
+ throw new Error(CONVERSATION_BUSY_MESSAGE);
1088
+ }
1019
1089
  const result = await persistQueuedMessageBody(ctx, {
1020
1090
  ...options,
1021
1091
  attachments,
1022
1092
  requestId: reqId,
1023
1093
  });
1024
1094
  if (result.deduplicated) {
1025
- ctx.setProcessing(false);
1095
+ ctx.releaseProcessing(owner);
1026
1096
  ctx.abortController = null;
1027
1097
  ctx.currentRequestId = undefined;
1028
1098
  }
1029
1099
  return result;
1030
1100
  } catch (err) {
1031
- // Clear the flag, but never let a clear failure mask the original error
1032
- // or skip the bookkeeping reset. `setProcessing(false)` releases in memory
1033
- // regardless of its advisory mirror write, so the guard here is purely
1034
- // defensive against future changes.
1101
+ // Release this turn's own hold, but never let a failure there mask the
1102
+ // original error or skip the bookkeeping reset. A hold another caller
1103
+ // claimed since is left alone, and one this call never took is nothing to
1104
+ // release.
1035
1105
  try {
1036
- ctx.setProcessing(false);
1106
+ if (owner !== null) {
1107
+ ctx.releaseProcessing(owner);
1108
+ }
1037
1109
  } catch (clearErr) {
1038
1110
  log.error(
1039
1111
  { err: clearErr, conversationId: ctx.conversationId },
@@ -1088,6 +1160,7 @@ export async function persistQueuedMessageBody(
1088
1160
  // exactly the shape a link-aware delete reads as collectible, so the cleanup
1089
1161
  // must stop at the insert.
1090
1162
  let preparedAttachments: PreparedUserAttachment[] = [];
1163
+ let portedAttachmentIds: string[] = [];
1091
1164
  let messageInserted = false;
1092
1165
 
1093
1166
  try {
@@ -1119,11 +1192,13 @@ export async function persistQueuedMessageBody(
1119
1192
  // never reported.
1120
1193
  const {
1121
1194
  slackInbound: rawSlackInbound,
1195
+ channelInbound: rawChannelInbound,
1122
1196
  scripted: rawScriptedFromMetadata,
1123
1197
  clientOsFromRequest: _rawClientOsFromRequest,
1124
1198
  ...metadataWithoutSlackInbound
1125
1199
  } = (metadata ?? {}) as Record<string, unknown> & {
1126
1200
  slackInbound?: SlackInboundMessageMetadata;
1201
+ channelInbound?: ProviderMessageMetadata;
1127
1202
  scripted?: unknown;
1128
1203
  clientOsFromRequest?: unknown;
1129
1204
  };
@@ -1131,6 +1206,10 @@ export async function persistQueuedMessageBody(
1131
1206
  slackInbound: rawSlackInbound,
1132
1207
  turnChannel: turnCtx?.userMessageChannel,
1133
1208
  });
1209
+ const providerMeta = buildProviderMetaForPersistence({
1210
+ channelInbound: rawChannelInbound,
1211
+ turnChannel: turnCtx?.userMessageChannel,
1212
+ });
1134
1213
 
1135
1214
  // See the `scripted` note on the merged metadata below. Only a real
1136
1215
  // boolean in the bag counts: a stray truthy string must not be read as a
@@ -1210,6 +1289,7 @@ export async function persistQueuedMessageBody(
1210
1289
  ...(clientOsFromRequest ? { clientOsFromRequest: true } : {}),
1211
1290
  ...(imageSourcePaths ? { imageSourcePaths } : {}),
1212
1291
  ...(slackMeta ? { slackMeta } : {}),
1292
+ ...(providerMeta ? { providerMeta } : {}),
1213
1293
  // Scripted-turn marker, forwarded by `turn-events-store` onto
1214
1294
  // `TurnTelemetryEvent.scripted`. Written LAST so it cannot be
1215
1295
  // half-overwritten by the raw metadata spread above.
@@ -1259,22 +1339,42 @@ export async function persistQueuedMessageBody(
1259
1339
  preparedAttachments,
1260
1340
  );
1261
1341
  const sightFrameRenames = sightFrameBlockRenames(sightFrameEntries);
1262
- // The blocks are built from the ids the caller held, so a cloned frame is
1263
- // pointed at its stored row before the message reaches the live history.
1342
+
1343
+ const portCtx: PortOversizedContext = {
1344
+ conversationId: ctx.conversationId,
1345
+ conversationCreatedAt,
1346
+ };
1347
+ const persistText = displayContent !== undefined ? displayContent : content;
1348
+ const persistOffload = await offloadOversizedText(persistText, portCtx);
1349
+ const liveOffload =
1350
+ persistText === content
1351
+ ? persistOffload
1352
+ : await offloadOversizedText(content, portCtx);
1353
+ const attachmentBlocks = preparedAttachments.map((p) => p.block);
1354
+ const persistBlocks = assembleUserContentBlocks(
1355
+ persistOffload.text,
1356
+ attachmentBlocks,
1357
+ persistOffload.fileBlock,
1358
+ );
1359
+ const liveBlocks = assembleUserContentBlocks(
1360
+ liveOffload.text,
1361
+ attachmentBlocks,
1362
+ liveOffload.fileBlock,
1363
+ );
1364
+ const offloadPlan = offloadLinkPlan(
1365
+ persistOffload.attachmentId,
1366
+ liveOffload.attachmentId,
1367
+ );
1368
+ portedAttachmentIds = offloadPlan.linkIds;
1369
+
1370
+ // Workspace-ref blocks (including offloaded oversized text) so the live
1371
+ // turn matches persist: video and huge strings stay off the prompt.
1264
1372
  const cleanMessage = restampInlineAttachmentIds(
1265
- await createUserMessage(content, sentAttachments),
1373
+ { role: "user", content: liveBlocks },
1266
1374
  sightFrameRenames,
1267
1375
  );
1268
1376
 
1269
- // When displayContent is provided (e.g. original text before recording
1270
- // intent stripping), persist that to DB so users see the full message
1271
- // after restart. The in-memory userMessage (sent to the LLM) still uses
1272
- // the stripped content.
1273
- const contentToPersist = serializeUserContentBlocks(
1274
- content,
1275
- displayContent,
1276
- preparedAttachments.map((p) => p.block),
1277
- );
1377
+ const contentToPersist = JSON.stringify(persistBlocks);
1278
1378
  // Composed here rather than with the rest of the metadata above, because
1279
1379
  // materialization is what decides the id each frame is stored under.
1280
1380
  const sightFrameIds = sightFrameTagIds(sightFrameEntries);
@@ -1292,11 +1392,17 @@ export async function persistQueuedMessageBody(
1292
1392
  clientMessageId,
1293
1393
  id: requestId,
1294
1394
  ...(skipIndexing ? { skipIndexing: true } : {}),
1395
+ // Handed down rather than answered here: the insert retries itself on
1396
+ // contention, so the question belongs where each attempt can ask it.
1397
+ ...(options.insertPrecondition
1398
+ ? { insertPrecondition: options.insertPrecondition }
1399
+ : {}),
1295
1400
  },
1296
1401
  );
1297
1402
  messageInserted = true;
1298
1403
 
1299
1404
  if (persistedUserMessage.deduplicated) {
1405
+ discardAttemptAttachments(portedAttachmentIds);
1300
1406
  return { id: persistedUserMessage.id, deduplicated: true };
1301
1407
  }
1302
1408
 
@@ -1371,14 +1477,52 @@ export async function persistQueuedMessageBody(
1371
1477
  if (repairedBlocks) {
1372
1478
  updateMessageContent(
1373
1479
  persistedUserMessage.id,
1374
- serializeUserContentBlocks(content, displayContent, repairedBlocks),
1480
+ JSON.stringify(
1481
+ assembleUserContentBlocks(
1482
+ persistOffload.text,
1483
+ repairedBlocks,
1484
+ persistOffload.fileBlock,
1485
+ ),
1486
+ ),
1375
1487
  );
1376
1488
  }
1377
1489
 
1378
- // Persist the resolved paths so history reloads can rebuild the same
1379
- // annotation block the in-memory message carries below.
1490
+ let nextPortPosition =
1491
+ preparedAttachments.reduce((max, p) => Math.max(max, p.position), -1) + 1;
1492
+ for (const attachmentId of portedAttachmentIds) {
1493
+ try {
1494
+ const scopedId = linkAttachmentToMessage(
1495
+ persistedUserMessage.id,
1496
+ attachmentId,
1497
+ nextPortPosition,
1498
+ );
1499
+ nextPortPosition += 1;
1500
+ const storedPath = getFilePathForAttachment(scopedId) ?? undefined;
1501
+ // Display-only offloads stay linked (GC + persisted workspace_ref)
1502
+ // but must not be named on the model-facing list.
1503
+ if (
1504
+ offloadPlan.modelFacingIds.has(attachmentId) &&
1505
+ liveOffload.filename
1506
+ ) {
1507
+ sentAttachments.push({
1508
+ filename: liveOffload.filename,
1509
+ mimeType: "text/plain",
1510
+ data: "",
1511
+ storedPath,
1512
+ });
1513
+ }
1514
+ } catch (err) {
1515
+ log.error(
1516
+ { attachmentId, err },
1517
+ "Failed to link offloaded oversized content attachment",
1518
+ );
1519
+ }
1520
+ }
1521
+
1522
+ // Same list enrichMessageWithSourcePaths sees, so history reload rebuilds
1523
+ // an identical annotation block (prefix-cache parity).
1380
1524
  const attachmentStoredPaths =
1381
- extractAttachmentStoredPaths(attachmentInputs);
1525
+ extractAttachmentStoredPaths(sentAttachments);
1382
1526
  if (attachmentStoredPaths) {
1383
1527
  updateMessageMetadata(persistedUserMessage.id, { attachmentStoredPaths });
1384
1528
  }
@@ -1428,9 +1572,13 @@ export async function persistQueuedMessageBody(
1428
1572
  ctx.messages.pop();
1429
1573
  }
1430
1574
  if (!messageInserted) {
1431
- discardAttemptAttachments(
1432
- attemptCreatedAttachmentIds(attachmentInputs, preparedAttachments),
1433
- );
1575
+ const undiscarded = discardAttemptAttachments([
1576
+ ...attemptCreatedAttachmentIds(attachmentInputs, preparedAttachments),
1577
+ ...portedAttachmentIds,
1578
+ ]);
1579
+ if (undiscarded.length > 0) {
1580
+ options.onUndiscardedAttachments?.(undiscarded);
1581
+ }
1434
1582
  }
1435
1583
  throw err;
1436
1584
  }
@@ -1932,7 +1932,6 @@ export async function processMessage(
1932
1932
  if (trimmedContent.length > 0) {
1933
1933
  const routerResult = await routeGuardianReply({
1934
1934
  messageText: trimmedContent,
1935
- channel: "vellum",
1936
1935
  actor: {
1937
1936
  actorPrincipalId:
1938
1937
  conversation.trustContext?.guardianPrincipalId ?? undefined,
@@ -2124,7 +2123,15 @@ export async function processMessage(
2124
2123
 
2125
2124
  // /compact — force context compaction, persist exchange, return message ID.
2126
2125
  if (slashResult.kind === "compact") {
2127
- conversation.setProcessing(true);
2126
+ // Taken rather than set: `processMessage` never checks the flag itself, so
2127
+ // the idle decision behind this branch was made by a caller several awaits
2128
+ // ago (history scoping, guardian routing, slash resolution all sit
2129
+ // between). A hold taken since belongs to a real turn, and this reports
2130
+ // busy the way that caller's own gate does rather than claiming it away.
2131
+ const compactOwner = await conversation.acquireProcessingFenced();
2132
+ if (compactOwner === null) {
2133
+ throw new Error(CONVERSATION_BUSY_MESSAGE);
2134
+ }
2128
2135
  let persistedCompactMessage = false;
2129
2136
  try {
2130
2137
  const pmTurnCtx = conversation.getTurnChannelContext();
@@ -2195,7 +2202,7 @@ export async function processMessage(
2195
2202
  }
2196
2203
  throw err;
2197
2204
  } finally {
2198
- conversation.setProcessing(false);
2205
+ conversation.releaseProcessing(compactOwner);
2199
2206
  // `kickQueueDrain` never rejects, so a failed drain in this `finally`
2200
2207
  // cannot mask the error the try block is unwinding, and its retry plus
2201
2208
  // sender notification replace a silently stranded queue.
@@ -2205,7 +2212,15 @@ export async function processMessage(
2205
2212
 
2206
2213
  // /clean — strip runtime injections, return message ID. No LLM call.
2207
2214
  if (slashResult.kind === "clean") {
2208
- conversation.setProcessing(true);
2215
+ // Taken rather than set: `processMessage` never checks the flag itself, so
2216
+ // the idle decision behind this branch was made by a caller several awaits
2217
+ // ago (history scoping, guardian routing, slash resolution all sit
2218
+ // between). A hold taken since belongs to a real turn, and this reports
2219
+ // busy the way that caller's own gate does rather than claiming it away.
2220
+ const cleanOwner = await conversation.acquireProcessingFenced();
2221
+ if (cleanOwner === null) {
2222
+ throw new Error(CONVERSATION_BUSY_MESSAGE);
2223
+ }
2209
2224
  let persistedCleanMessage = false;
2210
2225
  try {
2211
2226
  const pmTurnCtx = conversation.getTurnChannelContext();
@@ -2272,7 +2287,7 @@ export async function processMessage(
2272
2287
  }
2273
2288
  throw err;
2274
2289
  } finally {
2275
- conversation.setProcessing(false);
2290
+ conversation.releaseProcessing(cleanOwner);
2276
2291
  // Same never-rejecting drain as the `/compact` branch above.
2277
2292
  await kickQueueDrain(conversation, "loop_complete", "clean_command");
2278
2293
  }
@@ -27,15 +27,13 @@ import {
27
27
  quarantineRefusedExchanges,
28
28
  } from "../context/refusal-quarantine.js";
29
29
  import {
30
+ MEMORY_SPOTLIGHT_MATCHER,
30
31
  NOW_SCRATCHPAD_STRIP_PREFIXES,
31
- stripSpotlightInjections,
32
+ stripTailUserTextBlocksByPrefix,
32
33
  stripUserTextBlocksByPrefix,
33
34
  } from "../context/strip-injections.js";
34
35
  import { getDocumentsForConversation } from "../documents/document-store.js";
35
- import {
36
- readSlackMetadata,
37
- readSlackMetadataFromMessageMetadata,
38
- } from "../messaging/providers/slack/message-metadata.js";
36
+ import { readSlackMetadataFromMessageMetadata } from "../messaging/providers/slack/message-metadata.js";
39
37
  import {
40
38
  compareSlackTs,
41
39
  extractTagLineTexts,
@@ -64,6 +62,7 @@ import {
64
62
  import {
65
63
  MEMORY_V3_BLOCK_ID,
66
64
  MEMORY_V3_COMMIT_META_KEY,
65
+ MEMORY_V3_SPOTLIGHT_BLOCK_ID,
67
66
  } from "../plugins/defaults/memory/v3/types.js";
68
67
  import { getRegisteredInjectors } from "../plugins/injector-registry.js";
69
68
  import type {
@@ -74,9 +73,11 @@ import type {
74
73
  import type { ContentBlock, Message } from "../providers/types.js";
75
74
  import type { TrustClass } from "../runtime/actor-trust-resolver.js";
76
75
  import { resolveCapabilities } from "../runtime/capabilities.js";
76
+ import { trustClassSchema } from "../runtime/trust-class.js";
77
77
  import type { SubagentState } from "../subagent/types.js";
78
78
  import { TERMINAL_STATUSES } from "../subagent/types.js";
79
79
  import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
80
+ import { safeParseRecord } from "../util/json.js";
80
81
  import { getLogger } from "../util/logger.js";
81
82
  import { channelSupportsInlineOptions } from "./channel-ui-capability.js";
82
83
  import { findConversationOrSubagent } from "./conversation-registry.js";
@@ -821,7 +822,8 @@ export function buildChannelCapabilityBlock(
821
822
  caps.supportsVoiceInput &&
822
823
  !isGroupChatType(caps.chatType) &&
823
824
  clientOs !== "macos" &&
824
- clientOs !== "windows"
825
+ clientOs !== "windows" &&
826
+ clientOs !== "linux"
825
827
  ) {
826
828
  return null;
827
829
  }
@@ -849,6 +851,13 @@ export function buildChannelCapabilityBlock(
849
851
  );
850
852
  }
851
853
 
854
+ if (clientOs === "linux") {
855
+ lines.push("");
856
+ lines.push(
857
+ "On Linux, prefer CLI via `host_bash` over computer use tools, which take over the user's cursor. Use foreground computer use only when no scripting alternative exists or the user explicitly asks.",
858
+ );
859
+ }
860
+
852
861
  if (!caps.dashboardCapable) {
853
862
  lines.push("");
854
863
  lines.push("CHANNEL CONSTRAINTS:");
@@ -1152,29 +1161,12 @@ function placeholderForBlockType(type: ContentBlock["type"]): string | null {
1152
1161
  * renderer drops the redundant `@user` placeholder.
1153
1162
  */
1154
1163
  function rowToRenderable(row: SlackTranscriptInputRow): RenderableSlackMessage {
1155
- let slackMeta: ReturnType<typeof readSlackMetadata> = null;
1156
- let provenanceTrustClass: TrustClass | undefined;
1157
- if (row.metadata) {
1158
- try {
1159
- const outer = JSON.parse(row.metadata) as {
1160
- slackMeta?: unknown;
1161
- provenanceTrustClass?: unknown;
1162
- };
1163
- if (typeof outer.slackMeta === "string") {
1164
- slackMeta = readSlackMetadata(outer.slackMeta);
1165
- }
1166
- if (
1167
- outer.provenanceTrustClass === "guardian" ||
1168
- outer.provenanceTrustClass === "trusted_contact" ||
1169
- outer.provenanceTrustClass === "unverified_contact" ||
1170
- outer.provenanceTrustClass === "unknown"
1171
- ) {
1172
- provenanceTrustClass = outer.provenanceTrustClass;
1173
- }
1174
- } catch {
1175
- // Malformed metadata — fall through to legacy/null treatment.
1176
- }
1177
- }
1164
+ const slackMeta = readSlackMetadataFromMessageMetadata(row.metadata);
1165
+ const provenanceTrustClass = row.metadata
1166
+ ? trustClassSchema.safeParse(
1167
+ safeParseRecord(row.metadata).provenanceTrustClass,
1168
+ ).data
1169
+ : undefined;
1178
1170
 
1179
1171
  const isReaction = slackMeta?.eventKind === "reaction";
1180
1172
  let senderLabel: string | null;
@@ -1876,6 +1868,13 @@ export interface RuntimeInjectionBlocks {
1876
1868
  * (`MEMORY_V3_INJECTED_BLOCK_METADATA_KEY`).
1877
1869
  */
1878
1870
  memoryV3InjectedBlock?: string;
1871
+ /**
1872
+ * Rendered `<memory_spotlight>` body spliced onto this turn's user
1873
+ * message. Persisted by the user-prompt-submit hook under
1874
+ * `metadata.memoryV3SpotlightBlock`. Historical turns keep the block they
1875
+ * were sent with; a new spotlight is added only on the new tail.
1876
+ */
1877
+ memoryV3SpotlightBlock?: string;
1879
1878
  /**
1880
1879
  * True when memory-v3 superseded v2 as this turn's `<memory>` source —
1881
1880
  * `memory.v3.live` is on AND the v3 injector produced a block (possibly
@@ -2065,6 +2064,7 @@ function applyInjectionBlock(
2065
2064
  ];
2066
2065
  }
2067
2066
  }
2067
+ return runMessages;
2068
2068
  }
2069
2069
 
2070
2070
  /**
@@ -2297,7 +2297,7 @@ function fallbackTurnTrust(
2297
2297
  * the user tail content.
2298
2298
  *
2299
2299
  * Returns the final message array plus a `blocks` object holding the exact
2300
- * injected text for each captured block — callers persist those bytes to
2300
+ * injected text for each captured block. Callers persist those bytes to
2301
2301
  * message metadata for later byte-exact rehydration.
2302
2302
  */
2303
2303
  export async function applyRuntimeInjections(
@@ -2511,6 +2511,7 @@ export async function applyRuntimeInjections(
2511
2511
  let pkbSystemReminderCaptured: string | undefined;
2512
2512
  let memoryV2StaticCaptured: string | undefined;
2513
2513
  let memoryV3Captured: string | undefined;
2514
+ let memoryV3SpotlightCaptured: string | undefined;
2514
2515
  let backgroundTurnCaptured: string | undefined;
2515
2516
  let channelCapabilitiesCaptured: string | undefined;
2516
2517
  let nonInteractiveContextCaptured: string | undefined;
@@ -2560,6 +2561,11 @@ export async function applyRuntimeInjections(
2560
2561
  }
2561
2562
  break;
2562
2563
  }
2564
+ case MEMORY_V3_SPOTLIGHT_BLOCK_ID:
2565
+ if (block.text.length > 0) {
2566
+ memoryV3SpotlightCaptured = block.text;
2567
+ }
2568
+ break;
2563
2569
  }
2564
2570
  }
2565
2571
  }
@@ -2578,16 +2584,18 @@ export async function applyRuntimeInjections(
2578
2584
  ? injectorChainPieces.join("\n\n")
2579
2585
  : undefined;
2580
2586
 
2581
- // ── Step 0: memory-v3 ephemeral-spotlight strip + v2 tail suppression ──
2587
+ // ── Step 0: tail spotlight strip + v2 tail suppression ──
2582
2588
  //
2583
- // Spotlight strip (unconditional): the `<memory_spotlight>` block is
2584
- // ephemeral by contract — re-rendered at the current tail each turn — so any
2585
- // spotlight riding history from a previous turn is stale and is removed
2586
- // here. This is a SCOPED strip of only that block id: the frozen `<memory>`
2587
- // card blocks on historical messages are untouched (the cache contract).
2588
- // With the v3 flag off no spotlight blocks exist and this is a content
2589
- // no-op, keeping the v2 path bit-for-bit identical.
2590
- let runMessagesForAssembly = stripSpotlightInjections(runMessages);
2589
+ // Spotlight strip (tail only): mid-turn re-entry and post-compact can
2590
+ // hand back a tail that already carries this turn's `<memory_spotlight>`.
2591
+ // Strip that leftover from the tail so Step 2 splices a single fresh
2592
+ // copy. Historical user messages keep the spotlight they were sent with,
2593
+ // so the provider prefix through those messages stays byte-identical.
2594
+ // Frozen `<memory>` card blocks are untouched. With the v3 flag off no
2595
+ // spotlight blocks exist and this is a content no-op.
2596
+ let runMessagesForAssembly = stripTailUserTextBlocksByPrefix(runMessages, [
2597
+ MEMORY_SPOTLIGHT_MATCHER,
2598
+ ]);
2591
2599
 
2592
2600
  // v2 suppression: when `memory.v3.live` is on AND the v3 injector
2593
2601
  // produced a block this turn (possibly empty-text on an all-repeat turn), v3
@@ -2794,6 +2802,7 @@ export async function applyRuntimeInjections(
2794
2802
  pkbContextBlock: pkbContextCaptured,
2795
2803
  memoryV2StaticBlock: memoryV2StaticCaptured,
2796
2804
  memoryV3InjectedBlock: memoryV3Captured,
2805
+ memoryV3SpotlightBlock: memoryV3SpotlightCaptured,
2797
2806
  backgroundTurnBlock: backgroundTurnCaptured,
2798
2807
  channelCapabilitiesBlock: channelCapabilitiesCaptured,
2799
2808
  nonInteractiveContextBlock: nonInteractiveContextCaptured,