@sema-agent/server 1.302.0 → 1.304.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/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/http/server.js +19 -5
- package/dist/main.js +29 -10
- package/dist/plugins/blob-backend.d.ts +3 -0
- package/dist/plugins/blob-backend.js +13 -0
- package/dist/plugins/task-attachment-store.d.ts +5 -0
- package/dist/plugins/task-attachment-store.js +42 -4
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -538,6 +538,7 @@ export function loadConfig() {
|
|
|
538
538
|
: undefined,
|
|
539
539
|
snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
|
|
540
540
|
snapshotBlobAllowSql: process.env.SNAPSHOT_BLOB_ALLOW_SQL_BYTES === "true",
|
|
541
|
+
attachmentOrphanGraceMs: ((v) => (v !== undefined && Number.isFinite(v) && v >= 0 ? v : 3_600_000))(process.env.ATTACHMENT_ORPHAN_GRACE_MS !== undefined ? Number(process.env.ATTACHMENT_ORPHAN_GRACE_MS) : undefined),
|
|
541
542
|
workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
|
|
542
543
|
sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
543
544
|
? {
|
package/dist/http/server.js
CHANGED
|
@@ -274,7 +274,7 @@ export function createHttpServer(deps) {
|
|
|
274
274
|
void handle(req, res).catch((err) => {
|
|
275
275
|
if (err instanceof HttpError) {
|
|
276
276
|
if (!res.headersSent)
|
|
277
|
-
sendJson(res, err.status, { error: err.message, ...(err.code ? { code: err.code } : {}), ...(err.extra ?? {}) });
|
|
277
|
+
sendJson(res, err.status, { error: err.message, ...(err.code ? { code: err.code, errorCode: err.code } : {}), ...(err.extra ?? {}) });
|
|
278
278
|
else
|
|
279
279
|
res.end();
|
|
280
280
|
return;
|
|
@@ -3988,7 +3988,12 @@ export function createHttpServer(deps) {
|
|
|
3988
3988
|
}
|
|
3989
3989
|
snapshots.push({ key: k, files: m.size, ...(bytes !== undefined ? { bytes } : {}) });
|
|
3990
3990
|
}
|
|
3991
|
-
sendJson(res, 200, {
|
|
3991
|
+
sendJson(res, 200, {
|
|
3992
|
+
sessionId: wsSession,
|
|
3993
|
+
snapshots,
|
|
3994
|
+
...(snapshots.length > 0 ? { latest: snapshots[0].key } : {}),
|
|
3995
|
+
total: snapshots.length,
|
|
3996
|
+
});
|
|
3992
3997
|
return;
|
|
3993
3998
|
}
|
|
3994
3999
|
const keyRaw = safeDecode(wm[2]);
|
|
@@ -4089,8 +4094,16 @@ export function createHttpServer(deps) {
|
|
|
4089
4094
|
const write = async (buf) => {
|
|
4090
4095
|
if (res.writableEnded || res.destroyed)
|
|
4091
4096
|
throw new Error("client gone");
|
|
4092
|
-
if (!res.write(buf))
|
|
4093
|
-
await new Promise((
|
|
4097
|
+
if (!res.write(buf)) {
|
|
4098
|
+
await new Promise((resolve, reject) => {
|
|
4099
|
+
const onDrain = () => { cleanup(); resolve(); };
|
|
4100
|
+
const onGone = () => { cleanup(); reject(new Error("client gone (closed while draining)")); };
|
|
4101
|
+
const cleanup = () => { res.off("drain", onDrain); res.off("close", onGone); res.off("error", onGone); };
|
|
4102
|
+
res.once("drain", onDrain);
|
|
4103
|
+
res.once("close", onGone);
|
|
4104
|
+
res.once("error", onGone);
|
|
4105
|
+
});
|
|
4106
|
+
}
|
|
4094
4107
|
};
|
|
4095
4108
|
try {
|
|
4096
4109
|
for (const [p, h] of entries) {
|
|
@@ -4249,6 +4262,7 @@ export function createHttpServer(deps) {
|
|
|
4249
4262
|
if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
|
|
4250
4263
|
sendJson(res, 413, {
|
|
4251
4264
|
code: "blob_too_large_for_sql",
|
|
4265
|
+
errorCode: "blob_too_large_for_sql",
|
|
4252
4266
|
error: `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes; mysql-protocol packet limit) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs`,
|
|
4253
4267
|
});
|
|
4254
4268
|
return;
|
|
@@ -5180,7 +5194,7 @@ export function createHttpServer(deps) {
|
|
|
5180
5194
|
}
|
|
5181
5195
|
catch (err) {
|
|
5182
5196
|
if (err instanceof HttpError) {
|
|
5183
|
-
sendJson(res, err.status, { error: err.message, ...(err.code ? { code: err.code } : {}), ...(err.extra ?? {}) });
|
|
5197
|
+
sendJson(res, err.status, { error: err.message, ...(err.code ? { code: err.code, errorCode: err.code } : {}), ...(err.extra ?? {}) });
|
|
5184
5198
|
return null;
|
|
5185
5199
|
}
|
|
5186
5200
|
throw err;
|
package/dist/main.js
CHANGED
|
@@ -1050,9 +1050,6 @@ async function main() {
|
|
|
1050
1050
|
...(sessionPolicyStore ? { sessionPolicyStore } : {}),
|
|
1051
1051
|
...(runtimeCapsResolver ? { runtimeCapsResolver } : {}),
|
|
1052
1052
|
...(fileSnapshotStore ? { fileSnapshotStore } : {}),
|
|
1053
|
-
...(backend && backend.kind !== "local" && !config.snapshotBlobStore
|
|
1054
|
-
? ((cap) => (cap !== undefined ? { snapshotBlobSqlCapBytes: cap } : {}))(config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
|
|
1055
|
-
: {}),
|
|
1056
1053
|
...(executionEnvFactory ? { executionEnvFactory } : {}),
|
|
1057
1054
|
...(lspManager ? { lspManager } : {}),
|
|
1058
1055
|
onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
|
|
@@ -1318,7 +1315,20 @@ async function main() {
|
|
|
1318
1315
|
if (webSearch)
|
|
1319
1316
|
logger.info(`WebSearch enabled (provider=${webSearchCfg.provider}, maxResults=${webSearch.maxResults})`);
|
|
1320
1317
|
const ensureChildSessionDurable = checkpointStore && config.sessionBackend === "tidb" ? (sessionId) => ensureChildSessionDurableWithPromotion(sessionStore, subRunnerSessions, sessionId) : undefined;
|
|
1321
|
-
const webFetchSummarize =
|
|
1318
|
+
const webFetchSummarize = async (content, prompt, signal) => {
|
|
1319
|
+
const model = (() => {
|
|
1320
|
+
try {
|
|
1321
|
+
return coreResolveTaskModel({ modelRole: "summarize" }, { models: config.models, roles: config.roles }).model;
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
logger.warn("webfetch_summarizer_model_unresolved", { err: String(err), note: "this fetch falls back to raw page content" });
|
|
1325
|
+
return undefined;
|
|
1326
|
+
}
|
|
1327
|
+
})();
|
|
1328
|
+
if (!model)
|
|
1329
|
+
throw new Error("summarize model unresolved for this deployment");
|
|
1330
|
+
return makeWebFetchSummarizer(brain, model)(content, prompt, signal);
|
|
1331
|
+
};
|
|
1322
1332
|
const scenarioDeps = {
|
|
1323
1333
|
runner, subRunner, model: "default", skills, repoClient, requirePrincipal: config.requirePrincipal,
|
|
1324
1334
|
...(webSearch ? { webSearch } : {}), metrics, logger, webFetchSummarize,
|
|
@@ -1372,6 +1382,7 @@ async function main() {
|
|
|
1372
1382
|
}
|
|
1373
1383
|
};
|
|
1374
1384
|
let wfRunReapInFlight = false;
|
|
1385
|
+
let attachmentSweepInFlight = false;
|
|
1375
1386
|
let bgAgentReapInFlight = false;
|
|
1376
1387
|
const reapBgAgents = backgroundAgentStore
|
|
1377
1388
|
? async () => {
|
|
@@ -1440,12 +1451,17 @@ async function main() {
|
|
|
1440
1451
|
void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined);
|
|
1441
1452
|
void fileSnapshotStore?.sweepOrphanBlobs?.().catch(() => undefined);
|
|
1442
1453
|
void taskAttachmentStore?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs).then(reapCount("attachments_reaped_total", {})).catch(() => undefined);
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1454
|
+
if (config.attachmentOrphanGraceMs > 0 && !attachmentSweepInFlight && taskAttachmentStore?.sweepOrphanObjects) {
|
|
1455
|
+
attachmentSweepInFlight = true;
|
|
1456
|
+
void taskAttachmentStore
|
|
1457
|
+
.sweepOrphanObjects(config.attachmentOrphanGraceMs)
|
|
1458
|
+
.then((n) => { if (n > 0) {
|
|
1459
|
+
metrics.inc("attachment_orphan_objects_swept_total", {}, n);
|
|
1460
|
+
logger.info("attachment_orphan_objects_swept", { removed: n });
|
|
1461
|
+
} })
|
|
1462
|
+
.catch((err) => logger.warn("attachment_orphan_sweep_failed", { err: String(err) }))
|
|
1463
|
+
.finally(() => { attachmentSweepInFlight = false; });
|
|
1464
|
+
}
|
|
1449
1465
|
void backend?.session()?.sweepStagingSessions?.().catch(() => undefined);
|
|
1450
1466
|
void imageBakes?.reapStaleBakes(config.imageBakes.staleMs).then(reapCount("bakes_reaped_total", {})).catch(() => undefined);
|
|
1451
1467
|
void worktreeReap?.();
|
|
@@ -1825,6 +1841,9 @@ async function main() {
|
|
|
1825
1841
|
authorize,
|
|
1826
1842
|
sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
|
|
1827
1843
|
...(taskAttachmentStore ? { taskAttachmentStore } : {}),
|
|
1844
|
+
...(backend && backend.kind !== "local" && !config.snapshotBlobStore
|
|
1845
|
+
? ((cap) => (cap !== undefined ? { snapshotBlobSqlCapBytes: cap } : {}))(config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
|
|
1846
|
+
: {}),
|
|
1828
1847
|
modelReady: () => modelReadyState.ready,
|
|
1829
1848
|
scenarioDetails,
|
|
1830
1849
|
...(registryJwtVerifier ? { registryJwtVerifier } : {}),
|
|
@@ -51,6 +51,9 @@ export declare class MinioBlobBackend implements BlobBackend {
|
|
|
51
51
|
hash: string;
|
|
52
52
|
lastModifiedMs: number;
|
|
53
53
|
}>;
|
|
54
|
+
headObject(hash: string): Promise<{
|
|
55
|
+
lastModifiedMs: number;
|
|
56
|
+
} | undefined>;
|
|
54
57
|
deleteBlobs(hashes: string[]): Promise<number>;
|
|
55
58
|
}
|
|
56
59
|
//# sourceMappingURL=blob-backend.d.ts.map
|
|
@@ -194,7 +194,11 @@ export class MinioBlobBackend {
|
|
|
194
194
|
async *listObjects() {
|
|
195
195
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
196
196
|
let token;
|
|
197
|
+
const MAX_PAGES = 1000;
|
|
198
|
+
let pages = 0;
|
|
197
199
|
do {
|
|
200
|
+
if (++pages > MAX_PAGES)
|
|
201
|
+
throw new Error(`listObjects: exceeded ${MAX_PAGES} pages (max-keys 1000/page) — the backend is looping its continuation-token or the prefix is unexpectedly large; refusing to report a partial orphan set`);
|
|
198
202
|
const url = presignS3ListUrl({
|
|
199
203
|
endpoint: this.cfg.endpoint,
|
|
200
204
|
bucket: this.cfg.bucket,
|
|
@@ -225,6 +229,15 @@ export class MinioBlobBackend {
|
|
|
225
229
|
throw new Error("listObjects: truncated response without NextContinuationToken");
|
|
226
230
|
} while (token !== undefined);
|
|
227
231
|
}
|
|
232
|
+
async headObject(hash) {
|
|
233
|
+
const f = this.cfg.fetchImpl ?? fetch;
|
|
234
|
+
const res = await f(this.presign(hash, "HEAD"), { method: "HEAD" });
|
|
235
|
+
if (!res.ok)
|
|
236
|
+
return undefined;
|
|
237
|
+
const lm = res.headers.get("last-modified");
|
|
238
|
+
const ms = lm ? Date.parse(lm) : NaN;
|
|
239
|
+
return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
240
|
+
}
|
|
228
241
|
async deleteBlobs(hashes) {
|
|
229
242
|
if (hashes.length === 0)
|
|
230
243
|
return 0;
|
|
@@ -33,6 +33,9 @@ export interface AttachmentBytesStore {
|
|
|
33
33
|
hash: string;
|
|
34
34
|
lastModifiedMs: number;
|
|
35
35
|
}>;
|
|
36
|
+
headObject?(hash: string): Promise<{
|
|
37
|
+
lastModifiedMs: number;
|
|
38
|
+
} | undefined>;
|
|
36
39
|
}
|
|
37
40
|
export declare function materializedRelPaths(atts: TaskAttachmentRecord[]): Map<string, string>;
|
|
38
41
|
export declare function bindAttachmentsForTask(opts: {
|
|
@@ -85,6 +88,7 @@ export declare class TiDBTaskAttachmentStore implements TaskAttachmentStore {
|
|
|
85
88
|
reapUnbound(olderThanMs: number): Promise<number>;
|
|
86
89
|
sweepOrphanObjects(graceMs: number, nowMs?: number): Promise<number>;
|
|
87
90
|
private referencedShas;
|
|
91
|
+
private filterStillOrphan;
|
|
88
92
|
}
|
|
89
93
|
export declare class PgTaskAttachmentStore implements TaskAttachmentStore {
|
|
90
94
|
private readonly q;
|
|
@@ -103,5 +107,6 @@ export declare class PgTaskAttachmentStore implements TaskAttachmentStore {
|
|
|
103
107
|
reapUnbound(olderThanMs: number): Promise<number>;
|
|
104
108
|
sweepOrphanObjects(graceMs: number, nowMs?: number): Promise<number>;
|
|
105
109
|
private referencedShas;
|
|
110
|
+
private filterStillOrphan;
|
|
106
111
|
}
|
|
107
112
|
//# sourceMappingURL=task-attachment-store.d.ts.map
|
|
@@ -173,8 +173,9 @@ export class TiDBTaskAttachmentStore {
|
|
|
173
173
|
const batch = candidates.slice(i, i + 200);
|
|
174
174
|
const referenced = await this.referencedShas(batch);
|
|
175
175
|
const orphans = batch.filter((h) => !referenced.has(h));
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const safe = await this.filterStillOrphan(orphans, graceMs, nowMs);
|
|
177
|
+
if (safe.length > 0)
|
|
178
|
+
removed += await this.bytes.deleteBlobs(safe);
|
|
178
179
|
}
|
|
179
180
|
return removed;
|
|
180
181
|
}
|
|
@@ -185,6 +186,24 @@ export class TiDBTaskAttachmentStore {
|
|
|
185
186
|
const [rows] = await this.pool.query(`SELECT DISTINCT sha256 FROM ${TASK_ATTACHMENT_TABLE} WHERE sha256 IN (${ph})`, shas);
|
|
186
187
|
return new Set(rows.map((r) => String(r.sha256)));
|
|
187
188
|
}
|
|
189
|
+
async filterStillOrphan(hashes, graceMs, nowMs) {
|
|
190
|
+
if (hashes.length === 0)
|
|
191
|
+
return [];
|
|
192
|
+
const head = this.bytes.headObject?.bind(this.bytes);
|
|
193
|
+
if (!head)
|
|
194
|
+
return [];
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const h of hashes) {
|
|
197
|
+
try {
|
|
198
|
+
const cur = await head(h);
|
|
199
|
+
if (cur && nowMs - cur.lastModifiedMs >= graceMs)
|
|
200
|
+
out.push(h);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
188
207
|
}
|
|
189
208
|
export class PgTaskAttachmentStore {
|
|
190
209
|
q;
|
|
@@ -260,8 +279,9 @@ export class PgTaskAttachmentStore {
|
|
|
260
279
|
const batch = candidates.slice(i, i + 200);
|
|
261
280
|
const referenced = await this.referencedShas(batch);
|
|
262
281
|
const orphans = batch.filter((h) => !referenced.has(h));
|
|
263
|
-
|
|
264
|
-
|
|
282
|
+
const safe = await this.filterStillOrphan(orphans, graceMs, nowMs);
|
|
283
|
+
if (safe.length > 0)
|
|
284
|
+
removed += await this.bytes.deleteBlobs(safe);
|
|
265
285
|
}
|
|
266
286
|
return removed;
|
|
267
287
|
}
|
|
@@ -272,5 +292,23 @@ export class PgTaskAttachmentStore {
|
|
|
272
292
|
const r = await this.q(`SELECT DISTINCT sha256 FROM ${TASK_ATTACHMENT_TABLE} WHERE sha256 IN (${ph})`, shas);
|
|
273
293
|
return new Set(r.rows.map((row) => String(row.sha256)));
|
|
274
294
|
}
|
|
295
|
+
async filterStillOrphan(hashes, graceMs, nowMs) {
|
|
296
|
+
if (hashes.length === 0)
|
|
297
|
+
return [];
|
|
298
|
+
const head = this.bytes.headObject?.bind(this.bytes);
|
|
299
|
+
if (!head)
|
|
300
|
+
return [];
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const h of hashes) {
|
|
303
|
+
try {
|
|
304
|
+
const cur = await head(h);
|
|
305
|
+
if (cur && nowMs - cur.lastModifiedMs >= graceMs)
|
|
306
|
+
out.push(h);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
275
313
|
}
|
|
276
314
|
//# sourceMappingURL=task-attachment-store.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.304.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",
|