@vellumai/assistant 0.11.4 → 0.11.5-dev.202608220019.a778cf0

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 (457) hide show
  1. package/AGENTS.md +6 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  15. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  19. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  20. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  21. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  22. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +30 -32
  23. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  24. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  25. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  26. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  28. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  29. package/openapi.yaml +487 -11
  30. package/package.json +1 -1
  31. package/scripts/sync-web-search-catalog.ts +6 -0
  32. package/src/__tests__/anthropic-provider.test.ts +10 -3
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  34. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  35. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  36. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  37. package/src/__tests__/channel-readiness-service.test.ts +126 -0
  38. package/src/__tests__/channel-reply-delivery.test.ts +30 -17
  39. package/src/__tests__/checker.test.ts +20 -314
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  41. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  42. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  44. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  45. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  46. package/src/__tests__/conversation-attachments.test.ts +0 -1
  47. package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
  48. package/src/__tests__/conversation-error.test.ts +17 -0
  49. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  50. package/src/__tests__/conversation-pairing.test.ts +186 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  52. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  53. package/src/__tests__/credential-routes.test.ts +22 -3
  54. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  55. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  56. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  57. package/src/__tests__/dm-backfill.test.ts +63 -0
  58. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  59. package/src/__tests__/edit-propagation.test.ts +107 -4
  60. package/src/__tests__/events-client-registration.test.ts +28 -0
  61. package/src/__tests__/file-write-tool.test.ts +4 -2
  62. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  63. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  64. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  65. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  66. package/src/__tests__/host-shell-tool.test.ts +23 -4
  67. package/src/__tests__/image-conversion.test.ts +143 -1
  68. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  69. package/src/__tests__/input-repairs.test.ts +207 -0
  70. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  71. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  72. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  73. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  74. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  75. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  76. package/src/__tests__/mtime-cache.test.ts +2 -0
  77. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  78. package/src/__tests__/openai-provider.test.ts +40 -0
  79. package/src/__tests__/openai-responses-provider.test.ts +18 -0
  80. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  81. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  82. package/src/__tests__/platform.test.ts +16 -1
  83. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  84. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  85. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  86. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  87. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  88. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  89. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  90. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  91. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  92. package/src/__tests__/reaction-persistence.test.ts +200 -7
  93. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  94. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  95. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  96. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  97. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  98. package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
  99. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  100. package/src/__tests__/settings-routes.test.ts +85 -3
  101. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  102. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  103. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  104. package/src/__tests__/tool-executor.test.ts +63 -38
  105. package/src/__tests__/tool-policy.test.ts +97 -1
  106. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  107. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  108. package/src/__tests__/validate-input.test.ts +243 -7
  109. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  110. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  111. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  112. package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
  113. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  114. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  115. package/src/acp/acp-claude-oauth.ts +38 -21
  116. package/src/acp/prepare-agent-env.ts +146 -55
  117. package/src/agent/loop-tool-dedup.test.ts +161 -0
  118. package/src/agent/loop.ts +68 -22
  119. package/src/api/constants/app-tools.ts +34 -0
  120. package/src/api/constants/profile-config-validation.ts +60 -0
  121. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  122. package/src/api/events/tool-result.ts +6 -1
  123. package/src/api/events/watch-retro-completed.ts +52 -0
  124. package/src/api/index.ts +30 -0
  125. package/src/api/responses/resource-pressure-status.ts +25 -0
  126. package/src/approvals/guardian-channel-delivery.ts +70 -6
  127. package/src/approvals/guardian-request-resolvers.ts +32 -66
  128. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  129. package/src/calls/voice-session-bridge.ts +60 -6
  130. package/src/channels/__tests__/message-audience.test.ts +49 -0
  131. package/src/channels/__tests__/types.test.ts +17 -3
  132. package/src/channels/message-audience.ts +40 -0
  133. package/src/channels/types.ts +16 -12
  134. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  135. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  136. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  137. package/src/cli/commands/__tests__/conversations-slack.test.ts +2 -1
  138. package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
  139. package/src/cli/commands/__tests__/inference-providers.test.ts +135 -2
  140. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  141. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  142. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  143. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  144. package/src/cli/commands/conversations.help.ts +34 -0
  145. package/src/cli/commands/conversations.ts +125 -0
  146. package/src/cli/commands/inference-profiles.ts +56 -3
  147. package/src/cli/commands/inference-providers.ts +37 -6
  148. package/src/cli/commands/inference.help.ts +9 -4
  149. package/src/cli/commands/keys.help.ts +13 -1
  150. package/src/cli/commands/keys.ts +26 -15
  151. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  152. package/src/cli/commands/memory/index.help.ts +41 -27
  153. package/src/cli/commands/memory/index.ts +2 -0
  154. package/src/cli/commands/memory/memory-v2.ts +58 -54
  155. package/src/cli/commands/memory/memory-validate.ts +18 -0
  156. package/src/cli/commands/monitoring.ts +1 -0
  157. package/src/cli/commands/oauth/request.test.ts +93 -0
  158. package/src/cli/commands/oauth/request.ts +23 -9
  159. package/src/cli/commands/plugins.help.ts +28 -31
  160. package/src/cli/commands/plugins.ts +10 -0
  161. package/src/cli/commands/skills.help.ts +13 -16
  162. package/src/cli/commands/trust.ts +3 -14
  163. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  164. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  165. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  166. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  167. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  168. package/src/cli/lib/bundled-marketplace.json +26 -0
  169. package/src/cli/lib/daemon-credential-client.ts +25 -3
  170. package/src/cli/lib/install-from-github.ts +35 -24
  171. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  172. package/src/cli/lib/open-browser.test.ts +67 -0
  173. package/src/cli/lib/open-browser.ts +24 -5
  174. package/src/cli/lib/plugin-surfaces.ts +27 -0
  175. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  176. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  177. package/src/config/__tests__/profile-materialization.test.ts +26 -0
  178. package/src/config/default-profile-catalog.ts +5 -6
  179. package/src/config/feature-flag-registry.json +36 -28
  180. package/src/config/profile-materialization.ts +29 -0
  181. package/src/config/sanitize-for-transfer.ts +16 -0
  182. package/src/config/schemas/llm.ts +7 -0
  183. package/src/config/schemas/services.ts +6 -0
  184. package/src/config/webhook-routing.ts +8 -0
  185. package/src/context/compactor.ts +31 -2
  186. package/src/context/outbound-sanitize.ts +6 -0
  187. package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
  188. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  189. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  190. package/src/daemon/conversation-error.ts +19 -2
  191. package/src/daemon/conversation-messaging.ts +22 -2
  192. package/src/daemon/conversation-process.ts +11 -0
  193. package/src/daemon/conversation-store.ts +10 -0
  194. package/src/daemon/conversation-surfaces.ts +21 -8
  195. package/src/daemon/conversation.ts +12 -10
  196. package/src/daemon/daemon-control.ts +1 -0
  197. package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
  198. package/src/daemon/handlers/config-ingress.ts +115 -5
  199. package/src/daemon/lifecycle.ts +30 -2
  200. package/src/daemon/message-types/conversations.ts +5 -7
  201. package/src/daemon/message-types/web-activity.ts +3 -2
  202. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  203. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  204. package/src/daemon/resource-pressure-guard.ts +387 -0
  205. package/src/daemon/shutdown-handlers.ts +2 -0
  206. package/src/daemon/startup-error.ts +16 -0
  207. package/src/daemon/trust-context.ts +14 -10
  208. package/src/daemon/unsendable-image-notice.ts +76 -0
  209. package/src/hooks/registry.ts +65 -51
  210. package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
  211. package/src/inbound/tunnel-probe.ts +261 -0
  212. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  213. package/src/ipc/gateway-client.test.ts +1 -1
  214. package/src/ipc/gateway-client.ts +14 -21
  215. package/src/ipc/socket-cleanup.ts +2 -11
  216. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  217. package/src/messaging/provider-message-metadata.ts +128 -0
  218. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +138 -73
  219. package/src/messaging/providers/channel-transport.ts +100 -16
  220. package/src/messaging/providers/discord/send.ts +16 -0
  221. package/src/messaging/providers/discord/transport.ts +23 -1
  222. package/src/messaging/providers/index.ts +84 -16
  223. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  224. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  225. package/src/messaging/providers/slack/send.test.ts +88 -34
  226. package/src/messaging/providers/slack/send.ts +99 -50
  227. package/src/messaging/providers/slack/transport.ts +35 -37
  228. package/src/messaging/providers/telegram-bot/send.test.ts +88 -18
  229. package/src/messaging/providers/telegram-bot/send.ts +36 -0
  230. package/src/messaging/providers/telegram-bot/transport.ts +22 -8
  231. package/src/messaging/read-provider-metadata.test.ts +157 -0
  232. package/src/messaging/read-provider-metadata.ts +51 -0
  233. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  234. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  235. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  236. package/src/notifications/adapters/macos.ts +55 -0
  237. package/src/notifications/adapters/slack.ts +10 -5
  238. package/src/notifications/assistant-reply-producer.ts +37 -4
  239. package/src/notifications/conversation-pairing.ts +111 -21
  240. package/src/notifications/edit-notification.ts +38 -8
  241. package/src/notifications/emit-signal.ts +12 -14
  242. package/src/notifications/home-feed-side-effect.ts +218 -15
  243. package/src/notifications/types.ts +5 -0
  244. package/src/permissions/AGENTS.md +16 -0
  245. package/src/permissions/checker.test.ts +75 -122
  246. package/src/permissions/checker.ts +44 -560
  247. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  248. package/src/permissions/confirmation-guardian-request.ts +5 -1
  249. package/src/persistence/conversation-crud.ts +69 -20
  250. package/src/persistence/conversation-queries.ts +14 -1
  251. package/src/persistence/conversation-types.ts +22 -0
  252. package/src/persistence/db-async-query.ts +2 -1
  253. package/src/persistence/db-maintenance.ts +15 -0
  254. package/src/persistence/delivery-crud.ts +122 -15
  255. package/src/persistence/embeddings/qdrant-manager.ts +1 -0
  256. package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
  257. package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
  258. package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
  259. package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
  260. package/src/persistence/schema/index.ts +1 -0
  261. package/src/persistence/schema/watch.ts +29 -0
  262. package/src/persistence/steps.ts +4 -0
  263. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  264. package/src/plugin-api/conversation-turn.ts +27 -0
  265. package/src/plugin-api/credential-scope.test.ts +15 -0
  266. package/src/plugin-api/credential-scope.ts +14 -0
  267. package/src/plugin-api/index.ts +19 -1
  268. package/src/plugin-api/resolve-credential.ts +15 -10
  269. package/src/plugin-api/store-credential.ts +148 -0
  270. package/src/plugin-api/system-card.ts +36 -0
  271. package/src/plugin-api/vision-support.test.ts +82 -0
  272. package/src/plugin-api/vision-support.ts +14 -2
  273. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  274. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  275. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  276. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  277. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  278. package/src/plugins/defaults/main.ts +15 -0
  279. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  280. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  281. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  282. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  283. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  284. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  286. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  287. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  288. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  289. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  290. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  291. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  292. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  293. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  294. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  295. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  296. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  297. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  298. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  299. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  300. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  301. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  302. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  303. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  304. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  305. package/src/plugins/mtime-cache.ts +1 -1
  306. package/src/plugins/pipeline.ts +14 -7
  307. package/src/plugins/plugin-execution-context.ts +35 -11
  308. package/src/plugins/plugin-tree-walk.ts +63 -4
  309. package/src/providers/__tests__/retry-network-error.test.ts +84 -0
  310. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  311. package/src/providers/anthropic/client.ts +332 -241
  312. package/src/providers/connection-resolution.ts +25 -2
  313. package/src/providers/content-blocks.ts +22 -0
  314. package/src/providers/fetch-provider-catalog.ts +19 -0
  315. package/src/providers/gemini/client.ts +5 -2
  316. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  317. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  318. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  319. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  320. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  321. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  322. package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
  323. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  324. package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
  325. package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
  326. package/src/providers/inference/adapter-factory.ts +33 -2
  327. package/src/providers/inference/auth.ts +13 -2
  328. package/src/providers/inference/credential-usage.ts +37 -0
  329. package/src/providers/inference/endpoint-probe.ts +115 -0
  330. package/src/providers/inference/missing-credential-guard.ts +110 -0
  331. package/src/providers/inference/profile-probe.ts +256 -0
  332. package/src/providers/inference/resolve-auth.ts +7 -7
  333. package/src/providers/media-resolve.ts +176 -15
  334. package/src/providers/model-catalog.ts +10 -0
  335. package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
  336. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  337. package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
  338. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  339. package/src/providers/openai/api-error-normalization.ts +16 -2
  340. package/src/providers/openai/chat-completions-provider.ts +326 -44
  341. package/src/providers/openai/responses-provider.ts +5 -2
  342. package/src/providers/provider-send-message.ts +11 -0
  343. package/src/providers/retry.ts +6 -0
  344. package/src/providers/search-provider-catalog.ts +20 -0
  345. package/src/routes/route-host-protocol.ts +7 -0
  346. package/src/routes/worker.ts +31 -10
  347. package/src/runtime/AGENTS.md +36 -0
  348. package/src/runtime/__tests__/host-observe.test.ts +302 -0
  349. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  350. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  351. package/src/runtime/assistant-event-hub.ts +38 -0
  352. package/src/runtime/channel-readiness-service.ts +39 -13
  353. package/src/runtime/channel-readiness-types.ts +49 -2
  354. package/src/runtime/channel-reply-delivery.ts +55 -32
  355. package/src/runtime/channel-retry-sweep.ts +6 -4
  356. package/src/runtime/effective-capabilities.test.ts +19 -0
  357. package/src/runtime/effective-capabilities.ts +25 -0
  358. package/src/runtime/guardian-reply-router.ts +6 -2
  359. package/src/runtime/host-observe.ts +246 -0
  360. package/src/runtime/http-errors.ts +1 -0
  361. package/src/runtime/http-server.ts +185 -6
  362. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  363. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  364. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
  365. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  366. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
  367. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +72 -1
  368. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
  369. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  370. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  371. package/src/runtime/routes/__tests__/plugins-routes.test.ts +103 -71
  372. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  373. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  374. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  375. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  376. package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
  377. package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
  378. package/src/runtime/routes/approval-routes.ts +37 -4
  379. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  380. package/src/runtime/routes/canned-message-complete.ts +68 -29
  381. package/src/runtime/routes/channel-readiness-routes.ts +14 -2
  382. package/src/runtime/routes/client-routes.ts +113 -0
  383. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  384. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  385. package/src/runtime/routes/conversation-query-routes.ts +10 -0
  386. package/src/runtime/routes/conversation-routes.ts +36 -22
  387. package/src/runtime/routes/credential-in-use.ts +85 -0
  388. package/src/runtime/routes/credential-routes.ts +55 -85
  389. package/src/runtime/routes/guardian-approval-interception.ts +44 -53
  390. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  391. package/src/runtime/routes/host-cu-routes.ts +18 -0
  392. package/src/runtime/routes/identity-routes.ts +5 -121
  393. package/src/runtime/routes/inbound-message-handler.ts +74 -30
  394. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  395. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +182 -297
  396. package/src/runtime/routes/inbound-stages/background-dispatch.ts +173 -386
  397. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  398. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  399. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  400. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  401. package/src/runtime/routes/index.ts +4 -0
  402. package/src/runtime/routes/inference-profiles-routes.ts +232 -31
  403. package/src/runtime/routes/inference-provider-connection-routes.ts +33 -11
  404. package/src/runtime/routes/ingress-status-routes.ts +180 -0
  405. package/src/runtime/routes/monitoring-routes.ts +9 -1
  406. package/src/runtime/routes/oauth-commands-routes.ts +1 -1
  407. package/src/runtime/routes/plugins-routes.ts +28 -32
  408. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  409. package/src/runtime/routes/secret-routes.ts +39 -8
  410. package/src/runtime/routes/settings-routes.ts +15 -12
  411. package/src/runtime/routes/surface-action-routes.ts +20 -19
  412. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  413. package/src/runtime/routes/user-route-resolution.ts +18 -1
  414. package/src/runtime/routes/watch-routes.ts +807 -0
  415. package/src/runtime/slack-reply-session.test.ts +23 -13
  416. package/src/runtime/slack-reply-session.ts +38 -55
  417. package/src/runtime/web-presence.ts +90 -0
  418. package/src/security/__tests__/untrusted-content.test.ts +42 -0
  419. package/src/security/untrusted-content.ts +28 -9
  420. package/src/skills/validate-input.ts +177 -40
  421. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  422. package/src/subagent/consult-context.ts +7 -23
  423. package/src/tools/credentials/broker.ts +24 -79
  424. package/src/tools/credentials/ref-parse.ts +35 -0
  425. package/src/tools/credentials/resolve.ts +4 -10
  426. package/src/tools/credentials/store.ts +168 -0
  427. package/src/tools/credentials/tool-policy.ts +67 -0
  428. package/src/tools/executor.ts +53 -15
  429. package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
  430. package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
  431. package/src/tools/network/__tests__/web-search.test.ts +138 -3
  432. package/src/tools/network/firecrawl-compat.ts +90 -0
  433. package/src/tools/network/url-safety.ts +9 -16
  434. package/src/tools/network/web-fetch.ts +142 -62
  435. package/src/tools/network/web-search.ts +162 -58
  436. package/src/tools/permission-checker.ts +43 -52
  437. package/src/tools/schema-transforms.ts +40 -5
  438. package/src/tools/shared/input-repairs.ts +160 -0
  439. package/src/tools/skills/skill-tool-factory.ts +18 -4
  440. package/src/tools/subagent/spawn.ts +0 -1
  441. package/src/tools/tool-approval-handler.ts +16 -10
  442. package/src/tools/tool-types.ts +26 -13
  443. package/src/tools/types.ts +6 -6
  444. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  445. package/src/util/cgroup-memory.ts +3 -0
  446. package/src/util/container-cpu-sampler.ts +250 -0
  447. package/src/util/image-conversion.ts +178 -14
  448. package/src/util/platform.ts +93 -10
  449. package/src/util/worker-process.ts +1 -0
  450. package/src/watch/__tests__/watch-retro.test.ts +665 -0
  451. package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
  452. package/src/watch/__tests__/watch-timeline.test.ts +670 -0
  453. package/src/watch/watch-retro.ts +480 -0
  454. package/src/watch/watch-session-manager.ts +575 -0
  455. package/src/watch/watch-timeline.ts +848 -0
  456. package/src/permissions/ipc-risk-types.ts +0 -143
  457. package/src/permissions/risk-types.ts +0 -76
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Tests for the confirmation → guardian-request promotion, including the
3
- * in-flight race: a confirmation resolved while the fire-and-forget gateway
4
- * create is still pending must not strand a pending tool_approval row.
2
+ * Tests for the confirmation to guardian-request promotion, including the
3
+ * in-flight race (a confirmation resolved while the fire-and-forget gateway
4
+ * create is still pending must not strand a pending tool_approval row) and
5
+ * the deadline, which is derived from the approval-window resolver rather
6
+ * than restated as a constant.
5
7
  */
6
8
 
7
9
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -52,6 +54,15 @@ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
52
54
  },
53
55
  }));
54
56
 
57
+ // Deliberately not 300_000, the resolver's default. A fixture on the default
58
+ // passes whether the deadline is read from the resolver or restated as a
59
+ // constant beside it; this value only matches if it is read.
60
+ const APPROVAL_WINDOW_MS = 900_000;
61
+
62
+ mock.module("../tools/tool-approval-handler.js", () => ({
63
+ resolveInlineGrantWaitMs: () => APPROVAL_WINDOW_MS,
64
+ }));
65
+
55
66
  import { asConversation } from "../__tests__/helpers/mock-conversation.js";
56
67
  import type { AssistantEvent } from "../api/index.js";
57
68
  import type { TrustContext } from "../daemon/trust-context-types.js";
@@ -86,6 +97,20 @@ describe("createGuardianRequestForConfirmation", () => {
86
97
  expect(bridgeCalls).toHaveLength(1);
87
98
  });
88
99
 
100
+ test("takes the row deadline from the approval-window resolver", async () => {
101
+ const before = Date.now();
102
+ await createGuardianRequestForConfirmation(MSG, "conv-1");
103
+ const after = Date.now();
104
+
105
+ expect(createCalls).toHaveLength(1);
106
+ const expiresAt = createCalls[0].expiresAt as number;
107
+
108
+ // Bracketed against the clock either side of the call rather than compared
109
+ // to a single Date.now(), so the assertion cannot flake on a slow tick.
110
+ expect(expiresAt).toBeGreaterThanOrEqual(before + APPROVAL_WINDOW_MS);
111
+ expect(expiresAt).toBeLessThanOrEqual(after + APPROVAL_WINDOW_MS);
112
+ });
113
+
89
114
  test("expires the row and skips the bridge when the confirmation resolved mid-create", async () => {
90
115
  confirmationPending = false;
91
116
 
@@ -55,6 +55,7 @@ export async function createGuardianRequestForConfirmation(
55
55
  { summarizeToolInput },
56
56
  { DAEMON_INTERNAL_ASSISTANT_ID },
57
57
  { bridgeConfirmationRequestToGuardian },
58
+ { resolveInlineGrantWaitMs },
58
59
  ] = await Promise.all([
59
60
  import("../daemon/conversation-registry.js"),
60
61
  import("../channels/gateway-guardian-requests.js"),
@@ -62,6 +63,7 @@ export async function createGuardianRequestForConfirmation(
62
63
  import("../tools/tool-input-summary.js"),
63
64
  import("../runtime/assistant-scope.js"),
64
65
  import("../runtime/confirmation-request-guardian-bridge.js"),
66
+ import("../tools/tool-approval-handler.js"),
65
67
  ]);
66
68
 
67
69
  const conversation = findConversation(conversationId);
@@ -100,7 +102,9 @@ export async function createGuardianRequestForConfirmation(
100
102
  activityText: activityRaw ? redactSecrets(activityRaw) : undefined,
101
103
  executionTarget: msg.executionTarget,
102
104
  status: "pending",
103
- expiresAt: Date.now() + 5 * 60 * 1000,
105
+ // The row stays decidable exactly as long as the prompt stays parked:
106
+ // one budget, so raising `timeouts.permissionTimeoutSec` moves both.
107
+ expiresAt: Date.now() + resolveInlineGrantWaitMs(),
104
108
  });
105
109
 
106
110
  // The prompt is actionable before this fire-and-forget create lands, so
@@ -50,6 +50,10 @@ import {
50
50
  isRetryableSqliteError,
51
51
  withSqliteRetry,
52
52
  } from "../util/sqlite-retry.js";
53
+ import {
54
+ purgeAllWatchTimelines,
55
+ purgeWatchTimelineForConversation,
56
+ } from "../watch/watch-timeline.js";
53
57
  import {
54
58
  deleteOrphanAttachments,
55
59
  linkAttachmentToMessage,
@@ -87,6 +91,7 @@ import {
87
91
  type ConversationCreateType,
88
92
  type ConversationOrigin,
89
93
  isHiddenMessageMetadata,
94
+ isSystemCardMetadata,
90
95
  PINNED_GROUP_ID,
91
96
  UNGROUPED_GROUP_ID,
92
97
  } from "./conversation-types.js";
@@ -190,6 +195,36 @@ function dedicatedTableExists(db: DrizzleDb, table: string): boolean {
190
195
  * keys on `conversation_id` regardless of event name, so this covers every
191
196
  * conversation-scoped pending event.
192
197
  */
198
+ /**
199
+ * Remove a conversation's watch-session timeline: the narration, the screens,
200
+ * and the frames of them the entries carry.
201
+ *
202
+ * The rows are keyed by conversation id with no cascade behind them, so a
203
+ * delete that skipped them would keep pictures of the user's screen for a
204
+ * conversation they asked to remove. It runs after the row deletes, which is
205
+ * the order the watch store expects: once `conversations` no longer has the
206
+ * row, a later append is refused rather than left for a purge that has already
207
+ * been.
208
+ *
209
+ * Best-effort, like the rest of the post-transaction cleanup. The conversation
210
+ * row is already gone by this point, so throwing here would turn a completed
211
+ * delete into an error the caller cannot usefully retry. A swallowed failure
212
+ * is not the end of the story: `sweepOrphanedWatchTimelineEntries` runs from
213
+ * daemon startup and from database maintenance and deletes entries whose
214
+ * conversation no longer exists, so whatever this pass leaves behind is
215
+ * reclaimed on a later one.
216
+ */
217
+ function purgeWatchTimelineForDeletedConversation(id: string): void {
218
+ try {
219
+ purgeWatchTimelineForConversation(id);
220
+ } catch (err) {
221
+ log.warn(
222
+ { err, conversationId: id },
223
+ "Failed to purge the watch timeline for a deleted conversation",
224
+ );
225
+ }
226
+ }
227
+
193
228
  function deletePendingTelemetryEventsForConversation(id: string): void {
194
229
  const telemetry = getTelemetryDb({ createIfMissing: false });
195
230
  if (!telemetry || !dedicatedTableExists(telemetry, "telemetry_events")) {
@@ -259,6 +294,14 @@ const backgroundToolCompletionMetadataSchema = z.object({
259
294
 
260
295
  export const messageMetadataSchema = z
261
296
  .object({
297
+ /**
298
+ * Epoch ms the content actually happened, when that differs from when
299
+ * the row was written. Set wherever persistence lags the event: a queued
300
+ * turn draining, or channel history imported long after the fact.
301
+ * History serialization prefers it over `createdAt` for the display
302
+ * timestamp.
303
+ */
304
+ sentAt: z.number().optional(),
262
305
  userMessageChannel: channelIdSchema.optional(),
263
306
  assistantMessageChannel: channelIdSchema.optional(),
264
307
  userMessageInterface: interfaceIdSchema.optional(),
@@ -413,13 +456,15 @@ export {
413
456
  } from "./conversation-types.js";
414
457
 
415
458
  /**
416
- * `messageKind` value marking a daemon-authored system card — a pre-composed
417
- * status reply (the /compact, /clean, and summarize-up-to result cards) that
418
- * bypasses the agent loop. Cards render as standalone system notices, never
419
- * as the assistant persona speaking, and never merge into adjacent assistant
420
- * display turns.
459
+ * The system-card `messageKind` marker and its predicate live in the
460
+ * `conversation-types` leaf so a caller that only stamps or classifies the
461
+ * marker does not pull in this module's DB graph, and are re-exported here
462
+ * alongside the schema that carries them.
421
463
  */
422
- export const SYSTEM_CARD_MESSAGE_KIND = "system_card";
464
+ export {
465
+ isSystemCardMetadata,
466
+ SYSTEM_CARD_MESSAGE_KIND,
467
+ } from "./conversation-types.js";
423
468
 
424
469
  /**
425
470
  * `messageKind` value marking the synthetic assistant row the agent loop
@@ -431,18 +476,6 @@ export const SYSTEM_CARD_MESSAGE_KIND = "system_card";
431
476
  */
432
477
  export const PROVIDER_ERROR_MESSAGE_KIND = "provider_error";
433
478
 
434
- /**
435
- * Shared predicate for the system-card marker on assistant-message metadata
436
- * (see the `messageKind` field on {@link messageMetadataSchema}). One
437
- * definition so display merging, transcript rendering, and turn grouping
438
- * cannot drift.
439
- */
440
- export function isSystemCardMetadata(
441
- metadata: Record<string, unknown> | null | undefined,
442
- ): boolean {
443
- return metadata?.messageKind === SYSTEM_CARD_MESSAGE_KIND;
444
- }
445
-
446
479
  /**
447
480
  * Shared predicate for the provider-error marker on assistant-message
448
481
  * metadata (see the `messageKind` field on {@link messageMetadataSchema}).
@@ -2110,6 +2143,8 @@ export function deleteConversation(id: string): DeletedMemoryIds {
2110
2143
  removeConversationDir(id, createdAtForDiskCleanup);
2111
2144
  }
2112
2145
 
2146
+ purgeWatchTimelineForDeletedConversation(id);
2147
+
2113
2148
  // Notify `conversation-deleted` hooks (e.g. the memory plugin failing its
2114
2149
  // still-pending jobs for this conversation). Fire-and-forget from this
2115
2150
  // synchronous primitive — the pipeline contains per-hook failures, and
@@ -2260,6 +2295,8 @@ export async function deleteConversationGently(
2260
2295
  removeConversationDir(id, createdAtForDiskCleanup);
2261
2296
  }
2262
2297
 
2298
+ purgeWatchTimelineForDeletedConversation(id);
2299
+
2263
2300
  // Notify `conversation-deleted` hooks — fire-and-forget, same contract as
2264
2301
  // the synchronous delete primitive.
2265
2302
  void runHook(HOOKS.CONVERSATION_DELETED, {
@@ -2467,7 +2504,7 @@ export function getMessages(conversationId: string): MessageRow[] {
2467
2504
  * offsets until the target is reached (capped at a reasonable maximum to bound
2468
2505
  * scan cost).
2469
2506
  */
2470
- export function selectSlackMetaCandidateMetadata(
2507
+ export function selectProviderMetaCandidateMetadata(
2471
2508
  conversationId: string,
2472
2509
  limit: number,
2473
2510
  offset = 0,
@@ -2482,10 +2519,14 @@ export function selectSlackMetaCandidateMetadata(
2482
2519
  lineageFilter(conversationId),
2483
2520
  opts?.includeFlatLegacy
2484
2521
  ? or(
2522
+ like(messages.metadata, '%"providerMeta"%'),
2485
2523
  like(messages.metadata, '%"slackMeta"%'),
2486
2524
  like(messages.metadata, '%"source":"slack"%'),
2487
2525
  )
2488
- : like(messages.metadata, '%"slackMeta"%'),
2526
+ : or(
2527
+ like(messages.metadata, '%"providerMeta"%'),
2528
+ like(messages.metadata, '%"slackMeta"%'),
2529
+ ),
2489
2530
  ),
2490
2531
  )
2491
2532
  .orderBy(asc(messages.createdAt))
@@ -3585,6 +3626,14 @@ export async function clearAll(): Promise<{
3585
3626
  // cascade; wipe them explicitly so labels/objectives don't survive (or
3586
3627
  // rehydrate after) a clear-all.
3587
3628
  await runOrThrow("DELETE FROM subagents");
3629
+ // Watch-session timelines are conversation-keyed rows the cascade does not
3630
+ // reach. They come after `conversations` so this statement is the last thing
3631
+ // that needs to reach one: an append arriving from here on finds no
3632
+ // conversation to key itself to and is refused, and an append that already
3633
+ // ran is a row this deletes. The bulk deletes above run in a sqlite3
3634
+ // subprocess, so the event loop is free while they work; this one is a
3635
+ // synchronous in-process statement, which nothing can interleave with.
3636
+ purgeAllWatchTimelines();
3588
3637
 
3589
3638
  // The memory feature's relocated conversation-keyed tables lost their main-DB
3590
3639
  // cascade. Signal the wipe through the hook rather than reaching into the
@@ -199,7 +199,8 @@ function surfacedVisibilitySql(alias = "conversations"): string {
199
199
  * explicitly surfaced. {@link standardListingVisibilitySql} already admits
200
200
  * such rows through its surfaced and custom-group arms, so a caller that
201
201
  * must distinguish "visible in the listing" from "user-facing foreground"
202
- * (unread counting) applies this on top.
202
+ * (unread counting, the section index, a `foregroundOnly` list read) applies
203
+ * this on top.
203
204
  *
204
205
  * The SQL twin of `isBackgroundConversation` in the web client's
205
206
  * `utils/conversation-predicates.ts`.
@@ -282,6 +283,16 @@ export interface ConversationListFilter {
282
283
  * the rows its attention surfaces need instead of scanning for them.
283
284
  */
284
285
  needsAttention?: true;
286
+ /**
287
+ * Restrict to user-facing foreground conversations: drop the automated
288
+ * background/scheduled rows the standard listing admits through its
289
+ * custom-group arm ({@link notBackgroundVisibilitySql}, the same predicate
290
+ * the unread count and the section index apply on top of visibility).
291
+ * Only `true` narrows; omit to keep every visible row. A client that
292
+ * needs the newest conversation a user can open can ask for it in one
293
+ * row instead of paging past runs it would skip.
294
+ */
295
+ foregroundOnly?: true;
285
296
  }
286
297
 
287
298
  export interface ConversationListQuery extends ConversationListFilter {
@@ -364,6 +375,7 @@ function conversationListWhere(filter: ConversationListFilter) {
364
375
  originChannel,
365
376
  groupId,
366
377
  needsAttention,
378
+ foregroundOnly,
367
379
  } = filter;
368
380
  return and(
369
381
  conversationTypeClause(conversationType),
@@ -371,6 +383,7 @@ function conversationListWhere(filter: ConversationListFilter) {
371
383
  originChannel ? originChannelClause(originChannel) : undefined,
372
384
  groupId ? groupIdClause(groupId) : undefined,
373
385
  ...(needsAttention ? unseenAttentionStateConditions() : []),
386
+ foregroundOnly ? sql.raw(notBackgroundVisibilitySql()) : undefined,
374
387
  );
375
388
  }
376
389
 
@@ -135,6 +135,28 @@ export function isBackgroundEventMetadata(
135
135
  return typeof metadata?.backgroundEventSource === "string";
136
136
  }
137
137
 
138
+ /**
139
+ * `messageKind` value marking a daemon-authored system card: a notice that
140
+ * bypasses the agent loop (the /compact, /clean, and summarize-up-to result
141
+ * cards, and plugin notices about what a turn did to the user's input). Cards
142
+ * render as standalone system notices, never as the assistant persona
143
+ * speaking, and never merge into adjacent assistant display turns.
144
+ *
145
+ * Lives in this leaf so a caller that only stamps or classifies the marker
146
+ * does not pull in `conversation-crud`'s DB graph.
147
+ */
148
+ export const SYSTEM_CARD_MESSAGE_KIND = "system_card";
149
+
150
+ /**
151
+ * Shared predicate for the system-card marker on assistant-message metadata,
152
+ * so display merging, transcript rendering, and turn grouping cannot drift.
153
+ */
154
+ export function isSystemCardMetadata(
155
+ metadata: Record<string, unknown> | null | undefined,
156
+ ): boolean {
157
+ return metadata?.messageKind === SYSTEM_CARD_MESSAGE_KIND;
158
+ }
159
+
138
160
  /**
139
161
  * True when a role-`"user"` row is internal scaffolding rather than a person's
140
162
  * prompt: a daemon-injected run lifecycle notification (subagent
@@ -216,6 +216,7 @@ async function runViaCli(
216
216
  // never an issue regardless of the statement complexity.
217
217
  const proc = Bun.spawn({
218
218
  cmd: [sqlite3Path, dbPath],
219
+ windowsHide: true,
219
220
  stdin: "pipe",
220
221
  stdout: "pipe",
221
222
  stderr: "pipe",
@@ -396,7 +397,7 @@ export function spawnDetachedWalCheckpoint(): boolean {
396
397
  getDbPath(),
397
398
  `PRAGMA busy_timeout=${SQLITE_BUSY_TIMEOUT_MS}; PRAGMA wal_checkpoint(TRUNCATE);`,
398
399
  ],
399
- { detached: true, stdio: "ignore" },
400
+ { detached: true, stdio: "ignore", windowsHide: true },
400
401
  );
401
402
  // A post-spawn failure can't be acted on — the daemon is exiting — but an
402
403
  // unlistened ChildProcess "error" event would throw as an uncaught
@@ -3,6 +3,7 @@ import { statSync } from "node:fs";
3
3
  import { getConfig } from "../config/loader.js";
4
4
  import { getLogger } from "../util/logger.js";
5
5
  import { getDbPath } from "../util/platform.js";
6
+ import { sweepOrphanedWatchTimelineEntries } from "../watch/watch-timeline.js";
6
7
  import { pruneRuns } from "../workflows/journal-store.js";
7
8
  import { getMemoryCheckpoint, setMemoryCheckpoint } from "./checkpoints.js";
8
9
  import { getLastInteractiveUserMessageTimestamp } from "./conversation-crud.js";
@@ -81,6 +82,20 @@ async function runDbMaintenance(): Promise<void> {
81
82
  log.warn({ err }, "Workflow run pruning failed (non-fatal)");
82
83
  }
83
84
 
85
+ // Reclaim watch-timeline entries whose conversation is gone. Their purge runs
86
+ // after the conversation row is already deleted and cannot be retried by the
87
+ // caller, and nothing cascades into the table, so a failed purge or a crash
88
+ // between the two writes would otherwise keep screenshots of the user's
89
+ // screen indefinitely. Bounded per pass and best-effort inside the sweep,
90
+ // which reports zero rather than throwing, so it needs no guard here.
91
+ const sweptWatchEntries = sweepOrphanedWatchTimelineEntries();
92
+ if (sweptWatchEntries > 0) {
93
+ log.info(
94
+ { sweptWatchEntries },
95
+ "Swept watch timeline entries for deleted conversations",
96
+ );
97
+ }
98
+
84
99
  // Refresh the query planner's statistics (see PLANNER_OPTIMIZE_PRAGMA for
85
100
  // what the mask buys here). Routed through the async path to keep it off the
86
101
  // main thread when the sqlite3 CLI backend is available.
@@ -5,15 +5,15 @@
5
5
  * finding messages by source identifiers, and managing raw payload storage.
6
6
  */
7
7
 
8
- import { and, desc, eq, isNotNull, ne, or, sql } from "drizzle-orm";
8
+ import { and, desc, eq, isNotNull, like, ne, or, sql } from "drizzle-orm";
9
9
  import { v4 as uuid } from "uuid";
10
10
 
11
11
  import type { ChannelId } from "../channels/types.js";
12
- import { readSlackMetadataFromMessageMetadata } from "../messaging/providers/slack/message-metadata.js";
12
+ import { readProviderMetadata } from "../messaging/read-provider-metadata.js";
13
13
  import type { SlackInboundMessageMetadata } from "../runtime/http-types.js";
14
14
  import { parseJsonSafe } from "../util/json.js";
15
15
  import { isPlainObject } from "../util/object.js";
16
- import { selectSlackMetaCandidateMetadata } from "./conversation-crud.js";
16
+ import { selectProviderMetaCandidateMetadata } from "./conversation-crud.js";
17
17
  import {
18
18
  getConversationByKey,
19
19
  getOrCreateConversation,
@@ -21,7 +21,12 @@ import {
21
21
  } from "./conversation-key-store.js";
22
22
  import type { NonScheduledConversationType } from "./conversation-types.js";
23
23
  import { getDb } from "./db-connection.js";
24
- import { channelInboundEvents, conversations } from "./schema.js";
24
+ import {
25
+ channelInboundEvents,
26
+ conversationKeys,
27
+ conversations,
28
+ messages,
29
+ } from "./schema.js";
25
30
 
26
31
  export interface InboundResult {
27
32
  accepted: boolean;
@@ -33,11 +38,26 @@ export interface InboundResult {
33
38
  export interface RecordInboundOptions {
34
39
  sourceMessageId?: string;
35
40
  sourceThreadId?: string;
41
+ /**
42
+ * Record the event against this conversation instead of resolving one from
43
+ * the address. For events that belong to a message rather than to a chat: a
44
+ * reaction lives in the conversation of the message it was attached to, and
45
+ * resolving from its own address would mint a second one.
46
+ */
47
+ conversationId?: string;
36
48
  }
37
49
 
38
50
  const SLACK_LEGACY_THREAD_EVIDENCE_BATCH_SIZE = 50;
39
51
  const SLACK_LEGACY_THREAD_EVIDENCE_MAX_SCAN = 500;
40
52
 
53
+ /**
54
+ * Rows examined when locating a Slack message the assistant itself posted.
55
+ * Bounded on purpose: the scan runs on the inbound path while the gateway
56
+ * waits for its ack, and reactions land on recent messages, so a cap costs
57
+ * almost no recall and keeps the cost flat as the database grows.
58
+ */
59
+ const SLACK_OUTBOUND_TS_MAX_SCAN = 400;
60
+
41
61
  /**
42
62
  * Channels where an inbound thread id scopes the conversation: a Slack thread
43
63
  * or a Telegram private-chat topic each maps to its own conversation. A
@@ -99,7 +119,7 @@ function legacySlackConversationHasThreadEvidence(
99
119
  SLACK_LEGACY_THREAD_EVIDENCE_BATCH_SIZE,
100
120
  remaining,
101
121
  );
102
- const metadataRows = selectSlackMetaCandidateMetadata(
122
+ const metadataRows = selectProviderMetaCandidateMetadata(
103
123
  conversationId,
104
124
  batchLimit,
105
125
  offset,
@@ -110,12 +130,10 @@ function legacySlackConversationHasThreadEvidence(
110
130
  return false;
111
131
  }
112
132
  for (const metadata of metadataRows) {
113
- const slackMeta = readSlackMetadataFromMessageMetadata(metadata, {
114
- allowFlatLegacy: true,
115
- });
133
+ const meta = readProviderMetadata(metadata, { allowFlatLegacy: true });
116
134
  if (
117
- slackMeta?.channelId === externalChatId &&
118
- slackMeta.threadTs === sourceThreadId
135
+ meta?.conversationExternalId === externalChatId &&
136
+ meta.threadId === sourceThreadId
119
137
  ) {
120
138
  return true;
121
139
  }
@@ -248,6 +266,93 @@ export function findInboundConversationId(
248
266
  * the conversation and tracks the reply. Both must key on the same three
249
267
  * fields for either to mean anything.
250
268
  */
269
+ /**
270
+ * The inbound event already recorded for this address, if any. Read-only twin
271
+ * of the dedup check inside {@link recordInbound}, for callers that must know
272
+ * an event is a redelivery before doing work that recording would otherwise
273
+ * gate (routing a guardian decision, for one).
274
+ */
275
+ export function findInboundEvent(
276
+ sourceChannel: string,
277
+ externalChatId: string,
278
+ externalMessageId: string,
279
+ ): { eventId: string; conversationId: string } | null {
280
+ const row = getDb()
281
+ .select({
282
+ id: channelInboundEvents.id,
283
+ conversationId: channelInboundEvents.conversationId,
284
+ })
285
+ .from(channelInboundEvents)
286
+ .where(
287
+ and(
288
+ eq(channelInboundEvents.sourceChannel, sourceChannel),
289
+ eq(channelInboundEvents.externalChatId, externalChatId),
290
+ eq(channelInboundEvents.externalMessageId, externalMessageId),
291
+ ),
292
+ )
293
+ .get();
294
+ return row ? { eventId: row.id, conversationId: row.conversationId } : null;
295
+ }
296
+
297
+ /**
298
+ * The conversation holding the message with this provider id, found by reading
299
+ * the metadata the assistant's own posts carry.
300
+ *
301
+ * Reads through `readProviderMetadata`, so it matches any channel that
302
+ * describes its rows in the neutral shape as well as Slack's own envelope.
303
+ *
304
+ * `findMessageBySourceId` covers every message that arrived as an inbound
305
+ * event. It cannot see what the assistant posted, because an outbound reply
306
+ * opens no inbound event, so a reaction on the assistant's own message needs
307
+ * this. The search is confined to conversations already bound to the same
308
+ * Slack channel and to the most recent {@link SLACK_OUTBOUND_TS_MAX_SCAN}
309
+ * rows among them; beyond that it gives up and the caller drops the
310
+ * annotation, which is the same outcome as never finding it at all.
311
+ */
312
+ export function findSlackConversationByMessageTs(
313
+ externalChatId: string,
314
+ channelTs: string,
315
+ ): string | null {
316
+ const db = getDb();
317
+ const keyPrefix = `${CONVERSATION_KEY_SCOPE}:slack:${externalChatId}`;
318
+ const rows = db
319
+ .select({
320
+ conversationId: messages.conversationId,
321
+ metadata: messages.metadata,
322
+ })
323
+ .from(messages)
324
+ .innerJoin(
325
+ conversationKeys,
326
+ eq(conversationKeys.conversationId, messages.conversationId),
327
+ )
328
+ .where(
329
+ and(
330
+ or(
331
+ eq(conversationKeys.conversationKey, keyPrefix),
332
+ like(conversationKeys.conversationKey, `${keyPrefix}:thread:%`),
333
+ ),
334
+ or(
335
+ like(messages.metadata, '%"providerMeta"%'),
336
+ like(messages.metadata, '%"slackMeta"%'),
337
+ ),
338
+ ),
339
+ )
340
+ .orderBy(desc(messages.createdAt))
341
+ .limit(SLACK_OUTBOUND_TS_MAX_SCAN)
342
+ .all();
343
+
344
+ for (const row of rows) {
345
+ const meta = readProviderMetadata(row.metadata, { allowFlatLegacy: true });
346
+ if (
347
+ meta?.conversationExternalId === externalChatId &&
348
+ meta.messageId === channelTs
349
+ ) {
350
+ return row.conversationId;
351
+ }
352
+ }
353
+ return null;
354
+ }
355
+
251
356
  export function recordInbound(
252
357
  sourceChannel: string,
253
358
  externalChatId: string,
@@ -280,11 +385,13 @@ export function recordInbound(
280
385
  };
281
386
  }
282
387
 
283
- const mapping = resolveInboundConversation(
284
- sourceChannel,
285
- externalChatId,
286
- options?.sourceThreadId,
287
- );
388
+ const mapping = options?.conversationId
389
+ ? { conversationId: options.conversationId }
390
+ : resolveInboundConversation(
391
+ sourceChannel,
392
+ externalChatId,
393
+ options?.sourceThreadId,
394
+ );
288
395
  const now = Date.now();
289
396
  const eventId = uuid();
290
397
 
@@ -154,6 +154,7 @@ export class QdrantManager {
154
154
 
155
155
  const proc = Bun.spawn({
156
156
  cmd: [spawnPath],
157
+ windowsHide: true,
157
158
  env: {
158
159
  ...process.env,
159
160
  QDRANT__SERVICE__HOST: this.host,
@@ -0,0 +1,46 @@
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
+
3
+ /**
4
+ * Creates `watch_timeline_entries`: the per-session record of what a watch
5
+ * session's user narrated and what was on their screen.
6
+ *
7
+ * The table is the timeline's only home. Entries live outside conversation
8
+ * message history, so nothing here participates in turn alternation or
9
+ * provider-facing sanitization, and only the retrospective's rendered summary
10
+ * reaches a model.
11
+ *
12
+ * `screenshot_attachment_id` holds the id of a row in `attachments`, so the
13
+ * pixels stay on disk behind the attachment store rather than in this table.
14
+ * Timeline rows and the screenshots they own are removed together by the
15
+ * watch store's purge.
16
+ *
17
+ * The two indexes cover the two access paths: reads are scoped to one session
18
+ * and ordered by offset, and purges are scoped to one conversation.
19
+ *
20
+ * Idempotent: `IF NOT EXISTS` on the table and both indexes.
21
+ */
22
+ export function migrateCreateWatchTimelineEntries(db: DrizzleDb): void {
23
+ const raw = getSqliteFrom(db);
24
+
25
+ raw.exec(/*sql*/ `
26
+ CREATE TABLE IF NOT EXISTS watch_timeline_entries (
27
+ id TEXT PRIMARY KEY,
28
+ session_id TEXT NOT NULL,
29
+ conversation_id TEXT NOT NULL,
30
+ at_ms INTEGER NOT NULL,
31
+ kind TEXT NOT NULL,
32
+ text TEXT NOT NULL,
33
+ ax_tree TEXT,
34
+ ax_diff TEXT,
35
+ screenshot_attachment_id TEXT,
36
+ created_at INTEGER NOT NULL
37
+ )
38
+ `);
39
+
40
+ raw.exec(
41
+ `CREATE INDEX IF NOT EXISTS idx_watch_timeline_entries_session ON watch_timeline_entries(session_id, at_ms)`,
42
+ );
43
+ raw.exec(
44
+ `CREATE INDEX IF NOT EXISTS idx_watch_timeline_entries_conversation ON watch_timeline_entries(conversation_id)`,
45
+ );
46
+ }
@@ -0,0 +1,33 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { tableHasColumn } from "./schema-introspection.js";
3
+
4
+ /**
5
+ * Move a watch timeline entry's screenshot into the row that owns it.
6
+ *
7
+ * `screenshot` holds the frame's JPEG bytes directly, so an entry is one row
8
+ * with one lifetime: a single `DELETE` takes the narration, the screen, and
9
+ * the pixels together, with nothing staged elsewhere to coordinate against.
10
+ * `screenshot_attachment_id`, the id of a separately staged file, goes with
11
+ * it.
12
+ *
13
+ * Idempotent: each `ALTER TABLE` is guarded by a column check, so a crash
14
+ * between the two leaves the next boot able to finish the job.
15
+ */
16
+ export function migrateWatchTimelineScreenshotBlob(database: DrizzleDb): void {
17
+ if (!tableHasColumn(database, "watch_timeline_entries", "screenshot")) {
18
+ database.run(
19
+ /*sql*/ `ALTER TABLE watch_timeline_entries ADD COLUMN screenshot BLOB`,
20
+ );
21
+ }
22
+ if (
23
+ tableHasColumn(
24
+ database,
25
+ "watch_timeline_entries",
26
+ "screenshot_attachment_id",
27
+ )
28
+ ) {
29
+ database.run(
30
+ /*sql*/ `ALTER TABLE watch_timeline_entries DROP COLUMN screenshot_attachment_id`,
31
+ );
32
+ }
33
+ }