pi-fold 0.1.1 → 1.0.1

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 {
3
3
  denseOwnArrayValues,
4
4
  evidenceRef,
5
5
  evidenceSha256,
6
+ isPlainRecord,
6
7
  objectRefKey,
7
8
  sha256Text,
8
9
  sha256Value,
@@ -16,6 +17,7 @@ import {
16
17
  ownValue,
17
18
  sessionEntryMessages,
18
19
  usefulBrief,
20
+ utf8Slice,
19
21
  } from "./canonical.ts";
20
22
  import {
21
23
  branchSha256,
@@ -34,6 +36,7 @@ import {
34
36
  childFoldIds,
35
37
  clearPrepared,
36
38
  flattenFoldRefs,
39
+ foldBrief,
37
40
  foldIdFor,
38
41
  foldMap,
39
42
  normalizedPart,
@@ -46,8 +49,13 @@ import {
46
49
  activeContextBrand,
47
50
  activeContextSource,
48
51
  CONSOLIDATION_WIDTH_THRESHOLD,
52
+ CONTEXT_STATUS_RESPONSE_BYTES,
49
53
  EXPAND_LEASE_GENERATIONS,
50
54
  MAX_EXPAND_LEASES,
55
+ MAX_FOLD_SPAN_CHARS,
56
+ PEEK_DEFAULT_MAX_BYTES,
57
+ PEEK_HEAD_SHARE,
58
+ STATUS_DIET_SUGGESTIONS,
51
59
  } from "./policy.ts";
52
60
  import type {
53
61
  ActiveContextSnapshot,
@@ -76,6 +84,7 @@ export function selectAutomaticChapter(
76
84
  snapshot: ActiveContextSnapshot,
77
85
  state: ActiveContextState,
78
86
  maximumSourceRefs: number = snapshot.policy.maxFoldSourceRefs,
87
+ claimed: ReadonlySet<string> = new Set<string>(),
79
88
  ): FoldCandidate | null {
80
89
  const units = chapterUnits(snapshot);
81
90
  const allowedChildren = new Set<FoldKind>(["tool-result"]);
@@ -94,10 +103,21 @@ export function selectAutomaticChapter(
94
103
  if (!parts || parts.some((part) => part.kind === "fold" && state.expanded.includes(part.foldId))) continue;
95
104
  const refs = candidateSourceRefs(parts, state);
96
105
  if (refs.length > maximumSourceRefs) break;
106
+ if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref)))) continue;
97
107
  if (refsProtected(refs, state, snapshot)) continue;
98
108
  const size = bytes(encodedFoldSource(snapshot, state, parts, "chapter"));
99
- if (size > snapshot.policy.maxChapterChars) break;
100
- if (size >= snapshot.policy.minChapterChars) best = { kind: "chapter", parts, sourceRefs: refs };
109
+ // The chapter cap bounds what the rung COMPOSES, never what it may reclaim: a
110
+ // single closed unit that alone exceeds it is still accepted (rep 19: six status
111
+ // results, each above the cap, were exactly the mass nothing could fold).
112
+ if (size > snapshot.policy.maxChapterChars && endIndex > unitIndex) break;
113
+ // Bite-sized by construction. A multi-unit chapter never grows past the cap, so
114
+ // the ladder cannot build the 60kB fold whose tail hid the answer (rep 6); a
115
+ // SINGLE unit that alone exceeds it is still accepted, because there is no
116
+ // interior boundary to stop at and refusing would leave the biggest span in the
117
+ // window as the one thing nothing may reclaim.
118
+ const biteSized = size <= MAX_FOLD_SPAN_CHARS || endIndex === unitIndex;
119
+ if (size >= snapshot.policy.minChapterChars && biteSized) best = { kind: "chapter", parts, sourceRefs: refs };
120
+ if (size > MAX_FOLD_SPAN_CHARS) break;
101
121
  }
102
122
  if (best) return best;
103
123
  }
@@ -117,6 +137,14 @@ export interface AutomaticRungSelectionOptions {
117
137
  toolOnly?: boolean;
118
138
  summarizerAvailable?: boolean;
119
139
  failedPreparationIds?: ReadonlySet<string>;
140
+ /**
141
+ * Evidence and folds already spoken for by something that is not yet a fold, i.e.
142
+ * a pending epoch mark. Excluding them makes each eligible turn select NEW stale
143
+ * content instead of re-selecting the batch a mark already covers, which is the
144
+ * only way marks accumulate below the commit threshold.
145
+ */
146
+ claimed?: ReadonlySet<string>;
147
+ claimedFoldIds?: ReadonlySet<string>;
120
148
  }
121
149
 
122
150
  export function automaticPreparationId(candidate: FoldCandidate, state: ActiveContextState): string {
@@ -134,19 +162,21 @@ export function selectAutomaticRung(
134
162
  ratio: number,
135
163
  options: AutomaticRungSelectionOptions = {},
136
164
  ): AutomaticRungSelection | null {
165
+ const claimed = options.claimed ?? new Set<string>();
166
+ const claimedFoldIds = options.claimedFoldIds ?? new Set<string>();
137
167
  if (!options.toolOnly && state.prepared) {
138
168
  if (!Number.isFinite(ratio) || ratio < hardFenceRatio(snapshot)) return null;
139
- const candidate = selectAutomaticChapter(snapshot, state);
169
+ const candidate = selectAutomaticChapter(snapshot, state, snapshot.policy.maxFoldSourceRefs, claimed);
140
170
  return candidate ? { kind: "prepared-chapter", candidate } : null;
141
171
  }
142
- const tool = selectAutomaticToolForRung(snapshot, state, ratio, options.waiveToolCadence);
172
+ const tool = selectAutomaticToolForRung(snapshot, state, ratio, options.waiveToolCadence, claimed);
143
173
  if (tool) return { kind: "tool", candidate: tool };
144
174
  if (options.toolOnly || !Number.isFinite(ratio)) return null;
145
- const refold = selectAutomaticRefold(snapshot, state, ratio);
175
+ const refold = selectAutomaticRefold(snapshot, state, ratio, claimedFoldIds);
146
176
  if (refold) return { kind: "refold", foldId: refold };
147
- const consolidation = selectAutomaticConsolidation(snapshot, state, ratio);
177
+ const consolidation = selectAutomaticConsolidation(snapshot, state, ratio, claimed);
148
178
  if (consolidation) return { kind: "consolidation", candidate: consolidation };
149
- const chapter = selectAutomaticChapter(snapshot, state);
179
+ const chapter = selectAutomaticChapter(snapshot, state, snapshot.policy.maxFoldSourceRefs, claimed);
150
180
  if (!chapter) return null;
151
181
  const preparationFailed = options.failedPreparationIds?.has(
152
182
  automaticPreparationId(chapter, state),
@@ -315,7 +345,12 @@ export async function prepareFold(input: {
315
345
  state: ActiveContextState;
316
346
  generation: number;
317
347
  brief?: string;
318
- briefProvenance?: "supplied" | "deterministic";
348
+ /**
349
+ * Provenance for a supplied brief. A full record round-trips a brief whose
350
+ * generator already ran (a pending mark carrying a model brief); the string
351
+ * forms stay the shorthand for the two model-free kinds.
352
+ */
353
+ briefProvenance?: "supplied" | "deterministic" | BriefProvenance;
319
354
  summarize?: (request: Record<string, unknown>, ctx?: unknown) => Promise<Record<string, unknown>>;
320
355
  onSummarizerFailure?: (error: unknown) => void;
321
356
  ctx?: unknown;
@@ -350,7 +385,9 @@ export async function prepareFold(input: {
350
385
  throw new Error(`Supplied brief must be non-structural and at most ${snapshot.policy.maxBriefChars} characters`);
351
386
  }
352
387
  brief = input.brief.trim();
353
- provenance = { kind: input.briefProvenance ?? "supplied" };
388
+ provenance = typeof input.briefProvenance === "object"
389
+ ? clone(input.briefProvenance)
390
+ : { kind: input.briefProvenance ?? "supplied" };
354
391
  } else {
355
392
  let modelBrief: { brief: string; provenance: BriefProvenance } | null = null;
356
393
  if (input.summarize) {
@@ -701,7 +738,7 @@ export function foldPlaceholder(fold: ActiveFold, state: ActiveContextState, sna
701
738
  const parent = fold.parentId ?? "root";
702
739
  return [
703
740
  `[${activeContextBrand(snapshot.brandNoun)} fold ${fold.id}]`,
704
- fold.brief,
741
+ foldBrief(fold, state),
705
742
  `Topology: kind=${fold.kind}; parent=${parent}; children=${childFoldIds(fold).length}; ` +
706
743
  `previous=${navigation.previous ?? "none"}; next=${navigation.next ?? "none"}.`,
707
744
  `Expand exactly: ${snapshot.toolName} {"action":"expand","id":"${fold.id}"}`,
@@ -791,6 +828,50 @@ export function assertProjectionPreservesToolLinkage(source: unknown[], projecte
791
828
  }
792
829
  }
793
830
 
831
+ /**
832
+ * Peek mass the ladder cannot take: results whose evidence the agent protected
833
+ * outright. This is the number that made rep 7's starvation invisible. The mass sat
834
+ * raw in the window, the ROI trigger's eligibility arithmetic could never reach its
835
+ * threshold over it, and no accounting field said so while the window grew to 235k
836
+ * tokens.
837
+ */
838
+ export function pinnedPeekMass(
839
+ snapshot: ActiveContextSnapshot,
840
+ state: ActiveContextState,
841
+ ): { bytes: number; results: number } {
842
+ const explicitProtected = explicitProtectedKeys(state);
843
+ const folded = new Set<string>();
844
+ for (const fold of state.folds) {
845
+ for (const ref of flattenFoldRefs(fold, state)) folded.add(objectRefKey(ref));
846
+ }
847
+ const peekCalls = new Map<string, string>();
848
+ for (const message of snapshot.messages) {
849
+ if (messageRole(message) !== "assistant") continue;
850
+ for (const part of denseOwnArrayValues(ownValue(message, "content")) ?? []) {
851
+ if (ownValue(part, "type") !== "toolCall" || ownValue(part, "name") !== snapshot.toolName) continue;
852
+ const callId = ownValue(part, "id");
853
+ const args = ownValue(part, "arguments");
854
+ const foldId = ownValue(args, "id");
855
+ if (typeof callId !== "string" || !callId || ownValue(args, "action") !== "peek" ||
856
+ typeof foldId !== "string" || !foldId) continue;
857
+ peekCalls.set(callId, foldId);
858
+ }
859
+ }
860
+ let total = 0;
861
+ let results = 0;
862
+ for (const item of snapshot.mapped) {
863
+ if (messageRole(item.message) !== "toolResult" || !item.ref) continue;
864
+ if (folded.has(objectRefKey(item.ref))) continue;
865
+ const toolCallId = ownValue(item.message, "toolCallId");
866
+ const foldId = typeof toolCallId === "string" ? peekCalls.get(toolCallId) : undefined;
867
+ if (!foldId) continue;
868
+ if (!explicitProtected.has(objectRefKey(item.ref))) continue;
869
+ total += bytes(item.message);
870
+ results += 1;
871
+ }
872
+ return { bytes: total, results };
873
+ }
874
+
794
875
  export function projectActiveContext(
795
876
  snapshot: ActiveContextSnapshot,
796
877
  state: ActiveContextState,
@@ -812,6 +893,13 @@ export function projectActiveContext(
812
893
  index += 1;
813
894
  }
814
895
  }
896
+ // A peek result is APPEND-ONLY. It used to be rewritten in place once a model call
897
+ // had seen it, on the theory that the edit was tail-local; rep 22 disproved that.
898
+ // The result is only tail-local when it ARRIVES, and reclamation deliberately waits
899
+ // until a later assistant message exists, by which point the window has grown over
900
+ // it and the edit lands mid-prefix. Two peeks cost 100k fresh tokens that way. The
901
+ // duplicate bytes are still reclaimed, by the tool-fold rung at the next commit,
902
+ // which is the one moment a rewrite is already being paid for.
815
903
  assertProjectionPreservesToolLinkage(snapshot.messages, output);
816
904
  return output;
817
905
  }
@@ -840,6 +928,11 @@ export function foldStatusRow(fold: ActiveFold, state: ActiveContextState, snaps
840
928
  sourceCount: allSourceIds.length,
841
929
  sourceIdsTruncated: sourceIds.length < allSourceIds.length,
842
930
  sourceSha256: fold.sourceSha256,
931
+ // A paged row without the brief is a chronological id and nothing else: the
932
+ // agent that paged the tree to find WHICH fold holds something got the one
933
+ // field that answers it withheld, while the tree detail carried it all along.
934
+ brief: foldBrief(fold, state),
935
+ sourceChars: fold.sourceChars,
843
936
  actions: {
844
937
  primary: projection === "folded"
845
938
  ? { action: "expand", id: fold.id }
@@ -883,19 +976,70 @@ export function foldTreeDetail(
883
976
  kind: fold.kind,
884
977
  depth: foldDepth(state, fold),
885
978
  parentId: fold.parentId,
886
- brief: fold.brief,
979
+ brief: foldBrief(fold, state),
887
980
  sourceCount: flattenFoldRefs(fold, state).length,
888
981
  state: state.expanded.includes(fold.id) ? "expanded" : "folded",
889
982
  peekable: true,
890
983
  }));
891
984
  }
892
985
 
986
+ /**
987
+ * One compact row per collapsed fold: what is behind it, where its span starts and
988
+ * ends, and the brief that carries whatever key material the fold captured. This is
989
+ * the answer to "which fold has X" that agents were paying 56-83k chars per peek to
990
+ * reconstruct, and it costs a line.
991
+ */
992
+ export function foldIndexRow(
993
+ fold: ActiveFold,
994
+ state: ActiveContextState,
995
+ snapshot: ActiveContextSnapshot,
996
+ ): Record<string, unknown> {
997
+ const refs = flattenFoldRefs(fold, state);
998
+ const interval = foldInterval(fold, state, snapshot);
999
+ const replacement = renderFold(fold, state, snapshot);
1000
+ const source = renderFold(fold, { ...state, expanded: [...state.expanded, fold.id] }, snapshot);
1001
+ const reclaimableBytes = replacement && source ? Math.max(0, bytes(source) - bytes(replacement)) : 0;
1002
+ return {
1003
+ id: fold.id,
1004
+ kind: fold.kind,
1005
+ depth: foldDepth(state, fold),
1006
+ state: state.expanded.includes(fold.id) ? "expanded" : "folded",
1007
+ startId: refs[0]?.entryId ?? null,
1008
+ endId: refs.at(-1)?.entryId ?? null,
1009
+ startPosition: interval?.start ?? null,
1010
+ endPosition: interval?.end ?? null,
1011
+ sourceCount: refs.length,
1012
+ sourceBytes: fold.sourceChars,
1013
+ reclaimableBytes,
1014
+ brief: foldBrief(fold, state),
1015
+ peek: { action: "peek", id: fold.id },
1016
+ expand: { action: "expand", id: fold.id },
1017
+ };
1018
+ }
1019
+
1020
+ /** Every mapped source id to the fold that now holds it, so a lookup never needs a read. */
1021
+ export function foldSourceMap(
1022
+ state: ActiveContextState,
1023
+ snapshot: ActiveContextSnapshot,
1024
+ limit = 512,
1025
+ ): { entries: Array<[string, string]>; total: number; truncated: boolean } {
1026
+ const entries: Array<[string, string]> = [];
1027
+ for (const fold of orderedFoldTree(state, snapshot)) {
1028
+ for (const ref of flattenFoldRefs(fold, state)) entries.push([ref.entryId, fold.id]);
1029
+ }
1030
+ // Deepest wins: the innermost fold is the one that actually holds the source.
1031
+ const byEntry = new Map(entries);
1032
+ const all = [...byEntry.entries()];
1033
+ return { entries: all.slice(0, limit), total: all.length, truncated: all.length > limit };
1034
+ }
1035
+
893
1036
  export function activeContextStatus(
894
1037
  snapshot: ActiveContextSnapshot,
895
1038
  state: ActiveContextState,
896
1039
  offset = 0,
897
1040
  limit = 40,
898
1041
  maximumChapterSourceRefs = Number.MAX_SAFE_INTEGER,
1042
+ statusOptions: { diet?: boolean; suggestions?: number } = {},
899
1043
  ): Record<string, unknown> {
900
1044
  const roots = orderedRoots(state, snapshot).map((item) => item.fold.id);
901
1045
  const ordered = orderedFoldTree(state, snapshot);
@@ -918,18 +1062,49 @@ export function activeContextStatus(
918
1062
  const eligibleEndpoints = eligibleSourceIds.length
919
1063
  ? [...new Set([eligibleSourceIds[0], eligibleSourceIds.at(-1)!])]
920
1064
  : [];
1065
+ // The index that used to ride every status call grew from 12.6k to 86.5k chars in
1066
+ // one measured session, injected on every request that asked for state. On the diet
1067
+ // the default payload carries counts, the source map and the folds worth acting on;
1068
+ // the full tree and the object list move behind an explicit paged query.
1069
+ const index = statusOptions.diet
1070
+ ? visibleCollapsedFolds(state, snapshot)
1071
+ .map((fold) => foldIndexRow(fold, state, snapshot))
1072
+ .sort((left, right) =>
1073
+ Number(right.reclaimableBytes) - Number(left.reclaimableBytes) ||
1074
+ String(left.id).localeCompare(String(right.id)))
1075
+ .slice(0, statusOptions.suggestions ?? STATUS_DIET_SUGGESTIONS)
1076
+ : null;
1077
+ const sourceMap = statusOptions.diet ? foldSourceMap(state, snapshot) : null;
921
1078
  return {
922
1079
  version: 1,
923
1080
  service: "active-context-folding",
924
1081
  roots,
925
- folds: selected.map((fold) => foldStatusRow(fold, state, snapshot)),
1082
+ ...(statusOptions.diet
1083
+ ? {
1084
+ index: "diet",
1085
+ topFolds: index,
1086
+ sourceMap: sourceMap!.entries,
1087
+ sourceMapTotal: sourceMap!.total,
1088
+ sourceMapTruncated: sourceMap!.truncated,
1089
+ paging: {
1090
+ note: "Results are delivered in bounded pages.",
1091
+ folds: { action: "status", detail: "folds", offset: 0, limit: 40 },
1092
+ objects: { action: "status", detail: "objects", offset: 0, limit: 40 },
1093
+ tree: { action: "status", detail: "tree", offset: 0 },
1094
+ },
1095
+ }
1096
+ : {
1097
+ folds: selected.map((fold) => foldStatusRow(fold, state, snapshot)),
1098
+ objects: selectedObjects,
1099
+ nextObjectOffset: offset + selectedObjects.length < objects.length
1100
+ ? offset + selectedObjects.length
1101
+ : null,
1102
+ }),
926
1103
  offset,
927
1104
  nextOffset: offset + selected.length < ordered.length ? offset + selected.length : null,
928
1105
  totalFolds: ordered.length,
929
1106
  protectedSourceIds: state.protected.flatMap((ref) => exactMapped(snapshot, ref) ? [ref.entryId] : []),
930
- objects: selectedObjects,
931
1107
  totalObjects: objects.length,
932
- nextObjectOffset: offset + selectedObjects.length < objects.length ? offset + selectedObjects.length : null,
933
1108
  eligibleChapter: eligibleChapter ? {
934
1109
  kind: "chapter",
935
1110
  sourceCount: eligibleSourceIds.length,
@@ -949,6 +1124,91 @@ export function activeContextStatus(
949
1124
  };
950
1125
  }
951
1126
 
1127
+ /** Room reserved for the one-line continuation marker a truncated page carries. */
1128
+ const STATUS_CONTINUATION_RESERVE_BYTES = 400;
1129
+
1130
+ /**
1131
+ * Enforce the hard status page cap on one fully assembled status payload.
1132
+ *
1133
+ * Measured 2026-08-07 (rep 19): one status answer serialized to 526KB, 6.2x the
1134
+ * remaining headroom, and aborted the request. The paging affordance existed;
1135
+ * nothing forced it. This function is the force: every listing in the payload is
1136
+ * truncated at a unit boundary (whole rows, never mid-unit) until the serialized
1137
+ * page fits, and a one-line continuation marker names the next offset. Trim order
1138
+ * spends the diagnostics first and the listing the caller asked for last, and the
1139
+ * requested listing always keeps at least one unit so paging always progresses.
1140
+ */
1141
+ export function boundStatusPayload(
1142
+ payload: Record<string, unknown>,
1143
+ detail: string | null,
1144
+ offset: number,
1145
+ cap: number = CONTEXT_STATUS_RESPONSE_BYTES,
1146
+ ): Record<string, unknown> {
1147
+ const size = (): number => Buffer.byteLength(JSON.stringify(payload, null, 2), "utf8");
1148
+ if (size() <= cap) return payload;
1149
+ const target = cap - STATUS_CONTINUATION_RESERVE_BYTES;
1150
+ const fits = (): boolean => size() <= target;
1151
+ const omitted: string[] = [];
1152
+ // keepEnd keeps the NEWEST units of a time-ordered ledger; a paged listing keeps
1153
+ // its head so the continuation offset is the next unread unit. Binary search for
1154
+ // the largest keep that fits; a stage that cannot fit alone trims to its floor.
1155
+ const shrink = (owner: unknown, key: string, label: string, keepEnd = false, minKeep = 0): number => {
1156
+ if (!isPlainRecord(owner) || fits()) return 0;
1157
+ const original = ownValue(owner, key);
1158
+ if (!Array.isArray(original) || original.length <= minKeep) return 0;
1159
+ const container = owner as Record<string, unknown>;
1160
+ const apply = (keep: number): void => {
1161
+ container[key] = keepEnd ? original.slice(original.length - keep) : original.slice(0, keep);
1162
+ };
1163
+ let low = minKeep;
1164
+ let high = original.length - 1;
1165
+ let keep = minKeep;
1166
+ while (low <= high) {
1167
+ const mid = (low + high) >> 1;
1168
+ apply(mid);
1169
+ if (fits()) { keep = mid; low = mid + 1; } else { high = mid - 1; }
1170
+ }
1171
+ apply(keep);
1172
+ const dropped = original.length - keep;
1173
+ if (dropped > 0) omitted.push(`${dropped} ${label}`);
1174
+ return dropped;
1175
+ };
1176
+ const automatic = ownValue(payload, "automatic");
1177
+ const instrumentation = isPlainRecord(automatic) ? ownValue(automatic, "instrumentation") : undefined;
1178
+ const surfacing = isPlainRecord(automatic) ? ownValue(automatic, "surfacing") : undefined;
1179
+ shrink(instrumentation, "projectionRecords", "projection record(s)", true);
1180
+ shrink(instrumentation, "cacheObservations", "cache observation(s)", true);
1181
+ shrink(instrumentation, "events", "context event(s)", true);
1182
+ shrink(surfacing, "log", "surfacing log row(s)", true);
1183
+ if (shrink(payload, "sourceMap", "source map row(s)") > 0) payload.sourceMapTruncated = true;
1184
+ shrink(payload, "topFolds", "top fold row(s)");
1185
+ const rowLabel = (key: string): string => key === "folds" ? "fold row(s)" : key === "objects" ? "object row(s)" : "tree row(s)";
1186
+ // The listing the caller asked for goes last and never below one unit. Its
1187
+ // machine-readable next offset moves back to the first unit the page dropped.
1188
+ let continueAt: number | null = null;
1189
+ if (detail === "folds" || detail === "objects") {
1190
+ const rideKey = detail === "folds" ? "objects" : "folds";
1191
+ if (shrink(payload, rideKey, rowLabel(rideKey)) > 0) {
1192
+ const rideKept = (payload[rideKey] as unknown[]).length;
1193
+ if (rideKey === "folds") payload.nextOffset = offset + rideKept;
1194
+ else payload.nextObjectOffset = offset + rideKept;
1195
+ }
1196
+ }
1197
+ const primaryKey = detail === "objects" ? "objects" : detail === "tree" ? "tree" : detail === "folds" ? "folds" : null;
1198
+ if (primaryKey && shrink(payload, primaryKey, rowLabel(primaryKey), false, 1) > 0) {
1199
+ continueAt = offset + (payload[primaryKey] as unknown[]).length;
1200
+ if (primaryKey === "objects") payload.nextObjectOffset = continueAt;
1201
+ else payload.nextOffset = continueAt;
1202
+ }
1203
+ if (!omitted.length) return payload;
1204
+ const resume = continueAt === null
1205
+ ? "the full lists stay reachable through the paged status details"
1206
+ : `continue at ${JSON.stringify({ action: "status", detail, offset: continueAt })}`;
1207
+ payload.continuation =
1208
+ `Status pages are bounded to ${cap} bytes; omitted ${omitted.join(", ")}; ${resume}.`;
1209
+ return payload;
1210
+ }
1211
+
952
1212
  export function visibleCollapsedFolds(
953
1213
  state: ActiveContextState,
954
1214
  snapshot: ActiveContextSnapshot,
@@ -1044,7 +1304,7 @@ export function recoverFoldMessages(input: {
1044
1304
 
1045
1305
  /**
1046
1306
  * Ephemeral point read of one fold's exact source at any depth. It recovers the same
1047
- * SHA-256-verified messages expansion restores, bounded for the reply, and changes no
1307
+ * SHA-256-verified messages expansion restores, bounded for one model call, and changes no
1048
1308
  * projection: the fold stays collapsed and the durable state is untouched.
1049
1309
  */
1050
1310
  export function peekFoldSource(input: {
@@ -1052,7 +1312,11 @@ export function peekFoldSource(input: {
1052
1312
  state: ActiveContextState;
1053
1313
  entries: Array<Record<string, unknown>>;
1054
1314
  sessionId: string;
1315
+ /** Explicit widening. Absent, a peek returns the bounded index view. */
1055
1316
  maximumBytes?: number;
1317
+ /** Byte offset into the exact stored source; the narrowing half of admission control. */
1318
+ offset?: number;
1319
+ toolName?: string;
1056
1320
  projectEntry?: (entry: Record<string, unknown>) => unknown[];
1057
1321
  }): Record<string, unknown> {
1058
1322
  const fold = input.state.folds.find((item) => item.id === input.foldId);
@@ -1066,9 +1330,25 @@ export function peekFoldSource(input: {
1066
1330
  });
1067
1331
  const source = stableStringify(messages);
1068
1332
  const sourceBytes = bytes(source);
1069
- const returned = boundedUtf8(source, input.maximumBytes ?? ACTIVE_CONTEXT_POLICY.maxChapterChars);
1070
- const returnedBytes = bytes(returned);
1071
- const truncated = returnedBytes < sourceBytes;
1333
+ const offset = Math.min(Math.max(0, input.offset ?? 0), sourceBytes);
1334
+ const budget = input.maximumBytes ?? PEEK_DEFAULT_MAX_BYTES;
1335
+ const window = offset > 0 ? utf8Slice(source, offset) : source;
1336
+ // Offset zero is the INDEX read and takes head AND tail, because chain keys and
1337
+ // conclusions live at the end of a source and a head-only slice silently omits
1338
+ // exactly that region. An explicit offset is paging and stays contiguous.
1339
+ const view = offset > 0
1340
+ ? { text: boundedUtf8(window, budget), omittedBytes: 0, contiguous: true }
1341
+ : boundedHeadTail(window, budget);
1342
+ const returned = view.text;
1343
+ const returnedBytes = bytes(returned) - bytes(headTailMarker(view.omittedBytes));
1344
+ const nextOffset = view.contiguous && offset + returnedBytes < sourceBytes
1345
+ ? offset + returnedBytes
1346
+ : null;
1347
+ const truncated = view.omittedBytes > 0 || nextOffset !== null;
1348
+ const children = childFoldIds(fold);
1349
+ // The index view: every nested fold's brief in FULL. A brief is the navigable unit,
1350
+ // so truncating it is the one economy that costs the read its purpose.
1351
+ const index = descendantIndexRows(fold, input.state);
1072
1352
  return {
1073
1353
  version: 1,
1074
1354
  action: "peek",
@@ -1076,16 +1356,103 @@ export function peekFoldSource(input: {
1076
1356
  kind: fold.kind,
1077
1357
  parentId: fold.parentId,
1078
1358
  depth: foldDepth(input.state, fold),
1079
- brief: fold.brief,
1359
+ brief: foldBrief(fold, input.state),
1080
1360
  sourceCount: messages.length,
1081
1361
  sourceSha256: fold.sourceSha256,
1082
1362
  sourceBytes,
1363
+ offset,
1083
1364
  returnedBytes,
1365
+ omittedBytes: view.omittedBytes,
1366
+ nextOffset,
1084
1367
  truncated,
1368
+ view: offset > 0 ? "slice" : (view.omittedBytes > 0 ? "index" : "complete"),
1369
+ ...(index.length ? { index } : {}),
1370
+ // Narrower and WIDER reads that are always constructible, so a refusal or a
1371
+ // truncation can name one.
1372
+ children,
1373
+ narrower: {
1374
+ ...(nextOffset === null ? {} : { slice: { action: "peek", id: fold.id, offset: nextOffset, bytes: returnedBytes } }),
1375
+ ...(children.length ? { child: { action: "peek", id: children[0] } } : {}),
1376
+ },
1377
+ ...(truncated
1378
+ ? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxChapterChars) } }
1379
+ : {}),
1380
+ lifetime: "these bytes stay in the window exactly as returned, like any other tool " +
1381
+ "result, until the ladder folds them at a commit. Nothing rewrites this result in place.",
1085
1382
  note: truncated
1086
- ? `Truncated: this reply carries the first ${returnedBytes} of ${sourceBytes} exact source bytes; ` +
1087
- `expand ${fold.id} to restore the complete source in place.`
1383
+ ? `Bounded read: ${returnedBytes} of ${sourceBytes} exact source bytes, ${view.omittedBytes} omitted ` +
1384
+ `from the middle. Widen with bytes, page with offset, or expand ${fold.id} to restore it in place.`
1088
1385
  : "Complete exact source; the fold stayed collapsed and no projection changed.",
1089
1386
  source: returned,
1387
+ // Serialized AFTER the source on purpose: a reader that just consumed a bounded slice
1388
+ // decides its next action at the end of the payload, and a notice buried before ten
1389
+ // thousand source bytes is a notice unread (measured: a run concluded a staged chain
1390
+ // was finished because the chain key lived in the unshown tail of a truncated peek).
1391
+ ...(truncated
1392
+ ? {
1393
+ truncationReminder: `STOP: ${view.omittedBytes || sourceBytes - returnedBytes} of ${sourceBytes} ` +
1394
+ "bytes were NOT shown" +
1395
+ (view.omittedBytes ? " (the middle; the head and tail are both above)" : " (everything after this slice)") +
1396
+ `. Widen with {"action":"peek","id":"${fold.id}","bytes":${
1397
+ Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxChapterChars)}}` +
1398
+ (nextOffset === null ? "" : ` or read on with {"action":"peek","id":"${fold.id}","offset":${nextOffset}}`) +
1399
+ ".",
1400
+ }
1401
+ : {}),
1402
+ };
1403
+ }
1404
+
1405
+ /** The marker a head+tail view puts where the omitted middle was. */
1406
+ export function headTailMarker(omittedBytes: number): string {
1407
+ return omittedBytes > 0 ? `\n... [${omittedBytes} exact source bytes omitted] ...\n` : "";
1408
+ }
1409
+
1410
+ /**
1411
+ * A bounded view that keeps the head AND the tail. A head-only bound drops exactly the
1412
+ * region a staged chain keeps its key in; the omitted size is stated where the reading
1413
+ * ends so "there is more" is never an inference.
1414
+ */
1415
+ export function boundedHeadTail(
1416
+ source: string,
1417
+ budget: number,
1418
+ ): { text: string; omittedBytes: number; contiguous: boolean } {
1419
+ const total = bytes(source);
1420
+ if (total <= budget) return { text: source, omittedBytes: 0, contiguous: true };
1421
+ const headBudget = Math.max(1, Math.floor(budget * PEEK_HEAD_SHARE));
1422
+ const head = boundedUtf8(source, headBudget);
1423
+ const tailBudget = Math.max(1, budget - bytes(head));
1424
+ const tailStart = Math.max(bytes(head), total - tailBudget);
1425
+ const tail = boundedUtf8(utf8Slice(source, tailStart), tailBudget);
1426
+ const omittedBytes = Math.max(0, total - bytes(head) - bytes(tail));
1427
+ if (omittedBytes <= 0) return { text: source, omittedBytes: 0, contiguous: true };
1428
+ return {
1429
+ text: `${head}${headTailMarker(omittedBytes)}${tail}`,
1430
+ omittedBytes,
1431
+ contiguous: false,
1432
+ };
1433
+ }
1434
+
1435
+ /** Every descendant fold, in transcript order, with its brief in full. */
1436
+ export function descendantIndexRows(
1437
+ fold: ActiveFold,
1438
+ state: ActiveContextState,
1439
+ ): Array<Record<string, unknown>> {
1440
+ const byId = foldMap(state);
1441
+ const rows: Array<Record<string, unknown>> = [];
1442
+ const visit = (parent: ActiveFold): void => {
1443
+ for (const childId of childFoldIds(parent)) {
1444
+ const child = byId.get(childId);
1445
+ if (!child) continue;
1446
+ rows.push({
1447
+ id: child.id,
1448
+ kind: child.kind,
1449
+ depth: foldDepth(state, child),
1450
+ brief: foldBrief(child, state),
1451
+ peek: { action: "peek", id: child.id },
1452
+ });
1453
+ visit(child);
1454
+ }
1090
1455
  };
1456
+ visit(fold);
1457
+ return rows;
1091
1458
  }