@sema-agent/server 5.7.0 → 5.9.0
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/leader/diffup.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* pullDiff stays as the compat path for static (leader-owned) envs — M2 降兼容.
|
|
16
16
|
*/
|
|
17
17
|
import { safeRev } from "./diffout.js";
|
|
18
|
+
import { OBJECT_STORE_IO_TIMEOUT_MS } from "../plugins/blob-backend.js";
|
|
18
19
|
const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
|
|
19
20
|
/** The staged upload script. `repoDir`/`workDir` are deployment config; `baseSha` is interpolated into a
|
|
20
21
|
* shell line, so it is re-validated HERE (fail-closed) — callers should already have applied diffout's
|
|
@@ -52,7 +53,9 @@ export async function fetchUploadedDiff(getUrl, opts = {}) {
|
|
|
52
53
|
const f = opts.fetchImpl ?? fetch;
|
|
53
54
|
let res;
|
|
54
55
|
try {
|
|
55
|
-
|
|
56
|
+
// C6(2026-08-03):abort 上界(半开 MinIO 此前把 leader merge 阶段挂满 undici ~300s,钉死整个
|
|
57
|
+
// fanout)。maxBytes 检查仍在 text() 缓冲之后=已知残留(流式读上界另案),本修只收超时轴。
|
|
58
|
+
res = await f(getUrl, { signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
56
59
|
}
|
|
57
60
|
catch (e) {
|
|
58
61
|
return { ok: false, error: `diff fetch failed: ${e instanceof Error ? e.message.replaceAll(getUrl, "<presigned-url>") : "network error"}` };
|
|
@@ -102,6 +102,11 @@ export interface MinioBlobBackendConfig {
|
|
|
102
102
|
* under a hash it doesn't match) — treated as ABSENT (undefined), NEVER returning wrong bytes. This mirrors the
|
|
103
103
|
* importManifest content-address check the SQL stores already do.
|
|
104
104
|
*/
|
|
105
|
+
/** 鲁棒性批2 C2/C3/C6(2026-08-03):S3 兼容端点 IO 的统一 abort 上界(fleet-client C1 定常量形
|
|
106
|
+
* 先例)。半开端点此前各挂 undici 默认 ~300s headersTimeout——blob GET 钉死 restore/session-import
|
|
107
|
+
* 同步路径,SendUserFile 工具调用永不返回。120s 对慢链路 ~64MiB 单 PUT/GET 仍宽裕(presignTtlSec
|
|
108
|
+
* 注释同口径),远小于病形。send-user-file 与 leader/diffup 同 import 此常量。 */
|
|
109
|
+
export declare const OBJECT_STORE_IO_TIMEOUT_MS = 120000;
|
|
105
110
|
export declare class MinioBlobBackend implements BlobBackend {
|
|
106
111
|
private readonly cfg;
|
|
107
112
|
private readonly keyPrefix;
|
|
@@ -222,6 +222,11 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
|
222
222
|
* under a hash it doesn't match) — treated as ABSENT (undefined), NEVER returning wrong bytes. This mirrors the
|
|
223
223
|
* importManifest content-address check the SQL stores already do.
|
|
224
224
|
*/
|
|
225
|
+
/** 鲁棒性批2 C2/C3/C6(2026-08-03):S3 兼容端点 IO 的统一 abort 上界(fleet-client C1 定常量形
|
|
226
|
+
* 先例)。半开端点此前各挂 undici 默认 ~300s headersTimeout——blob GET 钉死 restore/session-import
|
|
227
|
+
* 同步路径,SendUserFile 工具调用永不返回。120s 对慢链路 ~64MiB 单 PUT/GET 仍宽裕(presignTtlSec
|
|
228
|
+
* 注释同口径),远小于病形。send-user-file 与 leader/diffup 同 import 此常量。 */
|
|
229
|
+
export const OBJECT_STORE_IO_TIMEOUT_MS = 120_000;
|
|
225
230
|
export class MinioBlobBackend {
|
|
226
231
|
cfg;
|
|
227
232
|
keyPrefix;
|
|
@@ -268,7 +273,9 @@ export class MinioBlobBackend {
|
|
|
268
273
|
* catches a corrupted PUT at the next getBlob sha-check (fail-closed); the one-time migration script DOES read-back. */
|
|
269
274
|
async putBlob(hash, bytes) {
|
|
270
275
|
const url = this.presign(hash, "PUT");
|
|
271
|
-
|
|
276
|
+
// C2(2026-08-03):走注入座(此前直用全局 fetch=测试桩接不进的站点)+ abort 上界(半开端点
|
|
277
|
+
// 此前挂 undici ~300s;120s 对慢链路大对象仍宽裕,presignTtlSec 注释同口径)。六站点同批。
|
|
278
|
+
const res = await (this.cfg.fetchImpl ?? fetch)(url, { method: "PUT", body: bytes, signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
272
279
|
if (!res.ok) {
|
|
273
280
|
const detail = await res.text().catch(() => "");
|
|
274
281
|
throw new Error(`MinioBlobBackend.putBlob ${hash}: HTTP ${res.status} ${detail.slice(0, 200)}`);
|
|
@@ -295,7 +302,7 @@ export class MinioBlobBackend {
|
|
|
295
302
|
const url = this.presign(hash, "GET");
|
|
296
303
|
let res;
|
|
297
304
|
try {
|
|
298
|
-
res = await fetch(url, { method: "GET" });
|
|
305
|
+
res = await (this.cfg.fetchImpl ?? fetch)(url, { method: "GET", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
299
306
|
}
|
|
300
307
|
catch (err) {
|
|
301
308
|
throw new Error(`MinioBlobBackend.getBlob ${hash}: transient fetch failure: ${err.message}`);
|
|
@@ -323,7 +330,7 @@ export class MinioBlobBackend {
|
|
|
323
330
|
const present = new Set();
|
|
324
331
|
await mapBounded(distinct, MINIO_OP_CONCURRENCY, async (hash) => {
|
|
325
332
|
try {
|
|
326
|
-
const res = await fetch(this.presign(hash, "HEAD"), { method: "HEAD" });
|
|
333
|
+
const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "HEAD"), { method: "HEAD", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
327
334
|
if (res.ok)
|
|
328
335
|
present.add(hash);
|
|
329
336
|
}
|
|
@@ -360,7 +367,7 @@ export class MinioBlobBackend {
|
|
|
360
367
|
...(this.cfg.region ? { region: this.cfg.region } : {}),
|
|
361
368
|
expiresSec: this.ttl,
|
|
362
369
|
});
|
|
363
|
-
const res = await f(url, { method: "GET" });
|
|
370
|
+
const res = await f(url, { method: "GET", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
364
371
|
if (!res.ok)
|
|
365
372
|
throw new Error(`listObjects: MinIO ${res.status}`);
|
|
366
373
|
const xml = await res.text();
|
|
@@ -384,7 +391,7 @@ export class MinioBlobBackend {
|
|
|
384
391
|
* put 会 PUT 同键 ⇒ 刷新 LastModified,所以「删前再 HEAD 一次」能看见「刚刚被重新引用」的对象。 */
|
|
385
392
|
async headObject(hash) {
|
|
386
393
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
387
|
-
const res = await f(this.presign(hash, "HEAD"), { method: "HEAD" });
|
|
394
|
+
const res = await f(this.presign(hash, "HEAD"), { method: "HEAD", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
388
395
|
if (!res.ok)
|
|
389
396
|
return undefined;
|
|
390
397
|
const lm = res.headers.get("last-modified");
|
|
@@ -417,7 +424,7 @@ export class MinioBlobBackend {
|
|
|
417
424
|
let okCount = 0;
|
|
418
425
|
await mapBounded(toDelete, MINIO_OP_CONCURRENCY, async (hash) => {
|
|
419
426
|
try {
|
|
420
|
-
const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "DELETE"), { method: "DELETE" });
|
|
427
|
+
const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "DELETE"), { method: "DELETE", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
421
428
|
// 🔴 复审 F4:统计**真实**删除数——index-less(附件)分支此前恒返回入参长度,MinIO 完全不可写
|
|
422
429
|
// 时指标/日志照报「清理成功」。2xx/404(已不在)都算删成功;其余=失败,不计数。
|
|
423
430
|
if (res.ok || res.status === 404)
|
|
@@ -116,5 +116,7 @@ export declare function createSendUserFileIssuer(cfg: SendUserFileConfig, deps?:
|
|
|
116
116
|
/** Injectable object delete (tests). Default = signed DELETE against the INTERNAL endpoint. */
|
|
117
117
|
remove?: (bucket: string, key: string) => Promise<void>;
|
|
118
118
|
now?: () => Date;
|
|
119
|
+
/** C3(2026-08-03)测试座:默认实现的六个 S3 fetch 站点走此座——signal 在场性自此可钉。 */
|
|
120
|
+
fetchImpl?: typeof fetch;
|
|
119
121
|
}): SendUserFileIssuer;
|
|
120
122
|
//# sourceMappingURL=send-user-file.d.ts.map
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { createHmac } from "node:crypto";
|
|
27
27
|
import { uuidv7 } from "@sema-agent/core";
|
|
28
28
|
import { presignS3CopyUrl, presignS3Url } from "./s3-presign.js";
|
|
29
|
+
import { OBJECT_STORE_IO_TIMEOUT_MS } from "./blob-backend.js";
|
|
29
30
|
/** SigV4 physical ceiling (AWS: X-Amz-Expires max 604800s = 7 days). */
|
|
30
31
|
export const PRESIGN_MAX_TTL_SEC = 604_800;
|
|
31
32
|
/**
|
|
@@ -80,6 +81,10 @@ export function sanitizeSendFileName(name) {
|
|
|
80
81
|
return clean.length > 0 ? clean : "file";
|
|
81
82
|
}
|
|
82
83
|
export function createSendUserFileIssuer(cfg, deps) {
|
|
84
|
+
// C3:六站点统一 abort 上界(半开 S3 端点此前把用户可见的工具调用挂满 undici ~300s,
|
|
85
|
+
// reclaim 也挂住 ⇒ staging 孤儿+误报「reclaim FAILED」)。上界常量与 blob-backend 同源。
|
|
86
|
+
const f = deps?.fetchImpl ?? fetch;
|
|
87
|
+
const bounded = () => ({ signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
83
88
|
const upload = deps?.upload ??
|
|
84
89
|
(async (bucket, key, bytes) => {
|
|
85
90
|
const url = presignS3Url({
|
|
@@ -93,7 +98,7 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
93
98
|
expiresSec: 600,
|
|
94
99
|
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
95
100
|
});
|
|
96
|
-
const res = await
|
|
101
|
+
const res = await f(url, { method: "PUT", body: Buffer.from(bytes), ...bounded() });
|
|
97
102
|
if (!res.ok)
|
|
98
103
|
throw new Error(`SendUserFile upload failed: ${res.status} ${res.statusText} (bucket=${bucket})`);
|
|
99
104
|
});
|
|
@@ -196,7 +201,7 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
196
201
|
// TRUE outcome so error messages never claim "reclaimed" for a delete that itself failed (F2 posture).
|
|
197
202
|
const reclaim = async (b, k) => {
|
|
198
203
|
try {
|
|
199
|
-
const r = await
|
|
204
|
+
const r = await f(sign("DELETE", b, k), { method: "DELETE", ...bounded() });
|
|
200
205
|
return r.ok || r.status === 404; // S3 DELETE is idempotent — a missing key IS reclaimed
|
|
201
206
|
}
|
|
202
207
|
catch {
|
|
@@ -208,7 +213,7 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
208
213
|
// (fail before copying a huge object). Advisory only for size — the authoritative reading is ④.
|
|
209
214
|
let head;
|
|
210
215
|
try {
|
|
211
|
-
head = await
|
|
216
|
+
head = await f(sign("HEAD", stagingBucket, stagingKey), { method: "HEAD", ...bounded() });
|
|
212
217
|
}
|
|
213
218
|
catch (e) {
|
|
214
219
|
const r = await reclaim(stagingBucket, stagingKey);
|
|
@@ -245,7 +250,7 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
245
250
|
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
246
251
|
});
|
|
247
252
|
try {
|
|
248
|
-
const res = await
|
|
253
|
+
const res = await f(copy.url, { method: "PUT", headers: copy.headers, ...bounded() });
|
|
249
254
|
const body = await res.text().catch(() => "");
|
|
250
255
|
if (!res.ok || !body.includes("CopyObjectResult"))
|
|
251
256
|
throw new Error(`copy returned ${res.status}`);
|
|
@@ -263,7 +268,7 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
263
268
|
// staging between ① and ②, swapping in different bytes).
|
|
264
269
|
let fin;
|
|
265
270
|
try {
|
|
266
|
-
fin = await
|
|
271
|
+
fin = await f(sign("HEAD", bucket, key), { method: "HEAD", ...bounded() });
|
|
267
272
|
}
|
|
268
273
|
catch (e) {
|
|
269
274
|
const r = await reclaim(bucket, key);
|
|
@@ -322,7 +327,7 @@ export function createSendUserFileIssuer(cfg, deps) {
|
|
|
322
327
|
expiresSec: 60,
|
|
323
328
|
...(deps?.now !== undefined ? { now: deps.now() } : {}),
|
|
324
329
|
});
|
|
325
|
-
const res = await
|
|
330
|
+
const res = await f(url, { method: "DELETE", ...bounded() });
|
|
326
331
|
if (!res.ok && res.status !== 404)
|
|
327
332
|
throw new Error(`SendUserFile object delete failed: ${res.status} ${res.statusText} (bucket=${bucket})`);
|
|
328
333
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
58
|
-
"@sema-agent/registry-core": "^0.
|
|
57
|
+
"@sema-agent/core": "^5.5.0",
|
|
58
|
+
"@sema-agent/registry-core": "^0.14.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
61
61
|
"mysql2": "^3.22.4",
|