@sema-agent/core 3.0.0 → 4.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 (127) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/cascade.js +31 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.js +27 -19
  9. package/dist/agents/retain-ledger.d.ts +2 -0
  10. package/dist/agents/retain-ledger.js +6 -5
  11. package/dist/agents/roster-store.js +3 -5
  12. package/dist/agents/subagent.js +66 -64
  13. package/dist/agents/teacher.d.ts +1 -9
  14. package/dist/agents/teacher.js +21 -24
  15. package/dist/agents/verify.js +15 -14
  16. package/dist/brain/anthropic.js +24 -21
  17. package/dist/brain/openai.js +24 -21
  18. package/dist/brain/stream-engine.js +5 -2
  19. package/dist/brain/stream-shared.d.ts +1 -0
  20. package/dist/brain/stream-shared.js +14 -0
  21. package/dist/brain/terminal-cause.d.ts +4 -0
  22. package/dist/brain/terminal-cause.js +12 -0
  23. package/dist/brain/tool-call-id.d.ts +1 -0
  24. package/dist/brain/tool-call-id.js +3 -0
  25. package/dist/brain/tool-call-repair.js +2 -1
  26. package/dist/core/auto-compaction.js +16 -25
  27. package/dist/core/background-agent-store.d.ts +22 -22
  28. package/dist/core/background-agent-store.js +19 -23
  29. package/dist/core/checkpoint-store.d.ts +6 -13
  30. package/dist/core/checkpoint-store.js +11 -5
  31. package/dist/core/file-snapshot-store.d.ts +8 -0
  32. package/dist/core/file-snapshot-store.js +4 -3
  33. package/dist/core/hooks.d.ts +3 -1
  34. package/dist/core/hooks.js +8 -18
  35. package/dist/core/lsp.js +4 -3
  36. package/dist/core/mailbox-store.d.ts +7 -10
  37. package/dist/core/mcp.d.ts +7 -10
  38. package/dist/core/mcp.js +7 -3
  39. package/dist/core/memory-engine/engine.d.ts +0 -1
  40. package/dist/core/memory-engine/engine.js +24 -34
  41. package/dist/core/memory-engine/file-backend.d.ts +1 -2
  42. package/dist/core/memory-engine/file-backend.js +25 -36
  43. package/dist/core/memory-engine/layout.d.ts +8 -0
  44. package/dist/core/memory-engine/layout.js +60 -4
  45. package/dist/core/memory-engine/scope-contract.js +9 -3
  46. package/dist/core/memory-engine/types.d.ts +1 -1
  47. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  48. package/dist/core/runner/active-skill-scope.js +1 -15
  49. package/dist/core/runner/assemble-result.d.ts +2 -7
  50. package/dist/core/runner/assemble-result.js +3 -4
  51. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  52. package/dist/core/runner/compaction-call-options.js +30 -0
  53. package/dist/core/runner/prepare-task.d.ts +6 -23
  54. package/dist/core/runner/prepare-task.js +20 -53
  55. package/dist/core/runner/runtask.d.ts +1 -0
  56. package/dist/core/runner/runtask.js +65 -156
  57. package/dist/core/runner/session-rule-policy.js +4 -1
  58. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  59. package/dist/core/runner/teardown-bounded.js +36 -0
  60. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  61. package/dist/core/runner/turn-attachments.d.ts +5 -15
  62. package/dist/core/runner/turn-attachments.js +1 -1
  63. package/dist/core/safe-notify.d.ts +17 -0
  64. package/dist/core/safe-notify.js +59 -0
  65. package/dist/core/sensitive-path-policy.js +2 -1
  66. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  67. package/dist/core/stub-env.d.ts +2 -6
  68. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  69. package/dist/core/surrogate-safe-slice.js +18 -0
  70. package/dist/core/task-registry-monitor.js +8 -13
  71. package/dist/core/task-registry-shared.d.ts +22 -0
  72. package/dist/core/task-registry-shared.js +16 -0
  73. package/dist/core/task-registry.d.ts +1 -0
  74. package/dist/core/task-registry.js +40 -57
  75. package/dist/core/tool-errors.js +5 -4
  76. package/dist/core/tool-name-aliases.js +2 -1
  77. package/dist/core/tool-policy.js +10 -5
  78. package/dist/core/tool-result-store.d.ts +1 -0
  79. package/dist/core/tool-result-store.js +17 -1
  80. package/dist/core/trace.d.ts +2 -9
  81. package/dist/core/types.d.ts +15 -12
  82. package/dist/core/untrusted-egress.js +7 -5
  83. package/dist/core/workflow-journal-store.d.ts +9 -20
  84. package/dist/core/workflow-run-store.d.ts +11 -10
  85. package/dist/core/workflow-run-store.js +22 -16
  86. package/dist/engine/compaction/utils.js +4 -2
  87. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  88. package/dist/engine/harness/agent-harness.d.ts +1 -0
  89. package/dist/engine/harness/agent-harness.js +13 -33
  90. package/dist/engine/harness/types.d.ts +29 -67
  91. package/dist/engine/llm/types.d.ts +1 -1
  92. package/dist/engine/loop/agent-loop.js +11 -4
  93. package/dist/engine/loop/types.d.ts +11 -9
  94. package/dist/engine/session/memory-repo.d.ts +2 -6
  95. package/dist/engine/session/session.d.ts +5 -30
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.js +2 -1
  98. package/dist/internal/harness-types.d.ts +3 -1
  99. package/dist/orchestration/run-spec.js +7 -4
  100. package/dist/orchestration/run-workflow-tool.js +3 -35
  101. package/dist/orchestration/workflow.d.ts +3 -11
  102. package/dist/orchestration/workflow.js +164 -204
  103. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  104. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  105. package/dist/stores/file/background-agent-store.d.ts +3 -12
  106. package/dist/stores/file/background-agent-store.js +3 -41
  107. package/dist/stores/file/checkpoint-store.js +6 -6
  108. package/dist/stores/file/mailbox-store.d.ts +2 -6
  109. package/dist/stores/file/session-policy-store.js +15 -1
  110. package/dist/stores/file/session-store.d.ts +2 -6
  111. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  112. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  113. package/dist/stores/file/workflow-run-store.js +4 -17
  114. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  115. package/dist/tools/fs/bash-readonly-classifier.js +60 -25
  116. package/dist/tools/fs/fs-bash.js +324 -96
  117. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  118. package/dist/tools/fs/fs-shared.js +2 -2
  119. package/dist/tools/fs/notebook.d.ts +2 -5
  120. package/dist/tools/fs/safety.d.ts +3 -0
  121. package/dist/tools/fs/safety.js +43 -14
  122. package/dist/tools/fs/search.js +5 -5
  123. package/dist/tools/monitor.d.ts +1 -0
  124. package/dist/tools/monitor.js +7 -5
  125. package/dist/tools/worktree.d.ts +2 -7
  126. package/dist/tools/worktree.js +3 -1
  127. package/package.json +6 -2
@@ -29,7 +29,7 @@ export interface CascadeAttempt {
29
29
  index: number;
30
30
  model: ModelRef;
31
31
  passed: boolean;
32
- costMicroUsd: number;
32
+ costMicroUsd?: number;
33
33
  status: TaskResult["status"];
34
34
  errorCode?: string;
35
35
  gateError?: string;
@@ -40,6 +40,7 @@ export interface CascadeAttempt {
40
40
  }
41
41
  export interface CascadeRunResult extends TaskResult {
42
42
  cascadeOutcome: "passed" | "exhausted";
43
+ costCeilingUnenforceable?: true;
43
44
  escalated: boolean;
44
45
  finalRung: number;
45
46
  attempts: CascadeAttempt[];
@@ -1,5 +1,8 @@
1
1
  import { releaseSession } from "./session-util.js";
2
2
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
3
+ import { buildCumulativeStats } from "./cumulative-stats.js";
4
+ import { createSafeNotifier } from "../core/safe-notify.js";
5
+ const CASCADE_ON_RUNG_SITE = "cascade.onRung";
3
6
  function createDefaultGate(spec) {
4
7
  const requiresStructured = spec.outputSchema != null;
5
8
  return (result) => result.status === "completed" && (!requiresStructured || result.structuredOutput !== undefined);
@@ -14,37 +17,33 @@ export async function runCascade(runner, spec, config) {
14
17
  const gate = config.gate ?? createDefaultGate(spec);
15
18
  const startedAt = Date.now();
16
19
  const deadlineAt = config.totalTimeoutMs != null ? startedAt + config.totalTimeoutMs : undefined;
20
+ const notifier = createSafeNotifier();
17
21
  const attempts = [];
18
22
  let totalCost = 0;
23
+ let ceilingCostKnown = true;
19
24
  let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownCached = 0, ownOutput = 0, ownCostMicro = 0;
20
25
  let ownTotalInput = 0;
21
26
  let ownCacheWrite = 0, ownCacheWriteLong = 0;
22
27
  let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0;
23
28
  let ownLlmRootMicro = 0, ownCompactionMicro = 0;
29
+ let anyRungUnpriced = false;
24
30
  let hrCount = 0, hrWaitMs = 0;
25
31
  const hrGates = [];
26
- const cumulativeStats = (baseStats) => ({
27
- ...baseStats,
32
+ const cumulativeStats = (baseStats) => buildCumulativeStats(baseStats, {
28
33
  tokens: ownTokens,
29
34
  turns: ownTurns,
30
- promptTokens: ownPrompt || undefined,
31
- totalInputTokens: ownTotalInput || undefined,
32
- cachedTokens: ownCached || undefined,
33
- outputTokens: ownOutput || undefined,
34
- cacheWriteTokens: ownCacheWrite || undefined,
35
- cacheWriteTokensLong: ownCacheWriteLong || undefined,
36
- cacheHitRate: ownTotalInput > 0 ? Math.min(1, ownCached / ownTotalInput) : undefined,
35
+ promptTokens: ownPrompt,
36
+ totalInputTokens: ownTotalInput,
37
+ cachedTokens: ownCached,
38
+ outputTokens: ownOutput,
39
+ cacheWriteTokens: ownCacheWrite,
40
+ cacheWriteTokensLong: ownCacheWriteLong,
37
41
  costMicroUsd: ownCostMicro,
38
- nested: nestedTokens > 0 || nestedTasks > 0
39
- ? { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro || undefined }
40
- : undefined,
41
- costBreakdown: {
42
- llmRootMicroUsd: ownLlmRootMicro,
43
- nestedSubagentMicroUsd: nestedCostMicro,
44
- memoryConsolidationMicroUsd: 0,
45
- compactionMicroUsd: ownCompactionMicro,
46
- },
47
- humanReview: hrCount > 0 ? { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates } : undefined,
42
+ anyLegUnpriced: anyRungUnpriced,
43
+ llmRootMicroUsd: ownLlmRootMicro,
44
+ compactionMicroUsd: ownCompactionMicro,
45
+ nested: { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro },
46
+ humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates },
48
47
  });
49
48
  let lastResult;
50
49
  let passedRung = -1;
@@ -52,8 +51,9 @@ export async function runCascade(runner, spec, config) {
52
51
  for (let i = 0; i < maxRungs; i++) {
53
52
  if (deadlineAt !== undefined && Date.now() >= deadlineAt)
54
53
  break;
55
- if (i > 0 && config.costCeilingMicroUsd != null && totalCost >= config.costCeilingMicroUsd)
54
+ if (i > 0 && config.costCeilingMicroUsd != null && (totalCost >= config.costCeilingMicroUsd || !ceilingCostKnown)) {
56
55
  break;
56
+ }
57
57
  const rung = ladder[i];
58
58
  let deadlineFired = false;
59
59
  const rungAbort = new AbortController();
@@ -80,8 +80,11 @@ export async function runCascade(runner, spec, config) {
80
80
  }
81
81
  lastResult = result;
82
82
  const s = result.stats;
83
- const rungCost = (s.costMicroUsd ?? 0) + (s.nested?.costMicroUsd ?? 0);
84
- totalCost += rungCost;
83
+ const rungCostKnown = s.costMicroUsd !== undefined && (s.nested === undefined || s.nested.costMicroUsd !== undefined);
84
+ const rungCost = rungCostKnown ? (s.costMicroUsd ?? 0) + (s.nested?.costMicroUsd ?? 0) : undefined;
85
+ totalCost += rungCost ?? 0;
86
+ if (!rungCostKnown)
87
+ ceilingCostKnown = false;
85
88
  ownTokens += s.tokens;
86
89
  ownTurns += s.turns;
87
90
  ownPrompt += s.promptTokens ?? 0;
@@ -91,6 +94,8 @@ export async function runCascade(runner, spec, config) {
91
94
  ownCacheWrite += s.cacheWriteTokens ?? 0;
92
95
  ownCacheWriteLong += s.cacheWriteTokensLong ?? 0;
93
96
  ownCostMicro += s.costMicroUsd ?? 0;
97
+ if (s.costMicroUsd === undefined)
98
+ anyRungUnpriced = true;
94
99
  if (s.costBreakdown) {
95
100
  ownLlmRootMicro += s.costBreakdown.llmRootMicroUsd;
96
101
  ownCompactionMicro += s.costBreakdown.compactionMicroUsd;
@@ -113,6 +118,7 @@ export async function runCascade(runner, spec, config) {
113
118
  ...mapped,
114
119
  stats: cumulativeStats(result.stats),
115
120
  cascadeOutcome: "exhausted",
121
+ ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}),
116
122
  escalated: i > 0,
117
123
  finalRung: i,
118
124
  attempts,
@@ -128,11 +134,7 @@ export async function runCascade(runner, spec, config) {
128
134
  errorCode: result.errorCode,
129
135
  ...(deadlineFired ? { timedOut: true } : { cancelled: true }),
130
136
  });
131
- try {
132
- config.onRung?.({ index: i, model: rung.model, passed: false, result });
133
- }
134
- catch {
135
- }
137
+ notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed: false, result }), CASCADE_ON_RUNG_SITE);
136
138
  await releaseSession(runner, result.sessionId);
137
139
  break;
138
140
  }
@@ -167,11 +169,7 @@ export async function runCascade(runner, spec, config) {
167
169
  diagnostics,
168
170
  ...(statusRejected ? { statusRejected: true } : {}),
169
171
  });
170
- try {
171
- config.onRung?.({ index: i, model: rung.model, passed, result });
172
- }
173
- catch {
174
- }
172
+ notifier.notify(() => config.onRung?.({ index: i, model: rung.model, passed, result }), CASCADE_ON_RUNG_SITE);
175
173
  }
176
174
  finally {
177
175
  await releaseSession(runner, result.sessionId);
@@ -187,6 +185,7 @@ export async function runCascade(runner, spec, config) {
187
185
  ...base,
188
186
  stats: cumulativeStats(base.stats),
189
187
  cascadeOutcome: passedRung >= 0 ? "passed" : "exhausted",
188
+ ...(config.costCeilingMicroUsd != null && !ceilingCostKnown ? { costCeilingUnenforceable: true } : {}),
190
189
  escalated: finalRung > 0,
191
190
  finalRung,
192
191
  attempts,
@@ -0,0 +1,29 @@
1
+ import type { TaskResult } from "../core/types.js";
2
+ export interface CumulativeNestedTotals {
3
+ tokens: number;
4
+ turns: number;
5
+ tasks: number;
6
+ costMicroUsd: number;
7
+ }
8
+ export interface CumulativeHumanReviewTotals {
9
+ count: number;
10
+ totalWaitMs: number;
11
+ gates: NonNullable<TaskResult["stats"]["humanReview"]>["gates"];
12
+ }
13
+ export interface CumulativeStatsAccumulator {
14
+ tokens: number;
15
+ turns: number;
16
+ promptTokens: number;
17
+ totalInputTokens: number;
18
+ cachedTokens: number;
19
+ outputTokens: number;
20
+ cacheWriteTokens: number;
21
+ cacheWriteTokensLong: number;
22
+ costMicroUsd: number | undefined;
23
+ anyLegUnpriced: boolean;
24
+ llmRootMicroUsd: number;
25
+ compactionMicroUsd: number;
26
+ nested: CumulativeNestedTotals;
27
+ humanReview: CumulativeHumanReviewTotals;
28
+ }
29
+ export declare function buildCumulativeStats(base: TaskResult["stats"], acc: CumulativeStatsAccumulator): TaskResult["stats"];
@@ -0,0 +1,29 @@
1
+ export function buildCumulativeStats(base, acc) {
2
+ return {
3
+ ...base,
4
+ tokens: acc.tokens,
5
+ turns: acc.turns,
6
+ promptTokens: acc.promptTokens || undefined,
7
+ totalInputTokens: acc.totalInputTokens || undefined,
8
+ cachedTokens: acc.cachedTokens || undefined,
9
+ outputTokens: acc.outputTokens || undefined,
10
+ cacheWriteTokens: acc.cacheWriteTokens || undefined,
11
+ cacheWriteTokensLong: acc.cacheWriteTokensLong || undefined,
12
+ cacheHitRate: acc.totalInputTokens > 0 ? Math.min(1, acc.cachedTokens / acc.totalInputTokens) : undefined,
13
+ costMicroUsd: acc.anyLegUnpriced ? undefined : acc.costMicroUsd,
14
+ nested: acc.nested.tokens > 0 || acc.nested.tasks > 0
15
+ ? { tokens: acc.nested.tokens, turns: acc.nested.turns, tasks: acc.nested.tasks, costMicroUsd: acc.nested.costMicroUsd || undefined }
16
+ : undefined,
17
+ costBreakdown: acc.anyLegUnpriced
18
+ ? undefined
19
+ : {
20
+ llmRootMicroUsd: acc.llmRootMicroUsd,
21
+ nestedSubagentMicroUsd: acc.nested.costMicroUsd,
22
+ memoryConsolidationMicroUsd: 0,
23
+ compactionMicroUsd: acc.compactionMicroUsd,
24
+ },
25
+ humanReview: acc.humanReview.count > 0
26
+ ? { count: acc.humanReview.count, totalWaitMs: acc.humanReview.totalWaitMs, gates: acc.humanReview.gates }
27
+ : undefined,
28
+ };
29
+ }
@@ -63,7 +63,9 @@ export declare class ObserverDigestTap {
63
63
  constructor(flush: (activity: ObserverDigestEvent[]) => void);
64
64
  record(e: TaskEvent): void;
65
65
  private flushText;
66
+ private readonly notifier;
66
67
  private safeFlush;
68
+ get flushFailures(): number;
67
69
  flushSegment(): void;
68
70
  finish(reason: string): void;
69
71
  }
@@ -112,7 +114,9 @@ export declare class ObserverPairing {
112
114
  });
113
115
  enqueueSegment(activity: ObserverDigestEvent[], trigger?: string): void;
114
116
  private pumping?;
117
+ private readonly notifier;
115
118
  private safeOnError;
119
+ get onErrorFailures(): number;
116
120
  drain(): Promise<void>;
117
121
  private pump;
118
122
  private deliverBatch;
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../core/tools.js";
3
+ import { createSafeNotifier } from "../core/safe-notify.js";
3
4
  export const OBSERVER_EVENT_TAGS = ["tool-call", "user-message", "tool-result", "turn-ended"];
4
5
  const TAG_ESCAPE_RE = new RegExp(`<(?=/?(?:${OBSERVER_EVENT_TAGS.join("|")})(?:[>\\s/]|$))`, "gi");
5
6
  export function escapeObserverTags(text) {
@@ -181,12 +182,12 @@ export class ObserverDigestTap {
181
182
  this.pending.push({ type: "assistant_text", text: this.textBuf });
182
183
  this.textBuf = "";
183
184
  }
185
+ notifier = createSafeNotifier();
184
186
  safeFlush(slice) {
185
- try {
186
- this.flush(slice);
187
- }
188
- catch {
189
- }
187
+ this.notifier.notify(() => this.flush(slice), "observer.digestTap.flush");
188
+ }
189
+ get flushFailures() {
190
+ return this.notifier.failuresAt("observer.digestTap.flush");
190
191
  }
191
192
  flushSegment() {
192
193
  if (this.finished)
@@ -270,12 +271,12 @@ export class ObserverPairing {
270
271
  }).catch(() => undefined);
271
272
  }
272
273
  pumping;
274
+ notifier = createSafeNotifier();
273
275
  safeOnError(err) {
274
- try {
275
- this.onError?.(err);
276
- }
277
- catch {
278
- }
276
+ this.notifier.notify(() => this.onError?.(err), "observer.pairing.onError");
277
+ }
278
+ get onErrorFailures() {
279
+ return this.notifier.failuresAt("observer.pairing.onError");
279
280
  }
280
281
  async drain() {
281
282
  while (this.pumping) {
@@ -1,6 +1,7 @@
1
1
  import { isDurablePause } from "./suspend-guard.js";
2
2
  import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js";
3
3
  import { assertOracleIsolation } from "../core/oracle-isolation.js";
4
+ import { buildCumulativeStats } from "./cumulative-stats.js";
4
5
  export function isolationPermitsAutoAccept(verdict) {
5
6
  return verdict.protected && verdict.isolationClass === "out_of_process";
6
7
  }
@@ -78,7 +79,10 @@ export async function runRepairLoop(runner, implSpec, config) {
78
79
  else if (config.workerEnv !== undefined && config.workerEnv === config.graderEnv) {
79
80
  return unprotectedResult("graderEnv must not be the worker env");
80
81
  }
81
- const maxAttempts = Math.max(1, Math.floor(config.maxAttempts));
82
+ const maxAttempts = Number.isFinite(config.maxAttempts) ? Math.max(1, Math.floor(config.maxAttempts)) : 1;
83
+ if (!Number.isFinite(config.maxAttempts)) {
84
+ config.onWarn?.(new Error(`repair-loop: maxAttempts must be a finite number (got ${config.maxAttempts}) — falling back to 1 attempt`));
85
+ }
82
86
  const bundle = config.resumeBundle
83
87
  ? { ...config.resumeBundle, diagnostics: [...config.resumeBundle.diagnostics], rejectedHypotheses: [...config.resumeBundle.rejectedHypotheses] }
84
88
  : freshBundle();
@@ -89,7 +93,10 @@ export async function runRepairLoop(runner, implSpec, config) {
89
93
  let ownTokens = 0, ownTurns = 0, ownPrompt = 0, ownTotalInput = 0, ownCached = 0, ownOutput = 0;
90
94
  let ownCacheWrite = 0, ownCacheWriteLong = 0, ownCostMicro = 0;
91
95
  let ownLlmRootMicro = 0, ownCompactionMicro = 0;
96
+ let anyAttemptUnpriced = false;
92
97
  let nestedTokens = 0, nestedTurns = 0, nestedTasks = 0, nestedCostMicro = 0;
98
+ let hrCount = 0, hrWaitMs = 0;
99
+ const hrGates = [];
93
100
  let oracleCostMicroUsd = 0;
94
101
  const accumulate = (s) => {
95
102
  ownTokens += s.tokens;
@@ -101,10 +108,17 @@ export async function runRepairLoop(runner, implSpec, config) {
101
108
  ownCacheWrite += s.cacheWriteTokens ?? 0;
102
109
  ownCacheWriteLong += s.cacheWriteTokensLong ?? 0;
103
110
  ownCostMicro += s.costMicroUsd ?? 0;
111
+ if (s.costMicroUsd === undefined)
112
+ anyAttemptUnpriced = true;
104
113
  if (s.costBreakdown) {
105
114
  ownLlmRootMicro += s.costBreakdown.llmRootMicroUsd;
106
115
  ownCompactionMicro += s.costBreakdown.compactionMicroUsd;
107
116
  }
117
+ if (s.humanReview) {
118
+ hrCount += s.humanReview.count;
119
+ hrWaitMs += s.humanReview.totalWaitMs;
120
+ hrGates.push(...s.humanReview.gates);
121
+ }
108
122
  if (s.nested) {
109
123
  nestedTokens += s.nested.tokens;
110
124
  nestedTurns += s.nested.turns;
@@ -112,27 +126,21 @@ export async function runRepairLoop(runner, implSpec, config) {
112
126
  nestedCostMicro += s.nested.costMicroUsd ?? 0;
113
127
  }
114
128
  };
115
- const cumulativeStats = (base) => ({
116
- ...base,
129
+ const cumulativeStats = (base) => buildCumulativeStats(base, {
117
130
  tokens: ownTokens,
118
131
  turns: ownTurns,
119
- promptTokens: ownPrompt || undefined,
120
- totalInputTokens: ownTotalInput || undefined,
121
- cachedTokens: ownCached || undefined,
122
- outputTokens: ownOutput || undefined,
123
- cacheWriteTokens: ownCacheWrite || undefined,
124
- cacheWriteTokensLong: ownCacheWriteLong || undefined,
125
- cacheHitRate: ownTotalInput > 0 ? Math.min(1, ownCached / ownTotalInput) : undefined,
132
+ promptTokens: ownPrompt,
133
+ totalInputTokens: ownTotalInput,
134
+ cachedTokens: ownCached,
135
+ outputTokens: ownOutput,
136
+ cacheWriteTokens: ownCacheWrite,
137
+ cacheWriteTokensLong: ownCacheWriteLong,
126
138
  costMicroUsd: ownCostMicro,
127
- nested: nestedTokens > 0 || nestedTasks > 0
128
- ? { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro || undefined }
129
- : undefined,
130
- costBreakdown: {
131
- llmRootMicroUsd: ownLlmRootMicro,
132
- nestedSubagentMicroUsd: nestedCostMicro,
133
- memoryConsolidationMicroUsd: base.costBreakdown?.memoryConsolidationMicroUsd ?? 0,
134
- compactionMicroUsd: ownCompactionMicro,
135
- },
139
+ anyLegUnpriced: anyAttemptUnpriced,
140
+ llmRootMicroUsd: ownLlmRootMicro,
141
+ compactionMicroUsd: ownCompactionMicro,
142
+ nested: { tokens: nestedTokens, turns: nestedTurns, tasks: nestedTasks, costMicroUsd: nestedCostMicro },
143
+ humanReview: { count: hrCount, totalWaitMs: hrWaitMs, gates: hrGates },
136
144
  });
137
145
  let restartedOnce = false;
138
146
  let restartUsed = false;
@@ -40,12 +40,14 @@ export declare class SubagentRetainLedger {
40
40
  private readonly hooks;
41
41
  private failedEvictions;
42
42
  private lastFailure;
43
+ private readonly notifier;
43
44
  constructor(config: true | {
44
45
  ttlMs?: number;
45
46
  max?: number;
46
47
  }, hooks?: RetainLedgerHooks);
47
48
  get evictionFailureCount(): number;
48
49
  get lastEvictionError(): Error | undefined;
50
+ get evictionSinkFailureCount(): number;
49
51
  noteDetachedFailure(trigger: RetainEvictionTrigger, error: unknown, parentToolCallId?: string): void;
50
52
  private evictDetached;
51
53
  disposeAllDetached(trigger: RetainEvictionTrigger): void;
@@ -1,4 +1,5 @@
1
1
  import { delimitUntrusted } from "../core/untrusted-text.js";
2
+ import { createSafeNotifier } from "../core/safe-notify.js";
2
3
  import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
3
4
  export const SUBAGENT_RESUME_CAP = 8;
4
5
  export class SubagentRetainLedger {
@@ -11,6 +12,7 @@ export class SubagentRetainLedger {
11
12
  hooks;
12
13
  failedEvictions = 0;
13
14
  lastFailure;
15
+ notifier = createSafeNotifier();
14
16
  constructor(config, hooks) {
15
17
  const cfg = config === true ? {} : config;
16
18
  this.ttlMs = cfg.ttlMs ?? RETAIN_DEFAULT_TTL_MS;
@@ -23,15 +25,14 @@ export class SubagentRetainLedger {
23
25
  get lastEvictionError() {
24
26
  return this.lastFailure;
25
27
  }
28
+ get evictionSinkFailureCount() {
29
+ return this.notifier.failuresAt("retainLedger.onEvictionError");
30
+ }
26
31
  noteDetachedFailure(trigger, error, parentToolCallId) {
27
32
  const err = error instanceof Error ? error : new Error(String(error));
28
33
  this.failedEvictions += 1;
29
34
  this.lastFailure = err;
30
- try {
31
- this.hooks?.onEvictionError?.({ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}), trigger, error: err });
32
- }
33
- catch {
34
- }
35
+ this.notifier.notify(() => this.hooks?.onEvictionError?.({ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}), trigger, error: err }), "retainLedger.onEvictionError");
35
36
  }
36
37
  evictDetached(parentToolCallId, trigger) {
37
38
  void this.evict(parentToolCallId).catch((e) => this.noteDetachedFailure(trigger, e, parentToolCallId));
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { atomicWriteFile } from "../stores/file/fs-atomic.js";
3
3
  import { dirname } from "node:path";
4
4
  import { normalizeAgentName } from "../core/task-registry.js";
5
+ import { createSafeNotifier } from "../core/safe-notify.js";
5
6
  export function entryAccessible(e, access) {
6
7
  if (e.scope === undefined || access.scope === undefined || access.scope !== e.scope)
7
8
  return false;
@@ -35,15 +36,12 @@ function capped(entries, opts) {
35
36
  const max = opts?.maxEntries;
36
37
  if (max === undefined || entries.length <= max)
37
38
  return entries;
39
+ const notifier = createSafeNotifier();
38
40
  const byAge = [...entries].sort((a, b) => a.createdAt - b.createdAt);
39
41
  const dropCount = Math.max(0, entries.length - Math.max(0, max));
40
42
  const dropped = new Set(byAge.slice(0, dropCount));
41
43
  for (const e of dropped) {
42
- try {
43
- opts?.onEvicted?.(e);
44
- }
45
- catch {
46
- }
44
+ notifier.notify(() => opts?.onEvicted?.(e), "rosterStore.onEvicted");
47
45
  }
48
46
  return entries.filter((e) => !dropped.has(e));
49
47
  }