@sema-agent/core 5.54.0 → 5.56.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 (85) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/dist/agents/cumulative-stats.d.ts +26 -0
  3. package/dist/agents/cumulative-stats.js +56 -0
  4. package/dist/agents/observer.d.ts +11 -7
  5. package/dist/agents/observer.js +2 -4
  6. package/dist/agents/send-message-tool.js +48 -2
  7. package/dist/agents/subagent.js +250 -89
  8. package/dist/agents/verify.d.ts +27 -3
  9. package/dist/agents/verify.js +7 -2
  10. package/dist/core/auto-compaction.d.ts +17 -4
  11. package/dist/core/auto-compaction.js +3 -0
  12. package/dist/core/context-edit.d.ts +55 -6
  13. package/dist/core/context-edit.js +12 -1
  14. package/dist/core/governance-codes.js +14 -0
  15. package/dist/core/hooks.d.ts +293 -11
  16. package/dist/core/hooks.js +159 -12
  17. package/dist/core/human-input-projection.d.ts +20 -2
  18. package/dist/core/human-input-projection.js +9 -0
  19. package/dist/core/lsp-diagnostics.d.ts +19 -17
  20. package/dist/core/lsp-diagnostics.js +11 -5
  21. package/dist/core/mcp.d.ts +46 -0
  22. package/dist/core/mcp.js +132 -6
  23. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  24. package/dist/core/memory-engine/consolidation.js +342 -0
  25. package/dist/core/memory-engine/dual-root.js +3 -0
  26. package/dist/core/memory-engine/engine.d.ts +237 -4
  27. package/dist/core/memory-engine/engine.js +1111 -4
  28. package/dist/core/memory-engine/export-bundle.js +9 -0
  29. package/dist/core/memory-engine/file-backend.js +27 -1
  30. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  31. package/dist/core/memory-engine/frontmatter.js +111 -0
  32. package/dist/core/memory-engine/index.d.ts +4 -2
  33. package/dist/core/memory-engine/index.js +3 -1
  34. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  35. package/dist/core/memory-engine/sync-client.js +26 -0
  36. package/dist/core/memory-engine/tools.d.ts +9 -0
  37. package/dist/core/memory-engine/tools.js +57 -13
  38. package/dist/core/memory-engine/types.d.ts +99 -0
  39. package/dist/core/memory-recall.js +4 -3
  40. package/dist/core/memory.d.ts +33 -3
  41. package/dist/core/memory.js +6 -4
  42. package/dist/core/permission-rules.d.ts +30 -0
  43. package/dist/core/permission-rules.js +71 -8
  44. package/dist/core/reminder-disclosure.d.ts +29 -4
  45. package/dist/core/reminder-disclosure.js +60 -12
  46. package/dist/core/runner/prepare-memory.js +7 -2
  47. package/dist/core/runner/prepare-task.d.ts +39 -1
  48. package/dist/core/runner/prepare-task.js +63 -35
  49. package/dist/core/runner/runtask.d.ts +8 -1
  50. package/dist/core/runner/runtask.js +170 -31
  51. package/dist/core/runner/session-rule-policy.js +5 -3
  52. package/dist/core/runner/synthetic-tools.js +4 -2
  53. package/dist/core/runner/turn-attachments.d.ts +16 -6
  54. package/dist/core/runner/turn-attachments.js +34 -20
  55. package/dist/core/session-reconcile.d.ts +32 -0
  56. package/dist/core/session-reconcile.js +15 -0
  57. package/dist/core/task-notification.d.ts +34 -7
  58. package/dist/core/task-notification.js +11 -1
  59. package/dist/core/task-registry-agent.d.ts +20 -3
  60. package/dist/core/task-registry-agent.js +31 -2
  61. package/dist/core/tool-policy.d.ts +23 -0
  62. package/dist/core/tool-policy.js +29 -13
  63. package/dist/core/types.d.ts +126 -17
  64. package/dist/core/untrusted-egress.js +12 -2
  65. package/dist/core/untrusted-text.d.ts +189 -3
  66. package/dist/core/untrusted-text.js +424 -6
  67. package/dist/engine/compaction/compaction.d.ts +77 -7
  68. package/dist/engine/compaction/compaction.js +98 -9
  69. package/dist/engine/compaction/utils.d.ts +4 -0
  70. package/dist/engine/compaction/utils.js +6 -0
  71. package/dist/engine/harness/agent-harness.d.ts +84 -0
  72. package/dist/engine/harness/agent-harness.js +88 -12
  73. package/dist/engine/harness/messages.d.ts +4 -2
  74. package/dist/engine/harness/messages.js +7 -2
  75. package/dist/engine/harness/types.d.ts +11 -5
  76. package/dist/engine/loop/types.d.ts +14 -0
  77. package/dist/engine/session/import-validate.js +10 -0
  78. package/dist/engine/session/session.js +2 -2
  79. package/dist/index.d.ts +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/orchestration/run-spec.js +8 -1
  82. package/dist/prompts/default.d.ts +22 -6
  83. package/dist/tools/fs/index.d.ts +3 -1
  84. package/package.json +1 -1
  85. package/test/export-surface.snapshot.json +28 -1
@@ -1032,6 +1032,134 @@ export function delegationEntryLedgerFootprint(registry) {
1032
1032
  handles += s.size;
1033
1033
  return { keys: byKey.size, handles };
1034
1034
  }
1035
+ const localDelegationInFlight = new WeakMap();
1036
+ function localDelegationInFlightCount(anchor, key) {
1037
+ return localDelegationInFlight.get(anchor)?.get(key) ?? 0;
1038
+ }
1039
+ function chargeLocalDelegationInFlight(anchor, key) {
1040
+ let byKey = localDelegationInFlight.get(anchor);
1041
+ if (byKey === undefined) {
1042
+ byKey = new Map();
1043
+ localDelegationInFlight.set(anchor, byKey);
1044
+ }
1045
+ byKey.set(key, (byKey.get(key) ?? 0) + 1);
1046
+ let released = false;
1047
+ return () => {
1048
+ if (released)
1049
+ return;
1050
+ released = true;
1051
+ const live = localDelegationInFlight.get(anchor);
1052
+ if (live === undefined)
1053
+ return;
1054
+ const n = (live.get(key) ?? 0) - 1;
1055
+ if (n > 0)
1056
+ live.set(key, n);
1057
+ else
1058
+ live.delete(key);
1059
+ };
1060
+ }
1061
+ const spawnAbortedResult = (wt) => ({
1062
+ isError: true,
1063
+ content: `Sub-agent not started: the delegating call was aborted.${wt ? `\n${wt}` : ""}`,
1064
+ details: { error: "aborted" },
1065
+ });
1066
+ const noopRelease = () => undefined;
1067
+ const DELEGATION_POOL_DEFAULT_SCOPE = "default";
1068
+ async function untilAbort(p, signal) {
1069
+ if (signal === undefined)
1070
+ return await p;
1071
+ if (signal.aborted)
1072
+ throw new Error("delegation entry gate: aborted before the durable floor was read");
1073
+ let onAbort;
1074
+ try {
1075
+ return await Promise.race([
1076
+ p,
1077
+ new Promise((_resolve, reject) => {
1078
+ onAbort = () => reject(new Error("delegation entry gate: aborted while reading the durable floor"));
1079
+ signal.addEventListener("abort", onAbort, { once: true });
1080
+ }),
1081
+ ]);
1082
+ }
1083
+ finally {
1084
+ if (onAbort !== undefined)
1085
+ signal.removeEventListener("abort", onAbort);
1086
+ }
1087
+ }
1088
+ async function openDelegationEntryGate(input) {
1089
+ const { poolAnchor, registry, store, root, caps, revival, signal } = input;
1090
+ if (root === undefined)
1091
+ return { key: undefined, admit: () => ({ admitted: true, release: noopRelease }) };
1092
+ const scope = input.scope ?? DELEGATION_POOL_DEFAULT_SCOPE;
1093
+ const key = JSON.stringify([scope, root]);
1094
+ let storedHandles = [];
1095
+ let storedEnumerationOk = false;
1096
+ if (store !== undefined && !revival) {
1097
+ try {
1098
+ storedHandles = (await untilAbort(store.listBySession(scope, root), signal)).map((r) => r.handle);
1099
+ storedEnumerationOk = true;
1100
+ }
1101
+ catch {
1102
+ storedHandles = [];
1103
+ }
1104
+ }
1105
+ const admit = (kind) => {
1106
+ const activeFace = registry === undefined ? undefined : registry.activeDelegationHandles;
1107
+ const active = typeof activeFace === "function" ? activeFace.call(registry, scope, root) : [];
1108
+ const local = localDelegationInFlightCount(poolAnchor, key);
1109
+ const running = active.length + local;
1110
+ if (running >= caps.maxConcurrent) {
1111
+ return {
1112
+ admitted: false,
1113
+ refusal: {
1114
+ code: "delegation.concurrency_cap",
1115
+ text: `this session tree already has ${running} agents running — the concurrency cap (${caps.maxConcurrent}; RunnerDeps.delegationEntryCaps.maxConcurrent) refuses another. ${kind === "registered" ? "Wait for one to complete (you will be notified) or stop one, then relaunch." : "Wait for one of them to finish or stop one, then try again."}`,
1116
+ },
1117
+ };
1118
+ }
1119
+ if (!revival) {
1120
+ const ledger = delegationEntryLedger(poolAnchor, key);
1121
+ let retained;
1122
+ if (store !== undefined && storedEnumerationOk) {
1123
+ const keep = new Set([...storedHandles, ...active]);
1124
+ if (kind === "registered") {
1125
+ for (const h of [...ledger])
1126
+ if (!keep.has(h))
1127
+ ledger.delete(h);
1128
+ retained = new Set([...storedHandles, ...ledger]).size;
1129
+ }
1130
+ else {
1131
+ const survivors = new Set(storedHandles);
1132
+ for (const h of ledger)
1133
+ if (keep.has(h))
1134
+ survivors.add(h);
1135
+ retained = survivors.size;
1136
+ }
1137
+ }
1138
+ else {
1139
+ retained = ledger.size;
1140
+ }
1141
+ const cumulative = retained + local;
1142
+ if (cumulative >= caps.maxCumulativePerSession) {
1143
+ return {
1144
+ admitted: false,
1145
+ refusal: {
1146
+ code: "delegation.session_cap",
1147
+ text: `this session tree has already launched ${cumulative} agents in its retained window — the cumulative cap (${caps.maxCumulativePerSession}; RunnerDeps.delegationEntryCaps.maxCumulativePerSession) refuses more. Continue an existing agent (SendMessage) instead of launching new ones.`,
1148
+ },
1149
+ };
1150
+ }
1151
+ }
1152
+ return { admitted: true, release: kind === "local" ? chargeLocalDelegationInFlight(poolAnchor, key) : noopRelease };
1153
+ };
1154
+ return { key, admit };
1155
+ }
1156
+ function chargeDelegationEntryHandle(registry, key, store, handle) {
1157
+ if (handle !== undefined)
1158
+ delegationEntryLedger(registry, key).add(handle);
1159
+ bindDelegationLedgerKeyStore(registry, key, store);
1160
+ armDelegationLedgerLifecycle(registry);
1161
+ sweepDelegationLedgerRound(registry, 3);
1162
+ }
1035
1163
  export function normalizeSubagentType(value) {
1036
1164
  return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
1037
1165
  }
@@ -1314,7 +1442,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1314
1442
  prepareArguments: (args) => foldGeneralPurposeAlias(args, generalPurposeShadowed),
1315
1443
  execute: async (args, ctx) => {
1316
1444
  const a = foldGeneralPurposeAlias(args, generalPurposeShadowed);
1317
- const wantsBackground = opts.background !== undefined && a.run_in_background !== false;
1318
1445
  const reviveClaim = ctx.reviveClaim;
1319
1446
  if (reviveClaim !== undefined && opts.background === undefined) {
1320
1447
  return {
@@ -1358,6 +1485,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1358
1485
  }
1359
1486
  const omitted = rawType === "" || (!generalPurposeShadowed && rawType === GENERAL_PURPOSE_SUBAGENT_TYPE);
1360
1487
  const def = hasAgents && !wantsFork && !omitted ? agentMap.get(rawType) : undefined;
1488
+ const wantsBackground = opts.background !== undefined && (a.run_in_background !== false || def?.background === true);
1361
1489
  const spawnAgentType = wantsFork ? FORK_SUBAGENT_TYPE : omitted ? GENERAL_PURPOSE_SUBAGENT_TYPE : rawType;
1362
1490
  if (!def && !wantsFork && !omitted) {
1363
1491
  return {
@@ -1789,6 +1917,21 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1789
1917
  return { ...(recentSteps ? { recentSteps } : {}), ...(editedFiles ? { editedFiles } : {}) };
1790
1918
  };
1791
1919
  const treeScope = reviveClaim?.row.scope ?? ctx.principal ?? opts.background?.scope;
1920
+ const entryCapPoolAnchor = opts.background?.registry ?? opts;
1921
+ const entryCaps = ctx.delegationEntryCaps ?? { maxConcurrent: DELEGATION_MAX_CONCURRENT_DEFAULT, maxCumulativePerSession: DELEGATION_MAX_PER_SESSION_DEFAULT };
1922
+ const entryCapRoot = reviveClaim !== undefined
1923
+ ? (reviveClaim.row.rootSessionId ?? reviveClaim.row.parentSessionId ?? (reviveClaim.row.sessionScoped ? reviveClaim.row.owner : undefined))
1924
+ : (ctx.rootSessionId ?? ctx.sessionId);
1925
+ const openEntryGate = () => openDelegationEntryGate({
1926
+ poolAnchor: entryCapPoolAnchor,
1927
+ registry: opts.background?.registry,
1928
+ store: opts.background?.agentStore,
1929
+ scope: treeScope,
1930
+ root: entryCapRoot,
1931
+ caps: entryCaps,
1932
+ revival: reviveClaim !== undefined,
1933
+ signal: ctx.signal,
1934
+ });
1792
1935
  const provenanceRequest = ctx.delegationProvenanceForChildren?.();
1793
1936
  const childProvenanceRef = provenanceRequest !== undefined ? { current: newDelegationProvenanceAggregate() } : undefined;
1794
1937
  const childAttestation = (status) => childProvenanceRef !== undefined ? reduceDelegationAttestation(childProvenanceRef.current, { completed: status === "completed" }) : undefined;
@@ -2053,7 +2196,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2053
2196
  if (ctx.signal?.aborted) {
2054
2197
  await cancelObserver();
2055
2198
  const wt = await finishWorktree();
2056
- return { isError: true, content: `Sub-agent not started: the delegating call was aborted.${wt ? `\n${wt}` : ""}`, details: { error: "aborted" } };
2199
+ return spawnAbortedResult(wt);
2057
2200
  }
2058
2201
  if (spawnVerdict.kind === "block") {
2059
2202
  await cancelObserver();
@@ -2106,6 +2249,21 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2106
2249
  const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
2107
2250
  const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2108
2251
  const bgScope = treeScope;
2252
+ const forkEntryGate = await openEntryGate();
2253
+ {
2254
+ const admission = forkEntryGate.admit("registered");
2255
+ if (!admission.admitted) {
2256
+ dropHostAbortListener();
2257
+ try {
2258
+ await (releaseForked ? releaseForked() : opts.runner.sessions.release?.(forkedId));
2259
+ }
2260
+ catch {
2261
+ }
2262
+ await cancelObserver();
2263
+ const wt = await finishWorktree();
2264
+ return errorResult(`Sub-agent not started in background: ${admission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: admission.refusal.code, code: admission.refusal.code });
2265
+ }
2266
+ }
2109
2267
  const forkSettlementSeat = ctx.delegationSettlement?.();
2110
2268
  const forkSettleId = forkSettlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
2111
2269
  if (forkSettlementSeat !== undefined && forkSettleId !== undefined) {
@@ -2195,6 +2353,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2195
2353
  }
2196
2354
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
2197
2355
  }
2356
+ if (forkEntryGate.key !== undefined)
2357
+ chargeDelegationEntryHandle(bg.registry, forkEntryGate.key, bg.agentStore, taskId);
2198
2358
  childInternals.peerSelfRef?.addAxis("h", taskId);
2199
2359
  bg.registry.bindBackgroundAgentSession(taskId, forkedId);
2200
2360
  const forkBgHangAt = Date.now();
@@ -2507,6 +2667,28 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2507
2667
  const bgIgnoredNote = a.run_in_background === true
2508
2668
  ? `note: run_in_background was ignored for subagent_type "${FORK_SUBAGENT_TYPE}" — the fork ran synchronously (this delegation tool has no background surface configured).`
2509
2669
  : undefined;
2670
+ const syncForkEntryGate = await openEntryGate();
2671
+ if (ctx.signal?.aborted) {
2672
+ try {
2673
+ await (releaseForked ? releaseForked() : opts.runner.sessions.release?.(forkedId));
2674
+ }
2675
+ catch {
2676
+ }
2677
+ await cancelObserver();
2678
+ const wt = await finishWorktree();
2679
+ return spawnAbortedResult(wt);
2680
+ }
2681
+ const syncForkAdmission = syncForkEntryGate.admit("local");
2682
+ if (!syncForkAdmission.admitted) {
2683
+ try {
2684
+ await (releaseForked ? releaseForked() : opts.runner.sessions.release?.(forkedId));
2685
+ }
2686
+ catch {
2687
+ }
2688
+ await cancelObserver();
2689
+ const wt = await finishWorktree();
2690
+ return errorResult(`Sub-agent not started: ${syncForkAdmission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: syncForkAdmission.refusal.code, code: syncForkAdmission.refusal.code });
2691
+ }
2510
2692
  let forkChild;
2511
2693
  try {
2512
2694
  forkChild = await opts.runner.runTask({ ...buildChildSpec(ctx.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective }, forkInternals);
@@ -2527,6 +2709,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2527
2709
  const wt = await finishWorktree();
2528
2710
  return { isError: true, content: `Fork failed: the forked run threw (${e instanceof Error ? e.message : String(e)}).${wt ? `\n${wt}` : ""}`, details: { error: "fork run failed" } };
2529
2711
  }
2712
+ finally {
2713
+ syncForkAdmission.release();
2714
+ }
2530
2715
  const forkHandbackWarning = await reviewHandback({
2531
2716
  review: ctx.autoModeReview,
2532
2717
  toolCallId: ctx.toolCallId,
@@ -2601,51 +2786,15 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2601
2786
  const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
2602
2787
  const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2603
2788
  const bgScope = treeScope;
2604
- const capScope = bgScope;
2605
- const capRoot = reviveRow !== undefined
2606
- ? (reviveRow.rootSessionId ?? reviveRow.parentSessionId ?? (reviveRow.sessionScoped ? reviveRow.owner : undefined))
2607
- : (ctx.rootSessionId ?? ctx.sessionId);
2608
- const entryCaps = ctx.delegationEntryCaps ?? { maxConcurrent: DELEGATION_MAX_CONCURRENT_DEFAULT, maxCumulativePerSession: DELEGATION_MAX_PER_SESSION_DEFAULT };
2609
- const capLedgerKey = capScope !== undefined && capRoot !== undefined ? JSON.stringify([capScope, capRoot]) : undefined;
2610
- if (capScope !== undefined && capRoot !== undefined) {
2611
- let storedHandles = [];
2612
- let storedEnumerationOk = false;
2613
- if (bg.agentStore !== undefined && reviveRow === undefined) {
2614
- try {
2615
- storedHandles = (await bg.agentStore.listBySession(capScope, capRoot)).map((r) => r.handle);
2616
- storedEnumerationOk = true;
2617
- }
2618
- catch {
2619
- storedHandles = [];
2620
- }
2621
- }
2622
- const capRefusal = async (code, text) => {
2789
+ const bgEntryGate = await openEntryGate();
2790
+ const capLedgerKey = bgEntryGate.key;
2791
+ {
2792
+ const admission = bgEntryGate.admit("registered");
2793
+ if (!admission.admitted) {
2623
2794
  dropHostAbortListener();
2624
2795
  await cancelObserver();
2625
2796
  const wt = await finishWorktree();
2626
- return errorResult(`Sub-agent not started in background: ${text}${wt ? `\n${wt}` : ""}`, { error: code, code });
2627
- };
2628
- const activeFace = bg.registry.activeDelegationHandles;
2629
- const active = typeof activeFace === "function" ? activeFace.call(bg.registry, capScope, capRoot) : [];
2630
- if (typeof activeFace === "function" && active.length >= entryCaps.maxConcurrent) {
2631
- return await capRefusal("delegation.concurrency_cap", `this session tree already has ${active.length} background agents running — the concurrency cap (${entryCaps.maxConcurrent}; RunnerDeps.delegationEntryCaps.maxConcurrent) refuses another. Wait for one to complete (you will be notified) or stop one, then relaunch.`);
2632
- }
2633
- if (reviveRow === undefined) {
2634
- const ledger = delegationEntryLedger(bg.registry, capLedgerKey);
2635
- let cumulative;
2636
- if (bg.agentStore !== undefined && storedEnumerationOk) {
2637
- const keep = new Set([...storedHandles, ...active]);
2638
- for (const h of [...ledger])
2639
- if (!keep.has(h))
2640
- ledger.delete(h);
2641
- cumulative = new Set([...storedHandles, ...ledger]).size;
2642
- }
2643
- else {
2644
- cumulative = ledger.size;
2645
- }
2646
- if (cumulative >= entryCaps.maxCumulativePerSession) {
2647
- return await capRefusal("delegation.session_cap", `this session tree has already launched ${cumulative} background agents in its retained window — the cumulative cap (${entryCaps.maxCumulativePerSession}; RunnerDeps.delegationEntryCaps.maxCumulativePerSession) refuses more. Continue an existing agent (SendMessage) instead of launching new ones.`);
2648
- }
2797
+ return errorResult(`Sub-agent not started in background: ${admission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: admission.refusal.code, code: admission.refusal.code });
2649
2798
  }
2650
2799
  }
2651
2800
  const settlementSeat = ctx.delegationSettlement?.();
@@ -2770,13 +2919,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2770
2919
  }
2771
2920
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
2772
2921
  }
2773
- if (capLedgerKey !== undefined && reviveRow === undefined)
2774
- delegationEntryLedger(bg.registry, capLedgerKey).add(taskId);
2775
- if (capLedgerKey !== undefined) {
2776
- bindDelegationLedgerKeyStore(bg.registry, capLedgerKey, bg.agentStore);
2777
- armDelegationLedgerLifecycle(bg.registry);
2778
- sweepDelegationLedgerRound(bg.registry, 3);
2779
- }
2922
+ if (capLedgerKey !== undefined)
2923
+ chargeDelegationEntryHandle(bg.registry, capLedgerKey, bg.agentStore, reviveRow === undefined ? taskId : undefined);
2780
2924
  childInternals.peerSelfRef?.addAxis("h", taskId);
2781
2925
  if (agentName !== undefined) {
2782
2926
  recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
@@ -3411,49 +3555,66 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3411
3555
  };
3412
3556
  }
3413
3557
  let child;
3558
+ const syncEntryGate = await openEntryGate();
3559
+ if (ctx.signal?.aborted) {
3560
+ await cancelObserver();
3561
+ const wt = await finishWorktree();
3562
+ return spawnAbortedResult(wt);
3563
+ }
3564
+ const syncAdmission = syncEntryGate.admit("local");
3565
+ if (!syncAdmission.admitted) {
3566
+ await cancelObserver();
3567
+ const wt = await finishWorktree();
3568
+ return errorResult(`Sub-agent not started: ${syncAdmission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: syncAdmission.refusal.code, code: syncAdmission.refusal.code });
3569
+ }
3414
3570
  const syncStartedAt = Date.now();
3415
3571
  let retainEntry;
3416
- if (ctx.onSubagentSpawn || observerPairing) {
3417
- retainEntry = ctx.onSubagentSpawn ? await tryRetainChild(ctx.subagentRetain) : undefined;
3418
- if (retainEntry)
3419
- stepRecorder.lockTo(retainEntry.childSessionId);
3420
- const stream = opts.runner.runTaskStream({ ...buildChildSpec(ctx.signal), ...(retainEntry ? { sessionId: retainEntry.childSessionId } : {}) }, undefined, childInternals);
3421
- observedSteerRef.steer = (text, o) => stream.steer(text, o);
3422
- let settleResolve;
3423
- const settled = new Promise((r) => { settleResolve = r; });
3424
- if (ctx.onSubagentSpawn) {
3425
- const resume = createSubagentResume({
3426
- ledger: ctx.subagentRetain,
3427
- parentToolCallId: ctx.toolCallId,
3428
- runner: opts.runner,
3429
- ...(opts.background?.notify ? { notify: opts.background.notify } : {}),
3430
- sink: ctx.onSubagentSpawn,
3431
- ...(opts.background ? { registry: opts.background.registry } : {}),
3432
- ...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
3433
- ...(ctx.onQuestion !== undefined ? { currentOnQuestion: ctx.onQuestion } : {}),
3434
- ...(ctx.autoModeReview !== undefined ? { currentAutoModeReview: ctx.autoModeReview } : {}),
3435
- });
3436
- notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
3437
- resume,
3438
- ...(retainEntry ? { childSessionId: retainEntry.childSessionId } : {}),
3439
- })), "subagent.onSubagentSpawn");
3440
- }
3441
- try {
3442
- for await (const ev of stream)
3443
- observerTap?.record(ev);
3444
- child = await stream.result();
3445
- }
3446
- catch (e) {
3447
- await settleObserver("failed");
3448
- throw e;
3572
+ try {
3573
+ if (ctx.onSubagentSpawn || observerPairing) {
3574
+ retainEntry = ctx.onSubagentSpawn ? await tryRetainChild(ctx.subagentRetain) : undefined;
3575
+ if (retainEntry)
3576
+ stepRecorder.lockTo(retainEntry.childSessionId);
3577
+ const stream = opts.runner.runTaskStream({ ...buildChildSpec(ctx.signal), ...(retainEntry ? { sessionId: retainEntry.childSessionId } : {}) }, undefined, childInternals);
3578
+ observedSteerRef.steer = (text, o) => stream.steer(text, o);
3579
+ let settleResolve;
3580
+ const settled = new Promise((r) => { settleResolve = r; });
3581
+ if (ctx.onSubagentSpawn) {
3582
+ const resume = createSubagentResume({
3583
+ ledger: ctx.subagentRetain,
3584
+ parentToolCallId: ctx.toolCallId,
3585
+ runner: opts.runner,
3586
+ ...(opts.background?.notify ? { notify: opts.background.notify } : {}),
3587
+ sink: ctx.onSubagentSpawn,
3588
+ ...(opts.background ? { registry: opts.background.registry } : {}),
3589
+ ...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
3590
+ ...(ctx.onQuestion !== undefined ? { currentOnQuestion: ctx.onQuestion } : {}),
3591
+ ...(ctx.autoModeReview !== undefined ? { currentAutoModeReview: ctx.autoModeReview } : {}),
3592
+ });
3593
+ notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
3594
+ resume,
3595
+ ...(retainEntry ? { childSessionId: retainEntry.childSessionId } : {}),
3596
+ })), "subagent.onSubagentSpawn");
3597
+ }
3598
+ try {
3599
+ for await (const ev of stream)
3600
+ observerTap?.record(ev);
3601
+ child = await stream.result();
3602
+ }
3603
+ catch (e) {
3604
+ await settleObserver("failed");
3605
+ throw e;
3606
+ }
3607
+ finally {
3608
+ settleResolve();
3609
+ ctx.subagentRetain?.markSettled(ctx.toolCallId);
3610
+ }
3449
3611
  }
3450
- finally {
3451
- settleResolve();
3452
- ctx.subagentRetain?.markSettled(ctx.toolCallId);
3612
+ else {
3613
+ child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
3453
3614
  }
3454
3615
  }
3455
- else {
3456
- child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
3616
+ finally {
3617
+ syncAdmission.release();
3457
3618
  }
3458
3619
  await settleObserver(child.status);
3459
3620
  const handbackWarning = await reviewHandback({
@@ -172,9 +172,20 @@ export interface VerificationOutcome {
172
172
  * Total cost (micro-USD) of the VERIFIER run(s) across all rounds — the verification OVERHEAD, separate from
173
173
  * the implementation's own cost (which is the returned `TaskResult.stats`, as the verifier runs in its own
174
174
  * session). Mirrors `runWithTeacher`'s `teacherStats` work-vs-overhead split. Σ of each verifier run's
175
- * cost+nested. `result.stats.costMicroUsd + verification.verifierCost` is the EXACT operation total for the
176
- * common single-pass case; in the rarer multi-round fix case `result.stats` is the FINAL impl attempt's cost
177
- * (the returned `...current`) so an earlier failed attempt's impl cost is not separately surfaced. Omitted
175
+ * cost+nested.
176
+ *
177
+ * THE OPERATION TOTAL, exactly (note the middle term it is easy to miss and this doc used to omit it):
178
+ * `stats.costMicroUsd + (stats.nested?.costMicroUsd ?? 0) + verification.verifierCost`. The axes are
179
+ * DISJOINT, so nothing is counted twice — but they are not symmetric, which is why the nested term is
180
+ * spelled out: THIS field already folds the verifier's own delegated spend into itself, while the
181
+ * implementation side keeps own and nested apart (the family convention — a consumer adds `stats.X +
182
+ * stats.nested.X` for the true total, and nested cost is never folded into `costMicroUsd`).
183
+ *
184
+ * That total is now exact **in the multi-round fix case too**, since the returned `stats` accumulates
185
+ * every impl-side leg (see {@link VerificationResult}). It held only for the single-pass case before: the
186
+ * returned stats were the FINAL impl attempt's alone, so once a fix round ran, the ORIGINAL attempt's
187
+ * cost was absent from the entire return value and any such sum silently undercounted the operation by
188
+ * exactly that much. Omitted
178
189
  * (undefined) when no verifier ran (e.g. an impl that suspended/was opted out before verification).
179
190
  */
180
191
  verifierCost?: number;
@@ -201,6 +212,19 @@ export interface VerificationOutcome {
201
212
  export interface VerificationResult extends TaskResult {
202
213
  /** The verification outcome. The task `result`/`status` is the implementation's; consult `verdict` for quality. */
203
214
  verification: VerificationOutcome;
215
+ /**
216
+ * ACCOUNTING NOTE for the inherited `stats` (it is the ONE inherited field the gate does not simply pass
217
+ * through). The gate can run the implementation session SEVERAL times — the impl leg, then a continuation
218
+ * per fix round — and each leg's own `stats` cover only that leg. So `stats` here is the CUMULATIVE
219
+ * impl-side account (every impl + fix leg summed, own and nested kept disjoint as everywhere else), not
220
+ * the final leg's; the non-accumulated fields such as `model` come from the final leg. When exactly one
221
+ * leg contributed (the common no-fix path, and every early hand-back before a fix round), it is that
222
+ * leg's own object, untouched.
223
+ *
224
+ * The VERIFIER's spend is NOT in here — it is the separate overhead axis on
225
+ * {@link VerificationOutcome.verifierCost}, which documents the sum that gives the operation total.
226
+ */
227
+ stats: TaskResult["stats"];
204
228
  }
205
229
  /**
206
230
  * Verify an already-**completed** implementation `result` behind the independent falsification-style verifier,
@@ -4,6 +4,7 @@ import { releaseSession } from "./session-util.js";
4
4
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
5
5
  import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js";
6
6
  import { createSafeNotifier } from "../core/safe-notify.js";
7
+ import { createCumulativeStatsTracker } from "./cumulative-stats.js";
7
8
  export const VERIFICATION_PROMPT = `You are a verification specialist. Your job is NOT to confirm the implementation works — it is to try to BREAK it.
8
9
 
9
10
  You have two documented failure patterns. First, verification avoidance: faced with a check, you find reasons not to run it — you read code, narrate what you would test, declare "PASS," and move on. Second, being seduced by the first 80%: a polished result or a passing test suite makes you inclined to pass it, not noticing the edge that crashes, the state that vanishes, the bad input that is unhandled. The first 80% is the easy part. Your entire value is in finding the last 20%.
@@ -172,6 +173,9 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
172
173
  }
173
174
  };
174
175
  let current = result;
176
+ const implAccount = createCumulativeStatsTracker();
177
+ implAccount.add(result.stats);
178
+ const withImplAccount = (r) => implAccount.legs > 1 ? { ...r, stats: implAccount.build(r.stats) } : r;
175
179
  let outcome = { verdict: "unverified", rounds: 0, findings: [] };
176
180
  const startedAt = Date.now();
177
181
  let spend = 0;
@@ -240,13 +244,14 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
240
244
  ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
241
245
  }, internals);
242
246
  spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
247
+ implAccount.add(current.stats);
243
248
  if (isDurablePause(current.status)) {
244
- return { ...mapNestedSuspend(carryFrozenPosture(current)), verification: outcome };
249
+ return { ...mapNestedSuspend(carryFrozenPosture(withImplAccount(current))), verification: outcome };
245
250
  }
246
251
  if (current.status !== "completed")
247
252
  break;
248
253
  }
249
- return { ...carryFrozenPosture(current), verification: outcome };
254
+ return { ...carryFrozenPosture(withImplAccount(current)), verification: outcome };
250
255
  }
251
256
  export async function runWithVerification(runner, implSpec, config = {}, internals) {
252
257
  refuseUnhonorableInternals(internals, "door");
@@ -383,7 +383,12 @@ export interface MaybeCompactOptions {
383
383
  * compaction is FORCED through the real `generateSummary` LLM path even if the provider would hit — this
384
384
  * bounds summary drift from indefinite reuse. The caller owns the counter via {@link onCompaction}
385
385
  * (`reused`) and feeds it back via this option; core treats `summaryProvider` as if absent for that one
386
- * boundary when `consecutiveProviderReuse >= maxConsecutiveProviderReuse`. Default `3` (CC parity).
386
+ * boundary when `consecutiveProviderReuse >= maxConsecutiveProviderReuse`. Default `3`, a
387
+ * SEMA choice — the earlier "(CC parity)" label was unsupported and is withdrawn (anchoring
388
+ * re-check 2026-08-23: CC 2.1.223 has no external summary provider and no reuse concept at all;
389
+ * its three 3s are the PTL retry cap `b$d`, the consecutive-compaction-failure breaker `C$d` and
390
+ * the rapid-refill trip `Q3u`, none of them this). 3 is picked for the same reason those are: a
391
+ * small bound on how long a degraded path may keep answering before the real one is forced.
387
392
  * `0`/undefined with no `consecutiveProviderReuse` = provider always consulted (no forced refresh).
388
393
  */
389
394
  maxConsecutiveProviderReuse?: number;
@@ -618,9 +623,17 @@ export declare function maybeCompact(opts: MaybeCompactOptions): Promise<{
618
623
  * (clamped ≥0, so ≤0 means "freed nothing") — orthogonal to rule 2/3's threshold test, which
619
624
  * misses this shape because the bloated post (44609) can still sit UNDER the threshold (44800),
620
625
  * so rule 3 alone would re-enable the force and repeat the negative-yield pass every boundary.
621
- * SEMA-ONLY DEFENSE, not a CC port: CC's full compact keeps tail 0 and monotonically shrinks
622
- * (capture §A4), and CC has no request-layer trim, so this regime cannot exist there it is a
623
- * derived interaction of our trim seam × keep-tail × attachment mass. Scoped to trim-forced
626
+ * SEMA-ONLY DEFENSE, not a CC port but the ORIGINAL justification for that label was wrong
627
+ * on both of its premises, and the corrected pair still supports it. (a) "CC's full compact
628
+ * keeps tail 0 and monotonically shrinks" describes the `GNo` branch, which a LOCAL threshold
629
+ * compaction never reaches (CC 2.1.223 `y9s` :432704 routes local passes to the reactive
630
+ * pipeline, which preserves the last group verbatim — `YMo` :399971, `s = 1` :399982); CC's
631
+ * default posture therefore has a keep-tail, exactly as ours does. (b) "CC has no
632
+ * request-layer trim" holds only for the MAIN conversation request: CC does own a
633
+ * keep-the-newest-groups-that-fit primitive (`UPb` :645223), whose only caller is the
634
+ * prompt-hook evaluator's own transcript window, not a main-lane request. What actually makes
635
+ * this regime ours is the third factor — the trim seam feeding a deceived usage anchor, and
636
+ * attachment mass re-entering after the fold — not the absence of a keep-tail. Scoped to trim-forced
624
637
  * passes only (`trimForced`), so Seam C reused-summary landings (freed≈0 by design) on the
625
638
  * natural/manual paths never trip it. Release path unchanged: backoff gates only the FORCE, a
626
639
  * later natural landing that posts under threshold (rule 3, non-trim-forced) turns it back off.
@@ -196,6 +196,9 @@ export async function maybeCompact(opts) {
196
196
  if (prep.value.elidedMessages !== undefined && prep.value.elidedMessages > 0) {
197
197
  details.elidedMessages = prep.value.elidedMessages;
198
198
  }
199
+ if (prep.value.carriedUnsummarizedMessages !== undefined && prep.value.carriedUnsummarizedMessages > 0) {
200
+ details.unsummarizedMessages = prep.value.carriedUnsummarizedMessages;
201
+ }
199
202
  }
200
203
  else {
201
204
  let summaryModel = opts.compactionModel ?? opts.model;
@@ -14,11 +14,18 @@ export declare const EDIT_FRACTION = 0.7;
14
14
  * effectiveWindow = autocompactWindow − min(maxOutputTokens, 20000) (CC `Nye`, cap `uMd`)
15
15
  * trigger = effectiveWindow − 13000 (CC `dSo`, buffer `rMd`)
16
16
  * i.e. trigger = W − 33000 for every model whose max output is ≥ 20k: a 200k window triggers at
17
- * 167000 (83.5% of nominal), a 1M window at 967000. (CC additionally ships a sonnet-5-only 1M
18
- * autocompact-window special of 967000 via dynamic config, making ITS 1M trigger 934000; that is a
19
- * per-model config delivery, not geometry an embedder reproduces it EXACTLY via
20
- * `model.autoCompactTokens = 967000`, which lowers only this trigger-side window while the guard
21
- * and physical request budgeting stay on the 1M window CC's dual-window shape.) We take the
17
+ * 167000 (83.5% of nominal), a 1M window at 967000.
18
+ *
19
+ * 1M DIVERGENCE, stated as fact rather than as an equivalence (anchoring re-check 2026-08-23): CC
20
+ * carries a MODEL-DEFAULT autocompact-window table (2.1.223 `j3u` :242918 `claude-sonnet-5`
21
+ * default 967000, and since 223 a surface dimension: `remote_cowork` / `local-agent` 500000), so a
22
+ * 1M sonnet-5 gets 967000 as its window and 934000 as its trigger WITHOUT anyone configuring
23
+ * anything. We ship no such table: a model declaring only `contextWindow: 1e6` triggers at 967000
24
+ * here, 33000 LATER than CC. `model.autoCompactTokens = 967000` reproduces CC's number — but that
25
+ * is an EMBEDDER action against CC's product default, not the same posture, and the choice not to
26
+ * follow 934000 is deliberate (design/146 §1.3 argues against living on that thin a cushion). The
27
+ * knob itself is real and unchanged: `autoCompactTokens` lowers only this trigger-side window while
28
+ * the guard and physical request budgeting stay on the 1M window — CC's dual-window shape. We take the
22
29
  * 20000 cap branch unconditionally (flat 33000), i.e. the `min(maxOutputTokens, 20000)` branch is
23
30
  * deliberately NOT ported: the deduction is exact parity only for models with max output ≥ 20k. A
24
31
  * model with a smaller max output would deduct less in CC (trigger LATER); we deduct the full
@@ -51,11 +58,53 @@ export declare function contextEditFrontier(window: number): number;
51
58
  * PowerShell is kept for CC fidelity even though sema does not mount it. Overridable per call via
52
59
  * {@link ContextEditOptions.compactableTools} for custom-tool-heavy embedders.
53
60
  */
61
+ /**
62
+ * How many most-recent CANDIDATE tool results keep their content when the stale-result pass runs.
63
+ *
64
+ * 3, UNCHANGED — and the fact that CC's corresponding number is 5 (2.1.223 `uAp`, and the same value
65
+ * on CC 88's `timeBasedMCConfig`, so it is stable across both corpus generations) is deliberately NOT
66
+ * adopted here. Two reasons, in order of weight:
67
+ *
68
+ * 1. MEASURED: widening the window removes this pass's only lever in the shape where a terminal
69
+ * parallel batch holds exactly as many large results as the window keeps. Probed on a 200k model
70
+ * with a 140k usage anchor and five 36k-char Bash results: at 3 the pass clears two and the
71
+ * request lands at ~167k, UNDER the 177k request guard; at 5 nothing is clearable, the request
72
+ * stays at 185k, and the guard cannot recover it either — `trimToBudget` must turn-align back to
73
+ * the emitting assistant, so the whole batch is retained. The window is count-based on both
74
+ * sides (CC's `slice(-keepRecent)` too), so this ceiling exists at every value; 5 simply widens
75
+ * the band that reaches it.
76
+ * 2. SEAT: the two numbers do not govern the same machine. CC's keep-recent clear runs ONLY on the
77
+ * `context_hint` rejection leg — gated off by default — and behind a hard "saves ≥ 20000 tokens
78
+ * or don't bother" test, with request REFUSAL as the real backstop. Ours is on by default, is
79
+ * the only reduction between the frontier and the guard, and its backstop drops messages instead
80
+ * of refusing. Copying a constant across that difference is the "same name, different question"
81
+ * mistake, not parity.
82
+ *
83
+ * So the VALUE is an open adjudication (recorded with the probe above), while the two things CC
84
+ * unambiguously answers — the floor and the candidate-scoped window — are followed exactly.
85
+ */
86
+ export declare const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
87
+ /** Minimum kept results — CC `EUs` :397710 `Math.max(1, keepRecent)`. See
88
+ * {@link ContextEditOptions.keepRecentToolResults} for why both degenerate ends are unusable. */
89
+ export declare const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
54
90
  export declare const COMPACTABLE_TOOLS: ReadonlySet<string>;
55
91
  export interface ContextEditOptions {
56
92
  /** Start clearing once estimated context tokens exceed this. */
57
93
  budgetTokens: number;
58
- /** Always keep the content of this many most-recent tool results. Default 3. */
94
+ /**
95
+ * Always keep the content of this many most-recent CANDIDATE tool results (candidates = results
96
+ * from {@link compactableTools}). Default {@link DEFAULT_KEEP_RECENT_TOOL_RESULTS} = 3 — see that
97
+ * constant for why CC's 5 is not adopted here.
98
+ *
99
+ * Floored at 1, which IS CC's rule (`EUs` `Math.max(1, keepRecent)`), whose own source note gives
100
+ * the reason: 0 leaves the model with zero working tool context, and in CC's `slice(-0)` spelling
101
+ * it degenerates the other way into keeping everything — two unusable extremes for one value.
102
+ * Ours degenerated identically (a 0 or negative `keep` cleared EVERY candidate). Values that are
103
+ * not a non-negative safe integer are REFUSED rather than floored: a NaN used to make this whole
104
+ * defense silently inert (`slice(0, NaN)` = clear nothing), which is the one outcome a context
105
+ * defense must never reach quietly, and there is no config door upstream to announce at — this
106
+ * option only ever arrives as a direct argument from an embedder.
107
+ */
59
108
  keepRecentToolResults?: number;
60
109
  /**
61
110
  * roadmap #6② (CC contentReplacementState parity): when set, a result's FULL TEXT is offloaded to