@vellumai/assistant 0.11.1 → 0.11.2-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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -13,6 +13,7 @@ import {
13
13
  getAppDirPath,
14
14
  listAppFiles,
15
15
  resolveAppDir,
16
+ resolveAppSource,
16
17
  } from "../apps/app-store.js";
17
18
  import { type ChannelId, parseInterfaceId } from "../channels/types.js";
18
19
  import { resolveDefaultProfileForProvider } from "../config/default-profile-catalog.js";
@@ -433,6 +434,54 @@ export function buildActiveDocuments(conversationId: string): Array<{
433
434
  : null;
434
435
  }
435
436
 
437
+ /**
438
+ * Resolves the app the client reported as on-screen into the summary the
439
+ * `unified-turn-context` injector renders as the `visible_app:` line. Returns
440
+ * `null` when no app is in view or the id no longer resolves to an app (e.g.
441
+ * it was deleted while open).
442
+ *
443
+ * Resolution goes through `resolveAppSource` rather than `getApp` so both id
444
+ * shapes the viewer can hold are covered: workspace apps (opaque id) and
445
+ * plugin-bundled apps (`plugins~<plugin>~<app>`), which the apps list and open
446
+ * routes serve alongside them. Plugin apps carry their owning plugin so the
447
+ * rendered line can say the source belongs to a plugin rather than reading as
448
+ * an ordinary sandbox app to rewrite.
449
+ *
450
+ * Both identifiers come back: a workspace app's `appId` is an opaque UUID and
451
+ * the readable handle is its `slug` (the directory stem, frozen at creation).
452
+ * The app tools key off the id, so the line carries it for tool calls and the
453
+ * slug for everything a human or the model would recognize.
454
+ */
455
+ export function buildVisibleAppContext(appId: string | undefined): {
456
+ appId: string;
457
+ name: string;
458
+ slug: string;
459
+ sourceDir: string;
460
+ pluginName?: string;
461
+ } | null {
462
+ if (!appId) {
463
+ return null;
464
+ }
465
+ try {
466
+ const source = resolveAppSource(appId);
467
+ if (!source) {
468
+ return null;
469
+ }
470
+ return {
471
+ appId: source.id,
472
+ name: source.name,
473
+ slug: source.dirName,
474
+ sourceDir: source.sourceDir,
475
+ ...(source.origin.kind === "plugin"
476
+ ? { pluginName: source.origin.pluginName }
477
+ : {}),
478
+ };
479
+ } catch {
480
+ // Malformed id (traversal-shaped, empty): treat as no app in view.
481
+ return null;
482
+ }
483
+ }
484
+
436
485
  const MAX_CONTEXT_LENGTH = 100_000;
437
486
 
438
487
  function truncateHtml(html: string, budget: number): string {
@@ -2195,6 +2244,13 @@ export async function applyRuntimeInjections(
2195
2244
  // `clientOs`) so a queued message from another surface can't perturb the
2196
2245
  // in-flight turn — same anti-race pattern as `clientTimezone`.
2197
2246
  const clientOs = liveConversation?.currentTurnClientOs ?? undefined;
2247
+ // The app the client has on screen (app viewer or the app-editing split),
2248
+ // resolved to its name and source directory so the assistant can act on it
2249
+ // without asking the user which app they mean. Frozen per turn like
2250
+ // `clientOs` for the same anti-race reason.
2251
+ const visibleApp = buildVisibleAppContext(
2252
+ liveConversation?.currentTurnVisibleAppId,
2253
+ );
2198
2254
  const channelName = liveConversation
2199
2255
  ? (liveConversation.currentTurnChannelContext?.userMessageChannel ??
2200
2256
  liveConversation.originChannel ??
@@ -2304,6 +2360,7 @@ export async function applyRuntimeInjections(
2304
2360
  timestamp,
2305
2361
  interfaceName,
2306
2362
  clientOs,
2363
+ visibleApp,
2307
2364
  channelName,
2308
2365
  actorContext: options.actorContext,
2309
2366
  configuredUserTimezone,
@@ -51,6 +51,30 @@ import { buildTransportHints } from "./transport-hints.js";
51
51
 
52
52
  const conversationOptions = new Map<string, ConversationCreateOptions>();
53
53
 
54
+ /**
55
+ * Drops the transport fields that describe what the client had on screen for a
56
+ * single message rather than for the conversation as a whole.
57
+ *
58
+ * `conversationOptions` is a durable map: a rebuilt conversation (evicted or
59
+ * gone stale) re-applies whatever transport was stored the last time anyone
60
+ * touched it. View state must not survive that, or a scheduled wake hours
61
+ * later would resurrect an app the user has since closed and assert it is on
62
+ * screen. The current call still applies the full transport to the live
63
+ * conversation, so only the persisted copy is trimmed.
64
+ */
65
+ export function withoutTurnScopedTransport(
66
+ options: ConversationCreateOptions,
67
+ ): ConversationCreateOptions {
68
+ const transport = options.transport;
69
+ if (!transport || transport.visibleAppId === undefined) {
70
+ return options;
71
+ }
72
+ return {
73
+ ...options,
74
+ transport: { ...transport, visibleAppId: undefined },
75
+ };
76
+ }
77
+
54
78
  export function mergeConversationOptions(
55
79
  conversationId: string,
56
80
  patch: Partial<ConversationCreateOptions>,
@@ -86,6 +110,7 @@ function applyTransportMetadata(
86
110
  conversation.applyHostEnvFromTransport(transport);
87
111
  conversation.applyClientTimezoneFromTransport(transport);
88
112
  conversation.applyClientOsFromTransport(transport);
113
+ conversation.applyVisibleAppFromTransport(transport);
89
114
  }
90
115
 
91
116
  /**
@@ -110,7 +135,10 @@ export async function getOrCreateConversation(
110
135
  ...persistentOptions
111
136
  } = options ?? {};
112
137
  if (Object.values(persistentOptions).some((v) => v !== undefined)) {
113
- mergeConversationOptions(conversationId, persistentOptions);
138
+ mergeConversationOptions(
139
+ conversationId,
140
+ withoutTurnScopedTransport(persistentOptions),
141
+ );
114
142
  }
115
143
 
116
144
  if (
@@ -221,6 +249,13 @@ export async function getOrCreateConversation(
221
249
  await newConversation.ensureActorScopedHistory();
222
250
  }
223
251
  applyTransportMetadata(newConversation, storedOptions);
252
+ // The stored transport is stripped of view state, so a rebuild driven by
253
+ // a live send takes the app on screen from THIS call. A rebuild with no
254
+ // inbound transport (a scheduled wake, a background follow-up) correctly
255
+ // leaves it unset rather than inheriting whatever was open last time.
256
+ if (options?.transport) {
257
+ newConversation.applyVisibleAppFromTransport(options.transport);
258
+ }
224
259
  setConversation(conversationId, newConversation);
225
260
  return newConversation;
226
261
  })();
@@ -13,6 +13,7 @@ import {
13
13
  isKnownSurfaceType,
14
14
  MODEL_INVOKABLE_SURFACE_TYPES,
15
15
  normalizeCopyBlockShowData,
16
+ normalizeVisualShowData,
16
17
  OAuthConnectSurfaceDataSchema,
17
18
  safeParseSurfaceData,
18
19
  SurfaceTypeSchema,
@@ -98,6 +99,33 @@ import type { HostAppControlInput } from "./message-types/host-app-control.js";
98
99
  import type { UserMessageAttachment } from "./message-types/shared.js";
99
100
  import type { TrustContext } from "./trust-context-types.js";
100
101
 
102
+ /**
103
+ * Prefix of the synthetic user-message text this module writes when a surface
104
+ * action has no custom prompt of its own (`[User action on app: ...]`,
105
+ * `[User action on <surface_type> surface: ...]`).
106
+ *
107
+ * Machine-authored, so turns carrying it must be stamped `scripted` and kept
108
+ * out of activation counts. Deliberately the SAME anchor the analytics
109
+ * classifier matches on (`stg_telemetry__scripted_turn.sql`, "Synthetic
110
+ * UI-surface action events"): the two signals are compared against each other
111
+ * by the `assert_scripted_signals_agree` dbt test, so if this string ever
112
+ * changes, that model's anchor has to change with it or the test fires.
113
+ */
114
+ const SYNTHETIC_SURFACE_ACTION_PREFIX = "[User action on ";
115
+
116
+ /**
117
+ * True when `content` is the synthetic fallback text above rather than a
118
+ * custom prompt supplied by the surface.
119
+ *
120
+ * Only the synthetic form is scripted. A surface that supplies its own prompt
121
+ * is treated as a real turn, because that is what the existing analytics
122
+ * classifier does. Widening this to every surface action would silently move
123
+ * activation beyond the bug being fixed.
124
+ */
125
+ function isSyntheticSurfaceActionContent(content: string): boolean {
126
+ return content.startsWith(SYNTHETIC_SURFACE_ACTION_PREFIX);
127
+ }
128
+
101
129
  const log = getLogger("conversation-surfaces");
102
130
 
103
131
  // Tolerant variant of SurfaceActionSchema for parsing raw model output.
@@ -112,6 +140,16 @@ const ModelActionSchema = SurfaceActionSchema.extend({
112
140
 
113
141
  const MAX_UNDO_DEPTH = 10;
114
142
 
143
+ /**
144
+ * Pending surface types that do not hold the one-interactive-surface-at-a-time
145
+ * lock. Both render content the user reads rather than a question they must
146
+ * answer, so a live one must not block the next surface.
147
+ */
148
+ const NON_BLOCKING_PENDING_SURFACE_TYPES = new Set<SurfaceType>([
149
+ "dynamic_page",
150
+ "visual",
151
+ ]);
152
+
115
153
  /**
116
154
  * Debounce window for persisting `ui_surface_update` data back to the
117
155
  * message row. Surfaces typically receive bursts of updates (e.g. a
@@ -2012,6 +2050,9 @@ export async function handleSurfaceAction(
2012
2050
  activeSurfaceId: surfaceId,
2013
2051
  displayContent,
2014
2052
  sourceActorPrincipalId,
2053
+ // Rides the metadata bag rather than a typed option: the queue
2054
+ // round-trips `metadata` but not `PersistMessageOptions`.
2055
+ metadata: { scripted: isSyntheticSurfaceActionContent(content) },
2015
2056
  });
2016
2057
 
2017
2058
  if (result.rejected) {
@@ -2069,6 +2110,7 @@ export async function handleSurfaceAction(
2069
2110
  activeSurfaceId: surfaceId,
2070
2111
  displayContent,
2071
2112
  sourceActorPrincipalId,
2113
+ scripted: isSyntheticSurfaceActionContent(content),
2072
2114
  })
2073
2115
  .catch((err) => {
2074
2116
  const message = err instanceof Error ? err.message : String(err);
@@ -2254,6 +2296,9 @@ export async function handleSurfaceAction(
2254
2296
  activeSurfaceId: surfaceId,
2255
2297
  displayContent,
2256
2298
  sourceActorPrincipalId,
2299
+ // Rides the metadata bag rather than a typed option: the queue
2300
+ // round-trips `metadata` but not `PersistMessageOptions`.
2301
+ metadata: { scripted: isSyntheticSurfaceActionContent(content) },
2257
2302
  });
2258
2303
  if (result.rejected) {
2259
2304
  ctx.surfaceActionRequestIds.delete(requestId);
@@ -2336,6 +2381,7 @@ export async function handleSurfaceAction(
2336
2381
  activeSurfaceId: surfaceId,
2337
2382
  displayContent,
2338
2383
  sourceActorPrincipalId,
2384
+ scripted: isSyntheticSurfaceActionContent(content),
2339
2385
  })
2340
2386
  .catch((err) => {
2341
2387
  const message = err instanceof Error ? err.message : String(err);
@@ -2992,9 +3038,9 @@ export async function surfaceProxyResolver(
2992
3038
  }
2993
3039
 
2994
3040
  // Every surface type parses through its canonical schema. Card,
2995
- // copy_block, and dynamic_page first run bespoke normalizers that
2996
- // recover fields the model placed at the top level of the tool input;
2997
- // the rest go straight through `safeParseSurfaceData`. Choice and
3041
+ // copy_block, dynamic_page, and visual first run bespoke normalizers
3042
+ // that recover fields the model placed at the top level of the tool
3043
+ // input; the rest go straight through `safeParseSurfaceData`. Choice and
2998
3044
  // oauth_connect parse into named bindings so their content guards below
2999
3045
  // read typed data instead of re-narrowing the union.
3000
3046
  const cardData =
@@ -3026,7 +3072,12 @@ export async function surfaceProxyResolver(
3026
3072
  surfaceType,
3027
3073
  data: normalizeDynamicPageShowData(input, rawData),
3028
3074
  }
3029
- : parseShowPairOrThrow(surfaceType, rawData);
3075
+ : surfaceType === "visual"
3076
+ ? {
3077
+ surfaceType,
3078
+ data: normalizeVisualShowData(input, rawData),
3079
+ }
3080
+ : parseShowPairOrThrow(surfaceType, rawData);
3030
3081
  // Parse actions through the schema instead of typecasting raw model output.
3031
3082
  // The model may place actions inside `data` instead of the top-level
3032
3083
  // `actions` param — recover them so they aren't silently dropped.
@@ -3103,10 +3154,12 @@ export async function surfaceProxyResolver(
3103
3154
 
3104
3155
  // Only one non-persistent interactive surface at a time. If another
3105
3156
  // surface is already awaiting user input, reject this one so the LLM
3106
- // presents surfaces sequentially.
3157
+ // presents surfaces sequentially. dynamic_page and visual are rendered
3158
+ // content rather than a question posed to the user, so a pending one
3159
+ // never blocks the next surface.
3107
3160
  if (awaitAction) {
3108
3161
  const hasExistingPending = [...ctx.pendingSurfaceActions.values()].some(
3109
- (entry) => entry.surfaceType !== "dynamic_page",
3162
+ (entry) => !NON_BLOCKING_PENDING_SURFACE_TYPES.has(entry.surfaceType),
3110
3163
  );
3111
3164
  if (hasExistingPending) {
3112
3165
  return {
@@ -3215,6 +3268,15 @@ export async function surfaceProxyResolver(
3215
3268
  yieldToUser: true,
3216
3269
  };
3217
3270
  }
3271
+ if (surfaceType === "visual") {
3272
+ return {
3273
+ content: JSON.stringify({
3274
+ surfaceId,
3275
+ note: "The visual is now visible inline in the chat. Continue your response in prose and do not describe or restate what the visual shows. To replace it, call ui_dismiss with this surfaceId and show a new one.",
3276
+ }),
3277
+ isError: false,
3278
+ };
3279
+ }
3218
3280
  return { content: JSON.stringify({ surfaceId }), isError: false };
3219
3281
  }
3220
3282
 
@@ -22,8 +22,10 @@ import {
22
22
  derefToolResultReReads,
23
23
  postTurnTruncateToolResults,
24
24
  } from "../context/post-turn-tool-result-truncation.js";
25
+ import { emitAssistantReplyNotification } from "../notifications/assistant-reply-producer.js";
25
26
  import { projectAssistantMessage } from "../persistence/conversation-attention-store.js";
26
27
  import {
28
+ type ConversationRow,
27
29
  getConversation,
28
30
  getMessageById,
29
31
  parseMessageMetadata,
@@ -142,12 +144,44 @@ export async function runDeferredTurnTail(params: {
142
144
  state: TurnTailState;
143
145
  rlog: pino.Logger;
144
146
  generationCompletedAt: number;
147
+ /**
148
+ * True only for a `message_complete` turn that produced a genuine reply. A
149
+ * handed-off generation is not the end of the run, a cancelled one has no
150
+ * final reply, and a provider-error turn's only assistant row is the
151
+ * synthetic error text.
152
+ */
153
+ turnCompleted: boolean;
154
+ /** Id of the row that opened this turn, for the notification producer. */
155
+ userMessageId: string | undefined;
156
+ /**
157
+ * True when this turn's reply streams to the app and nowhere else, for the
158
+ * notification producer's delivery-surface gates.
159
+ */
160
+ replyDeliveredInAppOnly?: boolean;
145
161
  }): Promise<void> {
146
- const { ctx, state, rlog, generationCompletedAt } = params;
162
+ const {
163
+ ctx,
164
+ state,
165
+ rlog,
166
+ generationCompletedAt,
167
+ turnCompleted,
168
+ userMessageId,
169
+ replyDeliveredInAppOnly,
170
+ } = params;
147
171
  const tailStartedAt = Date.now();
148
172
 
173
+ // Conversation row for the notification producer below. Guarded like every
174
+ // step below, because this runs after the terminal SSE and a SQLite hiccup
175
+ // must not escape into the loop's outer catch.
176
+ let conversation: ConversationRow | null = null;
177
+ try {
178
+ conversation = getConversation(ctx.conversationId);
179
+ } catch (err) {
180
+ rlog.warn({ err }, "Failed to read conversation for end-of-turn work");
181
+ }
182
+
149
183
  // Per-message memory/attention finalize side-effects deferred from
150
- // `handleMessageComplete` one closure per assistant row produced this turn,
184
+ // `handleMessageComplete`: one closure per assistant row produced this turn,
151
185
  // in production order.
152
186
  for (const effect of state.deferredFinalizeEffects) {
153
187
  try {
@@ -157,16 +191,44 @@ export async function runDeferredTurnTail(params: {
157
191
  }
158
192
  }
159
193
 
194
+ // Notify about a finished reply the user is no longer looking at. Ordered
195
+ // after the loop above so the producer's unseen check reads the attention
196
+ // cursor this turn's last row just projected. Not awaited: the pipeline
197
+ // awaits the platform adapter's HTTP dispatch, which retries with backoff,
198
+ // and nothing below here depends on the emit. The producer never rejects.
199
+ if (turnCompleted && state.lastAssistantMessageId) {
200
+ void emitAssistantReplyNotification({
201
+ conversationId: ctx.conversationId,
202
+ assistantMessageId: state.lastAssistantMessageId,
203
+ userMessageId,
204
+ ...(replyDeliveredInAppOnly ? { replyDeliveredInAppOnly: true } : {}),
205
+ rlog,
206
+ conversation,
207
+ });
208
+ }
209
+
210
+ // Fresh row for the two filesystem steps below. The awaited work above is
211
+ // long enough for the user to delete the conversation, and the row read at
212
+ // the top of the tail stays truthy across that deletion, so spooling or
213
+ // mirroring against it would rebuild a deleted conversation's directory from
214
+ // in-memory history. A row that is gone skips both steps exactly as an
215
+ // unreadable one does.
216
+ let liveConversation: ConversationRow | null = null;
217
+ try {
218
+ liveConversation = getConversation(ctx.conversationId);
219
+ } catch (err) {
220
+ rlog.warn({ err }, "Failed to re-read conversation for end-of-turn work");
221
+ }
222
+
160
223
  // Post-turn tool-result truncation: spool oversized results to disk and
161
224
  // replace their in-context content with a stub + pointer, shrinking the next
162
225
  // turn's context. Rewrites only the in-memory history, so it has no bearing on
163
226
  // the reply already delivered to the client.
164
227
  try {
165
- const conv = getConversation(ctx.conversationId);
166
- if (conv) {
228
+ if (liveConversation) {
167
229
  const convDir = getResolvedConversationDirPath(
168
230
  ctx.conversationId,
169
- conv.createdAt,
231
+ liveConversation.createdAt,
170
232
  );
171
233
  const { messages: derefMessages, dereferencedCount } =
172
234
  derefToolResultReReads(ctx.messages);
@@ -199,20 +261,16 @@ export async function runDeferredTurnTail(params: {
199
261
  }
200
262
 
201
263
  // Mirror the final assistant row into the JSONL disk view. Guarded like the
202
- // steps above: this runs AFTER the terminal SSE, so a throw here (e.g. a
203
- // SQLite read failure in `getConversation`) must not escape into the loop's
204
- // outer catch and emit a second, contradictory terminal event for a turn the
205
- // client already saw complete.
264
+ // steps above: this runs AFTER the terminal SSE, so a throw here must not
265
+ // escape into the loop's outer catch and emit a second, contradictory
266
+ // terminal event for a turn the client already saw complete.
206
267
  try {
207
- if (state.lastAssistantMessageId) {
208
- const convForDisk = getConversation(ctx.conversationId);
209
- if (convForDisk) {
210
- syncMessageToDisk(
211
- ctx.conversationId,
212
- state.lastAssistantMessageId,
213
- convForDisk.createdAt,
214
- );
215
- }
268
+ if (state.lastAssistantMessageId && liveConversation) {
269
+ syncMessageToDisk(
270
+ ctx.conversationId,
271
+ state.lastAssistantMessageId,
272
+ liveConversation.createdAt,
273
+ );
216
274
  }
217
275
  } catch (err) {
218
276
  rlog.warn({ err }, "Failed to sync assistant message to disk (non-fatal)");
@@ -622,6 +622,23 @@ export class Conversation {
622
622
  * @internal
623
623
  */
624
624
  currentTurnClientOs?: string;
625
+ /**
626
+ * @internal
627
+ * Id of the app the client currently has open on screen, reported by the
628
+ * client on each message. Drives the per-turn `visible_app:` context line so
629
+ * the assistant can resolve "the app" to what the user is looking at. This
630
+ * is the LIVE value; the assembly reads the frozen
631
+ * {@link currentTurnVisibleAppId}.
632
+ */
633
+ visibleAppId?: string;
634
+ /**
635
+ * Per-turn frozen copy of {@link visibleAppId}, captured by the agent loop at
636
+ * turn start for the same reason as {@link currentTurnClientOs}: a queued
637
+ * message sent from a different view re-applies transport metadata before it
638
+ * is enqueued, and must not swap the app under the in-flight turn.
639
+ * @internal
640
+ */
641
+ currentTurnVisibleAppId?: string;
625
642
  /**
626
643
  * Per-turn temporal snapshot frozen by the agent loop and read by
627
644
  * `applyRuntimeInjections` to build the `<turn_context>` timezone-mismatch
@@ -1957,26 +1974,64 @@ export class Conversation {
1957
1974
  }
1958
1975
  }
1959
1976
 
1960
- async forceCompact(): Promise<ContextWindowResult> {
1961
- // Report the user-facing before/after using the provider's real tokenizer
1962
- // (count_tokens) so the `/compact` line matches the context-window
1963
- // indicator, which reflects the provider's actual reported usage rather
1964
- // than the local chars/4 estimate the compaction pipeline runs internally
1965
- // (it under-counts by ~25% on typical histories). `calculateTokens`
1966
- // falls back to that estimate when the provider has no count endpoint or
1967
- // the count call fails, so behavior degrades gracefully.
1968
- //
1969
- // Only the *displayed* numbers are overridden the compaction log and
1970
- // circuit-breaker accounting inside `runCompaction` keep the estimate-based
1971
- // figures, leaving calibration and historical logs untouched.
1977
+ /**
1978
+ * Push the conversation's current context-window usage to clients so the
1979
+ * context-window indicator matches the numbers a user-initiated compaction
1980
+ * card reports. Turn-driven compaction needs no push: the turn's own
1981
+ * `usage_update` carries the post-compaction count.
1982
+ *
1983
+ * Defaults to the conversation's own sender, the channel `emitActivityState`
1984
+ * and `context_compacted` already use, so a queued `/compact` reaches the
1985
+ * same client its result card does. Routes that resolve a conversation
1986
+ * outside the send path never wire that sender and pass their own `onEvent`.
1987
+ */
1988
+ private emitContextWindowUsage(
1989
+ tokens: number,
1990
+ maxTokens: number,
1991
+ onEvent?: (msg: AssistantEvent) => void,
1992
+ ): void {
1993
+ try {
1994
+ (onEvent ?? this.sendToClient)({
1995
+ type: "context_window_usage",
1996
+ conversationId: this.conversationId,
1997
+ tokens,
1998
+ maxTokens,
1999
+ });
2000
+ } catch (err) {
2001
+ log.warn(
2002
+ { err, conversationId: this.conversationId },
2003
+ "sendToClient threw in emitContextWindowUsage",
2004
+ );
2005
+ }
2006
+ }
2007
+
2008
+ /**
2009
+ * Run a user-initiated compaction (`run`), reporting its before/after with
2010
+ * the provider's real tokenizer (count_tokens) rather than the local chars/4
2011
+ * estimate the compaction pipeline runs internally (it under-counts by ~25%
2012
+ * on typical histories), and pushing the resulting count to clients.
2013
+ * `calculateTokens` falls back to that estimate when the provider has no
2014
+ * count endpoint or the count call fails, so behavior degrades gracefully.
2015
+ *
2016
+ * Only the *displayed* numbers are overridden: the compaction log and
2017
+ * circuit-breaker accounting inside `runCompaction` keep the estimate-based
2018
+ * figures, leaving calibration and historical logs untouched.
2019
+ *
2020
+ * `run` must leave the compacted history applied to `this.messages`, which
2021
+ * every `runCompaction` path does. `onEvent` overrides the sink the usage
2022
+ * push goes to.
2023
+ */
2024
+ private async runUserCompaction(
2025
+ run: () => Promise<ContextWindowResult>,
2026
+ onEvent?: (msg: AssistantEvent) => void,
2027
+ ): Promise<ContextWindowResult> {
1972
2028
  const before = await this.calculateTokens(this.messages);
1973
- const result = await this.runCompaction(true);
1974
- // `runCompaction` applies the compacted history to `this.messages` in
1975
- // place, so after a successful compaction this re-counts the new history;
1976
- // a no-op leaves the context unchanged, so before === after.
2029
+ const result = await run();
2030
+ // A no-op leaves the context unchanged, so before === after.
1977
2031
  const after = result.compacted
1978
2032
  ? await this.calculateTokens(this.messages)
1979
2033
  : before;
2034
+ this.emitContextWindowUsage(after, result.maxInputTokens, onEvent);
1980
2035
  return {
1981
2036
  ...result,
1982
2037
  previousEstimatedInputTokens: before,
@@ -1984,6 +2039,20 @@ export class Conversation {
1984
2039
  };
1985
2040
  }
1986
2041
 
2042
+ /**
2043
+ * `/compact`. `onEvent` is the sink for the context-window usage push, and
2044
+ * callers pass whatever sink they render the result card through: the queue
2045
+ * drain carries the queued item's own `onEvent`, and `sendToClient` is reset
2046
+ * to a no-op once an interactive turn finishes (`process-message.ts`), so a
2047
+ * `/compact` draining behind that turn would otherwise push into nothing
2048
+ * while its card still reaches the client.
2049
+ */
2050
+ async forceCompact(
2051
+ onEvent?: (msg: AssistantEvent) => void,
2052
+ ): Promise<ContextWindowResult> {
2053
+ return this.runUserCompaction(() => this.runCompaction(true), onEvent);
2054
+ }
2055
+
1987
2056
  /**
1988
2057
  * "Summarize up to here": summarize everything before the turn containing
1989
2058
  * `beforeMessageId`, keeping that turn and everything after it verbatim.
@@ -1996,9 +2065,15 @@ export class Conversation {
1996
2065
  * `resolveMetaSlashCommand`. Throws {@link UserError} (messages are
1997
2066
  * user-facing) when the boundary cannot be resolved or the row→history
1998
2067
  * index mapping cannot be verified.
2068
+ *
2069
+ * `onEvent` is the sink for the context-window usage push. The management
2070
+ * route that owns this action resolves its conversation outside the send
2071
+ * path, so the instance can still hold the store's no-op sender; it passes
2072
+ * the same broadcast path its result card goes out on.
1999
2073
  */
2000
2074
  async summarizeUpToMessage(
2001
2075
  beforeMessageId: string,
2076
+ onEvent?: (msg: AssistantEvent) => void,
2002
2077
  ): Promise<ContextWindowResult> {
2003
2078
  const priorTrustContext = this.trustContext;
2004
2079
  if (!resolveCapabilities(priorTrustContext?.trustClass).canAccessMemory) {
@@ -2078,17 +2153,22 @@ export class Conversation {
2078
2153
  firstRowByHistoryIndex[historyIndex] = rowIndex;
2079
2154
  }
2080
2155
  }
2081
- return await this.runCompaction(true, undefined, {
2082
- fixedTailStartIndex: tailIndex,
2083
- // When repair merged preceding continuation rows into the boundary's
2084
- // message, the row boundary retreats to the message's first
2085
- // contributing row: the summary call reads messages[0..tailIndex),
2086
- // which excludes the merged rows' content, so the image manifest and
2087
- // watermarks must not treat them as summarized.
2088
- fixedBoundaryRowIndex:
2089
- firstRowByHistoryIndex[tailIndex] ?? boundaryRowIndex,
2090
- fixedBoundaryRowView: { rows, firstRowByHistoryIndex },
2091
- });
2156
+ return await this.runUserCompaction(
2157
+ () =>
2158
+ this.runCompaction(true, undefined, {
2159
+ fixedTailStartIndex: tailIndex,
2160
+ // When repair merged preceding continuation rows into the
2161
+ // boundary's message, the row boundary retreats to the message's
2162
+ // first contributing row: the summary call reads
2163
+ // messages[0..tailIndex), which excludes the merged rows' content,
2164
+ // so the image manifest and watermarks must not treat them as
2165
+ // summarized.
2166
+ fixedBoundaryRowIndex:
2167
+ firstRowByHistoryIndex[tailIndex] ?? boundaryRowIndex,
2168
+ fixedBoundaryRowView: { rows, firstRowByHistoryIndex },
2169
+ }),
2170
+ onEvent,
2171
+ );
2092
2172
  } finally {
2093
2173
  // Only undo the temporary guardian context this method installed. If
2094
2174
  // trustContext was legitimately updated at an `await` boundary, the
@@ -2398,6 +2478,10 @@ export class Conversation {
2398
2478
  this.clientOs = transport.clientOs ?? undefined;
2399
2479
  }
2400
2480
 
2481
+ applyVisibleAppFromTransport(transport: ConversationTransportMetadata): void {
2482
+ this.visibleAppId = transport.visibleAppId ?? undefined;
2483
+ }
2484
+
2401
2485
  setAssistantId(assistantId: string | null): void {
2402
2486
  this.assistantId = assistantId ?? undefined;
2403
2487
  }
@@ -2471,6 +2555,17 @@ export class Conversation {
2471
2555
  titleText?: string;
2472
2556
  /** See {@link runAgentLoopImpl} — hidden machine-signal turn marker. */
2473
2557
  isHiddenPrompt?: boolean;
2558
+ /**
2559
+ * See {@link runAgentLoopImpl}: the row the end-of-turn reply
2560
+ * notification treats as the prompt this turn answers.
2561
+ */
2562
+ notifyUserMessageId?: string;
2563
+ /**
2564
+ * See {@link runAgentLoopImpl}: this run's reply streams to the app
2565
+ * alone, so the reply notification ignores the initiating row's
2566
+ * channel/voice delivery markers.
2567
+ */
2568
+ replyDeliveredInAppOnly?: boolean;
2474
2569
  callSite?: LLMCallSite;
2475
2570
  /**
2476
2571
  * Optional ad-hoc inference-profile override applied to every LLM call
@@ -28,6 +28,7 @@ import { initializeDb } from "../persistence/db-init.js";
28
28
  import { startEmbeddingRuntimeManager } from "../persistence/embeddings/embedding-backend.js";
29
29
  import { maybeEnqueueLexicalBackfillOnUpgrade } from "../persistence/job-handlers/message-lexical-backfill.js";
30
30
  import { clearLifecycleQuiesce } from "../persistence/lifecycle-quiesce.js";
31
+ import { isPlatformClientConfigured } from "../platform/client.js";
31
32
  import { startConsentRefresh } from "../platform/consent-cache.js";
32
33
  import { syncWorkspaceIdentityToPlatform } from "../platform/sync-identity.js";
33
34
  import { ensurePromptFiles } from "../prompts/system-prompt.js";
@@ -198,6 +199,14 @@ export async function runDaemon(): Promise<void> {
198
199
  // failure is non-fatal — the daemon falls back to IPC-only operation.
199
200
  await startRuntimeHttpServer();
200
201
 
202
+ // Warms the configured-probe cache (credential reads only, no DB). Fired
203
+ // immediately after the transport binds, ahead of DB init and readiness,
204
+ // so an urgent signal arriving the moment requests are admitted is not
205
+ // decided on an empty cache.
206
+ void isPlatformClientConfigured().catch((err) =>
207
+ log.warn({ err }, "Platform configured-probe warmup failed"),
208
+ );
209
+
201
210
  // Pre-populate feature flag overrides so subsequent sync
202
211
  // isAssistantFeatureFlagEnabled() calls have data. Fired non-blocking
203
212
  // so a slow or unreachable gateway doesn't delay daemon startup (the