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.
@@ -22,6 +22,9 @@ import {
22
22
  EXPAND_LEASE_GENERATIONS,
23
23
  MAX_EXPAND_LEASES,
24
24
  MAX_PENDING_MARKS,
25
+ MEMORY_BODY_CHARS,
26
+ MEMORY_KEY_CHARS,
27
+ MEMORY_KEYS_MAX,
25
28
  } from "./policy.ts";
26
29
  import type {
27
30
  ActiveContextCheckpointV2,
@@ -32,6 +35,7 @@ import type {
32
35
  AdvisoryMilestone,
33
36
  BriefOverride,
34
37
  BriefProvenance,
38
+ ToolClip,
35
39
  FoldKind,
36
40
  FoldPart,
37
41
  FoldRecordEntry,
@@ -167,7 +171,8 @@ export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Reco
167
171
 
168
172
  export function validProvenance(value: unknown): value is BriefProvenance {
169
173
  if (exactRecord(value, ["kind"]) &&
170
- (ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic")) return true;
174
+ (ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic" ||
175
+ ownValue(value, "kind") === "augmented")) return true;
171
176
  const hasDigest = Boolean(value && typeof value === "object" &&
172
177
  Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
173
178
  if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
@@ -323,6 +328,12 @@ export function parseActiveContextState(
323
328
  const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
324
329
  const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
325
330
  const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
331
+ const hasClips = recordLike && Object.prototype.hasOwnProperty.call(value, "clips");
332
+ // Read and dropped, never stored (2026-08-28): the working memory is deleted, and a
333
+ // sealed state that carries it must still load. Tolerating the key while discarding the
334
+ // value is the legacy-rider treatment, not the retired-field refusal: sol-20260826-full2
335
+ // reps 4 and 6 ran with it, and a campaign this package still cites has to stay readable.
336
+ const hasMemory = recordLike && Object.prototype.hasOwnProperty.call(value, "memory");
326
337
  // Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
327
338
  // to reproduce, so here the tolerance is the whole of it.
328
339
  const hasLegacyRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
@@ -335,6 +346,8 @@ export function parseActiveContextState(
335
346
  ...(hasPendingMarks ? ["pendingMarks"] : []),
336
347
  ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
337
348
  ...(hasBriefs ? ["briefs"] : []),
349
+ ...(hasClips ? ["clips"] : []),
350
+ ...(hasMemory ? ["memory"] : []),
338
351
  ...(hasLegacyRider ? ["rider"] : []),
339
352
  ];
340
353
  if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
@@ -366,6 +379,7 @@ export function parseActiveContextState(
366
379
  throw new Error("Invalid active-context pending marks");
367
380
  }
368
381
  const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
382
+ const clips = hasClips ? parseToolClips(ownValue(value, "clips")) : [];
369
383
  const source = clone(value) as unknown as ActiveContextState;
370
384
  return {
371
385
  version: 1,
@@ -380,6 +394,7 @@ export function parseActiveContextState(
380
394
  leases,
381
395
  ...(marks.length ? { pendingMarks: marks } : {}),
382
396
  ...(Object.keys(briefs).length ? { briefs } : {}),
397
+ ...(clips.length ? { clips } : {}),
383
398
  ...(hasAdvisory
384
399
  ? { advisory: clone(source.advisory!) }
385
400
  : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
@@ -387,6 +402,23 @@ export function parseActiveContextState(
387
402
  };
388
403
  }
389
404
 
405
+ function parseToolClips(value: unknown): ToolClip[] {
406
+ const entries = denseOwnArrayValues(value);
407
+ if (!entries) throw new Error("Invalid active-context clips");
408
+ const clips = entries.map((entry) => {
409
+ if (!exactRecord(entry, ["callId", "entryId"]) ||
410
+ typeof ownValue(entry, "callId") !== "string" || !ownValue(entry, "callId") ||
411
+ typeof ownValue(entry, "entryId") !== "string" || !ownValue(entry, "entryId")) {
412
+ throw new Error("Invalid active-context clip entry");
413
+ }
414
+ return { callId: String(ownValue(entry, "callId")), entryId: String(ownValue(entry, "entryId")) };
415
+ });
416
+ if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
417
+ throw new Error("Invalid active-context clips: duplicate call id");
418
+ }
419
+ return clips;
420
+ }
421
+
390
422
  export function clearPrepared(state: ActiveContextState): ActiveContextState {
391
423
  const next = { ...state };
392
424
  delete next.prepared;
@@ -412,8 +444,24 @@ export function validatePreparedShape(value: unknown): asserts value is Prepared
412
444
  }
413
445
  }
414
446
 
447
+ // THE SPREAD IS THE COPY (2026-08-28). Roughly six calls per fold per persist, and only
448
+ // `deriveFoldParents` wants a record it can write to, where it writes the TOP-LEVEL
449
+ // `parentId` that the spread already gives it a fresh slot for. Every other caller hashes
450
+ // and discards: `foldRecordRef`, `sameFoldRecordIdentity`, and `makeFoldRecordEntry`, whose
451
+ // `parseFoldRecordEntry` deep-clones twice on its own.
452
+ //
453
+ // The digest cannot move. `safeJson` emits in `Reflect.ownKeys` order, a spread preserves
454
+ // insertion order, and overwriting an existing key keeps it in its original position, so
455
+ // `{ ...fold, parentId: null }` serializes to the same bytes as the cloned form.
456
+ //
457
+ // THE COST, STATED BECAUSE IT IS NOW LOAD-BEARING: the returned record shares every nested
458
+ // object with its input, so "no caller mutates a canonical record's nested fields" is a rule
459
+ // this function relies on rather than enforces. It holds today (`validateFoldForest` clones
460
+ // at :281, `parseFoldRecordEntry` twice at :567-568, the hashing callers never write), and a
461
+ // future caller that writes through `record.parts` would corrupt the fold it was built from.
462
+ // Write `{ ...record, field: value }` rather than assigning into a nested one.
415
463
  export function canonicalFoldRecord(fold: ActiveFold): ActiveFold {
416
- return { ...clone(fold), parentId: null };
464
+ return { ...fold, parentId: null };
417
465
  }
418
466
 
419
467
  export function sameFoldRecordIdentity(left: ActiveFold, right: ActiveFold): boolean {
@@ -475,11 +523,17 @@ export function legacyReplayOrderStateSha256(state: ActiveContextState): string
475
523
 
476
524
  export function sameStateProjection(left: ActiveContextState, right: ActiveContextState): boolean {
477
525
  const normalized = (value: ActiveContextState): Partial<ActiveContextState> => {
478
- const normalizedState = { ...clone(value), revision: 0 } as Partial<ActiveContextState>;
526
+ // A SHALLOW COPY IS ENOUGH TO DELETE TOP-LEVEL KEYS (2026-08-28). `delete` on a shallow
527
+ // copy cannot reach the original, the normalized object never escapes this function, and
528
+ // the runtime already stableStringifies live un-cloned state (`semanticStateSha256`), so
529
+ // un-cloned state is already required to be serializer-safe. Measured 97.6ms to 82.6ms
530
+ // on the largest real state in the corpus, at about two calls per provider request.
531
+ const normalizedState = { ...value, revision: 0 } as Partial<ActiveContextState>;
479
532
  if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
480
533
  if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
481
534
  if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
482
535
  if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
536
+ if (normalizedState.clips && normalizedState.clips.length === 0) delete normalizedState.clips;
483
537
  return normalizedState;
484
538
  };
485
539
  return stableStringify(normalized(left)) === stableStringify(normalized(right));
@@ -586,6 +640,14 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
586
640
  Object.prototype.hasOwnProperty.call(value, "addBriefs"));
587
641
  const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
588
642
  Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
643
+ const hasWireClips = Boolean(value && typeof value === "object" &&
644
+ Object.prototype.hasOwnProperty.call(value, "clips"));
645
+ const hasClipBase = Boolean(value && typeof value === "object" &&
646
+ Object.prototype.hasOwnProperty.call(value, "clipBase"));
647
+ const hasAddClips = Boolean(value && typeof value === "object" &&
648
+ Object.prototype.hasOwnProperty.call(value, "addClips"));
649
+ const hasWireMemory = Boolean(value && typeof value === "object" &&
650
+ Object.prototype.hasOwnProperty.call(value, "memory"));
589
651
  const hasLegacyRider = Boolean(value && typeof value === "object" &&
590
652
  Object.prototype.hasOwnProperty.call(value, "rider"));
591
653
  refuseRetiredStateFields(value);
@@ -600,6 +662,10 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
600
662
  ...(hasBriefs ? ["briefs"] : []),
601
663
  ...(hasAddBriefs ? ["addBriefs"] : []),
602
664
  ...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
665
+ ...(hasWireClips ? ["clips"] : []),
666
+ ...(hasClipBase ? ["clipBase"] : []),
667
+ ...(hasAddClips ? ["addClips"] : []),
668
+ ...(hasWireMemory ? ["memory"] : []),
603
669
  // Carried no further than the digest check in `materializeStatePersistence`, which is
604
670
  // the only thing on a load that the rider's absence can change.
605
671
  ...(hasLegacyRider ? ["rider"] : []),
@@ -619,6 +685,17 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
619
685
  ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
620
686
  ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
621
687
  );
688
+ if (hasWireClips) parseToolClips(ownValue(value, "clips"));
689
+ if (hasAddClips) parseToolClips(ownValue(value, "addClips"));
690
+ if (hasClipBase) {
691
+ const base = ownValue(value, "clipBase");
692
+ if (!Number.isSafeInteger(base) || Number(base) < 0) throw new Error("Invalid active-context clip base");
693
+ }
694
+ // A whole array and a change cannot both be the truth on one wire.
695
+ if (hasWireClips && (hasClipBase || hasAddClips)) {
696
+ throw new Error("Invalid active-context clips: whole array beside a change");
697
+ }
698
+ if (hasAddClips && !hasClipBase) throw new Error("Invalid active-context clips: change without a base");
622
699
  if (checkpoint) {
623
700
  const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
624
701
  if (!refs || refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Invalid active-context checkpoint refs");
@@ -693,7 +770,25 @@ export function normalizeFoldsForPersistedRecords(
693
770
  return deriveFoldParents(normalized);
694
771
  }
695
772
 
696
- export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
773
+ /**
774
+ * Rebuild the parent links a fold forest's records do not carry.
775
+ *
776
+ * THIS DOES NOT VALIDATE THE FOREST, and the split is deliberate. `validateFoldForest` deep
777
+ * clones the forest, recomputes `foldIdFor` per fold, and calls `flattenFoldRefs` per fold,
778
+ * which rebuilds `foldMap` over every fold; two of the four callers handed the result
779
+ * straight to `parseActiveContextState`, whose FIRST act is to validate it again. So a
780
+ * persist validated the same forest three times. Roughly 12ms saved at 88 folds, 28 at 178,
781
+ * 55 at 267, plus about 1.6x on the session-start load path.
782
+ *
783
+ * The child-record check above stays here, because it is about the ASSIGNMENT rather than
784
+ * the forest: a child claimed twice, or claimed by a parent that does not hold it, is a
785
+ * contradiction this loop is the only thing positioned to see.
786
+ *
787
+ * Callers that do not parse immediately afterwards want `deriveFoldParents` below. Reaching
788
+ * for this one is a claim that the very next statement validates the same array; nothing
789
+ * else makes it safe, and it reads at review as removing validation, which it is not.
790
+ */
791
+ export function assignFoldParents(records: ActiveFold[]): ActiveFold[] {
697
792
  const folds = records.map(canonicalFoldRecord);
698
793
  const byId = new Map(folds.map((fold) => [fold.id, fold]));
699
794
  const assigned = new Set<string>();
@@ -705,14 +800,19 @@ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
705
800
  assigned.add(childId);
706
801
  }
707
802
  }
708
- return validateFoldForest(folds);
803
+ return folds;
804
+ }
805
+
806
+ /** Assign the parents and validate the forest, for callers that do not parse next. */
807
+ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
808
+ return validateFoldForest(assignFoldParents(records));
709
809
  }
710
810
 
711
811
  export function stateFromFoldRefs(
712
812
  wire: Pick<
713
813
  ActiveContextCheckpointV2,
714
814
  "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
715
- "tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs"
815
+ "tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips"
716
816
  >,
717
817
  refs: FoldRecordRef[],
718
818
  records: Map<string, FoldRecordEntry>,
@@ -727,13 +827,16 @@ export function stateFromFoldRefs(
727
827
  version: 1,
728
828
  sessionId: wire.sessionId,
729
829
  revision: wire.revision,
730
- folds: deriveFoldParents(folds),
830
+ // `parseActiveContextState` at the end of this function validates the forest, so
831
+ // validating it here as well was the same walk twice on one persist.
832
+ folds: assignFoldParents(folds),
731
833
  expanded: clone(wire.expanded),
732
834
  protected: clone(wire.protected),
733
835
  tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
734
836
  leases: clone(wire.leases ?? {}),
735
837
  ...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
736
838
  ...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
839
+ ...(wire.clips?.length ? { clips: clone(wire.clips) } : {}),
737
840
  ...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
738
841
  ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
739
842
  };
@@ -869,7 +972,16 @@ export function materializeStatePersistence(
869
972
  wire.revision <= wire.baseRevision) {
870
973
  throw new Error("Broken active-context delta chain");
871
974
  }
872
- const refs = state.folds.map(foldRecordRef);
975
+ // THE RECORD MAP ALREADY HOLDS THIS DIGEST (2026-08-28). `foldRecordRef` canonicalizes
976
+ // and hashes a whole fold, and this ran once per delta entry over every standing fold:
977
+ // 48,715 calls in ONE load of a real sealed session, 2.6 to 3.7s of a 24 to 33s load.
978
+ // `records` is built by the same replay and carries the same digest verbatim. The
979
+ // fallback is unreachable, because the pre-scan raises on a delta before a checkpoint;
980
+ // it is kept so the change is exactly behaviour-preserving rather than nearly so.
981
+ const refs = state.folds.map((fold) => {
982
+ const record = records.get(fold.id);
983
+ return record ? { id: fold.id, sha256: record.recordSha256 } : foldRecordRef(fold);
984
+ });
873
985
  const byId = new Map(refs.map((ref) => [ref.id, ref]));
874
986
  for (const id of wire.removeFoldIds) {
875
987
  if (!byId.delete(id)) throw new Error(`Unknown active-context delta removal ${id}`);
@@ -925,11 +1037,25 @@ export function materializeStatePersistence(
925
1037
  return mark;
926
1038
  });
927
1039
  }
1040
+ // The clips replay by the same discriminator rule as the briefs and the marks: the
1041
+ // presence of the CHANGE is what says a wire carries one. A delta written before
1042
+ // 2026-08-24 states the whole array when it has one and states nothing when it does
1043
+ // not, so "no array" means "no clips" there, exactly as it does for the marks.
1044
+ const clips = wire.clipBase === undefined
1045
+ ? (wire.clips !== undefined ? clone(wire.clips) : [])
1046
+ : [...(state.clips ?? []).slice(0, wire.clipBase), ...clone(wire.addClips ?? [])];
1047
+ if (wire.clipBase !== undefined && wire.clipBase > (state.clips ?? []).length) {
1048
+ throw new Error(`Unknown active-context clip base ${wire.clipBase}`);
1049
+ }
1050
+ if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
1051
+ throw new Error("Invalid active-context clips: duplicate call id");
1052
+ }
928
1053
  state = stateFromFoldRefs(
929
1054
  {
930
1055
  ...wire,
931
1056
  briefs: Object.keys(briefs).length ? briefs : undefined,
932
1057
  pendingMarks: marks.length ? marks : undefined,
1058
+ clips: clips.length ? clips : undefined,
933
1059
  },
934
1060
  [...byId.values()],
935
1061
  records,
@@ -989,6 +1115,7 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
989
1115
  leases: clone(state.leases),
990
1116
  ...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
991
1117
  ...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
1118
+ ...(state.clips?.length ? { clips: clone(state.clips) } : {}),
992
1119
  ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
993
1120
  stateSha256: semanticStateSha256(state),
994
1121
  }, state.sessionId) as ActiveContextCheckpointV2;
@@ -1036,6 +1163,23 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
1036
1163
  return !held || sha256Value(held) !== sha256Value(mark);
1037
1164
  });
1038
1165
  const marksTravel = previousMarks.length > 0 || nextMarks.length > 0;
1166
+ // THE CLIPS ARE THE SAME RULE A THIRD TIME (2026-08-24). They shipped whole on every
1167
+ // delta, which was 50.1 percent of all state bytes on sol-20260823-live rep 11, 472,750
1168
+ // of 942,902, for an array that never held more than 34 entries: the same defect the two
1169
+ // comments above were written about, arriving with a feature that did not inherit their
1170
+ // encoding. Clips are APPENDED and never edited, so the change is a count of what the
1171
+ // base already holds plus the ones that are new, and a rollback that shortens the array
1172
+ // states a lower count. The marks' order list would be worse than the bug here: a callId
1173
+ // is about a hundred characters, so re-listing them all on every delta costs more than
1174
+ // the array it replaces.
1175
+ const previousClips = previous.clips ?? [];
1176
+ const nextClips = next.clips ?? [];
1177
+ let clipBase = 0;
1178
+ while (clipBase < previousClips.length && clipBase < nextClips.length &&
1179
+ previousClips[clipBase].callId === nextClips[clipBase].callId &&
1180
+ previousClips[clipBase].entryId === nextClips[clipBase].entryId) clipBase += 1;
1181
+ const addClips = nextClips.slice(clipBase);
1182
+ const clipsTravel = previousClips.length > 0 || nextClips.length > 0;
1039
1183
  // The rest travel whole and stay that way. Measured on the same run, the two that were
1040
1184
  // worth a diff were 20.0 MB of the 21.6 MB ledger; what remained were bounded objects
1041
1185
  // with their own caps rather than collections that grow with the epoch (the largest,
@@ -1061,13 +1205,41 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
1061
1205
  ...(addPendingMarks.length ? { addPendingMarks: clone(addPendingMarks) } : {}),
1062
1206
  ...(Object.keys(addBriefs).length ? { addBriefs } : {}),
1063
1207
  ...(removeBriefIds.length ? { removeBriefIds } : {}),
1208
+ ...(clipsTravel ? { clipBase: clipBase, ...(addClips.length ? { addClips: clone(addClips) } : {}) } : {}),
1064
1209
  ...(next.advisory ? { advisory: clone(next.advisory) } : {}),
1065
1210
  stateSha256: semanticStateSha256(next),
1066
1211
  }, next.sessionId) as ActiveContextDeltaV2;
1067
1212
  }
1068
1213
 
1214
+ // THE FOREST IS HASHED ONCE PER STATE OBJECT (2026-08-28). Nine call-site pairs reach this,
1215
+ // and the one that scales is the per-mark loop in `commitPendingMarks`: `prepareFold` hashes
1216
+ // `state`, and the `commitPreparedFold` on the very next statement re-hashes the SAME object
1217
+ // through `preparedFoldError`. So every applied mark paid one avoidable whole-forest hash,
1218
+ // and `state` grows by a fold each iteration, making a commit epoch O(marks x folds).
1219
+ // Instrumented across the suite: 6,363 calls, 63.8 percent of them repeats on an object
1220
+ // already hashed, peaking at 198 in a single context event.
1221
+ //
1222
+ // It is gate 113's rule, gate 120's and gate 121's, at a fourth address: kept against the
1223
+ // state OBJECT rather than its contents, because state is REPLACED on every change rather
1224
+ // than mutated, so a replaced one misses and hashes again instead of serving something
1225
+ // stale. Every transition in the runtime is a spread; nothing writes `state.folds`,
1226
+ // `state.expanded` or a fold in place.
1227
+ //
1228
+ // The accepted cost, which is the same one gate 121 accepted: a state mutated in place keeps
1229
+ // its first digest, and this digest is persisted into `PreparedFold.topologySha256`.
1230
+ //
1231
+ // NOT memoized beside it: `protectionSha256`, measured at 0.037 to 0.061ms on real states
1232
+ // because `state.protected` is empty in every sealed run, so a memo there buys about 3
1233
+ // percent and adds a second retained map for nothing.
1234
+ const TOPOLOGY_SHA256 = new WeakMap<object, string>();
1235
+
1069
1236
  export function topologySha256(state: Pick<ActiveContextState, "folds" | "expanded">): string {
1070
- return sha256Value({ folds: state.folds, expanded: [...state.expanded].sort() });
1237
+ const key = state as unknown as object;
1238
+ const memoized = TOPOLOGY_SHA256.get(key);
1239
+ if (memoized !== undefined) return memoized;
1240
+ const digest = sha256Value({ folds: state.folds, expanded: [...state.expanded].sort() });
1241
+ TOPOLOGY_SHA256.set(key, digest);
1242
+ return digest;
1071
1243
  }
1072
1244
 
1073
1245
  export function protectionSha256(state: Pick<ActiveContextState, "protected">): string {
@@ -108,6 +108,16 @@ export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
108
108
  */
109
109
  export const MAX_PINNED_SHARE = 0.25;
110
110
 
111
+ /**
112
+ * THE TOOL-CALL DIET's default share (Shane 2026-08-28, on the sol-20260826-full2
113
+ * verdict). Every fold repetition from 3 onward ran 0.50 and the campaign's headline
114
+ * results are all from that shape, so the shipped default is the measured value rather
115
+ * than the absence of one. A deployment turns the diet off with an explicit 0, which is
116
+ * why the option's range is [0, 1) instead of the open interval it shipped with: one
117
+ * meaning per value, and no absent-means-off special case.
118
+ */
119
+ export const DEFAULT_TOOL_FOLD_THRESHOLD = 0.50;
120
+
111
121
  export interface ActiveContextThresholds {
112
122
  maxTarget: number;
113
123
  minTarget: number;
@@ -141,9 +151,10 @@ export const MINIMUM_FOLD_CHARS_FLOOR = 2_000;
141
151
 
142
152
  export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
143
153
  maxTarget: 0.80,
144
- // 0.40, up from 0.20 (Shane 2026-08-23: "we change the thresholds to what hits that
145
- // 100k/250k or so"). This is the only threshold that moved, because the corpus says
146
- // the top of the band was already where he wants it and only the floor was wrong.
154
+ // 0.20 (Shane 2026-08-28, on the sol-20260826-full2 verdict). This number went 0.20 ->
155
+ // 0.40 on 2026-08-23 and came back on the campaign that was still running when it
156
+ // moved, so both readings are recorded here rather than one quietly replacing the
157
+ // other. The top of the band is not in question and never was.
147
158
  //
148
159
  // Read against the 19 mature sealed pifold runs that share a 251,520-token budget,
149
160
  // where the trigger sat at 0.80 of it, 201,216 tokens. Peak occupancy per run came in
@@ -153,26 +164,28 @@ export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.free
153
164
  // So 0.80 already delivers a 250k top, and raising it toward 250k directly would put
154
165
  // the median peak over the budget and into the fence.
155
166
  //
156
- // The floor was the miss. Across 206 commit landings the session came to rest at a
157
- // median 79,034 tokens, and 153 of the 206 landed below 100,000. Landings sit ABOVE
158
- // the aim rather than at it, since the class law and the available material both
159
- // outrank it, so moving the aim to 0.40 sets a floor near where it lands rather than
160
- // far under it: 102,246 tokens against a default 272,000-token descriptor, 100,608
161
- // against the harness budget.
167
+ // The 2026-08-23 case for 0.40 was that it seats the floor near where commits actually
168
+ // land: across 206 commit landings the session came to rest at a median 79,034 tokens,
169
+ // 153 of the 206 below 100,000, and landings sit ABOVE the aim rather than at it, since
170
+ // the class law and the available material both outrank it. That was bought knowing the
171
+ // price, about a fifth more epochs and so a fifth more full-prefix rewrites, to hold
172
+ // 100k of raw context across the whole cycle.
162
173
  //
163
- // 0.40 also makes the aim reachable for the first time. MAX_PINNED_SHARE is 0.25, so a
164
- // fully pinned session legally holds more than a 0.20 aim asks for and the target was
165
- // a number the class law outranked by construction. The epoch receipt still reports
166
- // targetBudgetShare beside actualFreedBudgetShare, so any remaining shortfall stays a
167
- // readable fact per epoch rather than a silent miss.
174
+ // The campaign priced the trade and it went the other way. The commit rewrite tax is
175
+ // minTarget / (maxTarget - minTarget), because every commit re-reads roughly
176
+ // minTarget x budget of prefix uncached and a shallower cut buys less runway for nearly
177
+ // the same bill: 1.0 at 0.80/0.40 against 0.33 at 0.80/0.20. Every sol-20260826-full2
178
+ // fold repetition from 3 onward ran 0.80/0.20, including the fold-plus-canon rep 5 that
179
+ // answered 14 of 16 with nothing wrong at $100.85 against native-plus-canon's $201.70.
180
+ // The shipped default is now the configuration that was measured.
168
181
  //
169
- // The cost is paid in cadence and it is the 2026-08-14 argument running backwards: a
170
- // shallower cut buys less runway for nearly the same commit, so the refill distance
171
- // goes from the observed 0.314 -> 0.80 to 0.40 -> 0.80 and the same session shape owes
172
- // about a fifth more epochs, which is a fifth more full-prefix rewrites. Shane bought
173
- // that deliberately: 100k of raw context held across the whole cycle is what he is
174
- // paying for. The validation laws below bind exactly as before.
175
- minTarget: 0.40,
182
+ // THE KNOWN LIMIT, unchanged and still true: MAX_PINNED_SHARE is 0.25, so a fully
183
+ // pinned session legally holds more than a 0.20 aim asks for, and there the commit
184
+ // stops short of its target by construction rather than by fault. The epoch receipt
185
+ // reports targetBudgetShare beside actualFreedBudgetShare, so the shortfall stays a
186
+ // readable fact per epoch rather than a silent miss. The validation laws below bind
187
+ // exactly as before.
188
+ minTarget: 0.20,
176
189
  consolidateAfter: 10,
177
190
  minFoldChars: 8_000,
178
191
  });
@@ -241,11 +254,6 @@ export function assertThresholdsServable(_thresholds: ActiveContextThresholds, b
241
254
  }
242
255
  }
243
256
 
244
- export function zoneBytes(share: number, budgetTokens: number): number {
245
- if (!Number.isFinite(share) || share <= 0 || !Number.isFinite(budgetTokens) || budgetTokens <= 0) return 0;
246
- return Math.floor(share * budgetTokens * ESTIMATED_BYTES_PER_TOKEN);
247
- }
248
-
249
257
  export function servingBudgetTokens(window: number): number {
250
258
  const resolved = Number.isFinite(window) && window > 0 ? window : DEFAULT_CONTEXT_WINDOW;
251
259
  return resolved - Math.min(ACTIVE_CONTEXT_POLICY.responseReserve, Math.floor(resolved * 0.1));
@@ -253,6 +261,22 @@ export function servingBudgetTokens(window: number): number {
253
261
 
254
262
  export const MAX_PENDING_MARKS = 256;
255
263
  export const ESTIMATED_BYTES_PER_TOKEN = 4;
264
+
265
+ /**
266
+ * What one image in the window is worth, in tokens, for estimation only.
267
+ *
268
+ * A provider prices an image from its DIMENSIONS, not its byte size: Anthropic bills
269
+ * about width x height / 750, which puts a 2000x563 screenshot near 1,500 tokens, and a
270
+ * large image caps close to this number once the long edge is scaled to the 1568px limit.
271
+ * The exact figure differs per provider and cannot be known from the base64 alone.
272
+ *
273
+ * It does not need to be exact. The value it replaces was the image's base64 length
274
+ * divided by a prose chars-per-token ratio, which read one screenshot as 110,000 tokens
275
+ * against a true cost near 1,500. Being within an order of magnitude ends the failure;
276
+ * the provider anchor corrects the rest as soon as a real measurement lands. A constant
277
+ * rather than an option, because nobody can tune this better than the anchor can.
278
+ */
279
+ export const IMAGE_ESTIMATED_TOKENS = 1_600;
256
280
  export const ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES = 240;
257
281
 
258
282
  export const PEEK_MIN_SLICE_BYTES = 1_024;
@@ -271,7 +295,7 @@ export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
271
295
  * agent answers. 1,200 seats the instruction and roughly eight folds; past that the list
272
296
  * gives way and states how many it could not name.
273
297
  */
274
- export const FOLD_NOTICE_BYTES = 1_200;
298
+ export const FOLD_NOTICE_BYTES = 2_400;
275
299
  export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
276
300
  export const MAX_UNMARKED_CANDIDATES = 3;
277
301
 
@@ -298,7 +322,7 @@ export function peekIsEphemeral(params: Record<string, unknown> | undefined | nu
298
322
  return params?.ephemeral !== false;
299
323
  }
300
324
 
301
- export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
325
+ export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek", "recall"]);
302
326
 
303
327
  /** "user" is a mark laid down by the human through /fold-editor; it stages and
304
328
  * commits through the same validated path as an agent mark and counts toward the
@@ -334,16 +358,32 @@ export const ACTIVE_CONTEXT_POLICY = Object.freeze({
334
358
  // clamps its `bytes` to it. Was two numbers until 2026-08-22, when the peek ceiling
335
359
  // was `maxChapterChars` 128,000 and that constant also claimed to cap a chapter.
336
360
  maxSourceChars: 200_000,
361
+ // THE TOOL-CALL DIET (2026-08-24): at COMMIT TIME ONLY, a stale tool result inside
362
+ // the configured toolFoldThreshold share of the projected window is clipped IN VIEW
363
+ // to an identified head, the full bytes staying peek-recoverable behind its entry id.
364
+ // The head keeps the leading paragraph up to its cap (gate 134's rule); results at or
365
+ // under the floor stay whole, because clipping a small result buys almost nothing and
366
+ // adds a peek indirection. Constants, not options, for v1: the share point is the one
367
+ // public knob.
368
+ toolClipHeadChars: 500,
369
+ toolClipFloorChars: 2_000,
337
370
  maxBriefChars: 2_000,
371
+ // The agent's share of maxBriefChars when a supplied brief augments the deterministic
372
+ // head (sol-20260823-live rep 7: supplied briefs REPLACED the fact-carrying head and
373
+ // 74 of 95 peeks recovered values the run's own briefs dropped). The head keeps the
374
+ // remainder, so identification can never be displaced by a long agent clause.
375
+ agentBriefReserve: 600,
338
376
  orientationMessages: 2,
339
377
  maxOrientationChars: 12_000,
340
378
  });
341
379
 
342
380
  export type FoldKind = "tool-result" | "chapter" | "consolidation";
381
+ export type ToolClip = { callId: string; entryId: string };
343
382
  export type FoldPart = { kind: "raw"; ref: EvidenceRef } | { kind: "fold"; foldId: string };
344
383
  export type BriefProvenance =
345
384
  | { kind: "supplied" }
346
385
  | { kind: "deterministic" }
386
+ | { kind: "augmented" }
347
387
  | {
348
388
  kind: "model" | "luna";
349
389
  provider: string;
@@ -395,6 +435,7 @@ export interface ActiveContextState {
395
435
  pendingMarks?: PendingMark[];
396
436
  pinnedPeeks?: never;
397
437
  briefs?: Record<string, BriefOverride>;
438
+ clips?: ToolClip[];
398
439
  prepared?: PreparedFold;
399
440
  advisory?: {
400
441
  highWater: number;
@@ -430,6 +471,7 @@ export interface ActiveContextCheckpointV2 {
430
471
  leases?: Record<string, number>;
431
472
  pendingMarks?: PendingMark[];
432
473
  briefs?: Record<string, BriefOverride>;
474
+ clips?: ToolClip[];
433
475
  advisory?: NonNullable<ActiveContextState["advisory"]>;
434
476
  rider?: NonNullable<ActiveContextState["rider"]>;
435
477
  lastCall?: NonNullable<ActiveContextState["lastCall"]>;
@@ -461,6 +503,16 @@ export interface ActiveContextDeltaV2 {
461
503
  briefs?: Record<string, BriefOverride>;
462
504
  addBriefs?: Record<string, BriefOverride>;
463
505
  removeBriefIds?: string[];
506
+ // `clips` on a delta is the WHOLE array: what every delta written before 2026-08-24
507
+ // carries, and what sealed sessions replay. New deltas carry the change instead. Clips
508
+ // are appended and never edited, so the change is a count of what the base already holds
509
+ // plus the ones that are new; a rollback that shortens the array states a lower count.
510
+ // The order list the marks use would be WORSE than the bug here, because a callId is
511
+ // about a hundred characters and re-listing thirty of them on every delta costs more
512
+ // than the array it replaces.
513
+ clips?: ToolClip[];
514
+ clipBase?: number;
515
+ addClips?: ToolClip[];
464
516
  advisory?: NonNullable<ActiveContextState["advisory"]>;
465
517
  rider?: NonNullable<ActiveContextState["rider"]>;
466
518
  lastCall?: NonNullable<ActiveContextState["lastCall"]>;