@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091926.ebb2d62

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 (490) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/docs/activation-funnel-telemetry.md +310 -0
  4. package/examples/plugins/echo/README.md +61 -60
  5. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  6. package/examples/plugins/echo/hooks/stop.ts +16 -0
  7. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  8. package/examples/plugins/echo/package.json +1 -2
  9. package/examples/plugins/echo/src/emit.ts +19 -0
  10. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  11. package/openapi.yaml +273 -121
  12. package/package.json +2 -2
  13. package/scripts/sync-llm-catalog.ts +2 -0
  14. package/src/__tests__/activation-early-marking.test.ts +120 -0
  15. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  16. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  17. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  18. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  19. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  21. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  22. package/src/__tests__/agent-loop.test.ts +441 -96
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  24. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  25. package/src/__tests__/anthropic-provider.test.ts +23 -8
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  27. package/src/__tests__/app-control-flow.test.ts +1 -1
  28. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  29. package/src/__tests__/app-executors.test.ts +132 -0
  30. package/src/__tests__/approval-cascade.test.ts +6 -5
  31. package/src/__tests__/approval-routes-http.test.ts +4 -1
  32. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  33. package/src/__tests__/build-persisted-content.test.ts +75 -1
  34. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  35. package/src/__tests__/channel-approvals.test.ts +1 -1
  36. package/src/__tests__/compaction-circuit.test.ts +258 -0
  37. package/src/__tests__/compaction-direct.test.ts +146 -0
  38. package/src/__tests__/compaction-events.test.ts +7 -7
  39. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  40. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  41. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  42. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  43. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  48. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  49. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  50. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  51. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  52. package/src/__tests__/conversation-error.test.ts +15 -1
  53. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  54. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  55. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  56. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  57. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  58. package/src/__tests__/conversation-queue.test.ts +10 -10
  59. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  60. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  61. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  62. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  63. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  64. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  65. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  66. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  67. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  68. package/src/__tests__/conversation-undo.test.ts +2 -2
  69. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  70. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  71. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  72. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  73. package/src/__tests__/corrected-target.test.ts +93 -0
  74. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -4
  75. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  76. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  77. package/src/__tests__/db-acp-history.test.ts +101 -0
  78. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  79. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  80. package/src/__tests__/empty-response-hook.test.ts +1 -1
  81. package/src/__tests__/file-write-tool.test.ts +63 -0
  82. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  83. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  84. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  85. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  86. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  87. package/src/__tests__/heartbeat-service.test.ts +1 -0
  88. package/src/__tests__/history-repair-hook.test.ts +5 -1
  89. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  90. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  91. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  92. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  93. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  94. package/src/__tests__/injector-background-turn.test.ts +13 -23
  95. package/src/__tests__/injector-chain.test.ts +268 -44
  96. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  97. package/src/__tests__/injector-document-comments.test.ts +97 -114
  98. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  99. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  100. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  101. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  102. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  103. package/src/__tests__/llm-resolver.test.ts +73 -0
  104. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  105. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  106. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  107. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  108. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  109. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  110. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  111. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  112. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  113. package/src/__tests__/plugin-registry.test.ts +3 -49
  114. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  115. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  116. package/src/__tests__/plugin-types.test.ts +0 -70
  117. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  118. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  119. package/src/__tests__/published-app-updater.test.ts +138 -0
  120. package/src/__tests__/reaction-persistence.test.ts +1 -1
  121. package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  123. package/src/__tests__/server-history-render.test.ts +39 -0
  124. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  125. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  126. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  127. package/src/__tests__/subagent-detail.test.ts +25 -7
  128. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  129. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  130. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  131. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  132. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  133. package/src/__tests__/title-generate-hook.test.ts +5 -1
  134. package/src/__tests__/tool-error-hook.test.ts +1 -1
  135. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  136. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  137. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  138. package/src/__tests__/web-fetch.test.ts +45 -0
  139. package/src/acp/__tests__/agent-process.test.ts +161 -0
  140. package/src/acp/__tests__/client-handler.test.ts +40 -0
  141. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  142. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  143. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  144. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  145. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  146. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  147. package/src/acp/agent-process.ts +61 -1
  148. package/src/acp/auto-install.test.ts +280 -0
  149. package/src/acp/auto-install.ts +232 -0
  150. package/src/acp/client-handler.ts +31 -0
  151. package/src/acp/prepare-agent-env.ts +80 -27
  152. package/src/acp/resolve-agent.test.ts +188 -28
  153. package/src/acp/resolve-agent.ts +119 -42
  154. package/src/acp/resume-hint.ts +23 -0
  155. package/src/acp/session-manager.ts +507 -73
  156. package/src/agent/compaction-circuit.ts +60 -102
  157. package/src/agent/loop.ts +403 -251
  158. package/src/api/events/tool-result.ts +9 -0
  159. package/src/api/events/tool-use-start.ts +7 -0
  160. package/src/api/responses/conversation-message.ts +72 -13
  161. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  162. package/src/approvals/guardian-request-resolvers.ts +27 -1
  163. package/src/background-wake/next-wake.ts +1 -0
  164. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  165. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  166. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  167. package/src/cli/commands/memory-v3.ts +57 -199
  168. package/src/cli/commands/plugins.ts +43 -37
  169. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  170. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  171. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  172. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  173. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  174. package/src/cli/lib/install-from-github.ts +797 -60
  175. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  176. package/src/cli/lib/plugin-details.ts +416 -0
  177. package/src/cli/lib/plugin-marketplace.ts +215 -0
  178. package/src/cli/lib/search-plugins.ts +201 -29
  179. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  180. package/src/config/acp-defaults.test.ts +10 -0
  181. package/src/config/acp-defaults.ts +9 -3
  182. package/src/config/acp-schema.ts +1 -7
  183. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  184. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  185. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  186. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  187. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  188. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  189. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  190. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  191. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  192. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  193. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  194. package/src/config/bundled-tool-registry.ts +4 -0
  195. package/src/config/call-site-defaults.ts +0 -3
  196. package/src/config/feature-flag-registry.json +10 -56
  197. package/src/config/llm-resolver.ts +39 -7
  198. package/src/config/loader.ts +5 -22
  199. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  200. package/src/config/schemas/call-site-catalog.ts +0 -21
  201. package/src/config/schemas/heartbeat.ts +9 -0
  202. package/src/config/schemas/llm.ts +17 -3
  203. package/src/config/schemas/memory-v3.ts +58 -8
  204. package/src/config/seed-inference-profiles.ts +18 -0
  205. package/src/context/compactor.ts +22 -4
  206. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  207. package/src/context/strip-injections.ts +8 -2
  208. package/src/daemon/conversation-agent-loop-handlers.ts +120 -42
  209. package/src/daemon/conversation-agent-loop.ts +308 -1297
  210. package/src/daemon/conversation-error.ts +31 -4
  211. package/src/daemon/conversation-history.ts +1 -1
  212. package/src/daemon/conversation-lifecycle.ts +11 -255
  213. package/src/daemon/conversation-media-retry.ts +19 -6
  214. package/src/daemon/conversation-messaging.ts +17 -0
  215. package/src/daemon/conversation-process.ts +22 -141
  216. package/src/daemon/conversation-queue-manager.ts +8 -0
  217. package/src/daemon/conversation-registry.ts +159 -0
  218. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  219. package/src/daemon/conversation-store.ts +9 -90
  220. package/src/daemon/conversation-surfaces.ts +165 -11
  221. package/src/daemon/conversation-workspace.ts +17 -0
  222. package/src/daemon/conversation.ts +444 -62
  223. package/src/daemon/disk-pressure-policy.ts +0 -1
  224. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  225. package/src/daemon/handlers/config-channels.ts +11 -2
  226. package/src/daemon/handlers/conversations.ts +3 -1
  227. package/src/daemon/handlers/shared.ts +9 -1
  228. package/src/daemon/handlers/skills.ts +4 -1
  229. package/src/daemon/host-app-control-proxy.ts +72 -57
  230. package/src/daemon/host-proxy-preactivation.ts +1 -3
  231. package/src/daemon/lifecycle.ts +21 -7
  232. package/src/daemon/persist-unsendable-image.ts +62 -25
  233. package/src/daemon/process-message.ts +1 -1
  234. package/src/daemon/server.ts +2 -0
  235. package/src/daemon/tool-side-effects.ts +15 -0
  236. package/src/daemon/wake-conversation-ops.ts +269 -0
  237. package/src/embedded/plugin-api.ts +2 -2
  238. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  239. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  240. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  241. package/src/heartbeat/heartbeat-service.ts +26 -0
  242. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  243. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  244. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  245. package/src/ipc/skill-routes/memory.ts +4 -2
  246. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  247. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  248. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  249. package/src/memory/activation-session-store.ts +43 -0
  250. package/src/memory/conversation-crud.ts +29 -19
  251. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  252. package/src/memory/db-init.ts +6 -0
  253. package/src/memory/embedding-backend.ts +5 -22
  254. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  255. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  256. package/src/memory/jobs-worker.ts +25 -1
  257. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  258. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  259. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  260. package/src/memory/migrations/index.ts +3 -0
  261. package/src/memory/onboarding-events-store.ts +66 -18
  262. package/src/memory/schema/acp.ts +4 -0
  263. package/src/memory/schema/infrastructure.ts +13 -0
  264. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  265. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  266. package/src/memory/v2/consolidation-job.ts +2 -63
  267. package/src/memory/v2/types.ts +5 -0
  268. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  269. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  270. package/src/notifications/access-request-copy.ts +298 -0
  271. package/src/notifications/adapters/slack.ts +3 -3
  272. package/src/notifications/adapters/telegram.ts +9 -2
  273. package/src/notifications/copy-composer.ts +49 -267
  274. package/src/notifications/decision-engine.ts +16 -35
  275. package/src/notifications/home-feed-side-effect.ts +1 -6
  276. package/src/plugin-api/constants.ts +4 -0
  277. package/src/plugin-api/index.ts +40 -11
  278. package/src/plugin-api/types.ts +108 -0
  279. package/src/plugins/defaults/compaction/compact.ts +106 -0
  280. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  281. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  282. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  283. package/src/plugins/defaults/compaction/package.json +1 -2
  284. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  285. package/src/plugins/defaults/empty-response/package.json +0 -1
  286. package/src/plugins/defaults/history-repair/package.json +0 -1
  287. package/src/plugins/defaults/index.ts +156 -26
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  291. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  297. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  304. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  306. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  307. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  308. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  309. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  310. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  311. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  312. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  313. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  314. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  315. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  316. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  317. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  318. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  319. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  321. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  322. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  323. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  324. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  325. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  326. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  328. package/src/plugins/defaults/title-generate/package.json +0 -1
  329. package/src/plugins/defaults/tool-error/package.json +0 -1
  330. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  331. package/src/plugins/external-api.ts +12 -2
  332. package/src/plugins/pipeline.ts +6 -293
  333. package/src/plugins/registry.ts +9 -37
  334. package/src/plugins/types.ts +76 -381
  335. package/src/plugins/user-loader.ts +30 -127
  336. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  337. package/src/prompts/system-prompt.ts +61 -10
  338. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  339. package/src/providers/anthropic/client.ts +9 -10
  340. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  341. package/src/providers/inference/logit-bias.ts +55 -0
  342. package/src/providers/model-catalog.ts +57 -0
  343. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  344. package/src/providers/openai/chat-completions-provider.ts +44 -1
  345. package/src/providers/retry.ts +57 -7
  346. package/src/providers/types.ts +6 -0
  347. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  348. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  349. package/src/runtime/agent-wake.ts +108 -209
  350. package/src/runtime/assistant-event-hub.ts +1 -1
  351. package/src/runtime/channel-approvals.ts +1 -1
  352. package/src/runtime/interactive-ui.ts +1 -1
  353. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  354. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  355. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  356. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  357. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  358. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  359. package/src/runtime/routes/acp-routes.test.ts +185 -32
  360. package/src/runtime/routes/acp-routes.ts +328 -30
  361. package/src/runtime/routes/app-management-routes.ts +3 -0
  362. package/src/runtime/routes/approval-routes.ts +1 -1
  363. package/src/runtime/routes/browser-routes.ts +1 -1
  364. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  365. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  366. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  367. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  368. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  369. package/src/runtime/routes/conversation-routes.ts +30 -2
  370. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  371. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  372. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  373. package/src/runtime/routes/host-cu-routes.ts +1 -1
  374. package/src/runtime/routes/identity-routes.ts +18 -3
  375. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  376. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  377. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  378. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  379. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  380. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  381. package/src/runtime/routes/playground/helpers.ts +1 -1
  382. package/src/runtime/routes/plugins-routes.ts +337 -35
  383. package/src/runtime/routes/stt-routes.ts +45 -12
  384. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  385. package/src/runtime/routes/work-items-routes.ts +2 -4
  386. package/src/runtime/routes/workspace-routes.ts +50 -15
  387. package/src/runtime/services/conversation-serializer.ts +1 -1
  388. package/src/runtime/verification-outbound-actions.ts +147 -2
  389. package/src/runtime/verification-templates.ts +29 -3
  390. package/src/services/published-app-updater.ts +30 -8
  391. package/src/signals/cancel.ts +2 -4
  392. package/src/subagent/manager.ts +21 -5
  393. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  394. package/src/telemetry/activation-funnel.ts +167 -0
  395. package/src/telemetry/types.ts +13 -0
  396. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  397. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  398. package/src/tools/acp/context.ts +20 -0
  399. package/src/tools/acp/list-agents.test.ts +9 -19
  400. package/src/tools/acp/list-agents.ts +3 -15
  401. package/src/tools/acp/spawn.test.ts +173 -202
  402. package/src/tools/acp/spawn.ts +37 -172
  403. package/src/tools/acp/steer.test.ts +105 -8
  404. package/src/tools/acp/steer.ts +48 -17
  405. package/src/tools/apps/executors.ts +166 -50
  406. package/src/tools/browser/browser-execution.ts +12 -2
  407. package/src/tools/filesystem/write.ts +34 -0
  408. package/src/tools/network/web-fetch.ts +65 -24
  409. package/src/tools/skills/load.ts +1 -1
  410. package/src/tools/subagent/spawn.ts +2 -4
  411. package/src/tools/ui-surface/definitions.ts +32 -5
  412. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  413. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  414. package/docs/plugins.md +0 -832
  415. package/examples/plugins/echo/register.ts +0 -143
  416. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  417. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  418. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  419. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  420. package/src/__tests__/pipeline-runner.test.ts +0 -554
  421. package/src/daemon/wake-target-adapter.ts +0 -253
  422. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  423. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  424. package/src/memory/v3/__tests__/core.test.ts +0 -39
  425. package/src/memory/v3/__tests__/health.test.ts +0 -219
  426. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  427. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  428. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  429. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  430. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  431. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  432. package/src/memory/v3/__tests__/router.test.ts +0 -332
  433. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  434. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  435. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  436. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  437. package/src/memory/v3/assign.ts +0 -268
  438. package/src/memory/v3/capabilities.ts +0 -124
  439. package/src/memory/v3/core.ts +0 -26
  440. package/src/memory/v3/health.ts +0 -0
  441. package/src/memory/v3/maintain-job.ts +0 -314
  442. package/src/memory/v3/needle.ts +0 -115
  443. package/src/memory/v3/orchestrate.ts +0 -126
  444. package/src/memory/v3/page-content.ts +0 -34
  445. package/src/memory/v3/provider-blocks.ts +0 -26
  446. package/src/memory/v3/reconcile.ts +0 -523
  447. package/src/memory/v3/router.ts +0 -190
  448. package/src/memory/v3/selection-log-store.ts +0 -84
  449. package/src/memory/v3/selector.ts +0 -226
  450. package/src/memory/v3/shadow-plugin.ts +0 -411
  451. package/src/memory/v3/snapshot.ts +0 -209
  452. package/src/memory/v3/tree.ts +0 -174
  453. package/src/memory/v3/types.ts +0 -59
  454. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  455. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  456. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  457. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  458. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  459. package/src/plugins/defaults/compaction/register.ts +0 -35
  460. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  461. package/src/plugins/defaults/empty-response/register.ts +0 -23
  462. package/src/plugins/defaults/history-repair/register.ts +0 -24
  463. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  464. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  465. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  466. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  467. package/src/plugins/defaults/title-generate/register.ts +0 -35
  468. package/src/plugins/defaults/tool-error/register.ts +0 -23
  469. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  470. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  471. package/src/proactive-artifact/decision.test.ts +0 -226
  472. package/src/proactive-artifact/decision.ts +0 -165
  473. package/src/proactive-artifact/index.ts +0 -7
  474. package/src/proactive-artifact/job.test.ts +0 -962
  475. package/src/proactive-artifact/job.ts +0 -372
  476. package/src/proactive-artifact/message-copy.ts +0 -58
  477. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  478. package/src/proactive-artifact/trigger-state.ts +0 -123
  479. package/src/util/map-limit.ts +0 -27
  480. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  481. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  482. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  483. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  484. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  485. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  486. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  487. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  488. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  489. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  490. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Deterministic helpers for building guardian-facing access-request copy.
3
+ *
4
+ * Used by the fallback template in copy-composer and the decision-engine
5
+ * post-generation enforcement to ensure required directives always appear.
6
+ */
7
+
8
+ // ── Local string utilities ──────────────────────────────────────────────────
9
+ //
10
+ // Tiny helpers duplicated from copy-composer to keep this module
11
+ // dependency-free (avoiding a circular import with copy-composer, which
12
+ // imports access-request helpers for its templates).
13
+
14
+ function str(value: unknown, fallback: string): string {
15
+ if (typeof value === "string" && value.length > 0) return value;
16
+ return fallback;
17
+ }
18
+
19
+ function nonEmpty(value: string | undefined): string | undefined {
20
+ if (typeof value !== "string") return undefined;
21
+ const trimmed = value.trim();
22
+ return trimmed.length > 0 ? trimmed : undefined;
23
+ }
24
+
25
+ // ── Identity sanitization ───────────────────────────────────────────────────
26
+
27
+ const IDENTITY_FIELD_MAX_LENGTH = 120;
28
+
29
+ /**
30
+ * Sanitize an untrusted identity field for inclusion in notification copy.
31
+ *
32
+ * - Strips control characters (U+0000–U+001F, U+007F–U+009F) and newlines.
33
+ * - Clamps to IDENTITY_FIELD_MAX_LENGTH characters.
34
+ * - Wraps in quotes to neutralize instruction-like payload text.
35
+ */
36
+ export function sanitizeIdentityField(value: string): string {
37
+ const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
38
+ const clamped =
39
+ stripped.length > IDENTITY_FIELD_MAX_LENGTH
40
+ ? stripped.slice(0, IDENTITY_FIELD_MAX_LENGTH) + "…"
41
+ : stripped;
42
+ return clamped;
43
+ }
44
+
45
+ export function buildAccessRequestIdentityLine(
46
+ payload: Record<string, unknown>,
47
+ ): string {
48
+ const requester = sanitizeIdentityField(
49
+ str(payload.senderIdentifier, "Someone"),
50
+ );
51
+ const sourceChannel =
52
+ typeof payload.sourceChannel === "string"
53
+ ? payload.sourceChannel
54
+ : undefined;
55
+ const callerName = nonEmpty(
56
+ typeof payload.actorDisplayName === "string"
57
+ ? payload.actorDisplayName
58
+ : undefined,
59
+ );
60
+ const actorUsername = nonEmpty(
61
+ typeof payload.actorUsername === "string"
62
+ ? payload.actorUsername
63
+ : undefined,
64
+ );
65
+ const actorExternalId = nonEmpty(
66
+ typeof payload.actorExternalId === "string"
67
+ ? payload.actorExternalId
68
+ : undefined,
69
+ );
70
+
71
+ if (sourceChannel === "phone" && callerName) {
72
+ const safeName = sanitizeIdentityField(callerName);
73
+ const safeId = sanitizeIdentityField(
74
+ str(payload.actorExternalId, requester),
75
+ );
76
+ return `${safeName} (${safeId}) is calling and requesting access to the assistant.`;
77
+ }
78
+
79
+ // Sanitize before comparing to avoid deduplication failures when identity
80
+ // fields contain control characters that are stripped from `requester`.
81
+ const sanitizedUsername = actorUsername
82
+ ? sanitizeIdentityField(actorUsername)
83
+ : undefined;
84
+ const sanitizedExternalId = actorExternalId
85
+ ? sanitizeIdentityField(actorExternalId)
86
+ : undefined;
87
+ // When the requester is a raw Slack user ID (e.g. the fallback path in
88
+ // access-request-helper sets senderIdentifier to the raw actorExternalId),
89
+ // format it as a Slack mention so it renders as a clickable display name.
90
+ const formattedRequester =
91
+ sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(requester)
92
+ ? `<@${requester}>`
93
+ : requester;
94
+ const parts = [formattedRequester];
95
+ if (sanitizedUsername && sanitizedUsername !== requester) {
96
+ parts.push(`@${sanitizedUsername}`);
97
+ }
98
+ if (
99
+ sanitizedExternalId &&
100
+ sanitizedExternalId !== requester &&
101
+ sanitizedExternalId !== sanitizedUsername
102
+ ) {
103
+ // For Slack, use the <@U...> mention format so Slack auto-renders
104
+ // the user ID as a clickable display name.
105
+ const formattedId =
106
+ sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(sanitizedExternalId)
107
+ ? `<@${sanitizedExternalId}>`
108
+ : `[${sanitizedExternalId}]`;
109
+ parts.push(formattedId);
110
+ }
111
+ if (sourceChannel) {
112
+ parts.push(`via ${sourceChannel}`);
113
+ }
114
+
115
+ return `${parts.join(" ")} is requesting access to the assistant.`;
116
+ }
117
+
118
+ // ── Message preview ─────────────────────────────────────────────────────────
119
+
120
+ export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
121
+
122
+ /**
123
+ * Sanitize an untrusted message preview for inclusion in notification copy.
124
+ *
125
+ * Like {@link sanitizeIdentityField} but uses the higher
126
+ * MESSAGE_PREVIEW_MAX_LENGTH limit (200 chars) instead of the identity
127
+ * field limit (120 chars).
128
+ */
129
+ export function sanitizeMessagePreview(value: string): string {
130
+ const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
131
+ const clamped =
132
+ stripped.length > MESSAGE_PREVIEW_MAX_LENGTH
133
+ ? stripped.slice(0, MESSAGE_PREVIEW_MAX_LENGTH) + "…"
134
+ : stripped;
135
+ return clamped;
136
+ }
137
+
138
+ /**
139
+ * Build a quoted preview of the requester's original message for inclusion
140
+ * in guardian-facing access-request copy. Sanitizes and truncates to keep
141
+ * the notification concise.
142
+ *
143
+ * Returns `undefined` when no usable preview is available.
144
+ */
145
+ function buildAccessRequestMessagePreview(
146
+ payload: Record<string, unknown>,
147
+ ): string | undefined {
148
+ const raw =
149
+ typeof payload.messagePreview === "string"
150
+ ? payload.messagePreview
151
+ : undefined;
152
+ if (!raw) return undefined;
153
+
154
+ const sanitized = sanitizeMessagePreview(raw);
155
+ if (sanitized.length === 0) return undefined;
156
+
157
+ return `> Their message: "${sanitized}"`;
158
+ }
159
+
160
+ // ── Directives ──────────────────────────────────────────────────────────────
161
+
162
+ export function buildAccessRequestInviteDirective(): string {
163
+ return 'Reply "open invite flow" to start Trusted Contacts invite flow.';
164
+ }
165
+
166
+ /**
167
+ * Normalize text before running directive-matching regexes.
168
+ *
169
+ * - Replaces smart/curly apostrophes (\u2018, \u2019, \u201B) with ASCII `'`
170
+ * so contractions like "Don\u2019t" are matched by the `n't` lookbehind.
171
+ * - Collapses runs of whitespace into a single space so "Do not reply"
172
+ * is matched by the single-space negative lookbehind.
173
+ * - Trims leading/trailing whitespace.
174
+ */
175
+ export function normalizeForDirectiveMatching(text: string): string {
176
+ return text
177
+ .replace(/[\u2018\u2019\u201B]/g, "'")
178
+ .replace(/\s+/g, " ")
179
+ .trim();
180
+ }
181
+
182
+ /**
183
+ * Check whether a text contains the required access-request instruction elements:
184
+ * 1. Approve directive: Reply "CODE approve"
185
+ * 2. Reject directive: Reply "CODE reject"
186
+ * 3. Invite directive: Reply "open invite flow"
187
+ *
188
+ * Each directive is matched independently using negative lookbehind to reject
189
+ * matches preceded by negation words ("not", "n't", "never"). This prevents
190
+ * contradictory copy like `Do not reply "CODE reject"` from satisfying the
191
+ * check even when a positive approve directive exists nearby.
192
+ *
193
+ * The text is normalized before matching to handle smart apostrophes and
194
+ * multiple whitespace characters that would otherwise bypass negation detection.
195
+ */
196
+ export function hasAccessRequestInstructions(
197
+ text: string | undefined,
198
+ requestCode: string,
199
+ ): boolean {
200
+ if (typeof text !== "string") return false;
201
+ const normalized = normalizeForDirectiveMatching(text);
202
+ const escapedCode = requestCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
203
+ // Each directive must follow "reply" without a preceding negation word.
204
+ // Negative lookbehinds reject "do not reply", "don't reply", "never reply".
205
+ const approveRe = new RegExp(
206
+ `(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+approve"`,
207
+ "i",
208
+ );
209
+ const rejectRe = new RegExp(
210
+ `(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+reject"`,
211
+ "i",
212
+ );
213
+ const inviteRe =
214
+ /(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
215
+
216
+ return (
217
+ approveRe.test(normalized) &&
218
+ rejectRe.test(normalized) &&
219
+ inviteRe.test(normalized)
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Check whether text contains the invite-flow directive ("open invite flow")
225
+ * using the same normalized negative-lookbehind pattern as the full check.
226
+ * This is used for enforcement when requestCode is absent but the invite
227
+ * directive should still be present.
228
+ */
229
+ export function hasInviteFlowDirective(text: string | undefined): boolean {
230
+ if (typeof text !== "string") return false;
231
+ const normalized = normalizeForDirectiveMatching(text);
232
+ const inviteRe =
233
+ /(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
234
+ return inviteRe.test(normalized);
235
+ }
236
+
237
+ // ── Contract text builder ───────────────────────────────────────────────────
238
+
239
+ /**
240
+ * Build the deterministic access-request contract text from payload fields.
241
+ * This is the canonical baseline that enforcement can append when generated
242
+ * copy is missing required elements.
243
+ *
244
+ * Channel-agnostic by design: this function reads from the generic
245
+ * `contextPayload` and works identically regardless of which channel
246
+ * (Slack, Telegram, desktop, etc.) the notification is delivered to.
247
+ * When `guardianResolutionSource` is present and not `"source-channel-contact"`,
248
+ * the guardian was resolved via fallback (e.g. vellum anchor) rather than
249
+ * a verified same-channel contact — downstream copy or routing can use
250
+ * this to append verification CTAs like "Was this you?".
251
+ */
252
+ export function buildAccessRequestContractText(
253
+ payload: Record<string, unknown>,
254
+ ): string {
255
+ const requestCode = nonEmpty(
256
+ typeof payload.requestCode === "string" ? payload.requestCode : undefined,
257
+ );
258
+ const previousMemberStatus =
259
+ typeof payload.previousMemberStatus === "string"
260
+ ? payload.previousMemberStatus
261
+ : undefined;
262
+
263
+ const guardianResolutionSource =
264
+ typeof payload.guardianResolutionSource === "string"
265
+ ? payload.guardianResolutionSource
266
+ : undefined;
267
+ const sourceChannel =
268
+ typeof payload.sourceChannel === "string"
269
+ ? payload.sourceChannel
270
+ : undefined;
271
+
272
+ const lines: string[] = [];
273
+ lines.push(buildAccessRequestIdentityLine(payload));
274
+ const preview = buildAccessRequestMessagePreview(payload);
275
+ if (preview) {
276
+ lines.push(preview);
277
+ }
278
+ if (previousMemberStatus === "revoked") {
279
+ lines.push("Note: this user was previously revoked.");
280
+ }
281
+ if (requestCode) {
282
+ const code = requestCode.toUpperCase();
283
+ lines.push(
284
+ `Reply "${code} approve" to grant access or "${code} reject" to deny.`,
285
+ );
286
+ }
287
+ lines.push(buildAccessRequestInviteDirective());
288
+ if (
289
+ (guardianResolutionSource === "vellum-anchor" ||
290
+ guardianResolutionSource === "none") &&
291
+ sourceChannel
292
+ ) {
293
+ lines.push(
294
+ `Note: You haven't verified your identity on ${sourceChannel} yet. If this was you trying to message your assistant, say "help me verify as guardian on ${sourceChannel}" to set up direct access.`,
295
+ );
296
+ }
297
+ return lines.join("\n");
298
+ }
@@ -5,13 +5,13 @@
5
5
 
6
6
  import { sendSlackReply } from "../../messaging/providers/slack/send.js";
7
7
  import { getLogger } from "../../util/logger.js";
8
- import { isConversationSeedSane } from "../conversation-seed-composer.js";
9
8
  import {
10
9
  buildAccessRequestIdentityLine,
11
10
  buildAccessRequestInviteDirective,
12
- nonEmpty,
13
11
  sanitizeIdentityField,
14
- } from "../copy-composer.js";
12
+ } from "../access-request-copy.js";
13
+ import { isConversationSeedSane } from "../conversation-seed-composer.js";
14
+ import { nonEmpty } from "../copy-composer.js";
15
15
  import type {
16
16
  ChannelAdapter,
17
17
  ChannelDeliveryPayload,
@@ -10,9 +10,11 @@
10
10
 
11
11
  import { sendTelegramReply } from "../../messaging/providers/telegram-bot/send.js";
12
12
  import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
13
+ import { ConfigError } from "../../util/errors.js";
13
14
  import { getLogger } from "../../util/logger.js";
15
+ import { buildAccessRequestContractText } from "../access-request-copy.js";
14
16
  import { isConversationSeedSane } from "../conversation-seed-composer.js";
15
- import { buildAccessRequestContractText, nonEmpty } from "../copy-composer.js";
17
+ import { nonEmpty } from "../copy-composer.js";
16
18
  import type {
17
19
  ChannelAdapter,
18
20
  ChannelDeliveryPayload,
@@ -136,7 +138,12 @@ export class TelegramAdapter implements ChannelAdapter {
136
138
  return { success: true };
137
139
  } catch (err) {
138
140
  const message = err instanceof Error ? err.message : String(err);
139
- log.error(
141
+ // A missing bot token means the operator simply hasn't configured
142
+ // Telegram; it is not a code fault, so log it at warn to keep it out
143
+ // of Sentry. Genuine and transient failures (e.g. an unreachable
144
+ // credential store) stay at error so they remain visible.
145
+ const logFn = err instanceof ConfigError ? log.warn : log.error;
146
+ logFn(
140
147
  { err, sourceEventName: payload.sourceEventName, chatId },
141
148
  "Failed to deliver Telegram notification",
142
149
  );
@@ -9,6 +9,10 @@
9
9
  * values from the context payload.
10
10
  */
11
11
 
12
+ import {
13
+ buildAccessRequestContractText,
14
+ sanitizeIdentityField,
15
+ } from "./access-request-copy.js";
12
16
  import {
13
17
  buildGuardianRequestCodeInstruction,
14
18
  resolveGuardianQuestionInstructionMode,
@@ -32,285 +36,39 @@ export function nonEmpty(value: string | undefined): string | undefined {
32
36
  return trimmed.length > 0 ? trimmed : undefined;
33
37
  }
34
38
 
35
- // ── Access-request copy contract ─────────────────────────────────────────────
36
- //
37
- // Deterministic helpers for building guardian-facing access-request copy.
38
- // These are used both by the fallback template and the decision-engine
39
- // post-generation enforcement to ensure required directives always appear.
40
-
41
- const IDENTITY_FIELD_MAX_LENGTH = 120;
42
-
43
39
  /**
44
- * Sanitize an untrusted identity field for inclusion in notification copy.
45
- *
46
- * - Strips control characters (U+0000–U+001F, U+007F–U+009F) and newlines.
47
- * - Clamps to IDENTITY_FIELD_MAX_LENGTH characters.
48
- * - Wraps in quotes to neutralize instruction-like payload text.
40
+ * Safely read a string property from an unknown-typed payload object.
41
+ * Returns `undefined` when the payload is falsy, not an object, or the
42
+ * key does not hold a string value.
49
43
  */
50
- export function sanitizeIdentityField(value: string): string {
51
- const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
52
- const clamped =
53
- stripped.length > IDENTITY_FIELD_MAX_LENGTH
54
- ? stripped.slice(0, IDENTITY_FIELD_MAX_LENGTH) + "…"
55
- : stripped;
56
- return clamped;
57
- }
58
-
59
- export function buildAccessRequestIdentityLine(
60
- payload: Record<string, unknown>,
61
- ): string {
62
- const requester = sanitizeIdentityField(
63
- str(payload.senderIdentifier, "Someone"),
64
- );
65
- const sourceChannel =
66
- typeof payload.sourceChannel === "string"
67
- ? payload.sourceChannel
68
- : undefined;
69
- const callerName = nonEmpty(
70
- typeof payload.actorDisplayName === "string"
71
- ? payload.actorDisplayName
72
- : undefined,
73
- );
74
- const actorUsername = nonEmpty(
75
- typeof payload.actorUsername === "string"
76
- ? payload.actorUsername
77
- : undefined,
78
- );
79
- const actorExternalId = nonEmpty(
80
- typeof payload.actorExternalId === "string"
81
- ? payload.actorExternalId
82
- : undefined,
83
- );
84
-
85
- if (sourceChannel === "phone" && callerName) {
86
- const safeName = sanitizeIdentityField(callerName);
87
- const safeId = sanitizeIdentityField(
88
- str(payload.actorExternalId, requester),
89
- );
90
- return `${safeName} (${safeId}) is calling and requesting access to the assistant.`;
91
- }
92
-
93
- // For non-voice, include extra context when available.
94
- // Sanitize before comparing to avoid deduplication failures when identity
95
- // fields contain control characters that are stripped from `requester`.
96
- const sanitizedUsername = actorUsername
97
- ? sanitizeIdentityField(actorUsername)
98
- : undefined;
99
- const sanitizedExternalId = actorExternalId
100
- ? sanitizeIdentityField(actorExternalId)
101
- : undefined;
102
- // When the requester is a raw Slack user ID (e.g. the fallback path in
103
- // access-request-helper sets senderIdentifier to the raw actorExternalId),
104
- // format it as a Slack mention so it renders as a clickable display name.
105
- const formattedRequester =
106
- sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(requester)
107
- ? `<@${requester}>`
108
- : requester;
109
- const parts = [formattedRequester];
110
- if (sanitizedUsername && sanitizedUsername !== requester) {
111
- parts.push(`@${sanitizedUsername}`);
112
- }
113
- if (
114
- sanitizedExternalId &&
115
- sanitizedExternalId !== requester &&
116
- sanitizedExternalId !== sanitizedUsername
117
- ) {
118
- // For Slack, use the <@U...> mention format so Slack auto-renders
119
- // the user ID as a clickable display name.
120
- const formattedId =
121
- sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(sanitizedExternalId)
122
- ? `<@${sanitizedExternalId}>`
123
- : `[${sanitizedExternalId}]`;
124
- parts.push(formattedId);
125
- }
126
- if (sourceChannel) {
127
- parts.push(`via ${sourceChannel}`);
128
- }
129
-
130
- return `${parts.join(" ")} is requesting access to the assistant.`;
131
- }
132
-
133
- export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
134
-
135
- /**
136
- * Sanitize an untrusted message preview for inclusion in notification copy.
137
- *
138
- * Like {@link sanitizeIdentityField} but uses the higher
139
- * MESSAGE_PREVIEW_MAX_LENGTH limit (200 chars) instead of the identity
140
- * field limit (120 chars).
141
- */
142
- export function sanitizeMessagePreview(value: string): string {
143
- const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
144
- const clamped =
145
- stripped.length > MESSAGE_PREVIEW_MAX_LENGTH
146
- ? stripped.slice(0, MESSAGE_PREVIEW_MAX_LENGTH) + "…"
147
- : stripped;
148
- return clamped;
149
- }
150
-
151
- /**
152
- * Build a quoted preview of the requester's original message for inclusion
153
- * in guardian-facing access-request copy. Sanitizes and truncates to keep
154
- * the notification concise.
155
- *
156
- * Returns `undefined` when no usable preview is available.
157
- */
158
- function buildAccessRequestMessagePreview(
159
- payload: Record<string, unknown>,
44
+ export function readPayloadString(
45
+ payload: unknown,
46
+ key: string,
160
47
  ): string | undefined {
161
- const raw =
162
- typeof payload.messagePreview === "string"
163
- ? payload.messagePreview
164
- : undefined;
165
- if (!raw) return undefined;
166
-
167
- const sanitized = sanitizeMessagePreview(raw);
168
- if (sanitized.length === 0) return undefined;
169
-
170
- return `> Their message: "${sanitized}"`;
171
- }
172
-
173
- export function buildAccessRequestInviteDirective(): string {
174
- return 'Reply "open invite flow" to start Trusted Contacts invite flow.';
48
+ if (!payload || typeof payload !== "object") return undefined;
49
+ const value = (payload as Record<string, unknown>)[key];
50
+ return typeof value === "string" ? value : undefined;
175
51
  }
176
52
 
177
53
  /**
178
- * Normalize text before running directive-matching regexes.
179
- *
180
- * - Replaces smart/curly apostrophes (\u2018, \u2019, \u201B) with ASCII `'`
181
- * so contractions like "Don\u2019t" are matched by the `n't` lookbehind.
182
- * - Collapses runs of whitespace into a single space so "Do not reply"
183
- * is matched by the single-space negative lookbehind.
184
- * - Trims leading/trailing whitespace.
54
+ * Derive a short notification title from a message body. Trims to the
55
+ * first sentence terminator when present, then caps the result at
56
+ * 60 characters with an ellipsis.
185
57
  */
186
- export function normalizeForDirectiveMatching(text: string): string {
187
- return text
188
- .replace(/[\u2018\u2019\u201B]/g, "'")
189
- .replace(/\s+/g, " ")
190
- .trim();
191
- }
192
-
193
- /**
194
- * Check whether a text contains the required access-request instruction elements:
195
- * 1. Approve directive: Reply "CODE approve"
196
- * 2. Reject directive: Reply "CODE reject"
197
- * 3. Invite directive: Reply "open invite flow"
198
- *
199
- * Each directive is matched independently using negative lookbehind to reject
200
- * matches preceded by negation words ("not", "n't", "never"). This prevents
201
- * contradictory copy like `Do not reply "CODE reject"` from satisfying the
202
- * check even when a positive approve directive exists nearby.
203
- *
204
- * The text is normalized before matching to handle smart apostrophes and
205
- * multiple whitespace characters that would otherwise bypass negation detection.
206
- */
207
- export function hasAccessRequestInstructions(
208
- text: string | undefined,
209
- requestCode: string,
210
- ): boolean {
211
- if (typeof text !== "string") return false;
212
- const normalized = normalizeForDirectiveMatching(text);
213
- const escapedCode = requestCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
214
- // Each directive must follow "reply" without a preceding negation word.
215
- // Negative lookbehinds reject "do not reply", "don't reply", "never reply".
216
- const approveRe = new RegExp(
217
- `(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+approve"`,
218
- "i",
219
- );
220
- const rejectRe = new RegExp(
221
- `(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+reject"`,
222
- "i",
223
- );
224
- const inviteRe =
225
- /(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
226
-
227
- return (
228
- approveRe.test(normalized) &&
229
- rejectRe.test(normalized) &&
230
- inviteRe.test(normalized)
231
- );
232
- }
233
-
234
- /**
235
- * Check whether text contains the invite-flow directive ("open invite flow")
236
- * using the same normalized negative-lookbehind pattern as the full check.
237
- * This is used for enforcement when requestCode is absent but the invite
238
- * directive should still be present.
239
- */
240
- export function hasInviteFlowDirective(text: string | undefined): boolean {
241
- if (typeof text !== "string") return false;
242
- const normalized = normalizeForDirectiveMatching(text);
243
- const inviteRe =
244
- /(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
245
- return inviteRe.test(normalized);
246
- }
247
-
248
- /**
249
- * Build the deterministic access-request contract text from payload fields.
250
- * This is the canonical baseline that enforcement can append when generated
251
- * copy is missing required elements.
252
- *
253
- * Channel-agnostic by design: this function reads from the generic
254
- * `contextPayload` and works identically regardless of which channel
255
- * (Slack, Telegram, desktop, etc.) the notification is delivered to.
256
- * When `guardianResolutionSource` is present and not `"source-channel-contact"`,
257
- * the guardian was resolved via fallback (e.g. vellum anchor) rather than
258
- * a verified same-channel contact — downstream copy or routing can use
259
- * this to append verification CTAs like "Was this you?".
260
- */
261
- export function buildAccessRequestContractText(
262
- payload: Record<string, unknown>,
263
- ): string {
264
- const requestCode = nonEmpty(
265
- typeof payload.requestCode === "string" ? payload.requestCode : undefined,
266
- );
267
- const previousMemberStatus =
268
- typeof payload.previousMemberStatus === "string"
269
- ? payload.previousMemberStatus
270
- : undefined;
271
-
272
- const guardianResolutionSource =
273
- typeof payload.guardianResolutionSource === "string"
274
- ? payload.guardianResolutionSource
275
- : undefined;
276
- const sourceChannel =
277
- typeof payload.sourceChannel === "string"
278
- ? payload.sourceChannel
279
- : undefined;
280
-
281
- const lines: string[] = [];
282
- lines.push(buildAccessRequestIdentityLine(payload));
283
- const preview = buildAccessRequestMessagePreview(payload);
284
- if (preview) {
285
- lines.push(preview);
286
- }
287
- if (previousMemberStatus === "revoked") {
288
- lines.push("Note: this user was previously revoked.");
289
- }
290
- if (requestCode) {
291
- const code = requestCode.toUpperCase();
292
- lines.push(
293
- `Reply "${code} approve" to grant access or "${code} reject" to deny.`,
294
- );
295
- }
296
- lines.push(buildAccessRequestInviteDirective());
297
- if (
298
- (guardianResolutionSource === "vellum-anchor" ||
299
- guardianResolutionSource === "none") &&
300
- sourceChannel
301
- ) {
302
- lines.push(
303
- `Note: You haven't verified your identity on ${sourceChannel} yet. If this was you trying to message your assistant, say "help me verify as guardian on ${sourceChannel}" to set up direct access.`,
304
- );
305
- }
306
- return lines.join("\n");
58
+ export function deriveTitle(body: string): string {
59
+ const firstSentenceEnd = body.search(/[.!?](\s|$)/);
60
+ const candidate =
61
+ firstSentenceEnd > 0 ? body.slice(0, firstSentenceEnd + 1) : body;
62
+ return candidate.length > 60
63
+ ? candidate.slice(0, 60).trim() + "\u2026"
64
+ : candidate.trim();
307
65
  }
308
66
 
309
67
  // Templates keyed by dot-separated sourceEventName strings matching producers.
310
68
  const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
311
69
  "schedule.notify": (payload) => ({
312
- title: "Reminder",
313
- body: str(payload.message, str(payload.label, "A reminder has fired")),
70
+ title: str(payload.label, "Reminder"),
71
+ body: str(payload.message, "A reminder has fired"),
314
72
  }),
315
73
 
316
74
  "guardian.question": (payload) => {
@@ -553,6 +311,30 @@ export function composeFallbackCopy(
553
311
  signal: NotificationSignal,
554
312
  channels: NotificationChannel[],
555
313
  ): Partial<Record<NotificationChannel, RenderedChannelCopy>> {
314
+ // Honor user-supplied content when present. The assistant_tool
315
+ // pass-through handles the happy path (sourceChannel === "assistant_tool");
316
+ // this catches the same payload fields when the LLM fallback fires for
317
+ // any source channel.
318
+ const msg = nonEmpty(
319
+ readPayloadString(signal.contextPayload, "requestedMessage"),
320
+ );
321
+ if (msg) {
322
+ const title =
323
+ nonEmpty(readPayloadString(signal.contextPayload, "requestedTitle")) ??
324
+ deriveTitle(msg);
325
+ const baseCopy: RenderedChannelCopy = {
326
+ title,
327
+ body: msg,
328
+ conversationSeedMessage: msg,
329
+ };
330
+ const result: Partial<Record<NotificationChannel, RenderedChannelCopy>> =
331
+ {};
332
+ for (const ch of channels) {
333
+ result[ch] = applyChannelDefaults(ch, baseCopy);
334
+ }
335
+ return result;
336
+ }
337
+
556
338
  const template =
557
339
  TEMPLATES[signal.sourceEventName as NotificationSourceEventName];
558
340