pi-fold 1.0.1 → 1.0.3

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.
@@ -2,25 +2,29 @@ import {
2
2
  denseOwnArrayValues,
3
3
  evidenceSha256,
4
4
  objectRefKey,
5
+ sha256Text,
5
6
  sha256Value,
6
7
  stableStringify,
7
8
  } from "./json.ts";
8
- import {
9
- } from "./lib/advisory.ts";
9
+ import type { EvidenceRef } from "./json.ts";
10
10
  import {
11
11
  bytes,
12
12
  clone,
13
13
  emptyActiveContextState,
14
14
  ownValue,
15
+ sessionEntryMessages,
15
16
  uniqueMessageDigestAnchor,
16
17
  } from "./lib/canonical.ts";
17
18
  import {
18
19
  activeContextStatus,
19
20
  automaticPreparationId,
21
+ boundedOrientation,
20
22
  boundStatusPayload,
21
23
  commitPreparedFold,
24
+ consolidationSourceText,
22
25
  descendantIds,
23
26
  encodedFoldSource,
27
+ generatedBriefs,
24
28
  foldCandidatesDetail,
25
29
  foldTreeDetail,
26
30
  peekFoldSource,
@@ -42,6 +46,8 @@ import {
42
46
  capacityAccounting,
43
47
  contextUsageRatio,
44
48
  contextWindowFor,
49
+ explicitProtectedMass,
50
+ budgetOccupancy,
45
51
  hardFenceRatio,
46
52
  latestProviderContextMeasurement,
47
53
  orderedRoots,
@@ -49,6 +55,7 @@ import {
49
55
  parseNativeCompactionDecision,
50
56
  parseProviderContextMeasurementReceipt,
51
57
  persistenceProjection,
58
+ protectedStaleMass,
52
59
  providerContextMeasurement,
53
60
  providerTokens,
54
61
  refsProtected,
@@ -74,6 +81,7 @@ import {
74
81
  makeStateDelta,
75
82
  MAX_ACTIVE_FOLD_RECORDS,
76
83
  materializeStatePersistence,
84
+ foldProvenance,
77
85
  normalizeFoldsForPersistedRecords,
78
86
  normalizeLegacyProvenance,
79
87
  protectionSha256,
@@ -93,47 +101,58 @@ import {
93
101
  DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL,
94
102
  DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
95
103
  COMMIT_RECLAIM_FLOOR_SHARE,
96
- CURATION_OCCUPANCY_SHARE,
97
- CURATION_STALE_TOOL_SHARE,
98
104
  CONTEXT_RECEIPT_BLOCK_BYTES,
99
- CURATION_TARGET_OCCUPANCY_SHARE,
100
105
  DEFAULT_CONTEXT_WINDOW,
101
- DEFAULT_FOLD_SCHEDULING,
106
+ EPOCH_CONSOLIDATION_ROUNDS,
107
+ MAX_THRESHOLD_NOTICES,
108
+ THRESHOLD_NOTICE_SHARES,
109
+ resolveGuidance,
110
+ assertThresholdsServable,
111
+ resolveThresholds,
112
+ servingBudgetTokens,
102
113
  ESTIMATED_BYTES_PER_TOKEN,
103
114
  entryTypeNamespace,
104
- EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS,
105
- EPOCH_COMMIT_TARGET_WINDOW_SHARE,
115
+ MAX_BRIEF_BATCH_SPANS,
116
+ MAX_BRIEF_UPGRADE_QUEUE,
117
+ MAX_BRIEF_UPGRADES_IN_FLIGHT,
106
118
  MAX_FOLD_SPAN_CHARS,
119
+ MAX_PINNED_SHARE,
107
120
  MAX_WEDGE_ABSORB_TOKENS,
108
121
  OVERFLOW_RECOVERY_MAX_ATTEMPTS,
109
122
  PEEK_DEFAULT_MAX_BYTES,
110
123
  PEEK_MIN_SLICE_BYTES,
111
124
  PEEK_READ_ONLY_CONTEXT_ACTIONS,
112
- FOLD_SCHEDULING_MODES,
113
- READ_ONLY_CONTEXT_ACTIONS_DEFAULT,
114
- READ_ONLY_TOOLS_DEFAULT,
125
+ AUTO_FOLD_BLACKLIST_DEFAULT,
126
+ SURFACING_BRIEF_HIT,
127
+ SURFACING_CONTENT_HIT,
128
+ SURFACING_DIVERGENCE_MARGIN,
129
+ SURFACING_OUTCOME_WINDOW_ORDINALS,
130
+ SURFACING_SLATE_SIZE,
115
131
  USER_RESCUE_MAX_SOURCE_CHARS,
116
132
  } from "./lib/policy.ts";
117
133
  import type {
134
+ ActiveContextGuidance,
118
135
  ActiveContextSnapshot,
119
136
  ActiveContextState,
137
+ ActiveContextThresholds,
120
138
  ActiveContextToolAction,
139
+ BriefProvenance,
121
140
  FoldCandidate,
122
141
  FoldKind,
123
142
  FoldRecordEntry,
124
- FoldSchedulingMode,
125
143
  PreparedFold,
126
- SuggestionSource,
127
144
  } from "./lib/policy.ts";
128
145
  import {
129
146
  absorbWedgeMarks,
130
147
  } from "./lib/scheduling.ts";
131
148
  import {
132
149
  contextReceipt,
150
+ contextRiderText,
133
151
  curationSignals,
134
- curationTriggerFires,
152
+ lastCallText,
135
153
  markAwarenessText,
136
154
  receiptBlockText,
155
+ thresholdNoticeText,
137
156
  withReceipt,
138
157
  } from "./lib/curation.ts";
139
158
  import type {
@@ -144,6 +163,7 @@ import {
144
163
  addPendingMark,
145
164
  claimedRefKeys,
146
165
  commitPendingMarks,
166
+ consolidationMarks,
147
167
  ephemeralPeekMarks,
148
168
  epochCommitDue,
149
169
  estimatedTokens,
@@ -169,16 +189,21 @@ import {
169
189
  deterministicChapterCandidateBrief,
170
190
  deterministicConsolidationBrief,
171
191
  manualFoldCandidate,
192
+ peekedSourceFoldIds,
172
193
  selectAutomaticToolBatch,
173
- selectAutomaticToolForRung,
174
194
  snapFoldCandidate,
175
195
  snapToFoldBoundaries,
176
196
  splitCandidateBySize,
177
197
  } from "./lib/selection.ts";
178
198
  import type { SpanCorrection } from "./lib/selection.ts";
179
199
  import {
180
- acceptSurfacingSuggestion,
181
- surfacingOrdinal,
200
+ issueSurfacing,
201
+ noteSurfacingAction,
202
+ resolveSurfacing,
203
+ selectSurfacingSlate,
204
+ surfacingLedger,
205
+ surfacingSilenced,
206
+ surfacingSlateText,
182
207
  } from "./lib/surfacing.ts";
183
208
  import {
184
209
  compareProjections,
@@ -199,11 +224,27 @@ import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-s
199
224
  import {
200
225
  currentTurnRefKeys,
201
226
  mapActiveContext,
202
- toolFreshIndices,
203
227
  } from "./lib/transcript.ts";
228
+ import {
229
+ findOverflowErrorEntry,
230
+ overflowEventShape,
231
+ preStripHolds,
232
+ probeRollbackSurfaces,
233
+ rollbackNoticeText,
234
+ unansweredToolCalls,
235
+ } from "./lib/rollback.ts";
236
+
237
+ /**
238
+ * How a projection's token reading was reached.
239
+ *
240
+ * "anchored": the provider's own count for the projection this one extends, plus a rate
241
+ * estimate of the bytes appended after it. "rewritten": the current projection does not
242
+ * begin with the measured one, so there is nothing to add a delta to and the whole
243
+ * projection is estimated. "unmeasured": no accepted provider count yet.
244
+ */
245
+ export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured";
204
246
 
205
247
  // Test seam only; the package API is registerPiFold because package.json exports block deep imports.
206
- export * from "./lib/advisory.ts";
207
248
  export * from "./lib/canonical.ts";
208
249
  export * from "./lib/curation.ts";
209
250
  export * from "./lib/folding.ts";
@@ -211,19 +252,12 @@ export * from "./lib/instrumentation.ts";
211
252
  export * from "./lib/measurement.ts";
212
253
  export * from "./lib/persistence.ts";
213
254
  export * from "./lib/policy.ts";
255
+ export * from "./lib/rollback.ts";
214
256
  export * from "./lib/scheduling.ts";
215
257
  export * from "./lib/selection.ts";
216
258
  export * from "./lib/surfacing.ts";
217
259
  export * from "./lib/transcript.ts";
218
260
 
219
- /** Handle a suggestion source keeps: withdraw itself, and report that its item was acted on. */
220
- export interface SuggestionSourceHandle {
221
- unregister: () => void;
222
- accepted: (id: string) => void;
223
- }
224
-
225
- export type SuggestionSourceRegistrar = (source: SuggestionSource) => SuggestionSourceHandle;
226
-
227
261
  /**
228
262
  * The batched fold request: several {span, brief} pairs in one call, with the single
229
263
  * `ids` form still accepted because it is the shape a status action hands back.
@@ -261,113 +295,139 @@ export function batchedMarkRequests(params: Record<string, unknown>): BatchedMar
261
295
  }
262
296
 
263
297
  export function registerActiveContext(pi: any, options: {
298
+ /**
299
+ * INTERNAL brief-generator seam, not a public option: `registerPiFold` refuses the
300
+ * name and builds the generator from `summarizer`. It stays a parameter because it
301
+ * is the runtime's only brief-generator interface.
302
+ */
264
303
  summarizeContextSpan?: (request: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;
265
- setProjectionProvider?: (provider: (ctx: any) => Array<Record<string, unknown>>) => void;
266
- setSuggestionSourceRegistrar?: (register: SuggestionSourceRegistrar) => void;
267
- toolActions?: readonly ActiveContextToolAction[];
304
+ /**
305
+ * INTERNAL deployment-identity seam, not public options: the package entry refuses all
306
+ * five by name and the shipped identity is the one in `policy.ts`. They survive here
307
+ * because the neutrality gate has to register a synthetic brand to prove none of it
308
+ * reaches the defaults, and the experiment harness registers the pi-fold identity
309
+ * explicitly so its sealed runs keep their entry types.
310
+ */
268
311
  toolName?: string;
269
312
  toolLabel?: string;
270
313
  brandNoun?: string;
271
314
  entryTypePrefix?: string;
272
- commandPrefix?: string;
273
315
  commandNames?: { status?: string; fold?: string };
274
- readOnlyTools?: ReadonlySet<string>;
275
- blockingTools?: readonly string[];
276
- foldScheduling?: FoldSchedulingMode;
277
- foldPeekResults?: boolean;
278
- providerTotalWindow?: number;
316
+ /**
317
+ * The auto-fold EXCEPTION list, empty by default: every completed tool batch folds
318
+ * unmarked, and this names the tools whose results must stay raw.
319
+ */
320
+ blacklistAutoFoldTools?: ReadonlySet<string>;
321
+ /** The serving budget itself, ALREADY NET of the deployment's output reservation. */
322
+ providerInputBudget?: number;
323
+ /**
324
+ * The thermostat, set whole or not at all. USER policy: no agent action reads it back
325
+ * as a mutable surface, and status reports the values without offering to change them.
326
+ */
327
+ thresholds?: ActiveContextThresholds;
328
+ /** Both guidance surfaces, set together, defaulting on. */
329
+ guidance?: Partial<ActiveContextGuidance>;
279
330
  }): {
280
331
  projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
281
- registerSuggestionSource: SuggestionSourceRegistrar;
282
332
  } {
283
333
  const toolName = options.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME;
284
334
  const toolLabel = options.toolLabel ?? DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL;
285
335
  const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
286
336
  const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
287
- const commandPrefix = options.commandPrefix ?? "";
288
- const readOnlyTools = options.readOnlyTools ?? READ_ONLY_TOOLS_DEFAULT;
289
- const foldScheduling = options.foldScheduling ?? DEFAULT_FOLD_SCHEDULING;
290
- if (!FOLD_SCHEDULING_MODES.includes(foldScheduling)) {
291
- throw new Error(`foldScheduling must be one of ${FOLD_SCHEDULING_MODES.join(", ")}`);
337
+ const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
338
+ // Deleted options are REFUSED by name, never ignored. A deployment still passing one
339
+ // believes it asked for something, and silence would hand it the opposite behavior:
340
+ // `foldScheduling` chose between epoch and the deleted immediate scheduler,
341
+ // `foldPeekResults` opted peek results out of the foldable classification, and
342
+ // `toolActions` narrowed the action surface. All three are unconditional now.
343
+ for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
344
+ if (Object.hasOwn(options, removed)) {
345
+ throw new Error(`${removed} is no longer an option: epoch scheduling, peek foldability ` +
346
+ "and the whole action surface are unconditional");
347
+ }
348
+ }
349
+ if (Object.hasOwn(options, "blockingTools")) {
350
+ throw new Error("blockingTools is no longer an option: a tool call never causes a rewrite of " +
351
+ "its own, and the commit epoch is the only path that mutates the projection");
352
+ }
353
+ // The per-request surfacing channel. `setProjectionProvider` was a second delivery
354
+ // path for the value this call already returns, and `setSuggestionSourceRegistrar`
355
+ // handed out registration into a carrier that no longer exists: the slate rode a
356
+ // per-request ephemeral tail, which moved the prefix on every single pass. The
357
+ // selector survives and returns at the commit boundary; the channel does not.
358
+ for (const removed of ["setProjectionProvider", "setSuggestionSourceRegistrar"]) {
359
+ if (Object.hasOwn(options, removed)) {
360
+ throw new Error(`${removed} is no longer an option: projection candidates are returned by ` +
361
+ "registration, and external suggestion sources have no carrier to render into");
362
+ }
292
363
  }
293
- const epochScheduling = foldScheduling === "epoch";
294
- if (options.foldPeekResults !== undefined && typeof options.foldPeekResults !== "boolean") {
295
- throw new Error("foldPeekResults must be a boolean");
364
+ // The renamed options are refused by their OLD names, each pointing at the new one.
365
+ // `readOnlyTools` and then `autoFoldableTools` were both ALLOW-lists, and the list runs
366
+ // the other way now, so neither may be forwarded silently: an allow-list read as a
367
+ // blacklist bars exactly the tools it meant to permit. `commandPrefix` derived two
368
+ // command names from a stem the full-name override then overrode anyway;
369
+ // `providerTotalWindow` was a gross window the runtime netted down with a GUESSED
370
+ // reservation, and the guess is what the reshape deleted.
371
+ for (const inverted of ["readOnlyTools", "autoFoldableTools"]) {
372
+ if (Object.hasOwn(options, inverted)) {
373
+ throw new Error(`${inverted} is now blacklistAutoFoldTools, and the sense is INVERTED: ` +
374
+ "every completed tool batch folds unmarked, and the list names the exceptions whose " +
375
+ "results must stay raw. An allow-list moved across verbatim would bar exactly the " +
376
+ "tools it meant to permit");
377
+ }
378
+ }
379
+ if (Object.hasOwn(options, "commandPrefix")) {
380
+ throw new Error("commandPrefix is no longer an option: give whole names with commandNames");
296
381
  }
297
- // Peek mutates nothing, so its own tool result is a foldable read batch. Epoch mode
298
- // adopts that classification with its scheduling; immediate mode keeps the narrower
299
- // one unless the deployment opts in, because a peek copies a fold's stored source
300
- // back into the window and a window that cannot reclaim it grows without bound.
301
- const foldPeekResults = options.foldPeekResults ?? epochScheduling;
302
- if (options.providerTotalWindow !== undefined &&
303
- (!Number.isSafeInteger(options.providerTotalWindow) || options.providerTotalWindow <= 0)) {
304
- throw new Error("providerTotalWindow must be a positive integer");
382
+ if (Object.hasOwn(options, "providerTotalWindow")) {
383
+ throw new Error("providerTotalWindow is now providerInputBudget, and it is ALREADY NET: " +
384
+ "pass the tokens the deployment may actually fill, not the total window the runtime " +
385
+ "then subtracts a guessed output reservation from");
386
+ }
387
+ if (options.providerInputBudget !== undefined &&
388
+ (!Number.isSafeInteger(options.providerInputBudget) || options.providerInputBudget <= 0)) {
389
+ throw new Error("providerInputBudget must be a positive integer");
305
390
  }
306
391
  // The deployment's own fact, and the only capacity knob: declaring it makes every
307
392
  // ratio, fence and budget truthful, and leaving it out falls back to the provider
308
393
  // descriptor and SAYS "descriptor" in the capacity accounting.
309
- const providerTotalWindow = options.providerTotalWindow ?? null;
310
- const readOnlyContextActions = foldPeekResults
311
- ? PEEK_READ_ONLY_CONTEXT_ACTIONS
312
- : READ_ONLY_CONTEXT_ACTIONS_DEFAULT;
313
- if (!toolName || !toolLabel || !brandNoun || !entryTypePrefix || typeof commandPrefix !== "string" ||
314
- (commandPrefix && !/^[a-z0-9-]+$/.test(commandPrefix)) ||
315
- [...readOnlyTools].some((name) => typeof name !== "string" || !name)) {
316
- throw new Error("Active-context names and read-only tools must be nonempty strings");
394
+ const providerInputBudget = options.providerInputBudget ?? null;
395
+ const readOnlyContextActions = PEEK_READ_ONLY_CONTEXT_ACTIONS;
396
+ if (!toolName || !toolLabel || !brandNoun || !entryTypePrefix ||
397
+ [...blacklistAutoFoldTools].some((name) => typeof name !== "string" || !name)) {
398
+ throw new Error("Active-context names and blacklisted auto-fold tools must be nonempty strings");
317
399
  }
318
- const commandStem = commandPrefix ? `${commandPrefix.replace(/-+$/, "")}-` : "";
319
- // Full-name override for hosts that need non-default command names (e.g. the
320
- // pi-fold package's neutral "context"); commandPrefix remains the derived form.
321
400
  const commandNames = {
322
- status: options.commandNames?.status ?? `${commandStem}${DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.status}`,
323
- fold: options.commandNames?.fold ?? `${commandStem}${DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.fold}`,
401
+ status: options.commandNames?.status ?? DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.status,
402
+ fold: options.commandNames?.fold ?? DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.fold,
324
403
  };
325
404
  if (![commandNames.status, commandNames.fold].every((name) =>
326
405
  typeof name === "string" && /^[a-z0-9][a-z0-9-]*$/.test(name)) ||
327
406
  commandNames.status === commandNames.fold) {
328
407
  throw new Error("Active-context command names must be distinct kebab-case strings");
329
408
  }
330
- const configuredBlockingTools = denseOwnArrayValues(options.blockingTools ?? ["Agent"]);
331
- if (!configuredBlockingTools || configuredBlockingTools.some((name) => typeof name !== "string" || !name) ||
332
- new Set(configuredBlockingTools).size !== configuredBlockingTools.length) {
333
- throw new Error("Blocking tools must be one dense array of unique nonempty strings");
334
- }
335
- const blockingTools = new Set(configuredBlockingTools as string[]);
409
+ // The thermostat, validated atomically before anything else reads a threshold, and
410
+ // then checked against the budget this deployment can actually serve.
411
+ const thresholds = resolveThresholds(options.thresholds);
412
+ assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
413
+ const guidance = resolveGuidance(options.guidance);
336
414
  const stateEntryType = `${entryTypePrefix}-state`;
337
415
  const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
338
416
  const milestoneProjectionType = `${entryTypePrefix}-milestone`;
339
417
  const advisoryProjectionType = `${entryTypePrefix}-advisory`;
340
418
  const surfacingProjectionType = `${entryTypePrefix}-surfacing`;
341
419
  const receiptProjectionType = `${entryTypePrefix}-receipts`;
420
+ const riderProjectionType = `${entryTypePrefix}-rider`;
421
+ const lastCallProjectionType = `${entryTypePrefix}-lastcall`;
422
+ const noticeProjectionType = `${entryTypePrefix}-notice`;
342
423
  const curationProjectionType = `${entryTypePrefix}-curation`;
343
424
  const entryNamespace = entryTypeNamespace(entryTypePrefix);
344
425
  const providerMeasurementEntryType = `${entryNamespace}-provider-context-measurement`;
345
426
  const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
346
427
  const contextEventEntryType = `${entryNamespace}-context-event`;
347
428
  const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
348
- // The commit verb only exists where marks exist; immediate mode keeps its exact
349
- // seven-action surface and description.
350
- const defaultToolActions = epochScheduling
351
- ? EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS
352
- : ACTIVE_CONTEXT_TOOL_ACTIONS;
353
- const configuredToolActions = denseOwnArrayValues(
354
- options.toolActions ?? defaultToolActions,
355
- );
356
- if (!configuredToolActions || configuredToolActions.length < 1) {
357
- throw new Error("Active-context tool actions must be one non-empty dense array");
358
- }
359
- const allowedToolActions: ActiveContextToolAction[] = [];
360
- const allowedToolActionSet = new Set<string>();
361
- for (const value of configuredToolActions) {
362
- if (typeof value !== "string" ||
363
- !EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS.includes(value as ActiveContextToolAction) ||
364
- allowedToolActionSet.has(value)) {
365
- throw new Error(`Invalid or duplicate active-context tool action '${String(value)}'`);
366
- }
367
- allowedToolActions.push(value as ActiveContextToolAction);
368
- allowedToolActionSet.add(value);
369
- }
370
- Object.freeze(allowedToolActions);
429
+ const allowedToolActions: readonly ActiveContextToolAction[] = ACTIVE_CONTEXT_TOOL_ACTIONS;
430
+ const allowedToolActionSet = new Set<string>(allowedToolActions);
371
431
 
372
432
  type AutomaticFailureState = {
373
433
  key: string;
@@ -408,6 +468,62 @@ export function registerActiveContext(pi: any, options: {
408
468
  actionQueue: Promise.resolve<unknown>(undefined),
409
469
  };
410
470
 
471
+ /**
472
+ * THE BRIEF UPGRADE LANE.
473
+ *
474
+ * A fold brief is model-written from this campaign forward; the deterministic brief is
475
+ * what a failure leaves behind. The automatic ladder cannot wait for a generator: the
476
+ * commit path is synchronous with a measured projection and a fold deferred until a
477
+ * provider answers is a fold that does not happen. So the ladder commits deterministic,
478
+ * a generator runs between boundaries, and the brief it writes lands as an UPGRADE
479
+ * riding the next commit: the same law wedge absorption and the surfacing slate follow,
480
+ * mutations only inside a rewrite the session already paid for.
481
+ *
482
+ * Everything here is process-local. An upgrade lost to a reload is a brief that stays
483
+ * deterministic, which is exactly the failure the fallback exists for, and the state
484
+ * wire keeps one shape instead of carrying work in progress.
485
+ */
486
+ const upgrades = {
487
+ /**
488
+ * BATCHES of folds committed deterministic, each holding the exact source its briefs
489
+ * will cover. One batch is one generator call: a commit folds its spans together, so
490
+ * they are briefed together, and the spans of one batch orient each other. Sending them
491
+ * one at a time is what left the 2026-08-11 rep with 14 model briefs across 87 folds.
492
+ */
493
+ queue: [] as Array<{
494
+ members: Array<{ foldId: string; sourceSha256: string }>;
495
+ request: Record<string, unknown>;
496
+ }>,
497
+ /**
498
+ * Generator calls in flight, keyed by every fold each one briefs, bounded by a constant
499
+ * and cancel-safe on session change or shutdown. Keyed rather than counted because
500
+ * the queue has to know a fold is already being briefed.
501
+ */
502
+ running: new Map<string, { controller: AbortController; promise: Promise<void> }>(),
503
+ /** Written briefs waiting for a boundary. They never apply anywhere else. */
504
+ ready: [] as Array<{ foldId: string; sourceSha256: string; brief: string; provenance: BriefProvenance }>,
505
+ /** A generator that failed a fold does not get it back: one attempt, then deterministic stands. */
506
+ failed: new Set<string>(),
507
+ /**
508
+ * Parents whose turn has not come: a group is read at depth one, so a child still
509
+ * waiting on its own brief would be read through the sentence that brief is about to
510
+ * replace. They are retried at every boundary and never dropped, because the wait ends
511
+ * the moment the lane drains and a parent that was skipped once is a parent no
512
+ * generator ever writes.
513
+ */
514
+ deferred: new Set<string>(),
515
+ /** Failures since the last commit record carried them, so none of this is silent. */
516
+ failures: 0,
517
+ /**
518
+ * Briefs that took a second ask before they met the contract. Counted because the cure
519
+ * is the mechanism that lets the brief cap be a real limit rather than a truncation
520
+ * point, so how often it fires is how we learn whether the cap is set anywhere near
521
+ * right (Shane 2026-08-11).
522
+ */
523
+ cures: 0,
524
+ lastError: null as string | null,
525
+ };
526
+
411
527
  // Owns provider receipts, anchors, and usage; providerMeasurementQueue serializes receipt writes.
412
528
  const measurements = {
413
529
  latestRatio: null as number | null,
@@ -430,10 +546,31 @@ export function registerActiveContext(pi: any, options: {
430
546
  lastProjectedEstimate: null as number | null,
431
547
  /** Whether that estimate used a measured ratio rather than the bootstrap constant. */
432
548
  lastProjectedEstimateCalibrated: false,
549
+ /** How that reading was reached, so an audit can tell an anchor from a fallback. */
550
+ lastProjectedEstimateBasis: "unmeasured" as ProjectionReadingBasis,
551
+ /** That projection's own size at one rate, the baseline a reduction is measured from. */
552
+ lastProjectedSizeTokens: null as number | null,
553
+ /**
554
+ * The provider's own count for the last projection this process TRANSMITTED, held
555
+ * with that projection's serialized text so a later projection can be tested against
556
+ * it by construction. `head` is the serialization minus its closing bracket, which is
557
+ * what makes "the same projection with more appended after it" a byte-exact prefix
558
+ * test rather than a size comparison.
559
+ */
560
+ projectionAnchor: null as {
561
+ tokens: number;
562
+ chars: number;
563
+ head: string;
564
+ messageSha256: string;
565
+ sessionId: string;
566
+ generation: number;
567
+ } | null,
433
568
  /** Signed estimator error against recent measurements, as a share of measured tokens. */
434
569
  estimatorErrors: [] as number[],
435
570
  /** Growth in measured tokens between consecutive requests. */
436
571
  inflowSteps: [] as number[],
572
+ /** Growth steps the OUTPUT WALL reads. Separate from `inflowSteps` on purpose: see noteWallInflow. */
573
+ wallInflowSteps: [] as number[],
437
574
  providerMeasurementQueue: Promise.resolve<void>(undefined),
438
575
  providerMeasurementReceipts: new Set<string>(),
439
576
  providerMeasurementRevisionByMessageSha: new Map<string, number>(),
@@ -517,6 +654,15 @@ export function registerActiveContext(pi: any, options: {
517
654
  * runtime rewrites the projection, which is the whole cost model.
518
655
  */
519
656
  reopenBaselineShare: null as number | null,
657
+ /** Whether the output wall's crossing has already spent its one forced commit. */
658
+ wallEpisodeOpen: false,
659
+ /**
660
+ * Where the armed last-call actually LANDED: the exposure it rendered and the
661
+ * ordinal of the pass that rendered it. In-memory on purpose: after a reload the
662
+ * carrier re-renders on the first projection, this re-arms, and the commit waits
663
+ * one more round rather than firing on a prompt nobody saw.
664
+ */
665
+ lastCallDelivery: null as { exposure: number; ordinal: number } | null,
520
666
  /** Recovery attempts spent on the CURRENT inflow; reset by any accepted request. */
521
667
  recoveryAttempts: 0,
522
668
  /** A provider rejection observed but not yet recovered from. */
@@ -525,6 +671,29 @@ export function registerActiveContext(pi: any, options: {
525
671
  instrumentationQueue: Promise.resolve<void>(undefined),
526
672
  };
527
673
 
674
+ /**
675
+ * The tree-rollback lane.
676
+ *
677
+ * Armed once per session against the pi surfaces it calls, because every one of them
678
+ * is an internal the extension contract does not promise. Disarmed, the lane is OFF
679
+ * and says so on the record: an overflow still emits `context.rollback` with
680
+ * `armed: false` and the reason, the user is notified, and the session behaves
681
+ * exactly as it did before this build -- the projection-budget fence aborts an
682
+ * over-budget request rather than half-performing a recovery.
683
+ */
684
+ const rollback = {
685
+ probes: null as ReturnType<typeof probeRollbackSurfaces> | null,
686
+ armed: false,
687
+ /** pi's own overflow classifier, resolved from the host; null when unreachable. */
688
+ classifier: null as ((message: unknown, contextWindow: number) => boolean) | null,
689
+ classifierSource: null as string | null,
690
+ /** Ordinals of episodes handled, so the one-shot and the ledger agree. */
691
+ attempts: 0,
692
+ last: null as Record<string, unknown> | null,
693
+ /** An overflow seen at message_end and not yet claimed by the compaction event. */
694
+ pendingOverflow: null as { at: number; entryId: string | null } | null,
695
+ };
696
+
528
697
  // Owns advisory arming and hard-fence delivery; durable effects use persistenceQueue.
529
698
  const advisory = {
530
699
  hardFenceNoticeKey: null as string | null,
@@ -532,16 +701,6 @@ export function registerActiveContext(pi: any, options: {
532
701
  hardFenceReleasedProjectionKeys: new Set<string>(),
533
702
  };
534
703
 
535
- // Registered suggestion sources. RETRIEVAL IS UNWIRED FROM THE PROJECTION as of this
536
- // build: the slate that rendered these was a per-pass tail carrier, and a tail carrier
537
- // is displaced by every append, so it diverged the prefix on every single pass. The
538
- // selector in lib/surfacing.ts is kept and still gated, because proactive retrieval is
539
- // the right idea in the wrong place: when it returns it must ride a commit like every
540
- // other carrier. Until then nothing renders a suggestion, so nothing selects one.
541
- const surfacing = {
542
- sources: [] as SuggestionSource[],
543
- };
544
-
545
704
  // Owns native-compaction decisions and completion retry state; nativeReceiptQueue serializes it.
546
705
  const nativeCompaction = {
547
706
  lastThresholdDecision: null as Record<string, unknown> | null,
@@ -560,8 +719,6 @@ export function registerActiveContext(pi: any, options: {
560
719
  shuttingDown: false,
561
720
  latestSnapshot: null as ActiveContextSnapshot | null,
562
721
  latestSnapshotError: null as string | null,
563
- blockingToolHarvestedThisTurn: false,
564
- blockingToolHarvestQueuedThisTurn: false,
565
722
  contextQueue: Promise.resolve<void>(undefined),
566
723
  };
567
724
 
@@ -620,6 +777,103 @@ export function registerActiveContext(pi: any, options: {
620
777
  return record;
621
778
  };
622
779
 
780
+ /**
781
+ * The generator, wrapped once so every brief it writes is on the record.
782
+ *
783
+ * Wrapping HERE rather than instrumenting the two call sites is deliberate. The inline
784
+ * path in `prepareFold` and the async upgrade lane both receive this function, so one
785
+ * wrapper covers both, no signature changes and no way to add a third caller that
786
+ * quietly escapes the ledger. It also covers a deployment's OWN summarizer, which is
787
+ * the case a call-site patch would have missed.
788
+ *
789
+ * A generator call is invisible everywhere else: it runs outside the session, so it
790
+ * lands in no provider ledger, and it runs off the turn, so it lands in no turn timing.
791
+ * Duration, queue wait and usage are therefore recorded here or nowhere.
792
+ *
793
+ * A failure is emitted and rethrown unchanged. This observes; it decides nothing, and
794
+ * the fallback to the deterministic brief stays exactly where it was.
795
+ */
796
+ const observedSummarize = options.summarizeContextSpan
797
+ ? async (request: Record<string, unknown>, summarizerCtx: unknown): Promise<Record<string, unknown>> => {
798
+ const startedAt = Date.now();
799
+ const queuedAt = typeof request.queuedAtMs === "number" ? request.queuedAtMs : null;
800
+ const spans = Array.isArray(request.spans)
801
+ ? request.spans as Array<Record<string, unknown>>
802
+ : null;
803
+ const sourceText = typeof request.sourceText === "string" ? request.sourceText : "";
804
+ // A span is a CONSOLIDATION when it is a group of folds, and an automatic fold
805
+ // otherwise. Both kinds are counted and both kinds' source is measured, because a
806
+ // call can carry a mix and the honest way to attribute its tokens across kinds is by
807
+ // the share of source each kind contributed, never by pretending the call was one
808
+ // kind (Shane 2026-08-11: track cures and tokens by fold type).
809
+ const groupSpan = (span: Record<string, unknown>): boolean =>
810
+ Number.isInteger(span.children) && (span.children as number) > 1;
811
+ const charsOf = (span: Record<string, unknown>): number =>
812
+ typeof span.sourceText === "string" ? span.sourceText.length : 0;
813
+ const kindCounts = spans
814
+ ? {
815
+ spans: spans.length,
816
+ group_spans: spans.filter(groupSpan).length,
817
+ leaf_spans: spans.filter((span) => !groupSpan(span)).length,
818
+ group_source_chars: spans.filter(groupSpan).reduce((sum, span) => sum + charsOf(span), 0),
819
+ leaf_source_chars: spans.filter((span) => !groupSpan(span))
820
+ .reduce((sum, span) => sum + charsOf(span), 0),
821
+ fold_ids: spans.map((span) => typeof span.candidateId === "string" ? span.candidateId : ""),
822
+ }
823
+ : {
824
+ spans: 1,
825
+ group_spans: groupSpan(request) ? 1 : 0,
826
+ leaf_spans: groupSpan(request) ? 0 : 1,
827
+ group_source_chars: groupSpan(request) ? sourceText.length : 0,
828
+ leaf_source_chars: groupSpan(request) ? 0 : sourceText.length,
829
+ };
830
+ const base = {
831
+ fold_id: typeof request.candidateId === "string" ? request.candidateId : "",
832
+ source_chars: spans ? spans.reduce((sum, span) => sum + charsOf(span), 0) : sourceText.length,
833
+ source_sha256: typeof request.sourceSha256 === "string" ? request.sourceSha256 : sha256Text(sourceText),
834
+ ...kindCounts,
835
+ // The second ask, not the first. How often this fires is how we learn whether the
836
+ // brief cap is anywhere near right, so it is a field rather than an inference.
837
+ cure: typeof request.cure === "string" && request.cure.length > 0,
838
+ ...(queuedAt === null ? {} : { queued_ms: Math.max(0, startedAt - queuedAt) }),
839
+ };
840
+ try {
841
+ const result = await options.summarizeContextSpan!(request, summarizerCtx);
842
+ const written = Array.isArray(result?.briefs)
843
+ ? (result.briefs as unknown[]).map((item) => typeof item === "string" ? item.trim() : "")
844
+ : [typeof result?.brief === "string" ? result.brief.trim() : ""];
845
+ emit("context.brief", {
846
+ ...base,
847
+ outcome: "ok",
848
+ duration_ms: Date.now() - startedAt,
849
+ provider: typeof result?.provider === "string" ? result.provider : "",
850
+ model: typeof result?.model === "string" ? result.model : "",
851
+ effort: typeof result?.effort === "string" ? result.effort : "",
852
+ brief_chars: written.reduce((sum, item) => sum + item.length, 0),
853
+ // Per span, so a batch where one brief came back empty is visible as that rather
854
+ // than as a slightly short total.
855
+ brief_chars_each: written.map((item) => item.length),
856
+ brief_sha256: written.length === 1 && written[0] ? sha256Text(written[0]) : "",
857
+ // Absent when the provider reported none. Never defaulted: a call whose cost we
858
+ // do not know and a call that cost nothing are different facts.
859
+ usage: result?.usage && typeof result.usage === "object" ? clone(result.usage) : null,
860
+ });
861
+ return result;
862
+ } catch (error) {
863
+ const message = error instanceof Error ? error.message : String(error);
864
+ emit("context.brief", {
865
+ ...base,
866
+ // A timeout is not a refusal and not a provider fault, and telling them apart is
867
+ // the difference between raising the bound and changing the generator.
868
+ outcome: /exceeded \d+ms/.test(message) ? "timeout" : "error",
869
+ duration_ms: Date.now() - startedAt,
870
+ error: boundReceiptText(message, 240, "brief generator"),
871
+ });
872
+ throw error;
873
+ }
874
+ }
875
+ : undefined;
876
+
623
877
  /**
624
878
  * Actions of ours that move bytes at or before a prefix position. Receipts, gate
625
879
  * notices and suggestions are appended after the whole projection, so they can only
@@ -641,6 +895,7 @@ export function registerActiveContext(pi: any, options: {
641
895
  split_from_chars: receipt.splitFromChars,
642
896
  absorbed_wedges: receipt.absorbedWedges,
643
897
  recovered: receipt.recovered,
898
+ protected_bytes: receipt.protectedBytes,
644
899
  note: receipt.note,
645
900
  });
646
901
  };
@@ -671,18 +926,18 @@ export function registerActiveContext(pi: any, options: {
671
926
  };
672
927
 
673
928
  /**
674
- * The window every ratio, fence and budget is computed against. With truthful
675
- * capacity on this is the provider's TOTAL admission window, not the per-request
676
- * max-input descriptor: the descriptor bakes in a full output reservation the
677
- * deployment never asked for, and treating it as the ceiling aborts requests inside
678
- * real headroom. The descriptor is still read, and still reported, so the gap stays
929
+ * The number every ratio, fence and budget is computed against. Declared, it is the
930
+ * deployment's own serving budget, already net of whatever output reservation it holds
931
+ * back; undeclared, it is the per-request max-input descriptor, which bakes in a full
932
+ * output reservation the deployment never asked for and so aborts requests inside real
933
+ * headroom. The descriptor is still read, and still reported, so the gap stays
679
934
  * auditable rather than assumed.
680
935
  */
681
936
  const budgetWindowFor = (ctx: any): number | null => {
682
937
  // Remembered so the ctx-free callers (trigger, gate, reminders, advisory) report the
683
938
  // same descriptor gap the fence does instead of re-reading a second source.
684
939
  measurements.descriptorWindow = contextWindowFor(ctx);
685
- return providerTotalWindow ?? measurements.descriptorWindow;
940
+ return providerInputBudget ?? measurements.descriptorWindow;
686
941
  };
687
942
 
688
943
  /**
@@ -698,7 +953,7 @@ export function registerActiveContext(pi: any, options: {
698
953
  const servingCapacity = (window: number | null): ReturnType<typeof capacityAccounting> =>
699
954
  capacityAccounting({
700
955
  window: window ?? lifecycle.latestSnapshot?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
701
- truthful: providerTotalWindow !== null,
956
+ truthful: providerInputBudget !== null,
702
957
  descriptorWindow: measurements.descriptorWindow,
703
958
  usedTokens: measurements.lastProviderMeasurement?.tokens ?? null,
704
959
  });
@@ -713,9 +968,11 @@ export function registerActiveContext(pi: any, options: {
713
968
  toolName,
714
969
  brandNoun,
715
970
  entryTypePrefix,
716
- readOnlyTools,
971
+ blacklistAutoFoldTools,
717
972
  readOnlyContextActions,
718
973
  contextWindow: budgetWindowFor(ctx) ?? undefined,
974
+ netBudget: providerInputBudget !== null,
975
+ thresholds,
719
976
  });
720
977
 
721
978
  const authoritativeSnapshotFor = (ctx: any): ActiveContextSnapshot => {
@@ -731,9 +988,11 @@ export function registerActiveContext(pi: any, options: {
731
988
  toolName,
732
989
  brandNoun,
733
990
  entryTypePrefix,
734
- readOnlyTools,
991
+ blacklistAutoFoldTools,
735
992
  readOnlyContextActions,
736
993
  contextWindow: budgetWindowFor(ctx) ?? undefined,
994
+ netBudget: providerInputBudget !== null,
995
+ thresholds,
737
996
  });
738
997
  };
739
998
 
@@ -777,13 +1036,15 @@ export function registerActiveContext(pi: any, options: {
777
1036
  lifecycle.generation += 1;
778
1037
  lifecycle.shuttingDown = false;
779
1038
  cancelPreparation();
1039
+ cancelBriefUpgrades();
780
1040
  lifecycle.latestSnapshot = null;
781
1041
  lifecycle.latestSnapshotError = null;
782
1042
  measurements.latestRatio = null;
783
1043
  measurements.lastProviderMeasurement = null;
1044
+ measurements.wallInflowSteps.length = 0;
1045
+ measurements.projectionAnchor = null;
1046
+ measurements.lastProjectedEstimateBasis = "unmeasured";
784
1047
  ladder.pendingManual = false;
785
- lifecycle.blockingToolHarvestedThisTurn = false;
786
- lifecycle.blockingToolHarvestQueuedThisTurn = false;
787
1048
  if (!preserveThresholdDecision) nativeCompaction.lastThresholdDecision = null;
788
1049
  ladder.lastPreparationError = null;
789
1050
  ladder.boundaryFailure = null;
@@ -1240,11 +1501,10 @@ export function registerActiveContext(pi: any, options: {
1240
1501
  protection: persistence.state ? protectionSha256(persistence.state) : null,
1241
1502
  selection,
1242
1503
  policy: snapshot ? {
1243
- toolFoldRatio: snapshot.policy.toolFoldRatio,
1244
- refoldRatio: snapshot.policy.refoldRatio,
1245
- prepareRatio: snapshot.policy.prepareRatio,
1504
+ maxTarget: snapshot.thresholds.maxTarget,
1505
+ minTarget: snapshot.thresholds.minTarget,
1506
+ freshTail: snapshot.thresholds.freshTail,
1246
1507
  hardFenceRatio: hardFenceRatio(snapshot),
1247
- consolidationRatio: snapshot.policy.consolidationRatio,
1248
1508
  } : null,
1249
1509
  });
1250
1510
  };
@@ -1385,7 +1645,7 @@ export function registerActiveContext(pi: any, options: {
1385
1645
  if (chars === null || chars < PROJECTION_CALIBRATION_MIN_CHARS) return;
1386
1646
  if (!Number.isFinite(measurement.tokens) || measurement.tokens < PROJECTION_CALIBRATION_MIN_TOKENS) return;
1387
1647
  // The signed error of the estimate we made for the projection this measurement
1388
- // describes. This is the only honest measure of how wrong the fence can be.
1648
+ // describes. This is the only direct measure of how wrong the fence can be.
1389
1649
  // Only a CALIBRATED estimate's error belongs in the window. The bootstrap constant
1390
1650
  // is wrong by design -- 76% high in rep12 -- and letting that one reading set the
1391
1651
  // margin would keep every later request permanently inside it.
@@ -1424,8 +1684,126 @@ export function registerActiveContext(pi: any, options: {
1424
1684
  );
1425
1685
  };
1426
1686
 
1427
- const projectedTokenEstimate = (projected: unknown[]): number =>
1428
- Math.ceil(bytes(projected) / projectionCharsPerToken());
1687
+ /**
1688
+ * Bind the provider's count to the projection it counted.
1689
+ *
1690
+ * The anchor is the number the provider reported for a request this process actually
1691
+ * TRANSMITTED, held with that projection's serialized bytes. Identity is carried three
1692
+ * ways so a stale anchor cannot pass for a fresh one: the session it belongs to, the
1693
+ * attribution generation (a model or thinking-level change is a different tokenizer, so
1694
+ * its counts describe nothing here), and the projection text itself, which the reading
1695
+ * re-checks byte for byte before adding anything to it.
1696
+ */
1697
+ const noteProviderProjectionAnchor = (measurement: ProviderContextMeasurement): void => {
1698
+ // ONE ANCHOR PER MEASURED RESPONSE. A pass that re-observes the same assistant
1699
+ // message -- the retry after a provider rejection, or any pass the host runs without
1700
+ // a new response -- would otherwise re-bind that count to a newer projection the
1701
+ // provider never counted, and the count would silently describe the wrong bytes.
1702
+ if (measurements.projectionAnchor?.messageSha256 === measurement.messageSha256) return;
1703
+ const text = instrumentation.previousText;
1704
+ // Only a TRANSMITTED projection can be anchored: an aborted pass built a projection
1705
+ // the provider never saw, so no reported count describes it.
1706
+ if (!text || text.length < 2 || !text.endsWith("]") || !persistence.state) {
1707
+ measurements.projectionAnchor = null;
1708
+ return;
1709
+ }
1710
+ const chars = bytes(text);
1711
+ // AND THE COUNT HAS TO FIT THE BYTES IT IS BOUND TO.
1712
+ //
1713
+ // An anchor is exact for the bytes it covers only if it actually describes them. A
1714
+ // pairing that implies fewer serialized chars per token than any tokenizer produces
1715
+ // is not describing this projection, and taken at face value it would put the fence
1716
+ // under permanent pressure a projection of that size cannot explain. The calibration
1717
+ // window already refuses such a ratio through the same floor; the anchor refuses it
1718
+ // outright and falls back to the estimate. Only the FLOOR applies: a rate ABOVE the
1719
+ // calibration ceiling is the signature-heavy composition this mechanism exists to
1720
+ // read, and clamping it would throw away the measurement that motivated the build.
1721
+ if (!(measurement.tokens > 0) || chars / measurement.tokens < PROJECTION_CHARS_PER_TOKEN_FLOOR) {
1722
+ measurements.projectionAnchor = null;
1723
+ return;
1724
+ }
1725
+ measurements.projectionAnchor = {
1726
+ tokens: measurement.tokens,
1727
+ chars,
1728
+ head: text.slice(0, -1),
1729
+ messageSha256: measurement.messageSha256,
1730
+ sessionId: persistence.state.sessionId,
1731
+ generation: lifecycle.generation,
1732
+ };
1733
+ };
1734
+
1735
+ /**
1736
+ * THE OCCUPANCY READING: what the provider counted, plus only what arrived after it.
1737
+ *
1738
+ * A whole-projection estimate divides every byte in the window by one rate, and one
1739
+ * rate does not fit the window's contents. Measured 2026-08-10 on the sealed rep-23
1740
+ * run, byte exact against that run's own projection records: the estimate is unbiased
1741
+ * at the pre-commit PEAKS (provider over estimate, mean 1.004 across six) and over-reads
1742
+ * the post-fold TROUGHS by 22 to 35 percent (mean ratio 0.784 across six). It is a phase
1743
+ * effect, not drift: encrypted reasoning-signature blobs on assistant messages grew from
1744
+ * 14.6 to 51.3 percent of projection bytes over that run and price at a fitted 24.8 chars
1745
+ * per token against 3.957 for ordinary content, so a projection's rate depends on what it
1746
+ * is made of. The runtime therefore believed its floor climbed 0.298 to 0.490 of budget
1747
+ * while the provider read 0.276 to 0.382: roughly 45 percent of the apparent floor rise
1748
+ * was measurement error, five of six true troughs sat at or below the 0.35 minTarget, and
1749
+ * the ladder was folding past its own floor without seeing it.
1750
+ *
1751
+ * So the rate is applied to the DELTA only. The provider's count for the projection this
1752
+ * one extends is exact for every byte it covers, whatever those bytes are made of, and
1753
+ * only the material appended since is estimated. Measured on the same run: mean absolute
1754
+ * occupancy error across all requests falls from 8.38 to 3.08 percent, and inside every
1755
+ * post-fold refill window it drops from the +9 to +27 percent band to under 4 percent from
1756
+ * the second request onward.
1757
+ *
1758
+ * Two things this deliberately does NOT do. It does not move the commit trigger: at all
1759
+ * twelve commit points of that run the delta since the last provider count was exactly
1760
+ * zero, because the commit hook runs after the response and its usage record have landed.
1761
+ * And it does not fix the one pass per cycle where the projection is REWRITTEN rather than
1762
+ * appended, right after a commit: the anchor describes a projection this one does not
1763
+ * begin with, so there is nothing to add a delta to and the whole-projection estimate
1764
+ * stands, error and all (+27 to +35 percent, measured). That case is detected by
1765
+ * construction, not by the sign of the delta: the current serialization must begin with
1766
+ * the anchored one verbatim, up to the array separator.
1767
+ *
1768
+ * The anchor is the provider's TOTAL for that request, which includes the output tokens
1769
+ * of the response the next projection appends and then prices again as bytes. That is one
1770
+ * response's worth of double count, it runs HIGH rather than low, and it keeps a single
1771
+ * definition of "the provider's number" across occupancy, the ratio and the calibration
1772
+ * instead of a second one that only this reading would use.
1773
+ */
1774
+ const projectedTokenReading = (projected: unknown[]): {
1775
+ tokens: number;
1776
+ basis: ProjectionReadingBasis;
1777
+ chars: number;
1778
+ anchorTokens: number | null;
1779
+ deltaChars: number | null;
1780
+ } => {
1781
+ const text = stableStringify(projected);
1782
+ const chars = Buffer.byteLength(text, "utf8");
1783
+ const charsPerToken = projectionCharsPerToken();
1784
+ const estimate = Math.ceil(chars / charsPerToken);
1785
+ const anchor = measurements.projectionAnchor;
1786
+ if (!anchor || anchor.generation !== lifecycle.generation ||
1787
+ anchor.sessionId !== persistence.state?.sessionId) {
1788
+ return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null };
1789
+ }
1790
+ // By construction: the anchored serialization minus its closing bracket must be a
1791
+ // byte-exact prefix, and the byte after it must be the array separator or the close.
1792
+ // A message that merely GREW in place fails this, because the anchored head ends with
1793
+ // that message's own closing brace.
1794
+ const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
1795
+ if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
1796
+ return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null };
1797
+ }
1798
+ const deltaChars = chars - anchor.chars;
1799
+ return {
1800
+ tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken)),
1801
+ basis: "anchored",
1802
+ chars,
1803
+ anchorTokens: anchor.tokens,
1804
+ deltaChars,
1805
+ };
1806
+ };
1429
1807
 
1430
1808
  /** The largest recent estimator error, as a share. Unmeasured sessions assume none. */
1431
1809
  const estimatorErrorShare = (): number => measurements.estimatorErrors.length
@@ -1437,6 +1815,38 @@ export function registerActiveContext(pi: any, options: {
1437
1815
  ? Math.max(...measurements.inflowSteps)
1438
1816
  : 0;
1439
1817
 
1818
+ /**
1819
+ * THE OUTPUT WALL'S OWN FORWARD LOOK, AND WHY IT IS NOT THE ONE ABOVE.
1820
+ *
1821
+ * `inflowSteps` is filled by `noteProjectionCalibration`, which runs on the context pass.
1822
+ * By then the message-end path has already advanced `lastProviderMeasurement` to the
1823
+ * count being calibrated, so the difference is a measurement against itself and the step
1824
+ * is zero. That window has therefore been empty in every session ever run, which means
1825
+ * the transmission fence margin and the commit depth floor, both of which read it, have
1826
+ * been running on their floor shares alone. Reviving it moves both, and both are
1827
+ * calibrated against reps recorded while it was empty, so that is its own build with its
1828
+ * own gates rather than a rider on this one.
1829
+ *
1830
+ * This window is filled where the previous count is still readable, and nothing but the
1831
+ * wall reads it. When a count reaches only the context pass the window stays empty and
1832
+ * the wall degrades to the condition it replaced, which is the safe direction.
1833
+ */
1834
+ const WALL_INFLOW_WINDOW = 8;
1835
+ const noteWallInflow = (measurement: ProviderContextMeasurement): void => {
1836
+ const previous = measurements.lastProviderMeasurement;
1837
+ if (!previous || !Number.isFinite(measurement.tokens) || !Number.isFinite(previous.tokens)) return;
1838
+ const step = measurement.tokens - previous.tokens;
1839
+ // Only GROWTH. A commit shrinks the window; that is what the wall has to survive, not
1840
+ // a size the next request can be predicted from.
1841
+ if (step <= 0) return;
1842
+ measurements.wallInflowSteps.push(step);
1843
+ if (measurements.wallInflowSteps.length > WALL_INFLOW_WINDOW) measurements.wallInflowSteps.shift();
1844
+ };
1845
+
1846
+ const expectedWallInflowTokens = (): number => measurements.wallInflowSteps.length
1847
+ ? Math.max(...measurements.wallInflowSteps)
1848
+ : 0;
1849
+
1440
1850
  /**
1441
1851
  * The safety margin.
1442
1852
  *
@@ -1478,7 +1888,18 @@ export function registerActiveContext(pi: any, options: {
1478
1888
  * transmission, which is the whole point of having a fence.
1479
1889
  */
1480
1890
  const projectionExceedsBudget = (projected: unknown[], ctx: any): {
1891
+ /** Occupancy: what the provider counted, plus what was appended after it. */
1481
1892
  tokens: number;
1893
+ /**
1894
+ * This projection's OWN size, every byte of it at one rate. Occupancy answers "how
1895
+ * full is the window"; a before-and-after comparison between two projections answers
1896
+ * "did this pass make the request smaller", and those are different questions. A
1897
+ * reduction is measured in size space on both sides, because the projection the pass
1898
+ * rebuilds is a rewrite the anchor does not describe, and subtracting an anchored
1899
+ * reading from an unanchored one reports a change that no folding caused.
1900
+ */
1901
+ sizeTokens: number;
1902
+ basis: ProjectionReadingBasis;
1482
1903
  budgetTokens: number;
1483
1904
  marginTokens: number;
1484
1905
  /** Past the wire: this request must not be transmitted at all. */
@@ -1490,12 +1911,16 @@ export function registerActiveContext(pi: any, options: {
1490
1911
  const budgetTokens = Number.isFinite(capacity.budgetTokens) && capacity.budgetTokens > 0
1491
1912
  ? capacity.budgetTokens
1492
1913
  : Number.POSITIVE_INFINITY;
1493
- const tokens = projectedTokenEstimate(projected);
1914
+ const reading = projectedTokenReading(projected);
1915
+ const tokens = reading.tokens;
1916
+ const sizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
1494
1917
  const marginTokens = Number.isFinite(budgetTokens)
1495
1918
  ? projectionMarginTokens(tokens, capacity.window)
1496
1919
  : 0;
1497
1920
  return {
1498
1921
  tokens,
1922
+ sizeTokens,
1923
+ basis: reading.basis,
1499
1924
  budgetTokens,
1500
1925
  marginTokens,
1501
1926
  over: tokens > budgetTokens,
@@ -1560,6 +1985,24 @@ export function registerActiveContext(pi: any, options: {
1560
1985
  // is the one built after this one. A provider rejection outranks our own estimate:
1561
1986
  // it is ground truth that the last request did not fit, whatever we measured.
1562
1987
  if (!measured.crowded && !rejected) return { projected, aborted: false };
1988
+ // Crowded is a MARGIN PREDICTION, not an overflow. While a last-call round is
1989
+ // genuinely OPEN, exposure rendered this pass or the agent's response still
1990
+ // outstanding, the prediction waits for it: this request still fits and the
1991
+ // round ends on the very next context pass. Once the round has elapsed the
1992
+ // margin lane acts freely, because a stale exposure parked behind the reopen
1993
+ // latch must never muzzle the one reducer that can still act (the 20k-window
1994
+ // probe climbed from crowded to the wire in exactly that state). Genuine
1995
+ // untransmissibility, an over-budget projection or a provider rejection, never
1996
+ // waits: a request whose projection exceeds the provider input budget is rejected
1997
+ // outright, so recovery must produce a window that fits. Economy does not outrank
1998
+ // the one round.
1999
+ const lastCall = persistence.state?.lastCall;
2000
+ const delivery = curation.lastCallDelivery;
2001
+ const roundOpen = Boolean(lastCall) && (!delivery || delivery.exposure !== lastCall.exposure ||
2002
+ markOrdinal(snapshot) <= delivery.ordinal);
2003
+ if (!measured.over && !rejected && roundOpen) {
2004
+ return { projected, aborted: false };
2005
+ }
1563
2006
  // Why this fired, recorded from the measurement that TRIGGERED it rather than the
1564
2007
  // one taken afterwards, which by then describes a projection that fits.
1565
2008
  const trigger = measured;
@@ -1574,7 +2017,6 @@ export function registerActiveContext(pi: any, options: {
1574
2017
  let action: Record<string, unknown> | null = null;
1575
2018
  try {
1576
2019
  action = await attemptAutomaticRung(snapshot, 1, ctx, "projection-budget", {
1577
- waiveToolCadence: true,
1578
2020
  waiverRatio: 1,
1579
2021
  });
1580
2022
  } catch (error) {
@@ -1589,8 +2031,11 @@ export function registerActiveContext(pi: any, options: {
1589
2031
  }
1590
2032
  if (reducedAtLeastOnce) {
1591
2033
  ladder.overBudgetReduction = {
1592
- estimatedTokensBefore: projectedTokenEstimate(projected),
1593
- estimatedTokensAfter: measured.tokens,
2034
+ // Size space on both sides: what this pass CHANGED, not how full the window is.
2035
+ estimatedTokensBefore: trigger.sizeTokens,
2036
+ estimatedTokensAfter: measured.sizeTokens,
2037
+ occupancyTokensAfter: measured.tokens,
2038
+ occupancyBasis: measured.basis,
1594
2039
  budgetTokens: trigger.budgetTokens,
1595
2040
  marginTokens: trigger.marginTokens,
1596
2041
  estimatorErrorShare: estimatorErrorShare(),
@@ -1603,39 +2048,87 @@ export function registerActiveContext(pi: any, options: {
1603
2048
  };
1604
2049
  }
1605
2050
  if (rejected) {
2051
+ const overflowBefore = trigger.sizeTokens;
2052
+ // RECOVERY IS A CHANGE, NOT AN OPINION.
2053
+ //
2054
+ // This read `!measured.over`: our own estimator answering the question the
2055
+ // provider had just answered differently. A rejection is ground truth that the
2056
+ // estimate was wrong, so the estimate cannot also be the evidence that the retry
2057
+ // will work. And a rejection mutates nothing durable -- the rolled-back branch
2058
+ // rebuilds the same projection -- so a pass that changed nothing hands the
2059
+ // provider a byte-identical request and calls it a rescue. Measured 2026-08-10
2060
+ // (luna-20260810 pifold rep 2): two rejections at stage 25, not one byte folded
2061
+ // either time, both recorded recovered true, and the run died anyway.
2062
+ //
2063
+ // The baseline is the request that was actually REJECTED, which is the last
2064
+ // projection this process built: it is written at the end of every pass that
2065
+ // builds one, and this lane runs before the current pass overwrites it. Measuring
2066
+ // against the projection handed to this function instead would credit only the
2067
+ // recovery loop's own folds and miss the ordinary commit that already ran earlier
2068
+ // in the same pass, which is the usual way a retried request gets smaller.
2069
+ //
2070
+ // Size space on both sides. Occupancy is anchored to the provider's count for the
2071
+ // projection it measured, and the projection this lane rebuilds is a rewrite that
2072
+ // anchor does not describe, so a rejected-minus-retried subtraction across the two
2073
+ // bases reports a change no folding caused.
2074
+ const rejectedTokens = measurements.lastProjectedSizeTokens;
2075
+ const freedTokens = typeof rejectedTokens === "number"
2076
+ ? Math.max(0, rejectedTokens - measured.sizeTokens)
2077
+ : 0;
2078
+ // Smaller than what the provider refused, AND inside the budget. Either half alone
2079
+ // is a claim this runtime has already been wrong about once.
2080
+ const recovered = freedTokens > 0 && !measured.over;
1606
2081
  curation.lastRecovery = {
1607
2082
  status: curation.pendingRejection?.status ?? null,
1608
2083
  attempts,
1609
- estimatedTokensAfter: measured.tokens,
2084
+ estimatedTokensAfter: measured.sizeTokens,
1610
2085
  budgetTokens: measured.budgetTokens,
1611
- recovered: !measured.over,
2086
+ recovered,
1612
2087
  };
1613
2088
  emit("context.recovery", {
1614
2089
  provider_status: curation.pendingRejection?.status ?? null,
1615
2090
  attempts,
1616
2091
  max_attempts: OVERFLOW_RECOVERY_MAX_ATTEMPTS,
1617
- tokens_before: projectedTokenEstimate(projected),
1618
- tokens_after: measured.tokens,
2092
+ tokens_before: overflowBefore,
2093
+ tokens_after: measured.sizeTokens,
2094
+ occupancy_tokens_after: measured.tokens,
2095
+ occupancy_basis: measured.basis,
1619
2096
  budget_tokens: measured.budgetTokens,
1620
2097
  margin_tokens: measured.marginTokens,
1621
- recovered: !measured.over,
2098
+ recovered,
2099
+ // What the verdict rests on, so a false claim cannot be read back as a true one:
2100
+ // the size of the request the provider refused, and how much smaller this one is.
2101
+ rejected_tokens: typeof rejectedTokens === "number" ? rejectedTokens : null,
2102
+ freed_tokens: freedTokens,
2103
+ // Whether the recovery loop itself folded, as distinct from the pass as a whole.
2104
+ loop_reduced: reducedAtLeastOnce,
2105
+ // The join. `context.rollback` records what left the branch; this records what
2106
+ // the retried pass folded to make the shorter window fit, and the two are one
2107
+ // episode. Null when a rejection was recorded without a tree rollback.
2108
+ rollback_seq: typeof rollback.last?.seq === "number" ? rollback.last.seq : null,
1622
2109
  });
1623
- const overflowBefore = projectedTokenEstimate(projected);
2110
+ const unchangedNote = "The provider rejected the last request and this pass made it no smaller, so the " +
2111
+ `rebuilt request is ${measured.tokens} estimated tokens against a ${measured.budgetTokens}-token ` +
2112
+ "serving budget. Our own estimate says it fits; the provider already said otherwise about a request " +
2113
+ "this size, so this is not a recovery. Fold or protect deliberately, or the next request meets the " +
2114
+ "same rejection.";
1624
2115
  deliverReceipt(contextReceipt({
1625
2116
  kind: "overflow-recovery",
1626
2117
  ordinal: markOrdinal(snapshot),
1627
2118
  trigger: `provider-rejection:${curation.pendingRejection?.status ?? "unknown"}`,
1628
- freedTokens: Math.max(0, overflowBefore - measured.tokens),
1629
- occupancyBefore: overflowBefore,
1630
- occupancyAfter: measured.tokens,
1631
- recovered: true,
2119
+ freedTokens,
2120
+ occupancyBefore: typeof rejectedTokens === "number" ? rejectedTokens : overflowBefore,
2121
+ occupancyAfter: measured.sizeTokens,
2122
+ recovered,
1632
2123
  note: measured.over
1633
2124
  ? `The rebuilt request is still ${measured.tokens} tokens against a ${measured.budgetTokens}-token ` +
1634
2125
  "serving budget, so the run stops here rather than sending a request the provider will reject again."
1635
- : "A rollback was required: the provider rejected the last request, which overfilled the serving " +
1636
- `budget at ${overflowBefore} estimated tokens against ${measured.budgetTokens}. ` +
1637
- `${attempts} reduction(s) landed it at ${measured.tokens} tokens. Nothing durable was written ` +
1638
- "for it, and the request was rebuilt inside the budget rather than dropped.",
2126
+ : recovered
2127
+ ? "A rollback was required: the provider rejected the last request, which overfilled the serving " +
2128
+ `budget at ${rejectedTokens} estimated tokens against ${measured.budgetTokens}. This pass ` +
2129
+ `landed it at ${measured.sizeTokens} tokens. Nothing durable was written for it, and the request ` +
2130
+ "was rebuilt inside the budget rather than dropped."
2131
+ : unchangedNote,
1639
2132
  }));
1640
2133
  curation.pendingRejection = null;
1641
2134
  }
@@ -1675,7 +2168,7 @@ export function registerActiveContext(pi: any, options: {
1675
2168
  snapshot,
1676
2169
  state: capturedState,
1677
2170
  generation: capturedGeneration,
1678
- summarize: ladder.failedPreparations.has(id) ? undefined : options.summarizeContextSpan,
2171
+ summarize: ladder.failedPreparations.has(id) ? undefined : observedSummarize,
1679
2172
  onSummarizerFailure: (error) => {
1680
2173
  ladder.lastPreparationError = error instanceof Error ? error.message : String(error);
1681
2174
  ladder.failedPreparations.add(id);
@@ -1745,6 +2238,96 @@ export function registerActiveContext(pi: any, options: {
1745
2238
  return projected;
1746
2239
  };
1747
2240
 
2241
+ /**
2242
+ * The rider rides the SAME admission as the receipt block: it renders from the
2243
+ * persisted literal bytes, lands once per freeze cycle, and the freeze closes over
2244
+ * it, so it can never diverge a prefix on its own. It is never regenerated: the
2245
+ * bytes the epoch persisted are the bytes every re-render carries.
2246
+ */
2247
+ const appendRider = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
2248
+ const rider = persistence.state?.rider;
2249
+ if (!rider) return projected;
2250
+ if (!carrierAdmitted("rider")) return projected;
2251
+ projected.push({
2252
+ role: "custom",
2253
+ customType: riderProjectionType,
2254
+ content: rider.text,
2255
+ display: false,
2256
+ details: {
2257
+ source: activeContextSource(entryTypePrefix),
2258
+ ephemeral: true,
2259
+ riderEpoch: rider.epoch,
2260
+ },
2261
+ timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
2262
+ ? ownValue(snapshot.messages.at(-1), "timestamp")
2263
+ : 0,
2264
+ });
2265
+ return projected;
2266
+ };
2267
+
2268
+ /**
2269
+ * Threshold notices are APPEND-ONCE: each delivered notice is its own carrier with
2270
+ * its own key, so a notice that fires mid-freeze lands at the tail as a pure append,
2271
+ * the freeze closes over it, and it then persists in the window the way a tool
2272
+ * result does. A freeze break re-renders every retained notice from its persisted
2273
+ * literal bytes inside the rewrite that broke the freeze.
2274
+ */
2275
+ const appendNotices = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
2276
+ const notices = persistence.state?.notices;
2277
+ if (!notices?.ring.length) return projected;
2278
+ for (const notice of notices.ring) {
2279
+ if (!carrierAdmitted(`notice-${notice.share}-${notice.ordinal}`)) continue;
2280
+ projected.push({
2281
+ role: "custom",
2282
+ customType: noticeProjectionType,
2283
+ content: notice.text,
2284
+ display: false,
2285
+ details: {
2286
+ source: activeContextSource(entryTypePrefix),
2287
+ ephemeral: true,
2288
+ share: notice.share,
2289
+ noticeOrdinal: notice.ordinal,
2290
+ },
2291
+ timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
2292
+ ? ownValue(snapshot.messages.at(-1), "timestamp")
2293
+ : 0,
2294
+ });
2295
+ }
2296
+ return projected;
2297
+ };
2298
+
2299
+ /**
2300
+ * The pre-commit last-call rides the commit BOUNDARY: it is armed when the band-top
2301
+ * trigger fires, appended at the tail (a pure append) for exactly one gated round,
2302
+ * and the commit that consumes it clears the state, so the carrier's disappearance
2303
+ * rides the rewrite that commit already pays for. Rendering records the delivery,
2304
+ * which is what the one-round clock is measured from.
2305
+ */
2306
+ const appendLastCall = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
2307
+ const lastCall = persistence.state?.lastCall;
2308
+ if (!lastCall) return projected;
2309
+ if (carrierAdmitted(`lastcall-${lastCall.exposure}`)) {
2310
+ projected.push({
2311
+ role: "custom",
2312
+ customType: lastCallProjectionType,
2313
+ content: lastCall.text,
2314
+ display: false,
2315
+ details: {
2316
+ source: activeContextSource(entryTypePrefix),
2317
+ ephemeral: true,
2318
+ exposure: lastCall.exposure,
2319
+ },
2320
+ timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
2321
+ ? ownValue(snapshot.messages.at(-1), "timestamp")
2322
+ : 0,
2323
+ });
2324
+ }
2325
+ if (curation.lastCallDelivery?.exposure !== lastCall.exposure) {
2326
+ curation.lastCallDelivery = { exposure: lastCall.exposure, ordinal: markOrdinal(snapshot) };
2327
+ }
2328
+ return projected;
2329
+ };
2330
+
1748
2331
  /**
1749
2332
  * Whatever this pass ended up sending becomes the frozen surface for the next one, in
1750
2333
  * full. Nothing rides after the frozen region.
@@ -1779,7 +2362,8 @@ export function registerActiveContext(pi: any, options: {
1779
2362
  const customType = ownValue(message, "customType");
1780
2363
  return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
1781
2364
  customType !== surfacingProjectionType && customType !== receiptProjectionType &&
1782
- customType !== curationProjectionType;
2365
+ customType !== riderProjectionType && customType !== curationProjectionType &&
2366
+ customType !== lastCallProjectionType && customType !== noticeProjectionType;
1783
2367
  });
1784
2368
  // The freeze test, stated as bytes rather than as intent: if the rebuilt body still
1785
2369
  // OPENS with the frozen body verbatim, nothing structural moved and the previous
@@ -1805,6 +2389,9 @@ export function registerActiveContext(pi: any, options: {
1805
2389
  // bought nothing anyway: voluntary fold share was 0.00, and the three runs built
1806
2390
  // specifically to invite curation produced 0, 1 and 0 voluntary folds.
1807
2391
  appendReceipts(projected, snapshot);
2392
+ appendRider(projected, snapshot);
2393
+ appendNotices(projected, snapshot);
2394
+ appendLastCall(projected, snapshot);
1808
2395
  return holdFrozen(projected);
1809
2396
  };
1810
2397
  /**
@@ -1830,14 +2417,20 @@ export function registerActiveContext(pi: any, options: {
1830
2417
  const charsPerToken = projectionCharsPerToken();
1831
2418
  const causes = instrumentation.sinceHandoff.filter((event) =>
1832
2419
  PREFIX_MUTATING_KINDS.has(event.kind));
2420
+ // The reading this projection was judged by, with the pieces it was built from, so
2421
+ // the anchored series is readable from the stream instead of reconstructed.
2422
+ const reading = projectedTokenReading(projected);
1833
2423
  emit("context.projection", {
1834
2424
  change: comparison.change,
1835
2425
  previous_count: comparison.previousCount,
1836
2426
  next_count: comparison.nextCount,
1837
2427
  appended_count: comparison.appendedCount,
1838
2428
  first_divergent_index: comparison.firstDivergentIndex,
1839
- chars: bytes(projected),
1840
- estimated_tokens: projectedTokenEstimate(projected),
2429
+ chars: reading.chars,
2430
+ estimated_tokens: reading.tokens,
2431
+ estimate_basis: reading.basis,
2432
+ anchor_tokens: reading.anchorTokens,
2433
+ delta_chars: reading.deltaChars,
1841
2434
  chars_per_token: charsPerToken,
1842
2435
  });
1843
2436
  // Emission only. Which of these is a provider-side miss and which is a rewrite we
@@ -1870,15 +2463,354 @@ export function registerActiveContext(pi: any, options: {
1870
2463
  instrumentation.mutationsSinceHandoff = 0;
1871
2464
  };
1872
2465
 
2466
+ /**
2467
+ * Enqueue the folds this commit created with a deterministic brief.
2468
+ *
2469
+ * The request is built HERE, against the snapshot and the pre-commit state, because
2470
+ * this is the last moment the fold's source is still exact active evidence: after the
2471
+ * commit its span is a placeholder and the orientation either side of it is gone.
2472
+ * These are the bytes `prepareFold` already hashed into the fold's identity, so the
2473
+ * generator briefs the span that was folded and nothing else.
2474
+ *
2475
+ * A PARENT is queued too, and reads its children at depth one rather than through their
2476
+ * briefs: `consolidationSourceText` opens each child and leaves the grandchildren folded,
2477
+ * so the group is briefed from material instead of from a summary of summaries. The one
2478
+ * thing it must not race is a child's own upgrade, so a parent whose children are still
2479
+ * in this lane is deferred to the next boundary instead of reading briefs about to be
2480
+ * replaced. Unlike a leaf, a parent's source keeps: its children are folds, and folds
2481
+ * stay resolvable after the commit that nested them.
2482
+ */
2483
+ const queueBriefUpgrades = (
2484
+ snapshot: ActiveContextSnapshot,
2485
+ stateBeforeCommit: ActiveContextState,
2486
+ foldIds: readonly string[],
2487
+ ): void => {
2488
+ if (!observedSummarize || !persistence.state) return;
2489
+ const queued = (id: string): boolean =>
2490
+ upgrades.queue.some((entry) => entry.members.some((member) => member.foldId === id));
2491
+ const pending = (id: string): boolean => upgrades.running.has(id) || queued(id) ||
2492
+ upgrades.ready.some((entry) => entry.foldId === id) ||
2493
+ upgrades.deferred.has(id);
2494
+ // Gathered before any packing, because a batch is a property of the whole commit and
2495
+ // not of the fold that happens to be read first.
2496
+ const gathered: Array<{
2497
+ foldId: string;
2498
+ sourceSha256: string;
2499
+ refs: EvidenceRef[];
2500
+ span: Record<string, unknown>;
2501
+ chars: number;
2502
+ parent: boolean;
2503
+ }> = [];
2504
+ // Parents that waited are considered first and at every boundary, so the wait ends as
2505
+ // soon as the children settle rather than whenever the ladder happens to build another.
2506
+ for (const foldId of [...upgrades.deferred, ...foldIds]) {
2507
+ const full = upgrades.queue.length >= MAX_BRIEF_UPGRADE_QUEUE;
2508
+ if (upgrades.failed.has(foldId) || upgrades.running.has(foldId) || queued(foldId) ||
2509
+ upgrades.ready.some((entry) => entry.foldId === foldId)) continue;
2510
+ const fold = persistence.state.folds.find((item) => item.id === foldId);
2511
+ // A supplied brief is agent judgment and outranks the generator; a model brief is
2512
+ // already what this lane exists to produce. A fold that is no longer there is no
2513
+ // longer owed anything, so a rolled-back parent leaves the wait rather than sitting
2514
+ // in it.
2515
+ if (!fold || foldProvenance(fold, persistence.state).kind !== "deterministic") {
2516
+ upgrades.deferred.delete(foldId);
2517
+ continue;
2518
+ }
2519
+ const parent = fold.parts.some((part) => part.kind === "fold");
2520
+ // A parent waits rather than being shed: its children are folds, so its source is
2521
+ // still there whenever the lane has room, and a group that loses its turn at a busy
2522
+ // boundary would otherwise keep the index its children could not write. A leaf is the
2523
+ // other way round. Its source is exact active evidence at this commit and a
2524
+ // placeholder after it, so a full queue drops it here and the receipt says so.
2525
+ if (parent && (full || childFoldIds(fold).some(pending))) {
2526
+ upgrades.deferred.add(foldId);
2527
+ continue;
2528
+ }
2529
+ if (full) continue;
2530
+ upgrades.deferred.delete(foldId);
2531
+ const refs = parent
2532
+ ? flattenFoldRefs(fold, persistence.state)
2533
+ : fold.parts.every((part) => part.kind === "raw")
2534
+ ? fold.parts.map((part) => (part as { kind: "raw"; ref: EvidenceRef }).ref)
2535
+ : null;
2536
+ if (!refs) continue;
2537
+ try {
2538
+ // The parent reads the state that holds its children and their upgraded briefs;
2539
+ // a leaf reads the state where its own span was still raw active evidence.
2540
+ const sourceText = parent
2541
+ ? consolidationSourceText(snapshot, persistence.state, fold.parts)
2542
+ : encodedFoldSource(snapshot, stateBeforeCommit, fold.parts, fold.kind);
2543
+ const chars = bytes(sourceText);
2544
+ if (chars > snapshot.policy.maxSourceChars) continue;
2545
+ gathered.push({
2546
+ foldId,
2547
+ sourceSha256: fold.sourceSha256,
2548
+ refs,
2549
+ chars,
2550
+ parent,
2551
+ span: {
2552
+ candidateId: fold.id,
2553
+ sourceRefs: clone(refs),
2554
+ sourceText,
2555
+ sourceSha256: sha256Text(sourceText),
2556
+ // What the PAYLOAD holds, which is one entry per part: a consolidation can
2557
+ // carry absorbed raw evidence beside its folded children, and the coverage
2558
+ // rule counts the things the model is actually shown. Counting only the fold
2559
+ // children told it to write fewer clauses than the payload has subjects.
2560
+ children: parent ? fold.parts.length : 0,
2561
+ },
2562
+ });
2563
+ } catch (error) {
2564
+ // Evidence drift between the commit and this read leaves the deterministic brief
2565
+ // standing, and the next commit record reports the attempt rather than hiding it.
2566
+ upgrades.failed.add(foldId);
2567
+ upgrades.failures += 1;
2568
+ upgrades.lastError = boundReceiptText(
2569
+ error instanceof Error ? error.message : String(error), 240, "brief upgrade",
2570
+ );
2571
+ }
2572
+ }
2573
+ // Pack the commit's spans into as few calls as the source budget allows. Order is the
2574
+ // order they were gathered, which is span order within the commit, so the numbered
2575
+ // spans a batch shows the generator run the way the session did and one really is the
2576
+ // orientation for the next.
2577
+ for (let at = 0; at < gathered.length;) {
2578
+ if (upgrades.queue.length >= MAX_BRIEF_UPGRADE_QUEUE) {
2579
+ // No room for another call. A parent goes back to waiting, because its children are
2580
+ // folds and its source will still be there whenever the lane frees up. A leaf
2581
+ // cannot wait: its source is exact active evidence at THIS commit and a placeholder
2582
+ // after it, so it keeps the deterministic brief it committed with.
2583
+ for (const item of gathered.slice(at)) {
2584
+ if (item.parent) upgrades.deferred.add(item.foldId);
2585
+ }
2586
+ break;
2587
+ }
2588
+ const members: typeof gathered = [];
2589
+ let chars = 0;
2590
+ while (at < gathered.length && members.length < MAX_BRIEF_BATCH_SPANS &&
2591
+ (!members.length || chars + gathered[at].chars <= snapshot.policy.maxSourceChars)) {
2592
+ chars += gathered[at].chars;
2593
+ members.push(gathered[at]);
2594
+ at += 1;
2595
+ }
2596
+ // Orientation is the run's, not each span's: where this batch of consecutive spans
2597
+ // sits in the wider conversation. Taken from the ends rather than from the union,
2598
+ // because a commit can fold spans with protected material still raw between them and
2599
+ // the union of those refs is not one contiguous range.
2600
+ const before = boundedOrientation(snapshot, members[0].refs);
2601
+ const after = boundedOrientation(snapshot, members[members.length - 1].refs);
2602
+ upgrades.queue.push({
2603
+ members: members.map((item) => ({ foldId: item.foldId, sourceSha256: item.sourceSha256 })),
2604
+ request: {
2605
+ spans: members.map((item) => item.span),
2606
+ beforeRefs: clone(before.beforeRefs),
2607
+ beforeText: before.beforeText,
2608
+ beforeSha256: sha256Text(before.beforeText),
2609
+ afterRefs: clone(after.afterRefs),
2610
+ afterText: after.afterText,
2611
+ afterSha256: sha256Text(after.afterText),
2612
+ maxBriefChars: snapshot.policy.maxBriefChars,
2613
+ // Read by the observed wrapper and by nothing else, so a queue that backs up is
2614
+ // visible as waiting rather than as a slow generator. The summarizer contract is
2615
+ // on the RESULT, so an extra request field reaches it harmlessly.
2616
+ queuedAtMs: Date.now(),
2617
+ },
2618
+ });
2619
+ }
2620
+ };
2621
+
2622
+ /**
2623
+ * One generator call, between boundaries, cancel-safe. It writes nothing durable: a
2624
+ * finished brief waits in `ready` for a commit to carry it, and an aborted or failed
2625
+ * call leaves the fold exactly as it committed.
2626
+ *
2627
+ * Returns whether a call was started, which is what lets the drain below stop.
2628
+ */
2629
+ const startBriefUpgrade = (snapshot: ActiveContextSnapshot, ctx: any): boolean => {
2630
+ const summarize = observedSummarize;
2631
+ // In CALLS, not folds. `running` is keyed by every fold a call briefs so the queue can
2632
+ // ask whether one is already being written, so its size is a fold count and reading it
2633
+ // as the in-flight bound would stop the lane after a single batch.
2634
+ const inFlight = new Set(upgrades.running.values()).size;
2635
+ if (!summarize || lifecycle.shuttingDown || !upgrades.queue.length ||
2636
+ inFlight >= MAX_BRIEF_UPGRADES_IN_FLIGHT) return false;
2637
+ const entry = upgrades.queue.shift()!;
2638
+ const controller = new AbortController();
2639
+ const slot = { controller, promise: Promise.resolve() };
2640
+ // Keyed by EVERY fold the call briefs, so the queue's "already being briefed" check and
2641
+ // the in-flight bound both read one call as the one thing it is.
2642
+ for (const member of entry.members) upgrades.running.set(member.foldId, slot);
2643
+ const capturedSessionId = snapshot.sessionId;
2644
+ const capturedGeneration = lifecycle.generation;
2645
+ let timeout: ReturnType<typeof setTimeout> | undefined;
2646
+ slot.promise = (async () => {
2647
+ const timed = new Promise<never>((_resolve, reject) => {
2648
+ timeout = setTimeout(() => {
2649
+ controller.abort();
2650
+ reject(new Error(`Brief upgrade exceeded ${snapshot.policy.briefTimeoutMs}ms`));
2651
+ }, snapshot.policy.briefTimeoutMs);
2652
+ });
2653
+ // The same contract `prepareFold` holds a generated brief to, through the same
2654
+ // function: an upgrade with its own copy of the check would drift from the fold
2655
+ // path's, and the cure a missed criterion earns belongs to both or to neither.
2656
+ const generated = await Promise.race([
2657
+ generatedBriefs({
2658
+ summarize: (request, callCtx) => summarize({ ...request, signal: controller.signal }, callCtx),
2659
+ request: entry.request,
2660
+ spans: entry.request.spans as Array<Record<string, unknown>>,
2661
+ ctx,
2662
+ maxBriefChars: snapshot.policy.maxBriefChars,
2663
+ toolName: snapshot.toolName,
2664
+ }),
2665
+ timed,
2666
+ ]);
2667
+ if (controller.signal.aborted ||
2668
+ !sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration)) return;
2669
+ // Per span, not per call. A batch is one request and many answers, so one span that
2670
+ // could not be cured is one fold keeping its deterministic brief while its neighbours
2671
+ // take theirs; the whole call failing on it would be the coverage problem batching
2672
+ // exists to solve, wearing a different hat.
2673
+ entry.members.forEach((member, at) => {
2674
+ const written = generated.briefs[at];
2675
+ if (!written) {
2676
+ upgrades.failed.add(member.foldId);
2677
+ upgrades.failures += 1;
2678
+ upgrades.lastError = boundReceiptText(
2679
+ `span ${at + 1} of ${entry.members.length}: ${generated.complaints[0] ?? "no brief"}`,
2680
+ 240, "brief upgrade",
2681
+ );
2682
+ return;
2683
+ }
2684
+ upgrades.ready.push({
2685
+ foldId: member.foldId,
2686
+ sourceSha256: member.sourceSha256,
2687
+ brief: written.brief,
2688
+ provenance: written.provenance,
2689
+ });
2690
+ });
2691
+ upgrades.cures += generated.cured;
2692
+ })().catch((error) => {
2693
+ if (controller.signal.aborted) return;
2694
+ // LOUD, and once, for every fold the call carried. They keep their deterministic
2695
+ // briefs, the failure is counted onto the next commit record, and none of them is
2696
+ // ever sent again: a generator that fails on a span fails on it again, and a retry
2697
+ // loop would spend the session's provider calls proving that.
2698
+ for (const member of entry.members) upgrades.failed.add(member.foldId);
2699
+ upgrades.failures += 1;
2700
+ upgrades.lastError = boundReceiptText(
2701
+ error instanceof Error ? error.message : String(error), 240, "brief upgrade",
2702
+ );
2703
+ }).finally(() => {
2704
+ clearTimeout(timeout);
2705
+ for (const member of entry.members) {
2706
+ if (upgrades.running.get(member.foldId) === slot) upgrades.running.delete(member.foldId);
2707
+ }
2708
+ // A finished call starts the next one, so the drain rate is the generator's latency
2709
+ // rather than however often a ladder pass happens to look. Still between
2710
+ // boundaries: nothing here touches the projection, and the generation check is the
2711
+ // same fence `cancelBriefUpgrades` is called behind, so a session that moved on
2712
+ // cannot be restarted by a call that outlived it.
2713
+ if (!lifecycle.shuttingDown && lifecycle.generation === capturedGeneration) {
2714
+ resumeBriefUpgrades(snapshot, ctx);
2715
+ }
2716
+ });
2717
+ return true;
2718
+ };
2719
+
2720
+ /**
2721
+ * Fill the in-flight slots from the queue. The bound is a constant, not a pace: the
2722
+ * lane starts everything it is allowed to start the moment there is work, because a
2723
+ * fold whose brief is written late is a fold the session read deterministic in the
2724
+ * meantime.
2725
+ */
2726
+ const startBriefUpgrades = (snapshot: ActiveContextSnapshot, ctx: any): void => {
2727
+ while (startBriefUpgrade(snapshot, ctx)) { /* Bounded by the in-flight constant. */ }
2728
+ };
2729
+
2730
+ /**
2731
+ * Take up the waiting parents, then fill the slots.
2732
+ *
2733
+ * A deferred parent was waiting on the lane, so the lane finishing is the news, not the
2734
+ * next commit. Boundaries are where briefs APPLY; hanging the retry on them too would
2735
+ * leave a session that stops folding holding parents whose children were briefed long
2736
+ * ago, and the last group the ladder built is exactly the one a session ends beside.
2737
+ */
2738
+ const resumeBriefUpgrades = (snapshot: ActiveContextSnapshot, ctx: any): void => {
2739
+ if (upgrades.deferred.size && persistence.state) {
2740
+ queueBriefUpgrades(snapshot, persistence.state, []);
2741
+ }
2742
+ startBriefUpgrades(snapshot, ctx);
2743
+ };
2744
+
2745
+ const cancelBriefUpgrades = (): void => {
2746
+ for (const slot of upgrades.running.values()) slot.controller.abort();
2747
+ upgrades.running.clear();
2748
+ upgrades.queue = [];
2749
+ upgrades.ready = [];
2750
+ upgrades.deferred.clear();
2751
+ };
2752
+
2753
+ /**
2754
+ * Apply finished upgrades INSIDE the commit's own mutation.
2755
+ *
2756
+ * Called between `commitPendingMarks` and the byte accounting, so an upgraded brief is
2757
+ * part of what this commit is measured to have done rather than a rewrite nobody
2758
+ * charged for. Nothing else may call this: an upgrade that finishes between boundaries
2759
+ * waits, however long the wait is.
2760
+ *
2761
+ * Identity is revalidated the way `preparedFoldError` revalidates a prepared fold. A
2762
+ * fold that was expanded, reclaimed, re-briefed by the agent, or rebuilt over a
2763
+ * different span is a different object than the one the generator read, and its upgrade
2764
+ * is DROPPED rather than deferred.
2765
+ *
2766
+ * It lands in the brief OVERRIDE map, the same channel the agent's own `rebrief` uses,
2767
+ * and carries its generator beside the text. A fold record is content-addressed and
2768
+ * immutable: rewriting one in place reports a conflicting durable fold at the next
2769
+ * persist and suspends automatic management, which is precisely why the override map
2770
+ * exists.
2771
+ *
2772
+ * EVERY finished brief lands, and there is no per-boundary cap. A prefix cache is
2773
+ * invalidated from the FIRST byte that differs, so the commit's mutation point is the
2774
+ * position of the earliest fold it upgrades and every later upgrade in the same commit
2775
+ * sits inside a suffix that break already re-sent: the second and third upgrade on one
2776
+ * boundary cost nothing the first did not already cost. Holding one back does not save
2777
+ * that cost, it defers the same break to a later commit and pays it there on top of
2778
+ * whatever that commit does. What bounds the count is the work that can be outstanding,
2779
+ * MAX_BRIEF_UPGRADE_QUEUE waiting plus MAX_BRIEF_UPGRADES_IN_FLIGHT running, and the
2780
+ * boundary that clears them.
2781
+ */
2782
+ const applyBriefUpgrades = (): string[] => {
2783
+ if (!upgrades.ready.length || !persistence.state) return [];
2784
+ const applied: string[] = [];
2785
+ for (const entry of upgrades.ready) {
2786
+ const state = persistence.state;
2787
+ const fold = state.folds.find((item) => item.id === entry.foldId);
2788
+ if (!fold || fold.sourceSha256 !== entry.sourceSha256 ||
2789
+ foldProvenance(fold, state).kind !== "deterministic" ||
2790
+ state.expanded.includes(fold.id) || state.briefs?.[fold.id] !== undefined) continue;
2791
+ persistence.state = {
2792
+ ...state,
2793
+ briefs: {
2794
+ ...(state.briefs ?? {}),
2795
+ [fold.id]: { brief: entry.brief, provenance: clone(entry.provenance) },
2796
+ },
2797
+ };
2798
+ applied.push(fold.id);
2799
+ }
2800
+ upgrades.ready = [];
2801
+ return applied;
2802
+ };
2803
+
1873
2804
  const commitDeterministicCandidate = async (
1874
2805
  snapshot: ActiveContextSnapshot,
1875
2806
  candidate: FoldCandidate,
1876
2807
  brief: string,
1877
2808
  ): Promise<string> => {
2809
+ const stateBeforeCommit = persistence.state!;
1878
2810
  const preparedFold = await prepareFold({
1879
2811
  candidate,
1880
2812
  snapshot,
1881
- state: persistence.state!,
2813
+ state: stateBeforeCommit,
1882
2814
  generation: lifecycle.generation,
1883
2815
  brief,
1884
2816
  briefProvenance: "deterministic",
@@ -1886,9 +2818,10 @@ export function registerActiveContext(pi: any, options: {
1886
2818
  persistence.state = commitPreparedFold({
1887
2819
  prepared: preparedFold,
1888
2820
  snapshot,
1889
- state: persistence.state!,
2821
+ state: stateBeforeCommit,
1890
2822
  generation: lifecycle.generation,
1891
2823
  });
2824
+ queueBriefUpgrades(snapshot, stateBeforeCommit, [preparedFold.id]);
1892
2825
  return preparedFold.id;
1893
2826
  };
1894
2827
 
@@ -1913,7 +2846,7 @@ export function registerActiveContext(pi: any, options: {
1913
2846
  state: baseState,
1914
2847
  generation: generationAtStart,
1915
2848
  brief: input.brief,
1916
- summarize: options.summarizeContextSpan,
2849
+ summarize: observedSummarize,
1917
2850
  ctx: input.ctx,
1918
2851
  signal: input.signal,
1919
2852
  });
@@ -1952,36 +2885,77 @@ export function registerActiveContext(pi: any, options: {
1952
2885
  return Math.max(COMMIT_RECLAIM_FLOOR_SHARE, inflow > 0 ? inflow / window : 0);
1953
2886
  };
1954
2887
 
1955
- const atOrAboveBackstop = (snapshot: ActiveContextSnapshot, ratio: number | null): boolean =>
1956
- typeof ratio === "number" && Number.isFinite(ratio) && ratio >= snapshot.policy.refoldRatio;
2888
+ const atOrAboveBackstop = (snapshot: ActiveContextSnapshot, ratio: number | null): boolean => {
2889
+ const occupancy = budgetOccupancy(snapshot, ratio);
2890
+ return occupancy !== null && occupancy >= snapshot.policy.refoldRatio;
2891
+ };
1957
2892
 
1958
2893
  /**
1959
- * A snapshot whose FRESH tail is narrowed.
2894
+ * THE OUTPUT WALL.
2895
+ *
2896
+ * A declared serving budget has already netted out the reservation the answer needs,
2897
+ * so the runtime treats the whole budget as spendable and fences only when it is
2898
+ * spent. That fence is the input one, and the input is not what fails first. The host
2899
+ * derives the answer's ceiling per request as its DESCRIPTOR window less its own
2900
+ * estimate of the projection less a fixed safety margin, so filling the declared
2901
+ * budget to the brim leaves the answer exactly the reservation and nothing else.
1960
2902
  *
1961
- * Raising the top-up target does nothing once the eligible mass is exhausted, and at
1962
- * high occupancy that is exactly the state: measured 2026-08-06 (rep13), the big
1963
- * stale mass was already folded and what remained was the fresh tail and the newest
1964
- * payloads, so six commits of two to four folds carried the window from 340k to 370k
1965
- * and into a provider rejection. Depth therefore means reaching into the fresh tail,
1966
- * not asking harder for evidence that is not there.
2903
+ * Which is a fence that fires after the wire, the same defect the transmission fence
2904
+ * was given a forward look to fix in rep 13. The commit trigger never got one.
2905
+ * Measured 2026-08-12 (sol-20260812 rep 1): occupancy walked 218,713 -> 236,595 over
2906
+ * twelve ordinals against a 251,520 budget with no commit, and the request built after
2907
+ * the last measurement was served 16 output tokens and aborted. Every number was known
2908
+ * to the runtime; nothing was weighing the request that had not been built yet.
1967
2909
  *
1968
- * The newest complete turn stays protected and so does everything the current-turn
1969
- * guard covers: this narrows freshness, it does not abolish it.
2910
+ * So the trigger carries what the transmission fence already carries: one worst recent
2911
+ * inflow step. With no measured inflow this is exactly the old `used > budget`
2912
+ * condition, which is the state it degrades to on a session's first turns.
1970
2913
  */
1971
- const DEEPENED_FRESH_TURNS = 1;
1972
- const DEEPENED_FRESH_BYTES_SHARE = 0.25;
1973
-
1974
- const deepenedFreshnessSnapshot = (snapshot: ActiveContextSnapshot): ActiveContextSnapshot => {
1975
- const policy = {
1976
- ...snapshot.policy,
1977
- freshTurns: Math.min(snapshot.policy.freshTurns, DEEPENED_FRESH_TURNS),
1978
- freshBytes: Math.floor(snapshot.policy.freshBytes * DEEPENED_FRESH_BYTES_SHARE),
2914
+ const outputWallDue = (snapshot: ActiveContextSnapshot): boolean => {
2915
+ const capacity = servingCapacity(snapshot.contextWindow);
2916
+ if (capacity.usedTokens === null || !(capacity.budgetTokens > 0)) return false;
2917
+ // THE EARLY ZONE ONLY, so this is strictly additive.
2918
+ //
2919
+ // Past the budget the pre-existing overflow paths already own the emergency: the
2920
+ // commit-path exemption, the last-call gate and the fence all read `used > budget`
2921
+ // and have since before this existed. A second trigger in that territory changes the
2922
+ // cadence of sessions that were already being handled, and gate 107 measured what
2923
+ // that costs: a fixture parked at 88,000 against an 83,616 budget put four generator
2924
+ // calls in flight against a bound of two. So the wall speaks only where nothing spoke
2925
+ // before, in the band one inflow step wide below the budget.
2926
+ if (capacity.usedTokens > capacity.budgetTokens) return false;
2927
+ return capacity.usedTokens + expectedWallInflowTokens() > capacity.budgetTokens;
2928
+ };
2929
+
2930
+ /**
2931
+ * What the gated round did, measured against the arming snapshot. Pins and unpins
2932
+ * are read from the stream itself (context.protect records after the exposure); the
2933
+ * call and mark deltas are clamped at zero because contextCalls is process-local and
2934
+ * a reload inside a round must not report a negative response.
2935
+ */
2936
+ const lastCallAttribution = (
2937
+ lastCall: NonNullable<ActiveContextState["lastCall"]>,
2938
+ agentMarksNow: number,
2939
+ ): { responded: boolean; context_calls: number; marks_added: number; protects: number; unprotects: number } => {
2940
+ const contextCalls = Math.max(0, curation.contextCalls - lastCall.contextCalls);
2941
+ const protectRecords = instrumentation.ledger.events.filter((record) =>
2942
+ record.seq > lastCall.exposure && record.kind === "context.protect");
2943
+ const protects = protectRecords.filter((record) => record.protect === true).length;
2944
+ return {
2945
+ responded: contextCalls > 0,
2946
+ context_calls: contextCalls,
2947
+ marks_added: Math.max(0, agentMarksNow - lastCall.agentMarks),
2948
+ protects,
2949
+ unprotects: protectRecords.length - protects,
1979
2950
  };
1980
- const toolProtectedIndices = toolFreshIndices(snapshot.messages, snapshot.completeTurns, policy);
1981
- // Unmapped messages are protected in every snapshot: nothing can fold what the
1982
- // durable branch cannot name.
1983
- for (const item of snapshot.mapped) if (!item.ref) toolProtectedIndices.add(item.index);
1984
- return { ...snapshot, toolProtectedIndices };
2951
+ };
2952
+
2953
+ const clearLastCall = (): void => {
2954
+ if (!persistence.state?.lastCall) return;
2955
+ const next = { ...persistence.state };
2956
+ delete next.lastCall;
2957
+ persistence.state = next;
2958
+ curation.lastCallDelivery = null;
1985
2959
  };
1986
2960
 
1987
2961
  const runCommitEpoch = async (
@@ -1993,44 +2967,85 @@ export function registerActiveContext(pi: any, options: {
1993
2967
  * over-budget reduction forces the fence so nothing survivable is held back.
1994
2968
  */
1995
2969
  waiverRatio: number | null = measurements.latestRatio,
2970
+ /**
2971
+ * A user asked for this commit. It outranks the two ECONOMIC guards -- the reclaim
2972
+ * floor and the one-mutation-per-handoff budget -- because those exist to spend the
2973
+ * session's rewrites well and the user just said where to spend one. It outranks
2974
+ * nothing else: freshness, pins, exactness and the provider fence are unchanged.
2975
+ */
2976
+ userRequested = false,
1996
2977
  ): Promise<Record<string, unknown> | null> => {
1997
2978
  const ordinal = markOrdinal(snapshot);
1998
2979
  let state = persistence.state!;
1999
2980
  let peekAdded = 0;
2000
2981
  let topUpAdded = 0;
2001
- let deepened = false;
2002
- let deepenedMarks = 0;
2003
- let preDeepenShare: number | null = null;
2004
- // The snapshot the commit adjudicates against: the deepened one narrows freshness
2005
- // so the marks the deep top-up just made are not refused as fresh.
2006
- let commitSnapshot = snapshot;
2982
+ let consolidationAdded = 0;
2983
+ let closingAdded = 0;
2007
2984
  for (const mark of ephemeralPeekMarks({ snapshot, state, ordinal })) {
2008
2985
  const addition = addPendingMark(state, mark);
2009
2986
  if (addition.added) { state = addition.state; peekAdded += 1; }
2010
2987
  }
2988
+ // THE COUNT LAW RUNS AT EVERY COMMIT EPOCH, BEFORE ANYTHING ELSE PROPOSES.
2989
+ //
2990
+ // The number of parents the eligible-root count owes is arithmetic, not a judgment
2991
+ // about pressure, so it is not the top-up's to make and not the user commit's to
2992
+ // skip: at any point the count allows k groups, k parents form. Running first is
2993
+ // what makes the rest coherent -- the groups claim their children before the top-up
2994
+ // reads the claim set, so nothing proposes a span over a fold a parent is taking.
2995
+ for (const mark of consolidationMarks({ snapshot, state, ordinal })) {
2996
+ const addition = addPendingMark(state, mark);
2997
+ if (addition.added) { state = addition.state; consolidationAdded += 1; }
2998
+ }
2011
2999
  const guarded = currentTurnRefKeys(snapshot);
2012
- // At the fence everything unpinned is fair game, INCLUDING the top-up's reach: a
2013
- // session whose only foldable evidence is the open excursion has nothing else to
2014
- // propose, and excluding it there leaves the commit with nothing to apply at the
2015
- // exact moment reduction is the only thing keeping the request sendable.
2016
- const fenceLevel = typeof waiverRatio === "number" && Number.isFinite(waiverRatio) &&
2017
- waiverRatio >= hardFenceRatio(snapshot);
3000
+ // THE GUARD IS ADJUDICATED AT COMMIT, NEVER AT PROPOSAL.
3001
+ //
3002
+ // `guarded` is every tool result since the last terminal assistant message. When no
3003
+ // turn has ever closed, `currentTurnBoundary` is -1 and that set is the WHOLE
3004
+ // window. Handing it to `topUpMarks` as `excludeRefKeys` therefore proposed nothing
3005
+ // at all on that shape, so `guardWaiverCount` below counted zero guarded marks and
3006
+ // the waiver it exists to grant could never fire. Measured 2026-08-10
3007
+ // (luna-20260810 pifold rep 2): 274,173 tokens of unmarked stale spans, zero marks
3008
+ // proposed, zero commits, two provider rejections.
3009
+ //
3010
+ // So the top-up proposes freely and `commitPendingMarks({ guardCurrentTurn: true,
3011
+ // guardWaiver })` decides. That is the only place the guard has ever had a waiver,
3012
+ // and one adjudicator is why the starvation case is reachable at all.
3013
+ //
3014
+ // Wedge absorption keeps the exclusion (below): it grows a mark backward over a
3015
+ // gap without any waiver of its own, so the guard there has no second chance.
3016
+ // THE CLASS LAW IS UNCONDITIONAL.
3017
+ //
3018
+ // There was a fence-only snapshot here: at high occupancy it narrowed the fresh
3019
+ // tail to a quarter and extended the automatic reach over the middle, so a reduction
3020
+ // that had to make a rejected request sendable had mass to reach. That existed
3021
+ // because the runtime had exactly one answer to a provider rejection, folding
3022
+ // harder, and folding harder inside a positional region runs out of legal material.
3023
+ // The rollback lane is the answer now: an overflow rolls the leaf back past the
3024
+ // request that failed and the ordinary commit runs on the shorter window. So the
3025
+ // law holds in EVERY snapshot at every occupancy -- membership decides what folds,
3026
+ // the fresh tail never folds, pins are exempt -- and there is one set of rules to
3027
+ // reason about instead of two.
2018
3028
  // The thermostat. Firing at the trigger line and folding down to the target line is
2019
3029
  // what makes event SPACING structural rather than hoped for.
2020
3030
  const capacity = servingCapacity(snapshot.contextWindow);
2021
3031
  const usedTokens = capacity.usedTokens;
2022
3032
  const budgetTokens = capacity.budgetTokens;
2023
- // THE SAFETY EXEMPTION, NARROWED.
3033
+ // THE UNTRANSMISSIBILITY EXEMPTION, NARROWED.
2024
3034
  //
2025
- // The hard fence is a RATIO prediction. Standing near it is not the same as being
2026
- // unable to send, and treating the two alike handed the fence path a standing
2027
- // waiver from both economy guards. Measured 2026-08-07 (rep 17): fence-path
3035
+ // A request whose projection exceeds the provider input budget is rejected outright,
3036
+ // so recovery must produce a window that fits. The hard fence, though, is a RATIO
3037
+ // prediction. Standing near it is not the same as being unable to send, and treating
3038
+ // the two alike handed the fence path a standing waiver from both economy guards. Measured 2026-08-07 (rep 17): fence-path
2028
3039
  // window-pressure commits fired at eligible-freed shares as low as 0.018, under the
2029
3040
  // 0.02 floor the guided path honors, and re-fired inside a single ordinal. So only
2030
3041
  // two states outrank the economy: the overflow recovery lane, which runs because a
2031
3042
  // request already did not fit, and an occupancy genuinely past the serving budget,
2032
3043
  // where the next request aborts unless something moves. Everything else defers.
2033
- const overflowExempt = curation.recoveryAttempts > 0 ||
3044
+ // The output wall deliberately does NOT join this list. It decides WHEN the one
3045
+ // structural mutation of a handoff fires, not how many fire: exempting it here let a
3046
+ // window parked one inflow step under the budget rebuild its prefix on every pass,
3047
+ // which is the cost the mutation budget exists to refuse and what gate 58 caught.
3048
+ const overflowExempt = userRequested || curation.recoveryAttempts > 0 ||
2034
3049
  (usedTokens !== null && budgetTokens > 0 && usedTokens > budgetTokens);
2035
3050
  // ONE STRUCTURAL MUTATION PER HANDOFF.
2036
3051
  //
@@ -2040,8 +3055,9 @@ export function registerActiveContext(pi: any, options: {
2040
3055
  // still CROWDED (inside the fence margin, but transmissible), and committed again.
2041
3056
  // The second commit bought margin the next pass would have bought anyway and cost a
2042
3057
  // whole second prefix rewrite. So a second commit in the same handoff defers unless
2043
- // the request is genuinely untransmittable: the fence and the recovery lane are
2044
- // safety, and safety outranks the budget.
3058
+ // the request is genuinely untransmittable: a request whose projection exceeds the
3059
+ // provider input budget is rejected outright, so recovery must produce a window that
3060
+ // fits, and the fence and the recovery lane spend the budget to get one.
2045
3061
  if (instrumentation.mutationsSinceHandoff > 0 && !overflowExempt) {
2046
3062
  emit("context.commit", {
2047
3063
  trigger,
@@ -2053,11 +3069,16 @@ export function registerActiveContext(pi: any, options: {
2053
3069
  });
2054
3070
  return null;
2055
3071
  }
2056
- const hysteresisShare = usedTokens === null || snapshot.contextWindow <= 0
3072
+ // THE HYSTERESIS, AND THE WHOLE FREEING TARGET.
3073
+ //
3074
+ // How deep this event cuts is one subtraction on one denominator: what is used,
3075
+ // less where the thermostat wants to land, over the serving budget. The separate
3076
+ // 0.40 floor is gone -- it was a share of the WINDOW under a share of the BUDGET,
3077
+ // so it never once bound (the hysteresis share at these thresholds bottoms out at
3078
+ // 0.405 of window), and a floor that cannot fire is a number that only drifts.
3079
+ const freeingTarget = usedTokens === null || budgetTokens <= 0
2057
3080
  ? 0
2058
- : Math.max(0, (usedTokens - CURATION_TARGET_OCCUPANCY_SHARE * budgetTokens) /
2059
- snapshot.contextWindow);
2060
- const freeingTarget = Math.max(EPOCH_COMMIT_TARGET_WINDOW_SHARE, hysteresisShare);
3081
+ : Math.max(0, (usedTokens - thresholds.minTarget * budgetTokens) / budgetTokens);
2061
3082
  if (topUp) {
2062
3083
  // Measuring top-up progress against ELIGIBLE mass is what keeps the pressure
2063
3084
  // backstop working under a peek-heavy agent: pinned and retained marks are mass
@@ -2066,41 +3087,35 @@ export function registerActiveContext(pi: any, options: {
2066
3087
  snapshot,
2067
3088
  state,
2068
3089
  ordinal,
2069
- excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
2070
3090
  eligibleOnly: true,
2071
- // The fence tops up until there is nothing left to propose: a reduction that
2072
- // stops at the ordinary target can leave the request still untransmittable.
2073
- targetShare: fenceLevel ? 1 : freeingTarget,
3091
+ targetShare: freeingTarget,
2074
3092
  })) {
2075
3093
  const addition = addPendingMark(state, mark);
2076
3094
  if (addition.added) { state = addition.state; topUpAdded += 1; }
2077
3095
  }
2078
- // High-occupancy depth. Near the top of the window a commit that frees less than
2079
- // one turn of inflow does not reduce anything: it pays a full prefix rewrite,
2080
- // gives back less than the next stage adds, and the window ratchets UP through
2081
- // commit after commit. Measured 2026-08-06 (rep13): once the big stale mass was
2082
- // folded, six commits of two to four folds each carried the window from 340k to
2083
- // 370k and into a provider rejection, with the backstop firing the whole way.
2084
- // So a commit at or above the backstop that has not reached one inflow step
2085
- // reaches further, into everything eligible, instead of billing a rewrite for
2086
- // crumbs.
2087
- preDeepenShare = markAccounting(snapshot, state).eligibleFreedWindowShare;
2088
- // Deepen whenever the ordinary reach fell short of the thermostat's target too:
2089
- // an event that does not reach the lower line is an event that re-fires soon.
2090
- const shallow = preDeepenShare < Math.max(commitDepthFloorShare(snapshot), freeingTarget);
2091
- if (fenceLevel || (shallow && atOrAboveBackstop(snapshot, waiverRatio))) {
2092
- deepened = true;
2093
- commitSnapshot = deepenedFreshnessSnapshot(snapshot);
3096
+ // DEPTH, inside the zones.
3097
+ //
3098
+ // Near the top of the window a commit that frees less than one turn of inflow
3099
+ // does not reduce anything: it pays a full prefix rewrite, gives back less than
3100
+ // the next stage adds, and the window ratchets UP through commit after commit.
3101
+ // Measured 2026-08-06 (rep13): once the big stale mass was folded, six commits of
3102
+ // two to four folds each carried the window from 340k to 370k and into a provider
3103
+ // rejection, with the backstop firing the whole way. So a commit at or above the
3104
+ // backstop that has not reached one inflow step tops up against everything
3105
+ // ELIGIBLE instead of billing a rewrite for crumbs. Eligible is the operative
3106
+ // word: this reaches harder inside what the class law admits, never outside it.
3107
+ const reachedShare = markAccounting(snapshot, state).eligibleFreedBudgetShare;
3108
+ const shallow = reachedShare < Math.max(commitDepthFloorShare(snapshot), freeingTarget);
3109
+ if (shallow && atOrAboveBackstop(snapshot, waiverRatio)) {
2094
3110
  for (const mark of topUpMarks({
2095
- snapshot: commitSnapshot,
3111
+ snapshot,
2096
3112
  state,
2097
3113
  ordinal,
2098
- excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
2099
3114
  eligibleOnly: true,
2100
3115
  targetShare: 1,
2101
3116
  })) {
2102
3117
  const addition = addPendingMark(state, mark);
2103
- if (addition.added) { state = addition.state; topUpAdded += 1; deepenedMarks += 1; }
3118
+ if (addition.added) { state = addition.state; topUpAdded += 1; }
2104
3119
  }
2105
3120
  }
2106
3121
  }
@@ -2111,18 +3126,44 @@ export function registerActiveContext(pi: any, options: {
2111
3126
  snapshot,
2112
3127
  state,
2113
3128
  charsPerToken: projectionCharsPerToken(),
2114
- excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
3129
+ excludeRefKeys: guarded,
2115
3130
  });
2116
3131
  state = wedges.state;
2117
3132
  const accounting = markAccounting(snapshot, state);
2118
- if (!accounting.pending) return null;
3133
+ // A COMMIT PASS THAT FINDS NOTHING SAYS SO.
3134
+ //
3135
+ // This was a bare `return null`, and it is how the rep-2 starvation stayed invisible
3136
+ // for 25 stages: the last call announced 274,173 tokens of unmarked stale spans, the
3137
+ // pass that answered it proposed nothing, and the stream carried no record that a
3138
+ // commit had even been attempted. Every other deferral on this path already names
3139
+ // itself; this one is the same shape with the reason that matters, and it reports
3140
+ // the remainder beside the emptiness so the contradiction -- mass on the table, no
3141
+ // proposal -- is readable on one record instead of inferred across two.
3142
+ //
3143
+ // Stream record, not a `context.receipt`: receipts are window carriers, and a pass
3144
+ // that changed nothing must not add bytes to the projection to announce it.
3145
+ if (!accounting.pending) {
3146
+ const remainder = unmarkedRemainder(snapshot, state, projectionCharsPerToken());
3147
+ emit("context.commit", {
3148
+ trigger,
3149
+ deferred: true,
3150
+ reason: "nothing-proposable",
3151
+ applied_marks: 0,
3152
+ pending_marks: 0,
3153
+ unmarked_stale_spans: remainder.spans,
3154
+ unmarked_stale_tokens: remainder.tokens,
3155
+ window_tokens: snapshot.contextWindow,
3156
+ });
3157
+ return null;
3158
+ }
2119
3159
  // The reclaim floor. One structural mutation per model call is the budget, so a
2120
3160
  // commit that would free crumbs spends the whole budget on nothing: it defers and
2121
- // the marks accumulate. Safety outranks economics, so genuine overflow -- the
2122
- // recovery lane, or an occupancy already past the serving budget -- fires
3161
+ // the marks accumulate. A request whose projection exceeds the provider input budget
3162
+ // is rejected outright, so recovery must produce a window that fits: genuine overflow
3163
+ // -- the recovery lane, or an occupancy already past the serving budget -- fires
2123
3164
  // regardless of what it frees. Standing at the fence RATIO does not.
2124
3165
  if (!overflowExempt &&
2125
- accounting.eligibleFreedWindowShare < COMMIT_RECLAIM_FLOOR_SHARE) {
3166
+ accounting.eligibleFreedBudgetShare < COMMIT_RECLAIM_FLOOR_SHARE) {
2126
3167
  persistence.state = state;
2127
3168
  emit("context.commit", {
2128
3169
  trigger,
@@ -2131,7 +3172,7 @@ export function registerActiveContext(pi: any, options: {
2131
3172
  applied_marks: 0,
2132
3173
  pending_marks: accounting.pending,
2133
3174
  eligible_marks: accounting.eligibleMarks,
2134
- eligible_freed_share: accounting.eligibleFreedWindowShare,
3175
+ eligible_freed_share: accounting.eligibleFreedBudgetShare,
2135
3176
  reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
2136
3177
  window_tokens: snapshot.contextWindow,
2137
3178
  });
@@ -2140,51 +3181,101 @@ export function registerActiveContext(pi: any, options: {
2140
3181
  const bytesBefore = bytes(projectActiveContext(snapshot, state));
2141
3182
  // The guard protects an in-flight excursion, never at the cost of the session's
2142
3183
  // ability to send a request at all: above the pressure backstop the oldest guarded
2143
- // marks are released, and at the fence all of them are.
3184
+ // marks are released. The guard is a protection with a WAIVER, which is why it
3185
+ // survived the fence-snapshot deletion: one protection, one owner, one waiver.
2144
3186
  const guardWaiver = guardWaiverCount({
2145
3187
  snapshot,
2146
3188
  ratio: waiverRatio,
2147
3189
  guardedMarks: pendingMarks(state).filter((mark) =>
2148
3190
  markTouchesCurrentTurn(state, mark, guarded)).length,
2149
- // Against commitSnapshot, NOT snapshot. The starvation test decides whether the
2150
- // guard may be waived, so it has to ask the same question the commit will: does
2151
- // this epoch have work that does not touch the open turn? A DEEPENED epoch tops
2152
- // up marks that the un-deepened freshness policy still calls protected, so
2153
- // weighing them under the shallow snapshot reported zero applicable marks while
2154
- // the commit went on to apply two. The waiver then fired on a false starvation
2155
- // reading and released eight open-turn marks, six of which folded evidence the
2156
- // turn was still using. Two snapshots, two answers, and the one that decides the
2157
- // commit was not the one being asked.
3191
+ // The starvation test asks the same question the commit will, against the same
3192
+ // snapshot the commit adjudicates against: does this epoch have work that does
3193
+ // not touch the open turn? Two snapshots gave two answers, and the one that
3194
+ // decided the commit was not the one being asked. There is one snapshot now.
2158
3195
  otherApplicableMarks: pendingMarks(state).filter((mark) =>
2159
3196
  !markTouchesCurrentTurn(state, mark, guarded) &&
2160
- markEligibility(commitSnapshot, state, mark) === "eligible").length,
3197
+ markEligibility(snapshot, state, mark) === "eligible").length,
2161
3198
  });
2162
- const result = await commitPendingMarks({
2163
- snapshot: commitSnapshot,
3199
+ let result = await commitPendingMarks({
3200
+ snapshot,
2164
3201
  state,
2165
3202
  generation: lifecycle.generation,
2166
3203
  retainIneligible: true,
2167
3204
  guardCurrentTurn: true,
2168
3205
  guardWaiver,
2169
3206
  });
3207
+ // THE EPOCH CLOSES ITS OWN COUNT.
3208
+ //
3209
+ // The folds this commit just landed are eligible roots the moment they exist, and
3210
+ // the law is about the count, not about which pass noticed it. Leaving them for the
3211
+ // next epoch is what let the pile stand between commits: measured 2026-08-10 (PT-3),
3212
+ // a commit that landed 26 tool folds left 32 visible roots up until the next band
3213
+ // crossing four cycles later. So the epoch re-reads its own count and parents what
3214
+ // it just made, inside the rewrite it has already paid for. No waiver here: a parent
3215
+ // this round cannot apply is held by the ordinary guard and waits, exactly like any
3216
+ // other fold. It converges because a parent is excluded from the count it came from.
3217
+ for (let round = 0; round < EPOCH_CONSOLIDATION_ROUNDS; round += 1) {
3218
+ let closing = result.state;
3219
+ for (const mark of consolidationMarks({ snapshot, state: closing, ordinal })) {
3220
+ const addition = addPendingMark(closing, mark);
3221
+ if (addition.added) { closing = addition.state; closingAdded += 1; }
3222
+ }
3223
+ if (closing === result.state) break;
3224
+ const closed = await commitPendingMarks({
3225
+ snapshot,
3226
+ state: closing,
3227
+ generation: lifecycle.generation,
3228
+ retainIneligible: true,
3229
+ guardCurrentTurn: true,
3230
+ });
3231
+ if (!closed.applied.length) break;
3232
+ // A mark the guard RETAINED is still pending, so the closing round sees it again
3233
+ // and reports it again; only the terminal refusals accumulate, and the last round
3234
+ // is the truth about what is still held.
3235
+ result = {
3236
+ ...closed,
3237
+ applied: [...result.applied, ...closed.applied],
3238
+ refused: [...result.refused.filter((mark) => !mark.retained), ...closed.refused],
3239
+ waived: [...result.waived, ...closed.waived],
3240
+ };
3241
+ }
2170
3242
  persistence.state = result.state;
2171
- const bytesAfter = bytes(projectActiveContext(snapshot, result.state));
3243
+ // The upgrade wedge: model briefs written since the last boundary replace their
3244
+ // deterministic ones HERE, inside the mutation this commit already pays for, before
3245
+ // anything measures what the commit did.
3246
+ const upgradedFolds = applyBriefUpgrades();
3247
+ const upgradeFailures = upgrades.failures;
3248
+ const upgradeError = upgrades.lastError;
3249
+ const upgradeCures = upgrades.cures;
3250
+ upgrades.failures = 0;
3251
+ upgrades.cures = 0;
3252
+ upgrades.lastError = null;
3253
+ const bytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
2172
3254
  const freedBytes = Math.max(0, bytesBefore - bytesAfter);
3255
+ // The cost of the agent's pins, measured at the moment it matters: mass this
3256
+ // commit could not touch because protect holds it.
3257
+ const pinHeld = protectedStaleMass(snapshot, result.state);
2173
3258
  const commitEvent = emit("context.commit", {
2174
3259
  trigger,
2175
3260
  deferred: false,
2176
3261
  reason: null,
2177
- eligible_freed_share: accounting.eligibleFreedWindowShare,
3262
+ eligible_freed_share: accounting.eligibleFreedBudgetShare,
2178
3263
  reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
2179
3264
  // The two first-class dials: how much ONE event reclaims, and how long since the
2180
3265
  // last one. A trigger that frees too little re-fires immediately, and that shape
2181
3266
  // is only visible with both numbers on the same record.
2182
3267
  target_freed_share: freeingTarget,
2183
- hysteresis_target_share: hysteresisShare,
2184
- target_occupancy_share: CURATION_TARGET_OCCUPANCY_SHARE,
2185
- shortfall_share: Math.max(0, freeingTarget - accounting.eligibleFreedWindowShare),
3268
+ hysteresis_target_share: freeingTarget,
3269
+ target_occupancy_share: thresholds.minTarget,
3270
+ shortfall_share: Math.max(0, freeingTarget - accounting.eligibleFreedBudgetShare),
2186
3271
  occupancy_tokens_before: usedTokens,
2187
3272
  budget_tokens: budgetTokens,
3273
+ // The forward look, on the record beside the occupancy it was added to. A commit
3274
+ // that fired because one more turn would have spent the answer's reservation reads
3275
+ // differently from one that fired at the band top, and the difference is only
3276
+ // legible with the allowance printed.
3277
+ expected_inflow_tokens: expectedWallInflowTokens(),
3278
+ output_wall: outputWallDue(snapshot),
2188
3279
  requests_since_previous: instrumentation.requests - instrumentation.lastMutationRequest,
2189
3280
  inflow_tokens_since_previous: usedTokens === null || instrumentation.lastMutationTokens === null
2190
3281
  ? null
@@ -2200,25 +3291,119 @@ export function registerActiveContext(pi: any, options: {
2200
3291
  ladder_marks: accounting.ladderMarks,
2201
3292
  peek_marks: peekAdded,
2202
3293
  topup_marks: topUpAdded,
2203
- deepened: deepened,
2204
- deepened_marks: deepenedMarks,
3294
+ consolidation_marks: consolidationAdded,
3295
+ closing_consolidation_marks: closingAdded,
2205
3296
  absorbed_wedges: wedges.absorbed.length,
3297
+ // The upgrade is a mutation of a fold whose `context.fold` record was written at
3298
+ // creation, so the stream would otherwise show a brief that silently changed. The
3299
+ // count, the ids, and the failures ride the boundary that carried them.
3300
+ brief_upgrades: upgradedFolds.length,
3301
+ brief_upgrade_ids: upgradedFolds.join(","),
3302
+ brief_upgrade_failures: upgradeFailures,
3303
+ // Briefs that needed a second ask before they met the contract. Beside the failures
3304
+ // rather than inside them: a cured brief is a brief that landed, and the two together
3305
+ // are how the brief cap gets judged against something other than intuition.
3306
+ brief_upgrade_cures: upgradeCures,
3307
+ brief_upgrade_error: upgradeError,
3308
+ // Everything still owed to a FOLD: queued, in flight, and (always zero once the
3309
+ // wedge above ran) written but uncarried. In flight is counted because the lane
3310
+ // holds several calls open and work nobody can see is work nobody bounds. Counted in
3311
+ // folds rather than calls because a call now carries a whole commit's spans, and the
3312
+ // question this answers is how many folds are still reading deterministic.
3313
+ brief_upgrades_waiting: upgrades.ready.length + upgrades.running.size +
3314
+ upgrades.queue.reduce((total, entry) => total + entry.members.length, 0),
3315
+ // The same work in the unit the lane's two constants actually bound: calls, queued
3316
+ // plus in flight. Reported beside the fold count so a backed-up lane cannot read as
3317
+ // a busy one, or the reverse.
3318
+ brief_upgrade_calls: upgrades.queue.length + new Set(upgrades.running.values()).size,
2206
3319
  freed_bytes: freedBytes,
2207
3320
  freed_tokens: estimatedTokens(freedBytes),
2208
3321
  rewrite_tokens: accounting.rewriteTokens,
2209
3322
  pinned_bytes: accounting.pinnedBytes,
2210
3323
  pinned_results: accounting.pinnedResults,
3324
+ protected_stale_bytes: pinHeld.bytes,
3325
+ protected_stale_refs: pinHeld.refs,
2211
3326
  window_tokens: snapshot.contextWindow,
2212
3327
  });
2213
3328
  instrumentation.mutationsSinceHandoff += 1;
2214
3329
  instrumentation.lastMutationRequest = instrumentation.requests;
2215
3330
  instrumentation.lastMutationTokens = usedTokens;
3331
+ // THE RESPONSE ATTRIBUTION. Whatever the gated round did, or that it did nothing,
3332
+ // is linked to the exposure it answered, and the exposure is consumed by the
3333
+ // commit whatever its trigger: a fence or user commit landing mid-round is still
3334
+ // the commit boundary the last-call announced. The carrier's disappearance rides
3335
+ // this commit's own rewrite.
3336
+ if (persistence.state.lastCall) {
3337
+ const lastCall = persistence.state.lastCall;
3338
+ const attribution = lastCallAttribution(lastCall, accounting.agentMarks);
3339
+ emit("context.response", {
3340
+ exposure_seq: lastCall.exposure,
3341
+ commit_seq: commitEvent.seq,
3342
+ trigger,
3343
+ outcome: attribution.responded ? "responded" : "silent",
3344
+ ...attribution,
3345
+ });
3346
+ clearLastCall();
3347
+ }
3348
+ // A commit that drops occupancy back under a waypoint re-arms its notice: the next
3349
+ // upward crossing is a new event and earns a new one. The estimate is the same one
3350
+ // the receipt reports: measured occupancy before, less what this commit freed.
3351
+ if (persistence.state.notices && usedTokens !== null && budgetTokens > 0) {
3352
+ const postOccupancy = Math.max(0, usedTokens - estimatedTokens(freedBytes)) / budgetTokens;
3353
+ const fired = persistence.state.notices.fired.filter((share) => postOccupancy >= share);
3354
+ if (fired.length !== persistence.state.notices.fired.length) {
3355
+ persistence.state = { ...persistence.state, notices: { ...persistence.state.notices, fired } };
3356
+ }
3357
+ }
3358
+ // The rider: at most ONE action prompt per fold epoch, composed from post-commit
3359
+ // numbers, persisted as literal bytes, delivered beside the receipt inside the
3360
+ // rewrite this commit already paid for. The epoch key is the commit event's own
3361
+ // stream sequence: strictly monotone, one per applied commit, so refold-only
3362
+ // epochs get their rider too and no two epochs can ever share a key. The next
3363
+ // epoch REPLACES the text; nothing stacks.
3364
+ const riderEpoch = commitEvent.seq;
3365
+ if (persistence.state.rider?.epoch !== riderEpoch) {
3366
+ const postAccounting = markAccounting(snapshot, persistence.state);
3367
+ const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
3368
+ const pinnedMass = explicitProtectedMass(snapshot, persistence.state);
3369
+ const pinnedShare = budgetTokens > 0
3370
+ ? estimatedTokens(pinnedMass.bytes) / budgetTokens
3371
+ : 0;
3372
+ const riderText = contextRiderText({
3373
+ toolName,
3374
+ brandNoun,
3375
+ suggestion: deliverSurfacing(snapshot, "rider"),
3376
+ pendingAgentMarks: postAccounting.agentMarks,
3377
+ eligibleMarks: postAccounting.eligibleMarks,
3378
+ freedTokens: postAccounting.freedTokens,
3379
+ eligibleFreedTokens: postAccounting.eligibleFreedTokens,
3380
+ anchors: remainder.candidates.slice(0, 3).map((candidate) => candidate.id),
3381
+ pinnedShare,
3382
+ maxPinnedShare: MAX_PINNED_SHARE,
3383
+ });
3384
+ persistence.state = { ...persistence.state, rider: { epoch: riderEpoch, text: riderText } };
3385
+ emit("context.rider", {
3386
+ epoch: riderEpoch,
3387
+ chars: riderText.length,
3388
+ anchors: Math.min(3, remainder.candidates.length),
3389
+ pending_agent_marks: postAccounting.agentMarks,
3390
+ eligible_marks: postAccounting.eligibleMarks,
3391
+ pinned_share: pinnedShare,
3392
+ max_pinned_share: MAX_PINNED_SHARE,
3393
+ });
3394
+ }
2216
3395
  let foldedToolResults = 0;
2217
3396
  let foldedSpans = 0;
2218
3397
  for (const applied of result.applied) {
2219
3398
  const fold = persistence.state.folds.find((item) => item.id === applied.foldId);
2220
3399
  if (fold?.kind === "tool-result") foldedToolResults += 1;
2221
3400
  else foldedSpans += 1;
3401
+ // A reclaimed peek copy names what it copied. Derived from the transcript rather
3402
+ // than carried on the mark, so the pointer is the same whichever mechanism claimed
3403
+ // the copy: the exposure's reclaim marking, the commit's, or the doorless ladder.
3404
+ const peekedSources = fold?.kind === "tool-result"
3405
+ ? peekedSourceFoldIds(snapshot, flattenFoldRefs(fold, persistence.state))
3406
+ : null;
2222
3407
  emit("context.fold", {
2223
3408
  commit_seq: commitEvent.seq,
2224
3409
  fold_id: applied.foldId,
@@ -2227,15 +3412,20 @@ export function registerActiveContext(pi: any, options: {
2227
3412
  mark_id: applied.id,
2228
3413
  fold_kind: fold?.kind ?? applied.mark,
2229
3414
  origin: applied.origin,
3415
+ peek_of: peekedSources ? peekedSources.join(",") : null,
2230
3416
  source_chars: fold?.sourceChars ?? 0,
2231
3417
  placeholder_chars: fold?.placeholderChars ?? 0,
2232
- brief_provenance: fold ? normalizeLegacyProvenance(fold.provenance).kind : null,
3418
+ brief_provenance: fold ? foldProvenance(fold, persistence.state).kind : null,
2233
3419
  });
2234
3420
  }
3421
+ // Queued AFTER the applications above, so no fold can be committed and upgraded on
3422
+ // one boundary: the generator has not run yet, and the wait is the mechanism.
3423
+ queueBriefUpgrades(snapshot, state, result.applied.map((applied) => applied.foldId));
2235
3424
  for (const wedge of wedges.absorbed) {
2236
3425
  emit("context.absorb", {
2237
3426
  commit_seq: commitEvent.seq,
2238
3427
  into_fold_id: wedge.intoMarkId,
3428
+ from_mark_id: wedge.fromMarkId,
2239
3429
  start_id: wedge.startId,
2240
3430
  end_id: wedge.endId,
2241
3431
  entries: wedge.entries,
@@ -2254,8 +3444,10 @@ export function registerActiveContext(pi: any, options: {
2254
3444
  ladderMarks: accounting.ladderMarks,
2255
3445
  peekMarks: peekAdded,
2256
3446
  topUpMarks: topUpAdded,
2257
- deepenedTarget: deepened,
2258
- deepenedMarks,
3447
+ consolidationMarks: consolidationAdded,
3448
+ // Parents formed over the folds THIS commit landed. They were never in
3449
+ // `pendingMarks`, so an epoch's arithmetic is applied = pending + closing.
3450
+ closingMarks: closingAdded,
2259
3451
  absorbedWedges: wedges.absorbed.length,
2260
3452
  absorbed: wedges.absorbed,
2261
3453
  // What the commit actually folded, the way an agent counts it, so the receipt can
@@ -2263,7 +3455,6 @@ export function registerActiveContext(pi: any, options: {
2263
3455
  foldedSpans,
2264
3456
  foldedToolResults,
2265
3457
  occupancyTokensBefore: usedTokens,
2266
- preDeepenFreedShare: preDeepenShare,
2267
3458
  depthFloorShare: commitDepthFloorShare(snapshot),
2268
3459
  reclaimFloorShare: COMMIT_RECLAIM_FLOOR_SHARE,
2269
3460
  guardWaived: result.waived.length > 0,
@@ -2273,19 +3464,21 @@ export function registerActiveContext(pi: any, options: {
2273
3464
  deferredMarks: result.retained.length,
2274
3465
  pinnedBytes: accounting.pinnedBytes,
2275
3466
  pinnedResults: accounting.pinnedResults,
3467
+ protectedStaleBytes: pinHeld.bytes,
3468
+ protectedStaleRefs: pinHeld.refs,
2276
3469
  retainedMarks: result.retained.length,
2277
3470
  currentTurnRetained: result.retained.filter((mark) =>
2278
3471
  markTouchesCurrentTurn(state, mark, guarded)).length,
2279
3472
  eligibleMarks: accounting.eligibleMarks,
2280
3473
  estimatedRewriteTokens: accounting.rewriteTokens,
2281
3474
  estimatedFreedTokens: accounting.freedTokens,
2282
- freedWindowShare: accounting.freedWindowShare,
3475
+ freedBudgetShare: accounting.freedBudgetShare,
2283
3476
  sourceBytesSaved: freedBytes,
2284
- actualFreedWindowShare: snapshot.contextWindow > 0
2285
- ? estimatedTokens(freedBytes) / snapshot.contextWindow
3477
+ actualFreedBudgetShare: snapshot.budgetTokens > 0
3478
+ ? estimatedTokens(freedBytes) / snapshot.budgetTokens
2286
3479
  : 0,
2287
- targetWindowShare: freeingTarget,
2288
- hysteresisTargetShare: hysteresisShare,
3480
+ targetBudgetShare: freeingTarget,
3481
+ hysteresisTargetShare: freeingTarget,
2289
3482
  requestsSincePreviousCommit: instrumentation.requests - instrumentation.lastMutationRequest,
2290
3483
  instrumentation: ledgerSummary(instrumentation.ledger),
2291
3484
  };
@@ -2311,12 +3504,78 @@ export function registerActiveContext(pi: any, options: {
2311
3504
  * that would free nothing, and the hysteresis stops a plateau from reading as a
2312
3505
  * stream of events. What is gone is only the announcement in front of it.
2313
3506
  */
2314
- const curationCommitDue = (snapshot: ActiveContextSnapshot): boolean => {
3507
+ /**
3508
+ * THE trigger, with its one piece of hysteresis.
3509
+ *
3510
+ * Occupancy at or above maxTarget, and then the reopen latch: a window that is still
3511
+ * full because the last commit could not reach the target is the SAME event, not a
3512
+ * new one, so a second commit waits for at least a reclaim floor of genuinely new
3513
+ * eligible mass. Without it a parked window fires a commit per pass and every one of
3514
+ * them rebuilds the prefix for crumbs.
3515
+ */
3516
+ const commitTriggerDue = (snapshot: ActiveContextSnapshot, ratio: number | null): boolean => {
2315
3517
  if (!persistence.state) return false;
2316
- if (!curationTriggerFires(measuredCurationSignals(snapshot))) return false;
3518
+ // The output wall outranks the quiet-runtime line rather than sitting under it. A
3519
+ // window one inflow step from spending the answer's whole reservation is past
3520
+ // economising, and the band top is not the thing it is about to cross.
3521
+ //
3522
+ // ONE COMMIT PER CROSSING, NOT ONE PER PASS. The wall is a level, and a window that
3523
+ // sits on it sits there for many passes: releasing the reopen latch on each of them
3524
+ // fires a commit per pass, which is the prefix-rewrite churn the latch exists to
3525
+ // prevent and which the quiet cadence is built around. Measured on gate 52's fixture:
3526
+ // the first wall commit applied 44 marks and correctly retained all ten of the open
3527
+ // turn, and the two that followed it applied nothing, rewrote the prefix anyway, and
3528
+ // dragged the guard's starvation waiver in on a session that had just made progress.
3529
+ // So the crossing is the event. While the wall stays due the ordinary hysteresis
3530
+ // decides, which is what makes a second commit wait for genuinely new foldable mass.
3531
+ const wall = outputWallDue(snapshot);
3532
+ if (!wall) curation.wallEpisodeOpen = false;
3533
+ if (!wall && !epochCommitDue(snapshot, ratio)) return false;
3534
+ measuredCurationSignals(snapshot);
3535
+ if (wall && !curation.wallEpisodeOpen) {
3536
+ curation.wallEpisodeOpen = true;
3537
+ curation.reopenBaselineShare = null;
3538
+ return true;
3539
+ }
3540
+ // The latch is an ECONOMY rule, and the fence is not latched: a request whose
3541
+ // projection exceeds the provider input budget is rejected outright, so recovery
3542
+ // must produce a window that fits.
3543
+ // A request that does not fit gets its commit whether or not the previous one left
3544
+ // new eligible mass behind; holding it back would be spending the session's
3545
+ // transmissibility to save a prefix rewrite.
3546
+ if (typeof ratio === "number" && Number.isFinite(ratio) && ratio >= hardFenceRatio(snapshot)) {
3547
+ curation.reopenBaselineShare = null;
3548
+ return true;
3549
+ }
2317
3550
  if (curation.reopenBaselineShare !== null) {
2318
- const eligibleShare = markAccounting(snapshot, persistence.state).eligibleFreedWindowShare;
2319
- if (eligibleShare - curation.reopenBaselineShare < COMMIT_RECLAIM_FLOOR_SHARE) return false;
3551
+ const eligibleShare = markAccounting(snapshot, persistence.state).eligibleFreedBudgetShare;
3552
+ if (eligibleShare - curation.reopenBaselineShare < COMMIT_RECLAIM_FLOOR_SHARE) {
3553
+ // A HOLD THAT SAYS SO.
3554
+ //
3555
+ // This was a bare `return false`, and it is the last silent deferral on the
3556
+ // commit path: every other one names itself and reports its numbers. In
3557
+ // sol-20260812 rep 1 the trigger was due for twelve straight ordinals while
3558
+ // occupancy climbed to 94 percent of budget, and the stream carries no record of
3559
+ // what held it, so the stall can be proven but not explained. Emitted on every
3560
+ // held pass rather than once per episode: under-reporting is the defect being
3561
+ // fixed, the record is a stream record that never reaches the projection, and
3562
+ // the volume is bounded by passes above the band top.
3563
+ const capacity = servingCapacity(snapshot.contextWindow);
3564
+ emit("context.commit", {
3565
+ trigger: "band-top",
3566
+ deferred: true,
3567
+ reason: "reopen-latch",
3568
+ applied_marks: 0,
3569
+ eligible_freed_share: eligibleShare,
3570
+ reopen_baseline_share: curation.reopenBaselineShare,
3571
+ reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
3572
+ occupancy_tokens: capacity.usedTokens,
3573
+ expected_inflow_tokens: expectedWallInflowTokens(),
3574
+ budget_tokens: capacity.budgetTokens,
3575
+ window_tokens: snapshot.contextWindow,
3576
+ });
3577
+ return false;
3578
+ }
2320
3579
  curation.reopenBaselineShare = null;
2321
3580
  }
2322
3581
  return true;
@@ -2332,7 +3591,7 @@ export function registerActiveContext(pi: any, options: {
2332
3591
  if (curation.reopenBaselineShare === null) return;
2333
3592
  const capacity = servingCapacity(lifecycle.latestSnapshot?.contextWindow ?? null);
2334
3593
  if (capacity.usedTokens === null || capacity.budgetTokens <= 0) return;
2335
- if (capacity.usedTokens / capacity.budgetTokens < CURATION_OCCUPANCY_SHARE) {
3594
+ if (capacity.usedTokens / capacity.budgetTokens < thresholds.maxTarget) {
2336
3595
  curation.reopenBaselineShare = null;
2337
3596
  }
2338
3597
  };
@@ -2373,26 +3632,250 @@ export function registerActiveContext(pi: any, options: {
2373
3632
  splitFromChars: Number(action.splitFromChars ?? 0),
2374
3633
  absorbedWedges: Number(epoch?.absorbedWedges ?? 0),
2375
3634
  recovered: curation.recoveryAttempts > 0,
2376
- note: epoch && Number(epoch.deepenedMarks ?? 0) > 0
2377
- ? `The commit reached into the fresh tail for ${epoch.deepenedMarks} further span(s), because a ` +
2378
- "shallower commit would have freed less than one turn of inflow."
2379
- : null,
3635
+ protectedBytes: Number(epoch?.protectedStaleBytes ?? 0),
3636
+ note: null,
2380
3637
  }));
2381
3638
  };
2382
3639
 
3640
+ /**
3641
+ * Threshold notices, evaluated on every pass the ladder runs on. One waypoint fires
3642
+ * once per upward crossing of the measured occupancy; delivery is an append-once
3643
+ * carrier plus its stream record, and re-arming belongs to the commit that drops
3644
+ * occupancy back under the line. Default on; `guidance.thresholdNotices: false`
3645
+ * registers the runtime with the waypoints silent.
3646
+ */
3647
+ const deliverThresholdNotices = (snapshot: ActiveContextSnapshot): boolean => {
3648
+ if (!guidance.thresholdNotices || !persistence.state) return false;
3649
+ const capacity = servingCapacity(snapshot.contextWindow);
3650
+ if (capacity.usedTokens === null || capacity.budgetTokens <= 0) return false;
3651
+ const occupancy = capacity.usedTokens / capacity.budgetTokens;
3652
+ const current = persistence.state.notices ?? { fired: [], ring: [] };
3653
+ let fired = current.fired;
3654
+ let ring = current.ring;
3655
+ let changed = false;
3656
+ for (const share of THRESHOLD_NOTICE_SHARES) {
3657
+ if (occupancy < share || fired.includes(share)) continue;
3658
+ const text = thresholdNoticeText({
3659
+ share,
3660
+ occupancyTokens: capacity.usedTokens,
3661
+ budgetTokens: capacity.budgetTokens,
3662
+ maxTarget: thresholds.maxTarget,
3663
+ toolName,
3664
+ brandNoun,
3665
+ });
3666
+ fired = [...fired, share];
3667
+ ring = [...ring, { share, ordinal: markOrdinal(snapshot), text }];
3668
+ if (ring.length > MAX_THRESHOLD_NOTICES) ring = ring.slice(ring.length - MAX_THRESHOLD_NOTICES);
3669
+ changed = true;
3670
+ emit("context.notice", {
3671
+ share,
3672
+ occupancy,
3673
+ occupancy_tokens: capacity.usedTokens,
3674
+ budget_tokens: capacity.budgetTokens,
3675
+ max_target: thresholds.maxTarget,
3676
+ chars: text.length,
3677
+ });
3678
+ }
3679
+ if (changed) persistence.state = { ...persistence.state, notices: { fired, ring } };
3680
+ return changed;
3681
+ };
3682
+
3683
+ /**
3684
+ * The same selection, read rather than delivered: what the next carrier WOULD say,
3685
+ * with the ledger that decides whether it says anything at all.
3686
+ */
3687
+ const statusSurfacing = (snapshot: ActiveContextSnapshot): Record<string, unknown> => {
3688
+ const ledger = persistence.state ? surfacingLedger(persistence.state) : [];
3689
+ if (!persistence.state) return { slate: [], line: null, ledger, silenced: [] };
3690
+ const selection = selectSurfacingSlate({
3691
+ state: persistence.state,
3692
+ snapshot,
3693
+ toolName,
3694
+ ordinal: markOrdinal(snapshot),
3695
+ });
3696
+ return {
3697
+ slate: selection.slate.map((suggestion) => ({
3698
+ id: suggestion.id,
3699
+ route: suggestion.route,
3700
+ contentScore: suggestion.contentScore,
3701
+ briefScore: suggestion.briefScore,
3702
+ margin: suggestion.margin,
3703
+ slot: suggestion.slot,
3704
+ })),
3705
+ line: surfacingSlateText({ slate: selection.slate, queryTerms: selection.queryTerms, brandNoun }),
3706
+ considered: selection.considered,
3707
+ divergent: selection.divergent,
3708
+ suppressed: selection.suppressed,
3709
+ intentTerms: selection.queryTerms.size,
3710
+ contentHit: SURFACING_CONTENT_HIT,
3711
+ briefHit: SURFACING_BRIEF_HIT,
3712
+ divergenceMargin: SURFACING_DIVERGENCE_MARGIN,
3713
+ slateSize: SURFACING_SLATE_SIZE,
3714
+ silenced: [...surfacingSilenced(ledger)],
3715
+ ledger,
3716
+ };
3717
+ };
3718
+
3719
+ /**
3720
+ * THE SURFACING SLATE, delivered.
3721
+ *
3722
+ * The agent cannot peek a fold it does not know to want, so one line names the fold
3723
+ * whose stored content matches the task in hand when its brief does not. Where it
3724
+ * lands is the whole economics: only carriers that ride a rewrite the runtime is
3725
+ * already paying for (the pre-commit last call and the post-commit rider) plus the
3726
+ * on-demand status surface, which the agent asked for. The per-request ephemeral
3727
+ * carrier stays dead; it cost 21.9% of every input token in rep 21 and nothing else
3728
+ * about this build changes that verdict.
3729
+ *
3730
+ * One suggestion per delivery point, and most delivery points get none. Every
3731
+ * delivery first resolves whatever offers have run out of window, so the ignore that
3732
+ * silences a fold is recorded before the next selection reads the ledger.
3733
+ */
3734
+ const deliverSurfacing = (snapshot: ActiveContextSnapshot, carrier: string): string | null => {
3735
+ if (!persistence.state) return null;
3736
+ const ordinal = markOrdinal(snapshot);
3737
+ const resolved = resolveSurfacing({ state: persistence.state, snapshot, ordinal });
3738
+ persistence.state = resolved.state;
3739
+ for (const transition of resolved.transitions) {
3740
+ emit("context.outcome", {
3741
+ fold_id: transition.id,
3742
+ from_outcome: transition.from,
3743
+ outcome: transition.to,
3744
+ outcome_ordinal: transition.ordinal,
3745
+ window_ordinals: SURFACING_OUTCOME_WINDOW_ORDINALS,
3746
+ });
3747
+ }
3748
+ const selection = selectSurfacingSlate({ state: persistence.state, snapshot, toolName, ordinal });
3749
+ const suggestion = selection.slate[0];
3750
+ if (!suggestion) return null;
3751
+ const text = surfacingSlateText({
3752
+ slate: selection.slate,
3753
+ queryTerms: selection.queryTerms,
3754
+ brandNoun,
3755
+ });
3756
+ if (!text) return null;
3757
+ persistence.state = issueSurfacing(persistence.state, suggestion.id, ordinal);
3758
+ emit("context.suggestion", {
3759
+ carrier,
3760
+ fold_id: suggestion.id,
3761
+ content_score: suggestion.contentScore,
3762
+ brief_score: suggestion.briefScore,
3763
+ margin: suggestion.margin,
3764
+ content_hit: SURFACING_CONTENT_HIT,
3765
+ brief_hit: SURFACING_BRIEF_HIT,
3766
+ divergence_margin: SURFACING_DIVERGENCE_MARGIN,
3767
+ slot: suggestion.slot,
3768
+ slate_size: SURFACING_SLATE_SIZE,
3769
+ fold_depth: suggestion.depth,
3770
+ considered: selection.considered,
3771
+ divergent: selection.divergent,
3772
+ suppressed: selection.suppressed,
3773
+ intent_terms: selection.queryTerms.size,
3774
+ chars: text.length,
3775
+ });
3776
+ return text;
3777
+ };
3778
+
3779
+ /**
3780
+ * THE PRE-COMMIT LAST-CALL GATE. When the band-top trigger fires, the commit defers
3781
+ * exactly one gated round behind one exposure of the ruled prompt; then it proceeds
3782
+ * with whatever marks exist. A request whose projection exceeds the provider input
3783
+ * budget is rejected outright, so recovery must produce a window that fits: fence
3784
+ * pressure, the recovery lane, or an occupancy already past the serving budget
3785
+ * commit NOW, and the user
3786
+ * command never reaches this gate because explicit intent is its own answer. The
3787
+ * round is measured from DELIVERY: the commit proceeds only on a context pass whose
3788
+ * ordinal is past the pass that rendered the exposure, which is exactly one agent
3789
+ * response between prompt and rewrite.
3790
+ */
3791
+ const lastCallVerdict = (
3792
+ snapshot: ActiveContextSnapshot,
3793
+ pressure: number | null,
3794
+ phase: string,
3795
+ ): "expose" | "hold" | "proceed" => {
3796
+ const state = persistence.state!;
3797
+ const capacity = servingCapacity(snapshot.contextWindow);
3798
+ const fenceLevel = typeof pressure === "number" && Number.isFinite(pressure) &&
3799
+ pressure >= hardFenceRatio(snapshot);
3800
+ // The output wall deliberately does NOT proceed here. It buys the crossing one inflow
3801
+ // step of lead, and the gated round is what that lead is for: the agent still gets its
3802
+ // one bounded turn to curate before a rewrite, which is a contract the wall has no
3803
+ // business cancelling. If the round overruns and occupancy passes the budget outright,
3804
+ // the condition below is what proceeds, on the pass that builds the next request.
3805
+ const overBudget = capacity.usedTokens !== null && capacity.budgetTokens > 0 &&
3806
+ capacity.usedTokens > capacity.budgetTokens;
3807
+ if (fenceLevel || overBudget || curation.recoveryAttempts > 0 || curation.pendingRejection) {
3808
+ return "proceed";
3809
+ }
3810
+ if (!state.lastCall) return "expose";
3811
+ if (phase !== "context") return "hold";
3812
+ const delivery = curation.lastCallDelivery;
3813
+ if (!delivery || delivery.exposure !== state.lastCall.exposure) return "hold";
3814
+ return markOrdinal(snapshot) > delivery.ordinal ? "proceed" : "hold";
3815
+ };
3816
+
3817
+ const exposeLastCall = (snapshot: ActiveContextSnapshot): void => {
3818
+ if (!persistence.state) return;
3819
+ // THE PEEK RECLAIM IS MARKED HERE, NOT ONLY AT THE COMMIT.
3820
+ //
3821
+ // A peek copy is reclaimed at the next commit by contract, and a contract the agent
3822
+ // cannot see until after it executes is not one it can veto. The exposure is the one
3823
+ // bounded round before the rewrite, so minting the reclaim marks now puts the pending
3824
+ // disposal in front of the agent while a pin still changes the outcome: a pinned mark
3825
+ // waits instead of applying, and lifting the pin hands the copy to a later commit.
3826
+ const exposureOrdinal = markOrdinal(snapshot);
3827
+ let peekReclaims = 0;
3828
+ for (const mark of ephemeralPeekMarks({ snapshot, state: persistence.state, ordinal: exposureOrdinal })) {
3829
+ const addition = addPendingMark(persistence.state, mark);
3830
+ if (addition.added) { persistence.state = addition.state; peekReclaims += 1; }
3831
+ }
3832
+ const signals = measuredCurationSignals(snapshot);
3833
+ const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
3834
+ const accounting = markAccounting(snapshot, persistence.state);
3835
+ const text = lastCallText({
3836
+ signals,
3837
+ unmarked: { spans: remainder.spans, tokens: remainder.tokens },
3838
+ pendingMarks: accounting.pending,
3839
+ peekReclaims,
3840
+ suggestion: deliverSurfacing(snapshot, "lastcall"),
3841
+ toolName,
3842
+ brandNoun,
3843
+ });
3844
+ const record = emit("context.lastcall", {
3845
+ occupancy: signals.occupancy,
3846
+ max_target: thresholds.maxTarget,
3847
+ occupancy_tokens: signals.occupancyTokens,
3848
+ budget_tokens: signals.budgetTokens,
3849
+ unmarked_stale_spans: remainder.spans,
3850
+ unmarked_stale_tokens: remainder.tokens,
3851
+ pending_marks: accounting.pending,
3852
+ pending_agent_marks: accounting.agentMarks,
3853
+ peek_marks: peekReclaims,
3854
+ chars: text.length,
3855
+ });
3856
+ persistence.state = {
3857
+ ...persistence.state,
3858
+ lastCall: {
3859
+ exposure: record.seq,
3860
+ ordinal: markOrdinal(snapshot),
3861
+ contextCalls: curation.contextCalls,
3862
+ agentMarks: accounting.agentMarks,
3863
+ text,
3864
+ },
3865
+ };
3866
+ };
3867
+
2383
3868
  const applyAutomaticRung = async (
2384
3869
  snapshot: ActiveContextSnapshot,
2385
3870
  ratio: number,
2386
3871
  rungOptions: {
2387
- waiveToolCadence?: boolean;
2388
3872
  toolOnly?: boolean;
2389
3873
  waiverRatio?: number;
2390
- announcing?: boolean;
2391
3874
  } = {},
3875
+ phase = "context",
2392
3876
  ): Promise<Record<string, unknown> | null> => {
2393
3877
  if (!persistence.state || ladder.automaticFailure || ladder.preparing) return null;
2394
3878
  const rungSelectionOptions = {
2395
- waiveToolCadence: rungOptions.waiveToolCadence,
2396
3879
  toolOnly: rungOptions.toolOnly,
2397
3880
  summarizerAvailable: Boolean(options.summarizeContextSpan),
2398
3881
  failedPreparationIds: ladder.failedPreparations,
@@ -2433,59 +3916,98 @@ export function registerActiveContext(pi: any, options: {
2433
3916
  };
2434
3917
  let epoch: Record<string, unknown> | null = null;
2435
3918
  let inlineRungs = true;
2436
- if (epochScheduling) {
2437
- // QUIET RUNTIME. A commit is an epoch transition: it rewrites the projection, and
2438
- // every rewrite risks the whole prefix cache, so the cadence has to be the fewest
2439
- // commits that still keep the window inside its budget.
2440
- //
2441
- // The economic eligible-share trigger is DELETED: it fired 164 commits from
2442
- // ordinal 17 in rep 15, each a fresh cache rebuild bought with marks that had not
2443
- // yet earned one. What remains is the two-signal curation trigger, which fired 10
2444
- // to 17 times across reps 17 and 20, over the pressure backstop.
2445
- //
2446
- // The trigger stays; only its ANNOUNCEMENT is gone. Nothing warns that a commit
2447
- // is coming and nothing waits for the agent to react, because a warning has to
2448
- // arrive before the event it warns about and therefore has to break a prefix
2449
- // nothing else was breaking. Deleting the trigger with the announcement was the
2450
- // wrong cut: it left the raw backstop alone in front of the window, and gate 58
2451
- // caught the projection overrunning its budget before anything reclaimed -- the
2452
- // rep 13 death. The runtime still decides early. It just no longer says so.
2453
- const backstopDue = epochCommitDue(snapshot, ratio);
2454
- // The trigger is evaluated on CONTEXT passes only. A commit is an epoch
2455
- // transition, and it belongs on the pass that builds the next projection rather
2456
- // than in the middle of a tool batch: firing it mid-batch makes a MARK rewrite
2457
- // the window, which is precisely the thing marking is supposed never to do.
2458
- const curationDue = !backstopDue && rungOptions.announcing === true &&
2459
- curationCommitDue(snapshot);
2460
- if (backstopDue || curationDue) {
3919
+ // QUIET RUNTIME, AND THE ONE TRIGGER.
3920
+ //
3921
+ // A commit is an epoch transition: it rewrites the projection, every rewrite risks
3922
+ // the whole prefix cache, so the cadence has to be the fewest commits that still
3923
+ // keep the window inside its budget. Occupancy reaching maxTarget is the whole
3924
+ // automatic condition. Below it the runtime is quiet and marks accumulate free;
3925
+ // at it, one commit folds down toward minTarget, and the gap between the two lines
3926
+ // is what makes the spacing of events structural rather than hoped for.
3927
+ //
3928
+ // Two other arms used to reach this line and both are deleted. The eligible-share
3929
+ // arm fired 164 commits from ordinal 17 in rep 15 on marks that had not earned a
3930
+ // rewrite, and it could fire below maxTarget, which is the quiet law it sat under.
3931
+ // The stale-mass AND-condition guarded an ANNOUNCEMENT that no longer exists: a
3932
+ // commit is never announced, so a commit with nothing eligible now applies nothing
3933
+ // and reports it rather than being suppressed in advance.
3934
+ //
3935
+ // The trigger decides early and says nothing. Nothing warns that a commit is
3936
+ // coming, because a warning has to arrive before the event it warns about and
3937
+ // therefore has to break a prefix nothing else was breaking.
3938
+ // Evaluated on EVERY pass the ladder runs on, which is where the pressure backstop
3939
+ // always sat. Crossing the band top is a property of occupancy, not of which
3940
+ // lifecycle hook happened to notice it, and a trigger that waits for the next
3941
+ // projection pass is a window that keeps climbing while it waits.
3942
+ const noticesChanged = deliverThresholdNotices(snapshot);
3943
+ let lastCallChanged = false;
3944
+ const commitDue = commitTriggerDue(snapshot, ratio);
3945
+ if (commitDue) {
3946
+ const verdict = lastCallVerdict(snapshot, rungOptions.waiverRatio ?? ratio, phase);
3947
+ if (verdict === "expose") {
3948
+ exposeLastCall(snapshot);
3949
+ lastCallChanged = true;
3950
+ } else if (verdict === "proceed") {
2461
3951
  epoch = await runCommitEpoch(
2462
3952
  snapshot,
2463
- backstopDue ? "window-pressure" : "curation-trigger",
3953
+ "band-top",
2464
3954
  true,
2465
3955
  rungOptions.waiverRatio ?? ratio,
2466
3956
  );
2467
- // Latch the eligible share this commit left behind, so the next one waits for
2468
- // genuinely new foldable mass rather than for the same window to still be full.
2469
- if (persistence.state && (curationDue || curation.reopenBaselineShare !== null)) {
3957
+ // Latch the eligible share this commit left behind, so the next crossing waits
3958
+ // for genuinely new foldable mass rather than for the same window to still be
3959
+ // full. The band top is a LINE, and a window parked on it is one event.
3960
+ if (persistence.state) {
2470
3961
  curation.reopenBaselineShare =
2471
- markAccounting(snapshot, persistence.state).eligibleFreedWindowShare;
3962
+ markAccounting(snapshot, persistence.state).eligibleFreedBudgetShare;
2472
3963
  }
2473
3964
  }
2474
- // An inline rung is free only INSIDE a rewrite the epoch already paid for. Below
2475
- // the threshold no commit ran, and an epoch that applied NOTHING -- every mark
2476
- // held back by an open turn or by ineligibility -- paid for no rewrite either,
2477
- // so folding inline there is a fresh single-fold rewrite of its own. Measured
2478
- // 2026-08-06 (rep 10): the current-turn guard retained all nine marks every turn
2479
- // while the inline rung folded one batch per turn anyway, 52 single-fold
2480
- // rewrites that left the prefix cache share at zero. Below a paid rewrite the
2481
- // ladder MARKS, so the decisions batch into the first commit that can apply them.
2482
- inlineRungs = Boolean(epoch) && Number(epoch?.appliedMarks ?? 0) > 0;
2483
- if (!inlineRungs) {
2484
- const marked = markLadderSelection();
2485
- if (marked) {
2486
- if (epoch) ladder.lastAutomaticAction = { ...marked, epoch };
2487
- return ladder.lastAutomaticAction;
2488
- }
3965
+ // "hold": the exposure is out and the round is still the agent's. Marks keep
3966
+ // accumulating below; nothing commits and nothing re-exposes.
3967
+ } else if (persistence.state.lastCall) {
3968
+ // The crossing died without its commit (a user rescue or expiry dropped
3969
+ // occupancy back under the trigger), so the exposure lapses: attributed,
3970
+ // cleared, and the next crossing is a fresh event with a fresh exposure.
3971
+ const capacity = servingCapacity(snapshot.contextWindow);
3972
+ if (capacity.usedTokens !== null && capacity.budgetTokens > 0 &&
3973
+ capacity.usedTokens / capacity.budgetTokens < thresholds.maxTarget) {
3974
+ const lastCall = persistence.state.lastCall;
3975
+ const attribution = lastCallAttribution(
3976
+ lastCall,
3977
+ markAccounting(snapshot, persistence.state).agentMarks,
3978
+ );
3979
+ emit("context.response", {
3980
+ exposure_seq: lastCall.exposure,
3981
+ commit_seq: null,
3982
+ trigger: null,
3983
+ outcome: "lapsed",
3984
+ ...attribution,
3985
+ });
3986
+ clearLastCall();
3987
+ lastCallChanged = true;
3988
+ }
3989
+ }
3990
+ // An inline rung is free only INSIDE a rewrite the epoch already paid for. Below
3991
+ // the threshold no commit ran, and an epoch that applied NOTHING -- every mark
3992
+ // held back by an open turn or by ineligibility -- paid for no rewrite either,
3993
+ // so folding inline there is a fresh single-fold rewrite of its own. Measured
3994
+ // 2026-08-06 (rep 10): the current-turn guard retained all nine marks every turn
3995
+ // while the inline rung folded one batch per turn anyway, 52 single-fold
3996
+ // rewrites that left the prefix cache share at zero. Below a paid rewrite the
3997
+ // ladder MARKS, so the decisions batch into the first commit that can apply them.
3998
+ inlineRungs = Boolean(epoch) && Number(epoch?.appliedMarks ?? 0) > 0;
3999
+ if (!inlineRungs) {
4000
+ const marked = markLadderSelection();
4001
+ if (marked) {
4002
+ if (epoch) ladder.lastAutomaticAction = { ...marked, epoch };
4003
+ return ladder.lastAutomaticAction;
4004
+ }
4005
+ // A carrier change is durable state even when no mark and no epoch moved: the
4006
+ // exposure and the notices must survive a reload, so the pass returns an action
4007
+ // for the transaction to persist. Never a receipt: a carrier reports itself.
4008
+ if (!epoch && (lastCallChanged || noticesChanged)) {
4009
+ ladder.lastAutomaticAction = { kind: "carrier", foldIds: [], sourceIds: [], sourceBytesSaved: 0 };
4010
+ return ladder.lastAutomaticAction;
2489
4011
  }
2490
4012
  }
2491
4013
  // Evidence a pending mark already covers is not the inline rung's to fold: a mark
@@ -2501,16 +4023,14 @@ export function registerActiveContext(pi: any, options: {
2501
4023
  // quiet cadence started landing a commit on the same context pass, which is what
2502
4024
  // turns the inline rung on in the first place.
2503
4025
  const selection = inlineRungs
2504
- ? selectAutomaticRung(snapshot, persistence.state, ratio, epochScheduling
2505
- ? {
2506
- ...rungSelectionOptions,
2507
- claimed: new Set([
2508
- ...claimedRefKeys(persistence.state),
2509
- ...currentTurnRefKeys(snapshot),
2510
- ]),
2511
- claimedFoldIds: markedFoldIds(persistence.state),
2512
- }
2513
- : rungSelectionOptions)
4026
+ ? selectAutomaticRung(snapshot, persistence.state, ratio, {
4027
+ ...rungSelectionOptions,
4028
+ claimed: new Set([
4029
+ ...claimedRefKeys(persistence.state),
4030
+ ...currentTurnRefKeys(snapshot),
4031
+ ]),
4032
+ claimedFoldIds: markedFoldIds(persistence.state),
4033
+ })
2514
4034
  : null;
2515
4035
  // Above the commit threshold we are already inside the epoch's rewrite turn, so
2516
4036
  // every rung applies inline: the extra rung costs at most one more invalidation
@@ -2631,10 +4151,8 @@ export function registerActiveContext(pi: any, options: {
2631
4151
  ctx: any,
2632
4152
  phase: string,
2633
4153
  rungOptions: {
2634
- waiveToolCadence?: boolean;
2635
4154
  toolOnly?: boolean;
2636
4155
  waiverRatio?: number;
2637
- announcing?: boolean;
2638
4156
  } = {},
2639
4157
  ): Promise<Record<string, unknown> | null> => {
2640
4158
  if (!persistence.state || !measurements.lastProviderMeasurement ||
@@ -2653,7 +4171,7 @@ export function registerActiveContext(pi: any, options: {
2653
4171
  const transientAtEntry = captureTransient();
2654
4172
  let action: Record<string, unknown> | null = null;
2655
4173
  try {
2656
- action = await applyAutomaticRung(snapshot, ratio, rungOptions);
4174
+ action = await applyAutomaticRung(snapshot, ratio, rungOptions, phase);
2657
4175
  if (action) await persist(ctx);
2658
4176
  if (action) ladder.boundaryFailure = null;
2659
4177
  return action;
@@ -2681,14 +4199,21 @@ export function registerActiveContext(pi: any, options: {
2681
4199
  ctx: any,
2682
4200
  phase: string,
2683
4201
  rungOptions: {
2684
- waiveToolCadence?: boolean;
2685
4202
  toolOnly?: boolean;
2686
4203
  waiverRatio?: number;
2687
- announcing?: boolean;
2688
4204
  } = {},
2689
4205
  ): Promise<Record<string, unknown> | null> => {
2690
- const operation = ladder.actionQueue.then(() =>
2691
- runAutomaticRungTransaction(snapshot, ratio, ctx, phase, rungOptions));
4206
+ const operation = ladder.actionQueue.then(async () => {
4207
+ const action = await runAutomaticRungTransaction(snapshot, ratio, ctx, phase, rungOptions);
4208
+ // Where the generator is started from a pass: after the transaction, so the calls
4209
+ // sit BETWEEN boundaries. Folds queued by the commit this pass just made are
4210
+ // briefed now and land on the next commit, which is the whole shape of the lane.
4211
+ // Afterwards each finishing call starts the next, so this is the lane's ignition
4212
+ // rather than its pace. Parents waiting on their children are taken up here too:
4213
+ // a pass is the other place we know the lane may have room.
4214
+ resumeBriefUpgrades(snapshot, ctx);
4215
+ return action;
4216
+ });
2692
4217
  ladder.actionQueue = operation.catch(() => undefined);
2693
4218
  return operation;
2694
4219
  };
@@ -2700,40 +4225,6 @@ export function registerActiveContext(pi: any, options: {
2700
4225
  return [];
2701
4226
  }
2702
4227
  };
2703
- options.setProjectionProvider?.(projectionCandidates);
2704
-
2705
- /**
2706
- * The suggestion-source hook. The built-in fold-brief source registers through this
2707
- * same call, so an external memory system's items reach the identical carrier,
2708
- * budget, ranking, and accept/reject log; pi-fold owns the channel, not the content.
2709
- */
2710
- const registerSuggestionSource: SuggestionSourceRegistrar = (source: SuggestionSource) => {
2711
- if (!source || typeof source !== "object" || typeof source.id !== "string" || !source.id ||
2712
- typeof source.candidates !== "function") {
2713
- throw new Error("A suggestion source needs a nonempty id and a candidates function");
2714
- }
2715
- if (surfacing.sources.some((registered) => registered.id === source.id)) {
2716
- throw new Error(`Suggestion source '${source.id}' is already registered`);
2717
- }
2718
- surfacing.sources.push(source);
2719
- return {
2720
- unregister: () => {
2721
- surfacing.sources = surfacing.sources.filter((registered) => registered !== source);
2722
- },
2723
- accepted: (id: string) => {
2724
- if (!persistence.state || typeof id !== "string" || !id) return;
2725
- // In-memory only: the next persisted state carries it. Reporting an outcome
2726
- // must never write a durable entry of its own.
2727
- persistence.state = acceptSurfacingSuggestion(
2728
- persistence.state,
2729
- id,
2730
- lifecycle.latestSnapshot ? surfacingOrdinal(lifecycle.latestSnapshot) : 0,
2731
- );
2732
- },
2733
- };
2734
- };
2735
- options.setSuggestionSourceRegistrar?.(registerSuggestionSource);
2736
-
2737
4228
  // A same-session start/tree reload is a projection-generation mutation.
2738
4229
  // Queue it behind every context authority → preparation → commit →
2739
4230
  // projection transaction, then serialize the actual load with the action
@@ -2784,6 +4275,7 @@ export function registerActiveContext(pi: any, options: {
2784
4275
  pi.on("session_start", async (_event: unknown, ctx: any) => {
2785
4276
  await safeLifecycleLoad(ctx, "session-start");
2786
4277
  recordResolvedCapacity(ctx);
4278
+ await armRollbackLane(ctx);
2787
4279
  });
2788
4280
  pi.on("session_tree", async (_event: unknown, ctx: any) => { await safeLifecycleLoad(ctx, "session-tree"); });
2789
4281
  pi.on("session_compact", async (event: Record<string, unknown>, ctx: any) => {
@@ -2831,9 +4323,15 @@ export function registerActiveContext(pi: any, options: {
2831
4323
  const attributionChanged = async (_event: unknown, ctx: any): Promise<void> => {
2832
4324
  lifecycle.generation += 1;
2833
4325
  cancelPreparation();
4326
+ cancelBriefUpgrades();
2834
4327
  if (persistence.state?.prepared) persistence.state = clearPrepared(persistence.state);
2835
4328
  measurements.latestRatio = null;
2836
4329
  measurements.lastProviderMeasurement = null;
4330
+ measurements.wallInflowSteps.length = 0;
4331
+ // A different model or thinking level is a different tokenizer, so counts taken
4332
+ // under the old one describe nothing here. The generation the anchor carries would
4333
+ // already refuse it; dropping it releases the projection text it held as well.
4334
+ measurements.projectionAnchor = null;
2837
4335
  nativeCompaction.lastThresholdDecision = null;
2838
4336
  updateStatus(ctx);
2839
4337
  };
@@ -2904,6 +4402,10 @@ export function registerActiveContext(pi: any, options: {
2904
4402
  // Calibrate BEFORE the new measurement becomes the previous one: the inflow
2905
4403
  // step is the difference between them.
2906
4404
  noteProjectionCalibration(observedMeasurement);
4405
+ // Anchor before this pass builds anything: `previousText` is still the
4406
+ // projection the provider just counted, and the projection built below is the
4407
+ // one that either extends it or does not.
4408
+ noteProviderProjectionAnchor(observedMeasurement);
2907
4409
  measurements.lastProviderMeasurement = observedMeasurement;
2908
4410
  startPreparation(snapshot, measurements.latestRatio, ctx);
2909
4411
  if (!ladder.automaticFailure && measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) {
@@ -2913,9 +4415,9 @@ export function registerActiveContext(pi: any, options: {
2913
4415
  return { messages: event.messages };
2914
4416
  }
2915
4417
  }
2916
- if (selectAutomaticToolForRung(snapshot, persistence.state, measurements.latestRatio)) mutationAttempted = true;
4418
+ if (selectAutomaticToolBatch(snapshot, persistence.state)[0]) mutationAttempted = true;
2917
4419
  const action = await attemptAutomaticRung(
2918
- snapshot, measurements.latestRatio, ctx, "context", { announcing: true },
4420
+ snapshot, measurements.latestRatio, ctx, "context",
2919
4421
  );
2920
4422
  if (action) {
2921
4423
  mutationAttempted = true;
@@ -2951,8 +4453,11 @@ export function registerActiveContext(pi: any, options: {
2951
4453
  // already-large session out of calibrating at all: its first projection aborts
2952
4454
  // under the uncalibrated constant, so nothing is ever sent, so nothing ever
2953
4455
  // measures the ratio, so it aborts forever.
2954
- measurements.lastProjectedChars = bytes(projected);
2955
- measurements.lastProjectedEstimate = projectedTokenEstimate(projected);
4456
+ const reading = projectedTokenReading(projected);
4457
+ measurements.lastProjectedChars = reading.chars;
4458
+ measurements.lastProjectedEstimate = reading.tokens;
4459
+ measurements.lastProjectedEstimateBasis = reading.basis;
4460
+ measurements.lastProjectedSizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
2956
4461
  measurements.lastProjectedEstimateCalibrated = measurements.projectionCalibrations.length > 0;
2957
4462
  // An aborted request still returns the PROJECTION. Handing back the raw branch
2958
4463
  // instead makes the aborted turn the largest message list the session ever
@@ -3015,6 +4520,7 @@ export function registerActiveContext(pi: any, options: {
3015
4520
  // inflow that is now behind us, and the cap resets for the next one.
3016
4521
  curation.recoveryAttempts = 0;
3017
4522
  curation.pendingRejection = null;
4523
+ noteWallInflow(measurement);
3018
4524
  measurements.lastProviderMeasurement = measurement;
3019
4525
  measurements.latestRatio = measuredRatio;
3020
4526
  let snapshot: ActiveContextSnapshot;
@@ -3045,6 +4551,7 @@ export function registerActiveContext(pi: any, options: {
3045
4551
  beginMutationPass();
3046
4552
  try {
3047
4553
  const message = ownValue(event, "message");
4554
+ noteOverflowAtMessageEnd(message, ctx);
3048
4555
  instrumentation.requests += 1;
3049
4556
  const observation = observeCacheUsage(instrumentation.ledger, {
3050
4557
  usage: ownValue(message, "usage"),
@@ -3089,58 +4596,17 @@ export function registerActiveContext(pi: any, options: {
3089
4596
  try { updateStatus(ctx); } catch { /* The provider loop must keep running. */ }
3090
4597
  }
3091
4598
  });
3092
- /**
3093
- * A provider rejection is ground truth that the last request did not fit, and it
3094
- * mutates nothing durable: no assistant message lands, and the projection is rebuilt
3095
- * from the branch on the next request. So it is recorded, not fatal -- the next
3096
- * context pass folds at fence pressure and rebuilds the request rather than dying.
3097
- * A 429 is a rate limit, not an overflow, and is deliberately not recovered from.
4599
+ /*
4600
+ * There was an `after_provider_response` handler here that treated `status >= 400` as
4601
+ * the rejection signal. It could never fire on the case it was written for. Every
4602
+ * provider adapter calls `onResponse` exactly once and only AFTER the request promise
4603
+ * resolves, so a 4xx rejects the promise and control never reaches that call at all
4604
+ * (verified across all seven adapters in pi-ai 0.83.0). `pendingRejection` was
4605
+ * therefore never set in production and the recovery branch of the projection budget
4606
+ * never ran. It is set by the rollback lane now, from an event that does fire.
3098
4607
  */
3099
- pi.on("after_provider_response", (event: Record<string, unknown>) => {
3100
- const status = ownValue(event, "status");
3101
- if (typeof status !== "number" || status < 400 || status === 429) return;
3102
- if (curation.recoveryAttempts >= OVERFLOW_RECOVERY_MAX_ATTEMPTS) return;
3103
- curation.pendingRejection = { status, ordinal: currentOrdinal() };
3104
- });
3105
- pi.on("tool_call", (event: Record<string, unknown>, ctx: any) => {
3106
- const calledTool = ownValue(event, "toolName");
3107
- if (lifecycle.shuttingDown || lifecycle.blockingToolHarvestedThisTurn || lifecycle.blockingToolHarvestQueuedThisTurn ||
3108
- typeof calledTool !== "string" || !blockingTools.has(calledTool)) return;
3109
- lifecycle.blockingToolHarvestQueuedThisTurn = true;
3110
- beginMutationPass();
3111
- const operation = ladder.actionQueue.then(async () => {
3112
- try {
3113
- if (!persistence.state || measurements.latestRatio === null || !measurements.lastProviderMeasurement || ladder.automaticFailure ||
3114
- !durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) return null;
3115
- cancelPreparation();
3116
- let snapshot: ActiveContextSnapshot;
3117
- try { snapshot = authoritativeSnapshotFor(ctx); }
3118
- catch { return null; }
3119
- const candidate = selectAutomaticToolForRung(snapshot, persistence.state, measurements.latestRatio, true);
3120
- if (!candidate) {
3121
- lifecycle.blockingToolHarvestedThisTurn = true;
3122
- return null;
3123
- }
3124
- const action = await runAutomaticRungTransaction(
3125
- snapshot,
3126
- measurements.latestRatio,
3127
- ctx,
3128
- "tool-call",
3129
- { waiveToolCadence: true, toolOnly: true },
3130
- );
3131
- if (action) lifecycle.blockingToolHarvestedThisTurn = true;
3132
- return action;
3133
- } finally {
3134
- lifecycle.blockingToolHarvestQueuedThisTurn = false;
3135
- try { updateStatus(ctx); } catch { /* A blocking tool call must never wait on presentation. */ }
3136
- }
3137
- });
3138
- ladder.actionQueue = operation.catch(() => undefined);
3139
- });
3140
4608
  pi.on("turn_end", async (_event: unknown, ctx: any) => {
3141
4609
  beginMutationPass();
3142
- lifecycle.blockingToolHarvestedThisTurn = false;
3143
- lifecycle.blockingToolHarvestQueuedThisTurn = false;
3144
4610
  if (lifecycle.shuttingDown || !persistence.state || !persistence.persisted) return;
3145
4611
  const stateAtEntry = clone(persistence.state);
3146
4612
  const persistedAtEntry = persistence.persisted;
@@ -3170,8 +4636,287 @@ export function registerActiveContext(pi: any, options: {
3170
4636
  return ownValue(preparation, "userWantsSummary") === true ? { cancel: true } : undefined;
3171
4637
  });
3172
4638
 
4639
+ /**
4640
+ * pi's own overflow classifier, borrowed rather than re-implemented.
4641
+ *
4642
+ * `isContextOverflow` owns the provider-message patterns AND the silent cases (usage
4643
+ * past the window with a clean stop, the truncating `length` stop). Re-stating that
4644
+ * pattern set here would drift on the first provider that reworded its 400. The
4645
+ * bare specifier is tried first, for a deployment where pi-ai is hoisted; the second
4646
+ * form reaches pi's own nested copy, which is where a default install puts it.
4647
+ */
4648
+ const resolveOverflowClassifier = async (): Promise<void> => {
4649
+ if (rollback.classifier || rollback.classifierSource === "unresolved") return;
4650
+ const host = "@earendil-works/pi-coding-agent";
4651
+ const marker = `/${host}/`;
4652
+ const specifiers = ["@earendil-works/pi-ai/compat"];
4653
+ try {
4654
+ const entry = import.meta.resolve(host);
4655
+ const at = entry.lastIndexOf(marker);
4656
+ if (at >= 0) {
4657
+ specifiers.push(`${entry.slice(0, at + marker.length)}node_modules/@earendil-works/pi-ai/dist/compat.js`);
4658
+ }
4659
+ } catch { /* An unresolvable host is the same answer as an unresolvable classifier. */ }
4660
+ for (const specifier of specifiers) {
4661
+ try {
4662
+ const module = await import(specifier) as Record<string, unknown>;
4663
+ if (typeof module.isContextOverflow === "function") {
4664
+ rollback.classifier = module.isContextOverflow as (message: unknown, contextWindow: number) => boolean;
4665
+ rollback.classifierSource = specifier;
4666
+ return;
4667
+ }
4668
+ } catch { /* Try the next form; the last failure is the answer. */ }
4669
+ }
4670
+ rollback.classifierSource = "unresolved";
4671
+ };
4672
+
4673
+ const armRollbackLane = async (ctx: any): Promise<void> => {
4674
+ const probes = probeRollbackSurfaces(ctx?.sessionManager);
4675
+ rollback.probes = probes;
4676
+ rollback.armed = probes.armed;
4677
+ await resolveOverflowClassifier();
4678
+ if (!probes.armed) {
4679
+ safeNotify(
4680
+ ctx,
4681
+ `${brandNoun} overflow rollback is DISARMED for this session (${probes.failures.join("; ")}). ` +
4682
+ "An input-overflow rejection will abort the request instead of rolling the session back.",
4683
+ "warning",
4684
+ );
4685
+ }
4686
+ };
4687
+
4688
+ /**
4689
+ * The disarmed answer, and the only one: say so on the record and leave the session
4690
+ * behaving exactly as it did before the lane existed.
4691
+ */
4692
+ const refuseRollback = (reason: string, trigger: string, ctx: any): void => {
4693
+ emit("context.rollback", {
4694
+ trigger,
4695
+ armed: false,
4696
+ disarm_reason: reason,
4697
+ error_entry_id: null,
4698
+ old_leaf_id: null,
4699
+ new_leaf_id: null,
4700
+ entries_abandoned: 0,
4701
+ occupancy_tokens_before: measurements.lastProviderMeasurement?.tokens ?? null,
4702
+ tokens_rolled_back: 0,
4703
+ replayed: false,
4704
+ replay_skip_reason: "lane-disarmed",
4705
+ notice_chars: 0,
4706
+ attempt_ordinal: rollback.attempts,
4707
+ probes_passed: rollback.probes ? rollback.probes.failures.length === 0 : false,
4708
+ });
4709
+ safeNotify(
4710
+ ctx,
4711
+ `${brandNoun} could not roll back after a provider input-overflow rejection: ${reason}. ` +
4712
+ "The request was not retried.",
4713
+ "error",
4714
+ );
4715
+ };
4716
+
4717
+ /**
4718
+ * The fallback trigger, and the honest limit of it.
4719
+ *
4720
+ * `session_before_compact` never fires for a deployment that turned auto-compaction
4721
+ * off, so the primary trigger is deaf there. This is the second detector: pi's own
4722
+ * classifier over the assistant message at `message_end`, which is where the error
4723
+ * message first becomes visible. It records the episode and, if no compaction event
4724
+ * claims it, reports it.
4725
+ *
4726
+ * It does NOT roll back, and the reason is mechanical rather than cautious. The
4727
+ * pre-strip this lane depends on happens INSIDE `_checkCompaction`, which is exactly
4728
+ * the code an auto-compaction-disabled deployment never reaches, so on this path
4729
+ * agent state still carries the oversized window plus the error message. A bare
4730
+ * `branch()` there would leave the tree and agent state disagreeing, which is the
4731
+ * failure mode the whole design exists to avoid. Held, and reported as held.
4732
+ */
4733
+ const noteOverflowAtMessageEnd = (message: unknown, ctx: any): void => {
4734
+ if (!rollback.classifier || !message || typeof message !== "object") return;
4735
+ const window = budgetWindowFor(ctx) ?? DEFAULT_CONTEXT_WINDOW;
4736
+ let overflow = false;
4737
+ try { overflow = rollback.classifier(message, window) === true; }
4738
+ catch { return; }
4739
+ if (!overflow) return;
4740
+ rollback.pendingOverflow = { at: currentOrdinal(), entryId: null };
4741
+ };
4742
+
4743
+ /**
4744
+ * An overflow that no compaction event claimed: the auto-compaction-disabled shape.
4745
+ * Loud, terminal, and never silent.
4746
+ */
4747
+ const reportUnclaimedOverflow = (ctx: any): void => {
4748
+ if (!rollback.pendingOverflow) return;
4749
+ rollback.pendingOverflow = null;
4750
+ rollback.attempts += 1;
4751
+ refuseRollback(
4752
+ "the overflow arrived without a compaction event, so pi never stripped the failed message from agent " +
4753
+ "state and a tree rollback would leave the tree and agent state disagreeing (auto-compaction is off)",
4754
+ "message_end",
4755
+ ctx,
4756
+ );
4757
+ };
4758
+
4759
+ /**
4760
+ * ROLLBACK, COMMIT, REPLAY.
4761
+ *
4762
+ * pi has already stripped the trailing error message from agent state by the time
4763
+ * this fires, so agent state IS the rolled-back window and all that remains is to
4764
+ * move the session leaf to match. The label goes on FIRST, because a label appends at
4765
+ * the current leaf and advances it, so applied after the branch it would mark the
4766
+ * surviving path instead of the abandoned one.
4767
+ *
4768
+ * The commit deliberately does NOT happen here. The retried request runs its own
4769
+ * `context` pass, and that pass folds and commits before the payload is built. A
4770
+ * commit here would adjudicate a snapshot taken outside a projection pass and then be
4771
+ * evaluated again by the retry, which is two mutations for one episode.
4772
+ */
4773
+ const rebuiltMessageCount = (manager: Record<string, any>): number | null => {
4774
+ try {
4775
+ const rebuilt = manager.buildSessionContext?.();
4776
+ return Array.isArray(rebuilt?.messages) ? rebuilt.messages.length : null;
4777
+ } catch { return null; }
4778
+ };
4779
+
4780
+ const recoverFromOverflow = (event: Record<string, unknown>, ctx: any): void => {
4781
+ rollback.attempts += 1;
4782
+ rollback.pendingOverflow = null;
4783
+ if (!rollback.armed) {
4784
+ refuseRollback(
4785
+ rollback.probes?.failures.join("; ") ?? "the rollback probes never ran",
4786
+ "session_before_compact",
4787
+ ctx,
4788
+ );
4789
+ return;
4790
+ }
4791
+ const shape = overflowEventShape(event);
4792
+ if (!shape.ok) {
4793
+ refuseRollback(`the overflow event is missing ${shape.missing.join(", ")}`, "session_before_compact", ctx);
4794
+ return;
4795
+ }
4796
+ const branchEntries = ownValue(event, "branchEntries");
4797
+ // Copied, not aliased. `getBranch()` hands back the live array and the branch call
4798
+ // below truncates it in place, so a handler holding the original reference watches
4799
+ // the entries it is still reasoning about disappear underneath it.
4800
+ const entries = Array.isArray(branchEntries) ? [...branchEntries] : [];
4801
+ const errorEntry = findOverflowErrorEntry(entries);
4802
+ if (!errorEntry) {
4803
+ refuseRollback("the rejected assistant entry is not the branch tail", "session_before_compact", ctx);
4804
+ return;
4805
+ }
4806
+ const manager = ctx.sessionManager as Record<string, any>;
4807
+ const oldLeafId = manager.getLeafId();
4808
+ const sessionMessagesBefore = rebuiltMessageCount(manager);
4809
+ const abandoned = entries.slice(errorEntry.index);
4810
+ const rolledBackBytes = bytes(abandoned.flatMap((entry: any) => sessionEntryMessages(entry) ?? []));
4811
+ // Lineage first. The abandoned path stays in the tree, and it says why it was left.
4812
+ try {
4813
+ manager.appendLabelChange(errorEntry.id, `${entryTypePrefix} overflow rollback`);
4814
+ } catch (error) {
4815
+ refuseRollback(`the lineage label failed (${String(error)})`, "session_before_compact", ctx);
4816
+ return;
4817
+ }
4818
+ try {
4819
+ if (errorEntry.parentId === null) manager.resetLeaf();
4820
+ else manager.branch(errorEntry.parentId);
4821
+ } catch (error) {
4822
+ refuseRollback(`the branch failed (${String(error)})`, "session_before_compact", ctx);
4823
+ return;
4824
+ }
4825
+ // The invariant, measured rather than assumed: the rolled-back window has to be
4826
+ // exactly the failed entry shorter, or the retry sends something nobody built.
4827
+ const errorEntryMessages = (sessionEntryMessages(entries[errorEntry.index]) ?? []).length;
4828
+ const sessionMessagesAfter = rebuiltMessageCount(manager);
4829
+ const holds = sessionMessagesBefore === null || sessionMessagesAfter === null ||
4830
+ preStripHolds({ sessionMessagesBefore, sessionMessagesAfter, errorEntryMessages });
4831
+ const unanswered = unansweredToolCalls(entries.slice(0, errorEntry.index));
4832
+ let replaySkipReason: string | null = null;
4833
+ if (!holds) {
4834
+ replaySkipReason = `the rolled-back window came back ${sessionMessagesAfter} messages against the ` +
4835
+ `${(sessionMessagesBefore ?? 0) - errorEntryMessages} the rollback should have left`;
4836
+ } else if (unanswered.length) {
4837
+ replaySkipReason = `the rolled-back tail leaves ${unanswered.length} tool call(s) unanswered, and a user ` +
4838
+ "turn after an unsatisfied tool call is a malformed transcript for every provider";
4839
+ }
4840
+ const occupancyBefore = measurements.lastProviderMeasurement?.tokens ?? null;
4841
+ const notice = rollbackNoticeText({
4842
+ brandNoun,
4843
+ toolName,
4844
+ tokensRolledBack: estimatedTokens(rolledBackBytes),
4845
+ entriesAbandoned: abandoned.length,
4846
+ replayed: replaySkipReason === null,
4847
+ replaySkipReason,
4848
+ });
4849
+ // Exactly ONE steered message. Steering drains one at a time at the top of the
4850
+ // retried loop, so a second would be silently held to a turn that never comes.
4851
+ let replayed = false;
4852
+ if (replaySkipReason === null) {
4853
+ if (typeof pi.sendMessage !== "function") {
4854
+ replaySkipReason = "this pi build exposes no message verb, so the retry cannot be steered";
4855
+ } else {
4856
+ try {
4857
+ pi.sendMessage(
4858
+ { customType: `${entryTypePrefix}-overflow-recovery`, content: notice },
4859
+ { deliverAs: "steer" },
4860
+ );
4861
+ replayed = true;
4862
+ } catch (error) {
4863
+ replaySkipReason = `the recovery notice could not be queued (${String(error)})`;
4864
+ }
4865
+ }
4866
+ }
4867
+ if (!replayed) safeNotify(ctx, notice, "warning");
4868
+ const record = emit("context.rollback", {
4869
+ trigger: "session_before_compact",
4870
+ armed: true,
4871
+ disarm_reason: null,
4872
+ error_entry_id: errorEntry.id,
4873
+ old_leaf_id: typeof oldLeafId === "string" ? oldLeafId : null,
4874
+ new_leaf_id: errorEntry.parentId,
4875
+ entries_abandoned: abandoned.length,
4876
+ occupancy_tokens_before: occupancyBefore,
4877
+ tokens_rolled_back: estimatedTokens(rolledBackBytes),
4878
+ replayed,
4879
+ replay_skip_reason: replaySkipReason,
4880
+ notice_chars: notice.length,
4881
+ attempt_ordinal: rollback.attempts,
4882
+ probes_passed: true,
4883
+ });
4884
+ rollback.last = {
4885
+ seq: record.seq,
4886
+ errorEntryId: errorEntry.id,
4887
+ newLeafId: errorEntry.parentId,
4888
+ entriesAbandoned: abandoned.length,
4889
+ tokensRolledBack: estimatedTokens(rolledBackBytes),
4890
+ replayed,
4891
+ replaySkipReason,
4892
+ noticeChars: notice.length,
4893
+ attemptOrdinal: rollback.attempts,
4894
+ };
4895
+ // The tree moved, so the mapping is rebuilt from it. THEN the fold is armed for the
4896
+ // retried pass, in that order: a reload clears the recovery flags, and arming before
4897
+ // it would hand the retry a cleared one. The guards are waived in that pass because
4898
+ // a request that already did not fit has nothing left to economize.
4899
+ load(ctx, true);
4900
+ curation.pendingRejection = { status: 400, ordinal: currentOrdinal() };
4901
+ };
4902
+
3173
4903
  pi.on("session_before_compact", (event: Record<string, unknown>, ctx: any) => {
3174
4904
  const reason = ownValue(event, "reason");
4905
+ // The overflow reason is a provider rejection pi is prepared to retry, and it is
4906
+ // the rollback lane's trigger. Threshold and manual compaction are unchanged.
4907
+ if (reason === "overflow" && ownValue(event, "willRetry") === true) {
4908
+ nativeCompaction.lastThresholdDecision = {
4909
+ handled: true,
4910
+ retry: true,
4911
+ reason: `provider input overflow; ${contextBrand(brandNoun)} rolled the session tree back instead`,
4912
+ compactionReason: reason,
4913
+ nativeCompactionCompleted: false,
4914
+ };
4915
+ try { recoverFromOverflow(event, ctx); }
4916
+ catch (error) { suspendAutomatic(error, "overflow-rollback", ctx); }
4917
+ try { updateStatus(ctx); } catch { /* Recovery must survive presentation failure. */ }
4918
+ return { cancel: true };
4919
+ }
3175
4920
  if (reason === "manual") {
3176
4921
  nativeCompaction.lastThresholdDecision = {
3177
4922
  handled: false,
@@ -3192,6 +4937,7 @@ export function registerActiveContext(pi: any, options: {
3192
4937
  return { cancel: true };
3193
4938
  });
3194
4939
  pi.on("agent_settled", async (_event: unknown, ctx: any) => {
4940
+ reportUnclaimedOverflow(ctx);
3195
4941
  if (ladder.pendingManual && persistence.persisted && ladder.boundaryFailure === null) {
3196
4942
  cancelPreparation();
3197
4943
  persistence.state = clone(persistence.persisted);
@@ -3300,31 +5046,34 @@ export function registerActiveContext(pi: any, options: {
3300
5046
  pendingMarks: accounting.pending,
3301
5047
  eligibleMarks: accounting.eligibleMarks,
3302
5048
  retainedMarks: accounting.retainedMarks,
3303
- eligibleMarkedShare: accounting.eligibleFreedWindowShare,
3304
- markedShare: accounting.freedWindowShare,
5049
+ eligibleMarkedShare: accounting.eligibleFreedBudgetShare,
5050
+ markedShare: accounting.freedBudgetShare,
3305
5051
  available: true,
3306
5052
  automatic: {
3307
5053
  pressureRatio: measurements.latestRatio,
3308
- surfacing: {
3309
- rendered: false,
3310
- sources: surfacing.sources.map((source) => source.id),
3311
- log: clone(persistence.state.surfacing ?? []),
3312
- },
3313
- warningRatio: snapshot.policy.warningRatio,
3314
- toolFoldRatio: snapshot.policy.toolFoldRatio,
5054
+ // Status is a READ of the slate, never a delivery of one. `status` is a
5055
+ // read-only context action and the tool-batch safety scan is built on
5056
+ // read-only actions not writing durable state, so asking must not issue a
5057
+ // suggestion, spend the precision budget, or move a suppression counter. An
5058
+ // agent that asks gets the answer; only the push carriers are metered.
5059
+ surfacing: statusSurfacing(snapshot),
3315
5060
  refoldRatio: snapshot.policy.refoldRatio,
3316
5061
  chapterPrepareRatio: snapshot.policy.prepareRatio,
3317
5062
  hardFenceRatio: hardFenceRatio(snapshot),
3318
- responseReserve: Math.min(
3319
- ACTIVE_CONTEXT_POLICY.responseReserve,
3320
- Math.floor(snapshot.contextWindow * 0.1),
3321
- ),
5063
+ // The declared policy, reported. No action reads it back as a setting.
5064
+ thresholds: { ...snapshot.thresholds },
5065
+ zones: {
5066
+ freshBoundary: snapshot.freshBoundary,
5067
+ budgetTokens: snapshot.budgetTokens,
5068
+ },
5069
+ // What THIS runtime withheld, which is nothing once the deployment declared an
5070
+ // already-net budget. One number, from the same accounting the fence divides by.
5071
+ responseReserve: currentCapacity(ctx).outputReservation,
3322
5072
  windowSource: snapshot.windowSource,
3323
5073
  capacity: {
3324
5074
  ...currentCapacity(ctx),
3325
5075
  bytesPerToken: projectionCharsPerToken(),
3326
5076
  },
3327
- consolidationRatio: snapshot.policy.consolidationRatio,
3328
5077
  providerMeasurement: measurements.lastProviderMeasurement ? {
3329
5078
  tokens: measurements.lastProviderMeasurement.tokens,
3330
5079
  contextWindow: measurements.lastProviderMeasurement.contextWindow,
@@ -3346,8 +5095,7 @@ export function registerActiveContext(pi: any, options: {
3346
5095
  lastAutomaticAction: ladder.lastAutomaticAction,
3347
5096
  overBudgetReduction: ladder.overBudgetReduction ? clone(ladder.overBudgetReduction) : null,
3348
5097
  curation: {
3349
- occupancyThreshold: CURATION_OCCUPANCY_SHARE,
3350
- staleToolThreshold: CURATION_STALE_TOOL_SHARE,
5098
+ occupancyThreshold: thresholds.maxTarget,
3351
5099
  signals: curation.lastSignals ? { ...curation.lastSignals } : null,
3352
5100
  contextCalls: curation.contextCalls,
3353
5101
  receipts: curation.receipts.map((receipt) => ({ ...receipt })),
@@ -3357,28 +5105,34 @@ export function registerActiveContext(pi: any, options: {
3357
5105
  maxAttempts: OVERFLOW_RECOVERY_MAX_ATTEMPTS,
3358
5106
  pendingRejection: curation.pendingRejection ? { ...curation.pendingRejection } : null,
3359
5107
  last: curation.lastRecovery ? clone(curation.lastRecovery) : null,
5108
+ rollback: {
5109
+ armed: rollback.armed,
5110
+ probeFailures: rollback.probes ? [...rollback.probes.failures] : null,
5111
+ classifier: rollback.classifierSource,
5112
+ attempts: rollback.attempts,
5113
+ last: rollback.last ? clone(rollback.last) : null,
5114
+ },
3360
5115
  },
3361
5116
  foldSpanCap: MAX_FOLD_SPAN_CHARS,
3362
5117
  projectionBudgetTokens: currentCapacity(ctx).budgetTokens,
3363
5118
  projectionCharsPerToken: projectionCharsPerToken(),
3364
5119
  projectionEstimatorErrorShare: estimatorErrorShare(),
3365
5120
  projectionExpectedInflowTokens: expectedInflowTokens(),
3366
- projectionMarginTokens: measurements.lastProjectedChars === null
3367
- ? null
3368
- : projectionMarginTokens(
3369
- Math.ceil(measurements.lastProjectedChars / projectionCharsPerToken()),
3370
- currentCapacity(ctx).window,
3371
- ),
3372
- projectionEstimatedTokens: measurements.lastProjectedChars === null
5121
+ // What the fence actually weighed, not a second arithmetic over the same bytes:
5122
+ // once occupancy is anchored, re-deriving it here from chars alone would report
5123
+ // a number no decision was made on.
5124
+ projectionMarginTokens: measurements.lastProjectedEstimate === null
3373
5125
  ? null
3374
- : Math.ceil(measurements.lastProjectedChars / projectionCharsPerToken()),
5126
+ : projectionMarginTokens(measurements.lastProjectedEstimate, currentCapacity(ctx).window),
5127
+ projectionEstimatedTokens: measurements.lastProjectedEstimate,
5128
+ projectionEstimateBasis: measurements.lastProjectedEstimateBasis,
5129
+ projectionAnchorTokens: measurements.projectionAnchor?.tokens ?? null,
3375
5130
  automaticSuspended: ladder.automaticFailure !== null,
3376
5131
  automaticFailure: ladder.automaticFailure ? clone(ladder.automaticFailure) : null,
3377
5132
  lastCompactionDecision: nativeCompaction.lastThresholdDecision,
3378
5133
  scheduling: schedulingStatus({
3379
5134
  snapshot,
3380
5135
  state: persistence.state,
3381
- mode: foldScheduling,
3382
5136
  ratio: measurements.latestRatio,
3383
5137
  }),
3384
5138
  nativeSummaries: "disabled",
@@ -3390,12 +5144,10 @@ export function registerActiveContext(pi: any, options: {
3390
5144
  // THE stream, as the durable entries carry it. One shape, one convention.
3391
5145
  events: clone(instrumentation.ledger.events),
3392
5146
  },
3393
- foldPeekResults,
3394
5147
  peek: {
3395
5148
  defaultMaxBytes: PEEK_DEFAULT_MAX_BYTES,
3396
- lifetime: "append-only",
5149
+ lifetime: "append-only; the copy is reclaimed at the next commit unless pinned",
3397
5150
  },
3398
- freeHarvest: blockingTools.size === 0 ? "disabled" : "enabled",
3399
5151
  pressureSource: "last-successful-provider-response-only",
3400
5152
  postOverflowCallback: "blocked-while-stock-native-compaction-is-disabled",
3401
5153
  sameOperationRetry: false,
@@ -3414,12 +5166,12 @@ export function registerActiveContext(pi: any, options: {
3414
5166
  ...(detail === "tree" ? { tree: foldTreeDetail(snapshot, persistence.state).slice(statusOffset) } : {}),
3415
5167
  }, typeof detail === "string" ? detail : null, statusOffset));
3416
5168
  }
3417
- // Peeking or expanding a suggested fold is the accept signal, recorded for the
3418
- // retrieval build even though nothing suggests anything today. It stays ephemeral:
3419
- // the mark rides the next persisted state, never an entry of its own.
5169
+ // Peeking or expanding a surfaced fold is the ACTED label, joined to the offer that
5170
+ // named it. It stays ephemeral: the ledger rides the next persisted state, never an
5171
+ // entry of its own, and the used/ignored grades land when the window closes.
3420
5172
  const noteSurfacingAccept = (id: string): void => {
3421
5173
  if (!persistence.state) return;
3422
- persistence.state = acceptSurfacingSuggestion(persistence.state, id, surfacingOrdinal(snapshot));
5174
+ persistence.state = noteSurfacingAction(persistence.state, id, markOrdinal(snapshot));
3423
5175
  };
3424
5176
  if (action === "peek") {
3425
5177
  const id = String(params.id ?? "").trim();
@@ -3592,7 +5344,7 @@ export function registerActiveContext(pi: any, options: {
3592
5344
  // An expand with marks pending opens the commit epoch: the restore and the
3593
5345
  // batch of folds then cost one rewrite between them instead of two.
3594
5346
  let epochApplied: unknown[] = [];
3595
- if (epochScheduling && action === "expand" && pendingMarks(persistence.state).length) {
5347
+ if (action === "expand" && pendingMarks(persistence.state).length) {
3596
5348
  const result = await commitPendingMarks({
3597
5349
  snapshot,
3598
5350
  state: persistence.state,
@@ -3637,12 +5389,39 @@ export function registerActiveContext(pi: any, options: {
3637
5389
  }
3638
5390
  if (action === "protect" || action === "unprotect") {
3639
5391
  const ids = stringIds(params.ids);
3640
- await persistManual(protectEvidence(snapshot, persistence.state, ids, action === "protect"), action, ctx);
5392
+ const refsBefore = persistence.state.protected.length;
5393
+ const nextProtection = protectEvidence(snapshot, persistence.state, ids, action === "protect");
5394
+ if (action === "protect") {
5395
+ // The pin ceiling: protect is a promise to hold bytes raw through every fold,
5396
+ // and a promise with no mass bound can pin the window solid. The refusal is
5397
+ // corrective, not denial: it names the cap and the release valve.
5398
+ const capacity = servingCapacity(snapshot.contextWindow);
5399
+ const prospectiveShare = capacity.budgetTokens > 0
5400
+ ? estimatedTokens(explicitProtectedMass(snapshot, nextProtection).bytes) / capacity.budgetTokens
5401
+ : 0;
5402
+ if (prospectiveShare > MAX_PINNED_SHARE) {
5403
+ throw new Error(
5404
+ `protect refused: these pins would hold ${Math.round(prospectiveShare * 100)}% of the ` +
5405
+ `working window raw, past the ${Math.round(MAX_PINNED_SHARE * 100)}% pinned-share cap. ` +
5406
+ 'Release earlier pins with {"action":"unprotect","ids":["<entry-id>"]} first; ' +
5407
+ "folding keeps entries exactly recoverable without pinning them.");
5408
+ }
5409
+ }
5410
+ await persistManual(nextProtection, action, ctx);
5411
+ const refsAfter = persistence.state.protected.length;
5412
+ emit("context.protect", {
5413
+ protect: action === "protect",
5414
+ ids: ids.join(","),
5415
+ id_count: ids.length,
5416
+ protected_refs_before: refsBefore,
5417
+ protected_refs_after: refsAfter,
5418
+ });
3641
5419
  updateStatus(ctx);
3642
5420
  return toolPayload({
3643
5421
  version: 1,
3644
5422
  action,
3645
5423
  ids,
5424
+ protectedRefs: refsAfter,
3646
5425
  activation: "durable immediately; projected on the next model call in this same turn",
3647
5426
  });
3648
5427
  }
@@ -3714,172 +5493,135 @@ export function registerActiveContext(pi: any, options: {
3714
5493
  }
3715
5494
  }
3716
5495
  const corrections = resolved.flatMap((item) => item.corrections);
3717
- if (epochScheduling) {
3718
- // Resolve every brief now, while the source is in hand, so the commit epoch
3719
- // itself stays deterministic and free of provider calls. A span that is not
3720
- // eligible YET cannot go through preparation at all, so it takes the
3721
- // deterministic brief: the mark is a decision, and refusing to record it
3722
- // because the span is momentarily fresh is exactly the drop being fixed.
3723
- const marks: Array<Record<string, unknown>> = [];
3724
- let staged = persistence.state;
3725
- for (const item of resolved) {
3726
- const { candidate } = item;
3727
- // ACCEPT AND HOLD. A span that is still fresh or protected is not a refusal:
3728
- // the mark is a standing decision, it is recorded now, and it folds at the
3729
- // first commit after the span ages out. Refusal is reserved for a mark that
3730
- // cannot be constructed at all.
3731
- // A REPEATED DECISION IS INERT. The fold id is derived from the span, so a
3732
- // span the ladder already prepared carries that exact id in the forest
3733
- // already, and preparing it a second time throws "Prepared fold already
3734
- // exists" out of the tool call. Under the old cadence commits were frequent
3735
- // enough to drain prepared folds before a second decision could land on one;
3736
- // the quiet runtime commits far less often, so the collision became reachable
3737
- // in ordinary use -- the ladder marks a span, the agent then folds the same
3738
- // span, and the agent's call fails. Marking is a standing decision about a
3739
- // span, and deciding the same thing twice has to be a no-op.
3740
- const alreadyPrepared = staged.folds.some((fold) =>
3741
- fold.id === foldIdFor(candidate.kind, candidate.parts));
3742
- const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
3743
- (candidate.kind === "tool-result" &&
3744
- toolRefsProtected(candidate.sourceRefs, staged, snapshot));
3745
- const briefed = !deferred && !alreadyPrepared
3746
- ? await prepareFold({
3747
- candidate,
3748
- snapshot,
3749
- state: staged,
3750
- generation: lifecycle.generation,
3751
- brief: item.brief,
3752
- summarize: options.summarizeContextSpan,
3753
- ctx,
3754
- signal,
3755
- })
3756
- : null;
3757
- const mark = foldMarkFor({
5496
+ // Resolve every brief now, while the source is in hand, so the commit epoch
5497
+ // itself stays deterministic and free of provider calls. A span that is not
5498
+ // eligible YET cannot go through preparation at all, so it takes the
5499
+ // deterministic brief: the mark is a decision, and refusing to record it
5500
+ // because the span is momentarily fresh is exactly the drop being fixed.
5501
+ const marks: Array<Record<string, unknown>> = [];
5502
+ let staged = persistence.state;
5503
+ for (const item of resolved) {
5504
+ const { candidate } = item;
5505
+ // ACCEPT AND HOLD. A span that is still fresh or protected is not a refusal:
5506
+ // the mark is a standing decision, it is recorded now, and it folds at the
5507
+ // first commit after the span ages out. Refusal is reserved for a mark that
5508
+ // cannot be constructed at all.
5509
+ // A REPEATED DECISION IS INERT. The fold id is derived from the span, so a
5510
+ // span the ladder already prepared carries that exact id in the forest
5511
+ // already, and preparing it a second time throws "Prepared fold already
5512
+ // exists" out of the tool call. Under the old cadence commits were frequent
5513
+ // enough to drain prepared folds before a second decision could land on one;
5514
+ // the quiet runtime commits far less often, so the collision became reachable
5515
+ // in ordinary use -- the ladder marks a span, the agent then folds the same
5516
+ // span, and the agent's call fails. Marking is a standing decision about a
5517
+ // span, and deciding the same thing twice has to be a no-op.
5518
+ const alreadyPrepared = staged.folds.some((fold) =>
5519
+ fold.id === foldIdFor(candidate.kind, candidate.parts));
5520
+ const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
5521
+ (candidate.kind === "tool-result" &&
5522
+ toolRefsProtected(candidate.sourceRefs, staged, snapshot));
5523
+ const briefed = !deferred && !alreadyPrepared
5524
+ ? await prepareFold({
3758
5525
  candidate,
3759
- brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
3760
- briefProvenance: briefed?.fold.provenance ??
3761
- (item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
3762
- origin: "agent",
3763
- ordinal: markOrdinal(snapshot),
3764
- });
3765
- const addition = addPendingMark(staged, mark);
3766
- if (!addition.added) {
3767
- marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
3768
- continue;
3769
- }
3770
- staged = addition.state;
3771
- marks.push({
3772
- id: mark.id,
3773
- kind: mark.kind,
3774
- ok: true,
3775
- deferred,
3776
- ...(deferred
3777
- ? {
3778
- scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
3779
- "first commit after it ages out",
3780
- }
3781
- : {}),
3782
- brief: mark.brief,
3783
- provenance: normalizeLegacyProvenance(mark.briefProvenance),
3784
- });
5526
+ snapshot,
5527
+ state: staged,
5528
+ generation: lifecycle.generation,
5529
+ brief: item.brief,
5530
+ summarize: observedSummarize,
5531
+ ctx,
5532
+ signal,
5533
+ })
5534
+ : null;
5535
+ const mark = foldMarkFor({
5536
+ candidate,
5537
+ brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
5538
+ briefProvenance: briefed?.fold.provenance ??
5539
+ (item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
5540
+ origin: "agent",
5541
+ ordinal: markOrdinal(snapshot),
5542
+ });
5543
+ const addition = addPendingMark(staged, mark);
5544
+ if (!addition.added) {
5545
+ marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
5546
+ continue;
3785
5547
  }
3786
- await persistManual(staged, action, ctx);
3787
- updateStatus(ctx);
3788
- const accounting = markAccounting(snapshot, persistence.state);
3789
- // MECHANISM 3. The projection is byte-frozen between fold events, so this reply
3790
- // is the only cache-free channel left: it carries the whole picture, not just
3791
- // an acknowledgement of the spans this call named.
3792
- const held = pendingMarks(persistence.state).map((mark) => ({
5548
+ staged = addition.state;
5549
+ marks.push({
3793
5550
  id: mark.id,
3794
5551
  kind: mark.kind,
3795
- tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
3796
- }));
3797
- const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
3798
- // The single-span shape is the head of the batched one: one call carrying one
3799
- // span answers exactly as it always did, and a batch adds fields rather than
3800
- // replacing them.
3801
- const only = marks.length === 1 && marks[0].ok === true ? marks[0] : null;
3802
- return toolPayload({
3803
- version: 1,
3804
- action,
3805
- scheduling: "epoch",
3806
- ok: marks.some((mark) => mark.ok === true),
3807
- deferredMarks: marks.filter((mark) => mark.deferred === true).length,
3808
- ...(only
5552
+ ok: true,
5553
+ deferred,
5554
+ ...(deferred
3809
5555
  ? {
3810
- id: only.id,
3811
- kind: only.kind,
3812
- brief: only.brief,
3813
- provenance: only.provenance,
3814
- deferred: only.deferred,
3815
- ...(only.scheduled ? { scheduled: only.scheduled } : {}),
5556
+ scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
5557
+ "first commit after it ages out",
3816
5558
  }
3817
5559
  : {}),
3818
- marks,
3819
- // Never silently reinterpreted: a span the runtime moved says so, here.
3820
- corrections,
3821
- argumentsSha256: executionArgumentsSha256,
3822
- durableRevision: persistence.state.revision,
3823
- pendingMarks: accounting.pending,
3824
- eligibleMarks: accounting.eligibleMarks,
3825
- retainedMarks: accounting.retainedMarks,
3826
- estimatedFreedWindowShare: accounting.freedWindowShare,
3827
- estimatedEligibleWindowShare: accounting.eligibleFreedWindowShare,
3828
- estimatedRewriteTokens: accounting.rewriteTokens,
3829
- held,
3830
- heldNote: "held until it ages out or the next fold event",
3831
- unmarkedSpans: remainder.spans,
3832
- unmarkedTokens: remainder.tokens,
3833
- unmarkedShare: remainder.share,
3834
- unmarkedCandidates: remainder.candidates,
3835
- awareness: markAwarenessText({ held, remainder, toolName, brandNoun }),
3836
- activation: "accepted as pending marks; no context bytes moved, and nothing else in your " +
3837
- "context changed either. They apply together at the next commit epoch, which the runtime " +
3838
- "opens at the fold event. " +
3839
- "A mark over a still-fresh span is held, not refused: it is scheduled, and it folds at the " +
3840
- "first commit after that span ages out of the fresh window. " +
3841
- "Mark several spans in one call: this whole picture comes back with each one.",
3842
- });
3843
- }
3844
- const applied: Array<Record<string, unknown>> = [];
3845
- for (const item of resolved) {
3846
- const { preparedFold, nextState } = await prepareAndCommitExplicit({
3847
- snapshot,
3848
- candidate: item.candidate,
3849
- brief: item.brief,
3850
- ctx,
3851
- signal,
3852
- });
3853
- persistence.state = nextState;
3854
- applied.push({
3855
- id: preparedFold.id,
3856
- kind: preparedFold.fold.kind,
3857
- brief: preparedFold.fold.brief,
3858
- provenance: normalizeLegacyProvenance(preparedFold.fold.provenance),
3859
- expand: { action: "expand", id: preparedFold.id },
5560
+ brief: mark.brief,
5561
+ provenance: normalizeLegacyProvenance(mark.briefProvenance),
3860
5562
  });
3861
5563
  }
3862
- await persistManual(persistence.state, "fold", ctx);
5564
+ await persistManual(staged, action, ctx);
3863
5565
  updateStatus(ctx);
3864
- const single = applied.length === 1 ? applied[0] : null;
5566
+ const accounting = markAccounting(snapshot, persistence.state);
5567
+ // MECHANISM 3. The projection is byte-frozen between fold events, so this reply
5568
+ // is the only cache-free channel left: it carries the whole picture, not just
5569
+ // an acknowledgement of the spans this call named.
5570
+ const held = pendingMarks(persistence.state).map((mark) => ({
5571
+ id: mark.id,
5572
+ kind: mark.kind,
5573
+ tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
5574
+ }));
5575
+ const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
5576
+ // The single-span shape is the head of the batched one: one call carrying one
5577
+ // span answers exactly as it always did, and a batch adds fields rather than
5578
+ // replacing them.
5579
+ const only = marks.length === 1 && marks[0].ok === true ? marks[0] : null;
3865
5580
  return toolPayload({
3866
5581
  version: 1,
3867
5582
  action,
3868
- scheduling: "immediate",
3869
- ...(single
5583
+ scheduling: "epoch",
5584
+ ok: marks.some((mark) => mark.ok === true),
5585
+ deferredMarks: marks.filter((mark) => mark.deferred === true).length,
5586
+ ...(only
3870
5587
  ? {
3871
- id: single.id,
3872
- kind: single.kind,
3873
- brief: single.brief,
3874
- provenance: single.provenance,
3875
- expand: single.expand,
5588
+ id: only.id,
5589
+ kind: only.kind,
5590
+ brief: only.brief,
5591
+ provenance: only.provenance,
5592
+ deferred: only.deferred,
5593
+ ...(only.scheduled ? { scheduled: only.scheduled } : {}),
3876
5594
  }
3877
5595
  : {}),
3878
- folds: applied,
5596
+ marks,
5597
+ // Never silently reinterpreted: a span the runtime moved says so, here.
3879
5598
  corrections,
3880
5599
  argumentsSha256: executionArgumentsSha256,
3881
5600
  durableRevision: persistence.state.revision,
3882
- activation: "durable immediately; projected on the next model call in this same turn",
5601
+ pendingMarks: accounting.pending,
5602
+ eligibleMarks: accounting.eligibleMarks,
5603
+ retainedMarks: accounting.retainedMarks,
5604
+ estimatedFreedWindowShare: accounting.freedBudgetShare,
5605
+ estimatedEligibleWindowShare: accounting.eligibleFreedBudgetShare,
5606
+ estimatedRewriteTokens: accounting.rewriteTokens,
5607
+ held,
5608
+ heldNote: "held until it ages out or the next fold event",
5609
+ unmarkedSpans: remainder.spans,
5610
+ unmarkedTokens: remainder.tokens,
5611
+ unmarkedShare: remainder.share,
5612
+ unmarkedCandidates: remainder.candidates,
5613
+ // The acknowledgement rides the tool result, so it PERSISTS in context the way
5614
+ // every tool result does. Default on;
5615
+ // `guidance.actionResponses: false` returns the accounting without the prose.
5616
+ ...(guidance.actionResponses ? {
5617
+ awareness: markAwarenessText({ held, remainder, toolName, brandNoun }),
5618
+ activation: "accepted as pending marks; no context bytes moved, and nothing else in your " +
5619
+ "context changed either. They apply together at the next commit epoch, which the runtime " +
5620
+ "opens at the fold event. " +
5621
+ "A mark over a still-fresh span is held, not refused: it is scheduled, and it folds at the " +
5622
+ "first commit after that span ages out of the fresh window. " +
5623
+ "Mark several spans in one call: this whole picture comes back with each one.",
5624
+ } : {}),
3883
5625
  });
3884
5626
  }
3885
5627
  throw new Error(`Unknown ${toolName} action '${action}'`);
@@ -3899,7 +5641,7 @@ export function registerActiveContext(pi: any, options: {
3899
5641
  };
3900
5642
 
3901
5643
  const toolHandler = async (
3902
- _toolCallId: string,
5644
+ toolCallId: string,
3903
5645
  params: Record<string, unknown>,
3904
5646
  signal: AbortSignal,
3905
5647
  _onUpdate: unknown,
@@ -3915,6 +5657,9 @@ export function registerActiveContext(pi: any, options: {
3915
5657
  action,
3916
5658
  ok,
3917
5659
  error,
5660
+ // The join key against the worker's own tool-call log: one logical row per
5661
+ // model-emitted invocation, matched by id and never by order or clock.
5662
+ tool_call_id: toolCallId,
3918
5663
  marks_requested: requestedMarkCount(params),
3919
5664
  corrections_applied: corrections.length,
3920
5665
  arguments_sha256: sha256Value(params ?? {}),
@@ -3972,11 +5717,39 @@ export function registerActiveContext(pi: any, options: {
3972
5717
  if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
3973
5718
  const divider = args.indexOf(" -- ");
3974
5719
  const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
5720
+ // THE USER COMMIT. One word, and the only commit verb any caller has: the model
5721
+ // tool still has no commit action, because a verb the runtime is entitled to
5722
+ // overrule is surface without function. A user is not overruled.
5723
+ //
5724
+ // Below maxTarget it applies the eligible marks in hand and stops -- no automatic
5725
+ // top-up, because the user asked to bank the curation they made, not to have the
5726
+ // runtime pick more. At or above maxTarget it is the ordinary event: the planner
5727
+ // may top up toward minTarget. Neither path touches freshness, pins, exactness or
5728
+ // the provider fence.
5729
+ if (selector === "commit") {
5730
+ const capacity = servingCapacity(snapshot.contextWindow);
5731
+ const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
5732
+ ? capacity.usedTokens / capacity.budgetTokens
5733
+ : null;
5734
+ const topUp = occupancy !== null && occupancy >= thresholds.maxTarget;
5735
+ const committed = await runCommitEpoch(snapshot, "user-command", topUp, measurements.latestRatio, true);
5736
+ try { await persist(ctx); } catch { /* The commit already reported its own outcome. */ }
5737
+ updateStatus(ctx);
5738
+ safeNotify(
5739
+ ctx,
5740
+ committed
5741
+ ? `Committed ${ownValue(committed, "applied_marks") ?? 0} mark(s)${topUp ? " with automatic top-up" : ""}. ` +
5742
+ "Exact source remains expandable."
5743
+ : "Nothing eligible to commit: no pending mark is outside the fresh tail.",
5744
+ "info",
5745
+ );
5746
+ return;
5747
+ }
3975
5748
  const supplied = (divider >= 0 ? args.slice(divider + 4) : "").trim() || undefined;
3976
5749
  const ids = selector ? selector.replace(/\.\./g, " ").split(/[\s,]+/).filter(Boolean) : [];
3977
5750
  const candidate = ids.length
3978
5751
  ? manualFoldCandidate(snapshot, persistence.state!, ids)
3979
- : selectAutomaticChapter(snapshot, persistence.state!) ?? selectAutomaticToolBatch(snapshot, persistence.state!, 1)[0] ?? null;
5752
+ : selectAutomaticChapter(snapshot, persistence.state!) ?? selectAutomaticToolBatch(snapshot, persistence.state!)[0] ?? null;
3980
5753
  if (!candidate) throw new Error("No exact stale rescue span is currently eligible");
3981
5754
  const stateBefore = persistence.state!;
3982
5755
  const persistedBefore = persistence.persisted ? clone(persistence.persisted) : null;
@@ -4027,7 +5800,7 @@ export function registerActiveContext(pi: any, options: {
4027
5800
  name: toolName,
4028
5801
  label: toolLabel,
4029
5802
  allowedActions: allowedToolActions,
4030
- fullSurface: allowedToolActions.length === defaultToolActions.length,
5803
+ fullSurface: true,
4031
5804
  maxBriefChars: ACTIVE_CONTEXT_POLICY.maxBriefChars,
4032
5805
  statusDetails: ["fold_candidates", "tree", "folds", "objects"],
4033
5806
  minPeekSliceBytes: PEEK_MIN_SLICE_BYTES,
@@ -4049,6 +5822,7 @@ export function registerActiveContext(pi: any, options: {
4049
5822
  lifecycle.generation += 1;
4050
5823
  lifecycle.shuttingDown = true;
4051
5824
  cancelPreparation();
5825
+ cancelBriefUpgrades();
4052
5826
  persistence.state = null;
4053
5827
  persistence.persisted = null;
4054
5828
  lifecycle.latestSnapshot = null;
@@ -4056,5 +5830,5 @@ export function registerActiveContext(pi: any, options: {
4056
5830
  try { ctx.ui?.setStatus?.(entryTypePrefix, undefined); } catch { /* Shutdown cannot be blocked by UI. */ }
4057
5831
  });
4058
5832
 
4059
- return { projectionCandidates, registerSuggestionSource };
5833
+ return { projectionCandidates };
4060
5834
  }