@vellumai/assistant 0.5.1 → 0.5.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 (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -1,8 +1,34 @@
1
1
  import type { z } from "zod";
2
2
 
3
+ /**
4
+ * Unwrap a Zod schema to reach its inner object shape, handling:
5
+ * - default/optional/nullable wrappers (innerType)
6
+ * - pipe/transform wrappers (in — the input side)
7
+ */
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ function unwrapToShape(schema: any): any {
10
+ let current = schema;
11
+ while (current && !current.shape) {
12
+ const def = current._zod?.def;
13
+ if (!def) break;
14
+ // Pipe/transform: follow the input side to get the pre-transform schema
15
+ if (def.type === "pipe" && def.in) {
16
+ current = def.in;
17
+ continue;
18
+ }
19
+ // Default/optional/nullable: follow innerType
20
+ if (def.innerType) {
21
+ current = def.innerType;
22
+ continue;
23
+ }
24
+ break;
25
+ }
26
+ return current;
27
+ }
28
+
3
29
  /**
4
30
  * Navigate a Zod schema by dotted path, unwrapping wrapper types
5
- * (default, optional, nullable) to reach inner object shapes.
31
+ * (default, optional, nullable, pipe/transform) to reach inner object shapes.
6
32
  * Returns the Zod schema at the given path, or null if the path is invalid.
7
33
  */
8
34
  export function getSchemaAtPath(
@@ -13,12 +39,7 @@ export function getSchemaAtPath(
13
39
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
40
  let current: any = schema;
15
41
  for (const key of keys) {
16
- // Unwrap default/optional/nullable wrappers to find inner object shape
17
- while (current && !current.shape) {
18
- const inner = current._zod?.def?.innerType;
19
- if (!inner) break;
20
- current = inner;
21
- }
42
+ current = unwrapToShape(current);
22
43
  if (!current || !current.shape) return null;
23
44
  current = current.shape[key];
24
45
  if (!current) return null;
@@ -38,6 +38,7 @@ export type { ElevenLabsConfig } from "./schemas/elevenlabs.js";
38
38
  export {
39
39
  DEFAULT_ELEVENLABS_VOICE_ID,
40
40
  ElevenLabsConfigSchema,
41
+ VALID_CONVERSATION_TIMEOUTS,
41
42
  } from "./schemas/elevenlabs.js";
42
43
  export type { HeartbeatConfig } from "./schemas/heartbeat.js";
43
44
  export { HeartbeatConfigSchema } from "./schemas/heartbeat.js";
@@ -321,6 +322,13 @@ export const AssistantConfigSchema = z
321
322
  .boolean()
322
323
  .default(true)
323
324
  .describe("Whether to send diagnostic/crash reports"),
325
+ maxStepsPerSession: z
326
+ .number({ error: "maxStepsPerSession must be a number" })
327
+ .int("maxStepsPerSession must be an integer")
328
+ .min(1, "maxStepsPerSession must be >= 1")
329
+ .max(200, "maxStepsPerSession must be <= 200")
330
+ .default(50)
331
+ .describe("Maximum number of computer-use steps per session"),
324
332
  })
325
333
  .superRefine((config, ctx) => {
326
334
  if (
@@ -5,6 +5,9 @@ import { z } from "zod";
5
5
  // Mirrored in: clients/macos/.../OpenAIVoiceService.swift (defaultVoiceId)
6
6
  export const DEFAULT_ELEVENLABS_VOICE_ID = "ZF6FPAbjXT4488VcRRnw";
7
7
 
8
+ /** Valid conversation timeout values (seconds). Shared with voice-config-update tool. */
9
+ export const VALID_CONVERSATION_TIMEOUTS = [5, 10, 15, 30, 60] as const;
10
+
8
11
  export const ElevenLabsConfigSchema = z
9
12
  .object({
10
13
  voiceId: z
@@ -42,6 +45,21 @@ export const ElevenLabsConfigSchema = z
42
45
  .describe(
43
46
  "How closely the output matches the original voice — higher values increase similarity",
44
47
  ),
48
+ conversationTimeoutSeconds: z
49
+ .number({
50
+ error: "elevenlabs.conversationTimeoutSeconds must be a number",
51
+ })
52
+ .refine(
53
+ (v) =>
54
+ VALID_CONVERSATION_TIMEOUTS.includes(
55
+ v as (typeof VALID_CONVERSATION_TIMEOUTS)[number],
56
+ ),
57
+ {
58
+ message: `elevenlabs.conversationTimeoutSeconds must be one of: ${VALID_CONVERSATION_TIMEOUTS.join(", ")}`,
59
+ },
60
+ )
61
+ .default(30)
62
+ .describe("Seconds of silence before voice conversation auto-ends"),
45
63
  })
46
64
  .describe("ElevenLabs text-to-speech configuration");
47
65
 
@@ -68,8 +68,10 @@ export const MemoryCleanupConfigSchema = z
68
68
  .nonnegative(
69
69
  "memory.cleanup.conversationRetentionDays must be non-negative",
70
70
  )
71
- .default(90)
72
- .describe("Number of days to retain conversation data before cleanup"),
71
+ .default(0)
72
+ .describe(
73
+ "Number of days to retain conversation data before cleanup (0 disables pruning)",
74
+ ),
73
75
  })
74
76
  .describe("Automatic memory cleanup and garbage collection settings");
75
77
 
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
- const VALID_MEMORY_EMBEDDING_PROVIDERS = [
3
+ export const VALID_MEMORY_EMBEDDING_PROVIDERS = [
4
4
  "auto",
5
5
  "local",
6
6
  "openai",
@@ -20,15 +20,18 @@ export const VALID_WEB_SEARCH_PROVIDERS = [
20
20
  "inference-provider-native",
21
21
  ] as const;
22
22
 
23
- export const InferenceServiceSchema = z.object({
23
+ export const BaseServiceSchema = z.object({
24
24
  mode: ServiceModeSchema.default("your-own"),
25
+ });
26
+ export type BaseService = z.infer<typeof BaseServiceSchema>;
27
+
28
+ export const InferenceServiceSchema = BaseServiceSchema.extend({
25
29
  provider: z.enum(VALID_INFERENCE_PROVIDERS).default("anthropic"),
26
30
  model: z.string().default("claude-opus-4-6"),
27
31
  });
28
32
  export type InferenceService = z.infer<typeof InferenceServiceSchema>;
29
33
 
30
- export const ImageGenerationServiceSchema = z.object({
31
- mode: ServiceModeSchema.default("your-own"),
34
+ export const ImageGenerationServiceSchema = BaseServiceSchema.extend({
32
35
  provider: z.enum(VALID_IMAGE_GEN_PROVIDERS).default("gemini"),
33
36
  model: z.string().default("gemini-3.1-flash-image-preview"),
34
37
  });
@@ -36,15 +39,14 @@ export type ImageGenerationService = z.infer<
36
39
  typeof ImageGenerationServiceSchema
37
40
  >;
38
41
 
39
- export const WebSearchServiceSchema = z.object({
40
- mode: ServiceModeSchema.default("your-own"),
42
+ export const WebSearchServiceSchema = BaseServiceSchema.extend({
41
43
  provider: z
42
44
  .enum(VALID_WEB_SEARCH_PROVIDERS)
43
45
  .default("inference-provider-native"),
44
46
  });
45
47
  export type WebSearchService = z.infer<typeof WebSearchServiceSchema>;
46
48
 
47
- export const GoogleOAuthServiceSchema = z.object({
49
+ export const GoogleOAuthServiceSchema = BaseServiceSchema.extend({
48
50
  mode: ServiceModeSchema.default("managed"),
49
51
  });
50
52
  export type GoogleOAuthService = z.infer<typeof GoogleOAuthServiceSchema>;
@@ -295,21 +295,28 @@ function syncChannels(
295
295
  .get();
296
296
 
297
297
  if (existing) {
298
+ // Preserve guardian blocks: if the channel is blocked, do not overwrite
299
+ // its status/policy — mirrors the guard in the cross-contact reassignment
300
+ // path so a blocked channel cannot be unblocked via a same-contact sync.
301
+ const isBlocked = existing.status === "blocked";
302
+
298
303
  const updateSet: Record<string, unknown> = {};
299
304
  if (ch.isPrimary !== undefined) updateSet.isPrimary = ch.isPrimary;
300
305
  if (ch.externalUserId !== undefined)
301
306
  updateSet.externalUserId = ch.externalUserId;
302
307
  if (ch.externalChatId !== undefined)
303
308
  updateSet.externalChatId = ch.externalChatId;
304
- if (ch.status !== undefined) updateSet.status = ch.status;
305
- if (ch.policy !== undefined) updateSet.policy = ch.policy;
309
+ if (!isBlocked) {
310
+ if (ch.status !== undefined) updateSet.status = ch.status;
311
+ if (ch.policy !== undefined) updateSet.policy = ch.policy;
312
+ if (ch.revokedReason !== undefined)
313
+ updateSet.revokedReason = ch.revokedReason;
314
+ if (ch.blockedReason !== undefined)
315
+ updateSet.blockedReason = ch.blockedReason;
316
+ }
306
317
  if (ch.verifiedAt !== undefined) updateSet.verifiedAt = ch.verifiedAt;
307
318
  if (ch.verifiedVia !== undefined) updateSet.verifiedVia = ch.verifiedVia;
308
319
  if (ch.inviteId !== undefined) updateSet.inviteId = ch.inviteId;
309
- if (ch.revokedReason !== undefined)
310
- updateSet.revokedReason = ch.revokedReason;
311
- if (ch.blockedReason !== undefined)
312
- updateSet.blockedReason = ch.blockedReason;
313
320
 
314
321
  if (Object.keys(updateSet).length > 0) {
315
322
  updateSet.updatedAt = now;
@@ -145,7 +145,6 @@ export function upsertContactChannel(params: {
145
145
  policy?: string;
146
146
  status?: string;
147
147
  inviteId?: string;
148
- sourceConversationId?: string;
149
148
  verifiedAt?: number;
150
149
  verifiedVia?: string;
151
150
  role?: ContactRole;
@@ -681,13 +681,24 @@ function countPersistedMessages(messages: Message[]): number {
681
681
  }).length;
682
682
  }
683
683
 
684
- /** A user message that contains ONLY tool_result blocks (no text or other content). */
684
+ function isSystemNoticeBlock(block: ContentBlock): boolean {
685
+ if (block.type !== "text") return false;
686
+ const text = (block as { text?: string }).text ?? "";
687
+ return (
688
+ text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
689
+ );
690
+ }
691
+
692
+ /** A user message that contains ONLY tool_result blocks (no text or other content).
693
+ * System notice text blocks (retry nudges, progress checks) do not count as user content. */
685
694
  function isToolResultOnly(message: Message): boolean {
686
695
  return (
687
696
  message.content.length > 0 &&
688
697
  message.content.every(
689
698
  (block) =>
690
- block.type === "tool_result" || block.type === "web_search_tool_result",
699
+ block.type === "tool_result" ||
700
+ block.type === "web_search_tool_result" ||
701
+ isSystemNoticeBlock(block),
691
702
  )
692
703
  );
693
704
  }
@@ -15,11 +15,16 @@ import type {
15
15
  } from "../channels/types.js";
16
16
  import {
17
17
  addMessage,
18
+ getConversation,
18
19
  getMessageById,
19
20
  provenanceFromTrustContext,
20
21
  updateMessageContent,
21
22
  } from "../memory/conversation-crud.js";
22
- import { recordRequestLog } from "../memory/llm-request-log-store.js";
23
+ import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
24
+ import {
25
+ backfillMessageIdOnLogs,
26
+ recordRequestLog,
27
+ } from "../memory/llm-request-log-store.js";
23
28
  import type { ContentBlock, ImageContent } from "../providers/types.js";
24
29
  import type { DirectiveRequest } from "./assistant-attachments.js";
25
30
  import {
@@ -48,6 +53,8 @@ export interface EventHandlerState {
48
53
  llmCallStartedEmitted: boolean;
49
54
  pendingDirectiveDisplayBuffer: string;
50
55
  firstAssistantText: string;
56
+ /** Most recent resolved provider for the current exchange's usage accounting. */
57
+ exchangeProviderName: string | undefined;
51
58
  exchangeInputTokens: number;
52
59
  exchangeCacheCreationInputTokens: number;
53
60
  exchangeCacheReadInputTokens: number;
@@ -114,6 +121,7 @@ export function createEventHandlerState(): EventHandlerState {
114
121
  llmCallStartedEmitted: false,
115
122
  pendingDirectiveDisplayBuffer: "",
116
123
  firstAssistantText: "",
124
+ exchangeProviderName: undefined,
117
125
  exchangeInputTokens: 0,
118
126
  exchangeCacheCreationInputTokens: 0,
119
127
  exchangeCacheReadInputTokens: 0,
@@ -167,6 +175,12 @@ export function emitLlmCallStartedIfNeeded(
167
175
  );
168
176
  }
169
177
 
178
+ // ── Client Payload Size Caps ─────────────────────────────────────────
179
+ // tool_input_delta streams accumulated JSON as tools run. For non-app
180
+ // tools the client discards it (extractCodePreview only handles app tools),
181
+ // so we skip forwarding entirely to avoid transport/decode overhead.
182
+ const APP_TOOL_NAMES = new Set(["app_create"]);
183
+
170
184
  // ── Friendly Tool Names ──────────────────────────────────────────────
171
185
 
172
186
  const TOOL_FRIENDLY_NAMES: Record<string, string> = {
@@ -183,11 +197,9 @@ const TOOL_FRIENDLY_NAMES: Record<string, string> = {
183
197
  browser_scroll: "browser",
184
198
  browser_wait: "browser",
185
199
  app_create: "app",
186
- app_update: "app",
200
+ app_refresh: "app refresh",
187
201
  skill_load: "skill",
188
202
  skill_execute: "skill",
189
- app_file_edit: "app file",
190
- app_file_write: "app file",
191
203
  };
192
204
 
193
205
  function friendlyToolName(name: string): string {
@@ -385,6 +397,10 @@ export function handleInputJsonDelta(
385
397
  deps: EventHandlerDeps,
386
398
  event: Extract<AgentEvent, { type: "input_json_delta" }>,
387
399
  ): void {
400
+ // Only forward input deltas for app tools — the client only uses this
401
+ // stream for app_create/app_update code previews. Non-app tools would
402
+ // send large cumulative JSON on every delta with no benefit.
403
+ if (!APP_TOOL_NAMES.has(event.toolName)) return;
388
404
  deps.onEvent({
389
405
  type: "tool_input_delta",
390
406
  toolName: event.toolName,
@@ -627,12 +643,21 @@ export async function handleMessageComplete(
627
643
  assistantMessageInterface:
628
644
  deps.turnInterfaceContext.assistantMessageInterface,
629
645
  };
630
- await addMessage(
646
+ const toolResultMsg = await addMessage(
631
647
  deps.ctx.conversationId,
632
648
  "user",
633
649
  JSON.stringify(toolResultBlocks),
634
650
  toolResultMetadata,
635
651
  );
652
+ // Sync tool-result user message to disk view
653
+ const convForToolResult = getConversation(deps.ctx.conversationId);
654
+ if (convForToolResult) {
655
+ syncMessageToDisk(
656
+ deps.ctx.conversationId,
657
+ toolResultMsg.id,
658
+ convForToolResult.createdAt,
659
+ );
660
+ }
636
661
  for (const id of state.pendingToolResults.keys()) {
637
662
  state.persistedToolUseIds.add(id);
638
663
  }
@@ -697,6 +722,18 @@ export async function handleMessageComplete(
697
722
  );
698
723
  state.lastAssistantMessageId = assistantMsg.id;
699
724
 
725
+ // Backfill message_id on all LLM request logs from this turn.
726
+ // The agent loop is single-threaded per conversation, so all rows with
727
+ // message_id IS NULL belong to the current turn.
728
+ try {
729
+ backfillMessageIdOnLogs(deps.ctx.conversationId, assistantMsg.id);
730
+ } catch (err) {
731
+ deps.rlog.warn(
732
+ { err },
733
+ "Failed to backfill message_id on LLM request logs (non-fatal)",
734
+ );
735
+ }
736
+
700
737
  deps.ctx.currentTurnSurfaces = [];
701
738
 
702
739
  // Emit trace event
@@ -724,6 +761,8 @@ export function handleUsage(
724
761
  deps: EventHandlerDeps,
725
762
  event: Extract<AgentEvent, { type: "usage" }>,
726
763
  ): void {
764
+ const providerName = event.actualProvider ?? deps.ctx.provider.name;
765
+ state.exchangeProviderName = providerName;
727
766
  state.exchangeInputTokens += event.inputTokens;
728
767
  state.exchangeCacheCreationInputTokens += event.cacheCreationInputTokens ?? 0;
729
768
  state.exchangeCacheReadInputTokens += event.cacheReadInputTokens ?? 0;
@@ -739,6 +778,8 @@ export function handleUsage(
739
778
  deps.ctx.conversationId,
740
779
  JSON.stringify(event.rawRequest),
741
780
  JSON.stringify(event.rawResponse),
781
+ undefined,
782
+ providerName,
742
783
  );
743
784
  } catch (err) {
744
785
  deps.rlog.warn({ err }, "Failed to persist LLM request log (non-fatal)");
@@ -749,12 +790,12 @@ export function handleUsage(
749
790
 
750
791
  deps.ctx.traceEmitter.emit(
751
792
  "llm_call_finished",
752
- `LLM call to ${deps.ctx.provider.name} finished`,
793
+ `LLM call to ${providerName} finished`,
753
794
  {
754
795
  requestId: deps.reqId,
755
796
  status: "success",
756
797
  attributes: {
757
- provider: deps.ctx.provider.name,
798
+ provider: providerName,
758
799
  model: event.model,
759
800
  inputTokens: event.inputTokens,
760
801
  outputTokens: event.outputTokens,
@@ -32,7 +32,7 @@ import {
32
32
  setSentryConversationContext,
33
33
  } from "../instrument.js";
34
34
  import { commitAppTurnChanges } from "../memory/app-git-service.js";
35
- import { getApp, listAppFiles } from "../memory/app-store.js";
35
+ import { getApp, listAppFiles, resolveAppDir } from "../memory/app-store.js";
36
36
  import {
37
37
  addMessage,
38
38
  deleteMessageById,
@@ -43,6 +43,10 @@ import {
43
43
  updateConversationContextWindow,
44
44
  updateConversationTitle,
45
45
  } from "../memory/conversation-crud.js";
46
+ import {
47
+ rebuildConversationDiskViewFromDbState,
48
+ syncMessageToDisk,
49
+ } from "../memory/conversation-disk-view.js";
46
50
  import {
47
51
  isReplaceableTitle,
48
52
  queueGenerateConversationTitle,
@@ -77,7 +81,6 @@ import {
77
81
  } from "./conversation-agent-loop-handlers.js";
78
82
  import {
79
83
  approveHostAttachmentRead,
80
- formatAttachmentWarnings,
81
84
  resolveAssistantAttachments,
82
85
  } from "./conversation-attachments.js";
83
86
  import {
@@ -173,11 +176,9 @@ const TOOL_FRIENDLY_LABEL: Record<string, string> = {
173
176
  browser_scroll: "Browser",
174
177
  browser_wait: "Browser",
175
178
  app_create: "Create App",
176
- app_update: "Update App",
179
+ app_refresh: "Refresh App",
177
180
  skill_load: "Load Skill",
178
181
  skill_execute: "Run Skill Tool",
179
- app_file_edit: "Edit App File",
180
- app_file_write: "Write App File",
181
182
  };
182
183
 
183
184
  type GitServiceInitializer = {
@@ -604,6 +605,7 @@ export async function runAgentLoopImpl(
604
605
  if (app) {
605
606
  activeSurface.appId = app.id;
606
607
  activeSurface.appName = app.name;
608
+ activeSurface.appDirName = resolveAppDir(app.id).dirName;
607
609
  activeSurface.appSchemaJson = app.schemaJson;
608
610
  activeSurface.appFiles = listAppFiles(app.id);
609
611
  if (app.pages && Object.keys(app.pages).length > 0) {
@@ -801,7 +803,16 @@ export async function runAgentLoopImpl(
801
803
  mode: currentInjectionMode,
802
804
  });
803
805
 
804
- if (step.estimatedTokens <= preflightBudget) break;
806
+ // Re-estimate with injections included — step.estimatedTokens was
807
+ // computed on bare history (ctx.messages) and doesn't account for
808
+ // tokens added by runtime injections.
809
+ const postInjectionTokens = estimatePromptTokens(
810
+ runMessages,
811
+ ctx.systemPrompt,
812
+ { providerName: ctx.provider.name, toolTokenBudget },
813
+ );
814
+
815
+ if (postInjectionTokens <= preflightBudget) break;
805
816
  }
806
817
  }
807
818
 
@@ -1173,6 +1184,7 @@ export async function runAgentLoopImpl(
1173
1184
  preRepairMessages = runMessages;
1174
1185
  preRunHistoryLength = runMessages.length;
1175
1186
  state.contextTooLargeDetected = false;
1187
+ yieldedForBudget = false;
1176
1188
 
1177
1189
  updatedHistory = await ctx.agentLoop.run(
1178
1190
  runMessages,
@@ -1195,6 +1207,15 @@ export async function runAgentLoopImpl(
1195
1207
  "Post-convergence rerun still yielded at checkpoint — continuing reduction",
1196
1208
  );
1197
1209
  state.contextTooLargeDetected = true;
1210
+
1211
+ // Fold rerun progress into ctx.messages so the next reducer
1212
+ // tier operates on up-to-date history instead of stale
1213
+ // pre-rerun messages.
1214
+ if (updatedHistory.length > preRunHistoryLength) {
1215
+ ctx.messages = stripInjectedContext(updatedHistory);
1216
+ preRepairMessages = updatedHistory;
1217
+ preRunHistoryLength = updatedHistory.length;
1218
+ }
1198
1219
  }
1199
1220
  }
1200
1221
 
@@ -1514,16 +1535,29 @@ export async function runAgentLoopImpl(
1514
1535
  state.exchangeCacheCreationInputTokens,
1515
1536
  state.exchangeCacheReadInputTokens,
1516
1537
  collapseRawResponses(state.exchangeRawResponses),
1538
+ state.exchangeProviderName,
1517
1539
  );
1518
1540
 
1519
1541
  void getHookManager().trigger("post-message", {
1520
1542
  conversationId: ctx.conversationId,
1521
1543
  });
1522
1544
 
1545
+ const syncLastAssistantMessageToDisk = (): void => {
1546
+ if (!state.lastAssistantMessageId) return;
1547
+ const convForDisk = getConversation(ctx.conversationId);
1548
+ if (!convForDisk) return;
1549
+ syncMessageToDisk(
1550
+ ctx.conversationId,
1551
+ state.lastAssistantMessageId,
1552
+ convForDisk.createdAt,
1553
+ );
1554
+ };
1555
+
1523
1556
  // Fast-path: when the user cancelled, skip expensive post-loop work
1524
1557
  // (attachment resolution) and emit the cancellation event immediately
1525
1558
  // so the client can re-enable the UI without delay.
1526
1559
  if (abortController.signal.aborted) {
1560
+ syncLastAssistantMessageToDisk();
1527
1561
  ctx.emitActivityState("idle", "generation_cancelled", "global", reqId);
1528
1562
  ctx.traceEmitter.emit(
1529
1563
  "generation_cancelled",
@@ -1559,17 +1593,7 @@ export async function runAgentLoopImpl(
1559
1593
 
1560
1594
  ctx.lastAssistantAttachments = assistantAttachments;
1561
1595
  ctx.lastAttachmentWarnings = attachmentResult.directiveWarnings;
1562
-
1563
- const warningText = formatAttachmentWarnings(
1564
- attachmentResult.directiveWarnings,
1565
- );
1566
- if (warningText) {
1567
- onEvent({
1568
- type: "assistant_text_delta",
1569
- text: warningText,
1570
- conversationId: ctx.conversationId,
1571
- });
1572
- }
1596
+ syncLastAssistantMessageToDisk();
1573
1597
 
1574
1598
  // Re-check: the user may have cancelled during attachment resolution
1575
1599
  if (abortController.signal.aborted) {
@@ -1604,6 +1628,9 @@ export async function runAgentLoopImpl(
1604
1628
  ...(emittedAttachments.length > 0
1605
1629
  ? { attachments: emittedAttachments }
1606
1630
  : {}),
1631
+ ...(ctx.lastAttachmentWarnings.length > 0
1632
+ ? { attachmentWarnings: ctx.lastAttachmentWarnings }
1633
+ : {}),
1607
1634
  ...(state.lastAssistantMessageId
1608
1635
  ? { messageId: state.lastAssistantMessageId }
1609
1636
  : {}),
@@ -1624,6 +1651,9 @@ export async function runAgentLoopImpl(
1624
1651
  ...(emittedAttachments.length > 0
1625
1652
  ? { attachments: emittedAttachments }
1626
1653
  : {}),
1654
+ ...(ctx.lastAttachmentWarnings.length > 0
1655
+ ? { attachmentWarnings: ctx.lastAttachmentWarnings }
1656
+ : {}),
1627
1657
  ...(state.lastAssistantMessageId
1628
1658
  ? { messageId: state.lastAssistantMessageId }
1629
1659
  : {}),
@@ -1739,7 +1769,13 @@ export async function runAgentLoopImpl(
1739
1769
  ctx.commandIntent = undefined;
1740
1770
 
1741
1771
  if (userMessageId) {
1742
- consolidateAssistantMessages(ctx.conversationId, userMessageId);
1772
+ const didMutateHistory = consolidateAssistantMessages(
1773
+ ctx.conversationId,
1774
+ userMessageId,
1775
+ );
1776
+ if (didMutateHistory) {
1777
+ rebuildConversationDiskViewFromDbState(ctx.conversationId);
1778
+ }
1743
1779
  }
1744
1780
 
1745
1781
  ctx.drainQueue(yieldedForHandoff ? "checkpoint_handoff" : "loop_complete");
@@ -1766,11 +1802,12 @@ function emitUsage(
1766
1802
  cacheCreationInputTokens = 0,
1767
1803
  cacheReadInputTokens = 0,
1768
1804
  rawResponse?: unknown,
1805
+ providerName?: string,
1769
1806
  ): void {
1770
1807
  recordUsage(
1771
1808
  {
1772
1809
  conversationId: ctx.conversationId,
1773
- providerName: ctx.provider.name,
1810
+ providerName: providerName ?? ctx.provider.name,
1774
1811
  usageStats: ctx.usageStats,
1775
1812
  },
1776
1813
  inputTokens,
@@ -1,11 +1,8 @@
1
1
  import {
2
+ attachInlineAttachmentToMessage,
2
3
  AttachmentUploadError,
3
- FILE_BACKED_THRESHOLD_BYTES,
4
- linkAttachmentToMessage,
4
+ getFilePathForAttachment,
5
5
  setAttachmentThumbnail,
6
- uploadAttachment,
7
- uploadFileBackedAttachment,
8
- writeAttachmentToDisk,
9
6
  } from "../memory/attachments-store.js";
10
7
  import {
11
8
  check,
@@ -107,12 +104,6 @@ export async function approveHostAttachmentRead(
107
104
  return isAllowDecision(response.decision);
108
105
  }
109
106
 
110
- export function formatAttachmentWarnings(warnings: string[]): string | null {
111
- if (warnings.length === 0) return null;
112
- const lines = warnings.map((warning) => `Attachment warning: ${warning}`);
113
- return `\n\n${lines.join("\n")}`;
114
- }
115
-
116
107
  export interface AttachmentResolutionResult {
117
108
  assistantAttachments: AssistantAttachmentDraft[];
118
109
  emittedAttachments: UserMessageAttachment[];
@@ -211,28 +202,16 @@ export async function resolveAssistantAttachments(
211
202
  if (assistantAttachments.length > 0 && lastAssistantMessageId) {
212
203
  for (let i = 0; i < assistantAttachments.length; i++) {
213
204
  const draft = assistantAttachments[i];
214
- const isFileBacked = draft.sizeBytes > FILE_BACKED_THRESHOLD_BYTES;
215
205
  let stored;
216
- let diskFilePath: string | undefined;
217
206
  try {
218
- if (isFileBacked) {
219
- diskFilePath = writeAttachmentToDisk(
220
- draft.dataBase64,
221
- draft.filename,
222
- );
223
- stored = uploadFileBackedAttachment(
224
- draft.filename,
225
- draft.mimeType,
226
- diskFilePath,
227
- draft.sizeBytes,
228
- );
229
- } else {
230
- stored = uploadAttachment(
231
- draft.filename,
232
- draft.mimeType,
233
- draft.dataBase64,
234
- );
235
- }
207
+ stored = attachInlineAttachmentToMessage(
208
+ lastAssistantMessageId,
209
+ i,
210
+ draft.filename,
211
+ draft.mimeType,
212
+ draft.dataBase64,
213
+ { skipSizeLimit: true },
214
+ );
236
215
  } catch (err) {
237
216
  if (err instanceof AttachmentUploadError) {
238
217
  log.warn(
@@ -246,11 +225,11 @@ export async function resolveAssistantAttachments(
246
225
  }
247
226
  throw err;
248
227
  }
249
- linkAttachmentToMessage(lastAssistantMessageId, stored.id, i);
250
228
  const isVideo = draft.mimeType.startsWith("video/");
251
- const omitData =
252
- isFileBacked ||
253
- (isVideo && draft.dataBase64.length > MAX_INLINE_B64_SIZE);
229
+ // Only omit data for videos — they have an end-to-end lazy-load path
230
+ // via /v1/attachments/:id/content. Other types (images, PDFs) still need
231
+ // inline data for thumbnails, preview, and file-save in the client.
232
+ const omitData = isVideo && draft.dataBase64.length > MAX_INLINE_B64_SIZE;
254
233
 
255
234
  // Generate and persist a thumbnail for video attachments.
256
235
  let thumbnailData: string | undefined;
@@ -259,6 +238,7 @@ export async function resolveAssistantAttachments(
259
238
  if (existing) {
260
239
  thumbnailData = existing;
261
240
  } else {
241
+ const diskFilePath = getFilePathForAttachment(stored.id);
262
242
  const generated = diskFilePath
263
243
  ? await generateVideoThumbnailFromPath(diskFilePath)
264
244
  : await generateVideoThumbnail(draft.dataBase64);
@@ -274,8 +254,9 @@ export async function resolveAssistantAttachments(
274
254
  filename: draft.filename,
275
255
  mimeType: draft.mimeType,
276
256
  data: omitData ? "" : draft.dataBase64,
257
+ sourceType: draft.sourceType,
277
258
  ...(omitData ? { sizeBytes: draft.sizeBytes } : {}),
278
- ...(isFileBacked ? { fileBacked: true } : {}),
259
+ fileBacked: true,
279
260
  ...(thumbnailData ? { thumbnailData } : {}),
280
261
  });
281
262
  }
@@ -285,6 +266,7 @@ export async function resolveAssistantAttachments(
285
266
  filename: draft.filename,
286
267
  mimeType: draft.mimeType,
287
268
  data: draft.dataBase64,
269
+ sourceType: draft.sourceType,
288
270
  });
289
271
  }
290
272
  }