@vellumai/assistant 0.11.3 → 0.11.4-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 (324) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +140 -38
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +55 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  67. package/src/__tests__/visible-app-context.test.ts +16 -9
  68. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  69. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  70. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  71. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  72. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  73. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  74. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  75. package/src/acp/__tests__/auth-required.test.ts +161 -0
  76. package/src/acp/acp-claude-oauth.ts +19 -2
  77. package/src/acp/agent-process.test.ts +100 -0
  78. package/src/acp/agent-process.ts +29 -26
  79. package/src/acp/auth-required.ts +102 -0
  80. package/src/acp/session-manager.test.ts +119 -0
  81. package/src/acp/session-manager.ts +68 -2
  82. package/src/api/events/acp-auth-required.ts +55 -0
  83. package/src/api/index.ts +7 -0
  84. package/src/apps/app-store.ts +3 -0
  85. package/src/bundler/package-resolver.ts +2 -30
  86. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  87. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  88. package/src/calls/call-controller.ts +9 -2
  89. package/src/calls/call-setup-flow.ts +0 -1
  90. package/src/calls/media-stream-stt-session.ts +15 -0
  91. package/src/calls/voice-session-bridge.ts +71 -16
  92. package/src/calls/voice-triage-escalate.ts +104 -2
  93. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  94. package/src/channels/config.ts +13 -0
  95. package/src/channels/plugin-channel-declarations.ts +108 -0
  96. package/src/channels/types.ts +30 -0
  97. package/src/cli/AGENTS.md +5 -2
  98. package/src/cli/commands/credentials.help.ts +2 -2
  99. package/src/cli/commands/inference-providers.ts +1 -1
  100. package/src/cli/commands/mcp.help.ts +13 -4
  101. package/src/cli/commands/mcp.ts +9 -0
  102. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  103. package/src/cli/commands/memory/index.help.ts +43 -1
  104. package/src/cli/commands/memory/memory-v3.ts +64 -0
  105. package/src/cli/commands/stt.help.ts +27 -2
  106. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  107. package/src/cli/lib/bundled-marketplace.json +13 -0
  108. package/src/cli/lib/upgrade-plugin.ts +42 -0
  109. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  110. package/src/config/__tests__/default-provider.test.ts +6 -1
  111. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  112. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  113. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  114. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  115. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  116. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  117. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  118. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  119. package/src/config/call-site-defaults.ts +4 -5
  120. package/src/config/default-profile-catalog.ts +82 -11
  121. package/src/config/default-profile-names.ts +4 -1
  122. package/src/config/default-provider-resolution.ts +4 -0
  123. package/src/config/llm-context-resolution.ts +11 -3
  124. package/src/config/llm-resolver.ts +28 -1
  125. package/src/config/profile-materialization.ts +70 -22
  126. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  127. package/src/config/schemas/call-site-catalog.ts +4 -4
  128. package/src/config/schemas/live-voice.ts +57 -23
  129. package/src/config/schemas/llm.ts +59 -32
  130. package/src/config/schemas/mcp.ts +23 -0
  131. package/src/config/schemas/plugin-updates.ts +6 -2
  132. package/src/config/schemas/stt.ts +1 -0
  133. package/src/context/outbound-sanitize.ts +96 -1
  134. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  135. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  136. package/src/daemon/conversation-agent-loop.ts +17 -6
  137. package/src/daemon/conversation-messaging.ts +5 -1
  138. package/src/daemon/conversation-notifiers.ts +9 -1
  139. package/src/daemon/conversation-process.ts +9 -6
  140. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  141. package/src/daemon/conversation-tool-setup.ts +1 -2
  142. package/src/daemon/conversation.ts +48 -0
  143. package/src/daemon/mcp-reload-service.ts +36 -6
  144. package/src/daemon/process-message.ts +13 -3
  145. package/src/daemon/providers-setup.ts +6 -3
  146. package/src/daemon/trust-context-types.ts +29 -0
  147. package/src/daemon/wake-conversation-ops.ts +3 -2
  148. package/src/documents/document-store.ts +138 -5
  149. package/src/hooks/hook-loader.ts +3 -3
  150. package/src/hooks/registry.ts +50 -6
  151. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  152. package/src/inbound/oauth-callback-url.ts +61 -0
  153. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  154. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  155. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  156. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  157. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  158. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  159. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  160. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  161. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  162. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  163. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  164. package/src/live-voice/live-voice-archive.ts +2 -0
  165. package/src/live-voice/live-voice-metrics.ts +57 -32
  166. package/src/live-voice/live-voice-photo.ts +1 -2
  167. package/src/live-voice/live-voice-session.ts +535 -314
  168. package/src/live-voice/progress-narration.ts +277 -0
  169. package/src/live-voice/protocol.ts +21 -1
  170. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  171. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  172. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  173. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  174. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  175. package/src/mcp/client.ts +16 -11
  176. package/src/mcp/effective-config.ts +113 -0
  177. package/src/mcp/manager.ts +11 -6
  178. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  179. package/src/mcp/mcp-oauth-provider.ts +205 -240
  180. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  181. package/src/monitoring/plugin-auto-update.ts +128 -24
  182. package/src/notifications/signal.ts +1 -0
  183. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  184. package/src/permissions/confirmation-guardian-request.ts +2 -2
  185. package/src/permissions/question-guardian-request.test.ts +14 -6
  186. package/src/permissions/question-guardian-request.ts +1 -2
  187. package/src/persistence/attachments-store.ts +8 -1
  188. package/src/persistence/bookmark-crud.ts +3 -7
  189. package/src/persistence/conversation-attention-store.ts +16 -45
  190. package/src/persistence/conversation-crud.ts +33 -4
  191. package/src/persistence/conversation-lineage.ts +9 -0
  192. package/src/persistence/conversation-queries.ts +108 -41
  193. package/src/persistence/delivery-crud.ts +38 -29
  194. package/src/persistence/external-conversation-store.ts +32 -4
  195. package/src/persistence/llm-request-log-store.ts +4 -10
  196. package/src/persistence/llm-usage-store.ts +8 -3
  197. package/src/persistence/message-reads.test.ts +197 -0
  198. package/src/persistence/message-reads.ts +211 -0
  199. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  200. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  201. package/src/persistence/real-user-turn-filter.ts +27 -3
  202. package/src/persistence/steps.ts +9 -0
  203. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  204. package/src/plugin-api/conversation-turn.ts +168 -5
  205. package/src/plugin-api/index.ts +21 -5
  206. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  207. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  208. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  209. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  210. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  211. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  212. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  213. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  214. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  215. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  216. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  217. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  218. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  219. package/src/plugins/defaults/memory/indexer.ts +3 -1
  220. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  221. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  222. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  223. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  224. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  225. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  226. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  227. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  228. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  229. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  230. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  231. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  232. package/src/plugins/defaults/memory/worker.ts +6 -3
  233. package/src/plugins/external-plugin-loader.ts +47 -0
  234. package/src/plugins/mcp-servers.ts +361 -0
  235. package/src/plugins/mtime-cache.ts +23 -49
  236. package/src/plugins/worker-plugin-surface.ts +33 -0
  237. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  238. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  239. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  240. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  241. package/src/providers/call-site-routing.ts +30 -3
  242. package/src/providers/connection-resolution.ts +194 -11
  243. package/src/providers/inference/auth.ts +6 -0
  244. package/src/providers/inference/connection-availability.ts +24 -2
  245. package/src/providers/inference/connections.ts +2 -0
  246. package/src/providers/model-intents.ts +26 -6
  247. package/src/providers/openai/codex-models.ts +2 -1
  248. package/src/providers/provider-send-message.ts +32 -3
  249. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  250. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  251. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  252. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  253. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  254. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  255. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  256. package/src/providers/speech-to-text/resolve.ts +25 -2
  257. package/src/routes/worker.ts +17 -5
  258. package/src/runtime/access-request-helper.ts +9 -12
  259. package/src/runtime/agent-wake.ts +3 -3
  260. package/src/runtime/pre-first-message-gate.ts +4 -0
  261. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  262. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  263. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  264. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  265. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  266. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  267. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  268. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  269. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  270. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  271. package/src/runtime/routes/channel-route-shared.ts +0 -6
  272. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  273. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  274. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  275. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  276. package/src/runtime/routes/default-provider-routes.ts +15 -0
  277. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  278. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  279. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  280. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  281. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  282. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  283. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  284. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  285. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  286. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  287. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  288. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  289. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  291. package/src/runtime/routes/log-export-routes.ts +3 -0
  292. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  293. package/src/runtime/routes/plugins-routes.ts +21 -3
  294. package/src/runtime/routes/stt-routes.ts +31 -25
  295. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  296. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  297. package/src/runtime/routes/user-route-import.ts +108 -0
  298. package/src/schedule/worker.ts +6 -0
  299. package/src/security/oauth2.ts +6 -22
  300. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  301. package/src/stt/__tests__/types.test.ts +94 -0
  302. package/src/stt/daemon-batch-transcriber.ts +10 -0
  303. package/src/stt/stt-stream-session.ts +8 -4
  304. package/src/stt/types.ts +103 -0
  305. package/src/subagent/manager.ts +1 -3
  306. package/src/subagent/types.ts +7 -6
  307. package/src/tools/acp/spawn.test.ts +97 -0
  308. package/src/tools/acp/spawn.ts +32 -0
  309. package/src/tools/document/document-tool.ts +12 -3
  310. package/src/tools/registry.ts +2 -1
  311. package/src/tools/workflows/run-workflow.ts +1 -2
  312. package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
  313. package/src/tts/reasoning-tag-filter.ts +110 -0
  314. package/src/workspace/byok-default-profile-ensure.ts +61 -18
  315. package/src/workspace/custom-profile-ensure.ts +4 -24
  316. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  317. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  318. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  319. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  320. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  321. package/src/workspace/migrations/registry.ts +8 -0
  322. package/src/workspace/provider-commit-message-generator.ts +7 -5
  323. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  324. package/src/live-voice/front-decision.ts +0 -476
@@ -9,6 +9,7 @@ import { applyRuntimeInjections } from "../daemon/conversation-runtime-assembly.
9
9
  import type { SurfaceData, SurfaceType } from "../daemon/message-protocol.js";
10
10
  import { registerDefaultPluginInjectors } from "../plugins/defaults/index.js";
11
11
  import type { Message } from "../providers/types.js";
12
+ import { asConversation } from "./helpers/mock-conversation.js";
12
13
 
13
14
  // Populate the injector registry with the default plugins' injectors the way
14
15
  // bootstrap does in production, so `applyRuntimeInjections` walks a non-empty
@@ -33,16 +34,19 @@ const FALLBACK_CONVERSATION_ID = "runtime-assembly-fallback";
33
34
  // injectors resolve their blocks from it (the orchestrator no longer threads
34
35
  // workspace or active-surface content as options).
35
36
  function registerFallbackConversation(fields: Record<string, unknown>): void {
36
- setConversation(FALLBACK_CONVERSATION_ID, {
37
- conversationId: FALLBACK_CONVERSATION_ID,
38
- workingDir: "/sandbox",
39
- // Non-dirty empty workspace by default so the workspace-context injector
40
- // skips both the filesystem rescan and the DB refresh unless a test
41
- // explicitly seeds a block via `workspaceTopLevelContext`.
42
- workspaceTopLevelContext: "",
43
- workspaceTopLevelDirty: false,
44
- ...fields,
45
- } as never);
37
+ setConversation(
38
+ FALLBACK_CONVERSATION_ID,
39
+ asConversation({
40
+ conversationId: FALLBACK_CONVERSATION_ID,
41
+ workingDir: "/sandbox",
42
+ // Non-dirty empty workspace by default so the workspace-context injector
43
+ // skips both the filesystem rescan and the DB refresh unless a test
44
+ // explicitly seeds a block via `workspaceTopLevelContext`.
45
+ workspaceTopLevelContext: "",
46
+ workspaceTopLevelDirty: false,
47
+ ...fields,
48
+ }),
49
+ );
46
50
  }
47
51
 
48
52
  // Seed the live conversation registry with a pre-rendered top-level block. The
@@ -209,8 +209,8 @@ describe("credentials/prompt route", () => {
209
209
  test("stores a non-slack credential to secure storage and reports the value as received", async () => {
210
210
  /**
211
211
  * The default delivery persists the value to secure storage and runs the
212
- * manual-token connection sync. The message must name the outcome — the
213
- * value is in hand — so a bare `ok: true` cannot be misread as "the prompt
212
+ * manual-token connection sync. The message must name the outcome (the
213
+ * value is in hand) so a bare `ok: true` cannot be misread as "the prompt
214
214
  * opened".
215
215
  */
216
216
  // GIVEN a standard store-delivery prompt for a non-slack credential
@@ -236,15 +236,12 @@ describe("credentials/prompt route", () => {
236
236
 
237
237
  test("every successful outcome states that the prompt has closed", async () => {
238
238
  /**
239
- * The prompt blocks until answered, so a caller reads the result only after
240
- * the secure input is gone. Without an explicit close signal a model
241
- * follows up with "the prompt is open, paste it there" and the user
242
- * re-submits into a surface that never reappears — the loop reported in
243
- * the Aug 2026 Claude Code auth feedback. Every success path must say so,
244
- * not just the plain vault write.
239
+ * The prompt blocks until answered, so a caller reads the result only once
240
+ * the secure input is gone. Every success path states the close, not just
241
+ * the plain vault write, so the result cannot be read as "the prompt is
242
+ * open, paste it there".
245
243
  */
246
- const closed =
247
- "The secure prompt has closed — do not ask the user to enter this value again.";
244
+ const closed = "The secure prompt has closed.";
248
245
 
249
246
  // GIVEN a plain vault write
250
247
  const stored = (await promptRoute!.handler({
@@ -53,6 +53,10 @@ describe("ensureCompleteCustomProfiles", () => {
53
53
  ensureCompleteCustomProfiles(workspaceDir);
54
54
  const saved = readProfiles().partial;
55
55
  expect(saved.model).toBe("claude-haiku-4-5-20251001");
56
+ // This workspace has no connection rows, so the binding is unverifiable
57
+ // and passes through in the legacy field for the collapse migration to
58
+ // judge; with a verified row it would fold into the provider value (see
59
+ // profile-materialization.test.ts).
56
60
  expect(saved.provider).toBe("anthropic");
57
61
  expect(saved.provider_connection).toBe("anthropic-personal");
58
62
  expect(saved.maxTokens).toBe(12345);
@@ -91,7 +95,7 @@ describe("ensureCompleteCustomProfiles", () => {
91
95
  },
92
96
  });
93
97
  ensureCompleteCustomProfiles(workspaceDir);
94
- expect(readProfiles().gpt.provider_connection).toBe("vellum");
98
+ expect(readProfiles().gpt.provider_connection).toBeUndefined();
95
99
  expect(readProfiles().router.provider_connection).toBeUndefined();
96
100
  });
97
101
 
@@ -342,7 +342,11 @@ describe("Discord access-request decisions stay out of the guild channel", () =>
342
342
  test("no Discord delivery is ever addressed to the originating conversation", async () => {
343
343
  // The umbrella invariant, swept across every decision this resolver takes,
344
344
  // so a newly added action cannot quietly reintroduce the leak.
345
- for (const action of ["verify_code", "block", "leave_unverified"] as const) {
345
+ for (const action of [
346
+ "verify_code",
347
+ "block",
348
+ "leave_unverified",
349
+ ] as const) {
346
350
  resetState();
347
351
  const req = makeDiscordAccessRequest();
348
352
 
@@ -27,9 +27,9 @@ describe("the dm-marked Discord route", () => {
27
27
  // regressed, the notice would fall through to the HTTP proxy, which cannot
28
28
  // fetch a base-less URL: the delivery would vanish rather than error, and
29
29
  // every behavioural assertion about where it landed would still pass.
30
- expect(channelForCallback(resolveDeliverCallbackUrlForChannel("discord")!)).toBe(
31
- "discord",
32
- );
30
+ expect(
31
+ channelForCallback(resolveDeliverCallbackUrlForChannel("discord")!),
32
+ ).toBe("discord");
33
33
  });
34
34
  });
35
35
 
@@ -0,0 +1,233 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { getDocumentById } from "../documents/document-store.js";
4
+ import { getSqlite } from "../persistence/db-connection.js";
5
+ import {
6
+ executeDocumentCreate,
7
+ executeDocumentUpdate,
8
+ } from "../tools/document/document-tool.js";
9
+ import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
10
+ import { resetDbForTesting } from "./db-test-helpers.js";
11
+
12
+ const CONVERSATION_ID = "conv-current";
13
+
14
+ /** Long enough to clear the store's minimum-duplicate-length floor. */
15
+ const OPENING =
16
+ "Remote work reshaped the office long before anyone had a name for it.";
17
+
18
+ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
19
+ return {
20
+ workingDir: "/tmp/project",
21
+ conversationId: CONVERSATION_ID,
22
+ trustClass: "trusted_contact",
23
+ executionChannel: "slack",
24
+ sendToClient: () => {},
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ function parseResult<T>(result: ToolExecutionResult): T {
30
+ return JSON.parse(result.content) as T;
31
+ }
32
+
33
+ function bootstrapDocumentTables(): void {
34
+ resetDbForTesting();
35
+ const raw = getSqlite();
36
+ raw.exec(/*sql*/ `
37
+ DROP TABLE IF EXISTS document_conversations;
38
+ DROP TABLE IF EXISTS documents;
39
+ DROP TABLE IF EXISTS conversations;
40
+
41
+ CREATE TABLE conversations (
42
+ id TEXT PRIMARY KEY,
43
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
44
+ );
45
+
46
+ CREATE TABLE documents (
47
+ surface_id TEXT PRIMARY KEY,
48
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
49
+ title TEXT NOT NULL,
50
+ content TEXT NOT NULL,
51
+ word_count INTEGER NOT NULL DEFAULT 0,
52
+ created_at INTEGER NOT NULL,
53
+ updated_at INTEGER NOT NULL,
54
+ workspace_path TEXT
55
+ );
56
+
57
+ CREATE TABLE document_conversations (
58
+ surface_id TEXT NOT NULL,
59
+ conversation_id TEXT NOT NULL,
60
+ created_at INTEGER NOT NULL,
61
+ PRIMARY KEY (surface_id, conversation_id),
62
+ FOREIGN KEY (surface_id) REFERENCES documents(surface_id) ON DELETE CASCADE
63
+ );
64
+ `);
65
+ raw
66
+ .query(`INSERT OR IGNORE INTO conversations (id, created_at) VALUES (?, ?)`)
67
+ .run(CONVERSATION_ID, Date.now());
68
+ }
69
+
70
+ function seedDocument(surfaceId: string, content: string): void {
71
+ const now = Date.now();
72
+ const raw = getSqlite();
73
+ raw
74
+ .query(
75
+ `INSERT INTO documents (surface_id, conversation_id, title, content, word_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
76
+ )
77
+ .run(
78
+ surfaceId,
79
+ CONVERSATION_ID,
80
+ "Draft",
81
+ content,
82
+ content.split(/\s+/).filter(Boolean).length,
83
+ now,
84
+ now,
85
+ );
86
+ raw
87
+ .query(
88
+ `INSERT OR IGNORE INTO document_conversations (surface_id, conversation_id, created_at) VALUES (?, ?, ?)`,
89
+ )
90
+ .run(surfaceId, CONVERSATION_ID, now);
91
+ }
92
+
93
+ describe("document append idempotency", () => {
94
+ beforeEach(() => {
95
+ bootstrapDocumentTables();
96
+ });
97
+
98
+ test("an opening passed to both document_create and the first append lands once", () => {
99
+ const created = executeDocumentCreate(
100
+ { title: "Draft", initial_content: `# Draft\n\n${OPENING}` },
101
+ makeContext(),
102
+ );
103
+ const surfaceId = parseResult<{ surface_id: string }>(created).surface_id;
104
+
105
+ const result = executeDocumentUpdate(
106
+ {
107
+ surface_id: surfaceId,
108
+ content: `# Draft\n\n${OPENING}\n\nThe second paragraph carries the argument forward.`,
109
+ mode: "append",
110
+ },
111
+ makeContext(),
112
+ );
113
+
114
+ expect(result.isError).toBe(false);
115
+ expect(getDocumentById(surfaceId)?.content).toBe(
116
+ `# Draft\n\n${OPENING}\n\nThe second paragraph carries the argument forward.`,
117
+ );
118
+ });
119
+
120
+ test("an append that only restates the tail writes nothing", () => {
121
+ seedDocument("doc-restate", `# Draft\n\n${OPENING}`);
122
+
123
+ const result = executeDocumentUpdate(
124
+ { surface_id: "doc-restate", content: OPENING, mode: "append" },
125
+ makeContext(),
126
+ );
127
+
128
+ expect(result.isError).toBe(false);
129
+ const body = parseResult<{ success: boolean; message: string }>(result);
130
+ expect(body.success).toBe(true);
131
+ expect(body.message).toBe(
132
+ "Nothing appended: the content is already at the end of the document",
133
+ );
134
+ expect(getDocumentById("doc-restate")?.content).toBe(
135
+ `# Draft\n\n${OPENING}`,
136
+ );
137
+ });
138
+
139
+ test("the client is told what landed, not what was submitted", () => {
140
+ seedDocument("doc-client", OPENING);
141
+ const sent: { markdown: unknown; mode: unknown }[] = [];
142
+
143
+ executeDocumentUpdate(
144
+ {
145
+ surface_id: "doc-client",
146
+ content: `${OPENING}\n\nAnd then the rest.`,
147
+ mode: "append",
148
+ },
149
+ makeContext({
150
+ sendToClient: (message) => {
151
+ if (message.type === "document_editor_update") {
152
+ sent.push({ markdown: message.markdown, mode: message.mode });
153
+ }
154
+ },
155
+ }),
156
+ );
157
+
158
+ expect(sent).toEqual([{ markdown: "And then the rest.", mode: "append" }]);
159
+ });
160
+
161
+ // ── Legitimate repetition the guard must not eat ──────────────────
162
+
163
+ test("keeps a short repeated line, even back to back", () => {
164
+ seedDocument("doc-refrain", "## Checklist\n\nTODO");
165
+
166
+ executeDocumentUpdate(
167
+ { surface_id: "doc-refrain", content: "TODO\n\nTODO", mode: "append" },
168
+ makeContext(),
169
+ );
170
+
171
+ expect(getDocumentById("doc-refrain")?.content).toBe(
172
+ "## Checklist\n\nTODO\n\nTODO\n\nTODO",
173
+ );
174
+ });
175
+
176
+ test("keeps a repeated heading followed by new prose", () => {
177
+ seedDocument("doc-heading", "# Weekly Notes\n\n## Monday");
178
+
179
+ executeDocumentUpdate(
180
+ {
181
+ surface_id: "doc-heading",
182
+ content: "## Monday\n\nShipped the importer.",
183
+ mode: "append",
184
+ },
185
+ makeContext(),
186
+ );
187
+
188
+ expect(getDocumentById("doc-heading")?.content).toBe(
189
+ "# Weekly Notes\n\n## Monday\n\n## Monday\n\nShipped the importer.",
190
+ );
191
+ });
192
+
193
+ test("keeps a long paragraph that repeats something other than the tail", () => {
194
+ seedDocument("doc-midway", `${OPENING}\n\nA later paragraph ends the doc.`);
195
+
196
+ executeDocumentUpdate(
197
+ { surface_id: "doc-midway", content: OPENING, mode: "append" },
198
+ makeContext(),
199
+ );
200
+
201
+ expect(getDocumentById("doc-midway")?.content).toBe(
202
+ `${OPENING}\n\nA later paragraph ends the doc.\n\n${OPENING}`,
203
+ );
204
+ });
205
+
206
+ test("keeps content whose repetition starts partway into the append", () => {
207
+ seedDocument("doc-partway", `# Draft\n\n${OPENING}`);
208
+
209
+ executeDocumentUpdate(
210
+ {
211
+ surface_id: "doc-partway",
212
+ content: `A fresh lead-in.\n\n${OPENING}`,
213
+ mode: "append",
214
+ },
215
+ makeContext(),
216
+ );
217
+
218
+ expect(getDocumentById("doc-partway")?.content).toBe(
219
+ `# Draft\n\n${OPENING}\n\nA fresh lead-in.\n\n${OPENING}`,
220
+ );
221
+ });
222
+
223
+ test("replace mode is never deduplicated", () => {
224
+ seedDocument("doc-replace", OPENING);
225
+
226
+ executeDocumentUpdate(
227
+ { surface_id: "doc-replace", content: OPENING, mode: "replace" },
228
+ makeContext(),
229
+ );
230
+
231
+ expect(getDocumentById("doc-replace")?.content).toBe(OPENING);
232
+ });
233
+ });
@@ -145,7 +145,6 @@ describe("Slack edit propagation", () => {
145
145
  conversationExternalId: seeded.conversationExternalId,
146
146
  externalMessageId: nextEditEventId(),
147
147
  sourceMessageId: seeded.channelTs,
148
- canonicalAssistantId: "self",
149
148
  assistantId: "self",
150
149
  content: "new text",
151
150
  });
@@ -187,7 +186,6 @@ describe("Slack edit propagation", () => {
187
186
  externalMessageId: nextEditEventId(),
188
187
  sourceMessageId: seeded.channelTs,
189
188
  sourceThreadId: threadTs,
190
- canonicalAssistantId: "self",
191
189
  assistantId: "self",
192
190
  content: "new text",
193
191
  });
@@ -216,7 +214,6 @@ describe("Slack edit propagation", () => {
216
214
  conversationExternalId: seeded.conversationExternalId,
217
215
  externalMessageId: nextEditEventId(),
218
216
  sourceMessageId: seeded.channelTs,
219
- canonicalAssistantId: "self",
220
217
  assistantId: "self",
221
218
  content: "first edit",
222
219
  });
@@ -239,7 +236,6 @@ describe("Slack edit propagation", () => {
239
236
  conversationExternalId: seeded.conversationExternalId,
240
237
  externalMessageId: nextEditEventId(),
241
238
  sourceMessageId: seeded.channelTs,
242
- canonicalAssistantId: "self",
243
239
  assistantId: "self",
244
240
  content: "second edit",
245
241
  });
@@ -272,7 +268,6 @@ describe("Slack edit propagation", () => {
272
268
  conversationExternalId: seeded.conversationExternalId,
273
269
  externalMessageId: nextEditEventId(),
274
270
  sourceMessageId: seeded.channelTs,
275
- canonicalAssistantId: "self",
276
271
  assistantId: "self",
277
272
  // Same text as stored -- simulates a Slack unfurl `message_changed`
278
273
  // where only attachments changed.
@@ -313,7 +308,6 @@ describe("Slack edit propagation", () => {
313
308
  conversationExternalId: seeded.conversationExternalId,
314
309
  externalMessageId: nextEditEventId(),
315
310
  sourceMessageId: seeded.channelTs,
316
- canonicalAssistantId: "self",
317
311
  assistantId: "self",
318
312
  content: "edited searchable text",
319
313
  });
@@ -341,7 +335,6 @@ describe("Slack edit propagation", () => {
341
335
  // sourceMessageId points at a ts that was never stored.
342
336
  externalMessageId: nextEditEventId(),
343
337
  sourceMessageId: "9999.0000",
344
- canonicalAssistantId: "self",
345
338
  assistantId: "self",
346
339
  content: "new text",
347
340
  });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Typed fixture factories for actor identity in tests.
3
+ *
4
+ * A fixture built here is a complete, type-checked `TrustContext` or
5
+ * `AuthContext` with conservative defaults; a test overrides only the fields
6
+ * its scenario is about, and the compiler guards the rest. Hand-rolled
7
+ * literals cannot make that guarantee: a missing required field needs a cast
8
+ * to satisfy the checker, and the cast then hides every other mistake in the
9
+ * literal.
10
+ *
11
+ * Type-only imports keep this helper out of the runtime import graph, per the
12
+ * test-machinery isolation rules in `assistant/CLAUDE.md`.
13
+ */
14
+
15
+ import type { TrustContext } from "../../daemon/trust-context-types.js";
16
+ import type { AuthContext, Scope } from "../../runtime/auth/types.js";
17
+
18
+ /**
19
+ * A complete `TrustContext`. Defaults to the guardian on the vellum channel;
20
+ * pass overrides for anything the test cares about.
21
+ */
22
+ export function mockTrustContext(
23
+ overrides: Partial<TrustContext> = {},
24
+ ): TrustContext {
25
+ return {
26
+ sourceChannel: "vellum",
27
+ trustClass: "guardian",
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * A complete `AuthContext`. Defaults to a local principal with no scopes, the
34
+ * conservative shape, so a test that forgets to opt in exercises the
35
+ * restrictive branch rather than a silently permissive one.
36
+ */
37
+ export function mockAuthContext(
38
+ overrides: Partial<AuthContext> = {},
39
+ ): AuthContext {
40
+ return {
41
+ subject: "local:self:test",
42
+ principalType: "local",
43
+ assistantId: "self",
44
+ scopeProfile: "local_v1",
45
+ scopes: new Set<Scope>(),
46
+ policyEpoch: 0,
47
+ ...overrides,
48
+ };
49
+ }
@@ -26,7 +26,19 @@ import type { ChannelCapabilities } from "../../daemon/conversation-runtime-asse
26
26
  export function asConversation<TExtra extends object = object>(
27
27
  mock: Partial<Conversation> & TExtra,
28
28
  ): Conversation & TExtra {
29
- return mock as unknown as Conversation & TExtra;
29
+ // Trust accessors, derived from the double's own fields exactly as the real
30
+ // class derives them, so a double that sets (or later mutates) the fields
31
+ // resolves the same values production would. Supplied only when the double
32
+ // does not bring its own, and reading through `merged` rather than `mock`
33
+ // so a test that reassigns the fields on the returned object is honoured.
34
+ const merged: Partial<Conversation> & TExtra = {
35
+ getTurnTrust: () => merged.currentTurnTrustContext,
36
+ getTrustContext: () => merged.trustContext,
37
+ getTurnOrRestingTrust: () =>
38
+ merged.currentTurnTrustContext ?? merged.trustContext,
39
+ ...mock,
40
+ };
41
+ return merged as unknown as Conversation & TExtra;
30
42
  }
31
43
 
32
44
  /**
@@ -67,6 +67,7 @@ const { registerDefaultPluginInjectors } =
67
67
  await import("../plugins/defaults/index.js");
68
68
  import { eq } from "drizzle-orm";
69
69
 
70
+ import type { InterfaceId } from "../channels/types.js";
70
71
  import {
71
72
  clearConversations,
72
73
  setConversation,
@@ -86,6 +87,7 @@ import type { Message } from "../providers/types.js";
86
87
  import { getSubagentManager } from "../subagent/index.js";
87
88
  import type { SubagentState } from "../subagent/types.js";
88
89
  import { getWorkspacePromptPath } from "../util/platform.js";
90
+ import { asConversation } from "./helpers/mock-conversation.js";
89
91
 
90
92
  // `applyRuntimeInjections` self-resolves the Slack active-thread focus block
91
93
  // from the persisted message rows, so the schema must exist for Slack-channel
@@ -153,26 +155,30 @@ function seedWorkspaceContext(
153
155
  text: string,
154
156
  currentTurnTemporalSnapshot?: {
155
157
  clientTimezone: string | null;
158
+ timeSinceLastMessage: string | null;
156
159
  },
157
- interfaceName?: string,
160
+ interfaceName?: InterfaceId,
158
161
  ): void {
159
- setConversation(TEST_CONVERSATION_ID, {
160
- conversationId: TEST_CONVERSATION_ID,
161
- workingDir: "/sandbox",
162
- workspaceTopLevelContext: text,
163
- workspaceTopLevelDirty: false,
164
- currentTurnTemporalSnapshot,
165
- currentTurnInterfaceContext: interfaceName
166
- ? {
167
- userMessageInterface: interfaceName,
168
- assistantMessageInterface: interfaceName,
169
- }
170
- : undefined,
171
- // Mirrors Conversation.getSubagentChildren: the `<active_subagents>` block
172
- // is sourced from the live conversation, which delegates to the manager.
173
- getSubagentChildren: () =>
174
- getSubagentManager().getChildrenOf(TEST_CONVERSATION_ID),
175
- } as never);
162
+ setConversation(
163
+ TEST_CONVERSATION_ID,
164
+ asConversation({
165
+ conversationId: TEST_CONVERSATION_ID,
166
+ workingDir: "/sandbox",
167
+ workspaceTopLevelContext: text,
168
+ workspaceTopLevelDirty: false,
169
+ currentTurnTemporalSnapshot,
170
+ currentTurnInterfaceContext: interfaceName
171
+ ? {
172
+ userMessageInterface: interfaceName,
173
+ assistantMessageInterface: interfaceName,
174
+ }
175
+ : undefined,
176
+ // Mirrors Conversation.getSubagentChildren: the `<active_subagents>` block
177
+ // is sourced from the live conversation, which delegates to the manager.
178
+ getSubagentChildren: () =>
179
+ getSubagentManager().getChildrenOf(TEST_CONVERSATION_ID),
180
+ }),
181
+ );
176
182
  }
177
183
 
178
184
  // `applyRuntimeInjections` gates the `<turn_context>` block on the live
@@ -181,13 +187,19 @@ function seedWorkspaceContext(
181
187
  // fall back to the runtime-assembly fallback conversation, so seed the snapshot
182
188
  // there.
183
189
  function seedFallbackTemporalSnapshot(): void {
184
- setConversation("runtime-assembly-fallback", {
185
- conversationId: "runtime-assembly-fallback",
186
- workingDir: "/sandbox",
187
- workspaceTopLevelContext: "",
188
- workspaceTopLevelDirty: false,
189
- currentTurnTemporalSnapshot: { clientTimezone: null },
190
- } as never);
190
+ setConversation(
191
+ "runtime-assembly-fallback",
192
+ asConversation({
193
+ conversationId: "runtime-assembly-fallback",
194
+ workingDir: "/sandbox",
195
+ workspaceTopLevelContext: "",
196
+ workspaceTopLevelDirty: false,
197
+ currentTurnTemporalSnapshot: {
198
+ clientTimezone: null,
199
+ timeSinceLastMessage: null,
200
+ },
201
+ }),
202
+ );
191
203
  }
192
204
 
193
205
  // Persist Slack-channel rows for the turn conversation so
@@ -484,7 +496,11 @@ describe("injector chain", () => {
484
496
  seedSubagentChild(TEST_CONVERSATION_ID, subagentChild);
485
497
  const subagentBlock = buildSubagentStatusBlock([subagentChild])!;
486
498
 
487
- seedWorkspaceContext(workspaceText, { clientTimezone: null }, "macos");
499
+ seedWorkspaceContext(
500
+ workspaceText,
501
+ { clientTimezone: null, timeSinceLastMessage: null },
502
+ "macos",
503
+ );
488
504
  const result = await applyRuntimeInjections(runMessages, {
489
505
  ...makeTurnContext(),
490
506
  });
@@ -562,20 +578,23 @@ describe("injector chain", () => {
562
578
  createdAt: 1700000005_000,
563
579
  },
564
580
  ]);
565
- setConversation(TEST_CONVERSATION_ID, {
566
- conversationId: TEST_CONVERSATION_ID,
567
- workingDir: "/sandbox",
568
- workspaceTopLevelContext: "",
569
- workspaceTopLevelDirty: false,
570
- trustContext: { trustClass: "guardian" },
571
- channelCapabilities: {
572
- channel: "slack",
573
- dashboardCapable: false,
574
- supportsDynamicUi: false,
575
- supportsVoiceInput: false,
576
- chatType: "channel",
577
- },
578
- } as never);
581
+ setConversation(
582
+ TEST_CONVERSATION_ID,
583
+ asConversation({
584
+ conversationId: TEST_CONVERSATION_ID,
585
+ workingDir: "/sandbox",
586
+ workspaceTopLevelContext: "",
587
+ workspaceTopLevelDirty: false,
588
+ trustContext: { trustClass: "guardian", sourceChannel: "vellum" },
589
+ channelCapabilities: {
590
+ channel: "slack",
591
+ dashboardCapable: false,
592
+ supportsDynamicUi: false,
593
+ supportsVoiceInput: false,
594
+ chatType: "channel",
595
+ },
596
+ }),
597
+ );
579
598
  const result = await applyRuntimeInjections(originalRun, {
580
599
  ...makeTurnContext(),
581
600
  });
@@ -614,7 +633,10 @@ describe("injector chain", () => {
614
633
  timestamp: FIXED_TURN_TIMESTAMP,
615
634
  interfaceName: "web",
616
635
  });
617
- seedWorkspaceContext("", { clientTimezone: null });
636
+ seedWorkspaceContext("", {
637
+ clientTimezone: null,
638
+ timeSinceLastMessage: null,
639
+ });
618
640
  seedSubagentChild(
619
641
  TEST_CONVERSATION_ID,
620
642
  makeSubagentState("sub-1", "worker", "running"),