@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
@@ -46,6 +46,7 @@ import {
46
46
  deleteApp,
47
47
  deleteConnection,
48
48
  disconnectOAuthProvider,
49
+ getActiveConnection,
49
50
  getApp,
50
51
  getAppByProviderAndClientId,
51
52
  getConnection,
@@ -631,6 +632,120 @@ describe("connection operations", () => {
631
632
  });
632
633
  });
633
634
 
635
+ describe("getActiveConnection", () => {
636
+ test("returns the most recent active connection with no filters", async () => {
637
+ const app = await createTestApp("github", "client-1");
638
+
639
+ createConnection({
640
+ oauthAppId: app.id,
641
+ providerKey: "github",
642
+ grantedScopes: ["repo"],
643
+ hasRefreshToken: false,
644
+ createdAt: 1000,
645
+ });
646
+
647
+ const conn2 = createConnection({
648
+ oauthAppId: app.id,
649
+ providerKey: "github",
650
+ grantedScopes: ["repo", "user"],
651
+ hasRefreshToken: true,
652
+ createdAt: 2000,
653
+ });
654
+
655
+ const result = getActiveConnection("github");
656
+ expect(result).toBeDefined();
657
+ expect(result!.id).toBe(conn2.id);
658
+ });
659
+
660
+ test("narrows by account when provided", async () => {
661
+ const app = await createTestApp("github", "client-1");
662
+
663
+ const conn1 = createConnection({
664
+ oauthAppId: app.id,
665
+ providerKey: "github",
666
+ accountInfo: "user1@example.com",
667
+ grantedScopes: ["repo"],
668
+ hasRefreshToken: false,
669
+ createdAt: 1000,
670
+ });
671
+
672
+ createConnection({
673
+ oauthAppId: app.id,
674
+ providerKey: "github",
675
+ accountInfo: "user2@example.com",
676
+ grantedScopes: ["repo"],
677
+ hasRefreshToken: false,
678
+ createdAt: 2000,
679
+ });
680
+
681
+ const result = getActiveConnection("github", {
682
+ account: "user1@example.com",
683
+ });
684
+ expect(result).toBeDefined();
685
+ expect(result!.id).toBe(conn1.id);
686
+ });
687
+
688
+ test("narrows by clientId when provided", async () => {
689
+ const app1 = await createTestApp("github", "client-a");
690
+ const app2 = await createTestApp("github", "client-b");
691
+
692
+ const conn1 = createConnection({
693
+ oauthAppId: app1.id,
694
+ providerKey: "github",
695
+ grantedScopes: ["repo"],
696
+ hasRefreshToken: false,
697
+ createdAt: 1000,
698
+ });
699
+
700
+ createConnection({
701
+ oauthAppId: app2.id,
702
+ providerKey: "github",
703
+ grantedScopes: ["repo"],
704
+ hasRefreshToken: false,
705
+ createdAt: 2000,
706
+ });
707
+
708
+ const result = getActiveConnection("github", { clientId: "client-a" });
709
+ expect(result).toBeDefined();
710
+ expect(result!.id).toBe(conn1.id);
711
+ });
712
+
713
+ test("returns undefined when clientId has no matching app", async () => {
714
+ const app = await createTestApp("github", "client-1");
715
+
716
+ createConnection({
717
+ oauthAppId: app.id,
718
+ providerKey: "github",
719
+ grantedScopes: ["repo"],
720
+ hasRefreshToken: false,
721
+ });
722
+
723
+ const result = getActiveConnection("github", {
724
+ clientId: "nonexistent",
725
+ });
726
+ expect(result).toBeUndefined();
727
+ });
728
+
729
+ test("skips revoked connections", async () => {
730
+ const app = await createTestApp("github", "client-1");
731
+
732
+ const conn = createConnection({
733
+ oauthAppId: app.id,
734
+ providerKey: "github",
735
+ grantedScopes: ["repo"],
736
+ hasRefreshToken: false,
737
+ });
738
+ updateConnection(conn.id, { status: "revoked" });
739
+
740
+ const result = getActiveConnection("github");
741
+ expect(result).toBeUndefined();
742
+ });
743
+
744
+ test("returns undefined when no connections exist", () => {
745
+ expect(getActiveConnection("github")).toBeUndefined();
746
+ });
747
+ });
748
+
634
749
  describe("getConnectionByProvider", () => {
635
750
  test("returns the most recent active connection", async () => {
636
751
  const app = await createTestApp("github", "client-1");
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Unit tests for identity field parsing and template placeholder filtering.
3
+ *
4
+ * Validates that parseIdentityFields correctly extracts real values from
5
+ * IDENTITY.md content while treating template placeholders (e.g.
6
+ * `_(not yet chosen)_`) as empty/unset.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import {
12
+ isTemplatePlaceholder,
13
+ parseIdentityFields,
14
+ } from "../daemon/handlers/identity.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // isTemplatePlaceholder
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe("isTemplatePlaceholder", () => {
21
+ test("returns true for _(not yet chosen)_", () => {
22
+ expect(isTemplatePlaceholder("_(not yet chosen)_")).toBe(true);
23
+ });
24
+
25
+ test("returns true for _(not yet established)_", () => {
26
+ expect(isTemplatePlaceholder("_(not yet established)_")).toBe(true);
27
+ });
28
+
29
+ test("returns true for any value matching _(…)_ pattern", () => {
30
+ expect(isTemplatePlaceholder("_(something else)_")).toBe(true);
31
+ });
32
+
33
+ test("returns false for normal values", () => {
34
+ expect(isTemplatePlaceholder("Your helpful coding assistant")).toBe(false);
35
+ expect(isTemplatePlaceholder("Jarvis")).toBe(false);
36
+ expect(isTemplatePlaceholder("")).toBe(false);
37
+ });
38
+
39
+ test("returns false for partial matches", () => {
40
+ expect(isTemplatePlaceholder("_(incomplete")).toBe(false);
41
+ expect(isTemplatePlaceholder("incomplete)_")).toBe(false);
42
+ expect(isTemplatePlaceholder("_(")).toBe(false);
43
+ expect(isTemplatePlaceholder(")_")).toBe(false);
44
+ });
45
+ });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // parseIdentityFields — placeholder filtering
49
+ // ---------------------------------------------------------------------------
50
+
51
+ describe("parseIdentityFields", () => {
52
+ test("returns empty strings for all template placeholder values", () => {
53
+ const content = [
54
+ "- **Name:** _(not yet chosen)_",
55
+ "- **Role:** _(not yet established)_",
56
+ "- **Personality:** _(not yet chosen)_",
57
+ "- **Emoji:** _(not yet chosen)_",
58
+ "- **Home:** _(not yet chosen)_",
59
+ ].join("\n");
60
+
61
+ const fields = parseIdentityFields(content);
62
+ expect(fields.name).toBe("");
63
+ expect(fields.role).toBe("");
64
+ expect(fields.personality).toBe("");
65
+ expect(fields.emoji).toBe("");
66
+ expect(fields.home).toBe("");
67
+ });
68
+
69
+ test("preserves real user-provided values", () => {
70
+ const content = [
71
+ "- **Name:** Jarvis",
72
+ "- **Role:** Coding assistant",
73
+ "- **Personality:** Friendly and helpful",
74
+ "- **Emoji:** 🤖",
75
+ "- **Home:** ~/projects",
76
+ ].join("\n");
77
+
78
+ const fields = parseIdentityFields(content);
79
+ expect(fields.name).toBe("Jarvis");
80
+ expect(fields.role).toBe("Coding assistant");
81
+ expect(fields.personality).toBe("Friendly and helpful");
82
+ expect(fields.emoji).toBe("🤖");
83
+ expect(fields.home).toBe("~/projects");
84
+ });
85
+
86
+ test("handles a mix of real and placeholder values", () => {
87
+ const content = [
88
+ "- **Name:** Jarvis",
89
+ "- **Role:** _(not yet established)_",
90
+ "- **Personality:** Friendly",
91
+ "- **Emoji:** _(not yet chosen)_",
92
+ "- **Home:** ~/dev",
93
+ ].join("\n");
94
+
95
+ const fields = parseIdentityFields(content);
96
+ expect(fields.name).toBe("Jarvis");
97
+ expect(fields.role).toBe("");
98
+ expect(fields.personality).toBe("Friendly");
99
+ expect(fields.emoji).toBe("");
100
+ expect(fields.home).toBe("~/dev");
101
+ });
102
+
103
+ test("returns role: '' when IDENTITY.md contains placeholder role", () => {
104
+ const content = "- **Role:** _(not yet established)_";
105
+ const fields = parseIdentityFields(content);
106
+ expect(fields.role).toBe("");
107
+ });
108
+
109
+ test("returns name: '' when IDENTITY.md contains placeholder name", () => {
110
+ const content = "- **Name:** _(not yet chosen)_";
111
+ const fields = parseIdentityFields(content);
112
+ expect(fields.name).toBe("");
113
+ });
114
+
115
+ test('parses role: "Coding assistant" for real values', () => {
116
+ const content = "- **Role:** Coding assistant";
117
+ const fields = parseIdentityFields(content);
118
+ expect(fields.role).toBe("Coding assistant");
119
+ });
120
+
121
+ test("returns empty strings when content has no identity fields", () => {
122
+ const fields = parseIdentityFields("# Some other content\nHello world");
123
+ expect(fields.name).toBe("");
124
+ expect(fields.role).toBe("");
125
+ expect(fields.personality).toBe("");
126
+ expect(fields.emoji).toBe("");
127
+ expect(fields.home).toBe("");
128
+ });
129
+ });
@@ -68,9 +68,7 @@ mock.module("../util/retry.js", () => {
68
68
  const causeCode = (error.cause as NodeJS.ErrnoException).code;
69
69
  if (causeCode && retryableCodes.has(causeCode)) return true;
70
70
  }
71
- if (
72
- RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))
73
- ) {
71
+ if (RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))) {
74
72
  return true;
75
73
  }
76
74
  const cause = error.cause;
@@ -0,0 +1,66 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../util/logger.js", () => ({
4
+ getLogger: () =>
5
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
6
+ }));
7
+
8
+ import { FailoverProvider } from "../providers/failover.js";
9
+ import type {
10
+ Message,
11
+ Provider,
12
+ ProviderResponse,
13
+ } from "../providers/types.js";
14
+ import { ProviderError } from "../util/errors.js";
15
+
16
+ const MESSAGES: Message[] = [
17
+ { role: "user", content: [{ type: "text", text: "Hello" }] },
18
+ ];
19
+
20
+ function successResponse(
21
+ overrides?: Partial<ProviderResponse>,
22
+ ): ProviderResponse {
23
+ return {
24
+ content: [{ type: "text", text: "ok" }],
25
+ model: "test-model",
26
+ usage: { inputTokens: 10, outputTokens: 5 },
27
+ stopReason: "end_turn",
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ describe("FailoverProvider actual provider propagation", () => {
33
+ test("stamps the winning provider when failover uses a fallback", async () => {
34
+ const primary: Provider = {
35
+ name: "openrouter",
36
+ async sendMessage() {
37
+ throw new ProviderError("down", "openrouter", 500);
38
+ },
39
+ };
40
+ const secondary: Provider = {
41
+ name: "fireworks",
42
+ async sendMessage() {
43
+ return successResponse();
44
+ },
45
+ };
46
+
47
+ const provider = new FailoverProvider([primary, secondary]);
48
+ const response = await provider.sendMessage(MESSAGES);
49
+
50
+ expect(response.actualProvider).toBe("fireworks");
51
+ });
52
+
53
+ test("preserves an inner provider's actual provider when already set", async () => {
54
+ const inner: Provider = {
55
+ name: "retry-wrapper",
56
+ async sendMessage() {
57
+ return successResponse({ actualProvider: "anthropic" });
58
+ },
59
+ };
60
+
61
+ const provider = new FailoverProvider([inner]);
62
+ const response = await provider.sendMessage(MESSAGES);
63
+
64
+ expect(response.actualProvider).toBe("anthropic");
65
+ });
66
+ });
@@ -81,6 +81,23 @@ const mockAttachments: Array<{
81
81
  let mockAttachmentIdCounter = 0;
82
82
 
83
83
  mock.module("../memory/attachments-store.js", () => ({
84
+ attachFileBackedAttachmentToMessage: (
85
+ _messageId: string,
86
+ _position: number,
87
+ filename: string,
88
+ mimeType: string,
89
+ _filePath: string,
90
+ sizeBytes: number,
91
+ ) => {
92
+ const att = {
93
+ id: `att-${++mockAttachmentIdCounter}`,
94
+ originalFilename: filename,
95
+ mimeType,
96
+ sizeBytes,
97
+ };
98
+ mockAttachments.push(att);
99
+ return att;
100
+ },
84
101
  uploadFileBackedAttachment: (
85
102
  filename: string,
86
103
  mimeType: string,
@@ -109,7 +109,7 @@ describe("tool registry dynamic-tools tools", () => {
109
109
 
110
110
  describe("tool manifest", () => {
111
111
  test("eager module tool names list contains expected count", () => {
112
- expect(eagerModuleToolNames.length).toBe(11);
112
+ expect(eagerModuleToolNames.length).toBe(9);
113
113
  });
114
114
 
115
115
  test("explicit tools list includes memory and credential tools", () => {
@@ -187,14 +187,9 @@ describe("baseline characterization: core app tool surface", () => {
187
187
 
188
188
  const nonProxyAppTools = [
189
189
  "app_create",
190
- "app_list",
191
- "app_query",
192
- "app_update",
193
190
  "app_delete",
194
- "app_file_list",
195
- "app_file_read",
196
- "app_file_edit",
197
- "app_file_write",
191
+ "app_generate_icon",
192
+ "app_refresh",
198
193
  ];
199
194
 
200
195
  for (const name of nonProxyAppTools) {
@@ -159,7 +159,7 @@ mock.module("../providers/registry.js", () => {
159
159
  getDefaultModel: (providerName: string) => {
160
160
  const defaults: Record<string, string> = {
161
161
  anthropic: "claude-opus-4-6",
162
- openai: "gpt-5.2",
162
+ openai: "gpt-5.4",
163
163
  gemini: "gemini-3-flash",
164
164
  ollama: "llama3.2",
165
165
  fireworks: "accounts/fireworks/models/kimi-k2p5",
@@ -116,7 +116,7 @@ describe("Runtime attachment metadata", () => {
116
116
  );
117
117
 
118
118
  // Upload and link an attachment using "self" as the assistantId
119
- const stored = uploadAttachment("chart.png", "image/png", "iVBOR");
119
+ const stored = uploadAttachment("chart.png", "image/png", "iVBORw==");
120
120
  linkAttachmentToMessage(assistantMsg.id, stored.id, 0);
121
121
 
122
122
  const res = await fetch(
@@ -181,7 +181,11 @@ describe("Runtime attachment metadata", () => {
181
181
  });
182
182
 
183
183
  test("GET /attachments/:id returns attachment with payload", async () => {
184
- const stored = uploadAttachment("report.pdf", "application/pdf", "JVBER");
184
+ const stored = uploadAttachment(
185
+ "report.pdf",
186
+ "application/pdf",
187
+ "JVBERA==",
188
+ );
185
189
 
186
190
  const res = await fetch(
187
191
  `http://127.0.0.1:${port}/v1/attachments/${stored.id}`,
@@ -201,7 +205,7 @@ describe("Runtime attachment metadata", () => {
201
205
  expect(body.filename).toBe("report.pdf");
202
206
  expect(body.mimeType).toBe("application/pdf");
203
207
  expect(body.kind).toBe("document");
204
- expect(body.data).toBe("JVBER");
208
+ expect(body.data).toBe("JVBERA==");
205
209
  expect(body.sizeBytes).toBeGreaterThan(0);
206
210
  });
207
211
 
@@ -70,21 +70,53 @@ describe("injectActivityField", () => {
70
70
  expect("activity" in props0).toBe(false);
71
71
  });
72
72
 
73
- test("skips tools that already have activity in properties", () => {
73
+ test("returns unchanged when activity is in top-level properties but not required", () => {
74
74
  const defs = [
75
75
  makeDef("my_tool", {
76
76
  type: "object",
77
77
  properties: { activity: { type: "number" } },
78
- required: [],
78
+ required: ["foo"],
79
79
  }),
80
80
  ];
81
81
  const result = injectActivityField(defs);
82
- // Should be the exact same object reference (no clone needed)
82
+ // Should be the exact same object reference (no modification)
83
83
  expect(Object.is(result[0], defs[0])).toBe(true);
84
84
  const schema = result[0].input_schema as Record<string, unknown>;
85
85
  const props = schema.properties as Record<string, unknown>;
86
86
  // Original activity type preserved
87
87
  expect(props.activity).toEqual({ type: "number" });
88
+ // required NOT modified — don't promote server-defined optional activity
89
+ expect(schema.required).toEqual(["foo"]);
90
+ });
91
+
92
+ test("returns unchanged when activity is in both top-level properties AND required", () => {
93
+ const defs = [
94
+ makeDef("my_tool", {
95
+ type: "object",
96
+ properties: { activity: { type: "string" } },
97
+ required: ["activity"],
98
+ }),
99
+ ];
100
+ const result = injectActivityField(defs);
101
+ const schema = result[0].input_schema as Record<string, unknown>;
102
+ const props = schema.properties as Record<string, unknown>;
103
+ // Original activity type preserved
104
+ expect(props.activity).toEqual({ type: "string" });
105
+ // activity must be in required even though it was already in properties
106
+ expect(schema.required).toEqual(["activity"]);
107
+ });
108
+
109
+ test("skips tools that already have activity in both properties and required", () => {
110
+ const defs = [
111
+ makeDef("my_tool", {
112
+ type: "object",
113
+ properties: { activity: { type: "number" } },
114
+ required: ["activity"],
115
+ }),
116
+ ];
117
+ const result = injectActivityField(defs);
118
+ // Should be the exact same object reference (no clone needed)
119
+ expect(Object.is(result[0], defs[0])).toBe(true);
88
120
  });
89
121
 
90
122
  test("does NOT mutate original definition objects", () => {
@@ -125,6 +157,60 @@ describe("injectActivityField", () => {
125
157
  expect(Object.is(result[0], defs[0])).toBe(true);
126
158
  });
127
159
 
160
+ test("does NOT add activity to top-level required when only in oneOf branch", () => {
161
+ const defs = [
162
+ makeDef("my_tool", {
163
+ type: "object",
164
+ properties: { shared: { type: "string" } },
165
+ oneOf: [
166
+ {
167
+ properties: {
168
+ activity: { type: "string" },
169
+ branch_a: { type: "number" },
170
+ },
171
+ },
172
+ {
173
+ properties: { branch_b: { type: "boolean" } },
174
+ },
175
+ ],
176
+ required: ["shared"],
177
+ }),
178
+ ];
179
+ const result = injectActivityField(defs);
180
+ // Should be the exact same object reference (no modification)
181
+ expect(Object.is(result[0], defs[0])).toBe(true);
182
+ const schema = result[0].input_schema as Record<string, unknown>;
183
+ // Top-level required should NOT include activity
184
+ expect(schema.required).toEqual(["shared"]);
185
+ // Top-level properties should NOT have activity injected
186
+ const props = schema.properties as Record<string, unknown>;
187
+ expect("activity" in props).toBe(false);
188
+ });
189
+
190
+ test("does NOT add activity to top-level required when only in allOf sub-schema with additionalProperties: false", () => {
191
+ const defs = [
192
+ makeDef("my_tool", {
193
+ type: "object",
194
+ properties: { foo: { type: "string" } },
195
+ additionalProperties: false,
196
+ allOf: [
197
+ {
198
+ properties: { activity: { type: "string" } },
199
+ },
200
+ ],
201
+ required: ["foo"],
202
+ }),
203
+ ];
204
+ const result = injectActivityField(defs);
205
+ // Should be the exact same object reference (no modification)
206
+ expect(Object.is(result[0], defs[0])).toBe(true);
207
+ const schema = result[0].input_schema as Record<string, unknown>;
208
+ // Top-level required should NOT include activity
209
+ expect(schema.required).toEqual(["foo"]);
210
+ const props = schema.properties as Record<string, unknown>;
211
+ expect("activity" in props).toBe(false);
212
+ });
213
+
128
214
  test("skips tools with activity defined inside allOf member (composite schema)", () => {
129
215
  const defs = [
130
216
  makeDef("my_tool", {
@@ -139,18 +225,92 @@ describe("injectActivityField", () => {
139
225
  }),
140
226
  ];
141
227
  const result = injectActivityField(defs);
142
- // Should be the exact same object reference (no injection)
228
+ // Should be the exact same object reference (no modification)
143
229
  expect(Object.is(result[0], defs[0])).toBe(true);
144
230
  const schema = result[0].input_schema as Record<string, unknown>;
145
231
  const props = schema.properties as Record<string, unknown>;
146
- // Top-level properties should NOT have activity injected
232
+ // Top-level properties should NOT have activity injected (it's in allOf)
147
233
  expect("activity" in props).toBe(false);
234
+ // Top-level required should NOT include activity (it's only in composite sub-schemas)
235
+ expect(schema.required).toEqual([]);
236
+ });
237
+
238
+ test("skips allOf composite schema where activity is already required", () => {
239
+ const defs = [
240
+ makeDef("my_tool", {
241
+ type: "object",
242
+ properties: { foo: { type: "string" } },
243
+ allOf: [
244
+ {
245
+ properties: { activity: { type: "string" } },
246
+ },
247
+ ],
248
+ required: ["activity"],
249
+ }),
250
+ ];
251
+ const result = injectActivityField(defs);
252
+ // Should be the exact same object reference (no change needed)
253
+ expect(Object.is(result[0], defs[0])).toBe(true);
148
254
  });
149
255
 
150
256
  test("handles empty definitions array", () => {
151
257
  const result = injectActivityField([]);
152
258
  expect(result).toEqual([]);
153
259
  });
260
+
261
+ test("injects activity only on tools that don't define it at all", () => {
262
+ const defs = [
263
+ // Normal tool without activity — should get it injected
264
+ makeDef("tool_a", {
265
+ type: "object",
266
+ properties: { foo: { type: "string" } },
267
+ required: ["foo"],
268
+ }),
269
+ // Tool defines activity in properties but NOT in required — left unchanged
270
+ makeDef("tool_b", {
271
+ type: "object",
272
+ properties: {
273
+ bar: { type: "string" },
274
+ activity: { type: "string", description: "custom activity" },
275
+ },
276
+ required: ["bar"],
277
+ }),
278
+ // Tool that defines activity in both properties AND required — left unchanged
279
+ makeDef("tool_c", {
280
+ type: "object",
281
+ properties: {
282
+ baz: { type: "number" },
283
+ activity: { type: "string", description: "custom activity" },
284
+ },
285
+ required: ["baz", "activity"],
286
+ }),
287
+ // Non-object schema — should be left alone
288
+ makeDef("tool_d", { type: "string" }),
289
+ // Object schema without properties — should be left alone
290
+ makeDef("tool_e", { type: "object" }),
291
+ ];
292
+
293
+ const result = injectActivityField(defs);
294
+
295
+ // tool_a: activity injected and required
296
+ const schemaA = result[0].input_schema as Record<string, unknown>;
297
+ expect(
298
+ (schemaA.properties as Record<string, unknown>).activity,
299
+ ).toBeDefined();
300
+ expect(schemaA.required).toEqual(["foo", "activity"]);
301
+
302
+ // tool_b: unchanged (activity optional, not promoted)
303
+ expect(Object.is(result[1], defs[1])).toBe(true);
304
+ const schemaB = result[1].input_schema as Record<string, unknown>;
305
+ expect(schemaB.required).toEqual(["bar"]);
306
+
307
+ // tool_c: unchanged (activity already present and required)
308
+ expect(Object.is(result[2], defs[2])).toBe(true);
309
+
310
+ // tool_d, tool_e: unchanged
311
+ expect(Object.is(result[3], defs[3])).toBe(true);
312
+ expect(Object.is(result[4], defs[4])).toBe(true);
313
+ });
154
314
  });
155
315
 
156
316
  describe("schemaDefinesProperty", () => {
@@ -330,7 +330,7 @@ describe("getAttachmentsForMessage", () => {
330
330
 
331
331
  test("returns attachments linked to a message", async () => {
332
332
  const msgId = await createMessage("assistant", "Here is a chart");
333
- const stored = uploadAttachment("chart.png", "image/png", "iVBOR");
333
+ const stored = uploadAttachment("chart.png", "image/png", "iVBORw==");
334
334
  linkAttachmentToMessage(msgId, stored.id, 0);
335
335
 
336
336
  const result = getAttachmentsForMessage(msgId);
@@ -338,7 +338,7 @@ describe("getAttachmentsForMessage", () => {
338
338
  expect(result[0].id).toBe(stored.id);
339
339
  expect(result[0].originalFilename).toBe("chart.png");
340
340
  expect(result[0].mimeType).toBe("image/png");
341
- expect(result[0].dataBase64).toBe("iVBOR");
341
+ expect(result[0].dataBase64).toBe("iVBORw==");
342
342
  });
343
343
 
344
344
  test("returns empty array when no attachments are linked", () => {