@poolzin/pool-bot 2026.2.0 → 2026.2.1

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 (230) hide show
  1. package/dist/agents/bash-tools.exec.js +76 -25
  2. package/dist/agents/cli-runner/helpers.js +9 -11
  3. package/dist/agents/identity.js +47 -7
  4. package/dist/agents/memory-search.js +25 -8
  5. package/dist/agents/model-selection.js +21 -0
  6. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  7. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  8. package/dist/agents/pi-embedded-helpers.js +1 -1
  9. package/dist/agents/pi-embedded-runner/compact.js +1 -0
  10. package/dist/agents/pi-embedded-runner/model.js +61 -2
  11. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  12. package/dist/agents/pi-embedded-runner/run.js +199 -46
  13. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  14. package/dist/agents/pi-embedded-subscribe.js +118 -29
  15. package/dist/agents/pi-tools.js +10 -5
  16. package/dist/agents/poolbot-tools.js +15 -10
  17. package/dist/agents/sandbox-paths.js +31 -0
  18. package/dist/agents/session-tool-result-guard.js +94 -15
  19. package/dist/agents/shell-utils.js +51 -0
  20. package/dist/agents/skills/bundled-context.js +23 -0
  21. package/dist/agents/skills/bundled-dir.js +41 -7
  22. package/dist/agents/skills-install.js +60 -23
  23. package/dist/agents/subagent-announce.js +79 -34
  24. package/dist/agents/tool-policy.conformance.js +14 -0
  25. package/dist/agents/tool-policy.js +24 -0
  26. package/dist/agents/tools/cron-tool.js +166 -19
  27. package/dist/agents/tools/discord-actions-presence.js +78 -0
  28. package/dist/agents/tools/message-tool.js +56 -2
  29. package/dist/agents/tools/sessions-history-tool.js +69 -1
  30. package/dist/agents/tools/web-search.js +211 -42
  31. package/dist/agents/usage.js +23 -1
  32. package/dist/agents/workspace-run.js +67 -0
  33. package/dist/agents/workspace-templates.js +44 -0
  34. package/dist/auto-reply/command-auth.js +121 -6
  35. package/dist/auto-reply/envelope.js +50 -72
  36. package/dist/auto-reply/reply/commands-compact.js +1 -0
  37. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  38. package/dist/auto-reply/reply/commands-context.js +1 -0
  39. package/dist/auto-reply/reply/commands-models.js +107 -60
  40. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  41. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  42. package/dist/auto-reply/reply/inbound-context.js +5 -1
  43. package/dist/auto-reply/reply/model-selection.js +3 -3
  44. package/dist/auto-reply/thinking.js +88 -43
  45. package/dist/browser/bridge-server.js +13 -0
  46. package/dist/browser/cdp.helpers.js +38 -24
  47. package/dist/browser/client-fetch.js +50 -7
  48. package/dist/browser/config.js +1 -10
  49. package/dist/browser/extension-relay.js +101 -40
  50. package/dist/browser/pw-ai.js +1 -1
  51. package/dist/browser/pw-session.js +143 -8
  52. package/dist/browser/pw-tools-core.interactions.js +125 -27
  53. package/dist/browser/pw-tools-core.responses.js +1 -1
  54. package/dist/browser/pw-tools-core.state.js +1 -1
  55. package/dist/browser/routes/agent.act.js +86 -41
  56. package/dist/browser/routes/dispatcher.js +4 -4
  57. package/dist/browser/screenshot.js +1 -1
  58. package/dist/browser/server.js +13 -0
  59. package/dist/build-info.json +3 -3
  60. package/dist/channels/reply-prefix.js +8 -1
  61. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  62. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  63. package/dist/cli/cron-cli/shared.js +56 -41
  64. package/dist/cli/dns-cli.js +26 -14
  65. package/dist/cli/gateway-cli/register.js +37 -19
  66. package/dist/cli/memory-cli.js +5 -5
  67. package/dist/cli/parse-bytes.js +37 -0
  68. package/dist/cli/update-cli.js +173 -52
  69. package/dist/commands/agent.js +1 -0
  70. package/dist/commands/doctor-config-flow.js +61 -5
  71. package/dist/commands/doctor-state-migrations.js +1 -1
  72. package/dist/commands/health.js +1 -1
  73. package/dist/commands/model-allowlist.js +29 -0
  74. package/dist/commands/model-picker.js +2 -1
  75. package/dist/commands/models/list.status-command.js +43 -23
  76. package/dist/commands/models/shared.js +15 -0
  77. package/dist/commands/onboard-custom.js +384 -0
  78. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  79. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  80. package/dist/commands/onboard-skills.js +63 -38
  81. package/dist/commands/openai-model-default.js +41 -0
  82. package/dist/config/defaults.js +3 -2
  83. package/dist/config/paths.js +136 -35
  84. package/dist/config/plugin-auto-enable.js +21 -5
  85. package/dist/config/redact-snapshot.js +153 -0
  86. package/dist/config/schema.field-metadata.js +590 -0
  87. package/dist/config/schema.js +2 -2
  88. package/dist/config/sessions/store.js +291 -23
  89. package/dist/config/zod-schema.agent-defaults.js +3 -0
  90. package/dist/config/zod-schema.agent-runtime.js +13 -2
  91. package/dist/config/zod-schema.providers-core.js +142 -0
  92. package/dist/config/zod-schema.session.js +3 -0
  93. package/dist/cron/delivery.js +57 -0
  94. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  95. package/dist/cron/isolated-agent/helpers.js +22 -5
  96. package/dist/cron/isolated-agent/run.js +171 -63
  97. package/dist/cron/isolated-agent/session.js +2 -0
  98. package/dist/cron/normalize.js +356 -28
  99. package/dist/cron/parse.js +10 -5
  100. package/dist/cron/run-log.js +35 -10
  101. package/dist/cron/schedule.js +41 -6
  102. package/dist/cron/service/jobs.js +208 -35
  103. package/dist/cron/service/ops.js +72 -16
  104. package/dist/cron/service/state.js +2 -0
  105. package/dist/cron/service/store.js +386 -14
  106. package/dist/cron/service/timer.js +390 -147
  107. package/dist/cron/session-reaper.js +86 -0
  108. package/dist/cron/store.js +23 -8
  109. package/dist/cron/validate-timestamp.js +43 -0
  110. package/dist/discord/monitor/agent-components.js +438 -0
  111. package/dist/discord/monitor/allow-list.js +28 -5
  112. package/dist/discord/monitor/gateway-registry.js +29 -0
  113. package/dist/discord/monitor/native-command.js +44 -23
  114. package/dist/discord/monitor/sender-identity.js +45 -0
  115. package/dist/discord/pluralkit.js +27 -0
  116. package/dist/discord/send.outbound.js +92 -5
  117. package/dist/discord/send.shared.js +60 -23
  118. package/dist/discord/targets.js +84 -1
  119. package/dist/entry.js +15 -9
  120. package/dist/extensionAPI.js +8 -0
  121. package/dist/gateway/control-ui.js +8 -1
  122. package/dist/gateway/hooks-mapping.js +3 -0
  123. package/dist/gateway/hooks.js +65 -0
  124. package/dist/gateway/net.js +96 -31
  125. package/dist/gateway/node-command-policy.js +50 -15
  126. package/dist/gateway/origin-check.js +56 -0
  127. package/dist/gateway/protocol/client-info.js +9 -0
  128. package/dist/gateway/protocol/index.js +9 -2
  129. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  130. package/dist/gateway/protocol/schema/cron.js +22 -10
  131. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  132. package/dist/gateway/protocol/schema/sessions.js +12 -0
  133. package/dist/gateway/server/hooks.js +1 -1
  134. package/dist/gateway/server-broadcast.js +26 -9
  135. package/dist/gateway/server-chat.js +112 -23
  136. package/dist/gateway/server-discovery-runtime.js +10 -2
  137. package/dist/gateway/server-http.js +109 -11
  138. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  139. package/dist/gateway/server-methods/agents.js +321 -2
  140. package/dist/gateway/server-methods/usage.js +559 -16
  141. package/dist/gateway/server-runtime-state.js +22 -8
  142. package/dist/gateway/server-startup-memory.js +16 -0
  143. package/dist/gateway/server.impl.js +5 -1
  144. package/dist/gateway/session-utils.fs.js +23 -25
  145. package/dist/gateway/session-utils.js +20 -10
  146. package/dist/gateway/sessions-patch.js +7 -22
  147. package/dist/gateway/test-helpers.server.js +35 -2
  148. package/dist/imessage/constants.js +2 -0
  149. package/dist/imessage/monitor/deliver.js +4 -1
  150. package/dist/imessage/monitor/monitor-provider.js +51 -1
  151. package/dist/infra/bonjour-discovery.js +131 -70
  152. package/dist/infra/control-ui-assets.js +134 -12
  153. package/dist/infra/errors.js +12 -0
  154. package/dist/infra/exec-approvals.js +266 -57
  155. package/dist/infra/format-time/format-datetime.js +79 -0
  156. package/dist/infra/format-time/format-duration.js +81 -0
  157. package/dist/infra/format-time/format-relative.js +80 -0
  158. package/dist/infra/heartbeat-runner.js +140 -49
  159. package/dist/infra/home-dir.js +54 -0
  160. package/dist/infra/net/fetch-guard.js +122 -0
  161. package/dist/infra/net/ssrf.js +65 -29
  162. package/dist/infra/outbound/abort.js +14 -0
  163. package/dist/infra/outbound/message-action-runner.js +77 -13
  164. package/dist/infra/outbound/outbound-session.js +143 -37
  165. package/dist/infra/poolbot-root.js +43 -1
  166. package/dist/infra/session-cost-usage.js +631 -41
  167. package/dist/infra/state-migrations.js +317 -47
  168. package/dist/infra/update-global.js +35 -0
  169. package/dist/infra/update-runner.js +149 -43
  170. package/dist/infra/warning-filter.js +65 -0
  171. package/dist/infra/widearea-dns.js +30 -9
  172. package/dist/logging/redact-identifier.js +12 -0
  173. package/dist/media/fetch.js +81 -58
  174. package/dist/media-understanding/apply.js +403 -3
  175. package/dist/media-understanding/attachments.js +38 -27
  176. package/dist/media-understanding/defaults.js +16 -0
  177. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  178. package/dist/media-understanding/providers/google/audio.js +24 -17
  179. package/dist/media-understanding/providers/google/video.js +24 -17
  180. package/dist/media-understanding/providers/image.js +2 -2
  181. package/dist/media-understanding/providers/index.js +4 -1
  182. package/dist/media-understanding/providers/openai/audio.js +22 -14
  183. package/dist/media-understanding/providers/shared.js +16 -11
  184. package/dist/media-understanding/providers/zai/index.js +6 -0
  185. package/dist/media-understanding/runner.js +158 -90
  186. package/dist/memory/batch-voyage.js +277 -0
  187. package/dist/memory/embeddings-voyage.js +75 -0
  188. package/dist/memory/embeddings.js +28 -16
  189. package/dist/memory/internal.js +101 -18
  190. package/dist/memory/manager.js +154 -48
  191. package/dist/memory/search-manager.js +173 -0
  192. package/dist/memory/session-files.js +9 -3
  193. package/dist/node-host/runner.js +34 -24
  194. package/dist/node-host/with-timeout.js +27 -0
  195. package/dist/plugins/commands.js +5 -1
  196. package/dist/plugins/config-state.js +86 -7
  197. package/dist/plugins/source-display.js +51 -0
  198. package/dist/process/exec.js +20 -2
  199. package/dist/routing/resolve-route.js +12 -0
  200. package/dist/routing/session-key.js +15 -0
  201. package/dist/runtime.js +2 -0
  202. package/dist/security/audit-extra.async.js +601 -0
  203. package/dist/security/audit-extra.js +2 -830
  204. package/dist/security/audit-extra.sync.js +505 -0
  205. package/dist/security/channel-metadata.js +34 -0
  206. package/dist/security/external-content.js +88 -6
  207. package/dist/security/skill-scanner.js +330 -0
  208. package/dist/sessions/session-key-utils.js +7 -0
  209. package/dist/signal/monitor/event-handler.js +80 -1
  210. package/dist/slack/monitor/media.js +85 -15
  211. package/dist/tailscale/detect.js +1 -2
  212. package/dist/telegram/bot/helpers.js +109 -28
  213. package/dist/telegram/bot-handlers.js +144 -3
  214. package/dist/telegram/bot-message-context.js +37 -10
  215. package/dist/telegram/bot-message-dispatch.js +48 -15
  216. package/dist/telegram/bot-native-commands.js +86 -29
  217. package/dist/telegram/bot.js +30 -29
  218. package/dist/telegram/model-buttons.js +163 -0
  219. package/dist/telegram/monitor.js +110 -85
  220. package/dist/telegram/send.js +129 -47
  221. package/dist/terminal/restore.js +45 -0
  222. package/dist/test-helpers/state-dir-env.js +16 -0
  223. package/dist/tts/tts.js +12 -6
  224. package/dist/tui/tui-session-actions.js +166 -54
  225. package/dist/utils/fetch-timeout.js +20 -0
  226. package/dist/utils/normalize-secret-input.js +19 -0
  227. package/dist/utils/transcript-tools.js +58 -0
  228. package/dist/utils.js +45 -14
  229. package/dist/version.js +42 -5
  230. package/package.json +1 -1
@@ -0,0 +1,277 @@
1
+ import { createInterface } from "node:readline";
2
+ import { Readable } from "node:stream";
3
+ import { retryAsync } from "../infra/retry.js";
4
+ import { hashText, runWithConcurrency } from "./internal.js";
5
+ export const VOYAGE_BATCH_ENDPOINT = "/v1/embeddings";
6
+ const VOYAGE_BATCH_COMPLETION_WINDOW = "12h";
7
+ const VOYAGE_BATCH_MAX_REQUESTS = 50000;
8
+ function getVoyageBaseUrl(client) {
9
+ return client.baseUrl?.replace(/\/$/, "") ?? "";
10
+ }
11
+ function getVoyageHeaders(client, params) {
12
+ const headers = client.headers ? { ...client.headers } : {};
13
+ if (params.json) {
14
+ if (!headers["Content-Type"] && !headers["content-type"]) {
15
+ headers["Content-Type"] = "application/json";
16
+ }
17
+ }
18
+ else {
19
+ delete headers["Content-Type"];
20
+ delete headers["content-type"];
21
+ }
22
+ return headers;
23
+ }
24
+ function splitVoyageBatchRequests(requests) {
25
+ if (requests.length <= VOYAGE_BATCH_MAX_REQUESTS) {
26
+ return [requests];
27
+ }
28
+ const groups = [];
29
+ for (let i = 0; i < requests.length; i += VOYAGE_BATCH_MAX_REQUESTS) {
30
+ groups.push(requests.slice(i, i + VOYAGE_BATCH_MAX_REQUESTS));
31
+ }
32
+ return groups;
33
+ }
34
+ async function submitVoyageBatch(params) {
35
+ const baseUrl = getVoyageBaseUrl(params.client);
36
+ const jsonl = params.requests.map((request) => JSON.stringify(request)).join("\n");
37
+ const form = new FormData();
38
+ form.append("purpose", "batch");
39
+ form.append("file", new Blob([jsonl], { type: "application/jsonl" }), `memory-embeddings.${hashText(String(Date.now()))}.jsonl`);
40
+ // 1. Upload file using Voyage Files API
41
+ const fileRes = await fetch(`${baseUrl}/files`, {
42
+ method: "POST",
43
+ headers: getVoyageHeaders(params.client, { json: false }),
44
+ body: form,
45
+ });
46
+ if (!fileRes.ok) {
47
+ const text = await fileRes.text();
48
+ throw new Error(`voyage batch file upload failed: ${fileRes.status} ${text}`);
49
+ }
50
+ const filePayload = (await fileRes.json());
51
+ if (!filePayload.id) {
52
+ throw new Error("voyage batch file upload failed: missing file id");
53
+ }
54
+ // 2. Create batch job using Voyage Batches API
55
+ const batchRes = await retryAsync(async () => {
56
+ const res = await fetch(`${baseUrl}/batches`, {
57
+ method: "POST",
58
+ headers: getVoyageHeaders(params.client, { json: true }),
59
+ body: JSON.stringify({
60
+ input_file_id: filePayload.id,
61
+ endpoint: VOYAGE_BATCH_ENDPOINT,
62
+ completion_window: VOYAGE_BATCH_COMPLETION_WINDOW,
63
+ request_params: {
64
+ model: params.client.model,
65
+ input_type: "document",
66
+ },
67
+ metadata: {
68
+ source: "clawdbot-memory",
69
+ agent: params.agentId,
70
+ },
71
+ }),
72
+ });
73
+ if (!res.ok) {
74
+ const text = await res.text();
75
+ const err = new Error(`voyage batch create failed: ${res.status} ${text}`);
76
+ err.status = res.status;
77
+ throw err;
78
+ }
79
+ return res;
80
+ }, {
81
+ attempts: 3,
82
+ minDelayMs: 300,
83
+ maxDelayMs: 2000,
84
+ jitter: 0.2,
85
+ shouldRetry: (err) => {
86
+ const status = err.status;
87
+ return status === 429 || (typeof status === "number" && status >= 500);
88
+ },
89
+ });
90
+ return (await batchRes.json());
91
+ }
92
+ async function fetchVoyageBatchStatus(params) {
93
+ const baseUrl = getVoyageBaseUrl(params.client);
94
+ const res = await fetch(`${baseUrl}/batches/${params.batchId}`, {
95
+ headers: getVoyageHeaders(params.client, { json: true }),
96
+ });
97
+ if (!res.ok) {
98
+ const text = await res.text();
99
+ throw new Error(`voyage batch status failed: ${res.status} ${text}`);
100
+ }
101
+ return (await res.json());
102
+ }
103
+ async function readVoyageBatchError(params) {
104
+ try {
105
+ const baseUrl = getVoyageBaseUrl(params.client);
106
+ const res = await fetch(`${baseUrl}/files/${params.errorFileId}/content`, {
107
+ headers: getVoyageHeaders(params.client, { json: true }),
108
+ });
109
+ if (!res.ok) {
110
+ const text = await res.text();
111
+ throw new Error(`voyage batch error file content failed: ${res.status} ${text}`);
112
+ }
113
+ const text = await res.text();
114
+ if (!text.trim()) {
115
+ return undefined;
116
+ }
117
+ const lines = text
118
+ .split("\n")
119
+ .map((line) => line.trim())
120
+ .filter(Boolean)
121
+ .map((line) => JSON.parse(line));
122
+ const first = lines.find((line) => line.error?.message || line.response?.body?.error);
123
+ const message = first?.error?.message ??
124
+ (typeof first?.response?.body?.error?.message === "string"
125
+ ? first?.response?.body?.error?.message
126
+ : undefined);
127
+ return message;
128
+ }
129
+ catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ return message ? `error file unavailable: ${message}` : undefined;
132
+ }
133
+ }
134
+ async function waitForVoyageBatch(params) {
135
+ const start = Date.now();
136
+ let current = params.initial;
137
+ while (true) {
138
+ const status = current ??
139
+ (await fetchVoyageBatchStatus({
140
+ client: params.client,
141
+ batchId: params.batchId,
142
+ }));
143
+ const state = status.status ?? "unknown";
144
+ if (state === "completed") {
145
+ if (!status.output_file_id) {
146
+ throw new Error(`voyage batch ${params.batchId} completed without output file`);
147
+ }
148
+ return {
149
+ outputFileId: status.output_file_id,
150
+ errorFileId: status.error_file_id ?? undefined,
151
+ };
152
+ }
153
+ if (["failed", "expired", "cancelled", "canceled"].includes(state)) {
154
+ const detail = status.error_file_id
155
+ ? await readVoyageBatchError({ client: params.client, errorFileId: status.error_file_id })
156
+ : undefined;
157
+ const suffix = detail ? `: ${detail}` : "";
158
+ throw new Error(`voyage batch ${params.batchId} ${state}${suffix}`);
159
+ }
160
+ if (!params.wait) {
161
+ throw new Error(`voyage batch ${params.batchId} still ${state}; wait disabled`);
162
+ }
163
+ if (Date.now() - start > params.timeoutMs) {
164
+ throw new Error(`voyage batch ${params.batchId} timed out after ${params.timeoutMs}ms`);
165
+ }
166
+ params.debug?.(`voyage batch ${params.batchId} ${state}; waiting ${params.pollIntervalMs}ms`);
167
+ await new Promise((resolve) => setTimeout(resolve, params.pollIntervalMs));
168
+ current = undefined;
169
+ }
170
+ }
171
+ export async function runVoyageEmbeddingBatches(params) {
172
+ if (params.requests.length === 0) {
173
+ return new Map();
174
+ }
175
+ const groups = splitVoyageBatchRequests(params.requests);
176
+ const byCustomId = new Map();
177
+ const tasks = groups.map((group, groupIndex) => async () => {
178
+ const batchInfo = await submitVoyageBatch({
179
+ client: params.client,
180
+ requests: group,
181
+ agentId: params.agentId,
182
+ });
183
+ if (!batchInfo.id) {
184
+ throw new Error("voyage batch create failed: missing batch id");
185
+ }
186
+ params.debug?.("memory embeddings: voyage batch created", {
187
+ batchId: batchInfo.id,
188
+ status: batchInfo.status,
189
+ group: groupIndex + 1,
190
+ groups: groups.length,
191
+ requests: group.length,
192
+ });
193
+ if (!params.wait && batchInfo.status !== "completed") {
194
+ throw new Error(`voyage batch ${batchInfo.id} submitted; enable remote.batch.wait to await completion`);
195
+ }
196
+ const completed = batchInfo.status === "completed"
197
+ ? {
198
+ outputFileId: batchInfo.output_file_id ?? "",
199
+ errorFileId: batchInfo.error_file_id ?? undefined,
200
+ }
201
+ : await waitForVoyageBatch({
202
+ client: params.client,
203
+ batchId: batchInfo.id,
204
+ wait: params.wait,
205
+ pollIntervalMs: params.pollIntervalMs,
206
+ timeoutMs: params.timeoutMs,
207
+ debug: params.debug,
208
+ initial: batchInfo,
209
+ });
210
+ if (!completed.outputFileId) {
211
+ throw new Error(`voyage batch ${batchInfo.id} completed without output file`);
212
+ }
213
+ const baseUrl = getVoyageBaseUrl(params.client);
214
+ const contentRes = await fetch(`${baseUrl}/files/${completed.outputFileId}/content`, {
215
+ headers: getVoyageHeaders(params.client, { json: true }),
216
+ });
217
+ if (!contentRes.ok) {
218
+ const text = await contentRes.text();
219
+ throw new Error(`voyage batch file content failed: ${contentRes.status} ${text}`);
220
+ }
221
+ const errors = [];
222
+ const remaining = new Set(group.map((request) => request.custom_id));
223
+ if (contentRes.body) {
224
+ const reader = createInterface({
225
+ input: Readable.fromWeb(contentRes.body),
226
+ terminal: false,
227
+ });
228
+ for await (const rawLine of reader) {
229
+ if (!rawLine.trim()) {
230
+ continue;
231
+ }
232
+ const line = JSON.parse(rawLine);
233
+ const customId = line.custom_id;
234
+ if (!customId) {
235
+ continue;
236
+ }
237
+ remaining.delete(customId);
238
+ if (line.error?.message) {
239
+ errors.push(`${customId}: ${line.error.message}`);
240
+ continue;
241
+ }
242
+ const response = line.response;
243
+ const statusCode = response?.status_code ?? 0;
244
+ if (statusCode >= 400) {
245
+ const message = response?.body?.error?.message ??
246
+ (typeof response?.body === "string" ? response.body : undefined) ??
247
+ "unknown error";
248
+ errors.push(`${customId}: ${message}`);
249
+ continue;
250
+ }
251
+ const data = response?.body?.data ?? [];
252
+ const embedding = data[0]?.embedding ?? [];
253
+ if (embedding.length === 0) {
254
+ errors.push(`${customId}: empty embedding`);
255
+ continue;
256
+ }
257
+ byCustomId.set(customId, embedding);
258
+ }
259
+ }
260
+ if (errors.length > 0) {
261
+ throw new Error(`voyage batch ${batchInfo.id} failed: ${errors.join("; ")}`);
262
+ }
263
+ if (remaining.size > 0) {
264
+ throw new Error(`voyage batch ${batchInfo.id} missing ${remaining.size} embedding responses`);
265
+ }
266
+ });
267
+ params.debug?.("memory embeddings: voyage batch submit", {
268
+ requests: params.requests.length,
269
+ groups: groups.length,
270
+ wait: params.wait,
271
+ concurrency: params.concurrency,
272
+ pollIntervalMs: params.pollIntervalMs,
273
+ timeoutMs: params.timeoutMs,
274
+ });
275
+ await runWithConcurrency(tasks, params.concurrency);
276
+ return byCustomId;
277
+ }
@@ -0,0 +1,75 @@
1
+ import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
2
+ export const DEFAULT_VOYAGE_EMBEDDING_MODEL = "voyage-4-large";
3
+ const DEFAULT_VOYAGE_BASE_URL = "https://api.voyageai.com/v1";
4
+ export function normalizeVoyageModel(model) {
5
+ const trimmed = model.trim();
6
+ if (!trimmed) {
7
+ return DEFAULT_VOYAGE_EMBEDDING_MODEL;
8
+ }
9
+ if (trimmed.startsWith("voyage/")) {
10
+ return trimmed.slice("voyage/".length);
11
+ }
12
+ return trimmed;
13
+ }
14
+ export async function createVoyageEmbeddingProvider(options) {
15
+ const client = await resolveVoyageEmbeddingClient(options);
16
+ const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`;
17
+ const embed = async (input, input_type) => {
18
+ if (input.length === 0) {
19
+ return [];
20
+ }
21
+ const body = {
22
+ model: client.model,
23
+ input,
24
+ };
25
+ if (input_type) {
26
+ body.input_type = input_type;
27
+ }
28
+ const res = await fetch(url, {
29
+ method: "POST",
30
+ headers: client.headers,
31
+ body: JSON.stringify(body),
32
+ });
33
+ if (!res.ok) {
34
+ const text = await res.text();
35
+ throw new Error(`voyage embeddings failed: ${res.status} ${text}`);
36
+ }
37
+ const payload = (await res.json());
38
+ const data = payload.data ?? [];
39
+ return data.map((entry) => entry.embedding ?? []);
40
+ };
41
+ return {
42
+ provider: {
43
+ id: "voyage",
44
+ model: client.model,
45
+ embedQuery: async (text) => {
46
+ const [vec] = await embed([text], "query");
47
+ return vec ?? [];
48
+ },
49
+ embedBatch: async (texts) => embed(texts, "document"),
50
+ },
51
+ client,
52
+ };
53
+ }
54
+ export async function resolveVoyageEmbeddingClient(options) {
55
+ const remote = options.remote;
56
+ const remoteApiKey = remote?.apiKey?.trim();
57
+ const remoteBaseUrl = remote?.baseUrl?.trim();
58
+ const apiKey = remoteApiKey
59
+ ? remoteApiKey
60
+ : requireApiKey(await resolveApiKeyForProvider({
61
+ provider: "voyage",
62
+ cfg: options.config,
63
+ agentDir: options.agentDir,
64
+ }), "voyage");
65
+ const providerConfig = options.config.models?.providers?.voyage;
66
+ const baseUrl = remoteBaseUrl || providerConfig?.baseUrl?.trim() || DEFAULT_VOYAGE_BASE_URL;
67
+ const headerOverrides = Object.assign({}, providerConfig?.headers, remote?.headers);
68
+ const headers = {
69
+ "Content-Type": "application/json",
70
+ Authorization: `Bearer ${apiKey}`,
71
+ ...headerOverrides,
72
+ };
73
+ const model = normalizeVoyageModel(options.model);
74
+ return { baseUrl, headers, model };
75
+ }
@@ -1,15 +1,27 @@
1
1
  import fsSync from "node:fs";
2
+ import { formatErrorMessage } from "../infra/errors.js";
2
3
  import { resolveUserPath } from "../utils.js";
3
4
  import { createGeminiEmbeddingProvider } from "./embeddings-gemini.js";
4
5
  import { createOpenAiEmbeddingProvider } from "./embeddings-openai.js";
6
+ import { createVoyageEmbeddingProvider } from "./embeddings-voyage.js";
5
7
  import { importNodeLlamaCpp } from "./node-llama.js";
8
+ function sanitizeAndNormalizeEmbedding(vec) {
9
+ const sanitized = vec.map((value) => (Number.isFinite(value) ? value : 0));
10
+ const magnitude = Math.sqrt(sanitized.reduce((sum, value) => sum + value * value, 0));
11
+ if (magnitude < 1e-10) {
12
+ return sanitized;
13
+ }
14
+ return sanitized.map((value) => value / magnitude);
15
+ }
6
16
  const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
7
17
  function canAutoSelectLocal(options) {
8
18
  const modelPath = options.local?.modelPath?.trim();
9
- if (!modelPath)
19
+ if (!modelPath) {
10
20
  return false;
11
- if (/^(hf:|https?:)/i.test(modelPath))
21
+ }
22
+ if (/^(hf:|https?:)/i.test(modelPath)) {
12
23
  return false;
24
+ }
13
25
  const resolved = resolveUserPath(modelPath);
14
26
  try {
15
27
  return fsSync.statSync(resolved).isFile();
@@ -19,7 +31,7 @@ function canAutoSelectLocal(options) {
19
31
  }
20
32
  }
21
33
  function isMissingApiKeyError(err) {
22
- const message = formatError(err);
34
+ const message = formatErrorMessage(err);
23
35
  return message.includes("No API key found for provider");
24
36
  }
25
37
  async function createLocalEmbeddingProvider(options) {
@@ -49,13 +61,13 @@ async function createLocalEmbeddingProvider(options) {
49
61
  embedQuery: async (text) => {
50
62
  const ctx = await ensureContext();
51
63
  const embedding = await ctx.getEmbeddingFor(text);
52
- return Array.from(embedding.vector);
64
+ return sanitizeAndNormalizeEmbedding(Array.from(embedding.vector));
53
65
  },
54
66
  embedBatch: async (texts) => {
55
67
  const ctx = await ensureContext();
56
68
  const embeddings = await Promise.all(texts.map(async (text) => {
57
69
  const embedding = await ctx.getEmbeddingFor(text);
58
- return Array.from(embedding.vector);
70
+ return sanitizeAndNormalizeEmbedding(Array.from(embedding.vector));
59
71
  }));
60
72
  return embeddings;
61
73
  },
@@ -73,10 +85,14 @@ export async function createEmbeddingProvider(options) {
73
85
  const { provider, client } = await createGeminiEmbeddingProvider(options);
74
86
  return { provider, gemini: client };
75
87
  }
88
+ if (id === "voyage") {
89
+ const { provider, client } = await createVoyageEmbeddingProvider(options);
90
+ return { provider, voyage: client };
91
+ }
76
92
  const { provider, client } = await createOpenAiEmbeddingProvider(options);
77
93
  return { provider, openAi: client };
78
94
  };
79
- const formatPrimaryError = (err, provider) => provider === "local" ? formatLocalSetupError(err) : formatError(err);
95
+ const formatPrimaryError = (err, provider) => provider === "local" ? formatLocalSetupError(err) : formatErrorMessage(err);
80
96
  if (requestedProvider === "auto") {
81
97
  const missingKeyErrors = [];
82
98
  let localError = null;
@@ -89,7 +105,7 @@ export async function createEmbeddingProvider(options) {
89
105
  localError = formatLocalSetupError(err);
90
106
  }
91
107
  }
92
- for (const provider of ["openai", "gemini"]) {
108
+ for (const provider of ["openai", "gemini", "voyage"]) {
93
109
  try {
94
110
  const result = await createProvider(provider);
95
111
  return { ...result, requestedProvider };
@@ -100,7 +116,7 @@ export async function createEmbeddingProvider(options) {
100
116
  missingKeyErrors.push(message);
101
117
  continue;
102
118
  }
103
- throw new Error(message);
119
+ throw new Error(message, { cause: err });
104
120
  }
105
121
  }
106
122
  const details = [...missingKeyErrors, localError].filter(Boolean);
@@ -126,17 +142,12 @@ export async function createEmbeddingProvider(options) {
126
142
  };
127
143
  }
128
144
  catch (fallbackErr) {
129
- throw new Error(`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`);
145
+ throw new Error(`${reason}\n\nFallback to ${fallback} failed: ${formatErrorMessage(fallbackErr)}`, { cause: fallbackErr });
130
146
  }
131
147
  }
132
- throw new Error(reason);
148
+ throw new Error(reason, { cause: primaryErr });
133
149
  }
134
150
  }
135
- function formatError(err) {
136
- if (err instanceof Error)
137
- return err.message;
138
- return String(err);
139
- }
140
151
  function isNodeLlamaCppMissing(err) {
141
152
  if (!(err instanceof Error))
142
153
  return false;
@@ -147,7 +158,7 @@ function isNodeLlamaCppMissing(err) {
147
158
  return false;
148
159
  }
149
160
  function formatLocalSetupError(err) {
150
- const detail = formatError(err);
161
+ const detail = formatErrorMessage(err);
151
162
  const missing = isNodeLlamaCppMissing(err);
152
163
  return [
153
164
  "Local embeddings unavailable.",
@@ -164,6 +175,7 @@ function formatLocalSetupError(err) {
164
175
  : null,
165
176
  "3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
166
177
  'Or set agents.defaults.memorySearch.provider = "openai" (remote).',
178
+ 'Or set agents.defaults.memorySearch.provider = "voyage" (remote).',
167
179
  ]
168
180
  .filter(Boolean)
169
181
  .join("\n");
@@ -13,6 +13,16 @@ export function normalizeRelPath(value) {
13
13
  const trimmed = value.trim().replace(/^[./]+/, "");
14
14
  return trimmed.replace(/\\/g, "/");
15
15
  }
16
+ export function normalizeExtraMemoryPaths(workspaceDir, extraPaths) {
17
+ if (!extraPaths?.length) {
18
+ return [];
19
+ }
20
+ const resolved = extraPaths
21
+ .map((value) => value.trim())
22
+ .filter(Boolean)
23
+ .map((value) => path.isAbsolute(value) ? path.resolve(value) : path.resolve(workspaceDir, value));
24
+ return Array.from(new Set(resolved));
25
+ }
16
26
  export function isMemoryPath(relPath) {
17
27
  const normalized = normalizeRelPath(relPath);
18
28
  if (!normalized)
@@ -21,15 +31,6 @@ export function isMemoryPath(relPath) {
21
31
  return true;
22
32
  return normalized.startsWith("memory/");
23
33
  }
24
- async function exists(filePath) {
25
- try {
26
- await fs.access(filePath);
27
- return true;
28
- }
29
- catch {
30
- return false;
31
- }
32
- }
33
34
  async function walkDir(dir, files) {
34
35
  const entries = await fs.readdir(dir, { withFileTypes: true });
35
36
  for (const entry of entries) {
@@ -45,20 +46,55 @@ async function walkDir(dir, files) {
45
46
  files.push(full);
46
47
  }
47
48
  }
48
- export async function listMemoryFiles(workspaceDir) {
49
+ export async function listMemoryFiles(workspaceDir, extraPaths) {
49
50
  const result = [];
50
51
  const memoryFile = path.join(workspaceDir, "MEMORY.md");
51
52
  const altMemoryFile = path.join(workspaceDir, "memory.md");
52
- if (await exists(memoryFile))
53
- result.push(memoryFile);
54
- if (await exists(altMemoryFile))
55
- result.push(altMemoryFile);
56
53
  const memoryDir = path.join(workspaceDir, "memory");
57
- if (await exists(memoryDir)) {
58
- await walkDir(memoryDir, result);
54
+ const addMarkdownFile = async (absPath) => {
55
+ try {
56
+ const stat = await fs.lstat(absPath);
57
+ if (stat.isSymbolicLink() || !stat.isFile()) {
58
+ return;
59
+ }
60
+ if (!absPath.endsWith(".md")) {
61
+ return;
62
+ }
63
+ result.push(absPath);
64
+ }
65
+ catch { }
66
+ };
67
+ await addMarkdownFile(memoryFile);
68
+ await addMarkdownFile(altMemoryFile);
69
+ try {
70
+ const dirStat = await fs.lstat(memoryDir);
71
+ if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) {
72
+ await walkDir(memoryDir, result);
73
+ }
59
74
  }
60
- if (result.length <= 1)
75
+ catch { }
76
+ const normalizedExtraPaths = normalizeExtraMemoryPaths(workspaceDir, extraPaths);
77
+ if (normalizedExtraPaths.length > 0) {
78
+ for (const inputPath of normalizedExtraPaths) {
79
+ try {
80
+ const stat = await fs.lstat(inputPath);
81
+ if (stat.isSymbolicLink()) {
82
+ continue;
83
+ }
84
+ if (stat.isDirectory()) {
85
+ await walkDir(inputPath, result);
86
+ continue;
87
+ }
88
+ if (stat.isFile() && inputPath.endsWith(".md")) {
89
+ result.push(inputPath);
90
+ }
91
+ }
92
+ catch { }
93
+ }
94
+ }
95
+ if (result.length <= 1) {
61
96
  return result;
97
+ }
62
98
  const seen = new Set();
63
99
  const deduped = [];
64
100
  for (const entry of result) {
@@ -67,8 +103,9 @@ export async function listMemoryFiles(workspaceDir) {
67
103
  key = await fs.realpath(entry);
68
104
  }
69
105
  catch { }
70
- if (seen.has(key))
106
+ if (seen.has(key)) {
71
107
  continue;
108
+ }
72
109
  seen.add(key);
73
110
  deduped.push(entry);
74
111
  }
@@ -187,3 +224,49 @@ export function cosineSimilarity(a, b) {
187
224
  return 0;
188
225
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
189
226
  }
227
+ export async function runWithConcurrency(tasks, limit) {
228
+ if (tasks.length === 0) {
229
+ return [];
230
+ }
231
+ const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
232
+ const results = Array.from({ length: tasks.length });
233
+ let next = 0;
234
+ let firstError = null;
235
+ const workers = Array.from({ length: resolvedLimit }, async () => {
236
+ while (true) {
237
+ if (firstError) {
238
+ return;
239
+ }
240
+ const index = next;
241
+ next += 1;
242
+ if (index >= tasks.length) {
243
+ return;
244
+ }
245
+ try {
246
+ results[index] = await tasks[index]();
247
+ }
248
+ catch (err) {
249
+ firstError = err;
250
+ return;
251
+ }
252
+ }
253
+ });
254
+ await Promise.allSettled(workers);
255
+ if (firstError) {
256
+ throw firstError;
257
+ }
258
+ return results;
259
+ }
260
+ /**
261
+ * Remap chunk startLine/endLine using a lineMap from session file building.
262
+ */
263
+ export function remapChunkLines(chunks, lineMap) {
264
+ if (!lineMap || lineMap.length === 0) {
265
+ return;
266
+ }
267
+ for (const chunk of chunks) {
268
+ // startLine/endLine are 1-indexed; lineMap is 0-indexed by content line
269
+ chunk.startLine = lineMap[chunk.startLine - 1] ?? chunk.startLine;
270
+ chunk.endLine = lineMap[chunk.endLine - 1] ?? chunk.endLine;
271
+ }
272
+ }