@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa

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 (295) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +26 -3
  3. package/docs/automatic-done.md +32 -0
  4. package/docs/desktop-browser-cli.md +3 -2
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
  10. package/node_modules/@vellumai/environments/src/shell.ts +22 -1
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  15. package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
  16. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
  17. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  18. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  21. package/openapi.yaml +270 -10
  22. package/package.json +1 -1
  23. package/src/__tests__/agent-loop.test.ts +76 -0
  24. package/src/__tests__/anthropic-provider.test.ts +2 -0
  25. package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
  26. package/src/__tests__/config-get-vision-flag.test.ts +1 -1
  27. package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
  28. package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
  29. package/src/__tests__/conversation-evictor.test.ts +54 -0
  30. package/src/__tests__/conversation-pairing.test.ts +112 -0
  31. package/src/__tests__/document-append-idempotency.test.ts +13 -0
  32. package/src/__tests__/document-create-dedupe.test.ts +13 -0
  33. package/src/__tests__/document-find-replace.test.ts +13 -0
  34. package/src/__tests__/document-revision-history.test.ts +390 -0
  35. package/src/__tests__/document-revision.test.ts +425 -0
  36. package/src/__tests__/document-tool-security.test.ts +13 -0
  37. package/src/__tests__/document-update-default-surface.test.ts +13 -0
  38. package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
  39. package/src/__tests__/gateway-flag-listener.test.ts +59 -6
  40. package/src/__tests__/headless-browser-navigate.test.ts +20 -30
  41. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  42. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  43. package/src/__tests__/host-shell-tool.test.ts +6 -2
  44. package/src/__tests__/identity-routes.test.ts +4 -3
  45. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +4 -4
  47. package/src/__tests__/llm-schema.test.ts +1 -1
  48. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  49. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  50. package/src/__tests__/openai-provider.test.ts +38 -11
  51. package/src/__tests__/pricing.test.ts +11 -0
  52. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  53. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  54. package/src/__tests__/skill-execute-input.test.ts +11 -0
  55. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  56. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  57. package/src/__tests__/usage-attribution.test.ts +31 -0
  58. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  59. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  60. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  61. package/src/acp/agent-process.ts +6 -1
  62. package/src/acp/client-handler.ts +6 -1
  63. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  64. package/src/agent/loop.ts +161 -17
  65. package/src/api/events/document-editor-show.ts +5 -0
  66. package/src/api/events/document-editor-update.ts +10 -0
  67. package/src/api/events/tool-result.ts +1 -0
  68. package/src/calls/__tests__/call-controller.test.ts +273 -1
  69. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  70. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  72. package/src/calls/call-constants.ts +10 -0
  73. package/src/calls/call-controller.ts +359 -25
  74. package/src/calls/channel-admission-reader.ts +15 -9
  75. package/src/calls/media-stream-server.ts +18 -0
  76. package/src/calls/media-stream-stt-session.ts +30 -9
  77. package/src/calls/twilio-provider.ts +14 -31
  78. package/src/calls/voice-session-bridge.ts +18 -1
  79. package/src/cli/commands/browser.ts +1 -2
  80. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  81. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  82. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  83. package/src/cli/lib/bundled-marketplace.json +54 -2
  84. package/src/cli/lib/inspect-plugin.ts +15 -15
  85. package/src/cli/lib/list-installed-plugins.ts +29 -28
  86. package/src/cli/lib/plugin-artifact.ts +74 -63
  87. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  88. package/src/cli/lib/plugin-details.ts +27 -42
  89. package/src/cli/lib/plugin-marketplace.ts +6 -8
  90. package/src/cli/lib/plugin-surfaces.ts +8 -7
  91. package/src/cli.ts +1 -3
  92. package/src/config/__tests__/chat-settings.test.ts +121 -0
  93. package/src/config/assistant-feature-flags.ts +28 -23
  94. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  95. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  96. package/src/config/chat-settings.ts +100 -0
  97. package/src/config/default-profile-catalog.ts +1 -1
  98. package/src/config/feature-flag-cache.ts +7 -25
  99. package/src/config/feature-flag-registry.json +9 -9
  100. package/src/config/loader.ts +5 -1
  101. package/src/config/profile-text-generation.ts +25 -21
  102. package/src/config/schemas/conversations.ts +14 -0
  103. package/src/config/schemas/llm.ts +1 -0
  104. package/src/config/schemas/notifications.ts +8 -1
  105. package/src/contacts/gateway-channel-read.ts +1 -1
  106. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  107. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  108. package/src/conversations/auto-archive-activity.ts +61 -0
  109. package/src/conversations/auto-archive.ts +201 -0
  110. package/src/daemon/computer-use-observation.test.ts +81 -0
  111. package/src/daemon/computer-use-observation.ts +23 -0
  112. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  113. package/src/daemon/conversation-agent-loop.ts +14 -6
  114. package/src/daemon/conversation-evictor.ts +50 -16
  115. package/src/daemon/conversation-messaging.ts +4 -0
  116. package/src/daemon/conversation-process.ts +15 -0
  117. package/src/daemon/conversation-queue-manager.ts +2 -0
  118. package/src/daemon/conversation.ts +2 -0
  119. package/src/daemon/doordash-steps.ts +1 -3
  120. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  121. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  122. package/src/daemon/handlers/config-a2a.ts +75 -2
  123. package/src/daemon/handlers/shared.ts +13 -2
  124. package/src/daemon/host-cu-proxy.ts +34 -3
  125. package/src/daemon/lifecycle.ts +15 -2
  126. package/src/daemon/process-message.ts +5 -0
  127. package/src/daemon/shutdown-handlers.ts +3 -0
  128. package/src/daemon/turn-finalization.ts +4 -0
  129. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  130. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  131. package/src/desktop/desktop-chrome-policy.ts +7 -1
  132. package/src/desktop/desktop-computer-use.test.ts +14 -4
  133. package/src/desktop/desktop-session-manager.ts +5 -3
  134. package/src/documents/document-revisions-store.ts +153 -0
  135. package/src/documents/document-store.ts +471 -133
  136. package/src/followups/followup-store.ts +0 -4
  137. package/src/i18n/__tests__/i18n.test.ts +25 -0
  138. package/src/i18n/assistant-text.ts +18 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/messages.ts +11 -0
  141. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  142. package/src/inbound/oauth-callback-url.ts +30 -1
  143. package/src/inbound/public-ingress-urls.ts +12 -2
  144. package/src/ipc/gateway-client.test.ts +40 -0
  145. package/src/ipc/gateway-client.ts +12 -29
  146. package/src/ipc/gateway-flag-listener.ts +7 -10
  147. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  148. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  149. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  150. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  151. package/src/live-voice/live-voice-session.ts +23 -6
  152. package/src/live-voice/protocol.ts +22 -0
  153. package/src/live-voice/share-targets.ts +225 -0
  154. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  155. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  156. package/src/mcp/client.ts +1 -19
  157. package/src/mcp/mcp-oauth-provider.ts +66 -49
  158. package/src/media/openrouter-image-service.ts +39 -22
  159. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  160. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  161. package/src/monitoring/control.ts +2 -1
  162. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  163. package/src/monitoring/oom-kill-reporter.ts +469 -0
  164. package/src/monitoring/resource-sampler.ts +30 -0
  165. package/src/notifications/README.md +53 -4
  166. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  167. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  168. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  169. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  170. package/src/notifications/adapters/platform.ts +14 -0
  171. package/src/notifications/assistant-initiated-thread.ts +101 -0
  172. package/src/notifications/broadcaster.ts +94 -16
  173. package/src/notifications/chat-reply-policy.ts +11 -0
  174. package/src/notifications/completion-policy.ts +12 -2
  175. package/src/notifications/conversation-pairing.ts +15 -70
  176. package/src/notifications/emit-signal.ts +34 -3
  177. package/src/notifications/types.ts +2 -0
  178. package/src/oauth/connect-orchestrator.ts +1 -2
  179. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  180. package/src/persistence/conversation-auto-archive.ts +170 -0
  181. package/src/persistence/conversation-crud.ts +2 -1
  182. package/src/persistence/conversation-queries.ts +15 -8
  183. package/src/persistence/embeddings/embed.test.ts +96 -0
  184. package/src/persistence/embeddings/embed.ts +20 -2
  185. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  186. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  187. package/src/persistence/embeddings/embedding-local.ts +6 -0
  188. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  189. package/src/persistence/job-utils.ts +10 -0
  190. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  191. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  192. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  193. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  194. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  195. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  196. package/src/persistence/schema/conversations.ts +1 -0
  197. package/src/persistence/schema/documents.ts +20 -0
  198. package/src/persistence/steps.ts +6 -0
  199. package/src/platform/managed-speech.ts +42 -46
  200. package/src/plugin-api/vision-support.test.ts +21 -21
  201. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  202. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  203. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  204. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  205. package/src/providers/anthropic/client.ts +1 -1
  206. package/src/providers/fireworks/client.test.ts +168 -0
  207. package/src/providers/fireworks/client.ts +24 -0
  208. package/src/providers/inference/adapter-factory.ts +6 -0
  209. package/src/providers/ionet/client.test.ts +143 -0
  210. package/src/providers/ionet/client.ts +37 -0
  211. package/src/providers/model-catalog.ts +141 -84
  212. package/src/providers/model-intents.ts +4 -4
  213. package/src/providers/openai/chat-completions-provider.ts +33 -2
  214. package/src/providers/retry.ts +56 -4
  215. package/src/providers/search-provider-catalog.ts +10 -0
  216. package/src/providers/types.ts +10 -2
  217. package/src/providers/unparseable-tool-args.ts +29 -5
  218. package/src/providers/usage-tracking.ts +5 -3
  219. package/src/runtime/assistant-event-hub.ts +23 -0
  220. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  221. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  222. package/src/runtime/channel-reply-delivery.ts +2 -0
  223. package/src/runtime/channel-retry-sweep.ts +1 -2
  224. package/src/runtime/http-types.ts +3 -0
  225. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  226. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  227. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  228. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  229. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  230. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  231. package/src/runtime/pending-interactions.ts +20 -2
  232. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  233. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  234. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  235. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  236. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  237. package/src/runtime/routes/contact-routes.ts +2 -2
  238. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  239. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  240. package/src/runtime/routes/conversation-routes.ts +7 -0
  241. package/src/runtime/routes/documents-routes.ts +185 -47
  242. package/src/runtime/routes/events-routes.ts +1 -3
  243. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  244. package/src/runtime/routes/host-bash-routes.ts +1 -0
  245. package/src/runtime/routes/host-browser-routes.ts +15 -0
  246. package/src/runtime/routes/host-cu-routes.ts +4 -1
  247. package/src/runtime/routes/host-file-routes.ts +1 -0
  248. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  249. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  250. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  251. package/src/runtime/routes/identity-routes.ts +3 -3
  252. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  253. package/src/runtime/routes/integrations/a2a.ts +4 -1
  254. package/src/runtime/routes/migration-routes.ts +3 -1
  255. package/src/runtime/routes/notification-routes.ts +1 -2
  256. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  257. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  258. package/src/runtime/verification-templates.ts +1 -2
  259. package/src/sequence/analytics.ts +0 -4
  260. package/src/sequence/engine.ts +1 -11
  261. package/src/sequence/guardrails.ts +1 -6
  262. package/src/sequence/importer.ts +0 -8
  263. package/src/sequence/store.ts +0 -10
  264. package/src/signals/user-message.ts +0 -6
  265. package/src/skills/catalog-install.ts +52 -23
  266. package/src/tools/browser/browser-execution.ts +20 -23
  267. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  268. package/src/tools/document/document-tool.ts +13 -11
  269. package/src/tools/executor.ts +1 -2
  270. package/src/tools/host-terminal/host-shell.ts +4 -2
  271. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  272. package/src/tools/network/web-fetch.ts +2 -4
  273. package/src/tools/network/web-search.ts +239 -12
  274. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  275. package/src/tools/shared/shell-output.test.ts +42 -0
  276. package/src/tools/shared/shell-output.ts +27 -4
  277. package/src/tools/skills/execute.ts +10 -7
  278. package/src/tools/terminal/shell.ts +3 -3
  279. package/src/tools/tool-approval-handler.ts +9 -1
  280. package/src/tools/ui-surface/visual-validation.ts +1 -2
  281. package/src/usage/attribution.ts +18 -1
  282. package/src/util/__tests__/oom-priority.test.ts +55 -0
  283. package/src/util/__tests__/worker-memory.test.ts +16 -0
  284. package/src/util/debounce.ts +0 -4
  285. package/src/util/oom-priority.ts +79 -0
  286. package/src/util/platform.ts +0 -2
  287. package/src/util/plugin-manifest.ts +5 -7
  288. package/src/util/pricing.ts +25 -0
  289. package/src/util/worker-memory.ts +9 -3
  290. package/src/util/worker-process.ts +8 -0
  291. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  292. package/src/workspace/hatched-date.ts +11 -4
  293. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  294. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  295. package/src/workspace/migrations/registry.ts +4 -0
@@ -13,8 +13,6 @@ import {
13
13
  } from "./store.js";
14
14
  import type { Sequence } from "./types.js";
15
15
 
16
- // ── Event tracking ──────────────────────────────────────────────────
17
-
18
16
  type SequenceEventType =
19
17
  | "send"
20
18
  | "reply"
@@ -72,8 +70,6 @@ function getRecentEvents(limit = 20): SequenceEvent[] {
72
70
  return eventLog.slice(-limit).reverse();
73
71
  }
74
72
 
75
- // ── Metrics ─────────────────────────────────────────────────────────
76
-
77
73
  interface SequenceMetrics {
78
74
  sequenceId: string;
79
75
  sequenceName: string;
@@ -105,8 +105,7 @@ async function processEnrollment(
105
105
  },
106
106
  "Sequence not active, skipping",
107
107
  );
108
- // Re-set nextStepAt so it can be picked up when the sequence resumes
109
- advanceEnrollmentToCurrentStep(enrollment, sequence);
108
+ rescheduleEnrollment(enrollment.id, Date.now() + 60_000);
110
109
  return;
111
110
  }
112
111
 
@@ -302,15 +301,6 @@ function buildStepPrompt(
302
301
  return parts.join("\n");
303
302
  }
304
303
 
305
- /** Re-schedule the enrollment for the current step (used when sequence is paused). */
306
- function advanceEnrollmentToCurrentStep(
307
- enrollment: SequenceEnrollment,
308
- _sequence: Sequence,
309
- ): void {
310
- // Re-schedule 60 seconds from now so it gets picked up after the sequence resumes
311
- rescheduleEnrollment(enrollment.id, Date.now() + 60_000);
312
- }
313
-
314
304
  /**
315
305
  * Scan conversation messages for an email thread ID returned by tool
316
306
  * invocations. Looks for common patterns like "thread_id": "..." or
@@ -12,8 +12,6 @@ import type { SequenceEnrollment } from "./types.js";
12
12
 
13
13
  const log = getLogger("sequence:guardrails");
14
14
 
15
- // ── Defaults ────────────────────────────────────────────────────────
16
-
17
15
  export interface GuardrailConfig {
18
16
  dailySendCap: number;
19
17
  perSequenceHourlyRate: number;
@@ -45,13 +43,12 @@ export function setGuardrailConfig(
45
43
  return { ...config };
46
44
  }
47
45
 
48
- // ── Send tracking (in-memory, resets on daemon restart) ─────────────
49
-
50
46
  interface SendRecord {
51
47
  sequenceId: string;
52
48
  timestamp: number;
53
49
  }
54
50
 
51
+ // In-memory; resets on assistant restart.
55
52
  const sendLog: SendRecord[] = [];
56
53
 
57
54
  export function recordSend(sequenceId: string): void {
@@ -72,8 +69,6 @@ function getSendsThisHour(sequenceId: string): number {
72
69
  ).length;
73
70
  }
74
71
 
75
- // ── Guardrail checks ────────────────────────────────────────────────
76
-
77
72
  export type GuardrailResult =
78
73
  | { ok: true }
79
74
  | { ok: false; reason: string; guardrail: string };
@@ -17,8 +17,6 @@ import { enrollContact, getSequence } from "./store.js";
17
17
 
18
18
  const log = getLogger("sequence:importer");
19
19
 
20
- // ── Types ───────────────────────────────────────────────────────────
21
-
22
20
  export interface ParsedContact {
23
21
  email: string;
24
22
  name?: string;
@@ -37,16 +35,12 @@ export interface EnrollResult {
37
35
  failed: Array<{ email: string; reason: string }>;
38
36
  }
39
37
 
40
- // ── Email validation ────────────────────────────────────────────────
41
-
42
38
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
43
39
 
44
40
  function isValidEmail(email: string): boolean {
45
41
  return EMAIL_RE.test(email);
46
42
  }
47
43
 
48
- // ── Column detection ────────────────────────────────────────────────
49
-
50
44
  // Alias lists are pre-normalized (same transform as normalizeHeader) so lookups
51
45
  // match regardless of the original casing, spacing, or punctuation in the file.
52
46
  const EMAIL_HEADERS = [
@@ -101,8 +95,6 @@ function detectColumns(headers: string[]): {
101
95
  return { emailIdx, nameIdx, firstNameIdx, lastNameIdx, contextIdxs };
102
96
  }
103
97
 
104
- // ── CSV parsing ─────────────────────────────────────────────────────
105
-
106
98
  function detectDelimiter(firstLine: string): string {
107
99
  if (firstLine.includes("\t")) {
108
100
  return "\t";
@@ -33,8 +33,6 @@ import type {
33
33
  UpdateSequenceInput,
34
34
  } from "./types.js";
35
35
 
36
- // ── Row Mappers ─────────────────────────────────────────────────────
37
-
38
36
  const parseSequenceRow = createRowMapper<
39
37
  typeof sequences.$inferSelect,
40
38
  Sequence
@@ -70,8 +68,6 @@ const parseEnrollmentRow = createRowMapper<
70
68
  updatedAt: "updatedAt",
71
69
  });
72
70
 
73
- // ── Sequence CRUD ───────────────────────────────────────────────────
74
-
75
71
  export function createSequence(input: CreateSequenceInput): Sequence {
76
72
  const db = getDb();
77
73
  const now = Date.now();
@@ -156,8 +152,6 @@ export function deleteSequence(id: string): void {
156
152
  db.delete(sequences).where(eq(sequences.id, id)).run();
157
153
  }
158
154
 
159
- // ── Enrollment CRUD ─────────────────────────────────────────────────
160
-
161
155
  export function enrollContact(input: EnrollContactInput): SequenceEnrollment {
162
156
  const db = getDb();
163
157
  const now = Date.now();
@@ -265,8 +259,6 @@ export function claimDueEnrollments(
265
259
 
266
260
  const claimed: SequenceEnrollment[] = [];
267
261
  for (const row of candidates) {
268
- // Optimistic lock: set status to 'active' (no-op value-wise) but bump updatedAt
269
- // The WHERE ensures only one claimer succeeds
270
262
  // Optimistic lock: null out nextStepAt to prevent the row from being
271
263
  // picked up by a concurrent claim. The WHERE on the old nextStepAt value
272
264
  // ensures only one claimer wins (same pattern as reminders changing
@@ -358,8 +350,6 @@ export function resumeEnrollment(id: string): void {
358
350
  .run();
359
351
  }
360
352
 
361
- // ── Query Helpers ───────────────────────────────────────────────────
362
-
363
353
  export function findActiveEnrollmentsByEmail(
364
354
  email: string,
365
355
  ): SequenceEnrollment[] {
@@ -43,8 +43,6 @@ import { getSignalsDir } from "../util/platform.js";
43
43
 
44
44
  const log = getLogger("signal:user-message");
45
45
 
46
- // ── Attachment descriptor ───────────────────────────────────────────
47
-
48
46
  /** A file-backed attachment included in a signal payload. */
49
47
  export interface SignalAttachment {
50
48
  /** Absolute path to the file on disk. */
@@ -55,8 +53,6 @@ export interface SignalAttachment {
55
53
  mimeType: string;
56
54
  }
57
55
 
58
- // ── Dispatch helper ──────────────────────────────────────────────────
59
-
60
56
  async function dispatchUserMessage(params: {
61
57
  conversationKey: string;
62
58
  content: string;
@@ -279,8 +275,6 @@ async function dispatchUserMessage(params: {
279
275
  return { accepted: true };
280
276
  }
281
277
 
282
- // ── Signal handler ───────────────────────────────────────────────────
283
-
284
278
  /**
285
279
  * Read a `signals/user-message.<requestId>` file and dispatch the message
286
280
  * through the daemon's send pipeline. Writes
@@ -14,6 +14,8 @@ import { homedir } from "node:os";
14
14
  import { dirname, join, posix, resolve, sep } from "node:path";
15
15
  import { gunzipSync } from "node:zlib";
16
16
 
17
+ import { z } from "zod";
18
+
17
19
  import {
18
20
  MALFORMED_USTAR_SIZE,
19
21
  parseUstarSizeField,
@@ -27,6 +29,7 @@ import { computeSkillHash, writeInstallMeta } from "./install-meta.js";
27
29
  import {
28
30
  isSkillCompatibleWithPlatform,
29
31
  normalizeSkillPlatforms,
32
+ SKILL_PLATFORM_VALUES,
30
33
  type SkillPlatform,
31
34
  skillPlatformUnavailableMessage,
32
35
  } from "./platform-compatibility.js";
@@ -142,21 +145,46 @@ export function getRepoSkillsDir(): string | undefined {
142
145
  * `metadata.vellum.*` shape used by the local `catalog.json`. Both shapes are
143
146
  * accepted here so the daemon has a single canonical representation downstream.
144
147
  */
145
- interface RawCatalogEntry {
146
- id?: unknown;
147
- name?: unknown;
148
- description?: unknown;
149
- icon?: unknown;
150
- emoji?: string;
151
- includes?: string[];
152
- version?: string;
153
- updatedAt?: unknown;
154
- display_name?: unknown;
155
- category?: unknown;
156
- platforms?: unknown;
157
- updated_at?: unknown;
158
- metadata?: CatalogSkill["metadata"];
159
- }
148
+ const CatalogPlatformsSchema = z
149
+ .array(z.enum(SKILL_PLATFORM_VALUES).optional().catch(undefined))
150
+ .transform((platforms) =>
151
+ platforms.filter((platform): platform is SkillPlatform => platform != null),
152
+ );
153
+ const CatalogVellumMetadataSchema = z
154
+ .object({
155
+ "display-name": z.string().optional().catch(undefined),
156
+ "activation-hints": z.array(z.string()).optional().catch(undefined),
157
+ "avoid-when": z.array(z.string()).optional().catch(undefined),
158
+ "feature-flag": z.string().optional().catch(undefined),
159
+ category: z.string().optional().catch(undefined),
160
+ platforms: CatalogPlatformsSchema.optional().catch(undefined),
161
+ })
162
+ .passthrough();
163
+ const CatalogMetadataSchema = z
164
+ .object({
165
+ icon: z.string().optional().catch(undefined),
166
+ emoji: z.string().optional().catch(undefined),
167
+ vellum: CatalogVellumMetadataSchema.optional().catch(undefined),
168
+ })
169
+ .passthrough();
170
+ const RawCatalogEntrySchema = z.object({
171
+ id: z.string().optional().catch(undefined),
172
+ name: z.string().optional().catch(undefined),
173
+ description: z.string().optional().catch(undefined),
174
+ icon: z.string().optional().catch(undefined),
175
+ emoji: z.string().optional().catch(undefined),
176
+ includes: z.array(z.string()).optional().catch(undefined),
177
+ version: z.string().optional().catch(undefined),
178
+ updatedAt: z.string().optional().catch(undefined),
179
+ display_name: z.string().optional().catch(undefined),
180
+ category: z.string().optional().catch(undefined),
181
+ platforms: CatalogPlatformsSchema.optional().catch(undefined),
182
+ updated_at: z.string().optional().catch(undefined),
183
+ metadata: CatalogMetadataSchema.optional().catch(undefined),
184
+ });
185
+ const CatalogManifestSchema = z.object({
186
+ skills: z.array(z.unknown()),
187
+ });
160
188
 
161
189
  function asStr(value: unknown): string | undefined {
162
190
  return typeof value === "string" && value.length > 0 ? value : undefined;
@@ -172,10 +200,11 @@ function asStr(value: unknown): string | undefined {
172
200
  * so a future API change to the nested shape keeps working.
173
201
  */
174
202
  function normalizeCatalogEntry(raw: unknown): CatalogSkill | null {
175
- if (typeof raw !== "object" || raw === null) {
203
+ const parsed = RawCatalogEntrySchema.safeParse(raw);
204
+ if (!parsed.success) {
176
205
  return null;
177
206
  }
178
- const entry = raw as RawCatalogEntry;
207
+ const entry = parsed.data;
179
208
 
180
209
  const id = asStr(entry.id);
181
210
  if (!id) {
@@ -227,11 +256,11 @@ export async function fetchCatalog(): Promise<CatalogSkill[]> {
227
256
  );
228
257
  }
229
258
 
230
- const manifest = (await response.json()) as { skills?: unknown };
231
- if (!Array.isArray(manifest.skills)) {
259
+ const manifest = CatalogManifestSchema.safeParse(await response.json());
260
+ if (!manifest.success) {
232
261
  throw new Error("Platform catalog has invalid skills array");
233
262
  }
234
- return manifest.skills
263
+ return manifest.data.skills
235
264
  .map((s) => normalizeCatalogEntry(s))
236
265
  .filter((s): s is CatalogSkill => s !== null);
237
266
  }
@@ -239,11 +268,11 @@ export async function fetchCatalog(): Promise<CatalogSkill[]> {
239
268
  export function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
240
269
  try {
241
270
  const raw = readFileSync(join(repoSkillsDir, "catalog.json"), "utf-8");
242
- const manifest = JSON.parse(raw) as { skills?: unknown };
243
- if (!Array.isArray(manifest.skills)) {
271
+ const manifest = CatalogManifestSchema.safeParse(JSON.parse(raw));
272
+ if (!manifest.success) {
244
273
  return [];
245
274
  }
246
- return manifest.skills
275
+ return manifest.data.skills
247
276
  .map((s) => normalizeCatalogEntry(s))
248
277
  .filter((s): s is CatalogSkill => s !== null);
249
278
  } catch {
@@ -287,8 +287,7 @@ export const EXTRACT_LINKS_EXPRESSION = `
287
287
  export function parseBrowserMode(
288
288
  input: Record<string, unknown>,
289
289
  ): { ok: true; mode: BrowserMode } | { ok: false; error: string } {
290
- const raw = input.browser_mode;
291
- const result = normalizeBrowserMode(raw);
290
+ const result = normalizeBrowserMode(input.browser_mode);
292
291
  if ("error" in result) {
293
292
  return { ok: false, error: `Error: ${result.error}` };
294
293
  }
@@ -900,7 +899,7 @@ export async function executeBrowserNavigate(
900
899
  // authoritative — otherwise a pin from an earlier navigate would
901
900
  // still capture the command and route it to the dedicated tab.
902
901
  clearPinnedTab(context.conversationId, targetClientId);
903
- cdp.setCdpSessionId?.(undefined);
902
+ cdp.setCdpSessionId?.("active");
904
903
  } else if (cdp.kind === "extension") {
905
904
  const alreadyPinned =
906
905
  getPinnedTab(context.conversationId, targetClientId) !== undefined;
@@ -921,24 +920,20 @@ export async function executeBrowserNavigate(
921
920
  ? result.clientId
922
921
  : undefined;
923
922
  if (!tabId) {
924
- // Malformed createTab response (no tabId). We're nominally falling
925
- // back to active-tab routing — but the live `cdp` instance was
926
- // already constructed with whatever pin was in scope for this
927
- // conversation, AND the pin store still holds it for future
928
- // client construction. Clear both: the pin store (so the next
929
- // executeBrowserNavigate builds a clean client) AND the current
930
- // cdp instance's session (so the Page.navigate that runs in a
931
- // few lines targets the active tab rather than the stale pin).
932
- // Without the setCdpSessionId(undefined) call, the warn message
933
- // is a lie: navigation would still route to the dead tab via the
934
- // already-injected cdpSessionId and likely fail with
935
- // cdp_session_not_found.
936
923
  clearPinnedTab(context.conversationId, targetClientId);
937
- cdp.setCdpSessionId?.(undefined);
938
- log.warn(
939
- { conversationId: context.conversationId, result },
940
- "Vellum.createTab returned no tabId; cleared stale pin and live session, falling back to active-tab routing",
941
- );
924
+ try {
925
+ cdp.dispose();
926
+ } catch (disposeErr) {
927
+ log.warn(
928
+ { conversationId: context.conversationId, err: disposeErr },
929
+ "Failed to dispose CDP client after Vellum.createTab returned no tabId",
930
+ );
931
+ }
932
+ return {
933
+ content:
934
+ "Error: Failed to open a new tab for navigation: Vellum.createTab returned no tabId. Pass --use-active-tab to navigate the currently-active tab instead.",
935
+ isError: true,
936
+ };
942
937
  } else {
943
938
  cdp.setCdpSessionId?.(tabId);
944
939
  setPinnedTab(context.conversationId, tabId, clientId);
@@ -2672,9 +2667,11 @@ async function checkExtensionModeStatus(
2672
2667
  context: ToolContext,
2673
2668
  autoCandidate: boolean,
2674
2669
  ): Promise<BrowserStatusModeResult> {
2675
- const proxy = HostBrowserProxy.instance;
2676
-
2677
- if (!proxy.hasExtensionClient(context.sourceActorPrincipalId)) {
2670
+ if (
2671
+ !HostBrowserProxy.instance.hasExtensionClient(
2672
+ context.sourceActorPrincipalId,
2673
+ )
2674
+ ) {
2678
2675
  return {
2679
2676
  mode: BROWSER_STATUS_MODE.EXTENSION,
2680
2677
  available: false,
@@ -465,7 +465,7 @@ function createTransport(ws: WsLike): CdpWsTransport {
465
465
  ws.addEventListener("close", handleClose);
466
466
  ws.addEventListener("error", handleError);
467
467
 
468
- const transport: CdpWsTransport = {
468
+ return {
469
469
  get closed() {
470
470
  return disposed || closed;
471
471
  },
@@ -607,6 +607,4 @@ function createTransport(ws: WsLike): CdpWsTransport {
607
607
  }
608
608
  },
609
609
  };
610
-
611
- return transport;
612
610
  }
@@ -158,6 +158,7 @@ export function executeDocumentOpen(
158
158
  surfaceId: doc.surfaceId,
159
159
  title: doc.title,
160
160
  initialContent: doc.content,
161
+ revision: doc.revision,
161
162
  });
162
163
 
163
164
  context.sendToClient({
@@ -244,6 +245,7 @@ function maybeReuseEmptyDocument(
244
245
  surfaceId,
245
246
  markdown: initialContent,
246
247
  mode: "replace",
248
+ revision: update.revision,
247
249
  });
248
250
 
249
251
  context.sendToClient({
@@ -304,7 +306,7 @@ export function executeDocumentCreate(
304
306
  isError: true,
305
307
  };
306
308
  }
307
- const { surfaceId } = created;
309
+ const { surfaceId, revision } = created;
308
310
 
309
311
  // Send document_editor_show message to open the built-in RTE
310
312
  if (context.sendToClient) {
@@ -314,6 +316,7 @@ export function executeDocumentCreate(
314
316
  surfaceId,
315
317
  title,
316
318
  initialContent,
319
+ revision,
317
320
  });
318
321
 
319
322
  context.sendToClient({
@@ -441,6 +444,7 @@ export function executeDocumentUpdate(
441
444
  surfaceId,
442
445
  markdown: applied,
443
446
  mode,
447
+ revision: result.revision,
444
448
  });
445
449
 
446
450
  return {
@@ -684,16 +688,14 @@ export function executeDocumentReplaceText(
684
688
  }
685
689
 
686
690
  if (context.sendToClient && result.content_changed) {
687
- const doc = getDocumentById(surfaceId);
688
- if (doc) {
689
- context.sendToClient({
690
- type: "document_editor_update",
691
- conversationId: context.conversationId,
692
- surfaceId,
693
- markdown: doc.content,
694
- mode: "replace",
695
- });
696
- }
691
+ context.sendToClient({
692
+ type: "document_editor_update",
693
+ conversationId: context.conversationId,
694
+ surfaceId,
695
+ markdown: result.content,
696
+ mode: "replace",
697
+ revision: result.revision,
698
+ });
697
699
  }
698
700
 
699
701
  return {
@@ -482,8 +482,7 @@ export function computePerToolTimeoutMs(
482
482
  const { questionResponseTimeoutSec } = getConfig().timeouts;
483
483
  return (questionResponseTimeoutSec + 5) * 1000;
484
484
  }
485
- const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
486
- return safeTimeoutMs(rawTimeoutSec);
485
+ return safeTimeoutMs(getConfig().timeouts.toolExecutionTimeoutSec);
487
486
  }
488
487
 
489
488
  /**
@@ -486,7 +486,7 @@ export const hostShellTool = {
486
486
  // Only the first handler to fire should wake the agent.
487
487
  let completed = false;
488
488
 
489
- child.on("close", (code) => {
489
+ child.on("close", (code, signal) => {
490
490
  if (completed) {
491
491
  return;
492
492
  }
@@ -494,6 +494,7 @@ export const hostShellTool = {
494
494
  clearTimeout(timer);
495
495
  const result = collector.format(code, timedOut, timeoutSec, {
496
496
  started: launch.didStart(),
497
+ signal: aborted ? null : signal,
497
498
  });
498
499
  // Cancel takes precedence over the SIGKILL-induced error result.
499
500
  const status = aborted
@@ -667,12 +668,13 @@ export const hostShellTool = {
667
668
  }
668
669
  }
669
670
 
670
- child.on("close", (code) => {
671
+ child.on("close", (code, signal) => {
671
672
  clearTimeout(timer);
672
673
  context.signal?.removeEventListener("abort", onAbort);
673
674
 
674
675
  const result = collector.format(code, timedOut, timeoutSec, {
675
676
  started: launch.didStart(),
677
+ signal: context.signal?.aborted ? null : signal,
676
678
  });
677
679
 
678
680
  resolve({
@@ -13,6 +13,7 @@ let mockFastcrwSecureKey: string | undefined;
13
13
  let mockSearxngSecureKey: string | undefined;
14
14
  let mockTinyfishSecureKey: string | undefined;
15
15
  let mockExaSecureKey: string | undefined;
16
+ let mockSerplySecureKey: string | undefined;
16
17
  let mockManagedSearchProxyResult: any;
17
18
  let mockManagedSearchAvailable = true;
18
19
  let mockManagedSearchProxyCalls: Array<{
@@ -69,6 +70,9 @@ mock.module("../../../security/secure-keys.js", () => ({
69
70
  if (provider === "exa") {
70
71
  return mockExaSecureKey;
71
72
  }
73
+ if (provider === "serply") {
74
+ return mockSerplySecureKey;
75
+ }
72
76
  return undefined;
73
77
  },
74
78
  }));
@@ -116,6 +120,7 @@ describe("web_search tool", () => {
116
120
  mockSearxngSecureKey = undefined;
117
121
  mockTinyfishSecureKey = undefined;
118
122
  mockExaSecureKey = undefined;
123
+ mockSerplySecureKey = undefined;
119
124
  mockManagedSearchProxyCalls = [];
120
125
  mockManagedSearchAvailable = true;
121
126
  mockManagedSearchProxyResult = {
@@ -1441,6 +1446,106 @@ describe("web_search tool", () => {
1441
1446
  expect(result.activityMetadata?.webSearch?.provider).toBe("exa");
1442
1447
  });
1443
1448
 
1449
+ // ---- Serply provider ---------------------------------------------------
1450
+
1451
+ test("Serply search sends the query form request and maps freshness", async () => {
1452
+ seedWebSearch("your-own", "serply");
1453
+ mockSerplySecureKey = "serply_test";
1454
+ let capturedUrl = "";
1455
+ let capturedInit: RequestInit | undefined;
1456
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
1457
+ capturedUrl = url;
1458
+ capturedInit = init;
1459
+ return new Response(
1460
+ JSON.stringify({
1461
+ query: "fresh tools",
1462
+ total: 1,
1463
+ results: [
1464
+ {
1465
+ title: "Serply Result",
1466
+ link: "https://example.com/serply",
1467
+ description: "Fresh from Serply",
1468
+ position: 1,
1469
+ },
1470
+ ],
1471
+ }),
1472
+ { status: 200, headers: { "content-type": "application/json" } },
1473
+ );
1474
+ }) as any;
1475
+
1476
+ const result = await execute({
1477
+ query: "fresh tools",
1478
+ freshness: "pw",
1479
+ count: 5,
1480
+ });
1481
+
1482
+ expect(result.isError).toBe(false);
1483
+ const parsed = new URL(capturedUrl);
1484
+ expect(parsed.origin + parsed.pathname).toBe(
1485
+ "https://api.serply.io/v1/search",
1486
+ );
1487
+ expect(parsed.searchParams.get("q")).toBe("fresh tools");
1488
+ expect(parsed.searchParams.get("num")).toBe("5");
1489
+ expect(parsed.searchParams.get("tbs")).toBe("qdr:w");
1490
+ expect(capturedInit?.method).toBe("GET");
1491
+ const headers = new Headers(capturedInit?.headers);
1492
+ expect(headers.get("x-api-key")).toBe("serply_test");
1493
+ expect(headers.get("user-agent")).toBe("vellum-assistant");
1494
+ expect(result.content).toContain("Serply Result");
1495
+ expect(result.content).toContain("Fresh from Serply");
1496
+ expect(result.activityMetadata?.webSearch?.provider).toBe("serply");
1497
+ });
1498
+
1499
+ test("Serply omits tbs without freshness, clamps num, and trims to count", async () => {
1500
+ seedWebSearch("your-own", "serply");
1501
+ mockSerplySecureKey = "serply_test";
1502
+ let capturedUrl = "";
1503
+ globalThis.fetch = (async (url: string) => {
1504
+ capturedUrl = url;
1505
+ return new Response(
1506
+ JSON.stringify({
1507
+ results: [
1508
+ { title: "A", link: "https://a.example.com", description: "a" },
1509
+ { title: "B", link: "https://b.example.com", description: "b" },
1510
+ { title: "C", link: "https://c.example.com", description: "c" },
1511
+ ],
1512
+ }),
1513
+ { status: 200, headers: { "content-type": "application/json" } },
1514
+ );
1515
+ }) as any;
1516
+
1517
+ const result = await execute({ query: "test", count: 2 });
1518
+
1519
+ const parsed = new URL(capturedUrl);
1520
+ expect(parsed.searchParams.get("tbs")).toBeNull();
1521
+ expect(parsed.searchParams.get("num")).toBe("2");
1522
+ expect(result.content).toContain("A");
1523
+ expect(result.content).toContain("B");
1524
+ expect(result.content).not.toContain("https://c.example.com");
1525
+ expect(result.activityMetadata?.webSearch?.resultCount).toBe(2);
1526
+
1527
+ const clamped = await execute({ query: "test", count: 20 });
1528
+ expect(new URL(capturedUrl).searchParams.get("num")).toBe("10");
1529
+ expect(clamped.isError).toBe(false);
1530
+ });
1531
+
1532
+ test.each([401, 402, 403])(
1533
+ "Serply handles %d access error",
1534
+ async (status) => {
1535
+ seedWebSearch("your-own", "serply");
1536
+ mockSerplySecureKey = "serply_test";
1537
+ globalThis.fetch = (async () =>
1538
+ new Response(JSON.stringify({ detail: "Invalid API key" }), {
1539
+ status,
1540
+ })) as any;
1541
+
1542
+ const result = await execute({ query: "test" });
1543
+
1544
+ expect(result.isError).toBe(true);
1545
+ expect(result.activityMetadata?.webSearch?.provider).toBe("serply");
1546
+ },
1547
+ );
1548
+
1444
1549
  // ---- Provider fallback --------------------------------------------------
1445
1550
 
1446
1551
  test("falls back from perplexity to brave when perplexity has no key", async () => {
@@ -1530,9 +1530,8 @@ export async function executeFastcrwScrape(
1530
1530
  input: Record<string, unknown>,
1531
1531
  options: { apiKey: string; signal?: AbortSignal },
1532
1532
  ): Promise<ToolExecutionResult> {
1533
- const apiBase = getConfig().services["web-fetch"]?.apiBase;
1534
1533
  const endpoint = resolveProviderApiUrl(
1535
- apiBase,
1534
+ getConfig().services["web-fetch"]?.apiBase,
1536
1535
  FASTCRW_SCRAPE_PATH,
1537
1536
  FASTCRW_DEFAULT_API_BASE,
1538
1537
  );
@@ -1587,9 +1586,8 @@ export async function executeTinyfishFetch(
1587
1586
  1,
1588
1587
  MAX_TIMEOUT_SECONDS,
1589
1588
  );
1590
- const apiBase = getConfig().services["web-fetch"]?.apiBase;
1591
1589
  const endpoint = resolveProviderApiUrl(
1592
- apiBase,
1590
+ getConfig().services["web-fetch"]?.apiBase,
1593
1591
  "/",
1594
1592
  TINYFISH_DEFAULT_FETCH_API_BASE,
1595
1593
  );