@vellumai/assistant 0.10.9 → 0.10.10-dev.202607162206.d08e98e

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 (456) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +8 -0
  3. package/docs/activation-funnel-telemetry.md +13 -7
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  12. package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  13. package/openapi.yaml +530 -107
  14. package/package.json +1 -1
  15. package/scripts/generate-openapi.ts +8 -0
  16. package/src/__tests__/activation-early-marking.test.ts +6 -5
  17. package/src/__tests__/agent-loop-override-profile.test.ts +22 -25
  18. package/src/__tests__/agent-wake-override-profile.test.ts +21 -48
  19. package/src/__tests__/app-builder-tool-scripts.test.ts +0 -1
  20. package/src/__tests__/app-bundler.test.ts +4 -12
  21. package/src/__tests__/app-executors.test.ts +2 -49
  22. package/src/__tests__/app-routes-csp.test.ts +114 -146
  23. package/src/__tests__/auth-fallback-events-store.test.ts +8 -1
  24. package/src/__tests__/build-persisted-content.test.ts +96 -0
  25. package/src/__tests__/bundle-scanner.test.ts +27 -1
  26. package/src/__tests__/call-controller.test.ts +291 -0
  27. package/src/__tests__/call-site-routing-connection-auto-resolve.test.ts +165 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +1395 -0
  29. package/src/__tests__/chat-reveal-guard-priming.test.ts +791 -0
  30. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  31. package/src/__tests__/compactor-image-manifest-trust.test.ts +50 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +9 -4
  33. package/src/__tests__/config-schema-cmd.test.ts +10 -11
  34. package/src/__tests__/config-schema.test.ts +190 -257
  35. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +208 -0
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +5 -19
  37. package/src/__tests__/conversation-agent-loop.test.ts +22 -9
  38. package/src/__tests__/conversation-error.test.ts +31 -0
  39. package/src/__tests__/conversation-load-history-repair.test.ts +110 -0
  40. package/src/__tests__/conversation-process-callsite.test.ts +12 -19
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +7 -21
  42. package/src/__tests__/conversation-summarize-route.test.ts +36 -44
  43. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  44. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +7 -7
  45. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +0 -1
  46. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -1
  47. package/src/__tests__/conversation-usage.test.ts +4 -12
  48. package/src/__tests__/credential-routes.test.ts +250 -0
  49. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  50. package/src/__tests__/db-migration-rollback.test.ts +22 -0
  51. package/src/__tests__/empty-response-hook.test.ts +186 -1
  52. package/src/__tests__/external-plugin-loader.test.ts +11 -5
  53. package/src/__tests__/heartbeat-service.test.ts +0 -28
  54. package/src/__tests__/host-shell-tool.test.ts +2 -0
  55. package/src/__tests__/inactive-tool-error-messages.test.ts +2 -2
  56. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +52 -8
  57. package/src/__tests__/internal-telemetry-routes.test.ts +23 -4
  58. package/src/__tests__/invite-routes-http.test.ts +12 -16
  59. package/src/__tests__/list-all-apps.test.ts +0 -4
  60. package/src/__tests__/llm-context-resolution.test.ts +32 -56
  61. package/src/__tests__/llm-request-log-turn-query.test.ts +109 -0
  62. package/src/__tests__/llm-resolver-override-or-default.test.ts +3 -52
  63. package/src/__tests__/llm-resolver.test.ts +342 -602
  64. package/src/__tests__/llm-schema.test.ts +79 -37
  65. package/src/__tests__/max-tokens-continue-hook.test.ts +19 -0
  66. package/src/__tests__/media-stream-output.test.ts +259 -3
  67. package/src/__tests__/media-stream-server-integration.test.ts +22 -1
  68. package/src/__tests__/media-stream-stt-session.test.ts +47 -0
  69. package/src/__tests__/memory-jobs-worker-cleanup-cadence.test.ts +33 -0
  70. package/src/__tests__/memory-recall-log-store.test.ts +47 -13
  71. package/src/__tests__/mock-gateway-ipc.ts +46 -1
  72. package/src/__tests__/mtime-cache.test.ts +61 -0
  73. package/src/__tests__/navigate-settings-tab.test.ts +2 -0
  74. package/src/__tests__/normalize-onboarding.test.ts +33 -0
  75. package/src/__tests__/onboarding-persona-write.test.ts +26 -0
  76. package/src/__tests__/plugin-api-resolve-credential.test.ts +140 -0
  77. package/src/__tests__/plugin-app-serve-routes.test.ts +166 -14
  78. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +2 -0
  79. package/src/__tests__/post-turn-tool-result-truncation.test.ts +38 -0
  80. package/src/__tests__/provider-commit-message-generator.test.ts +27 -20
  81. package/src/__tests__/provider-connections-backfill.test.ts +138 -0
  82. package/src/__tests__/provider-platform-proxy-integration.test.ts +10 -31
  83. package/src/__tests__/provider-registry-ollama.test.ts +8 -18
  84. package/src/__tests__/provider-send-message-override-profile.test.ts +23 -23
  85. package/src/__tests__/provider-usage-tracking.test.ts +9 -19
  86. package/src/__tests__/prune-old-conversations-job.test.ts +12 -0
  87. package/src/__tests__/published-app-updater.test.ts +22 -16
  88. package/src/__tests__/registry.test.ts +5 -16
  89. package/src/__tests__/retry-openrouter-only-normalization.test.ts +22 -23
  90. package/src/__tests__/retry-thinking-adaptive-only.test.ts +43 -48
  91. package/src/__tests__/retry-thinking-tool-choice.test.ts +57 -66
  92. package/src/__tests__/retry-verbosity-normalization.test.ts +24 -23
  93. package/src/__tests__/reveal-success-registry.test.ts +123 -0
  94. package/src/__tests__/run-conversation-turn-persistence.test.ts +130 -0
  95. package/src/__tests__/secret-fixtures.ts +9 -0
  96. package/src/__tests__/server-history-render.test.ts +28 -0
  97. package/src/__tests__/skills.test.ts +9 -4
  98. package/src/__tests__/slack-share-routes.test.ts +0 -1
  99. package/src/__tests__/stt-stream-session.test.ts +6 -5
  100. package/src/__tests__/subagent-call-site-routing.test.ts +69 -95
  101. package/src/__tests__/subagent-disposal.test.ts +2 -0
  102. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  103. package/src/__tests__/subagent-fork-spawn.test.ts +2 -0
  104. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  105. package/src/__tests__/subagent-role-registry.test.ts +37 -0
  106. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  107. package/src/__tests__/subagent-terminal-message.test.ts +50 -0
  108. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -5
  109. package/src/__tests__/surface-completion-nudge-hook.test.ts +19 -0
  110. package/src/__tests__/telemetry-routes.test.ts +99 -19
  111. package/src/__tests__/tool-audit.test.ts +34 -4
  112. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  113. package/src/__tests__/tool-profiler.test.ts +72 -1
  114. package/src/__tests__/tool-result-spool.test.ts +49 -4
  115. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -1
  116. package/src/__tests__/ui-channel-variants.test.ts +108 -0
  117. package/src/__tests__/ui-shape-teaching.test.ts +255 -0
  118. package/src/__tests__/usage-attribution.test.ts +18 -41
  119. package/src/__tests__/user-plugin-loader.test.ts +4 -4
  120. package/src/__tests__/voice-config-update.test.ts +46 -0
  121. package/src/__tests__/voice-session-bridge.test.ts +216 -84
  122. package/src/__tests__/workspace-migration-131-drop-web-fetch-mode.test.ts +120 -0
  123. package/src/agent/loop.ts +4 -3
  124. package/src/api/events/open-conversation.test.ts +64 -0
  125. package/src/api/events/open-conversation.ts +33 -0
  126. package/src/api/index.ts +6 -0
  127. package/src/api/responses/conversation-message.ts +5 -0
  128. package/src/apps/app-store.ts +25 -35
  129. package/src/bundler/app-bundler.ts +34 -48
  130. package/src/bundler/app-compiler.ts +39 -4
  131. package/src/bundler/bundle-scanner.ts +13 -0
  132. package/src/bundler/manifest.ts +1 -1
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +47 -0
  134. package/src/calls/call-constants.ts +5 -0
  135. package/src/calls/call-controller.ts +100 -32
  136. package/src/calls/call-transport.ts +9 -0
  137. package/src/calls/media-stream-output.ts +107 -4
  138. package/src/calls/media-stream-server.ts +29 -9
  139. package/src/calls/media-stream-stt-session.ts +7 -1
  140. package/src/calls/media-turn-detector.ts +11 -1
  141. package/src/calls/voice-session-bridge.ts +84 -63
  142. package/src/cli/commands/__tests__/inference-providers.test.ts +270 -33
  143. package/src/cli/commands/__tests__/notifications.test.ts +24 -3
  144. package/src/cli/commands/config.help.ts +6 -6
  145. package/src/cli/commands/credentials.help.ts +13 -0
  146. package/src/cli/commands/credentials.ts +6 -1
  147. package/src/cli/commands/email.help.ts +7 -0
  148. package/src/cli/commands/email.ts +35 -1
  149. package/src/cli/commands/inference-providers.ts +168 -107
  150. package/src/cli/commands/inference.help.ts +118 -46
  151. package/src/cli/commands/memory/index.help.ts +23 -0
  152. package/src/cli/commands/memory/nodes.ts +146 -0
  153. package/src/cli/commands/notifications.help.ts +10 -10
  154. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +40 -0
  155. package/src/cli/commands/oauth/connect-surface-guidance.ts +54 -0
  156. package/src/cli/commands/oauth/connect.test.ts +126 -0
  157. package/src/cli/commands/oauth/connect.ts +29 -6
  158. package/src/cli/commands/oauth/index.help.ts +7 -1
  159. package/src/cli/commands/oauth/status.test.ts +69 -3
  160. package/src/cli/commands/oauth/status.ts +50 -12
  161. package/src/cli/commands/plugins.help.ts +13 -2
  162. package/src/cli/commands/plugins.ts +61 -7
  163. package/src/cli/commands/telemetry.help.ts +13 -0
  164. package/src/cli/commands/telemetry.ts +45 -5
  165. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +8 -2
  166. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +213 -9
  167. package/src/cli/lib/bundled-marketplace.json +93 -36
  168. package/src/cli/lib/inspect-plugin.ts +5 -1
  169. package/src/cli/lib/install-from-github.ts +68 -4
  170. package/src/cli/lib/plugin-catalog-local.ts +6 -2
  171. package/src/cli/lib/plugin-fingerprint.ts +3 -3
  172. package/src/cli/lib/upgrade-plugin.ts +236 -35
  173. package/src/config/__tests__/default-profile-catalog.test.ts +8 -12
  174. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +137 -0
  175. package/src/config/__tests__/profile-materialization.test.ts +1 -88
  176. package/src/config/bundled-skills/AGENTS.md +3 -30
  177. package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
  178. package/src/config/bundled-skills/app-builder/TOOLS.json +23 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-open.ts +32 -0
  180. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
  181. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +7 -7
  182. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  183. package/src/config/bundled-skills/schedule/TOOLS.json +1 -1
  184. package/src/config/bundled-skills/settings/TOOLS.json +3 -1
  185. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +2 -0
  186. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +18 -10
  187. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  188. package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
  189. package/src/config/call-site-defaults.ts +3 -3
  190. package/src/config/feature-flag-registry.json +24 -39
  191. package/src/config/llm-resolver.ts +88 -475
  192. package/src/config/profile-materialization.ts +11 -11
  193. package/src/config/schema.ts +93 -127
  194. package/src/config/schemas/__tests__/live-voice.test.ts +24 -6
  195. package/src/config/schemas/__tests__/stt.test.ts +31 -3
  196. package/src/config/schemas/live-voice.ts +10 -4
  197. package/src/config/schemas/llm.ts +52 -15
  198. package/src/config/schemas/memory-lifecycle.ts +1 -1
  199. package/src/config/schemas/memory-retrospective.ts +8 -0
  200. package/src/config/schemas/memory-v2.ts +2 -2
  201. package/src/config/schemas/memory-v3.ts +1 -1
  202. package/src/config/schemas/services.ts +6 -3
  203. package/src/config/schemas/stt.ts +38 -21
  204. package/src/config/schemas/tts.ts +13 -18
  205. package/src/config/skills.ts +31 -21
  206. package/src/context/compactor.ts +44 -7
  207. package/src/context/post-turn-tool-result-truncation.ts +4 -2
  208. package/src/context/tool-result-spool.ts +26 -5
  209. package/src/conversations/__tests__/message-consolidation.test.ts +48 -0
  210. package/src/conversations/message-consolidation.ts +22 -2
  211. package/src/daemon/__tests__/conversation-tool-setup.test.ts +5 -12
  212. package/src/daemon/app-source-watcher.ts +17 -23
  213. package/src/daemon/chat-credential-redaction.ts +1365 -0
  214. package/src/daemon/conversation-agent-loop-handlers.ts +627 -34
  215. package/src/daemon/conversation-agent-loop.ts +71 -31
  216. package/src/daemon/conversation-error.ts +36 -14
  217. package/src/daemon/conversation-process.ts +22 -0
  218. package/src/daemon/conversation-store.ts +35 -0
  219. package/src/daemon/conversation-surfaces.ts +25 -25
  220. package/src/daemon/conversation-tool-setup.ts +33 -7
  221. package/src/daemon/conversation.ts +55 -5
  222. package/src/daemon/handlers/shared.ts +33 -2
  223. package/src/daemon/lifecycle.ts +4 -4
  224. package/src/daemon/message-types/conversations.ts +6 -17
  225. package/src/daemon/providers-setup.ts +8 -0
  226. package/src/daemon/tool-setup-types.ts +7 -1
  227. package/src/daemon/wake-conversation-ops.ts +10 -1
  228. package/src/hooks/types.ts +9 -0
  229. package/src/ipc/__tests__/email-ipc.test.ts +90 -0
  230. package/src/ipc/gateway-client.test.ts +59 -0
  231. package/src/ipc/gateway-client.ts +70 -27
  232. package/src/live-voice/__tests__/live-voice-events.test.ts +14 -2
  233. package/src/live-voice/__tests__/live-voice-integration.test.ts +116 -2
  234. package/src/live-voice/__tests__/live-voice-vad.test.ts +804 -13
  235. package/src/live-voice/__tests__/protocol.test.ts +122 -0
  236. package/src/live-voice/live-voice-session.ts +443 -40
  237. package/src/live-voice/protocol.ts +143 -1
  238. package/src/monitoring/__tests__/plugin-source-watch.test.ts +3 -1
  239. package/src/monitoring/plugin-source-watch.ts +3 -62
  240. package/src/notifications/README.md +1 -1
  241. package/src/permissions/checker.ts +8 -4
  242. package/src/persistence/__tests__/db-init-migrations-ok.test.ts +26 -0
  243. package/src/persistence/conversation-crud.ts +104 -2
  244. package/src/persistence/db-init.ts +14 -3
  245. package/src/persistence/job-handlers/cleanup.ts +15 -7
  246. package/src/persistence/llm-request-log-store.ts +88 -52
  247. package/src/persistence/migrations/298-move-memory-jobs-to-memory-db.ts +7 -31
  248. package/src/persistence/migrations/305-drop-contact-acl-columns.ts +3 -2
  249. package/src/persistence/migrations/326-move-injection-events-to-memory-db.ts +8 -34
  250. package/src/persistence/migrations/336-move-memory-v2-activation-logs-to-memory-db.ts +90 -0
  251. package/src/persistence/migrations/337-move-memory-recall-logs-to-memory-db.ts +114 -0
  252. package/src/persistence/migrations/338-move-memory-v3-selections-to-memory-db.ts +84 -0
  253. package/src/persistence/migrations/339-move-activation-sessions-to-memory-db.ts +52 -0
  254. package/src/persistence/migrations/__tests__/run-migrations.test.ts +155 -0
  255. package/src/persistence/migrations/helpers/relocation.ts +44 -1
  256. package/src/persistence/migrations/run-migrations.ts +25 -1
  257. package/src/persistence/schema/infrastructure.ts +9 -0
  258. package/src/persistence/schema/memory-core.ts +3 -0
  259. package/src/persistence/schema/memory-injection.ts +2 -0
  260. package/src/persistence/steps.ts +36 -0
  261. package/src/platform/client.test.ts +1 -44
  262. package/src/platform/client.ts +10 -20
  263. package/src/platform/consent-cache.test.ts +89 -23
  264. package/src/platform/consent-cache.ts +71 -33
  265. package/src/plugin-api/constants.ts +12 -0
  266. package/src/plugin-api/conversation-turn.ts +37 -14
  267. package/src/plugin-api/index.ts +11 -1
  268. package/src/plugin-api/resolve-credential.ts +75 -0
  269. package/src/plugin-api/vision-support.test.ts +8 -19
  270. package/src/plugin-api/vision-support.ts +26 -17
  271. package/src/plugins/collect-source-versions.ts +77 -0
  272. package/src/plugins/defaults/compaction/compact.ts +6 -0
  273. package/src/plugins/defaults/compaction/window-manager.ts +9 -0
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +18 -24
  275. package/src/plugins/defaults/empty-response/hooks/user-prompt-submit.ts +38 -0
  276. package/src/plugins/defaults/empty-response/refusal-quarantine.ts +99 -0
  277. package/src/plugins/defaults/image-fallback/__tests__/caption-cache-persistence.test.ts +7 -3
  278. package/src/plugins/defaults/index.ts +8 -1
  279. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +4 -1
  280. package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +48 -6
  281. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +28 -22
  282. package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +148 -0
  283. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +80 -8
  284. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +202 -0
  285. package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +64 -25
  286. package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +24 -11
  287. package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +70 -6
  288. package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +278 -0
  289. package/src/plugins/defaults/memory/activation-session-store.ts +27 -20
  290. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -9
  291. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -8
  292. package/src/plugins/defaults/memory/graph/retriever.test.ts +6 -6
  293. package/src/plugins/defaults/memory/graph/store.ts +114 -0
  294. package/src/plugins/defaults/memory/graph/tool-handlers.ts +36 -1
  295. package/src/plugins/defaults/memory/graph/tools.ts +47 -13
  296. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +16 -35
  297. package/src/plugins/defaults/memory/jobs-worker.ts +10 -1
  298. package/src/plugins/defaults/memory/memory-db.ts +3 -2
  299. package/src/plugins/defaults/memory/memory-recall-log-store.ts +129 -66
  300. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +8 -0
  301. package/src/plugins/defaults/memory/memory-retrospective-job.ts +11 -122
  302. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +216 -0
  303. package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +84 -46
  304. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +42 -32
  305. package/src/plugins/defaults/memory/path-containment.ts +21 -0
  306. package/src/plugins/defaults/memory/prompt-override.ts +51 -13
  307. package/src/plugins/defaults/memory/tools.test.ts +34 -0
  308. package/src/plugins/defaults/memory/tools.ts +12 -2
  309. package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +19 -15
  310. package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +24 -19
  311. package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +19 -15
  312. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +22 -2
  313. package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +5 -3
  314. package/src/plugins/defaults/memory/v2/harness/oracle.ts +59 -41
  315. package/src/plugins/defaults/memory/v2/harness/replay-input.ts +29 -25
  316. package/src/plugins/defaults/memory/v2/migration.ts +46 -16
  317. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +4 -0
  318. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +17 -15
  319. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +24 -22
  320. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +9 -5
  321. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +277 -10
  322. package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +57 -5
  323. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +9 -7
  324. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +38 -7
  325. package/src/plugins/defaults/memory/v3/hot-set.test.ts +36 -18
  326. package/src/plugins/defaults/memory/v3/hot-set.ts +12 -14
  327. package/src/plugins/defaults/memory/v3/learned-edges.test.ts +47 -27
  328. package/src/plugins/defaults/memory/v3/learned-edges.ts +12 -14
  329. package/src/plugins/defaults/memory/v3/orchestrate.ts +139 -22
  330. package/src/plugins/defaults/memory/v3/prune.test.ts +10 -3
  331. package/src/plugins/defaults/memory/v3/prune.ts +17 -11
  332. package/src/plugins/defaults/memory/v3/selection-log-store.ts +23 -11
  333. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +70 -53
  334. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +9 -6
  335. package/src/plugins/external-plugin-loader.ts +15 -15
  336. package/src/plugins/mtime-cache.ts +47 -0
  337. package/src/plugins/pipeline.ts +9 -1
  338. package/src/plugins/plugin-execution-context.ts +44 -0
  339. package/src/plugins/plugin-tree-walk.ts +31 -24
  340. package/src/plugins/source-fingerprint.ts +3 -4
  341. package/src/prompts/normalize-onboarding.ts +12 -0
  342. package/src/prompts/persona-resolver.ts +8 -0
  343. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +1 -1
  344. package/src/providers/__tests__/dispatch-connection-routing.test.ts +6 -9
  345. package/src/providers/__tests__/registry-native-web-search.test.ts +4 -10
  346. package/src/providers/__tests__/retry-callsite.test.ts +235 -215
  347. package/src/providers/__tests__/satellite-connection-routing.test.ts +8 -16
  348. package/src/providers/atlascloud/client.ts +10 -49
  349. package/src/providers/baseten/client.ts +43 -0
  350. package/src/providers/call-site-routing.ts +26 -13
  351. package/src/providers/connection-resolution.ts +9 -11
  352. package/src/providers/fetch-provider-catalog.ts +4 -2
  353. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +29 -4
  354. package/src/providers/inference/__tests__/connection-availability-keyless.test.ts +78 -0
  355. package/src/providers/inference/adapter-factory.ts +27 -6
  356. package/src/providers/inference/auth.ts +29 -1
  357. package/src/providers/inference/backfill.ts +61 -10
  358. package/src/providers/inference/connection-availability.ts +3 -2
  359. package/src/providers/inference/resolve-auth.ts +9 -1
  360. package/src/providers/model-catalog.ts +37 -0
  361. package/src/providers/openai/__tests__/api-error-normalization.test.ts +24 -2
  362. package/src/providers/openai/api-key-validation.ts +70 -0
  363. package/src/providers/retry.ts +2 -0
  364. package/src/providers/types.ts +11 -4
  365. package/src/providers/vellum-model-routing.test.ts +26 -0
  366. package/src/providers/vellum-model-routing.ts +30 -0
  367. package/src/providers/voice-error-copy.ts +47 -0
  368. package/src/runtime/agent-wake.ts +40 -24
  369. package/src/runtime/for-chat-mint-registry.ts +118 -0
  370. package/src/runtime/reveal-nonce.ts +49 -0
  371. package/src/runtime/reveal-success-registry.ts +306 -0
  372. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +88 -3
  373. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +161 -30
  374. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +7 -0
  375. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +15 -0
  376. package/src/runtime/routes/app-management-routes.ts +152 -45
  377. package/src/runtime/routes/app-routes.ts +16 -77
  378. package/src/runtime/routes/canned-message-complete.ts +16 -16
  379. package/src/runtime/routes/conversation-management-routes.ts +17 -14
  380. package/src/runtime/routes/conversation-query-routes.ts +54 -6
  381. package/src/runtime/routes/conversation-routes.ts +48 -2
  382. package/src/runtime/routes/credential-routes.ts +116 -3
  383. package/src/runtime/routes/email-routes.ts +17 -1
  384. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +33 -5
  385. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +6 -5
  386. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -28
  387. package/src/runtime/routes/inference-send-routes.ts +1 -1
  388. package/src/runtime/routes/internal-telemetry-routes.ts +7 -6
  389. package/src/runtime/routes/plugins-routes.ts +37 -6
  390. package/src/runtime/routes/publish-routes.ts +15 -18
  391. package/src/runtime/routes/schedule-worker-routes.ts +9 -0
  392. package/src/runtime/routes/secret-routes.ts +10 -0
  393. package/src/runtime/routes/telemetry-routes.ts +149 -45
  394. package/src/schedule/__tests__/schedule-timezone.test.ts +101 -0
  395. package/src/schedule/__tests__/worker-watchdog.test.ts +209 -0
  396. package/src/schedule/schedule-store.ts +12 -1
  397. package/src/schedule/schedule-timezone.ts +63 -0
  398. package/src/schedule/scheduler.ts +100 -6
  399. package/src/schedule/worker-control.ts +19 -0
  400. package/src/security/auth-fallback-events-store.ts +7 -6
  401. package/src/security/secret-scanner.ts +26 -1
  402. package/src/services/published-app-updater.ts +6 -11
  403. package/src/stt/stt-stream-session.ts +36 -16
  404. package/src/subagent/manager.ts +34 -8
  405. package/src/telemetry/AGENTS.md +30 -1
  406. package/src/telemetry/__tests__/config-setting-snapshot.test.ts +18 -0
  407. package/src/telemetry/__tests__/outbox-test-harness.ts +5 -3
  408. package/src/telemetry/config-setting-snapshot.ts +44 -10
  409. package/src/telemetry/telemetry-event-sources.test.ts +124 -30
  410. package/src/telemetry/telemetry-event-sources.ts +137 -83
  411. package/src/telemetry/telemetry-events-outbox.test.ts +46 -1
  412. package/src/telemetry/telemetry-events-outbox.ts +60 -9
  413. package/src/telemetry/telemetry-wire-source.json +1 -1
  414. package/src/telemetry/telemetry-wire-validation.ts +39 -2
  415. package/src/telemetry/telemetry-wire.generated.ts +8 -0
  416. package/src/telemetry/tool-audit.ts +15 -9
  417. package/src/telemetry/tool-executed-events-store.test.ts +1 -1
  418. package/src/telemetry/turn-events-store.ts +20 -0
  419. package/src/telemetry/types.ts +45 -12
  420. package/src/telemetry/usage-telemetry-reporter.test.ts +294 -25
  421. package/src/telemetry/usage-telemetry-reporter.ts +117 -18
  422. package/src/telemetry/watchdog-direct-emit.test.ts +11 -3
  423. package/src/telemetry/watchdog-direct-emit.ts +11 -6
  424. package/src/tools/apps/executors.ts +11 -36
  425. package/src/tools/executor.ts +14 -1
  426. package/src/tools/host-terminal/host-shell.ts +6 -2
  427. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +1 -1
  428. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +25 -0
  429. package/src/tools/network/web-fetch.ts +6 -2
  430. package/src/tools/skills/sandbox-runner.ts +5 -2
  431. package/src/tools/subagent/spawn.ts +7 -11
  432. package/src/tools/terminal/shell.ts +5 -2
  433. package/src/tools/tool-manifest.ts +0 -2
  434. package/src/tools/tool-profiler.ts +37 -6
  435. package/src/tools/ui-surface/channel-variants.ts +101 -0
  436. package/src/tools/ui-surface/definitions.ts +23 -67
  437. package/src/tools/ui-surface/surface-shape-docs.ts +229 -0
  438. package/src/tts/__tests__/provider-adapters.test.ts +18 -0
  439. package/src/tts/provider-catalog.ts +2 -4
  440. package/src/tts/providers/deepgram-provider.ts +10 -1
  441. package/src/types/onboarding-context.ts +8 -0
  442. package/src/usage/attribution.ts +18 -112
  443. package/src/{config/bundled-skills/messaging/tools/gmail-mime-helpers.ts → util/mime-type.ts} +4 -1
  444. package/src/util/provider-error-patterns.ts +7 -1
  445. package/src/util/worker-process.ts +105 -0
  446. package/src/watcher/__tests__/telemetry.test.ts +17 -4
  447. package/src/watcher/telemetry.ts +5 -5
  448. package/src/workspace/migrations/131-drop-web-fetch-mode.ts +61 -0
  449. package/src/workspace/migrations/registry.ts +2 -0
  450. package/src/workspace/provider-commit-message-generator.ts +6 -4
  451. package/src/__tests__/app-open-proxy.test.ts +0 -67
  452. package/src/onboarding/onboarding-research-events-store.test.ts +0 -230
  453. package/src/onboarding/onboarding-research-events-store.ts +0 -104
  454. package/src/runtime/routes/assets/vellum-design-system.css +0 -2236
  455. package/src/tools/apps/definitions.ts +0 -73
  456. package/src/tools/apps/open-proxy.ts +0 -43
@@ -6,6 +6,7 @@
6
6
  * testable while keeping shared mutable state bundled in EventHandlerState.
7
7
  */
8
8
 
9
+ import { SENTINEL_REDACTION_VERSION } from "@vellumai/service-contracts/redacted-credential";
9
10
  import type pino from "pino";
10
11
  import { v4 as uuid } from "uuid";
11
12
 
@@ -15,6 +16,7 @@ import type {
15
16
  TurnChannelContext,
16
17
  TurnInterfaceContext,
17
18
  } from "../channels/types.js";
19
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
18
20
  import { getConfig } from "../config/loader.js";
19
21
  import { recordEstimate } from "../context/estimator-calibration.js";
20
22
  import { stripInjectionsForCompaction } from "../context/strip-injections.js";
@@ -61,7 +63,18 @@ import type {
61
63
  Message,
62
64
  } from "../providers/types.js";
63
65
  import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
64
- import { redactSecrets } from "../security/secret-scanner.js";
66
+ import type { ForChatMint } from "../runtime/for-chat-mint-registry.js";
67
+ import {
68
+ currentForChatMintWatermark,
69
+ forChatMintsSince,
70
+ } from "../runtime/for-chat-mint-registry.js";
71
+ import { conversationRevealNonce } from "../runtime/reveal-nonce.js";
72
+ import {
73
+ closeRevealProofWindow,
74
+ currentRevealSuccessWatermark,
75
+ openRevealProofWindow,
76
+ } from "../runtime/reveal-success-registry.js";
77
+ import { credentialKey } from "../security/credential-key.js";
65
78
  import { extractDomain } from "../tools/network/domain-normalize.js";
66
79
  import {
67
80
  classifyWebSearchFailure,
@@ -81,6 +94,26 @@ import {
81
94
  cleanAssistantContent,
82
95
  drainDirectiveDisplayBuffer,
83
96
  } from "./assistant-attachments.js";
97
+ import type {
98
+ LiveRevealGuardEntry,
99
+ ResolvedRevealCandidate,
100
+ RevealCandidateRef,
101
+ } from "./chat-credential-redaction.js";
102
+ import {
103
+ buildLiveRevealGuardEntries,
104
+ collectRevealRefsFromCommand,
105
+ drainCandidateGuardedChunk,
106
+ drainSentinelGuardedText,
107
+ filterRefsByRevealProof,
108
+ neutralizeAndSwapLiveRevealValues,
109
+ redactCandidateValuesLegacy,
110
+ redactSecretsForChat,
111
+ remintAuthoritiesFromCandidates,
112
+ resolveProvenRevealCandidates,
113
+ resolveRefIdentities,
114
+ resolveRevealCandidates,
115
+ type SentinelRemintAuthority,
116
+ } from "./chat-credential-redaction.js";
84
117
  import type { Conversation } from "./conversation.js";
85
118
  import type { AssistantSurface } from "./conversation-agent-loop.js";
86
119
  import {
@@ -351,6 +384,111 @@ export interface EventHandlerState {
351
384
  * every produced assistant row is indexed.
352
385
  */
353
386
  readonly deferredFinalizeEffects: Array<() => Promise<void>>;
387
+ /**
388
+ * Credential refs parsed from `credentials reveal` invocations in this
389
+ * turn's shell-style tool commands (see `chat-credential-redaction.ts`).
390
+ * Staged per tool_use in {@link pendingRevealRefsByToolUse} and promoted
391
+ * here only when that tool's result arrives successfully; consumed at the
392
+ * persist seams to scope the candidate fetch for redaction-sentinel
393
+ * enrichment. Only ever read when the `chat-credential-reveal` feature
394
+ * flag is on.
395
+ */
396
+ readonly revealCandidateRefs: RevealCandidateRef[];
397
+ /**
398
+ * Reveal refs parsed at `tool_use` time, staged until the reveal route
399
+ * itself proves it executed. `tool_use` is emitted BEFORE tool
400
+ * execution — approval denial, cancellation, or the route's
401
+ * untrusted-shell block can still stop the command — and candidate
402
+ * resolution reads plaintext straight from the store, so resolving at
403
+ * propose time would fetch secrets for a reveal that never ran,
404
+ * side-stepping the reveal route's own policy gates. The enclosing
405
+ * tool's success is not proof either (`reveal … || true`, or an echo of
406
+ * the command text), so each staging captures a reveal-success-registry
407
+ * watermark and `handleToolResult` promotes only the refs whose
408
+ * identity the route actually served after it (see
409
+ * `filterRefsByRevealProof`); the rest are dropped.
410
+ */
411
+ readonly pendingRevealRefsByToolUse: Map<
412
+ string,
413
+ /** `proofWindowToken` arms registry recording for this staging's
414
+ * lifetime (see `openRevealProofWindow`) and is closed at result. */
415
+ { refs: RevealCandidateRef[]; watermark: number; proofWindowToken: number }
416
+ >;
417
+ /**
418
+ * Tool stdout held back from live `tool_output_chunk` emission because
419
+ * it ends in a partial occurrence of a reveal candidate's plaintext
420
+ * (keyed by toolUseId). Re-prepended to the tool's next chunk and
421
+ * flushed, redacted, when its tool_result arrives — nothing can complete
422
+ * the partial after that. Only ever populated while proven reveal
423
+ * candidates exist (see `handleToolOutputChunk`).
424
+ */
425
+ readonly toolOutputGuardBuffers: Map<string, string>;
426
+ /**
427
+ * Live text the sentinel stream guard held back from
428
+ * `assistant_text_delta` emission — a trailing partial `〔redacted:`
429
+ * trigger or reveal-candidate plaintext prefix that a later chunk may
430
+ * complete. Re-prepended to the next delta and flushed
431
+ * (neutralized + candidate-swapped) at message_complete. See
432
+ * `drainSentinelGuardedText`.
433
+ */
434
+ pendingSentinelGuardBuffer: string;
435
+ /**
436
+ * Precomputed live-swap entries for this turn's reveal candidates: when
437
+ * the model echoes a candidate's plaintext, the stream guard replaces it
438
+ * with its enriched sentinel before emission so the secret never reaches
439
+ * the wire. Primed asynchronously when `handleToolResult` confirms a
440
+ * staged reveal invocation actually succeeded — before the model's next
441
+ * text can echo the tool's stdout. Empty when the
442
+ * `chat-credential-reveal` flag is off.
443
+ */
444
+ liveRevealGuardEntries: readonly LiveRevealGuardEntry[];
445
+ /**
446
+ * Re-mint authorities derived from this turn's proven reveal candidates
447
+ * (see `remintAuthoritiesFromCandidates`), primed together with
448
+ * {@link liveRevealGuardEntries} behind the same dispatcher barrier so
449
+ * the live guard can consult them synchronously. Combined at each guard
450
+ * site with the `--for-chat` mints the reveal route recorded this run.
451
+ */
452
+ candidateRemintAuthorities: readonly SentinelRemintAuthority[];
453
+ /**
454
+ * `--for-chat` mint-registry watermark captured when this run's state
455
+ * was created. `turnForChatMints` returns only mints the reveal route
456
+ * recorded after this point — the guard's authority for re-minting a
457
+ * sentinel identity is an actually-executed reveal, never a parse of
458
+ * the requested command (which would let a quoted/commented-out
459
+ * invocation allowlist a forgery).
460
+ */
461
+ readonly forChatMintWatermark: number;
462
+ /**
463
+ * `credentialKey`-formatted identities of every reveal invocation THIS run
464
+ * staged from its own `tool_use` commands (`credentialKey` format). The
465
+ * conversation-scoping leg of re-mint authority: registry mints are
466
+ * global (a mint's request body carries no trustworthy conversation
467
+ * identity — see the registry module doc), so `turnForChatMints` accepts
468
+ * only mints whose identity this run itself requested. Staging parses
469
+ * the command the daemon actually dispatched — trusted context a
470
+ * subprocess env override can never rewrite.
471
+ */
472
+ readonly stagedRevealIdentities: Set<string>;
473
+ /**
474
+ * In-flight priming of {@link liveRevealGuardEntries}. The dispatcher
475
+ * awaits this before processing a `text_delta` (and before the
476
+ * end-of-message guard flush) so a fast reveal echo can never race the
477
+ * guard: without the barrier, a quick tool return plus a slow store read
478
+ * would let `drainSentinelGuardedText` run with an empty entry list and
479
+ * send the plaintext over SSE even though the persisted row redacts it.
480
+ * Cleared once settled — steady-state deltas await nothing.
481
+ */
482
+ liveRevealGuardPriming: Promise<void> | undefined;
483
+ /**
484
+ * Memoized resolution of {@link revealCandidateRefs} so a turn with many
485
+ * persist flushes fetches each candidate's plaintext once. Invalidated by
486
+ * ref-count when a later tool call adds more reveal invocations mid-turn.
487
+ */
488
+ revealCandidateCache?: {
489
+ refCount: number;
490
+ candidates: Promise<ResolvedRevealCandidate[]>;
491
+ };
354
492
  }
355
493
 
356
494
  /** Immutable context shared across event handlers within a single agent loop run. */
@@ -441,16 +579,211 @@ export function createEventHandlerState(): EventHandlerState {
441
579
  compactionStartMessages: new Map(),
442
580
  latencyCursor: 0,
443
581
  deferredFinalizeEffects: [],
582
+ revealCandidateRefs: [],
583
+ pendingRevealRefsByToolUse: new Map(),
584
+ toolOutputGuardBuffers: new Map(),
585
+ revealCandidateCache: undefined,
586
+ pendingSentinelGuardBuffer: "",
587
+ liveRevealGuardEntries: [],
588
+ candidateRemintAuthorities: [],
589
+ forChatMintWatermark: currentForChatMintWatermark(),
590
+ stagedRevealIdentities: new Set(),
591
+ liveRevealGuardPriming: undefined,
444
592
  };
445
593
  }
446
594
 
595
+ /**
596
+ * Resolve this turn's reveal candidates for chat sentinel redaction.
597
+ *
598
+ * Returns `undefined` when the `chat-credential-reveal` flag is off — the
599
+ * persist seams then use the legacy `<redacted type="…" />` marker for
600
+ * SCANNER matches, byte-identical to today (the candidate-aware
601
+ * exact-match fallback still applies via
602
+ * {@link resolvedRevealCandidatesForState}, which is deliberately not
603
+ * flag-gated: keeping a proven revealed plaintext out of persisted rows
604
+ * is independent of which marker format the client renders). When the
605
+ * flag is on, returns the resolved candidates (possibly empty: sentinel
606
+ * mode with nothing revealable). Resolution is memoized on the ref count
607
+ * so plaintexts are fetched at most once per new batch of reveal
608
+ * invocations.
609
+ */
610
+ async function chatRevealCandidates(
611
+ state: EventHandlerState,
612
+ ): Promise<readonly ResolvedRevealCandidate[] | undefined> {
613
+ if (!isAssistantFeatureFlagEnabled("chat-credential-reveal", getConfig())) {
614
+ return undefined;
615
+ }
616
+ return resolvedRevealCandidatesForState(state);
617
+ }
618
+
619
+ /**
620
+ * Flag-independent candidate resolution for the legacy-marker fallbacks:
621
+ * a route-proven reveal plaintext must be kept out of persisted rows in
622
+ * BOTH modes, so the fallback surfaces (tool results always; assistant
623
+ * text when the sentinel flag is off) resolve candidates here directly.
624
+ * Refs only exist when the reveal route actually served the identity
625
+ * (see `filterRefsByRevealProof`), so with the flag off this performs no
626
+ * store reads until a genuine reveal has already happened. Shares the
627
+ * per-state memoization with {@link chatRevealCandidates}.
628
+ */
629
+ async function resolvedRevealCandidatesForState(
630
+ state: EventHandlerState,
631
+ ): Promise<readonly ResolvedRevealCandidate[]> {
632
+ const refCount = state.revealCandidateRefs.length;
633
+ if (refCount === 0) {
634
+ return [];
635
+ }
636
+ if (
637
+ state.revealCandidateCache === undefined ||
638
+ state.revealCandidateCache.refCount !== refCount
639
+ ) {
640
+ state.revealCandidateCache = {
641
+ refCount,
642
+ candidates: resolveRevealCandidates([...state.revealCandidateRefs]),
643
+ };
644
+ }
645
+ return state.revealCandidateCache.candidates;
646
+ }
647
+
648
+ /**
649
+ * Kick off (or refresh) the live stream guard's swap entries from the
650
+ * memoized candidate resolution. Called from `handleToolResult` once a
651
+ * staged reveal invocation is confirmed successful — never at propose
652
+ * time, since candidate resolution reads plaintext straight from the
653
+ * store and the tool may yet be denied or cancelled. The store read is
654
+ * asynchronous while the dispatcher moves on, so a slow
655
+ * `getSecureKeyAsync` could let the next text deltas reach the guard
656
+ * before entries exist. The pending promise is therefore recorded on
657
+ * state and awaited by the dispatcher at the delta boundary (see
658
+ * `dispatchAgentEvent`), turning the race into a barrier. If resolution
659
+ * fails, the guard stays on its previous entries and the persist seams
660
+ * still redact; the stream guard remains a wire-level layer, persistence
661
+ * the redaction boundary.
662
+ */
663
+ function primeLiveRevealGuard(state: EventHandlerState): void {
664
+ const priming = chatRevealCandidates(state)
665
+ .then((candidates) => {
666
+ if (candidates !== undefined && candidates.length > 0) {
667
+ state.liveRevealGuardEntries = buildLiveRevealGuardEntries(candidates);
668
+ state.candidateRemintAuthorities =
669
+ remintAuthoritiesFromCandidates(candidates);
670
+ }
671
+ })
672
+ .catch((err: unknown) => {
673
+ log.debug(
674
+ { err },
675
+ "live reveal guard priming failed; stream swap stays inactive",
676
+ );
677
+ })
678
+ .finally(() => {
679
+ // Only clear our own registration — a later reveal in the same turn
680
+ // may have replaced the pending promise with a fresh one.
681
+ if (state.liveRevealGuardPriming === priming) {
682
+ state.liveRevealGuardPriming = undefined;
683
+ }
684
+ });
685
+ state.liveRevealGuardPriming = priming;
686
+ }
687
+
688
+ /**
689
+ * Barrier for the live reveal guard: resolves once any in-flight priming
690
+ * has settled. Awaited before text deltas are guarded and before the
691
+ * end-of-message guard flush, so echoed plaintext can never beat the
692
+ * guard entries onto the wire. No-op (no await, no microtask churn) when
693
+ * nothing is being primed.
694
+ */
695
+ async function awaitLiveRevealGuardReady(
696
+ state: EventHandlerState,
697
+ ): Promise<void> {
698
+ // Loop: priming that settles can be superseded by a newer registration
699
+ // (two reveals back-to-back) before this await resumes.
700
+ while (state.liveRevealGuardPriming !== undefined) {
701
+ await state.liveRevealGuardPriming;
702
+ }
703
+ }
704
+
705
+ /**
706
+ * `--for-chat` mints authorized for THIS run: recorded by the reveal route
707
+ * after this run's watermark AND matching an identity this run itself
708
+ * staged from its own `tool_use` commands. Both legs are load-bearing —
709
+ * the registry proves a reveal EXECUTED (a quoted/commented-out invocation
710
+ * never reaches the route), while the staging set scopes authority to the
711
+ * conversation that actually requested it (registry records carry no
712
+ * trustworthy conversation identity, so without this leg one
713
+ * conversation's approved reveal could authorize a concurrent
714
+ * conversation's forged sentinel). Read fresh at each guard site rather
715
+ * than cached: the route records a mint while the reveal tool is still
716
+ * executing, so by the time the model echoes the sentinel back the
717
+ * registry is already current — no priming race like the async candidate
718
+ * fetch has.
719
+ */
720
+ function turnForChatMints(
721
+ state: EventHandlerState,
722
+ deps: EventHandlerDeps,
723
+ ): ForChatMint[] {
724
+ if (state.stagedRevealIdentities.size === 0) {
725
+ return [];
726
+ }
727
+ const nonce = conversationRevealNonce(deps.ctx.conversationId);
728
+ return forChatMintsSince(state.forChatMintWatermark).filter(
729
+ (mint) =>
730
+ mint.nonce === nonce &&
731
+ state.stagedRevealIdentities.has(credentialKey(mint.service, mint.field)),
732
+ );
733
+ }
734
+
735
+ /**
736
+ * Combined re-mint authorities for the LIVE emit sites (synchronous):
737
+ * route-recorded `--for-chat` mints plus the candidate-derived authorities
738
+ * primed behind the dispatcher barrier alongside the swap entries.
739
+ */
740
+ function liveRemintAuthorities(
741
+ state: EventHandlerState,
742
+ deps: EventHandlerDeps,
743
+ ): SentinelRemintAuthority[] {
744
+ return [
745
+ ...turnForChatMints(state, deps),
746
+ ...state.candidateRemintAuthorities,
747
+ ];
748
+ }
749
+
750
+ /**
751
+ * Combined re-mint authorities for the PERSIST sites, deriving the
752
+ * candidate half from the resolved candidate set the call site already
753
+ * fetched — persistence must not depend on whether live priming ran
754
+ * (a flush can outlive the stream), so it never reads the primed state.
755
+ */
756
+ function persistRemintAuthorities(
757
+ state: EventHandlerState,
758
+ deps: EventHandlerDeps,
759
+ candidates: readonly ResolvedRevealCandidate[] | undefined,
760
+ ): SentinelRemintAuthority[] {
761
+ return [
762
+ ...turnForChatMints(state, deps),
763
+ ...(candidates !== undefined && candidates.length > 0
764
+ ? remintAuthoritiesFromCandidates(candidates)
765
+ : []),
766
+ ];
767
+ }
768
+
447
769
  // ── Partial-persistence helpers ──────────────────────────────────────
448
770
 
449
- /** Canonical persisted-content build: clean → append surfaces → redact. */
771
+ /**
772
+ * Canonical persisted-content build: clean → append surfaces → redact.
773
+ *
774
+ * `revealCandidates` (defined only when the sentinel flag is on) selects
775
+ * sentinel-mode redaction; `legacyFallbackCandidates` feeds the
776
+ * flag-independent exact-match fallback in legacy mode, so a route-proven
777
+ * reveal plaintext the scanner cannot classify never persists raw
778
+ * regardless of which marker format is active.
779
+ */
450
780
  export function buildPersistedAssistantContent(
451
781
  rawBlocks: readonly ContentBlock[],
452
782
  surfaces: readonly AssistantSurface[],
453
783
  activityByToolUseId?: ReadonlyMap<string, ToolActivityMetadata>,
784
+ revealCandidates?: readonly ResolvedRevealCandidate[],
785
+ legacyFallbackCandidates: readonly ResolvedRevealCandidate[] = [],
786
+ forChatMints: readonly SentinelRemintAuthority[] = [],
454
787
  ): ContentBlock[] {
455
788
  const { cleanedContent } = cleanAssistantContent(rawBlocks);
456
789
  const cleaned = cleanedContent as ContentBlock[];
@@ -471,7 +804,21 @@ export function buildPersistedAssistantContent(
471
804
  return withSurfaces.map((block) => {
472
805
  if (block.type === "text") {
473
806
  const tb = block as Extract<ContentBlock, { type: "text" }>;
474
- return { ...tb, text: redactSecrets(tb.text) };
807
+ // Sentinel mode (chat-credential-reveal flag on) persists redactions
808
+ // the client can render as chips; legacy mode keeps the marker
809
+ // byte-identical to today. Detection is the same scanner either way.
810
+ // Both modes neutralize forged sentinel-shaped strings first so
811
+ // arbitrary content can never manufacture a reveal chip — only
812
+ // redactor-inserted sentinels survive persistence. The
813
+ // `_redactionVersion` rider (internal, same convention as `_startedAt`)
814
+ // marks the block as neutralization-aware; `renderHistoryContent`
815
+ // neutralizes unmarked (pre-feature) blocks at read so forged sentinels
816
+ // in old history rows can never chip-ify either.
817
+ const text =
818
+ revealCandidates !== undefined
819
+ ? redactSecretsForChat(tb.text, revealCandidates, forChatMints)
820
+ : redactCandidateValuesLegacy(tb.text, legacyFallbackCandidates);
821
+ return { ...tb, text, _redactionVersion: SENTINEL_REDACTION_VERSION };
475
822
  }
476
823
  // Native server tools (Anthropic web_search) resolve mid-stream — their
477
824
  // `server_tool_complete` fires before `message_complete` — so the captured
@@ -584,6 +931,13 @@ function resetPartialPersistAccumulator(state: EventHandlerState): void {
584
931
  state.currentThinkingTimestamps = [];
585
932
  state.lastPersistedContentSeq = undefined;
586
933
  state.pendingPartialFlushPromise = undefined;
934
+ // If a previous LLM call (e.g. a retried/replaced stream) held back
935
+ // sentinel-guarded text via `drainSentinelGuardedText`, the stale
936
+ // bytes would be prepended to the retry's first text_delta, potentially
937
+ // emitting a raw credential prefix if it no longer matches a candidate.
938
+ // Clear alongside the other per-row accumulators so the new LLM call
939
+ // starts from an empty buffer.
940
+ state.pendingSentinelGuardBuffer = "";
587
941
  }
588
942
 
589
943
  /**
@@ -672,10 +1026,14 @@ async function flushAccumulatedContent(
672
1026
  return;
673
1027
  }
674
1028
 
1029
+ const revealCandidates = await chatRevealCandidates(state);
675
1030
  const built = buildPersistedAssistantContent(
676
1031
  state.currentMessageContent,
677
1032
  [],
678
1033
  state.toolActivityMetadata,
1034
+ revealCandidates,
1035
+ await resolvedRevealCandidatesForState(state),
1036
+ persistRemintAuthorities(state, deps, revealCandidates),
679
1037
  );
680
1038
  // Pair the seq with the exact content snapshot taken above: deltas that
681
1039
  // arrive while the write is in flight bump `lastPersistedContentSeq`
@@ -969,24 +1327,43 @@ function handleTextDelta(
969
1327
  statusText: "Thinking",
970
1328
  });
971
1329
  }
972
- deps.onEvent({
973
- type: "assistant_text_delta",
974
- text: drained.emitText,
975
- conversationId: deps.ctx.conversationId,
976
- messageId: state.lastAssistantMessageId,
977
- });
1330
+ // Live stream guard: neutralize forged sentinels (a genuine sentinel is
1331
+ // created at persist time, never in raw model output) and swap a reveal
1332
+ // candidate's echoed plaintext for its enriched sentinel so the secret
1333
+ // never flashes in the live transcript or crosses the wire. A trigger or
1334
+ // candidate prefix split across chunks is held back in
1335
+ // `pendingSentinelGuardBuffer` and flushed at message end.
1336
+ const guarded = drainSentinelGuardedText(
1337
+ state.pendingSentinelGuardBuffer + drained.emitText,
1338
+ state.liveRevealGuardEntries,
1339
+ liveRemintAuthorities(state, deps),
1340
+ );
1341
+ state.pendingSentinelGuardBuffer = guarded.bufferedRemainder;
1342
+ if (guarded.emitText.length > 0) {
1343
+ deps.onEvent({
1344
+ type: "assistant_text_delta",
1345
+ text: guarded.emitText,
1346
+ conversationId: deps.ctx.conversationId,
1347
+ messageId: state.lastAssistantMessageId,
1348
+ });
1349
+ // Mirror the RAW consumed bytes (not the emitted swap) into
1350
+ // currentMessageContent: the partial flush re-redacts them through
1351
+ // `redactSecretsForChat`, which derives the same enriched sentinel
1352
+ // from the plaintext — whereas a mirrored, already-swapped sentinel
1353
+ // would be indistinguishable from a forgery there and get
1354
+ // neutralized. Buffered bytes (partial triggers / candidate
1355
+ // prefixes) stay excluded until a later chunk emits them.
1356
+ appendTextToCurrentMessage(state, guarded.consumedRaw);
1357
+ // The hub stamps `seq` synchronously on the delta emitted above, so
1358
+ // `getCurrentSeq()` here is that delta's seq -- the position the
1359
+ // mirrored content now reflects. A partial flush snapshots this to
1360
+ // record how far the durable rows track the live stream.
1361
+ state.lastPersistedContentSeq = getCurrentSeq();
1362
+ schedulePartialFlush(state, deps);
1363
+ }
978
1364
  if (deps.shouldGenerateTitle) {
979
1365
  state.firstAssistantText += drained.emitText;
980
1366
  }
981
- // Mirror the drained delta into state.currentMessageContent so partial
982
- // flushes mid-turn see the same content the user is watching live.
983
- appendTextToCurrentMessage(state, drained.emitText);
984
- // The hub stamps `seq` synchronously on the delta emitted above, so
985
- // `getCurrentSeq()` here is that delta's seq -- the position the
986
- // mirrored content now reflects. A partial flush snapshots this to
987
- // record how far the durable rows track the live stream.
988
- state.lastPersistedContentSeq = getCurrentSeq();
989
- schedulePartialFlush(state, deps);
990
1367
  }
991
1368
  }
992
1369
 
@@ -1042,6 +1419,46 @@ export function handleToolUse(
1042
1419
  if (event.name === "app_create" || event.name === "app_refresh") {
1043
1420
  state.appBuildToolUsedThisRun = true;
1044
1421
  }
1422
+ // Record `credentials reveal` invocations so the persist seams can enrich
1423
+ // redaction sentinels with a proven vault identity (chat-credential-reveal).
1424
+ // Tool-name agnostic on purpose: any shell-style tool (bash, host_bash)
1425
+ // carries the command in `input.command`. Pure string parse — no store
1426
+ // access happens here: `tool_use` precedes execution, and the refs are
1427
+ // only STAGED until `handleToolResult` sees the reveal actually succeed
1428
+ // (see `pendingRevealRefsByToolUse` — priming at propose time would read
1429
+ // plaintext for a command that approval/cancellation may still block).
1430
+ const command = (event.input as { command?: unknown } | undefined)?.command;
1431
+ if (typeof command === "string" && command.length > 0) {
1432
+ // Id-form refs resolve to service/field NOW (metadata-only lookup): the
1433
+ // same compound command may remove the credential after revealing it,
1434
+ // and by result time the id would no longer resolve — dropping the
1435
+ // proof for a value the tool already printed.
1436
+ const refs = resolveRefIdentities(collectRevealRefsFromCommand(command));
1437
+ if (refs.length > 0) {
1438
+ state.pendingRevealRefsByToolUse.set(event.id, {
1439
+ refs,
1440
+ // Captured before execution: only reveal-route successes recorded
1441
+ // AFTER this point can prove these refs at result time.
1442
+ watermark: currentRevealSuccessWatermark(),
1443
+ // Arms registry recording for this staging's lifetime — the route
1444
+ // retains plaintext only while some tool's proof is pending.
1445
+ proofWindowToken: openRevealProofWindow(),
1446
+ });
1447
+ // The conversation-scoping leg of `--for-chat` re-mint authority:
1448
+ // record which identities THIS run's own commands named. Retained for
1449
+ // the whole run (unlike the staging entry, which handleToolResult
1450
+ // consumes) — the model echoes the sentinel back only after the tool
1451
+ // completes. Parse-only, so by itself this authorizes nothing; a
1452
+ // registry mint (an executed reveal) must also exist.
1453
+ for (const ref of refs) {
1454
+ if (ref.service !== undefined && ref.field !== undefined) {
1455
+ state.stagedRevealIdentities.add(
1456
+ credentialKey(ref.service, ref.field),
1457
+ );
1458
+ }
1459
+ }
1460
+ }
1461
+ }
1045
1462
  const startedAt = Date.now();
1046
1463
  state.toolCallTimestamps.set(event.id, { startedAt });
1047
1464
  state.currentToolUseId = event.id;
@@ -1182,15 +1599,59 @@ function handleToolOutputChunk(
1182
1599
  subToolIsError: structured.subToolIsError,
1183
1600
  subToolId: structured.subToolId,
1184
1601
  });
1185
- } else {
1186
- deps.onEvent({
1187
- type: "tool_output_chunk",
1188
- chunk: event.chunk,
1189
- conversationId: deps.ctx.conversationId,
1190
- toolUseId: event.toolUseId,
1191
- messageId: state.lastAssistantMessageId,
1192
- });
1602
+ return;
1193
1603
  }
1604
+
1605
+ // Redact revealed plaintext from the LIVE stdout stream. The final
1606
+ // tool_result is redacted at its seam, but these chunks reach the client
1607
+ // first and the web drawer renders them until the result replaces them —
1608
+ // without this guard a `credentials reveal` value flashes raw for the
1609
+ // whole tool run. By the time printed bytes arrive here the route has
1610
+ // already recorded any success (the record precedes the CLI receiving
1611
+ // the plaintext), so proven candidates are available SYNCHRONOUSLY from
1612
+ // the registry — no vault read, and the reveal-free path stays untouched.
1613
+ // Covers both this tool's own staged reveals and values already promoted
1614
+ // by an earlier tool in the turn (a later `echo <value>` streams too). A
1615
+ // trailing partial occurrence is held back for the next chunk; the
1616
+ // tool_result seam flushes the remainder. Structured control frames
1617
+ // above are forwarded untouched — they are parsed subtool events, not
1618
+ // reveal stdout, and rewriting their raw JSON could corrupt them.
1619
+ let chunk = event.chunk;
1620
+ const staged = state.pendingRevealRefsByToolUse.get(event.toolUseId);
1621
+ const guardRefs =
1622
+ staged === undefined
1623
+ ? state.revealCandidateRefs
1624
+ : [
1625
+ ...state.revealCandidateRefs,
1626
+ ...filterRefsByRevealProof(
1627
+ staged.refs,
1628
+ staged.watermark,
1629
+ conversationRevealNonce(deps.ctx.conversationId),
1630
+ ),
1631
+ ];
1632
+ if (guardRefs.length > 0) {
1633
+ const candidates = resolveProvenRevealCandidates(guardRefs);
1634
+ if (candidates.length > 0) {
1635
+ const held = state.toolOutputGuardBuffers.get(event.toolUseId) ?? "";
1636
+ const drained = drainCandidateGuardedChunk(held + chunk, candidates);
1637
+ state.toolOutputGuardBuffers.set(
1638
+ event.toolUseId,
1639
+ drained.bufferedRemainder,
1640
+ );
1641
+ if (drained.emitText.length === 0) {
1642
+ return;
1643
+ }
1644
+ chunk = drained.emitText;
1645
+ }
1646
+ }
1647
+
1648
+ deps.onEvent({
1649
+ type: "tool_output_chunk",
1650
+ chunk,
1651
+ conversationId: deps.ctx.conversationId,
1652
+ toolUseId: event.toolUseId,
1653
+ messageId: state.lastAssistantMessageId,
1654
+ });
1194
1655
  }
1195
1656
 
1196
1657
  export function handleInputJsonDelta(
@@ -1222,17 +1683,30 @@ export function handleInputJsonDelta(
1222
1683
  */
1223
1684
  function buildToolResultBlocks(
1224
1685
  pending: ReadonlyMap<string, PendingToolResult>,
1686
+ revealCandidates: readonly ResolvedRevealCandidate[] = [],
1225
1687
  ) {
1688
+ // Tool results keep the legacy `<redacted type/>` marker (NOT sentinels):
1689
+ // history maps tool_result content to `toolCall.result`, which the tool
1690
+ // detail panel renders via CodeBlock — a path with no markdown/chip
1691
+ // support, where a sentinel would show as an inert glyph string. Convert
1692
+ // here only once that surface can render chips. Forged sentinel-shaped
1693
+ // strings in tool output are still neutralized so they can never reach a
1694
+ // chip-enabled surface via quoting. Proven reveal-candidate plaintexts
1695
+ // the scanner cannot classify (opaque manual tokens in the reveal's own
1696
+ // stdout) get a candidate-aware legacy-marker fallback — the tool detail
1697
+ // panel and history must not retain a value every other surface redacts.
1698
+ const redact = (text: string): string =>
1699
+ redactCandidateValuesLegacy(text, revealCandidates);
1226
1700
  return Array.from(pending.entries()).map(([toolUseId, result]) => ({
1227
1701
  type: "tool_result",
1228
1702
  tool_use_id: toolUseId,
1229
- content: redactSecrets(result.content),
1703
+ content: redact(result.content),
1230
1704
  is_error: result.isError,
1231
1705
  ...(result.contentBlocks
1232
1706
  ? {
1233
1707
  contentBlocks: result.contentBlocks.map((block) =>
1234
1708
  block.type === "text"
1235
- ? { ...block, text: redactSecrets(block.text) }
1709
+ ? { ...block, text: redact(block.text) }
1236
1710
  : block,
1237
1711
  ),
1238
1712
  }
@@ -1316,6 +1790,7 @@ async function persistPendingToolResultRow(
1316
1790
  // the in-flight delta file; the finalize seam folds the row inline.
1317
1791
  const batchBlocks = buildToolResultBlocks(
1318
1792
  state.pendingToolResults,
1793
+ await resolvedRevealCandidatesForState(state),
1319
1794
  ) as ContentBlock[];
1320
1795
  const writer = state.inflightWriters.get(rowId);
1321
1796
  const persisted = writer
@@ -1365,7 +1840,10 @@ export async function finalizePendingToolResultRow(
1365
1840
  // for workspace references so the blob stays in the attachment store, out of
1366
1841
  // this row and the lexical index. Runs once, here at finalize (on-arrival
1367
1842
  // writes keep base64 for durability); the send boundary re-inflates the refs.
1368
- const blocks = buildToolResultBlocks(state.pendingToolResults);
1843
+ const blocks = buildToolResultBlocks(
1844
+ state.pendingToolResults,
1845
+ await resolvedRevealCandidatesForState(state),
1846
+ );
1369
1847
  const contentJson = JSON.stringify(
1370
1848
  conv != null
1371
1849
  ? referenceMediaBlocksForPersist(
@@ -1454,6 +1932,76 @@ export async function handleToolResult(
1454
1932
  deps: EventHandlerDeps,
1455
1933
  event: Extract<AgentEvent, { type: "tool_result" }>,
1456
1934
  ): Promise<void> {
1935
+ // Promote staged reveal refs now that the tool has finished: a ref is
1936
+ // promoted only if the reveal ROUTE recorded a success for its identity
1937
+ // after the staging watermark — the enclosing tool's exit status proves
1938
+ // nothing (`reveal … || true` succeeds when the route failed; an echo of
1939
+ // the command text never calls the route; conversely, a compound command
1940
+ // can print the secret and then exit non-zero, in which case the model
1941
+ // HAS the plaintext and the guard entry is protective). Only then may
1942
+ // candidate resolution read the plaintext from the store. Synchronous —
1943
+ // the dispatcher awaits `tool_result` before any later `text_delta`, so
1944
+ // the priming promise is registered before the barrier can be consulted.
1945
+ const stagedReveal = state.pendingRevealRefsByToolUse.get(event.toolUseId);
1946
+ if (stagedReveal !== undefined) {
1947
+ state.pendingRevealRefsByToolUse.delete(event.toolUseId);
1948
+ const provenRefs = filterRefsByRevealProof(
1949
+ stagedReveal.refs,
1950
+ stagedReveal.watermark,
1951
+ conversationRevealNonce(deps.ctx.conversationId),
1952
+ );
1953
+ // The proof is consumed (proven values now ride the refs), so this
1954
+ // staging no longer needs the registry to record.
1955
+ closeRevealProofWindow(stagedReveal.proofWindowToken);
1956
+ if (provenRefs.length > 0) {
1957
+ state.revealCandidateRefs.push(...provenRefs);
1958
+ primeLiveRevealGuard(state);
1959
+ }
1960
+ }
1961
+
1962
+ // Redact THIS tool's own stdout before it leaves the daemon. The reveal
1963
+ // command that just proved a candidate printed the plaintext into its own
1964
+ // `event.content`; priming the assistant-text guard only protects a later
1965
+ // echo, not this result. The persist path already redacts via
1966
+ // `buildToolResultBlocks`, but the LIVE `tool_result` SSE below forwards
1967
+ // `event.content` verbatim and the web reducer stores `event.result`
1968
+ // directly — so an opaque/manual value the scanner cannot classify would
1969
+ // flash in the tool card until a history refetch. Apply the same
1970
+ // candidate-aware legacy redaction the persisted tool-result row uses, to
1971
+ // both the buffered content and the emitted result, so wire and storage
1972
+ // agree from the first frame.
1973
+ //
1974
+ // Guard the resolution behind the ref count: candidate resolution is async
1975
+ // (it reads the vault), and awaiting it here would push every side effect
1976
+ // below — the cancellation emit, the pending-result buffering, the
1977
+ // activity/risk metadata capture, `annotatePersistedAssistantMessage`, and
1978
+ // the live `tool_result` emit — onto a later microtask. Callers that drive
1979
+ // this handler synchronously (the dispatcher, and the metadata/preview
1980
+ // tests) rely on those effects landing before the returned promise's first
1981
+ // suspension, so a reveal-free tool result must stay fully synchronous and
1982
+ // pass its content through untouched — exactly as before this guard shipped.
1983
+ // The refs are non-empty only after the reveal route actually served an
1984
+ // identity this turn, which is precisely when redaction must fire.
1985
+ let redactedContent = event.content;
1986
+ // DISCARD (never emit) stdout the live chunk guard held back for this
1987
+ // tool. The buffer was held precisely because it contains or ends in a
1988
+ // PARTIAL occurrence of a candidate's plaintext, and complete-value
1989
+ // redaction cannot mask a partial — flushing it would put up to
1990
+ // value-length-minus-one raw credential bytes on the wire. Nothing is
1991
+ // lost: the redacted `event.content` emitted below carries the full
1992
+ // output and supersedes the streamed view immediately.
1993
+ state.toolOutputGuardBuffers.delete(event.toolUseId);
1994
+ if (state.revealCandidateRefs.length > 0) {
1995
+ const revealCandidatesForResult =
1996
+ await resolvedRevealCandidatesForState(state);
1997
+ if (revealCandidatesForResult.length > 0) {
1998
+ redactedContent = redactCandidateValuesLegacy(
1999
+ event.content,
2000
+ revealCandidatesForResult,
2001
+ );
2002
+ }
2003
+ }
2004
+
1457
2005
  // A synthesized cancellation (the tool never executed) is captured for
1458
2006
  // persistence and forwarded to the client like any result, but skips every
1459
2007
  // side effect that assumes the tool ran. A real result already captured or
@@ -1465,6 +2013,10 @@ export async function handleToolResult(
1465
2013
  ) {
1466
2014
  return;
1467
2015
  }
2016
+ // Buffer the RAW content: every persist path redacts exactly once via
2017
+ // `buildToolResultBlocks`, and buffering already-redacted bytes would
2018
+ // redact twice — a candidate value overlapping the marker's own text
2019
+ // would corrupt the persisted marker on the second pass.
1468
2020
  state.pendingToolResults.set(event.toolUseId, {
1469
2021
  content: event.content,
1470
2022
  isError: event.isError,
@@ -1473,7 +2025,7 @@ export async function handleToolResult(
1473
2025
  deps.onEvent({
1474
2026
  type: "tool_result",
1475
2027
  toolName: "",
1476
- result: event.content,
2028
+ result: redactedContent,
1477
2029
  isError: event.isError,
1478
2030
  conversationId: deps.ctx.conversationId,
1479
2031
  messageId: state.lastAssistantMessageId,
@@ -1507,6 +2059,12 @@ export async function handleToolResult(
1507
2059
  // Perform state mutations before deps.onEvent() so that if onEvent throws
1508
2060
  // (e.g. SSE disconnection) and the error is suppressed by dispatchAgentEvent,
1509
2061
  // critical state like pendingToolResults and currentToolUseId is still updated.
2062
+ // Buffer the RAW content: every persist path redacts exactly once via
2063
+ // `buildToolResultBlocks` (with the fullest candidate set at flush time),
2064
+ // so wire and storage still agree. Buffering the already-redacted bytes
2065
+ // would redact twice — a candidate value that overlaps the marker's own
2066
+ // text (e.g. a manual value `redacted`) would match inside the
2067
+ // first-pass marker and corrupt the persisted row.
1510
2068
  state.pendingToolResults.set(event.toolUseId, {
1511
2069
  content: event.content,
1512
2070
  isError: event.isError,
@@ -1614,10 +2172,12 @@ export async function handleToolResult(
1614
2172
  }
1615
2173
 
1616
2174
  // Send to client last so state is consistent even if onEvent throws.
2175
+ // `result` carries the reveal-redacted stdout (see above) so the live tool
2176
+ // card never shows a revealed plaintext the persisted row hides.
1617
2177
  deps.onEvent({
1618
2178
  type: "tool_result",
1619
2179
  toolName: "",
1620
- result: event.content,
2180
+ result: redactedContent,
1621
2181
  isError: event.isError,
1622
2182
  diff: event.diff,
1623
2183
  status: event.status,
@@ -2053,18 +2613,42 @@ export async function handleMessageComplete(
2053
2613
  state.pendingPartialFlushPromise = undefined;
2054
2614
  }
2055
2615
 
2056
- // Flush any remaining directive display buffer
2057
- if (state.pendingDirectiveDisplayBuffer.length > 0) {
2616
+ // Flush any remaining directive display buffer, prepending live text the
2617
+ // sentinel guard held back (a split trigger or candidate-prefix tail).
2618
+ // The concatenation is candidate-swapped and gap-neutralized as a whole:
2619
+ // at end-of-message nothing can complete a partial trigger (a completed
2620
+ // one here would be a forged sentinel), and a reveal-candidate plaintext
2621
+ // that completes across the two buffers still swaps to its sentinel.
2622
+ const trailingLiveText =
2623
+ state.pendingSentinelGuardBuffer + state.pendingDirectiveDisplayBuffer;
2624
+ if (trailingLiveText.length > 0) {
2625
+ // Same barrier as the text-delta path: the flush below swaps against
2626
+ // `liveRevealGuardEntries`, so an in-flight priming must settle first.
2627
+ await awaitLiveRevealGuardReady(state);
2058
2628
  deps.onEvent({
2059
2629
  type: "assistant_text_delta",
2060
- text: state.pendingDirectiveDisplayBuffer,
2630
+ text: neutralizeAndSwapLiveRevealValues(
2631
+ trailingLiveText,
2632
+ state.liveRevealGuardEntries,
2633
+ liveRemintAuthorities(state, deps),
2634
+ ),
2061
2635
  conversationId: deps.ctx.conversationId,
2062
2636
  messageId: state.lastAssistantMessageId,
2063
2637
  });
2638
+ // The hub stamps `seq` synchronously on the delta emitted above, so
2639
+ // `getCurrentSeq()` is that delta's position — advance the persisted-seq
2640
+ // mirror exactly like the normal text-delta path. The finalize below
2641
+ // records this value; without the advance it would record the PREVIOUS
2642
+ // emitted chunk's seq, so a `/messages` snapshot could contain this tail
2643
+ // while advertising a seq before the delta that carried it — and a
2644
+ // reconnecting client applying `seq > snapshot.seq` would append the
2645
+ // tail a second time.
2646
+ state.lastPersistedContentSeq = getCurrentSeq();
2064
2647
  if (deps.shouldGenerateTitle) {
2065
2648
  state.firstAssistantText += state.pendingDirectiveDisplayBuffer;
2066
2649
  }
2067
2650
  state.pendingDirectiveDisplayBuffer = "";
2651
+ state.pendingSentinelGuardBuffer = "";
2068
2652
  }
2069
2653
 
2070
2654
  // Finalize the grouped tool-result row. Each result was persisted into this
@@ -2111,11 +2695,15 @@ export async function handleMessageComplete(
2111
2695
  // redacted) via the shared helper. The partial-persist flush uses
2112
2696
  // the same helper with `surfaces=[]` so a mid-turn snapshot lands in
2113
2697
  // the same shape as the finalize.
2698
+ const finalRevealCandidates = await chatRevealCandidates(state);
2114
2699
  const contentForPersistence = stampThinkingTiming(
2115
2700
  buildPersistedAssistantContent(
2116
2701
  event.message.content as ContentBlock[],
2117
2702
  deps.ctx.currentTurnSurfaces,
2118
2703
  state.toolActivityMetadata,
2704
+ finalRevealCandidates,
2705
+ await resolvedRevealCandidatesForState(state),
2706
+ persistRemintAuthorities(state, deps, finalRevealCandidates),
2119
2707
  ),
2120
2708
  state.currentThinkingTimestamps,
2121
2709
  );
@@ -2431,6 +3019,11 @@ export async function dispatchAgentEvent(
2431
3019
  await handleLlmCallStarted(state, deps);
2432
3020
  break;
2433
3021
  case "text_delta":
3022
+ // Reveal-guard barrier: if a `credentials reveal` tool_use just
3023
+ // started priming the live guard, resolve it before this delta is
3024
+ // guarded — otherwise a fast reveal echo could cross SSE with an
3025
+ // empty entry list. Steady state awaits nothing.
3026
+ await awaitLiveRevealGuardReady(state);
2434
3027
  handleTextDelta(state, deps, event);
2435
3028
  break;
2436
3029
  case "thinking_delta":