@sema-agent/core 5.55.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.
- package/CHANGELOG.md +66 -0
- package/dist/agents/send-message-tool.js +48 -2
- package/dist/agents/subagent.js +250 -89
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +158 -11
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/permission-rules.d.ts +23 -15
- package/dist/core/permission-rules.js +40 -31
- package/dist/core/runner/prepare-task.d.ts +8 -0
- package/dist/core/runner/prepare-task.js +34 -23
- package/dist/core/runner/runtask.js +158 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +14 -9
- package/dist/core/tool-policy.js +27 -22
- package/dist/core/types.d.ts +37 -11
- package/dist/core/untrusted-text.js +8 -0
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +88 -12
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +11 -5
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +10 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.56.0 — 2026-08-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **A2A covering rules join the mcp form** (#410, ruled): `a2a__peer` / `a2a__peer__*` deny, ask
|
|
7
|
+
and allow now fire in all five name-keyed lanes (rule DSL, allow/deny policy, approval policy,
|
|
8
|
+
frozen projection replay, persisted session rules) via the protocol-table-driven
|
|
9
|
+
`namespacedRuleNameCovers`. **Narrowings**: a frozen projection replayed under 5.56 honors
|
|
10
|
+
covering spellings it previously read as inert (heaviest); covering-name parenthesised rules
|
|
11
|
+
refuse (`unsupported.covering_paren`, additive union member); empty-peer spellings refuse at
|
|
12
|
+
construction. **Widening / false-refusal repair**: real minted names with separator-bearing tool
|
|
13
|
+
segments (`mcp__srv____foo` and twins) that previously THREW at construction now compile as
|
|
14
|
+
exact entries (the name-set screen judges the peer boundary only).
|
|
15
|
+
- **Delegation entry caps cover every local spawn leg** (#385, anchor campaign): sync spawn,
|
|
16
|
+
sync fork and background fork now pass the same concurrency/cumulative gate as background
|
|
17
|
+
spawns (one pool, four admit sites; in-flight reservations returned on settle). Sync legs mint
|
|
18
|
+
`delegation.concurrency_cap` / `delegation.session_cap` under the `Sub-agent not started: `
|
|
19
|
+
prefix (new code+prefix combination); counted nouns drop the word "background";
|
|
20
|
+
`AgentDefinition.background: true` is honored (previously a dead field); no-principal mounts
|
|
21
|
+
fold to the engine default pool instead of skipping the gate.
|
|
22
|
+
- **Every deployment hook seat is time-bounded** (#388, anchor campaign): the ten `Hooks` seats
|
|
23
|
+
ride one bounded family (default 600s; `Hooks.timeoutMs` knob, bad values refused loudly).
|
|
24
|
+
Decision seats fail closed on expiry (preToolUse → deny + permissionDenied(source "hook");
|
|
25
|
+
userPromptSubmit → block; stop → run ends); observation seats discard and shrink to a 1s grace
|
|
26
|
+
after task abort; hook contexts carry `signal` and live tracked `cwd`. Pre-ship rescan closures:
|
|
27
|
+
the runSpec hook mirror now carries `timeoutMs` (a deployment's bound was silently replaced by
|
|
28
|
+
the default on composed runs); stopFailure reads the deadline rather than a constant grace; the
|
|
29
|
+
post-abort grace caps remaining time instead of restarting the clock.
|
|
30
|
+
- **interrupt() keeps accepted user input; an interrupt leaves CC's transcript marker** (#389,
|
|
31
|
+
anchor campaign): steer queues survive `interrupt()` (one-way abort latch + in-flight slot);
|
|
32
|
+
a durable park migrates queued steers onto the checkpoint (idempotent CAS verb) and the
|
|
33
|
+
undrained account announces only the remainder; an explicit interrupt that won the abort mints
|
|
34
|
+
CC's verbatim `[Request interrupted by user]` marker (double-gated); unknown
|
|
35
|
+
`SystemInjectionPriority` values refuse and the unimplemented `"now"` ladder is disclosed once
|
|
36
|
+
per run (`task.injection_priority_unimplemented`); the untrusted caller steer lane gains CC's
|
|
37
|
+
mid-turn delivery frame.
|
|
38
|
+
- **A SendMessage name that changed hands refuses instead of silently redirecting** (CC's
|
|
39
|
+
`send_message_pin_guard` rebound arm): "Nothing was sent." plus both ways forward; a precise
|
|
40
|
+
task_id is never refused and re-pins; the tool description states the refusal.
|
|
41
|
+
- **A legacy-decided parked row converges** (#400, test P0-KPI): the live reconcile's resolved
|
|
42
|
+
skip now means "a claim is in flight" (read from the store row) — a checkpoint decided by the
|
|
43
|
+
task-level legacy lane settles the row instead of leaving a process-lifetime zombie; the
|
|
44
|
+
decision is re-read immediately before the verdict so a reopen CAS is never judged dead.
|
|
45
|
+
- **Compaction PTL retries disclose what they dropped** (#407, test P0-KPI):
|
|
46
|
+
`CompactionDetails.unsummarizedMessages` accumulates the true uncovered count across split
|
|
47
|
+
legs, with a shell-copy clause and an import-gate check. The summary prompt carries CC's
|
|
48
|
+
`<example>` skeleton and a compliant `<summary>` envelope is unwrapped in the persisted body
|
|
49
|
+
(pre-ship rescan closure: a fence-only EMPTY response is refused loudly, same class and wording
|
|
50
|
+
as the bare-empty arm — it could previously persist a footer-only summary).
|
|
51
|
+
`keepRecentToolResults` floors at 1 and refuses NaN/negative loudly.
|
|
52
|
+
- **#384 (keepRecentTokens default) closed as REFUTED, with evidence**: the anchor-campaign claim
|
|
53
|
+
that the 0 default meant "no kept tail" was disproved on a real Runner — `findCutPoint` has a
|
|
54
|
+
structural floor, the kept tail is never empty, and the pinned behavior matches CC's posture
|
|
55
|
+
via the other knob. The default is unchanged; the floor is now pinned
|
|
56
|
+
(test/compaction-keep-tail-floor.test.ts) and four stale anchor comments were corrected.
|
|
57
|
+
|
|
58
|
+
### Notes
|
|
59
|
+
- Anchor-campaign satellite dispositions ride along (A3/A4/A8/A14 candidate tables in the
|
|
60
|
+
respective cars' receipts): truthfulness rewrites, registry entries, and the
|
|
61
|
+
PermissionDenied trigger-surface divergence registered.
|
|
62
|
+
- Erratum owed to sibling repos (also stated in src/core/hooks.ts): "CC fails open on hook
|
|
63
|
+
faults except exit 2" is true only of the command(shell) hook channel; CC's in-process
|
|
64
|
+
callback channel is fail-closed with dedicated copy — cited as a fail-open precedent it is
|
|
65
|
+
the wrong channel.
|
|
66
|
+
- Residuals filed, not shipped silently: #417-#423 (auth seats, Agent(<type>) governance,
|
|
67
|
+
lifecycle legs, postToolUse payload asymmetry, reopen fence, classic-arm Trust-but-verify).
|
|
68
|
+
|
|
3
69
|
## 5.55.0 — 2026-08-23
|
|
4
70
|
|
|
5
71
|
### BREAKING
|
|
@@ -5,6 +5,7 @@ import { announceTranscriptIntegrityGapOnce, canAccessAgentRecord, clearRevivedR
|
|
|
5
5
|
import { uuidv7 } from "../internal/harness.js";
|
|
6
6
|
import { MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../core/mailbox-store.js";
|
|
7
7
|
import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
8
|
+
import { inlineUntrusted } from "../core/untrusted-text.js";
|
|
8
9
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
|
|
9
10
|
import { createSubagentResume } from "./subagent.js";
|
|
10
11
|
import { appendHopToken, judgePeerAdmission, peerAxisToken, resolvePeerAdmissionConfig, PEER_MESSAGE_NOTICE, } from "./peer-admission.js";
|
|
@@ -45,6 +46,35 @@ function targetLaneKey(scope, targetId) {
|
|
|
45
46
|
const OPERATOR_CONTINUATION_CTX = Symbol("sema.operator_continuation");
|
|
46
47
|
export function createSendMessageTool(opts) {
|
|
47
48
|
const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
|
|
49
|
+
const sendMessagePins = new Map();
|
|
50
|
+
const pinGuard = (targetId, targetName, rung, to) => {
|
|
51
|
+
if (rung === "other")
|
|
52
|
+
return undefined;
|
|
53
|
+
const pinKey = normalizeAgentName(targetName ?? "");
|
|
54
|
+
if (pinKey === "")
|
|
55
|
+
return undefined;
|
|
56
|
+
const pinned = sendMessagePins.get(pinKey);
|
|
57
|
+
if (pinned === undefined || pinned.id === targetId) {
|
|
58
|
+
sendMessagePins.set(pinKey, { id: targetId, name: targetName ?? to });
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (rung === "ref") {
|
|
62
|
+
sendMessagePins.set(pinKey, { id: targetId, name: targetName ?? to });
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (targetName !== undefined && normalizeAgentName(to) === pinKey && normalizeAgentName(pinned.name) !== pinKey) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: `Message not sent: "${inlineUntrusted(to, 120)}" now resolves to a different agent than it did earlier in ` +
|
|
70
|
+
`this run — this name previously resolved to ${pinned.id.slice(0, 64)}, which it no longer reaches. Nothing was sent.\n` +
|
|
71
|
+
`It now resolves to ${targetId.slice(0, 64)}. To message that agent, re-send with its task_id: ` +
|
|
72
|
+
`{"to": "${targetId.slice(0, 64)}", …}. To reach the earlier agent, use its task_id ` +
|
|
73
|
+
`${pinned.id.slice(0, 64)} from its spawn result.`,
|
|
74
|
+
details: { error: "rebound", to, taskId: targetId, previousTaskId: pinned.id },
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
48
78
|
return defineTool({
|
|
49
79
|
name: SEND_MESSAGE_TOOL_NAME,
|
|
50
80
|
contract: { contractId: "core.send_message@1", implementationRevision: "1" },
|
|
@@ -54,8 +84,9 @@ export function createSendMessageTool(opts) {
|
|
|
54
84
|
`'to' is the agent's name (set at spawn via Agent({name})) or its task_id (a…). A RUNNING agent receives the ` +
|
|
55
85
|
`message at its next turn (queued — never interrupts its current work); a FINISHED agent resumes as a new ` +
|
|
56
86
|
`background run with its full prior conversation preserved, so don't re-explain what it already knows. Names ` +
|
|
57
|
-
`keep working after an agent completes;
|
|
58
|
-
`
|
|
87
|
+
`keep working after an agent completes; but if a name you already messaged is later taken by a NEWER agent, the ` +
|
|
88
|
+
`send is refused rather than silently redirected — re-send with the task_id of whichever agent you meant. ` +
|
|
89
|
+
`You will be notified automatically when it completes — prefer ending your turn; do not ` +
|
|
59
90
|
`poll. ` +
|
|
60
91
|
(tier3Capable
|
|
61
92
|
? `Continuing a finished agent works for agents with a durable record — by name or task_id, even across restarts — and for runs that retain sub-agent sessions; when neither covers it you get an honest error and should launch a new agent with the needed context instead.`
|
|
@@ -523,6 +554,11 @@ export function createSendMessageTool(opts) {
|
|
|
523
554
|
}
|
|
524
555
|
}
|
|
525
556
|
if (rosterHit !== undefined) {
|
|
557
|
+
{
|
|
558
|
+
const guard = pinGuard(rosterHit.agentId, to, "name", to);
|
|
559
|
+
if (guard !== undefined)
|
|
560
|
+
return guard;
|
|
561
|
+
}
|
|
526
562
|
const revived = await tier3Revive(rosterHit.agentId);
|
|
527
563
|
if (revived !== undefined)
|
|
528
564
|
return revived;
|
|
@@ -563,6 +599,16 @@ export function createSendMessageTool(opts) {
|
|
|
563
599
|
if (row.type !== "background_agent") {
|
|
564
600
|
return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to }, isError: true };
|
|
565
601
|
}
|
|
602
|
+
{
|
|
603
|
+
const rung = idRow !== undefined
|
|
604
|
+
? "ref"
|
|
605
|
+
: row.name !== undefined && (row.name === to || normalizeAgentName(row.name) === normalizeAgentName(to))
|
|
606
|
+
? "name"
|
|
607
|
+
: "other";
|
|
608
|
+
const guard = pinGuard(targetId, row.name, rung, to);
|
|
609
|
+
if (guard !== undefined)
|
|
610
|
+
return guard;
|
|
611
|
+
}
|
|
566
612
|
if (row.status === "running" || row.status === "pending") {
|
|
567
613
|
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
568
614
|
const liveRecipientKey = peerAxisToken(resolvedAccess.scope, "h", targetId);
|
package/dist/agents/subagent.js
CHANGED
|
@@ -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
|
|
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
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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
|
|
2774
|
-
|
|
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
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
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
|
-
|
|
3451
|
-
|
|
3452
|
-
ctx.subagentRetain?.markSettled(ctx.toolCallId);
|
|
3612
|
+
else {
|
|
3613
|
+
child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
|
|
3453
3614
|
}
|
|
3454
3615
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3616
|
+
finally {
|
|
3617
|
+
syncAdmission.release();
|
|
3457
3618
|
}
|
|
3458
3619
|
await settleObserver(child.status);
|
|
3459
3620
|
const handbackWarning = await reviewHandback({
|
|
@@ -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
|
|
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
|
|
622
|
-
*
|
|
623
|
-
*
|
|
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;
|