@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 CHANGED
@@ -164,6 +164,7 @@ export interface ServiceConfig {
164
164
  };
165
165
  snapshotBlobSqlMaxBytes?: number;
166
166
  snapshotBlobAllowSql: boolean;
167
+ attachmentOrphanGraceMs: number;
167
168
  workspaceFileMaxBytes: number;
168
169
  sendUserFile?: {
169
170
  endpoint: string;
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
  ? {
@@ -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, { sessionId: wsSession, snapshots, ...(keys.length > 0 ? { latest: keys[0] } : {}), total: keys.length });
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((r) => res.once("drain", r));
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 = makeWebFetchSummarizer(brain, coreResolveTaskModel({ modelRole: "summarize" }, { models: config.models, roles: config.roles }).model);
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
- void taskAttachmentStore?.sweepOrphanObjects?.(3_600_000)
1444
- .then((n) => { if (n > 0) {
1445
- metrics.inc("attachment_orphan_objects_swept_total", {}, n);
1446
- logger.info("attachment_orphan_objects_swept", { removed: n });
1447
- } })
1448
- .catch((err) => logger.warn("attachment_orphan_sweep_failed", { err: String(err) }));
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
- if (orphans.length > 0)
177
- removed += await this.bytes.deleteBlobs(orphans);
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
- if (orphans.length > 0)
264
- removed += await this.bytes.deleteBlobs(orphans);
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.302.0",
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",