@sema-agent/core 5.59.0 → 5.60.1

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 (45) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/brain/anthropic.js +15 -5
  3. package/dist/brain/errors.d.ts +18 -1
  4. package/dist/brain/errors.js +7 -1
  5. package/dist/brain/input-too-long.d.ts +57 -0
  6. package/dist/brain/input-too-long.js +35 -0
  7. package/dist/brain/stream-engine.js +9 -1
  8. package/dist/core/auto-compaction.js +2 -2
  9. package/dist/core/checkpoint-store.d.ts +172 -25
  10. package/dist/core/checkpoint-store.js +15 -8
  11. package/dist/core/context-edit.d.ts +243 -41
  12. package/dist/core/context-edit.js +247 -32
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +5 -0
  15. package/dist/core/locked-config.d.ts +36 -4
  16. package/dist/core/locked-config.js +34 -1
  17. package/dist/core/mcp.d.ts +4 -5
  18. package/dist/core/mcp.js +10 -6
  19. package/dist/core/memory-engine/content-origin.d.ts +24 -2
  20. package/dist/core/memory-engine/content-origin.js +6 -1
  21. package/dist/core/memory-engine/engine.d.ts +5 -6
  22. package/dist/core/memory.d.ts +10 -0
  23. package/dist/core/park-selfcheck.js +1 -0
  24. package/dist/core/permission-rule-consent.js +9 -5
  25. package/dist/core/runner/prepare-config-doors.d.ts +22 -1
  26. package/dist/core/runner/prepare-config-doors.js +36 -0
  27. package/dist/core/runner/prepare-task.d.ts +28 -1
  28. package/dist/core/runner/prepare-task.js +107 -8
  29. package/dist/core/runner/runtask.js +97 -9
  30. package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
  31. package/dist/core/tool-policy.d.ts +32 -10
  32. package/dist/core/tool-policy.js +3 -3
  33. package/dist/core/tools.js +1 -1
  34. package/dist/core/trace.d.ts +36 -0
  35. package/dist/core/types.d.ts +127 -2
  36. package/dist/core/untrusted-text.d.ts +11 -0
  37. package/dist/core/untrusted-text.js +1 -0
  38. package/dist/engine/llm/types.d.ts +21 -2
  39. package/dist/engine/loop/agent-loop.js +7 -1
  40. package/dist/engine/loop/types.d.ts +4 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/tools/fs/fs-bash.js +1 -2
  44. package/package.json +1 -1
  45. package/test/export-surface.snapshot.json +5 -1
@@ -1,6 +1,8 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { uuidv7 } from "../internal/harness.js";
3
- import { inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
3
+ import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
4
+ import { carriesBidiControls } from "./tool-policy.js";
5
+ import { renderUntrustedCommandText } from "./permission-rule-model.js";
4
6
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
5
7
  export function mintCheckpointToken() {
6
8
  return randomBytes(16).toString("hex");
@@ -9,7 +11,6 @@ export function mintCheckpointId() {
9
11
  return `cp_${randomUUID()}`;
10
12
  }
11
13
  export const PROBE_REASON_MAX = 200;
12
- export const PROBE_CAUSE_PATH_MAX = 200;
13
14
  const PROBE_CAUSE_MAX_SHOWN = 8;
14
15
  export function normalizeProbeCause(value) {
15
16
  if (!isPlainRecord(value))
@@ -62,6 +63,8 @@ const MAX_TOUCHED_PATHS = 8;
62
63
  const MAX_DIGEST_KEYS = 16;
63
64
  const MAX_DIGEST_SCAN_KEYS = 256;
64
65
  export const MAX_TOOL_INPUT_PREVIEW_CHARS = 512;
66
+ const BIDI_BACKFILL_MAX_CHARS = 32_000;
67
+ const BIDI_BACKFILL_MAX_NODES = 512;
65
68
  export const MAX_PENDING_STEER_CHARS = 16_000;
66
69
  export const PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48_000;
67
70
  export const MAX_PENDING_STEER_ENTRIES = Math.floor(PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES / MAX_PENDING_STEER_CHARS);
@@ -121,7 +124,7 @@ export function buildRiskDescriptor(input) {
121
124
  if (shell || toolName === "Bash") {
122
125
  const cmd = isPlainRecord(args) ? safeDataValue(args, "command") : undefined;
123
126
  if (typeof cmd === "string" && cmd.length > 0)
124
- summary = inlineUntrusted(cmd, SUMMARY_CMD_MAX);
127
+ summary = renderUntrustedCommandText(cmd, SUMMARY_CMD_MAX);
125
128
  const bg = isPlainRecord(args) ? safeDataValue(args, "run_in_background") : undefined;
126
129
  if (bg === true)
127
130
  summary = `[background persistent process — no per-step recheck] ${summary ?? ""}`.trimEnd();
@@ -135,20 +138,20 @@ export function buildRiskDescriptor(input) {
135
138
  const v = safeDataValue(args, key);
136
139
  if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
137
140
  const encDigest = (s) => s.replace(/%/g, "%25").replace(/=/g, "%3D").replace(/ /g, "%20");
138
- const k = encDigest(inlineUntrusted(key, 40));
139
- const val = encDigest(inlineUntrusted(String(v), SUMMARY_VALUE_MAX));
141
+ const k = encDigest(renderUntrustedCommandText(key, 40));
142
+ const val = encDigest(renderUntrustedCommandText(String(v), SUMMARY_VALUE_MAX));
140
143
  parts.push(`${k}=${val}`);
141
144
  }
142
145
  }
143
146
  if (parts.length > 0)
144
- summary = inlineUntrusted(parts.join(" "), SUMMARY_TOTAL_MAX);
147
+ summary = renderUntrustedCommandText(parts.join(" "), SUMMARY_TOTAL_MAX);
145
148
  }
146
149
  }
147
150
  const touched = [];
148
151
  if (!shell && toolName !== "Bash") {
149
152
  const p = fsPathArg(args);
150
153
  if (p !== undefined)
151
- touched.push(inlineUntrusted(p, PATH_MAX));
154
+ touched.push(renderUntrustedCommandText(p, PATH_MAX));
152
155
  }
153
156
  const touchedPaths = touched.length > 0 ? touched.slice(0, MAX_TOUCHED_PATHS) : undefined;
154
157
  return {
@@ -156,7 +159,7 @@ export function buildRiskDescriptor(input) {
156
159
  axes,
157
160
  toolName,
158
161
  ...(input.shellGated && input.shellGateDoctrine !== undefined ? { shellGateDoctrine: input.shellGateDoctrine } : {}),
159
- ...(input.shadowedRule !== undefined ? { shadowedRule: inlineUntrusted(input.shadowedRule, 200) } : {}),
162
+ ...(input.shadowedRule !== undefined ? { shadowedRule: renderUntrustedCommandText(input.shadowedRule, 200) } : {}),
160
163
  ...(input.probeReason !== undefined ? { probeReason: inlineUntrusted(input.probeReason, PROBE_REASON_MAX) } : {}),
161
164
  ...(() => {
162
165
  if (input.probeCause === undefined)
@@ -235,6 +238,9 @@ export function summarizeCheckpoint(cp) {
235
238
  toolInput = undefined;
236
239
  }
237
240
  }
241
+ const hasBidi = tool !== undefined &&
242
+ (tool.hasBidiControls === true ||
243
+ carriesBidiControls([toolInput, tool.preview], { maxNodes: BIDI_BACKFILL_MAX_NODES, maxChars: BIDI_BACKFILL_MAX_CHARS }));
238
244
  return {
239
245
  token: cp.token,
240
246
  ...(cp.checkpointId !== undefined ? { checkpointId: cp.checkpointId } : {}),
@@ -254,6 +260,7 @@ export function summarizeCheckpoint(cp) {
254
260
  ? { restoreMode: cp.state.workspaceHandle.restoreMode === "park_only" ? "park_only" : "snapshot" }
255
261
  : {}),
256
262
  ...(tool?.preview !== undefined ? { preview: tool.preview } : {}),
263
+ ...(hasBidi ? { hasBidiControls: true } : {}),
257
264
  };
258
265
  }
259
266
  export class CheckpointError extends Error {
@@ -48,6 +48,53 @@ export declare const COMPACTION_TRIGGER_BUFFER_TOKENS = 13000;
48
48
  * and the fixed deduction strictly wins from W = 110001 up.
49
49
  */
50
50
  export declare function contextEditFrontier(window: number): number;
51
+ /**
52
+ * design/374 — which CLEARING MACHINE `clearStaleToolResults` runs.
53
+ *
54
+ * - `"legacy"` (default): the historical sema machine — keep 3, no minimum-savings gate,
55
+ * incremental oldest-first clearing that STOPS once the budget is met, legacy marker bytes.
56
+ * - `"cc"`: the CC 2.1.223 `yId` machine — keep 5, a ≥20000-token minimum-savings gate (below it
57
+ * the pass is byte-identical no-op), ONE deep clear of everything beyond the keep window, the
58
+ * window base counts already-cleared/offloaded occurrences (they hold their keep slots), and
59
+ * CC's plain marker bytes.
60
+ *
61
+ * ⚠️ P-FORM WARNING (design/374 r2-B⑨, constraint C-5): until the slice-3 fallback re-ordering
62
+ * ships (blocking-point machine re-run → in-turn forced compaction → trim demoted to last resort),
63
+ * selecting `"cc"` here puts the 20k gate IN FRONT of the only reduction between the frontier and
64
+ * the guard trim while the OLD guard-trim backstop still drops messages: a pass that now correctly
65
+ * refuses an under-20k clear hands the request straight to the message-dropping trim — WORSE than
66
+ * the legacy default in that band. Opt in before slice 3 only if you accept that trade (e.g. to
67
+ * exercise the machine); the default flip to `"cc"` ships together with the re-ordered backstops.
68
+ */
69
+ export type ContextEditMachine = "legacy" | "cc";
70
+ /**
71
+ * How many most-recent CANDIDATE tool results keep their content when the stale-result pass runs —
72
+ * per machine (design/374: the two values govern DIFFERENT machines, so they are separate
73
+ * constants, not one adjudicated number):
74
+ *
75
+ * - LEGACY machine: 3. The measured objection to 5 stands FOR THIS MACHINE (probed on a 200k model,
76
+ * 140k usage anchor, five 36k-char Bash results: at 3 the pass clears two and lands under the
77
+ * 177k guard; at 5 nothing is clearable and the guard trim cannot recover the batch either) —
78
+ * on the legacy machine the keep window is the only lever between the frontier and the
79
+ * message-dropping trim, so widening it disables the whole defense in exactly the terminal-batch
80
+ * shape that needs it.
81
+ * - CC machine: 5 (2.1.223 `uAp`; same value on CC 88's `timeBasedMCConfig` — stable across both
82
+ * corpus generations). The #384 adjudication adopted 5 CONDITIONAL on the whole machine coming
83
+ * with it (20k gate + deep clear + re-ordered backstops); the cc machine is that condition's
84
+ * carrier, and the 200k probe above stops being an objection there (§3.5: the gate correctly
85
+ * refuses the no-win clear and the recovery chain owns the request instead).
86
+ */
87
+ export declare const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
88
+ export declare const CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS = 5;
89
+ /** design/374 §3.1 — CC's minimum-savings gate (`vUs` :397773): the cc machine refuses to clear at
90
+ * all unless the structural estimate of what WOULD be cleared reaches this many tokens. Rationale
91
+ * (independent of "CC does it"): every clear breaks the provider prefix cache once, so a clear that
92
+ * saves less than the re-prefill it causes is a net loss; the gate prices that trade. The LEGACY
93
+ * machine has no such gate (1 token over budget clears) — that difference is machine-defining. */
94
+ export declare const MIN_CLEAR_SAVINGS_TOKENS = 20000;
95
+ /** Minimum kept results — CC `EUs` :397710 `Math.max(1, keepRecent)`. See
96
+ * {@link ContextEditOptions.keepRecentToolResults} for why both degenerate ends are unusable. */
97
+ export declare const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
51
98
  /**
52
99
  * checklist #54 (CC microCompact.ts:41-50 `COMPACTABLE_TOOLS` parity): the INCLUSION whitelist of
53
100
  * tools whose results may be content-cleared by the stale-result pass. CC's set is exactly
@@ -58,43 +105,43 @@ export declare function contextEditFrontier(window: number): number;
58
105
  * PowerShell is kept for CC fidelity even though sema does not mount it. Overridable per call via
59
106
  * {@link ContextEditOptions.compactableTools} for custom-tool-heavy embedders.
60
107
  */
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;
90
108
  export declare const COMPACTABLE_TOOLS: ReadonlySet<string>;
91
109
  export interface ContextEditOptions {
92
110
  /** Start clearing once estimated context tokens exceed this. */
93
111
  budgetTokens: number;
112
+ /**
113
+ * design/374 — which clearing machine runs; see {@link ContextEditMachine} (including its P-form
114
+ * warning about enabling `"cc"` before the slice-3 backstop re-ordering). Default `"legacy"`:
115
+ * the pre-374 machine, byte-identical.
116
+ */
117
+ machine?: ContextEditMachine;
118
+ /**
119
+ * Mixed-configuration protection (design/374 r2-Md⑪ family): make the LEGACY machine recognize
120
+ * the CC marker forms as already-cleared, so a ledger-replayed CC marker is never re-cleared
121
+ * into a legacy marker (destroying its ref/media disclosure). Wired by the pipeline whenever a
122
+ * cc-minting knob is on (machine "cc" — where it is implied — or the rejection-recovery arm over
123
+ * a legacy frontier machine). Default false: in a pure default deployment tool output that
124
+ * merely SPELLS the CC marker is untrusted bytes and stays clearable exactly as before.
125
+ */
126
+ recognizeCcMarkers?: boolean;
127
+ /**
128
+ * Fired AT MOST ONCE per pass, only when the pass cleared at least one result: the cleared
129
+ * indexes (into the returned array) and the pass's savings estimate (cc machine: the structural
130
+ * minimum-savings estimate it gated on; legacy machine: the accounted structural delta). The
131
+ * request-projection ledger (design/374 §3.2.6) records cleared occurrences through this seat —
132
+ * observation only, a throwing callback is swallowed and never fails the request build.
133
+ */
134
+ onCleared?: (pass: {
135
+ clears: ReadonlyArray<{
136
+ index: number;
137
+ }>;
138
+ tokensSavedEstimate: number;
139
+ }) => void;
94
140
  /**
95
141
  * 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.
142
+ * from {@link compactableTools}). Default: the selected machine's own default
143
+ * {@link DEFAULT_KEEP_RECENT_TOOL_RESULTS} = 3 (legacy) / {@link CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS}
144
+ * = 5 (cc); see those constants for why the two machines carry different numbers.
98
145
  *
99
146
  * Floored at 1, which IS CC's rule (`EUs` `Math.max(1, keepRecent)`), whose own source note gives
100
147
  * the reason: 0 leaves the model with zero working tool context, and in CC's `slice(-0)` spelling
@@ -139,16 +186,20 @@ export interface ContextEditOptions {
139
186
  * registry state are compaction's job, not a blanking marker's.
140
187
  */
141
188
  compactableTools?: ReadonlySet<string>;
189
+ /**
190
+ * design/374 (RB-212 durable half): the PRE-CAP counterpart array of `messages`, position-aligned
191
+ * (the pipeline's aggregate caps replace in place, never drop). When present, the CC machine
192
+ * composes each clear's marker + offload text from `clearSource[idx]` — the same occurrence
193
+ * bytes the cleared-projection ledger repaints the marker onto at the next session rebuild — so
194
+ * the durable marker discloses the occurrence's REAL media and the page-back ref resolves to its
195
+ * real text rather than to a cap-stripped copy. Ignored by the legacy machine (its clears are
196
+ * request-local and correctly describe the projection they were handed — default bytes do not
197
+ * move). An entry that is not a tool result, or that carries a different `toolCallId` than the
198
+ * target, is ignored per index (composition falls back to the projection copy — a misaligned
199
+ * source must never mis-attribute content).
200
+ */
201
+ clearSource?: ReadonlyArray<AgentMessage>;
142
202
  }
143
- /**
144
- * Context-editing (the lightest, safest compaction; Anthropic reports ~29% improvement alone):
145
- * when context exceeds the budget, replace the CONTENT of older tool-result messages with a short
146
- * marker, keeping the turn structure (tool_call/tool_result pairing) intact. The most recent
147
- * `keepRecentToolResults` results are preserved.
148
- *
149
- * Request-only and non-destructive: returns a new array; the durable session keeps the full results.
150
- * Older results are cleared first, stopping as soon as the budget is met.
151
- */
152
203
  /**
153
204
  * audit A-1 (view-level heal for HISTORICAL pollution): drop synthetic EMPTY failure assistants —
154
205
  * stopReason aborted/error with no tool calls and no non-whitespace text/thinking. Sessions persisted
@@ -159,10 +210,161 @@ export interface ContextEditOptions {
159
210
  * New sessions never contain these (the loop exits cleanly on abort; the harness skips persisting them).
160
211
  */
161
212
  export declare function dropEmptyFailureAssistants(messages: AgentMessage[]): AgentMessage[];
213
+ /**
214
+ * Context-editing — the lightest, safest reduction: when context exceeds the budget, replace the
215
+ * CONTENT of older tool-result messages with a short marker, keeping the turn structure
216
+ * (tool_call/tool_result pairing) intact; the most recent `keepRecentToolResults` CANDIDATE
217
+ * results keep their content. Request-only and non-destructive: returns a new array, the durable
218
+ * session keeps the full results (the request-projection ledger, not this function, is what makes
219
+ * a clear DECISION durable across requests — see `replayClearedProjection`).
220
+ *
221
+ * ANCHOR (design/374 片4 re-anchoring): this proactive frontier hook is the client-side form of
222
+ * Anthropic's PLATFORM context-management policy (`clear_tool_uses_20250919` — same
223
+ * trigger/keep/clear-at-least construction), NOT a CC-CLI parity claim: CC 2.1.x's client
224
+ * keep-recent clear lives exclusively on its `context_hint` REJECTION leg (default-gated off), a
225
+ * machine whose sema form is the MC-R arm (`planRejectionClears` + the runner's recovery chain).
226
+ * The per-mechanism CC anchors that DO hold (the COMPACTABLE_TOOLS set, the candidate-scoped keep
227
+ * window, the floor, and the whole `machine: "cc"` clearing form) are cited at their sites.
228
+ * (An earlier note here cited an unverified "~29% improvement" figure — removed: no first-party
229
+ * source in the corpus; the machine's case rests on the measured mechanics, not that number.)
230
+ *
231
+ * Clear ORDER is machine-dependent: legacy clears oldest-first and stops at the budget; the cc
232
+ * machine deep-clears everything beyond the keep window behind the ≥20k gate (see
233
+ * {@link ContextEditMachine}).
234
+ */
162
235
  export declare function clearStaleToolResults(messages: AgentMessage[], opts: ContextEditOptions): AgentMessage[];
236
+ /** design/374 slice 1b (X5) — the resolved trigger-side window and whether the clamp repaired it. */
237
+ export interface TriggerWindowResolution {
238
+ /** The effective trigger-side window: `autoCompactTokens ?? contextTokens ?? contextWindow`,
239
+ * CLAMPED to the physical window when the declared autocompact window exceeds it. */
240
+ window: number;
241
+ /** True = the declared `autoCompactTokens` was ABOVE the physical window and got clamped down —
242
+ * a bad deployment value the caller should announce loudly (坏值响亮度默认律). */
243
+ clamped: boolean;
244
+ /** Present on the clamped arm: the declared (refused) value and the physical window it was
245
+ * clamped to, for the announcement text. */
246
+ declaredAutoCompactTokens?: number;
247
+ physicalWindow?: number;
248
+ }
249
+ /**
250
+ * design/374 slice 1b (X5, r2-M⑤) — SINGLE SOURCE for the trigger-side window chain
251
+ * (`autoCompactTokens ?? contextTokens ?? contextWindow`), now with the inversion clamp:
252
+ * `autoCompactTokens` is a TRIGGER-side lowering knob (dual-window semantics — the guard trim and
253
+ * physical request budgeting stay on `contextTokens ?? contextWindow`), so a declared value ABOVE
254
+ * the physical window inverts the whole geometry: the frontier (W−33000 of the declared window)
255
+ * lands HIGHER than the guard (physical−23000), every "the frontier machine runs first, the guard
256
+ * only backstops" argument silently reverses (967k declared vs 177k physical is the recorded
257
+ * counterexample), and the misconfiguration announces nothing. Clamp = `min(declared, physical)`,
258
+ * reported via `clamped` so the ONE announce point (prepare) can be loud about it. Every consumer
259
+ * of the chain (editBudget here / maybeCompact's window derivation / the compaction-window-hazard
260
+ * estimate / runtask's prefix-overhead disclosure) reads THIS helper — a fifth copy of the `??`
261
+ * chain is the drift this function exists to end. Non-finite/≤0 declared values keep their historic
262
+ * meaning (each consumer's own unknown-window sentinel), deliberately untouched here.
263
+ */
264
+ export declare function resolveTriggerWindow(model: Model): TriggerWindowResolution;
163
265
  /** The token budget at which context-editing begins for a model (below the harder guard trim).
164
266
  * = {@link contextEditFrontier} of the model's AUTOCOMPACT window (CC-parity fixed deduction with
165
267
  * a 0.7 small-window floor) — the same point the compaction threshold is clamped to. Dual-window
166
268
  * semantics: `autoCompactTokens` (when declared) feeds this trigger-side geometry only; the guard
167
- * trim (`guardBudget`) stays on the physical window. */
269
+ * trim (`guardBudget`) stays on the physical window — which is why the window is read through
270
+ * {@link resolveTriggerWindow}: an autocompact window declared ABOVE the physical one would put
271
+ * this frontier above the guard (inverted geometry), so it is clamped there (slice 1b, X5). */
168
272
  export declare function editBudget(model: Model): number;
273
+ /** Content fingerprint of an occurrence — a sha-256 digest over the FULL canonical content form
274
+ * (per block: type + complete text bytes, and for non-text blocks the mime type + payload length
275
+ * + a payload head), replay's mis-paint guard (adversarial rounds 1+2): ordinal mapping alone
276
+ * cannot see an equal-size cut-and-append churn inside a same-(id,timestamp) group — the mapped
277
+ * slot may hold a DIFFERENT occurrence, and painting it would clear content that was never
278
+ * cleared. r2 rejected the first cut's {chars, head-64} pair (same-prefix/same-length tails and
279
+ * media-only churn collide); the digest covers whole text bytes and block structure. A non-marker
280
+ * replay target must match or the entry prunes (full text returns — always the safe direction).
281
+ * Content-identical occurrences remain indistinguishable by construction and paint harmlessly. */
282
+ export type OccurrenceFingerprint = string;
283
+ export declare function fingerprintToolResultOccurrence(m: AgentMessage): OccurrenceFingerprint;
284
+ /** One recorded clear: the exact marker bytes to re-apply, the occurrence-group size at record
285
+ * time (the shrink-mapping input — see the lifecycle note above), and the original content's
286
+ * {@link OccurrenceFingerprint} (the mis-paint guard). */
287
+ export interface ClearedOccurrence {
288
+ marker: string;
289
+ groupCount: number;
290
+ fp: OccurrenceFingerprint;
291
+ }
292
+ /** The per-task ledger. Keys are OPAQUE occurrence coordinates minted by
293
+ * {@link replayClearedProjection} / {@link OccurrenceIndex} — derived per view, never persisted. */
294
+ export interface ClearedProjectionLedger {
295
+ entries: Map<string, ClearedOccurrence>;
296
+ }
297
+ export declare function createClearedProjectionLedger(): ClearedProjectionLedger;
298
+ /** One minted occurrence coordinate: the opaque ledger key, the group size at mint, the
299
+ * INDEXED-view fingerprint, and (when the index can hand it back) the indexed-view occurrence
300
+ * MESSAGE itself — the durable-composition seat (RB-212 durable half): a ledger-bound clear
301
+ * composes its marker + offload text from this occurrence, never from a cap-transformed copy, so
302
+ * the recorded marker discloses the occurrence's real media and the page-back ref resolves to its
303
+ * real text (the same bytes the replay repaints the marker onto). */
304
+ export interface OccurrenceKey {
305
+ key: string;
306
+ groupCount: number;
307
+ fp: OccurrenceFingerprint;
308
+ occurrence?: AgentMessage;
309
+ }
310
+ /** Occurrence coordinates for ONE request view (minted fresh per view — see the ledger note). */
311
+ export interface OccurrenceIndex {
312
+ /** Key the tool-result occurrence at this index of the indexed array. `fp` is the occurrence's
313
+ * fingerprint IN THE INDEXED (pre-cap) view — the same bytes the next session rebuild presents,
314
+ * which is what the replay guard compares against (r2: fingerprinting the post-cap projection
315
+ * instead made an MC-R clear of a cap-transformed result prune ITSELF on the retry build — an
316
+ * advertised clear that never reached the wire). */
317
+ keyAt(index: number): OccurrenceKey | undefined;
318
+ /** Key a tool-result by OBJECT identity against the indexed view (the MC-R seat: the rejected
319
+ * projection reuses the view's message objects for everything the caps did not replace); falls
320
+ * back to the unambiguous single-member (id, timestamp) group for cap-replaced copies, and
321
+ * answers undefined — "not clearable via the ledger" — for anything ambiguous. The returned
322
+ * `fp` (and `occurrence`) are ALWAYS of the indexed-view occurrence (never of a cap-replaced
323
+ * copy the caller holds). */
324
+ keyOf(m: AgentMessage): OccurrenceKey | undefined;
325
+ }
326
+ /** The MC-R planner's verdict — either the entries to record (with the pass's gated savings) or a
327
+ * typed decline the caller reports on the `context.mc_null` observation face. */
328
+ export type RejectionClearPlan = {
329
+ cleared: Array<{
330
+ key: string;
331
+ groupCount: number;
332
+ marker: string;
333
+ fp: OccurrenceFingerprint;
334
+ }>;
335
+ tokensSavedEstimate: number;
336
+ } | {
337
+ declined: "no_candidates" | "below_min_savings";
338
+ };
339
+ /**
340
+ * design/374 §3.2 (slice 2) — plan the MC-R rejection clear over the REJECTED projection: the same
341
+ * cc machine (keep 5 default, window base counts cleared occurrences, five-form exclusion, Sby
342
+ * savings, ≥20k gate) with the TRIGGER replaced by the rejection itself — the provider already said
343
+ * the input does not fit, so there is no budget coordinate to compare against (which is also why an
344
+ * enabled MC-R always runs the cc form regardless of the frontier machine knob: the legacy
345
+ * incremental form is defined by "stop when the budget is met" and has no meaning here).
346
+ * `keyOf` threads occurrence identity (the ledger is the ONLY way a rejection-arm clear survives
347
+ * the session rebuild — §1.2's adoption chain discards the in-memory view); a candidate the index
348
+ * cannot key is skipped from BOTH the clear set and the savings (never promise savings the ledger
349
+ * cannot deliver). Marker composition (offload persist included) runs only on the accept path —
350
+ * a declined plan has zero side effects.
351
+ */
352
+ export declare function planRejectionClears(messages: AgentMessage[], opts: {
353
+ keyOf: OccurrenceIndex["keyOf"];
354
+ offload?: ContextEditOptions["offload"];
355
+ compactableTools?: ReadonlySet<string>;
356
+ keepRecentToolResults?: number;
357
+ }): RejectionClearPlan;
358
+ /**
359
+ * Replay the ledger onto a freshly rebuilt request view (design/374 §3.2.6, r2-M⑩: positioned
360
+ * AFTER the heal, BEFORE the aggregate caps — so every estimator, cap and trigger downstream
361
+ * accounts the REPLAYED view, and a replayed marker never re-enters the cap store or re-emits a
362
+ * cap frame). Also the ledger's intersection point: entries that no longer resolve are pruned, and
363
+ * surviving entries are RE-KEYED to the current view's coordinates. Returns the SAME array instance
364
+ * when nothing changed (reference inequality is the pipeline's lossy-change signal), plus the
365
+ * view's occurrence index for the record/MC-R seats.
366
+ */
367
+ export declare function replayClearedProjection(messages: AgentMessage[], ledger: ClearedProjectionLedger): {
368
+ messages: AgentMessage[];
369
+ index: OccurrenceIndex;
370
+ };