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.
@@ -6,10 +6,12 @@ import {
6
6
  stableStringify,
7
7
  } from "./json.ts";
8
8
  import {
9
+ contentText,
9
10
  bytes,
10
11
  clone,
11
12
  emptyActiveContextState,
12
13
  ownValue,
14
+ serializedPrefixHolds,
13
15
  sessionEntryMessages,
14
16
  uniqueMessageDigestAnchor,
15
17
  } from "./lib/canonical.ts";
@@ -44,6 +46,7 @@ import {
44
46
  explicitProtectedMass,
45
47
  budgetOccupancy,
46
48
  hardFenceRatio,
49
+ imageMass,
47
50
  latestProviderContextMeasurement,
48
51
  orderedRoots,
49
52
  parseNativeCompactionCompletion,
@@ -70,6 +73,7 @@ import {
70
73
  deriveFoldParents,
71
74
  flattenFoldRefs,
72
75
  foldIdFor,
76
+ foldRecordRef,
73
77
  makeFoldRecordEntry,
74
78
  makeStateCheckpoint,
75
79
  makeStateDelta,
@@ -102,8 +106,10 @@ import {
102
106
  resolveThresholds,
103
107
  servingBudgetTokens,
104
108
  ESTIMATED_BYTES_PER_TOKEN,
109
+ IMAGE_ESTIMATED_TOKENS,
105
110
  entryTypeNamespace,
106
111
  MAX_FOLD_SPAN_CHARS,
112
+ DEFAULT_TOOL_FOLD_THRESHOLD,
107
113
  MAX_PINNED_SHARE,
108
114
  PEEK_DEFAULT_MAX_BYTES,
109
115
  peekIsEphemeral,
@@ -118,11 +124,12 @@ import type {
118
124
  ActiveContextState,
119
125
  ActiveContextThresholds,
120
126
  ActiveContextToolAction,
127
+ BriefProvenance,
121
128
  FoldCandidate,
122
129
  FoldKind,
123
130
  FoldRecordEntry,
124
- MarkOrigin,
125
131
  PreparedFold,
132
+ WorkingMemoryEntry,
126
133
  } from "./lib/policy.ts";
127
134
  import {
128
135
  absorbWedgeMarks,
@@ -131,6 +138,7 @@ import {
131
138
  import {
132
139
  contextReceipt,
133
140
  foldNoticeText,
141
+ foldStatusText,
134
142
  receiptBlockText,
135
143
  withReceipt,
136
144
  } from "./lib/curation.ts";
@@ -142,6 +150,7 @@ import {
142
150
  commitCoverage,
143
151
  windowClaims,
144
152
  commitPendingMarks,
153
+ toolClipAdditions,
145
154
  consolidationMarks,
146
155
  ephemeralPeekMarks,
147
156
  epochCommitDue,
@@ -149,6 +158,7 @@ import {
149
158
  foldMarkFor,
150
159
  ladderBrief,
151
160
  markAccounting,
161
+ markRewriteTokens,
152
162
  markFreedBytes,
153
163
  unmarkedRemainder,
154
164
  markClaimingRef,
@@ -211,38 +221,6 @@ export * from "./lib/scheduling.ts";
211
221
  export * from "./lib/selection.ts";
212
222
  export * from "./lib/transcript.ts";
213
223
 
214
- export interface BatchedMarkRequest {
215
- ids: string[];
216
- brief?: string;
217
- }
218
-
219
- export function batchedMarkRequests(params: Record<string, unknown>): BatchedMarkRequest[] {
220
- const batched = params.marks;
221
- if (batched === undefined) {
222
- return [{
223
- ids: stringIds(params.ids),
224
- ...(typeof params.brief === "string" && params.brief.trim() ? { brief: params.brief } : {}),
225
- }];
226
- }
227
- const values = denseOwnArrayValues(batched);
228
- if (!values || !values.length || values.length > 64) {
229
- throw new Error("marks must be a dense array of 1-64 {ids, brief} objects");
230
- }
231
- return values.map((value, index) => {
232
- if (!value || typeof value !== "object" || Array.isArray(value)) {
233
- throw new Error(`marks[${index}] must be an object with ids and an optional brief`);
234
- }
235
- const brief = ownValue(value, "brief");
236
- if (brief !== undefined && (typeof brief !== "string" || !brief.trim())) {
237
- throw new Error(`marks[${index}].brief must be a nonempty string`);
238
- }
239
- return {
240
- ids: stringIds(ownValue(value, "ids")),
241
- ...(typeof brief === "string" && brief.trim() ? { brief } : {}),
242
- };
243
- });
244
- }
245
-
246
224
  export function registerActiveContext(pi: any, options: {
247
225
  toolName?: string;
248
226
  toolLabel?: string;
@@ -252,6 +230,21 @@ export function registerActiveContext(pi: any, options: {
252
230
  blacklistAutoFoldTools?: ReadonlySet<string>;
253
231
  providerInputBudget?: number;
254
232
  thresholds?: ActiveContextThresholds;
233
+ /** THE TOOL-CALL DIET's share point (2026-08-24; default 0.50 since 2026-08-28, Shane,
234
+ * on the sol-20260826-full2 verdict): tool results in the oldest toolFoldThreshold
235
+ * share of the projected window are clipped IN VIEW at each commit, identified head
236
+ * kept, full bytes peek-recoverable behind the entry id the marker names. A share in
237
+ * [0, 1), where 0 turns the diet off; 0.50 is what every fold repetition from 3 onward
238
+ * ran, so the default is the value that was measured rather than the absence of one. */
239
+ toolFoldThreshold?: number;
240
+ /** THE INVITATION SWITCH (public since 2026-08-27; DEFAULT FALSE since 2026-08-28,
241
+ * Shane, on the sol-20260826-full2 verdict). false is the deterministic shape the
242
+ * campaign measured and the one it recommends: no carrier ever invites a brief and
243
+ * every fold goes out with the runtime's own words, which drew 14, 14, 9 and 8 correct
244
+ * against the invited condition's 3. true restores the standing invitation for the
245
+ * agent to improve a brief. Either way the agent verbs stay on the tool, so an agent
246
+ * that finds a fold can still annotate what it reads. */
247
+ postFoldNotice?: boolean;
255
248
  }): {
256
249
  projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
257
250
  } {
@@ -260,6 +253,19 @@ export function registerActiveContext(pi: any, options: {
260
253
  const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
261
254
  const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
262
255
  const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
256
+ if (options.postFoldNotice !== undefined && typeof options.postFoldNotice !== "boolean") {
257
+ throw new Error("postFoldNotice must be a boolean: true keeps the post-fold invitation " +
258
+ "notice, false ships every fold with the runtime's deterministic brief and no " +
259
+ "carrier inviting the agent to improve it");
260
+ }
261
+ const postFoldNotice = options.postFoldNotice ?? false;
262
+ const toolFoldThreshold = options.toolFoldThreshold ?? DEFAULT_TOOL_FOLD_THRESHOLD;
263
+ if (typeof toolFoldThreshold !== "number" || !Number.isFinite(toolFoldThreshold) ||
264
+ toolFoldThreshold < 0 || toolFoldThreshold >= 1) {
265
+ throw new Error("toolFoldThreshold must be a share in [0, 1): it names the oldest " +
266
+ "fraction of the projected window whose tool results are clipped in view at each " +
267
+ "commit, and 0 turns the diet off");
268
+ }
263
269
  for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
264
270
  if (Object.hasOwn(options, removed)) {
265
271
  throw new Error(`${removed} is no longer an option: epoch scheduling, peek foldability ` +
@@ -378,6 +384,7 @@ export function registerActiveContext(pi: any, options: {
378
384
  lastProviderMeasurement: null as ProviderContextMeasurement | null,
379
385
  descriptorWindow: null as number | null,
380
386
  lastProjectedChars: null as number | null,
387
+ lastProjectedImages: 0,
381
388
  projectionCalibrations: [] as Array<{ chars: number; tokens: number }>,
382
389
  lastProjectedEstimate: null as number | null,
383
390
  lastProjectedEstimateCalibrated: false,
@@ -386,6 +393,10 @@ export function registerActiveContext(pi: any, options: {
386
393
  projectionAnchor: null as {
387
394
  tokens: number;
388
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;
389
400
  head: string;
390
401
  messageSha256: string;
391
402
  sessionId: string;
@@ -405,6 +416,10 @@ export function registerActiveContext(pi: any, options: {
405
416
  ledger: emptyLedger(),
406
417
  previousDigests: null as string[] | null,
407
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,
408
423
  lastChange: "append" as ProjectionChange,
409
424
  lastPreservedShare: null as number | null,
410
425
  sinceHandoff: [] as Array<{
@@ -468,9 +483,6 @@ export function registerActiveContext(pi: any, options: {
468
483
  };
469
484
 
470
485
  const advisory = {
471
- hardFenceNoticeKey: null as string | null,
472
- hardFenceReleaseSessionId: null as string | null,
473
- hardFenceReleasedProjectionKeys: new Set<string>(),
474
486
  };
475
487
 
476
488
  // EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
@@ -573,10 +585,9 @@ export function registerActiveContext(pi: any, options: {
573
585
  };
574
586
 
575
587
  const PREFIX_MUTATING_KINDS: ReadonlySet<string> = new Set([
576
- "context.commit", "context.fold", "context.absorb", "context.split", "context.recovery",
588
+ "context.commit", "context.fold", "context.absorb", "context.recovery",
577
589
  ]);
578
590
  const CACHE_ACTION_REQUEST_CLASS: Readonly<Record<string, string>> = Object.freeze({
579
- fold: "after-mark",
580
591
  peek: "after-peek",
581
592
  expand: "after-expand",
582
593
  refold: "after-refold",
@@ -590,8 +601,6 @@ export function registerActiveContext(pi: any, options: {
590
601
  folds_committed: receipt.foldsCommitted,
591
602
  folds_created: receipt.foldsCreated,
592
603
  freed_tokens: receipt.freedTokens,
593
- split_folds: receipt.splitFolds,
594
- split_from_chars: receipt.splitFromChars,
595
604
  absorbed_wedges: receipt.absorbedWedges,
596
605
  recovered: receipt.recovered,
597
606
  protected_bytes: receipt.protectedBytes,
@@ -612,15 +621,43 @@ export function registerActiveContext(pi: any, options: {
612
621
  lifecycle.generation === expectedGeneration && persistence.state?.sessionId === sessionId &&
613
622
  (!ctx || contextSessionMatches(ctx, sessionId));
614
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
+ */
615
634
  const updateStatus = (ctx: any): void => {
616
635
  try {
617
636
  const roots = persistence.state && lifecycle.latestSnapshot ? orderedRoots(persistence.state, lifecycle.latestSnapshot).length : 0;
618
- const prepared = persistence.state?.prepared ? " · brief ready" : ladder.preparing ? " · briefing" : "";
619
- const usage = measurements.lastProviderMeasurement
620
- ? ` · provider ${measurements.lastProviderMeasurement.tokens}/${measurements.lastProviderMeasurement.contextWindow}`
621
- : " · provider usage unmeasured";
622
- const suspended = ladder.automaticFailure ? " · automatic suspended" : "";
623
- 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(" · ")}`);
624
661
  } catch { }
625
662
  };
626
663
 
@@ -759,6 +796,8 @@ export function registerActiveContext(pi: any, options: {
759
796
  instrumentation.ledger = emptyLedger();
760
797
  instrumentation.previousDigests = null;
761
798
  instrumentation.previousText = null;
799
+ instrumentation.previousImages = 0;
800
+ instrumentation.previousImageChars = 0;
762
801
  instrumentation.requests = 0;
763
802
  instrumentation.lastMutationRequest = 0;
764
803
  instrumentation.lastMutationTokens = null;
@@ -773,16 +812,11 @@ export function registerActiveContext(pi: any, options: {
773
812
  curation.lastRecovery = null;
774
813
  ladder.lastAutomaticAction = null;
775
814
  ladder.automaticFailure = null;
776
- advisory.hardFenceNoticeKey = null;
777
815
  measurements.providerMeasurementReceipts.clear();
778
816
  measurements.providerMeasurementRevisionByMessageSha.clear();
779
817
  measurements.providerMeasurementByMessageSha.clear();
780
818
  measurements.providerMeasurementAnchorByMessageSha.clear();
781
819
  const sessionId = ctx.sessionManager.getSessionId();
782
- if (advisory.hardFenceReleaseSessionId !== sessionId) {
783
- advisory.hardFenceReleaseSessionId = sessionId;
784
- advisory.hardFenceReleasedProjectionKeys.clear();
785
- }
786
820
  let restored: ActiveContextState | null = null;
787
821
  let restoreError: unknown = null;
788
822
  let restoredPersistence: MaterializedStatePersistence | null = null;
@@ -897,15 +931,28 @@ export function registerActiveContext(pi: any, options: {
897
931
  if (next.folds.length > MAX_ACTIVE_FOLD_RECORDS) {
898
932
  throw new Error("Active-context fold closure exceeds persistence limit");
899
933
  }
934
+ // AN ALREADY-DURABLE FOLD NEEDS A DIGEST, NOT A RECORD (2026-08-28). This built the
935
+ // whole entry for every fold on every persist just to compare one hash and continue:
936
+ // `makeFoldRecordEntry` canonicalizes, hashes, then `parseFoldRecordEntry` re-validates,
937
+ // clones the fold again, recomputes `foldIdFor`, hashes a second time and stringifies
938
+ // for the wire cap. Replaying a real sealed session, 98.2 percent of 4,947 calls hit
939
+ // the `continue`, and 126 of 135 persists appended nothing while rebuilding all of it.
940
+ // `foldRecordRef` is the same digest by construction (`makeFoldRecordEntry` stores
941
+ // `canonicalFoldRecord`, and `stateFromFoldRefs` already matches records to refs on
942
+ // this equality at persistence.ts:809); gate 152 pins it. Equal digests mean
943
+ // byte-identical canonical bytes to a record that already passed
944
+ // `parseFoldRecordEntry`, so skipping the rebuild cannot reach a different verdict.
945
+ // Measured 85.0ms to 19.6ms at 400 folds.
900
946
  for (const fold of next.folds) {
901
- const record = makeFoldRecordEntry(fold, sessionId);
902
- const existing = persistence.persistedFoldRecords.get(record.foldId);
947
+ const ref = foldRecordRef(fold);
948
+ const existing = persistence.persistedFoldRecords.get(ref.id);
903
949
  if (existing) {
904
- if (existing.recordSha256 !== record.recordSha256) {
905
- throw new Error(`Conflicting durable active-context fold ${record.foldId}`);
950
+ if (existing.recordSha256 !== ref.sha256) {
951
+ throw new Error(`Conflicting durable active-context fold ${ref.id}`);
906
952
  }
907
953
  continue;
908
954
  }
955
+ const record = makeFoldRecordEntry(fold, sessionId);
909
956
  await pi.appendEntry(foldRecordEntryType, record);
910
957
  persistence.persistedFoldRecords.set(record.foldId, record);
911
958
  if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
@@ -914,13 +961,20 @@ export function registerActiveContext(pi: any, options: {
914
961
  }
915
962
  }
916
963
  const wire = persistence.persistedWireVersion === 2 ? makeStateDelta(persistence.persisted, next) : makeStateCheckpoint(next);
917
- if (persistence.persistedWireVersion === 2 && persistence.persistedStateSha256 !== semanticStateSha256(persistence.persisted)) {
964
+ // THE WIRE CARRIES BOTH DIGESTS IT JUST DERIVED (2026-08-28). `makeStateDelta` writes
965
+ // `baseStateSha256` from `persistence.persisted` and `stateSha256` from `next`, and
966
+ // nothing mutates either state between that call and here, so re-hashing two whole
967
+ // states was pure repetition at about 7ms per durable write. The drift check keeps its
968
+ // teeth: `baseStateSha256` is still derived from `persistence.persisted`, so a
969
+ // persisted digest that has drifted from the state it claims to describe still raises.
970
+ // Gate 152 pins both equalities.
971
+ if (wire.kind === "delta" && persistence.persistedStateSha256 !== wire.baseStateSha256) {
918
972
  throw new Error("Active-context durable base digest drift");
919
973
  }
920
974
  await pi.appendEntry(stateEntryType, wire);
921
975
  persistence.persisted = clone(next);
922
976
  persistence.persistedWireVersion = 2;
923
- persistence.persistedStateSha256 = semanticStateSha256(next);
977
+ persistence.persistedStateSha256 = wire.stateSha256;
924
978
  persistence.state = lifecycle.shuttingDown ? null : clone(next);
925
979
  if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
926
980
  if (ctx && !contextSessionMatches(ctx, sessionId)) load(ctx);
@@ -1148,7 +1202,6 @@ export function registerActiveContext(pi: any, options: {
1148
1202
 
1149
1203
  const persistManual = async (
1150
1204
  next: ActiveContextState,
1151
- action: Exclude<ActiveContextToolAction, "status">,
1152
1205
  ctx: any,
1153
1206
  ): Promise<void> => {
1154
1207
  const stateAtEntry = persistence.state ? clone(persistence.state) : null;
@@ -1279,53 +1332,6 @@ export function registerActiveContext(pi: any, options: {
1279
1332
  safeNotify(ctx, `Automatic context management suspended: ${message}`, "warning");
1280
1333
  };
1281
1334
 
1282
- const abortUnsafeHardContext = (
1283
- snapshot: ActiveContextSnapshot | null,
1284
- ctx: any,
1285
- allowUnmeasuredRevisionRelease = false,
1286
- ): boolean => {
1287
- if (measurements.latestRatio === null || measurements.latestRatio < hardFenceRatio(snapshot ?? undefined, ctx) || !persistence.state) return false;
1288
- const measuredRevision = measurements.lastProviderMeasurement
1289
- ? measurements.providerMeasurementRevisionByMessageSha.get(measurements.lastProviderMeasurement.messageSha256)
1290
- : undefined;
1291
- if (allowUnmeasuredRevisionRelease && !ladder.automaticFailure &&
1292
- measuredRevision !== undefined && measurements.lastProviderMeasurement &&
1293
- !durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
1294
- const releaseKey = sha256Value({
1295
- sessionId: persistence.state.sessionId,
1296
- topologySha256: topologySha256(persistence.state),
1297
- protectionSha256: protectionSha256(persistence.state),
1298
- measuredRevision,
1299
- providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
1300
- });
1301
- if (!advisory.hardFenceReleasedProjectionKeys.has(releaseKey) &&
1302
- advisory.hardFenceReleasedProjectionKeys.size < 4_096) {
1303
- advisory.hardFenceReleasedProjectionKeys.add(releaseKey);
1304
- return false;
1305
- }
1306
- }
1307
- const key = ladder.automaticFailure?.key ?? sha256Value({
1308
- sessionId: snapshot?.sessionId ?? persistence.state.sessionId,
1309
- revision: persistence.state.revision,
1310
- providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
1311
- phase: "hard-provider-fence",
1312
- });
1313
- ladder.pendingContextNote = `Provider context reached the hard ${brandNoun} fence without a newly committed lossless fold. ` +
1314
- "The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
1315
- if (advisory.hardFenceNoticeKey !== key) {
1316
- advisory.hardFenceNoticeKey = key;
1317
- safeNotify(
1318
- ctx,
1319
- "Provider request aborted at the hard context fence; run /compact or make an explicit bounded context fold.",
1320
- "error",
1321
- );
1322
- }
1323
- if (typeof ctx.abort !== "function") {
1324
- throw new Error(`Pi hard-fence abort capability is unavailable at ratio ${measurements.latestRatio}`);
1325
- }
1326
- ctx.abort();
1327
- return true;
1328
- };
1329
1335
 
1330
1336
  const PROJECTION_CALIBRATION_MIN_CHARS = 20_000;
1331
1337
  const PROJECTION_CALIBRATION_MIN_TOKENS = 5_000;
@@ -1346,6 +1352,12 @@ export function registerActiveContext(pi: any, options: {
1346
1352
  }
1347
1353
  const chars = measurements.lastProjectedChars;
1348
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;
1349
1361
  if (!Number.isFinite(measurement.tokens) || measurement.tokens < PROJECTION_CALIBRATION_MIN_TOKENS) return;
1350
1362
  if (measurements.lastProjectedEstimate !== null && measurements.lastProjectedEstimateCalibrated &&
1351
1363
  measurement.tokens > 0) {
@@ -1379,13 +1391,26 @@ export function registerActiveContext(pi: any, options: {
1379
1391
  return;
1380
1392
  }
1381
1393
  const chars = bytes(text);
1382
- 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)) {
1383
1406
  measurements.projectionAnchor = null;
1384
1407
  return;
1385
1408
  }
1386
1409
  measurements.projectionAnchor = {
1387
1410
  tokens: measurement.tokens,
1388
1411
  chars,
1412
+ textChars,
1413
+ images,
1389
1414
  head: text.slice(0, -1),
1390
1415
  messageSha256: measurement.messageSha256,
1391
1416
  sessionId: persistence.state.sessionId,
@@ -1393,33 +1418,61 @@ export function registerActiveContext(pi: any, options: {
1393
1418
  };
1394
1419
  };
1395
1420
 
1396
- const projectedTokenReading = (projected: unknown[]): {
1421
+ // THE SERIALIZATION TRAVELS, THE READING DOES NOT (2026-08-28). Six whole-window
1422
+ // serializations rode every provider request and four were the same string from the same
1423
+ // unmutated array. Only the TEXT is threaded, never a finished reading: each site still
1424
+ // re-reads `measurements.projectionAnchor` and `projectionCharsPerToken()` at its own
1425
+ // time, so every emitted number stays what it was. That matters on the commit path, where
1426
+ // `enforceProjectionBudget` awaits `attemptAutomaticCommit` and an `attributionChanged`
1427
+ // can null the anchor between the last measurement and the read: threading a reading would
1428
+ // freeze `basis` at "measured" where today it flips to "unmeasured". Roughly 50ms per
1429
+ // request at a 467KB window, 100ms at 1MB, all of it blocking main-loop CPU. Honest
1430
+ // framing: six become three, not one.
1431
+ const projectedTokenReading = (projected: unknown[], serialized?: string): {
1397
1432
  tokens: number;
1398
1433
  basis: ProjectionReadingBasis;
1399
1434
  chars: number;
1435
+ /** The image-corrected size of THIS projection, independent of any anchor. */
1436
+ sizeTokens: number;
1437
+ images: number;
1400
1438
  anchorTokens: number | null;
1401
1439
  deltaChars: number | null;
1440
+ text: string;
1402
1441
  } => {
1403
- const text = stableStringify(projected);
1442
+ const text = serialized ?? stableStringify(projected);
1404
1443
  const chars = Buffer.byteLength(text, "utf8");
1405
1444
  const charsPerToken = projectionCharsPerToken();
1406
- 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;
1407
1453
  const anchor = measurements.projectionAnchor;
1408
1454
  if (!anchor || anchor.generation !== lifecycle.generation ||
1409
1455
  anchor.sessionId !== persistence.state?.sessionId) {
1410
- return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null };
1456
+ return { tokens: estimate, basis: "unmeasured", chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
1411
1457
  }
1412
1458
  const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
1413
1459
  if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
1414
- return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null };
1460
+ return { tokens: estimate, basis: "rewritten", chars, sizeTokens: estimate, images: mass.images, anchorTokens: anchor.tokens, deltaChars: null, text };
1415
1461
  }
1416
- 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;
1417
1466
  return {
1418
- 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),
1419
1469
  basis: "anchored",
1420
1470
  chars,
1471
+ sizeTokens: estimate,
1472
+ images: mass.images,
1421
1473
  anchorTokens: anchor.tokens,
1422
1474
  deltaChars,
1475
+ text,
1423
1476
  };
1424
1477
  };
1425
1478
 
@@ -1450,7 +1503,7 @@ export function registerActiveContext(pi: any, options: {
1450
1503
  estimatorErrorShare() * estimate + expectedInflowTokens(),
1451
1504
  ));
1452
1505
 
1453
- const projectionExceedsBudget = (projected: unknown[], ctx: any): {
1506
+ const projectionExceedsBudget = (projected: unknown[], ctx: any, serialized?: string): {
1454
1507
  tokens: number;
1455
1508
  sizeTokens: number;
1456
1509
  basis: ProjectionReadingBasis;
@@ -1458,14 +1511,15 @@ export function registerActiveContext(pi: any, options: {
1458
1511
  marginTokens: number;
1459
1512
  over: boolean;
1460
1513
  crowded: boolean;
1514
+ text: string;
1461
1515
  } => {
1462
1516
  const capacity = currentCapacity(ctx);
1463
1517
  const budgetTokens = Number.isFinite(capacity.budgetTokens) && capacity.budgetTokens > 0
1464
1518
  ? capacity.budgetTokens
1465
1519
  : Number.POSITIVE_INFINITY;
1466
- const reading = projectedTokenReading(projected);
1520
+ const reading = projectedTokenReading(projected, serialized);
1467
1521
  const tokens = reading.tokens;
1468
- const sizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
1522
+ const sizeTokens = reading.sizeTokens;
1469
1523
  const marginTokens = Number.isFinite(budgetTokens)
1470
1524
  ? projectionMarginTokens(tokens, capacity.window)
1471
1525
  : 0;
@@ -1477,43 +1531,10 @@ export function registerActiveContext(pi: any, options: {
1477
1531
  marginTokens,
1478
1532
  over: tokens > budgetTokens,
1479
1533
  crowded: tokens + marginTokens > budgetTokens,
1534
+ text: reading.text,
1480
1535
  };
1481
1536
  };
1482
1537
 
1483
- const abortOverBudgetProjection = (
1484
- tokens: number,
1485
- budgetTokens: number,
1486
- ctx: any,
1487
- recoveryAttempts = 0,
1488
- ): void => {
1489
- const key = sha256Value({
1490
- sessionId: persistence.state?.sessionId ?? null,
1491
- revision: persistence.state?.revision ?? null,
1492
- tokens,
1493
- budgetTokens,
1494
- phase: "projection-budget-fence",
1495
- });
1496
- ladder.pendingContextNote =
1497
- `The ${brandNoun} projection estimates ${tokens} tokens against a ${budgetTokens}-token serving budget` +
1498
- (recoveryAttempts
1499
- ? `, after ${recoveryAttempts} recovery attempt(s) that folded everything foldable. The remaining ` +
1500
- "inflow does not fit at any folding depth, which is an impossibility rather than a recoverable state."
1501
- : ".") +
1502
- " The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
1503
- if (advisory.hardFenceNoticeKey !== key) {
1504
- advisory.hardFenceNoticeKey = key;
1505
- safeNotify(
1506
- ctx,
1507
- "Provider request aborted: the projection exceeds the serving budget. " +
1508
- "Run /compact or make an explicit bounded context fold.",
1509
- "error",
1510
- );
1511
- }
1512
- if (typeof ctx.abort !== "function") {
1513
- throw new Error(`Pi projection-budget abort capability is unavailable at ${tokens} estimated tokens`);
1514
- }
1515
- ctx.abort();
1516
- };
1517
1538
 
1518
1539
  // THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
1519
1540
  //
@@ -1626,9 +1647,13 @@ export function registerActiveContext(pi: any, options: {
1626
1647
  * and it does not: the rebuild reads the marks, not its own last text.
1627
1648
  */
1628
1649
  const appendFoldNotice = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
1650
+ // The deterministic experiment condition: cuts stage and commits land exactly as
1651
+ // ever, but nothing invites a brief, so briefs stay the runtime's own.
1652
+ if (!postFoldNotice) return projected;
1629
1653
  if (!persistence.state) return projected;
1630
1654
  const held = pendingMarks(persistence.state);
1631
- const unbriefed = held.filter((mark) => mark.briefProvenance?.kind !== "supplied");
1655
+ const unbriefed = held.filter((mark) =>
1656
+ mark.briefProvenance?.kind !== "supplied" && mark.briefProvenance?.kind !== "augmented");
1632
1657
  // BATCHED, AND APPENDED ONCE. A notice per cut is noise, and noise is how guidance
1633
1658
  // gets ignored; the frontier cuts often and this speaks only when a batch has built
1634
1659
  // up. `carrierAdmitted` is what makes it an APPEND rather than a rewrite: once the
@@ -1649,6 +1674,7 @@ export function registerActiveContext(pi: any, options: {
1649
1674
  id: mark.id,
1650
1675
  kind: mark.kind,
1651
1676
  tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
1677
+ brief: typeof mark.brief === "string" ? mark.brief : undefined,
1652
1678
  })),
1653
1679
  pending: held.length,
1654
1680
  toolName,
@@ -1673,6 +1699,7 @@ export function registerActiveContext(pi: any, options: {
1673
1699
  return projected;
1674
1700
  };
1675
1701
 
1702
+
1676
1703
  const enforceBandTop = async (
1677
1704
  snapshot: ActiveContextSnapshot,
1678
1705
  ctx: any,
@@ -1711,10 +1738,10 @@ export function registerActiveContext(pi: any, options: {
1711
1738
  snapshot: ActiveContextSnapshot,
1712
1739
  projected: unknown[],
1713
1740
  ctx: any,
1714
- ): Promise<{ projected: unknown[]; aborted: boolean }> => {
1741
+ ): Promise<{ projected: unknown[]; text: string }> => {
1715
1742
  let measured = projectionExceedsBudget(projected, ctx);
1716
1743
  const rejected = curation.pendingRejection !== null;
1717
- if (!measured.crowded && !rejected) return { projected, aborted: false };
1744
+ if (!measured.crowded && !rejected) return { projected, text: measured.text };
1718
1745
  const trigger = measured;
1719
1746
  let reduced = projected;
1720
1747
  let attempts = 0;
@@ -1818,9 +1845,24 @@ export function registerActiveContext(pi: any, options: {
1818
1845
  }));
1819
1846
  curation.pendingRejection = null;
1820
1847
  }
1821
- if (!measured.over) return { projected: reduced, aborted: false };
1822
- abortOverBudgetProjection(measured.tokens, measured.budgetTokens, ctx, attempts);
1823
- return { projected: reduced, aborted: true };
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 };
1824
1866
  };
1825
1867
 
1826
1868
  const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
@@ -1984,25 +2026,34 @@ export function registerActiveContext(pi: any, options: {
1984
2026
  if (freeze.projection) carryWithdrawalIntoFreeze(freeze.projection, withdrawn);
1985
2027
  }
1986
2028
  const held = freeze.body?.length ?? 0;
1987
- freeze.active = freeze.projection !== null && body.length >= held &&
1988
- stableStringify(body.slice(0, held)) === freeze.bodyText;
2029
+ // THE PREFIX IS READ OUT OF THE WHOLE SERIALIZATION (2026-08-28). This asked
2030
+ // `stableStringify(body.slice(0, held)) === freeze.bodyText`, and in steady state `held`
2031
+ // is `body.length` minus the couple of messages that just arrived, so it was a second
2032
+ // full serialization of the window plus a slice allocation, beside the one below that
2033
+ // every pass does anyway. `serializedPrefixHolds` carries why that is sound; a false
2034
+ // POSITIVE here splices the previous pass's `freeze.projection` in front of the new tail
2035
+ // and emits stale messages, which is a content bug rather than a slowdown, and gate 154
2036
+ // drives the predicate itself rather than restating it.
2037
+ const bodyText = stableStringify(body);
2038
+ freeze.active = freeze.projection !== null &&
2039
+ serializedPrefixHolds(bodyText, freeze.bodyText, held, body.length);
1989
2040
  const projected = freeze.active
1990
2041
  ? [...freeze.projection!, ...body.slice(held)]
1991
2042
  : [...body];
1992
2043
  if (!freeze.active) freeze.keys.clear();
1993
2044
  freeze.body = body;
1994
- freeze.bodyText = stableStringify(body);
2045
+ freeze.bodyText = bodyText;
1995
2046
  appendReceipts(projected, snapshot);
1996
2047
  appendFoldNotice(projected, snapshot);
1997
2048
  return holdFrozen(projected);
1998
2049
  };
1999
- const noteProjection = (projected: unknown[]): void => {
2050
+ const noteProjection = (projected: unknown[], serialized?: string): void => {
2000
2051
  const digests = messageDigests(projected);
2001
2052
  const comparison = compareProjections(instrumentation.previousDigests, digests);
2002
2053
  recordProjection(instrumentation.ledger, comparison, digests);
2003
2054
  instrumentation.previousDigests = digests;
2004
2055
  instrumentation.lastChange = comparison.change;
2005
- const text = stableStringify(projected);
2056
+ const text = serialized ?? stableStringify(projected);
2006
2057
  const divergence = prefixDivergence(instrumentation.previousText, text);
2007
2058
  const previousChars = instrumentation.previousText?.length ?? 0;
2008
2059
  instrumentation.lastPreservedShare = previousChars > 0
@@ -2030,7 +2081,7 @@ export function registerActiveContext(pi: any, options: {
2030
2081
  : latestCacheActionClass
2031
2082
  ? latestCacheActionClass
2032
2083
  : divergence.index === null ? "steady-state" : "after-message";
2033
- const reading = projectedTokenReading(projected);
2084
+ const reading = projectedTokenReading(projected, text);
2034
2085
  emit("context.projection", {
2035
2086
  change: comparison.change,
2036
2087
  previous_count: comparison.previousCount,
@@ -2076,6 +2127,7 @@ export function registerActiveContext(pi: any, options: {
2076
2127
  snapshot: ActiveContextSnapshot;
2077
2128
  candidate: FoldCandidate;
2078
2129
  brief?: string;
2130
+ briefProvenance?: "supplied" | "deterministic" | BriefProvenance;
2079
2131
  ctx: any;
2080
2132
  signal?: AbortSignal;
2081
2133
  maximumSourceChars?: number;
@@ -2093,6 +2145,7 @@ export function registerActiveContext(pi: any, options: {
2093
2145
  state: baseState,
2094
2146
  generation: generationAtStart,
2095
2147
  brief: input.brief,
2148
+ briefProvenance: input.briefProvenance,
2096
2149
  ctx: input.ctx,
2097
2150
  signal: input.signal,
2098
2151
  });
@@ -2226,6 +2279,10 @@ export function registerActiveContext(pi: any, options: {
2226
2279
  });
2227
2280
  state = wedges.state;
2228
2281
  const accounting = markAccounting(snapshot, state);
2282
+ // Derived ONCE, here, against the pre-commit state both readers below report against.
2283
+ // It left `MarkAccounting` because it serializes the whole marked tail and every other
2284
+ // caller of `markAccounting` was paying for a number only this path reads.
2285
+ const rewriteTokens = markRewriteTokens(snapshot, state);
2229
2286
  if (!accounting.pending) {
2230
2287
  const remainder = unmarkedRemainder(snapshot, state, projectionCharsPerToken());
2231
2288
  emit("context.commit", {
@@ -2328,6 +2385,24 @@ export function registerActiveContext(pi: any, options: {
2328
2385
  !closed.applied.some((appliedMark) => appliedMark.id === mark.id)), ...closed.retained],
2329
2386
  };
2330
2387
  }
2388
+ // THE TOOL-CALL DIET RIDES THE SAME TRANSACTION (2026-08-24): clips are selected
2389
+ // against the post-fold state and land in the same revision the commit minted, so
2390
+ // the one paid rewrite carries both and no non-commit pass ever moves a byte. The
2391
+ // full bytes stay in the transcript behind each clip's entry id.
2392
+ let clippedResults = 0;
2393
+ if (toolFoldThreshold > 0) {
2394
+ const clipAdditions = toolClipAdditions({
2395
+ snapshot,
2396
+ state: result.state,
2397
+ threshold: toolFoldThreshold,
2398
+ blacklist: blacklistAutoFoldTools,
2399
+ });
2400
+ if (clipAdditions.length) {
2401
+ clippedResults = clipAdditions.length;
2402
+ result = { ...result, state: { ...result.state,
2403
+ clips: [...(result.state.clips ?? []), ...clipAdditions] } };
2404
+ }
2405
+ }
2331
2406
  persistence.state = result.state;
2332
2407
  const bytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
2333
2408
  const freedBytes = Math.max(0, bytesBefore - bytesAfter);
@@ -2362,9 +2437,10 @@ export function registerActiveContext(pi: any, options: {
2362
2437
  consolidation_marks: consolidationAdded,
2363
2438
  closing_consolidation_marks: closingAdded,
2364
2439
  absorbed_wedges: wedges.absorbed.length,
2440
+ clipped_results: clippedResults,
2365
2441
  freed_bytes: freedBytes,
2366
2442
  freed_tokens: estimatedTokens(freedBytes),
2367
- rewrite_tokens: accounting.rewriteTokens,
2443
+ rewrite_tokens: rewriteTokens,
2368
2444
  pinned_bytes: accounting.pinnedBytes,
2369
2445
  pinned_results: accounting.pinnedResults,
2370
2446
  protected_stale_bytes: pinHeld.bytes,
@@ -2436,7 +2512,7 @@ export function registerActiveContext(pi: any, options: {
2436
2512
  protectedStaleRefs: pinHeld.refs,
2437
2513
  retainedMarks: result.retained.length,
2438
2514
  eligibleMarks: accounting.eligibleMarks,
2439
- estimatedRewriteTokens: accounting.rewriteTokens,
2515
+ estimatedRewriteTokens: rewriteTokens,
2440
2516
  estimatedFreedTokens: accounting.freedTokens,
2441
2517
  freedBudgetShare: accounting.freedBudgetShare,
2442
2518
  sourceBytesSaved: freedBytes,
@@ -2486,8 +2562,6 @@ export function registerActiveContext(pi: any, options: {
2486
2562
  occupancyAfter: occupancyBefore === null ? null : Math.max(0, occupancyBefore - freedTokens),
2487
2563
  spansFolded: epoch ? Number(epoch.foldedSpans ?? 0) : foldIds,
2488
2564
  toolResultsFolded: epoch ? Number(epoch.foldedToolResults ?? 0) : 0,
2489
- splitFolds: Number(action.splitFolds ?? 0),
2490
- splitFromChars: Number(action.splitFromChars ?? 0),
2491
2565
  absorbedWedges: Number(epoch?.absorbedWedges ?? 0),
2492
2566
  recovered: curation.recoveryAttempts > 0,
2493
2567
  protectedBytes: Number(epoch?.protectedStaleBytes ?? 0),
@@ -2759,11 +2833,6 @@ export function registerActiveContext(pi: any, options: {
2759
2833
  try { projected = projectWithAdvisory(snapshot); }
2760
2834
  catch (error) {
2761
2835
  suspendAutomatic(error, "projection", ctx);
2762
- abortUnsafeHardContext(snapshot, ctx);
2763
- return { messages: event.messages };
2764
- }
2765
- if (abortUnsafeHardContext(snapshot, ctx, true)) {
2766
- updateStatus(ctx);
2767
2836
  return { messages: event.messages };
2768
2837
  }
2769
2838
  // THE FRONTIER CUTS FIRST, and it costs the projection nothing to do so: a pending
@@ -2779,23 +2848,21 @@ export function registerActiveContext(pi: any, options: {
2779
2848
  try { projected = projectWithAdvisory(snapshot); }
2780
2849
  catch (error) {
2781
2850
  suspendAutomatic(error, "projection", ctx);
2782
- abortUnsafeHardContext(snapshot, ctx);
2783
2851
  return { messages: event.messages };
2784
2852
  }
2785
2853
  }
2786
2854
  const budgeted = await enforceProjectionBudget(snapshot, projected, ctx);
2787
2855
  projected = budgeted.projected;
2788
- const reading = projectedTokenReading(projected);
2856
+ const reading = projectedTokenReading(projected, budgeted.text);
2789
2857
  measurements.lastProjectedChars = reading.chars;
2858
+ measurements.lastProjectedImages = reading.images;
2790
2859
  measurements.lastProjectedEstimate = reading.tokens;
2791
2860
  measurements.lastProjectedEstimateBasis = reading.basis;
2792
- 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;
2793
2864
  measurements.lastProjectedEstimateCalibrated = measurements.projectionCalibrations.length > 0;
2794
- if (budgeted.aborted) {
2795
- updateStatus(ctx);
2796
- return { messages: projected };
2797
- }
2798
- noteProjection(projected);
2865
+ noteProjection(projected, budgeted.text);
2799
2866
  updateStatus(ctx);
2800
2867
  return { messages: projected };
2801
2868
  } catch (error) {
@@ -2808,7 +2875,6 @@ export function registerActiveContext(pi: any, options: {
2808
2875
  if (!persistedSucceeded) ladder.boundaryFailure = lifecycle.latestSnapshotError;
2809
2876
  suspendAutomatic(error, persistedSucceeded ? "post-persist-projection" : "context", ctx);
2810
2877
  }
2811
- abortUnsafeHardContext(lifecycle.latestSnapshot, ctx);
2812
2878
  return { messages: event.messages };
2813
2879
  }
2814
2880
  };
@@ -3255,15 +3321,6 @@ export function registerActiveContext(pi: any, options: {
3255
3321
  );
3256
3322
  };
3257
3323
 
3258
- /**
3259
- * THE ONE STAGING PATH FOR FOLD MARKS (2026-08-23). The tool's fold action and the
3260
- * /fold-editor's user marks run THIS code and nothing else: span snapping, claim
3261
- * refusal, brief contract, prepareFold, replacement of a standing mark over the
3262
- * same span. Only the ORIGIN differs -- "agent" for the model, "user" for a human
3263
- * laying marks in the editor -- and origin changes nothing mechanical: a user mark
3264
- * counts toward the commit's coverage, answers with its commit arithmetic, and
3265
- * folds at the epoch exactly as an agent mark does.
3266
- */
3267
3324
  /**
3268
3325
  * THE WITHDRAWAL PATH, shared by the tool's unmark action and the editor's
3269
3326
  * withdraw intent: same refusal for an unknown id, same persist, same status.
@@ -3275,7 +3332,7 @@ export function registerActiveContext(pi: any, options: {
3275
3332
  const removed = before.filter((mark) => requested.has(mark.id));
3276
3333
  const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
3277
3334
  if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
3278
- await persistManual(withPendingMarks(persistence.state!, kept), "unmark", ctx);
3335
+ await persistManual(withPendingMarks(persistence.state!, kept), ctx);
3279
3336
  updateStatus(ctx);
3280
3337
  return removed as Array<Record<string, unknown>>;
3281
3338
  };
@@ -3305,7 +3362,7 @@ export function registerActiveContext(pi: any, options: {
3305
3362
  "folding keeps entries exactly recoverable without pinning them.");
3306
3363
  }
3307
3364
  }
3308
- await persistManual(nextProtection, pin ? "pin" : "unpin", ctx);
3365
+ await persistManual(nextProtection, ctx);
3309
3366
  emit("context.pin", {
3310
3367
  pin,
3311
3368
  ids: ids.join(","),
@@ -3316,140 +3373,140 @@ export function registerActiveContext(pi: any, options: {
3316
3373
  updateStatus(ctx);
3317
3374
  };
3318
3375
 
3319
- const stageFoldMarks = async (
3320
- requests: BatchedMarkRequest[],
3321
- origin: MarkOrigin,
3376
+ /**
3377
+ * THE ONE STAGING PATH FOR FOLD MARKS (2026-08-23). The /fold-editor's user marks
3378
+ * run THIS code and nothing else: span snapping, claim refusal, brief contract,
3379
+ * prepareFold, replacement of a standing mark over the same span. A user mark is
3380
+ * mechanically an ordinary mark: it counts toward the commit's coverage, answers
3381
+ * with its commit arithmetic, and folds at the epoch exactly as a runtime mark does.
3382
+ */
3383
+ const stageFoldMark = async (
3384
+ ids: string[],
3385
+ supplied: string | undefined,
3322
3386
  signal: AbortSignal,
3323
3387
  ctx: any,
3324
3388
  snapshot: ActiveContextSnapshot,
3325
- ): Promise<{ marks: Array<Record<string, unknown>>; corrections: SpanCorrection[]; staged: ActiveContextState }> => {
3326
- const resolved: Array<{
3327
- candidate: FoldCandidate;
3328
- brief?: string;
3329
- corrections: SpanCorrection[];
3330
- splitFrom: number;
3331
- }> = [];
3332
- for (const request of requests) {
3333
- const snapped = snapFoldCandidate(snapshot, persistence.state, request.ids, { allowProtected: true });
3334
- // AN AGENT'S SPAN IS TAKEN WHOLE (Shane, 2026-08-22). The bite-size cap was
3335
- // applied to manual marks too, and it was the only caller: the ladder never
3336
- // splits, because `selectAutomaticChapter` stops accumulating at the cap on its
3337
- // own. So the split existed solely to second-guess the one judgement the design
3338
- // wants to encourage, and it charged for it twice. One intended fold became five
3339
- // or ten placeholders, and every piece was handed the SAME brief, so each one
3340
- // claimed to describe a span it held a fraction of, while the correction text
3341
- // said "each with its own brief", which was never true.
3342
- //
3343
- // The cap stays for the ladder, where it was earned (2026-08-06 rep 6: one
3344
- // 60,432-byte chapter hid the fact the run needed). The difference is who chose
3345
- // the boundary. A ladder fold is a span nobody vouched for, so it is kept small
3346
- // enough to read back cheaply. An agent fold is a span the agent cut and wrote a
3347
- // brief for, and it stays navigable by a route that did not exist in 2026-08:
3348
- // peek takes offset and bytes, states the omitted middle, and reads any child id,
3349
- // so a large fold has a narrow read. An expand that will not fit is refused with
3350
- // a peek offered instead, so size costs a refusal rather than a window.
3351
- resolved.push({
3352
- candidate: snapped.candidate,
3353
- brief: request.brief,
3354
- corrections: snapped.corrections,
3355
- splitFrom: 0,
3356
- });
3357
- }
3358
- const corrections = resolved.flatMap((item) => item.corrections);
3359
- const marks: Array<Record<string, unknown>> = [];
3360
- let staged = persistence.state!;
3361
- for (const item of resolved) {
3362
- const { candidate } = item;
3363
- const alreadyPrepared = staged.folds.some((fold) =>
3364
- fold.id === foldIdFor(candidate.kind, candidate.parts));
3365
- const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
3366
- (candidate.kind === "tool-result" &&
3367
- refsProtected(candidate.sourceRefs, staged, snapshot));
3368
- // A MARK IS EDITABLE UNTIL IT COMMITS (Shane, 2026-08-21). Marking a span you
3369
- // already marked REPLACES the mark, so a brief written in haste is corrected by
3370
- // writing it again: the span is identical, the id derives from the span, and no
3371
- // byte has moved yet, so the correction costs nothing at all. After the commit
3372
- // the fold is standing and there is no rewrite verb, because rewriting a
3373
- // standing brief rewrites the projection at its placeholder; `reboundary`
3374
- // re-cuts the span instead, which was going to rewrite anyway.
3375
- const replacing = foldIdFor(candidate.kind, candidate.parts);
3376
- const claimed = markClaimingRef(staged, candidate.sourceRefs);
3377
- if (claimed && claimed.markId !== replacing) {
3378
- marks.push({
3379
- id: replacing,
3380
- kind: candidate.kind,
3381
- ok: false,
3382
- deferred: false,
3383
- reason: `Evidence ${claimed.entryId} is already held by pending mark ${claimed.markId}, ` +
3384
- `which folds at the next commit. Unmark ${claimed.markId} first if you want to ` +
3385
- "mark this span differently.",
3386
- });
3387
- continue;
3388
- }
3389
- const suppliedComplaint = item.brief === undefined
3390
- ? null
3391
- : briefContractComplaint(item.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
3392
- if (suppliedComplaint !== null) {
3393
- marks.push({
3394
- id: foldIdFor(candidate.kind, candidate.parts),
3395
- kind: candidate.kind,
3396
- ok: false,
3397
- deferred: false,
3398
- reason: `Supplied brief rejected. ${suppliedComplaint}`,
3399
- });
3400
- continue;
3401
- }
3402
- const briefed = !deferred && !alreadyPrepared
3403
- ? await prepareFold({
3404
- candidate,
3405
- snapshot,
3406
- state: staged,
3407
- generation: lifecycle.generation,
3408
- brief: item.brief,
3409
- ctx,
3410
- signal,
3411
- })
3412
- : null;
3413
- const mark = foldMarkFor({
3414
- candidate,
3415
- brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
3416
- briefProvenance: briefed?.fold.provenance ??
3417
- (item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
3418
- origin,
3419
- ordinal: markOrdinal(snapshot),
3420
- });
3421
- // Drop the standing mark over this exact span before adding, so a re-mark is a
3422
- // replacement rather than a duplicate refusal. `claimed` is this same mark by the
3423
- // check above, and the id is derived from the span, so the state after is the
3424
- // state a first mark with the new brief would have produced.
3425
- const replaced = claimed !== null;
3426
- if (replaced) {
3427
- staged = withPendingMarks(staged,
3428
- pendingMarks(staged).filter((standing) => standing.id !== replacing));
3429
- }
3430
- const addition = addPendingMark(staged, mark);
3431
- if (!addition.added) {
3432
- marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
3433
- continue;
3434
- }
3435
- staged = addition.state;
3436
- marks.push({
3437
- id: mark.id,
3438
- kind: mark.kind,
3439
- ok: true,
3440
- deferred,
3441
- ...(replaced ? { replaced: true } : {}),
3442
- ...(deferred
3443
- ? {
3444
- scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
3445
- "first commit after it ages out",
3446
- }
3447
- : {}),
3448
- brief: mark.brief,
3449
- provenance: normalizeLegacyProvenance(mark.briefProvenance),
3450
- });
3451
- }
3452
- return { marks, corrections, staged };
3389
+ ): Promise<{ mark: Record<string, unknown>; corrections: SpanCorrection[]; staged: ActiveContextState }> => {
3390
+ const snapped = snapFoldCandidate(snapshot, persistence.state, ids, { allowProtected: true });
3391
+ // AN AGENT'S SPAN IS TAKEN WHOLE (Shane, 2026-08-22). The bite-size cap was
3392
+ // applied to manual marks too, and it was the only caller: the ladder never
3393
+ // splits, because `selectAutomaticChapter` stops accumulating at the cap on its
3394
+ // own. So the split existed solely to second-guess the one judgement the design
3395
+ // wants to encourage, and it charged for it twice. One intended fold became five
3396
+ // or ten placeholders, and every piece was handed the SAME brief, so each one
3397
+ // claimed to describe a span it held a fraction of, while the correction text
3398
+ // said "each with its own brief", which was never true.
3399
+ //
3400
+ // The cap stays for the ladder, where it was earned (2026-08-06 rep 6: one
3401
+ // 60,432-byte chapter hid the fact the run needed). The difference is who chose
3402
+ // the boundary. A ladder fold is a span nobody vouched for, so it is kept small
3403
+ // enough to read back cheaply. An agent fold is a span the agent cut and wrote a
3404
+ // brief for, and it stays navigable by a route that did not exist in 2026-08:
3405
+ // peek takes offset and bytes, states the omitted middle, and reads any child id,
3406
+ // so a large fold has a narrow read. An expand that will not fit is refused with
3407
+ // a peek offered instead, so size costs a refusal rather than a window.
3408
+ const candidate = snapped.candidate;
3409
+ const corrections = snapped.corrections;
3410
+ let staged = persistence.state!;
3411
+ const alreadyPrepared = staged.folds.some((fold) =>
3412
+ fold.id === foldIdFor(candidate.kind, candidate.parts));
3413
+ const deferred = refsProtected(candidate.sourceRefs, staged, snapshot);
3414
+ // A MARK IS EDITABLE UNTIL IT COMMITS (Shane, 2026-08-21). Marking a span you
3415
+ // already marked REPLACES the mark, so a brief written in haste is corrected by
3416
+ // writing it again: the span is identical, the id derives from the span, and no
3417
+ // byte has moved yet, so the correction costs nothing at all. After the commit
3418
+ // the fold is standing and there is no rewrite verb, because rewriting a
3419
+ // standing brief rewrites the projection at its placeholder; `reboundary`
3420
+ // re-cuts the span instead, which was going to rewrite anyway.
3421
+ const replacing = foldIdFor(candidate.kind, candidate.parts);
3422
+ const claimed = markClaimingRef(staged, candidate.sourceRefs);
3423
+ if (claimed && claimed.markId !== replacing) {
3424
+ return {
3425
+ mark: {
3426
+ id: replacing,
3427
+ kind: candidate.kind,
3428
+ ok: false,
3429
+ deferred: false,
3430
+ reason: `Evidence ${claimed.entryId} is already held by pending mark ${claimed.markId}, ` +
3431
+ `which folds at the next commit. Unmark ${claimed.markId} first if you want to ` +
3432
+ "mark this span differently.",
3433
+ },
3434
+ corrections,
3435
+ staged,
3436
+ };
3437
+ }
3438
+ const suppliedComplaint = supplied === undefined
3439
+ ? null
3440
+ : briefContractComplaint(supplied.trim(), snapshot.policy.agentBriefReserve, snapshot.toolName);
3441
+ if (suppliedComplaint !== null) {
3442
+ return {
3443
+ mark: {
3444
+ id: replacing,
3445
+ kind: candidate.kind,
3446
+ ok: false,
3447
+ deferred: false,
3448
+ reason: `Supplied brief rejected. ${suppliedComplaint}`,
3449
+ },
3450
+ corrections,
3451
+ staged,
3452
+ };
3453
+ }
3454
+ const briefed = !deferred && !alreadyPrepared
3455
+ ? await prepareFold({
3456
+ candidate,
3457
+ snapshot,
3458
+ state: staged,
3459
+ generation: lifecycle.generation,
3460
+ brief: supplied,
3461
+ ctx,
3462
+ signal,
3463
+ })
3464
+ : null;
3465
+ const mark = foldMarkFor({
3466
+ candidate,
3467
+ brief: briefed?.fold.brief ?? supplied ?? ladderBrief(snapshot, staged, candidate),
3468
+ briefProvenance: briefed?.fold.provenance ??
3469
+ (supplied ? { kind: "supplied" } : { kind: "deterministic" }),
3470
+ origin: "user",
3471
+ ordinal: markOrdinal(snapshot),
3472
+ });
3473
+ // Drop the standing mark over this exact span before adding, so a re-mark is a
3474
+ // replacement rather than a duplicate refusal. `claimed` is this same mark by the
3475
+ // check above, and the id is derived from the span, so the state after is the
3476
+ // state a first mark with the new brief would have produced.
3477
+ const replaced = claimed !== null;
3478
+ if (replaced) {
3479
+ staged = withPendingMarks(staged,
3480
+ pendingMarks(staged).filter((standing) => standing.id !== replacing));
3481
+ }
3482
+ const addition = addPendingMark(staged, mark);
3483
+ if (!addition.added) {
3484
+ return {
3485
+ mark: { id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason },
3486
+ corrections,
3487
+ staged,
3488
+ };
3489
+ }
3490
+ staged = addition.state;
3491
+ return {
3492
+ mark: {
3493
+ id: mark.id,
3494
+ kind: mark.kind,
3495
+ ok: true,
3496
+ deferred,
3497
+ ...(replaced ? { replaced: true } : {}),
3498
+ ...(deferred
3499
+ ? {
3500
+ scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
3501
+ "first commit after it ages out",
3502
+ }
3503
+ : {}),
3504
+ brief: mark.brief,
3505
+ provenance: normalizeLegacyProvenance(mark.briefProvenance),
3506
+ },
3507
+ corrections,
3508
+ staged,
3509
+ };
3453
3510
  };
3454
3511
 
3455
3512
  const executeAction = async (
@@ -3584,7 +3641,8 @@ export function registerActiveContext(pi: any, options: {
3584
3641
  id: mark.id,
3585
3642
  kind: mark.kind,
3586
3643
  tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
3587
- briefed: mark.briefProvenance?.kind === "supplied",
3644
+ briefed: mark.briefProvenance?.kind === "supplied" ||
3645
+ mark.briefProvenance?.kind === "augmented",
3588
3646
  brief: mark.brief ?? null,
3589
3647
  briefProvenance: mark.briefProvenance ? clone(mark.briefProvenance) : null,
3590
3648
  })),
@@ -3631,7 +3689,41 @@ export function registerActiveContext(pi: any, options: {
3631
3689
  "bytes",
3632
3690
  );
3633
3691
  const target = persistence.state.folds.find((item) => item.id === id);
3634
- if (!target) throw new Error(`Unknown active-context fold ${id}`);
3692
+ // A CLIPPED RESULT ANSWERS TO ITS ENTRY ID (2026-08-24, the tool-call diet): the
3693
+ // clip marker names the entry, and the full bytes are in the transcript rather
3694
+ // than behind a fold, so the read serves them from the mapped message directly,
3695
+ // through the same admission fence and the same slice bounds as a fold peek.
3696
+ if (!target) {
3697
+ const clip = (persistence.state.clips ?? []).find((item) =>
3698
+ item.entryId === id || item.callId === id);
3699
+ if (clip) {
3700
+ const mapped = snapshot.mapped.find((item) => item.ref?.entryId === clip.entryId);
3701
+ if (!mapped) throw new Error(`Clipped result ${id} is no longer on this branch`);
3702
+ const full = contentText(snapshot.messages[mapped.index]);
3703
+ admit({
3704
+ action: "peek",
3705
+ ctx,
3706
+ foldId: clip.entryId,
3707
+ requestedBytes: Math.max(0, Math.min(sliceBytes, full.length - offset)),
3708
+ children: [],
3709
+ });
3710
+ const slice = full.slice(offset, offset + sliceBytes);
3711
+ return toolPayload({
3712
+ id: clip.entryId,
3713
+ source: slice,
3714
+ truncated: offset + slice.length < full.length,
3715
+ sourceChars: full.length,
3716
+ offset,
3717
+ ...(peekIsEphemeral(params) ? {
3718
+ ephemeral: "this result rides your context exactly until your next message; " +
3719
+ "extract what your current task needs into that reply, which takes over its " +
3720
+ "place. Peeking again is lossless and cheap; pass ephemeral false when you " +
3721
+ "need these bytes standing for several turns",
3722
+ } : { durable: "this result stays in your context like any other tool result" }),
3723
+ });
3724
+ }
3725
+ throw new Error(`Unknown active-context fold ${id}`);
3726
+ }
3635
3727
  admit({
3636
3728
  action: "peek",
3637
3729
  ctx,
@@ -3690,7 +3782,12 @@ export function registerActiveContext(pi: any, options: {
3690
3782
  for (const part of [recut]) {
3691
3783
  const durable = persistence.persistedFoldRecords.get(foldIdFor(part.kind, part.parts));
3692
3784
  const { preparedFold, nextState } = await prepareAndCommitExplicit({
3693
- snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied, ctx, signal,
3785
+ // A re-cut carries the durable fold's OWN brief and provenance: those words
3786
+ // were validated when they were first accepted, and re-judging them as a
3787
+ // fresh agent submission would refuse a deterministic or composed brief
3788
+ // against the agent's reserve.
3789
+ snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied,
3790
+ briefProvenance: durable ? durable.fold.provenance : undefined, ctx, signal,
3694
3791
  });
3695
3792
  persistence.state = nextState;
3696
3793
  if (durable && supplied) correctedBriefs[preparedFold.id] = supplied;
@@ -3706,7 +3803,7 @@ export function registerActiveContext(pi: any, options: {
3706
3803
  briefs: { ...(persistence.state.briefs ?? {}), ...correctedBriefs },
3707
3804
  };
3708
3805
  }
3709
- await persistManual(persistence.state, action, ctx);
3806
+ await persistManual(persistence.state, ctx);
3710
3807
  updateStatus(ctx);
3711
3808
  return toolPayload({
3712
3809
  version: 1,
@@ -3744,7 +3841,7 @@ export function registerActiveContext(pi: any, options: {
3744
3841
  if (!Object.keys(briefs).length) delete next.briefs;
3745
3842
  const leases = { ...next.leases };
3746
3843
  delete leases[id];
3747
- await persistManual({ ...next, leases }, action, ctx);
3844
+ await persistManual({ ...next, leases }, ctx);
3748
3845
  updateStatus(ctx);
3749
3846
  return toolPayload({
3750
3847
  version: 1,
@@ -3790,11 +3887,7 @@ export function registerActiveContext(pi: any, options: {
3790
3887
  delete leases[id];
3791
3888
  next = { ...next, leases };
3792
3889
  }
3793
- await persistManual(
3794
- next,
3795
- action,
3796
- ctx,
3797
- );
3890
+ await persistManual(next, ctx);
3798
3891
  updateStatus(ctx);
3799
3892
  return toolPayload({
3800
3893
  version: 1,
@@ -3890,7 +3983,7 @@ export function registerActiveContext(pi: any, options: {
3890
3983
  );
3891
3984
  }
3892
3985
  const text = String(params.brief ?? "").trim();
3893
- const complaint = briefContractComplaint(text, snapshot.policy.maxBriefChars, snapshot.toolName);
3986
+ const complaint = briefContractComplaint(text, snapshot.policy.agentBriefReserve, snapshot.toolName);
3894
3987
  if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
3895
3988
  const nextState = {
3896
3989
  ...persistence.state!,
@@ -3900,7 +3993,7 @@ export function registerActiveContext(pi: any, options: {
3900
3993
  : mark),
3901
3994
  };
3902
3995
  persistence.state = nextState;
3903
- await persistManual(nextState, action, ctx);
3996
+ await persistManual(nextState, ctx);
3904
3997
  updateStatus(ctx);
3905
3998
  emit("context.brief", { id, chars: text.length, origin: "agent" });
3906
3999
  const accounting = markAccounting(snapshot, persistence.state!);
@@ -3916,7 +4009,8 @@ export function registerActiveContext(pi: any, options: {
3916
4009
  durableRevision: persistence.state!.revision,
3917
4010
  pendingMarks: accounting.pending,
3918
4011
  unbriefed: pendingMarks(persistence.state!)
3919
- .filter((mark) => mark.briefProvenance?.kind !== "supplied").map((mark) => mark.id),
4012
+ .filter((mark) => mark.briefProvenance?.kind !== "supplied" &&
4013
+ mark.briefProvenance?.kind !== "augmented").map((mark) => mark.id),
3920
4014
  unmarkedSpans: remainder.spans,
3921
4015
  unmarkedTokens: remainder.tokens,
3922
4016
  activation: "durable immediately, and it costs your window nothing: this fold is " +
@@ -3926,12 +4020,6 @@ export function registerActiveContext(pi: any, options: {
3926
4020
  }
3927
4021
  throw new Error(`Unknown ${toolName} action '${action}'`);
3928
4022
  };
3929
- const requestedMarkCount = (params: Record<string, unknown>): number => {
3930
- const batched = denseOwnArrayValues(params?.marks);
3931
- if (batched) return batched.length;
3932
- return params?.ids === undefined && params?.id === undefined ? 0 : 1;
3933
- };
3934
-
3935
4023
  const toolHandler = async (
3936
4024
  toolCallId: string,
3937
4025
  params: Record<string, unknown>,
@@ -3948,7 +4036,6 @@ export function registerActiveContext(pi: any, options: {
3948
4036
  ok,
3949
4037
  error,
3950
4038
  tool_call_id: toolCallId,
3951
- marks_requested: requestedMarkCount(params),
3952
4039
  corrections_applied: corrections.length,
3953
4040
  arguments_sha256: sha256Value(params ?? {}),
3954
4041
  ...(action === "peek" ? { ephemeral: peekIsEphemeral(params) } : {}),
@@ -3986,12 +4073,24 @@ export function registerActiveContext(pi: any, options: {
3986
4073
  try {
3987
4074
  const snapshot = authoritativeSnapshotFor(ctx);
3988
4075
  const status = activeContextStatus(snapshot, persistence.state, 0, 40);
3989
- safeNotify(
3990
- ctx,
3991
- `Active context: ${status.totalFolds} fold(s), roots ${(status.roots as string[]).join(", ") || "none"}. ` +
3992
- `Use ${toolName} status for exact recursive actions.`,
3993
- "info",
3994
- );
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");
3995
4094
  } catch (error) {
3996
4095
  safeNotify(ctx, `Active-context status unavailable; native Pi context is unchanged: ${String(error)}`, "warning");
3997
4096
  }
@@ -4131,12 +4230,15 @@ export function registerActiveContext(pi: any, options: {
4131
4230
  : null;
4132
4231
  const accounting = markAccounting(snapshot, state);
4133
4232
  return {
4134
- 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`,
4135
4236
  occupancy: {
4136
4237
  usedTokens: capacity.usedTokens,
4137
4238
  budgetTokens: capacity.budgetTokens,
4138
4239
  commitOccupancy: thresholds.maxTarget,
4139
4240
  commitDue: occupancy !== null && occupancy >= thresholds.maxTarget,
4241
+ suspended: Boolean(ladder.automaticFailure),
4140
4242
  },
4141
4243
  blocks: buildFoldEditorData(snapshot, state, {
4142
4244
  foldRows: () => (state.folds ?? []).map((fold: any) => {
@@ -4234,20 +4336,13 @@ export function registerActiveContext(pi: any, options: {
4234
4336
  onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
4235
4337
  await runQueued(async () => {
4236
4338
  const fresh = authoritativeSnapshotFor(ctx);
4237
- const request = brief && brief.trim()
4238
- ? { ids: [fromId, toId], brief: brief.trim() }
4239
- : { ids: [fromId, toId] };
4240
- const { marks, staged } = await stageFoldMarks(
4241
- batchedMarkRequests(request), "user",
4339
+ const { mark, staged } = await stageFoldMark(
4340
+ [fromId, toId], brief && brief.trim() ? brief.trim() : undefined,
4242
4341
  new AbortController().signal, ctx, fresh);
4243
- // Refuse BEFORE persisting: an all-refused batch leaves state untouched and
4342
+ // Refuse BEFORE persisting: a refused mark leaves state untouched and
4244
4343
  // persisting it would be a no-op write against the same-state projection.
4245
- if (!marks.some((mark) => mark.ok === true)) {
4246
- const reason = marks.map((mark) => mark.reason).filter(Boolean).join("; ") ||
4247
- "the span was refused";
4248
- throw new Error(reason);
4249
- }
4250
- await persistManual(staged, "fold", ctx);
4344
+ if (mark.ok !== true) throw new Error(String(mark.reason ?? "the span was refused"));
4345
+ await persistManual(staged, ctx);
4251
4346
  updateStatus(ctx);
4252
4347
  });
4253
4348
  safeNotify(ctx, "Staged user mark(s); they apply at the next commit epoch.", "info");
@@ -4291,7 +4386,7 @@ export function registerActiveContext(pi: any, options: {
4291
4386
  label: toolLabel,
4292
4387
  allowedActions: allowedToolActions,
4293
4388
  fullSurface: true,
4294
- maxBriefChars: ACTIVE_CONTEXT_POLICY.maxBriefChars,
4389
+ maxBriefChars: ACTIVE_CONTEXT_POLICY.agentBriefReserve,
4295
4390
  statusDetails: ["fold_candidates", "tree", "folds", "objects"],
4296
4391
  minPeekSliceBytes: PEEK_MIN_SLICE_BYTES,
4297
4392
  defaultPeekBytes: PEEK_DEFAULT_MAX_BYTES,