@vellumai/assistant 0.6.4 → 0.6.6

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 (1008) hide show
  1. package/.prettierignore +5 -0
  2. package/AGENTS.md +9 -1
  3. package/ARCHITECTURE.md +43 -49
  4. package/Dockerfile +17 -3
  5. package/README.md +3 -4
  6. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  7. package/bun.lock +8 -3
  8. package/docs/architecture/integrations.md +33 -59
  9. package/docs/architecture/memory.md +25 -30
  10. package/docs/architecture/security.md +19 -18
  11. package/docs/browser-use-architecture-phase2.md +63 -20
  12. package/docs/error-handling.md +111 -0
  13. package/docs/plugins.md +761 -0
  14. package/docs/skills.md +10 -10
  15. package/docs/stt-provider-onboarding.md +2 -1
  16. package/examples/plugins/echo/README.md +132 -0
  17. package/examples/plugins/echo/package.json +17 -0
  18. package/examples/plugins/echo/register.ts +187 -0
  19. package/knip.json +9 -2
  20. package/node_modules/@vellumai/ces-contracts/package.json +2 -1
  21. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  22. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  23. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  24. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  25. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  26. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  27. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  28. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  29. package/openapi.yaml +334 -78
  30. package/package.json +6 -3
  31. package/scripts/generate-openapi.ts +50 -11
  32. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  33. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  34. package/src/__tests__/agent-loop.test.ts +112 -1
  35. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  36. package/src/__tests__/anthropic-provider.test.ts +171 -2
  37. package/src/__tests__/app-compiler.test.ts +57 -0
  38. package/src/__tests__/approval-cascade.test.ts +36 -10
  39. package/src/__tests__/approval-routes-http.test.ts +134 -10
  40. package/src/__tests__/assistant-attachments.test.ts +44 -0
  41. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  42. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  43. package/src/__tests__/avatar-generator.test.ts +4 -2
  44. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  45. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  46. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  47. package/src/__tests__/browser-skill-endstate.test.ts +51 -182
  48. package/src/__tests__/btw-routes.test.ts +47 -1
  49. package/src/__tests__/bundled-asset.test.ts +6 -6
  50. package/src/__tests__/call-controller.test.ts +1 -2
  51. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  52. package/src/__tests__/catalog-cache.test.ts +96 -4
  53. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  54. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  55. package/src/__tests__/checker.test.ts +870 -655
  56. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  57. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  58. package/src/__tests__/compaction-events.test.ts +501 -0
  59. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  60. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  61. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  62. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  63. package/src/__tests__/config-analysis.test.ts +11 -28
  64. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  65. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  66. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  67. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  68. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  69. package/src/__tests__/config-schema.test.ts +440 -114
  70. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  71. package/src/__tests__/config-watcher.test.ts +2 -2
  72. package/src/__tests__/contact-store-user-file.test.ts +72 -73
  73. package/src/__tests__/contacts-tools.test.ts +26 -0
  74. package/src/__tests__/contacts-write.test.ts +4 -4
  75. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  76. package/src/__tests__/context-token-estimator.test.ts +191 -1
  77. package/src/__tests__/context-window-manager.test.ts +883 -4
  78. package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
  79. package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
  80. package/src/__tests__/conversation-agent-loop.test.ts +435 -216
  81. package/src/__tests__/conversation-attachments.test.ts +1 -1
  82. package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
  83. package/src/__tests__/conversation-error.test.ts +37 -6
  84. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  85. package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
  86. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  87. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  88. package/src/__tests__/conversation-pairing.test.ts +174 -10
  89. package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
  90. package/src/__tests__/conversation-process-callsite.test.ts +309 -0
  91. package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
  92. package/src/__tests__/conversation-queue.test.ts +68 -38
  93. package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
  94. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  95. package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
  96. package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
  97. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  98. package/src/__tests__/conversation-skill-tools.test.ts +12 -146
  99. package/src/__tests__/conversation-slash-queue.test.ts +39 -19
  100. package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
  101. package/src/__tests__/conversation-speed-override.test.ts +36 -12
  102. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  103. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  104. package/src/__tests__/conversation-title-service.test.ts +118 -2
  105. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  106. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
  107. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  108. package/src/__tests__/conversation-usage.test.ts +4 -2
  109. package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
  110. package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
  111. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
  112. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  113. package/src/__tests__/credential-health-service.test.ts +78 -9
  114. package/src/__tests__/credential-security-invariants.test.ts +5 -2
  115. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  116. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  117. package/src/__tests__/credential-vault-unit.test.ts +135 -19
  118. package/src/__tests__/credentials-cli.test.ts +1 -9
  119. package/src/__tests__/cross-provider-web-search.test.ts +84 -0
  120. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  121. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  122. package/src/__tests__/delete-propagation.test.ts +437 -0
  123. package/src/__tests__/dm-backfill.test.ts +417 -0
  124. package/src/__tests__/dm-persistence.test.ts +227 -0
  125. package/src/__tests__/edit-propagation.test.ts +280 -0
  126. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  127. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  128. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  129. package/src/__tests__/estimator-calibration.test.ts +213 -0
  130. package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
  131. package/src/__tests__/file-write-tool.test.ts +151 -1
  132. package/src/__tests__/filing-service.test.ts +255 -0
  133. package/src/__tests__/first-greeting.test.ts +247 -5
  134. package/src/__tests__/gemini-provider.test.ts +0 -3
  135. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  136. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  137. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  138. package/src/__tests__/heartbeat-service.test.ts +96 -15
  139. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  140. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  141. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  142. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  143. package/src/__tests__/host-shell-tool.test.ts +124 -18
  144. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  145. package/src/__tests__/image-credentials.test.ts +137 -0
  146. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  147. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  148. package/src/__tests__/injector-chain.test.ts +526 -0
  149. package/src/__tests__/intent-routing.test.ts +1 -66
  150. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  151. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  152. package/src/__tests__/llm-context-normalization.test.ts +121 -0
  153. package/src/__tests__/llm-resolver.test.ts +214 -0
  154. package/src/__tests__/llm-schema.test.ts +223 -0
  155. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  156. package/src/__tests__/media-generate-image.test.ts +119 -13
  157. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  158. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  159. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  160. package/src/__tests__/migration-import-from-url.test.ts +621 -0
  161. package/src/__tests__/model-intents.test.ts +11 -83
  162. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  163. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  164. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  165. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  166. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  167. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  168. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  169. package/src/__tests__/oauth-cli.test.ts +14 -12
  170. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  171. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  172. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  173. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  174. package/src/__tests__/oauth-store.test.ts +46 -78
  175. package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
  176. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  177. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  178. package/src/__tests__/openai-image-service.test.ts +368 -0
  179. package/src/__tests__/openai-provider.test.ts +7 -0
  180. package/src/__tests__/openai-responses-provider.test.ts +396 -0
  181. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  182. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  183. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  184. package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
  185. package/src/__tests__/permission-mode.test.ts +16 -0
  186. package/src/__tests__/permission-types.test.ts +0 -1
  187. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  188. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  189. package/src/__tests__/persona-resolver.test.ts +13 -13
  190. package/src/__tests__/pipeline-runner.test.ts +565 -0
  191. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  192. package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
  193. package/src/__tests__/platform.test.ts +5 -2
  194. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  195. package/src/__tests__/plugin-registry.test.ts +273 -0
  196. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  197. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  198. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  199. package/src/__tests__/plugin-types.test.ts +320 -0
  200. package/src/__tests__/pricing.test.ts +93 -14
  201. package/src/__tests__/profiler-routes.test.ts +1 -1
  202. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  203. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  204. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  205. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  206. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  207. package/src/__tests__/proxy-approval-callback.test.ts +69 -9
  208. package/src/__tests__/reaction-persistence.test.ts +561 -0
  209. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  210. package/src/__tests__/registry.test.ts +0 -2
  211. package/src/__tests__/relay-server.test.ts +1 -1
  212. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  213. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  214. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  215. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  216. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  217. package/src/__tests__/schedule-routes.test.ts +131 -1
  218. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  219. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  220. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  221. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  222. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  223. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  224. package/src/__tests__/secret-scanner-executor.test.ts +1 -1
  225. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  226. package/src/__tests__/server-history-render.test.ts +31 -0
  227. package/src/__tests__/shell-identity.test.ts +0 -134
  228. package/src/__tests__/shell-parser-property.test.ts +13 -13
  229. package/src/__tests__/skill-cache-store.test.ts +182 -0
  230. package/src/__tests__/skills.test.ts +19 -33
  231. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  232. package/src/__tests__/slack-skill.test.ts +3 -8
  233. package/src/__tests__/starter-bundle.test.ts +35 -0
  234. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  235. package/src/__tests__/suggestion-routes.test.ts +259 -3
  236. package/src/__tests__/system-prompt.test.ts +22 -35
  237. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  238. package/src/__tests__/task-runner.test.ts +3 -1
  239. package/src/__tests__/task-scheduler.test.ts +3 -15
  240. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  241. package/src/__tests__/terminal-tools.test.ts +8 -0
  242. package/src/__tests__/test-preload.ts +11 -0
  243. package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
  244. package/src/__tests__/thread-backfill.test.ts +941 -0
  245. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  246. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  247. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  248. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  249. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
  250. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  251. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  252. package/src/__tests__/tool-executor.test.ts +201 -94
  253. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  254. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  255. package/src/__tests__/trust-store.test.ts +442 -109
  256. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  257. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  258. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  259. package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
  260. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  261. package/src/__tests__/volume-security-guard.test.ts +3 -2
  262. package/src/__tests__/web-search-history.test.ts +337 -0
  263. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  264. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  265. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  266. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  267. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  268. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  269. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  270. package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
  271. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  272. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  273. package/src/__tests__/workspace-policy.test.ts +22 -16
  274. package/src/acp/client-handler.ts +1 -2
  275. package/src/agent/loop.ts +545 -115
  276. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  277. package/src/approvals/guardian-request-resolvers.ts +80 -0
  278. package/src/avatar/resvg-lazy.test.ts +136 -0
  279. package/src/avatar/resvg-lazy.ts +82 -9
  280. package/src/avatar/traits-png-sync.ts +21 -1
  281. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  282. package/src/backup/backup-worker.ts +3 -15
  283. package/src/browser/__tests__/operations.test.ts +163 -0
  284. package/src/browser/identifiers.ts +51 -0
  285. package/src/browser/operations.ts +660 -0
  286. package/src/browser/types.ts +81 -0
  287. package/src/bundler/app-compiler.ts +84 -1
  288. package/src/calls/call-state.ts +2 -2
  289. package/src/calls/guardian-question-copy.ts +2 -2
  290. package/src/calls/telephony-stt-routing.ts +1 -1
  291. package/src/calls/voice-session-bridge.ts +1 -0
  292. package/src/channels/__tests__/types.test.ts +3 -3
  293. package/src/channels/types.ts +6 -4
  294. package/src/cli/AGENTS.md +1 -1
  295. package/src/cli/__tests__/notifications.test.ts +87 -211
  296. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  297. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  298. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  299. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  300. package/src/cli/commands/__tests__/email-list.test.ts +6 -0
  301. package/src/cli/commands/__tests__/email-send.test.ts +93 -1
  302. package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
  303. package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
  304. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  305. package/src/cli/commands/__tests__/task.test.ts +913 -0
  306. package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
  307. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  308. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  309. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  310. package/src/cli/commands/attachment.ts +182 -0
  311. package/src/cli/commands/backup.ts +2 -2
  312. package/src/cli/commands/browser.ts +350 -0
  313. package/src/cli/commands/cache.ts +341 -0
  314. package/src/cli/commands/clients.ts +138 -0
  315. package/src/cli/commands/completions.ts +2 -12
  316. package/src/cli/commands/config.ts +6 -6
  317. package/src/cli/commands/conversations-import.ts +347 -0
  318. package/src/cli/commands/conversations.ts +69 -8
  319. package/src/cli/commands/email.ts +234 -194
  320. package/src/cli/commands/image-generation.ts +299 -0
  321. package/src/cli/commands/inference.ts +200 -0
  322. package/src/cli/commands/memory.ts +127 -17
  323. package/src/cli/commands/notifications.ts +68 -103
  324. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  325. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  326. package/src/cli/commands/oauth/connect.ts +2 -2
  327. package/src/cli/commands/oauth/providers.ts +176 -8
  328. package/src/cli/commands/oauth/status.ts +46 -36
  329. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  330. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
  331. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
  332. package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
  333. package/src/cli/commands/skills.ts +3 -4
  334. package/src/cli/commands/stt.ts +339 -0
  335. package/src/cli/commands/task.ts +795 -0
  336. package/src/cli/commands/trust.ts +50 -19
  337. package/src/cli/commands/tts.ts +273 -0
  338. package/src/cli/commands/ui.ts +670 -0
  339. package/src/cli/commands/watchers.ts +509 -0
  340. package/src/cli/lib/daemon-credential-client.ts +0 -19
  341. package/src/cli/program.ts +39 -24
  342. package/src/cli.ts +0 -37
  343. package/src/config/__tests__/backup-schema.test.ts +7 -2
  344. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  345. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  346. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  347. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  348. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  349. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  350. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  351. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  352. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  353. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  354. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  355. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  356. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
  357. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  358. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  359. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
  360. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
  361. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  362. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  363. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  364. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  365. package/src/config/bundled-tool-registry.ts +0 -190
  366. package/src/config/env.ts +7 -2
  367. package/src/config/feature-flag-registry.json +42 -10
  368. package/src/config/llm-resolver.ts +128 -0
  369. package/src/config/loader.ts +194 -10
  370. package/src/config/raw-config-utils.ts +30 -2
  371. package/src/config/sanitize-for-transfer.ts +35 -0
  372. package/src/config/schema.ts +49 -41
  373. package/src/config/schemas/analysis.ts +3 -22
  374. package/src/config/schemas/backup.ts +1 -1
  375. package/src/config/schemas/calls.ts +0 -4
  376. package/src/config/schemas/conversations.ts +16 -0
  377. package/src/config/schemas/filing.ts +2 -7
  378. package/src/config/schemas/heartbeat.ts +0 -5
  379. package/src/config/schemas/inference.ts +3 -23
  380. package/src/config/schemas/llm.ts +317 -0
  381. package/src/config/schemas/memory-processing.ts +1 -9
  382. package/src/config/schemas/notifications.ts +4 -11
  383. package/src/config/schemas/platform.ts +3 -9
  384. package/src/config/schemas/security.ts +33 -0
  385. package/src/config/schemas/services.ts +9 -4
  386. package/src/config/schemas/stt.ts +1 -0
  387. package/src/config/schemas/tts.ts +64 -0
  388. package/src/config/schemas/updates.ts +1 -1
  389. package/src/config/schemas/workspace-git.ts +3 -40
  390. package/src/config/skill-state.ts +6 -2
  391. package/src/config/skills.ts +96 -7
  392. package/src/context/__tests__/compact-prompt.test.ts +63 -0
  393. package/src/context/__tests__/microcompact.test.ts +805 -0
  394. package/src/context/estimator-calibration.ts +136 -0
  395. package/src/context/microcompact.ts +443 -0
  396. package/src/context/prompts/compact.md +26 -0
  397. package/src/context/token-estimator.ts +61 -3
  398. package/src/context/tool-result-truncation.ts +3 -63
  399. package/src/context/window-manager.ts +417 -39
  400. package/src/credential-execution/approval-bridge.ts +0 -1
  401. package/src/credential-execution/executable-discovery.ts +19 -8
  402. package/src/credential-execution/process-manager.test.ts +109 -0
  403. package/src/credential-execution/process-manager.ts +65 -2
  404. package/src/credential-health/credential-health-service.ts +19 -6
  405. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  406. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  407. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  408. package/src/daemon/approval-generators.ts +29 -4
  409. package/src/daemon/assistant-attachments.ts +24 -13
  410. package/src/daemon/classifier.ts +2 -2
  411. package/src/daemon/config-watcher.ts +0 -3
  412. package/src/daemon/context-overflow-policy.ts +4 -13
  413. package/src/daemon/context-overflow-reducer.ts +4 -1
  414. package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
  415. package/src/daemon/conversation-agent-loop.ts +1282 -599
  416. package/src/daemon/conversation-attachments.ts +2 -6
  417. package/src/daemon/conversation-error.ts +36 -1
  418. package/src/daemon/conversation-history.ts +10 -19
  419. package/src/daemon/conversation-lifecycle.ts +59 -17
  420. package/src/daemon/conversation-messaging.ts +73 -4
  421. package/src/daemon/conversation-notifiers.ts +2 -110
  422. package/src/daemon/conversation-process.ts +24 -11
  423. package/src/daemon/conversation-queue-manager.ts +3 -0
  424. package/src/daemon/conversation-runtime-assembly.ts +1063 -211
  425. package/src/daemon/conversation-slash.ts +2 -2
  426. package/src/daemon/conversation-surfaces.ts +389 -1
  427. package/src/daemon/conversation-tool-setup.ts +51 -9
  428. package/src/daemon/conversation-usage.ts +1 -1
  429. package/src/daemon/conversation.ts +197 -64
  430. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  431. package/src/daemon/external-skills-bootstrap.ts +41 -0
  432. package/src/daemon/first-greeting.ts +191 -14
  433. package/src/daemon/guardian-action-generators.ts +34 -14
  434. package/src/daemon/handlers/config-model.test.ts +86 -0
  435. package/src/daemon/handlers/config-model.ts +65 -12
  436. package/src/daemon/handlers/conversations.ts +9 -2
  437. package/src/daemon/handlers/shared.ts +39 -11
  438. package/src/daemon/handlers/skills.ts +7 -3
  439. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  440. package/src/daemon/lifecycle.ts +109 -82
  441. package/src/daemon/message-types/computer-use.ts +2 -34
  442. package/src/daemon/message-types/conversations.ts +63 -0
  443. package/src/daemon/message-types/messages.ts +21 -1
  444. package/src/daemon/message-types/trust.ts +0 -2
  445. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  446. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  447. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  448. package/src/daemon/pkb-context-tracker.ts +125 -0
  449. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  450. package/src/daemon/pkb-reminder-builder.ts +31 -0
  451. package/src/daemon/providers-setup.ts +6 -0
  452. package/src/daemon/server.ts +122 -12
  453. package/src/daemon/shutdown-handlers.ts +2 -12
  454. package/src/daemon/tool-side-effects.ts +14 -65
  455. package/src/daemon/web-search-history.ts +126 -0
  456. package/src/events/domain-events.ts +0 -1
  457. package/src/filing/filing-service.ts +9 -10
  458. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  459. package/src/heartbeat/heartbeat-service.ts +99 -28
  460. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  461. package/src/home/__tests__/feed-scheduler.test.ts +39 -11
  462. package/src/home/__tests__/rollup-producer.test.ts +44 -0
  463. package/src/home/assistant-feed-authoring.ts +4 -0
  464. package/src/home/emit-feed-event.ts +11 -0
  465. package/src/home/feed-scheduler.ts +20 -4
  466. package/src/home/feed-types.ts +97 -4
  467. package/src/home/relationship-state-writer.ts +2 -2
  468. package/src/home/rewrite-command-preview.ts +66 -0
  469. package/src/home/rollup-producer.ts +34 -5
  470. package/src/home/suggested-prompts.ts +101 -0
  471. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  472. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  473. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  474. package/src/ipc/__tests__/socket-path.test.ts +34 -0
  475. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  476. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  477. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  478. package/src/ipc/cli-client.ts +2 -1
  479. package/src/ipc/cli-server.ts +26 -8
  480. package/src/ipc/gateway-client.ts +6 -3
  481. package/src/ipc/routes/attachment.ts +114 -0
  482. package/src/ipc/routes/browser-context.ts +63 -0
  483. package/src/ipc/routes/browser.ts +97 -0
  484. package/src/ipc/routes/cache.ts +96 -0
  485. package/src/ipc/routes/get-contact.ts +16 -0
  486. package/src/ipc/routes/index.ts +31 -1
  487. package/src/ipc/routes/list-clients.ts +31 -0
  488. package/src/ipc/routes/merge-contacts.ts +17 -0
  489. package/src/ipc/routes/notification.ts +133 -0
  490. package/src/ipc/routes/rename-conversation.ts +59 -0
  491. package/src/ipc/routes/search-contacts.ts +19 -0
  492. package/src/ipc/routes/task-queue.ts +226 -0
  493. package/src/ipc/routes/task.ts +173 -0
  494. package/src/ipc/routes/ui-request.ts +50 -0
  495. package/src/ipc/routes/upsert-contact.ts +25 -0
  496. package/src/ipc/routes/watcher.ts +203 -0
  497. package/src/ipc/socket-path.ts +76 -0
  498. package/src/media/app-icon-generator.ts +23 -46
  499. package/src/media/avatar-router.ts +26 -41
  500. package/src/media/gemini-image-service.ts +8 -41
  501. package/src/media/image-credentials.ts +73 -0
  502. package/src/media/image-service.ts +85 -0
  503. package/src/media/openai-image-service.ts +131 -0
  504. package/src/media/types.ts +46 -0
  505. package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
  506. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  507. package/src/memory/admin.ts +18 -0
  508. package/src/memory/conversation-analyze-job.ts +14 -13
  509. package/src/memory/conversation-attention-store.ts +13 -6
  510. package/src/memory/conversation-crud.ts +133 -3
  511. package/src/memory/conversation-group-migration.ts +38 -6
  512. package/src/memory/conversation-queries.ts +57 -4
  513. package/src/memory/conversation-title-service.ts +32 -4
  514. package/src/memory/db-init.ts +10 -0
  515. package/src/memory/embedding-backend.ts +1 -1
  516. package/src/memory/embedding-gemini.test.ts +41 -2
  517. package/src/memory/embedding-gemini.ts +6 -1
  518. package/src/memory/graph/bootstrap.test.ts +282 -0
  519. package/src/memory/graph/bootstrap.ts +8 -5
  520. package/src/memory/graph/compaction.ts +299 -0
  521. package/src/memory/graph/consolidation.ts +4 -4
  522. package/src/memory/graph/conversation-graph-memory.ts +89 -29
  523. package/src/memory/graph/extraction.test.ts +272 -2
  524. package/src/memory/graph/extraction.ts +183 -53
  525. package/src/memory/graph/graph-search.test.ts +93 -0
  526. package/src/memory/graph/graph-search.ts +4 -1
  527. package/src/memory/graph/inspect.ts +2 -2
  528. package/src/memory/graph/narrative.ts +2 -2
  529. package/src/memory/graph/pattern-scan.ts +2 -2
  530. package/src/memory/graph/retriever.test.ts +459 -0
  531. package/src/memory/graph/retriever.ts +237 -48
  532. package/src/memory/graph/store.ts +41 -0
  533. package/src/memory/graph/tool-handlers.ts +27 -0
  534. package/src/memory/graph/tools.ts +6 -1
  535. package/src/memory/indexer.ts +5 -5
  536. package/src/memory/job-handlers/conversation-starters.ts +23 -20
  537. package/src/memory/job-handlers/summarization.ts +2 -2
  538. package/src/memory/job-utils.ts +7 -1
  539. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  540. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  541. package/src/memory/jobs-store.ts +44 -3
  542. package/src/memory/jobs-worker.ts +4 -0
  543. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  544. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  545. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  546. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
  547. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  548. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  549. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  550. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  551. package/src/memory/migrations/index.ts +5 -0
  552. package/src/memory/pkb/pkb-index.test.ts +369 -0
  553. package/src/memory/pkb/pkb-index.ts +255 -0
  554. package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
  555. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  556. package/src/memory/pkb/pkb-search.test.ts +499 -0
  557. package/src/memory/pkb/pkb-search.ts +159 -0
  558. package/src/memory/pkb/types.ts +53 -0
  559. package/src/memory/qdrant-client.test.ts +60 -0
  560. package/src/memory/qdrant-client.ts +147 -1
  561. package/src/memory/schema/infrastructure.ts +1 -0
  562. package/src/memory/schema/oauth.ts +4 -1
  563. package/src/memory/slack-thread-store.ts +37 -0
  564. package/src/messaging/providers/gmail/adapter.ts +6 -16
  565. package/src/messaging/providers/gmail/client.ts +22 -0
  566. package/src/messaging/providers/gmail/types.ts +7 -0
  567. package/src/messaging/providers/slack/adapter.ts +14 -2
  568. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  569. package/src/messaging/providers/slack/backfill.ts +101 -0
  570. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  571. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  572. package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
  573. package/src/messaging/providers/slack/render-transcript.ts +501 -0
  574. package/src/messaging/style-analyzer.ts +5 -2
  575. package/src/notifications/README.md +9 -5
  576. package/src/notifications/conversation-pairing.ts +78 -19
  577. package/src/notifications/copy-composer.ts +0 -5
  578. package/src/notifications/decision-engine.ts +3 -9
  579. package/src/notifications/emit-signal.ts +1 -1
  580. package/src/notifications/preference-extractor.ts +2 -6
  581. package/src/notifications/signal.ts +1 -2
  582. package/src/oauth/AGENTS.md +1 -1
  583. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  584. package/src/oauth/connect-orchestrator.ts +8 -34
  585. package/src/oauth/connect-types.ts +6 -10
  586. package/src/oauth/manual-token-connection.ts +23 -0
  587. package/src/oauth/oauth-store.ts +31 -14
  588. package/src/oauth/platform-connection.test.ts +47 -0
  589. package/src/oauth/platform-connection.ts +15 -5
  590. package/src/oauth/provider-serializer.ts +6 -1
  591. package/src/oauth/seed-providers.ts +56 -106
  592. package/src/outbound-proxy/http-forwarder.ts +9 -0
  593. package/src/permissions/approval-policy.test.ts +1223 -0
  594. package/src/permissions/approval-policy.ts +309 -0
  595. package/src/permissions/arg-parser.test.ts +161 -0
  596. package/src/permissions/arg-parser.ts +141 -0
  597. package/src/permissions/bash-risk-classifier.test.ts +1620 -0
  598. package/src/permissions/bash-risk-classifier.ts +950 -0
  599. package/src/permissions/checker.ts +348 -711
  600. package/src/permissions/command-registry.test.ts +774 -0
  601. package/src/permissions/command-registry.ts +1005 -0
  602. package/src/permissions/defaults.ts +28 -79
  603. package/src/permissions/file-risk-classifier.test.ts +535 -0
  604. package/src/permissions/file-risk-classifier.ts +274 -0
  605. package/src/permissions/gateway-threshold-reader.ts +196 -0
  606. package/src/permissions/prompter.ts +4 -0
  607. package/src/permissions/risk-types.ts +262 -0
  608. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  609. package/src/permissions/schedule-risk-classifier.ts +85 -0
  610. package/src/permissions/secret-prompter.ts +53 -2
  611. package/src/permissions/shell-identity.ts +2 -42
  612. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  613. package/src/permissions/skill-risk-classifier.ts +214 -0
  614. package/src/permissions/trust-client.ts +52 -25
  615. package/src/permissions/trust-store-interface.ts +1 -6
  616. package/src/permissions/trust-store.ts +161 -62
  617. package/src/permissions/types.ts +25 -14
  618. package/src/permissions/web-risk-classifier.test.ts +170 -0
  619. package/src/permissions/web-risk-classifier.ts +89 -0
  620. package/src/permissions/workspace-policy.ts +9 -19
  621. package/src/platform/client.ts +19 -1
  622. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  623. package/src/plugins/defaults/compaction.ts +145 -0
  624. package/src/plugins/defaults/empty-response.ts +126 -0
  625. package/src/plugins/defaults/history-repair.ts +85 -0
  626. package/src/plugins/defaults/index.ts +116 -0
  627. package/src/plugins/defaults/injectors.ts +491 -0
  628. package/src/plugins/defaults/llm-call.ts +82 -0
  629. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  630. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  631. package/src/plugins/defaults/persistence.ts +129 -0
  632. package/src/plugins/defaults/title-generate.ts +95 -0
  633. package/src/plugins/defaults/token-estimate.ts +104 -0
  634. package/src/plugins/defaults/tool-error.ts +126 -0
  635. package/src/plugins/defaults/tool-execute.ts +89 -0
  636. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  637. package/src/plugins/pipeline.ts +316 -0
  638. package/src/plugins/plugin-skill-contributions.ts +292 -0
  639. package/src/plugins/registry.ts +241 -0
  640. package/src/plugins/types.ts +1134 -0
  641. package/src/plugins/user-loader.ts +177 -0
  642. package/src/prompts/persona-resolver.ts +3 -3
  643. package/src/prompts/system-prompt.ts +19 -20
  644. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  645. package/src/prompts/templates/SOUL.md +2 -2
  646. package/src/prompts/update-bulletin-job.ts +190 -0
  647. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  648. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  649. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  650. package/src/providers/anthropic/client.ts +183 -14
  651. package/src/providers/call-site-routing.ts +71 -0
  652. package/src/providers/gemini/client.ts +65 -2
  653. package/src/providers/managed-proxy/constants.ts +2 -1
  654. package/src/providers/model-catalog.ts +524 -33
  655. package/src/providers/model-intents.ts +4 -4
  656. package/src/providers/openai/chat-completions-provider.ts +57 -1
  657. package/src/providers/openai/responses-provider.ts +86 -9
  658. package/src/providers/openrouter/client.ts +80 -9
  659. package/src/providers/provider-env-vars.ts +56 -0
  660. package/src/providers/provider-send-message.ts +22 -5
  661. package/src/providers/ratelimit.ts +4 -0
  662. package/src/providers/registry.ts +19 -8
  663. package/src/providers/retry.ts +174 -39
  664. package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
  665. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  666. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  667. package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
  668. package/src/providers/speech-to-text/provider-catalog.ts +17 -0
  669. package/src/providers/speech-to-text/resolve.ts +7 -0
  670. package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
  671. package/src/providers/speech-to-text/xai-realtime.ts +821 -0
  672. package/src/providers/speech-to-text/xai.test.ts +155 -0
  673. package/src/providers/speech-to-text/xai.ts +97 -0
  674. package/src/providers/types.ts +93 -3
  675. package/src/runtime/AGENTS.md +27 -18
  676. package/src/runtime/__tests__/agent-wake.test.ts +43 -2
  677. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  678. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  679. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  680. package/src/runtime/agent-wake.ts +63 -22
  681. package/src/runtime/auth/route-policy.ts +4 -0
  682. package/src/runtime/btw-sidechain.ts +13 -3
  683. package/src/runtime/channel-reply-delivery.ts +106 -2
  684. package/src/runtime/client-registry.ts +261 -0
  685. package/src/runtime/decision-token.ts +116 -0
  686. package/src/runtime/gateway-client.ts +2 -2
  687. package/src/runtime/http-router.ts +32 -0
  688. package/src/runtime/http-server.ts +129 -9
  689. package/src/runtime/http-types.ts +23 -3
  690. package/src/runtime/interactive-ui.ts +362 -0
  691. package/src/runtime/invite-instruction-generator.ts +2 -2
  692. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  693. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  694. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  695. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  696. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  697. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  698. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  699. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  700. package/src/runtime/migrations/vbundle-importer.ts +154 -9
  701. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  702. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  703. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  704. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  705. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  706. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
  707. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
  708. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  709. package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
  710. package/src/runtime/routes/approval-routes.ts +29 -17
  711. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  712. package/src/runtime/routes/avatar-routes.ts +20 -4
  713. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  714. package/src/runtime/routes/btw-routes.ts +1 -4
  715. package/src/runtime/routes/conversation-management-routes.ts +20 -2
  716. package/src/runtime/routes/conversation-routes.ts +351 -138
  717. package/src/runtime/routes/debug-routes.ts +1 -1
  718. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  719. package/src/runtime/routes/events-routes.ts +16 -0
  720. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  721. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  722. package/src/runtime/routes/home-feed-routes.ts +120 -2
  723. package/src/runtime/routes/inbound-message-handler.ts +987 -2
  724. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  725. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  726. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  727. package/src/runtime/routes/integrations/slack/channel.ts +25 -3
  728. package/src/runtime/routes/llm-context-normalization.ts +23 -1
  729. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  730. package/src/runtime/routes/migration-routes.ts +720 -127
  731. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  732. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  733. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  734. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  735. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  736. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  737. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  738. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  739. package/src/runtime/routes/playground/deps.ts +56 -0
  740. package/src/runtime/routes/playground/force-compact.ts +73 -0
  741. package/src/runtime/routes/playground/guard.ts +37 -0
  742. package/src/runtime/routes/playground/index.ts +28 -0
  743. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  744. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  745. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  746. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  747. package/src/runtime/routes/playground/state.ts +78 -0
  748. package/src/runtime/routes/schedule-routes.ts +89 -8
  749. package/src/runtime/routes/settings-routes.ts +4 -2
  750. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  751. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  752. package/src/runtime/routes/work-items-routes.ts +3 -2
  753. package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
  754. package/src/runtime/services/analyze-conversation.ts +12 -16
  755. package/src/runtime/skill-route-registry.ts +97 -15
  756. package/src/schedule/run-script.ts +68 -0
  757. package/src/schedule/schedule-store.ts +7 -1
  758. package/src/schedule/scheduler.ts +56 -8
  759. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  760. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  761. package/src/security/oauth2.ts +98 -35
  762. package/src/security/secure-keys.ts +7 -8
  763. package/src/security/token-manager.ts +27 -13
  764. package/src/security/untrusted-content.ts +102 -0
  765. package/src/skills/catalog-cache.ts +35 -9
  766. package/src/skills/catalog-install.ts +31 -3
  767. package/src/skills/skill-cache-store.ts +97 -0
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
  769. package/src/stt/daemon-batch-transcriber.ts +33 -0
  770. package/src/stt/stt-stream-session.ts +8 -1
  771. package/src/stt/types.ts +5 -1
  772. package/src/subagent/manager.ts +41 -13
  773. package/src/tasks/ephemeral-permissions.ts +9 -4
  774. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  775. package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
  776. package/src/tools/browser/browser-execution.ts +150 -54
  777. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  778. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  779. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
  780. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  781. package/src/tools/browser/cdp-client/factory.ts +15 -4
  782. package/src/tools/credentials/tool-policy.ts +39 -5
  783. package/src/tools/credentials/vault.ts +9 -4
  784. package/src/tools/executor.ts +129 -73
  785. package/src/tools/filesystem/write.ts +52 -0
  786. package/src/tools/host-terminal/host-shell.ts +45 -5
  787. package/src/tools/memory/register.test.ts +185 -0
  788. package/src/tools/memory/register.ts +3 -1
  789. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  790. package/src/tools/network/web-fetch.ts +20 -10
  791. package/src/tools/network/web-search.ts +19 -4
  792. package/src/tools/permission-checker.ts +116 -46
  793. package/src/tools/policy-context.ts +29 -8
  794. package/src/tools/registry.ts +195 -6
  795. package/src/tools/schedule/create.ts +23 -8
  796. package/src/tools/schedule/update.ts +3 -1
  797. package/src/tools/secret-detection-handler.ts +0 -51
  798. package/src/tools/side-effects.ts +0 -11
  799. package/src/tools/skills/execute.ts +2 -2
  800. package/src/tools/skills/sandbox-runner.ts +5 -2
  801. package/src/tools/system/avatar-generator.ts +6 -2
  802. package/src/tools/terminal/backends/native.ts +51 -2
  803. package/src/tools/terminal/safe-env.ts +3 -2
  804. package/src/tools/terminal/shell.ts +1 -0
  805. package/src/tools/tool-manifest.ts +6 -21
  806. package/src/tools/types.ts +40 -5
  807. package/src/tools/verification-control-plane-policy.ts +1 -1
  808. package/src/tts/__tests__/provider-adapters.test.ts +240 -13
  809. package/src/tts/provider-catalog.ts +18 -0
  810. package/src/tts/providers/index.ts +2 -0
  811. package/src/tts/providers/xai-provider.ts +224 -0
  812. package/src/tts/types.ts +46 -0
  813. package/src/types/tar-stream.d.ts +66 -0
  814. package/src/util/json.ts +17 -0
  815. package/src/util/platform.ts +9 -4
  816. package/src/util/pricing.ts +41 -8
  817. package/src/watcher/engine.ts +1 -1
  818. package/src/watcher/providers/google-calendar.ts +134 -8
  819. package/src/watcher/providers/outlook-calendar.ts +42 -2
  820. package/src/workspace/git-service.ts +23 -4
  821. package/src/workspace/migrations/006-services-config.ts +2 -4
  822. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  823. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  824. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  825. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  826. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
  827. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  828. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  829. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  830. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  831. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  832. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  833. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  834. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  835. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  836. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  837. package/src/workspace/migrations/AGENTS.md +1 -1
  838. package/src/workspace/migrations/registry.ts +28 -0
  839. package/src/workspace/provider-commit-message-generator.ts +19 -38
  840. package/tsconfig.json +1 -1
  841. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  842. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  843. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  844. package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
  845. package/src/__tests__/gmail-archive-gate.test.ts +0 -246
  846. package/src/__tests__/gmail-preferences.test.ts +0 -117
  847. package/src/__tests__/hooks-blocking.test.ts +0 -178
  848. package/src/__tests__/hooks-cli.test.ts +0 -182
  849. package/src/__tests__/hooks-config.test.ts +0 -108
  850. package/src/__tests__/hooks-discovery.test.ts +0 -211
  851. package/src/__tests__/hooks-integration.test.ts +0 -196
  852. package/src/__tests__/hooks-manager.test.ts +0 -226
  853. package/src/__tests__/hooks-runner.test.ts +0 -175
  854. package/src/__tests__/hooks-settings.test.ts +0 -160
  855. package/src/__tests__/hooks-templates.test.ts +0 -169
  856. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  857. package/src/__tests__/hooks-watch.test.ts +0 -112
  858. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  859. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  860. package/src/__tests__/outlook-attachments.test.ts +0 -301
  861. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  862. package/src/__tests__/outlook-categories.test.ts +0 -212
  863. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  864. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  865. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  866. package/src/__tests__/outlook-trash.test.ts +0 -77
  867. package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
  868. package/src/__tests__/send-notification-tool.test.ts +0 -83
  869. package/src/__tests__/update-bulletin-format.test.ts +0 -181
  870. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  871. package/src/__tests__/update-bulletin.test.ts +0 -478
  872. package/src/__tests__/update-template-contract.test.ts +0 -29
  873. package/src/cli/commands/doctor.ts +0 -341
  874. package/src/cli/commands/shotgun.ts +0 -266
  875. package/src/config/bundled-skills/browser/SKILL.md +0 -88
  876. package/src/config/bundled-skills/browser/TOOLS.json +0 -516
  877. package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
  878. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  879. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  880. package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
  881. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  882. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  883. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  884. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  885. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  886. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  887. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  888. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  889. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  890. package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
  891. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  892. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
  893. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  894. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  895. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  896. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  897. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  898. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  899. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
  900. package/src/config/bundled-skills/gmail/SKILL.md +0 -221
  901. package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
  902. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
  903. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  904. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  905. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  906. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  907. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  908. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  909. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
  910. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
  911. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
  912. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  913. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
  914. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  915. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  916. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  917. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  918. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  919. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  920. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  921. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  922. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  923. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  924. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  925. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  926. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  927. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  928. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  929. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  930. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  931. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  932. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  933. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  934. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  935. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  936. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  937. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  938. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  939. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  940. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  941. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  942. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  943. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  944. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  945. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  946. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  947. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  948. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  949. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  950. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  951. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  952. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  953. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  954. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  955. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  956. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  957. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  958. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  959. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  960. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  961. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  962. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  963. package/src/config/bundled-skills/slack/SKILL.md +0 -108
  964. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  965. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  966. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  967. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  968. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  969. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  970. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  971. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  972. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  973. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  974. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  975. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  976. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  977. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  978. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  979. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  980. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  981. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  982. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  983. package/src/daemon/context-overflow-approval.ts +0 -52
  984. package/src/daemon/watch-handler.ts +0 -399
  985. package/src/hooks/cli.ts +0 -253
  986. package/src/hooks/config.ts +0 -100
  987. package/src/hooks/discovery.ts +0 -135
  988. package/src/hooks/manager.ts +0 -179
  989. package/src/hooks/runner.ts +0 -117
  990. package/src/hooks/templates.ts +0 -77
  991. package/src/hooks/types.ts +0 -75
  992. package/src/oauth/scope-policy.ts +0 -89
  993. package/src/prompts/templates/UPDATES.md +0 -50
  994. package/src/prompts/update-bulletin-format.ts +0 -85
  995. package/src/prompts/update-bulletin-state.ts +0 -58
  996. package/src/prompts/update-bulletin-template-path.ts +0 -13
  997. package/src/prompts/update-bulletin.ts +0 -139
  998. package/src/runtime/gateway-internal-client.ts +0 -94
  999. package/src/runtime/routes/watch-routes.ts +0 -156
  1000. package/src/shared/provider-env-vars.ts +0 -19
  1001. package/src/signals/shotgun.ts +0 -203
  1002. package/src/tools/watch/screen-watch.ts +0 -144
  1003. package/src/tools/watch/watch-state.ts +0 -142
  1004. package/src/tools/watcher/create.ts +0 -86
  1005. package/src/tools/watcher/delete.ts +0 -36
  1006. package/src/tools/watcher/digest.ts +0 -54
  1007. package/src/tools/watcher/list.ts +0 -83
  1008. package/src/tools/watcher/update.ts +0 -71
@@ -10,14 +10,43 @@ import { join, resolve } from "node:path";
10
10
 
11
11
  import { type ChannelId, parseInterfaceId } from "../channels/types.js";
12
12
  import { getAppDirPath, listAppFiles } from "../memory/app-store.js";
13
+ import {
14
+ getMessages as defaultGetMessages,
15
+ type MessageRow,
16
+ } from "../memory/conversation-crud.js";
17
+ import {
18
+ countMemoryPrefixBlocks,
19
+ extractMemoryPrefixBlocks,
20
+ } from "../memory/graph/conversation-graph-memory.js";
21
+ import type { QdrantSparseVector } from "../memory/qdrant-client.js";
22
+ import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
23
+ import {
24
+ extractTagLineTexts,
25
+ isReactionTagLine,
26
+ type RenderableSlackMessage,
27
+ renderSlackTranscript,
28
+ } from "../messaging/providers/slack/render-transcript.js";
13
29
  import { isPermissionControlsV2Enabled } from "../permissions/v2-consent-policy.js";
14
- import type { Message } from "../providers/types.js";
15
- import type { ActorTrustContext } from "../runtime/actor-trust-resolver.js";
30
+ import { getInjectors } from "../plugins/registry.js";
31
+ import type {
32
+ InjectionBlock,
33
+ InjectionPlacement,
34
+ TurnContext,
35
+ TurnInjectionInputs,
36
+ } from "../plugins/types.js";
37
+ import type { ContentBlock, Message } from "../providers/types.js";
38
+ import {
39
+ type ActorTrustContext,
40
+ isUntrustedTrustClass,
41
+ type TrustClass,
42
+ } from "../runtime/actor-trust-resolver.js";
16
43
  import { channelStatusToMemberStatus } from "../runtime/routes/inbound-stages/acl-enforcement.js";
17
44
  import type { SubagentState } from "../subagent/types.js";
18
45
  import { TERMINAL_STATUSES } from "../subagent/types.js";
19
46
  import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
20
47
  import { stripCommentLines } from "../util/strip-comment-lines.js";
48
+ import { filterMessagesForUntrustedActor } from "./conversation-lifecycle.js";
49
+ import { type PkbContextConversation } from "./pkb-context-tracker.js";
21
50
 
22
51
  /**
23
52
  * Describes the capabilities of the channel through which the user is
@@ -493,16 +522,11 @@ export function buildSubagentStatusBlock(
493
522
  return lines.join("\n");
494
523
  }
495
524
 
496
- /** Append a subagent status block to the last user message. */
497
- export function injectSubagentStatus(
498
- message: Message,
499
- statusBlock: string,
500
- ): Message {
501
- return {
502
- ...message,
503
- content: [...message.content, { type: "text" as const, text: statusBlock }],
504
- };
505
- }
525
+ // The `<active_subagents>` block is emitted by the `subagent-status` default
526
+ // injector (`plugins/defaults/injectors.ts`) as an `append-user-tail`
527
+ // placement. Use {@link applyRuntimeInjections} with
528
+ // `options.subagentStatusBlock` set, or drive the injector chain directly
529
+ // via `collectInjectorBlocks`.
506
530
 
507
531
  /**
508
532
  * Append voice call-control protocol instructions to the last user
@@ -546,47 +570,17 @@ export function readNowScratchpad(): string | null {
546
570
  }
547
571
 
548
572
  /**
549
- * Insert NOW.md scratchpad content into the user message, after any
550
- * injected context blocks (e.g. memory_context) but before the user's
551
- * original content. This keeps the user's actual message as the last
552
- * thing the model reads.
573
+ * The `<NOW.md>` block is emitted by the `now-md` default injector
574
+ * (`plugins/defaults/injectors.ts`) as an `after-memory-prefix` placement.
575
+ * Use {@link applyRuntimeInjections} with `options.nowScratchpad` set.
553
576
  */
554
- export function injectNowScratchpad(
555
- message: Message,
556
- content: string,
557
- ): Message {
558
- const scratchpadBlock = {
559
- type: "text" as const,
560
- text: `<NOW.md Always keep this up to date>\n${content}\n</NOW.md>`,
561
- };
562
-
563
- // Find insertion point: skip any leading injected-context text blocks
564
- // (e.g. memory_context) so the scratchpad lands between injected context
565
- // and the user's original content.
566
- let insertIdx = 0;
567
- for (let i = 0; i < message.content.length; i++) {
568
- const block = message.content[i];
569
- if (block.type === "text" && block.text.startsWith("<memory_context")) {
570
- insertIdx = i + 1;
571
- } else {
572
- break;
573
- }
574
- }
575
-
576
- return {
577
- ...message,
578
- content: [
579
- ...message.content.slice(0, insertIdx),
580
- scratchpadBlock,
581
- ...message.content.slice(insertIdx),
582
- ],
583
- };
584
- }
585
577
 
586
578
  /** Strip `<NOW.md>` blocks injected by `injectNowScratchpad`. */
587
579
  export function stripNowScratchpad(messages: Message[]): Message[] {
588
580
  return stripUserTextBlocksByPrefix(messages, [
589
- "<NOW.md Always keep this up to date>",
581
+ // Shared prefix catches both the current tag and any pre-line-limit
582
+ // variant that may linger in in-flight histories during a rolling deploy.
583
+ "<NOW.md Always keep this up to date",
590
584
  "<now_scratchpad>", // backward-compat: strip legacy blocks from pre-rename history
591
585
  ]);
592
586
  }
@@ -607,14 +601,6 @@ const AUTOINJECT_FILENAME = "_autoinject.md";
607
601
  /** Max buffer.md lines injected into prompts — keeps context bounded even when filing is off. */
608
602
  const MAX_BUFFER_LINES = 50;
609
603
 
610
- const PKB_SYSTEM_REMINDER =
611
- "<system_reminder>" +
612
- "\n**CRITICAL:** you MUST read any PKB files that might be relevant to this conversation — " +
613
- "INDEX.md is your table of contents. Don't wait to be asked. " +
614
- "Use `remember` OFTEN for EVERY new fact you learn IMMEDIATELY, don't wait for the next turn. " +
615
- "Corrections to things you had wrong are the highest-priority remembers — never skip them." +
616
- "\n</system_reminder>";
617
-
618
604
  /**
619
605
  * Read `_autoinject.md` from the PKB directory and return the list of
620
606
  * filenames to inject.
@@ -639,6 +625,21 @@ export function readAutoinjectList(pkbDir: string): string[] | null {
639
625
  }
640
626
  }
641
627
 
628
+ /**
629
+ * Resolve the effective list of auto-inject filenames for a PKB directory.
630
+ *
631
+ * This is the single source of truth used both by `readPkbContext` (which
632
+ * actually injects the files) and by the PKB reminder-hint tracker in
633
+ * `conversation-agent-loop.ts` (which needs to know what's already in
634
+ * context so it doesn't redundantly recommend those files).
635
+ *
636
+ * Returns `PKB_DEFAULT_FILES` when `_autoinject.md` is missing/unreadable,
637
+ * or the parsed list (possibly empty) when it is present.
638
+ */
639
+ export function getPkbAutoInjectList(pkbRoot: string): string[] {
640
+ return readAutoinjectList(pkbRoot) ?? PKB_DEFAULT_FILES;
641
+ }
642
+
642
643
  /**
643
644
  * Read the always-loaded PKB files and append a nudge encouraging the
644
645
  * assistant to proactively read topic files and use `remember` aggressively.
@@ -653,7 +654,7 @@ export function readPkbContext(): string | null {
653
654
  const pkbDir = join(getWorkspaceDir(), "pkb");
654
655
  if (!existsSync(pkbDir)) return null;
655
656
 
656
- const filesToInject = readAutoinjectList(pkbDir) ?? PKB_DEFAULT_FILES;
657
+ const filesToInject = getPkbAutoInjectList(pkbDir);
657
658
 
658
659
  const parts: string[] = [];
659
660
  for (const file of filesToInject) {
@@ -680,50 +681,17 @@ export function readPkbContext(): string | null {
680
681
  return parts.length > 0 ? parts.join("\n\n") : null;
681
682
  }
682
683
 
683
- /**
684
- * Insert PKB context into the user message, after any injected memory
685
- * blocks but before NOW.md and the user's original content.
686
- */
687
- export function injectPkbContext(message: Message, content: string): Message {
688
- // Escape closing tags that could break out of the XML wrapper
689
- const escaped = content.replace(/<\/pkb\s*>/gi, "&lt;/pkb&gt;");
690
- const pkbBlock = {
691
- type: "text" as const,
692
- text: `<pkb>\n${escaped}\n</pkb>`,
693
- };
684
+ // The `<knowledge_base>` block is emitted by the `pkb-context` default
685
+ // injector (`plugins/defaults/injectors.ts`) as an `after-memory-prefix`
686
+ // placement, splicing immediately after any leading memory-prefix blocks.
687
+ // Use {@link applyRuntimeInjections} with `options.pkbContext` set.
694
688
 
695
- // Find insertion point: skip any leading memory/image blocks
696
- let insertIdx = 0;
697
- for (let i = 0; i < message.content.length; i++) {
698
- const block = message.content[i];
699
- if (
700
- block.type === "text" &&
701
- (block.text.startsWith("<memory") ||
702
- block.text.startsWith("</memory_image>") ||
703
- block.text.startsWith("<memory_context"))
704
- ) {
705
- insertIdx = i + 1;
706
- } else if (block.type === "image") {
707
- // Memory images precede the memory text block
708
- insertIdx = i + 1;
709
- } else {
710
- break;
711
- }
712
- }
713
-
714
- return {
715
- ...message,
716
- content: [
717
- ...message.content.slice(0, insertIdx),
718
- pkbBlock,
719
- ...message.content.slice(insertIdx),
720
- ],
721
- };
722
- }
723
-
724
- /** Strip `<pkb>` blocks injected by `injectPkbContext`. */
689
+ /** Strip `<knowledge_base>` blocks injected by `injectPkbContext`. */
725
690
  export function stripPkbContext(messages: Message[]): Message[] {
726
- return stripUserTextBlocksByPrefix(messages, ["<pkb>"]);
691
+ return stripUserTextBlocksByPrefix(messages, [
692
+ "<knowledge_base>",
693
+ "<pkb>", // backward-compat: strip legacy blocks from pre-rename history
694
+ ]);
727
695
  }
728
696
 
729
697
  /**
@@ -909,6 +877,10 @@ export function buildUnifiedTurnContextBlock(
909
877
  .replace(/[\r\n\u0085\u2028\u2029]+/g, " ")
910
878
  // Replace remaining ASCII C0/C1 control characters and DEL.
911
879
  .replace(/[\x00-\x1F\x7F-\x9F]/g, " ")
880
+ // Escape XML special characters to prevent turn_context breakout.
881
+ .replace(/&/g, "&amp;")
882
+ .replace(/</g, "&lt;")
883
+ .replace(/>/g, "&gt;")
912
884
  .trim();
913
885
  return singleLine.length > 0 ? singleLine : "unknown";
914
886
  };
@@ -1088,18 +1060,10 @@ export function stripChannelCapabilityContext(messages: Message[]): Message[] {
1088
1060
  return stripUserTextBlocksByPrefix(messages, ["<channel_capabilities>"]);
1089
1061
  }
1090
1062
 
1091
- /**
1092
- * Prepend workspace top-level directory context to a user message.
1093
- */
1094
- export function injectWorkspaceTopLevelContext(
1095
- message: Message,
1096
- contextText: string,
1097
- ): Message {
1098
- return {
1099
- ...message,
1100
- content: [{ type: "text", text: contextText }, ...message.content],
1101
- };
1102
- }
1063
+ // The workspace top-level context block is emitted by the
1064
+ // `workspace-context` default injector (`plugins/defaults/injectors.ts`)
1065
+ // as a `prepend-user-tail` placement. Use {@link applyRuntimeInjections}
1066
+ // with `options.workspaceTopLevelContext` set.
1103
1067
 
1104
1068
  /**
1105
1069
  * Strip `<active_workspace>` (and legacy `<active_dynamic_page>`) blocks
@@ -1138,6 +1102,445 @@ export function stripTransportHints(messages: Message[]): Message[] {
1138
1102
  return stripUserTextBlocksByPrefix(messages, ["<transport_hints>"]);
1139
1103
  }
1140
1104
 
1105
+ // ---------------------------------------------------------------------------
1106
+ // Slack chronological transcript assembly
1107
+ // ---------------------------------------------------------------------------
1108
+
1109
+ /**
1110
+ * True when the channel capabilities describe a Slack non-DM conversation
1111
+ * (group/channel/mpim). Used to gate thread-only behavior such as the
1112
+ * `<active_thread>` focus block. DMs are excluded because they have no
1113
+ * threads.
1114
+ *
1115
+ * The gateway normalizer sets `chatType: "channel"` for every non-DM Slack
1116
+ * conversation (public, private, and mpim alike — see
1117
+ * `gateway/src/slack/normalize.ts`) and omits the field entirely for DMs.
1118
+ * We therefore accept only `chatType === "channel"` — when the gateway
1119
+ * omits `chatType` (as it does for DMs), the check correctly returns
1120
+ * `false`.
1121
+ *
1122
+ * The chronological-transcript override applies to ALL Slack
1123
+ * conversations (channels and DMs) — gate that on
1124
+ * `channelCapabilities.channel === "slack"` rather than this helper.
1125
+ */
1126
+ export function isSlackChannelConversation(
1127
+ channelCapabilities?: ChannelCapabilities | null,
1128
+ ): boolean {
1129
+ return (
1130
+ channelCapabilities?.channel === "slack" &&
1131
+ channelCapabilities.chatType === "channel"
1132
+ );
1133
+ }
1134
+
1135
+ /**
1136
+ * Minimal structural shape of a persisted message row used by the Slack
1137
+ * chronological-transcript assembly path. Decouples the assembly logic from
1138
+ * the DB-row type so it can be unit-tested with plain literals.
1139
+ */
1140
+ export interface SlackTranscriptInputRow {
1141
+ role: "user" | "assistant";
1142
+ /** Raw persisted content column. JSON-encoded `ContentBlock[]` in production. */
1143
+ content: string;
1144
+ /** Epoch ms when the row was created. */
1145
+ createdAt: number;
1146
+ /** Raw `metadata` column value (JSON string with optional `slackMeta` sub-key). */
1147
+ metadata: string | null;
1148
+ }
1149
+
1150
+ /**
1151
+ * Extract the user-facing plain text from an already-parsed `ContentBlock[]`.
1152
+ * Only `text` blocks contribute to the rendered transcript line. Tool-use /
1153
+ * tool-result / thinking blocks are intentionally elided — they would clutter
1154
+ * the Slack-style transcript and the model can already recall them from the
1155
+ * surrounding turn structure.
1156
+ *
1157
+ * Rows with no text blocks (e.g. images, file uploads, pure tool turns) would
1158
+ * otherwise render as an empty transcript line like `[14:25 @alice]: `;
1159
+ * surface the attachment/tool context instead so the model can tell something
1160
+ * was actually said on that turn.
1161
+ */
1162
+ function extractPlainTextFromBlocks(blocks: ContentBlock[]): string {
1163
+ const parts: string[] = [];
1164
+ const placeholderLabels: string[] = [];
1165
+ for (const block of blocks) {
1166
+ if (!block || typeof block !== "object") continue;
1167
+ if (block.type === "text") {
1168
+ parts.push(block.text);
1169
+ continue;
1170
+ }
1171
+ const label = placeholderForBlockType(block.type);
1172
+ if (label && !placeholderLabels.includes(label)) {
1173
+ placeholderLabels.push(label);
1174
+ }
1175
+ }
1176
+ if (parts.length > 0) {
1177
+ return parts.join("\n");
1178
+ }
1179
+ return placeholderLabels.join(" ");
1180
+ }
1181
+
1182
+ function placeholderForBlockType(type: ContentBlock["type"]): string | null {
1183
+ switch (type) {
1184
+ case "image":
1185
+ return "[image]";
1186
+ case "file":
1187
+ return "[file]";
1188
+ case "tool_use":
1189
+ case "server_tool_use":
1190
+ return "[tool call]";
1191
+ case "tool_result":
1192
+ case "web_search_tool_result":
1193
+ return "[tool result]";
1194
+ case "thinking":
1195
+ case "redacted_thinking":
1196
+ case "text":
1197
+ return null;
1198
+ }
1199
+ }
1200
+
1201
+ /**
1202
+ * Convert a persisted row into the {@link RenderableSlackMessage} shape
1203
+ * consumed by `renderSlackTranscript`.
1204
+ *
1205
+ * Legacy pre-upgrade rows (no `slackMeta` sub-key, malformed metadata, etc.)
1206
+ * yield `metadata: null`; the renderer then takes its flat-render fallback
1207
+ * path and the row stays in chronological order via `createdAt`.
1208
+ *
1209
+ * Sender labels are emitted only when they add information beyond the role
1210
+ * slot:
1211
+ * - Reaction rows: always labeled — `@assistant` for the assistant, the real
1212
+ * `slackMeta.displayName` for a known user, or `@user` as a last-resort
1213
+ * subject so the rendered `[time X reacted ...]` line still parses.
1214
+ * - Assistant message rows: `null` — the role slot already says "assistant".
1215
+ * - User message rows: real `slackMeta.displayName` when available (to
1216
+ * disambiguate speakers in multi-party channels); `null` otherwise so the
1217
+ * renderer drops the redundant `@user` placeholder.
1218
+ */
1219
+ function rowToRenderable(row: SlackTranscriptInputRow): RenderableSlackMessage {
1220
+ let slackMeta: ReturnType<typeof readSlackMetadata> = null;
1221
+ if (row.metadata) {
1222
+ try {
1223
+ const outer = JSON.parse(row.metadata) as { slackMeta?: unknown };
1224
+ if (typeof outer.slackMeta === "string") {
1225
+ slackMeta = readSlackMetadata(outer.slackMeta);
1226
+ }
1227
+ } catch {
1228
+ // Malformed metadata — fall through to legacy/null treatment.
1229
+ }
1230
+ }
1231
+
1232
+ const isReaction = slackMeta?.eventKind === "reaction";
1233
+ let senderLabel: string | null;
1234
+ if (isReaction) {
1235
+ senderLabel =
1236
+ row.role === "assistant"
1237
+ ? "@assistant"
1238
+ : (slackMeta?.displayName ?? "@user");
1239
+ } else if (row.role === "assistant") {
1240
+ senderLabel = null;
1241
+ } else {
1242
+ senderLabel = slackMeta?.displayName ?? null;
1243
+ }
1244
+
1245
+ // Parse `row.content` once and derive both the structured `contentBlocks`
1246
+ // view (for downstream tool-block preservation) and the flattened
1247
+ // `plainText` view (used for tag-line rendering) from the same parsed
1248
+ // result. Large Slack histories with many tool payloads would otherwise
1249
+ // pay a double JSON-parse cost per row.
1250
+ let contentBlocks: ContentBlock[] = [];
1251
+ let plainText: string;
1252
+ try {
1253
+ const parsed = JSON.parse(row.content);
1254
+ if (Array.isArray(parsed)) {
1255
+ contentBlocks = parsed as ContentBlock[];
1256
+ plainText = extractPlainTextFromBlocks(contentBlocks);
1257
+ } else if (typeof parsed === "string") {
1258
+ plainText = parsed;
1259
+ } else {
1260
+ plainText = row.content;
1261
+ }
1262
+ } catch {
1263
+ // Plain string row (legacy) — no structured blocks to preserve.
1264
+ plainText = row.content;
1265
+ }
1266
+
1267
+ // Attachment-only rows (images, files) carry no text block, so the
1268
+ // transcript renderer would normally emit them *without* a tag line —
1269
+ // the model sees the image but loses sender/timestamp attribution.
1270
+ // Synthesize a leading text block carrying the placeholder so the
1271
+ // renderer emits `[14:25 @alice]: [image]` and then the image itself.
1272
+ // Pure tool-only rows (tool_use / tool_result) are intentionally
1273
+ // excluded — those are synthetic turn continuations that should stay
1274
+ // tag-line-free, matching the documented behaviour in
1275
+ // `buildMessageContentBlocks`.
1276
+ const hasTextBlock = contentBlocks.some((b) => b?.type === "text");
1277
+ const hasAttachmentBlock = contentBlocks.some(
1278
+ (b) => b?.type === "image" || b?.type === "file",
1279
+ );
1280
+ if (!hasTextBlock && hasAttachmentBlock && plainText !== "") {
1281
+ contentBlocks = [{ type: "text", text: plainText }, ...contentBlocks];
1282
+ }
1283
+
1284
+ return {
1285
+ role: row.role,
1286
+ content: plainText,
1287
+ metadata: slackMeta,
1288
+ senderLabel,
1289
+ createdAt: row.createdAt,
1290
+ contentBlocks,
1291
+ };
1292
+ }
1293
+
1294
+ /**
1295
+ * Build a chronological Slack transcript for Slack conversations (both DMs
1296
+ * and group/channel/mpim) and project it onto the LLM-facing `Message[]`
1297
+ * shape.
1298
+ *
1299
+ * Returns `null` when the channel is not Slack (caller should fall through
1300
+ * to the default message history). Legacy pre-upgrade rows without
1301
+ * `slackMeta` are tolerated: the renderer's flat fallback orders them by
1302
+ * `createdAt` alongside post-upgrade rows.
1303
+ *
1304
+ * For ALL Slack conversations (channels and DMs), `<transport_hints>`
1305
+ * injection is suppressed by `applyRuntimeInjections` so the model sees
1306
+ * one consistent persisted view instead of a duplicated gateway hint.
1307
+ */
1308
+ export function assembleSlackChronologicalMessages(
1309
+ rows: SlackTranscriptInputRow[],
1310
+ capabilities: ChannelCapabilities,
1311
+ ): Message[] | null {
1312
+ if (capabilities.channel !== "slack") {
1313
+ return null;
1314
+ }
1315
+ const renderable = rows.map(rowToRenderable);
1316
+ return renderSlackTranscript(renderable);
1317
+ }
1318
+
1319
+ /**
1320
+ * Load DB rows for a Slack conversation and project them onto the
1321
+ * chronological transcript shape.
1322
+ *
1323
+ * Convenience wrapper over `getMessages` + `assembleSlackChronologicalMessages`.
1324
+ * The loader is exposed as a parameter so tests can substitute a stub. In
1325
+ * production it defaults to `getMessages` from `conversation-crud.ts`.
1326
+ *
1327
+ * When `trustClass` identifies an untrusted actor (guardian-scoped rows
1328
+ * must not leak into the model context), rows are passed through
1329
+ * `filterMessagesForUntrustedActor` before assembly — mirroring the
1330
+ * filtering applied in `loadFromDb` so the chronological transcript
1331
+ * respects the same per-actor scoping as the default history path.
1332
+ *
1333
+ * Returns `null` when the channel is not Slack — callers should fall
1334
+ * through to the default in-memory message history.
1335
+ */
1336
+ export function loadSlackChronologicalMessages(
1337
+ conversationId: string,
1338
+ capabilities: ChannelCapabilities,
1339
+ options: {
1340
+ loader?: (id: string) => MessageRow[];
1341
+ trustClass?: TrustClass;
1342
+ } = {},
1343
+ ): Message[] | null {
1344
+ if (capabilities.channel !== "slack") {
1345
+ return null;
1346
+ }
1347
+ const loader = options.loader ?? defaultGetMessages;
1348
+ const allRows = loader(conversationId);
1349
+ const scopedRows = isUntrustedTrustClass(options.trustClass)
1350
+ ? filterMessagesForUntrustedActor(allRows)
1351
+ : allRows;
1352
+ // Coerce MessageRow.role (string) to the structural row's stricter union.
1353
+ const rows: SlackTranscriptInputRow[] = scopedRows.map((row) => ({
1354
+ role: row.role === "assistant" ? "assistant" : "user",
1355
+ content: row.content,
1356
+ createdAt: row.createdAt,
1357
+ metadata: row.metadata,
1358
+ }));
1359
+ return assembleSlackChronologicalMessages(rows, capabilities);
1360
+ }
1361
+
1362
+ // ---------------------------------------------------------------------------
1363
+ // Active-thread focus block (non-persisted; appended to current user turn)
1364
+ // ---------------------------------------------------------------------------
1365
+
1366
+ /**
1367
+ * Detect the "active" Slack thread ts for the current turn.
1368
+ *
1369
+ * The active thread is the thread the current inbound user message belongs
1370
+ * to: scan from newest to oldest and return the `slackMeta.threadTs` of the
1371
+ * most recent user row that carries one. Returns `null` when no recent user
1372
+ * row sits inside a thread (e.g. the inbound was a top-level channel post,
1373
+ * or the conversation has no Slack-tagged user rows yet).
1374
+ *
1375
+ * Pure: takes pre-mapped renderable rows and returns the ts string only.
1376
+ */
1377
+ function detectActiveThreadTs(rows: RenderableSlackMessage[]): string | null {
1378
+ for (let i = rows.length - 1; i >= 0; i--) {
1379
+ const row = rows[i];
1380
+ if (row.role !== "user") continue;
1381
+ const meta = row.metadata;
1382
+ if (!meta) continue;
1383
+ if (meta.eventKind !== "message") continue;
1384
+ if (typeof meta.threadTs === "string" && meta.threadTs.length > 0) {
1385
+ return meta.threadTs;
1386
+ }
1387
+ // First non-thread user row wins: the inbound is top-level, no active
1388
+ // thread to focus on.
1389
+ return null;
1390
+ }
1391
+ return null;
1392
+ }
1393
+
1394
+ /**
1395
+ * Build a focus block listing every message belonging to the active thread:
1396
+ * the parent (whose `channelTs` equals `activeThreadTs`) plus every reply
1397
+ * (whose `threadTs` equals `activeThreadTs`). Reactions targeting any of
1398
+ * those messages are also pulled in via their `targetChannelTs`. Edits and
1399
+ * deletions surface through the existing renderer markers.
1400
+ *
1401
+ * Returns `null` when no rows match (e.g. parent backfill hasn't run yet
1402
+ * AND the thread has no replies in storage either) so the caller can skip
1403
+ * the empty block. Otherwise returns the rendered XML block ready to append
1404
+ * to the user's tail message.
1405
+ *
1406
+ * Pure: takes pre-mapped renderable rows + a thread ts, returns text only.
1407
+ */
1408
+ function buildActiveThreadBlockFromRenderable(
1409
+ rows: RenderableSlackMessage[],
1410
+ activeThreadTs: string,
1411
+ ): string | null {
1412
+ const members: RenderableSlackMessage[] = [];
1413
+ for (const row of rows) {
1414
+ const meta = row.metadata;
1415
+ if (!meta) continue;
1416
+ if (meta.eventKind === "message") {
1417
+ if (
1418
+ meta.channelTs === activeThreadTs ||
1419
+ meta.threadTs === activeThreadTs
1420
+ ) {
1421
+ members.push(row);
1422
+ }
1423
+ continue;
1424
+ }
1425
+ if (
1426
+ meta.eventKind === "reaction" &&
1427
+ meta.reaction &&
1428
+ meta.reaction.targetChannelTs === activeThreadTs
1429
+ ) {
1430
+ members.push(row);
1431
+ continue;
1432
+ }
1433
+ // Reactions targeting a reply within the thread also belong in the
1434
+ // focus block — collect them by checking the reaction target against
1435
+ // any thread reply's channelTs we've already accepted. We do this in a
1436
+ // second pass below to avoid an O(n^2) inner scan here.
1437
+ }
1438
+
1439
+ // Second pass: pull in reactions whose target is one of the already-
1440
+ // collected reply messages. Using a Set keeps this O(n).
1441
+ const memberChannelTs = new Set(
1442
+ members
1443
+ .map((m) => m.metadata?.channelTs)
1444
+ .filter((v): v is string => typeof v === "string"),
1445
+ );
1446
+ for (const row of rows) {
1447
+ const meta = row.metadata;
1448
+ if (!meta || meta.eventKind !== "reaction" || !meta.reaction) continue;
1449
+ if (meta.reaction.targetChannelTs === activeThreadTs) continue; // already added
1450
+ if (memberChannelTs.has(meta.reaction.targetChannelTs)) {
1451
+ members.push(row);
1452
+ }
1453
+ }
1454
+
1455
+ if (members.length === 0) return null;
1456
+
1457
+ // The active-thread block is flattened to plain text below, which discards
1458
+ // `Message.role`. Assistant rows are relabeled in the post-render step:
1459
+ // `renderSlackTranscript` emits assistant content with no tag-line wrapper
1460
+ // (to prevent the model mimicking `[MM/DD/YY HH:MM]:` prefixes in outbound
1461
+ // replies), so we prepend an explicit `@assistant:` label to the flattened
1462
+ // line. Unnamed user rows (no real Slack displayName) get a `@user`
1463
+ // senderLabel here so their tag line carries attribution through the
1464
+ // renderer. Labeled user rows and assistant rows pass through unchanged.
1465
+ const labeledMembers = members.map((m) => {
1466
+ if (m.role === "assistant") return m;
1467
+ if (m.senderLabel !== null) return m;
1468
+ return { ...m, senderLabel: "@user" };
1469
+ });
1470
+
1471
+ const rendered = renderSlackTranscript(labeledMembers);
1472
+ if (rendered.length === 0) return null;
1473
+ // Reaction / overflow-trailer lines already embed `@assistant` inline, so
1474
+ // `isReactionTagLine` is used to skip those and avoid double-attribution
1475
+ // (`@assistant: [... @assistant reacted ...]`). Regular content and the
1476
+ // `[deleted]` sentinel get the prefix so attribution survives flattening.
1477
+ const lines = rendered
1478
+ .map((msg) => {
1479
+ const text = extractTagLineTexts([msg])[0] ?? "";
1480
+ return msg.role === "assistant" && !isReactionTagLine(text)
1481
+ ? `@assistant: ${text}`
1482
+ : text;
1483
+ })
1484
+ .join("\n");
1485
+ return `<active_thread>\n${lines}\n</active_thread>`;
1486
+ }
1487
+
1488
+ /**
1489
+ * Build the Slack active-thread focus block from raw rows.
1490
+ *
1491
+ * Pure assembly entrypoint mirroring `assembleSlackChronologicalMessages`.
1492
+ * Returns the rendered `<active_thread>` block as a string, or `null` when:
1493
+ * - the channel is not Slack, OR
1494
+ * - the channel is a Slack DM (DMs do not have threads), OR
1495
+ * - the latest user row is top-level (not in a thread), OR
1496
+ * - no rows belong to the active thread.
1497
+ */
1498
+ export function assembleSlackActiveThreadFocusBlock(
1499
+ rows: SlackTranscriptInputRow[],
1500
+ capabilities: ChannelCapabilities,
1501
+ ): string | null {
1502
+ if (capabilities.channel !== "slack") return null;
1503
+ // DMs do not have threads, so the focus block is always a no-op.
1504
+ // The gateway sets `chatType: "channel"` for every non-DM Slack
1505
+ // conversation and omits the field for DMs, so gate the focus block
1506
+ // on the positive `"channel"` match.
1507
+ if (capabilities.chatType !== "channel") return null;
1508
+ const renderable = rows.map(rowToRenderable);
1509
+ const activeThreadTs = detectActiveThreadTs(renderable);
1510
+ if (!activeThreadTs) return null;
1511
+ return buildActiveThreadBlockFromRenderable(renderable, activeThreadTs);
1512
+ }
1513
+
1514
+ /**
1515
+ * Loader convenience over `assembleSlackActiveThreadFocusBlock` mirroring
1516
+ * `loadSlackChronologicalMessages`. Returns `null` when the channel is not
1517
+ * Slack, or when it is a Slack DM (DMs have no threads), so callers can
1518
+ * skip the injection entirely without paying for a DB read.
1519
+ */
1520
+ export function loadSlackActiveThreadFocusBlock(
1521
+ conversationId: string,
1522
+ capabilities: ChannelCapabilities,
1523
+ options: {
1524
+ loader?: (id: string) => MessageRow[];
1525
+ trustClass?: TrustClass;
1526
+ } = {},
1527
+ ): string | null {
1528
+ if (capabilities.channel !== "slack") return null;
1529
+ if (capabilities.chatType !== "channel") return null;
1530
+ const loader = options.loader ?? defaultGetMessages;
1531
+ const allRows = loader(conversationId);
1532
+ const scopedRows = isUntrustedTrustClass(options.trustClass)
1533
+ ? filterMessagesForUntrustedActor(allRows)
1534
+ : allRows;
1535
+ const rows: SlackTranscriptInputRow[] = scopedRows.map((row) => ({
1536
+ role: row.role === "assistant" ? "assistant" : "user",
1537
+ content: row.content,
1538
+ createdAt: row.createdAt,
1539
+ metadata: row.metadata,
1540
+ }));
1541
+ return assembleSlackActiveThreadFocusBlock(rows, capabilities);
1542
+ }
1543
+
1141
1544
  /** Prefixes stripped by the pipeline (order doesn't matter — single pass). */
1142
1545
  const RUNTIME_INJECTION_PREFIXES = [
1143
1546
  "<channel_capabilities>",
@@ -1164,11 +1567,18 @@ const RUNTIME_INJECTION_PREFIXES = [
1164
1567
  "<active_workspace>",
1165
1568
  "<active_dynamic_page>",
1166
1569
  "<non_interactive_context>",
1167
- "<NOW.md Always keep this up to date>",
1570
+ // Shared prefix catches both the current NOW.md tag and any pre-line-limit
1571
+ // variant that may linger in in-flight histories during a rolling deploy.
1572
+ "<NOW.md Always keep this up to date",
1168
1573
  "<now_scratchpad>", // backward-compat: strip legacy blocks from pre-rename history
1169
- "<pkb>",
1574
+ "<knowledge_base>",
1575
+ "<pkb>", // backward-compat: strip legacy tag from pre-rename history
1170
1576
  "<system_reminder>",
1171
1577
  "<transport_hints>",
1578
+ // The Slack active-thread focus block is non-persisted and injected on
1579
+ // the FINAL user turn only. Strip it here so re-assembly during compaction
1580
+ // and overflow recovery does not duplicate it across turns.
1581
+ "<active_thread>",
1172
1582
  "<system_notice>One or more tool calls returned an error.",
1173
1583
  ];
1174
1584
 
@@ -1189,16 +1599,23 @@ export function stripInjectionsForCompaction(messages: Message[]): Message[] {
1189
1599
  * Returns null if no NOW.md injection is found.
1190
1600
  */
1191
1601
  export function findLastInjectedNowContent(messages: Message[]): string | null {
1192
- const prefix = "<NOW.md Always keep this up to date>\n";
1602
+ // Matches every NOW.md opening tag we emit (the tag text may evolve over
1603
+ // time, e.g. adding a line-limit hint), so in-flight histories with older
1604
+ // tag variants remain discoverable during a rolling deploy.
1605
+ const openTagPrefix = "<NOW.md Always keep this up to date";
1193
1606
  const suffix = "\n</NOW.md>";
1194
1607
  for (let i = messages.length - 1; i >= 0; i--) {
1195
1608
  const msg = messages[i];
1196
1609
  if (msg.role !== "user") continue;
1197
1610
  for (const block of msg.content) {
1198
- if (block.type === "text" && block.text.startsWith(prefix)) {
1199
- const end = block.text.lastIndexOf(suffix);
1200
- if (end > prefix.length) return block.text.slice(prefix.length, end);
1611
+ if (block.type !== "text" || !block.text.startsWith(openTagPrefix)) {
1612
+ continue;
1201
1613
  }
1614
+ const tagEnd = block.text.indexOf(">\n");
1615
+ if (tagEnd < 0) continue;
1616
+ const contentStart = tagEnd + ">\n".length;
1617
+ const end = block.text.lastIndexOf(suffix);
1618
+ if (end > contentStart) return block.text.slice(contentStart, end);
1202
1619
  }
1203
1620
  }
1204
1621
  return null;
@@ -1216,76 +1633,519 @@ export function findLastInjectedNowContent(messages: Message[]): string | null {
1216
1633
  export type InjectionMode = "full" | "minimal";
1217
1634
 
1218
1635
  /**
1219
- * Apply a chain of user-message injections to `runMessages`.
1636
+ * Per-turn injection bytes captured so `loadFromDb` can rehydrate historical
1637
+ * user messages byte-for-byte after a daemon restart or conversation
1638
+ * eviction. Persisting the exact injected text onto message metadata keeps
1639
+ * Anthropic's prefix cache anchored to msg[0] instead of invalidating every
1640
+ * turn on reload. Any field left `undefined` means that block was not
1641
+ * injected on this turn.
1642
+ */
1643
+ export interface RuntimeInjectionBlocks {
1644
+ unifiedTurnContext?: string;
1645
+ pkbSystemReminder?: string;
1646
+ workspaceBlock?: string;
1647
+ nowScratchpadBlock?: string;
1648
+ pkbContextBlock?: string;
1649
+ /**
1650
+ * Composed output of every plugin-registered {@link Injector}, concatenated
1651
+ * in ascending `order`. Empty string when every injector opted out (returned
1652
+ * `null`). Today the default injectors (`default-injectors` plugin)
1653
+ * placeholder-return `null`, so this is only non-empty when a third-party
1654
+ * plugin registers an injector that emits content.
1655
+ *
1656
+ * Populated by {@link composeInjectorChain} during
1657
+ * {@link applyRuntimeInjections}. Distinct from the other `blocks` fields
1658
+ * because those track specific hardcoded injections today; this field is
1659
+ * the extensibility seam for {@link Injector} plugins.
1660
+ */
1661
+ injectorChainBlock?: string;
1662
+ }
1663
+
1664
+ export interface RuntimeInjectionResult {
1665
+ messages: Message[];
1666
+ blocks: RuntimeInjectionBlocks;
1667
+ }
1668
+
1669
+ /**
1670
+ * Run every registered {@link Injector}'s `produce()` in ascending `order`
1671
+ * and return every non-null block the chain produced.
1672
+ *
1673
+ * Injectors returning `null` are omitted from the result. The returned array
1674
+ * preserves ascending-`order` sort so downstream callers (notably
1675
+ * {@link applyRuntimeInjections}) can group blocks by `placement` and apply
1676
+ * them declaratively without losing per-injector ordering within each slot.
1677
+ */
1678
+ export async function collectInjectorBlocks(
1679
+ ctx: TurnContext,
1680
+ ): Promise<InjectionBlock[]> {
1681
+ const injectors = getInjectors();
1682
+ if (injectors.length === 0) return [];
1683
+ const out: InjectionBlock[] = [];
1684
+ for (const injector of injectors) {
1685
+ const block = await injector.produce(ctx);
1686
+ if (block) out.push(block);
1687
+ }
1688
+ return out;
1689
+ }
1690
+
1691
+ /**
1692
+ * Run every registered {@link Injector}'s `produce()` in ascending
1693
+ * `order`, concatenate the non-null results into a single block of text,
1694
+ * and return it.
1695
+ *
1696
+ * Separator: blank line between blocks. Injectors returning `null` are
1697
+ * skipped entirely (no leading/trailing blank lines). When no injector
1698
+ * contributes, the function returns an empty string.
1220
1699
  *
1221
- * Each injection is optional pass `null`/`undefined` to skip it.
1222
- * Returns the final message array ready for the provider.
1700
+ * Used by tests that assert the concatenation contract and by callers that
1701
+ * want a single informational string view of the chain. The canonical
1702
+ * integration point is {@link applyRuntimeInjections}, which uses
1703
+ * {@link collectInjectorBlocks} + placement-aware application to splice
1704
+ * each block into the per-turn message array.
1223
1705
  */
1224
- export function applyRuntimeInjections(
1706
+ export async function composeInjectorChain(ctx: TurnContext): Promise<string> {
1707
+ const blocks = await collectInjectorBlocks(ctx);
1708
+ const pieces: string[] = [];
1709
+ for (const block of blocks) {
1710
+ if (block.text.length > 0) pieces.push(block.text);
1711
+ }
1712
+ return pieces.join("\n\n");
1713
+ }
1714
+
1715
+ /**
1716
+ * Default block placement. Kept in sync with {@link InjectionBlock} so
1717
+ * blocks produced without an explicit `placement` (e.g. third-party
1718
+ * injectors written against the pre-G2.1 API) behave predictably.
1719
+ */
1720
+ const DEFAULT_PLACEMENT: InjectionPlacement = "append-user-tail";
1721
+
1722
+ /**
1723
+ * Count leading memory-prefix blocks on a user message's `content`.
1724
+ *
1725
+ * Delegates to {@link countMemoryPrefixBlocks} from
1726
+ * `memory/graph/conversation-graph-memory.js` — the same state-machine the
1727
+ * pre-migration PKB-reminder branch used to find its splice point. The
1728
+ * pre-migration `injectPkbContext` and `injectNowScratchpad` helpers used
1729
+ * slightly simpler rules inline; reusing the canonical counter here
1730
+ * collapses the three near-identical splice rules into one source of truth
1731
+ * so the ordering of PKB-context / PKB-reminder / NOW blocks relative to
1732
+ * any memory prefix is stable and testable. For the common case (just
1733
+ * `<memory __injected>` text, no images), the output is byte-identical to
1734
+ * the pre-migration helpers.
1735
+ */
1736
+ function countMemoryPrefixBlocksOnContent(content: ContentBlock[]): number {
1737
+ return countMemoryPrefixBlocks(content);
1738
+ }
1739
+
1740
+ /**
1741
+ * Apply one injector block to a `runMessages` array according to its
1742
+ * declared {@link InjectionPlacement}.
1743
+ *
1744
+ * Preserves the byte-for-byte positional semantics of the pre-migration
1745
+ * `inject*` helpers:
1746
+ * - `"prepend-user-tail"` — prepend to the tail user message's content.
1747
+ * - `"append-user-tail"` — append to the tail user message's content.
1748
+ * - `"after-memory-prefix"` — splice immediately after any leading memory
1749
+ * prefix blocks (mirrors `injectPkbContext` / `injectNowScratchpad`).
1750
+ * - `"replace-run-messages"` — replace `runMessages` wholesale with
1751
+ * `block.messagesOverride`.
1752
+ *
1753
+ * Blocks with empty `text` on non-replace placements are no-ops (the
1754
+ * pre-migration branches also short-circuited on empty strings).
1755
+ */
1756
+ function applyInjectionBlock(
1225
1757
  runMessages: Message[],
1226
- options: {
1227
- activeSurface?: ActiveSurfaceContext | null;
1228
- workspaceTopLevelContext?: string | null;
1229
- channelCapabilities?: ChannelCapabilities | null;
1230
- channelCommandContext?: ChannelCommandContext | null;
1231
- unifiedTurnContext?: string | null;
1232
- voiceCallControlPrompt?: string | null;
1233
- pkbContext?: string | null;
1234
- pkbActive?: boolean;
1235
- nowScratchpad?: string | null;
1236
- subagentStatusBlock?: string | null;
1237
- isNonInteractive?: boolean;
1238
- transportHints?: string[] | null;
1239
- mode?: InjectionMode;
1240
- },
1758
+ block: InjectionBlock,
1241
1759
  ): Message[] {
1242
- const mode = options.mode ?? "full";
1243
- let result = runMessages;
1760
+ const placement = block.placement ?? DEFAULT_PLACEMENT;
1244
1761
 
1245
- // For non-interactive conversations (scheduled jobs, work items), instruct the
1246
- // model to never ask for clarification — there is no human present to answer.
1247
- if (options.isNonInteractive) {
1248
- const userTail = result[result.length - 1];
1249
- if (userTail && userTail.role === "user") {
1250
- result = [
1251
- ...result.slice(0, -1),
1762
+ if (placement === "replace-run-messages") {
1763
+ if (!block.messagesOverride) return runMessages;
1764
+ return block.messagesOverride;
1765
+ }
1766
+
1767
+ if (block.text.length === 0) return runMessages;
1768
+
1769
+ const userTail = runMessages[runMessages.length - 1];
1770
+ if (!userTail || userTail.role !== "user") return runMessages;
1771
+
1772
+ const textBlock = { type: "text" as const, text: block.text };
1773
+
1774
+ switch (placement) {
1775
+ case "prepend-user-tail":
1776
+ return [
1777
+ ...runMessages.slice(0, -1),
1778
+ { ...userTail, content: [textBlock, ...userTail.content] },
1779
+ ];
1780
+ case "append-user-tail":
1781
+ return [
1782
+ ...runMessages.slice(0, -1),
1783
+ { ...userTail, content: [...userTail.content, textBlock] },
1784
+ ];
1785
+ case "after-memory-prefix": {
1786
+ const memoryPrefixCount = countMemoryPrefixBlocksOnContent(
1787
+ userTail.content,
1788
+ );
1789
+ return [
1790
+ ...runMessages.slice(0, -1),
1252
1791
  {
1253
1792
  ...userTail,
1254
1793
  content: [
1255
- ...userTail.content,
1256
- {
1257
- type: "text" as const,
1258
- text: "<non_interactive_context>\nNon-interactive scheduled task — do not ask for clarification or confirmation. Follow the instructions exactly using your best judgment. If recalled memory contains conflicting notes, prefer the explicit instruction in this message.\n</non_interactive_context>",
1259
- },
1794
+ ...userTail.content.slice(0, memoryPrefixCount),
1795
+ textBlock,
1796
+ ...userTail.content.slice(memoryPrefixCount),
1260
1797
  ],
1261
1798
  },
1262
1799
  ];
1263
1800
  }
1264
1801
  }
1802
+ }
1265
1803
 
1266
- if (options.voiceCallControlPrompt) {
1267
- const userTail = result[result.length - 1];
1268
- if (userTail && userTail.role === "user") {
1269
- result = [
1270
- ...result.slice(0, -1),
1271
- injectVoiceCallControlContext(userTail, options.voiceCallControlPrompt),
1272
- ];
1804
+ /**
1805
+ * Per-turn options accepted by {@link applyRuntimeInjections}.
1806
+ *
1807
+ * Most fields flow through to the per-injector {@link TurnInjectionInputs}
1808
+ * bag attached to the {@link TurnContext} the caller provides (or to an
1809
+ * ephemeral {@link TurnContext} synthesized for test call sites). A small
1810
+ * number of fields drive hardcoded branches that live outside the injector
1811
+ * chain — `activeSurface`, `channelCapabilities`, `channelCommandContext`,
1812
+ * `voiceCallControlPrompt`, `transportHints`, and `isNonInteractive` —
1813
+ * because they are orchestrator-owned content that never made sense as
1814
+ * plugin-overridable default injectors.
1815
+ */
1816
+ export interface RuntimeInjectionOptions {
1817
+ /**
1818
+ * Active dashboard-surface context (read from `<active_workspace>`). Kept
1819
+ * on the options bag rather than an injector because it is a
1820
+ * channel-capability concern that has never been gated as a default
1821
+ * injector.
1822
+ */
1823
+ activeSurface?: ActiveSurfaceContext | null;
1824
+ workspaceTopLevelContext?: string | null;
1825
+ channelCapabilities?: ChannelCapabilities | null;
1826
+ channelCommandContext?: ChannelCommandContext | null;
1827
+ unifiedTurnContext?: string | null;
1828
+ voiceCallControlPrompt?: string | null;
1829
+ pkbContext?: string | null;
1830
+ pkbActive?: boolean;
1831
+ /**
1832
+ * Dense query vector surfaced from the graph memory retriever.
1833
+ * When present together with `pkbActive`, used to run `searchPkbFiles`
1834
+ * to surface relevance hints in the PKB system reminder. When missing,
1835
+ * the reminder falls back to the flat static text.
1836
+ */
1837
+ pkbQueryVector?: number[];
1838
+ /** Optional sparse vector accompanying `pkbQueryVector`. */
1839
+ pkbSparseVector?: QdrantSparseVector;
1840
+ /** Memory scope id used to filter PKB search results. */
1841
+ pkbScopeId?: string;
1842
+ /**
1843
+ * The live conversation (or a minimal shape containing `messages`) used
1844
+ * to compute which PKB paths are already "in context" and therefore
1845
+ * suppressed from hint suggestions.
1846
+ */
1847
+ pkbConversation?: PkbContextConversation;
1848
+ /** Auto-injected PKB filenames (resolved relative to `pkbRoot`). */
1849
+ pkbAutoInjectList?: string[];
1850
+ /** Absolute path to the PKB directory (e.g. `<workspace>/pkb`). */
1851
+ pkbRoot?: string;
1852
+ /**
1853
+ * Working directory against which relative `file_read` tool paths
1854
+ * resolve, used to detect workspace-relative reads like
1855
+ * `pkb/threads.md`. Falls back to `pkbRoot` when omitted.
1856
+ */
1857
+ pkbWorkingDir?: string;
1858
+ nowScratchpad?: string | null;
1859
+ subagentStatusBlock?: string | null;
1860
+ isNonInteractive?: boolean;
1861
+ transportHints?: string[] | null;
1862
+ /**
1863
+ * Pre-rendered Slack chronological transcript that replaces the
1864
+ * default `runMessages` history for any Slack conversation (channels
1865
+ * and DMs alike).
1866
+ *
1867
+ * When `channelCapabilities` describes a Slack conversation and this
1868
+ * array is non-empty, the `slack-messages` default injector emits a
1869
+ * `replace-run-messages` block that swaps `runMessages` with this
1870
+ * transcript. Channel renders include sibling-thread tags; DM renders
1871
+ * are flat (DMs have no threads). The `transportHints` pipeline is
1872
+ * skipped for any Slack conversation so the persisted view isn't
1873
+ * duplicated by gateway-side hints.
1874
+ *
1875
+ * Callers build this via `loadSlackChronologicalMessages` (or the
1876
+ * underlying `assembleSlackChronologicalMessages`) before invoking
1877
+ * this function so the assembly path stays free of direct DB calls
1878
+ * and remains easy to test.
1879
+ */
1880
+ slackChronologicalMessages?: Message[] | null;
1881
+ /**
1882
+ * Pre-rendered `<active_thread>` focus block listing the messages of
1883
+ * the thread the current inbound user message belongs to.
1884
+ *
1885
+ * Appended to the FINAL user message ONLY when `channelCapabilities`
1886
+ * describes a Slack non-DM channel. The block is non-persisted: history
1887
+ * rebuilds re-derive it from storage on each turn, and
1888
+ * `RUNTIME_INJECTION_PREFIXES` strips any `<active_thread>` blocks from
1889
+ * prior turns so they do not accumulate.
1890
+ *
1891
+ * Callers build this via `loadSlackActiveThreadFocusBlock` (or the
1892
+ * underlying `assembleSlackActiveThreadFocusBlock`). Pass `null` /
1893
+ * `undefined` when the inbound is a top-level (non-thread) post.
1894
+ */
1895
+ slackActiveThreadFocusBlock?: string | null;
1896
+ mode?: InjectionMode;
1897
+ /**
1898
+ * Per-turn {@link TurnContext} forwarded to plugin-registered
1899
+ * {@link Injector}s via {@link collectInjectorBlocks}. When omitted,
1900
+ * `applyRuntimeInjections` synthesizes an ephemeral context (with a
1901
+ * fallback `trust` classification) so the default-injector chain still
1902
+ * runs — call sites that build the options bag without holding a full
1903
+ * `TurnContext` get the same chain output.
1904
+ *
1905
+ * When provided, the caller's `trust`, `conversationId`, `turnIndex`,
1906
+ * etc. are preserved; the function layers its per-turn
1907
+ * {@link TurnInjectionInputs} onto a shallow clone so the caller's
1908
+ * `TurnContext` is not mutated.
1909
+ */
1910
+ turnContext?: TurnContext;
1911
+ }
1912
+
1913
+ /**
1914
+ * Build the {@link TurnInjectionInputs} bag from the options bag.
1915
+ *
1916
+ * Exposed so callers that already hold a {@link TurnContext} can layer the
1917
+ * same per-turn inputs onto it before handing control to
1918
+ * {@link collectInjectorBlocks} directly — useful for tests and for the
1919
+ * overflow-reducer reinject path.
1920
+ */
1921
+ export function buildTurnInjectionInputs(
1922
+ options: RuntimeInjectionOptions,
1923
+ ): TurnInjectionInputs {
1924
+ return {
1925
+ mode: options.mode,
1926
+ workspaceTopLevelContext: options.workspaceTopLevelContext,
1927
+ unifiedTurnContext: options.unifiedTurnContext,
1928
+ pkbContext: options.pkbContext,
1929
+ pkbActive: options.pkbActive,
1930
+ pkbQueryVector: options.pkbQueryVector,
1931
+ pkbSparseVector: options.pkbSparseVector,
1932
+ pkbScopeId: options.pkbScopeId,
1933
+ pkbConversation: options.pkbConversation,
1934
+ pkbAutoInjectList: options.pkbAutoInjectList,
1935
+ pkbRoot: options.pkbRoot,
1936
+ pkbWorkingDir: options.pkbWorkingDir,
1937
+ nowScratchpad: options.nowScratchpad,
1938
+ subagentStatusBlock: options.subagentStatusBlock,
1939
+ channelCapabilities: options.channelCapabilities,
1940
+ slackChronologicalMessages: options.slackChronologicalMessages,
1941
+ slackActiveThreadFocusBlock: options.slackActiveThreadFocusBlock,
1942
+ activeSurface: options.activeSurface,
1943
+ channelCommandContext: options.channelCommandContext,
1944
+ voiceCallControlPrompt: options.voiceCallControlPrompt,
1945
+ transportHints: options.transportHints,
1946
+ isNonInteractive: options.isNonInteractive,
1947
+ };
1948
+ }
1949
+
1950
+ /** Minimal synthetic TurnContext used when the caller omits one. */
1951
+ function synthesizeFallbackTurnContext(
1952
+ inputs: TurnInjectionInputs,
1953
+ ): TurnContext {
1954
+ return {
1955
+ requestId: "runtime-assembly-fallback",
1956
+ conversationId: "runtime-assembly-fallback",
1957
+ turnIndex: 0,
1958
+ trust: {
1959
+ sourceChannel: inputs.channelCapabilities?.channel
1960
+ ? (inputs.channelCapabilities.channel as TrustContext["sourceChannel"])
1961
+ : "vellum",
1962
+ trustClass: "unknown",
1963
+ },
1964
+ injectionInputs: inputs,
1965
+ };
1966
+ }
1967
+
1968
+ /**
1969
+ * Apply the runtime-injection chain to `runMessages`.
1970
+ *
1971
+ * The canonical per-turn assembly pipeline for every provider call:
1972
+ *
1973
+ * 1. Build the per-turn {@link TurnInjectionInputs} bag from `options`.
1974
+ * 2. Layer it onto a {@link TurnContext} — either the one the caller
1975
+ * supplies via `options.turnContext` (preserving its `requestId`,
1976
+ * trust, and other fields) or an ephemeral fallback synthesized here.
1977
+ * 3. Drive the default + third-party {@link Injector} chain via
1978
+ * {@link collectInjectorBlocks}.
1979
+ * 4. Apply the chain's `"replace-run-messages"` block (Slack chronological
1980
+ * transcript) first so subsequent branches operate on the replaced
1981
+ * tail. When replacement fires, re-prepend any memory-prefix blocks
1982
+ * that `graphMemory.prepareMemory` had attached to the original tail —
1983
+ * the Slack transcript is rendered fresh from persisted rows and
1984
+ * carries no memory prefix of its own.
1985
+ * 5. Apply the chain's `"after-memory-prefix"` blocks in ascending
1986
+ * `order`. This runs BEFORE step 6's hardcoded prepends so the
1987
+ * memory-prefix counter sees only the memory blocks on the tail —
1988
+ * any `<channel_capabilities>` / `<channel_command_context>` /
1989
+ * `<transport_hints>` prepended first would push the count to zero
1990
+ * and force PKB / NOW to splice at the top of the tail. Within the
1991
+ * after-memory block, each successive splice lands at the memory
1992
+ * boundary, pushing earlier splices further from memory — so
1993
+ * higher-`order` blocks end up closer to the memory prefix.
1994
+ * 6. Run the remaining hardcoded branches (`isNonInteractive`,
1995
+ * `voiceCallControlPrompt`, `activeSurface`, `channelCapabilities`,
1996
+ * `channelCommandContext`, `transportHints`) in their historical order.
1997
+ * 7. Finally, apply the chain's remaining blocks by placement:
1998
+ * `"append-user-tail"` in ascending `order`, then `"prepend-user-tail"`
1999
+ * in descending `order` so the lowest-`order` prepend lands topmost in
2000
+ * the user tail content.
2001
+ *
2002
+ * Returns the final message array plus a `blocks` object holding the exact
2003
+ * injected text for each captured block — callers persist those bytes to
2004
+ * message metadata for later byte-exact rehydration.
2005
+ */
2006
+ export async function applyRuntimeInjections(
2007
+ runMessages: Message[],
2008
+ options: RuntimeInjectionOptions,
2009
+ ): Promise<RuntimeInjectionResult> {
2010
+ const mode = options.mode ?? "full";
2011
+ const slackConversation = options.channelCapabilities?.channel === "slack";
2012
+
2013
+ // Build the per-injector inputs and attach them to the caller's
2014
+ // TurnContext (without mutating it). When the caller didn't supply one,
2015
+ // synthesize a minimal fallback so the chain still runs — test call sites
2016
+ // that drive injection via `options` without constructing a full context
2017
+ // continue to work.
2018
+ const injectionInputs = buildTurnInjectionInputs(options);
2019
+ const turnCtx: TurnContext = options.turnContext
2020
+ ? { ...options.turnContext, injectionInputs }
2021
+ : synthesizeFallbackTurnContext(injectionInputs);
2022
+
2023
+ const chainBlocks = await collectInjectorBlocks(turnCtx);
2024
+
2025
+ // Split the chain output by placement so the downstream assembly can
2026
+ // process each slot with the correct ordering rule.
2027
+ const prepends: InjectionBlock[] = [];
2028
+ const appends: InjectionBlock[] = [];
2029
+ const afterMemory: InjectionBlock[] = [];
2030
+ let replaceBlock: InjectionBlock | null = null;
2031
+ for (const block of chainBlocks) {
2032
+ switch (block.placement ?? "append-user-tail") {
2033
+ case "replace-run-messages":
2034
+ // Later replace-run-messages blocks would overwrite earlier ones;
2035
+ // the default chain only registers one (the Slack transcript).
2036
+ replaceBlock = block;
2037
+ break;
2038
+ case "after-memory-prefix":
2039
+ afterMemory.push(block);
2040
+ break;
2041
+ case "prepend-user-tail":
2042
+ prepends.push(block);
2043
+ break;
2044
+ case "append-user-tail":
2045
+ appends.push(block);
2046
+ break;
1273
2047
  }
1274
2048
  }
1275
2049
 
1276
- if (mode === "full" && options.pkbContext) {
1277
- const userTail = result[result.length - 1];
1278
- if (userTail && userTail.role === "user") {
1279
- result = [
1280
- ...result.slice(0, -1),
1281
- injectPkbContext(userTail, options.pkbContext),
1282
- ];
2050
+ // Track captured text for metadata persistence. Each field corresponds
2051
+ // to a specific default-injector block id so the loop below can pick up
2052
+ // the right capture without re-rendering.
2053
+ //
2054
+ // The capture is gated on the tail actually being a user message — if it
2055
+ // isn't, `applyInjectionBlock` no-ops the block and no content is actually
2056
+ // injected, so the persisted metadata must be undefined (matches
2057
+ // pre-migration behaviour where the `inject*` helpers short-circuited the
2058
+ // same way).
2059
+ let turnContextCaptured: string | undefined;
2060
+ let workspaceCaptured: string | undefined;
2061
+ let nowScratchpadCaptured: string | undefined;
2062
+ let pkbContextCaptured: string | undefined;
2063
+ let pkbSystemReminderCaptured: string | undefined;
2064
+ const initialTail = runMessages[runMessages.length - 1];
2065
+ const initialTailIsUser = !!initialTail && initialTail.role === "user";
2066
+ if (initialTailIsUser) {
2067
+ for (const block of chainBlocks) {
2068
+ switch (block.id) {
2069
+ case "unified-turn-context":
2070
+ turnContextCaptured = block.text;
2071
+ break;
2072
+ case "workspace-context":
2073
+ workspaceCaptured = block.text;
2074
+ break;
2075
+ case "now-md":
2076
+ nowScratchpadCaptured = block.text;
2077
+ break;
2078
+ case "pkb-context":
2079
+ pkbContextCaptured = block.text;
2080
+ break;
2081
+ case "pkb-reminder":
2082
+ pkbSystemReminderCaptured = block.text;
2083
+ break;
2084
+ }
2085
+ }
2086
+ }
2087
+
2088
+ // Compose the block text into a single informational string for
2089
+ // `injectorChainBlock`. Matches the pre-migration behaviour where the
2090
+ // field captured the composed view of every third-party injector on
2091
+ // the turn. We include default injectors here too so downstream
2092
+ // observers see the full set.
2093
+ const injectorChainPieces: string[] = [];
2094
+ for (const block of chainBlocks) {
2095
+ if (block.text.length > 0) injectorChainPieces.push(block.text);
2096
+ }
2097
+ const injectorChainBlock =
2098
+ injectorChainPieces.length > 0
2099
+ ? injectorChainPieces.join("\n\n")
2100
+ : undefined;
2101
+
2102
+ let result = runMessages;
2103
+
2104
+ // ── Step 1: Slack chronological replacement (chain "replace" block) ──
2105
+ if (replaceBlock && replaceBlock.messagesOverride) {
2106
+ // `graphMemory.prepareMemory` prepends a `<memory __injected>` block
2107
+ // (and any memory-image groups) to the last user message before
2108
+ // runtime assembly runs. The Slack transcript is freshly rendered
2109
+ // from persisted rows and has no such prefix, so swap it in and then
2110
+ // re-prepend the captured prefix onto the new tail user message.
2111
+ const carriedMemoryBlocks = extractMemoryPrefixBlocks(runMessages);
2112
+ result = replaceBlock.messagesOverride;
2113
+ if (carriedMemoryBlocks.length > 0) {
2114
+ const slackTail = result[result.length - 1];
2115
+ if (slackTail && slackTail.role === "user") {
2116
+ result = [
2117
+ ...result.slice(0, -1),
2118
+ {
2119
+ ...slackTail,
2120
+ content: [...carriedMemoryBlocks, ...slackTail.content],
2121
+ },
2122
+ ];
2123
+ }
1283
2124
  }
1284
2125
  }
1285
2126
 
1286
- // PKB behavioral nudge injected on every turn when PKB is active so
1287
- // the model keeps reading topic files and calling `remember`.
1288
- if (mode === "full" && options.pkbActive) {
2127
+ // ── Step 2: after-memory-prefix chain blocks ──
2128
+ // These splice relative to the memory-prefix count on the tail content,
2129
+ // so they must run BEFORE the hardcoded prepends in step 3. Otherwise
2130
+ // any prepended `<channel_capabilities>` / `<channel_command_context>` /
2131
+ // `<transport_hints>` (none of which are memory-prefix blocks) would
2132
+ // drop the count to 0 and PKB / NOW would splice at the very top of
2133
+ // the tail instead of immediately after memory.
2134
+ //
2135
+ // Ascending `order`: each splice lands at the memory-prefix boundary,
2136
+ // pushing any previously-spliced block one slot further from memory.
2137
+ // So higher-`order` blocks end up closer to the memory prefix.
2138
+ for (const block of afterMemory) {
2139
+ result = applyInjectionBlock(result, block);
2140
+ }
2141
+
2142
+ // ── Step 3: hardcoded branches that stayed outside the injector chain ──
2143
+ // These run in the same historical order as before G2.1 so their
2144
+ // interleaving with any prior tail content stays stable.
2145
+
2146
+ // For non-interactive conversations (scheduled jobs, work items), instruct the
2147
+ // model to never ask for clarification — there is no human present to answer.
2148
+ if (options.isNonInteractive) {
1289
2149
  const userTail = result[result.length - 1];
1290
2150
  if (userTail && userTail.role === "user") {
1291
2151
  result = [
@@ -1294,19 +2154,22 @@ export function applyRuntimeInjections(
1294
2154
  ...userTail,
1295
2155
  content: [
1296
2156
  ...userTail.content,
1297
- { type: "text" as const, text: PKB_SYSTEM_REMINDER },
2157
+ {
2158
+ type: "text" as const,
2159
+ text: "<non_interactive_context>\nNon-interactive scheduled task — do not ask for clarification or confirmation. Follow the instructions exactly using your best judgment. If recalled memory contains conflicting notes, prefer the explicit instruction in this message.\n</non_interactive_context>",
2160
+ },
1298
2161
  ],
1299
2162
  },
1300
2163
  ];
1301
2164
  }
1302
2165
  }
1303
2166
 
1304
- if (mode === "full" && options.nowScratchpad) {
2167
+ if (options.voiceCallControlPrompt) {
1305
2168
  const userTail = result[result.length - 1];
1306
2169
  if (userTail && userTail.role === "user") {
1307
2170
  result = [
1308
2171
  ...result.slice(0, -1),
1309
- injectNowScratchpad(userTail, options.nowScratchpad),
2172
+ injectVoiceCallControlContext(userTail, options.voiceCallControlPrompt),
1310
2173
  ];
1311
2174
  }
1312
2175
  }
@@ -1341,34 +2204,15 @@ export function applyRuntimeInjections(
1341
2204
  }
1342
2205
  }
1343
2206
 
1344
- if (mode === "full" && options.subagentStatusBlock) {
1345
- const userTail = result[result.length - 1];
1346
- if (userTail && userTail.role === "user") {
1347
- result = [
1348
- ...result.slice(0, -1),
1349
- injectSubagentStatus(userTail, options.subagentStatusBlock),
1350
- ];
1351
- }
1352
- }
1353
-
1354
- if (options.unifiedTurnContext) {
1355
- const userTail = result[result.length - 1];
1356
- if (userTail && userTail.role === "user") {
1357
- result = [
1358
- ...result.slice(0, -1),
1359
- {
1360
- ...userTail,
1361
- content: [
1362
- { type: "text" as const, text: options.unifiedTurnContext },
1363
- ...userTail.content,
1364
- ],
1365
- },
1366
- ];
1367
- }
1368
- }
1369
-
2207
+ // Slack conversations (both channels and DMs) build their own
2208
+ // chronological transcript from persisted messages and intentionally do
2209
+ // not receive the per-turn `<transport_hints>` block — the rendered
2210
+ // history already covers the active thread / DM, so duplicating it
2211
+ // would confuse the model. Other channels (telegram, email, etc.) keep
2212
+ // the existing injection.
1370
2213
  if (
1371
2214
  mode === "full" &&
2215
+ !slackConversation &&
1372
2216
  options.transportHints &&
1373
2217
  options.transportHints.length > 0
1374
2218
  ) {
@@ -1381,21 +2225,29 @@ export function applyRuntimeInjections(
1381
2225
  }
1382
2226
  }
1383
2227
 
1384
- // Workspace top-level context is injected last so it appears first
1385
- // (prepended) in the user message content, keeping cache breakpoints
1386
- // anchored to the trailing blocks.
1387
- if (mode === "full" && options.workspaceTopLevelContext) {
1388
- const userTail = result[result.length - 1];
1389
- if (userTail && userTail.role === "user") {
1390
- result = [
1391
- ...result.slice(0, -1),
1392
- injectWorkspaceTopLevelContext(
1393
- userTail,
1394
- options.workspaceTopLevelContext,
1395
- ),
1396
- ];
1397
- }
2228
+ // ── Step 4: apply remaining chain blocks by placement ──
2229
+ // append-user-tail: ascending `order` so lower-order blocks come first
2230
+ // in the append sequence.
2231
+ for (const block of appends) {
2232
+ result = applyInjectionBlock(result, block);
2233
+ }
2234
+
2235
+ // prepend-user-tail: descending `order` so the lowest-order block lands
2236
+ // topmost in the tail content (each successive prepend pushes the
2237
+ // previous one further down).
2238
+ for (let i = prepends.length - 1; i >= 0; i--) {
2239
+ result = applyInjectionBlock(result, prepends[i]);
1398
2240
  }
1399
2241
 
1400
- return result;
2242
+ return {
2243
+ messages: result,
2244
+ blocks: {
2245
+ unifiedTurnContext: turnContextCaptured,
2246
+ pkbSystemReminder: pkbSystemReminderCaptured,
2247
+ workspaceBlock: workspaceCaptured,
2248
+ nowScratchpadBlock: nowScratchpadCaptured,
2249
+ pkbContextBlock: pkbContextCaptured,
2250
+ injectorChainBlock,
2251
+ },
2252
+ };
1401
2253
  }