@sema-agent/core 2.13.0 → 3.0.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.
Files changed (95) hide show
  1. package/dist/agents/cascade.d.ts +4 -0
  2. package/dist/agents/cascade.js +54 -9
  3. package/dist/agents/repair-loop.d.ts +2 -0
  4. package/dist/agents/repair-loop.js +101 -2
  5. package/dist/agents/retain-ledger.d.ts +18 -2
  6. package/dist/agents/retain-ledger.js +37 -8
  7. package/dist/agents/roster-store.d.ts +4 -0
  8. package/dist/agents/roster-store.js +22 -1
  9. package/dist/agents/teacher.js +4 -2
  10. package/dist/agents/team.js +7 -4
  11. package/dist/agents/verify.d.ts +15 -2
  12. package/dist/agents/verify.js +36 -1
  13. package/dist/bin/sema-tb.js +2 -2
  14. package/dist/brain/circuit-breaker.d.ts +1 -0
  15. package/dist/brain/circuit-breaker.js +26 -9
  16. package/dist/brain/degrading.js +13 -1
  17. package/dist/brain/failover.js +2 -0
  18. package/dist/brain/repetition.js +10 -1
  19. package/dist/brain/routing.js +5 -1
  20. package/dist/brain/status-sink.js +5 -1
  21. package/dist/brain/tool-call-repair.js +32 -14
  22. package/dist/core/auto-compaction.js +17 -7
  23. package/dist/core/checkpoint-store.d.ts +1 -0
  24. package/dist/core/checkpoint-store.js +28 -10
  25. package/dist/core/consolidate-scope.js +4 -3
  26. package/dist/core/context-edit.js +3 -0
  27. package/dist/core/context-guard.js +3 -0
  28. package/dist/core/git-worktree-env.d.ts +6 -1
  29. package/dist/core/git-worktree-env.js +12 -1
  30. package/dist/core/hooks.d.ts +1 -0
  31. package/dist/core/hooks.js +40 -2
  32. package/dist/core/lsp-diagnostics.js +13 -7
  33. package/dist/core/lsp-protocol.d.ts +1 -1
  34. package/dist/core/lsp-protocol.js +20 -8
  35. package/dist/core/lsp-session.d.ts +12 -2
  36. package/dist/core/lsp-session.js +114 -49
  37. package/dist/core/lsp.d.ts +13 -1
  38. package/dist/core/lsp.js +64 -9
  39. package/dist/core/memory-engine/data-plane.js +4 -0
  40. package/dist/core/memory-engine/engine.d.ts +2 -1
  41. package/dist/core/memory-engine/engine.js +82 -7
  42. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  43. package/dist/core/memory-engine/file-backend.js +13 -3
  44. package/dist/core/memory-engine/layout.js +12 -1
  45. package/dist/core/memory.js +3 -0
  46. package/dist/core/permission-rules.d.ts +2 -1
  47. package/dist/core/permission-rules.js +24 -3
  48. package/dist/core/runner/assemble-result.d.ts +1 -1
  49. package/dist/core/runner/assemble-result.js +0 -1
  50. package/dist/core/runner/prepare-task.js +11 -0
  51. package/dist/core/runner/runtask.js +21 -22
  52. package/dist/core/runner/usage-accounting.d.ts +6 -0
  53. package/dist/core/runner/usage-accounting.js +9 -8
  54. package/dist/core/secret-env.js +5 -1
  55. package/dist/core/sensitive-path-policy.js +9 -3
  56. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  57. package/dist/core/task-registry-monitor.js +7 -1
  58. package/dist/core/tool-policy.js +65 -17
  59. package/dist/core/trace.d.ts +1 -0
  60. package/dist/core/types.d.ts +1 -0
  61. package/dist/engine/compaction/compaction.d.ts +1 -0
  62. package/dist/engine/compaction/compaction.js +14 -5
  63. package/dist/engine/lsp/frame-decoder.js +6 -3
  64. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  65. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  66. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  67. package/dist/internal/harness.d.ts +1 -1
  68. package/dist/internal/harness.js +1 -1
  69. package/dist/orchestration/workflow-governance.js +43 -9
  70. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  71. package/dist/orchestration/workflow-script-store.js +34 -4
  72. package/dist/orchestration/workflow.js +2 -2
  73. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  74. package/dist/prompt-assembly/artifact-store.js +39 -24
  75. package/dist/prompt-assembly/assemble.js +38 -7
  76. package/dist/prompt-assembly/epoch.js +19 -2
  77. package/dist/prompt-assembly/event-registry.js +3 -3
  78. package/dist/prompt-assembly/explain.js +1 -1
  79. package/dist/prompt-assembly/tool-catalog.js +16 -1
  80. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  81. package/dist/stores/file/fs-atomic.d.ts +3 -0
  82. package/dist/stores/file/fs-atomic.js +53 -8
  83. package/dist/stores/file/index.d.ts +1 -0
  84. package/dist/stores/file/index.js +7 -0
  85. package/dist/stores/file/session-store.d.ts +2 -0
  86. package/dist/stores/file/session-store.js +28 -1
  87. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  88. package/dist/tools/fs/bash-readonly-classifier.js +19 -2
  89. package/dist/tools/fs/fs-bash.d.ts +1 -1
  90. package/dist/tools/fs/fs-bash.js +93 -5
  91. package/dist/tools/loop-tick.js +1 -1
  92. package/dist/tools/monitor.js +55 -9
  93. package/dist/tools/worktree.d.ts +2 -0
  94. package/dist/tools/worktree.js +233 -143
  95. package/package.json +5 -2
@@ -147,6 +147,18 @@ export class MemoryEngine {
147
147
  handle.baseIds.set(path, entry.id);
148
148
  }
149
149
  }
150
+ const indexGate = this.gateDerivedIndex(handle);
151
+ if (indexGate !== undefined) {
152
+ try {
153
+ enqueueMemoryAnnouncement(this.controlDir, {
154
+ kind: "gate",
155
+ at: this.now(),
156
+ items: [`memory index rejected and rebuilt: ${inlineUntrusted(indexGate.reason)}`],
157
+ });
158
+ }
159
+ catch {
160
+ }
161
+ }
150
162
  const indexText = this.rebuildIndex(handle, headers, { write: writeScope !== null });
151
163
  handle.indexBaselineLines = countIndexLines(indexText);
152
164
  handle.indexText = indexText;
@@ -364,6 +376,7 @@ export class MemoryEngine {
364
376
  basePathById.set(id, p);
365
377
  const patches = [];
366
378
  const pendingProjections = [];
379
+ const idsAddedThisHarvest = new Set();
367
380
  let processed = 0;
368
381
  for (let i = 0; i < kept.length; i++) {
369
382
  const f = kept[i];
@@ -465,7 +478,28 @@ export class MemoryEngine {
465
478
  patches.push({ op: "update", id: parsed.id, entry: { ...entry, id: parsed.id, rev: computeEntryRev({ id: parsed.id, frontmatter: fm, body: parsed.body }) }, ...(revByBasePath.get(renamedFrom) !== undefined ? { baseRev: revByBasePath.get(renamedFrom) } : {}) });
466
479
  continue;
467
480
  }
468
- pendingProjections.push({ path: f.canonical, entry, needed: minted || needsCompletion(parsed, fm) });
481
+ let reboundId;
482
+ if (parsed.id !== undefined && (renamedFrom !== undefined || idsAddedThisHarvest.has(parsed.id))) {
483
+ reboundId = parsed.id;
484
+ const idless = serializeEntryFile({ id: "", frontmatter: fm, body: parsed.body }).replace(/^id:[ \t]*$\r?\n/m, "");
485
+ try {
486
+ writeFileSync(f.canonical, idless, "utf8");
487
+ }
488
+ catch (err) {
489
+ report.rejections.push({
490
+ path: rel,
491
+ code: "filename",
492
+ reason: `memory write blocked: this file carries the entry id of another memory file (a copy is not a rename) and its frontmatter could not be rewritten (${err instanceof Error ? err.message : String(err)}) — remove the "id:" line so it can be committed as a new entry`,
493
+ });
494
+ continue;
495
+ }
496
+ id = uuidv7();
497
+ entry.id = id;
498
+ entry.rev = computeEntryRev(entry);
499
+ report.warnings.push(`${rel}: frontmatter id ${reboundId} already has a projection (${renamedFrom !== undefined ? relative(handle.memoryDir, renamedFrom) : "another new file in this harvest"}) — a copy is not a rename, so this file was committed as a NEW entry with a fresh id; the existing entry was left untouched`);
500
+ }
501
+ pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || needsCompletion(parsed, fm) });
502
+ idsAddedThisHarvest.add(entry.id);
469
503
  patches.push({ op: "add", id: entry.id, entry });
470
504
  }
471
505
  let patchReport;
@@ -503,12 +537,15 @@ export class MemoryEngine {
503
537
  const drained = this.backend.drainInboundFindings?.();
504
538
  if (drained !== undefined && drained.length > 0)
505
539
  report.inboundFindings = drained;
540
+ const indexRejection = this.gateDerivedIndex(handle);
541
+ if (indexRejection !== undefined)
542
+ report.rejections.push(indexRejection);
506
543
  const headers = await this.backend.listHeaders([...new Set([...handle.scopes, writeScope])]);
507
544
  handle.indexText = this.rebuildIndex(handle, headers, { write: true }, report.warnings);
508
- await this.rebaseline(handle);
545
+ await this.rebaseline(handle, new Set(report.degraded?.pending ?? []));
509
546
  return report;
510
547
  }
511
- async rebaseline(handle) {
548
+ async rebaseline(handle, keepBaseline = new Set()) {
512
549
  const writeScope = handle.writeScope;
513
550
  if (writeScope === null)
514
551
  return;
@@ -535,12 +572,17 @@ export class MemoryEngine {
535
572
  if (id === undefined)
536
573
  continue;
537
574
  const stub = stubs.get(path);
538
- const rev = revOfText(text, id);
575
+ const relPath = relative(handle.memoryDir, path);
576
+ const deferred = keepBaseline.has(relPath);
577
+ const priorRev = deferred ? handle.baseRevs.get(path) : undefined;
578
+ const priorId = deferred ? handle.baseIds.get(path) : undefined;
579
+ const rev = priorRev ?? revOfText(text, priorId ?? id);
580
+ const baselineId = priorId ?? id;
539
581
  next.push({
540
582
  path,
541
- relPath: relative(handle.memoryDir, path),
583
+ relPath,
542
584
  scope: writeScope,
543
- id,
585
+ id: baselineId,
544
586
  slug: f.slug,
545
587
  rev,
546
588
  stub: stub !== undefined && stub.rev === rev,
@@ -548,7 +590,7 @@ export class MemoryEngine {
548
590
  });
549
591
  fileToScope.set(path, writeScope);
550
592
  baseRevs.set(path, rev);
551
- baseIds.set(path, id);
593
+ baseIds.set(path, baselineId);
552
594
  }
553
595
  handle.materialized = next;
554
596
  handle.fileToScope = fileToScope;
@@ -650,6 +692,39 @@ export class MemoryEngine {
650
692
  return viaBackend;
651
693
  return readSafe(join(this.controlDir, "shadow", `${id}.md`));
652
694
  }
695
+ gateDerivedIndex(handle) {
696
+ if (handle.writeScope === null)
697
+ return undefined;
698
+ const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
699
+ const text = readSafe(indexPath);
700
+ if (text === undefined || text === "")
701
+ return undefined;
702
+ const finding = scanMemoryWrite(text).find((f) => f.code === "secret");
703
+ if (finding === undefined)
704
+ return undefined;
705
+ let detail = "";
706
+ try {
707
+ const dest = join(this.controlDir, "quarantine", `${this.now()}-${MEMORY_INDEX_FILENAME}`);
708
+ mkdirSync(dirname(dest), { recursive: true });
709
+ writeFileSync(dest, text, "utf8");
710
+ }
711
+ catch (err) {
712
+ detail = `; quarantine capture failed: ${err instanceof Error ? err.message : String(err)}`;
713
+ }
714
+ let cleared = false;
715
+ try {
716
+ writeFileSync(indexPath, "", "utf8");
717
+ cleared = true;
718
+ }
719
+ catch (err) {
720
+ detail = `${detail}; the derived index could NOT be cleared: ${err instanceof Error ? err.message : String(err)}`;
721
+ }
722
+ return {
723
+ path: MEMORY_INDEX_FILENAME,
724
+ code: finding.code,
725
+ reason: `memory index blocked: ${finding.reason} (${cleared ? "index cleared and rebuilt from entry frontmatter; the offending prose was captured to quarantine" : "index still carries the offending content"})${detail}`,
726
+ };
727
+ }
653
728
  quarantineFile(path, content) {
654
729
  const dest = join(this.controlDir, "quarantine", `${this.now()}-${basename(path)}`);
655
730
  let captured;
@@ -30,6 +30,7 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
30
30
  private loadLedger;
31
31
  private saveLedger;
32
32
  private recoverJournal;
33
+ private txnInFlight;
33
34
  private sweepStagedFiles;
34
35
  private excludedSubdirNames;
35
36
  private scopeDir;
@@ -79,10 +79,10 @@ export class FileMemoryEngineBackend {
79
79
  this.controlPlaneRoot = opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), dir);
80
80
  ensureDirExists(dir);
81
81
  ensureDirExists(this.controlPlaneRoot);
82
- this.recoverJournal();
82
+ this.recoverJournal({ unlocked: true });
83
83
  }
84
84
  checkControlPlane() {
85
- this.recoverJournal();
85
+ this.recoverJournal({ unlocked: true });
86
86
  this.ledger = undefined;
87
87
  this.loadLedger();
88
88
  registeredScopes(this.controlPlaneRoot);
@@ -164,8 +164,10 @@ export class FileMemoryEngineBackend {
164
164
  return;
165
165
  atomicWriteFileSync(join(this.controlPlaneRoot, LEDGER_FILE), `${JSON.stringify(this.ledger, null, 2)}\n`);
166
166
  }
167
- recoverJournal() {
167
+ recoverJournal(opts = {}) {
168
168
  const jp = join(this.controlPlaneRoot, JOURNAL_FILE);
169
+ if (opts.unlocked && this.txnInFlight())
170
+ return;
169
171
  const raw = readSafe(jp);
170
172
  if (raw !== undefined) {
171
173
  let journal;
@@ -205,6 +207,14 @@ export class FileMemoryEngineBackend {
205
207
  }
206
208
  this.sweepStagedFiles();
207
209
  }
210
+ txnInFlight() {
211
+ try {
212
+ return this.now() - statSync(this.txnLockDir()).mtimeMs < TXN_LOCK_STALE_MS;
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ }
208
218
  sweepStagedFiles() {
209
219
  const walk = (d, depth) => {
210
220
  let names;
@@ -216,11 +216,22 @@ function acquireSidecarLock(lockDir, opts = {}) {
216
216
  try {
217
217
  mkdirSync(lockDir);
218
218
  const token = `${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
219
+ let fd;
219
220
  try {
220
- writeSync(openSync(join(lockDir, "owner"), "w"), token);
221
+ fd = openSync(join(lockDir, "owner"), "w");
222
+ writeSync(fd, token);
221
223
  }
222
224
  catch {
223
225
  }
226
+ finally {
227
+ if (fd !== undefined) {
228
+ try {
229
+ closeSync(fd);
230
+ }
231
+ catch {
232
+ }
233
+ }
234
+ }
224
235
  return token;
225
236
  }
226
237
  catch {
@@ -32,6 +32,9 @@ const SECRET_PATTERNS = [
32
32
  { label: "slack_token", re: /\bxox[baprs]-(?:[A-Za-z0-9]+-){1,3}[A-Za-z0-9]{16,}\b/ },
33
33
  { label: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
34
34
  { label: "private_key", re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/ },
35
+ { label: "jwt", re: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{16,}\b/ },
36
+ { label: "azure_storage_account_key", re: /AccountKey=[A-Za-z0-9+/]{40,}={0,2}/ },
37
+ { label: "bearer_token", re: /\bAuthorization:\s*Bearer\s+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{20,}=*/ },
35
38
  ];
36
39
  export function detectSecret(text) {
37
40
  if (!text)
@@ -11,7 +11,7 @@ export interface ParsedPermissionRule {
11
11
  }
12
12
  export interface PermissionRuleIssue {
13
13
  rule: string;
14
- code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.file_glob" | "unsupported.mcp_paren";
14
+ code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.file_glob" | "unsupported.mcp_paren" | "unsupported.unknown_param";
15
15
  message: string;
16
16
  }
17
17
  export interface PermissionRuleCaps {
@@ -30,5 +30,6 @@ export interface PermissionRulePolicyOptions {
30
30
  }
31
31
  export declare function parsePermissionRule(rule: string): ParsedPermissionRule;
32
32
  export declare function wildcardMatch(pattern: string, value: string): boolean;
33
+ export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
33
34
  export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps">): PermissionRuleIssue[];
34
35
  export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): ToolPolicy;
@@ -90,6 +90,18 @@ const PRIMARY_FIELDS = {
90
90
  WebSearch: new Set(["query"]),
91
91
  };
92
92
  const FILE_TOOLS = new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Grep", "Glob"]);
93
+ const FILE_TOOL_PARAMS = {
94
+ Read: new Set(["file_path", "path", "offset", "limit", "pages"]),
95
+ Edit: new Set(["file_path", "path", "old_string", "new_string", "replace_all"]),
96
+ Write: new Set(["file_path", "path", "content"]),
97
+ NotebookEdit: new Set(["notebook_path", "path", "cell_id", "cell_type", "edit_mode", "new_source"]),
98
+ Grep: new Set([
99
+ "pattern", "path", "glob", "type", "output_mode", "head_limit", "max_results", "offset",
100
+ "multiline", "context", "ignore_case", "-i", "-n", "-o", "-A", "-B", "-C",
101
+ ]),
102
+ Glob: new Set(["pattern", "path", "max_results"]),
103
+ };
104
+ export const fileToolParamVocabulary = (canonicalTool) => FILE_TOOL_PARAMS[canonicalTool];
93
105
  function countStars(s) {
94
106
  let n = 0;
95
107
  for (const c of s)
@@ -151,9 +163,18 @@ function compile(rules, caps, primaryFieldGeneric) {
151
163
  continue;
152
164
  }
153
165
  }
154
- else if (FILE_TOOLS.has(toolName) && colon <= 0) {
155
- bad(text, "unsupported.file_glob", "file-tool glob rules (`Edit(src/**)`) are the tool-specific lane — not supported by the generic param DSL");
156
- continue;
166
+ else if (FILE_TOOLS.has(toolName)) {
167
+ if (colon <= 0) {
168
+ bad(text, "unsupported.file_glob", "file-tool glob rules (`Edit(src/**)`) are the tool-specific lane — not supported by the generic param DSL");
169
+ continue;
170
+ }
171
+ const paramPart = content.slice(0, colon).trim();
172
+ const vocabulary = FILE_TOOL_PARAMS[toolName];
173
+ if (vocabulary !== undefined && !vocabulary.has(paramPart)) {
174
+ bad(text, "unsupported.unknown_param", `"${paramPart}" is not a parameter of ${toolName} (it carries ${[...vocabulary].sort().join(", ")}) — the rule would compile and never match. ` +
175
+ `For a path glob use the tool-specific lane (\`${toolName}(src/**)\` is not the generic param DSL); a Windows path such as \`${toolName}(C:/foo)\` parses as the param "C".`);
176
+ continue;
177
+ }
157
178
  }
158
179
  if (colon <= 0) {
159
180
  bad(text, "invalid.param_split", "param-level ruleContent must be `param:pattern`");
@@ -10,7 +10,7 @@ export interface Stats {
10
10
  cacheWriteTokensLong: number;
11
11
  outputTokens: number;
12
12
  cacheHitRate?: number;
13
- totalInputTokens?: number;
13
+ totalInputTokens: number;
14
14
  costMicroUsd: number;
15
15
  nested?: {
16
16
  tokens: number;
@@ -35,7 +35,6 @@ function assistantText(msg) {
35
35
  export function assembleResult(spec, sessionId, final, stats, flags) {
36
36
  const taskId = spec.taskId ?? sessionId;
37
37
  const text = final ? assistantText(final) : "";
38
- stats.totalInputTokens = stats.promptTokens;
39
38
  if (!flags.unpricedSpend) {
40
39
  const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
41
40
  const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
@@ -433,6 +433,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
433
433
  }
434
434
  const taskScope = internals?.registryScope ?? spec.principal ?? "default";
435
435
  const offloadScope = spec.principal ?? "default";
436
+ if (internals?.registryScope !== undefined && internals.registryScope !== offloadScope) {
437
+ deps.onError?.(new Error(`offload/tool-result namespace note: this run mounts in registry domain "${internals.registryScope}" but its offloaded tool results live under the TRUST identity "${offloadScope}" (principal${spec.principal === undefined ? " absent → default" : ""}). Before 2.13.0 the registry domain named this namespace too; if a deployment used registryScope as its ref-tenancy boundary, that boundary is now the principal — pass one, or read refs under "${offloadScope}".`), { phase: "config", sessionId });
438
+ }
436
439
  defaultTaskRegistry.maybeGc();
437
440
  const forgetOnThrow = () => forgetQuietly(sessions, sessionId);
438
441
  const extraBodyCollisions = reservedCollisions(model.extraBody, reservedFor(model.api));
@@ -2743,6 +2746,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2743
2746
  }
2744
2747
  }
2745
2748
  : undefined;
2749
+ const notifyHookError = (err) => {
2750
+ try {
2751
+ deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
2752
+ }
2753
+ catch {
2754
+ }
2755
+ };
2746
2756
  const checkpointStore = spec.checkpointStore ?? deps.checkpointStore;
2747
2757
  const durableApproval = spec.durableApproval ??
2748
2758
  (runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
@@ -3183,6 +3193,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3183
3193
  reversibilityProbe: reversibilityProbes.get(e.toolName),
3184
3194
  abortSignal: abortController.signal,
3185
3195
  ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
3196
+ onHookError: notifyHookError,
3186
3197
  shellGated: (e.toolName === "Bash" && shellGatedBash) || (e.toolName === "Monitor" && shellGatedMonitor),
3187
3198
  ...(autoModeDecider ? { autoMode: { decider: autoModeDecider } } : {}),
3188
3199
  isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),
@@ -341,8 +341,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
341
341
  const scan = await Promise.race([
342
342
  prepared.detectExternalChanges(maxFiles),
343
343
  new Promise((resolve) => {
344
- const t = setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
345
- t.unref?.();
344
+ setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
346
345
  }),
347
346
  ]);
348
347
  for (const p of scan.evicted)
@@ -475,8 +474,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
475
474
  probed = await Promise.race([
476
475
  rs.attach.instrProbe(prepared.instructionSources),
477
476
  new Promise((resolve) => {
478
- const t = setTimeout(() => resolve(null), PROBE_CEILING_MS);
479
- t.unref?.();
477
+ setTimeout(() => resolve(null), PROBE_CEILING_MS);
480
478
  }),
481
479
  ]);
482
480
  }
@@ -811,16 +809,18 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
811
809
  rs.turn.turnUsageMissing = true;
812
810
  const u = m.usage;
813
811
  stats.tokens += u.totalTokens || 0;
814
- const { totalInputTokens: turnInput, costMicroUsd: turnCostMicroUsd } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
812
+ const { totalInputTokens: turnTotal, uncachedInputTokens: turnUncached, costMicroUsd: turnCostMicroUsd, } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
815
813
  if (!rs.telemetry.pricingConfigured)
816
814
  rs.telemetry.unpricedSpend = true;
817
- stats.promptTokens += turnInput;
815
+ stats.promptTokens += turnUncached;
816
+ stats.totalInputTokens += turnTotal;
818
817
  stats.cachedTokens += u.cacheRead || 0;
819
818
  stats.cacheWriteTokens += u.cacheWrite || 0;
820
819
  stats.outputTokens += u.output || 0;
821
820
  stats.costMicroUsd += turnCostMicroUsd;
822
- rs.turn.turnUsage ??= { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
823
- rs.turn.turnUsage.inputTokens += turnInput;
821
+ rs.turn.turnUsage ??= { inputTokens: 0, totalInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
822
+ rs.turn.turnUsage.inputTokens += turnUncached;
823
+ rs.turn.turnUsage.totalInputTokens += turnTotal;
824
824
  rs.turn.turnUsage.outputTokens += u.output || 0;
825
825
  rs.turn.turnUsage.cacheReadTokens += u.cacheRead || 0;
826
826
  rs.turn.turnUsage.cacheWriteTokens += u.cacheWrite || 0;
@@ -846,7 +846,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
846
846
  model: m.model,
847
847
  provider: m.provider,
848
848
  turn: stats.turns + 1,
849
- promptTokens: turnInput,
849
+ promptTokens: turnUncached,
850
+ totalInputTokens: turnTotal,
850
851
  completionTokens: u.output || 0,
851
852
  cacheRead: u.cacheRead || 0,
852
853
  cacheWrite: u.cacheWrite || 0,
@@ -1262,7 +1263,6 @@ export class Runner {
1262
1263
  let t;
1263
1264
  const timeout = new Promise((res2) => {
1264
1265
  t = setTimeout(() => res2(undefined), READY_TIMEOUT_MS);
1265
- t.unref?.();
1266
1266
  });
1267
1267
  return Promise.race([p, timeout]).finally(() => clearTimeout(t));
1268
1268
  };
@@ -1650,7 +1650,7 @@ export class Runner {
1650
1650
  }
1651
1651
  }
1652
1652
  onReady({ harness: prepared.harness, abortController: prepared.abortController });
1653
- const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
1653
+ const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
1654
1654
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
1655
1655
  if (resume &&
1656
1656
  resume.cp.suspendedAt !== undefined &&
@@ -2132,9 +2132,10 @@ export class Runner {
2132
2132
  const priced = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
2133
2133
  if (!priced)
2134
2134
  rs.telemetry.unpricedSpend = true;
2135
- const { totalInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2135
+ const { totalInputTokens, uncachedInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2136
2136
  stats.tokens += u.totalTokens || 0;
2137
- stats.promptTokens += totalInputTokens;
2137
+ stats.promptTokens += uncachedInputTokens;
2138
+ stats.totalInputTokens += totalInputTokens;
2138
2139
  stats.cachedTokens += u.cacheRead || 0;
2139
2140
  stats.cacheWriteTokens += u.cacheWrite || 0;
2140
2141
  stats.outputTokens += u.output || 0;
@@ -2142,7 +2143,7 @@ export class Runner {
2142
2143
  stats.compactionMicroUsd = (stats.compactionMicroUsd ?? 0) + costMicroUsd;
2143
2144
  emitTrace(rs.telemetry.tracer, () => ({
2144
2145
  kind: "brain.call", version: 1, taskId: rs.telemetry.taskId, model: m.id, provider: m.provider,
2145
- promptTokens: totalInputTokens, completionTokens: u.output || 0,
2146
+ promptTokens: uncachedInputTokens, totalInputTokens, completionTokens: u.output || 0,
2146
2147
  cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0,
2147
2148
  latencyMs: 0, ...(priced ? { costMicroUsd } : {}), ...(typeof msg?.stopReason === "string" ? { stopReason: msg.stopReason } : {}), ts: Date.now(),
2148
2149
  }));
@@ -2741,17 +2742,17 @@ export class Runner {
2741
2742
  costMicroUsd: prepared.nestedStats.costMicroUsd || undefined,
2742
2743
  };
2743
2744
  }
2744
- if (stats.promptTokens > 0) {
2745
- const rawHit = stats.cachedTokens / stats.promptTokens;
2746
- if (stats.cachedTokens > stats.promptTokens) {
2747
- this.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) > promptTokens (${stats.promptTokens}); the cache family is likely wrong. Does model.api match the brain serving it? Override with model.params.promptCacheFamily = "input-includes-cached" | "input-excludes-cached".`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2745
+ if (stats.totalInputTokens > 0) {
2746
+ const rawHit = stats.cachedTokens / stats.totalInputTokens;
2747
+ if (stats.cachedTokens > stats.totalInputTokens) {
2748
+ this.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) > totalInputTokens (${stats.totalInputTokens}); the cache family is likely wrong. Does model.api match the brain serving it? Override with model.params.promptCacheFamily = "input-includes-cached" | "input-excludes-cached".`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2748
2749
  }
2749
2750
  stats.cacheHitRate = Math.min(1, rawHit);
2750
- if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.promptTokens >= 8000 && stats.cacheHitRate < 0.15) {
2751
+ if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.totalInputTokens >= 8000 && stats.cacheHitRate < 0.15) {
2751
2752
  const cause = rs.degrade.degraded
2752
2753
  ? `a mid-task model switch (${rs.degrade.degraded.from} → ${rs.degrade.degraded.to}, degradation) reset the prefix cache — this is the likely cause`
2753
2754
  : `the cacheable prefix looks unstable. Keep volatile content (memory/timestamps/ids) out of the prompt prefix and tool order stable, or it's a server-side/TTL miss`;
2754
- this.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.promptTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2755
+ this.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.totalInputTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2755
2756
  }
2756
2757
  }
2757
2758
  if (prepared.resourceLedger &&
@@ -3035,7 +3036,6 @@ export class Runner {
3035
3036
  ac.abort();
3036
3037
  resolve([]);
3037
3038
  }, SUGGESTIONS_TIMEOUT_MS);
3038
- timer.unref?.();
3039
3039
  });
3040
3040
  const work = (async () => {
3041
3041
  let model = prepared.model;
@@ -3107,7 +3107,6 @@ export class Runner {
3107
3107
  ac.abort();
3108
3108
  resolve({ ok: false, error: { code: "aborted", message: "snapshot timed out" } });
3109
3109
  }, SNAPSHOT_TIMEOUT_MS);
3110
- timer.unref?.();
3111
3110
  });
3112
3111
  const work = store.snapshot(prepared.sessionId, leafId, env, root, ac.signal);
3113
3112
  work.catch(() => { });
@@ -7,6 +7,11 @@ export declare function promptTokensOf(family: CacheFamily, u: {
7
7
  cacheRead: number;
8
8
  cacheWrite: number;
9
9
  }): number;
10
+ export declare function uncachedInputTokensOf(family: CacheFamily, u: {
11
+ input: number;
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ }): number;
10
15
  export declare function usageCostMicroUsd(family: CacheFamily, u: {
11
16
  input?: number;
12
17
  output?: number;
@@ -14,5 +19,6 @@ export declare function usageCostMicroUsd(family: CacheFamily, u: {
14
19
  cacheWrite?: number;
15
20
  }, pricing: ModelPricing): {
16
21
  totalInputTokens: number;
22
+ uncachedInputTokens: number;
17
23
  costMicroUsd: number;
18
24
  };
@@ -12,18 +12,19 @@ export function cacheFamilyOf(model) {
12
12
  export function promptTokensOf(family, u) {
13
13
  return family === "input-excludes-cached" ? u.input + u.cacheRead + u.cacheWrite : u.input;
14
14
  }
15
+ export function uncachedInputTokensOf(family, u) {
16
+ return Math.max(0, promptTokensOf(family, u) - u.cacheRead - u.cacheWrite);
17
+ }
15
18
  export function usageCostMicroUsd(family, u, pricing) {
16
- const totalInputTokens = promptTokensOf(family, {
17
- input: u.input || 0,
18
- cacheRead: u.cacheRead || 0,
19
- cacheWrite: u.cacheWrite || 0,
20
- });
19
+ const normalized = { input: u.input || 0, cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0 };
20
+ const totalInputTokens = promptTokensOf(family, normalized);
21
+ const uncachedInputTokens = uncachedInputTokensOf(family, normalized);
21
22
  const costMicroUsd = computeCostMicroUsd({
22
23
  totalInputTokens,
23
- cacheReadTokens: u.cacheRead || 0,
24
- cacheWriteTokens: u.cacheWrite || 0,
24
+ cacheReadTokens: normalized.cacheRead,
25
+ cacheWriteTokens: normalized.cacheWrite,
25
26
  cacheWriteTokensLong: 0,
26
27
  outputTokens: u.output || 0,
27
28
  }, pricing);
28
- return { totalInputTokens, costMicroUsd };
29
+ return { totalInputTokens, uncachedInputTokens, costMicroUsd };
29
30
  }
@@ -1,5 +1,9 @@
1
1
  const SECRET_ENV_RE = /(^|_)(KEY|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\d+|ID))?$/i;
2
- const SECRET_ENV_EXACT_NAMES = new Set(["DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH"]);
2
+ const SECRET_ENV_EXACT_NAMES = new Set([
3
+ "DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH",
4
+ "PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
5
+ "NPM_CONFIG__AUTH", "NPM_CONFIG__AUTHTOKEN",
6
+ ]);
3
7
  export function isSecretEnvKey(key) {
4
8
  return SECRET_ENV_RE.test(key) || SECRET_ENV_EXACT_NAMES.has(key.toUpperCase());
5
9
  }
@@ -14,6 +14,9 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
14
14
  ".ssh",
15
15
  ".git/hooks",
16
16
  ".git/config",
17
+ ".git/modules",
18
+ ".mcp.json",
19
+ ".claude*",
17
20
  ".aws",
18
21
  ".config/gcloud",
19
22
  ".azure",
@@ -24,15 +27,18 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
24
27
  ".zsh_history",
25
28
  ];
26
29
  function segmentGlobToRegExp(segment) {
27
- const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
30
+ const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*");
28
31
  return new RegExp(`^${escaped}$`);
29
32
  }
30
33
  function compilePatterns(patterns) {
31
34
  const out = [];
32
35
  for (const raw of patterns) {
33
36
  const segments = raw.split("/").filter(Boolean).map(segmentGlobToRegExp);
34
- if (segments.length > 0)
35
- out.push({ raw, segments });
37
+ if (segments.length === 0) {
38
+ throw new Error(`createSensitivePathPolicy: pattern ${JSON.stringify(raw)} contains no path segments and would guard nothing. ` +
39
+ `Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".git/hooks", "*.pem"); remove the entry or spell the segments.`);
40
+ }
41
+ out.push({ raw, segments });
36
42
  }
37
43
  return out;
38
44
  }
@@ -32,5 +32,45 @@ export async function sessionRepoContract(make, runAssertion) {
32
32
  const repo = make();
33
33
  await assert.rejects(repo.open({ id: "0198fa00-0000-7000-8000-0000000000ff", createdAt: "" }), /not.?found/i);
34
34
  });
35
+ run("delete() makes the id not_found — the history is gone, not merely hidden from open()", async () => {
36
+ const repo = make();
37
+ const sess = await repo.create({ id: A });
38
+ await sess.getStorage().appendEntry(SESSION_ENTRY("e1", null));
39
+ await repo.delete({ id: A, createdAt: "" });
40
+ await assert.rejects(repo.open({ id: A, createdAt: "" }), /not.?found/i);
41
+ const fresh = await repo.create({ id: A });
42
+ assert.deepEqual((await fresh.getStorage().getEntries()).map((e) => e.id), []);
43
+ });
44
+ run("fork() copies the log, stamps forkedFrom, and leaves the SOURCE untouched", async () => {
45
+ const repo = make();
46
+ const src = await repo.create({ id: A });
47
+ await src.getStorage().appendEntry(SESSION_ENTRY("e1", null));
48
+ await src.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
49
+ const B = "0198fa00-0000-7000-8000-00000000000b";
50
+ const forked = await repo.fork({ id: A, createdAt: "" }, { id: B });
51
+ assert.deepEqual((await forked.getStorage().getEntries()).map((e) => e.id), ["e1", "e2"]);
52
+ assert.equal((await forked.getMetadata()).forkedFrom, A, "fork lineage must be stamped (a reap walks it)");
53
+ await src.getStorage().appendEntry(SESSION_ENTRY("e3", "e2"));
54
+ assert.deepEqual((await (await repo.open({ id: A, createdAt: "" })).getStorage().getEntries()).map((e) => e.id), ["e1", "e2", "e3"]);
55
+ assert.deepEqual((await (await repo.open({ id: B, createdAt: "" })).getStorage().getEntries()).map((e) => e.id), ["e1", "e2"]);
56
+ });
57
+ run("exportEntries → importEntries round-trips the log verbatim, and the imported session REPLACES what was there", async () => {
58
+ const repo = make();
59
+ if (repo.exportEntries === undefined || repo.importEntries === undefined)
60
+ return;
61
+ const src = await repo.create({ id: A });
62
+ await src.getStorage().appendEntry(SESSION_ENTRY("e1", null));
63
+ await src.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
64
+ const donor = await repo.exportEntries(A);
65
+ assert.deepEqual(donor.map((e) => e.id), ["e1", "e2"]);
66
+ const C = "0198fa00-0000-7000-8000-00000000000c";
67
+ const target = await repo.create({ id: C });
68
+ await target.getStorage().appendEntry(SESSION_ENTRY("old", null));
69
+ await repo.importEntries(C, undefined, donor);
70
+ const after = await repo.exportEntries(C);
71
+ assert.deepEqual(after.map((e) => e.id), ["e1", "e2"]);
72
+ assert.deepEqual(after.map((e) => e.parentId), [null, "e1"]);
73
+ assert.equal((await repo.exportEntries(A)).length, 2, "importing into C must not disturb the donor session");
74
+ });
35
75
  await settle();
36
76
  }
@@ -42,7 +42,13 @@ export function registerMonitorLane(core, input) {
42
42
  ...(input.sessionId !== undefined ? { spillSessionId: input.sessionId } : {}),
43
43
  };
44
44
  core.handles.set(id, handle);
45
- startMonitorWatcherLane(core, id);
45
+ try {
46
+ startMonitorWatcherLane(core, id);
47
+ }
48
+ catch (e) {
49
+ core.handles.delete(id);
50
+ throw e;
51
+ }
46
52
  return id;
47
53
  }
48
54
  function spillRolledMonitorChunk(handle, stream, dropped) {