@vellumai/assistant 0.5.1 → 0.5.3

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