@sema-agent/server 1.303.0 → 1.305.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,7 +4262,8 @@ 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",
4252
- 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`,
4265
+ errorCode: "blob_too_large_for_sql",
4266
+ error: `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`,
4253
4267
  });
4254
4268
  return;
4255
4269
  }
@@ -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;
@@ -6807,7 +6821,9 @@ function routeLabel(_method, url) {
6807
6821
  return "other";
6808
6822
  }
6809
6823
  function workspaceContentType(relPath) {
6810
- const ext = relPath.slice(relPath.lastIndexOf(".") + 1).toLowerCase();
6824
+ const base = relPath.slice(relPath.lastIndexOf("/") + 1);
6825
+ const dot = base.lastIndexOf(".");
6826
+ const ext = dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
6811
6827
  const TABLE = {
6812
6828
  txt: "text/plain; charset=utf-8", md: "text/plain; charset=utf-8", log: "text/plain; charset=utf-8",
6813
6829
  json: "application/json; charset=utf-8", csv: "text/csv; charset=utf-8",
package/dist/http/tar.js CHANGED
@@ -19,7 +19,11 @@ export function splitTarPath(relPath) {
19
19
  }
20
20
  return null;
21
21
  }
22
+ const USTAR_MAX_SIZE = 8 * 1024 * 1024 * 1024 - 1;
22
23
  export function tarHeader(relPath, sizeBytes, mtimeSec) {
24
+ if (!Number.isFinite(sizeBytes) || sizeBytes < 0 || sizeBytes > USTAR_MAX_SIZE) {
25
+ throw new Error(`tar: size ${sizeBytes} not representable in a ustar header (12-octal-byte field, max 8GiB-1): ${relPath}`);
26
+ }
23
27
  const split = splitTarPath(relPath);
24
28
  if (split === null)
25
29
  throw new Error(`tar: path not representable in ustar (must be ≤255 bytes and splittable at "/"): ${relPath}`);
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
@@ -75,14 +75,24 @@ export async function sqlBlobSizes(dialect, pool, hashes) {
75
75
  if (hashes.length === 0)
76
76
  return new Map();
77
77
  const distinct = [...new Set(hashes)];
78
- if (dialect === "tidb") {
79
- const ph = distinct.map(() => "?").join(",");
80
- const [rows] = await pool.query(`SELECT blob_hash, byte_len FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
81
- return new Map(rows.map((r) => [String(r.blob_hash), Number(r.byte_len)]));
78
+ const CHUNK = 1000;
79
+ const out = new Map();
80
+ for (let i = 0; i < distinct.length; i += CHUNK) {
81
+ const part = distinct.slice(i, i + CHUNK);
82
+ if (dialect === "tidb") {
83
+ const ph = part.map(() => "?").join(",");
84
+ const [rows] = await pool.query(`SELECT blob_hash, byte_len FROM snapshot_blob WHERE blob_hash IN (${ph})`, part);
85
+ for (const r of rows)
86
+ out.set(String(r.blob_hash), Number(r.byte_len));
87
+ }
88
+ else {
89
+ const ph = part.map((_, j) => `$${j + 1}`).join(",");
90
+ const r = await pool.query(`SELECT blob_hash, byte_len FROM snapshot_blob WHERE blob_hash IN (${ph})`, part);
91
+ for (const row of r.rows)
92
+ out.set(row.blob_hash, Number(row.byte_len));
93
+ }
82
94
  }
83
- const ph = distinct.map((_, i) => `$${i + 1}`).join(",");
84
- const r = await pool.query(`SELECT blob_hash, byte_len FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
85
- return new Map(r.rows.map((row) => [row.blob_hash, Number(row.byte_len)]));
95
+ return out;
86
96
  }
87
97
  async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
88
98
  if (hashes.length === 0)
@@ -194,7 +204,11 @@ export class MinioBlobBackend {
194
204
  async *listObjects() {
195
205
  const f = this.cfg.fetchImpl ?? fetch;
196
206
  let token;
207
+ const MAX_PAGES = 1000;
208
+ let pages = 0;
197
209
  do {
210
+ if (++pages > MAX_PAGES)
211
+ 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
212
  const url = presignS3ListUrl({
199
213
  endpoint: this.cfg.endpoint,
200
214
  bucket: this.cfg.bucket,
@@ -225,6 +239,15 @@ export class MinioBlobBackend {
225
239
  throw new Error("listObjects: truncated response without NextContinuationToken");
226
240
  } while (token !== undefined);
227
241
  }
242
+ async headObject(hash) {
243
+ const f = this.cfg.fetchImpl ?? fetch;
244
+ const res = await f(this.presign(hash, "HEAD"), { method: "HEAD" });
245
+ if (!res.ok)
246
+ return undefined;
247
+ const lm = res.headers.get("last-modified");
248
+ const ms = lm ? Date.parse(lm) : NaN;
249
+ return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
250
+ }
228
251
  async deleteBlobs(hashes) {
229
252
  if (hashes.length === 0)
230
253
  return 0;
@@ -237,14 +260,17 @@ export class MinioBlobBackend {
237
260
  }
238
261
  if (toDelete.length === 0)
239
262
  return 0;
263
+ let okCount = 0;
240
264
  await mapBounded(toDelete, MINIO_OP_CONCURRENCY, async (hash) => {
241
265
  try {
242
- await fetch(this.presign(hash, "DELETE"), { method: "DELETE" });
266
+ const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "DELETE"), { method: "DELETE" });
267
+ if (res.ok || res.status === 404)
268
+ okCount++;
243
269
  }
244
270
  catch {
245
271
  }
246
272
  });
247
- return toDelete.length;
273
+ return okCount;
248
274
  }
249
275
  }
250
276
  //# sourceMappingURL=blob-backend.js.map
@@ -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
@@ -163,19 +163,26 @@ export class TiDBTaskAttachmentStore {
163
163
  const lister = this.bytes;
164
164
  if (typeof lister.listObjects !== "function")
165
165
  return 0;
166
- const candidates = [];
167
- for await (const o of lister.listObjects()) {
168
- if (nowMs - o.lastModifiedMs >= graceMs)
169
- candidates.push(o.hash);
170
- }
171
166
  let removed = 0;
172
- for (let i = 0; i < candidates.length; i += 200) {
173
- const batch = candidates.slice(i, i + 200);
167
+ let batch = [];
168
+ const flush = async () => {
169
+ if (batch.length === 0)
170
+ return;
174
171
  const referenced = await this.referencedShas(batch);
175
172
  const orphans = batch.filter((h) => !referenced.has(h));
176
- if (orphans.length > 0)
177
- removed += await this.bytes.deleteBlobs(orphans);
173
+ const safe = await this.filterStillOrphan(orphans, graceMs, nowMs);
174
+ if (safe.length > 0)
175
+ removed += await this.bytes.deleteBlobs(safe);
176
+ batch = [];
177
+ };
178
+ for await (const o of lister.listObjects()) {
179
+ if (nowMs - o.lastModifiedMs < graceMs)
180
+ continue;
181
+ batch.push(o.hash);
182
+ if (batch.length >= 200)
183
+ await flush();
178
184
  }
185
+ await flush();
179
186
  return removed;
180
187
  }
181
188
  async referencedShas(shas) {
@@ -185,6 +192,24 @@ export class TiDBTaskAttachmentStore {
185
192
  const [rows] = await this.pool.query(`SELECT DISTINCT sha256 FROM ${TASK_ATTACHMENT_TABLE} WHERE sha256 IN (${ph})`, shas);
186
193
  return new Set(rows.map((r) => String(r.sha256)));
187
194
  }
195
+ async filterStillOrphan(hashes, graceMs, nowMs) {
196
+ if (hashes.length === 0)
197
+ return [];
198
+ const head = this.bytes.headObject?.bind(this.bytes);
199
+ if (!head)
200
+ return [];
201
+ const out = [];
202
+ for (const h of hashes) {
203
+ try {
204
+ const cur = await head(h);
205
+ if (cur && nowMs - cur.lastModifiedMs >= graceMs)
206
+ out.push(h);
207
+ }
208
+ catch {
209
+ }
210
+ }
211
+ return out;
212
+ }
188
213
  }
189
214
  export class PgTaskAttachmentStore {
190
215
  q;
@@ -250,19 +275,26 @@ export class PgTaskAttachmentStore {
250
275
  const lister = this.bytes;
251
276
  if (typeof lister.listObjects !== "function")
252
277
  return 0;
253
- const candidates = [];
254
- for await (const o of lister.listObjects()) {
255
- if (nowMs - o.lastModifiedMs >= graceMs)
256
- candidates.push(o.hash);
257
- }
258
278
  let removed = 0;
259
- for (let i = 0; i < candidates.length; i += 200) {
260
- const batch = candidates.slice(i, i + 200);
279
+ let batch = [];
280
+ const flush = async () => {
281
+ if (batch.length === 0)
282
+ return;
261
283
  const referenced = await this.referencedShas(batch);
262
284
  const orphans = batch.filter((h) => !referenced.has(h));
263
- if (orphans.length > 0)
264
- removed += await this.bytes.deleteBlobs(orphans);
285
+ const safe = await this.filterStillOrphan(orphans, graceMs, nowMs);
286
+ if (safe.length > 0)
287
+ removed += await this.bytes.deleteBlobs(safe);
288
+ batch = [];
289
+ };
290
+ for await (const o of lister.listObjects()) {
291
+ if (nowMs - o.lastModifiedMs < graceMs)
292
+ continue;
293
+ batch.push(o.hash);
294
+ if (batch.length >= 200)
295
+ await flush();
265
296
  }
297
+ await flush();
266
298
  return removed;
267
299
  }
268
300
  async referencedShas(shas) {
@@ -272,5 +304,23 @@ export class PgTaskAttachmentStore {
272
304
  const r = await this.q(`SELECT DISTINCT sha256 FROM ${TASK_ATTACHMENT_TABLE} WHERE sha256 IN (${ph})`, shas);
273
305
  return new Set(r.rows.map((row) => String(row.sha256)));
274
306
  }
307
+ async filterStillOrphan(hashes, graceMs, nowMs) {
308
+ if (hashes.length === 0)
309
+ return [];
310
+ const head = this.bytes.headObject?.bind(this.bytes);
311
+ if (!head)
312
+ return [];
313
+ const out = [];
314
+ for (const h of hashes) {
315
+ try {
316
+ const cur = await head(h);
317
+ if (cur && nowMs - cur.lastModifiedMs >= graceMs)
318
+ out.push(h);
319
+ }
320
+ catch {
321
+ }
322
+ }
323
+ return out;
324
+ }
275
325
  }
276
326
  //# sourceMappingURL=task-attachment-store.js.map
@@ -62,7 +62,7 @@ export function toolEndEventData(ev) {
62
62
  isError: ev.isError,
63
63
  ...(ev.output !== undefined ? { output: redactDeep(ev.output) } : {}),
64
64
  ...(ev.truncated ? { truncated: true } : {}),
65
- ...(typeof ev.totalChars === "number" ? { totalChars: ev.totalChars } : {}),
65
+ ...(Number.isFinite(ev.totalChars) ? { totalChars: ev.totalChars } : {}),
66
66
  ...(ev.structured !== undefined ? { structured: redactDeep(ev.structured) } : {}),
67
67
  ...identityFields(ev),
68
68
  };
@@ -241,7 +241,7 @@ export function projectEvents(events, opts = {}) {
241
241
  break;
242
242
  case "tool_end": {
243
243
  const callId = String(d.toolCallId ?? "");
244
- blocks.push({ type: "tool-result", callId, ...(typeof d.label === "string" ? { label: d.label } : {}), output: d.output, ...(d.isError ? { isError: true } : {}), ...(d.truncated ? { truncated: true } : {}), ...(typeof d.totalChars === "number" ? { totalChars: d.totalChars } : {}), ...(d.structured !== undefined ? { structured: d.structured } : {}), ...identityFields(d) });
244
+ blocks.push({ type: "tool-result", callId, ...(typeof d.label === "string" ? { label: d.label } : {}), output: d.output, ...(d.isError ? { isError: true } : {}), ...(d.truncated ? { truncated: true } : {}), ...(Number.isFinite(d.totalChars) ? { totalChars: d.totalChars } : {}), ...(d.structured !== undefined ? { structured: d.structured } : {}), ...identityFields(d) });
245
245
  break;
246
246
  }
247
247
  case "prompt_assembled":
@@ -307,7 +307,7 @@ export function mapTraceEvent(type, seq, data) {
307
307
  case "tool_start":
308
308
  return { event: "tool-call", data: { seq, id: data.toolCallId, name: data.toolName, ...(typeof data.label === "string" ? { label: data.label } : {}), input: data.args, ...identityFields(data) } };
309
309
  case "tool_end":
310
- return { event: "tool-result", data: { seq, callId: data.toolCallId, ...(typeof data.label === "string" ? { label: data.label } : {}), isError: Boolean(data.isError), ...(data.output !== undefined ? { output: data.output } : {}), ...(data.truncated ? { truncated: true } : {}), ...(typeof data.totalChars === "number" ? { totalChars: data.totalChars } : {}), ...(data.structured !== undefined ? { structured: data.structured } : {}), ...identityFields(data) } };
310
+ return { event: "tool-result", data: { seq, callId: data.toolCallId, ...(typeof data.label === "string" ? { label: data.label } : {}), isError: Boolean(data.isError), ...(data.output !== undefined ? { output: data.output } : {}), ...(data.truncated ? { truncated: true } : {}), ...(Number.isFinite(data.totalChars) ? { totalChars: data.totalChars } : {}), ...(data.structured !== undefined ? { structured: data.structured } : {}), ...identityFields(data) } };
311
311
  case "turn_end": {
312
312
  const tokens = toContractTokens(data.usage);
313
313
  return { event: "turn", data: { seq, ...(tokens ? { tokens } : {}) } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "1.303.0",
3
+ "version": "1.305.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",