pi-fold 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,
@@ -71,7 +72,9 @@ import {
71
72
  chapterUnits,
72
73
  unpinnedStaleFolds,
73
74
  deterministicChapterCandidateBrief,
75
+ toolClipHead,
74
76
  deterministicConsolidationBrief,
77
+ oneLine,
75
78
  NO_FOLD_KINDS,
76
79
  partsForRange,
77
80
  resultCall,
@@ -79,6 +82,7 @@ import {
79
82
  selectAutomaticRefold,
80
83
  selectAutomaticToolBatch,
81
84
  } from "./selection.ts";
85
+ import { staleSpanMatureEnd } from "./transcript.ts";
82
86
 
83
87
  export function selectAutomaticChapter(
84
88
  snapshot: ActiveContextSnapshot,
@@ -87,19 +91,44 @@ export function selectAutomaticChapter(
87
91
  ): FoldCandidate | null {
88
92
  const units = chapterUnits(snapshot);
89
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);
90
97
  for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
91
98
  const first = units[unitIndex];
99
+ if (first.end > matureEnd) break;
92
100
  let best: FoldCandidate | null = null;
93
101
  for (let endIndex = unitIndex; endIndex < units.length; endIndex += 1) {
94
102
  const unit = units[endIndex];
103
+ if (unit.end > matureEnd) break;
95
104
  if (endIndex > unitIndex && unit.start !== units[endIndex - 1].end) break;
96
105
  const coherentSegment = endIndex > unitIndex || first.end - first.start > 1;
97
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.
98
127
  const parts = partsForRange(snapshot, state, first.start, unit.end - 1, allowedChildren);
99
- 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;
100
129
  const refs = candidateSourceRefs(parts, state);
101
- if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref)))) continue;
102
- 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;
103
132
  const size = bytes(encodedFoldSource(snapshot, state, parts, "chapter"));
104
133
  const biteSized = size <= MAX_FOLD_SPAN_CHARS || endIndex === unitIndex;
105
134
  // ACCUMULATE UNTIL OVER, never cut to hit the number. The walk grows the span one
@@ -414,13 +443,43 @@ export async function prepareFold(input: {
414
443
  let brief: string;
415
444
  let provenance: BriefProvenance;
416
445
  if (input.brief !== undefined) {
417
- const complaint = briefContractComplaint(
418
- input.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
419
- if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
420
- brief = input.brief.trim();
421
- provenance = typeof input.briefProvenance === "object"
422
- ? clone(input.briefProvenance)
423
- : { 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
+ }
424
483
  } else {
425
484
  brief = candidate.kind === "consolidation"
426
485
  ? deterministicConsolidationBrief(candidate, state, snapshot.toolName, snapshot)
@@ -806,6 +865,13 @@ export function pinnedPeekMass(
806
865
  state: ActiveContextState,
807
866
  ): { bytes: number; results: number } {
808
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 };
809
875
  const folded = new Set<string>();
810
876
  for (const fold of state.folds) {
811
877
  for (const ref of flattenFoldRefs(fold, state)) folded.add(objectRefKey(ref));
@@ -859,10 +925,55 @@ export function projectActiveContext(
859
925
  index += 1;
860
926
  }
861
927
  }
928
+ applyToolClips(output, state, snapshot);
862
929
  assertProjectionPreservesToolLinkage(snapshot.messages, output);
863
930
  return output;
864
931
  }
865
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
+
866
977
  export function foldStatusRow(fold: ActiveFold, state: ActiveContextState, snapshot: ActiveContextSnapshot): Record<string, unknown> {
867
978
  const navigation = foldNavigation(fold, state, snapshot);
868
979
  const interval = foldInterval(fold, state, snapshot);
@@ -1299,11 +1410,26 @@ export function peekFoldSource(input: {
1299
1410
  const source = stableStringify(messages);
1300
1411
  const sourceBytes = bytes(source);
1301
1412
  const offset = Math.min(Math.max(0, input.offset ?? 0), sourceBytes);
1302
- 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;
1303
1429
  const window = offset > 0 ? utf8Slice(source, offset) : source;
1304
1430
  const view = offset > 0
1305
- ? { text: boundedUtf8(window, budget), omittedBytes: 0, contiguous: true }
1306
- : boundedHeadTail(window, budget);
1431
+ ? { text: boundedUtf8(window, sourceBudget), omittedBytes: 0, contiguous: true }
1432
+ : boundedHeadTail(window, sourceBudget);
1307
1433
  const returned = view.text;
1308
1434
  const returnedBytes = bytes(returned) - bytes(headTailMarker(view.omittedBytes));
1309
1435
  const nextOffset = view.contiguous && offset + returnedBytes < sourceBytes
@@ -1311,7 +1437,7 @@ export function peekFoldSource(input: {
1311
1437
  : null;
1312
1438
  const truncated = view.omittedBytes > 0 || nextOffset !== null;
1313
1439
  const children = childFoldIds(fold);
1314
- const index = descendantIndexRows(fold, input.state);
1440
+ const index = catalog.rows;
1315
1441
  return {
1316
1442
  version: 1,
1317
1443
  action: "peek",
@@ -1330,25 +1456,31 @@ export function peekFoldSource(input: {
1330
1456
  truncated,
1331
1457
  view: offset > 0 ? "slice" : (view.omittedBytes > 0 ? "index" : "complete"),
1332
1458
  ...(index.length ? { index } : {}),
1459
+ ...(catalog.omitted ? { indexOmitted: catalog.omitted } : {}),
1333
1460
  children,
1334
1461
  narrower: {
1335
1462
  ...(nextOffset === null ? {} : { slice: { action: "peek", id: fold.id, offset: nextOffset, bytes: returnedBytes } }),
1336
1463
  ...(children.length ? { child: { action: "peek", id: children[0] } } : {}),
1337
1464
  },
1338
- ...(truncated
1465
+ ...(truncated || catalog.omitted
1339
1466
  ? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars) } }
1340
1467
  : {}),
1341
1468
  lifetime: "these bytes stay in the window exactly as returned, like any other tool result, and " +
1342
1469
  `nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s stored source, so the ` +
1343
1470
  `next commit reclaims it behind a placeholder naming ${fold.id}; peek ${fold.id} again for the ` +
1344
1471
  "same verbatim bytes, or pin this result to keep the copy raw.",
1345
- note: truncated
1472
+ note: (truncated
1346
1473
  ? `Bounded read: ${returnedBytes} of ${sourceBytes} exact source bytes, ${view.omittedBytes} omitted ` +
1347
1474
  `from the middle. Widen with bytes, page with offset, or expand ${fold.id} to restore it in place.`
1348
1475
  : children.length
1349
1476
  ? `Complete stored span, one level: raw entries exactly, and ${children.length} child fold(s) still ` +
1350
1477
  "placeheld. Peek a child id to read its own span; the fold stayed collapsed and no projection changed."
1351
- : "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
+ : ""),
1352
1484
  source: returned,
1353
1485
  ...(truncated
1354
1486
  ? {
@@ -1388,6 +1520,38 @@ export function boundedHeadTail(
1388
1520
  };
1389
1521
  }
1390
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
+
1391
1555
  export function descendantIndexRows(
1392
1556
  fold: ActiveFold,
1393
1557
  state: ActiveContextState,
@@ -87,17 +87,11 @@ export type ContextEventKind =
87
87
  | "context.pin"
88
88
  | "context.capacity"
89
89
  | "context.receipt"
90
- | "context.lastcall"
91
- | "context.response"
92
- | "context.suggestion"
93
- | "context.outcome"
94
- | "context.notice"
95
90
  | "context.frontier"
96
91
  | "context.commit"
97
92
  | "context.fold"
98
93
  | "context.brief"
99
94
  | "context.absorb"
100
- | "context.split"
101
95
  | "context.recovery"
102
96
  | "context.suspend"
103
97
  | "context.rollback"
@@ -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
 
@@ -537,3 +570,50 @@ export function parseNativeCompactionCompletion(value: unknown, expectedSessionI
537
570
  }
538
571
  return { ...(clone(value) as unknown as NativeCompactionCompletionReceipt), decision };
539
572
  }
573
+
574
+ /**
575
+ * AN IMAGE IS NOT TEXT, AND MEASURING IT AS TEXT KILLS SESSIONS (2026-08-28).
576
+ *
577
+ * The projection is sized by serializing it and counting UTF-8 bytes, which is right for
578
+ * prose and catastrophically wrong for an image: a content block carries its pixels as
579
+ * base64, so a screenshot lands as several hundred thousand characters that the estimator
580
+ * then divides by a chars-per-token ratio learned from PROSE. A live session read three
581
+ * PNGs of 364,801, 631,045 and 827,513 bytes; the projection went 636,687 chars to
582
+ * 1,268,275 in one step and the estimate went 110,687 tokens to 220,883, while the
583
+ * provider's own count for that same request came back 111,837. Every token of the
584
+ * 109,046 difference was base64 counted as if it were sentences.
585
+ *
586
+ * The calibration cannot learn its way out, and that is the part worth understanding.
587
+ * `projectionCharsPerToken` takes the MINIMUM ratio over its window so the estimate never
588
+ * under-reads. An image pushes the true ratio UP, so the minimum discards exactly the
589
+ * sample that would have corrected it and keeps dividing base64 by the prose ratio. The
590
+ * conservative rule that protects against under-reading is what locks in the over-read.
591
+ *
592
+ * So image payload is removed from the character count and replaced by a flat per-image
593
+ * token cost. The count cannot be exact across providers, and it does not need to be: it
594
+ * needs to be the right order of magnitude instead of wrong by seventy times, and the
595
+ * provider anchor corrects the remainder the moment a real measurement lands.
596
+ */
597
+ export function imageMass(projected: unknown[]): { base64Chars: number; images: number } {
598
+ let base64Chars = 0;
599
+ let images = 0;
600
+ for (const message of projected) {
601
+ const content = ownValue(message, "content");
602
+ const blocks = denseOwnArrayValues(content);
603
+ if (!blocks) continue;
604
+ for (const block of blocks) {
605
+ if (ownValue(block, "type") !== "image") continue;
606
+ images += 1;
607
+ // Two shapes reach here: the flat `data` a Pi tool result carries, and the nested
608
+ // `source.data` of a provider-native block. Both are counted; neither is required.
609
+ const direct = ownValue(block, "data");
610
+ const nested = ownValue(ownValue(block, "source"), "data");
611
+ const payload = typeof direct === "string" ? direct
612
+ : typeof nested === "string" ? nested : "";
613
+ // The serialization counts the JSON-escaped string, and base64 has no character
614
+ // JSON escapes, so its byte length in the serialized form is its own length.
615
+ base64Chars += bytes(payload);
616
+ }
617
+ }
618
+ return { base64Chars, images };
619
+ }