pi-fold 2.0.0 → 3.1.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.
@@ -3,6 +3,7 @@ import type { EvidenceRef } from "../json.ts";
3
3
  import {
4
4
  bytes,
5
5
  clone,
6
+ contentText,
6
7
  messageRole,
7
8
  } from "./canonical.ts";
8
9
  import {
@@ -10,38 +11,39 @@ import {
10
11
  pinnedPeekMass,
11
12
  preparedMatchesCandidate,
12
13
  prepareFold,
14
+ projectActiveContext,
13
15
  renderFold,
14
16
  renderFoldParts,
17
+ selectAutomaticChapter,
15
18
  selectAutomaticSpan,
16
19
  setFoldProjectionState,
17
20
  } from "./folding.ts";
18
21
  import type { AutomaticRungSelection } from "./folding.ts";
22
+ import { staleSpanMatureEnd } from "./transcript.ts";
19
23
  import {
20
24
  foldInterval,
21
25
  budgetOccupancy,
22
- hardFenceRatio,
23
26
  mappedByKey,
24
27
  orderedRoots,
28
+ protectedStaleMass,
25
29
  refsProtected,
26
- toolRefsProtected,
27
30
  } from "./measurement.ts";
28
31
  import {
29
32
  childFoldIds,
30
- clearPrepared,
31
33
  flattenFoldRefs,
32
34
  foldIdFor,
33
35
  parsePendingMarks,
34
36
  pendingMarkKey,
35
37
  } from "./persistence.ts";
36
- import { currentTurnRefKeys } from "./transcript.ts";
37
38
  import {
38
39
  ESTIMATED_BYTES_PER_TOKEN,
39
- MAX_WEDGE_ABSORB_TOKENS,
40
40
  ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES,
41
+ MAX_FRONTIER_CUTS_PER_PASS,
41
42
  MAX_PENDING_MARKS,
42
43
  MAX_UNMARKED_CANDIDATES,
43
44
  } from "./policy.ts";
44
45
  import type {
46
+ ToolClip,
45
47
  ActiveContextSnapshot,
46
48
  ActiveContextState,
47
49
  FoldCandidate,
@@ -58,6 +60,7 @@ import {
58
60
  partsForRange,
59
61
  peekedSourceFoldIds,
60
62
  selectAutomaticConsolidations,
63
+ selectAutomaticRefold,
61
64
  spanBytes,
62
65
  deterministicChapterCandidateBrief,
63
66
  deterministicConsolidationBrief,
@@ -185,15 +188,6 @@ export function markFreedBytes(
185
188
  return Math.max(0, bytes(source) - placeholder);
186
189
  }
187
190
 
188
- export function markTouchesCurrentTurn(
189
- state: ActiveContextState,
190
- mark: PendingMark,
191
- currentTurn: ReadonlySet<string>,
192
- ): boolean {
193
- if (!currentTurn.size) return false;
194
- return markSpanRefs(state, mark).refs.some((ref) => currentTurn.has(objectRefKey(ref)));
195
- }
196
-
197
191
  export function markSpanStart(
198
192
  snapshot: ActiveContextSnapshot,
199
193
  state: ActiveContextState,
@@ -210,46 +204,6 @@ export function markSpanStart(
210
204
  return indices.length ? Math.min(...indices) : snapshot.mapped.length;
211
205
  }
212
206
 
213
- export const GUARD_WAIVER_PROTECTED_MARKS = 2;
214
-
215
- export const GUARD_WAIVER_MINIMUM_MARKS = 4;
216
-
217
- export function guardWaiverCount(input: {
218
- snapshot: ActiveContextSnapshot;
219
- ratio: number | null;
220
- guardedMarks: number;
221
- otherApplicableMarks: number;
222
- /**
223
- * THE BOUNDARY IS ITS OWN SIGNAL, and a better one than the occupancy anchor.
224
- *
225
- * The anchor exists to answer "is this session in enough trouble to spend the open
226
- * turn's own evidence": a proxy, because nothing else was asking. At a compaction
227
- * boundary Pi has already answered it. Cancelling the compaction and then applying
228
- * nothing hands Pi's own threshold a byte-identical projection, which is worse than
229
- * letting it compact, because compaction would at least have shed something.
230
- *
231
- * Measured on the sol-20260813 smoke: the boundary fired, cancelled the compaction and
232
- * retained all seven marks at 0.20 occupancy, freeing nothing. The full run reaches the
233
- * anchor 12,576 tokens past its own trigger, so it self-corrects there and this is
234
- * about what happens in between; a smoke, or any session whose budget it never
235
- * approaches, never corrects at all.
236
- *
237
- * Everything else about the waiver is unchanged, and deliberately: the newest marks
238
- * still stay raw, a small guarded set still holds whole, and other applicable work
239
- * still takes precedence over spending the open turn.
240
- */
241
- boundary?: boolean;
242
- }): number {
243
- const { snapshot, ratio, guardedMarks, otherApplicableMarks, boundary } = input;
244
- if (guardedMarks <= 0 || typeof ratio !== "number" || !Number.isFinite(ratio)) return 0;
245
- if (ratio >= hardFenceRatio(snapshot)) return guardedMarks;
246
- const occupancy = budgetOccupancy(snapshot, ratio);
247
- if (occupancy === null || otherApplicableMarks > 0) return 0;
248
- if (occupancy < snapshot.policy.refoldRatio && !boundary) return 0;
249
- if (guardedMarks < GUARD_WAIVER_MINIMUM_MARKS) return 0;
250
- return Math.max(1, guardedMarks - GUARD_WAIVER_PROTECTED_MARKS);
251
- }
252
-
253
207
  export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
254
208
 
255
209
  export function markEligibility(
@@ -266,11 +220,16 @@ export function markEligibility(
266
220
  if (span.unresolved) return span.unresolved.pending ? "protected" : "unfulfillable";
267
221
  const refs = span.refs;
268
222
  if (!refs.length) return "unfulfillable";
269
- const mapped = new Set(snapshot.mapped.flatMap((item) => item.ref ? [objectRefKey(item.ref)] : []));
223
+ // THE MAPPED INDEX IS ALREADY DERIVED (2026-08-28). This built a fresh Set over the whole
224
+ // window to answer a membership question about the handful of refs in one mark's span, and
225
+ // `markAccounting` calls this once per pending mark while the commit fill calls
226
+ // `markAccounting` once per staged mark, so it was O(marks^2 x window). `mappedByKey` is
227
+ // gate 113's memo, keyed on the snapshot OBJECT, and `refsProtected` on the next line
228
+ // already depends on it being fresh: if it could serve stale, this function was already
229
+ // wrong. Measured 177.2ms to 41.6ms at the observed peak of 39 pending marks.
230
+ const mapped = mappedByKey(snapshot);
270
231
  if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
271
- const blocked = mark.kind === "tool-result"
272
- ? toolRefsProtected(refs, state, snapshot)
273
- : refsProtected(refs, state, snapshot);
232
+ const blocked = refsProtected(refs, state, snapshot);
274
233
  return blocked ? "protected" : "eligible";
275
234
  }
276
235
 
@@ -278,10 +237,10 @@ export interface MarkAccounting {
278
237
  pending: number;
279
238
  agentMarks: number;
280
239
  ladderMarks: number;
240
+ userMarks: number;
281
241
  freedBytes: number;
282
242
  freedTokens: number;
283
243
  freedBudgetShare: number;
284
- rewriteTokens: number;
285
244
  eligibleMarks: number;
286
245
  retainedMarks: number;
287
246
  eligibleFreedBytes: number;
@@ -296,13 +255,10 @@ export function markAccounting(
296
255
  state: ActiveContextState,
297
256
  ): MarkAccounting {
298
257
  const marks = pendingMarks(state);
299
- const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
300
- item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
301
258
  let freedBytes = 0;
302
259
  let eligibleFreedBytes = 0;
303
260
  let eligibleMarks = 0;
304
261
  let retainedMarks = 0;
305
- let earliest = -1;
306
262
  for (const mark of marks) {
307
263
  const freed = markFreedBytes(snapshot, state, mark);
308
264
  freedBytes += freed;
@@ -311,15 +267,7 @@ export function markAccounting(
311
267
  eligibleMarks += 1;
312
268
  eligibleFreedBytes += freed;
313
269
  } else if (eligibility === "protected") retainedMarks += 1;
314
- for (const ref of markSpanRefs(state, mark).refs) {
315
- const index = indexByKey.get(objectRefKey(ref));
316
- if (index === undefined) continue;
317
- if (earliest < 0 || index < earliest) earliest = index;
318
- }
319
270
  }
320
- const rewriteBytes = earliest < 0
321
- ? 0
322
- : bytes(snapshot.messages.slice(earliest));
323
271
  const freedTokens = estimatedTokens(freedBytes);
324
272
  const eligibleFreedTokens = estimatedTokens(eligibleFreedBytes);
325
273
  const pinned = pinnedPeekMass(snapshot, state);
@@ -327,10 +275,10 @@ export function markAccounting(
327
275
  pending: marks.length,
328
276
  agentMarks: marks.filter((mark) => mark.origin === "agent").length,
329
277
  ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
278
+ userMarks: marks.filter((mark) => mark.origin === "user").length,
330
279
  freedBytes,
331
280
  freedTokens,
332
281
  freedBudgetShare: snapshot.budgetTokens > 0 ? freedTokens / snapshot.budgetTokens : 0,
333
- rewriteTokens: estimatedTokens(rewriteBytes),
334
282
  eligibleMarks,
335
283
  retainedMarks,
336
284
  eligibleFreedBytes,
@@ -343,6 +291,40 @@ export function markAccounting(
343
291
  };
344
292
  }
345
293
 
294
+ /**
295
+ * What one commit would make the provider re-read: the marked tail, from the earliest
296
+ * marked message to the end of the window.
297
+ *
298
+ * IT IS DERIVED WHERE IT IS READ (2026-08-28). This was a field on `MarkAccounting`, which
299
+ * meant `bytes(snapshot.messages.slice(earliest))`, a serialization of the whole marked
300
+ * tail, ran on EVERY accounting call: the commit fill calls one per staged mark, and the
301
+ * `brief` handler calls one to read `pending` alone. Two readers want the number and both
302
+ * already hold the snapshot and the state, so the work moved to them. A memo could not have
303
+ * helped: `snapshot.messages` is `clone(input.eventMessages)` (transcript.ts:531), so its
304
+ * elements are fresh objects on every derivation and nothing keyed on them can ever hit.
305
+ *
306
+ * `schedulingStatus` still publishes `rewriteTokens` and now derives it here, so the status
307
+ * surface is unchanged. It is NOT a getter on the accounting object: `schedulingStatus`
308
+ * spreads the accounting, and a spread evaluates getters, which would put the cost straight
309
+ * back on every reader it was moved away from.
310
+ */
311
+ export function markRewriteTokens(
312
+ snapshot: ActiveContextSnapshot,
313
+ state: ActiveContextState,
314
+ ): number {
315
+ const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
316
+ item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
317
+ let earliest = -1;
318
+ for (const mark of pendingMarks(state)) {
319
+ for (const ref of markSpanRefs(state, mark).refs) {
320
+ const index = indexByKey.get(objectRefKey(ref));
321
+ if (index === undefined) continue;
322
+ if (earliest < 0 || index < earliest) earliest = index;
323
+ }
324
+ }
325
+ return earliest < 0 ? 0 : estimatedTokens(bytes(snapshot.messages.slice(earliest)));
326
+ }
327
+
346
328
  export function epochCommitDue(snapshot: ActiveContextSnapshot, ratio: number | null): boolean {
347
329
  const occupancy = budgetOccupancy(snapshot, ratio);
348
330
  return occupancy !== null && occupancy >= snapshot.thresholds.maxTarget;
@@ -442,8 +424,7 @@ export function ephemeralPeekMarks(input: {
442
424
  if (!peeked) continue;
443
425
  if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
444
426
  if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
445
- refs.length > snapshot.policy.maxFoldSourceRefs ||
446
- toolRefsProtected(refs, state, snapshot)) continue;
427
+ refsProtected(refs, state, snapshot)) continue;
447
428
  const candidate: FoldCandidate = {
448
429
  kind: "tool-result",
449
430
  parts: refs.map((ref) => ({ kind: "raw" as const, ref })),
@@ -468,9 +449,7 @@ function peekedFoldProtected(
468
449
  const fold = state.folds.find((item) => item.id === foldId);
469
450
  if (!fold) return false;
470
451
  const refs = flattenFoldRefs(fold, state);
471
- return fold.kind === "tool-result"
472
- ? toolRefsProtected(refs, state, snapshot)
473
- : refsProtected(refs, state, snapshot);
452
+ return refsProtected(refs, state, snapshot);
474
453
  }
475
454
 
476
455
  export function claimedRefKeys(state: ActiveContextState): Set<string> {
@@ -488,6 +467,8 @@ export function claimedRefKeys(state: ActiveContextState): Set<string> {
488
467
  export interface UnmarkedRemainder {
489
468
  spans: number;
490
469
  tokens: number;
470
+ /** Raw bytes, which is what the fold frontier measures itself against. */
471
+ chars: number;
491
472
  share: number;
492
473
  candidates: Array<{ id: string; tokens: number }>;
493
474
  }
@@ -504,6 +485,33 @@ export function unmarkedRemainder(
504
485
  for (const batch of automaticToolBatches(snapshot, state)) {
505
486
  for (const index of batch.indices) members.add(index);
506
487
  }
488
+ // CHAPTERS ARE FOLDABLE MASS TOO (Shane, 2026-08-23).
489
+ //
490
+ // The population used to be tool batches alone, while `selectAutomaticSpan` proposes a
491
+ // chapter whenever no batch is available, so on a chapter-heavy session this reported a
492
+ // remainder of zero over material automatic folding was about to take. It is the reading
493
+ // a deferred commit uses to say whether its mass is HELD or simply absent, which is the
494
+ // one distinction that record exists to draw, and it was answering "absent" for mass in
495
+ // plain view. It also broke the frontier's first cut, which gated on this number before
496
+ // the threshold moved into the selector where it belongs.
497
+ //
498
+ // Walked with the selector rather than re-derived, so the population is what automatic
499
+ // folding can actually reach and cannot drift from it. Walked from an EMPTY claim set,
500
+ // because this is the DENOMINATOR: a chapter already claimed by a mark is still foldable
501
+ // mass, it is just mass that is already spoken for, and the numerator below is what
502
+ // subtracts it. Bounded by the mapped length, which no terminating walk can exceed since
503
+ // every pass claims at least one ref.
504
+ const indexed = mappedByKey(snapshot);
505
+ const walked = new Set<string>();
506
+ for (let pass = 0; pass < snapshot.mapped.length; pass += 1) {
507
+ const chapter = selectAutomaticChapter(snapshot, state, walked);
508
+ if (!chapter) break;
509
+ for (const ref of chapter.sourceRefs) {
510
+ walked.add(objectRefKey(ref));
511
+ const item = indexed.get(objectRefKey(ref));
512
+ if (item) members.add(item.index);
513
+ }
514
+ }
507
515
  const candidates: Array<{ id: string; tokens: number }> = [];
508
516
  let unmarkedBytes = 0;
509
517
  let memberBytes = 0;
@@ -519,11 +527,193 @@ export function unmarkedRemainder(
519
527
  return {
520
528
  spans: candidates.length,
521
529
  tokens: Math.ceil(unmarkedBytes / perToken),
530
+ chars: unmarkedBytes,
522
531
  share: memberBytes > 0 ? unmarkedBytes / memberBytes : 0,
523
532
  candidates: candidates.slice(0, Math.max(0, limit)),
524
533
  };
525
534
  }
526
535
 
536
+ /**
537
+ * THE FOLD FRONTIER: THE RUNTIME CUTS, AND THE AGENT EDITS WHAT IT CUT
538
+ * (Shane, 2026-08-23).
539
+ *
540
+ * The ladder used to stage nothing between commits, and before that it staged a mark on
541
+ * every measured response. Both were answers to the same question, WHEN SHOULD THE AGENT
542
+ * CURATE, and both got it wrong in the same way: they asked the agent to CHOOSE SPANS.
543
+ * Eager staging chose them first and crowded the agent out; standing down left the choice
544
+ * open until the commit, by which time the material was long out of view and the only
545
+ * information available was staleness, which the runtime already has.
546
+ *
547
+ * So the choice moves. As raw material accumulates past `minFoldChars`, the runtime cuts
548
+ * it, stalest first, exactly as a commit would. The cut is a PENDING MARK and pending
549
+ * marks are byte-inert: nothing moves, the projection does not change, the prefix cache is
550
+ * untouched, and the agent keeps seeing the raw material until a commit applies it. What
551
+ * the agent gets is a fold to annotate while the content is still fresh, which is the one
552
+ * thing it knows that the runtime does not.
553
+ *
554
+ * The loop bounds itself twice over. It stops when the unmarked remainder falls under the
555
+ * threshold, and it stops when the selector runs out of members, so a window whose whole
556
+ * remainder is pinned or blacklisted stages nothing and says so by returning empty rather
557
+ * than spinning.
558
+ */
559
+ export function frontierMarks(input: {
560
+ snapshot: ActiveContextSnapshot;
561
+ state: ActiveContextState;
562
+ ordinal: number;
563
+ }): PendingFoldMark[] {
564
+ const { snapshot } = input;
565
+ const claimed = claimedRefKeys(input.state);
566
+ const marks: PendingFoldMark[] = [];
567
+ let state = input.state;
568
+ while (marks.length < MAX_FRONTIER_CUTS_PER_PASS) {
569
+ // THE THRESHOLD IS THE SELECTOR'S OWN, which is `minFoldChars`, and it is read there
570
+ // rather than counted here. The first cut of this measured the unmarked remainder
571
+ // instead, and that reads TOOL BATCHES ONLY: a chapter-only session had a remainder of
572
+ // zero however much raw prose it held, so the frontier never fired on it at all and
573
+ // gate 7's fixture produced no cut. One threshold, in the one place that knows every
574
+ // span kind.
575
+ const candidate = selectAutomaticSpan(snapshot, state, claimed);
576
+ if (!candidate) break;
577
+ const mark = foldMarkFor({
578
+ candidate,
579
+ ...automaticMarkBrief(snapshot, state, candidate),
580
+ origin: "ladder",
581
+ ordinal: input.ordinal,
582
+ });
583
+ const addition = addPendingMark(state, mark);
584
+ if (!addition.added) break;
585
+ state = addition.state;
586
+ marks.push(mark);
587
+ for (const ref of candidate.sourceRefs) claimed.add(objectRefKey(ref));
588
+ }
589
+ return marks;
590
+ }
591
+
592
+ /**
593
+ * The pending mark that already holds a piece of this evidence, if any.
594
+ *
595
+ * The commit fills from the stale end and the depth bound retains what it does not
596
+ * reach, so a ladder mark routinely outlives the commit that staged it. An agent
597
+ * marking the same evidence afterwards would stand a second owner over one span and
598
+ * lose at apply time with a forest invariant for a message. Refusing at the tool says
599
+ * so while the agent can still act on it.
600
+ */
601
+ export function markClaimingRef(
602
+ state: ActiveContextState,
603
+ refs: readonly EvidenceRef[],
604
+ ): { markId: string; entryId: string } | null {
605
+ for (const mark of pendingMarks(state)) {
606
+ if (mark.mark !== "fold") continue;
607
+ const claimed = new Set(markSpanRefs(state, mark).refs.map(objectRefKey));
608
+ for (const ref of refs) {
609
+ if (claimed.has(objectRefKey(ref))) return { markId: mark.id, entryId: ref.entryId };
610
+ }
611
+ }
612
+ return null;
613
+ }
614
+
615
+ export interface WindowClaims {
616
+ /** Tokens standing marks will free at the next commit. */
617
+ markedTokens: number;
618
+ /** Tokens the agent pinned raw: a claim that frees nothing and is never taken. */
619
+ pinnedTokens: number;
620
+ pinnedRefs: number;
621
+ /** Pinned mass as a share of the serving budget, against its cap. */
622
+ pinnedShare: number;
623
+ /** Completed units neither marked nor pinned: what the ladder takes by age. */
624
+ unclaimedTokens: number;
625
+ unclaimedSpans: number;
626
+ }
627
+
628
+ /**
629
+ * A PIN IS A CLAIM, NOT A FOLD (Shane, 2026-08-22: "if they do a pin on raw span I think
630
+ * we also treat that as a marked fold but a pinned one").
631
+ *
632
+ * Read as accounting, that is exactly right and already half true: pinned evidence is
633
+ * refused to every automatic rung, skipped by wedge absorption, and excluded from the
634
+ * unmarked remainder, so the ladder cannot take it and it is never reported as an
635
+ * oversight. What was missing is that the agent could not SEE it, and the whole point of
636
+ * asking an agent to account for its window is that the gaps become visible.
637
+ *
638
+ * A pin cannot count toward the DROP, and that is not a technicality. Pinned bytes stay
639
+ * in the window, so they free nothing; if they counted, an agent could "cover" a commit
640
+ * with pins, the commit would still be short, and it would fold something else anyway.
641
+ * The number would be a lie at the moment it mattered most. So the three buckets are
642
+ * reported side by side and only one of them pays.
643
+ */
644
+ export function windowClaims(input: {
645
+ snapshot: ActiveContextSnapshot;
646
+ state: ActiveContextState;
647
+ budgetTokens: number;
648
+ charsPerToken: number;
649
+ }): WindowClaims {
650
+ const { snapshot, state, budgetTokens } = input;
651
+ const accounting = markAccounting(snapshot, state);
652
+ const remainder = unmarkedRemainder(snapshot, state, input.charsPerToken);
653
+ const pinned = protectedStaleMass(snapshot, state);
654
+ const pinnedTokens = estimatedTokens(pinned.bytes);
655
+ return {
656
+ markedTokens: Math.round(accounting.eligibleFreedBudgetShare * budgetTokens),
657
+ pinnedTokens,
658
+ pinnedRefs: pinned.refs,
659
+ pinnedShare: budgetTokens > 0 ? pinnedTokens / budgetTokens : 0,
660
+ unclaimedTokens: remainder.tokens,
661
+ unclaimedSpans: remainder.spans,
662
+ };
663
+ }
664
+
665
+ export interface CommitCoverage {
666
+ /** The share of the serving budget the next commit is sized to free. */
667
+ targetShare: number;
668
+ targetTokens: number;
669
+ /** What the standing marks will actually free when that commit runs. */
670
+ markedShare: number;
671
+ markedTokens: number;
672
+ /** What the ladder will take by staleness unless the agent marks it first. */
673
+ remainingTokens: number;
674
+ covered: boolean;
675
+ }
676
+
677
+ /**
678
+ * WHAT THE NEXT COMMIT NEEDS, AND HOW MUCH OF IT THE AGENT HAS COVERED.
679
+ *
680
+ * One definition, read by three callers that used to reason about the drop separately:
681
+ * the commit fill that sizes itself to it, the advisory that warns before it, and the
682
+ * mark response that tells the agent where it stands. An agent asked to govern its own
683
+ * context cannot do it against a number nobody tells it.
684
+ *
685
+ * The marked side is the ELIGIBLE share, not every standing mark, because a mark over
686
+ * protected evidence frees nothing at this commit however good it is. That makes
687
+ * `remainingTokens` the honest answer to "what will fold without me".
688
+ *
689
+ * `targetShare` moves as the window grows: it is measured from occupancy NOW, and two
690
+ * large results landing after the reading raise it again. Callers that render it say so.
691
+ */
692
+ export function commitCoverage(input: {
693
+ snapshot: ActiveContextSnapshot;
694
+ state: ActiveContextState;
695
+ usedTokens: number | null;
696
+ budgetTokens: number;
697
+ }): CommitCoverage {
698
+ const { snapshot, state, usedTokens, budgetTokens } = input;
699
+ const targetShare = usedTokens === null || !(budgetTokens > 0)
700
+ ? 0
701
+ : Math.max(0, (usedTokens - snapshot.thresholds.minTarget * budgetTokens) / budgetTokens);
702
+ const accounting = markAccounting(snapshot, state);
703
+ const markedShare = accounting.eligibleFreedBudgetShare;
704
+ const targetTokens = Math.round(targetShare * budgetTokens);
705
+ const markedTokens = Math.round(markedShare * budgetTokens);
706
+ const remainingTokens = Math.max(0, targetTokens - markedTokens);
707
+ return {
708
+ targetShare,
709
+ targetTokens,
710
+ markedShare,
711
+ markedTokens,
712
+ remainingTokens,
713
+ covered: remainingTokens === 0,
714
+ };
715
+ }
716
+
527
717
  export function markedFoldIds(state: ActiveContextState): Set<string> {
528
718
  const ids = new Set<string>();
529
719
  for (const mark of pendingMarks(state)) if (mark.mark === "refold") ids.add(mark.id);
@@ -548,13 +738,11 @@ export function topUpMarks(input: {
548
738
  state: ActiveContextState;
549
739
  ordinal: number;
550
740
  targetShare: number;
551
- excludeRefKeys?: ReadonlySet<string>;
552
741
  eligibleOnly?: boolean;
553
742
  }): PendingFoldMark[] {
554
743
  const { snapshot } = input;
555
744
  const target = input.targetShare;
556
745
  const claimed = claimedRefKeys(input.state);
557
- for (const key of input.excludeRefKeys ?? []) claimed.add(key);
558
746
  const marks: PendingFoldMark[] = [];
559
747
  let state = input.state;
560
748
  const progress = (value: ActiveContextState): number => {
@@ -581,6 +769,50 @@ export function topUpMarks(input: {
581
769
  return marks;
582
770
  }
583
771
 
772
+ /**
773
+ * REFOLD AS THE LAST FILL, NOT THE FIRST (Shane, 2026-08-21).
774
+ *
775
+ * An expanded fold is stale mass sitting in the window, so the commit may reclaim it,
776
+ * but only after raw stale material has been spent. An agent that expands a fold is
777
+ * reading it; taking it back while there is still unfolded material to take is hostile
778
+ * for no gain. Pin holds a span past that point, and a live lease holds it for the
779
+ * generations right after the expand. Stalest first, and never past the freeing target.
780
+ */
781
+ export function refoldMarks(input: {
782
+ snapshot: ActiveContextSnapshot;
783
+ state: ActiveContextState;
784
+ ordinal: number;
785
+ targetShare: number;
786
+ eligibleOnly?: boolean;
787
+ }): PendingMark[] {
788
+ const { snapshot } = input;
789
+ const marks: PendingMark[] = [];
790
+ let state = input.state;
791
+ const claimedFoldIds = markedFoldIds(state);
792
+ const progress = (value: ActiveContextState): number => {
793
+ const accounting = markAccounting(snapshot, value);
794
+ return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
795
+ };
796
+ let share = progress(state);
797
+ while (share < input.targetShare) {
798
+ const foldId = selectAutomaticRefold(snapshot, state, claimedFoldIds);
799
+ if (!foldId) break;
800
+ const mark: PendingMark = {
801
+ mark: "refold",
802
+ id: foldId,
803
+ origin: "ladder",
804
+ ordinal: input.ordinal,
805
+ };
806
+ const addition = addPendingMark(state, mark);
807
+ if (!addition.added) break;
808
+ state = addition.state;
809
+ marks.push(mark);
810
+ claimedFoldIds.add(foldId);
811
+ share = progress(state);
812
+ }
813
+ return marks;
814
+ }
815
+
584
816
  export interface AbsorbedWedge {
585
817
  intoMarkId: string;
586
818
  fromMarkId: string;
@@ -588,6 +820,7 @@ export interface AbsorbedWedge {
588
820
  endId: string;
589
821
  entries: number;
590
822
  tokens: number;
823
+ chars: number;
591
824
  }
592
825
 
593
826
  export function absorbWedgeMarks(input: {
@@ -631,8 +864,16 @@ export function absorbWedgeMarks(input: {
631
864
  const gapEnd = interval.start - 1;
632
865
  if (gapStart < 0 || gapEnd < gapStart) continue;
633
866
  if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
634
- const gapTokens = Math.ceil(spanBytes(snapshot, gapStart, gapEnd + 1) / charsPerToken);
635
- if (gapTokens > MAX_WEDGE_ABSORB_TOKENS) continue;
867
+ // THE GAP FLOOR IS THE FOLD FLOOR (Shane, 2026-08-21). A gap between two folds
868
+ // that could not stand as a fold on its own has no business staying raw: it costs
869
+ // its own bytes and buys nothing, and the neighbour beside it can hold it for the
870
+ // price of one sentence in the brief. So anything under minFoldChars is absorbed
871
+ // and anything at or over it is left alone, because at that size it is a fold the
872
+ // ladder will take on its own terms. This replaced MAX_WEDGE_ABSORB_TOKENS, a
873
+ // separate 256-token constant that let 1,024-char slivers survive between folds.
874
+ const gapBytes = spanBytes(snapshot, gapStart, gapEnd + 1);
875
+ if (gapBytes >= snapshot.thresholds.minFoldChars) continue;
876
+ const gapTokens = Math.ceil(gapBytes / charsPerToken);
636
877
  const gapRefs = [];
637
878
  let usable = true;
638
879
  for (let index = gapStart; index <= gapEnd; index += 1) {
@@ -669,6 +910,7 @@ export function absorbWedgeMarks(input: {
669
910
  endId: gapRefs.at(-1)!.entryId,
670
911
  entries: gapRefs.length,
671
912
  tokens: gapTokens,
913
+ chars: gapBytes,
672
914
  });
673
915
  applied = true;
674
916
  break;
@@ -698,7 +940,6 @@ export interface CommitEpochResult {
698
940
  applied: AppliedMark[];
699
941
  refused: RefusedMark[];
700
942
  retained: PendingMark[];
701
- waived: PendingMark[];
702
943
  }
703
944
 
704
945
  export async function commitPendingMarks(input: {
@@ -707,48 +948,40 @@ export async function commitPendingMarks(input: {
707
948
  generation: number;
708
949
  now?: () => number;
709
950
  retainIneligible?: boolean;
710
- guardCurrentTurn?: boolean;
711
- guardWaiver?: number;
951
+ /**
952
+ * How many bytes this commit needs to free. Marks past it are RETAINED, not applied.
953
+ *
954
+ * This bound did not exist before the frontier (Shane, 2026-08-23), and it did not need
955
+ * to: staging was sized to the drop by `topUpMarks`, so every pending mark was one the
956
+ * commit had already decided it wanted. The frontier stages as material ARRIVES, which
957
+ * is the whole point of it, so by the time a commit fires the pending set is everything
958
+ * the window holds. Applying all of it would cut the window to nothing at the first
959
+ * commit, which is neither what the budget asks for nor what the agent is owed: what it
960
+ * is owed is that material stays raw for as long as there is room for it.
961
+ *
962
+ * Undefined means no bound, which is what the emergency lanes want: a fence or a
963
+ * rollback is not housekeeping and takes everything it can.
964
+ */
965
+ applyTargetBytes?: number;
712
966
  }): Promise<CommitEpochResult> {
967
+ // THE CURRENT-TURN GUARD IS DELETED (Shane, 2026-08-23: "you're using turns as the
968
+ // boundaries, which should not be the case. It should be at the most granular level,
969
+ // which is events"). The guard retained every mark touching a toolResult newer than
970
+ // the last terminal assistant stop, and a terminal stop is a TURN artifact: the first
971
+ // live session (sol-20260823-live rep 1) ran its whole workload as one turn, every
972
+ // assistant response stopping "toolUse", so the guard's "current turn" was the entire
973
+ // window, four band-top commits applied nothing, and the projection fence swept 18
974
+ // marks at 0.958 occupancy down to a 0.031 landing against a 0.40 aim. The waiver
975
+ // machinery existed only to bail the guard out at the fence and the boundary, so it
976
+ // is deleted with it. What protects the working set now is structural and event-level:
977
+ // an incomplete batch is never proposable, the depth bound stops the routine commit at
978
+ // the aim, and the stalest-first cut order leaves the newest events raw.
713
979
  const marks = pendingMarks(input.state);
714
980
  const retained: PendingMark[] = [];
715
- const waived: PendingMark[] = [];
716
- const guardReasons = new Map<string, string>();
717
981
  const held = new Set(input.state.folds.map((fold) => fold.id));
718
982
  const deferred: PendingMark[] = [];
719
983
  const awaitingMintedChild = (mark: PendingMark): boolean =>
720
984
  markSpanRefs(input.state, mark).unresolved?.pending === true;
721
- const currentTurn = input.guardCurrentTurn
722
- ? currentTurnRefKeys(input.snapshot)
723
- : new Set<string>();
724
- if (input.guardCurrentTurn) {
725
- const applicable: PendingMark[] = [];
726
- const guarded: PendingMark[] = [];
727
- for (const mark of marks) {
728
- if (markTouchesCurrentTurn(input.state, mark, currentTurn)) guarded.push(mark);
729
- else applicable.push(mark);
730
- }
731
- const guardedStart = new Map(guarded.map((mark) =>
732
- [pendingMarkKey(mark), markSpanStart(input.snapshot, input.state, mark)] as const));
733
- guarded.sort((left, right) =>
734
- guardedStart.get(pendingMarkKey(left))! - guardedStart.get(pendingMarkKey(right))! ||
735
- left.id.localeCompare(right.id));
736
- const waiverCount = Math.max(0, Math.min(guarded.length, Math.trunc(input.guardWaiver ?? 0)));
737
- for (const [index, mark] of guarded.entries()) {
738
- if (index < waiverCount) {
739
- waived.push(mark);
740
- applicable.push(mark);
741
- continue;
742
- }
743
- retained.push(mark);
744
- guardReasons.set(
745
- pendingMarkKey(mark),
746
- "evidence was gathered in the current turn; the mark stays pending until the turn closes",
747
- );
748
- }
749
- marks.length = 0;
750
- marks.push(...applicable);
751
- }
752
985
  if (input.retainIneligible) {
753
986
  const applicable: PendingMark[] = [];
754
987
  for (const mark of marks) {
@@ -765,8 +998,7 @@ export async function commitPendingMarks(input: {
765
998
  mark: mark.mark,
766
999
  id: mark.id,
767
1000
  origin: mark.origin,
768
- reason: guardReasons.get(pendingMarkKey(mark)) ??
769
- "span is still fresh or protected; the mark stays pending until it is eligible",
1001
+ reason: "span is still fresh or protected; the mark stays pending until it is eligible",
770
1002
  retained: true,
771
1003
  }));
772
1004
  const minting = new Map(marks.flatMap((mark) =>
@@ -793,6 +1025,40 @@ export async function commitPendingMarks(input: {
793
1025
  placed.add(key);
794
1026
  ordered.push(mark);
795
1027
  };
1028
+ // THE DEPTH CUT, STALEST FIRST. `base` is already in span order, so taking a prefix of
1029
+ // it takes the oldest material and leaves the newest raw, which is the ordering the
1030
+ // agent would choose if it were asked. A mark left over here is RETAINED rather than
1031
+ // refused: it is still pending, still briefable, still editable, and the next commit
1032
+ // that needs depth will reach it.
1033
+ const bounded: PendingMark[] = [];
1034
+ if (input.applyTargetBytes !== undefined && input.applyTargetBytes > 0) {
1035
+ let freed = 0;
1036
+ for (const mark of base) {
1037
+ if (freed >= input.applyTargetBytes) {
1038
+ bounded.push(mark);
1039
+ continue;
1040
+ }
1041
+ freed += markFreedBytes(input.snapshot, input.state, mark);
1042
+ }
1043
+ if (bounded.length) {
1044
+ const spare = new Set(bounded.map((mark) => pendingMarkKey(mark)));
1045
+ const kept = base.filter((mark) => !spare.has(pendingMarkKey(mark)));
1046
+ base.length = 0;
1047
+ base.push(...kept);
1048
+ for (const mark of bounded) {
1049
+ retained.push(mark);
1050
+ refused.push({
1051
+ mark: mark.mark,
1052
+ id: mark.id,
1053
+ origin: mark.origin,
1054
+ reason: "the commit reached its target before this mark; it stays pending and " +
1055
+ "the span stays raw until a later commit needs the depth",
1056
+ retained: true,
1057
+ });
1058
+ }
1059
+ state = withPendingMarks(input.state, retained);
1060
+ }
1061
+ }
796
1062
  for (const mark of base) place(mark);
797
1063
  for (const mark of ordered) {
798
1064
  try {
@@ -825,13 +1091,11 @@ export async function commitPendingMarks(input: {
825
1091
  continue;
826
1092
  }
827
1093
  const sourceRefs = span.refs;
828
- const blocked = mark.kind === "tool-result"
829
- ? toolRefsProtected(sourceRefs, state, input.snapshot)
830
- : refsProtected(sourceRefs, state, input.snapshot);
1094
+ const blocked = refsProtected(sourceRefs, state, input.snapshot);
831
1095
  if (blocked) {
832
1096
  throw new Error(
833
1097
  `pending fold mark ${mark.id} covers protected or fresh evidence; ` +
834
- "unprotect that evidence and mark it again",
1098
+ "unpin that evidence and mark it again",
835
1099
  );
836
1100
  }
837
1101
  const prepared = await prepareFold({
@@ -850,9 +1114,6 @@ export async function commitPendingMarks(input: {
850
1114
  generation: input.generation,
851
1115
  });
852
1116
  applied.push({ mark: "fold", id: mark.id, origin: mark.origin, foldId: prepared.id });
853
- if (state.prepared && state.folds.some((fold) => fold.id === state.prepared!.id)) {
854
- state = clearPrepared(state);
855
- }
856
1117
  } catch (error) {
857
1118
  refused.push({
858
1119
  mark: mark.mark,
@@ -867,7 +1128,7 @@ export async function commitPendingMarks(input: {
867
1128
  state = withPendingMarks(state, [...pendingMarks(state), ...deferred]);
868
1129
  retained.push(...deferred);
869
1130
  }
870
- return { state, applied, refused, retained, waived };
1131
+ return { state, applied, refused, retained };
871
1132
  }
872
1133
 
873
1134
  export function schedulingStatus(input: {
@@ -879,6 +1140,11 @@ export function schedulingStatus(input: {
879
1140
  return {
880
1141
  mode: "epoch",
881
1142
  ...accounting,
1143
+ // The status surface keeps this field; the accounting no longer carries it. Status is a
1144
+ // read the agent or the human asks for, so paying for the tail serialization HERE is
1145
+ // the point of moving it: the commit fill calls `markAccounting` once per staged mark
1146
+ // and none of those calls wants this number.
1147
+ rewriteTokens: markRewriteTokens(input.snapshot, input.state),
882
1148
  thresholds: { ...input.snapshot.thresholds },
883
1149
  budgetTokens: input.snapshot.budgetTokens,
884
1150
  commitDue: epochCommitDue(input.snapshot, input.ratio),
@@ -898,3 +1164,57 @@ export function schedulingStatus(input: {
898
1164
  })),
899
1165
  };
900
1166
  }
1167
+
1168
+ // THE TOOL-CALL DIET SELECTS AT COMMIT TIME ONLY (2026-08-24, Shane's ToolFoldThreshold).
1169
+ // The zone is the oldest `threshold` share of the projected window measured in bytes,
1170
+ // because "the tool calls prior to the X point" is a statement about what the provider
1171
+ // is being made to re-read, and the projection is exactly that. Eligibility mirrors the
1172
+ // fold lanes' own laws one view over: the result must be MATURE (past
1173
+ // staleSpanMatureEnd, the batch lane's consumption proof), unprotected (a pinned ref is
1174
+ // a durable hold), not blacklisted (a tool whose results must stay raw stays raw in
1175
+ // view too), bigger than the floor (small calls are cheap; the win is the multi-KB
1176
+ // result), not a rendering the projection already substituted, and not already clipped.
1177
+ // The additions are VIEW state: encodedFoldSource reads raw parts, so a later fold of a
1178
+ // clipped span still encodes the full bytes and losslessness is untouched.
1179
+ export function toolClipAdditions(input: {
1180
+ snapshot: ActiveContextSnapshot;
1181
+ state: ActiveContextState;
1182
+ threshold: number;
1183
+ blacklist: ReadonlySet<string>;
1184
+ }): ToolClip[] {
1185
+ const { snapshot, state, threshold, blacklist } = input;
1186
+ const projected = projectActiveContext(snapshot, state);
1187
+ const perMessage = projected.map((message) => bytes([message]));
1188
+ const total = perMessage.reduce((sum, size) => sum + size, 0);
1189
+ const zoneEnd = threshold * total;
1190
+ const matureEnd = staleSpanMatureEnd(snapshot.messages);
1191
+ const snapshotIndexByCallId = new Map<string, number>();
1192
+ for (let index = 0; index < snapshot.messages.length; index += 1) {
1193
+ const message = snapshot.messages[index] as { role?: unknown; toolCallId?: unknown };
1194
+ if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
1195
+ snapshotIndexByCallId.set(message.toolCallId, index);
1196
+ }
1197
+ }
1198
+ const already = new Set((state.clips ?? []).map((clip) => clip.callId));
1199
+ const additions: ToolClip[] = [];
1200
+ let offset = 0;
1201
+ for (let index = 0; index < projected.length; index += 1) {
1202
+ const start = offset;
1203
+ offset += perMessage[index];
1204
+ if (start >= zoneEnd) break;
1205
+ const message = projected[index] as {
1206
+ role?: unknown; toolCallId?: unknown; toolName?: unknown; details?: unknown;
1207
+ };
1208
+ if (message?.role !== "toolResult" || typeof message.toolCallId !== "string") continue;
1209
+ if (already.has(message.toolCallId)) continue;
1210
+ if (message.details && typeof (message.details as { projection?: unknown }).projection === "string") continue;
1211
+ if (typeof message.toolName === "string" && blacklist.has(message.toolName)) continue;
1212
+ const snapshotIndex = snapshotIndexByCallId.get(message.toolCallId);
1213
+ if (snapshotIndex === undefined || snapshotIndex >= matureEnd) continue;
1214
+ const ref = snapshot.mapped.find((item) => item.index === snapshotIndex)?.ref;
1215
+ if (!ref || refsProtected([ref], state, snapshot)) continue;
1216
+ if (contentText(message).length <= snapshot.policy.toolClipFloorChars) continue;
1217
+ additions.push({ callId: message.toolCallId, entryId: ref.entryId });
1218
+ }
1219
+ return additions;
1220
+ }