pi-fold 1.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ import {
12
12
  prepareFold,
13
13
  renderFold,
14
14
  renderFoldParts,
15
+ selectAutomaticChapter,
15
16
  selectAutomaticSpan,
16
17
  setFoldProjectionState,
17
18
  } from "./folding.ts";
@@ -19,11 +20,10 @@ import type { AutomaticRungSelection } from "./folding.ts";
19
20
  import {
20
21
  foldInterval,
21
22
  budgetOccupancy,
22
- hardFenceRatio,
23
23
  mappedByKey,
24
24
  orderedRoots,
25
+ protectedStaleMass,
25
26
  refsProtected,
26
- toolRefsProtected,
27
27
  } from "./measurement.ts";
28
28
  import {
29
29
  childFoldIds,
@@ -33,12 +33,10 @@ import {
33
33
  parsePendingMarks,
34
34
  pendingMarkKey,
35
35
  } from "./persistence.ts";
36
- import { currentTurnRefKeys } from "./transcript.ts";
37
36
  import {
38
- EPOCH_MAX_TOPUP_MARKS,
39
37
  ESTIMATED_BYTES_PER_TOKEN,
40
- MAX_WEDGE_ABSORB_TOKENS,
41
38
  ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES,
39
+ MAX_FRONTIER_CUTS_PER_PASS,
42
40
  MAX_PENDING_MARKS,
43
41
  MAX_UNMARKED_CANDIDATES,
44
42
  } from "./policy.ts";
@@ -59,66 +57,30 @@ import {
59
57
  partsForRange,
60
58
  peekedSourceFoldIds,
61
59
  selectAutomaticConsolidations,
60
+ selectAutomaticRefold,
62
61
  spanBytes,
63
62
  deterministicChapterCandidateBrief,
64
63
  deterministicConsolidationBrief,
65
64
  resultCall,
66
65
  } from "./selection.ts";
67
66
 
68
- /**
69
- * Two-phase fold scheduling.
70
- *
71
- * A provider prefix cache is positional: editing the middle of the window
72
- * invalidates every byte after the edit, so per-turn folding pays a full uncached
73
- * prefix over and over. Nothing here changes what a fold MEANS. A fold decision
74
- * becomes a MARK, which moves no projection byte, and a later COMMIT applies every
75
- * pending mark through the same preparation and commit machinery an immediate fold
76
- * uses, in one rewrite.
77
- *
78
- * Every function is deterministic: transcript ordinals and positions only, never a
79
- * wall clock and never randomness.
80
- */
81
-
82
- /** The empty read, shared and frozen, so a state with no marks allocates nothing. */
83
67
  const NO_PENDING_MARKS: readonly PendingMark[] = Object.freeze([]);
84
68
 
85
- /**
86
- * The pending marks as a READ VIEW, not a copy.
87
- *
88
- * Every caller here reads: membership tests, accounting sums, and filters that build
89
- * a fresh array anyway. The copy belongs on the WRITE path, and `withPendingMarks`
90
- * already takes it, so taking a second one on every read bought nothing and cost a
91
- * great deal: `markSpanRefs` calls this once per mark and is itself called once per
92
- * mark by the accounting, eligibility, staleness, current-turn and absorption
93
- * readings, which made a deep clone of the whole array quadratic in the mark count
94
- * per pass. Profiled at 21 percent of a 120-turn session, the single largest cost in
95
- * the runtime, ahead of every projection and hash.
96
- *
97
- * The view is safe because the state is replaced rather than edited: no code path
98
- * mutates `state.pendingMarks` in place, and the readonly type is what keeps it so.
99
- */
100
69
  export function pendingMarks(state: Pick<ActiveContextState, "pendingMarks">): readonly PendingMark[] {
101
70
  return state.pendingMarks ?? NO_PENDING_MARKS;
102
71
  }
103
72
 
104
- /**
105
- * Property ORDER matters here: the state digest is a stable stringify, so an
106
- * appended key would replay as digest drift. Rebuild the tail of the record in the
107
- * same order `parseActiveContextState` produces.
108
- */
109
73
  export function withPendingMarks(
110
74
  state: ActiveContextState,
111
75
  marks: readonly PendingMark[],
112
76
  ): ActiveContextState {
113
77
  const next = marks.length ? parsePendingMarks(clone(marks) as PendingMark[]) : [];
114
78
  const head = { ...state };
115
- delete head.surfacing;
116
79
  delete head.pendingMarks;
117
80
  delete head.advisory;
118
81
  delete head.prepared;
119
82
  return {
120
83
  ...head,
121
- ...(state.surfacing?.length ? { surfacing: clone(state.surfacing) } : {}),
122
84
  ...(next.length ? { pendingMarks: next } : {}),
123
85
  ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
124
86
  ...(state.prepared ? { prepared: clone(state.prepared) } : {}),
@@ -131,7 +93,6 @@ export interface MarkAddition {
131
93
  reason: string | null;
132
94
  }
133
95
 
134
- /** Idempotent by mark key, bounded, and never silently dropping the oldest mark. */
135
96
  export function addPendingMark(state: ActiveContextState, mark: PendingMark): MarkAddition {
136
97
  const marks = pendingMarks(state);
137
98
  if (marks.some((existing) => pendingMarkKey(existing) === pendingMarkKey(mark))) {
@@ -151,43 +112,15 @@ export function estimatedTokens(sizeInBytes: number): number {
151
112
  return Math.max(0, Math.ceil(sizeInBytes / ESTIMATED_BYTES_PER_TOKEN));
152
113
  }
153
114
 
154
- /** Transcript-position ordering used for marks; identical to the surfacing ordinal. */
155
115
  export function markOrdinal(snapshot: Pick<ActiveContextSnapshot, "mapped">): number {
156
116
  return snapshot.mapped.length;
157
117
  }
158
118
 
159
119
  export interface MarkSpanRefs {
160
- /** Every evidence ref the mark's parts resolve to against the state in hand. */
161
120
  refs: EvidenceRef[];
162
- /**
163
- * The first part naming a fold the state does not hold, or null when the span reads
164
- * in full. `pending` is the difference between a defer and a drop: a mark still
165
- * standing carries the id its committed fold will have, so that fold is about to
166
- * exist, and where no mark names it nothing ever will.
167
- */
168
121
  unresolved: { foldId: string; pending: boolean } | null;
169
122
  }
170
123
 
171
- /**
172
- * THE ONE READING OF A MARK'S SPAN, AND IT ANSWERS RATHER THAN THROWS.
173
- *
174
- * A mark's parts name folds by id. Between the mark and the commit the state can stop
175
- * holding one: `reboundary` dissolves a root while consulting only `fold.parentId`, so
176
- * nothing there consults the pending marks, and the mark is left naming a fold that is
177
- * gone. `candidateSourceRefs` answers that with `Missing candidate child`, which is
178
- * right for a candidate being prepared and wrong for every reading that merely reports
179
- * on a mark: accounting, eligibility, staleness, the current-turn guard, the claimed
180
- * keys and wedge absorption all ran that throw out of a status call or a commit pass,
181
- * on state the runtime itself produced (Shane 2026-08-10).
182
- *
183
- * So the readings share this, and it is total. Raw parts resolve; a held child is
184
- * flattened; an unheld child is REPORTED, with the refs either side of it still
185
- * collected, because a partial span is the honest reading of a partly resolvable mark
186
- * and a guard that can see half the evidence is better than one that sees none. A
187
- * repeated fold on the path reads as unresolved and not pending: a corrupt forest costs
188
- * one mark the ordinary drop rather than hanging the caller, which is the same stop the
189
- * apply order's `placing` set carries.
190
- */
191
124
  export function markSpanRefs(state: ActiveContextState, mark: PendingMark): MarkSpanRefs {
192
125
  const byId = new Map(state.folds.map((fold) => [fold.id, fold] as const));
193
126
  const marks = pendingMarks(state);
@@ -230,11 +163,6 @@ export function markSpanRefs(state: ActiveContextState, mark: PendingMark): Mark
230
163
  return { refs, unresolved };
231
164
  }
232
165
 
233
- /**
234
- * Estimated bytes a mark would remove from the projection. Fold marks use a
235
- * placeholder estimate rather than a trial fold: the number is presentational, so
236
- * paying a full preparation per mark on every status call is not worth exactness.
237
- */
238
166
  export function markFreedBytes(
239
167
  snapshot: ActiveContextSnapshot,
240
168
  state: ActiveContextState,
@@ -257,41 +185,6 @@ export function markFreedBytes(
257
185
  return Math.max(0, bytes(source) - placeholder);
258
186
  }
259
187
 
260
- /**
261
- * Whether a pending mark covers any evidence the current excursion just gathered.
262
- *
263
- * Read off whatever the span resolves to. A mark naming an unheld fold still answers
264
- * here, on the parts that DO resolve, so a span holding both fresh raw evidence and a
265
- * child that has not landed is guarded on the fresh evidence instead of throwing the
266
- * whole commit.
267
- */
268
- export function markTouchesCurrentTurn(
269
- state: ActiveContextState,
270
- mark: PendingMark,
271
- currentTurn: ReadonlySet<string>,
272
- ): boolean {
273
- if (!currentTurn.size) return false;
274
- return markSpanRefs(state, mark).refs.some((ref) => currentTurn.has(objectRefKey(ref)));
275
- }
276
-
277
- /**
278
- * The real staleness of a mark: the earliest window index its span covers, in branch
279
- * order.
280
- *
281
- * A mark's `ordinal` is the transcript position at MARK time, so across epochs it
282
- * carries when the DECISION was made and nothing about the age of what the decision
283
- * covers, and inside one epoch every mark shares one value. Ordering a release by it
284
- * therefore fell through to comparing mark ids, which are content hashes. Measured on the
285
- * sealed rep-3 state 2026-08-10: the bounded release held back the marks covering window
286
- * indices 101 and 111 and surrendered the one covering 117, the newest material on the
287
- * table, so the two marks the guard kept were not the two it exists to keep.
288
- *
289
- * A span that no longer maps sorts past the newest entry: there is no staleness to read
290
- * on material that has left the branch, and releasing such a mark frees no window byte.
291
- * A span that does not resolve in full sorts there too, for the same reason and by the
292
- * same number: while a named fold is missing there is no start to read, and the apply
293
- * order places such a mark off its naming rather than off this reading.
294
- */
295
188
  export function markSpanStart(
296
189
  snapshot: ActiveContextSnapshot,
297
190
  state: ActiveContextState,
@@ -308,71 +201,8 @@ export function markSpanStart(
308
201
  return indices.length ? Math.min(...indices) : snapshot.mapped.length;
309
202
  }
310
203
 
311
- /**
312
- * How many of the newest guarded marks the pressure waiver still protects. The agent's
313
- * MOST RECENT reads are the ones an in-flight excursion is about to work from, so they
314
- * are the last thing surrendered; everything older is fair game once the window is the
315
- * bigger risk.
316
- */
317
- export const GUARD_WAIVER_PROTECTED_MARKS = 2;
318
-
319
- /**
320
- * How many marks the guard must be holding before the pressure waiver releases any.
321
- * Without a floor the waiver degrades into exactly the per-pass folding epoch
322
- * scheduling exists to prevent: the batch drains, one new mark is guarded on the next
323
- * pass, and releasing that one mark buys a whole prefix invalidation. Below the fence
324
- * the waiver only ever releases a BATCH. The fence itself ignores this floor.
325
- */
326
- export const GUARD_WAIVER_MINIMUM_MARKS = 4;
327
-
328
- /**
329
- * Survivability outranks the guard.
330
- *
331
- * The current-turn guard is correct while there is room to wait, and fatal once there
332
- * is not: a workload that never emits a terminal assistant message has ONE turn, so
333
- * the boundary never advances and every mark is guarded forever. Measured 2026-08-06
334
- * (rep 11): 58 assistant messages, all stopReason "toolUse", ZERO terminal ones, so
335
- * the automatic commit applied nothing all run; the only marks that ever landed came
336
- * from the agent's own two explicit commits. The window reached 359,625 tokens and the
337
- * next request went out at ~458k estimated and was rejected by the provider.
338
- *
339
- * So: the guard holds in full while the commit has other work. It is waived only when
340
- * it would STARVE the commit -- no other mark could be applied -- and pressure has
341
- * reached the backstop, and then only for the OLDEST guarded marks. At the hard fence
342
- * every guarded mark is waived, because the only useful action left is the fold that
343
- * keeps the request transmissible.
344
- */
345
- export function guardWaiverCount(input: {
346
- snapshot: ActiveContextSnapshot;
347
- ratio: number | null;
348
- guardedMarks: number;
349
- /** Marks this commit can apply WITHOUT the waiver. While any exist, the guard holds. */
350
- otherApplicableMarks: number;
351
- }): number {
352
- const { snapshot, ratio, guardedMarks, otherApplicableMarks } = input;
353
- if (guardedMarks <= 0 || typeof ratio !== "number" || !Number.isFinite(ratio)) return 0;
354
- if (ratio >= hardFenceRatio(snapshot)) return guardedMarks;
355
- const occupancy = budgetOccupancy(snapshot, ratio);
356
- if (occupancy === null || occupancy < snapshot.policy.refoldRatio || otherApplicableMarks > 0) return 0;
357
- if (guardedMarks < GUARD_WAIVER_MINIMUM_MARKS) return 0;
358
- return Math.max(1, guardedMarks - GUARD_WAIVER_PROTECTED_MARKS);
359
- }
360
-
361
204
  export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
362
205
 
363
- /**
364
- * Whether a pending mark can be applied right now.
365
- *
366
- * "protected" is a WAITING state, not a failure: the span is fresh or the agent
367
- * protected it, and both conditions expire. "unfulfillable" is terminal: the evidence
368
- * or the fold the mark names has left the branch, so no later commit can honour it.
369
- * Collapsing the two is what made a refusal look like a drop.
370
- *
371
- * A mark naming a fold the state does not hold is judged by the same two words, and
372
- * the classification is the whole answer: a fold another pending mark is about to mint
373
- * is a span still WAITING to be readable, and a fold nothing will mint has left the
374
- * branch exactly as lost evidence has. No third word (Shane 2026-08-10).
375
- */
376
206
  export function markEligibility(
377
207
  snapshot: ActiveContextSnapshot,
378
208
  state: ActiveContextState,
@@ -389,9 +219,7 @@ export function markEligibility(
389
219
  if (!refs.length) return "unfulfillable";
390
220
  const mapped = new Set(snapshot.mapped.flatMap((item) => item.ref ? [objectRefKey(item.ref)] : []));
391
221
  if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
392
- const blocked = mark.kind === "tool-result"
393
- ? toolRefsProtected(refs, state, snapshot)
394
- : refsProtected(refs, state, snapshot);
222
+ const blocked = refsProtected(refs, state, snapshot);
395
223
  return blocked ? "protected" : "eligible";
396
224
  }
397
225
 
@@ -399,29 +227,20 @@ export interface MarkAccounting {
399
227
  pending: number;
400
228
  agentMarks: number;
401
229
  ladderMarks: number;
230
+ userMarks: number;
402
231
  freedBytes: number;
403
232
  freedTokens: number;
404
233
  freedBudgetShare: number;
405
234
  rewriteTokens: number;
406
- /** Marks a commit could apply right now. */
407
235
  eligibleMarks: number;
408
- /** Marks whose span is still fresh or protected, waiting rather than lost. */
409
236
  retainedMarks: number;
410
237
  eligibleFreedBytes: number;
411
238
  eligibleFreedTokens: number;
412
- /** The ROI signal: eligible marked mass as a share of the truthful window. */
413
239
  eligibleFreedBudgetShare: number;
414
- /** Raw peek mass no reclamation can take: pinned reads and protected results. */
415
240
  pinnedBytes: number;
416
- /** How many peek results that mass is spread over. */
417
241
  pinnedResults: number;
418
242
  }
419
243
 
420
- /**
421
- * What a commit of the current marks would cost and free. `rewriteTokens` is the
422
- * prefix a commit invalidates: everything from the earliest marked message to the
423
- * end of the window, which is exactly what a positional cache must re-send.
424
- */
425
244
  export function markAccounting(
426
245
  snapshot: ActiveContextSnapshot,
427
246
  state: ActiveContextState,
@@ -458,6 +277,7 @@ export function markAccounting(
458
277
  pending: marks.length,
459
278
  agentMarks: marks.filter((mark) => mark.origin === "agent").length,
460
279
  ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
280
+ userMarks: marks.filter((mark) => mark.origin === "user").length,
461
281
  freedBytes,
462
282
  freedTokens,
463
283
  freedBudgetShare: snapshot.budgetTokens > 0 ? freedTokens / snapshot.budgetTokens : 0,
@@ -474,22 +294,6 @@ export function markAccounting(
474
294
  };
475
295
  }
476
296
 
477
- /**
478
- * THE commit trigger. There is exactly one.
479
- *
480
- * Occupancy of the serving budget reaches maxTarget and a commit fires. Nothing else
481
- * fires one automatically: the user command and the fence/recovery lane are the only
482
- * other paths into a commit, and both are authority rather than economics.
483
- *
484
- * Two arms used to sit here. The eligible-share arm asked whether accumulated marks
485
- * were worth a rewrite and fired 164 commits from ordinal 17 in rep 15, each a fresh
486
- * cache rebuild bought with marks that had not yet earned one; worse, it could fire
487
- * BELOW maxTarget, which is the quiet-runtime law it was sitting underneath. The
488
- * pressure arm read a window ratio against the refold rung, which after
489
- * re-denomination is simply a second line above this one. Both are gone. Marking is
490
- * doorless below the commit line and costs nothing, so what a pass accumulates is never a
491
- * reason to commit; crossing the band top is.
492
- */
493
297
  export function epochCommitDue(snapshot: ActiveContextSnapshot, ratio: number | null): boolean {
494
298
  const occupancy = budgetOccupancy(snapshot, ratio);
495
299
  return occupancy !== null && occupancy >= snapshot.thresholds.maxTarget;
@@ -505,8 +309,6 @@ export function foldMarkFor(input: {
505
309
  const { candidate } = input;
506
310
  return {
507
311
  mark: "fold",
508
- // The same identity the committed fold will get, so a repeated decision is
509
- // inert rather than a second fold of the same span.
510
312
  id: foldIdFor(candidate.kind, candidate.parts),
511
313
  kind: candidate.kind,
512
314
  parts: clone(candidate.parts),
@@ -517,25 +319,18 @@ export function foldMarkFor(input: {
517
319
  };
518
320
  }
519
321
 
520
- /** The deterministic brief a ladder decision would have folded with. */
521
322
  export function ladderBrief(
522
323
  snapshot: ActiveContextSnapshot,
523
324
  state: ActiveContextState,
524
325
  candidate: FoldCandidate,
525
326
  ): string {
526
327
  if (candidate.kind === "tool-result") return automaticToolBrief(snapshot, candidate);
527
- if (candidate.kind === "consolidation") return deterministicConsolidationBrief(candidate, state);
328
+ if (candidate.kind === "consolidation") {
329
+ return deterministicConsolidationBrief(candidate, state, snapshot.toolName, snapshot);
330
+ }
528
331
  return deterministicChapterCandidateBrief(snapshot, candidate);
529
332
  }
530
333
 
531
- /**
532
- * The brief and provenance an automatic mark carries.
533
- *
534
- * A warmed chapter brief the runtime already paid a model call for is REUSED when the
535
- * prepared fold is exactly this candidate. Under epoch scheduling every automatic fold
536
- * arrives through a mark, so a preparation whose brief the mark path ignored would be a
537
- * model call spent on nothing.
538
- */
539
334
  export function automaticMarkBrief(
540
335
  snapshot: ActiveContextSnapshot,
541
336
  state: ActiveContextState,
@@ -548,11 +343,6 @@ export function automaticMarkBrief(
548
343
  return { brief: ladderBrief(snapshot, state, candidate), briefProvenance: { kind: "deterministic" } };
549
344
  }
550
345
 
551
- /**
552
- * Turn one automatic rung selection into a mark. Prepared chapters are excluded on
553
- * purpose: that rung only fires at the hard provider fence, which is already inside
554
- * a commit epoch, and deferring it would strand a model-generated brief.
555
- */
556
346
  export function ladderSelectionMark(input: {
557
347
  snapshot: ActiveContextSnapshot;
558
348
  state: ActiveContextState;
@@ -574,24 +364,6 @@ export function ladderSelectionMark(input: {
574
364
  });
575
365
  }
576
366
 
577
- /**
578
- * PEEK COPIES ARE EPHEMERAL BY CONTRACT.
579
- *
580
- * A peek returns a fold's exact stored source, so the copy sits in the window as raw
581
- * mass beside that fold's own placeholder: the same bytes held twice, with the second
582
- * copy re-retrievable forever from the id the agent already used. So a completed peek
583
- * read is reclaimed at the NEXT commit rather than aging through the ladder, and the
584
- * window being append-only is why the reclaim can only happen at a commit boundary.
585
- *
586
- * Two survival paths, and only two. Expanding the fold it peeked means the agent asked
587
- * for those bytes in place, and pinning is the veto: a pin holds the copy raw through
588
- * every commit, exactly as it holds any other evidence, and lifting it hands the copy
589
- * back to the next commit. Both are read here at marking time, and a pin made AFTER the
590
- * mark exists still vetoes, because a protected mark waits instead of applying.
591
- *
592
- * These marks are minted at the last-call exposure as well as at the commit, so the
593
- * pending disposal is visible during the gated round the pin has to fit inside.
594
- */
595
367
  export function ephemeralPeekMarks(input: {
596
368
  snapshot: ActiveContextSnapshot;
597
369
  state: ActiveContextState;
@@ -619,11 +391,9 @@ export function ephemeralPeekMarks(input: {
619
391
  .map(({ index }) => snapshot.mapped[index].ref!);
620
392
  const peeked = peekedSourceFoldIds(snapshot, refs);
621
393
  if (!peeked) continue;
622
- // The agent committed to what it peeked: leave the read in the raw window.
623
394
  if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
624
395
  if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
625
- refs.length > snapshot.policy.maxFoldSourceRefs ||
626
- toolRefsProtected(refs, state, snapshot)) continue;
396
+ refsProtected(refs, state, snapshot)) continue;
627
397
  const candidate: FoldCandidate = {
628
398
  kind: "tool-result",
629
399
  parts: refs.map((ref) => ({ kind: "raw" as const, ref })),
@@ -633,8 +403,6 @@ export function ephemeralPeekMarks(input: {
633
403
  candidate,
634
404
  brief: automaticToolBrief(snapshot, candidate),
635
405
  briefProvenance: { kind: "deterministic" },
636
- // A peek is an agent-initiated read; attributing its disposal to the ladder
637
- // would deflate the agent-vs-ladder mark ratio the adjudication reads.
638
406
  origin: "agent",
639
407
  ordinal: input.ordinal,
640
408
  }));
@@ -650,12 +418,9 @@ function peekedFoldProtected(
650
418
  const fold = state.folds.find((item) => item.id === foldId);
651
419
  if (!fold) return false;
652
420
  const refs = flattenFoldRefs(fold, state);
653
- return fold.kind === "tool-result"
654
- ? toolRefsProtected(refs, state, snapshot)
655
- : refsProtected(refs, state, snapshot);
421
+ return refsProtected(refs, state, snapshot);
656
422
  }
657
423
 
658
- /** Every evidence key already spoken for by a fold or a pending fold mark. */
659
424
  export function claimedRefKeys(state: ActiveContextState): Set<string> {
660
425
  const keys = new Set<string>();
661
426
  for (const fold of state.folds) {
@@ -668,35 +433,15 @@ export function claimedRefKeys(state: ActiveContextState): Set<string> {
668
433
  return keys;
669
434
  }
670
435
 
671
- /** What is still on the table after the marks in hand: the steering number and its parts. */
672
436
  export interface UnmarkedRemainder {
673
- /** Tool results in a MEMBER batch that no fold and no pending mark owns. */
674
437
  spans: number;
675
438
  tokens: number;
676
- /** Unmarked member tokens as a share of the member tool mass. The steering number. */
439
+ /** Raw bytes, which is what the fold frontier measures itself against. */
440
+ chars: number;
677
441
  share: number;
678
- /** The largest few, by reclaim value, so the next batch is one read away. */
679
442
  candidates: Array<{ id: string; tokens: number }>;
680
443
  }
681
444
 
682
- /**
683
- * The unmarked remainder, as an AGGREGATE plus a bounded head.
684
- *
685
- * An exhaustive list is what the status index already is, and re-rendering it is what
686
- * this build removed from the projection. What an agent needs to decide the NEXT batch
687
- * is one percentage and the few largest names; everything else is a total.
688
- *
689
- * ONE DEFINITION OF STALE, AND IT IS THE SELECTOR'S OWN. The scope is class membership:
690
- * every result inside a batch `automaticToolBatches` admits, which is the same
691
- * enumeration `selectAutomaticToolBatch` picks its proposal out of. So the last call and
692
- * the deferred-commit record announce mass a commit can actually take, by construction
693
- * rather than by two filters kept in step. Counting whole-window tool results instead
694
- * announced material no rung could ever propose: rep 3 named 280k tokens while every
695
- * selector had nothing, which reads as a runtime refusing work it had just described.
696
- *
697
- * Membership is not ownership: the denominator is every member result, taken or not, so
698
- * the share reads as the fraction of reachable tool mass still unspoken for.
699
- */
700
445
  export function unmarkedRemainder(
701
446
  snapshot: ActiveContextSnapshot,
702
447
  state: ActiveContextState,
@@ -709,6 +454,33 @@ export function unmarkedRemainder(
709
454
  for (const batch of automaticToolBatches(snapshot, state)) {
710
455
  for (const index of batch.indices) members.add(index);
711
456
  }
457
+ // CHAPTERS ARE FOLDABLE MASS TOO (Shane, 2026-08-23).
458
+ //
459
+ // The population used to be tool batches alone, while `selectAutomaticSpan` proposes a
460
+ // chapter whenever no batch is available, so on a chapter-heavy session this reported a
461
+ // remainder of zero over material automatic folding was about to take. It is the reading
462
+ // a deferred commit uses to say whether its mass is HELD or simply absent, which is the
463
+ // one distinction that record exists to draw, and it was answering "absent" for mass in
464
+ // plain view. It also broke the frontier's first cut, which gated on this number before
465
+ // the threshold moved into the selector where it belongs.
466
+ //
467
+ // Walked with the selector rather than re-derived, so the population is what automatic
468
+ // folding can actually reach and cannot drift from it. Walked from an EMPTY claim set,
469
+ // because this is the DENOMINATOR: a chapter already claimed by a mark is still foldable
470
+ // mass, it is just mass that is already spoken for, and the numerator below is what
471
+ // subtracts it. Bounded by the mapped length, which no terminating walk can exceed since
472
+ // every pass claims at least one ref.
473
+ const indexed = mappedByKey(snapshot);
474
+ const walked = new Set<string>();
475
+ for (let pass = 0; pass < snapshot.mapped.length; pass += 1) {
476
+ const chapter = selectAutomaticChapter(snapshot, state, walked);
477
+ if (!chapter) break;
478
+ for (const ref of chapter.sourceRefs) {
479
+ walked.add(objectRefKey(ref));
480
+ const item = indexed.get(objectRefKey(ref));
481
+ if (item) members.add(item.index);
482
+ }
483
+ }
712
484
  const candidates: Array<{ id: string; tokens: number }> = [];
713
485
  let unmarkedBytes = 0;
714
486
  let memberBytes = 0;
@@ -724,29 +496,199 @@ export function unmarkedRemainder(
724
496
  return {
725
497
  spans: candidates.length,
726
498
  tokens: Math.ceil(unmarkedBytes / perToken),
499
+ chars: unmarkedBytes,
727
500
  share: memberBytes > 0 ? unmarkedBytes / memberBytes : 0,
728
501
  candidates: candidates.slice(0, Math.max(0, limit)),
729
502
  };
730
503
  }
731
504
 
732
- /** Every fold id already spoken for by a pending refold mark. */
505
+ /**
506
+ * THE FOLD FRONTIER: THE RUNTIME CUTS, AND THE AGENT EDITS WHAT IT CUT
507
+ * (Shane, 2026-08-23).
508
+ *
509
+ * The ladder used to stage nothing between commits, and before that it staged a mark on
510
+ * every measured response. Both were answers to the same question, WHEN SHOULD THE AGENT
511
+ * CURATE, and both got it wrong in the same way: they asked the agent to CHOOSE SPANS.
512
+ * Eager staging chose them first and crowded the agent out; standing down left the choice
513
+ * open until the commit, by which time the material was long out of view and the only
514
+ * information available was staleness, which the runtime already has.
515
+ *
516
+ * So the choice moves. As raw material accumulates past `minFoldChars`, the runtime cuts
517
+ * it, stalest first, exactly as a commit would. The cut is a PENDING MARK and pending
518
+ * marks are byte-inert: nothing moves, the projection does not change, the prefix cache is
519
+ * untouched, and the agent keeps seeing the raw material until a commit applies it. What
520
+ * the agent gets is a fold to annotate while the content is still fresh, which is the one
521
+ * thing it knows that the runtime does not.
522
+ *
523
+ * The loop bounds itself twice over. It stops when the unmarked remainder falls under the
524
+ * threshold, and it stops when the selector runs out of members, so a window whose whole
525
+ * remainder is pinned or blacklisted stages nothing and says so by returning empty rather
526
+ * than spinning.
527
+ */
528
+ export function frontierMarks(input: {
529
+ snapshot: ActiveContextSnapshot;
530
+ state: ActiveContextState;
531
+ ordinal: number;
532
+ }): PendingFoldMark[] {
533
+ const { snapshot } = input;
534
+ const claimed = claimedRefKeys(input.state);
535
+ const marks: PendingFoldMark[] = [];
536
+ let state = input.state;
537
+ while (marks.length < MAX_FRONTIER_CUTS_PER_PASS) {
538
+ // THE THRESHOLD IS THE SELECTOR'S OWN, which is `minFoldChars`, and it is read there
539
+ // rather than counted here. The first cut of this measured the unmarked remainder
540
+ // instead, and that reads TOOL BATCHES ONLY: a chapter-only session had a remainder of
541
+ // zero however much raw prose it held, so the frontier never fired on it at all and
542
+ // gate 7's fixture produced no cut. One threshold, in the one place that knows every
543
+ // span kind.
544
+ const candidate = selectAutomaticSpan(snapshot, state, claimed);
545
+ if (!candidate) break;
546
+ const mark = foldMarkFor({
547
+ candidate,
548
+ ...automaticMarkBrief(snapshot, state, candidate),
549
+ origin: "ladder",
550
+ ordinal: input.ordinal,
551
+ });
552
+ const addition = addPendingMark(state, mark);
553
+ if (!addition.added) break;
554
+ state = addition.state;
555
+ marks.push(mark);
556
+ for (const ref of candidate.sourceRefs) claimed.add(objectRefKey(ref));
557
+ }
558
+ return marks;
559
+ }
560
+
561
+ /**
562
+ * The pending mark that already holds a piece of this evidence, if any.
563
+ *
564
+ * The commit fills from the stale end and the depth bound retains what it does not
565
+ * reach, so a ladder mark routinely outlives the commit that staged it. An agent
566
+ * marking the same evidence afterwards would stand a second owner over one span and
567
+ * lose at apply time with a forest invariant for a message. Refusing at the tool says
568
+ * so while the agent can still act on it.
569
+ */
570
+ export function markClaimingRef(
571
+ state: ActiveContextState,
572
+ refs: readonly EvidenceRef[],
573
+ ): { markId: string; entryId: string } | null {
574
+ for (const mark of pendingMarks(state)) {
575
+ if (mark.mark !== "fold") continue;
576
+ const claimed = new Set(markSpanRefs(state, mark).refs.map(objectRefKey));
577
+ for (const ref of refs) {
578
+ if (claimed.has(objectRefKey(ref))) return { markId: mark.id, entryId: ref.entryId };
579
+ }
580
+ }
581
+ return null;
582
+ }
583
+
584
+ export interface WindowClaims {
585
+ /** Tokens standing marks will free at the next commit. */
586
+ markedTokens: number;
587
+ /** Tokens the agent pinned raw: a claim that frees nothing and is never taken. */
588
+ pinnedTokens: number;
589
+ pinnedRefs: number;
590
+ /** Pinned mass as a share of the serving budget, against its cap. */
591
+ pinnedShare: number;
592
+ /** Completed units neither marked nor pinned: what the ladder takes by age. */
593
+ unclaimedTokens: number;
594
+ unclaimedSpans: number;
595
+ }
596
+
597
+ /**
598
+ * A PIN IS A CLAIM, NOT A FOLD (Shane, 2026-08-22: "if they do a pin on raw span I think
599
+ * we also treat that as a marked fold but a pinned one").
600
+ *
601
+ * Read as accounting, that is exactly right and already half true: pinned evidence is
602
+ * refused to every automatic rung, skipped by wedge absorption, and excluded from the
603
+ * unmarked remainder, so the ladder cannot take it and it is never reported as an
604
+ * oversight. What was missing is that the agent could not SEE it, and the whole point of
605
+ * asking an agent to account for its window is that the gaps become visible.
606
+ *
607
+ * A pin cannot count toward the DROP, and that is not a technicality. Pinned bytes stay
608
+ * in the window, so they free nothing; if they counted, an agent could "cover" a commit
609
+ * with pins, the commit would still be short, and it would fold something else anyway.
610
+ * The number would be a lie at the moment it mattered most. So the three buckets are
611
+ * reported side by side and only one of them pays.
612
+ */
613
+ export function windowClaims(input: {
614
+ snapshot: ActiveContextSnapshot;
615
+ state: ActiveContextState;
616
+ budgetTokens: number;
617
+ charsPerToken: number;
618
+ }): WindowClaims {
619
+ const { snapshot, state, budgetTokens } = input;
620
+ const accounting = markAccounting(snapshot, state);
621
+ const remainder = unmarkedRemainder(snapshot, state, input.charsPerToken);
622
+ const pinned = protectedStaleMass(snapshot, state);
623
+ const pinnedTokens = estimatedTokens(pinned.bytes);
624
+ return {
625
+ markedTokens: Math.round(accounting.eligibleFreedBudgetShare * budgetTokens),
626
+ pinnedTokens,
627
+ pinnedRefs: pinned.refs,
628
+ pinnedShare: budgetTokens > 0 ? pinnedTokens / budgetTokens : 0,
629
+ unclaimedTokens: remainder.tokens,
630
+ unclaimedSpans: remainder.spans,
631
+ };
632
+ }
633
+
634
+ export interface CommitCoverage {
635
+ /** The share of the serving budget the next commit is sized to free. */
636
+ targetShare: number;
637
+ targetTokens: number;
638
+ /** What the standing marks will actually free when that commit runs. */
639
+ markedShare: number;
640
+ markedTokens: number;
641
+ /** What the ladder will take by staleness unless the agent marks it first. */
642
+ remainingTokens: number;
643
+ covered: boolean;
644
+ }
645
+
646
+ /**
647
+ * WHAT THE NEXT COMMIT NEEDS, AND HOW MUCH OF IT THE AGENT HAS COVERED.
648
+ *
649
+ * One definition, read by three callers that used to reason about the drop separately:
650
+ * the commit fill that sizes itself to it, the advisory that warns before it, and the
651
+ * mark response that tells the agent where it stands. An agent asked to govern its own
652
+ * context cannot do it against a number nobody tells it.
653
+ *
654
+ * The marked side is the ELIGIBLE share, not every standing mark, because a mark over
655
+ * protected evidence frees nothing at this commit however good it is. That makes
656
+ * `remainingTokens` the honest answer to "what will fold without me".
657
+ *
658
+ * `targetShare` moves as the window grows: it is measured from occupancy NOW, and two
659
+ * large results landing after the reading raise it again. Callers that render it say so.
660
+ */
661
+ export function commitCoverage(input: {
662
+ snapshot: ActiveContextSnapshot;
663
+ state: ActiveContextState;
664
+ usedTokens: number | null;
665
+ budgetTokens: number;
666
+ }): CommitCoverage {
667
+ const { snapshot, state, usedTokens, budgetTokens } = input;
668
+ const targetShare = usedTokens === null || !(budgetTokens > 0)
669
+ ? 0
670
+ : Math.max(0, (usedTokens - snapshot.thresholds.minTarget * budgetTokens) / budgetTokens);
671
+ const accounting = markAccounting(snapshot, state);
672
+ const markedShare = accounting.eligibleFreedBudgetShare;
673
+ const targetTokens = Math.round(targetShare * budgetTokens);
674
+ const markedTokens = Math.round(markedShare * budgetTokens);
675
+ const remainingTokens = Math.max(0, targetTokens - markedTokens);
676
+ return {
677
+ targetShare,
678
+ targetTokens,
679
+ markedShare,
680
+ markedTokens,
681
+ remainingTokens,
682
+ covered: remainingTokens === 0,
683
+ };
684
+ }
685
+
733
686
  export function markedFoldIds(state: ActiveContextState): Set<string> {
734
687
  const ids = new Set<string>();
735
688
  for (const mark of pendingMarks(state)) if (mark.mark === "refold") ids.add(mark.id);
736
689
  return ids;
737
690
  }
738
691
 
739
- /**
740
- * THE COUNT LAW, AS MARKS. Every parent the eligible-root count owes right now, all of
741
- * them, in the epoch that notices.
742
- *
743
- * Separate from the top-up on purpose. The top-up is pressure arithmetic: it stops the
744
- * moment the marks in hand reach the freeing target, which is the right rule for how
745
- * DEEP a commit cuts and the wrong rule entirely for a law that says a count of eligible
746
- * roots at or above the width is a state that must not persist. So this runs at every
747
- * commit epoch and it runs first, whatever the pressure and whatever the top-up would
748
- * have proposed (Shane 2026-08-10).
749
- */
750
692
  export function consolidationMarks(input: {
751
693
  snapshot: ActiveContextSnapshot;
752
694
  state: ActiveContextState;
@@ -760,28 +702,16 @@ export function consolidationMarks(input: {
760
702
  }));
761
703
  }
762
704
 
763
- /**
764
- * Agent judgment leads; automation guarantees the floor. If the marks the agent made
765
- * would free less than the target share of the window, automation adds the stalest
766
- * unprotected eligible SPANS until they do: completed tool batches and raw narrative
767
- * chapters. Placeholders are not on that list any more; a placeholder gets a parent from
768
- * the count law above, which is not pressure arithmetic and does not stop at a target.
769
- */
770
705
  export function topUpMarks(input: {
771
706
  snapshot: ActiveContextSnapshot;
772
707
  state: ActiveContextState;
773
708
  ordinal: number;
774
- /** Freeing target as a share of the SERVING BUDGET. Required: there is no second default. */
775
709
  targetShare: number;
776
- /** Evidence keys the top-up may never propose, e.g. the current excursion's reads. */
777
- excludeRefKeys?: ReadonlySet<string>;
778
- /** Measure progress against ELIGIBLE mass, so pinned or protected marks never count. */
779
710
  eligibleOnly?: boolean;
780
711
  }): PendingFoldMark[] {
781
712
  const { snapshot } = input;
782
713
  const target = input.targetShare;
783
714
  const claimed = claimedRefKeys(input.state);
784
- for (const key of input.excludeRefKeys ?? []) claimed.add(key);
785
715
  const marks: PendingFoldMark[] = [];
786
716
  let state = input.state;
787
717
  const progress = (value: ActiveContextState): number => {
@@ -789,7 +719,7 @@ export function topUpMarks(input: {
789
719
  return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
790
720
  };
791
721
  let share = progress(state);
792
- for (let attempt = 0; attempt < EPOCH_MAX_TOPUP_MARKS && share < target; attempt += 1) {
722
+ while (share < target) {
793
723
  const candidate = selectAutomaticSpan(snapshot, state, claimed);
794
724
  if (!candidate) break;
795
725
  const mark = foldMarkFor({
@@ -808,44 +738,64 @@ export function topUpMarks(input: {
808
738
  return marks;
809
739
  }
810
740
 
811
- /** One sliver a commit swallowed into its later neighbour, reported never inferred. */
741
+ /**
742
+ * REFOLD AS THE LAST FILL, NOT THE FIRST (Shane, 2026-08-21).
743
+ *
744
+ * An expanded fold is stale mass sitting in the window, so the commit may reclaim it,
745
+ * but only after raw stale material has been spent. An agent that expands a fold is
746
+ * reading it; taking it back while there is still unfolded material to take is hostile
747
+ * for no gain. Pin holds a span past that point, and a live lease holds it for the
748
+ * generations right after the expand. Stalest first, and never past the freeing target.
749
+ */
750
+ export function refoldMarks(input: {
751
+ snapshot: ActiveContextSnapshot;
752
+ state: ActiveContextState;
753
+ ordinal: number;
754
+ targetShare: number;
755
+ eligibleOnly?: boolean;
756
+ }): PendingMark[] {
757
+ const { snapshot } = input;
758
+ const marks: PendingMark[] = [];
759
+ let state = input.state;
760
+ const claimedFoldIds = markedFoldIds(state);
761
+ const progress = (value: ActiveContextState): number => {
762
+ const accounting = markAccounting(snapshot, value);
763
+ return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
764
+ };
765
+ let share = progress(state);
766
+ while (share < input.targetShare) {
767
+ const foldId = selectAutomaticRefold(snapshot, state, claimedFoldIds);
768
+ if (!foldId) break;
769
+ const mark: PendingMark = {
770
+ mark: "refold",
771
+ id: foldId,
772
+ origin: "ladder",
773
+ ordinal: input.ordinal,
774
+ };
775
+ const addition = addPendingMark(state, mark);
776
+ if (!addition.added) break;
777
+ state = addition.state;
778
+ marks.push(mark);
779
+ claimedFoldIds.add(foldId);
780
+ share = progress(state);
781
+ }
782
+ return marks;
783
+ }
784
+
812
785
  export interface AbsorbedWedge {
813
- /** The mark that grew backward to cover it. */
814
786
  intoMarkId: string;
815
- /**
816
- * The id that mark carried BEFORE it grew. A mark's id is derived from its span, so
817
- * growing the span mints a new one; without this the trail from the decision the
818
- * agent made to the fold that carries it is broken, and a mark appears to vanish.
819
- */
820
787
  fromMarkId: string;
821
788
  startId: string;
822
789
  endId: string;
823
790
  entries: number;
824
791
  tokens: number;
792
+ chars: number;
825
793
  }
826
794
 
827
- /**
828
- * Wedge absorption. THE ANTI-LCM PIN LIVES HERE.
829
- *
830
- * A sliver of stale raw content hugging a fold boundary is a crumb nothing will ever
831
- * reclaim: it is below the minimum chapter size, so no chapter rung will take it, and
832
- * it is not a tool batch, so no tool rung will either. Absorbing it costs nothing
833
- * because it happens INSIDE a commit the epoch has already paid for -- it grows a
834
- * pending mark backward rather than performing a mutation of its own.
835
- *
836
- * What it must never do is erode deliberate non-sequential curation. An agent holding
837
- * folds at 10:20, 40:55 and 60:70 with raw spans between them chose that shape, and a
838
- * mechanism that swallowed those gaps would delete the curation element outright. So
839
- * the threshold is tiny, measured in the session's own calibrated tokens, and NOT
840
- * pressure-scaled: above it, a gap stays raw permanently no matter how full the window
841
- * gets or how ragged the projection looks.
842
- */
843
795
  export function absorbWedgeMarks(input: {
844
796
  snapshot: ActiveContextSnapshot;
845
797
  state: ActiveContextState;
846
- /** The session's measured serialized chars per token; never a fixed constant. */
847
798
  charsPerToken: number;
848
- /** Evidence absorption may never touch, e.g. the current excursion's own reads. */
849
799
  excludeRefKeys?: ReadonlySet<string>;
850
800
  }): { state: ActiveContextState; absorbed: AbsorbedWedge[] } {
851
801
  const { snapshot } = input;
@@ -863,9 +813,7 @@ export function absorbWedgeMarks(input: {
863
813
  .filter((index): index is number => index !== undefined);
864
814
  return indices.length ? { start: Math.min(...indices), end: Math.max(...indices) } : null;
865
815
  };
866
- // One pass, oldest first. Each absorption rewrites exactly one mark, and a mark that
867
- // has already grown is not a candidate to grow again in the same commit.
868
- for (let guard = 0; guard < MAX_PENDING_MARKS; guard += 1) {
816
+ for (;;) {
869
817
  const marks = pendingMarks(state);
870
818
  const occupied = [
871
819
  ...orderedRoots(state, snapshot).map((root) => ({ start: root.start, end: root.end })),
@@ -885,9 +833,16 @@ export function absorbWedgeMarks(input: {
885
833
  const gapEnd = interval.start - 1;
886
834
  if (gapStart < 0 || gapEnd < gapStart) continue;
887
835
  if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
888
- const gapTokens = Math.ceil(spanBytes(snapshot, gapStart, gapEnd + 1) / charsPerToken);
889
- // The whole discriminator, in one line: above this, the gap is the agent's.
890
- if (gapTokens > MAX_WEDGE_ABSORB_TOKENS) continue;
836
+ // THE GAP FLOOR IS THE FOLD FLOOR (Shane, 2026-08-21). A gap between two folds
837
+ // that could not stand as a fold on its own has no business staying raw: it costs
838
+ // its own bytes and buys nothing, and the neighbour beside it can hold it for the
839
+ // price of one sentence in the brief. So anything under minFoldChars is absorbed
840
+ // and anything at or over it is left alone, because at that size it is a fold the
841
+ // ladder will take on its own terms. This replaced MAX_WEDGE_ABSORB_TOKENS, a
842
+ // separate 256-token constant that let 1,024-char slivers survive between folds.
843
+ const gapBytes = spanBytes(snapshot, gapStart, gapEnd + 1);
844
+ if (gapBytes >= snapshot.thresholds.minFoldChars) continue;
845
+ const gapTokens = Math.ceil(gapBytes / charsPerToken);
891
846
  const gapRefs = [];
892
847
  let usable = true;
893
848
  for (let index = gapStart; index <= gapEnd; index += 1) {
@@ -897,15 +852,19 @@ export function absorbWedgeMarks(input: {
897
852
  }
898
853
  if (!usable || !gapRefs.length) continue;
899
854
  if (refsProtected(gapRefs, state, snapshot)) continue;
900
- // Structural safety: the grown span must still be a valid closed chapter, which
901
- // is what keeps every tool call paired with its result in the projection.
902
855
  if (!chapterRangeIsUnitAligned(snapshot, gapStart, interval.end)) continue;
903
856
  const parts = partsForRange(snapshot, state, gapStart, interval.end, new Set<FoldKind>(["tool-result"]));
904
857
  if (!parts) continue;
858
+ // ELIGIBILITY, not truncation (Shane 2026-08-14): the grown fold's brief
859
+ // must name what it absorbed, and a brief already near the policy cap has
860
+ // no room for that sentence. Slicing dropped exactly the words that made
861
+ // the absorption honest, so a wedge whose truthful suffix does not fit is
862
+ // simply not absorbed; the gap stays raw for a later commit.
863
+ const absorbSuffix = ` It also holds ${gapRefs.length} short adjacent entry(s) absorbed at commit.`;
864
+ if (mark.brief.length + absorbSuffix.length > snapshot.policy.maxBriefChars) continue;
905
865
  const grown = foldMarkFor({
906
866
  candidate: { kind: "chapter", parts, sourceRefs: candidateSourceRefs(parts, state) },
907
- brief: `${mark.brief} It also holds ${gapRefs.length} short adjacent entry(s) absorbed at commit.`
908
- .slice(0, snapshot.policy.maxBriefChars),
867
+ brief: `${mark.brief}${absorbSuffix}`,
909
868
  briefProvenance: mark.briefProvenance,
910
869
  origin: mark.origin,
911
870
  ordinal: mark.ordinal,
@@ -920,6 +879,7 @@ export function absorbWedgeMarks(input: {
920
879
  endId: gapRefs.at(-1)!.entryId,
921
880
  entries: gapRefs.length,
922
881
  tokens: gapTokens,
882
+ chars: gapBytes,
923
883
  });
924
884
  applied = true;
925
885
  break;
@@ -941,7 +901,6 @@ export interface RefusedMark {
941
901
  id: string;
942
902
  origin: MarkOrigin;
943
903
  reason: string;
944
- /** Whether the mark survives as pending rather than being discarded. */
945
904
  retained: boolean;
946
905
  }
947
906
 
@@ -950,98 +909,51 @@ export interface CommitEpochResult {
950
909
  applied: AppliedMark[];
951
910
  refused: RefusedMark[];
952
911
  retained: PendingMark[];
953
- /** Guarded marks the pressure waiver released into this commit, oldest material first. */
954
- waived: PendingMark[];
955
912
  }
956
913
 
957
- /**
958
- * Apply every pending mark. This is the ONE mutation of the epoch: it reuses
959
- * prepareFold/commitPreparedFold exactly as the immediate path does, so a committed
960
- * mark and an immediate fold produce the same fold record.
961
- */
962
914
  export async function commitPendingMarks(input: {
963
915
  snapshot: ActiveContextSnapshot;
964
916
  state: ActiveContextState;
965
917
  generation: number;
966
918
  now?: () => number;
967
- /**
968
- * Keep marks the commit could not apply YET. A mark is a standing decision; dropping
969
- * it because the span happened to be fresh at commit time silently discards the
970
- * agent's judgment and leaves nothing for the next commit to act on.
971
- */
972
919
  retainIneligible?: boolean;
973
920
  /**
974
- * Never fold evidence the CURRENT excursion gathered. A commit that runs between
975
- * an agent's read and its use of that read hands the agent a window where its own
976
- * just-gathered evidence is a placeholder. Such marks stay pending for the next
977
- * commit rather than being applied or dropped.
921
+ * How many bytes this commit needs to free. Marks past it are RETAINED, not applied.
922
+ *
923
+ * This bound did not exist before the frontier (Shane, 2026-08-23), and it did not need
924
+ * to: staging was sized to the drop by `topUpMarks`, so every pending mark was one the
925
+ * commit had already decided it wanted. The frontier stages as material ARRIVES, which
926
+ * is the whole point of it, so by the time a commit fires the pending set is everything
927
+ * the window holds. Applying all of it would cut the window to nothing at the first
928
+ * commit, which is neither what the budget asks for nor what the agent is owed: what it
929
+ * is owed is that material stays raw for as long as there is room for it.
930
+ *
931
+ * Undefined means no bound, which is what the emergency lanes want: a fence or a
932
+ * rollback is not housekeeping and takes everything it can.
978
933
  */
979
- guardCurrentTurn?: boolean;
980
- /**
981
- * How many guarded marks the pressure waiver releases anyway, oldest material first. The
982
- * guard protects an in-flight excursion; it may never cost the session its ability
983
- * to send a request at all. See `guardWaiverCount`.
984
- */
985
- guardWaiver?: number;
934
+ applyTargetBytes?: number;
986
935
  }): Promise<CommitEpochResult> {
936
+ // THE CURRENT-TURN GUARD IS DELETED (Shane, 2026-08-23: "you're using turns as the
937
+ // boundaries, which should not be the case. It should be at the most granular level,
938
+ // which is events"). The guard retained every mark touching a toolResult newer than
939
+ // the last terminal assistant stop, and a terminal stop is a TURN artifact: the first
940
+ // live session (sol-20260823-live rep 1) ran its whole workload as one turn, every
941
+ // assistant response stopping "toolUse", so the guard's "current turn" was the entire
942
+ // window, four band-top commits applied nothing, and the projection fence swept 18
943
+ // marks at 0.958 occupancy down to a 0.031 landing against a 0.40 aim. The waiver
944
+ // machinery existed only to bail the guard out at the fence and the boundary, so it
945
+ // is deleted with it. What protects the working set now is structural and event-level:
946
+ // an incomplete batch is never proposable, the depth bound stops the routine commit at
947
+ // the aim, and the stalest-first cut order leaves the newest events raw.
987
948
  const marks = pendingMarks(input.state);
988
949
  const retained: PendingMark[] = [];
989
- const waived: PendingMark[] = [];
990
- const guardReasons = new Map<string, string>();
991
950
  const held = new Set(input.state.folds.map((fold) => fold.id));
992
951
  const deferred: PendingMark[] = [];
993
- /**
994
- * Whether the mark names a fold some other standing mark is about to mint. Until that
995
- * fold exists there is no eligibility worth judging: the question is about material
996
- * the child owns, and the apply loop is where it gets asked, after the child has
997
- * landed. A mark naming a fold NOBODY will mint is not this case; it is judged
998
- * normally, and the apply loop drops it by name.
999
- */
1000
952
  const awaitingMintedChild = (mark: PendingMark): boolean =>
1001
953
  markSpanRefs(input.state, mark).unresolved?.pending === true;
1002
- const currentTurn = input.guardCurrentTurn
1003
- ? currentTurnRefKeys(input.snapshot)
1004
- : new Set<string>();
1005
- if (input.guardCurrentTurn) {
1006
- const applicable: PendingMark[] = [];
1007
- const guarded: PendingMark[] = [];
1008
- for (const mark of marks) {
1009
- if (markTouchesCurrentTurn(input.state, mark, currentTurn)) guarded.push(mark);
1010
- else applicable.push(mark);
1011
- }
1012
- // Oldest MATERIAL first: the newest reads are the ones the excursion is about to
1013
- // use, so they are the last evidence the waiver surrenders. Staleness is a property
1014
- // of the span, never of when the mark was proposed; see `markSpanStart`. The id
1015
- // comparison is the final tiebreak between marks covering the same start, and
1016
- // nothing else.
1017
- const guardedStart = new Map(guarded.map((mark) =>
1018
- [pendingMarkKey(mark), markSpanStart(input.snapshot, input.state, mark)] as const));
1019
- guarded.sort((left, right) =>
1020
- guardedStart.get(pendingMarkKey(left))! - guardedStart.get(pendingMarkKey(right))! ||
1021
- left.id.localeCompare(right.id));
1022
- const waiverCount = Math.max(0, Math.min(guarded.length, Math.trunc(input.guardWaiver ?? 0)));
1023
- for (const [index, mark] of guarded.entries()) {
1024
- if (index < waiverCount) {
1025
- waived.push(mark);
1026
- applicable.push(mark);
1027
- continue;
1028
- }
1029
- retained.push(mark);
1030
- guardReasons.set(
1031
- pendingMarkKey(mark),
1032
- "evidence was gathered in the current turn; the mark stays pending until the turn closes",
1033
- );
1034
- }
1035
- marks.length = 0;
1036
- marks.push(...applicable);
1037
- }
1038
954
  if (input.retainIneligible) {
1039
955
  const applicable: PendingMark[] = [];
1040
956
  for (const mark of marks) {
1041
- // A mark awaiting a child another mark will mint is not judged here. Its
1042
- // eligibility is a question about material the child fold owns, and that fold is
1043
- // what this commit is about to create. The apply loop asks it once the child has
1044
- // landed, and answers by name when it does not.
1045
957
  if (!awaitingMintedChild(mark) &&
1046
958
  markEligibility(input.snapshot, input.state, mark) === "protected") retained.push(mark);
1047
959
  else applicable.push(mark);
@@ -1055,27 +967,9 @@ export async function commitPendingMarks(input: {
1055
967
  mark: mark.mark,
1056
968
  id: mark.id,
1057
969
  origin: mark.origin,
1058
- reason: guardReasons.get(pendingMarkKey(mark)) ??
1059
- "span is still fresh or protected; the mark stays pending until it is eligible",
970
+ reason: "span is still fresh or protected; the mark stays pending until it is eligible",
1060
971
  retained: true,
1061
972
  }));
1062
- // CHILDREN BEFORE THE SPANS THAT ABSORB THEM, AND TRANSCRIPT ORDER IS NOT THAT ORDER.
1063
- //
1064
- // The invariant is real: a span whose parts name another mark's fold must find that
1065
- // fold in the forest when it applies. `foldMarkFor` gives a mark the id its committed
1066
- // fold will carry, so the naming is exact and readable off the parts. What it is NOT is
1067
- // geometry. A parent CONTAINS its child, so the earliest window index a parent covers
1068
- // is at or before its child's, and ordering oldest material first applies the parent
1069
- // FIRST in exactly the case the invariant exists for. The cost is not a reordering:
1070
- // `candidateSourceRefs` throws `Missing candidate child` on the unresolved part, the
1071
- // apply loop catches it, and the mark is refused with `retained: false`, so the parent
1072
- // applied one place too early loses the agent's decision outright rather than waiting.
1073
- //
1074
- // So the dependency is the law and the sort key is the tiebreak. Among marks with no
1075
- // such tie, real transcript order: the earliest window index the span covers, oldest
1076
- // material first, id last. The `ordinal` this used to sort on is the transcript position
1077
- // at MARK time, one value shared by every mark an epoch proposes, so within an epoch it
1078
- // decided nothing and the order fell through to comparing content hashes.
1079
973
  const minting = new Map(marks.flatMap((mark) =>
1080
974
  mark.mark === "fold" && !held.has(mark.id) ? [[mark.id, mark] as const] : []));
1081
975
  const applyStart = new Map(marks.map((mark) =>
@@ -1088,9 +982,6 @@ export async function commitPendingMarks(input: {
1088
982
  const placing = new Set<string>();
1089
983
  const place = (mark: PendingMark): void => {
1090
984
  const key = pendingMarkKey(mark);
1091
- // `placing` is the cycle stop. Fold ids are hashes OF the parts, so a cycle would
1092
- // need a hash to name its own ancestor; the guard is here so a corrupt state loses
1093
- // one mark to the ordinary refusal rather than hanging the commit.
1094
985
  if (placed.has(key) || placing.has(key)) return;
1095
986
  placing.add(key);
1096
987
  if (mark.mark === "fold") {
@@ -1103,6 +994,40 @@ export async function commitPendingMarks(input: {
1103
994
  placed.add(key);
1104
995
  ordered.push(mark);
1105
996
  };
997
+ // THE DEPTH CUT, STALEST FIRST. `base` is already in span order, so taking a prefix of
998
+ // it takes the oldest material and leaves the newest raw, which is the ordering the
999
+ // agent would choose if it were asked. A mark left over here is RETAINED rather than
1000
+ // refused: it is still pending, still briefable, still editable, and the next commit
1001
+ // that needs depth will reach it.
1002
+ const bounded: PendingMark[] = [];
1003
+ if (input.applyTargetBytes !== undefined && input.applyTargetBytes > 0) {
1004
+ let freed = 0;
1005
+ for (const mark of base) {
1006
+ if (freed >= input.applyTargetBytes) {
1007
+ bounded.push(mark);
1008
+ continue;
1009
+ }
1010
+ freed += markFreedBytes(input.snapshot, input.state, mark);
1011
+ }
1012
+ if (bounded.length) {
1013
+ const spare = new Set(bounded.map((mark) => pendingMarkKey(mark)));
1014
+ const kept = base.filter((mark) => !spare.has(pendingMarkKey(mark)));
1015
+ base.length = 0;
1016
+ base.push(...kept);
1017
+ for (const mark of bounded) {
1018
+ retained.push(mark);
1019
+ refused.push({
1020
+ mark: mark.mark,
1021
+ id: mark.id,
1022
+ origin: mark.origin,
1023
+ reason: "the commit reached its target before this mark; it stays pending and " +
1024
+ "the span stays raw until a later commit needs the depth",
1025
+ retained: true,
1026
+ });
1027
+ }
1028
+ state = withPendingMarks(input.state, retained);
1029
+ }
1030
+ }
1106
1031
  for (const mark of base) place(mark);
1107
1032
  for (const mark of ordered) {
1108
1033
  try {
@@ -1117,15 +1042,6 @@ export async function commitPendingMarks(input: {
1117
1042
  continue;
1118
1043
  }
1119
1044
  const parts = clone(mark.parts);
1120
- // THE ANSWER TO A NAME THE STATE CANNOT RESOLVE, AND IT IS TWO ANSWERS.
1121
- //
1122
- // A mark still standing will mint the fold this one names, so the span becomes
1123
- // readable at the next commit: that is a DEFER, and the mark stays pending. This
1124
- // is the child a guard held while its parent was applicable, which the epoch used
1125
- // to discard outright. Nothing standing names it and it is a DROP: `reboundary`
1126
- // dissolved the fold, or the mark that would have minted it was itself refused,
1127
- // and no later commit can honour the decision. Same two words the eligibility
1128
- // reading uses, and the same receipt shape a dropped brief upgrade gets.
1129
1045
  const span = markSpanRefs(state, mark);
1130
1046
  if (span.unresolved) {
1131
1047
  const { foldId, pending } = span.unresolved;
@@ -1144,13 +1060,11 @@ export async function commitPendingMarks(input: {
1144
1060
  continue;
1145
1061
  }
1146
1062
  const sourceRefs = span.refs;
1147
- const blocked = mark.kind === "tool-result"
1148
- ? toolRefsProtected(sourceRefs, state, input.snapshot)
1149
- : refsProtected(sourceRefs, state, input.snapshot);
1063
+ const blocked = refsProtected(sourceRefs, state, input.snapshot);
1150
1064
  if (blocked) {
1151
1065
  throw new Error(
1152
1066
  `pending fold mark ${mark.id} covers protected or fresh evidence; ` +
1153
- "unprotect that evidence and mark it again",
1067
+ "unpin that evidence and mark it again",
1154
1068
  );
1155
1069
  }
1156
1070
  const prepared = await prepareFold({
@@ -1169,9 +1083,6 @@ export async function commitPendingMarks(input: {
1169
1083
  generation: input.generation,
1170
1084
  });
1171
1085
  applied.push({ mark: "fold", id: mark.id, origin: mark.origin, foldId: prepared.id });
1172
- // A warmed preparation the commit just materialized through its mark is spent:
1173
- // leaving it standing would hold a prepared fold whose id already exists in the
1174
- // forest, and every later drift check would refuse it by name.
1175
1086
  if (state.prepared && state.folds.some((fold) => fold.id === state.prepared!.id)) {
1176
1087
  state = clearPrepared(state);
1177
1088
  }
@@ -1185,17 +1096,13 @@ export async function commitPendingMarks(input: {
1185
1096
  });
1186
1097
  }
1187
1098
  }
1188
- // Deferrals rejoin the pending set. A mark the loop could not read yet is a standing
1189
- // decision exactly as a guard-held one is, and `retained` is what the commit record
1190
- // counts as deferred, so both live in the same list.
1191
1099
  if (deferred.length) {
1192
1100
  state = withPendingMarks(state, [...pendingMarks(state), ...deferred]);
1193
1101
  retained.push(...deferred);
1194
1102
  }
1195
- return { state, applied, refused, retained, waived };
1103
+ return { state, applied, refused, retained };
1196
1104
  }
1197
1105
 
1198
- /** The scheduling block reported by the status action. */
1199
1106
  export function schedulingStatus(input: {
1200
1107
  snapshot: ActiveContextSnapshot;
1201
1108
  state: ActiveContextState;
@@ -1203,16 +1110,11 @@ export function schedulingStatus(input: {
1203
1110
  }): Record<string, unknown> {
1204
1111
  const accounting = markAccounting(input.snapshot, input.state);
1205
1112
  return {
1206
- // A literal, not a setting: epoch is the only scheduler there is.
1207
1113
  mode: "epoch",
1208
1114
  ...accounting,
1209
- // The declared policy, reported never mutable.
1210
1115
  thresholds: { ...input.snapshot.thresholds },
1211
1116
  budgetTokens: input.snapshot.budgetTokens,
1212
1117
  commitDue: epochCommitDue(input.snapshot, input.ratio),
1213
- // One trigger, reported as one line. The eligible share is still reported above in
1214
- // the accounting because it is what a commit WOULD free; it is no longer a reason
1215
- // to fire one.
1216
1118
  commitTrigger: {
1217
1119
  mode: "band-top",
1218
1120
  commitOccupancy: input.snapshot.thresholds.maxTarget,