@sema-agent/server 7.79.0 → 7.79.2

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/main.js CHANGED
@@ -28,6 +28,7 @@ import { createToolTracer, createPermissionDeniedMeter } from "./observability/t
28
28
  import { createRegistryJwtVerifier } from "./auth-bridge.js";
29
29
  import { createMetrics } from "./observability/metrics.js";
30
30
  import { installFailOpenRecorder } from "./observability/fail-open.js";
31
+ import { installFileHistoryFailureRecorder } from "./file-history-disclosure.js";
31
32
  import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
32
33
  import { setRunTerminalObserver } from "./observability/run-terminal-log.js";
33
34
  import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
@@ -96,6 +97,7 @@ async function main() {
96
97
  const metrics = createMetrics();
97
98
  installFailOpenRecorder({ logger, metrics });
98
99
  installSecretEnvScrubRecorder({ logger, metrics });
100
+ installFileHistoryFailureRecorder({ logger, metrics });
99
101
  logConfigDiagnostics(logger);
100
102
  for (const w of drainNumEnvWarnings())
101
103
  logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
@@ -221,7 +221,7 @@ export function createMetrics() {
221
221
  m.counter("degraded_total", "Tasks degraded to a cheaper model (1.40), by reason (budget/rate_limit/breaker_open)");
222
222
  m.counter("task_model_not_allowlisted_resume_total", "Resumed legs whose model/compactionModel is outside the @-mention allowlist (allowed to keep running by design — the allowlist gates fresh picks only)");
223
223
  m.counter("prompt_constitution_total", "Prompt assembly constitution mode per task (core 1.243 提示词主权): steady state = core; provider-assembled/replaced/legacy warrant eyes");
224
- m.counter("rewind_snapshot_failed_total", "Per-turn rewind file snapshots that failed, by code (too_large = big working tree, expected/benign; other codes warrant eyes)");
224
+ m.counter("rewind_snapshot_failed_total", "Per-turn rewind file snapshots that failed, by the engine's FileHistoryResult.error.code (snapshot_failed/aborted/…; `unknown` = the disclosure text carried no readable code). COARSE on purpose — whether a failure is permanent (the same file will fail every turn until the deployment changes) is counted by file_history_permanent_failure_total instead, from the typed error object rather than from prose");
225
225
  m.counter("verifications_total", "Tasks run through the verification gate (1.44), by final verdict");
226
226
  m.counter("cascade_total", "Tasks run through the model cascade (1.45), by outcome (passed/exhausted)");
227
227
  m.counter("degenerate_total", "Degenerate-output failures (1.59), by salvage class (a/b/unknown)");
@@ -4,6 +4,7 @@ import { SqlBlobBackend } from "./blob-backend.js";
4
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
5
  import { isMissingColumnError } from "./sql-errors.js";
6
6
  import { recordFailOpen } from "../observability/fail-open.js";
7
+ import { discloseCaughtFailure } from "../file-history-disclosure.js";
7
8
  export const FILE_HISTORY_BLOB_TABLE = "file_history_blob";
8
9
  export const FILE_HISTORY_BLOB_KEY_PREFIX = "file-history/";
9
10
  const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
@@ -116,7 +117,7 @@ export class SqlFileHistoryStore {
116
117
  return { ok: false, error: { code: "track_failed", message: `first-touch record for ${absPath} kept racing a concurrent retraction — retry` } };
117
118
  }
118
119
  catch (err) {
119
- return { ok: false, error: { code: "track_failed", message: `first-touch record for ${absPath} could not be persisted: ${err.message}` } };
120
+ return { ok: false, error: { code: "track_failed", message: `first-touch record for ${absPath} could not be persisted: ${discloseCaughtFailure(err)}` } };
120
121
  }
121
122
  }
122
123
  async annulIneligibleReason(scope, keyHash, absPath) {
@@ -189,7 +190,7 @@ export class SqlFileHistoryStore {
189
190
  return { ok: false, error: { code: "conflict", message: `first-touch record for ${absPath} was replaced while this retraction was being proven` } };
190
191
  }
191
192
  catch (err) {
192
- return { ok: false, error: { code: "track_failed", message: `annulling the first-touch record for ${absPath} failed: ${err.message}` } };
193
+ return { ok: false, error: { code: "track_failed", message: `annulling the first-touch record for ${absPath} failed: ${discloseCaughtFailure(err)}` } };
193
194
  }
194
195
  }
195
196
  async loadTracked(exec, scope, forUpdate = false) {
@@ -296,7 +297,7 @@ export class SqlFileHistoryStore {
296
297
  }
297
298
  }
298
299
  catch (err) {
299
- return { ok: false, error: { code: "snapshot_failed", message: err.message } };
300
+ return { ok: false, error: { code: "snapshot_failed", message: discloseCaughtFailure(err) } };
300
301
  }
301
302
  this.activeAttempts.add(lease);
302
303
  try {
@@ -398,7 +399,7 @@ export class SqlFileHistoryStore {
398
399
  catch (err) {
399
400
  await conn.rollback().catch(() => undefined);
400
401
  this.spentAttempts.add(lease);
401
- return { ok: false, error: { code: "snapshot_failed", message: `boundary for entry "${entryId}" could not be committed: ${err.message}` } };
402
+ return { ok: false, error: { code: "snapshot_failed", message: `boundary for entry "${entryId}" could not be committed: ${discloseCaughtFailure(err)}` } };
402
403
  }
403
404
  finally {
404
405
  conn.release();
@@ -408,7 +409,7 @@ export class SqlFileHistoryStore {
408
409
  }
409
410
  catch (err) {
410
411
  this.spentAttempts.add(lease);
411
- return { ok: false, error: { code: "snapshot_failed", message: err.message } };
412
+ return { ok: false, error: { code: "snapshot_failed", message: discloseCaughtFailure(err) } };
412
413
  }
413
414
  finally {
414
415
  this.activeAttempts.delete(lease);
@@ -475,7 +476,7 @@ export class SqlFileHistoryStore {
475
476
  });
476
477
  }
477
478
  catch (err) {
478
- applied = { outcome: "failed", reason: err.message };
479
+ applied = { outcome: "failed", reason: discloseCaughtFailure(err) };
479
480
  }
480
481
  if (applied.outcome === "applied")
481
482
  ledger.filesChanged.push(t.trackKey);
@@ -492,7 +493,7 @@ export class SqlFileHistoryStore {
492
493
  }
493
494
  catch (err) {
494
495
  const touched = empty.filesChanged.length > 0 || empty.identicalSkipped.length > 0 || empty.refused.length > 0 || empty.failed.length > 0;
495
- return { outcome: touched ? "partial" : "failed", ...empty, error: { code: "restore_failed", message: err.message } };
496
+ return { outcome: touched ? "partial" : "failed", ...empty, error: { code: "restore_failed", message: discloseCaughtFailure(err) } };
496
497
  }
497
498
  }
498
499
  async restorePlan(scope, entryId) {
@@ -593,7 +594,7 @@ export class SqlFileHistoryStore {
593
594
  return { ok: true };
594
595
  }
595
596
  catch (err) {
596
- return { ok: false, error: { code: "snapshot_failed", message: `putBlob ${hash} write/upload failed: ${err.message}` } };
597
+ return { ok: false, error: { code: "snapshot_failed", message: `putBlob ${hash} write/upload failed: ${discloseCaughtFailure(err)}` } };
597
598
  }
598
599
  }
599
600
  async getBlob(hash) {
@@ -636,7 +637,7 @@ export class SqlFileHistoryStore {
636
637
  }
637
638
  }
638
639
  catch (err) {
639
- return { ok: false, error: { code: "conflict", message: `adoptScope failed: ${err.message}` } };
640
+ return { ok: false, error: { code: "conflict", message: `adoptScope failed: ${discloseCaughtFailure(err)}` } };
640
641
  }
641
642
  }
642
643
  async scopeEmpty(exec, scope, forUpdate = false) {
@@ -735,7 +736,7 @@ export class SqlFileHistoryStore {
735
736
  bytes = await srcGetBlob(hash);
736
737
  }
737
738
  catch (err) {
738
- return { ok: false, error: { code: "unsupported_format", message: `source blob ${hash} fetch failed: ${err.message}` } };
739
+ return { ok: false, error: { code: "unsupported_format", message: `source blob ${hash} fetch failed: ${discloseCaughtFailure(err)}` } };
739
740
  }
740
741
  if (bytes === undefined)
741
742
  return { ok: false, error: { code: "not_found", message: `source blob ${hash} is missing — the envelope's graph cannot be completed, so nothing is published` } };
@@ -786,7 +787,7 @@ export class SqlFileHistoryStore {
786
787
  }
787
788
  }
788
789
  catch (err) {
789
- return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${err.message}` } };
790
+ return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${discloseCaughtFailure(err)}` } };
790
791
  }
791
792
  }
792
793
  }
@@ -51,6 +51,15 @@ import { stripFormatCharacters } from "@sema-agent/core";
51
51
  * 现役消费方:`src/tool-approval.ts` 的 `durableRuleMaterial`(规则车道素材闸)与语料的 FP=0 格。
52
52
  */
53
53
  export declare const normalizeForRedaction: typeof stripFormatCharacters;
54
+ export declare function armPoolBudgetForTest(): {
55
+ spanCount: number;
56
+ coveredPositions: number;
57
+ headScreenHits: number;
58
+ fullYCalls: number;
59
+ armCount: number;
60
+ boundaryArmCount: number;
61
+ };
62
+ export declare function resetArmPoolBudgetForTest(): void;
54
63
  /** 测试可见:扫描器 indexOf 实际扫过的字节数(摊还线性 ⇒ ≤ 常数 × 输入长)。 */
55
64
  export declare function ppkScanBytesForTest(): number;
56
65
  export declare function resetPpkScanBytesForTest(): void;
@@ -206,7 +206,7 @@ function forgetEscapedSeparatorTails() {
206
206
  escapedTailsValue = undefined;
207
207
  }
208
208
  const VALUE_ARM_STICKY = new Map();
209
- const VALUE_ARMS = new Set();
209
+ const POOL_ONLY_ARMS = new Set();
210
210
  const VALUE_ARM_BODIES = [];
211
211
  const valueArm = (prefixSrc, valueClass, min, evidence) => {
212
212
  const body = `${prefixSrc}${twoStrongAhead(valueClass)}${atLeast(valueClass, min)}`;
@@ -226,29 +226,142 @@ const valueArm = (prefixSrc, valueClass, min, evidence) => {
226
226
  }
227
227
  }
228
228
  })();
229
- VALUE_ARMS.add(g);
229
+ POOL_ONLY_ARMS.add(g);
230
230
  return g;
231
231
  };
232
232
  let anyValueArm;
233
233
  function anyValueArmRe() {
234
234
  return (anyValueArm ??= new RegExp(`(?:${VALUE_ARM_BODIES.join("|")})`, "y"));
235
235
  }
236
+ const ARM_RANK = { keyword: 1, shape: 2, fragment: 3 };
237
+ let poolSpanCount = 0;
238
+ let poolArmCount = 0;
239
+ let poolBoundaryArmCount = 0;
240
+ let coveredPositions = 0;
241
+ let headScreenHits = 0;
242
+ let fullYCalls = 0;
243
+ export function armPoolBudgetForTest() {
244
+ return { spanCount: poolSpanCount, coveredPositions, headScreenHits, fullYCalls, armCount: poolArmCount, boundaryArmCount: poolBoundaryArmCount };
245
+ }
246
+ export function resetArmPoolBudgetForTest() {
247
+ poolSpanCount = 0;
248
+ coveredPositions = 0;
249
+ headScreenHits = 0;
250
+ fullYCalls = 0;
251
+ }
252
+ const unitBit = (t, c) => (t[c >>> 5] >>> (c & 31)) & 1;
253
+ let codeUnitDomain;
254
+ const codeUnitMembersCache = new Map();
255
+ function codeUnitMembers(unitSrc) {
256
+ let table = codeUnitMembersCache.get(unitSrc);
257
+ if (table === undefined) {
258
+ if (codeUnitDomain === undefined) {
259
+ const parts = [];
260
+ for (let c = 0; c < 0x10000; c++)
261
+ parts.push(String.fromCharCode(c));
262
+ codeUnitDomain = parts.join("");
263
+ }
264
+ table = new Uint32Array(0x10000 >>> 5);
265
+ const re = new RegExp(unitSrc, "g");
266
+ let members = 0;
267
+ for (let m = re.exec(codeUnitDomain); m !== null; m = re.exec(codeUnitDomain)) {
268
+ if (m[0].length !== 1)
269
+ throw new Error(`redact: /${unitSrc}/ is not a single-code-unit matcher`);
270
+ table[m.index >>> 5] = table[m.index >>> 5] | (1 << (m.index & 31));
271
+ members += 1;
272
+ re.lastIndex = m.index + 1;
273
+ }
274
+ if (members === 0)
275
+ throw new Error(`redact: /${unitSrc}/ matches no code unit — it cannot serve as a member set`);
276
+ codeUnitMembersCache.set(unitSrc, table);
277
+ }
278
+ return table;
279
+ }
280
+ function unitsDisjoint(a, b) {
281
+ const ta = codeUnitMembers(a);
282
+ const tb = codeUnitMembers(b);
283
+ for (let i = 0; i < ta.length; i++)
284
+ if ((ta[i] & tb[i]) !== 0)
285
+ return false;
286
+ return true;
287
+ }
288
+ function unionMembers(tables) {
289
+ const out = new Uint32Array(0x10000 >>> 5);
290
+ for (const t of tables) {
291
+ if (t === undefined)
292
+ return undefined;
293
+ for (let i = 0; i < out.length; i++)
294
+ out[i] = out[i] | t[i];
295
+ }
296
+ return out;
297
+ }
236
298
  const HEAD_CLASS_ESCAPES = new Set(["d", "D", "w", "W", "s", "S"]);
237
299
  const HEAD_LITERAL_ESCAPES = new Set(["\\", "/", "-", ".", "+", "*", "?", "(", ")", "[", "]", "{", "}", "|", "^", "$"]);
300
+ function assertWall(atoms, w, src) {
301
+ const wall = atoms[w];
302
+ const fail = (why) => {
303
+ throw new Error(`redact: head derivation cannot bound the unbounded head atom at ${w} in /${src}/ — ${why}`);
304
+ };
305
+ if (wall.unit === undefined)
306
+ fail("the wall is not a single-code-unit atom");
307
+ const wallUnit = wall.unit;
308
+ const firstUnit = atoms[0].unit;
309
+ for (let k = 0; k < w; k++) {
310
+ const a = atoms[k];
311
+ if (a.unit === undefined || a.unbounded)
312
+ fail(`the head atom at ${k} is not a bounded single-code-unit atom`);
313
+ if (!unitsDisjoint(a.unit, wallUnit))
314
+ fail(`the head atom at ${k} can hold the wall's own character`);
315
+ if (k > 0 && (firstUnit === undefined || !unitsDisjoint(a.unit, firstUnit)))
316
+ fail(`the head overlaps itself at ${k}`);
317
+ }
318
+ for (let k = w + 1; k < atoms.length; k++) {
319
+ const a = atoms[k];
320
+ if (a.unit === undefined || !unitsDisjoint(a.unit, wallUnit))
321
+ fail(`the atom at ${k} can hold the wall's own character`);
322
+ }
323
+ }
238
324
  function headOfAtoms(atoms, src) {
239
- const tail = atoms[atoms.length - 1];
240
- if (tail === undefined || !tail.unbounded || tail.cls === undefined) {
241
- throw new Error(`redact: arm-pool pattern /${src}/ must end in a greedy unbounded single-class value run`);
325
+ const tailForm = () => {
326
+ throw new Error(`redact: arm-pool pattern /${src}/ must end in a greedy unbounded single-class value run (optionally followed by a bounded literal tail run disjoint from it)`);
327
+ };
328
+ let vi = atoms.length - 1;
329
+ const last = atoms[vi];
330
+ if (last === undefined)
331
+ tailForm();
332
+ if (last.cls === undefined) {
333
+ const prev = atoms[vi - 1];
334
+ if (last.unit === undefined || prev === undefined || !prev.unbounded || prev.cls === undefined || !unitsDisjoint(last.unit, prev.cls))
335
+ tailForm();
336
+ vi -= 1;
242
337
  }
338
+ const value = atoms[vi];
339
+ if (value === undefined || !value.unbounded || value.cls === undefined)
340
+ tailForm();
341
+ const cls = value.cls;
243
342
  let pos = 0;
244
343
  let head = 0;
245
- for (let k = 0; k < atoms.length - 1; k++) {
344
+ const parts = [];
345
+ for (let k = 0; k < vi; k++) {
246
346
  const a = atoms[k];
247
- if (a.cls !== tail.cls)
347
+ if (a.unbounded) {
348
+ assertWall(atoms, k, src);
349
+ return { len: pos + 1, head: undefined, first: undefined };
350
+ }
351
+ parts.push(a.src);
352
+ if (a.cls !== cls)
248
353
  head = pos + a.len;
249
354
  pos += a.len;
250
355
  }
251
- return head;
356
+ return { len: head, head: parts.length === 0 ? undefined : parts.join(""), first: firstOfAtoms(atoms.slice(0, vi)) };
357
+ }
358
+ function firstOfAtoms(atoms) {
359
+ for (const a of atoms) {
360
+ if (a.len === 0)
361
+ continue;
362
+ return a.first;
363
+ }
364
+ return undefined;
252
365
  }
253
366
  const HEAD_GROUP_OPEN = /^\((\?:|\?=|\?!|\?<=|\?<!)/;
254
367
  function parseHeadAlternative(src, from) {
@@ -261,6 +374,7 @@ function parseHeadAlternative(src, from) {
261
374
  break;
262
375
  if (closed)
263
376
  throw new Error(`redact: arm-pool pattern /${src}/ has trailing syntax after its value run`);
377
+ const atomFrom = i;
264
378
  let base;
265
379
  if (c === "(") {
266
380
  const open = HEAD_GROUP_OPEN.exec(src.slice(i));
@@ -278,7 +392,7 @@ function parseHeadAlternative(src, from) {
278
392
  closed = true;
279
393
  continue;
280
394
  }
281
- base = { len: Math.max(...inner.alts.map((alt) => alt.reduce((n, a) => n + a.len, 0))), cls: undefined };
395
+ base = { len: Math.max(...inner.alts.map((alt) => alt.reduce((n, a) => n + a.len, 0))), cls: undefined, unit: undefined, first: unionMembers(inner.alts.map(firstOfAtoms)) };
282
396
  }
283
397
  else if (c === "[") {
284
398
  let k = i + 1;
@@ -286,7 +400,7 @@ function parseHeadAlternative(src, from) {
286
400
  k += src[k] === "\\" ? 2 : 1;
287
401
  if (k >= src.length)
288
402
  throw new Error(`redact: unterminated character class at ${i} in /${src}/`);
289
- base = { len: 1, cls: src.slice(i, k + 1) };
403
+ base = { len: 1, cls: src.slice(i, k + 1), unit: src.slice(i, k + 1), first: codeUnitMembers(src.slice(i, k + 1)) };
290
404
  i = k + 1;
291
405
  }
292
406
  else if (c === "\\") {
@@ -294,9 +408,9 @@ function parseHeadAlternative(src, from) {
294
408
  if (e === undefined)
295
409
  throw new Error(`redact: dangling escape at ${i} in /${src}/`);
296
410
  if (HEAD_CLASS_ESCAPES.has(e))
297
- base = { len: 1, cls: `\\${e}` };
411
+ base = { len: 1, cls: `\\${e}`, unit: `\\${e}`, first: codeUnitMembers(`\\${e}`) };
298
412
  else if (HEAD_LITERAL_ESCAPES.has(e))
299
- base = { len: 1, cls: undefined };
413
+ base = { len: 1, cls: undefined, unit: `\\${e}`, first: codeUnitMembers(`\\${e}`) };
300
414
  else
301
415
  throw new Error(`redact: head derivation cannot bound escape \\${e} at ${i} in /${src}/`);
302
416
  i += 2;
@@ -305,16 +419,17 @@ function parseHeadAlternative(src, from) {
305
419
  throw new Error(`redact: head derivation cannot bound ${JSON.stringify(c)} at ${i} in /${src}/`);
306
420
  }
307
421
  else {
308
- base = { len: 1, cls: undefined };
422
+ base = { len: 1, cls: undefined, unit: c, first: codeUnitMembers(c) };
309
423
  i += 1;
310
424
  }
311
- let atom = { ...base, unbounded: false };
425
+ let atom = { ...base, unbounded: false, src: "" };
312
426
  const q = src[i];
313
427
  if (q === "*" || q === "+") {
314
- atom = { ...base, unbounded: true };
428
+ atom = { ...base, unbounded: true, src: "" };
315
429
  i += 1;
316
430
  }
317
431
  else if (q === "?") {
432
+ atom = { ...base, unbounded: false, src: "", first: undefined };
318
433
  i += 1;
319
434
  }
320
435
  else if (q === "{") {
@@ -323,15 +438,14 @@ function parseHeadAlternative(src, from) {
323
438
  throw new Error(`redact: head derivation cannot bound quantifier at ${i} in /${src}/`);
324
439
  const lo = Number(m[1]);
325
440
  const hi = m[2] === undefined ? lo : m[3] === "" ? -1 : Number(m[3]);
326
- atom = hi < 0 ? { ...base, unbounded: true } : { len: base.len * hi, cls: base.cls, unbounded: false };
441
+ atom = hi < 0 ? { ...base, unbounded: true, src: "" } : { len: base.len * hi, cls: base.cls, unit: base.unit, unbounded: false, src: "", first: lo === 0 ? undefined : base.first };
327
442
  i += m[0].length;
328
443
  }
329
444
  if (src[i] === "?")
330
445
  throw new Error(`redact: head derivation cannot bound a lazy quantifier at ${i} in /${src}/`);
331
- if (atom.unbounded)
332
- closed = true;
446
+ const withSrc = { ...atom, src: src.slice(atomFrom, i) };
333
447
  for (const s of seqs)
334
- s.push(atom);
448
+ s.push(withSrc);
335
449
  }
336
450
  return { seqs, next: i };
337
451
  }
@@ -348,39 +462,35 @@ function parseHeadAlternatives(src, from) {
348
462
  }
349
463
  return { alts, next: i };
350
464
  }
351
- function literalHeadLength(re) {
465
+ function deriveLiteralHead(re) {
352
466
  if (/[uv]/.test(re.flags))
353
467
  throw new Error(`redact: head derivation does not support the u/v flags on /${re.source}/`);
354
468
  const src = re.source;
355
469
  const parsed = parseHeadAlternatives(src, 0);
356
470
  if (parsed.next !== src.length)
357
471
  throw new Error(`redact: head derivation stopped at ${parsed.next} in /${src}/`);
358
- return Math.max(...parsed.alts.map((alt) => headOfAtoms(alt, src)));
472
+ const per = parsed.alts.map((alt) => headOfAtoms(alt, src));
473
+ const heads = per.map((p) => p.head);
474
+ return {
475
+ len: Math.max(...per.map((p) => p.len)),
476
+ src: heads.some((h) => h === undefined) ? undefined : `(?:${heads.join("|")})`,
477
+ first: unionMembers(per.map((p) => p.first)),
478
+ };
359
479
  }
360
480
  function scanResumeAfterHit(start, end, head) {
361
481
  return Math.max(start + 1, end - Math.max(head - 1, 0));
362
482
  }
363
- function headWindowCandidates(at, maxHead) {
364
- const out = [];
365
- let last = -1;
366
- for (const e of at) {
367
- for (let p = Math.max(e - Math.max(maxHead - 1, 0), last + 1, 0); p <= e; p++) {
368
- out.push({ p, back: e - p });
369
- last = p;
370
- }
371
- }
372
- return out;
373
- }
374
- function collectRegexSpans(g, head, repl, order, text, out) {
483
+ function collectRegexSpans(a, text, out) {
484
+ const g = a.g;
375
485
  g.lastIndex = 0;
376
486
  for (let m = g.exec(text); m !== null; m = g.exec(text)) {
377
487
  const end = m.index + m[0].length;
378
- out.push({ start: m.index, end, repl, order });
379
- g.lastIndex = scanResumeAfterHit(m.index, end, head);
488
+ out.push({ start: m.index, end, repl: a.repl, rank: a.rank, order: a.order });
489
+ g.lastIndex = scanResumeAfterHit(m.index, end, a.head);
380
490
  }
381
491
  }
382
492
  function resolveSpanOverlaps(spans) {
383
- spans.sort((a, b) => a.start - b.start || b.end - a.end || a.order - b.order);
493
+ spans.sort((a, b) => a.start - b.start || b.end - a.end || a.rank - b.rank || a.order - b.order);
384
494
  const kept = [];
385
495
  let covered = 0;
386
496
  for (const s of spans) {
@@ -403,10 +513,11 @@ function applySpans(text, spans) {
403
513
  parts.push(text.slice(copied));
404
514
  return { out: parts.join(""), count: spans.length };
405
515
  }
406
- function collectValueArmSpans(g, y, head, repl, order, text, out) {
516
+ function collectValueArmSpans(arm, text, out) {
517
+ const { g, y, head, repl, rank, order } = arm;
407
518
  const tails = escapedSeparatorTails(text);
408
519
  if (tails === undefined) {
409
- collectRegexSpans(g, head, repl, order, text, out);
520
+ collectRegexSpans(arm, text, out);
410
521
  return;
411
522
  }
412
523
  let pos = 0;
@@ -451,29 +562,33 @@ function collectValueArmSpans(g, y, head, repl, order, text, out) {
451
562
  }
452
563
  else
453
564
  break;
454
- out.push({ start: idx, end, repl, order });
565
+ out.push({ start: idx, end, repl, rank, order });
455
566
  pos = scanResumeAfterHit(idx, end, head);
456
567
  }
457
568
  }
458
- function collectStickySpans(y, head, repl, order, text, at, out) {
569
+ function collectStickySpans(a, text, at, out) {
570
+ const { y, head, repl, rank, order } = a;
459
571
  let pos = 0;
460
- for (const { p, back } of at) {
461
- if (back >= head)
462
- continue;
572
+ for (const p of at) {
463
573
  if (p < pos)
464
574
  continue;
465
575
  y.lastIndex = p;
576
+ fullYCalls += 1;
466
577
  const m = y.exec(text);
467
578
  if (m === null)
468
579
  continue;
469
580
  const end = p + m[0].length;
470
- out.push({ start: p, end, repl, order });
581
+ out.push({ start: p, end, repl, rank, order });
471
582
  pos = scanResumeAfterHit(p, end, head);
472
583
  }
473
584
  }
474
585
  const WORD_SEGMENT_CLASS = "[A-Za-z0-9._~+/-]";
475
- const keywordArm = (keywordSrc, valueClass, min, tailSrc = "") => new RegExp(`(?:${keywordSrc}\\s+${atLeast(valueClass, min)}${tailSrc}` +
476
- `|(?<!${WORD_SEGMENT_CLASS})${keywordSrc}(?:${twoStrongAhead(valueClass)}|${noConnectorAhead(valueClass)})${atLeast(valueClass, min)}${tailSrc})`, "g");
586
+ const keywordArm = (keywordSrc, valueClass, min, tailSrc = "") => {
587
+ const re = new RegExp(`(?:${keywordSrc}\\s+${atLeast(valueClass, min)}${tailSrc}` +
588
+ `|(?<!${WORD_SEGMENT_CLASS})${keywordSrc}(?:${twoStrongAhead(valueClass)}|${noConnectorAhead(valueClass)})${atLeast(valueClass, min)}${tailSrc})`, "g");
589
+ POOL_ONLY_ARMS.add(re);
590
+ return re;
591
+ };
477
592
  const STRUCTURAL_SECRET_PATTERNS = [
478
593
  [/-----BEGIN(?:[^-](?:[^-](?:[^-]|-(?!-))*)?)?PRIVATE KEY(?: BLOCK)?-----[\s\S]{0,8192}?-----END(?:[^-](?:[^-](?:[^-]|-(?!-))*)?)?PRIVATE KEY(?: BLOCK)?-----/g, "«redacted:private-key»"],
479
594
  [/---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----[\s\S]{0,8192}?---- END SSH2 ENCRYPTED PRIVATE KEY ----/g, "«redacted:private-key»"],
@@ -926,7 +1041,7 @@ const regexRule = ([re, repl]) => {
926
1041
  }
927
1042
  const expand = refs.length === 0 ? undefined : (args) => repl.replace(/\$([1-9])/g, (_, d) => args[Number(d)] ?? "");
928
1043
  const arm = armOfToken(repl);
929
- if (VALUE_ARMS.has(re) || arm === "fragment")
1044
+ if (POOL_ONLY_ARMS.has(re) || arm === "fragment")
930
1045
  throw new Error(`redact: rule ${JSON.stringify(repl)} belongs in the arm pool, not a per-arm section`);
931
1046
  const label = labelOfToken(repl);
932
1047
  const applyOnce = (text, wantCount) => {
@@ -1137,45 +1252,63 @@ function redactPpkContainers(text, record) {
1137
1252
  const URL_USERINFO_LABEL = labelOfToken(URL_USERINFO_PATTERN[1]);
1138
1253
  const urlUserinfoRule = { run: (text, _wantCount, json) => singleHit(URL_USERINFO_LABEL, "full", redactUrlUserinfo(text, json)) };
1139
1254
  const armPoolRule = (() => {
1140
- const arms = [...SHAPE_SECRET_PATTERNS, ...FRAGMENT_SECRET_PATTERNS].map(([re, repl], order) => {
1255
+ const arms = [
1256
+ ...KEYWORD_SECRET_PATTERNS.map((row) => [row, ARM_RANK.keyword]),
1257
+ ...SHAPE_SECRET_PATTERNS.map((row) => [row, ARM_RANK.shape]),
1258
+ ...FRAGMENT_SECRET_PATTERNS.map((row) => [row, ARM_RANK.fragment]),
1259
+ ].map(([[re, repl], rank], order) => {
1141
1260
  if (/\$./.test(repl))
1142
1261
  throw new Error(`redact: arm-pool replacement ${JSON.stringify(repl)} must not use $N refs`);
1143
1262
  const y = VALUE_ARM_STICKY.get(re);
1144
- const head = literalHeadLength(re);
1145
- if (y !== undefined && head === 0)
1146
- throw new Error(`redact: boundary arm /${re.source}/ has a zero-length structural head — the head-window screen would skip every candidate position`);
1147
- return { g: re, y, head, repl, order };
1263
+ const head = deriveLiteralHead(re);
1264
+ if (y !== undefined && (head.len === 0 || head.src === undefined || head.first === undefined)) {
1265
+ throw new Error(`redact: boundary arm /${re.source}/ has a zero-length structural head — the bounded head screen would admit every candidate position`);
1266
+ }
1267
+ return { g: re, y, head: head.len, headSrc: head.src, headFirst: head.first, repl, rank, order };
1148
1268
  });
1149
- const boundaryArms = arms.flatMap((a) => (a.y === undefined ? [] : [{ y: a.y, head: a.head, repl: a.repl, order: a.order }]));
1150
- const maxBoundaryHead = Math.max(1, ...boundaryArms.map((a) => a.head));
1269
+ const boundaryArms = arms.flatMap((a) => (a.y === undefined || a.headSrc === undefined || a.headFirst === undefined ? [] : [{ ...a, y: a.y, headSrc: a.headSrc, headFirst: a.headFirst }]));
1270
+ const plainArms = arms.filter((a) => a.y === undefined);
1271
+ const headScreen = new RegExp(`(?:${boundaryArms.map((a) => a.headSrc).join("|")})`, "y");
1272
+ const headFirstUnit = unionMembers(boundaryArms.map((a) => a.headFirst)) ?? new Uint32Array(0x10000 >>> 5).fill(0xffffffff);
1273
+ poolArmCount = arms.length;
1274
+ poolBoundaryArmCount = boundaryArms.length;
1151
1275
  return {
1152
1276
  run: (text, wantCount) => {
1153
1277
  const spans = [];
1154
- for (const a of arms) {
1155
- if (a.y === undefined)
1156
- collectRegexSpans(a.g, a.head, a.repl, a.order, text, spans);
1157
- else
1158
- collectValueArmSpans(a.g, a.y, a.head, a.repl, a.order, text, spans);
1159
- }
1160
- const tried = new Set();
1161
- const freshEnds = (from) => {
1162
- const at = [];
1163
- for (let k = from; k < spans.length; k++) {
1164
- const e = spans[k].end;
1165
- if (tried.has(e))
1166
- continue;
1167
- tried.add(e);
1168
- at.push(e);
1278
+ for (const a of plainArms)
1279
+ collectRegexSpans(a, text, spans);
1280
+ for (const a of boundaryArms)
1281
+ collectValueArmSpans(a, text, spans);
1282
+ if (boundaryArms.length > 0 && spans.length > 0) {
1283
+ const tried = new Uint8Array(text.length + 1);
1284
+ const freshCovered = (from) => {
1285
+ const at = [];
1286
+ for (let k = from; k < spans.length; k++) {
1287
+ const s = spans[k];
1288
+ for (let p = s.start; p <= s.end; p++) {
1289
+ if (tried[p] === 1)
1290
+ continue;
1291
+ tried[p] = 1;
1292
+ coveredPositions += 1;
1293
+ if (unitBit(headFirstUnit, text.charCodeAt(p)) !== 1)
1294
+ continue;
1295
+ headScreen.lastIndex = p;
1296
+ if (headScreen.exec(text) === null)
1297
+ continue;
1298
+ at.push(p);
1299
+ }
1300
+ }
1301
+ headScreenHits += at.length;
1302
+ return at.sort((x, y) => x - y);
1303
+ };
1304
+ for (let at = freshCovered(0); at.length > 0;) {
1305
+ const before = spans.length;
1306
+ for (const a of boundaryArms)
1307
+ collectStickySpans(a, text, at, spans);
1308
+ at = freshCovered(before);
1169
1309
  }
1170
- return at.sort((x, y) => x - y);
1171
- };
1172
- for (let at = freshEnds(0); at.length > 0;) {
1173
- const before = spans.length;
1174
- const cand = headWindowCandidates(at, maxBoundaryHead);
1175
- for (const a of boundaryArms)
1176
- collectStickySpans(a.y, a.head, a.repl, a.order, text, cand, spans);
1177
- at = freshEnds(before);
1178
1310
  }
1311
+ poolSpanCount += spans.length;
1179
1312
  const kept = resolveSpanOverlaps(spans);
1180
1313
  const out = applySpans(text, kept).out;
1181
1314
  if (!wantCount || kept.length === 0)
@@ -1206,7 +1339,6 @@ const SECRET_RULES = [
1206
1339
  { run: (text, _wantCount, _json, observe) => singleHit(labelOfToken(PPK_CONTAINER_TOKEN), armOfToken(PPK_CONTAINER_TOKEN), redactPpkContainers(text, observe)) },
1207
1340
  { run: (text, _wantCount, json) => singleHit(labelOfToken(PEM_FRAGMENT_TOKEN), armOfToken(PEM_FRAGMENT_TOKEN), redactPemFragments(text, json)) },
1208
1341
  { run: (text) => singleHit(labelOfToken(HEADLESS_PPK_TOKEN), armOfToken(HEADLESS_PPK_TOKEN), redactHeadlessPpkBodies(text)) },
1209
- ...KEYWORD_SECRET_PATTERNS.map((p) => regexRule(p)),
1210
1342
  armPoolRule,
1211
1343
  { run: (text, _wantCount, json) => ({ out: redactUrlUserinfo(text, json).out, hits: NO_HITS }) },
1212
1344
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.79.0",
3
+ "version": "7.79.2",
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",