@vellumai/assistant 0.10.2 → 0.10.3-dev.202606252046.9075fd5

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 (546) hide show
  1. package/bun.lock +20 -0
  2. package/docs/workspace-tools.md +42 -33
  3. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  5. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
  7. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  9. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
  12. package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
  13. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/openapi.yaml +196 -56
  15. package/package.json +4 -1
  16. package/scripts/test.sh +36 -15
  17. package/src/__tests__/actor-token-service.test.ts +36 -14
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  19. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  21. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  22. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  23. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  24. package/src/__tests__/approval-cascade.test.ts +2 -0
  25. package/src/__tests__/assistant-attachments.test.ts +42 -0
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  27. package/src/__tests__/btw-routes.test.ts +2 -0
  28. package/src/__tests__/build-persisted-content.test.ts +2 -0
  29. package/src/__tests__/call-controller.test.ts +19 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  31. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  32. package/src/__tests__/channel-guardian.test.ts +164 -78
  33. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  34. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  35. package/src/__tests__/code-search-tool.test.ts +585 -0
  36. package/src/__tests__/compaction-events.test.ts +2 -0
  37. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  38. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  39. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  40. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  41. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  43. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  44. package/src/__tests__/config-loader-backfill.test.ts +123 -10
  45. package/src/__tests__/config-schema.test.ts +1 -0
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  47. package/src/__tests__/consult-deadline.test.ts +60 -0
  48. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  49. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  50. package/src/__tests__/contacts-relay-reads.test.ts +19 -24
  51. package/src/__tests__/contacts-write.test.ts +0 -2
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +134 -0
  57. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  58. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  61. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  62. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  63. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  64. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  65. package/src/__tests__/conversation-pairing.test.ts +2 -0
  66. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  67. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  68. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  69. package/src/__tests__/conversation-queue.test.ts +91 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  72. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  73. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  74. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  75. package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
  76. package/src/__tests__/conversation-title-service.test.ts +2 -0
  77. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  78. package/src/__tests__/conversation-usage.test.ts +2 -0
  79. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  80. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  81. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  82. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  83. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  84. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  85. package/src/__tests__/db-test-helpers.ts +5 -4
  86. package/src/__tests__/delete-propagation.test.ts +5 -3
  87. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  88. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  89. package/src/__tests__/dm-backfill.test.ts +6 -4
  90. package/src/__tests__/dm-persistence.test.ts +2 -0
  91. package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
  92. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  93. package/src/__tests__/exploration-drift-hook.test.ts +3 -2
  94. package/src/__tests__/filing-service.test.ts +2 -0
  95. package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
  96. package/src/__tests__/guardian-dispatch.test.ts +140 -1
  97. package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
  98. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  99. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  100. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  101. package/src/__tests__/heartbeat-service.test.ts +2 -0
  102. package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
  103. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  104. package/src/__tests__/helpers/mock-logger.ts +1 -0
  105. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  106. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  107. package/src/__tests__/host-bash-routes.test.ts +31 -41
  108. package/src/__tests__/host-browser-routes.test.ts +26 -32
  109. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  110. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  111. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  112. package/src/__tests__/http-user-message-parity.test.ts +290 -8
  113. package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
  114. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  115. package/src/__tests__/invite-redemption-service.test.ts +471 -53
  116. package/src/__tests__/invite-routes-http.test.ts +34 -0
  117. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  118. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  119. package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
  120. package/src/__tests__/llm-usage-store.test.ts +25 -0
  121. package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
  122. package/src/__tests__/mcp-health-check.test.ts +2 -1
  123. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  124. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  125. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  126. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  127. package/src/__tests__/mtime-cache.test.ts +275 -5
  128. package/src/__tests__/native-web-search.test.ts +2 -0
  129. package/src/__tests__/non-member-access-request.test.ts +191 -17
  130. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  131. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  132. package/src/__tests__/notification-deep-link.test.ts +6 -0
  133. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  134. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  135. package/src/__tests__/openai-provider.test.ts +22 -12
  136. package/src/__tests__/openai-responses-provider.test.ts +12 -2
  137. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  138. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  139. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  140. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  141. package/src/__tests__/persona-resolver.test.ts +75 -45
  142. package/src/__tests__/plugin-bootstrap.test.ts +21 -83
  143. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  144. package/src/__tests__/plugin-pipeline.test.ts +96 -0
  145. package/src/__tests__/plugin-route-contribution.test.ts +6 -19
  146. package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
  147. package/src/__tests__/plugin-types.test.ts +2 -4
  148. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  149. package/src/__tests__/process-message-display-content.test.ts +2 -0
  150. package/src/__tests__/provider-error-scenarios.test.ts +5 -4
  151. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  152. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  153. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  154. package/src/__tests__/reaction-persistence.test.ts +51 -4
  155. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  156. package/src/__tests__/registry.test.ts +4 -1
  157. package/src/__tests__/relay-server.test.ts +758 -32
  158. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
  159. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  160. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  161. package/src/__tests__/settings-routes.test.ts +32 -0
  162. package/src/__tests__/skills.test.ts +44 -0
  163. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  164. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
  165. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  166. package/src/__tests__/stt-hints.test.ts +49 -15
  167. package/src/__tests__/subagent-detail.test.ts +27 -0
  168. package/src/__tests__/subagent-disposal.test.ts +65 -0
  169. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  170. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  171. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  172. package/src/__tests__/subagent-role-registry.test.ts +24 -6
  173. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  174. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  175. package/src/__tests__/subagent-tools.test.ts +398 -1
  176. package/src/__tests__/suggestion-routes.test.ts +2 -0
  177. package/src/__tests__/thread-backfill.test.ts +3 -3
  178. package/src/__tests__/title-generate-hook.test.ts +2 -0
  179. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  180. package/src/__tests__/tool-executor.test.ts +16 -11
  181. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  182. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  183. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  184. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  185. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
  186. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  187. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  188. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  189. package/src/__tests__/twilio-routes.test.ts +96 -0
  190. package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
  191. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  192. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  193. package/src/__tests__/voice-invite-redemption.test.ts +216 -20
  194. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  195. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  196. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
  197. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  198. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  199. package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
  200. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  201. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  202. package/src/agent/loop-exclusive-tool.test.ts +154 -0
  203. package/src/agent/loop-native-web-search.test.ts +200 -0
  204. package/src/agent/loop.ts +164 -1
  205. package/src/api/constants/sse-replay.ts +41 -0
  206. package/src/api/events/conversation-notice.ts +26 -0
  207. package/src/api/index.ts +19 -1
  208. package/src/api/responses/llm-request-log-entry.ts +29 -0
  209. package/src/api/responses/subagent-detail.ts +17 -0
  210. package/src/api/surfaces.ts +39 -3
  211. package/src/approvals/guardian-channel-delivery.ts +30 -0
  212. package/src/approvals/guardian-expiry-notifier.ts +148 -0
  213. package/src/approvals/guardian-request-resolvers.ts +17 -15
  214. package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
  215. package/src/calls/call-domain.ts +3 -3
  216. package/src/calls/guardian-dispatch.ts +14 -9
  217. package/src/calls/inbound-trust-reader.ts +24 -2
  218. package/src/calls/media-stream-server.ts +21 -0
  219. package/src/calls/relay-access-wait.ts +6 -6
  220. package/src/calls/relay-server.ts +188 -51
  221. package/src/calls/relay-setup-router.ts +47 -17
  222. package/src/calls/relay-verification.ts +4 -4
  223. package/src/calls/stt-hints.ts +9 -12
  224. package/src/calls/twilio-routes.ts +14 -4
  225. package/src/channels/types.ts +10 -20
  226. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  227. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  228. package/src/cli/commands/cache.ts +194 -181
  229. package/src/cli/commands/contacts.ts +10 -7
  230. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  231. package/src/cli/commands/db/status.ts +37 -1
  232. package/src/cli/commands/mcp.ts +252 -218
  233. package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
  234. package/src/cli/commands/memory/index.ts +2 -0
  235. package/src/cli/commands/memory/worker.ts +242 -0
  236. package/src/cli/commands/plugins.ts +125 -13
  237. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  238. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  239. package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
  240. package/src/cli/lib/list-installed-plugins.ts +179 -1
  241. package/src/cli/lib/publish-plugin.ts +633 -0
  242. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  243. package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
  244. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  245. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  246. package/src/config/bundled-skills/subagent/SKILL.md +17 -2
  247. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  248. package/src/config/call-site-defaults.ts +0 -6
  249. package/src/config/feature-flag-registry.json +5 -13
  250. package/src/config/llm-resolver.ts +0 -3
  251. package/src/config/loader.ts +38 -5
  252. package/src/config/prune-seeded-callsite-defaults.ts +110 -0
  253. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  254. package/src/config/schemas/call-site-catalog.ts +0 -7
  255. package/src/config/schemas/heartbeat.ts +2 -5
  256. package/src/config/schemas/llm.ts +3 -12
  257. package/src/config/schemas/memory-lifecycle.ts +12 -0
  258. package/src/config/schemas/memory-v3.ts +7 -0
  259. package/src/config/schemas/memory.ts +4 -0
  260. package/src/config/schemas/timeouts.ts +8 -0
  261. package/src/config/seed-inference-profiles.ts +30 -34
  262. package/src/config/skills.ts +27 -5
  263. package/src/config/sync-gated-profiles.ts +12 -16
  264. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
  265. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  266. package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
  267. package/src/contacts/contact-store.ts +27 -237
  268. package/src/contacts/contacts-write.ts +18 -55
  269. package/src/contacts/gateway-channel-read.ts +51 -0
  270. package/src/contacts/guardian-delivery-reader.ts +223 -0
  271. package/src/contacts/member-write-relay.ts +183 -0
  272. package/src/contacts/types.ts +3 -15
  273. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
  274. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  275. package/src/daemon/assistant-attachments.ts +27 -4
  276. package/src/daemon/conversation-agent-loop.ts +28 -0
  277. package/src/daemon/conversation-notices.ts +60 -0
  278. package/src/daemon/conversation-process.ts +35 -16
  279. package/src/daemon/conversation-surfaces.ts +111 -38
  280. package/src/daemon/conversation-tool-setup.ts +71 -30
  281. package/src/daemon/conversation.ts +23 -1
  282. package/src/daemon/disk-pressure-guard.ts +12 -2
  283. package/src/daemon/event-loop-watchdog.ts +28 -1
  284. package/src/daemon/external-plugins-bootstrap.ts +17 -41
  285. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  287. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
  288. package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
  289. package/src/daemon/handlers/config-a2a.ts +6 -14
  290. package/src/daemon/handlers/config-channels.ts +67 -26
  291. package/src/daemon/handlers/conversations.ts +77 -0
  292. package/src/daemon/lifecycle.ts +16 -0
  293. package/src/daemon/mcp-reload-service.ts +10 -0
  294. package/src/daemon/memory-v2-startup.test.ts +72 -0
  295. package/src/daemon/memory-v2-startup.ts +87 -19
  296. package/src/daemon/message-types/conversations.ts +2 -0
  297. package/src/daemon/message-types/surfaces.ts +12 -12
  298. package/src/daemon/server.ts +0 -4
  299. package/src/daemon/shutdown-handlers.ts +20 -0
  300. package/src/daemon/tool-setup-types.ts +9 -0
  301. package/src/heartbeat/heartbeat-service.ts +5 -0
  302. package/src/home/relationship-state-writer.ts +5 -0
  303. package/src/hooks/hook-loader.ts +341 -0
  304. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  305. package/src/ipc/assistant-server.ts +2 -2
  306. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
  307. package/src/mcp/client.ts +15 -1
  308. package/src/mcp/mcp-auth-orchestrator.ts +6 -1
  309. package/src/mcp/mcp-header-store.ts +134 -0
  310. package/src/mcp/mcp-oauth-provider.ts +19 -8
  311. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  312. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  313. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  314. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  315. package/src/memory/compaction-ledger-store.ts +107 -0
  316. package/src/memory/conversation-crud.ts +129 -61
  317. package/src/memory/db-connection.ts +22 -3
  318. package/src/memory/db-init.ts +37 -503
  319. package/src/memory/db-singleton.ts +6 -4
  320. package/src/memory/jobs-worker.ts +104 -0
  321. package/src/memory/llm-request-log-store.ts +26 -1
  322. package/src/memory/llm-usage-store.ts +48 -20
  323. package/src/memory/memory-retrospective-job.ts +14 -8
  324. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
  325. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  326. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  327. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  328. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
  329. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  330. package/src/memory/migrations/run-migrations.ts +106 -10
  331. package/src/memory/migrations/validate-migration-state.ts +105 -67
  332. package/src/memory/prompt-override.ts +129 -0
  333. package/src/memory/schema/contacts.ts +6 -2
  334. package/src/memory/schema/conversations.ts +37 -0
  335. package/src/memory/schema/infrastructure.ts +20 -0
  336. package/src/memory/steps.ts +1294 -0
  337. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  338. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  339. package/src/memory/v2/cli-command-store.ts +75 -38
  340. package/src/memory/v2/prompts/consolidation.ts +13 -82
  341. package/src/memory/v2/prompts/router.ts +21 -93
  342. package/src/memory/v2/skill-store.ts +68 -31
  343. package/src/memory/watchdog-events-store.ts +87 -0
  344. package/src/memory/worker-control.ts +204 -0
  345. package/src/memory/worker-process.ts +89 -0
  346. package/src/messaging/channel-binding-metadata.ts +31 -0
  347. package/src/messaging/provider-types.ts +8 -0
  348. package/src/messaging/providers/slack/binding-metadata.ts +60 -0
  349. package/src/notifications/__tests__/broadcaster.test.ts +8 -8
  350. package/src/notifications/__tests__/connected-channels.test.ts +86 -0
  351. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  352. package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
  353. package/src/notifications/broadcaster.ts +8 -1
  354. package/src/notifications/decision-engine.ts +15 -7
  355. package/src/notifications/destination-resolver.ts +53 -25
  356. package/src/notifications/emit-signal.ts +34 -15
  357. package/src/onboarding/checkin-event.test.ts +222 -0
  358. package/src/onboarding/checkin-event.ts +321 -0
  359. package/src/onboarding/schedule-checkin.ts +190 -0
  360. package/src/permissions/question-prompter.test.ts +1 -1
  361. package/src/permissions/question-prompter.ts +7 -4
  362. package/src/plugin-api/index.ts +12 -12
  363. package/src/plugin-api/types.ts +12 -14
  364. package/src/plugin-api/vision-support.test.ts +28 -4
  365. package/src/plugin-api/vision-support.ts +66 -31
  366. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  367. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  368. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
  369. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  370. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  371. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  372. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  373. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
  374. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
  375. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  376. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  377. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  378. package/src/plugins/defaults/index.ts +0 -35
  379. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  380. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  381. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  382. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
  384. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
  385. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
  386. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  387. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  388. package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
  389. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  390. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  391. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
  392. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
  393. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  394. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  395. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  396. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  397. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  398. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  399. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  400. package/src/plugins/disabled-state.ts +31 -0
  401. package/src/plugins/external-plugin-loader.ts +2 -2
  402. package/src/plugins/mtime-cache.ts +186 -330
  403. package/src/plugins/pipeline.ts +111 -13
  404. package/src/plugins/registry.ts +59 -16
  405. package/src/plugins/surface-import.ts +121 -0
  406. package/src/plugins/types.ts +7 -9
  407. package/src/prompts/persona-resolver.ts +43 -11
  408. package/src/providers/anthropic/client.ts +5 -0
  409. package/src/providers/call-site-routing.ts +45 -0
  410. package/src/providers/model-catalog.ts +16 -0
  411. package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
  412. package/src/providers/openai/api-error-normalization.ts +270 -0
  413. package/src/providers/openai/chat-completions-provider.ts +37 -83
  414. package/src/providers/openai/responses-provider.ts +50 -46
  415. package/src/providers/openrouter/client.ts +5 -0
  416. package/src/providers/provider-send-message.ts +10 -0
  417. package/src/providers/ratelimit.ts +10 -0
  418. package/src/providers/registry.ts +1 -1
  419. package/src/providers/retry.ts +10 -0
  420. package/src/providers/types.ts +22 -0
  421. package/src/providers/usage-tracking.ts +10 -0
  422. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  423. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
  424. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  425. package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
  426. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  427. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
  428. package/src/runtime/access-request-helper.ts +18 -39
  429. package/src/runtime/actor-trust-resolver.ts +46 -19
  430. package/src/runtime/anchored-guardian.test.ts +109 -0
  431. package/src/runtime/anchored-guardian.ts +86 -0
  432. package/src/runtime/assistant-event-hub.ts +1 -1
  433. package/src/runtime/assistant-stream-state.ts +9 -2
  434. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  435. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  436. package/src/runtime/channel-verification-service.ts +56 -31
  437. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  438. package/src/runtime/guardian-vellum-migration.ts +69 -8
  439. package/src/runtime/invite-redemption-service.ts +213 -187
  440. package/src/runtime/local-actor-identity.test.ts +108 -0
  441. package/src/runtime/local-actor-identity.ts +85 -13
  442. package/src/runtime/local-principal-trust.ts +52 -0
  443. package/src/runtime/member-verdict-cache.ts +0 -0
  444. package/src/runtime/pending-interactions.ts +11 -1
  445. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  446. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  447. package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
  448. package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
  449. package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
  450. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
  451. package/src/runtime/routes/browser-routes.ts +1 -1
  452. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
  453. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  454. package/src/runtime/routes/contact-routes.ts +48 -57
  455. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  456. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  457. package/src/runtime/routes/conversation-query-routes.ts +72 -0
  458. package/src/runtime/routes/conversation-routes.ts +84 -85
  459. package/src/runtime/routes/events-routes.ts +2 -2
  460. package/src/runtime/routes/global-search-routes.ts +3 -1
  461. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  462. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  463. package/src/runtime/routes/host-bash-routes.ts +5 -4
  464. package/src/runtime/routes/host-browser-routes.ts +9 -11
  465. package/src/runtime/routes/host-cu-routes.ts +5 -4
  466. package/src/runtime/routes/host-file-routes.ts +5 -4
  467. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  468. package/src/runtime/routes/http-adapter.ts +16 -1
  469. package/src/runtime/routes/identity-routes.ts +3 -2
  470. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  471. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  472. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
  473. package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
  474. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  475. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  476. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  477. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  478. package/src/runtime/routes/index.ts +2 -0
  479. package/src/runtime/routes/llm-context-normalization.ts +83 -0
  480. package/src/runtime/routes/mcp-auth-routes.ts +171 -19
  481. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  482. package/src/runtime/routes/migration-routes.ts +4 -1
  483. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  484. package/src/runtime/routes/settings-routes.ts +8 -3
  485. package/src/runtime/routes/subagents-routes.ts +5 -0
  486. package/src/runtime/routes/surface-action-routes.ts +42 -56
  487. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  488. package/src/runtime/services/conversation-serializer.ts +13 -58
  489. package/src/runtime/tool-grant-request-helper.ts +3 -3
  490. package/src/runtime/trust-verdict-consumer.ts +111 -40
  491. package/src/runtime/verification-outbound-actions.ts +18 -18
  492. package/src/signals/user-message.ts +16 -0
  493. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  494. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  495. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
  496. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  497. package/src/subagent/index.ts +1 -1
  498. package/src/subagent/manager.ts +254 -33
  499. package/src/subagent/types.ts +11 -4
  500. package/src/telemetry/types.ts +34 -1
  501. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  502. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  503. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  504. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  505. package/src/tools/executor.ts +4 -4
  506. package/src/tools/filesystem/search.ts +543 -0
  507. package/src/tools/registry.ts +38 -0
  508. package/src/tools/shared/filesystem/path-policy.ts +12 -5
  509. package/src/tools/subagent/consult-deadline.ts +49 -0
  510. package/src/tools/subagent/spawn.ts +234 -5
  511. package/src/tools/tool-approval-handler.ts +1 -1
  512. package/src/tools/tool-defaults.ts +9 -2
  513. package/src/tools/tool-manifest.ts +3 -0
  514. package/src/tools/types.ts +131 -25
  515. package/src/tools/workspace-tools/loader.ts +348 -244
  516. package/src/util/errors.ts +26 -1
  517. package/src/util/logger.ts +9 -0
  518. package/src/util/platform.ts +19 -0
  519. package/src/util/telemetry-db-path.ts +24 -0
  520. package/src/workflows/library.test.ts +140 -0
  521. package/src/workflows/library.ts +82 -28
  522. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  523. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  524. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  525. package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
  526. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
  527. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  528. package/src/workspace/migrations/registry.ts +4 -0
  529. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  530. package/src/daemon/workspace-tools-watcher.ts +0 -328
  531. package/src/memory/migrations/registry.ts +0 -573
  532. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  533. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  534. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  535. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
  536. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  537. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  538. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  539. package/src/plugins/defaults/advisor/config.ts +0 -21
  540. package/src/plugins/defaults/advisor/consult.ts +0 -93
  541. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  542. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  543. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  544. package/src/plugins/defaults/advisor/package.json +0 -14
  545. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
  546. package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
@@ -26,6 +26,7 @@ import {
26
26
  linkAttachmentToMessage,
27
27
  uploadAttachment,
28
28
  } from "../memory/attachments-store.js";
29
+ import { appendCompactionEvent } from "../memory/compaction-ledger-store.js";
29
30
  import {
30
31
  getAttentionStateByConversationIds,
31
32
  markConversationUnread,
@@ -54,6 +55,7 @@ import {
54
55
  activationState,
55
56
  channelInboundEvents,
56
57
  conversationAssistantAttentionState,
58
+ conversationCompactionEvents,
57
59
  conversationGraphMemoryState,
58
60
  conversations,
59
61
  externalConversationBindings,
@@ -78,6 +80,7 @@ function resetTables(): void {
78
80
  db.delete(externalConversationBindings).run();
79
81
  db.delete(conversationAssistantAttentionState).run();
80
82
  db.delete(activationState).run();
83
+ db.delete(conversationCompactionEvents).run();
81
84
  db.delete(conversationGraphMemoryState).run();
82
85
  db.delete(memoryRetrospectiveState).run();
83
86
  getLogsDb()!.delete(llmRequestLogs).run();
@@ -346,24 +349,37 @@ describe("forkConversation", () => {
346
349
  expect(toolResultUserRow.id).not.toBe(anchor.id);
347
350
  });
348
351
 
349
- test("preserves compacted context when forking from the visible window", async () => {
352
+ test("inherits the most recent compaction at-or-before the forked-from message", async () => {
350
353
  const source = createConversation("Compacted thread");
351
- await addMessage(source.id, "user", "Message 1", {
354
+ const m1 = await addMessage(source.id, "user", "Message 1", {
352
355
  skipIndexing: true,
353
356
  });
354
- await addMessage(source.id, "assistant", "Message 2", {
357
+ const m2 = await addMessage(source.id, "assistant", "Message 2", {
355
358
  skipIndexing: true,
356
359
  });
357
360
  const branchPoint = await addMessage(source.id, "user", "Message 3", {
358
361
  skipIndexing: true,
359
362
  });
360
- await addMessage(source.id, "assistant", "Message 4", {
363
+ const m4 = await addMessage(source.id, "assistant", "Message 4", {
361
364
  skipIndexing: true,
362
365
  });
363
366
 
364
- const compactedAt = Date.now();
365
- getDb()
366
- .update(conversations)
367
+ // Pin timestamps so the compaction event sits strictly between M2 and M3:
368
+ // it covers M1+M2 and ran before M3 was sent.
369
+ const db = getDb();
370
+ const base = Date.now();
371
+ db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
372
+ db.run(
373
+ `UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
374
+ );
375
+ db.run(
376
+ `UPDATE messages SET created_at = ${base + 3} WHERE id = '${branchPoint.id}'`,
377
+ );
378
+ db.run(
379
+ `UPDATE messages SET created_at = ${base + 4} WHERE id = '${m4.id}'`,
380
+ );
381
+ const compactedAt = base + 2;
382
+ db.update(conversations)
367
383
  .set({
368
384
  contextSummary: "Compacted summary",
369
385
  contextCompactedMessageCount: 2,
@@ -371,7 +387,14 @@ describe("forkConversation", () => {
371
387
  })
372
388
  .where(eq(conversations.id, source.id))
373
389
  .run();
390
+ appendCompactionEvent(source.id, {
391
+ compactedAt,
392
+ summary: "Compacted summary",
393
+ compactedMessageCount: 2,
394
+ });
374
395
 
396
+ // M3 was sent after the compaction, so forking through it reproduces the
397
+ // compacted working context.
375
398
  const fork = forkConversation({
376
399
  conversationId: source.id,
377
400
  throughMessageId: branchPoint.id,
@@ -384,6 +407,67 @@ describe("forkConversation", () => {
384
407
  expect(fork.forkParentMessageId).toBe(branchPoint.id);
385
408
  });
386
409
 
410
+ test("does not inherit a compaction that ran after the forked-from message", async () => {
411
+ const source = createConversation("Compacted thread");
412
+ const m1 = await addMessage(source.id, "user", "Message 1", {
413
+ skipIndexing: true,
414
+ });
415
+ const m2 = await addMessage(source.id, "assistant", "Message 2", {
416
+ skipIndexing: true,
417
+ });
418
+ const m3 = await addMessage(source.id, "user", "Message 3", {
419
+ skipIndexing: true,
420
+ });
421
+ const m4 = await addMessage(source.id, "assistant", "Message 4", {
422
+ skipIndexing: true,
423
+ });
424
+
425
+ // `/compact` ran after M4 — the compaction postdates every message.
426
+ const db = getDb();
427
+ const base = Date.now();
428
+ db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
429
+ db.run(
430
+ `UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
431
+ );
432
+ db.run(
433
+ `UPDATE messages SET created_at = ${base + 2} WHERE id = '${m3.id}'`,
434
+ );
435
+ db.run(
436
+ `UPDATE messages SET created_at = ${base + 3} WHERE id = '${m4.id}'`,
437
+ );
438
+ const compactedAt = base + 10;
439
+ db.update(conversations)
440
+ .set({
441
+ contextSummary: "Compacted summary",
442
+ contextCompactedMessageCount: 2,
443
+ contextCompactedAt: compactedAt,
444
+ })
445
+ .where(eq(conversations.id, source.id))
446
+ .run();
447
+ appendCompactionEvent(source.id, {
448
+ compactedAt,
449
+ summary: "Compacted summary",
450
+ compactedMessageCount: 2,
451
+ });
452
+
453
+ // Forking through the final message yields the full uncompacted history:
454
+ // the compaction did not exist when M4 was the latest turn.
455
+ const fork = forkConversation({
456
+ conversationId: source.id,
457
+ throughMessageId: m4.id,
458
+ });
459
+
460
+ expect(fork.contextSummary).toBeNull();
461
+ expect(fork.contextCompactedMessageCount).toBe(0);
462
+ expect(fork.contextCompactedAt).toBeNull();
463
+ expect(getMessages(fork.id).map((message) => message.content)).toEqual([
464
+ "Message 1",
465
+ "Message 2",
466
+ "Message 3",
467
+ "Message 4",
468
+ ]);
469
+ });
470
+
387
471
  test("forks from the compacted-away prefix without inheriting source compaction state", async () => {
388
472
  const source = createConversation("Compacted thread");
389
473
  const compactedBranchPoint = await addMessage(
@@ -399,15 +483,21 @@ describe("forkConversation", () => {
399
483
  skipIndexing: true,
400
484
  });
401
485
 
486
+ const compactedAt = Date.now();
402
487
  getDb()
403
488
  .update(conversations)
404
489
  .set({
405
490
  contextSummary: "Compacted summary",
406
491
  contextCompactedMessageCount: 2,
407
- contextCompactedAt: Date.now(),
492
+ contextCompactedAt: compactedAt,
408
493
  })
409
494
  .where(eq(conversations.id, source.id))
410
495
  .run();
496
+ appendCompactionEvent(source.id, {
497
+ compactedAt,
498
+ summary: "Compacted summary",
499
+ compactedMessageCount: 2,
500
+ });
411
501
 
412
502
  const fork = forkConversation({
413
503
  conversationId: source.id,
@@ -1000,35 +1090,70 @@ describe("forkConversation", () => {
1000
1090
 
1001
1091
  test("truncated fork ignores attachments behind an inherited compaction boundary", async () => {
1002
1092
  const source = createConversation("Compacted truncated thread");
1003
- await addMessage(source.id, "user", "compacted turn", {
1004
- metadata: {
1005
- memoryInjectedBlock:
1006
- "# memory/concepts/topics/page-compacted.md\nOld summary",
1093
+ const compactedTurn = await addMessage(
1094
+ source.id,
1095
+ "user",
1096
+ "compacted turn",
1097
+ {
1098
+ metadata: {
1099
+ memoryInjectedBlock:
1100
+ "# memory/concepts/topics/page-compacted.md\nOld summary",
1101
+ },
1102
+ skipIndexing: true,
1007
1103
  },
1008
- skipIndexing: true,
1009
- });
1010
- await addMessage(source.id, "assistant", "compacted reply", {
1011
- skipIndexing: true,
1012
- });
1104
+ );
1105
+ const compactedReply = await addMessage(
1106
+ source.id,
1107
+ "assistant",
1108
+ "compacted reply",
1109
+ { skipIndexing: true },
1110
+ );
1013
1111
  const boundaryMessage = await addMessage(source.id, "user", "live turn", {
1014
1112
  metadata: {
1015
1113
  memoryInjectedBlock: "# memory/concepts/topics/page-live.md\nSummary",
1016
1114
  },
1017
1115
  skipIndexing: true,
1018
1116
  });
1019
- await addMessage(source.id, "assistant", "live reply", {
1117
+ const liveReply = await addMessage(source.id, "assistant", "live reply", {
1020
1118
  skipIndexing: true,
1021
1119
  });
1022
- await addMessage(source.id, "user", "past boundary", {
1120
+ const pastBoundary = await addMessage(source.id, "user", "past boundary", {
1023
1121
  skipIndexing: true,
1024
1122
  });
1025
- // First two messages sit behind the compaction boundary: their
1026
- // attachments are not rendered, so the fork must not claim them.
1027
- getDb()
1028
- .update(conversations)
1029
- .set({ contextCompactedMessageCount: 2 })
1123
+ // First two messages sit behind a compaction that ran before the live
1124
+ // turn: their injected blocks are not rendered, so the fork must not
1125
+ // claim them.
1126
+ const db = getDb();
1127
+ const base = Date.now();
1128
+ db.run(
1129
+ `UPDATE messages SET created_at = ${base} WHERE id = '${compactedTurn.id}'`,
1130
+ );
1131
+ db.run(
1132
+ `UPDATE messages SET created_at = ${base + 1} WHERE id = '${compactedReply.id}'`,
1133
+ );
1134
+ db.run(
1135
+ `UPDATE messages SET created_at = ${base + 3} WHERE id = '${boundaryMessage.id}'`,
1136
+ );
1137
+ db.run(
1138
+ `UPDATE messages SET created_at = ${base + 4} WHERE id = '${liveReply.id}'`,
1139
+ );
1140
+ db.run(
1141
+ `UPDATE messages SET created_at = ${base + 5} WHERE id = '${pastBoundary.id}'`,
1142
+ );
1143
+ const compactedAt = base + 2;
1144
+ db.update(conversations)
1145
+ .set({
1146
+ contextSummary: "Compacted summary",
1147
+ contextCompactedMessageCount: 2,
1148
+ contextCompactedAt: compactedAt,
1149
+ })
1030
1150
  .where(eq(conversations.id, source.id))
1031
1151
  .run();
1152
+ appendCompactionEvent(source.id, {
1153
+ compactedAt,
1154
+ summary: "Compacted summary",
1155
+ compactedMessageCount: 2,
1156
+ });
1032
1157
 
1033
1158
  const fork = forkConversation({
1034
1159
  conversationId: source.id,
@@ -1382,4 +1507,209 @@ describe("forkConversation + memory_retrospective_state", () => {
1382
1507
 
1383
1508
  expect(getRetrospectiveState(fork.id)?.lastRunAt).toBe(1_700_000_000_000);
1384
1509
  });
1510
+
1511
+ test("inherits the earlier of two compactions when forking between them", async () => {
1512
+ const source = createConversation("Twice-compacted thread");
1513
+ const m1 = await addMessage(source.id, "user", "Message 1", {
1514
+ skipIndexing: true,
1515
+ });
1516
+ const m2 = await addMessage(source.id, "assistant", "Message 2", {
1517
+ skipIndexing: true,
1518
+ });
1519
+ const m3 = await addMessage(source.id, "user", "Message 3", {
1520
+ skipIndexing: true,
1521
+ });
1522
+ const m4 = await addMessage(source.id, "assistant", "Message 4", {
1523
+ skipIndexing: true,
1524
+ });
1525
+
1526
+ const db = getDb();
1527
+ const base = Date.now();
1528
+ db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
1529
+ db.run(
1530
+ `UPDATE messages SET created_at = ${base + 2} WHERE id = '${m2.id}'`,
1531
+ );
1532
+ db.run(
1533
+ `UPDATE messages SET created_at = ${base + 4} WHERE id = '${m3.id}'`,
1534
+ );
1535
+ db.run(
1536
+ `UPDATE messages SET created_at = ${base + 6} WHERE id = '${m4.id}'`,
1537
+ );
1538
+ // C1 ran after M1 (covers 1 message); C2 ran after M3 (covers 3).
1539
+ appendCompactionEvent(source.id, {
1540
+ compactedAt: base + 1,
1541
+ summary: "Summary 1",
1542
+ compactedMessageCount: 1,
1543
+ });
1544
+ appendCompactionEvent(source.id, {
1545
+ compactedAt: base + 5,
1546
+ summary: "Summary 2",
1547
+ compactedMessageCount: 3,
1548
+ });
1549
+
1550
+ // Forking through M3 lands between the two compactions, so it inherits the
1551
+ // earlier one — the capability a single stored pointer cannot provide.
1552
+ const fork = forkConversation({
1553
+ conversationId: source.id,
1554
+ throughMessageId: m3.id,
1555
+ });
1556
+ expect(fork.contextSummary).toBe("Summary 1");
1557
+ expect(fork.contextCompactedMessageCount).toBe(1);
1558
+ expect(fork.contextCompactedAt).toBe(base + 1);
1559
+ });
1560
+
1561
+ test("carries a ledger into the fork so re-forks resolve compaction", async () => {
1562
+ const source = createConversation("Re-fork thread");
1563
+ const m1 = await addMessage(source.id, "user", "Message 1", {
1564
+ skipIndexing: true,
1565
+ });
1566
+ const m2 = await addMessage(source.id, "assistant", "Message 2", {
1567
+ skipIndexing: true,
1568
+ });
1569
+ const m3 = await addMessage(source.id, "user", "Message 3", {
1570
+ skipIndexing: true,
1571
+ });
1572
+
1573
+ const db = getDb();
1574
+ const base = Date.now();
1575
+ db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
1576
+ db.run(
1577
+ `UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
1578
+ );
1579
+ db.run(
1580
+ `UPDATE messages SET created_at = ${base + 3} WHERE id = '${m3.id}'`,
1581
+ );
1582
+ const compactedAt = base + 2; // covers M1+M2, before M3
1583
+ db.update(conversations)
1584
+ .set({
1585
+ contextSummary: "Compacted summary",
1586
+ contextCompactedMessageCount: 2,
1587
+ contextCompactedAt: compactedAt,
1588
+ })
1589
+ .where(eq(conversations.id, source.id))
1590
+ .run();
1591
+ appendCompactionEvent(source.id, {
1592
+ compactedAt,
1593
+ summary: "Compacted summary",
1594
+ compactedMessageCount: 2,
1595
+ });
1596
+
1597
+ const fork = forkConversation({ conversationId: source.id });
1598
+ expect(fork.contextCompactedMessageCount).toBe(2);
1599
+
1600
+ // The fork owns a copy of the ledger, so a re-fork resolves the inherited
1601
+ // compaction without walking back to the original source.
1602
+ const reFork = forkConversation({ conversationId: fork.id });
1603
+ expect(reFork.contextSummary).toBe("Compacted summary");
1604
+ expect(reFork.contextCompactedMessageCount).toBe(2);
1605
+ });
1606
+
1607
+ test("drops the stale Slack watermark when forking inherits an older compaction", async () => {
1608
+ const source = createConversation("Slack twice-compacted thread");
1609
+ const m1 = await addMessage(source.id, "user", "Message 1", {
1610
+ skipIndexing: true,
1611
+ });
1612
+ const m2 = await addMessage(source.id, "assistant", "Message 2", {
1613
+ skipIndexing: true,
1614
+ });
1615
+ const m3 = await addMessage(source.id, "user", "Message 3", {
1616
+ skipIndexing: true,
1617
+ });
1618
+ const m4 = await addMessage(source.id, "assistant", "Message 4", {
1619
+ skipIndexing: true,
1620
+ });
1621
+
1622
+ const db = getDb();
1623
+ const base = Date.now();
1624
+ db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
1625
+ db.run(
1626
+ `UPDATE messages SET created_at = ${base + 2} WHERE id = '${m2.id}'`,
1627
+ );
1628
+ db.run(
1629
+ `UPDATE messages SET created_at = ${base + 4} WHERE id = '${m3.id}'`,
1630
+ );
1631
+ db.run(
1632
+ `UPDATE messages SET created_at = ${base + 6} WHERE id = '${m4.id}'`,
1633
+ );
1634
+ appendCompactionEvent(source.id, {
1635
+ compactedAt: base + 1,
1636
+ summary: "Summary 1",
1637
+ compactedMessageCount: 1,
1638
+ });
1639
+ appendCompactionEvent(source.id, {
1640
+ compactedAt: base + 5,
1641
+ summary: "Summary 2",
1642
+ compactedMessageCount: 3,
1643
+ });
1644
+ // The source's single-valued watermark reflects only the latest compaction.
1645
+ db.update(conversations)
1646
+ .set({
1647
+ contextSummary: "Summary 2",
1648
+ contextCompactedMessageCount: 3,
1649
+ contextCompactedAt: base + 5,
1650
+ slackContextCompactionWatermarkTs: "ts-latest",
1651
+ slackContextCompactionWatermarkAt: base + 5,
1652
+ })
1653
+ .where(eq(conversations.id, source.id))
1654
+ .run();
1655
+
1656
+ // Forking through M3 inherits the OLDER compaction (Summary 1); the latest
1657
+ // watermark must not ride along, or it would hide Slack messages the older
1658
+ // summary does not cover.
1659
+ const fork = forkConversation({
1660
+ conversationId: source.id,
1661
+ throughMessageId: m3.id,
1662
+ });
1663
+ expect(fork.contextSummary).toBe("Summary 1");
1664
+ expect(fork.contextCompactedMessageCount).toBe(1);
1665
+ expect(fork.slackContextCompactionWatermarkTs).toBeNull();
1666
+ expect(fork.slackContextCompactionWatermarkAt).toBeNull();
1667
+ });
1668
+
1669
+ test("carries the Slack watermark when forking inherits the latest compaction", async () => {
1670
+ const source = createConversation("Slack compacted thread");
1671
+ const m1 = await addMessage(source.id, "user", "Message 1", {
1672
+ skipIndexing: true,
1673
+ });
1674
+ const m2 = await addMessage(source.id, "assistant", "Message 2", {
1675
+ skipIndexing: true,
1676
+ });
1677
+ const m3 = await addMessage(source.id, "user", "Message 3", {
1678
+ skipIndexing: true,
1679
+ });
1680
+
1681
+ const db = getDb();
1682
+ const base = Date.now();
1683
+ db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
1684
+ db.run(
1685
+ `UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
1686
+ );
1687
+ db.run(
1688
+ `UPDATE messages SET created_at = ${base + 3} WHERE id = '${m3.id}'`,
1689
+ );
1690
+ const compactedAt = base + 2; // latest (and only) compaction, covers M1+M2
1691
+ appendCompactionEvent(source.id, {
1692
+ compactedAt,
1693
+ summary: "Compacted summary",
1694
+ compactedMessageCount: 2,
1695
+ });
1696
+ db.update(conversations)
1697
+ .set({
1698
+ contextSummary: "Compacted summary",
1699
+ contextCompactedMessageCount: 2,
1700
+ contextCompactedAt: compactedAt,
1701
+ slackContextCompactionWatermarkTs: "ts-latest",
1702
+ slackContextCompactionWatermarkAt: compactedAt,
1703
+ })
1704
+ .where(eq(conversations.id, source.id))
1705
+ .run();
1706
+
1707
+ const fork = forkConversation({
1708
+ conversationId: source.id,
1709
+ throughMessageId: m3.id,
1710
+ });
1711
+ expect(fork.contextCompactedMessageCount).toBe(2);
1712
+ expect(fork.slackContextCompactionWatermarkTs).toBe("ts-latest");
1713
+ expect(fork.slackContextCompactionWatermarkAt).toBe(compactedAt);
1714
+ });
1385
1715
  });
@@ -36,6 +36,8 @@ let deletedMessageIds: string[] = [];
36
36
  let updatedMessages: Array<{ id: string; content: string }> = [];
37
37
 
38
38
  mock.module("../memory/conversation-crud.js", () => ({
39
+ setConversationProcessingStartedAt: () => {},
40
+ isConversationProcessing: () => false,
39
41
  getMessages: (conversationId: string) =>
40
42
  dbMessages.filter((m) => m.conversationId === conversationId),
41
43
  deleteMessageById: (messageId: string) => {
@@ -74,6 +74,8 @@ let mockConversation: Record<string, unknown> | null = null;
74
74
  let nextMockMessageId = 1;
75
75
 
76
76
  mock.module("../memory/conversation-crud.js", () => ({
77
+ setConversationProcessingStartedAt: () => {},
78
+ isConversationProcessing: () => false,
77
79
  updateConversationContextWindow: () => {},
78
80
  deleteMessageById: () => {},
79
81
  updateConversationTitle: () => {},
@@ -70,6 +70,8 @@ let mockDbMessages: Array<{
70
70
  let mockConversation: Record<string, unknown> | null = null;
71
71
 
72
72
  mock.module("../memory/conversation-crud.js", () => ({
73
+ setConversationProcessingStartedAt: () => {},
74
+ isConversationProcessing: () => false,
73
75
  updateConversationContextWindow: () => {},
74
76
  deleteMessageById: () => {},
75
77
  updateConversationTitle: () => {},
@@ -51,6 +51,8 @@ const getConversationMock = mock((id: string) => {
51
51
  });
52
52
 
53
53
  mock.module("../memory/conversation-crud.js", () => ({
54
+ setConversationProcessingStartedAt: () => {},
55
+ isConversationProcessing: () => false,
54
56
  setConversationOriginChannelIfUnset: () => {},
55
57
  updateConversationContextWindow: () => {},
56
58
  deleteMessageById: () => {},
@@ -46,6 +46,8 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
46
46
  }));
47
47
 
48
48
  mock.module("../memory/conversation-crud.js", () => ({
49
+ setConversationProcessingStartedAt: () => {},
50
+ isConversationProcessing: () => false,
49
51
  setConversationOriginChannelIfUnset: () => {},
50
52
  setConversationOriginInterfaceIfUnset: () => {},
51
53
  provenanceFromTrustContext: () => ({
@@ -157,6 +157,8 @@ let mockDbMessages: Array<{ id: string; role: string; content: string }> = [];
157
157
  let mockConversation: Record<string, unknown> | null = null;
158
158
 
159
159
  mock.module("../memory/conversation-crud.js", () => ({
160
+ setConversationProcessingStartedAt: () => {},
161
+ isConversationProcessing: () => false,
160
162
  setConversationOriginChannelIfUnset: () => {},
161
163
  setConversationOriginInterfaceIfUnset: () => {},
162
164
  updateConversationContextWindow: () => {},
@@ -98,6 +98,8 @@ mock.module("../security/secret-allowlist.js", () => ({
98
98
  }));
99
99
 
100
100
  mock.module("../memory/conversation-crud.js", () => ({
101
+ setConversationProcessingStartedAt: () => {},
102
+ isConversationProcessing: () => false,
101
103
  setConversationOriginChannelIfUnset: () => {},
102
104
  deleteMessageById: () => {},
103
105
  getMessages: () => [],
@@ -149,6 +149,8 @@ mock.module("../security/secret-allowlist.js", () => ({
149
149
  }));
150
150
 
151
151
  mock.module("../memory/conversation-crud.js", () => ({
152
+ setConversationProcessingStartedAt: () => {},
153
+ isConversationProcessing: () => false,
152
154
  setConversationOriginChannelIfUnset: () => {},
153
155
  setConversationOriginInterfaceIfUnset: () => {},
154
156
  updateConversationContextWindow: () => {},
@@ -2911,3 +2913,92 @@ describe("MessageQueue byte budget", () => {
2911
2913
  ).toBe(false);
2912
2914
  });
2913
2915
  });
2916
+
2917
+ describe("subagent notification user_message_echo suppression", () => {
2918
+ beforeEach(() => {
2919
+ pendingRuns = [];
2920
+ capturedAddMessages.length = 0;
2921
+ });
2922
+
2923
+ test("drained subagent-notification message persists and wakes the agent but emits no user_message_echo", async () => {
2924
+ const conversation = makeConversation();
2925
+ await conversation.loadFromDb();
2926
+
2927
+ const events1: ServerMessage[] = [];
2928
+ const eventsNotif: ServerMessage[] = [];
2929
+
2930
+ // Occupy the conversation so the injected notification queues.
2931
+ const p1 = conversation.processMessage({
2932
+ content: "msg-1",
2933
+ attachments: [],
2934
+ onEvent: (e) => events1.push(e),
2935
+ requestId: "req-1",
2936
+ });
2937
+ await waitForPendingRun(1);
2938
+ expect(conversation.isProcessing()).toBe(true);
2939
+
2940
+ // A daemon-injected subagent completion notification carries
2941
+ // `subagentNotification` metadata.
2942
+ conversation.enqueueMessage({
2943
+ content: '[Subagent "research" completed]',
2944
+ onEvent: (e) => eventsNotif.push(e),
2945
+ requestId: "req-notif",
2946
+ metadata: {
2947
+ subagentNotification: {
2948
+ subagentId: "sub-1",
2949
+ label: "research",
2950
+ status: "completed",
2951
+ },
2952
+ },
2953
+ });
2954
+
2955
+ // Resolving the first run drains the queued notification.
2956
+ await resolveRun(0);
2957
+ await p1;
2958
+ await waitForPendingRun(2);
2959
+
2960
+ // It is still persisted (so the orchestrator LLM sees it in the transcript)
2961
+ // and still wakes the agent (a run was created for the drained message)...
2962
+ expect(
2963
+ capturedAddMessages.some((m) => m.content.includes("Subagent")),
2964
+ ).toBe(true);
2965
+ expect(pendingRuns.length).toBe(2);
2966
+ // ...but no user_message_echo is broadcast, so the client never renders it
2967
+ // as a live user bubble.
2968
+ expect(eventsNotif.some((e) => e.type === "user_message_echo")).toBe(false);
2969
+
2970
+ await resolveRun(1);
2971
+ await new Promise((r) => setTimeout(r, 10));
2972
+ });
2973
+
2974
+ test("drained ordinary message still emits user_message_echo", async () => {
2975
+ const conversation = makeConversation();
2976
+ await conversation.loadFromDb();
2977
+
2978
+ const events1: ServerMessage[] = [];
2979
+ const eventsNormal: ServerMessage[] = [];
2980
+
2981
+ const p1 = conversation.processMessage({
2982
+ content: "msg-1",
2983
+ attachments: [],
2984
+ onEvent: (e) => events1.push(e),
2985
+ requestId: "req-1",
2986
+ });
2987
+ await waitForPendingRun(1);
2988
+
2989
+ conversation.enqueueMessage({
2990
+ content: "ordinary message",
2991
+ onEvent: (e) => eventsNormal.push(e),
2992
+ requestId: "req-normal",
2993
+ });
2994
+
2995
+ await resolveRun(0);
2996
+ await p1;
2997
+ await waitForPendingRun(2);
2998
+
2999
+ expect(eventsNormal.some((e) => e.type === "user_message_echo")).toBe(true);
3000
+
3001
+ await resolveRun(1);
3002
+ await new Promise((r) => setTimeout(r, 10));
3003
+ });
3004
+ });
@@ -77,6 +77,8 @@ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
77
77
  }));
78
78
 
79
79
  mock.module("../memory/conversation-crud.js", () => ({
80
+ setConversationProcessingStartedAt: () => {},
81
+ isConversationProcessing: () => false,
80
82
  addMessage: (
81
83
  conversationId: string,
82
84
  role: string,
@@ -104,6 +106,18 @@ mock.module("../runtime/trust-context-resolver.js", () => ({
104
106
  }),
105
107
  }));
106
108
 
109
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
110
+ getGuardianDelivery: async () => [
111
+ {
112
+ channelType: "vellum",
113
+ contactId: "guardian-contact",
114
+ principalId: "test-user",
115
+ address: "test-user",
116
+ status: "active",
117
+ },
118
+ ],
119
+ }));
120
+
107
121
  import type { AuthContext } from "../runtime/auth/types.js";
108
122
  import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
109
123
  import { callHandler } from "./helpers/call-route-handler.js";