pi-fold 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { objectRefKey } from "../json.ts";
2
+ import type { EvidenceRef } from "../json.ts";
2
3
  import {
3
4
  bytes,
4
5
  clone,
@@ -7,20 +8,26 @@ import {
7
8
  import {
8
9
  commitPreparedFold,
9
10
  pinnedPeekMass,
11
+ preparedMatchesCandidate,
10
12
  prepareFold,
11
13
  renderFold,
12
14
  renderFoldParts,
15
+ selectAutomaticSpan,
13
16
  setFoldProjectionState,
14
17
  } from "./folding.ts";
15
18
  import type { AutomaticRungSelection } from "./folding.ts";
16
19
  import {
17
20
  foldInterval,
21
+ budgetOccupancy,
18
22
  hardFenceRatio,
23
+ mappedByKey,
19
24
  orderedRoots,
20
25
  refsProtected,
21
26
  toolRefsProtected,
22
27
  } from "./measurement.ts";
23
28
  import {
29
+ childFoldIds,
30
+ clearPrepared,
24
31
  flattenFoldRefs,
25
32
  foldIdFor,
26
33
  parsePendingMarks,
@@ -28,8 +35,6 @@ import {
28
35
  } from "./persistence.ts";
29
36
  import { currentTurnRefKeys } from "./transcript.ts";
30
37
  import {
31
- EPOCH_COMMIT_TARGET_WINDOW_SHARE,
32
- EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD,
33
38
  EPOCH_MAX_TOPUP_MARKS,
34
39
  ESTIMATED_BYTES_PER_TOKEN,
35
40
  MAX_WEDGE_ABSORB_TOKENS,
@@ -41,21 +46,23 @@ import type {
41
46
  ActiveContextSnapshot,
42
47
  ActiveContextState,
43
48
  FoldCandidate,
49
+ FoldPart,
44
50
  MarkOrigin,
45
51
  PendingFoldMark,
46
52
  PendingMark,
47
53
  } from "./policy.ts";
48
54
  import {
55
+ automaticToolBatches,
49
56
  automaticToolBrief,
50
57
  candidateSourceRefs,
51
58
  chapterRangeIsUnitAligned,
52
59
  partsForRange,
60
+ peekedSourceFoldIds,
61
+ selectAutomaticConsolidations,
53
62
  spanBytes,
54
63
  deterministicChapterCandidateBrief,
55
64
  deterministicConsolidationBrief,
56
65
  resultCall,
57
- selectAutomaticToolBatch,
58
- toolCallArguments,
59
66
  } from "./selection.ts";
60
67
 
61
68
  /**
@@ -72,12 +79,30 @@ import {
72
79
  * wall clock and never randomness.
73
80
  */
74
81
 
75
- export function pendingMarks(state: Pick<ActiveContextState, "pendingMarks">): PendingMark[] {
76
- return state.pendingMarks ? clone(state.pendingMarks) : [];
82
+ /** The empty read, shared and frozen, so a state with no marks allocates nothing. */
83
+ const NO_PENDING_MARKS: readonly PendingMark[] = Object.freeze([]);
84
+
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
+ export function pendingMarks(state: Pick<ActiveContextState, "pendingMarks">): readonly PendingMark[] {
101
+ return state.pendingMarks ?? NO_PENDING_MARKS;
77
102
  }
78
103
 
79
104
  /**
80
- * Property ORDER is load-bearing: the state digest is a stable stringify, so an
105
+ * Property ORDER matters here: the state digest is a stable stringify, so an
81
106
  * appended key would replay as digest drift. Rebuild the tail of the record in the
82
107
  * same order `parseActiveContextState` produces.
83
108
  */
@@ -131,6 +156,80 @@ export function markOrdinal(snapshot: Pick<ActiveContextSnapshot, "mapped">): nu
131
156
  return snapshot.mapped.length;
132
157
  }
133
158
 
159
+ export interface MarkSpanRefs {
160
+ /** Every evidence ref the mark's parts resolve to against the state in hand. */
161
+ 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
+ unresolved: { foldId: string; pending: boolean } | null;
169
+ }
170
+
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
+ export function markSpanRefs(state: ActiveContextState, mark: PendingMark): MarkSpanRefs {
192
+ const byId = new Map(state.folds.map((fold) => [fold.id, fold] as const));
193
+ const marks = pendingMarks(state);
194
+ const refs: EvidenceRef[] = [];
195
+ let unresolved: MarkSpanRefs["unresolved"] = null;
196
+ const path = new Set<string>();
197
+ const miss = (foldId: string, pending: boolean): void => {
198
+ if (!unresolved) unresolved = { foldId, pending };
199
+ };
200
+ const collect = (parts: readonly FoldPart[]): void => {
201
+ for (const part of parts) {
202
+ if (part.kind === "raw") {
203
+ refs.push(part.ref);
204
+ continue;
205
+ }
206
+ const child = byId.get(part.foldId);
207
+ if (!child) {
208
+ miss(part.foldId, marks.some((item) => item.mark === "fold" && item.id === part.foldId));
209
+ continue;
210
+ }
211
+ if (path.has(child.id)) {
212
+ miss(child.id, false);
213
+ continue;
214
+ }
215
+ path.add(child.id);
216
+ collect(child.parts);
217
+ path.delete(child.id);
218
+ }
219
+ };
220
+ if (mark.mark === "refold") {
221
+ const fold = byId.get(mark.id);
222
+ if (!fold) {
223
+ miss(mark.id, marks.some((item) => item.mark === "fold" && item.id === mark.id));
224
+ } else {
225
+ path.add(fold.id);
226
+ collect(fold.parts);
227
+ path.delete(fold.id);
228
+ }
229
+ } else collect(mark.parts);
230
+ return { refs, unresolved };
231
+ }
232
+
134
233
  /**
135
234
  * Estimated bytes a mark would remove from the projection. Fold marks use a
136
235
  * placeholder estimate rather than a trial fold: the number is presentational, so
@@ -158,20 +257,55 @@ export function markFreedBytes(
158
257
  return Math.max(0, bytes(source) - placeholder);
159
258
  }
160
259
 
161
- /** Whether a pending mark covers any evidence the current excursion just gathered. */
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
+ */
162
268
  export function markTouchesCurrentTurn(
163
269
  state: ActiveContextState,
164
270
  mark: PendingMark,
165
271
  currentTurn: ReadonlySet<string>,
166
272
  ): boolean {
167
273
  if (!currentTurn.size) return false;
168
- const refs = mark.mark === "refold"
169
- ? (() => {
170
- const fold = state.folds.find((item) => item.id === mark.id);
171
- return fold ? flattenFoldRefs(fold, state) : [];
172
- })()
173
- : candidateSourceRefs(mark.parts, state);
174
- return refs.some((ref) => currentTurn.has(objectRefKey(ref)));
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
+ export function markSpanStart(
296
+ snapshot: ActiveContextSnapshot,
297
+ state: ActiveContextState,
298
+ mark: PendingMark,
299
+ ): number {
300
+ const span = markSpanRefs(state, mark);
301
+ if (span.unresolved) return snapshot.mapped.length;
302
+ const refs = span.refs;
303
+ const indexed = mappedByKey(snapshot);
304
+ const indices = refs.flatMap((ref) => {
305
+ const item = indexed.get(objectRefKey(ref));
306
+ return item ? [item.index] : [];
307
+ });
308
+ return indices.length ? Math.min(...indices) : snapshot.mapped.length;
175
309
  }
176
310
 
177
311
  /**
@@ -218,7 +352,8 @@ export function guardWaiverCount(input: {
218
352
  const { snapshot, ratio, guardedMarks, otherApplicableMarks } = input;
219
353
  if (guardedMarks <= 0 || typeof ratio !== "number" || !Number.isFinite(ratio)) return 0;
220
354
  if (ratio >= hardFenceRatio(snapshot)) return guardedMarks;
221
- if (ratio < snapshot.policy.refoldRatio || otherApplicableMarks > 0) return 0;
355
+ const occupancy = budgetOccupancy(snapshot, ratio);
356
+ if (occupancy === null || occupancy < snapshot.policy.refoldRatio || otherApplicableMarks > 0) return 0;
222
357
  if (guardedMarks < GUARD_WAIVER_MINIMUM_MARKS) return 0;
223
358
  return Math.max(1, guardedMarks - GUARD_WAIVER_PROTECTED_MARKS);
224
359
  }
@@ -232,6 +367,11 @@ export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
232
367
  * protected it, and both conditions expire. "unfulfillable" is terminal: the evidence
233
368
  * or the fold the mark names has left the branch, so no later commit can honour it.
234
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).
235
375
  */
236
376
  export function markEligibility(
237
377
  snapshot: ActiveContextSnapshot,
@@ -243,7 +383,9 @@ export function markEligibility(
243
383
  if (!fold || !foldInterval(fold, state, snapshot)) return "unfulfillable";
244
384
  return state.expanded.includes(mark.id) ? "eligible" : "unfulfillable";
245
385
  }
246
- const refs = candidateSourceRefs(mark.parts, state);
386
+ const span = markSpanRefs(state, mark);
387
+ if (span.unresolved) return span.unresolved.pending ? "protected" : "unfulfillable";
388
+ const refs = span.refs;
247
389
  if (!refs.length) return "unfulfillable";
248
390
  const mapped = new Set(snapshot.mapped.flatMap((item) => item.ref ? [objectRefKey(item.ref)] : []));
249
391
  if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
@@ -259,7 +401,7 @@ export interface MarkAccounting {
259
401
  ladderMarks: number;
260
402
  freedBytes: number;
261
403
  freedTokens: number;
262
- freedWindowShare: number;
404
+ freedBudgetShare: number;
263
405
  rewriteTokens: number;
264
406
  /** Marks a commit could apply right now. */
265
407
  eligibleMarks: number;
@@ -268,7 +410,7 @@ export interface MarkAccounting {
268
410
  eligibleFreedBytes: number;
269
411
  eligibleFreedTokens: number;
270
412
  /** The ROI signal: eligible marked mass as a share of the truthful window. */
271
- eligibleFreedWindowShare: number;
413
+ eligibleFreedBudgetShare: number;
272
414
  /** Raw peek mass no reclamation can take: pinned reads and protected results. */
273
415
  pinnedBytes: number;
274
416
  /** How many peek results that mass is spread over. */
@@ -300,11 +442,7 @@ export function markAccounting(
300
442
  eligibleMarks += 1;
301
443
  eligibleFreedBytes += freed;
302
444
  } else if (eligibility === "protected") retainedMarks += 1;
303
- const fold = mark.mark === "refold" ? state.folds.find((item) => item.id === mark.id) : null;
304
- const refs = mark.mark === "refold"
305
- ? (fold ? flattenFoldRefs(fold, state) : [])
306
- : candidateSourceRefs(mark.parts, state);
307
- for (const ref of refs) {
445
+ for (const ref of markSpanRefs(state, mark).refs) {
308
446
  const index = indexByKey.get(objectRefKey(ref));
309
447
  if (index === undefined) continue;
310
448
  if (earliest < 0 || index < earliest) earliest = index;
@@ -322,14 +460,14 @@ export function markAccounting(
322
460
  ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
323
461
  freedBytes,
324
462
  freedTokens,
325
- freedWindowShare: snapshot.contextWindow > 0 ? freedTokens / snapshot.contextWindow : 0,
463
+ freedBudgetShare: snapshot.budgetTokens > 0 ? freedTokens / snapshot.budgetTokens : 0,
326
464
  rewriteTokens: estimatedTokens(rewriteBytes),
327
465
  eligibleMarks,
328
466
  retainedMarks,
329
467
  eligibleFreedBytes,
330
468
  eligibleFreedTokens,
331
- eligibleFreedWindowShare: snapshot.contextWindow > 0
332
- ? eligibleFreedTokens / snapshot.contextWindow
469
+ eligibleFreedBudgetShare: snapshot.budgetTokens > 0
470
+ ? eligibleFreedTokens / snapshot.budgetTokens
333
471
  : 0,
334
472
  pinnedBytes: pinned.bytes,
335
473
  pinnedResults: pinned.results,
@@ -337,23 +475,24 @@ export function markAccounting(
337
475
  }
338
476
 
339
477
  /**
340
- * The commit trigger.
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.
341
483
  *
342
- * The ROI half asks the economic question: commit when the marks that could apply
343
- * RIGHT NOW would free enough to pay for the rewrite. The pressure half reuses the
344
- * ladder's own refold threshold and stays underneath as the SAFETY backstop -- it
345
- * fires when the window is nearly full, which is the worst moment to discover the
346
- * marks are worth nothing (the rep4 abort fired there with zero pending marks). The
347
- * agent's own commit is authoritative and immediate regardless of either.
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.
348
492
  */
349
- export function epochCommitDue(
350
- snapshot: ActiveContextSnapshot,
351
- ratio: number | null,
352
- eligibleShare: number | null = null,
353
- ): boolean {
354
- if (typeof eligibleShare === "number" && Number.isFinite(eligibleShare) &&
355
- eligibleShare >= EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD) return true;
356
- return typeof ratio === "number" && Number.isFinite(ratio) && ratio >= snapshot.policy.refoldRatio;
493
+ export function epochCommitDue(snapshot: ActiveContextSnapshot, ratio: number | null): boolean {
494
+ const occupancy = budgetOccupancy(snapshot, ratio);
495
+ return occupancy !== null && occupancy >= snapshot.thresholds.maxTarget;
357
496
  }
358
497
 
359
498
  export function foldMarkFor(input: {
@@ -389,6 +528,26 @@ export function ladderBrief(
389
528
  return deterministicChapterCandidateBrief(snapshot, candidate);
390
529
  }
391
530
 
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
+ export function automaticMarkBrief(
540
+ snapshot: ActiveContextSnapshot,
541
+ state: ActiveContextState,
542
+ candidate: FoldCandidate,
543
+ ): { brief: string; briefProvenance: PendingFoldMark["briefProvenance"] } {
544
+ const prepared = state.prepared;
545
+ if (prepared && preparedMatchesCandidate(prepared, candidate)) {
546
+ return { brief: prepared.fold.brief, briefProvenance: clone(prepared.fold.provenance) };
547
+ }
548
+ return { brief: ladderBrief(snapshot, state, candidate), briefProvenance: { kind: "deterministic" } };
549
+ }
550
+
392
551
  /**
393
552
  * Turn one automatic rung selection into a mark. Prepared chapters are excluded on
394
553
  * purpose: that rung only fires at the hard provider fence, which is already inside
@@ -409,17 +568,29 @@ export function ladderSelectionMark(input: {
409
568
  }
410
569
  return foldMarkFor({
411
570
  candidate: selection.candidate,
412
- brief: ladderBrief(input.snapshot, input.state, selection.candidate),
413
- briefProvenance: { kind: "deterministic" },
571
+ ...automaticMarkBrief(input.snapshot, input.state, selection.candidate),
414
572
  origin: "ladder",
415
573
  ordinal: input.ordinal,
416
574
  });
417
575
  }
418
576
 
419
577
  /**
420
- * Peek is the ephemeral read: look, decide, discard. Its own tool result is a
421
- * completed read batch, so at the next commit it folds away automatically, unless
422
- * the agent committed to the fold it peeked by expanding or protecting it.
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.
423
594
  */
424
595
  export function ephemeralPeekMarks(input: {
425
596
  snapshot: ActiveContextSnapshot;
@@ -443,25 +614,13 @@ export function ephemeralPeekMarks(input: {
443
614
  const expected = entries[0].call.batch;
444
615
  const ids = new Set(entries.map(({ call }) => call.id));
445
616
  if (ids.size !== expected.length || expected.some((id) => !ids.has(id))) continue;
446
- const peeked: string[] = [];
447
- let everyCallIsPeek = true;
448
- for (const { call } of entries) {
449
- const args = toolCallArguments(snapshot, call.assistantIndex, call.id);
450
- const action = args && typeof args === "object" && !Array.isArray(args)
451
- ? (args as Record<string, unknown>).action
452
- : undefined;
453
- const id = args && typeof args === "object" && !Array.isArray(args)
454
- ? (args as Record<string, unknown>).id
455
- : undefined;
456
- if (action !== "peek" || typeof id !== "string" || !id) { everyCallIsPeek = false; break; }
457
- peeked.push(id);
458
- }
459
- if (!everyCallIsPeek) continue;
460
- // The agent committed to what it peeked: leave the read in the raw window.
461
- if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
462
617
  const refs = entries
463
618
  .sort((left, right) => left.index - right.index)
464
619
  .map(({ index }) => snapshot.mapped[index].ref!);
620
+ const peeked = peekedSourceFoldIds(snapshot, refs);
621
+ if (!peeked) continue;
622
+ // The agent committed to what it peeked: leave the read in the raw window.
623
+ if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
465
624
  if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
466
625
  refs.length > snapshot.policy.maxFoldSourceRefs ||
467
626
  toolRefsProtected(refs, state, snapshot)) continue;
@@ -504,17 +663,17 @@ export function claimedRefKeys(state: ActiveContextState): Set<string> {
504
663
  }
505
664
  for (const mark of pendingMarks(state)) {
506
665
  if (mark.mark !== "fold") continue;
507
- for (const ref of candidateSourceRefs(mark.parts, state)) keys.add(objectRefKey(ref));
666
+ for (const ref of markSpanRefs(state, mark).refs) keys.add(objectRefKey(ref));
508
667
  }
509
668
  return keys;
510
669
  }
511
670
 
512
671
  /** What is still on the table after the marks in hand: the steering number and its parts. */
513
672
  export interface UnmarkedRemainder {
514
- /** Stale tool results outside the fresh tail that no fold and no pending mark owns. */
673
+ /** Tool results in a MEMBER batch that no fold and no pending mark owns. */
515
674
  spans: number;
516
675
  tokens: number;
517
- /** Unmarked stale tokens as a share of the non-fresh window. The steering number. */
676
+ /** Unmarked member tokens as a share of the member tool mass. The steering number. */
518
677
  share: number;
519
678
  /** The largest few, by reclaim value, so the next batch is one read away. */
520
679
  candidates: Array<{ id: string; tokens: number }>;
@@ -526,6 +685,17 @@ export interface UnmarkedRemainder {
526
685
  * An exhaustive list is what the status index already is, and re-rendering it is what
527
686
  * this build removed from the projection. What an agent needs to decide the NEXT batch
528
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.
529
699
  */
530
700
  export function unmarkedRemainder(
531
701
  snapshot: ActiveContextSnapshot,
@@ -535,14 +705,17 @@ export function unmarkedRemainder(
535
705
  ): UnmarkedRemainder {
536
706
  const perToken = Number.isFinite(charsPerToken) && charsPerToken > 0 ? charsPerToken : ESTIMATED_BYTES_PER_TOKEN;
537
707
  const claimed = claimedRefKeys(state);
708
+ const members = new Set<number>();
709
+ for (const batch of automaticToolBatches(snapshot, state)) {
710
+ for (const index of batch.indices) members.add(index);
711
+ }
538
712
  const candidates: Array<{ id: string; tokens: number }> = [];
539
713
  let unmarkedBytes = 0;
540
- let staleBytes = 0;
714
+ let memberBytes = 0;
541
715
  for (const item of snapshot.mapped) {
542
- if (!item.ref || messageRole(item.message) !== "toolResult") continue;
543
- if (snapshot.toolProtectedIndices.has(item.index)) continue;
716
+ if (!item.ref || !members.has(item.index)) continue;
544
717
  const size = bytes(item.message);
545
- staleBytes += size;
718
+ memberBytes += size;
546
719
  if (claimed.has(objectRefKey(item.ref))) continue;
547
720
  unmarkedBytes += size;
548
721
  candidates.push({ id: item.ref.entryId, tokens: Math.ceil(size / perToken) });
@@ -551,7 +724,7 @@ export function unmarkedRemainder(
551
724
  return {
552
725
  spans: candidates.length,
553
726
  tokens: Math.ceil(unmarkedBytes / perToken),
554
- share: staleBytes > 0 ? unmarkedBytes / staleBytes : 0,
727
+ share: memberBytes > 0 ? unmarkedBytes / memberBytes : 0,
555
728
  candidates: candidates.slice(0, Math.max(0, limit)),
556
729
  };
557
730
  }
@@ -564,38 +737,64 @@ export function markedFoldIds(state: ActiveContextState): Set<string> {
564
737
  }
565
738
 
566
739
  /**
567
- * Agent judgment leads; automation guarantees the floor. If the marks the agent
568
- * made would free less than the target share of the window, the ladder adds the
569
- * stalest unprotected eligible tool batches until they do.
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
+ export function consolidationMarks(input: {
751
+ snapshot: ActiveContextSnapshot;
752
+ state: ActiveContextState;
753
+ ordinal: number;
754
+ }): PendingFoldMark[] {
755
+ return selectAutomaticConsolidations(input.snapshot, input.state).map((candidate) => foldMarkFor({
756
+ candidate,
757
+ ...automaticMarkBrief(input.snapshot, input.state, candidate),
758
+ origin: "ladder",
759
+ ordinal: input.ordinal,
760
+ }));
761
+ }
762
+
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.
570
769
  */
571
770
  export function topUpMarks(input: {
572
771
  snapshot: ActiveContextSnapshot;
573
772
  state: ActiveContextState;
574
773
  ordinal: number;
575
- targetShare?: number;
774
+ /** Freeing target as a share of the SERVING BUDGET. Required: there is no second default. */
775
+ targetShare: number;
576
776
  /** Evidence keys the top-up may never propose, e.g. the current excursion's reads. */
577
777
  excludeRefKeys?: ReadonlySet<string>;
578
778
  /** Measure progress against ELIGIBLE mass, so pinned or protected marks never count. */
579
779
  eligibleOnly?: boolean;
580
780
  }): PendingFoldMark[] {
581
781
  const { snapshot } = input;
582
- const target = input.targetShare ?? EPOCH_COMMIT_TARGET_WINDOW_SHARE;
782
+ const target = input.targetShare;
583
783
  const claimed = claimedRefKeys(input.state);
584
784
  for (const key of input.excludeRefKeys ?? []) claimed.add(key);
585
785
  const marks: PendingFoldMark[] = [];
586
786
  let state = input.state;
587
787
  const progress = (value: ActiveContextState): number => {
588
788
  const accounting = markAccounting(snapshot, value);
589
- return input.eligibleOnly ? accounting.eligibleFreedWindowShare : accounting.freedWindowShare;
789
+ return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
590
790
  };
591
791
  let share = progress(state);
592
792
  for (let attempt = 0; attempt < EPOCH_MAX_TOPUP_MARKS && share < target; attempt += 1) {
593
- const candidate = selectAutomaticToolBatch(snapshot, state, 1, claimed)[0];
793
+ const candidate = selectAutomaticSpan(snapshot, state, claimed);
594
794
  if (!candidate) break;
595
795
  const mark = foldMarkFor({
596
796
  candidate,
597
- brief: automaticToolBrief(snapshot, candidate),
598
- briefProvenance: { kind: "deterministic" },
797
+ ...automaticMarkBrief(snapshot, state, candidate),
599
798
  origin: "ladder",
600
799
  ordinal: input.ordinal,
601
800
  });
@@ -613,6 +812,12 @@ export function topUpMarks(input: {
613
812
  export interface AbsorbedWedge {
614
813
  /** The mark that grew backward to cover it. */
615
814
  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
+ fromMarkId: string;
616
821
  startId: string;
617
822
  endId: string;
618
823
  entries: number;
@@ -642,10 +847,8 @@ export function absorbWedgeMarks(input: {
642
847
  charsPerToken: number;
643
848
  /** Evidence absorption may never touch, e.g. the current excursion's own reads. */
644
849
  excludeRefKeys?: ReadonlySet<string>;
645
- maxTokens?: number;
646
850
  }): { state: ActiveContextState; absorbed: AbsorbedWedge[] } {
647
851
  const { snapshot } = input;
648
- const maxTokens = input.maxTokens ?? MAX_WEDGE_ABSORB_TOKENS;
649
852
  const charsPerToken = Number.isFinite(input.charsPerToken) && input.charsPerToken > 0
650
853
  ? input.charsPerToken
651
854
  : ESTIMATED_BYTES_PER_TOKEN;
@@ -655,7 +858,7 @@ export function absorbWedgeMarks(input: {
655
858
  const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
656
859
  item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
657
860
  const markInterval = (mark: PendingFoldMark): { start: number; end: number } | null => {
658
- const indices = candidateSourceRefs(mark.parts, state)
861
+ const indices = markSpanRefs(state, mark).refs
659
862
  .map((ref) => indexByKey.get(objectRefKey(ref)))
660
863
  .filter((index): index is number => index !== undefined);
661
864
  return indices.length ? { start: Math.min(...indices), end: Math.max(...indices) } : null;
@@ -684,7 +887,7 @@ export function absorbWedgeMarks(input: {
684
887
  if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
685
888
  const gapTokens = Math.ceil(spanBytes(snapshot, gapStart, gapEnd + 1) / charsPerToken);
686
889
  // The whole discriminator, in one line: above this, the gap is the agent's.
687
- if (gapTokens > maxTokens) continue;
890
+ if (gapTokens > MAX_WEDGE_ABSORB_TOKENS) continue;
688
891
  const gapRefs = [];
689
892
  let usable = true;
690
893
  for (let index = gapStart; index <= gapEnd; index += 1) {
@@ -712,6 +915,7 @@ export function absorbWedgeMarks(input: {
712
915
  state = withPendingMarks(state, [...kept, grown]);
713
916
  absorbed.push({
714
917
  intoMarkId: grown.id,
918
+ fromMarkId: mark.id,
715
919
  startId: gapRefs[0].entryId,
716
920
  endId: gapRefs.at(-1)!.entryId,
717
921
  entries: gapRefs.length,
@@ -746,7 +950,7 @@ export interface CommitEpochResult {
746
950
  applied: AppliedMark[];
747
951
  refused: RefusedMark[];
748
952
  retained: PendingMark[];
749
- /** Guarded marks the pressure waiver released into this commit, oldest first. */
953
+ /** Guarded marks the pressure waiver released into this commit, oldest material first. */
750
954
  waived: PendingMark[];
751
955
  }
752
956
 
@@ -774,7 +978,7 @@ export async function commitPendingMarks(input: {
774
978
  */
775
979
  guardCurrentTurn?: boolean;
776
980
  /**
777
- * How many guarded marks the pressure waiver releases anyway, oldest first. The
981
+ * How many guarded marks the pressure waiver releases anyway, oldest material first. The
778
982
  * guard protects an in-flight excursion; it may never cost the session its ability
779
983
  * to send a request at all. See `guardWaiverCount`.
780
984
  */
@@ -784,6 +988,17 @@ export async function commitPendingMarks(input: {
784
988
  const retained: PendingMark[] = [];
785
989
  const waived: PendingMark[] = [];
786
990
  const guardReasons = new Map<string, string>();
991
+ const held = new Set(input.state.folds.map((fold) => fold.id));
992
+ 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
+ const awaitingMintedChild = (mark: PendingMark): boolean =>
1001
+ markSpanRefs(input.state, mark).unresolved?.pending === true;
787
1002
  const currentTurn = input.guardCurrentTurn
788
1003
  ? currentTurnRefKeys(input.snapshot)
789
1004
  : new Set<string>();
@@ -794,9 +1009,16 @@ export async function commitPendingMarks(input: {
794
1009
  if (markTouchesCurrentTurn(input.state, mark, currentTurn)) guarded.push(mark);
795
1010
  else applicable.push(mark);
796
1011
  }
797
- // Oldest first: the newest reads are the ones the excursion is about to use, so
798
- // they are the last evidence the waiver surrenders.
799
- guarded.sort((left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id));
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));
800
1022
  const waiverCount = Math.max(0, Math.min(guarded.length, Math.trunc(input.guardWaiver ?? 0)));
801
1023
  for (const [index, mark] of guarded.entries()) {
802
1024
  if (index < waiverCount) {
@@ -816,7 +1038,12 @@ export async function commitPendingMarks(input: {
816
1038
  if (input.retainIneligible) {
817
1039
  const applicable: PendingMark[] = [];
818
1040
  for (const mark of marks) {
819
- if (markEligibility(input.snapshot, input.state, mark) === "protected") retained.push(mark);
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
+ if (!awaitingMintedChild(mark) &&
1046
+ markEligibility(input.snapshot, input.state, mark) === "protected") retained.push(mark);
820
1047
  else applicable.push(mark);
821
1048
  }
822
1049
  marks.length = 0;
@@ -832,10 +1059,51 @@ export async function commitPendingMarks(input: {
832
1059
  "span is still fresh or protected; the mark stays pending until it is eligible",
833
1060
  retained: true,
834
1061
  }));
835
- // Oldest first, then by id: a chapter mark that absorbs an earlier tool-result
836
- // mark must find that child already folded, which is transcript order.
837
- const ordered = [...marks].sort((left, right) =>
838
- left.ordinal - right.ordinal || left.id.localeCompare(right.id));
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
+ const minting = new Map(marks.flatMap((mark) =>
1080
+ mark.mark === "fold" && !held.has(mark.id) ? [[mark.id, mark] as const] : []));
1081
+ const applyStart = new Map(marks.map((mark) =>
1082
+ [pendingMarkKey(mark), markSpanStart(input.snapshot, input.state, mark)] as const));
1083
+ const base = [...marks].sort((left, right) =>
1084
+ applyStart.get(pendingMarkKey(left))! - applyStart.get(pendingMarkKey(right))! ||
1085
+ left.id.localeCompare(right.id));
1086
+ const ordered: PendingMark[] = [];
1087
+ const placed = new Set<string>();
1088
+ const placing = new Set<string>();
1089
+ const place = (mark: PendingMark): void => {
1090
+ 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
+ if (placed.has(key) || placing.has(key)) return;
1095
+ placing.add(key);
1096
+ if (mark.mark === "fold") {
1097
+ for (const childId of childFoldIds(mark)) {
1098
+ const child = minting.get(childId);
1099
+ if (child) place(child);
1100
+ }
1101
+ }
1102
+ placing.delete(key);
1103
+ placed.add(key);
1104
+ ordered.push(mark);
1105
+ };
1106
+ for (const mark of base) place(mark);
839
1107
  for (const mark of ordered) {
840
1108
  try {
841
1109
  if (mark.mark === "refold") {
@@ -849,7 +1117,33 @@ export async function commitPendingMarks(input: {
849
1117
  continue;
850
1118
  }
851
1119
  const parts = clone(mark.parts);
852
- const sourceRefs = candidateSourceRefs(parts, state);
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
+ const span = markSpanRefs(state, mark);
1130
+ if (span.unresolved) {
1131
+ const { foldId, pending } = span.unresolved;
1132
+ if (pending) deferred.push(mark);
1133
+ refused.push({
1134
+ mark: mark.mark,
1135
+ id: mark.id,
1136
+ origin: mark.origin,
1137
+ reason: pending
1138
+ ? `pending fold mark ${mark.id} names fold ${foldId}, which a still-pending mark ` +
1139
+ "has not minted yet; the mark stays pending until that fold lands"
1140
+ : `pending fold mark ${mark.id} names fold ${foldId}, which the context no longer ` +
1141
+ "holds and no pending mark will mint; the decision cannot be honoured",
1142
+ retained: pending,
1143
+ });
1144
+ continue;
1145
+ }
1146
+ const sourceRefs = span.refs;
853
1147
  const blocked = mark.kind === "tool-result"
854
1148
  ? toolRefsProtected(sourceRefs, state, input.snapshot)
855
1149
  : refsProtected(sourceRefs, state, input.snapshot);
@@ -875,6 +1169,12 @@ export async function commitPendingMarks(input: {
875
1169
  generation: input.generation,
876
1170
  });
877
1171
  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
+ if (state.prepared && state.folds.some((fold) => fold.id === state.prepared!.id)) {
1176
+ state = clearPrepared(state);
1177
+ }
878
1178
  } catch (error) {
879
1179
  refused.push({
880
1180
  mark: mark.mark,
@@ -885,6 +1185,13 @@ export async function commitPendingMarks(input: {
885
1185
  });
886
1186
  }
887
1187
  }
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
+ if (deferred.length) {
1192
+ state = withPendingMarks(state, [...pendingMarks(state), ...deferred]);
1193
+ retained.push(...deferred);
1194
+ }
888
1195
  return { state, applied, refused, retained, waived };
889
1196
  }
890
1197
 
@@ -892,25 +1199,26 @@ export async function commitPendingMarks(input: {
892
1199
  export function schedulingStatus(input: {
893
1200
  snapshot: ActiveContextSnapshot;
894
1201
  state: ActiveContextState;
895
- mode: string;
896
1202
  ratio: number | null;
897
1203
  }): Record<string, unknown> {
898
1204
  const accounting = markAccounting(input.snapshot, input.state);
899
- const threshold = EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD;
900
1205
  return {
901
- mode: input.mode,
1206
+ // A literal, not a setting: epoch is the only scheduler there is.
1207
+ mode: "epoch",
902
1208
  ...accounting,
903
- targetWindowShare: EPOCH_COMMIT_TARGET_WINDOW_SHARE,
904
- commitDue: epochCommitDue(input.snapshot, input.ratio, accounting.eligibleFreedWindowShare),
1209
+ // The declared policy, reported never mutable.
1210
+ thresholds: { ...input.snapshot.thresholds },
1211
+ budgetTokens: input.snapshot.budgetTokens,
1212
+ 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.
905
1216
  commitTrigger: {
906
- mode: "eligible-share",
907
- eligibleShareThreshold: threshold,
908
- eligibleShare: accounting.eligibleFreedWindowShare,
909
- backstopRatio: input.snapshot.policy.refoldRatio,
910
- pressureDue: epochCommitDue(input.snapshot, input.ratio),
911
- roiDue: accounting.eligibleFreedWindowShare >= threshold,
1217
+ mode: "band-top",
1218
+ commitOccupancy: input.snapshot.thresholds.maxTarget,
1219
+ occupancy: budgetOccupancy(input.snapshot, input.ratio),
1220
+ due: epochCommitDue(input.snapshot, input.ratio),
912
1221
  },
913
- commitRatio: input.snapshot.policy.refoldRatio,
914
1222
  marks: pendingMarks(input.state).map((mark) => ({
915
1223
  mark: mark.mark,
916
1224
  id: mark.id,