@botlearn-course/daemon 0.0.17-beta.1 → 0.0.17-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/input-attachments.js +52 -11
- package/dist/run-dispatcher.js +1 -11
- package/package.json +1 -1
|
@@ -3,6 +3,11 @@ import { chmod, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const MAX_ATTACHMENTS = 5;
|
|
5
5
|
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
6
|
+
// Abort a stalled transfer without rejecting a slow cross-cloud source while verified bytes
|
|
7
|
+
// continue to arrive. The absolute cap prevents a trickle response from holding a daemon turn
|
|
8
|
+
// indefinitely. These are daemon compatibility invariants, not environment tuning knobs.
|
|
9
|
+
const ATTACHMENT_DOWNLOAD_IDLE_TIMEOUT_MS = 60_000;
|
|
10
|
+
const ATTACHMENT_DOWNLOAD_MAX_DURATION_MS = 10 * 60_000;
|
|
6
11
|
const ATTACHMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
7
12
|
const SHA256 = /^[0-9a-f]{64}$/i;
|
|
8
13
|
const CONTENT_TYPE_EXTENSIONS = {
|
|
@@ -126,7 +131,7 @@ async function verifyExisting(filePath, attachment) {
|
|
|
126
131
|
throw error;
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
|
-
async function writeResponseToFile(response, partPath, attachment, signal) {
|
|
134
|
+
async function writeResponseToFile(response, partPath, attachment, signal, onDownloadProgress) {
|
|
130
135
|
if (!response.body)
|
|
131
136
|
throw new Error("input attachment response has no body");
|
|
132
137
|
const declaredLength = response.headers.get("content-length");
|
|
@@ -146,13 +151,17 @@ async function writeResponseToFile(response, partPath, attachment, signal) {
|
|
|
146
151
|
try {
|
|
147
152
|
const reader = response.body.getReader();
|
|
148
153
|
while (true) {
|
|
149
|
-
if (signal.aborted)
|
|
154
|
+
if (signal.aborted) {
|
|
155
|
+
if (signal.reason instanceof Error)
|
|
156
|
+
throw signal.reason;
|
|
150
157
|
throw new Error("input attachment download aborted");
|
|
158
|
+
}
|
|
151
159
|
const { done, value } = await reader.read();
|
|
152
160
|
if (done)
|
|
153
161
|
break;
|
|
154
162
|
const chunk = Buffer.from(value);
|
|
155
163
|
received += chunk.length;
|
|
164
|
+
onDownloadProgress?.();
|
|
156
165
|
if (received > attachment.size_bytes || received > MAX_ATTACHMENT_BYTES) {
|
|
157
166
|
throw new Error("input attachment response exceeds declared size");
|
|
158
167
|
}
|
|
@@ -183,6 +192,36 @@ async function writeResponseToFile(response, partPath, attachment, signal) {
|
|
|
183
192
|
await file.close();
|
|
184
193
|
}
|
|
185
194
|
}
|
|
195
|
+
function startAttachmentDownloadBudget(parentSignal) {
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const abortFromParent = () => controller.abort(parentSignal.reason);
|
|
198
|
+
const abortStalledDownload = () => controller.abort(new Error("input attachment download stalled for 60s"));
|
|
199
|
+
const abortOverlongDownload = () => controller.abort(new Error("input attachment download exceeded the 600s maximum duration"));
|
|
200
|
+
if (parentSignal.aborted) {
|
|
201
|
+
abortFromParent();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
parentSignal.addEventListener("abort", abortFromParent, { once: true });
|
|
205
|
+
}
|
|
206
|
+
const maxDurationTimer = setTimeout(abortOverlongDownload, ATTACHMENT_DOWNLOAD_MAX_DURATION_MS);
|
|
207
|
+
let idleTimer;
|
|
208
|
+
const recordProgress = () => {
|
|
209
|
+
if (idleTimer !== undefined)
|
|
210
|
+
clearTimeout(idleTimer);
|
|
211
|
+
idleTimer = setTimeout(abortStalledDownload, ATTACHMENT_DOWNLOAD_IDLE_TIMEOUT_MS);
|
|
212
|
+
};
|
|
213
|
+
recordProgress();
|
|
214
|
+
return {
|
|
215
|
+
signal: controller.signal,
|
|
216
|
+
recordProgress,
|
|
217
|
+
dispose: () => {
|
|
218
|
+
if (idleTimer !== undefined)
|
|
219
|
+
clearTimeout(idleTimer);
|
|
220
|
+
clearTimeout(maxDurationTimer);
|
|
221
|
+
parentSignal.removeEventListener("abort", abortFromParent);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
186
225
|
export async function materializeInputAttachments(workspaceDir, attachments, grant, signal) {
|
|
187
226
|
if (attachments.length > MAX_ATTACHMENTS) {
|
|
188
227
|
throw new Error("too many input attachments");
|
|
@@ -215,20 +254,22 @@ export async function materializeInputAttachments(workspaceDir, attachments, gra
|
|
|
215
254
|
const partPath = `${filePath}.part`;
|
|
216
255
|
await rm(partPath, { force: true });
|
|
217
256
|
const downloadUrl = downloadUrls.get(attachment.attachment_id);
|
|
218
|
-
const
|
|
219
|
-
headers: { "accept-encoding": "identity" },
|
|
220
|
-
redirect: "error",
|
|
221
|
-
signal,
|
|
222
|
-
});
|
|
223
|
-
if (!response.ok) {
|
|
224
|
-
throw new Error(`input attachment download failed with HTTP ${response.status}`);
|
|
225
|
-
}
|
|
257
|
+
const budget = startAttachmentDownloadBudget(signal);
|
|
226
258
|
try {
|
|
227
|
-
|
|
259
|
+
const response = await fetch(downloadUrl, {
|
|
260
|
+
headers: { "accept-encoding": "identity" },
|
|
261
|
+
redirect: "error",
|
|
262
|
+
signal: budget.signal,
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
throw new Error(`input attachment download failed with HTTP ${response.status}`);
|
|
266
|
+
}
|
|
267
|
+
await writeResponseToFile(response, partPath, attachment, budget.signal, budget.recordProgress);
|
|
228
268
|
await rename(partPath, filePath);
|
|
229
269
|
await chmod(filePath, 0o640);
|
|
230
270
|
}
|
|
231
271
|
finally {
|
|
272
|
+
budget.dispose();
|
|
232
273
|
await rm(partPath, { force: true });
|
|
233
274
|
}
|
|
234
275
|
}
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -446,17 +446,7 @@ export class RunDispatcher {
|
|
|
446
446
|
if (!persistentTurn?.inputAttachmentGrant) {
|
|
447
447
|
throw new Error("input attachment download authorization is unavailable");
|
|
448
448
|
}
|
|
449
|
-
|
|
450
|
-
const abortAttachmentDownload = () => attachmentController.abort();
|
|
451
|
-
controller.signal.addEventListener("abort", abortAttachmentDownload, { once: true });
|
|
452
|
-
const attachmentTimer = setTimeout(abortAttachmentDownload, 60_000);
|
|
453
|
-
try {
|
|
454
|
-
inputAttachments = await materializeInputAttachments(workspaceDir, requestedAttachments, persistentTurn.inputAttachmentGrant, attachmentController.signal);
|
|
455
|
-
}
|
|
456
|
-
finally {
|
|
457
|
-
clearTimeout(attachmentTimer);
|
|
458
|
-
controller.signal.removeEventListener("abort", abortAttachmentDownload);
|
|
459
|
-
}
|
|
449
|
+
inputAttachments = await materializeInputAttachments(workspaceDir, requestedAttachments, persistentTurn.inputAttachmentGrant, controller.signal);
|
|
460
450
|
}
|
|
461
451
|
const missingCapabilities = missingRunCapabilities(payload, workspaceDir);
|
|
462
452
|
if (missingCapabilities.length > 0) {
|
package/package.json
CHANGED