pi-fold 3.1.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.
@@ -46,6 +46,7 @@ import {
46
46
  explicitProtectedMass,
47
47
  budgetOccupancy,
48
48
  hardFenceRatio,
49
+ imageMass,
49
50
  latestProviderContextMeasurement,
50
51
  orderedRoots,
51
52
  parseNativeCompactionCompletion,
@@ -105,6 +106,7 @@ import {
105
106
  resolveThresholds,
106
107
  servingBudgetTokens,
107
108
  ESTIMATED_BYTES_PER_TOKEN,
109
+ IMAGE_ESTIMATED_TOKENS,
108
110
  entryTypeNamespace,
109
111
  MAX_FOLD_SPAN_CHARS,
110
112
  DEFAULT_TOOL_FOLD_THRESHOLD,
@@ -116,10 +118,6 @@ import {
116
118
  RETIRED_TOOL_ACTIONS,
117
119
  AUTO_FOLD_BLACKLIST_DEFAULT,
118
120
  USER_RESCUE_MAX_SOURCE_CHARS,
119
- MEMORY_BODY_CHARS,
120
- MEMORY_KEY_CHARS,
121
- MEMORY_KEYS_MAX,
122
- WORKING_MEMORY_TOOL_ACTIONS,
123
121
  } from "./lib/policy.ts";
124
122
  import type {
125
123
  ActiveContextSnapshot,
@@ -140,7 +138,7 @@ import {
140
138
  import {
141
139
  contextReceipt,
142
140
  foldNoticeText,
143
- memoryTocText,
141
+ foldStatusText,
144
142
  receiptBlockText,
145
143
  withReceipt,
146
144
  } from "./lib/curation.ts";
@@ -247,13 +245,6 @@ export function registerActiveContext(pi: any, options: {
247
245
  * agent to improve a brief. Either way the agent verbs stay on the tool, so an agent
248
246
  * that finds a fold can still annotate what it reads. */
249
247
  postFoldNotice?: boolean;
250
- /** THE WORKING MEMORY (Shane, 2026-08-26): a session-scoped ordered dictionary the
251
- * agent maintains beside the fold index. `remember` writes or removes an entry,
252
- * `recall` reads bodies on demand, and the projection carries one table of contents
253
- * that refreshes at each commit. Off when absent: the digest channel earned its
254
- * place in a measured gap (running tallies lost 0/6 against native's 6/6 while the
255
- * index won everything else), and it must stay separable from the index it is not. */
256
- workingMemory?: boolean;
257
248
  }): {
258
249
  projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
259
250
  } {
@@ -268,11 +259,6 @@ export function registerActiveContext(pi: any, options: {
268
259
  "carrier inviting the agent to improve it");
269
260
  }
270
261
  const postFoldNotice = options.postFoldNotice ?? false;
271
- if (options.workingMemory !== undefined && typeof options.workingMemory !== "boolean") {
272
- throw new Error("workingMemory must be a boolean: it enables the session-scoped " +
273
- "working-memory dictionary and its two actions, remember and recall");
274
- }
275
- const workingMemory = options.workingMemory === true;
276
262
  const toolFoldThreshold = options.toolFoldThreshold ?? DEFAULT_TOOL_FOLD_THRESHOLD;
277
263
  if (typeof toolFoldThreshold !== "number" || !Number.isFinite(toolFoldThreshold) ||
278
264
  toolFoldThreshold < 0 || toolFoldThreshold >= 1) {
@@ -345,7 +331,6 @@ export function registerActiveContext(pi: any, options: {
345
331
  const milestoneProjectionType = `${entryTypePrefix}-milestone`;
346
332
  const advisoryProjectionType = `${entryTypePrefix}-advisory`;
347
333
  const foldNoticeProjectionType = `${entryTypePrefix}-fold-notice`;
348
- const memoryTocProjectionType = `${entryTypePrefix}-memory-toc`;
349
334
  const receiptProjectionType = `${entryTypePrefix}-receipts`;
350
335
  const curationProjectionType = `${entryTypePrefix}-curation`;
351
336
  const entryNamespace = entryTypeNamespace(entryTypePrefix);
@@ -353,9 +338,7 @@ export function registerActiveContext(pi: any, options: {
353
338
  const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
354
339
  const contextEventEntryType = `${entryNamespace}-context-event`;
355
340
  const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
356
- const allowedToolActions: readonly ActiveContextToolAction[] = workingMemory
357
- ? [...ACTIVE_CONTEXT_TOOL_ACTIONS, ...WORKING_MEMORY_TOOL_ACTIONS]
358
- : ACTIVE_CONTEXT_TOOL_ACTIONS;
341
+ const allowedToolActions: readonly ActiveContextToolAction[] = ACTIVE_CONTEXT_TOOL_ACTIONS;
359
342
  const allowedToolActionSet = new Set<string>(allowedToolActions);
360
343
 
361
344
  type AutomaticFailureState = {
@@ -401,6 +384,7 @@ export function registerActiveContext(pi: any, options: {
401
384
  lastProviderMeasurement: null as ProviderContextMeasurement | null,
402
385
  descriptorWindow: null as number | null,
403
386
  lastProjectedChars: null as number | null,
387
+ lastProjectedImages: 0,
404
388
  projectionCalibrations: [] as Array<{ chars: number; tokens: number }>,
405
389
  lastProjectedEstimate: null as number | null,
406
390
  lastProjectedEstimateCalibrated: false,
@@ -409,6 +393,10 @@ export function registerActiveContext(pi: any, options: {
409
393
  projectionAnchor: null as {
410
394
  tokens: number;
411
395
  chars: number;
396
+ /** `chars` less the base64 of any image, which is what the delta is taken on. */
397
+ textChars: number;
398
+ /** Images inside the anchored prefix, each worth IMAGE_ESTIMATED_TOKENS. */
399
+ images: number;
412
400
  head: string;
413
401
  messageSha256: string;
414
402
  sessionId: string;
@@ -428,6 +416,10 @@ export function registerActiveContext(pi: any, options: {
428
416
  ledger: emptyLedger(),
429
417
  previousDigests: null as string[] | null,
430
418
  previousText: null as string | null,
419
+ // The image mass of `previousText`, kept beside it because the provider anchor is
420
+ // built from that text and has no access to the array it came from.
421
+ previousImages: 0,
422
+ previousImageChars: 0,
431
423
  lastChange: "append" as ProjectionChange,
432
424
  lastPreservedShare: null as number | null,
433
425
  sinceHandoff: [] as Array<{
@@ -491,9 +483,6 @@ export function registerActiveContext(pi: any, options: {
491
483
  };
492
484
 
493
485
  const advisory = {
494
- hardFenceNoticeKey: null as string | null,
495
- hardFenceReleaseSessionId: null as string | null,
496
- hardFenceReleasedProjectionKeys: new Set<string>(),
497
486
  };
498
487
 
499
488
  // EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
@@ -632,15 +621,43 @@ export function registerActiveContext(pi: any, options: {
632
621
  lifecycle.generation === expectedGeneration && persistence.state?.sessionId === sessionId &&
633
622
  (!ctx || contextSessionMatches(ctx, sessionId));
634
623
 
624
+ /**
625
+ * THE ONE LINE THAT IS ALWAYS ON SCREEN, so it answers the question a person actually
626
+ * has: how full am I, and is anything about to happen.
627
+ *
628
+ * It used to read `pi_fold_context folds: 3 · provider 187234/272000`. That led with the
629
+ * AGENT TOOL'S NAME, which a person cannot call and does not care about, and then spent
630
+ * its remaining width on two raw counts they would have to divide themselves. The brand
631
+ * leads now, occupancy is a percentage of the serving budget, and the commit point is
632
+ * named only when it is close enough to matter.
633
+ */
635
634
  const updateStatus = (ctx: any): void => {
636
635
  try {
637
636
  const roots = persistence.state && lifecycle.latestSnapshot ? orderedRoots(persistence.state, lifecycle.latestSnapshot).length : 0;
638
- const prepared = persistence.state?.prepared ? " · brief ready" : ladder.preparing ? " · briefing" : "";
639
- const usage = measurements.lastProviderMeasurement
640
- ? ` · provider ${measurements.lastProviderMeasurement.tokens}/${measurements.lastProviderMeasurement.contextWindow}`
641
- : " · provider usage unmeasured";
642
- const suspended = ladder.automaticFailure ? " · automatic suspended" : "";
643
- ctx.ui?.setStatus?.(entryTypePrefix, `${toolName} folds: ${roots}${prepared}${usage}${suspended}`);
637
+ const staged = persistence.state ? pendingMarks(persistence.state).length : 0;
638
+ const capacity = currentCapacity(ctx);
639
+ const used = measurements.lastProviderMeasurement?.tokens ?? null;
640
+ const share = used !== null && capacity.budgetTokens > 0 ? used / capacity.budgetTokens : null;
641
+ const parts: string[] = [];
642
+ // FAILURE LEADS. This line is cut to whatever width the host gives it, so the last
643
+ // segment is the first one a narrow terminal removes, and folding having stopped is
644
+ // the one state on this line a person has to act on. It was appended last.
645
+ if (ladder.automaticFailure) parts.push("FOLDING STOPPED");
646
+ // Unmeasured is stated, never guessed: an estimate rendered as a fact is the habit
647
+ // that cost a live session, and this line is the most-read surface there is.
648
+ parts.push(share === null ? "not measured" : `${Math.round(share * 100)}% full`);
649
+ const trigger = lifecycle.latestSnapshot?.thresholds.maxTarget ?? null;
650
+ // WHEN, not just how full. "68% full" only answers the second question a reader has
651
+ // if they already remember where the trigger sits; naming it answers both.
652
+ //
653
+ // NOT WHILE STOPPED, though: the line rendered "COMMIT DUE" and "FOLDING STOPPED"
654
+ // in one breath, promising the thing it was announcing would not happen.
655
+ if (share !== null && trigger !== null && !ladder.automaticFailure) {
656
+ parts.push(share >= trigger ? "COMMIT DUE" : `commit at ${Math.round(trigger * 100)}%`);
657
+ }
658
+ if (staged > 0) parts.push(`${staged} staged`);
659
+ if (roots > 0) parts.push(`${roots} fold${roots === 1 ? "" : "s"}`);
660
+ ctx.ui?.setStatus?.(entryTypePrefix, `${brandNoun} ${parts.join(" · ")}`);
644
661
  } catch { }
645
662
  };
646
663
 
@@ -779,6 +796,8 @@ export function registerActiveContext(pi: any, options: {
779
796
  instrumentation.ledger = emptyLedger();
780
797
  instrumentation.previousDigests = null;
781
798
  instrumentation.previousText = null;
799
+ instrumentation.previousImages = 0;
800
+ instrumentation.previousImageChars = 0;
782
801
  instrumentation.requests = 0;
783
802
  instrumentation.lastMutationRequest = 0;
784
803
  instrumentation.lastMutationTokens = null;
@@ -793,16 +812,11 @@ export function registerActiveContext(pi: any, options: {
793
812
  curation.lastRecovery = null;
794
813
  ladder.lastAutomaticAction = null;
795
814
  ladder.automaticFailure = null;
796
- advisory.hardFenceNoticeKey = null;
797
815
  measurements.providerMeasurementReceipts.clear();
798
816
  measurements.providerMeasurementRevisionByMessageSha.clear();
799
817
  measurements.providerMeasurementByMessageSha.clear();
800
818
  measurements.providerMeasurementAnchorByMessageSha.clear();
801
819
  const sessionId = ctx.sessionManager.getSessionId();
802
- if (advisory.hardFenceReleaseSessionId !== sessionId) {
803
- advisory.hardFenceReleaseSessionId = sessionId;
804
- advisory.hardFenceReleasedProjectionKeys.clear();
805
- }
806
820
  let restored: ActiveContextState | null = null;
807
821
  let restoreError: unknown = null;
808
822
  let restoredPersistence: MaterializedStatePersistence | null = null;
@@ -1318,53 +1332,6 @@ export function registerActiveContext(pi: any, options: {
1318
1332
  safeNotify(ctx, `Automatic context management suspended: ${message}`, "warning");
1319
1333
  };
1320
1334
 
1321
- const abortUnsafeHardContext = (
1322
- snapshot: ActiveContextSnapshot | null,
1323
- ctx: any,
1324
- allowUnmeasuredRevisionRelease = false,
1325
- ): boolean => {
1326
- if (measurements.latestRatio === null || measurements.latestRatio < hardFenceRatio(snapshot ?? undefined, ctx) || !persistence.state) return false;
1327
- const measuredRevision = measurements.lastProviderMeasurement
1328
- ? measurements.providerMeasurementRevisionByMessageSha.get(measurements.lastProviderMeasurement.messageSha256)
1329
- : undefined;
1330
- if (allowUnmeasuredRevisionRelease && !ladder.automaticFailure &&
1331
- measuredRevision !== undefined && measurements.lastProviderMeasurement &&
1332
- !durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
1333
- const releaseKey = sha256Value({
1334
- sessionId: persistence.state.sessionId,
1335
- topologySha256: topologySha256(persistence.state),
1336
- protectionSha256: protectionSha256(persistence.state),
1337
- measuredRevision,
1338
- providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
1339
- });
1340
- if (!advisory.hardFenceReleasedProjectionKeys.has(releaseKey) &&
1341
- advisory.hardFenceReleasedProjectionKeys.size < 4_096) {
1342
- advisory.hardFenceReleasedProjectionKeys.add(releaseKey);
1343
- return false;
1344
- }
1345
- }
1346
- const key = ladder.automaticFailure?.key ?? sha256Value({
1347
- sessionId: snapshot?.sessionId ?? persistence.state.sessionId,
1348
- revision: persistence.state.revision,
1349
- providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
1350
- phase: "hard-provider-fence",
1351
- });
1352
- ladder.pendingContextNote = `Provider context reached the hard ${brandNoun} fence without a newly committed lossless fold. ` +
1353
- "The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
1354
- if (advisory.hardFenceNoticeKey !== key) {
1355
- advisory.hardFenceNoticeKey = key;
1356
- safeNotify(
1357
- ctx,
1358
- "Provider request aborted at the hard context fence; run /compact or make an explicit bounded context fold.",
1359
- "error",
1360
- );
1361
- }
1362
- if (typeof ctx.abort !== "function") {
1363
- throw new Error(`Pi hard-fence abort capability is unavailable at ratio ${measurements.latestRatio}`);
1364
- }
1365
- ctx.abort();
1366
- return true;
1367
- };
1368
1335
 
1369
1336
  const PROJECTION_CALIBRATION_MIN_CHARS = 20_000;
1370
1337
  const PROJECTION_CALIBRATION_MIN_TOKENS = 5_000;
@@ -1385,6 +1352,12 @@ export function registerActiveContext(pi: any, options: {
1385
1352
  }
1386
1353
  const chars = measurements.lastProjectedChars;
1387
1354
  if (chars === null || chars < PROJECTION_CALIBRATION_MIN_CHARS) return;
1355
+ // NEVER LEARN THE RATIO FROM A WINDOW HOLDING AN IMAGE. chars/token is a fact about
1356
+ // prose; a projection carrying base64 has a far higher true ratio, and because
1357
+ // projectionCharsPerToken takes the MINIMUM over the window such a sample is either
1358
+ // discarded (leaving the over-read in place) or, once images dominate, poisons the
1359
+ // ratio for the text around them. The clean rule is to calibrate on text alone.
1360
+ if (measurements.lastProjectedImages > 0) return;
1388
1361
  if (!Number.isFinite(measurement.tokens) || measurement.tokens < PROJECTION_CALIBRATION_MIN_TOKENS) return;
1389
1362
  if (measurements.lastProjectedEstimate !== null && measurements.lastProjectedEstimateCalibrated &&
1390
1363
  measurement.tokens > 0) {
@@ -1418,13 +1391,26 @@ export function registerActiveContext(pi: any, options: {
1418
1391
  return;
1419
1392
  }
1420
1393
  const chars = bytes(text);
1421
- if (!(measurement.tokens > 0) || chars / measurement.tokens < PROJECTION_CHARS_PER_TOKEN_FLOOR) {
1394
+ // The anchor is differenced against TEXT characters, so it must store them. The mass
1395
+ // belongs to the same `previousText` this anchor is built from, recorded by
1396
+ // noteProjection when that text was produced.
1397
+ const textChars = Math.max(0, chars - instrumentation.previousImageChars);
1398
+ const images = instrumentation.previousImages;
1399
+ // The floor test reads TEXT against tokens: an image-heavy projection has a raw ratio
1400
+ // far above any prose floor and would sail through a test that is meant to reject a
1401
+ // measurement the text cannot explain.
1402
+ if (!(measurement.tokens > 0) ||
1403
+ (measurement.tokens > images * IMAGE_ESTIMATED_TOKENS &&
1404
+ textChars / (measurement.tokens - images * IMAGE_ESTIMATED_TOKENS) <
1405
+ PROJECTION_CHARS_PER_TOKEN_FLOOR)) {
1422
1406
  measurements.projectionAnchor = null;
1423
1407
  return;
1424
1408
  }
1425
1409
  measurements.projectionAnchor = {
1426
1410
  tokens: measurement.tokens,
1427
1411
  chars,
1412
+ textChars,
1413
+ images,
1428
1414
  head: text.slice(0, -1),
1429
1415
  messageSha256: measurement.messageSha256,
1430
1416
  sessionId: persistence.state.sessionId,
@@ -1446,6 +1432,9 @@ export function registerActiveContext(pi: any, options: {
1446
1432
  tokens: number;
1447
1433
  basis: ProjectionReadingBasis;
1448
1434
  chars: number;
1435
+ /** The image-corrected size of THIS projection, independent of any anchor. */
1436
+ sizeTokens: number;
1437
+ images: number;
1449
1438
  anchorTokens: number | null;
1450
1439
  deltaChars: number | null;
1451
1440
  text: string;
@@ -1453,21 +1442,34 @@ export function registerActiveContext(pi: any, options: {
1453
1442
  const text = serialized ?? stableStringify(projected);
1454
1443
  const chars = Buffer.byteLength(text, "utf8");
1455
1444
  const charsPerToken = projectionCharsPerToken();
1456
- const estimate = Math.ceil(chars / charsPerToken);
1445
+ // AN IMAGE IS NOT TEXT (2026-08-28, see imageMass). Its base64 leaves the character
1446
+ // count and comes back as a flat per-image cost, because dividing base64 by a ratio
1447
+ // learned from prose read one screenshot as 110,000 tokens against a true 1,500 and
1448
+ // aborted a session that was less than half full.
1449
+ const mass = imageMass(projected);
1450
+ const textChars = Math.max(0, chars - mass.base64Chars);
1451
+ const imageTokens = mass.images * IMAGE_ESTIMATED_TOKENS;
1452
+ const estimate = Math.ceil(textChars / charsPerToken) + imageTokens;
1457
1453
  const anchor = measurements.projectionAnchor;
1458
1454
  if (!anchor || anchor.generation !== lifecycle.generation ||
1459
1455
  anchor.sessionId !== persistence.state?.sessionId) {
1460
- return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null, text };
1456
+ return { tokens: estimate, basis: "unmeasured", chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
1461
1457
  }
1462
1458
  const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
1463
1459
  if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
1464
- return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null, text };
1460
+ return { tokens: estimate, basis: "rewritten", chars, sizeTokens: estimate, images: mass.images, anchorTokens: anchor.tokens, deltaChars: null, text };
1465
1461
  }
1466
- const deltaChars = chars - anchor.chars;
1462
+ // The delta is taken on TEXT characters and images separately, so an image arriving
1463
+ // after the anchor costs its own flat price rather than its base64 length.
1464
+ const deltaChars = textChars - anchor.textChars;
1465
+ const deltaImages = mass.images - anchor.images;
1467
1466
  return {
1468
- tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken)),
1467
+ tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken) +
1468
+ deltaImages * IMAGE_ESTIMATED_TOKENS),
1469
1469
  basis: "anchored",
1470
1470
  chars,
1471
+ sizeTokens: estimate,
1472
+ images: mass.images,
1471
1473
  anchorTokens: anchor.tokens,
1472
1474
  deltaChars,
1473
1475
  text,
@@ -1517,7 +1519,7 @@ export function registerActiveContext(pi: any, options: {
1517
1519
  : Number.POSITIVE_INFINITY;
1518
1520
  const reading = projectedTokenReading(projected, serialized);
1519
1521
  const tokens = reading.tokens;
1520
- const sizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
1522
+ const sizeTokens = reading.sizeTokens;
1521
1523
  const marginTokens = Number.isFinite(budgetTokens)
1522
1524
  ? projectionMarginTokens(tokens, capacity.window)
1523
1525
  : 0;
@@ -1533,40 +1535,6 @@ export function registerActiveContext(pi: any, options: {
1533
1535
  };
1534
1536
  };
1535
1537
 
1536
- const abortOverBudgetProjection = (
1537
- tokens: number,
1538
- budgetTokens: number,
1539
- ctx: any,
1540
- recoveryAttempts = 0,
1541
- ): void => {
1542
- const key = sha256Value({
1543
- sessionId: persistence.state?.sessionId ?? null,
1544
- revision: persistence.state?.revision ?? null,
1545
- tokens,
1546
- budgetTokens,
1547
- phase: "projection-budget-fence",
1548
- });
1549
- ladder.pendingContextNote =
1550
- `The ${brandNoun} projection estimates ${tokens} tokens against a ${budgetTokens}-token serving budget` +
1551
- (recoveryAttempts
1552
- ? `, after ${recoveryAttempts} recovery attempt(s) that folded everything foldable. The remaining ` +
1553
- "inflow does not fit at any folding depth, which is an impossibility rather than a recoverable state."
1554
- : ".") +
1555
- " The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
1556
- if (advisory.hardFenceNoticeKey !== key) {
1557
- advisory.hardFenceNoticeKey = key;
1558
- safeNotify(
1559
- ctx,
1560
- "Provider request aborted: the projection exceeds the serving budget. " +
1561
- "Run /compact or make an explicit bounded context fold.",
1562
- "error",
1563
- );
1564
- }
1565
- if (typeof ctx.abort !== "function") {
1566
- throw new Error(`Pi projection-budget abort capability is unavailable at ${tokens} estimated tokens`);
1567
- }
1568
- ctx.abort();
1569
- };
1570
1538
 
1571
1539
  // THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
1572
1540
  //
@@ -1731,41 +1699,6 @@ export function registerActiveContext(pi: any, options: {
1731
1699
  return projected;
1732
1700
  };
1733
1701
 
1734
- /**
1735
- * THE TABLE OF CONTENTS IS ONE COPY PER COMMIT (Shane, 2026-08-26). It is admitted
1736
- * once per frozen projection, exactly as the fold notice above, and that admission
1737
- * rule IS the refresh law: between commits the copy stays buried at its index, so a
1738
- * quiet pass moves no byte and costs no occupancy anchor, and the tool result of every
1739
- * `remember` carries the live key list in the meantime. A commit rewrites the
1740
- * projection, the freeze drops, the body filter strips the old copy, and the next pass
1741
- * seats a fresh one at the tail with the latest keys. Nothing here re-renders in
1742
- * place, because a carrier that moves on a quiet pass is the exact defect the fold
1743
- * notice's comment records.
1744
- */
1745
- const appendMemoryToc = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
1746
- if (!workingMemory) return projected;
1747
- if (!persistence.state) return projected;
1748
- const held = persistence.state.memory ?? [];
1749
- if (!held.length) return projected;
1750
- if (!carrierAdmitted("memory-toc")) return projected;
1751
- const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
1752
- const text = memoryTocText({
1753
- entries: entries.map((entry) => ({ key: entry.key, chars: entry.body.length })),
1754
- toolName,
1755
- brandNoun,
1756
- });
1757
- projected.push({
1758
- role: "custom",
1759
- customType: memoryTocProjectionType,
1760
- content: text,
1761
- display: false,
1762
- details: { source: activeContextSource(entryTypePrefix), ephemeral: true },
1763
- timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
1764
- ? ownValue(snapshot.messages.at(-1), "timestamp")
1765
- : 0,
1766
- });
1767
- return projected;
1768
- };
1769
1702
 
1770
1703
  const enforceBandTop = async (
1771
1704
  snapshot: ActiveContextSnapshot,
@@ -1805,10 +1738,10 @@ export function registerActiveContext(pi: any, options: {
1805
1738
  snapshot: ActiveContextSnapshot,
1806
1739
  projected: unknown[],
1807
1740
  ctx: any,
1808
- ): Promise<{ projected: unknown[]; aborted: boolean; text: string }> => {
1741
+ ): Promise<{ projected: unknown[]; text: string }> => {
1809
1742
  let measured = projectionExceedsBudget(projected, ctx);
1810
1743
  const rejected = curation.pendingRejection !== null;
1811
- if (!measured.crowded && !rejected) return { projected, aborted: false, text: measured.text };
1744
+ if (!measured.crowded && !rejected) return { projected, text: measured.text };
1812
1745
  const trigger = measured;
1813
1746
  let reduced = projected;
1814
1747
  let attempts = 0;
@@ -1912,11 +1845,24 @@ export function registerActiveContext(pi: any, options: {
1912
1845
  }));
1913
1846
  curation.pendingRejection = null;
1914
1847
  }
1915
- // `measured` is recomputed on `reduced` at every turn of the loop above, so its text is
1916
- // the serialization of the array both of these exits return.
1917
- if (!measured.over) return { projected: reduced, aborted: false, text: measured.text };
1918
- abortOverBudgetProjection(measured.tokens, measured.budgetTokens, ctx, attempts);
1919
- return { projected: reduced, aborted: true, text: measured.text };
1848
+ // THE RUNTIME COMMITS AND TRANSMITS; ONLY THE PROVIDER REFUSES (Shane, 2026-08-28).
1849
+ //
1850
+ // This used to abort the request when the projection still read over budget after the
1851
+ // loop. It was deleted because it let an ESTIMATE veto a request that ground truth
1852
+ // would have accepted: a live session read 220,883 tokens where the provider counted
1853
+ // 111,837, was refused before transmission, and could not be rescued, because the
1854
+ // commit before the refusal had applied 28 marks and the next applied 0. The advice it
1855
+ // printed, fold manually or run /compact, named the two things the mechanism had just
1856
+ // proven it could not do.
1857
+ //
1858
+ // maxTarget is a TRIGGER meaning "commit now", not a bound meaning "refuse to
1859
+ // proceed", and only the first is something an estimate can be right about. What
1860
+ // remains is the loop above, which commits everything it can reach; the request then
1861
+ // goes to the provider, and a genuine overflow is handled by the rejection path, which
1862
+ // branches back, folds and retries on the provider's own count rather than ours.
1863
+ // `measured` is recomputed on `reduced` at every turn of the loop, so its text is the
1864
+ // serialization of the array this returns.
1865
+ return { projected: reduced, text: measured.text };
1920
1866
  };
1921
1867
 
1922
1868
  const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
@@ -2071,7 +2017,7 @@ export function registerActiveContext(pi: any, options: {
2071
2017
  const customType = ownValue(message, "customType");
2072
2018
  return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
2073
2019
  customType !== receiptProjectionType && customType !== curationProjectionType &&
2074
- customType !== foldNoticeProjectionType && customType !== memoryTocProjectionType;
2020
+ customType !== foldNoticeProjectionType;
2075
2021
  });
2076
2022
  const withdrawn = withdrawConsumedEphemeralPeeks(body);
2077
2023
  if (withdrawn.size && freeze.body) {
@@ -2099,7 +2045,6 @@ export function registerActiveContext(pi: any, options: {
2099
2045
  freeze.bodyText = bodyText;
2100
2046
  appendReceipts(projected, snapshot);
2101
2047
  appendFoldNotice(projected, snapshot);
2102
- appendMemoryToc(projected, snapshot);
2103
2048
  return holdFrozen(projected);
2104
2049
  };
2105
2050
  const noteProjection = (projected: unknown[], serialized?: string): void => {
@@ -2888,11 +2833,6 @@ export function registerActiveContext(pi: any, options: {
2888
2833
  try { projected = projectWithAdvisory(snapshot); }
2889
2834
  catch (error) {
2890
2835
  suspendAutomatic(error, "projection", ctx);
2891
- abortUnsafeHardContext(snapshot, ctx);
2892
- return { messages: event.messages };
2893
- }
2894
- if (abortUnsafeHardContext(snapshot, ctx, true)) {
2895
- updateStatus(ctx);
2896
2836
  return { messages: event.messages };
2897
2837
  }
2898
2838
  // THE FRONTIER CUTS FIRST, and it costs the projection nothing to do so: a pending
@@ -2908,7 +2848,6 @@ export function registerActiveContext(pi: any, options: {
2908
2848
  try { projected = projectWithAdvisory(snapshot); }
2909
2849
  catch (error) {
2910
2850
  suspendAutomatic(error, "projection", ctx);
2911
- abortUnsafeHardContext(snapshot, ctx);
2912
2851
  return { messages: event.messages };
2913
2852
  }
2914
2853
  }
@@ -2916,14 +2855,13 @@ export function registerActiveContext(pi: any, options: {
2916
2855
  projected = budgeted.projected;
2917
2856
  const reading = projectedTokenReading(projected, budgeted.text);
2918
2857
  measurements.lastProjectedChars = reading.chars;
2858
+ measurements.lastProjectedImages = reading.images;
2919
2859
  measurements.lastProjectedEstimate = reading.tokens;
2920
2860
  measurements.lastProjectedEstimateBasis = reading.basis;
2921
- measurements.lastProjectedSizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
2861
+ // The size is the reading's own image-corrected figure. Recomputing it from raw
2862
+ // chars here is what fed base64 straight into the fence's own accounting.
2863
+ measurements.lastProjectedSizeTokens = reading.sizeTokens;
2922
2864
  measurements.lastProjectedEstimateCalibrated = measurements.projectionCalibrations.length > 0;
2923
- if (budgeted.aborted) {
2924
- updateStatus(ctx);
2925
- return { messages: projected };
2926
- }
2927
2865
  noteProjection(projected, budgeted.text);
2928
2866
  updateStatus(ctx);
2929
2867
  return { messages: projected };
@@ -2937,7 +2875,6 @@ export function registerActiveContext(pi: any, options: {
2937
2875
  if (!persistedSucceeded) ladder.boundaryFailure = lifecycle.latestSnapshotError;
2938
2876
  suspendAutomatic(error, persistedSucceeded ? "post-persist-projection" : "context", ctx);
2939
2877
  }
2940
- abortUnsafeHardContext(lifecycle.latestSnapshot, ctx);
2941
2878
  return { messages: event.messages };
2942
2879
  }
2943
2880
  };
@@ -4081,102 +4018,6 @@ export function registerActiveContext(pi: any, options: {
4081
4018
  "reaches it only when the commit folds this span. Nothing in your context moved.",
4082
4019
  });
4083
4020
  }
4084
- if (action === "remember") {
4085
- // THE DIGEST'S OWN CHANNEL. A working-memory write is durable state outside the
4086
- // projection, so like `brief` it moves no bytes and costs no prefix cache: the
4087
- // table of contents the projection carries refreshes at the next commit, and this
4088
- // result carries the live key list in the meantime.
4089
- const key = String(params.key ?? "").trim();
4090
- if (!key) throw new Error("remember needs a key");
4091
- if (key.length > MEMORY_KEY_CHARS) {
4092
- throw new Error(`Working-memory keys have a hard ${MEMORY_KEY_CHARS}-character ` +
4093
- "maximum: the key is a table-of-contents line, not the note. Put the detail " +
4094
- "in the body.");
4095
- }
4096
- const body = String(params.body ?? "").trim();
4097
- if (body.length > MEMORY_BODY_CHARS) {
4098
- throw new Error(`Working-memory bodies have a hard ${MEMORY_BODY_CHARS}-character ` +
4099
- `maximum and this one is ${body.length}. Split the entry, or keep the part ` +
4100
- "that changes and let the transcript carry the rest: folded material stays " +
4101
- "losslessly recoverable by peek.");
4102
- }
4103
- const held = persistence.state!.memory ?? [];
4104
- const existing = held.find((entry) => entry.key === key);
4105
- let nextMemory: WorkingMemoryEntry[];
4106
- if (!body) {
4107
- if (!existing) {
4108
- throw new Error(`No working-memory entry '${key}' to remove. ${held.length
4109
- ? `The keys are: ${held.map((entry) => entry.key).join(", ")}.`
4110
- : "Working memory is empty."}`);
4111
- }
4112
- nextMemory = held.filter((entry) => entry.key !== key);
4113
- } else {
4114
- if (!existing && held.length >= MEMORY_KEYS_MAX) {
4115
- throw new Error(`Working memory holds its maximum of ${MEMORY_KEYS_MAX} ` +
4116
- "entries. Merge two entries, or remove one whose facts no longer matter " +
4117
- `(${toolName} {"action":"remember","key":"<key>","body":""}), then write ` +
4118
- "this one.");
4119
- }
4120
- const ordinal = held.reduce((max, entry) => Math.max(max, entry.ordinal), 0) + 1;
4121
- nextMemory = existing
4122
- ? held.map((entry) => entry.key === key ? { key, body, ordinal } : entry)
4123
- : [...held, { key, body, ordinal }];
4124
- }
4125
- const nextState = {
4126
- ...persistence.state!,
4127
- revision: persistence.state!.revision + 1,
4128
- ...(nextMemory.length ? { memory: nextMemory } : {}),
4129
- };
4130
- if (!nextMemory.length) delete (nextState as Partial<ActiveContextState>).memory;
4131
- persistence.state = nextState;
4132
- await persistManual(nextState, ctx);
4133
- updateStatus(ctx);
4134
- const toc = [...nextMemory].sort((left, right) => right.ordinal - left.ordinal)
4135
- .map((entry) => ({ key: entry.key, chars: entry.body.length }));
4136
- return toolPayload({
4137
- version: 1,
4138
- action,
4139
- key,
4140
- ...(body ? { chars: body.length } : { removed: true }),
4141
- argumentsSha256: executionArgumentsSha256,
4142
- durableRevision: persistence.state!.revision,
4143
- entries: toc,
4144
- activation: body
4145
- ? "durable immediately, and it costs your window nothing: the body lives in " +
4146
- "state, this list is the live view, and the table of contents in your " +
4147
- "context refreshes at the next commit."
4148
- : "removed. The table of contents in your context refreshes at the next commit.",
4149
- });
4150
- }
4151
- if (action === "recall") {
4152
- const held = persistence.state!.memory ?? [];
4153
- if (!held.length) {
4154
- throw new Error(`Working memory is empty. Write an entry with ${toolName} ` +
4155
- '{"action":"remember","key":"<key>","body":"..."}.');
4156
- }
4157
- const requested = params.keys === undefined && params.key === undefined
4158
- ? null
4159
- : (denseOwnArrayValues(params.keys) ?? [params.key ?? params.keys])
4160
- .map((value) => String(value ?? "").trim()).filter(Boolean);
4161
- const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
4162
- const chosen = requested === null
4163
- ? entries
4164
- : entries.filter((entry) => requested.includes(entry.key));
4165
- if (requested !== null) {
4166
- const missing = requested.filter((key) => !held.some((entry) => entry.key === key));
4167
- if (missing.length) {
4168
- throw new Error(`No working-memory entr${missing.length === 1 ? "y" : "ies"} ` +
4169
- `${missing.join(", ")}. The keys are: ` +
4170
- `${entries.map((entry) => entry.key).join(", ")}.`);
4171
- }
4172
- }
4173
- return toolPayload({
4174
- version: 1,
4175
- action,
4176
- argumentsSha256: executionArgumentsSha256,
4177
- entries: chosen.map((entry) => ({ key: entry.key, chars: entry.body.length, body: entry.body })),
4178
- });
4179
- }
4180
4021
  throw new Error(`Unknown ${toolName} action '${action}'`);
4181
4022
  };
4182
4023
  const toolHandler = async (
@@ -4232,12 +4073,24 @@ export function registerActiveContext(pi: any, options: {
4232
4073
  try {
4233
4074
  const snapshot = authoritativeSnapshotFor(ctx);
4234
4075
  const status = activeContextStatus(snapshot, persistence.state, 0, 40);
4235
- safeNotify(
4236
- ctx,
4237
- `Active context: ${status.totalFolds} fold(s), roots ${(status.roots as string[]).join(", ") || "none"}. ` +
4238
- `Use ${toolName} status for exact recursive actions.`,
4239
- "info",
4240
- );
4076
+ const capacity = currentCapacity(ctx);
4077
+ const staged = pendingMarks(persistence.state);
4078
+ safeNotify(ctx, foldStatusText({
4079
+ brand: brandNoun,
4080
+ usedTokens: measurements.lastProviderMeasurement?.tokens ?? null,
4081
+ budgetTokens: capacity.budgetTokens,
4082
+ commitAtShare: snapshot.thresholds.maxTarget,
4083
+ aimShare: snapshot.thresholds.minTarget,
4084
+ roots: (status.roots as string[]).length,
4085
+ totalFolds: Number(status.totalFolds ?? 0),
4086
+ stagedMarks: staged.length,
4087
+ stagedTokens: staged.reduce((total, mark) =>
4088
+ total + estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)), 0),
4089
+ pinned: persistence.state.protected.length,
4090
+ suspended: ladder.automaticFailure?.message ?? null,
4091
+ foldCommand: `/${commandNames.fold}`,
4092
+ editorCommand: "/fold-editor",
4093
+ }), "info");
4241
4094
  } catch (error) {
4242
4095
  safeNotify(ctx, `Active-context status unavailable; native Pi context is unchanged: ${String(error)}`, "warning");
4243
4096
  }
@@ -4377,12 +4230,15 @@ export function registerActiveContext(pi: any, options: {
4377
4230
  : null;
4378
4231
  const accounting = markAccounting(snapshot, state);
4379
4232
  return {
4380
- title: `pi-fold context editor · ${snapshot.sessionId.slice(0, 8)}`,
4233
+ // NO SESSION ID. Eight hex characters of a value a person never types, on the
4234
+ // title line of the surface, is the same defect the rows below it just lost.
4235
+ title: `${brandNoun} window`,
4381
4236
  occupancy: {
4382
4237
  usedTokens: capacity.usedTokens,
4383
4238
  budgetTokens: capacity.budgetTokens,
4384
4239
  commitOccupancy: thresholds.maxTarget,
4385
4240
  commitDue: occupancy !== null && occupancy >= thresholds.maxTarget,
4241
+ suspended: Boolean(ladder.automaticFailure),
4386
4242
  },
4387
4243
  blocks: buildFoldEditorData(snapshot, state, {
4388
4244
  foldRows: () => (state.folds ?? []).map((fold: any) => {