@vellumai/assistant 0.11.2 → 0.11.3-staging.2

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 (720) hide show
  1. package/ARCHITECTURE.md +58 -3
  2. package/docs/trusted-contact-access.md +9 -5
  3. package/docs/vellum-doctor.md +195 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  7. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +19 -2
  9. package/node_modules/@vellumai/environments/package.json +2 -1
  10. package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  11. package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  12. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  13. package/node_modules/@vellumai/environments/src/index.ts +17 -6
  14. package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  15. package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  19. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -0
  20. package/node_modules/@vellumai/gateway-client/src/verification-session-contract.ts +91 -6
  21. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
  22. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
  23. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +2 -0
  24. package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
  25. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +15 -1
  26. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +17 -2
  27. package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  28. package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  29. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  30. package/node_modules/@vellumai/slack-text/src/index.ts +16 -2
  31. package/openapi.yaml +589 -30
  32. package/package.json +1 -1
  33. package/src/__tests__/agent-image-optimize.test.ts +4 -4
  34. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +113 -66
  35. package/src/__tests__/agent-loop-override-profile.test.ts +38 -48
  36. package/src/__tests__/agent-loop-subagent-billing-headers.test.ts +166 -0
  37. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  38. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  39. package/src/__tests__/answered-question-persistence.test.ts +240 -0
  40. package/src/__tests__/anthropic-provider.test.ts +165 -30
  41. package/src/__tests__/app-control-flow.test.ts +9 -8
  42. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  43. package/src/__tests__/attachments-store-heic-normalize.test.ts +16 -12
  44. package/src/__tests__/attachments-store.test.ts +130 -68
  45. package/src/__tests__/attachments.test.ts +36 -36
  46. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/call-start-guardian-guard.test.ts +18 -4
  48. package/src/__tests__/canned-reply-release.test.ts +86 -0
  49. package/src/__tests__/channel-guardian.test.ts +59 -29
  50. package/src/__tests__/channel-readiness-telegram-remote.test.ts +154 -0
  51. package/src/__tests__/channel-setup-panel-ack.test.ts +6 -5
  52. package/src/__tests__/client-os-metadata-persistence.test.ts +103 -4
  53. package/src/__tests__/compactor-image-manifest-trust.test.ts +1 -1
  54. package/src/__tests__/consult-deadline.test.ts +65 -0
  55. package/src/__tests__/conversation-agent-loop.test.ts +438 -2
  56. package/src/__tests__/conversation-disk-view-integration.test.ts +5 -1
  57. package/src/__tests__/conversation-disk-view.test.ts +4 -4
  58. package/src/__tests__/conversation-error.test.ts +22 -3
  59. package/src/__tests__/conversation-fork-crud.test.ts +5 -1
  60. package/src/__tests__/conversation-fork-referential.test.ts +352 -0
  61. package/src/__tests__/conversation-fork-retrospective.test.ts +5 -1
  62. package/src/__tests__/conversation-group-tools.test.ts +32 -2
  63. package/src/__tests__/conversation-inference-profile-route.test.ts +27 -1
  64. package/src/__tests__/conversation-initial-prompt.test.ts +51 -22
  65. package/src/__tests__/conversation-key-store-bootstrap-cleanup.test.ts +119 -0
  66. package/src/__tests__/conversation-key-store-origin.test.ts +105 -0
  67. package/src/__tests__/conversation-list-recency-ordering.test.ts +104 -0
  68. package/src/__tests__/conversation-origin-at-creation.test.ts +153 -0
  69. package/src/__tests__/conversation-origin-channel-filter.test.ts +95 -0
  70. package/src/__tests__/conversation-placement-promotion.test.ts +172 -0
  71. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +123 -0
  72. package/src/__tests__/conversation-queue.test.ts +433 -1
  73. package/src/__tests__/conversation-retry-route.test.ts +3 -1
  74. package/src/__tests__/conversation-routes-hidden-queue.test.ts +64 -1
  75. package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
  76. package/src/__tests__/conversation-store.test.ts +26 -10
  77. package/src/__tests__/conversation-summarize-route.test.ts +1 -1
  78. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +15 -6
  79. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -5
  80. package/src/__tests__/conversation-surfaces-app-control.test.ts +12 -11
  81. package/src/__tests__/conversation-surfaces-app-open.test.ts +11 -5
  82. package/src/__tests__/conversation-surfaces-data-persist.test.ts +5 -4
  83. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +967 -0
  84. package/src/__tests__/conversation-surfaces-persisted-info.test.ts +312 -0
  85. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +6 -5
  86. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -8
  87. package/src/__tests__/conversation-surfaces-standalone.test.ts +21 -11
  88. package/src/__tests__/conversation-surfaces-state-update.test.ts +7 -11
  89. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -6
  90. package/src/__tests__/conversation-surfaces-task-progress.test.ts +11 -6
  91. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +9 -6
  92. package/src/__tests__/conversation-tool-setup-attribution.test.ts +70 -7
  93. package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
  94. package/src/__tests__/credential-prompt-route.test.ts +57 -7
  95. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  96. package/src/__tests__/cu-unified-flow.test.ts +89 -28
  97. package/src/__tests__/custom-group-listing-visibility.test.ts +1 -1
  98. package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -1
  99. package/src/__tests__/discord-access-request-privacy.test.ts +366 -0
  100. package/src/__tests__/discord-requester-notice-privacy.test.ts +87 -0
  101. package/src/__tests__/disk-pressure-guard.test.ts +55 -55
  102. package/src/__tests__/disk-pressure-routes.test.ts +10 -10
  103. package/src/__tests__/disk-pressure-tools.test.ts +2 -2
  104. package/src/__tests__/disk-usage.test.ts +35 -20
  105. package/src/__tests__/document-sync-tags.test.ts +379 -0
  106. package/src/__tests__/document-update-default-surface.test.ts +28 -0
  107. package/src/__tests__/dynamic-skill-background-guard.test.ts +2 -2
  108. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -4
  109. package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
  110. package/src/__tests__/events-dev-bypass-actor.test.ts +183 -1
  111. package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
  112. package/src/__tests__/file-list-tool.test.ts +12 -10
  113. package/src/__tests__/file-ops-service.test.ts +151 -44
  114. package/src/__tests__/file-read-container-boundary.test.ts +137 -0
  115. package/src/__tests__/file-read-tool.test.ts +21 -9
  116. package/src/__tests__/file-url-path-guard.test.ts +83 -0
  117. package/src/__tests__/filesystem-tools.test.ts +82 -66
  118. package/src/__tests__/guardian-outbound-http.test.ts +8 -8
  119. package/src/__tests__/helpers/gateway-verification-sessions-stub.ts +14 -1
  120. package/src/__tests__/helpers/hub-clients.ts +42 -0
  121. package/src/__tests__/helpers/mock-conversation.ts +46 -0
  122. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +61 -57
  123. package/src/__tests__/history-repair.test.ts +112 -12
  124. package/src/__tests__/host-bash-proxy.test.ts +10 -5
  125. package/src/__tests__/host-bash-routes.test.ts +67 -0
  126. package/src/__tests__/host-browser-proxy.test.ts +2 -2
  127. package/src/__tests__/host-cu-proxy.test.ts +3 -7
  128. package/src/__tests__/host-file-proxy.test.ts +3 -7
  129. package/src/__tests__/host-file-read-tool.test.ts +5 -1
  130. package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
  131. package/src/__tests__/http-conversation-lineage.test.ts +2 -5
  132. package/src/__tests__/http-user-message-parity.test.ts +44 -0
  133. package/src/__tests__/identity-routes.test.ts +13 -13
  134. package/src/__tests__/image-conversion.test.ts +29 -26
  135. package/src/__tests__/image-source-path-reinject.test.ts +2 -2
  136. package/src/__tests__/inference-profile-session-handler.test.ts +95 -0
  137. package/src/__tests__/inference-profile-session-ipc.test.ts +25 -0
  138. package/src/__tests__/introduction-card-resolver.test.ts +6 -1
  139. package/src/__tests__/list-messages-attachments.test.ts +56 -32
  140. package/src/__tests__/list-messages-background-tool-completion.test.ts +4 -4
  141. package/src/__tests__/list-messages-client-message-id.test.ts +2 -2
  142. package/src/__tests__/list-messages-hidden-metadata.test.ts +16 -16
  143. package/src/__tests__/list-messages-page-latest.test.ts +61 -61
  144. package/src/__tests__/list-messages-provider-error.test.ts +6 -6
  145. package/src/__tests__/list-messages-queued.test.ts +49 -6
  146. package/src/__tests__/list-messages-system-card.test.ts +4 -4
  147. package/src/__tests__/list-messages-tool-merge.test.ts +9 -9
  148. package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
  149. package/src/__tests__/llm-usage-store.test.ts +55 -0
  150. package/src/__tests__/manual-token-reconciliation.test.ts +45 -0
  151. package/src/__tests__/mcp-tool-annotations-risk.test.ts +133 -0
  152. package/src/__tests__/media-stream-server-integration.test.ts +36 -0
  153. package/src/__tests__/migration-export-http.test.ts +1 -1
  154. package/src/__tests__/migration-import-commit-http.test.ts +1 -1
  155. package/src/__tests__/migration-import-preflight-http.test.ts +1 -1
  156. package/src/__tests__/migration-validate-http.test.ts +1 -1
  157. package/src/__tests__/mtime-cache.test.ts +65 -3
  158. package/src/__tests__/notification-platform-adapter.test.ts +104 -0
  159. package/src/__tests__/notification-schedule-notify-dedup.test.ts +1 -1
  160. package/src/__tests__/oauth-provider-profiles.test.ts +4 -2
  161. package/src/__tests__/oauth-provider-seed-logos.test.ts +65 -0
  162. package/src/__tests__/oauth-provider-serializer.test.ts +17 -0
  163. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  164. package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
  165. package/src/__tests__/path-policy.test.ts +164 -1
  166. package/src/__tests__/persist-media-references.test.ts +2 -2
  167. package/src/__tests__/persist-unsendable-image-downscale.test.ts +5 -5
  168. package/src/__tests__/persist-unsendable-image.test.ts +23 -23
  169. package/src/__tests__/persona-resolver.test.ts +154 -0
  170. package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
  171. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
  172. package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
  173. package/src/__tests__/profile-availability-incomplete.test.ts +113 -0
  174. package/src/__tests__/profiler-routes.test.ts +13 -13
  175. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
  176. package/src/__tests__/proxy-approval-callback.test.ts +3 -3
  177. package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
  178. package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
  179. package/src/__tests__/resolve-trust-class.test.ts +20 -0
  180. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
  181. package/src/__tests__/run-due-schedules.test.ts +174 -0
  182. package/src/__tests__/runtime-attachment-metadata.test.ts +15 -7
  183. package/src/__tests__/scaffold-managed-skill-tool.test.ts +166 -0
  184. package/src/__tests__/schedule-routes.test.ts +730 -8
  185. package/src/__tests__/schedule-store.test.ts +540 -0
  186. package/src/__tests__/schedule-tools.test.ts +103 -0
  187. package/src/__tests__/scheduler-wake.test.ts +5 -0
  188. package/src/__tests__/secret-routes-platform-proxy.test.ts +29 -1
  189. package/src/__tests__/secure-keys.test.ts +91 -0
  190. package/src/__tests__/server-history-render.test.ts +97 -7
  191. package/src/__tests__/size-guard.test.ts +8 -8
  192. package/src/__tests__/skill-docs-sync-guard.test.ts +3 -2
  193. package/src/__tests__/skill-tool-factory.test.ts +247 -0
  194. package/src/__tests__/skills-install-staging.test.ts +15 -6
  195. package/src/__tests__/slack-edit-ordering-characterization.test.ts +103 -0
  196. package/src/__tests__/slack-inbound-verification.test.ts +4 -1
  197. package/src/__tests__/slack-mention-binding-invariance.test.ts +103 -0
  198. package/src/__tests__/slack-mention-provider-leak-audit.test.ts +135 -0
  199. package/src/__tests__/starter-task-flow.test.ts +5 -4
  200. package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
  201. package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
  202. package/src/__tests__/subagent-allowlist-validation.test.ts +26 -4
  203. package/src/__tests__/subagent-cron-run-attribution.test.ts +301 -0
  204. package/src/__tests__/subagent-disposal.test.ts +10 -0
  205. package/src/__tests__/subagent-fork-notifications.test.ts +10 -0
  206. package/src/__tests__/subagent-fork-prompt-role.test.ts +85 -11
  207. package/src/__tests__/subagent-fork-spawn.test.ts +15 -7
  208. package/src/__tests__/subagent-manager-notify.test.ts +148 -0
  209. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  210. package/src/__tests__/subagent-persistence.test.ts +141 -0
  211. package/src/__tests__/subagent-role-registry.test.ts +172 -63
  212. package/src/__tests__/subagent-role-resolution.test.ts +88 -0
  213. package/src/__tests__/subagent-spawn-and-await.test.ts +48 -0
  214. package/src/__tests__/subagent-spawn-tool-fork.test.ts +111 -10
  215. package/src/__tests__/subagent-terminal-message.test.ts +88 -1
  216. package/src/__tests__/subagent-tool-gate-mode.test.ts +225 -10
  217. package/src/__tests__/subagent-tools.test.ts +1901 -108
  218. package/src/__tests__/surface-completion-compaction-boundary.test.ts +124 -0
  219. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +298 -0
  220. package/src/__tests__/system-prompt.test.ts +82 -7
  221. package/src/__tests__/tool-grant-request-escalation.test.ts +40 -1
  222. package/src/__tests__/tool-result-spool.test.ts +74 -18
  223. package/src/__tests__/tool-side-effects-documents.test.ts +174 -0
  224. package/src/__tests__/tool-side-effects-hook-failures.test.ts +129 -0
  225. package/src/__tests__/tools-audio-read.test.ts +10 -10
  226. package/src/__tests__/tools-get-route.test.ts +85 -12
  227. package/src/__tests__/tools-list-cli.test.ts +171 -0
  228. package/src/__tests__/top-level-renderer.test.ts +54 -1
  229. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +68 -1
  230. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -1
  231. package/src/__tests__/turn-tail-chain.test.ts +135 -0
  232. package/src/__tests__/ui-choice-copy-surfaces.test.ts +6 -4
  233. package/src/__tests__/ui-visual-surface.test.ts +5 -4
  234. package/src/__tests__/ui-work-result-surface.test.ts +5 -4
  235. package/src/__tests__/unified-turn-context-visible-app.test.ts +7 -8
  236. package/src/__tests__/usage-routes.test.ts +62 -0
  237. package/src/__tests__/verification-outbound-delivery.test.ts +176 -0
  238. package/src/__tests__/visible-app-context.test.ts +20 -4
  239. package/src/__tests__/web-search-history.test.ts +96 -0
  240. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +6 -6
  241. package/src/__tests__/workspace-migration-140-repair-seed-pinned-memory-v3-live.test.ts +330 -0
  242. package/src/acp/failure-error.ts +3 -5
  243. package/src/agent/attachments.ts +26 -24
  244. package/src/agent/history-repair/history-repair.ts +27 -26
  245. package/src/agent/image-optimize.ts +6 -6
  246. package/src/agent/loop.ts +40 -22
  247. package/src/agent/message-types.ts +3 -3
  248. package/src/api/constants/document-tools.ts +55 -0
  249. package/src/api/events/question-answered.ts +53 -0
  250. package/src/api/events/tool-result.ts +7 -0
  251. package/src/api/index.ts +13 -0
  252. package/src/api/responses/conversation-message.ts +11 -0
  253. package/src/api/surfaces-oauth-connect.test.ts +96 -0
  254. package/src/api/surfaces.ts +36 -8
  255. package/src/approvals/guardian-channel-delivery.ts +83 -0
  256. package/src/approvals/guardian-expiry-notifier.ts +13 -11
  257. package/src/approvals/guardian-request-resolvers.ts +222 -135
  258. package/src/calls/__tests__/voice-session-bridge.test.ts +50 -0
  259. package/src/calls/media-stream-server.ts +9 -1
  260. package/src/calls/voice-session-bridge.ts +38 -0
  261. package/src/channels/__tests__/gateway-verification-sessions.test.ts +3 -3
  262. package/src/channels/config.ts +7 -2
  263. package/src/channels/gateway-verification-sessions.ts +7 -2
  264. package/src/channels/types.ts +7 -1
  265. package/src/cli/commands/__tests__/cache.test.ts +7 -4
  266. package/src/cli/commands/__tests__/cli-test-harness.ts +82 -0
  267. package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
  268. package/src/cli/commands/__tests__/inference-models.test.ts +14 -0
  269. package/src/cli/commands/__tests__/inference-profiles.test.ts +44 -1
  270. package/src/cli/commands/__tests__/inference-providers.test.ts +7 -0
  271. package/src/cli/commands/__tests__/plugins.test.ts +692 -0
  272. package/src/cli/commands/__tests__/schedules.test.ts +136 -37
  273. package/src/cli/commands/conversations.ts +7 -3
  274. package/src/cli/commands/credentials.help.ts +7 -0
  275. package/src/cli/commands/inference-profiles.ts +9 -3
  276. package/src/cli/commands/inference.help.ts +16 -2
  277. package/src/cli/commands/mcp.help.ts +4 -2
  278. package/src/cli/commands/memory/memory-retrospective.ts +18 -0
  279. package/src/cli/commands/notifications.ts +3 -6
  280. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +16 -0
  281. package/src/cli/commands/oauth/connect-surface-guidance.ts +25 -7
  282. package/src/cli/commands/oauth/connect.test.ts +26 -1
  283. package/src/cli/commands/oauth/connect.ts +4 -1
  284. package/src/cli/commands/oauth/index.help.ts +3 -1
  285. package/src/cli/commands/oauth/providers.ts +3 -0
  286. package/src/cli/commands/plugins.help.ts +25 -2
  287. package/src/cli/commands/plugins.ts +289 -10
  288. package/src/cli/commands/schedules.help.ts +14 -12
  289. package/src/cli/commands/schedules.ts +19 -5
  290. package/src/cli/commands/tools.help.ts +4 -2
  291. package/src/cli/commands/tools.ts +38 -0
  292. package/src/cli/lib/__tests__/inspect-plugin.test.ts +1 -0
  293. package/src/cli/lib/__tests__/install-from-github.test.ts +79 -0
  294. package/src/cli/lib/__tests__/install-from-platform.test.ts +47 -1
  295. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +86 -7
  296. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +122 -0
  297. package/src/cli/lib/install-from-github.ts +61 -0
  298. package/src/cli/lib/install-from-platform.ts +13 -2
  299. package/src/cli/lib/plugin-surfaces.ts +124 -4
  300. package/src/cli/lib/upgrade-plugin.ts +18 -0
  301. package/src/cli.ts +1 -1
  302. package/src/config/__tests__/deployment-context-defaults.test.ts +73 -0
  303. package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
  304. package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
  305. package/src/config/__tests__/profile-tool-support.test.ts +131 -0
  306. package/src/config/bundled-skills/AGENTS.md +1 -1
  307. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  308. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
  309. package/src/config/bundled-skills/schedule/SKILL.md +5 -1
  310. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  311. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  312. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
  313. package/src/config/bundled-skills/subagent/SKILL.md +53 -22
  314. package/src/config/bundled-skills/subagent/TOOLS.json +14 -13
  315. package/src/config/bundled-skills/visualize/SKILL.md +1 -1
  316. package/src/config/default-profile-catalog.ts +6 -2
  317. package/src/config/feature-flag-registry.json +25 -17
  318. package/src/config/llm-resolver.ts +45 -15
  319. package/src/config/loader.ts +70 -1
  320. package/src/config/profile-tool-support.ts +110 -0
  321. package/src/config/sanitize-for-transfer.ts +11 -0
  322. package/src/config/schema.ts +4 -0
  323. package/src/config/schemas/__tests__/stt.test.ts +20 -5
  324. package/src/config/schemas/channels.ts +13 -0
  325. package/src/config/schemas/memory-retrospective.ts +25 -0
  326. package/src/config/schemas/plugin-updates.ts +55 -0
  327. package/src/config/schemas/services.ts +8 -4
  328. package/src/config/schemas/stt.ts +17 -5
  329. package/src/config/schemas/timeouts.ts +1 -1
  330. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +16 -0
  331. package/src/contacts/guardian-delivery-reader.ts +29 -2
  332. package/src/context/compactor.ts +4 -4
  333. package/src/context/post-turn-tool-result-truncation.ts +32 -16
  334. package/src/context/strip-injections.ts +42 -0
  335. package/src/context/tool-result-spool.ts +32 -18
  336. package/src/credential-execution/ces-runtime.ts +3 -0
  337. package/src/credential-execution/client.ts +22 -1
  338. package/src/credential-execution/process-manager.test.ts +99 -0
  339. package/src/credential-execution/process-manager.ts +62 -16
  340. package/src/daemon/__tests__/abort-null-controller.test.ts +141 -5
  341. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +8 -12
  342. package/src/daemon/__tests__/install-assistant-command.test.ts +234 -0
  343. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
  344. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
  345. package/src/daemon/channel-ui-capability.ts +4 -4
  346. package/src/daemon/conversation-agent-loop-handlers.ts +117 -61
  347. package/src/daemon/conversation-agent-loop.ts +318 -148
  348. package/src/daemon/conversation-attachments.ts +1 -1
  349. package/src/daemon/conversation-error.ts +8 -1
  350. package/src/daemon/conversation-initial-prompt.ts +16 -38
  351. package/src/daemon/conversation-lifecycle.ts +154 -32
  352. package/src/daemon/conversation-messaging.ts +106 -43
  353. package/src/daemon/conversation-process.ts +131 -33
  354. package/src/daemon/conversation-queue-manager.ts +10 -0
  355. package/src/daemon/conversation-runtime-assembly.ts +9 -4
  356. package/src/daemon/conversation-store.ts +52 -3
  357. package/src/daemon/conversation-surface-state.ts +9 -0
  358. package/src/daemon/conversation-surfaces.ts +403 -210
  359. package/src/daemon/conversation-tool-setup.ts +56 -7
  360. package/src/daemon/conversation-turn-finalize.ts +134 -89
  361. package/src/daemon/conversation.ts +111 -27
  362. package/src/daemon/disk-pressure-guard-lifecycle.ts +2 -2
  363. package/src/daemon/disk-pressure-guard.ts +3 -3
  364. package/src/daemon/doordash-steps.ts +5 -5
  365. package/src/daemon/external-plugins-bootstrap.ts +7 -1
  366. package/src/daemon/handlers/config-channels.ts +48 -5
  367. package/src/daemon/handlers/conversation-history.ts +1 -1
  368. package/src/daemon/handlers/conversations.ts +62 -17
  369. package/src/daemon/handlers/shared.ts +12 -0
  370. package/src/daemon/handlers/skills.ts +11 -1
  371. package/src/daemon/identity-helpers.ts +38 -10
  372. package/src/daemon/install-assistant-command.ts +322 -0
  373. package/src/daemon/lifecycle.ts +14 -3
  374. package/src/daemon/message-types/conversations.ts +6 -7
  375. package/src/daemon/message-types/sync.ts +1 -0
  376. package/src/daemon/persist-media-references.ts +23 -14
  377. package/src/daemon/process-message.ts +6 -6
  378. package/src/daemon/tool-setup-types.ts +19 -122
  379. package/src/daemon/tool-side-effects.ts +17 -94
  380. package/src/daemon/trust-context-types.ts +39 -0
  381. package/src/daemon/trust-context.ts +14 -6
  382. package/src/daemon/turn-tail-chain.ts +73 -0
  383. package/src/daemon/web-search-history.ts +47 -33
  384. package/src/heartbeat/heartbeat-service.ts +6 -6
  385. package/src/ipc/__tests__/socket-path.test.ts +21 -0
  386. package/src/ipc/assistant-server.ts +17 -10
  387. package/src/ipc/cli-client.ts +10 -1
  388. package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
  389. package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
  390. package/src/ipc/socket-cleanup.ts +9 -7
  391. package/src/ipc/socket-path.ts +4 -96
  392. package/src/live-voice/__tests__/live-voice-archive.test.ts +16 -16
  393. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
  394. package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
  395. package/src/live-voice/__tests__/live-voice-integration.test.ts +7 -5
  396. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +265 -0
  397. package/src/live-voice/__tests__/live-voice-stt.test.ts +6 -1
  398. package/src/live-voice/__tests__/live-voice-vad.test.ts +35 -15
  399. package/src/live-voice/__tests__/protocol.test.ts +52 -0
  400. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +3 -0
  401. package/src/live-voice/live-voice-archive.ts +13 -13
  402. package/src/live-voice/live-voice-connection.ts +7 -1
  403. package/src/live-voice/live-voice-photo.ts +169 -0
  404. package/src/live-voice/live-voice-session.ts +108 -4
  405. package/src/live-voice/protocol.ts +89 -1
  406. package/src/mcp/client.ts +10 -0
  407. package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
  408. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
  409. package/src/messaging/providers/callback-routing.ts +1 -0
  410. package/src/messaging/providers/discord/api.ts +312 -0
  411. package/src/messaging/providers/discord/dm-delivery.test.ts +204 -0
  412. package/src/messaging/providers/discord/render.test.ts +204 -0
  413. package/src/messaging/providers/discord/render.ts +195 -0
  414. package/src/messaging/providers/discord/send.test.ts +179 -0
  415. package/src/messaging/providers/discord/send.ts +212 -0
  416. package/src/messaging/providers/discord/transport.ts +81 -0
  417. package/src/messaging/providers/index.ts +3 -1
  418. package/src/messaging/providers/retry-policy.test.ts +247 -0
  419. package/src/messaging/providers/retry-policy.ts +206 -0
  420. package/src/messaging/providers/slack/mention-source.test.ts +285 -0
  421. package/src/messaging/providers/slack/mention-source.ts +287 -0
  422. package/src/messaging/providers/telegram-bot/api.ts +47 -130
  423. package/src/messaging/providers/whatsapp/api.ts +47 -136
  424. package/src/monitoring/__tests__/db-integrity-sample.test.ts +4 -1
  425. package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
  426. package/src/monitoring/db-integrity-sample.ts +6 -1
  427. package/src/monitoring/plugin-auto-update.ts +359 -0
  428. package/src/monitoring/resource-sampler.ts +21 -5
  429. package/src/monitoring/worker.ts +13 -0
  430. package/src/notifications/__tests__/assistant-reply-producer.test.ts +375 -2
  431. package/src/notifications/__tests__/broadcaster.test.ts +37 -1
  432. package/src/notifications/__tests__/copy-composer.test.ts +165 -0
  433. package/src/notifications/__tests__/emit-signal-pipeline-failure.test.ts +205 -0
  434. package/src/notifications/__tests__/notification-utils.test.ts +249 -0
  435. package/src/notifications/adapters/platform.ts +31 -2
  436. package/src/notifications/assistant-reply-producer.ts +79 -32
  437. package/src/notifications/broadcaster.ts +30 -10
  438. package/src/notifications/copy-composer.ts +69 -0
  439. package/src/notifications/emit-signal.ts +82 -15
  440. package/src/notifications/events-store.ts +10 -2
  441. package/src/notifications/home-feed-side-effect.ts +6 -3
  442. package/src/notifications/notification-utils.ts +173 -5
  443. package/src/notifications/signal.ts +12 -0
  444. package/src/oauth/AGENTS.md +4 -2
  445. package/src/oauth/byo-connection.ts +6 -1
  446. package/src/oauth/credential-token-resolver.ts +3 -10
  447. package/src/oauth/manual-token-connection.ts +29 -64
  448. package/src/oauth/manual-token-providers.ts +62 -0
  449. package/src/oauth/provider-serializer.ts +13 -0
  450. package/src/oauth/seed-providers.ts +62 -11
  451. package/src/onboarding/onboarding-events-store.ts +91 -1
  452. package/src/permissions/question-prompter.test.ts +4 -0
  453. package/src/permissions/question-prompter.ts +21 -4
  454. package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
  455. package/src/persistence/attachments-store.ts +15 -15
  456. package/src/persistence/conversation-attention-store.ts +30 -10
  457. package/src/persistence/conversation-bootstrap.ts +17 -0
  458. package/src/persistence/conversation-crud.ts +363 -97
  459. package/src/persistence/conversation-group-migration.ts +1 -1
  460. package/src/persistence/conversation-key-store.ts +38 -10
  461. package/src/persistence/conversation-lineage.ts +226 -0
  462. package/src/persistence/conversation-plugin-facade.ts +28 -3
  463. package/src/persistence/conversation-queries.ts +207 -33
  464. package/src/persistence/conversation-types.ts +107 -1
  465. package/src/persistence/jobs-store.ts +32 -8
  466. package/src/persistence/llm-usage-store.ts +22 -0
  467. package/src/persistence/migrations/361-normalize-managed-connection-rows.test.ts +168 -0
  468. package/src/persistence/migrations/361-normalize-managed-connection-rows.ts +76 -0
  469. package/src/persistence/migrations/362-add-conversation-subagent-kind.test.ts +187 -0
  470. package/src/persistence/migrations/362-add-conversation-subagent-kind.ts +93 -0
  471. package/src/persistence/migrations/363-backfill-schedule-inference-profile.test.ts +229 -0
  472. package/src/persistence/migrations/363-backfill-schedule-inference-profile.ts +127 -0
  473. package/src/persistence/migrations/364-add-schedule-source-key.test.ts +120 -0
  474. package/src/persistence/migrations/364-add-schedule-source-key.ts +43 -0
  475. package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
  476. package/src/persistence/schema/conversations.ts +32 -0
  477. package/src/persistence/schema/infrastructure.ts +3 -0
  478. package/src/persistence/steps.ts +30 -0
  479. package/src/persistence/subagent-store.ts +148 -6
  480. package/src/platform/managed-speech.test.ts +20 -0
  481. package/src/platform/managed-speech.ts +9 -0
  482. package/src/plugin-api/index.ts +8 -0
  483. package/src/plugin-api/webhook-url.ts +151 -0
  484. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +82 -0
  485. package/src/plugins/__tests__/source-fingerprint.test.ts +5 -1
  486. package/src/plugins/collect-source-versions.ts +4 -27
  487. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -5
  488. package/src/plugins/defaults/exploration-drift/package.json +1 -1
  489. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +4 -2
  490. package/src/plugins/defaults/image-recovery/recover.ts +67 -57
  491. package/src/plugins/defaults/index.ts +1 -1
  492. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +64 -18
  493. package/src/plugins/defaults/memory/__tests__/jobs-worker-outcome-contract.test.ts +242 -0
  494. package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
  495. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
  496. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +511 -19
  497. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +39 -3
  498. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +493 -0
  499. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +5 -4
  500. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -1
  501. package/src/plugins/defaults/memory/job-handlers.ts +87 -5
  502. package/src/plugins/defaults/memory/jobs-worker.ts +60 -6
  503. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +11 -0
  504. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
  505. package/src/plugins/defaults/memory/memory-retrospective-job.ts +342 -60
  506. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +23 -3
  507. package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +9 -4
  508. package/src/plugins/defaults/memory/substrate/sweep-job.ts +7 -0
  509. package/src/plugins/defaults/memory/v1/graph/injection.ts +1 -1
  510. package/src/plugins/defaults/memory/v2/router.ts +8 -0
  511. package/src/plugins/defaults/turn-context/unified-turn-context.ts +1 -2
  512. package/src/plugins/external-plugin-loader.ts +17 -8
  513. package/src/plugins/installed-plugin-dirs.ts +101 -0
  514. package/src/plugins/mtime-cache.ts +34 -22
  515. package/src/plugins/types.ts +69 -8
  516. package/src/prompts/persona-resolver.ts +6 -2
  517. package/src/prompts/system-prompt.ts +72 -17
  518. package/src/providers/__tests__/inference.test.ts +40 -0
  519. package/src/providers/__tests__/preflight-resolved-config.test.ts +58 -1
  520. package/src/providers/__tests__/retry-callsite.test.ts +67 -0
  521. package/src/providers/__tests__/vellum-connection-routing.test.ts +42 -11
  522. package/src/providers/anthropic/client.ts +112 -95
  523. package/src/providers/connection-resolution.ts +35 -1
  524. package/src/providers/gemini/client.ts +5 -5
  525. package/src/providers/inference/adapter-factory.ts +16 -10
  526. package/src/providers/inference/auth.ts +16 -0
  527. package/src/providers/inference/connection-availability.ts +164 -12
  528. package/src/providers/inference/connections.ts +25 -1
  529. package/src/providers/media-resolve.ts +21 -11
  530. package/src/providers/model-catalog.ts +15 -0
  531. package/src/providers/openai/chat-completions-provider.ts +4 -4
  532. package/src/providers/openai/responses-provider.ts +12 -25
  533. package/src/providers/registry.ts +9 -4
  534. package/src/providers/retry.ts +25 -0
  535. package/src/providers/server-tool-pairing.ts +134 -0
  536. package/src/providers/speech-to-text/__tests__/resolve.test.ts +187 -6
  537. package/src/providers/speech-to-text/deepgram.ts +1 -0
  538. package/src/providers/speech-to-text/resolve.ts +80 -10
  539. package/src/providers/speech-to-text/vellum-managed.ts +7 -1
  540. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
  541. package/src/providers/types.ts +26 -6
  542. package/src/providers/vellum-model-routing.ts +6 -7
  543. package/src/runtime/AGENTS.md +2 -0
  544. package/src/runtime/__tests__/agent-wake.test.ts +16 -16
  545. package/src/runtime/__tests__/background-job-runner.test.ts +0 -144
  546. package/src/runtime/__tests__/desktop-presence.test.ts +162 -0
  547. package/src/runtime/agent-wake.ts +63 -24
  548. package/src/runtime/assistant-event-hub.ts +87 -12
  549. package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
  550. package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
  551. package/src/runtime/auth/same-actor.ts +56 -8
  552. package/src/runtime/auth/scopes.ts +12 -0
  553. package/src/runtime/auth/types.ts +2 -0
  554. package/src/runtime/background-job-runner.ts +5 -27
  555. package/src/runtime/channel-readiness-service.ts +59 -3
  556. package/src/runtime/channel-readiness-types.ts +18 -0
  557. package/src/runtime/desktop-presence.ts +69 -0
  558. package/src/runtime/http-server.ts +12 -0
  559. package/src/runtime/local-actor-identity.ts +11 -9
  560. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  561. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  562. package/src/runtime/routes/__tests__/client-routes.test.ts +186 -14
  563. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +547 -1
  564. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +162 -7
  565. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
  566. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +340 -3
  567. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +28 -1
  568. package/src/runtime/routes/__tests__/plugins-routes.test.ts +173 -1
  569. package/src/runtime/routes/__tests__/question-routes.test.ts +64 -0
  570. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
  571. package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
  572. package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
  573. package/src/runtime/routes/archive-utils.ts +16 -12
  574. package/src/runtime/routes/attachment-routes.ts +13 -6
  575. package/src/runtime/routes/canned-reply-release.ts +54 -0
  576. package/src/runtime/routes/channel-verification-routes.ts +2 -27
  577. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +5 -0
  578. package/src/runtime/routes/client-routes.ts +114 -2
  579. package/src/runtime/routes/conversation-cli-routes.ts +10 -12
  580. package/src/runtime/routes/conversation-list-routes.ts +59 -10
  581. package/src/runtime/routes/conversation-management-routes.ts +47 -18
  582. package/src/runtime/routes/conversation-query-routes.ts +32 -53
  583. package/src/runtime/routes/conversation-routes.ts +360 -293
  584. package/src/runtime/routes/credential-prompt-routes.ts +23 -2
  585. package/src/runtime/routes/debug-routes.ts +4 -1
  586. package/src/runtime/routes/default-provider-routes.ts +7 -11
  587. package/src/runtime/routes/documents-routes.ts +44 -1
  588. package/src/runtime/routes/events-routes.ts +28 -1
  589. package/src/runtime/routes/host-app-control-routes.ts +2 -0
  590. package/src/runtime/routes/host-bash-routes.ts +2 -0
  591. package/src/runtime/routes/host-browser-routes.ts +2 -0
  592. package/src/runtime/routes/host-cu-routes.ts +2 -0
  593. package/src/runtime/routes/host-file-routes.ts +2 -0
  594. package/src/runtime/routes/host-transfer-routes.ts +4 -0
  595. package/src/runtime/routes/identity-routes.ts +6 -6
  596. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  597. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -3
  598. package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
  599. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -2
  600. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +14 -11
  601. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -17
  602. package/src/runtime/routes/inference-profile-availability-guard.ts +171 -0
  603. package/src/runtime/routes/inference-profile-session-handler.ts +26 -2
  604. package/src/runtime/routes/inference-profile-session-routes.ts +2 -1
  605. package/src/runtime/routes/inference-profiles-routes.ts +152 -73
  606. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -16
  607. package/src/runtime/routes/llm-call-sites-routes.ts +13 -0
  608. package/src/runtime/routes/log-export-routes.ts +1 -1
  609. package/src/runtime/routes/notification-routes.ts +0 -14
  610. package/src/runtime/routes/oauth-providers.ts +1 -0
  611. package/src/runtime/routes/plugins-routes.ts +106 -1
  612. package/src/runtime/routes/profiler-routes.ts +4 -2
  613. package/src/runtime/routes/question-routes.ts +28 -10
  614. package/src/runtime/routes/retrospective-routes.ts +9 -3
  615. package/src/runtime/routes/schedule-routes.ts +210 -12
  616. package/src/runtime/routes/secret-routes.ts +40 -23
  617. package/src/runtime/routes/settings-routes.ts +41 -13
  618. package/src/runtime/routes/skills-routes.ts +79 -0
  619. package/src/runtime/routes/ui-snapshot-routes.ts +1 -0
  620. package/src/runtime/routes/wake-conversation-routes.ts +23 -0
  621. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  622. package/src/runtime/services/conversation-serializer.ts +24 -9
  623. package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
  624. package/src/runtime/sync/resource-sync-events.ts +79 -1
  625. package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
  626. package/src/runtime/sync/worker-daemon-notify.ts +44 -6
  627. package/src/runtime/verification-outbound-actions.ts +250 -222
  628. package/src/runtime/verification-templates.ts +41 -30
  629. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +614 -0
  630. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1066 -0
  631. package/src/schedule/inference-profile.ts +64 -3
  632. package/src/schedule/plugin-schedule-declarations.ts +518 -0
  633. package/src/schedule/plugin-schedule-reconciler.ts +605 -0
  634. package/src/schedule/plugin-schedules-gate.ts +23 -0
  635. package/src/schedule/schedule-store.ts +520 -26
  636. package/src/schedule/scheduler.ts +38 -0
  637. package/src/security/credential-backend.ts +51 -4
  638. package/src/security/encrypted-store.ts +64 -42
  639. package/src/security/secure-keys.ts +55 -7
  640. package/src/skills/catalog-install.ts +20 -7
  641. package/src/skills/inline-command-runner.ts +2 -7
  642. package/src/skills/skill-history.test.ts +269 -0
  643. package/src/skills/skill-history.ts +250 -0
  644. package/src/skills/skillssh-registry.ts +1 -1
  645. package/src/stt/daemon-batch-transcriber.ts +8 -6
  646. package/src/subagent/__tests__/consult-prompt.test.ts +25 -0
  647. package/src/subagent/consult-prompt.ts +3 -1
  648. package/src/subagent/consult-transcript.ts +1 -1
  649. package/src/subagent/manager.ts +304 -44
  650. package/src/subagent/progress-events.ts +35 -0
  651. package/src/subagent/role-resolution.ts +88 -0
  652. package/src/subagent/types.ts +302 -67
  653. package/src/subagent/validate-allowlists.ts +3 -4
  654. package/src/telegram/__tests__/webhook-health.test.ts +73 -1
  655. package/src/telegram/webhook-health.ts +101 -3
  656. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +2 -0
  657. package/src/telemetry/activation-funnel.ts +6 -1
  658. package/src/telemetry/live-voice-funnel.ts +75 -0
  659. package/src/telemetry/telemetry-event-sources.ts +6 -0
  660. package/src/telemetry/telemetry-wire-source.json +1 -1
  661. package/src/telemetry/telemetry-wire.generated.ts +2 -0
  662. package/src/telemetry/types.ts +23 -4
  663. package/src/telemetry/usage-telemetry-reporter.test.ts +4 -0
  664. package/src/tools/__tests__/subagent-workflow-tool-input-schemas.test.ts +1 -1
  665. package/src/tools/ask-question/ask-question-tool.test.ts +118 -3
  666. package/src/tools/ask-question/ask-question-tool.ts +56 -2
  667. package/src/tools/browser/browser-execution.ts +1 -1
  668. package/src/tools/calls/call-start.ts +8 -5
  669. package/src/tools/conversation-groups/move_to_group.ts +10 -12
  670. package/src/tools/document/document-tool.ts +11 -4
  671. package/src/tools/execution-timeout.ts +9 -2
  672. package/src/tools/executor.ts +7 -6
  673. package/src/tools/filesystem/edit.ts +1 -1
  674. package/src/tools/filesystem/list.ts +1 -1
  675. package/src/tools/filesystem/read.ts +7 -13
  676. package/src/tools/filesystem/search.ts +23 -21
  677. package/src/tools/filesystem/write.ts +4 -1
  678. package/src/tools/host-filesystem/edit.ts +7 -2
  679. package/src/tools/host-filesystem/read.ts +21 -8
  680. package/src/tools/host-filesystem/write.ts +10 -2
  681. package/src/tools/host-terminal/host-shell.ts +2 -2
  682. package/src/tools/mcp/mcp-tool-factory.ts +25 -1
  683. package/src/tools/permission-checker.ts +8 -3
  684. package/src/tools/schedule/delete.ts +13 -1
  685. package/src/tools/schedule/list.ts +14 -3
  686. package/src/tools/schedule/update.ts +56 -33
  687. package/src/tools/shared/filesystem/audio-read.ts +6 -4
  688. package/src/tools/shared/filesystem/file-ops-service.ts +176 -116
  689. package/src/tools/shared/filesystem/image-read.ts +12 -12
  690. package/src/tools/shared/filesystem/path-policy.ts +70 -5
  691. package/src/tools/shared/filesystem/size-guard.ts +6 -6
  692. package/src/tools/shared/input-misuse.ts +72 -0
  693. package/src/tools/skills/scaffold-managed.ts +82 -0
  694. package/src/tools/skills/skill-tool-factory.ts +14 -1
  695. package/src/tools/subagent/consult-deadline.ts +12 -7
  696. package/src/tools/subagent/message.ts +3 -1
  697. package/src/tools/subagent/read.ts +66 -4
  698. package/src/tools/subagent/spawn.ts +500 -78
  699. package/src/tools/tool-approval-handler.ts +45 -4
  700. package/src/tools/types.ts +22 -3
  701. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  702. package/src/tools/ui-surface/visual-validation.ts +2 -0
  703. package/src/usage/__tests__/subagent-attribution.test.ts +109 -0
  704. package/src/usage/subagent-attribution.ts +108 -0
  705. package/src/util/__tests__/sqlite-retry.test.ts +112 -0
  706. package/src/util/abort-reasons.ts +16 -0
  707. package/src/util/ansi.ts +44 -0
  708. package/src/util/disk-usage.ts +17 -13
  709. package/src/util/image-conversion.ts +20 -13
  710. package/src/util/schedule-source-key.ts +23 -0
  711. package/src/util/worker-process.ts +7 -1
  712. package/src/workspace/git-service.ts +33 -9
  713. package/src/workspace/migrations/140-repair-seed-pinned-memory-v3-live.ts +260 -0
  714. package/src/workspace/migrations/141-stt-english-default-to-multilingual.ts +111 -0
  715. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +126 -0
  716. package/src/workspace/migrations/registry.ts +4 -0
  717. package/src/workspace/top-level-renderer.ts +15 -2
  718. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -357
  719. package/src/daemon/install-symlink.ts +0 -207
  720. package/src/notifications/deferred-emit.ts +0 -147
@@ -16,6 +16,7 @@ import {
16
16
  lte,
17
17
  notInArray,
18
18
  or,
19
+ type SQL,
19
20
  sql,
20
21
  } from "drizzle-orm";
21
22
  import { v4 as uuid, v7 as uuidv7 } from "uuid";
@@ -70,11 +71,24 @@ import {
70
71
  } from "./conversation-disk-view.js";
71
72
  import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
72
73
  import { ensureGroupMigration } from "./conversation-group-migration.js";
74
+ import {
75
+ isReferentialFork,
76
+ type LineageBound,
77
+ type LineageConversationRow,
78
+ lineageMessageFilter,
79
+ lineageMessagesAfterFilter,
80
+ type LineageSegment,
81
+ REFERENTIAL_FORK_STRATEGY,
82
+ resolveConversationLineage,
83
+ } from "./conversation-lineage.js";
73
84
  import { deleteConversationRowsInBatches } from "./conversation-row-batch-delete.js";
74
85
  import {
75
86
  BACKGROUND_CONVERSATION_TYPES,
76
87
  type ConversationCreateType,
88
+ type ConversationOrigin,
77
89
  isHiddenMessageMetadata,
90
+ PINNED_GROUP_ID,
91
+ UNGROUPED_GROUP_ID,
78
92
  } from "./conversation-types.js";
79
93
  import { runAsyncSqlite } from "./db-async-query.js";
80
94
  import {
@@ -252,9 +266,10 @@ export const messageMetadataSchema = z
252
266
  /**
253
267
  * Optional client-side metadata bag attached to user messages at persist
254
268
  * time. `os` carries the client-reported OS surface ("web" | "ios" |
255
- * "macos" | "android") from the request body's `clientOs` field, stamped
256
- * by `persistQueuedMessageBody` the transport `userMessageInterface` is
257
- * "web" for the web, iOS, and macOS apps alike, so this is the only
269
+ * "macos" | "windows" | "android") from the request body's `clientOs`
270
+ * field, stamped by `persistQueuedMessageBody`. The transport
271
+ * `userMessageInterface` is
272
+ * "web" for the web, mobile, and desktop apps alike, so this is the only
258
273
  * per-platform attribution. `browser_family` / `browser_version` /
259
274
  * `interface_version` (and an `os` override) come from the sanitized
260
275
  * `x-vellum-*` client-metadata headers read by `handleSendMessage`
@@ -264,6 +279,20 @@ export const messageMetadataSchema = z
264
279
  * require a migration -- dbt can unpack later via JSON_VALUE.
265
280
  */
266
281
  client: z.record(z.string(), z.unknown()).optional(),
282
+ /**
283
+ * True when the `client.os` above was reported by this row's own request
284
+ * or transport rather than inherited from the conversation's live client
285
+ * state. `Conversation.clientOs` is refreshed only by a message that
286
+ * carries transport metadata, so a transport-less turn (surface action,
287
+ * signal ingress) stamps the OS of an earlier send. Consumers that must
288
+ * not misattribute a turn to a surface require this marker (the
289
+ * `chat.assistant_reply` presence gate, which drops the push when the Mac
290
+ * that opened the turn is attended). Absent reads as "origin unknown".
291
+ * Deliberately a sibling of `client` rather than an entry inside it:
292
+ * `turn-events-store` forwards `$.client` verbatim onto
293
+ * `TurnTelemetryEvent.client`.
294
+ */
295
+ clientOsFromRequest: z.boolean().optional(),
267
296
  subagentNotification: subagentNotificationSchema.optional(),
268
297
  acpNotification: acpNotificationSchema.optional(),
269
298
  /**
@@ -379,6 +408,7 @@ export {
379
408
  isBackgroundEventMetadata,
380
409
  isEchoSuppressedUserMessage,
381
410
  isHiddenMessageMetadata,
411
+ isSuppressedQueuedMessage,
382
412
  isVoiceSessionUserMessage,
383
413
  } from "./conversation-types.js";
384
414
 
@@ -571,6 +601,8 @@ export interface ConversationRow {
571
601
  originInterface: string | null;
572
602
  forkParentConversationId: string | null;
573
603
  forkParentMessageId: string | null;
604
+ /** `"reference"` on referential forks; `"cloning"` or null on copied ones. */
605
+ forkStrategy: string | null;
574
606
  isAutoTitle: number;
575
607
  scheduleJobId: string | null;
576
608
  lastMessageAt: number | null;
@@ -611,6 +643,7 @@ export const parseConversation = createRowMapper<
611
643
  originInterface: "originInterface",
612
644
  forkParentConversationId: "forkParentConversationId",
613
645
  forkParentMessageId: "forkParentMessageId",
646
+ forkStrategy: "forkStrategy",
614
647
  isAutoTitle: "isAutoTitle",
615
648
  scheduleJobId: "scheduleJobId",
616
649
  lastMessageAt: "lastMessageAt",
@@ -951,6 +984,17 @@ export function createConversation(
951
984
  source?: string;
952
985
  scheduleJobId?: string;
953
986
  groupId?: string;
987
+ /**
988
+ * Where this conversation came from. Stamped into
989
+ * `origin_channel` at insert, so the row is attributed from the
990
+ * moment it exists.
991
+ *
992
+ * Pass it whenever the origin is known. Omitting it leaves the
993
+ * column unset, and the conversation is instead attributed by
994
+ * `setConversationOriginChannelIfUnset` when its first inbound
995
+ * message arrives.
996
+ */
997
+ origin?: ConversationOrigin;
954
998
  forkParentConversationId?: string;
955
999
  /**
956
1000
  * Id of the conversation that spawned this one (subagent spawns).
@@ -958,6 +1002,14 @@ export function createConversation(
958
1002
  * `forkParentConversationId` it implies no history inheritance.
959
1003
  */
960
1004
  parentConversationId?: string;
1005
+ /**
1006
+ * Role of the subagent that owns this conversation, and how it was
1007
+ * spawned. Persisted here (not only on the ephemeral `subagents` row)
1008
+ * so usage telemetry can decompose delegated spend by variety long
1009
+ * after the subagent record is disposed. See migration 362.
1010
+ */
1011
+ subagentRole?: string;
1012
+ subagentSpawnMode?: string;
961
1013
  },
962
1014
  ) {
963
1015
  const db = getDb();
@@ -972,6 +1024,7 @@ export function createConversation(
972
1024
  requestedConversationType ?? "standard";
973
1025
  const source = opts.source ?? "user";
974
1026
  const groupId = opts.groupId;
1027
+ const originChannel = resolveConversationOrigin(opts.origin);
975
1028
  // Time-ordered UUIDv7 for server-minted conversation ids (see message id).
976
1029
  const id = opts.id ?? uuidv7();
977
1030
 
@@ -998,9 +1051,12 @@ export function createConversation(
998
1051
  slackContextCompactionWatermarkAt: null as number | null,
999
1052
  conversationType,
1000
1053
  source,
1054
+ originChannel,
1001
1055
  scheduleJobId: opts.scheduleJobId ?? null,
1002
1056
  forkParentConversationId: opts.forkParentConversationId ?? null,
1003
1057
  parentConversationId: opts.parentConversationId ?? null,
1058
+ subagentRole: opts.subagentRole ?? null,
1059
+ subagentSpawnMode: opts.subagentSpawnMode ?? null,
1004
1060
  // Snapshot↔stream alignment baseline, captured at the creation instant.
1005
1061
  // 0 (nothing stamped yet this process) is stored as NULL so `/messages`
1006
1062
  // reports null and the client cold-starts rather than aligning to seq 0.
@@ -1016,7 +1072,7 @@ export function createConversation(
1016
1072
  // write-contended paths wrap the call in `withSqliteRetry`, and because the
1017
1073
  // insert+update are atomic here, such a retry re-runs the whole thing cleanly
1018
1074
  // (a failed attempt rolls back, so no half-written row is ever left behind).
1019
- const effectiveGroupId = groupId ?? "system:all";
1075
+ const effectiveGroupId = groupId ?? UNGROUPED_GROUP_ID;
1020
1076
  const raw = getSqliteFrom(db);
1021
1077
  raw.exec("SAVEPOINT create_conv");
1022
1078
  try {
@@ -1025,7 +1081,7 @@ export function createConversation(
1025
1081
  "conversation:create:setGroup",
1026
1082
  "UPDATE conversations SET group_id = ?, is_pinned = ? WHERE id = ?",
1027
1083
  effectiveGroupId,
1028
- effectiveGroupId === "system:pinned" ? 1 : 0,
1084
+ effectiveGroupId === PINNED_GROUP_ID ? 1 : 0,
1029
1085
  id,
1030
1086
  );
1031
1087
  raw.exec("RELEASE create_conv");
@@ -1035,11 +1091,55 @@ export function createConversation(
1035
1091
  throw err;
1036
1092
  }
1037
1093
 
1038
- initConversationDir({ ...conversation, originChannel: null });
1094
+ // The disk view records the same origin the row holds.
1095
+ initConversationDir(conversation);
1039
1096
 
1040
1097
  return conversation;
1041
1098
  }
1042
1099
 
1100
+ /**
1101
+ * Resolve a caller's {@link ConversationOrigin} to the string stored in
1102
+ * `origin_channel`.
1103
+ *
1104
+ * `inheritFrom` reads the parent's value, which is how a fork, a subagent
1105
+ * spawn, or a scheduled wake lands on the surface its parent belongs to.
1106
+ *
1107
+ * A parent whose own origin is unset yields an unset origin: the child
1108
+ * inherits the parent's state faithfully, including "not yet attributed".
1109
+ * Most rows are in exactly that state until they are claimed by a message or
1110
+ * swept at startup, so treating it as an error would fail the majority of
1111
+ * forks and subagent spawns.
1112
+ *
1113
+ * A parent that does not exist throws. That is not the same condition: it
1114
+ * means a caller named a conversation that is not there, and guessing an
1115
+ * origin for it would be a trust grant rather than a cosmetic default.
1116
+ * `recoverRestingTrustContext` reads this column, and the native channel
1117
+ * recovers `INTERNAL_GUARDIAN_TRUST_CONTEXT` on every later wake and
1118
+ * boot-resume, so a fork of a remote conversation that silently defaulted
1119
+ * would come back as the guardian's own. Parent ids here come from daemon
1120
+ * internals rather than user input, so a missing one is a programming error.
1121
+ *
1122
+ * `undefined` maps to NULL, leaving the row for first-message attribution.
1123
+ */
1124
+ function resolveConversationOrigin(
1125
+ origin: ConversationOrigin | undefined,
1126
+ ): string | null {
1127
+ if (origin === undefined) {
1128
+ return null;
1129
+ }
1130
+ if (typeof origin === "string") {
1131
+ return origin;
1132
+ }
1133
+ const parent = getConversation(origin.inheritFrom);
1134
+ if (!parent) {
1135
+ throw new Error(
1136
+ `Cannot inherit conversation origin from ${origin.inheritFrom}: ` +
1137
+ "no such conversation",
1138
+ );
1139
+ }
1140
+ return parent.originChannel;
1141
+ }
1142
+
1043
1143
  /**
1044
1144
  * A conversation id adopted verbatim from an untrusted source must be safe to
1045
1145
  * embed as a single path component of the on-disk conversation dir
@@ -1069,7 +1169,10 @@ export const ADOPTABLE_CONVERSATION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
1069
1169
  * is validated first — a value like `../../tmp/x` would otherwise write
1070
1170
  * `meta.json` outside the conversations directory.
1071
1171
  */
1072
- export function ensureConversationExists(id: string): boolean {
1172
+ export function ensureConversationExists(
1173
+ id: string,
1174
+ origin: ConversationOrigin | undefined,
1175
+ ): boolean {
1073
1176
  if (getConversation(id)) {
1074
1177
  return false;
1075
1178
  }
@@ -1079,7 +1182,7 @@ export function ensureConversationExists(id: string): boolean {
1079
1182
  );
1080
1183
  }
1081
1184
  try {
1082
- createConversation({ id });
1185
+ createConversation({ id, origin });
1083
1186
  return true;
1084
1187
  } catch (err) {
1085
1188
  // A concurrent caller may have created the row between the check and the
@@ -1701,9 +1804,26 @@ export async function forkConversationForRetrospective(params: {
1701
1804
  const hiddenRowIds = new Set(
1702
1805
  loadOrderIds.slice(0, inheritedCompaction?.compactedMessageCount ?? 0),
1703
1806
  );
1704
- const rowsToCopy = messagesToCopy.filter(
1705
- (message) => !hiddenRowIds.has(message.id),
1706
- );
1807
+ // Read straight from config rather than taking a caller-supplied strategy:
1808
+ // how a fork is materialized is a workspace-wide storage decision, and a
1809
+ // per-call override would let two callers disagree about it on the same
1810
+ // workspace.
1811
+ const isReferential =
1812
+ getConfig().memory.retrospective.forkStrategy === "reference";
1813
+ // A referential fork copies nothing: its inherited window is read back
1814
+ // through `forkParentMessageId` by the lineage resolver.
1815
+ const rowsToCopy = isReferential
1816
+ ? []
1817
+ : messagesToCopy.filter((message) => !hiddenRowIds.has(message.id));
1818
+ // The compacted prefix is dropped from a copied fork, so its own row count
1819
+ // behind the boundary is 0. A referential fork reads the source's rows
1820
+ // through the fork point, prefix included, so it must carry the source's
1821
+ // own count for the render to hide the same rows the source hides. Getting
1822
+ // this wrong does not lose data, it re-shows compacted history the summary
1823
+ // already covers.
1824
+ const forkCompactedMessageCount = isReferential
1825
+ ? (inheritedCompaction?.compactedMessageCount ?? 0)
1826
+ : 0;
1707
1827
  const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
1708
1828
  const forkTitle =
1709
1829
  params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
@@ -1744,11 +1864,12 @@ export async function forkConversationForRetrospective(params: {
1744
1864
  // value from the copied rows when the tail is non-empty.
1745
1865
  lastMessageAt: boundaryMessageCreatedAt,
1746
1866
  contextSummary: inheritedCompaction?.summary ?? null,
1747
- // Zero of the fork's own rows sit behind the boundary (the
1748
- // compacted prefix is not copied). The summary still renders:
1749
- // the history render keys on `contextSummary` presence, not on
1750
- // this count.
1751
- contextCompactedMessageCount: 0,
1867
+ // On a copied fork zero of its own rows sit behind the boundary
1868
+ // (the compacted prefix is not copied). The summary still
1869
+ // renders: the history render keys on `contextSummary` presence,
1870
+ // not on this count.
1871
+ contextCompactedMessageCount: forkCompactedMessageCount,
1872
+ forkStrategy: isReferential ? REFERENTIAL_FORK_STRATEGY : null,
1752
1873
  contextCompactedAt: inheritedCompaction?.compactedAt ?? null,
1753
1874
  slackContextCompactionWatermarkTs: inheritsLatestCompaction
1754
1875
  ? sourceConversation.slackContextCompactionWatermarkTs
@@ -1773,15 +1894,20 @@ export async function forkConversationForRetrospective(params: {
1773
1894
 
1774
1895
  try {
1775
1896
  // Phase 2 (off the event loop): copy the message rows in a sqlite3
1776
- // subprocess so the daemon stays responsive during the heavy copy.
1777
- const copy = await copyForkMessagesViaSubprocess({
1778
- forkConversationId: fork.id,
1779
- idPairs,
1780
- });
1781
- if (!copy.ok) {
1782
- throw new Error(
1783
- `fork message copy failed (${copy.backend}): ${copy.error ?? "unknown"}`,
1784
- );
1897
+ // subprocess so the daemon stays responsive during the heavy copy. The
1898
+ // referential fork has no rows to copy, which is the whole point of it:
1899
+ // the write burst this phase exists to keep off the event loop does not
1900
+ // happen at all.
1901
+ if (!isReferential) {
1902
+ const copy = await copyForkMessagesViaSubprocess({
1903
+ forkConversationId: fork.id,
1904
+ idPairs,
1905
+ });
1906
+ if (!copy.ok) {
1907
+ throw new Error(
1908
+ `fork message copy failed (${copy.backend}): ${copy.error ?? "unknown"}`,
1909
+ );
1910
+ }
1785
1911
  }
1786
1912
 
1787
1913
  // Phase 3 (in-process): attachments + memory-state seeding, reusing the
@@ -1813,7 +1939,7 @@ export async function forkConversationForRetrospective(params: {
1813
1939
  isFullHistoryFork:
1814
1940
  copyBoundaryIndex === sourceMessages.length - 1,
1815
1941
  // The copied range already starts at the visible window.
1816
- inheritedCompactedMessageCount: 0,
1942
+ inheritedCompactedMessageCount: forkCompactedMessageCount,
1817
1943
  skipCompactionLedgerCopy: inheritedCompaction != null,
1818
1944
  });
1819
1945
  // The fork owns none of the source's ledger events — their
@@ -1831,7 +1957,7 @@ export async function forkConversationForRetrospective(params: {
1831
1957
  appendCompactionEvent(fork.id, {
1832
1958
  compactedAt: inheritedCompaction.compactedAt,
1833
1959
  summary: inheritedCompaction.summary,
1834
- compactedMessageCount: 0,
1960
+ compactedMessageCount: forkCompactedMessageCount,
1835
1961
  });
1836
1962
  }
1837
1963
  },
@@ -1862,6 +1988,33 @@ export async function forkConversationForRetrospective(params: {
1862
1988
  * artifacts (embeddings). Returns segment IDs so callers can clean up
1863
1989
  * the corresponding Qdrant vector entries.
1864
1990
  */
1991
+ /**
1992
+ * Whether this conversation reads its inherited history from a parent that no
1993
+ * longer exists.
1994
+ *
1995
+ * Deleting a conversation ORPHANS the referential forks taken from it: the
1996
+ * delete touches only what it names, and each fork keeps the rows it owns
1997
+ * while the lineage walk truncates at the missing parent. Cascading would make
1998
+ * a delete cost time proportional to how many forks were ever taken from a
1999
+ * conversation, and refusing would make most active conversations undeletable,
2000
+ * since a retrospective forks every few minutes. Neither is worth paying on
2001
+ * every delete for a state that renders fine and can be explained.
2002
+ *
2003
+ * Explaining it is what this is for: an orphan otherwise looks like a
2004
+ * conversation that mysteriously starts mid-thought. Costs nothing on ordinary
2005
+ * conversations, which fail `isReferentialFork` before any query runs.
2006
+ */
2007
+ export function isReferentialHistoryOrphaned(row: {
2008
+ forkStrategy: string | null;
2009
+ forkParentConversationId: string | null;
2010
+ forkParentMessageId: string | null;
2011
+ }): boolean {
2012
+ if (!isReferentialFork(row)) {
2013
+ return false;
2014
+ }
2015
+ return getConversation(row.forkParentConversationId as string) === null;
2016
+ }
2017
+
1865
2018
  export function deleteConversation(id: string): DeletedMemoryIds {
1866
2019
  const db = getDb();
1867
2020
  const result: DeletedMemoryIds = {
@@ -2198,18 +2351,73 @@ export async function addMessage(
2198
2351
  return message;
2199
2352
  }
2200
2353
 
2354
+ /**
2355
+ * Load only the columns the lineage walk reads. A conversation-row primary-key
2356
+ * lookup per message read is the cost of referential forks; selecting four
2357
+ * columns instead of the full row keeps it to an index hit.
2358
+ */
2359
+ function loadLineageRow(id: string): LineageConversationRow | null {
2360
+ const row = getDb()
2361
+ .select({
2362
+ id: conversations.id,
2363
+ forkStrategy: conversations.forkStrategy,
2364
+ forkParentConversationId: conversations.forkParentConversationId,
2365
+ forkParentMessageId: conversations.forkParentMessageId,
2366
+ })
2367
+ .from(conversations)
2368
+ .where(eq(conversations.id, id))
2369
+ .get();
2370
+ return row ?? null;
2371
+ }
2372
+
2373
+ /** The `(createdAt, id)` bound of a single message, or null when it is gone. */
2374
+ function loadMessageBound(messageId: string): LineageBound | null {
2375
+ const row = getDb()
2376
+ .select({ createdAt: messages.createdAt, id: messages.id })
2377
+ .from(messages)
2378
+ .where(eq(messages.id, messageId))
2379
+ .get();
2380
+ return row ?? null;
2381
+ }
2382
+
2383
+ /**
2384
+ * The segments making up a conversation's logical message set. Non-forks and
2385
+ * copied forks resolve to a single unbounded segment after one row lookup, so
2386
+ * the referential path costs them nothing beyond that lookup.
2387
+ */
2388
+ function resolveLineage(conversationId: string): LineageSegment[] {
2389
+ return resolveConversationLineage(conversationId, {
2390
+ loadConversation: loadLineageRow,
2391
+ loadMessageBound,
2392
+ });
2393
+ }
2394
+
2395
+ /** `messages` predicate covering every row a conversation logically owns. */
2396
+ function lineageFilter(conversationId: string): SQL {
2397
+ return lineageMessageFilter(resolveLineage(conversationId));
2398
+ }
2399
+
2201
2400
  export function getMessages(conversationId: string): MessageRow[] {
2202
2401
  const db = getDb();
2203
2402
  // Synchronous read of every row for the conversation — the dominant
2204
2403
  // per-turn main-thread cost on large conversations. Timed so a freeze the
2205
2404
  // event-loop watchdog detects can be attributed here (see slow-sync-log).
2405
+ //
2406
+ // Ordered by `createdAt` ALONE. Rows sharing a millisecond fall back to
2407
+ // SQLite's rowid order, which is insertion order, and callers depend on
2408
+ // that: the compaction render slices a row count off the front of this
2409
+ // exact order, and a fork's copied prefix must match the order the source
2410
+ // renders. Adding an `id` tiebreak here reorders same-millisecond rows and
2411
+ // silently shifts that boundary. A lineage read inherits the same rule, and
2412
+ // an ancestor's rows sort ahead of the fork's own on a tie because they were
2413
+ // inserted first.
2206
2414
  return timeSyncSection(
2207
2415
  "conversation-crud:get-messages",
2208
2416
  () =>
2209
2417
  db
2210
2418
  .select()
2211
2419
  .from(messages)
2212
- .where(eq(messages.conversationId, conversationId))
2420
+ .where(lineageFilter(conversationId))
2213
2421
  .orderBy(asc(messages.createdAt))
2214
2422
  .all()
2215
2423
  .map(parseMessage),
@@ -2242,7 +2450,7 @@ export function selectSlackMetaCandidateMetadata(
2242
2450
  .from(messages)
2243
2451
  .where(
2244
2452
  and(
2245
- eq(messages.conversationId, conversationId),
2453
+ lineageFilter(conversationId),
2246
2454
  opts?.includeFlatLegacy
2247
2455
  ? or(
2248
2456
  like(messages.metadata, '%"slackMeta"%'),
@@ -2280,19 +2488,16 @@ export function countMessagesAfter(
2280
2488
  afterMessageId: string | null,
2281
2489
  ): number {
2282
2490
  const db = getDb();
2491
+ const segments = resolveLineage(conversationId);
2283
2492
  if (afterMessageId === null || afterMessageId === "") {
2284
2493
  const row = db
2285
2494
  .select({ c: count() })
2286
2495
  .from(messages)
2287
- .where(eq(messages.conversationId, conversationId))
2496
+ .where(lineageMessageFilter(segments))
2288
2497
  .get();
2289
2498
  return row?.c ?? 0;
2290
2499
  }
2291
- const ref = db
2292
- .select({ createdAt: messages.createdAt })
2293
- .from(messages)
2294
- .where(eq(messages.id, afterMessageId))
2295
- .get();
2500
+ const ref = loadMessageBound(afterMessageId);
2296
2501
  if (!ref) {
2297
2502
  return 0;
2298
2503
  }
@@ -2303,18 +2508,7 @@ export function countMessagesAfter(
2303
2508
  const row = db
2304
2509
  .select({ c: count() })
2305
2510
  .from(messages)
2306
- .where(
2307
- and(
2308
- eq(messages.conversationId, conversationId),
2309
- or(
2310
- gt(messages.createdAt, ref.createdAt),
2311
- and(
2312
- eq(messages.createdAt, ref.createdAt),
2313
- gt(messages.id, afterMessageId),
2314
- ),
2315
- ),
2316
- ),
2317
- )
2511
+ .where(lineageMessagesAfterFilter(segments, ref))
2318
2512
  .get();
2319
2513
  return row?.c ?? 0;
2320
2514
  }
@@ -2331,6 +2525,7 @@ export function getMessagesAfter(
2331
2525
  afterMessageId: string | null,
2332
2526
  ): MessageRow[] {
2333
2527
  const db = getDb();
2528
+ const segments = resolveLineage(conversationId);
2334
2529
  if (afterMessageId === null || afterMessageId === "") {
2335
2530
  // Secondary `asc(messages.id)` matches the non-null path's cursor
2336
2531
  // ordering, so callers tracking `cutoffMessageId` across runs see a
@@ -2338,16 +2533,12 @@ export function getMessagesAfter(
2338
2533
  return db
2339
2534
  .select()
2340
2535
  .from(messages)
2341
- .where(eq(messages.conversationId, conversationId))
2536
+ .where(lineageMessageFilter(segments))
2342
2537
  .orderBy(asc(messages.createdAt), asc(messages.id))
2343
2538
  .all()
2344
2539
  .map(parseMessage);
2345
2540
  }
2346
- const ref = db
2347
- .select({ createdAt: messages.createdAt })
2348
- .from(messages)
2349
- .where(eq(messages.id, afterMessageId))
2350
- .get();
2541
+ const ref = loadMessageBound(afterMessageId);
2351
2542
  if (!ref) {
2352
2543
  return [];
2353
2544
  }
@@ -2356,18 +2547,7 @@ export function getMessagesAfter(
2356
2547
  return db
2357
2548
  .select()
2358
2549
  .from(messages)
2359
- .where(
2360
- and(
2361
- eq(messages.conversationId, conversationId),
2362
- or(
2363
- gt(messages.createdAt, ref.createdAt),
2364
- and(
2365
- eq(messages.createdAt, ref.createdAt),
2366
- gt(messages.id, afterMessageId),
2367
- ),
2368
- ),
2369
- ),
2370
- )
2550
+ .where(lineageMessagesAfterFilter(segments, ref))
2371
2551
  .orderBy(asc(messages.createdAt), asc(messages.id))
2372
2552
  .all()
2373
2553
  .map(parseMessage);
@@ -2383,7 +2563,7 @@ export function hasMessages(conversationId: string): boolean {
2383
2563
  const row = db
2384
2564
  .select({ one: sql`1` })
2385
2565
  .from(messages)
2386
- .where(eq(messages.conversationId, conversationId))
2566
+ .where(lineageFilter(conversationId))
2387
2567
  .limit(1)
2388
2568
  .get();
2389
2569
  return row !== undefined;
@@ -2424,8 +2604,10 @@ export function getMessagesPaginated(
2424
2604
  ): PaginatedMessagesResult {
2425
2605
  const db = getDb();
2426
2606
 
2607
+ const segments = resolveLineage(conversationId);
2608
+
2427
2609
  if (limit === undefined) {
2428
- const conditions = [eq(messages.conversationId, conversationId)];
2610
+ const conditions = [lineageMessageFilter(segments)];
2429
2611
  if (beforeTimestamp !== undefined) {
2430
2612
  conditions.push(lt(messages.createdAt, beforeTimestamp));
2431
2613
  }
@@ -2485,7 +2667,7 @@ export function getMessagesPaginated(
2485
2667
  const chunk = db
2486
2668
  .select()
2487
2669
  .from(messages)
2488
- .where(and(eq(messages.conversationId, conversationId), cursorPredicate))
2670
+ .where(and(lineageMessageFilter(segments), cursorPredicate))
2489
2671
  .orderBy(desc(messages.createdAt), desc(messages.id))
2490
2672
  .limit(chunkSize)
2491
2673
  .all()
@@ -2824,6 +3006,21 @@ export function isConversationProcessing(id: string): boolean {
2824
3006
  return row?.processing_started_at != null;
2825
3007
  }
2826
3008
 
3009
+ /**
3010
+ * The persisted `processing_started_at` stamp, or null when idle or missing.
3011
+ * Deliberately reads only the column (no in-memory fallback): consumers use
3012
+ * it to age a flag {@link isConversationProcessing} already reported set, and
3013
+ * the column is the only signal that carries a start time.
3014
+ */
3015
+ export function getConversationProcessingStartedAt(id: string): number | null {
3016
+ const row = rawGet<{ processing_started_at: number | null }>(
3017
+ "conversation:getProcessingStartedAt",
3018
+ "SELECT processing_started_at FROM conversations WHERE id = ?",
3019
+ id,
3020
+ );
3021
+ return row?.processing_started_at ?? null;
3022
+ }
3023
+
2827
3024
  /**
2828
3025
  * Conversations currently mid-turn, longest-running first. Throws on a read
2829
3026
  * failure — drain callers must distinguish "nothing processing" from "could
@@ -3214,6 +3411,50 @@ export function getConversationOverrideProfile(
3214
3411
  return resolveOverrideProfile(getConversation(conversationId));
3215
3412
  }
3216
3413
 
3414
+ /**
3415
+ * The conversation fields needed to tell a durable inference-profile pin from
3416
+ * a session-backed one. Extends {@link OverrideProfileFields} with the session
3417
+ * marker, so callers can decide from a {@link ConversationRow} or a live
3418
+ * in-memory `Conversation` without a redundant row fetch.
3419
+ */
3420
+ export interface DurableProfileFields extends OverrideProfileFields {
3421
+ inferenceProfileSessionId?: string | null;
3422
+ }
3423
+
3424
+ /**
3425
+ * Resolve a conversation's inference-profile pin only when the user set it to
3426
+ * stick, ignoring a TTL-limited profile session.
3427
+ *
3428
+ * {@link resolveOverrideProfile} is read at use time, so a lapsed session
3429
+ * simply reads as absent on the next turn. Anything that copies a pin into a
3430
+ * record outliving that read (a schedule row that fires days later) needs the
3431
+ * stricter question: a copied session would keep billing the model the user
3432
+ * picked for ten minutes long after the session ended.
3433
+ *
3434
+ * A session-backed pin carries a non-null `inferenceProfileSessionId`;
3435
+ * {@link setConversationInferenceProfile}, the sticky setter, clears those
3436
+ * columns. So a null session id is exactly the durable case.
3437
+ */
3438
+ export function resolveDurableProfile(
3439
+ fields: DurableProfileFields | null,
3440
+ ): string | undefined {
3441
+ if (fields?.inferenceProfileSessionId != null) {
3442
+ return undefined;
3443
+ }
3444
+ return resolveOverrideProfile(fields);
3445
+ }
3446
+
3447
+ /**
3448
+ * Resolve a conversation's durable inference-profile pin by id. Convenience
3449
+ * wrapper over {@link resolveDurableProfile} for callers that don't already
3450
+ * hold the row.
3451
+ */
3452
+ export function getConversationDurableProfile(
3453
+ conversationId: string,
3454
+ ): string | undefined {
3455
+ return resolveDurableProfile(getConversation(conversationId));
3456
+ }
3457
+
3217
3458
  export function setLastNotifiedInferenceProfile(
3218
3459
  conversationId: string,
3219
3460
  profileKey: string | null,
@@ -3968,13 +4209,19 @@ export function getConversationRecentProvenanceTrustClass(
3968
4209
  }
3969
4210
 
3970
4211
  // ---------------------------------------------------------------------------
3971
- // CRUD functions for display_order and is_pinned
4212
+ // Conversation placement (group + pin) and display metadata
3972
4213
  // ---------------------------------------------------------------------------
3973
4214
 
3974
- export function batchSetDisplayOrders(
4215
+ /**
4216
+ * Move conversations between sections: set the group, and the pin state
4217
+ * derived from it.
4218
+ *
4219
+ * Writes no ordering. Every list is ordered by recency, so a conversation's
4220
+ * placement is which section holds it, not where it sits inside one.
4221
+ */
4222
+ export function batchSetConversationPlacement(
3975
4223
  updates: Array<{
3976
4224
  id: string;
3977
- displayOrder: number | null;
3978
4225
  isPinned?: boolean;
3979
4226
  groupId?: string | null;
3980
4227
  }>,
@@ -3992,15 +4239,15 @@ export function batchSetDisplayOrders(
3992
4239
  // the entire batch.
3993
4240
  let safeGroupId = update.groupId;
3994
4241
  if (safeGroupId === null) {
3995
- safeGroupId = "system:all";
4242
+ safeGroupId = UNGROUPED_GROUP_ID;
3996
4243
  } else if (
3997
4244
  !rawGet<{ id: string }>(
3998
- "conversation:batchSetDisplayOrders:groupCheck",
4245
+ "conversation:batchSetConversationPlacement:groupCheck",
3999
4246
  "SELECT id FROM conversation_groups WHERE id = ?",
4000
4247
  safeGroupId,
4001
4248
  )
4002
4249
  ) {
4003
- safeGroupId = "system:all";
4250
+ safeGroupId = UNGROUPED_GROUP_ID;
4004
4251
  }
4005
4252
  // Moving a conversation into the Scheduled/Background system groups
4006
4253
  // is an explicit demotion out of Recents, so clear any `surfaced_at`
@@ -4010,37 +4257,57 @@ export function batchSetDisplayOrders(
4010
4257
  const clearsSurfaced =
4011
4258
  safeGroupId === "system:background" ||
4012
4259
  safeGroupId === "system:scheduled";
4013
- rawRun(
4014
- "conversation:batchSetDisplayOrders:group",
4015
- `UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ?${
4016
- clearsSurfaced ? ", surfaced_at = NULL" : ""
4017
- } WHERE id = ?`,
4018
- update.displayOrder,
4019
- safeGroupId === "system:pinned" ? 1 : 0,
4260
+ // Filing a conversation into a section the user reads (Pinned or one
4261
+ // of their own groups) is the mirror image: an explicit promotion, so
4262
+ // it stamps `surfaced_at` the way the surface API does.
4263
+ //
4264
+ // The stamp is what makes the promotion durable. Visibility must not
4265
+ // depend on where the row currently sits, or removing it from a group
4266
+ // would hide it instead of returning it to Recents, and pinning would
4267
+ // not show it at all: `system:pinned` fails the custom-group arm of
4268
+ // `standardListingVisibilitySql` on that arm's `system:` prefix check.
4269
+ //
4270
+ // Only background and scheduled rows need it: everything else is
4271
+ // already visible, and stamping them would leave `surfaced_at`
4272
+ // meaning nothing. `COALESCE` keeps the original promotion time when
4273
+ // the row is re-filed later.
4274
+ const promotes =
4275
+ !clearsSurfaced &&
4276
+ safeGroupId !== UNGROUPED_GROUP_ID &&
4277
+ (safeGroupId === PINNED_GROUP_ID ||
4278
+ !safeGroupId.startsWith("system:"));
4279
+ const setClauses = ["is_pinned = ?", "group_id = ?"];
4280
+ const params: Array<string | number | null> = [
4281
+ safeGroupId === PINNED_GROUP_ID ? 1 : 0,
4020
4282
  safeGroupId,
4021
- update.id,
4022
- );
4023
- } else if (update.isPinned === undefined) {
4024
- // Only displayOrder provided — preserve existing pin state and group.
4283
+ ];
4284
+ if (clearsSurfaced) {
4285
+ setClauses.push("surfaced_at = NULL");
4286
+ } else if (promotes) {
4287
+ setClauses.push(
4288
+ "surfaced_at = CASE WHEN conversation_type IN ('background', 'scheduled')" +
4289
+ " THEN COALESCE(surfaced_at, ?) ELSE surfaced_at END",
4290
+ );
4291
+ params.push(Date.now());
4292
+ }
4293
+ params.push(update.id);
4025
4294
  rawRun(
4026
- "conversation:batchSetDisplayOrders:orderOnly",
4027
- "UPDATE conversations SET display_order = ? WHERE id = ?",
4028
- update.displayOrder,
4029
- update.id,
4295
+ "conversation:batchSetConversationPlacement:group",
4296
+ `UPDATE conversations SET ${setClauses.join(", ")} WHERE id = ?`,
4297
+ ...params,
4030
4298
  );
4031
- } else if (update.isPinned) {
4299
+ } else if (update.isPinned === true) {
4032
4300
  rawRun(
4033
- "conversation:batchSetDisplayOrders:pin",
4034
- "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
4035
- update.displayOrder,
4301
+ "conversation:batchSetConversationPlacement:pin",
4302
+ "UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
4036
4303
  update.id,
4037
4304
  );
4038
- } else {
4305
+ } else if (update.isPinned === false) {
4039
4306
  // Restore system group from source/conversationType when unpinning,
4040
4307
  // instead of clearing to NULL (which would lose provenance).
4041
4308
  rawRun(
4042
- "conversation:batchSetDisplayOrders:unpin",
4043
- `UPDATE conversations SET display_order = ?, is_pinned = 0,
4309
+ "conversation:batchSetConversationPlacement:unpin",
4310
+ `UPDATE conversations SET is_pinned = 0,
4044
4311
  group_id = CASE WHEN group_id = 'system:pinned' THEN
4045
4312
  CASE
4046
4313
  WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
@@ -4050,7 +4317,6 @@ export function batchSetDisplayOrders(
4050
4317
  END
4051
4318
  ELSE group_id END
4052
4319
  WHERE id = ?`,
4053
- update.displayOrder,
4054
4320
  update.id,
4055
4321
  );
4056
4322
  }