@vellumai/assistant 0.5.0 → 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 (347) 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__/assistant-feature-flags-integration.test.ts +7 -9
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-queue.test.ts +36 -1
  37. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  38. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  39. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  41. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  42. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  43. package/src/__tests__/conversation-store.test.ts +24 -21
  44. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  45. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  46. package/src/__tests__/conversation-title-service.test.ts +137 -0
  47. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  48. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  49. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  51. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  52. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  53. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  54. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  55. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  56. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  57. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  58. package/src/__tests__/diagnostics-export.test.ts +70 -1
  59. package/src/__tests__/filesystem-tools.test.ts +4 -2
  60. package/src/__tests__/first-greeting.test.ts +80 -0
  61. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  63. package/src/__tests__/history-repair.test.ts +103 -10
  64. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  65. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  66. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  67. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  68. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  69. package/src/__tests__/media-generate-image.test.ts +47 -94
  70. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  71. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  72. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  73. package/src/__tests__/migration-export-http.test.ts +3 -1
  74. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  75. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  76. package/src/__tests__/mime-builder.test.ts +3 -2
  77. package/src/__tests__/non-member-access-request.test.ts +12 -1
  78. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  80. package/src/__tests__/oauth-store.test.ts +115 -0
  81. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  82. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  83. package/src/__tests__/recording-handler.test.ts +17 -0
  84. package/src/__tests__/registry.test.ts +3 -8
  85. package/src/__tests__/relay-server.test.ts +1 -1
  86. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  87. package/src/__tests__/schema-transforms.test.ts +165 -5
  88. package/src/__tests__/server-history-render.test.ts +2 -2
  89. package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
  90. package/src/__tests__/skill-feature-flags.test.ts +13 -13
  91. package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
  92. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  93. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  94. package/src/__tests__/starter-task-flow.test.ts +1 -0
  95. package/src/__tests__/suggestion-routes.test.ts +443 -0
  96. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  97. package/src/__tests__/swarm-recursion.test.ts +1 -0
  98. package/src/__tests__/swarm-tool.test.ts +1 -0
  99. package/src/__tests__/system-prompt.test.ts +8 -0
  100. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  101. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  102. package/src/__tests__/top-level-renderer.test.ts +22 -0
  103. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  104. package/src/__tests__/web-fetch.test.ts +6 -2
  105. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  106. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  107. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  108. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  109. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  110. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  111. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  112. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  113. package/src/agent/attachments.ts +27 -1
  114. package/src/agent/loop.ts +29 -1
  115. package/src/avatar/traits-png-sync.ts +80 -25
  116. package/src/bundler/app-bundler.ts +4 -4
  117. package/src/calls/call-domain.ts +1 -0
  118. package/src/calls/voice-session-bridge.ts +1 -0
  119. package/src/cli/commands/auth.ts +92 -0
  120. package/src/cli/commands/avatar.ts +7 -6
  121. package/src/cli/commands/config.ts +2 -0
  122. package/src/cli/commands/oauth/providers.ts +29 -0
  123. package/src/cli/program.ts +12 -0
  124. package/src/cli.ts +15 -48
  125. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  126. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  127. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  128. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  129. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  130. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  131. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  135. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  136. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  137. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  139. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  140. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  141. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  142. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  143. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  144. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  145. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  146. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  148. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  149. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  150. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  151. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  152. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  153. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  154. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  155. package/src/config/bundled-tool-registry.ts +2 -14
  156. package/src/config/feature-flag-registry.json +16 -0
  157. package/src/config/loader.ts +64 -0
  158. package/src/config/raw-config-utils.ts +30 -0
  159. package/src/config/schema-utils.ts +28 -7
  160. package/src/config/schema.ts +8 -0
  161. package/src/config/schemas/elevenlabs.ts +18 -0
  162. package/src/config/schemas/memory-lifecycle.ts +4 -2
  163. package/src/config/schemas/memory-storage.ts +1 -1
  164. package/src/config/schemas/services.ts +8 -6
  165. package/src/contacts/contact-store.ts +13 -6
  166. package/src/contacts/contacts-write.ts +0 -1
  167. package/src/context/window-manager.ts +13 -2
  168. package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
  169. package/src/daemon/conversation-agent-loop.ts +56 -19
  170. package/src/daemon/conversation-attachments.ts +18 -36
  171. package/src/daemon/conversation-error.ts +2 -1
  172. package/src/daemon/conversation-history.ts +18 -4
  173. package/src/daemon/conversation-lifecycle.ts +39 -15
  174. package/src/daemon/conversation-messaging.ts +70 -26
  175. package/src/daemon/conversation-process.ts +58 -34
  176. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  177. package/src/daemon/conversation-slash.ts +121 -256
  178. package/src/daemon/conversation-surfaces.ts +143 -20
  179. package/src/daemon/conversation-tool-setup.ts +0 -6
  180. package/src/daemon/conversation-workspace.ts +21 -1
  181. package/src/daemon/conversation.ts +51 -29
  182. package/src/daemon/first-greeting.ts +35 -0
  183. package/src/daemon/handlers/config-embeddings.ts +148 -0
  184. package/src/daemon/handlers/config-model.ts +71 -26
  185. package/src/daemon/handlers/conversations.ts +0 -23
  186. package/src/daemon/handlers/recording.ts +26 -21
  187. package/src/daemon/history-repair.ts +28 -8
  188. package/src/daemon/host-cu-proxy.ts +2 -2
  189. package/src/daemon/lifecycle.ts +106 -64
  190. package/src/daemon/message-protocol.ts +3 -0
  191. package/src/daemon/message-types/conversations.ts +19 -0
  192. package/src/daemon/message-types/messages.ts +1 -0
  193. package/src/daemon/message-types/shared.ts +2 -0
  194. package/src/daemon/message-types/surfaces.ts +2 -0
  195. package/src/daemon/message-types/upgrades.ts +23 -0
  196. package/src/daemon/server.ts +83 -12
  197. package/src/daemon/shutdown-handlers.ts +8 -5
  198. package/src/daemon/startup-error.ts +9 -0
  199. package/src/daemon/tool-side-effects.ts +11 -28
  200. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  201. package/src/instrument.ts +0 -4
  202. package/src/media/app-icon-generator.ts +2 -2
  203. package/src/memory/app-git-service.ts +28 -16
  204. package/src/memory/app-store.ts +230 -41
  205. package/src/memory/attachments-store.ts +558 -130
  206. package/src/memory/conversation-attention-store.ts +70 -0
  207. package/src/memory/conversation-crud.ts +442 -3
  208. package/src/memory/conversation-directories.ts +125 -0
  209. package/src/memory/conversation-disk-view.ts +390 -0
  210. package/src/memory/conversation-key-store.ts +17 -5
  211. package/src/memory/conversation-queries.ts +5 -1
  212. package/src/memory/conversation-title-service.ts +21 -49
  213. package/src/memory/db-init.ts +28 -0
  214. package/src/memory/embedding-backend.ts +42 -53
  215. package/src/memory/embedding-gemini.test.ts +4 -4
  216. package/src/memory/embedding-local.ts +1 -3
  217. package/src/memory/embedding-ollama.ts +1 -3
  218. package/src/memory/embedding-openai.ts +1 -3
  219. package/src/memory/indexer.ts +9 -7
  220. package/src/memory/items-extractor.ts +42 -13
  221. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  222. package/src/memory/job-handlers/embedding.test.ts +1 -4
  223. package/src/memory/llm-request-log-store.ts +100 -1
  224. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  225. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  226. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  227. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  228. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  229. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  230. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  231. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  232. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  233. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  234. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  235. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  236. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  237. package/src/memory/migrations/index.ts +7 -0
  238. package/src/memory/migrations/registry.ts +13 -0
  239. package/src/memory/retriever.test.ts +601 -2
  240. package/src/memory/retriever.ts +85 -9
  241. package/src/memory/schema/conversations.ts +6 -0
  242. package/src/memory/schema/infrastructure.ts +13 -7
  243. package/src/memory/schema/oauth.ts +6 -0
  244. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  245. package/src/notifications/copy-composer.ts +26 -0
  246. package/src/notifications/decision-engine.ts +14 -1
  247. package/src/notifications/emit-signal.ts +1 -1
  248. package/src/notifications/signal.ts +36 -0
  249. package/src/oauth/byo-connection.test.ts +1 -45
  250. package/src/oauth/byo-connection.ts +2 -8
  251. package/src/oauth/connect-orchestrator.ts +15 -11
  252. package/src/oauth/connection-resolver.test.ts +191 -0
  253. package/src/oauth/connection-resolver.ts +66 -38
  254. package/src/oauth/connection.ts +0 -1
  255. package/src/oauth/oauth-store.ts +97 -47
  256. package/src/oauth/platform-connection.test.ts +0 -1
  257. package/src/oauth/platform-connection.ts +11 -3
  258. package/src/oauth/seed-providers.ts +78 -3
  259. package/src/oauth/token-persistence.ts +16 -10
  260. package/src/permissions/checker.ts +62 -19
  261. package/src/prompts/system-prompt.ts +2 -0
  262. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  263. package/src/providers/anthropic/client.ts +8 -1
  264. package/src/providers/failover.ts +4 -1
  265. package/src/providers/gemini/client.ts +50 -0
  266. package/src/providers/model-catalog.ts +92 -0
  267. package/src/providers/model-intents.ts +29 -20
  268. package/src/providers/openai/client.ts +49 -0
  269. package/src/providers/types.ts +2 -0
  270. package/src/runtime/access-request-helper.ts +16 -7
  271. package/src/runtime/auth/credential-service.ts +3 -1
  272. package/src/runtime/auth/route-policy.ts +14 -1
  273. package/src/runtime/btw-sidechain.ts +101 -0
  274. package/src/runtime/channel-reply-delivery.ts +17 -1
  275. package/src/runtime/http-router.ts +3 -1
  276. package/src/runtime/http-server.ts +196 -141
  277. package/src/runtime/http-types.ts +1 -0
  278. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  279. package/src/runtime/routes/access-request-decision.ts +41 -0
  280. package/src/runtime/routes/app-management-routes.ts +6 -3
  281. package/src/runtime/routes/app-routes.ts +7 -3
  282. package/src/runtime/routes/approval-routes.ts +1 -0
  283. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  284. package/src/runtime/routes/attachment-routes.ts +45 -15
  285. package/src/runtime/routes/btw-routes.ts +21 -61
  286. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  287. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  288. package/src/runtime/routes/conversation-routes.ts +222 -28
  289. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  290. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  291. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  292. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  293. package/src/runtime/routes/log-export-routes.ts +3 -0
  294. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  295. package/src/runtime/routes/memory-item-routes.ts +4 -0
  296. package/src/runtime/routes/migration-routes.ts +4 -1
  297. package/src/runtime/routes/oauth-apps.ts +291 -0
  298. package/src/runtime/routes/secret-routes.ts +28 -1
  299. package/src/runtime/routes/settings-routes.ts +14 -0
  300. package/src/runtime/routes/trace-event-routes.ts +4 -1
  301. package/src/schedule/schedule-store.ts +9 -21
  302. package/src/security/secure-keys.ts +21 -0
  303. package/src/signals/bash.ts +1 -1
  304. package/src/swarm/backend-claude-code.ts +3 -6
  305. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  306. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  307. package/src/tools/AGENTS.md +6 -10
  308. package/src/tools/apps/executors.ts +17 -232
  309. package/src/tools/claude-code/claude-code.ts +2 -3
  310. package/src/tools/credentials/vault.ts +7 -12
  311. package/src/tools/host-filesystem/read.ts +13 -10
  312. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  313. package/src/tools/schedule/list.ts +2 -7
  314. package/src/tools/schema-transforms.ts +5 -0
  315. package/src/tools/shared/filesystem/format-diff.ts +4 -21
  316. package/src/tools/skills/execute.ts +1 -1
  317. package/src/tools/tool-manifest.ts +0 -6
  318. package/src/tools/ui-surface/definitions.ts +2 -2
  319. package/src/util/device-id.ts +28 -5
  320. package/src/util/platform.ts +6 -0
  321. package/src/util/pricing.ts +1 -0
  322. package/src/util/retry.ts +1 -3
  323. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  324. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  325. package/src/workspace/migrations/006-services-config.ts +5 -0
  326. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  327. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  328. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  329. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  330. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  331. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  332. package/src/workspace/migrations/registry.ts +10 -0
  333. package/src/workspace/top-level-renderer.ts +12 -0
  334. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  335. package/src/__tests__/asset-search-tool.test.ts +0 -536
  336. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  337. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  338. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  339. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  340. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  341. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  342. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  343. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  344. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  345. package/src/daemon/media-visibility-policy.ts +0 -59
  346. package/src/tools/assets/materialize.ts +0 -248
  347. package/src/tools/assets/search.ts +0 -400
@@ -117,32 +117,60 @@ describe("getSchemaAtPath", () => {
117
117
  "memory.segmentation",
118
118
  );
119
119
  expect(result).not.toBeNull();
120
- // Unwrap to check it has targetTokens and overlapTokens
121
- let schema: any = result;
122
- while (schema && !schema.shape) {
123
- const inner = schema._zod?.def?.innerType;
124
- if (!inner) break;
125
- schema = inner;
126
- }
127
- expect(schema.shape).toBeDefined();
128
- expect(schema.shape.targetTokens).toBeDefined();
129
- expect(schema.shape.overlapTokens).toBeDefined();
120
+ // Verify we can produce JSON Schema with the expected properties
121
+ const jsonSchema = z.toJSONSchema(result!, {
122
+ unrepresentable: "any",
123
+ io: "input",
124
+ }) as Record<string, unknown>;
125
+ const properties = jsonSchema.properties as Record<string, unknown>;
126
+ expect(properties).toBeDefined();
127
+ expect(properties.targetTokens).toBeDefined();
128
+ expect(properties.overlapTokens).toBeDefined();
130
129
  });
131
130
 
132
131
  test("navigates through .default() wrappers (calls → object schema)", () => {
133
132
  const result = getSchemaAtPath(AssistantConfigSchema, "calls");
134
133
  expect(result).not.toBeNull();
135
- // Unwrap to check it has shape (it's a ZodDefault wrapping ZodObject)
136
- let schema: any = result;
137
- while (schema && !schema.shape) {
138
- const inner = schema._zod?.def?.innerType;
139
- if (!inner) break;
140
- schema = inner;
141
- }
142
- expect(schema.shape).toBeDefined();
143
- expect(schema.shape.enabled).toBeDefined();
144
- expect(schema.shape.voice).toBeDefined();
145
- expect(schema.shape.safety).toBeDefined();
134
+ // Verify we can produce JSON Schema with the expected properties
135
+ const jsonSchema = z.toJSONSchema(result!, {
136
+ unrepresentable: "any",
137
+ io: "input",
138
+ }) as Record<string, unknown>;
139
+ const properties = jsonSchema.properties as Record<string, unknown>;
140
+ expect(properties).toBeDefined();
141
+ expect(properties.enabled).toBeDefined();
142
+ expect(properties.voice).toBeDefined();
143
+ expect(properties.safety).toBeDefined();
144
+ });
145
+
146
+ test("navigates through .transform() wrappers (ingress → object schema)", () => {
147
+ const result = getSchemaAtPath(AssistantConfigSchema, "ingress");
148
+ expect(result).not.toBeNull();
149
+ // ingress uses .transform() which creates a pipe — getSchemaAtPath
150
+ // must unwrap through the pipe to reach the input object shape
151
+ const jsonSchema = z.toJSONSchema(result!, {
152
+ unrepresentable: "any",
153
+ io: "input",
154
+ }) as Record<string, unknown>;
155
+ const properties = jsonSchema.properties as Record<string, unknown>;
156
+ expect(properties).toBeDefined();
157
+ expect(properties.enabled).toBeDefined();
158
+ expect(properties.webhook).toBeDefined();
159
+ expect(properties.rateLimit).toBeDefined();
160
+ });
161
+
162
+ test("navigates nested path through .transform() wrapper (ingress.webhook)", () => {
163
+ const result = getSchemaAtPath(AssistantConfigSchema, "ingress.webhook");
164
+ expect(result).not.toBeNull();
165
+ const jsonSchema = z.toJSONSchema(result!, {
166
+ unrepresentable: "any",
167
+ io: "input",
168
+ }) as Record<string, unknown>;
169
+ const properties = jsonSchema.properties as Record<string, unknown>;
170
+ expect(properties).toBeDefined();
171
+ expect(properties.secret).toBeDefined();
172
+ expect(properties.timeoutMs).toBeDefined();
173
+ expect(properties.maxRetries).toBeDefined();
146
174
  });
147
175
 
148
176
  test("returns null for non-existent top-level path", () => {
@@ -170,6 +198,7 @@ describe("z.toJSONSchema integration", () => {
170
198
  test("full schema produces valid JSON Schema with type object and properties", () => {
171
199
  const jsonSchema = z.toJSONSchema(AssistantConfigSchema, {
172
200
  unrepresentable: "any",
201
+ io: "input",
173
202
  }) as Record<string, unknown>;
174
203
  expect(jsonSchema.type).toBe("object");
175
204
  const properties = jsonSchema.properties as Record<string, unknown>;
@@ -184,11 +213,27 @@ describe("z.toJSONSchema integration", () => {
184
213
  expect(properties.sandbox).toBeDefined();
185
214
  });
186
215
 
216
+ test("full schema emits real properties for transformed fields (ingress)", () => {
217
+ const jsonSchema = z.toJSONSchema(AssistantConfigSchema, {
218
+ unrepresentable: "any",
219
+ io: "input",
220
+ }) as Record<string, unknown>;
221
+ const properties = jsonSchema.properties as Record<string, unknown>;
222
+ const ingress = properties.ingress as Record<string, unknown>;
223
+ // Without io: "input", transforms produce empty {} — verify we get real content
224
+ expect(ingress.properties).toBeDefined();
225
+ const ingressProps = ingress.properties as Record<string, unknown>;
226
+ expect(ingressProps.enabled).toBeDefined();
227
+ expect(ingressProps.webhook).toBeDefined();
228
+ expect(ingressProps.rateLimit).toBeDefined();
229
+ });
230
+
187
231
  test("sub-schema at calls produces JSON Schema with expected properties", () => {
188
232
  const callsSchema = getSchemaAtPath(AssistantConfigSchema, "calls");
189
233
  expect(callsSchema).not.toBeNull();
190
234
  const jsonSchema = z.toJSONSchema(callsSchema!, {
191
235
  unrepresentable: "any",
236
+ io: "input",
192
237
  }) as Record<string, unknown>;
193
238
  const properties = jsonSchema.properties as
194
239
  | Record<string, unknown>
@@ -204,6 +249,7 @@ describe("z.toJSONSchema integration", () => {
204
249
  expect(maxTokensSchema).not.toBeNull();
205
250
  const jsonSchema = z.toJSONSchema(maxTokensSchema!, {
206
251
  unrepresentable: "any",
252
+ io: "input",
207
253
  }) as Record<string, unknown>;
208
254
  expect(jsonSchema.type).toBe("integer");
209
255
  });
@@ -216,6 +262,7 @@ describe("z.toJSONSchema integration", () => {
216
262
  expect(segSchema).not.toBeNull();
217
263
  const jsonSchema = z.toJSONSchema(segSchema!, {
218
264
  unrepresentable: "any",
265
+ io: "input",
219
266
  }) as Record<string, unknown>;
220
267
  expect(jsonSchema.type).toBe("object");
221
268
  const properties = jsonSchema.properties as
@@ -186,7 +186,7 @@ describe("AssistantConfigSchema", () => {
186
186
  enabled: true,
187
187
  enqueueIntervalMs: 6 * 60 * 60 * 1000,
188
188
  supersededItemRetentionMs: 30 * 24 * 60 * 60 * 1000,
189
- conversationRetentionDays: 90,
189
+ conversationRetentionDays: 0,
190
190
  });
191
191
  });
192
192
 
@@ -62,12 +62,13 @@ mock.module("../config/loader.js", () => ({
62
62
  // ── Overflow recovery mocks ──────────────────────────────────────────
63
63
 
64
64
  // Token estimator — controllable per-test via mockEstimateTokens.
65
- // Can be a number (constant) or a function for dynamic behavior.
66
- let mockEstimateTokens: number | (() => number) = 1000;
65
+ // Can be a number (constant), a no-arg function, or a function that
66
+ // receives the messages array for dynamic behavior based on content.
67
+ let mockEstimateTokens: number | ((msgs?: Message[]) => number) = 1000;
67
68
  mock.module("../context/token-estimator.js", () => ({
68
- estimatePromptTokens: () =>
69
+ estimatePromptTokens: (msgs: Message[]) =>
69
70
  typeof mockEstimateTokens === "function"
70
- ? mockEstimateTokens()
71
+ ? mockEstimateTokens(msgs)
71
72
  : mockEstimateTokens,
72
73
  }));
73
74
 
@@ -208,8 +209,9 @@ mock.module("../daemon/conversation-memory.js", () => ({
208
209
  }),
209
210
  }));
210
211
 
212
+ let mockApplyRuntimeInjections: (msgs: Message[]) => Message[] = (msgs) => msgs;
211
213
  mock.module("../daemon/conversation-runtime-assembly.js", () => ({
212
- applyRuntimeInjections: (msgs: Message[]) => msgs,
214
+ applyRuntimeInjections: (msgs: Message[]) => mockApplyRuntimeInjections(msgs),
213
215
  stripInjectedContext: (msgs: Message[]) => msgs,
214
216
  }));
215
217
 
@@ -327,6 +329,7 @@ mock.module("../agent/message-types.js", () => ({
327
329
 
328
330
  mock.module("../memory/llm-request-log-store.js", () => ({
329
331
  recordRequestLog: () => {},
332
+ backfillMessageIdOnLogs: () => {},
330
333
  }));
331
334
 
332
335
  // ── Imports (after mocks) ────────────────────────────────────────────
@@ -520,6 +523,7 @@ beforeEach(() => {
520
523
  mockReducerStepFn = null;
521
524
  mockOverflowAction = "fail_gracefully";
522
525
  mockApprovalResult = { approved: false };
526
+ mockApplyRuntimeInjections = (msgs) => msgs;
523
527
  recordUsageMock.mockClear();
524
528
  });
525
529
 
@@ -1929,4 +1933,144 @@ describe("session-agent-loop overflow recovery (JARVIS-110)", () => {
1929
1933
  // Agent loop: 1 initial + 3 mid-loop re-entries + 2 convergence re-runs = 6 calls
1930
1934
  expect(agentLoopCallCount).toBe(6);
1931
1935
  });
1936
+
1937
+ // ── Test 8 ────────────────────────────────────────────────────────
1938
+ // BUG: The preflight overflow reducer's budget check uses
1939
+ // step.estimatedTokens (computed on bare ctx.messages) without
1940
+ // accounting for tokens added by applyRuntimeInjections(). This
1941
+ // causes the reducer to stop early when the bare estimate is under
1942
+ // budget, even though post-injection tokens exceed it — leading to
1943
+ // a wasted provider round-trip that gets rejected.
1944
+ //
1945
+ // After fix: the budget check re-estimates on runMessages (with
1946
+ // injections) so the reducer continues to the next tier.
1947
+ test("preflight reducer continues when post-injection tokens exceed budget", async () => {
1948
+ const events: ServerMessage[] = [];
1949
+
1950
+ // Injections add an extra message, bumping the token count.
1951
+ const injectionMessage: Message = {
1952
+ role: "user" as const,
1953
+ content: [
1954
+ {
1955
+ type: "text" as const,
1956
+ text: "injected context " + "x".repeat(500),
1957
+ },
1958
+ ],
1959
+ };
1960
+ mockApplyRuntimeInjections = (msgs) => [...msgs, injectionMessage];
1961
+
1962
+ // Budget = 200_000 * 0.95 = 190_000
1963
+ // The estimator returns different values based on whether the
1964
+ // injection message is present:
1965
+ // - bare history (no injection msg) → 195_000 (triggers preflight)
1966
+ // - after tier 1 bare → 185_000 (under budget, would stop early without fix)
1967
+ // - after tier 1 with injection → 195_000 (still over budget)
1968
+ // - after tier 2 bare → 170_000
1969
+ // - after tier 2 with injection → 175_000 (under budget, reducer stops)
1970
+ let reducerCallCount = 0;
1971
+ mockEstimateTokens = (msgs?: Message[]) => {
1972
+ const hasInjection = msgs?.some(
1973
+ (m) =>
1974
+ m.role === "user" &&
1975
+ Array.isArray(m.content) &&
1976
+ m.content.some(
1977
+ (b: { type: string; text?: string }) =>
1978
+ b.type === "text" &&
1979
+ typeof b.text === "string" &&
1980
+ b.text.startsWith("injected context"),
1981
+ ),
1982
+ );
1983
+ if (reducerCallCount === 0) {
1984
+ // Before any reduction: preflight check on runMessages (with injection)
1985
+ return 195_000;
1986
+ }
1987
+ if (reducerCallCount === 1) {
1988
+ // After tier 1
1989
+ return hasInjection ? 195_000 : 185_000;
1990
+ }
1991
+ // After tier 2
1992
+ return hasInjection ? 175_000 : 170_000;
1993
+ };
1994
+
1995
+ mockReducerStepFn = (msgs: Message[]) => {
1996
+ reducerCallCount++;
1997
+ const tier =
1998
+ reducerCallCount === 1 ? "forced_compaction" : "tool_result_truncation";
1999
+ return {
2000
+ messages: msgs,
2001
+ tier,
2002
+ state: {
2003
+ appliedTiers:
2004
+ reducerCallCount === 1
2005
+ ? ["forced_compaction"]
2006
+ : ["forced_compaction", "tool_result_truncation"],
2007
+ injectionMode: "full" as const,
2008
+ exhausted: reducerCallCount >= 2,
2009
+ },
2010
+ // Bare-history estimate (what the reducer sees on ctx.messages)
2011
+ estimatedTokens: reducerCallCount === 1 ? 185_000 : 170_000,
2012
+ compactionResult: {
2013
+ compacted: true,
2014
+ messages: msgs,
2015
+ compactedPersistedMessages: 5,
2016
+ summaryText: "Summary",
2017
+ previousEstimatedInputTokens: 195_000,
2018
+ estimatedInputTokens: reducerCallCount === 1 ? 185_000 : 170_000,
2019
+ maxInputTokens: 200_000,
2020
+ thresholdTokens: 160_000,
2021
+ compactedMessages: 10,
2022
+ summaryCalls: 1,
2023
+ summaryInputTokens: 500,
2024
+ summaryOutputTokens: 200,
2025
+ summaryModel: "mock-model",
2026
+ },
2027
+ };
2028
+ };
2029
+
2030
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
2031
+ onEvent({
2032
+ type: "message_complete",
2033
+ message: {
2034
+ role: "assistant",
2035
+ content: [{ type: "text", text: "done" }],
2036
+ },
2037
+ });
2038
+ onEvent({
2039
+ type: "usage",
2040
+ inputTokens: 170_000,
2041
+ outputTokens: 200,
2042
+ model: "test-model",
2043
+ providerDurationMs: 500,
2044
+ });
2045
+ return [
2046
+ ...messages,
2047
+ {
2048
+ role: "assistant" as const,
2049
+ content: [{ type: "text", text: "done" }] as ContentBlock[],
2050
+ },
2051
+ ];
2052
+ };
2053
+
2054
+ const ctx = makeCtx({
2055
+ agentLoopRun,
2056
+ contextWindowManager: {
2057
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
2058
+ maybeCompact: async () => ({ compacted: false }),
2059
+ } as unknown as AgentLoopConversationContext["contextWindowManager"],
2060
+ });
2061
+
2062
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
2063
+
2064
+ // The reducer must be called twice — the first tier's bare estimate
2065
+ // (185k) is under budget (190k), but post-injection tokens (195k)
2066
+ // still exceed it. Without the fix, the reducer would stop after
2067
+ // tier 1 and the provider call would likely fail.
2068
+ expect(reducerCallCount).toBe(2);
2069
+
2070
+ // Should succeed without errors
2071
+ const conversationError = events.find(
2072
+ (e) => e.type === "conversation_error",
2073
+ );
2074
+ expect(conversationError).toBeUndefined();
2075
+ });
1932
2076
  });
@@ -143,6 +143,14 @@ mock.module("../memory/conversation-crud.js", () => ({
143
143
  getConversationOriginChannel: () => null,
144
144
  }));
145
145
 
146
+ const syncMessageToDiskMock = mock(() => {});
147
+ const rebuildConversationDiskViewFromDbStateMock = mock(() => {});
148
+ mock.module("../memory/conversation-disk-view.js", () => ({
149
+ syncMessageToDisk: syncMessageToDiskMock,
150
+ rebuildConversationDiskViewFromDbState:
151
+ rebuildConversationDiskViewFromDbStateMock,
152
+ }));
153
+
146
154
  mock.module("../memory/retriever.js", () => ({
147
155
  buildMemoryRecall: async () => ({
148
156
  enabled: false,
@@ -213,11 +221,13 @@ mock.module("../daemon/history-repair.js", () => ({
213
221
  deepRepairHistory: (msgs: Message[]) => ({ messages: msgs, stats: {} }),
214
222
  }));
215
223
 
224
+ const consolidateAssistantMessagesMock = mock(() => false);
216
225
  mock.module("../daemon/conversation-history.js", () => ({
217
- consolidateAssistantMessages: () => {},
226
+ consolidateAssistantMessages: consolidateAssistantMessagesMock,
218
227
  }));
219
228
 
220
229
  const recordUsageMock = mock(() => {});
230
+ const recordRequestLogMock = mock(() => {});
221
231
  mock.module("../daemon/conversation-usage.js", () => ({
222
232
  recordUsage: recordUsageMock,
223
233
  }));
@@ -306,7 +316,8 @@ mock.module("../agent/message-types.js", () => ({
306
316
  }));
307
317
 
308
318
  mock.module("../memory/llm-request-log-store.js", () => ({
309
- recordRequestLog: () => {},
319
+ recordRequestLog: recordRequestLogMock,
320
+ backfillMessageIdOnLogs: () => {},
310
321
  }));
311
322
 
312
323
  // ── Imports (after mocks) ────────────────────────────────────────────
@@ -447,6 +458,11 @@ beforeEach(() => {
447
458
  mockOverflowAction = "fail_gracefully";
448
459
  mockApprovalResult = { approved: false };
449
460
  recordUsageMock.mockClear();
461
+ recordRequestLogMock.mockClear();
462
+ syncMessageToDiskMock.mockClear();
463
+ rebuildConversationDiskViewFromDbStateMock.mockClear();
464
+ consolidateAssistantMessagesMock.mockReset();
465
+ consolidateAssistantMessagesMock.mockImplementation(() => false);
450
466
  });
451
467
 
452
468
  describe("session-agent-loop", () => {
@@ -590,6 +606,235 @@ describe("session-agent-loop", () => {
590
606
  });
591
607
  });
592
608
 
609
+ describe("LLM request log persistence", () => {
610
+ test("record request log prefers the actual provider from failover", async () => {
611
+ const events: ServerMessage[] = [];
612
+ const rawRequest = {
613
+ model: "gpt-4.1",
614
+ messages: [{ role: "user", content: "Hello" }],
615
+ };
616
+ const rawResponse = {
617
+ model: "gpt-4.1-2026-03-01",
618
+ choices: [
619
+ {
620
+ finish_reason: "stop",
621
+ message: {
622
+ role: "assistant",
623
+ content: "Hi there.",
624
+ },
625
+ },
626
+ ],
627
+ usage: {
628
+ prompt_tokens: 12,
629
+ completion_tokens: 3,
630
+ },
631
+ };
632
+
633
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
634
+ onEvent({
635
+ type: "message_complete",
636
+ message: {
637
+ role: "assistant",
638
+ content: [{ type: "text", text: "Hi there." }],
639
+ },
640
+ });
641
+ onEvent({
642
+ type: "usage",
643
+ inputTokens: 12,
644
+ outputTokens: 3,
645
+ model: "gpt-4.1-2026-03-01",
646
+ actualProvider: "fireworks",
647
+ providerDurationMs: 45,
648
+ rawRequest,
649
+ rawResponse,
650
+ });
651
+ return [
652
+ ...messages,
653
+ {
654
+ role: "assistant" as const,
655
+ content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
656
+ },
657
+ ];
658
+ };
659
+
660
+ const ctx = makeCtx({
661
+ agentLoopRun,
662
+ provider: {
663
+ name: "openrouter",
664
+ sendMessage: async () => ({
665
+ content: [{ type: "text", text: "title" }],
666
+ model: "mock",
667
+ usage: { inputTokens: 0, outputTokens: 0 },
668
+ stopReason: "end_turn",
669
+ }),
670
+ } as unknown as AgentLoopConversationContext["provider"],
671
+ });
672
+
673
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
674
+
675
+ expect(recordRequestLogMock).toHaveBeenCalledTimes(1);
676
+ const call = recordRequestLogMock.mock.calls[0] as unknown as [
677
+ string,
678
+ string,
679
+ string,
680
+ undefined,
681
+ string,
682
+ ];
683
+ expect(call).toEqual([
684
+ "test-conv",
685
+ JSON.stringify(rawRequest),
686
+ JSON.stringify(rawResponse),
687
+ undefined,
688
+ "fireworks",
689
+ ]);
690
+ });
691
+
692
+ test("record request log falls back to the runtime provider when no actual provider is supplied", async () => {
693
+ const rawRequest = {
694
+ model: "gpt-4.1",
695
+ messages: [{ role: "user", content: "Hello" }],
696
+ };
697
+ const rawResponse = {
698
+ model: "gpt-4.1-2026-03-01",
699
+ choices: [
700
+ {
701
+ finish_reason: "stop",
702
+ message: {
703
+ role: "assistant",
704
+ content: "Hi there.",
705
+ },
706
+ },
707
+ ],
708
+ };
709
+
710
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
711
+ onEvent({
712
+ type: "message_complete",
713
+ message: {
714
+ role: "assistant",
715
+ content: [{ type: "text", text: "Hi there." }],
716
+ },
717
+ });
718
+ onEvent({
719
+ type: "usage",
720
+ inputTokens: 12,
721
+ outputTokens: 3,
722
+ model: "gpt-4.1-2026-03-01",
723
+ providerDurationMs: 45,
724
+ rawRequest,
725
+ rawResponse,
726
+ });
727
+ return [
728
+ ...messages,
729
+ {
730
+ role: "assistant" as const,
731
+ content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
732
+ },
733
+ ];
734
+ };
735
+
736
+ const ctx = makeCtx({
737
+ agentLoopRun,
738
+ provider: {
739
+ name: "openrouter",
740
+ sendMessage: async () => ({
741
+ content: [{ type: "text", text: "title" }],
742
+ model: "mock",
743
+ usage: { inputTokens: 0, outputTokens: 0 },
744
+ stopReason: "end_turn",
745
+ }),
746
+ } as unknown as AgentLoopConversationContext["provider"],
747
+ });
748
+
749
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
750
+
751
+ expect(recordRequestLogMock).toHaveBeenCalledTimes(1);
752
+ const call = recordRequestLogMock.mock.calls[0] as unknown as [
753
+ string,
754
+ string,
755
+ string,
756
+ undefined,
757
+ string,
758
+ ];
759
+ expect(call[4]).toBe("openrouter");
760
+ });
761
+ });
762
+
763
+ describe("usage accounting", () => {
764
+ test("records the actual provider for failover-served usage", async () => {
765
+ const events: ServerMessage[] = [];
766
+
767
+ const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
768
+ onEvent({
769
+ type: "message_complete",
770
+ message: {
771
+ role: "assistant",
772
+ content: [{ type: "text", text: "Hi there." }],
773
+ },
774
+ });
775
+ onEvent({
776
+ type: "usage",
777
+ inputTokens: 12,
778
+ outputTokens: 3,
779
+ model: "gpt-4.1-2026-03-01",
780
+ actualProvider: "fireworks",
781
+ providerDurationMs: 45,
782
+ rawRequest: {
783
+ model: "gpt-4.1",
784
+ messages: [{ role: "user", content: "Hello" }],
785
+ },
786
+ rawResponse: {
787
+ model: "gpt-4.1-2026-03-01",
788
+ choices: [
789
+ {
790
+ finish_reason: "stop",
791
+ message: {
792
+ role: "assistant",
793
+ content: "Hi there.",
794
+ },
795
+ },
796
+ ],
797
+ },
798
+ });
799
+ return [
800
+ ...messages,
801
+ {
802
+ role: "assistant" as const,
803
+ content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
804
+ },
805
+ ];
806
+ };
807
+
808
+ const ctx = makeCtx({
809
+ agentLoopRun,
810
+ provider: {
811
+ name: "openrouter",
812
+ sendMessage: async () => ({
813
+ content: [{ type: "text", text: "title" }],
814
+ model: "mock",
815
+ usage: { inputTokens: 0, outputTokens: 0 },
816
+ stopReason: "end_turn",
817
+ }),
818
+ } as unknown as AgentLoopConversationContext["provider"],
819
+ });
820
+
821
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
822
+
823
+ const mainAgentCall = recordUsageMock.mock.calls.find(
824
+ (call) => (call as unknown[])[5] === "main_agent",
825
+ ) as unknown[] | undefined;
826
+
827
+ expect(mainAgentCall).toBeDefined();
828
+ expect(mainAgentCall?.[0]).toMatchObject({
829
+ conversationId: "test-conv",
830
+ providerName: "fireworks",
831
+ });
832
+ expect(mainAgentCall?.[1]).toBe(12);
833
+ expect(mainAgentCall?.[2]).toBe(3);
834
+ expect(mainAgentCall?.[3]).toBe("gpt-4.1-2026-03-01");
835
+ });
836
+ });
837
+
593
838
  describe("context window exhaustion (context-too-large recovery)", () => {
594
839
  test("forwards cache-aware compaction usage to recordUsage", async () => {
595
840
  const events: ServerMessage[] = [];
@@ -1688,6 +1933,49 @@ describe("session-agent-loop", () => {
1688
1933
 
1689
1934
  expect(drainReason).toBe("loop_complete");
1690
1935
  });
1936
+
1937
+ test("rebuilds disk view after consolidation mutates persisted history", async () => {
1938
+ consolidateAssistantMessagesMock.mockReturnValue(true);
1939
+
1940
+ const ctx = makeCtx({
1941
+ agentLoopRun: async (
1942
+ messages: Message[],
1943
+ onEvent: (event: AgentEvent) => void,
1944
+ ) => {
1945
+ onEvent({
1946
+ type: "message_complete",
1947
+ message: {
1948
+ role: "assistant",
1949
+ content: [{ type: "text", text: "done" }],
1950
+ },
1951
+ });
1952
+ onEvent({
1953
+ type: "usage",
1954
+ inputTokens: 10,
1955
+ outputTokens: 5,
1956
+ model: "test",
1957
+ providerDurationMs: 50,
1958
+ });
1959
+ return [
1960
+ ...messages,
1961
+ {
1962
+ role: "assistant" as const,
1963
+ content: [{ type: "text", text: "done" }] as ContentBlock[],
1964
+ },
1965
+ ];
1966
+ },
1967
+ });
1968
+
1969
+ await runAgentLoopImpl(ctx, "hi", "msg-consolidate", () => {});
1970
+
1971
+ expect(consolidateAssistantMessagesMock).toHaveBeenCalledWith(
1972
+ "test-conv",
1973
+ "msg-consolidate",
1974
+ );
1975
+ expect(rebuildConversationDiskViewFromDbStateMock).toHaveBeenCalledWith(
1976
+ "test-conv",
1977
+ );
1978
+ });
1691
1979
  });
1692
1980
 
1693
1981
  describe("stale pending surface cleanup", () => {