@sema-agent/core 5.55.0 → 5.57.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 +140 -0
- package/dist/agents/send-message-tool.d.ts +11 -0
- package/dist/agents/send-message-tool.js +81 -13
- package/dist/agents/subagent.js +250 -89
- package/dist/agents/team.d.ts +10 -1
- package/dist/agents/team.js +1 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.js +4 -1
- package/dist/brain/failover.js +16 -1
- package/dist/brain/open-responses.js +15 -5
- package/dist/brain/openai.js +16 -5
- package/dist/brain/request-params.d.ts +30 -27
- package/dist/brain/request-params.js +1 -7
- package/dist/brain/route-adjudicator.d.ts +190 -0
- package/dist/brain/route-adjudicator.js +189 -0
- package/dist/brain/route-conformance.d.ts +55 -0
- package/dist/brain/route-conformance.js +136 -0
- package/dist/brain/routing.js +8 -3
- 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/mcp.js +4 -4
- package/dist/core/memory-engine/engine.d.ts +15 -5
- package/dist/core/memory-engine/engine.js +3 -1
- package/dist/core/permission-rule-consent.d.ts +45 -0
- package/dist/core/permission-rule-consent.js +40 -11
- package/dist/core/permission-rule-model.d.ts +110 -75
- package/dist/core/permission-rule-model.js +61 -28
- 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 +66 -26
- package/dist/core/runner/runtask.d.ts +4 -1
- package/dist/core/runner/runtask.js +206 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/scheduler.d.ts +5 -0
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/side-query.d.ts +12 -5
- 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 +69 -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 +114 -13
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +16 -6
- package/dist/engine/llm/types.d.ts +65 -0
- 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/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +12 -6
- package/dist/prompts/default.js +2 -0
- package/dist/scenarios/scenario-registry.d.ts +5 -1
- package/dist/scenarios/scenario-registry.js +4 -2
- package/dist/tools/fs/index.js +8 -1
- package/dist/tools/scheduler-tools.js +28 -6
- package/dist/tools/web.d.ts +15 -0
- package/dist/tools/web.js +8 -2
- package/dist/tools/worktree.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -1
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* lines from a still-running watch). Everything else is a terminal completion notification. */
|
|
3
3
|
export type TaskNotificationStatus = "completed" | "failed" | "killed" | "cancelled" | "event";
|
|
4
4
|
export type SystemInjectionPriority = "now" | "next" | "later";
|
|
5
|
+
/** The closed value set of {@link SystemInjectionPriority}, as data — the wire-facing entries validate
|
|
6
|
+
* against THIS rather than against three inline string literals, so widening the type widens the guard.
|
|
7
|
+
* (Names taken verbatim from CC 2.1.223's `UYo` @ `CC:644918`; see the semantics note on
|
|
8
|
+
* {@link SystemInjection.priority} for which of CC's behaviors this engine implements.) */
|
|
9
|
+
export declare const SYSTEM_INJECTION_PRIORITIES: readonly SystemInjectionPriority[];
|
|
10
|
+
/** backlog #389 伴生 (D-3, bad-value loudness) — is this a priority this engine knows at all? A value
|
|
11
|
+
* outside the set is a caller bug that must be refused, never silently folded into a default: the
|
|
12
|
+
* no-subscriber retain path indexes `queues[priority]` and the park path stores it verbatim, so an
|
|
13
|
+
* unknown value used to travel until something downstream failed on it (or didn't, and lied). */
|
|
14
|
+
export declare function isSystemInjectionPriority(value: unknown): value is SystemInjectionPriority;
|
|
5
15
|
export interface TaskNotificationPayload {
|
|
6
16
|
task_id: string;
|
|
7
17
|
/** design/144 §2: `"external"` is CORE-MINTED ONLY — `TaskStream.notify()` casts it onto every
|
|
@@ -137,13 +147,30 @@ export interface ExternalNotificationInput {
|
|
|
137
147
|
}
|
|
138
148
|
export interface SystemInjection<TPayload = unknown> {
|
|
139
149
|
kind: "task_notification";
|
|
140
|
-
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
150
|
+
/**
|
|
151
|
+
* design/116 §7 — in THIS engine every priority delivers at the NEXT turn boundary via
|
|
152
|
+
* `harness.steer()`, mid-work included, in ARRIVAL order (consecutive frames batch); `priority`
|
|
153
|
+
* affects only the park/uplink path. A delivery that races the agent going idle parks on
|
|
154
|
+
* PendingSessionNotifications for the session's next run; `drain()` serves that parked lane.
|
|
155
|
+
*
|
|
156
|
+
* **Anchor correction (backlog #389 伴生 / hallucination audit H-1).** The 2026-08-05 re-anchor
|
|
157
|
+
* justified flattening the ladder with "CC's queued task-notification inputs are UNCONDITIONALLY
|
|
158
|
+
* deliverable at the boundary (CC 2.1.221)". That sentence is FALSE as a statement about CC, on
|
|
159
|
+
* 221 and 223 alike: the mid-turn fold is gated at `getCommandsByMaxPriority("next")`
|
|
160
|
+
* (`pretty221.js:449195` / `pretty223.js:415586`), which admits `now`+`next` and EXCLUDES `later`
|
|
161
|
+
* — and `enqueuePendingNotification` defaults to `later`. CC's background-completion notices fold
|
|
162
|
+
* mid-turn because they explicitly say `priority:"next"`; its ultraplan/artifact notices take the
|
|
163
|
+
* default and deliberately do NOT. So all three of CC's values carry live delivery semantics
|
|
164
|
+
* (`now` = abort the running turn, `next` = fold into it, `later` = wait for the next one).
|
|
165
|
+
*
|
|
166
|
+
* The engineering conclusion the re-anchor reached — a background completion must reach a busy
|
|
167
|
+
* model at the boundary rather than starve behind a "deliver only when it would otherwise stop"
|
|
168
|
+
* rule — stands on its own. What does not stand is the claim that CC has no ladder. Restoring the
|
|
169
|
+
* `later` = "do not fold into the running turn" arm is a behavior-face change and `now` = "abort
|
|
170
|
+
* the running turn" is a new capability; both are ruled changes, not silent ones. Until then the
|
|
171
|
+
* gap is DISCLOSED at the injection funnel rather than left as a silently inert knob (`now` is
|
|
172
|
+
* announced, an unknown value is refused) — the bad-value loudness rule.
|
|
173
|
+
*/
|
|
147
174
|
priority: SystemInjectionPriority;
|
|
148
175
|
dedupKey: string;
|
|
149
176
|
payload: TPayload;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
2
|
+
export const SYSTEM_INJECTION_PRIORITIES = ["now", "next", "later"];
|
|
3
|
+
export function isSystemInjectionPriority(value) {
|
|
4
|
+
return typeof value === "string" && SYSTEM_INJECTION_PRIORITIES.includes(value);
|
|
5
|
+
}
|
|
2
6
|
const TASK_NOTIFICATION_TAGS = [
|
|
3
7
|
"task-notification",
|
|
4
8
|
"task-id",
|
|
@@ -214,6 +218,9 @@ export class SystemInjectionQueue {
|
|
|
214
218
|
listeners = new Set();
|
|
215
219
|
droppedOldest = 0;
|
|
216
220
|
enqueue(item) {
|
|
221
|
+
if (!isSystemInjectionPriority(item.priority)) {
|
|
222
|
+
throw new Error(`SystemInjectionQueue: unknown priority ${JSON.stringify(item.priority)} — expected one of ${SYSTEM_INJECTION_PRIORITIES.join("/")}`);
|
|
223
|
+
}
|
|
217
224
|
if (this.dedup.has(item.dedupKey))
|
|
218
225
|
return false;
|
|
219
226
|
this.dedup.add(item.dedupKey);
|
|
@@ -239,7 +246,10 @@ export class SystemInjectionQueue {
|
|
|
239
246
|
return true;
|
|
240
247
|
}
|
|
241
248
|
drain(priority) {
|
|
242
|
-
|
|
249
|
+
if (priority !== undefined && !isSystemInjectionPriority(priority)) {
|
|
250
|
+
throw new Error(`SystemInjectionQueue: unknown priority ${JSON.stringify(priority)} — expected one of ${SYSTEM_INJECTION_PRIORITIES.join("/")}`);
|
|
251
|
+
}
|
|
252
|
+
const priorities = priority ? [priority] : [...SYSTEM_INJECTION_PRIORITIES];
|
|
243
253
|
const out = [];
|
|
244
254
|
for (const p of priorities) {
|
|
245
255
|
const q = this.queues[p];
|
|
@@ -166,11 +166,23 @@ export declare function parkBackgroundAgentLane(core: DurableAgentCore, id: stri
|
|
|
166
166
|
* design/153 §7.1 (件1 codex HIGH split-brain fence) — the registry-aware reconciliation entry an
|
|
167
167
|
* IN-PROCESS deployment calls on its retention cadence. Two halves, one call:
|
|
168
168
|
* 1. LIVE half: this instance's own parked handles are probed against their checkpoint and, on
|
|
169
|
-
* expired/missing, settled failed THROUGH the handle (poll/stop/durable row
|
|
170
|
-
* quiescence poked, arbitration closure dropped, durable clear rides the settle
|
|
169
|
+
* expired/missing/DECIDED-ELSEWHERE, settled failed THROUGH the handle (poll/stop/durable row
|
|
170
|
+
* stay coherent — quiescence poked, arbitration closure dropped, durable clear rides the settle
|
|
171
|
+
* write).
|
|
171
172
|
* 2. STORE half: delegates to the store-level {@link reconcileParkedAgents} with
|
|
172
173
|
* `excludeWriterId` = this instance's writerId, so foreign (dead-process) rows are cleaned
|
|
173
174
|
* without ever touching a row this process is live-managing.
|
|
175
|
+
*
|
|
176
|
+
* backlog #400 — the live half used to skip EVERY non-expired/missing state, `resolved` included, and
|
|
177
|
+
* that was the whole zombie: a parked row whose checkpoint was decided by the task-level
|
|
178
|
+
* `resumeCheckpoint` leg (a serving layer's documented fallback when its parked-row discriminator
|
|
179
|
+
* misses) is decided but still says `parked`, its side effects already executed by a path that never
|
|
180
|
+
* touched this registry. Nothing else could reach it either — a re-claim answers
|
|
181
|
+
* `checkpoint_not_pending`, and the store half excludes exactly the rows this process live-manages —
|
|
182
|
+
* so it stayed `parked` for the life of the process. It now converges here. The one `resolved` case
|
|
183
|
+
* that must still be LET THROUGH is the registry's own claim window (reservation → consume flip): that
|
|
184
|
+
* skip is load-bearing, and it is now expressed as what it actually is — a claim in flight — instead
|
|
185
|
+
* of as a blanket state filter that also swallowed the case with no claimant at all.
|
|
174
186
|
*/
|
|
175
187
|
export declare function reconcileParkedAgentsLane(core: DurableAgentCore, stores: {
|
|
176
188
|
agentStore: import("./background-agent-store.js").BackgroundAgentStore;
|
|
@@ -275,7 +287,12 @@ export declare function abortBackgroundAgentsForOwnerLane(core: DurableAgentCore
|
|
|
275
287
|
* SendMessage consume, so the two verbs can never drift. Ladder:
|
|
276
288
|
* 0a. verbatim `handle.name` match — LATEST-WINS (CC SendMessage.md: "a newer agent took the name
|
|
277
289
|
* (latest wins)"; older rows stay task_id-addressable), any status (names keep working after
|
|
278
|
-
* completion — the resume face is the CC "resumes it from its transcript" counterpart)
|
|
290
|
+
* completion — the resume face is the CC "resumes it from its transcript" counterpart).
|
|
291
|
+
* **Latest-wins is RESOLUTION, not permission to redirect a conversation**: CC pairs the same
|
|
292
|
+
* resolver with a per-conversation pin guard (`send_message_pin_guard`), and SendMessage REFUSES
|
|
293
|
+
* a send whose name has changed hands since this caller last used it. That guard lives at the
|
|
294
|
+
* SendMessage face (`createSendMessageTool`), exactly as it does upstream — this resolver stays
|
|
295
|
+
* a pure resolver, and TaskStop keeps its own (preferRunning) posture untouched;
|
|
279
296
|
* 0b. normalized `handle.name` match — same latest-wins (the spawn regex bans whitespace, so
|
|
280
297
|
* normalization here is effectively NFKC+lowercase: "Builder"/"builder" share the name pool);
|
|
281
298
|
* 1+. the pre-[c209] description layers, UNCHANGED (exact → normalized, running-preferred +
|
|
@@ -484,12 +484,41 @@ export async function reconcileParkedAgentsLane(core, stores, scope, now, opts)
|
|
|
484
484
|
state = "unreadable";
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
|
-
|
|
487
|
+
let error;
|
|
488
|
+
if (state === "expired" || state === "missing") {
|
|
489
|
+
error = `approval checkpoint ${state} while parked (reconciled)`;
|
|
490
|
+
}
|
|
491
|
+
else if (state === "resolved") {
|
|
492
|
+
let row;
|
|
493
|
+
try {
|
|
494
|
+
row = await stores.agentStore.get(handle.id, scope);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (row === null || row.status !== "parked" || row.parkClaimId !== undefined)
|
|
500
|
+
continue;
|
|
501
|
+
try {
|
|
502
|
+
const reread = (await stores.checkpointStore.get(token));
|
|
503
|
+
if (reread === null || reread.status !== "resolved")
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
error =
|
|
510
|
+
"approval checkpoint resolved outside the registry's claim path while parked — the decision was " +
|
|
511
|
+
"redeemed by another leg and this row's outcome is unknown (reconciled)";
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
488
514
|
continue;
|
|
515
|
+
}
|
|
489
516
|
if (handle.status !== "parked")
|
|
490
517
|
continue;
|
|
518
|
+
if (handle.parkedCheckpointToken !== token)
|
|
519
|
+
continue;
|
|
491
520
|
handle.status = "failed";
|
|
492
|
-
handle.error =
|
|
521
|
+
handle.error = error;
|
|
493
522
|
handle.parkedCheckpointToken = undefined;
|
|
494
523
|
handle.resolveParkedStop = undefined;
|
|
495
524
|
handle.updatedAt = now;
|
|
@@ -384,12 +384,17 @@ export interface ToolPolicyProjection {
|
|
|
384
384
|
readonly requiresLiveRemainder: boolean;
|
|
385
385
|
}
|
|
386
386
|
/**
|
|
387
|
-
* The entries of a tool-NAME list whose reach is a set of names rather than one name — the
|
|
388
|
-
*
|
|
389
|
-
* an exact-membership test silently cannot. Every name-keyed lane in this file
|
|
390
|
-
* ALONGSIDE its exact set: a covering spelling can never equal a minted tool name (`*`
|
|
391
|
-
* minted charset and a minted name always carries a tool segment), so leaving it in the
|
|
392
|
-
* costs nothing and keeps the projection/audit faces reporting the operator's own spelling.
|
|
387
|
+
* The entries of a tool-NAME list whose reach is a set of names rather than one name — the covering
|
|
388
|
+
* spellings (`<ns>__<peer>`, `<ns>__<peer>__<glob>`) of ANY protocol namespace, which CC's rule
|
|
389
|
+
* matcher resolves and an exact-membership test silently cannot. Every name-keyed lane in this file
|
|
390
|
+
* consults these ALONGSIDE its exact set: a covering spelling can never equal a minted tool name (`*`
|
|
391
|
+
* is outside the minted charset and a minted name always carries a tool segment), so leaving it in the
|
|
392
|
+
* exact set too costs nothing and keeps the projection/audit faces reporting the operator's own spelling.
|
|
393
|
+
*
|
|
394
|
+
* Namespace-generic on purpose: the covering grammar is a property of the shared `<prefix><peer>__<tool>`
|
|
395
|
+
* name shape, not of MCP, and keying it to one prefix made every sibling-namespace covering rule
|
|
396
|
+
* (`a2a__<peer>__*`) compile clean and match nothing — inert in the deny half, and in an ALLOW list the
|
|
397
|
+
* removal of that peer's whole tool set.
|
|
393
398
|
*
|
|
394
399
|
* The list is usually empty, which is why every call site tests it before scanning.
|
|
395
400
|
*
|
|
@@ -398,9 +403,9 @@ export interface ToolPolicyProjection {
|
|
|
398
403
|
* list these): all four publishers of {@link ToolPolicyNameSets} must resolve a covering entry the same
|
|
399
404
|
* way, because the prepare-time audit's exemption for these spellings speaks for all of them at once.
|
|
400
405
|
*/
|
|
401
|
-
export declare function
|
|
402
|
-
/** Does any covering entry reach `toolName`? See {@link
|
|
403
|
-
export declare function
|
|
406
|
+
export declare function namespacedCoveringEntries(entries: readonly string[] | undefined): readonly string[];
|
|
407
|
+
/** Does any covering entry reach `toolName`? See {@link namespacedCoveringEntries}. */
|
|
408
|
+
export declare function namespacedCoveringHit(covering: readonly string[], toolName: string): boolean;
|
|
404
409
|
/**
|
|
405
410
|
* Execute a persisted {@link ToolPolicyProjection} against a call (F-012 L1): returns the first
|
|
406
411
|
* component's deny, or `undefined` when the projection has no opinion (it is deny-only by
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -5,7 +5,8 @@ import { join, normalize as normalizePath, posix as posixPath, sep, win32 as win
|
|
|
5
5
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
6
6
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
7
7
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
|
|
8
|
-
import {
|
|
8
|
+
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
9
|
+
import { protocolOf } from "./protocol-table.js";
|
|
9
10
|
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
10
11
|
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
11
12
|
const DECISION_REASON_SET = new Set(DECISION_REASONS);
|
|
@@ -36,22 +37,22 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
|
|
|
36
37
|
export function decisionText(d) {
|
|
37
38
|
return d.message;
|
|
38
39
|
}
|
|
39
|
-
export function
|
|
40
|
-
return (entries ?? []).filter(
|
|
40
|
+
export function namespacedCoveringEntries(entries) {
|
|
41
|
+
return (entries ?? []).filter(isNamespacedCoveringRuleName);
|
|
41
42
|
}
|
|
42
|
-
export function
|
|
43
|
-
return covering.length > 0 && covering.some((e) =>
|
|
43
|
+
export function namespacedCoveringHit(covering, toolName) {
|
|
44
|
+
return covering.length > 0 && covering.some((e) => namespacedRuleNameCovers(e, toolName));
|
|
44
45
|
}
|
|
45
46
|
export function checkToolPolicyProjection(projection, req) {
|
|
46
47
|
for (const c of projection.components) {
|
|
47
48
|
if (c.kind === "tool_deny") {
|
|
48
|
-
if (c.names.includes(req.toolName) ||
|
|
49
|
+
if (c.names.includes(req.toolName) || namespacedCoveringHit(namespacedCoveringEntries(c.names), req.toolName)) {
|
|
49
50
|
return { action: "deny", message: `tool "${req.toolName}" is denied by a frozen inherited policy projection` };
|
|
50
51
|
}
|
|
51
52
|
continue;
|
|
52
53
|
}
|
|
53
54
|
if (c.kind === "tool_allowlist") {
|
|
54
|
-
if (!c.names.includes(req.toolName) && !
|
|
55
|
+
if (!c.names.includes(req.toolName) && !namespacedCoveringHit(namespacedCoveringEntries(c.names), req.toolName)) {
|
|
55
56
|
return { action: "deny", message: `tool "${req.toolName}" is not in a frozen inherited policy projection's allowlist` };
|
|
56
57
|
}
|
|
57
58
|
continue;
|
|
@@ -179,11 +180,15 @@ export function createAllowDenyPolicy(opts) {
|
|
|
179
180
|
return undefined;
|
|
180
181
|
const kept = [];
|
|
181
182
|
for (const entry of entries) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
const ns = protocolOf(entry);
|
|
184
|
+
if (ns !== undefined) {
|
|
185
|
+
const suffix = entry.slice(ns.prefix.length);
|
|
186
|
+
const sep = suffix.indexOf("__");
|
|
187
|
+
const malformed = suffix.length === 0 || sep === 0 || (sep > 0 && suffix.length === sep + 2);
|
|
188
|
+
if (malformed) {
|
|
189
|
+
const family = ns.id.toUpperCase();
|
|
190
|
+
const lesson = `a malformed ${family} tool name (empty peer or tool segment) — it can never match any mounted tool. ` +
|
|
191
|
+
`Use \`${ns.prefix}<peer>\` for every tool of a peer, or \`${ns.prefix}<peer>__<tool>\` for one tool.`;
|
|
187
192
|
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
188
193
|
continue;
|
|
189
194
|
}
|
|
@@ -224,8 +229,8 @@ export function createAllowDenyPolicy(opts) {
|
|
|
224
229
|
opts = { ...opts, ...(screenedAllow ? { allow: screenedAllow } : {}), ...(screenedDeny ? { deny: screenedDeny } : {}) };
|
|
225
230
|
const allow = opts.allow ? new Set(opts.allow) : undefined;
|
|
226
231
|
const deny = new Set(opts.deny ?? []);
|
|
227
|
-
const denyCovering =
|
|
228
|
-
const allowCovering =
|
|
232
|
+
const denyCovering = namespacedCoveringEntries(opts.deny);
|
|
233
|
+
const allowCovering = namespacedCoveringEntries(opts.allow);
|
|
229
234
|
return {
|
|
230
235
|
projection: {
|
|
231
236
|
components: [
|
|
@@ -237,10 +242,10 @@ export function createAllowDenyPolicy(opts) {
|
|
|
237
242
|
nameSets: [{ ...(opts.allow ? { allow: [...opts.allow] } : {}), ...(opts.deny ? { deny: [...opts.deny] } : {}) }],
|
|
238
243
|
check(req) {
|
|
239
244
|
const toolName = req.toolName;
|
|
240
|
-
if (deny.has(toolName) ||
|
|
245
|
+
if (deny.has(toolName) || namespacedCoveringHit(denyCovering, toolName)) {
|
|
241
246
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
242
247
|
}
|
|
243
|
-
if (allow && !allow.has(toolName) && !
|
|
248
|
+
if (allow && !allow.has(toolName) && !namespacedCoveringHit(allowCovering, toolName)) {
|
|
244
249
|
return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
|
|
245
250
|
}
|
|
246
251
|
return ALLOW;
|
|
@@ -257,9 +262,9 @@ export function createApprovalPolicy(opts) {
|
|
|
257
262
|
const need = new Set(opts.requireApproval);
|
|
258
263
|
const deny = new Set(opts.deny ?? []);
|
|
259
264
|
const auto = new Set(opts.autoAllow ?? []);
|
|
260
|
-
const needCovering =
|
|
261
|
-
const denyCovering =
|
|
262
|
-
const autoCovering =
|
|
265
|
+
const needCovering = namespacedCoveringEntries(opts.requireApproval);
|
|
266
|
+
const denyCovering = namespacedCoveringEntries(opts.deny);
|
|
267
|
+
const autoCovering = namespacedCoveringEntries(opts.autoAllow);
|
|
263
268
|
return {
|
|
264
269
|
projection: {
|
|
265
270
|
components: [
|
|
@@ -277,10 +282,10 @@ export function createApprovalPolicy(opts) {
|
|
|
277
282
|
],
|
|
278
283
|
async check(req, signal) {
|
|
279
284
|
const toolName = req.toolName;
|
|
280
|
-
if (deny.has(toolName) ||
|
|
285
|
+
if (deny.has(toolName) || namespacedCoveringHit(denyCovering, toolName)) {
|
|
281
286
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
282
287
|
}
|
|
283
|
-
if (need.has(toolName) ||
|
|
288
|
+
if (need.has(toolName) || namespacedCoveringHit(needCovering, toolName)) {
|
|
284
289
|
if (signal?.aborted) {
|
|
285
290
|
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, settledBy: "aborted" }, "task_aborted", req);
|
|
286
291
|
}
|
|
@@ -314,7 +319,7 @@ export function createApprovalPolicy(opts) {
|
|
|
314
319
|
}
|
|
315
320
|
return withCoreMintedResolution({ action: "deny", message: `approval denied for "${req.toolName}"`, settledBy: "human" }, "human_refused", req);
|
|
316
321
|
}
|
|
317
|
-
if (opts.denyByDefault && !auto.has(toolName) && !
|
|
322
|
+
if (opts.denyByDefault && !auto.has(toolName) && !namespacedCoveringHit(autoCovering, toolName)) {
|
|
318
323
|
return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
|
|
319
324
|
}
|
|
320
325
|
return ALLOW;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -72,6 +72,24 @@ export interface Brain {
|
|
|
72
72
|
* - honor `options.maxTokens` — that is the escalating budget the recovery re-issues the call with.
|
|
73
73
|
*/
|
|
74
74
|
complete?: CompleteSimpleFn;
|
|
75
|
+
/**
|
|
76
|
+
* Optional key↔URL pairing judge (see `src/brain/route-adjudicator.ts` for the law). Answers, for a
|
|
77
|
+
* model this brain would serve, whether the credential the request would carry belongs to the URL
|
|
78
|
+
* it would target — WITHOUT sending anything. `perModelAuth` is the caller's already-resolved
|
|
79
|
+
* per-model auth (the `getApiKeyAndHeaders` result), so a resolution seat can pre-flight the exact
|
|
80
|
+
* request it is about to make. First-party brains implement it against their own config; the
|
|
81
|
+
* decorators (routing/failover/degrading/circuit-breaker) re-dispatch it the way their `stream`
|
|
82
|
+
* would. `undefined` = this brain cannot judge (a custom brain without the face) — callers must
|
|
83
|
+
* treat that as "no judgment", never as OK or as broken.
|
|
84
|
+
*
|
|
85
|
+
* The same law runs again inside the first-party brains' request build (single source, called
|
|
86
|
+
* twice): a broken pairing that skips the pre-flight still refuses loudly at the request instead
|
|
87
|
+
* of sending a credential to a host it is not paired with.
|
|
88
|
+
*/
|
|
89
|
+
adjudicateRoute?: (model: Model, perModelAuth?: {
|
|
90
|
+
apiKey?: string;
|
|
91
|
+
headers?: Record<string, string>;
|
|
92
|
+
}) => import("../internal/llm.js").RouteAdjudication | undefined;
|
|
75
93
|
}
|
|
76
94
|
/**
|
|
77
95
|
* Side-effect class of a tool. Used by wake/resume reconciliation: when a call was interrupted
|
|
@@ -657,8 +675,9 @@ export interface ToolExecuteContext {
|
|
|
657
675
|
/**
|
|
658
676
|
* Subagent transcript persistence — the RESOLVED delegation entry caps for this run
|
|
659
677
|
* ({@link RunnerDeps.delegationEntryCaps} after prepare's loud validation; both members always
|
|
660
|
-
* present). Runner-filled trusted seat, never a model argument — the Agent tool's
|
|
661
|
-
*
|
|
678
|
+
* present). Runner-filled trusted seat, never a model argument — the Agent tool's spawn gate reads
|
|
679
|
+
* it on EVERY local lane (background, background-fork, sync fork, sync spawn), each at its own
|
|
680
|
+
* launch point. Undefined outside a Runner task (the lane then applies the
|
|
662
681
|
* exported defaults itself, so a directly-driven tool is bounded too).
|
|
663
682
|
*/
|
|
664
683
|
delegationEntryCaps?: {
|
|
@@ -2543,9 +2562,12 @@ export interface TaskSpec {
|
|
|
2543
2562
|
* Override auto-compaction thresholds for this task's session. Compaction (LLM summary, persisted
|
|
2544
2563
|
* to the session) runs in two places sharing these settings: at every within-task turn boundary
|
|
2545
2564
|
* (design/64 §25 (A) — the long-single-task case) and once at task end (the between-task case).
|
|
2546
|
-
* Defaults: enabled, reserveTokens 16384, keepRecentTokens 0 (design/169-C
|
|
2547
|
-
*
|
|
2548
|
-
*
|
|
2565
|
+
* Defaults: enabled, reserveTokens 16384, keepRecentTokens 0 (design/169-C; 20000 was the
|
|
2566
|
+
* pre-169 default). A zero budget is NOT "no verbatim tail": the cut-point search has a
|
|
2567
|
+
* structural floor under the budget, so the default already leaves the last exchange verbatim
|
|
2568
|
+
* after the summary (a trailing tool-result batch keeps its emitting assistant with it) — the
|
|
2569
|
+
* same one-group posture CC's reactive compaction ships. Set `keepRecentTokens` explicitly only
|
|
2570
|
+
* to keep a WIDER tail than that. Repaired
|
|
2549
2571
|
* automatically when
|
|
2550
2572
|
* pathological for the model's window: threshold ≤ 0, keepRecent ≥ threshold, or a threshold above
|
|
2551
2573
|
* the clearStale point (0.7×window — the prefix-cache "death band", design/64 §26.4) is clamped
|
|
@@ -4825,6 +4847,20 @@ export interface EngineNotice {
|
|
|
4825
4847
|
* catalog itself. In-flight tasks finish on the models they resolved at prepare (natural
|
|
4826
4848
|
* snapshot); every later prepare resolves against the new generation. A failed swap (illegal
|
|
4827
4849
|
* tier binding) throws atomically and mints nothing.
|
|
4850
|
+
* - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
|
|
4851
|
+
* DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
|
|
4852
|
+
* the pairing pre-flight and the seat fell back to the primary model instead of sinking the
|
|
4853
|
+
* task; the notice is the loud half of that swap. `detail: { seat, from, to, cause, fixHint }`
|
|
4854
|
+
* — `cause` is the refusal code (`route.credential_mismatch` / `route.credential_missing`).
|
|
4855
|
+
* Explicitly-named models never mint this: they refuse at the brain's request gate instead.
|
|
4856
|
+
* - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapModels` moved a
|
|
4857
|
+
* same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
|
|
4858
|
+
* did not change: legal (a provider changing domains), but worth one loud line — if the
|
|
4859
|
+
* PROVIDER changed, the credential reference needs the same update. Advisory only, never a
|
|
4860
|
+
* refusal; per-model-hook credentials are invisible to the catalog swap, so the notice is
|
|
4861
|
+
* deliberately conservative (it may fire when a hook-side credential DID change in lockstep).
|
|
4862
|
+
* One aggregated notice per swap; `detail: { entries: [{ modelId, from, to }], total }`
|
|
4863
|
+
* (rendered list bounded, total always exact).
|
|
4828
4864
|
* - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
|
|
4829
4865
|
* in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
|
|
4830
4866
|
* (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
|
|
@@ -4862,6 +4898,24 @@ export interface EngineNotice {
|
|
|
4862
4898
|
* #257 contract's "accepted = enqueued, not consumed" sentence;
|
|
4863
4899
|
* `detail: { steer, taskId? }` / `{ followUp, taskId? }`. Per-run, at most once per family
|
|
4864
4900
|
* (the terminal sweep is a single site).
|
|
4901
|
+
* **#389 (two corrections).** ① The family now fires on the INTERRUPT path too: `abort()` used
|
|
4902
|
+
* to empty both queues before agent_end could count them, so the one loss path an operator most
|
|
4903
|
+
* needs to hear about was the one path that stayed silent. ② On a DURABLE PARK the verdict is
|
|
4904
|
+
* settled at the run tail instead of at agent_end, because the tail first carries the still-
|
|
4905
|
+
* undelivered input onto the committed checkpoint's parked-steer queue (the resume delivers it)
|
|
4906
|
+
* — what is announced is the remainder that could NOT be carried, so a fully-migrated park says
|
|
4907
|
+
* nothing at all and a queue-full / no-longer-pending row still says exactly what was lost.
|
|
4908
|
+
*
|
|
4909
|
+
* - `"task.injection_priority_unimplemented"` (#389 伴生, D-3) — a notification was injected with
|
|
4910
|
+
* `priority: "now"`. The three values are CC's names (`now`/`next`/`later`) but this engine
|
|
4911
|
+
* delivers all three identically at the next turn boundary: there is no arm that aborts the
|
|
4912
|
+
* running turn, so a caller writing `"now"` and expecting an interruption gets a plain queued
|
|
4913
|
+
* delivery. The frame IS delivered (this is disclosure, not a refusal — dropping an accepted
|
|
4914
|
+
* notification would be the worse error); what is announced is that the knob's promise is not
|
|
4915
|
+
* honored, so an operator can stop building on it. Once per run (the injection funnel is a
|
|
4916
|
+
* single site, and a busy lane must not narrate the same gap once per frame);
|
|
4917
|
+
* `detail: { priority, taskId? }`. An UNKNOWN priority value is a different fact with a
|
|
4918
|
+
* different posture — `TaskStream.notify` refuses it typed (`notify.invalid_payload`).
|
|
4865
4919
|
*
|
|
4866
4920
|
* - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336) —
|
|
4867
4921
|
* this session's memory crossed into the one-way externally-exposed state (a tool classified
|
|
@@ -5498,12 +5552,16 @@ export interface RunnerDeps {
|
|
|
5498
5552
|
* Subagent transcript persistence — the delegation ENTRY caps (CC parity values: 20 concurrent /
|
|
5499
5553
|
* 200 cumulative per session tree; defaults exported as `DELEGATION_MAX_CONCURRENT_DEFAULT` /
|
|
5500
5554
|
* `DELEGATION_MAX_PER_SESSION_DEFAULT`). Key = `(scope, rootSessionId)`, full depth (grandchildren
|
|
5501
|
-
* share the tree's pool). `maxConcurrent` bounds
|
|
5502
|
-
*
|
|
5503
|
-
*
|
|
5504
|
-
*
|
|
5505
|
-
*
|
|
5506
|
-
*
|
|
5555
|
+
* share the tree's pool). `maxConcurrent` bounds the tree's RUNNING children of every local lane:
|
|
5556
|
+
* running/pending a* handles in this process's registry PLUS the synchronous and forked children in
|
|
5557
|
+
* flight in this process (parked does not burn a slot — a suspension is not concurrency; a revival
|
|
5558
|
+
* claim counts like a spawn); `maxCumulativePerSession` bounds the RETAINED-WINDOW cumulative count
|
|
5559
|
+
* (registry-retained + store-retained rows, plus those same in-flight local children — a reaped row
|
|
5560
|
+
* returns its quota, and a synchronous child retains nothing so it leaves the window when it
|
|
5561
|
+
* settles; deliberately NOT CC's lifetime-monotonic session counter, which would require a
|
|
5562
|
+
* persistent counting surface this economic bound does not justify — registered divergence). One
|
|
5563
|
+
* consequence, stated rather than left to be discovered: a tree that only ever spawns synchronously
|
|
5564
|
+
* is bounded by the concurrency cap alone. Refusals are coded (`delegation.concurrency_cap` /
|
|
5507
5565
|
* `delegation.session_cap`) with the current value and this knob's name in the text.
|
|
5508
5566
|
*
|
|
5509
5567
|
* BAD VALUES REFUSE LOUDLY at prepare (`config.delegation_entry_caps`, the #123 posture): a
|
|
@@ -240,6 +240,14 @@ export const ENGINE_ENVELOPES = Object.freeze([
|
|
|
240
240
|
fenced: false,
|
|
241
241
|
disclosed: false,
|
|
242
242
|
},
|
|
243
|
+
{
|
|
244
|
+
tag: "example",
|
|
245
|
+
kind: "not-an-envelope",
|
|
246
|
+
mint: "engine/compaction SUMMARIZATION_PROMPT (CC 2.1.223 `qby` verbatim — the output skeleton shown to the summarizer)",
|
|
247
|
+
guard: "no untrusted bytes reach it: the block is a CONSTANT that frames placeholder text (`[Concept 1]`), it wraps nothing, and it is never emitted back — the summarizer's own answer is read through the `<summary>` envelope, not this one",
|
|
248
|
+
fenced: false,
|
|
249
|
+
disclosed: false,
|
|
250
|
+
},
|
|
243
251
|
{
|
|
244
252
|
tag: "task-id",
|
|
245
253
|
kind: "not-an-envelope",
|
|
@@ -35,6 +35,22 @@ export interface CompactionDetails {
|
|
|
35
35
|
* on pre-field entries (⇒ no disclosure, byte-identical legacy wrapper).
|
|
36
36
|
*/
|
|
37
37
|
elidedMessages?: number;
|
|
38
|
+
/**
|
|
39
|
+
* PTL-shortfall accounting: CUMULATIVE count of folded messages the summary does NOT cover — the ones a reactive
|
|
40
|
+
* prompt-too-long retry shed from the summarization input (`summarizeWithPtlRetry`'s group
|
|
41
|
+
* rollback) after `prepareCompaction` had already committed the cut. Those messages leave the
|
|
42
|
+
* transcript with everyone else (so {@link elidedMessages} stays the honest FOLD count) but
|
|
43
|
+
* nothing in the summary stands in for them, and the wrapper used to claim otherwise.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately a SECOND number rather than a correction of the first: lowering `elidedMessages`
|
|
46
|
+
* to the covered count would under-report the fold scale, which is the exact judgment that field
|
|
47
|
+
* exists to inform ("was a whole investigation folded, or three turns?"). Same cumulative +
|
|
48
|
+
* write-side + optional-absent discipline as its sibling; a lossless pass omits it entirely, so
|
|
49
|
+
* details written by the common path stay byte-identical. Counted in the summarization-input
|
|
50
|
+
* coordinate — a LOWER BOUND on uncovered transcript messages (see
|
|
51
|
+
* {@link SummarizationInputTruncation.droppedMessages}).
|
|
52
|
+
*/
|
|
53
|
+
unsummarizedMessages?: number;
|
|
38
54
|
/**
|
|
39
55
|
* RB-398-c (blackboard [2105]): `<persisted-output ref="…">` handles found in the COMPACTED range
|
|
40
56
|
* (merged over the previous compaction's retained set, oldest-first, capped at
|
|
@@ -105,12 +121,37 @@ export interface CompactionSettings {
|
|
|
105
121
|
* AND DISCLOSED (`compaction.clamp_disclosure`), not "lossless" — the honest wording is in the
|
|
106
122
|
* design doc. Above this, a cheap-model summary is judged too lossy and the main model takes over. */
|
|
107
123
|
export declare const DEFAULT_CLAMP_TOLERANCE = 0.1;
|
|
108
|
-
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
124
|
+
/**
|
|
125
|
+
* Default compaction settings used by the harness.
|
|
126
|
+
*
|
|
127
|
+
* `keepRecentTokens: 0` is a ZERO BUDGET, not "keep nothing" — {@link findCutPoint} has a
|
|
128
|
+
* structural floor underneath the budget (see its doc), so the cut still lands on the last valid
|
|
129
|
+
* cut point and the summary is followed by the last exchange VERBATIM. Measured on the real
|
|
130
|
+
* Runner: the first request after an auto-compaction is `[summary wrapper][last assistant turn]
|
|
131
|
+
* [new user]`, and a within-turn boundary keeps `[emitting assistant][its tool results]` because
|
|
132
|
+
* a trailing tool-result batch pulls the floor back to its call site.
|
|
133
|
+
*
|
|
134
|
+
* That is the CC posture, arrived at through a different knob. CC 2.1.223's LOCAL threshold
|
|
135
|
+
* compaction always routes to the reactive pipeline (`y9s` :432704 — the `GNo` full-compaction
|
|
136
|
+
* branch with `messagesToKeep: []` :431788 is reached only for a remote/in-process runner, and a
|
|
137
|
+
* `source:"auto"` window makes the local threshold not fire at all, `hky` :432687 / `aYe` :242816).
|
|
138
|
+
* Reactive splits the session into GROUPS (`Ubr` :399459 — a new assistant `message.id` starts a
|
|
139
|
+
* group and its tool results ride with it) and preserves the last `s = 1` of them verbatim
|
|
140
|
+
* (`YMo` :399971 / :399982), zeroing the preserved assistants' usage on the way out (`ryt` :401285
|
|
141
|
+
* — our `buildSessionContext` kept-tail `stripAssistantUsage`, design/123 D1, is the same act).
|
|
142
|
+
* `s` only grows on ITS retry path, so one group is the default posture on both sides.
|
|
143
|
+
*
|
|
144
|
+
* design/169-C moved this default from 20000 to 0 and anchored the change on `messagesToKeep: []`;
|
|
145
|
+
* the anchor names the minority path, but the VALUE lands on the majority path's behavior, and
|
|
146
|
+
* raising it again would keep MORE than CC does (measured: a 2000-token budget keeps two whole
|
|
147
|
+
* exchanges). A positive `keepRecentTokens` remains the explicit knob for callers that want a
|
|
148
|
+
* wider verbatim tail than either engine's default.
|
|
149
|
+
*
|
|
150
|
+
* Known residual (recorded, not repaired): when the last entry is a USER message the floor takes
|
|
151
|
+
* it alone, where CC's group would also carry the assistant before it. The runner's lanes evaluate
|
|
152
|
+
* compaction at turn boundaries and at finish — after the assistant/tool results, before the next
|
|
153
|
+
* prompt — so that shape belongs to direct `maybeCompact` callers.
|
|
154
|
+
*/
|
|
114
155
|
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
115
156
|
/**
|
|
116
157
|
* design/123 D2 — default structural coefficient (chars per token) for every structural token
|
|
@@ -166,7 +207,19 @@ export interface CutPointResult {
|
|
|
166
207
|
}
|
|
167
208
|
/** Find the compaction cut point that keeps approximately the requested recent-token budget.
|
|
168
209
|
* `charsPerToken` (design/123 D2): the keep-recent accumulation must live in the SAME structural
|
|
169
|
-
* coordinate as the trigger estimate, or the kept tail is systematically over/under-sized.
|
|
210
|
+
* coordinate as the trigger estimate, or the kept tail is systematically over/under-sized.
|
|
211
|
+
*
|
|
212
|
+
* STRUCTURAL FLOOR — the budget can raise the kept tail, never empty it. The backward scan breaks
|
|
213
|
+
* at the FIRST message it visits when the budget is already satisfied (a budget of 0 is satisfied
|
|
214
|
+
* immediately), and then lands on a valid cut point, so the kept tail is never smaller than
|
|
215
|
+
* "the last cut point onward". Two shapes follow, and both are load-bearing:
|
|
216
|
+
* - the tail ends on an assistant/user message ⇒ that message is itself a cut point and is kept;
|
|
217
|
+
* - the tail ends inside a tool-result batch ⇒ tool results are not cut points, so the `!found`
|
|
218
|
+
* fallback below pulls the floor back to the LARGEST cut point ≤ i, i.e. the emitting
|
|
219
|
+
* assistant, and the whole batch is kept with it.
|
|
220
|
+
* Reading `keepRecentTokens: 0` as "no verbatim tail" is therefore wrong at both the name and the
|
|
221
|
+
* behavior (see {@link DEFAULT_COMPACTION_SETTINGS} for the measured shape and the CC coordinates
|
|
222
|
+
* it matches). The pins live in test/compaction-keep-tail-floor.test.ts. */
|
|
170
223
|
export declare function findCutPoint(entries: SessionTreeEntry[], startIndex: number, endIndex: number, keepRecentTokens: number, charsPerToken?: number): CutPointResult;
|
|
171
224
|
export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
|
|
172
225
|
/**
|
|
@@ -250,6 +303,18 @@ export interface SummarizationInputTruncation {
|
|
|
250
303
|
label: "history" | "turn_prefix";
|
|
251
304
|
droppedChars: number;
|
|
252
305
|
keptChars: number;
|
|
306
|
+
/**
|
|
307
|
+
* How many WHOLE messages this drop removed from the summarization input, when the drop was the
|
|
308
|
+
* PTL retry's group rollback (`summarizeWithPtlRetry`) rather than the proactive head+tail clamp.
|
|
309
|
+
* The clamp cuts a mid-section of TEXT and leaves every message present, so it reports chars only
|
|
310
|
+
* and omits this field; the rollback shifts entire assistant-turn groups off the front, and those
|
|
311
|
+
* messages are folded out of the transcript by the same pass WITHOUT ever reaching the summarizer
|
|
312
|
+
* — the fact the wrapper's scale disclosure has to state (see
|
|
313
|
+
* {@link CompactionDetails.unsummarizedMessages}). Counted in the summarization-INPUT coordinate
|
|
314
|
+
* (messages actually handed to the summarizer, i.e. post-`convertToLlm`), so it is a LOWER BOUND
|
|
315
|
+
* on the transcript messages the summary does not cover.
|
|
316
|
+
*/
|
|
317
|
+
droppedMessages?: number;
|
|
253
318
|
}
|
|
254
319
|
/** design/145 §1 — result of a window-safety clamp DRY RUN (see {@link dryRunSummarizationClamp}). */
|
|
255
320
|
export interface SummarizationClampDryRun {
|
|
@@ -313,6 +378,11 @@ export interface CompactionPreparation {
|
|
|
313
378
|
/** RB-398-b①: cumulative folded-message count (this range + the previous carrier's count) —
|
|
314
379
|
* ready to persist as `CompactionDetails.elidedMessages`. Optional (same rule as above). */
|
|
315
380
|
elidedMessages?: number;
|
|
381
|
+
/** PTL-shortfall accounting: the previous carrier's uncovered-message count, CARRIED FORWARD only — this pass's own
|
|
382
|
+
* uncovered count is not knowable at prepare time (it is produced by the summary leg's PTL
|
|
383
|
+
* rollback), so `compact()` adds its own drops on top before persisting
|
|
384
|
+
* `CompactionDetails.unsummarizedMessages`. Absent = nothing carried. */
|
|
385
|
+
carriedUnsummarizedMessages?: number;
|
|
316
386
|
/** Settings used to prepare compaction. */
|
|
317
387
|
settings: CompactionSettings;
|
|
318
388
|
}
|