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.
@@ -10,6 +10,7 @@ import {
10
10
  stableStringify,
11
11
  } from "../json.ts";
12
12
  import {
13
+ contentText,
13
14
  boundedUtf8,
14
15
  bytes,
15
16
  clone,
@@ -28,7 +29,6 @@ import {
28
29
  orderedRoots,
29
30
  refsInOrder,
30
31
  refsProtected,
31
- toolRefsProtected,
32
32
  } from "./measurement.ts";
33
33
  import {
34
34
  childFoldIds,
@@ -54,7 +54,6 @@ import {
54
54
  PEEK_DEFAULT_MAX_BYTES,
55
55
  PEEK_HEAD_SHARE,
56
56
  STATUS_DIET_INDEX_ROWS,
57
- zoneBytes,
58
57
  } from "./policy.ts";
59
58
  import type {
60
59
  ActiveContextSnapshot,
@@ -73,7 +72,9 @@ import {
73
72
  chapterUnits,
74
73
  unpinnedStaleFolds,
75
74
  deterministicChapterCandidateBrief,
75
+ toolClipHead,
76
76
  deterministicConsolidationBrief,
77
+ oneLine,
77
78
  NO_FOLD_KINDS,
78
79
  partsForRange,
79
80
  resultCall,
@@ -81,36 +82,59 @@ import {
81
82
  selectAutomaticRefold,
82
83
  selectAutomaticToolBatch,
83
84
  } from "./selection.ts";
85
+ import { staleSpanMatureEnd } from "./transcript.ts";
84
86
 
85
87
  export function selectAutomaticChapter(
86
88
  snapshot: ActiveContextSnapshot,
87
89
  state: ActiveContextState,
88
- maximumSourceRefs: number = snapshot.policy.maxFoldSourceRefs,
89
90
  claimed: ReadonlySet<string> = new Set<string>(),
90
91
  ): FoldCandidate | null {
91
92
  const units = chapterUnits(snapshot);
92
93
  const allowedChildren = NO_FOLD_KINDS;
94
+ // The maturity floor: a stale span may end only where the batch lane has already had
95
+ // its chance, so a chapter can never claim a tool batch one turn before it qualifies.
96
+ const matureEnd = staleSpanMatureEnd(snapshot.messages);
93
97
  for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
94
98
  const first = units[unitIndex];
99
+ if (first.end > matureEnd) break;
95
100
  let best: FoldCandidate | null = null;
96
- const turnStarts = new Set<number>();
97
101
  for (let endIndex = unitIndex; endIndex < units.length; endIndex += 1) {
98
102
  const unit = units[endIndex];
103
+ if (unit.end > matureEnd) break;
99
104
  if (endIndex > unitIndex && unit.start !== units[endIndex - 1].end) break;
100
- turnStarts.add(unit.turnStart);
101
- if (turnStarts.size > snapshot.policy.maxChapterTurns) break;
102
105
  const coherentSegment = endIndex > unitIndex || first.end - first.start > 1;
103
106
  if (!coherentSegment) continue;
107
+ // A DISQUALIFIED RANGE DISQUALIFIES EVERY LARGER ONE, so the walk stops here rather
108
+ // than rediscovering the same obstruction at every remaining endIndex (issue #2,
109
+ // 2026-08-28). The inner loop only ever EXTENDS the range to the right, and all
110
+ // three conditions below are monotone under that extension: an unmapped index or an
111
+ // overlapping root stays inside a larger range, a claimed ref stays claimed because
112
+ // `claimed` is fixed for the whole walk, and a protected ref stays protected. Only
113
+ // `size > MAX_FOLD_SPAN_CHARS` bounded this loop, and these three `continue`s skipped
114
+ // the sizing that would have hit it, so an obstruction near the start of the window
115
+ // let the walk run to the end of `units` for every start: O(units^2) iterations doing
116
+ // O(span) work each. Instrumented on a 549KB window, ONE context event ran 457,986
117
+ // inner iterations and allocated 98,811,691 FoldPart objects, of which 456,724
118
+ // iterations (99.7 percent) died on the `claimed` check and only 72 ever reached the
119
+ // sizing. That is 42 seconds of CPU at 824KB, ungated by occupancy, on every event.
120
+ // With the walk stopped at the first obstruction the same event costs 377ms.
121
+ //
122
+ // This is 00dfeb3's defect one layer out. That commit memoized `orderedRoots` to kill
123
+ // the O(roots x folds) rebuild INSIDE each iteration and its own comment named "a walk
124
+ // that is quadratic in units" as the standing cost; this is that walk. Nothing about
125
+ // WHICH candidate is selected changes: every iteration this skips would have been
126
+ // skipped by the same condition.
104
127
  const parts = partsForRange(snapshot, state, first.start, unit.end - 1, allowedChildren);
105
- if (!parts || parts.some((part) => part.kind === "fold" && state.expanded.includes(part.foldId))) continue;
128
+ if (!parts || parts.some((part) => part.kind === "fold" && state.expanded.includes(part.foldId))) break;
106
129
  const refs = candidateSourceRefs(parts, state);
107
- if (refs.length > maximumSourceRefs) break;
108
- if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref)))) continue;
109
- if (refsProtected(refs, state, snapshot)) continue;
130
+ if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref)))) break;
131
+ if (refsProtected(refs, state, snapshot)) break;
110
132
  const size = bytes(encodedFoldSource(snapshot, state, parts, "chapter"));
111
- if (size > snapshot.policy.maxChapterChars && endIndex > unitIndex) break;
112
133
  const biteSized = size <= MAX_FOLD_SPAN_CHARS || endIndex === unitIndex;
113
- if (size >= snapshot.policy.minChapterChars && biteSized) best = { kind: "chapter", parts, sourceRefs: refs };
134
+ // ACCUMULATE UNTIL OVER, never cut to hit the number. The walk grows the span one
135
+ // whole unit at a time and records the first one that carries it past the floor,
136
+ // so a fold is always at least minFoldChars and always ends on a message boundary.
137
+ if (size >= snapshot.thresholds.minFoldChars && biteSized) best = { kind: "chapter", parts, sourceRefs: refs };
114
138
  if (size > MAX_FOLD_SPAN_CHARS) break;
115
139
  }
116
140
  if (best) return best;
@@ -123,7 +147,7 @@ export function selectAutomaticStaleSpan(
123
147
  state: ActiveContextState,
124
148
  claimed: ReadonlySet<string> = new Set<string>(),
125
149
  ): FoldCandidate | null {
126
- const chapter = selectAutomaticChapter(snapshot, state, snapshot.policy.maxFoldSourceRefs, claimed);
150
+ const chapter = selectAutomaticChapter(snapshot, state, claimed);
127
151
  const folds = selectAutomaticConsolidations(snapshot, state)[0] ?? null;
128
152
  if (!chapter || !folds) return chapter ?? folds;
129
153
  const at = (candidate: FoldCandidate): number =>
@@ -173,7 +197,7 @@ export function selectAutomaticRung(
173
197
  const claimedFoldIds = options.claimedFoldIds ?? new Set<string>();
174
198
  if (!options.toolOnly && state.prepared) {
175
199
  if (!Number.isFinite(ratio) || ratio < hardFenceRatio(snapshot)) return null;
176
- const candidate = selectAutomaticChapter(snapshot, state, snapshot.policy.maxFoldSourceRefs, claimed);
200
+ const candidate = selectAutomaticChapter(snapshot, state, claimed);
177
201
  return candidate ? { kind: "prepared-chapter", candidate } : null;
178
202
  }
179
203
  const tool = selectAutomaticToolBatch(snapshot, state, claimed)[0] ?? null;
@@ -405,10 +429,8 @@ export async function prepareFold(input: {
405
429
  now?: () => number;
406
430
  }): Promise<PreparedFold> {
407
431
  const { candidate, snapshot, state } = input;
408
- const protectedSource = candidate.kind === "tool-result"
409
- ? toolRefsProtected(candidate.sourceRefs, state, snapshot)
410
- : refsProtected(candidate.sourceRefs, state, snapshot);
411
- if (!candidate.sourceRefs.length || candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs ||
432
+ const protectedSource = refsProtected(candidate.sourceRefs, state, snapshot);
433
+ if (!candidate.sourceRefs.length ||
412
434
  refsInOrder(snapshot, candidate.sourceRefs) === null || protectedSource) {
413
435
  throw new Error("Fold source is not exact, stale, and unprotected");
414
436
  }
@@ -421,13 +443,43 @@ export async function prepareFold(input: {
421
443
  let brief: string;
422
444
  let provenance: BriefProvenance;
423
445
  if (input.brief !== undefined) {
424
- const complaint = briefContractComplaint(
425
- input.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
426
- if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
427
- brief = input.brief.trim();
428
- provenance = typeof input.briefProvenance === "object"
429
- ? clone(input.briefProvenance)
430
- : { kind: input.briefProvenance ?? "supplied" };
446
+ const declaredKind = typeof input.briefProvenance === "object"
447
+ ? input.briefProvenance.kind
448
+ : input.briefProvenance ?? "supplied";
449
+ if (declaredKind === "supplied") {
450
+ // A SUPPLIED BRIEF AUGMENTS THE HEAD, NEVER REPLACES IT (rep 7 of
451
+ // sol-20260823-live: replacement deleted the fact-carrying identification and 74
452
+ // of 95 peeks recovered values the run's own briefs dropped). The agent's clause
453
+ // is judged against its own reserve so the refusal names the budget the agent
454
+ // actually has, and the deterministic head keeps the remainder of the one policy
455
+ // cap. Only the agent's OWN words take this path: a deterministic brief arriving
456
+ // through the same door IS the head, and a re-prepared augmented brief is already
457
+ // composed, so each passes through under the whole-brief cap instead.
458
+ const complaint = briefContractComplaint(
459
+ input.brief.trim(), snapshot.policy.agentBriefReserve, snapshot.toolName);
460
+ if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
461
+ const clause = input.brief.trim();
462
+ const head = candidate.kind === "consolidation"
463
+ ? deterministicConsolidationBrief(candidate, state, snapshot.toolName, snapshot)
464
+ : deterministicChapterCandidateBrief(snapshot, candidate);
465
+ if (head) {
466
+ const seam = " · Agent: ";
467
+ const headRoom = Math.max(0, snapshot.policy.maxBriefChars - seam.length - clause.length);
468
+ brief = `${oneLine(head, headRoom)}${seam}${clause}`;
469
+ provenance = { kind: "augmented" };
470
+ } else {
471
+ brief = clause;
472
+ provenance = { kind: "supplied" };
473
+ }
474
+ } else {
475
+ const complaint = briefContractComplaint(
476
+ input.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
477
+ if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
478
+ brief = input.brief.trim();
479
+ provenance = typeof input.briefProvenance === "object"
480
+ ? clone(input.briefProvenance)
481
+ : { kind: declaredKind } as BriefProvenance;
482
+ }
431
483
  } else {
432
484
  brief = candidate.kind === "consolidation"
433
485
  ? deterministicConsolidationBrief(candidate, state, snapshot.toolName, snapshot)
@@ -580,13 +632,12 @@ export function preparedFoldError(input: {
580
632
  if (prepared.branchSha256 !== branchSha256(snapshot, [
581
633
  ...prepared.beforeRefs, ...prepared.sourceRefs, ...prepared.afterRefs,
582
634
  ])) return "branch drift";
583
- const toolSource = prepared.fold.kind === "tool-result";
584
- if (indices.some((index) => toolSource
585
- ? snapshot.toolProtectedIndices.has(index)
586
- : snapshot.protectedIndices.has(index))) return "fresh-tail drift";
587
- if (toolSource
588
- ? toolRefsProtected(prepared.sourceRefs, state, snapshot)
589
- : refsProtected(prepared.sourceRefs, state, snapshot)) return "source became protected";
635
+ // One protected set now: an entry is unfoldable because its turn has not completed or
636
+ // because it carries no evidence ref, and both are validity, not recency. The branch on
637
+ // fold kind went with fresh-tail protection: it read `toolRefsProtected` on one side,
638
+ // and once that collapsed onto `refsProtected` both arms were the same call.
639
+ if (indices.some((index) => snapshot.protectedIndices.has(index))) return "protection drift";
640
+ if (refsProtected(prepared.sourceRefs, state, snapshot)) return "source became protected";
590
641
  if (Object.prototype.hasOwnProperty.call(input, "ratio")) {
591
642
  if (input.ratio === null || typeof input.ratio !== "number" || !Number.isFinite(input.ratio)) {
592
643
  return "current pressure unavailable";
@@ -686,7 +737,7 @@ export function protectEvidence(
686
737
  if (byKey.size > MAX_ACTIVE_PROTECTED) {
687
738
  throw new Error(
688
739
  `Protecting ${ids.join(", ")} would hold ${byKey.size} refs, over the ${MAX_ACTIVE_PROTECTED}-ref cap; ` +
689
- "unprotect something first",
740
+ "unpin something first",
690
741
  );
691
742
  }
692
743
  return clearPrepared({
@@ -737,9 +788,7 @@ export function renderFold(
737
788
  const refs = flattenFoldRefs(fold, state);
738
789
  const indices = refsInOrder(snapshot, refs);
739
790
  if (!indices) return null;
740
- const mustReveal = state.expanded.includes(fold.id) || (fold.kind === "tool-result"
741
- ? toolRefsProtected(refs, state, snapshot)
742
- : refsProtected(refs, state, snapshot));
791
+ const mustReveal = state.expanded.includes(fold.id) || (refsProtected(refs, state, snapshot));
743
792
  if (!mustReveal) {
744
793
  const first = snapshot.messages[indices[0]] as Record<string, unknown>;
745
794
  const text = foldPlaceholder(fold, state, snapshot);
@@ -816,6 +865,13 @@ export function pinnedPeekMass(
816
865
  state: ActiveContextState,
817
866
  ): { bytes: number; results: number } {
818
867
  const explicitProtected = explicitProtectedKeys(state);
868
+ // ITS TWO SIBLINGS ALREADY RETURN HERE (2026-08-28). `protectedStaleMass` and
869
+ // `explicitProtectedMass` both open with this guard; this one did the whole quadratic walk
870
+ // and then returned zero, because every increment in its final loop is gated on
871
+ // `explicitProtected.has(...)`. So the guard is exact, not an approximation. It is not
872
+ // hypothetical either: `state.protected` is empty in all 10,521 persisted state records
873
+ // across 36 sealed runs, the corpus carrying no `context.pin` event at all.
874
+ if (!explicitProtected.size) return { bytes: 0, results: 0 };
819
875
  const folded = new Set<string>();
820
876
  for (const fold of state.folds) {
821
877
  for (const ref of flattenFoldRefs(fold, state)) folded.add(objectRefKey(ref));
@@ -869,19 +925,62 @@ export function projectActiveContext(
869
925
  index += 1;
870
926
  }
871
927
  }
928
+ applyToolClips(output, state, snapshot);
872
929
  assertProjectionPreservesToolLinkage(snapshot.messages, output);
873
930
  return output;
874
931
  }
875
932
 
933
+ // THE TOOL-CALL DIET'S VIEW (2026-08-24). A clipped result renders in place as its
934
+ // identified head with the cut stated and the recovery named, matched BY CALL ID and
935
+ // identity-preserving on role, call id, tool name, error flag and timestamp, exactly
936
+ // the ephemeral-peek withdrawal's shape one mechanism over. Clips are only ever ADDED
937
+ // inside a commit transaction, and a commit already rewrites the projection, so a
938
+ // non-commit pass with unchanged clips renders byte-identically and the frozen prefix
939
+ // never moves. Every reader shares this pass because every reader shares
940
+ // projectActiveContext: the live projection, the persistence readers and the
941
+ // attribution lens all see the same clipped view, and the full bytes stay in the
942
+ // transcript behind the entry id the marker names.
943
+ function applyToolClips(
944
+ output: unknown[],
945
+ state: ActiveContextState,
946
+ snapshot: ActiveContextSnapshot,
947
+ ): void {
948
+ if (!state.clips?.length) return;
949
+ const byCallId = new Map(state.clips.map((clip) => [clip.callId, clip]));
950
+ for (let index = 0; index < output.length; index += 1) {
951
+ const message = output[index] as Record<string, unknown> | null;
952
+ if (!message || (message as { role?: unknown }).role !== "toolResult") continue;
953
+ const callId = (message as { toolCallId?: unknown }).toolCallId;
954
+ if (typeof callId !== "string") continue;
955
+ const clip = byCallId.get(callId);
956
+ if (!clip) continue;
957
+ const details = (message as { details?: unknown }).details;
958
+ if (details && typeof (details as { projection?: unknown }).projection === "string") continue;
959
+ const full = contentText(message);
960
+ if (full.length <= snapshot.policy.toolClipFloorChars) continue;
961
+ const head = toolClipHead(full, snapshot.policy.toolClipHeadChars);
962
+ const hidden = full.length - head.length;
963
+ if (hidden <= 0) continue;
964
+ output[index] = {
965
+ role: "toolResult",
966
+ toolCallId: callId,
967
+ toolName: (message as { toolName?: unknown }).toolName,
968
+ content: [{ type: "text", text: `${head}\n... [${hidden} more chars clipped; ` +
969
+ `{"action":"peek","id":"${clip.entryId}"} returns the full result]` }],
970
+ isError: (message as { isError?: unknown }).isError ?? false,
971
+ details: { projection: "tool-clip" },
972
+ timestamp: (message as { timestamp?: unknown }).timestamp,
973
+ };
974
+ }
975
+ }
976
+
876
977
  export function foldStatusRow(fold: ActiveFold, state: ActiveContextState, snapshot: ActiveContextSnapshot): Record<string, unknown> {
877
978
  const navigation = foldNavigation(fold, state, snapshot);
878
979
  const interval = foldInterval(fold, state, snapshot);
879
980
  const refs = flattenFoldRefs(fold, state);
880
981
  const allSourceIds = refs.map((ref) => ref.entryId);
881
982
  const sourceIds = allSourceIds.slice(0, 64);
882
- const blocked = fold.kind === "tool-result"
883
- ? toolRefsProtected(refs, state, snapshot)
884
- : refsProtected(refs, state, snapshot);
983
+ const blocked = refsProtected(refs, state, snapshot);
885
984
  const projection = state.expanded.includes(fold.id) ? "expanded" : "folded";
886
985
  return {
887
986
  id: fold.id,
@@ -905,7 +1004,7 @@ export function foldStatusRow(fold: ActiveFold, state: ActiveContextState, snaps
905
1004
  : { action: "refold", id: fold.id },
906
1005
  expand: { action: "expand", id: fold.id },
907
1006
  refold: { action: "refold", id: fold.id },
908
- protect: { action: "protect", ids: [fold.id] },
1007
+ pin: { action: "pin", ids: [fold.id] },
909
1008
  },
910
1009
  };
911
1010
  }
@@ -995,7 +1094,6 @@ export function activeContextStatus(
995
1094
  state: ActiveContextState,
996
1095
  offset = 0,
997
1096
  limit = 40,
998
- maximumChapterSourceRefs = Number.MAX_SAFE_INTEGER,
999
1097
  statusOptions: { diet?: boolean; indexRows?: number } = {},
1000
1098
  ): Record<string, unknown> {
1001
1099
  const roots = orderedRoots(state, snapshot).map((item) => item.fold.id);
@@ -1010,11 +1108,11 @@ export function activeContextStatus(
1010
1108
  stale: !snapshot.protectedIndices.has(item.index),
1011
1109
  protected: snapshot.protectedIndices.has(item.index) || protectedKeys.has(objectRefKey(item.ref)),
1012
1110
  staleToolResult: item.ref.role === "toolResult" &&
1013
- !snapshot.toolProtectedIndices.has(item.index) && !protectedKeys.has(objectRefKey(item.ref)) &&
1111
+ !snapshot.protectedIndices.has(item.index) && !protectedKeys.has(objectRefKey(item.ref)) &&
1014
1112
  resultCall(snapshot, item.index, true) !== null,
1015
1113
  }] : []);
1016
1114
  const selectedObjects = objects.slice(offset, offset + limit);
1017
- const eligibleChapter = selectAutomaticChapter(snapshot, state, maximumChapterSourceRefs);
1115
+ const eligibleChapter = selectAutomaticChapter(snapshot, state);
1018
1116
  const eligibleSourceIds = eligibleChapter?.sourceRefs.map((ref) => ref.entryId) ?? [];
1019
1117
  const eligibleEndpoints = eligibleSourceIds.length
1020
1118
  ? [...new Set([eligibleSourceIds[0], eligibleSourceIds.at(-1)!])]
@@ -1067,12 +1165,11 @@ export function activeContextStatus(
1067
1165
  endId: eligibleSourceIds.at(-1),
1068
1166
  action: { action: "fold", ids: eligibleEndpoints, brief: "<factual brief, at most 1000 characters>" },
1069
1167
  } : null,
1070
- rawTailMinimumBytes: zoneBytes(snapshot.thresholds.freshTail, snapshot.budgetTokens),
1071
1168
  currentTurnRequiresBoundary: false,
1072
1169
  actions: {
1073
1170
  status: { action: "status", offset, limit },
1074
1171
  fold: { action: "fold", ids: ["<source-or-fold-id>"], brief: "<optional factual brief, at most 1000 characters>" },
1075
- protect: { action: "protect", ids: ["<source-or-fold-id>"] },
1172
+ pin: { action: "pin", ids: ["<source-or-fold-id>"] },
1076
1173
  },
1077
1174
  };
1078
1175
  }
@@ -1120,8 +1217,8 @@ export function boundStatusPayload(
1120
1217
  shrink(instrumentation, "events", "context event(s)", true);
1121
1218
  if (shrink(payload, "sourceMap", "source map row(s)") > 0) payload.sourceMapTruncated = true;
1122
1219
  shrink(payload, "topFolds", "top fold row(s)");
1123
- // The last epoch's receipt lists grow with commit depth (a 0.20 minTarget epoch
1124
- // applies dozens of marks). They are the commit's own receipt, so they outlast
1220
+ // The last epoch's receipt lists grow with commit depth (a deep epoch applies dozens
1221
+ // of marks). They are the commit's own receipt, so they outlast
1125
1222
  // every other diagnostic, but they still page away before any listing row the
1126
1223
  // caller actually asked for: on the detail pages, where none of the diagnostics
1127
1224
  // above exist, they are the mass that made one fold row miss the cap.
@@ -1170,9 +1267,7 @@ export function visibleCollapsedFolds(
1170
1267
  const visit = (fold: ActiveFold): void => {
1171
1268
  const refs = flattenFoldRefs(fold, state);
1172
1269
  if (!foldInterval(fold, state, snapshot)) return;
1173
- const protectedSource = fold.kind === "tool-result"
1174
- ? toolRefsProtected(refs, state, snapshot)
1175
- : refsProtected(refs, state, snapshot);
1270
+ const protectedSource = refsProtected(refs, state, snapshot);
1176
1271
  if (!state.expanded.includes(fold.id) && !protectedSource) {
1177
1272
  output.push(fold);
1178
1273
  return;
@@ -1315,11 +1410,26 @@ export function peekFoldSource(input: {
1315
1410
  const source = stableStringify(messages);
1316
1411
  const sourceBytes = bytes(source);
1317
1412
  const offset = Math.min(Math.max(0, input.offset ?? 0), sourceBytes);
1318
- const budget = input.maximumBytes ?? PEEK_DEFAULT_MAX_BYTES;
1413
+ // The widest read anyone may ask for is the policy's own source cap, the same ceiling
1414
+ // the `wider` hint has always offered; an unclamped `bytes` was the one door left to a
1415
+ // single result larger than any fold the runtime would cut.
1416
+ const budget = Math.min(
1417
+ input.maximumBytes ?? PEEK_DEFAULT_MAX_BYTES,
1418
+ ACTIVE_CONTEXT_POLICY.maxSourceChars,
1419
+ );
1420
+ // ONE BUDGET GOVERNS THE WHOLE RESULT (sol-20260826-full2, pifold rep 1). The byte
1421
+ // bound used to govern `source` alone while the descendant index rode free, so peeking
1422
+ // the root of a deep consolidation tree returned 320 nested briefs, 423KB against a
1423
+ // stated 16KB bound, durably into a window already near its budget; that one result is
1424
+ // what the projection fence then aborted thirty-five requests over. The index seats
1425
+ // first, into at most half the budget, because a parent's index is the point read; the
1426
+ // source view takes everything the seated rows leave.
1427
+ const catalog = boundedDescendantIndex(fold, input.state, Math.floor(budget / 2));
1428
+ const sourceBudget = budget - catalog.seatedBytes;
1319
1429
  const window = offset > 0 ? utf8Slice(source, offset) : source;
1320
1430
  const view = offset > 0
1321
- ? { text: boundedUtf8(window, budget), omittedBytes: 0, contiguous: true }
1322
- : boundedHeadTail(window, budget);
1431
+ ? { text: boundedUtf8(window, sourceBudget), omittedBytes: 0, contiguous: true }
1432
+ : boundedHeadTail(window, sourceBudget);
1323
1433
  const returned = view.text;
1324
1434
  const returnedBytes = bytes(returned) - bytes(headTailMarker(view.omittedBytes));
1325
1435
  const nextOffset = view.contiguous && offset + returnedBytes < sourceBytes
@@ -1327,7 +1437,7 @@ export function peekFoldSource(input: {
1327
1437
  : null;
1328
1438
  const truncated = view.omittedBytes > 0 || nextOffset !== null;
1329
1439
  const children = childFoldIds(fold);
1330
- const index = descendantIndexRows(fold, input.state);
1440
+ const index = catalog.rows;
1331
1441
  return {
1332
1442
  version: 1,
1333
1443
  action: "peek",
@@ -1346,25 +1456,31 @@ export function peekFoldSource(input: {
1346
1456
  truncated,
1347
1457
  view: offset > 0 ? "slice" : (view.omittedBytes > 0 ? "index" : "complete"),
1348
1458
  ...(index.length ? { index } : {}),
1459
+ ...(catalog.omitted ? { indexOmitted: catalog.omitted } : {}),
1349
1460
  children,
1350
1461
  narrower: {
1351
1462
  ...(nextOffset === null ? {} : { slice: { action: "peek", id: fold.id, offset: nextOffset, bytes: returnedBytes } }),
1352
1463
  ...(children.length ? { child: { action: "peek", id: children[0] } } : {}),
1353
1464
  },
1354
- ...(truncated
1355
- ? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxChapterChars) } }
1465
+ ...(truncated || catalog.omitted
1466
+ ? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars) } }
1356
1467
  : {}),
1357
1468
  lifetime: "these bytes stay in the window exactly as returned, like any other tool result, and " +
1358
1469
  `nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s stored source, so the ` +
1359
1470
  `next commit reclaims it behind a placeholder naming ${fold.id}; peek ${fold.id} again for the ` +
1360
1471
  "same verbatim bytes, or pin this result to keep the copy raw.",
1361
- note: truncated
1472
+ note: (truncated
1362
1473
  ? `Bounded read: ${returnedBytes} of ${sourceBytes} exact source bytes, ${view.omittedBytes} omitted ` +
1363
1474
  `from the middle. Widen with bytes, page with offset, or expand ${fold.id} to restore it in place.`
1364
1475
  : children.length
1365
1476
  ? `Complete stored span, one level: raw entries exactly, and ${children.length} child fold(s) still ` +
1366
1477
  "placeheld. Peek a child id to read its own span; the fold stayed collapsed and no projection changed."
1367
- : "Complete exact source; the fold stayed collapsed and no projection changed.",
1478
+ : "Complete exact source; the fold stayed collapsed and no projection changed.") +
1479
+ (catalog.omitted
1480
+ ? ` The nested index seats the shallowest ${index.length} of ${index.length + catalog.omitted} ` +
1481
+ "descendant briefs inside the byte budget; the deeper rows are one hop away by peeking a child id, " +
1482
+ "or widen with bytes."
1483
+ : ""),
1368
1484
  source: returned,
1369
1485
  ...(truncated
1370
1486
  ? {
@@ -1372,7 +1488,7 @@ export function peekFoldSource(input: {
1372
1488
  "bytes were NOT shown" +
1373
1489
  (view.omittedBytes ? " (the middle; the head and tail are both above)" : " (everything after this slice)") +
1374
1490
  `. Widen with {"action":"peek","id":"${fold.id}","bytes":${
1375
- Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxChapterChars)}}` +
1491
+ Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars)}}` +
1376
1492
  (nextOffset === null ? "" : ` or read on with {"action":"peek","id":"${fold.id}","offset":${nextOffset}}`) +
1377
1493
  ".",
1378
1494
  }
@@ -1404,6 +1520,38 @@ export function boundedHeadTail(
1404
1520
  };
1405
1521
  }
1406
1522
 
1523
+ /**
1524
+ * The descendant index, seated inside a byte budget: shallowest rows first, whole rows
1525
+ * only, the omitted counted rather than dropped. Depth order is the navigational order,
1526
+ * because a row's own children are reachable by peeking it, so the rows worth seating
1527
+ * first are the ones every deeper row is reachable THROUGH. Walk order breaks depth
1528
+ * ties, which keeps siblings in span order. The seating stops at the first row that
1529
+ * does not fit, so what is omitted is always a contiguous deep tail of the depth order
1530
+ * and the note can state it as one count.
1531
+ */
1532
+ export function boundedDescendantIndex(
1533
+ fold: ActiveFold,
1534
+ state: ActiveContextState,
1535
+ budget: number,
1536
+ ): { rows: Array<Record<string, unknown>>; omitted: number; seatedBytes: number } {
1537
+ const walked = descendantIndexRows(fold, state)
1538
+ .map((row, walk) => ({ row, walk }))
1539
+ .sort((left, right) =>
1540
+ Number(left.row.depth) - Number(right.row.depth) || left.walk - right.walk);
1541
+ const rows: Array<Record<string, unknown>> = [];
1542
+ // Counted as the array actually serializes, brackets and joining commas included, so
1543
+ // "seated bytes" is the bytes the result carries and not an under-read of them.
1544
+ let seatedBytes = 2;
1545
+ for (const { row } of walked) {
1546
+ const cost = bytes(stableStringify(row)) + (rows.length ? 1 : 0);
1547
+ if (seatedBytes + cost > budget) break;
1548
+ seatedBytes += cost;
1549
+ rows.push(row);
1550
+ }
1551
+ if (!rows.length) seatedBytes = 0;
1552
+ return { rows, omitted: walked.length - rows.length, seatedBytes };
1553
+ }
1554
+
1407
1555
  export function descendantIndexRows(
1408
1556
  fold: ActiveFold,
1409
1557
  state: ActiveContextState,
@@ -64,6 +64,7 @@ export interface CacheObservation {
64
64
  change: ProjectionChange;
65
65
  inputTokens: number;
66
66
  cacheReadTokens: number;
67
+ cacheWriteTokens: number;
67
68
  providerSideMiss: boolean;
68
69
  }
69
70
 
@@ -83,20 +84,14 @@ export interface ContextEvent {
83
84
  export type ContextEventKind =
84
85
  | "context.attempt"
85
86
  | "context.correction"
86
- | "context.protect"
87
+ | "context.pin"
87
88
  | "context.capacity"
88
89
  | "context.receipt"
89
- | "context.rider"
90
- | "context.lastcall"
91
- | "context.response"
92
- | "context.suggestion"
93
- | "context.outcome"
94
- | "context.notice"
90
+ | "context.frontier"
95
91
  | "context.commit"
96
92
  | "context.fold"
97
93
  | "context.brief"
98
94
  | "context.absorb"
99
- | "context.split"
100
95
  | "context.recovery"
101
96
  | "context.suspend"
102
97
  | "context.rollback"
@@ -197,9 +192,14 @@ export function observeCacheUsage(
197
192
  const usage = input.usage;
198
193
  const inputTokens = ownValue(usage, "input");
199
194
  const cacheReadTokens = ownValue(usage, "cacheRead");
195
+ const rawCacheWriteTokens = ownValue(usage, "cacheWrite");
200
196
  if (typeof inputTokens !== "number" || !Number.isFinite(inputTokens) || inputTokens <= 0 ||
201
197
  typeof cacheReadTokens !== "number" || !Number.isFinite(cacheReadTokens) ||
202
198
  cacheReadTokens < 0) return null;
199
+ const cacheWriteTokens = typeof rawCacheWriteTokens === "number" &&
200
+ Number.isFinite(rawCacheWriteTokens) && rawCacheWriteTokens >= 0
201
+ ? rawCacheWriteTokens
202
+ : 0;
203
203
  const missed = cacheReadTokens === 0;
204
204
  const preserved = typeof input.preservedShare === "number" && Number.isFinite(input.preservedShare)
205
205
  ? input.preservedShare
@@ -209,7 +209,8 @@ export function observeCacheUsage(
209
209
  change: input.change,
210
210
  inputTokens,
211
211
  cacheReadTokens,
212
- providerSideMiss: missed && (input.change !== "rewrite" ||
212
+ cacheWriteTokens,
213
+ providerSideMiss: missed && cacheWriteTokens === 0 && (input.change !== "rewrite" ||
213
214
  (preserved !== null && preserved >= PROVIDER_SIDE_MISS_PRESERVED_SHARE)),
214
215
  };
215
216
  if (missed) ledger.observedMisses += 1;
@@ -15,7 +15,7 @@ import {
15
15
  } from "./canonical.ts";
16
16
  import {
17
17
  childFoldIds,
18
- deriveFoldParents,
18
+ assignFoldParents,
19
19
  flattenFoldRefs,
20
20
  foldMap,
21
21
  parseActiveContextState,
@@ -270,11 +270,42 @@ export function foldInterval(fold: ActiveFold, state: ActiveContextState, snapsh
270
270
  return indices ? { start: indices[0], end: indices.at(-1)! } : null;
271
271
  }
272
272
 
273
- export function orderedRoots(state: ActiveContextState, snapshot: ActiveContextSnapshot): Array<{ fold: ActiveFold; start: number; end: number }> {
274
- return rootFolds(state).flatMap((fold) => {
273
+ export type OrderedRoot = { fold: ActiveFold; start: number; end: number };
274
+
275
+ // ROOT INTERVALS ARE DERIVED ONCE PER (STATE, SNAPSHOT) PAIR (2026-08-24). This derivation
276
+ // is O(roots x folds) on its own, because `foldInterval` calls `flattenFoldRefs`, which
277
+ // rebuilds `foldMap` for every root it walks. `partsForRange` opens with it, and
278
+ // `selectAutomaticChapter` calls `partsForRange` from a walk that is quadratic in units,
279
+ // so the cost was O(units^2 x roots x folds) for an answer that cannot change inside a
280
+ // pass. Replaying sealed sol-20260823-live rep 3 at 125 folds, ONE frontier walk made
281
+ // 12,333 calls and spent 22.5 seconds, and 12,328 of them returned null without reading a
282
+ // root, because the automatic chapter passes an EMPTY allowed-child set and any overlap
283
+ // refuses. At 140 folds the walk was 98.8 percent of the runtime's whole per-request cost;
284
+ // the per-turn gap between a response and the next projection ran 3s at zero folds to 164s
285
+ // past 150, and five per-turn call sites each pay a walk.
286
+ //
287
+ // The memo is gate 113's rule and gate 121's rule at a third address: kept against the
288
+ // state and snapshot OBJECTS rather than their contents, because both are REPLACED on
289
+ // every change rather than mutated, so a replaced one misses and derives again instead of
290
+ // serving something stale. It hands every caller the SAME array, which is safe only while
291
+ // no reading mutates it; gate 149 drives every call site against a frozen one so a reading
292
+ // that starts mutating throws rather than quietly poisoning the memo.
293
+ export const ORDERED_ROOTS = new WeakMap<ActiveContextState, WeakMap<ActiveContextSnapshot, OrderedRoot[]>>();
294
+
295
+ export function orderedRoots(state: ActiveContextState, snapshot: ActiveContextSnapshot): OrderedRoot[] {
296
+ let bySnapshot = ORDERED_ROOTS.get(state);
297
+ if (!bySnapshot) {
298
+ bySnapshot = new WeakMap<ActiveContextSnapshot, OrderedRoot[]>();
299
+ ORDERED_ROOTS.set(state, bySnapshot);
300
+ }
301
+ const memoized = bySnapshot.get(snapshot);
302
+ if (memoized) return memoized;
303
+ const roots = rootFolds(state).flatMap((fold) => {
275
304
  const interval = foldInterval(fold, state, snapshot);
276
305
  return interval ? [{ fold, ...interval }] : [];
277
306
  }).sort((left, right) => left.start - right.start);
307
+ bySnapshot.set(snapshot, roots);
308
+ return roots;
278
309
  }
279
310
 
280
311
  export function visibleCollapsedRoots(state: ActiveContextState, snapshot: ActiveContextSnapshot) {
@@ -317,7 +348,9 @@ export function persistenceProjection(state: ActiveContextState, snapshot: Activ
317
348
  if (projected.prepared && projected.prepared.sourceRefs.some((ref) => !mapped.has(objectRefKey(ref)))) {
318
349
  delete projected.prepared;
319
350
  }
320
- projected.folds = deriveFoldParents(projected.folds);
351
+ // The parse on the next line validates this forest, so `assignFoldParents` is the whole
352
+ // job here; `deriveFoldParents` would walk it twice.
353
+ projected.folds = assignFoldParents(projected.folds);
321
354
  return parseActiveContextState(projected, state.sessionId);
322
355
  }
323
356
 
@@ -325,6 +358,12 @@ export function explicitProtectedKeys(state: ActiveContextState): Set<string> {
325
358
  return new Set(state.protected.map(objectRefKey));
326
359
  }
327
360
 
361
+ /**
362
+ * Held against folding: pinned by name, or unfoldable for validity (an unfinished turn, an
363
+ * entry with no evidence ref). There used to be a second reading of this for tool spans,
364
+ * differing only in which protected set it consulted; the two sets collapsed into one when
365
+ * fresh-tail was deleted, and the two functions were byte-identical after that.
366
+ */
328
367
  export function refsProtected(
329
368
  refs: EvidenceRef[],
330
369
  state: ActiveContextState,
@@ -337,18 +376,6 @@ export function refsProtected(
337
376
  });
338
377
  }
339
378
 
340
- export function toolRefsProtected(
341
- refs: EvidenceRef[],
342
- state: ActiveContextState,
343
- snapshot: ActiveContextSnapshot,
344
- ): boolean {
345
- const explicit = explicitProtectedKeys(state);
346
- return refs.some((ref) => {
347
- const item = exactMapped(snapshot, ref);
348
- return !item || explicit.has(objectRefKey(ref)) || snapshot.toolProtectedIndices.has(item.index);
349
- });
350
- }
351
-
352
379
  export function protectedStaleMass(
353
380
  snapshot: ActiveContextSnapshot,
354
381
  state: ActiveContextState,