pi-fold 3.0.0 → 3.1.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,8 @@ import type {
32
35
  AdvisoryMilestone,
33
36
  BriefOverride,
34
37
  BriefProvenance,
38
+ ToolClip,
39
+ WorkingMemoryEntry,
35
40
  FoldKind,
36
41
  FoldPart,
37
42
  FoldRecordEntry,
@@ -167,7 +172,8 @@ export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Reco
167
172
 
168
173
  export function validProvenance(value: unknown): value is BriefProvenance {
169
174
  if (exactRecord(value, ["kind"]) &&
170
- (ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic")) return true;
175
+ (ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic" ||
176
+ ownValue(value, "kind") === "augmented")) return true;
171
177
  const hasDigest = Boolean(value && typeof value === "object" &&
172
178
  Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
173
179
  if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
@@ -323,6 +329,8 @@ export function parseActiveContextState(
323
329
  const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
324
330
  const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
325
331
  const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
332
+ const hasClips = recordLike && Object.prototype.hasOwnProperty.call(value, "clips");
333
+ const hasMemory = recordLike && Object.prototype.hasOwnProperty.call(value, "memory");
326
334
  // Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
327
335
  // to reproduce, so here the tolerance is the whole of it.
328
336
  const hasLegacyRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
@@ -335,6 +343,8 @@ export function parseActiveContextState(
335
343
  ...(hasPendingMarks ? ["pendingMarks"] : []),
336
344
  ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
337
345
  ...(hasBriefs ? ["briefs"] : []),
346
+ ...(hasClips ? ["clips"] : []),
347
+ ...(hasMemory ? ["memory"] : []),
338
348
  ...(hasLegacyRider ? ["rider"] : []),
339
349
  ];
340
350
  if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
@@ -366,6 +376,8 @@ export function parseActiveContextState(
366
376
  throw new Error("Invalid active-context pending marks");
367
377
  }
368
378
  const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
379
+ const clips = hasClips ? parseToolClips(ownValue(value, "clips")) : [];
380
+ const memory = hasMemory ? parseWorkingMemory(ownValue(value, "memory")) : [];
369
381
  const source = clone(value) as unknown as ActiveContextState;
370
382
  return {
371
383
  version: 1,
@@ -380,6 +392,8 @@ export function parseActiveContextState(
380
392
  leases,
381
393
  ...(marks.length ? { pendingMarks: marks } : {}),
382
394
  ...(Object.keys(briefs).length ? { briefs } : {}),
395
+ ...(clips.length ? { clips } : {}),
396
+ ...(memory.length ? { memory } : {}),
383
397
  ...(hasAdvisory
384
398
  ? { advisory: clone(source.advisory!) }
385
399
  : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
@@ -387,6 +401,47 @@ export function parseActiveContextState(
387
401
  };
388
402
  }
389
403
 
404
+ function parseToolClips(value: unknown): ToolClip[] {
405
+ const entries = denseOwnArrayValues(value);
406
+ if (!entries) throw new Error("Invalid active-context clips");
407
+ const clips = entries.map((entry) => {
408
+ if (!exactRecord(entry, ["callId", "entryId"]) ||
409
+ typeof ownValue(entry, "callId") !== "string" || !ownValue(entry, "callId") ||
410
+ typeof ownValue(entry, "entryId") !== "string" || !ownValue(entry, "entryId")) {
411
+ throw new Error("Invalid active-context clip entry");
412
+ }
413
+ return { callId: String(ownValue(entry, "callId")), entryId: String(ownValue(entry, "entryId")) };
414
+ });
415
+ if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
416
+ throw new Error("Invalid active-context clips: duplicate call id");
417
+ }
418
+ return clips;
419
+ }
420
+
421
+ function parseWorkingMemory(value: unknown): WorkingMemoryEntry[] {
422
+ const entries = denseOwnArrayValues(value);
423
+ if (!entries) throw new Error("Invalid active-context working memory");
424
+ const memory = entries.map((entry) => {
425
+ const key = ownValue(entry, "key");
426
+ const body = ownValue(entry, "body");
427
+ const ordinal = ownValue(entry, "ordinal");
428
+ if (!exactRecord(entry, ["key", "body", "ordinal"]) ||
429
+ typeof key !== "string" || !key || key.length > MEMORY_KEY_CHARS ||
430
+ typeof body !== "string" || !body || body.length > MEMORY_BODY_CHARS ||
431
+ !Number.isSafeInteger(ordinal) || Number(ordinal) < 0) {
432
+ throw new Error("Invalid active-context working-memory entry");
433
+ }
434
+ return { key, body, ordinal: Number(ordinal) };
435
+ });
436
+ if (memory.length > MEMORY_KEYS_MAX) {
437
+ throw new Error(`Invalid active-context working memory: over ${MEMORY_KEYS_MAX} entries`);
438
+ }
439
+ if (new Set(memory.map((entry) => entry.key)).size !== memory.length) {
440
+ throw new Error("Invalid active-context working memory: duplicate key");
441
+ }
442
+ return memory;
443
+ }
444
+
390
445
  export function clearPrepared(state: ActiveContextState): ActiveContextState {
391
446
  const next = { ...state };
392
447
  delete next.prepared;
@@ -412,8 +467,24 @@ export function validatePreparedShape(value: unknown): asserts value is Prepared
412
467
  }
413
468
  }
414
469
 
470
+ // THE SPREAD IS THE COPY (2026-08-28). Roughly six calls per fold per persist, and only
471
+ // `deriveFoldParents` wants a record it can write to, where it writes the TOP-LEVEL
472
+ // `parentId` that the spread already gives it a fresh slot for. Every other caller hashes
473
+ // and discards: `foldRecordRef`, `sameFoldRecordIdentity`, and `makeFoldRecordEntry`, whose
474
+ // `parseFoldRecordEntry` deep-clones twice on its own.
475
+ //
476
+ // The digest cannot move. `safeJson` emits in `Reflect.ownKeys` order, a spread preserves
477
+ // insertion order, and overwriting an existing key keeps it in its original position, so
478
+ // `{ ...fold, parentId: null }` serializes to the same bytes as the cloned form.
479
+ //
480
+ // THE COST, STATED BECAUSE IT IS NOW LOAD-BEARING: the returned record shares every nested
481
+ // object with its input, so "no caller mutates a canonical record's nested fields" is a rule
482
+ // this function relies on rather than enforces. It holds today (`validateFoldForest` clones
483
+ // at :281, `parseFoldRecordEntry` twice at :567-568, the hashing callers never write), and a
484
+ // future caller that writes through `record.parts` would corrupt the fold it was built from.
485
+ // Write `{ ...record, field: value }` rather than assigning into a nested one.
415
486
  export function canonicalFoldRecord(fold: ActiveFold): ActiveFold {
416
- return { ...clone(fold), parentId: null };
487
+ return { ...fold, parentId: null };
417
488
  }
418
489
 
419
490
  export function sameFoldRecordIdentity(left: ActiveFold, right: ActiveFold): boolean {
@@ -475,11 +546,18 @@ export function legacyReplayOrderStateSha256(state: ActiveContextState): string
475
546
 
476
547
  export function sameStateProjection(left: ActiveContextState, right: ActiveContextState): boolean {
477
548
  const normalized = (value: ActiveContextState): Partial<ActiveContextState> => {
478
- const normalizedState = { ...clone(value), revision: 0 } as Partial<ActiveContextState>;
549
+ // A SHALLOW COPY IS ENOUGH TO DELETE TOP-LEVEL KEYS (2026-08-28). `delete` on a shallow
550
+ // copy cannot reach the original, the normalized object never escapes this function, and
551
+ // the runtime already stableStringifies live un-cloned state (`semanticStateSha256`), so
552
+ // un-cloned state is already required to be serializer-safe. Measured 97.6ms to 82.6ms
553
+ // on the largest real state in the corpus, at about two calls per provider request.
554
+ const normalizedState = { ...value, revision: 0 } as Partial<ActiveContextState>;
479
555
  if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
480
556
  if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
481
557
  if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
482
558
  if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
559
+ if (normalizedState.clips && normalizedState.clips.length === 0) delete normalizedState.clips;
560
+ if (normalizedState.memory && normalizedState.memory.length === 0) delete normalizedState.memory;
483
561
  return normalizedState;
484
562
  };
485
563
  return stableStringify(normalized(left)) === stableStringify(normalized(right));
@@ -586,6 +664,14 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
586
664
  Object.prototype.hasOwnProperty.call(value, "addBriefs"));
587
665
  const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
588
666
  Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
667
+ const hasWireClips = Boolean(value && typeof value === "object" &&
668
+ Object.prototype.hasOwnProperty.call(value, "clips"));
669
+ const hasClipBase = Boolean(value && typeof value === "object" &&
670
+ Object.prototype.hasOwnProperty.call(value, "clipBase"));
671
+ const hasAddClips = Boolean(value && typeof value === "object" &&
672
+ Object.prototype.hasOwnProperty.call(value, "addClips"));
673
+ const hasWireMemory = Boolean(value && typeof value === "object" &&
674
+ Object.prototype.hasOwnProperty.call(value, "memory"));
589
675
  const hasLegacyRider = Boolean(value && typeof value === "object" &&
590
676
  Object.prototype.hasOwnProperty.call(value, "rider"));
591
677
  refuseRetiredStateFields(value);
@@ -600,6 +686,10 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
600
686
  ...(hasBriefs ? ["briefs"] : []),
601
687
  ...(hasAddBriefs ? ["addBriefs"] : []),
602
688
  ...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
689
+ ...(hasWireClips ? ["clips"] : []),
690
+ ...(hasClipBase ? ["clipBase"] : []),
691
+ ...(hasAddClips ? ["addClips"] : []),
692
+ ...(hasWireMemory ? ["memory"] : []),
603
693
  // Carried no further than the digest check in `materializeStatePersistence`, which is
604
694
  // the only thing on a load that the rider's absence can change.
605
695
  ...(hasLegacyRider ? ["rider"] : []),
@@ -619,6 +709,18 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
619
709
  ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
620
710
  ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
621
711
  );
712
+ if (hasWireClips) parseToolClips(ownValue(value, "clips"));
713
+ if (hasAddClips) parseToolClips(ownValue(value, "addClips"));
714
+ if (hasWireMemory) parseWorkingMemory(ownValue(value, "memory"));
715
+ if (hasClipBase) {
716
+ const base = ownValue(value, "clipBase");
717
+ if (!Number.isSafeInteger(base) || Number(base) < 0) throw new Error("Invalid active-context clip base");
718
+ }
719
+ // A whole array and a change cannot both be the truth on one wire.
720
+ if (hasWireClips && (hasClipBase || hasAddClips)) {
721
+ throw new Error("Invalid active-context clips: whole array beside a change");
722
+ }
723
+ if (hasAddClips && !hasClipBase) throw new Error("Invalid active-context clips: change without a base");
622
724
  if (checkpoint) {
623
725
  const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
624
726
  if (!refs || refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Invalid active-context checkpoint refs");
@@ -693,7 +795,25 @@ export function normalizeFoldsForPersistedRecords(
693
795
  return deriveFoldParents(normalized);
694
796
  }
695
797
 
696
- export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
798
+ /**
799
+ * Rebuild the parent links a fold forest's records do not carry.
800
+ *
801
+ * THIS DOES NOT VALIDATE THE FOREST, and the split is deliberate. `validateFoldForest` deep
802
+ * clones the forest, recomputes `foldIdFor` per fold, and calls `flattenFoldRefs` per fold,
803
+ * which rebuilds `foldMap` over every fold; two of the four callers handed the result
804
+ * straight to `parseActiveContextState`, whose FIRST act is to validate it again. So a
805
+ * persist validated the same forest three times. Roughly 12ms saved at 88 folds, 28 at 178,
806
+ * 55 at 267, plus about 1.6x on the session-start load path.
807
+ *
808
+ * The child-record check above stays here, because it is about the ASSIGNMENT rather than
809
+ * the forest: a child claimed twice, or claimed by a parent that does not hold it, is a
810
+ * contradiction this loop is the only thing positioned to see.
811
+ *
812
+ * Callers that do not parse immediately afterwards want `deriveFoldParents` below. Reaching
813
+ * for this one is a claim that the very next statement validates the same array; nothing
814
+ * else makes it safe, and it reads at review as removing validation, which it is not.
815
+ */
816
+ export function assignFoldParents(records: ActiveFold[]): ActiveFold[] {
697
817
  const folds = records.map(canonicalFoldRecord);
698
818
  const byId = new Map(folds.map((fold) => [fold.id, fold]));
699
819
  const assigned = new Set<string>();
@@ -705,14 +825,19 @@ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
705
825
  assigned.add(childId);
706
826
  }
707
827
  }
708
- return validateFoldForest(folds);
828
+ return folds;
829
+ }
830
+
831
+ /** Assign the parents and validate the forest, for callers that do not parse next. */
832
+ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
833
+ return validateFoldForest(assignFoldParents(records));
709
834
  }
710
835
 
711
836
  export function stateFromFoldRefs(
712
837
  wire: Pick<
713
838
  ActiveContextCheckpointV2,
714
839
  "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
715
- "tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs"
840
+ "tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips" | "memory"
716
841
  >,
717
842
  refs: FoldRecordRef[],
718
843
  records: Map<string, FoldRecordEntry>,
@@ -727,13 +852,17 @@ export function stateFromFoldRefs(
727
852
  version: 1,
728
853
  sessionId: wire.sessionId,
729
854
  revision: wire.revision,
730
- folds: deriveFoldParents(folds),
855
+ // `parseActiveContextState` at the end of this function validates the forest, so
856
+ // validating it here as well was the same walk twice on one persist.
857
+ folds: assignFoldParents(folds),
731
858
  expanded: clone(wire.expanded),
732
859
  protected: clone(wire.protected),
733
860
  tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
734
861
  leases: clone(wire.leases ?? {}),
735
862
  ...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
736
863
  ...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
864
+ ...(wire.clips?.length ? { clips: clone(wire.clips) } : {}),
865
+ ...(wire.memory?.length ? { memory: clone(wire.memory) } : {}),
737
866
  ...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
738
867
  ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
739
868
  };
@@ -869,7 +998,16 @@ export function materializeStatePersistence(
869
998
  wire.revision <= wire.baseRevision) {
870
999
  throw new Error("Broken active-context delta chain");
871
1000
  }
872
- const refs = state.folds.map(foldRecordRef);
1001
+ // THE RECORD MAP ALREADY HOLDS THIS DIGEST (2026-08-28). `foldRecordRef` canonicalizes
1002
+ // and hashes a whole fold, and this ran once per delta entry over every standing fold:
1003
+ // 48,715 calls in ONE load of a real sealed session, 2.6 to 3.7s of a 24 to 33s load.
1004
+ // `records` is built by the same replay and carries the same digest verbatim. The
1005
+ // fallback is unreachable, because the pre-scan raises on a delta before a checkpoint;
1006
+ // it is kept so the change is exactly behaviour-preserving rather than nearly so.
1007
+ const refs = state.folds.map((fold) => {
1008
+ const record = records.get(fold.id);
1009
+ return record ? { id: fold.id, sha256: record.recordSha256 } : foldRecordRef(fold);
1010
+ });
873
1011
  const byId = new Map(refs.map((ref) => [ref.id, ref]));
874
1012
  for (const id of wire.removeFoldIds) {
875
1013
  if (!byId.delete(id)) throw new Error(`Unknown active-context delta removal ${id}`);
@@ -925,11 +1063,26 @@ export function materializeStatePersistence(
925
1063
  return mark;
926
1064
  });
927
1065
  }
1066
+ // The clips replay by the same discriminator rule as the briefs and the marks: the
1067
+ // presence of the CHANGE is what says a wire carries one. A delta written before
1068
+ // 2026-08-24 states the whole array when it has one and states nothing when it does
1069
+ // not, so "no array" means "no clips" there, exactly as it does for the marks.
1070
+ const clips = wire.clipBase === undefined
1071
+ ? (wire.clips !== undefined ? clone(wire.clips) : [])
1072
+ : [...(state.clips ?? []).slice(0, wire.clipBase), ...clone(wire.addClips ?? [])];
1073
+ if (wire.clipBase !== undefined && wire.clipBase > (state.clips ?? []).length) {
1074
+ throw new Error(`Unknown active-context clip base ${wire.clipBase}`);
1075
+ }
1076
+ if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
1077
+ throw new Error("Invalid active-context clips: duplicate call id");
1078
+ }
928
1079
  state = stateFromFoldRefs(
929
1080
  {
930
1081
  ...wire,
931
1082
  briefs: Object.keys(briefs).length ? briefs : undefined,
932
1083
  pendingMarks: marks.length ? marks : undefined,
1084
+ clips: clips.length ? clips : undefined,
1085
+ memory: wire.memory?.length ? clone(wire.memory) : undefined,
933
1086
  },
934
1087
  [...byId.values()],
935
1088
  records,
@@ -989,6 +1142,8 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
989
1142
  leases: clone(state.leases),
990
1143
  ...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
991
1144
  ...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
1145
+ ...(state.clips?.length ? { clips: clone(state.clips) } : {}),
1146
+ ...(state.memory?.length ? { memory: clone(state.memory) } : {}),
992
1147
  ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
993
1148
  stateSha256: semanticStateSha256(state),
994
1149
  }, state.sessionId) as ActiveContextCheckpointV2;
@@ -1036,6 +1191,23 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
1036
1191
  return !held || sha256Value(held) !== sha256Value(mark);
1037
1192
  });
1038
1193
  const marksTravel = previousMarks.length > 0 || nextMarks.length > 0;
1194
+ // THE CLIPS ARE THE SAME RULE A THIRD TIME (2026-08-24). They shipped whole on every
1195
+ // delta, which was 50.1 percent of all state bytes on sol-20260823-live rep 11, 472,750
1196
+ // of 942,902, for an array that never held more than 34 entries: the same defect the two
1197
+ // comments above were written about, arriving with a feature that did not inherit their
1198
+ // encoding. Clips are APPENDED and never edited, so the change is a count of what the
1199
+ // base already holds plus the ones that are new, and a rollback that shortens the array
1200
+ // states a lower count. The marks' order list would be worse than the bug here: a callId
1201
+ // is about a hundred characters, so re-listing them all on every delta costs more than
1202
+ // the array it replaces.
1203
+ const previousClips = previous.clips ?? [];
1204
+ const nextClips = next.clips ?? [];
1205
+ let clipBase = 0;
1206
+ while (clipBase < previousClips.length && clipBase < nextClips.length &&
1207
+ previousClips[clipBase].callId === nextClips[clipBase].callId &&
1208
+ previousClips[clipBase].entryId === nextClips[clipBase].entryId) clipBase += 1;
1209
+ const addClips = nextClips.slice(clipBase);
1210
+ const clipsTravel = previousClips.length > 0 || nextClips.length > 0;
1039
1211
  // The rest travel whole and stay that way. Measured on the same run, the two that were
1040
1212
  // worth a diff were 20.0 MB of the 21.6 MB ledger; what remained were bounded objects
1041
1213
  // with their own caps rather than collections that grow with the epoch (the largest,
@@ -1061,13 +1233,44 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
1061
1233
  ...(addPendingMarks.length ? { addPendingMarks: clone(addPendingMarks) } : {}),
1062
1234
  ...(Object.keys(addBriefs).length ? { addBriefs } : {}),
1063
1235
  ...(removeBriefIds.length ? { removeBriefIds } : {}),
1236
+ ...(clipsTravel ? { clipBase: clipBase, ...(addClips.length ? { addClips: clone(addClips) } : {}) } : {}),
1237
+ // The memory travels whole when it exists: bounded at MEMORY_KEYS_MAX entries and
1238
+ // edited in place, so the change encodings above have nothing to save here.
1239
+ ...(next.memory?.length ? { memory: clone(next.memory) } : {}),
1064
1240
  ...(next.advisory ? { advisory: clone(next.advisory) } : {}),
1065
1241
  stateSha256: semanticStateSha256(next),
1066
1242
  }, next.sessionId) as ActiveContextDeltaV2;
1067
1243
  }
1068
1244
 
1245
+ // THE FOREST IS HASHED ONCE PER STATE OBJECT (2026-08-28). Nine call-site pairs reach this,
1246
+ // and the one that scales is the per-mark loop in `commitPendingMarks`: `prepareFold` hashes
1247
+ // `state`, and the `commitPreparedFold` on the very next statement re-hashes the SAME object
1248
+ // through `preparedFoldError`. So every applied mark paid one avoidable whole-forest hash,
1249
+ // and `state` grows by a fold each iteration, making a commit epoch O(marks x folds).
1250
+ // Instrumented across the suite: 6,363 calls, 63.8 percent of them repeats on an object
1251
+ // already hashed, peaking at 198 in a single context event.
1252
+ //
1253
+ // It is gate 113's rule, gate 120's and gate 121's, at a fourth address: kept against the
1254
+ // state OBJECT rather than its contents, because state is REPLACED on every change rather
1255
+ // than mutated, so a replaced one misses and hashes again instead of serving something
1256
+ // stale. Every transition in the runtime is a spread; nothing writes `state.folds`,
1257
+ // `state.expanded` or a fold in place.
1258
+ //
1259
+ // The accepted cost, which is the same one gate 121 accepted: a state mutated in place keeps
1260
+ // its first digest, and this digest is persisted into `PreparedFold.topologySha256`.
1261
+ //
1262
+ // NOT memoized beside it: `protectionSha256`, measured at 0.037 to 0.061ms on real states
1263
+ // because `state.protected` is empty in every sealed run, so a memo there buys about 3
1264
+ // percent and adds a second retained map for nothing.
1265
+ const TOPOLOGY_SHA256 = new WeakMap<object, string>();
1266
+
1069
1267
  export function topologySha256(state: Pick<ActiveContextState, "folds" | "expanded">): string {
1070
- return sha256Value({ folds: state.folds, expanded: [...state.expanded].sort() });
1268
+ const key = state as unknown as object;
1269
+ const memoized = TOPOLOGY_SHA256.get(key);
1270
+ if (memoized !== undefined) return memoized;
1271
+ const digest = sha256Value({ folds: state.folds, expanded: [...state.expanded].sort() });
1272
+ TOPOLOGY_SHA256.set(key, digest);
1273
+ return digest;
1071
1274
  }
1072
1275
 
1073
1276
  export function protectionSha256(state: Pick<ActiveContextState, "protected">): string {
@@ -91,7 +91,9 @@ export const RETIRED_TOOL_ACTIONS: Readonly<Record<string, string>> = Object.fre
91
91
  "peek already carries losslessly. Write the brief you want when you mark the span, or " +
92
92
  "use reboundary to re-cut it.",
93
93
  });
94
- export type ActiveContextToolAction = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
94
+ export type ActiveContextToolAction =
95
+ | typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number]
96
+ | typeof WORKING_MEMORY_TOOL_ACTIONS[number];
95
97
  export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
96
98
  export const DEFAULT_CONTEXT_WINDOW = 272_000;
97
99
  export const EXPAND_LEASE_GENERATIONS = 8;
@@ -108,6 +110,16 @@ export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
108
110
  */
109
111
  export const MAX_PINNED_SHARE = 0.25;
110
112
 
113
+ /**
114
+ * THE TOOL-CALL DIET's default share (Shane 2026-08-28, on the sol-20260826-full2
115
+ * verdict). Every fold repetition from 3 onward ran 0.50 and the campaign's headline
116
+ * results are all from that shape, so the shipped default is the measured value rather
117
+ * than the absence of one. A deployment turns the diet off with an explicit 0, which is
118
+ * why the option's range is [0, 1) instead of the open interval it shipped with: one
119
+ * meaning per value, and no absent-means-off special case.
120
+ */
121
+ export const DEFAULT_TOOL_FOLD_THRESHOLD = 0.50;
122
+
111
123
  export interface ActiveContextThresholds {
112
124
  maxTarget: number;
113
125
  minTarget: number;
@@ -141,9 +153,10 @@ export const MINIMUM_FOLD_CHARS_FLOOR = 2_000;
141
153
 
142
154
  export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
143
155
  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.
156
+ // 0.20 (Shane 2026-08-28, on the sol-20260826-full2 verdict). This number went 0.20 ->
157
+ // 0.40 on 2026-08-23 and came back on the campaign that was still running when it
158
+ // moved, so both readings are recorded here rather than one quietly replacing the
159
+ // other. The top of the band is not in question and never was.
147
160
  //
148
161
  // Read against the 19 mature sealed pifold runs that share a 251,520-token budget,
149
162
  // where the trigger sat at 0.80 of it, 201,216 tokens. Peak occupancy per run came in
@@ -153,26 +166,28 @@ export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.free
153
166
  // So 0.80 already delivers a 250k top, and raising it toward 250k directly would put
154
167
  // the median peak over the budget and into the fence.
155
168
  //
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.
169
+ // The 2026-08-23 case for 0.40 was that it seats the floor near where commits actually
170
+ // land: across 206 commit landings the session came to rest at a median 79,034 tokens,
171
+ // 153 of the 206 below 100,000, and landings sit ABOVE the aim rather than at it, since
172
+ // the class law and the available material both outrank it. That was bought knowing the
173
+ // price, about a fifth more epochs and so a fifth more full-prefix rewrites, to hold
174
+ // 100k of raw context across the whole cycle.
162
175
  //
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.
176
+ // The campaign priced the trade and it went the other way. The commit rewrite tax is
177
+ // minTarget / (maxTarget - minTarget), because every commit re-reads roughly
178
+ // minTarget x budget of prefix uncached and a shallower cut buys less runway for nearly
179
+ // the same bill: 1.0 at 0.80/0.40 against 0.33 at 0.80/0.20. Every sol-20260826-full2
180
+ // fold repetition from 3 onward ran 0.80/0.20, including the fold-plus-canon rep 5 that
181
+ // answered 14 of 16 with nothing wrong at $100.85 against native-plus-canon's $201.70.
182
+ // The shipped default is now the configuration that was measured.
168
183
  //
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,
184
+ // THE KNOWN LIMIT, unchanged and still true: MAX_PINNED_SHARE is 0.25, so a fully
185
+ // pinned session legally holds more than a 0.20 aim asks for, and there the commit
186
+ // stops short of its target by construction rather than by fault. The epoch receipt
187
+ // reports targetBudgetShare beside actualFreedBudgetShare, so the shortfall stays a
188
+ // readable fact per epoch rather than a silent miss. The validation laws below bind
189
+ // exactly as before.
190
+ minTarget: 0.20,
176
191
  consolidateAfter: 10,
177
192
  minFoldChars: 8_000,
178
193
  });
@@ -241,11 +256,6 @@ export function assertThresholdsServable(_thresholds: ActiveContextThresholds, b
241
256
  }
242
257
  }
243
258
 
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
259
  export function servingBudgetTokens(window: number): number {
250
260
  const resolved = Number.isFinite(window) && window > 0 ? window : DEFAULT_CONTEXT_WINDOW;
251
261
  return resolved - Math.min(ACTIVE_CONTEXT_POLICY.responseReserve, Math.floor(resolved * 0.1));
@@ -271,7 +281,7 @@ export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
271
281
  * agent answers. 1,200 seats the instruction and roughly eight folds; past that the list
272
282
  * gives way and states how many it could not name.
273
283
  */
274
- export const FOLD_NOTICE_BYTES = 1_200;
284
+ export const FOLD_NOTICE_BYTES = 2_400;
275
285
  export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
276
286
  export const MAX_UNMARKED_CANDIDATES = 3;
277
287
 
@@ -298,7 +308,35 @@ export function peekIsEphemeral(params: Record<string, unknown> | undefined | nu
298
308
  return params?.ephemeral !== false;
299
309
  }
300
310
 
301
- export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
311
+ export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek", "recall"]);
312
+
313
+ /**
314
+ * THE WORKING MEMORY (Shane, 2026-08-26). A session-scoped ordered dictionary the agent
315
+ * maintains beside the fold index: keys list in a table of contents the projection
316
+ * carries, bodies live in state and are read on demand with `recall`. It exists because
317
+ * the campaign measured a structural gap, not a tuning one: a maintained digest (running
318
+ * tallies, current totals) is a different operation from a lossless index, and blending
319
+ * digest prose into the index's briefs was measured making BOTH worse (sol-20260826-full2
320
+ * rep 4: nine confident wrong end-block answers against the deterministic arm's zero).
321
+ * The digest gets its own channel and the index stays mechanical.
322
+ *
323
+ * Three constants, not knobs. The key cap keeps the table of contents a table of
324
+ * contents; the body cap is deliberately larger than a brief because a digest is
325
+ * maintained prose, not an index row; the entry cap bounds the whole store at a size
326
+ * whose table of contents stays one glance.
327
+ */
328
+ export const MEMORY_KEY_CHARS = 64;
329
+ export const MEMORY_BODY_CHARS = 4_000;
330
+ export const MEMORY_KEYS_MAX = 24;
331
+
332
+ export const WORKING_MEMORY_TOOL_ACTIONS = Object.freeze(["remember", "recall"] as const);
333
+
334
+ export interface WorkingMemoryEntry {
335
+ key: string;
336
+ body: string;
337
+ /** Write counter at last update; the table of contents lists freshest first. */
338
+ ordinal: number;
339
+ }
302
340
 
303
341
  /** "user" is a mark laid down by the human through /fold-editor; it stages and
304
342
  * commits through the same validated path as an agent mark and counts toward the
@@ -334,16 +372,32 @@ export const ACTIVE_CONTEXT_POLICY = Object.freeze({
334
372
  // clamps its `bytes` to it. Was two numbers until 2026-08-22, when the peek ceiling
335
373
  // was `maxChapterChars` 128,000 and that constant also claimed to cap a chapter.
336
374
  maxSourceChars: 200_000,
375
+ // THE TOOL-CALL DIET (2026-08-24): at COMMIT TIME ONLY, a stale tool result inside
376
+ // the configured toolFoldThreshold share of the projected window is clipped IN VIEW
377
+ // to an identified head, the full bytes staying peek-recoverable behind its entry id.
378
+ // The head keeps the leading paragraph up to its cap (gate 134's rule); results at or
379
+ // under the floor stay whole, because clipping a small result buys almost nothing and
380
+ // adds a peek indirection. Constants, not options, for v1: the share point is the one
381
+ // public knob.
382
+ toolClipHeadChars: 500,
383
+ toolClipFloorChars: 2_000,
337
384
  maxBriefChars: 2_000,
385
+ // The agent's share of maxBriefChars when a supplied brief augments the deterministic
386
+ // head (sol-20260823-live rep 7: supplied briefs REPLACED the fact-carrying head and
387
+ // 74 of 95 peeks recovered values the run's own briefs dropped). The head keeps the
388
+ // remainder, so identification can never be displaced by a long agent clause.
389
+ agentBriefReserve: 600,
338
390
  orientationMessages: 2,
339
391
  maxOrientationChars: 12_000,
340
392
  });
341
393
 
342
394
  export type FoldKind = "tool-result" | "chapter" | "consolidation";
395
+ export type ToolClip = { callId: string; entryId: string };
343
396
  export type FoldPart = { kind: "raw"; ref: EvidenceRef } | { kind: "fold"; foldId: string };
344
397
  export type BriefProvenance =
345
398
  | { kind: "supplied" }
346
399
  | { kind: "deterministic" }
400
+ | { kind: "augmented" }
347
401
  | {
348
402
  kind: "model" | "luna";
349
403
  provider: string;
@@ -395,6 +449,7 @@ export interface ActiveContextState {
395
449
  pendingMarks?: PendingMark[];
396
450
  pinnedPeeks?: never;
397
451
  briefs?: Record<string, BriefOverride>;
452
+ clips?: ToolClip[];
398
453
  prepared?: PreparedFold;
399
454
  advisory?: {
400
455
  highWater: number;
@@ -402,6 +457,8 @@ export interface ActiveContextState {
402
457
  armed?: { milestone: AdvisoryMilestone; threshold: number; scheduleKey: string };
403
458
  };
404
459
  rider?: { epoch: number; text: string };
460
+ /** The working memory's entries, in write order; absent when empty. */
461
+ memory?: WorkingMemoryEntry[];
405
462
  }
406
463
 
407
464
  export interface FoldRecordRef {
@@ -430,6 +487,8 @@ export interface ActiveContextCheckpointV2 {
430
487
  leases?: Record<string, number>;
431
488
  pendingMarks?: PendingMark[];
432
489
  briefs?: Record<string, BriefOverride>;
490
+ clips?: ToolClip[];
491
+ memory?: WorkingMemoryEntry[];
433
492
  advisory?: NonNullable<ActiveContextState["advisory"]>;
434
493
  rider?: NonNullable<ActiveContextState["rider"]>;
435
494
  lastCall?: NonNullable<ActiveContextState["lastCall"]>;
@@ -461,6 +520,21 @@ export interface ActiveContextDeltaV2 {
461
520
  briefs?: Record<string, BriefOverride>;
462
521
  addBriefs?: Record<string, BriefOverride>;
463
522
  removeBriefIds?: string[];
523
+ // `clips` on a delta is the WHOLE array: what every delta written before 2026-08-24
524
+ // carries, and what sealed sessions replay. New deltas carry the change instead. Clips
525
+ // are appended and never edited, so the change is a count of what the base already holds
526
+ // plus the ones that are new; a rollback that shortens the array states a lower count.
527
+ // The order list the marks use would be WORSE than the bug here, because a callId is
528
+ // about a hundred characters and re-listing thirty of them on every delta costs more
529
+ // than the array it replaces.
530
+ clips?: ToolClip[];
531
+ clipBase?: number;
532
+ addClips?: ToolClip[];
533
+ // `memory` on a delta is the WHOLE array. It is bounded by MEMORY_KEYS_MAX entries and
534
+ // edited in place by key, so a change form would have to restate the key order the way
535
+ // the marks do while saving at most a handful of bounded bodies: the whole array is the
536
+ // cheaper wire. Absent means empty, by the same discriminator rule as the marks.
537
+ memory?: WorkingMemoryEntry[];
464
538
  advisory?: NonNullable<ActiveContextState["advisory"]>;
465
539
  rider?: NonNullable<ActiveContextState["rider"]>;
466
540
  lastCall?: NonNullable<ActiveContextState["lastCall"]>;