@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -1,324 +1,9 @@
1
- import {
2
- getAttachmentsForMessage,
3
- getFilePathForAttachment,
4
- setAttachmentThumbnail,
5
- } from "../../memory/attachments-store.js";
6
1
  import { getMessageById } from "../../memory/conversation-crud.js";
7
2
  import {
8
- getMessagesPaginated,
9
3
  listConversations,
10
4
  searchConversations,
11
5
  } from "../../memory/conversation-queries.js";
12
- import { silentlyWithLog } from "../../util/silently.js";
13
- import { truncate } from "../../util/truncate.js";
14
- import type {
15
- ConversationSearchRequest,
16
- HistoryRequest,
17
- MessageContentRequest,
18
- UserMessageAttachment,
19
- } from "../message-protocol.js";
20
- import { generateVideoThumbnail } from "../video-thumbnail.js";
21
- import {
22
- type HandlerContext,
23
- type HistorySurface,
24
- type HistoryToolCall,
25
- log,
26
- type ParsedHistoryMessage,
27
- renderHistoryContent,
28
- } from "./shared.js";
29
-
30
- /**
31
- * In light mode, strip heavy payloads (e.g. full HTML) from surface data
32
- * but preserve the fields the client needs to parse and render the surface.
33
- */
34
- function lightModeSurfaceData(s: HistorySurface): Record<string, unknown> {
35
- switch (s.surfaceType) {
36
- case "dynamic_page":
37
- return {
38
- ...(s.data.preview ? { preview: s.data.preview } : {}),
39
- ...(s.data.appId ? { appId: s.data.appId } : {}),
40
- };
41
- case "card":
42
- return {
43
- ...(typeof s.data.title === "string" ? { title: s.data.title } : {}),
44
- ...(typeof s.data.body === "string" ? { body: s.data.body } : {}),
45
- ...(typeof s.data.template === "string"
46
- ? { template: s.data.template }
47
- : {}),
48
- ...(s.data.templateData ? { templateData: s.data.templateData } : {}),
49
- };
50
- case "document_preview":
51
- return {
52
- ...(typeof s.data.surfaceId === "string"
53
- ? { surfaceId: s.data.surfaceId }
54
- : {}),
55
- ...(typeof s.data.title === "string" ? { title: s.data.title } : {}),
56
- ...(typeof s.data.content === "string"
57
- ? { content: s.data.content }
58
- : {}),
59
- };
60
- default:
61
- // For other types (list, table, form, confirmation, etc.),
62
- // preserve the full data — these are generally small.
63
- return s.data;
64
- }
65
- }
66
-
67
- export function handleHistoryRequest(
68
- msg: HistoryRequest,
69
- ctx: HandlerContext,
70
- ): void {
71
- // No limit means return all messages.
72
- const limit = msg.limit;
73
-
74
- // Resolve include flags: explicit flags override mode, mode provides defaults.
75
- // Default mode is 'light' when no mode and no include flags are specified.
76
- const isFullMode = msg.mode === "full";
77
- const includeAttachments = msg.includeAttachments ?? isFullMode;
78
- const includeToolImages = msg.includeToolImages ?? isFullMode;
79
- const includeSurfaceData = msg.includeSurfaceData ?? isFullMode;
80
-
81
- const { messages: dbMessages, hasMore } = getMessagesPaginated(
82
- msg.sessionId,
83
- limit,
84
- msg.beforeTimestamp,
85
- msg.beforeMessageId,
86
- );
87
-
88
- const parsed: ParsedHistoryMessage[] = dbMessages.map((m) => {
89
- let text = "";
90
- let toolCalls: HistoryToolCall[] = [];
91
- let toolCallsBeforeText = false;
92
- let textSegments: string[] = [];
93
- let contentOrder: string[] = [];
94
- let surfaces: HistorySurface[] = [];
95
- try {
96
- const content = JSON.parse(m.content);
97
- const rendered = renderHistoryContent(content);
98
- text = rendered.text;
99
- toolCalls = rendered.toolCalls;
100
- toolCallsBeforeText = rendered.toolCallsBeforeText;
101
- textSegments = rendered.textSegments;
102
- contentOrder = rendered.contentOrder;
103
- surfaces = rendered.surfaces;
104
- if (m.role === "assistant" && toolCalls.length > 0) {
105
- log.info(
106
- {
107
- messageId: m.id,
108
- toolCallCount: toolCalls.length,
109
- text: truncate(text, 100, ""),
110
- },
111
- "History message with tool calls",
112
- );
113
- }
114
- } catch (err) {
115
- log.debug(
116
- { err, messageId: m.id },
117
- "Failed to parse message content as JSON, using raw text",
118
- );
119
- text = m.content;
120
- textSegments = text ? [text] : [];
121
- contentOrder = text ? ["text:0"] : [];
122
- surfaces = [];
123
- }
124
- let subagentNotification: ParsedHistoryMessage["subagentNotification"];
125
- if (m.metadata) {
126
- try {
127
- subagentNotification = (
128
- JSON.parse(m.metadata) as {
129
- subagentNotification?: ParsedHistoryMessage["subagentNotification"];
130
- }
131
- ).subagentNotification;
132
- } catch (err) {
133
- log.debug(
134
- { err, messageId: m.id },
135
- "Failed to parse message metadata as JSON, ignoring",
136
- );
137
- }
138
- }
139
- return {
140
- id: m.id,
141
- role: m.role,
142
- text,
143
- timestamp: m.createdAt,
144
- toolCalls,
145
- toolCallsBeforeText,
146
- textSegments,
147
- contentOrder,
148
- surfaces,
149
- ...(subagentNotification ? { subagentNotification } : {}),
150
- };
151
- });
152
-
153
- const historyMessages = parsed.map((m) => {
154
- let attachments: UserMessageAttachment[] | undefined;
155
- if (m.role === "assistant" && m.id) {
156
- const linked = getAttachmentsForMessage(m.id);
157
- if (linked.length > 0) {
158
- if (includeAttachments) {
159
- // Full attachment data: same behavior as before
160
- const MAX_INLINE_B64_SIZE = 512 * 1024;
161
- attachments = linked.map((a) => {
162
- const isFileBacked = !a.dataBase64;
163
- const omit =
164
- isFileBacked ||
165
- (a.mimeType.startsWith("video/") &&
166
- a.dataBase64.length > MAX_INLINE_B64_SIZE);
167
-
168
- if (
169
- a.mimeType.startsWith("video/") &&
170
- !a.thumbnailBase64 &&
171
- a.dataBase64
172
- ) {
173
- const attachmentId = a.id;
174
- const base64 = a.dataBase64;
175
- silentlyWithLog(
176
- generateVideoThumbnail(base64).then((thumb) => {
177
- if (thumb) setAttachmentThumbnail(attachmentId, thumb);
178
- }),
179
- "video thumbnail generation",
180
- );
181
- }
182
-
183
- const fp = getFilePathForAttachment(a.id);
184
- return {
185
- id: a.id,
186
- filename: a.originalFilename,
187
- mimeType: a.mimeType,
188
- data: omit ? "" : a.dataBase64,
189
- ...(omit ? { sizeBytes: a.sizeBytes } : {}),
190
- ...(a.thumbnailBase64
191
- ? { thumbnailData: a.thumbnailBase64 }
192
- : {}),
193
- ...(fp ? { filePath: fp } : {}),
194
- };
195
- });
196
- } else {
197
- // Light mode: metadata only, strip base64 data
198
- attachments = linked.map((a) => {
199
- const fp = getFilePathForAttachment(a.id);
200
- return {
201
- id: a.id,
202
- filename: a.originalFilename,
203
- mimeType: a.mimeType,
204
- data: "",
205
- sizeBytes: a.sizeBytes,
206
- ...(a.thumbnailBase64
207
- ? { thumbnailData: a.thumbnailBase64 }
208
- : {}),
209
- ...(fp ? { filePath: fp } : {}),
210
- };
211
- });
212
- }
213
- }
214
- }
215
-
216
- // In light mode, strip imageData from tool calls
217
- const filteredToolCalls =
218
- m.toolCalls.length > 0
219
- ? includeToolImages
220
- ? m.toolCalls
221
- : m.toolCalls.map((tc) => {
222
- if (tc.imageData) {
223
- const { imageData: _, ...rest } = tc;
224
- return rest;
225
- }
226
- return tc;
227
- })
228
- : m.toolCalls;
229
-
230
- // In light mode, strip heavy payloads but keep essential fields so
231
- // the client can still parse and render surfaces (e.g. card title/body,
232
- // dynamic_page preview, document_preview metadata).
233
- const filteredSurfaces =
234
- m.surfaces.length > 0
235
- ? includeSurfaceData
236
- ? m.surfaces
237
- : m.surfaces.map((s) => ({
238
- surfaceId: s.surfaceId,
239
- surfaceType: s.surfaceType,
240
- title: s.title,
241
- data: lightModeSurfaceData(s),
242
- ...(s.actions ? { actions: s.actions } : {}),
243
- ...(s.display ? { display: s.display } : {}),
244
- }))
245
- : m.surfaces;
246
-
247
- // Apply text truncation when maxTextChars is set
248
- let wasTruncated = false;
249
- let textWasTruncated = false;
250
- let text = m.text;
251
- if (msg.maxTextChars !== undefined && text.length > msg.maxTextChars) {
252
- text = text.slice(0, msg.maxTextChars) + " \u2026 [truncated]";
253
- wasTruncated = true;
254
- textWasTruncated = true;
255
- }
256
-
257
- // Apply tool result truncation when maxToolResultChars is set
258
- const truncatedToolCalls =
259
- msg.maxToolResultChars !== undefined && filteredToolCalls.length > 0
260
- ? filteredToolCalls.map((tc) => {
261
- if (
262
- tc.result !== undefined &&
263
- tc.result.length > msg.maxToolResultChars!
264
- ) {
265
- wasTruncated = true;
266
- return {
267
- ...tc,
268
- result:
269
- tc.result.slice(0, msg.maxToolResultChars!) +
270
- " \u2026 [truncated]",
271
- };
272
- }
273
- return tc;
274
- })
275
- : filteredToolCalls;
276
-
277
- return {
278
- ...(m.id ? { id: m.id } : {}),
279
- role: m.role,
280
- text,
281
- timestamp: m.timestamp,
282
- ...(truncatedToolCalls.length > 0
283
- ? {
284
- toolCalls: truncatedToolCalls,
285
- toolCallsBeforeText: m.toolCallsBeforeText,
286
- }
287
- : {}),
288
- ...(attachments ? { attachments } : {}),
289
- ...(!textWasTruncated && m.textSegments.length > 0
290
- ? { textSegments: m.textSegments }
291
- : {}),
292
- ...(!textWasTruncated && m.contentOrder.length > 0
293
- ? { contentOrder: m.contentOrder }
294
- : {}),
295
- ...(filteredSurfaces.length > 0 ? { surfaces: filteredSurfaces } : {}),
296
- ...(m.subagentNotification
297
- ? { subagentNotification: m.subagentNotification }
298
- : {}),
299
- ...(wasTruncated ? { wasTruncated: true } : {}),
300
- };
301
- });
302
-
303
- const oldestTimestamp =
304
- historyMessages.length > 0 ? historyMessages[0].timestamp : undefined;
305
- // Provide the oldest message ID as a tie-breaker cursor so clients can
306
- // paginate without skipping same-millisecond messages at page boundaries.
307
- const oldestMessageId =
308
- historyMessages.length > 0 ? historyMessages[0].id : undefined;
309
-
310
- ctx.send({
311
- type: "history_response",
312
- sessionId: msg.sessionId,
313
- messages: historyMessages,
314
- hasMore,
315
- ...(oldestTimestamp !== undefined ? { oldestTimestamp } : {}),
316
- ...(oldestMessageId ? { oldestMessageId } : {}),
317
- });
318
-
319
- // Surfaces are now included directly in the history_response message (in the surfaces array),
320
- // so we no longer emit separate ui_surface_show messages during history loading.
321
- }
6
+ import { renderHistoryContent } from "./shared.js";
322
7
 
323
8
  // ---------------------------------------------------------------------------
324
9
  // Shared business logic (transport-agnostic)
@@ -400,45 +85,3 @@ export function getMessageContent(
400
85
  ...(toolCalls ? { toolCalls } : {}),
401
86
  };
402
87
  }
403
-
404
- // ---------------------------------------------------------------------------
405
- // HTTP handlers (delegate to shared logic)
406
- // ---------------------------------------------------------------------------
407
-
408
- export function handleConversationSearch(
409
- msg: ConversationSearchRequest,
410
- ctx: HandlerContext,
411
- ): void {
412
- const results = performConversationSearch({
413
- query: msg.query,
414
- limit: msg.limit,
415
- maxMessagesPerConversation: msg.maxMessagesPerConversation,
416
- });
417
- ctx.send({
418
- type: "conversation_search_response",
419
- query: msg.query,
420
- results,
421
- });
422
- }
423
-
424
- export function handleMessageContentRequest(
425
- msg: MessageContentRequest,
426
- ctx: HandlerContext,
427
- ): void {
428
- const result = getMessageContent(msg.messageId, msg.sessionId);
429
- if (!result) {
430
- ctx.send({
431
- type: "error",
432
- message: `Message ${msg.messageId} not found in session ${msg.sessionId}`,
433
- });
434
- return;
435
- }
436
-
437
- ctx.send({
438
- type: "message_content_response",
439
- sessionId: msg.sessionId,
440
- messageId: msg.messageId,
441
- ...(result.text !== undefined ? { text: result.text } : {}),
442
- ...(result.toolCalls ? { toolCalls: result.toolCalls } : {}),
443
- });
444
- }
@@ -421,8 +421,11 @@ export async function handleSessionCreate(
421
421
  pendingInteractions.resolve(requestId);
422
422
  });
423
423
  session.setHostFileProxy(fileProxy);
424
- const cuProxy = new HostCuProxy(sendEvent);
424
+ const cuProxy = new HostCuProxy(sendEvent, (requestId) => {
425
+ pendingInteractions.resolve(requestId);
426
+ });
425
427
  session.setHostCuProxy(cuProxy);
428
+ session.addPreactivatedSkillId("computer-use");
426
429
  }
427
430
  session.updateClient(sendEvent, false);
428
431
  session
@@ -575,23 +578,24 @@ export function handleCancel(msg: CancelRequest, ctx: HandlerContext): void {
575
578
  }
576
579
 
577
580
  /**
578
- * Undo the last message in a session. Returns the removed count, or null if session not found.
581
+ * Undo the last message in a session. Returns the removed count, or null if
582
+ * the conversation does not exist. Restores evicted sessions from the database.
579
583
  */
580
- export function undoLastMessage(
584
+ export async function undoLastMessage(
581
585
  sessionId: string,
582
586
  ctx: HandlerContext,
583
- ): { removedCount: number } | null {
584
- const session = ctx.sessions.get(sessionId);
585
- if (!session) {
587
+ ): Promise<{ removedCount: number } | null> {
588
+ if (!getConversation(sessionId)) {
586
589
  return null;
587
590
  }
591
+ const session = await ctx.getOrCreateSession(sessionId);
588
592
  ctx.touchSession(sessionId);
589
593
  const removedCount = session.undo();
590
594
  return { removedCount };
591
595
  }
592
596
 
593
- export function handleUndo(msg: UndoRequest, ctx: HandlerContext): void {
594
- const result = undoLastMessage(msg.sessionId, ctx);
597
+ export async function handleUndo(msg: UndoRequest, ctx: HandlerContext): Promise<void> {
598
+ const result = await undoLastMessage(msg.sessionId, ctx);
595
599
  if (!result) {
596
600
  ctx.send({ type: "error", message: "No active session" });
597
601
  return;
@@ -606,17 +610,18 @@ export function handleUndo(msg: UndoRequest, ctx: HandlerContext): void {
606
610
  /**
607
611
  * Regenerate the last assistant response for a session. The caller provides
608
612
  * a `sendEvent` callback for delivering streaming events via HTTP/SSE.
609
- * Returns null if the session is not found. Throws on regeneration errors.
613
+ * Returns null if the conversation does not exist. Restores evicted sessions
614
+ * from the database when needed. Throws on regeneration errors.
610
615
  */
611
616
  export async function regenerateResponse(
612
617
  sessionId: string,
613
618
  ctx: HandlerContext,
614
619
  sendEvent: (event: ServerMessage) => void,
615
620
  ): Promise<{ requestId: string } | null> {
616
- const session = ctx.sessions.get(sessionId);
617
- if (!session) {
621
+ if (!getConversation(sessionId)) {
618
622
  return null;
619
623
  }
624
+ const session = await ctx.getOrCreateSession(sessionId);
620
625
  ctx.touchSession(sessionId);
621
626
  session.updateClient(sendEvent, false);
622
627
  const requestId = uuid();
@@ -647,11 +652,11 @@ export async function handleRegenerate(
647
652
  msg: RegenerateRequest,
648
653
  ctx: HandlerContext,
649
654
  ): Promise<void> {
650
- const session = ctx.sessions.get(msg.sessionId);
651
- if (!session) {
655
+ if (!getConversation(msg.sessionId)) {
652
656
  ctx.send({ type: "error", message: "No active session" });
653
657
  return;
654
658
  }
659
+ const session = await ctx.getOrCreateSession(msg.sessionId);
655
660
 
656
661
  const regenerateChannel =
657
662
  parseChannelId(session.getTurnChannelContext()?.assistantMessageChannel) ??
@@ -176,22 +176,6 @@ function clampAttachmentText(text: string): string {
176
176
  return `${text.slice(0, HISTORY_ATTACHMENT_TEXT_LIMIT)}<truncated />`;
177
177
  }
178
178
 
179
- function renderImageBlockForHistory(block: Record<string, unknown>): string {
180
- const source = isRecord(block.source) ? block.source : null;
181
- const mediaType =
182
- source && typeof source.media_type === "string"
183
- ? source.media_type
184
- : "image/*";
185
- const sizeBytes =
186
- source && typeof source.data === "string"
187
- ? estimateBase64Bytes(source.data)
188
- : 0;
189
- if (sizeBytes <= 0) {
190
- return `[Image attachment] ${mediaType}`;
191
- }
192
- return `[Image attachment] ${mediaType}, ${formatBytes(sizeBytes)}`;
193
- }
194
-
195
179
  function renderFileBlockForHistory(block: Record<string, unknown>): string {
196
180
  const source = isRecord(block.source) ? block.source : null;
197
181
  const mediaType =
@@ -328,7 +312,9 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
328
312
  continue;
329
313
  }
330
314
  if (block.type === "image") {
331
- attachmentParts.push(renderImageBlockForHistory(block));
315
+ // Image data is sent as a separate attachment — skip the placeholder
316
+ // text so the client doesn't render both "[Image attachment]" and the
317
+ // actual image thumbnail.
332
318
  continue;
333
319
  }
334
320
  if (block.type === "tool_use") {
@@ -250,12 +250,20 @@ export interface SkillListItem {
250
250
  provenance: SkillProvenance;
251
251
  }
252
252
 
253
+ /** Sorting rank for provenance-based ordering: first-party first, local last. */
254
+ function provenanceSortRank(p: SkillProvenance): number {
255
+ if (p.kind === "first-party") return 0;
256
+ if (p.kind === "third-party" && p.provider) return 1;
257
+ if (p.kind === "third-party") return 2;
258
+ return 3; // local
259
+ }
260
+
253
261
  export function listSkills(_ctx: SkillOperationContext): SkillListItem[] {
254
262
  const config = getConfig();
255
263
  const catalog = loadSkillCatalog();
256
264
  const resolved = resolveSkillStates(catalog, config);
257
265
 
258
- return resolved.map((r) => ({
266
+ const items = resolved.map((r) => ({
259
267
  id: r.summary.id,
260
268
  name: r.summary.displayName,
261
269
  description: r.summary.description,
@@ -272,6 +280,17 @@ export function listSkills(_ctx: SkillOperationContext): SkillListItem[] {
272
280
  userInvocable: r.summary.userInvocable,
273
281
  provenance: resolveProvenance(r.summary),
274
282
  }));
283
+
284
+ // Sort: first-party > third-party with provider > third-party without > local,
285
+ // alphabetical by name within each tier.
286
+ items.sort((a, b) => {
287
+ const rankDiff =
288
+ provenanceSortRank(a.provenance) - provenanceSortRank(b.provenance);
289
+ if (rankDiff !== 0) return rankDiff;
290
+ return a.name.localeCompare(b.name);
291
+ });
292
+
293
+ return items;
275
294
  }
276
295
 
277
296
  export function enableSkill(
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  ContentBlock,
3
3
  Message,
4
+ ServerToolUseContent,
4
5
  ToolResultContent,
5
6
  ToolUseContent,
6
7
  } from "../providers/types.js";
@@ -20,6 +21,11 @@ export interface RepairResult {
20
21
  const SYNTHETIC_RESULT =
21
22
  "<synthesized_result>tool result missing from history</synthesized_result>";
22
23
 
24
+ const SYNTHETIC_WEB_SEARCH_ERROR = {
25
+ type: "web_search_tool_result_error",
26
+ error_code: "unavailable",
27
+ };
28
+
23
29
  export function repairHistory(messages: Message[]): RepairResult {
24
30
  const stats: RepairStats = {
25
31
  assistantToolResultsMigrated: 0,
@@ -45,12 +51,15 @@ export function repairHistory(messages: Message[]): RepairResult {
45
51
  recoveredResults = new Map();
46
52
  }
47
53
 
48
- // Strip tool_result blocks from assistant messages, preserving them
49
- // so they can be migrated to the correct user message position
54
+ // Strip client-side tool_result blocks from assistant messages,
55
+ // preserving them so they can be migrated to the correct user message.
56
+ // Server-side tools (server_tool_use / web_search_tool_result) are
57
+ // self-paired within the assistant message and must NOT be separated.
50
58
  const cleanedContent: ContentBlock[] = [];
51
59
  const newRecovered = new Map<string, ToolResultContent>();
52
60
  for (const block of msg.content) {
53
61
  if (block.type === "tool_result") {
62
+ // guard:allow-tool-result-only — only client-side tool_result belongs in recovered; web_search_tool_result stays in the assistant message
54
63
  const tr = block as ToolResultContent;
55
64
  newRecovered.set(tr.tool_use_id, tr);
56
65
  stats.assistantToolResultsMigrated++;
@@ -59,9 +68,34 @@ export function repairHistory(messages: Message[]): RepairResult {
59
68
  }
60
69
  }
61
70
 
71
+ // Ensure every server_tool_use has a paired web_search_tool_result
72
+ // in the same assistant message (handles interrupted streams)
73
+ const serverToolIds = new Set(
74
+ cleanedContent
75
+ .filter(
76
+ (b): b is ServerToolUseContent => b.type === "server_tool_use",
77
+ )
78
+ .map((b) => b.id),
79
+ );
80
+ const matchedServerIds = new Set(
81
+ cleanedContent
82
+ .filter((b) => b.type === "web_search_tool_result")
83
+ .map((b) => (b as { tool_use_id: string }).tool_use_id),
84
+ );
85
+ for (const id of serverToolIds) {
86
+ if (!matchedServerIds.has(id)) {
87
+ cleanedContent.push({
88
+ type: "web_search_tool_result",
89
+ tool_use_id: id,
90
+ content: SYNTHETIC_WEB_SEARCH_ERROR,
91
+ });
92
+ stats.missingToolResultsInserted++;
93
+ }
94
+ }
95
+
62
96
  result.push({ role: "assistant", content: cleanedContent });
63
97
 
64
- // Collect tool_use IDs from this assistant message
98
+ // Only track client-side tool_use IDs as pending (not server_tool_use)
65
99
  pendingToolUseIds = new Set(
66
100
  cleanedContent
67
101
  .filter((b): b is ToolUseContent => b.type === "tool_use")
@@ -76,14 +110,28 @@ export function repairHistory(messages: Message[]): RepairResult {
76
110
 
77
111
  for (const block of msg.content) {
78
112
  if (block.type === "tool_result") {
113
+ // guard:allow-tool-result-only — matches client-side tool_use; web_search_tool_result is handled separately below
79
114
  const tr = block as ToolResultContent;
80
115
  if (pendingToolUseIds.has(tr.tool_use_id)) {
81
116
  matchedIds.add(tr.tool_use_id);
82
117
  newContent.push(block);
83
118
  } else {
84
119
  stats.orphanToolResultsDowngraded++;
85
- newContent.push(downgradeToolResult(tr));
120
+ newContent.push(downgradeResult(tr));
86
121
  }
122
+ } else if (block.type === "web_search_tool_result") {
123
+ // web_search_tool_result in a user message is orphaned — server-side
124
+ // results belong in the assistant message, not here
125
+ stats.orphanToolResultsDowngraded++;
126
+ newContent.push(
127
+ downgradeResult(
128
+ block as {
129
+ type: "web_search_tool_result";
130
+ tool_use_id: string;
131
+ content: unknown;
132
+ },
133
+ ),
134
+ );
87
135
  } else {
88
136
  newContent.push(block);
89
137
  }
@@ -112,11 +160,21 @@ export function repairHistory(messages: Message[]): RepairResult {
112
160
  pendingToolUseIds = new Set();
113
161
  recoveredResults = new Map();
114
162
  } else {
115
- // No pending tool_use — any tool_result here is orphaned
163
+ // No pending tool_use — any tool_result/web_search_tool_result here is orphaned
116
164
  const newContent: ContentBlock[] = msg.content.map((block) => {
117
165
  if (block.type === "tool_result") {
118
166
  stats.orphanToolResultsDowngraded++;
119
- return downgradeToolResult(block as ToolResultContent);
167
+ return downgradeResult(block as ToolResultContent);
168
+ }
169
+ if (block.type === "web_search_tool_result") {
170
+ stats.orphanToolResultsDowngraded++;
171
+ return downgradeResult(
172
+ block as {
173
+ type: "web_search_tool_result";
174
+ tool_use_id: string;
175
+ content: unknown;
176
+ },
177
+ );
120
178
  }
121
179
  return block;
122
180
  });
@@ -207,9 +265,15 @@ export function deepRepairHistory(messages: Message[]): RepairResult {
207
265
  return repairHistory(merged);
208
266
  }
209
267
 
210
- function downgradeToolResult(tr: ToolResultContent): ContentBlock {
268
+ function downgradeResult(tr: {
269
+ type: string;
270
+ tool_use_id: string;
271
+ content?: unknown;
272
+ }): ContentBlock {
273
+ const content =
274
+ tr.type === "tool_result" ? tr.content : "[web search result]"; // guard:allow-tool-result-only — distinguishes content format between the two types
211
275
  return {
212
276
  type: "text",
213
- text: `[orphaned tool_result for ${tr.tool_use_id}]: ${tr.content}`,
277
+ text: `[orphaned ${tr.type} for ${tr.tool_use_id}]: ${content}`,
214
278
  };
215
279
  }