pi-fold 2.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,
@@ -56,7 +61,7 @@ export const PENDING_FOLD_MARK_KEYS = [
56
61
  "mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
57
62
  ] as const;
58
63
  export const PENDING_REFOLD_MARK_KEYS = ["mark", "id", "origin", "ordinal"] as const;
59
- export const MARK_ORIGINS = Object.freeze(["agent", "ladder"] as const);
64
+ export const MARK_ORIGINS = Object.freeze(["agent", "ladder", "user"] as const);
60
65
  export const STATE_CHECKPOINT_V2_KEYS = [
61
66
  "version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
62
67
  ] as const;
@@ -91,8 +96,7 @@ export function validPendingMark(value: unknown): value is PendingMark {
91
96
  const kind = ownValue(value, "kind");
92
97
  const parts = denseOwnArrayValues(ownValue(value, "parts"));
93
98
  return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
94
- Boolean(parts?.length) && parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
95
- parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
99
+ Boolean(parts?.length) && parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
96
100
  validProvenance(ownValue(value, "briefProvenance")) &&
97
101
  id === foldIdFor(kind as FoldKind, parts as FoldPart[]);
98
102
  }
@@ -168,7 +172,8 @@ export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Reco
168
172
 
169
173
  export function validProvenance(value: unknown): value is BriefProvenance {
170
174
  if (exactRecord(value, ["kind"]) &&
171
- (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;
172
177
  const hasDigest = Boolean(value && typeof value === "object" &&
173
178
  Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
174
179
  if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
@@ -210,7 +215,6 @@ export function validFoldShape(value: unknown): value is ActiveFold {
210
215
  return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
211
216
  typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
212
217
  (parentId === null || typeof parentId === "string") && Boolean(parts?.length) &&
213
- parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
214
218
  parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
215
219
  validProvenance(ownValue(value, "provenance")) && typeof sourceSha256 === "string" &&
216
220
  /^[a-f0-9]{64}$/.test(sourceSha256) && Number.isSafeInteger(ownValue(value, "sourceChars")) &&
@@ -325,7 +329,11 @@ export function parseActiveContextState(
325
329
  const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
326
330
  const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
327
331
  const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
328
- const hasRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
332
+ const hasClips = recordLike && Object.prototype.hasOwnProperty.call(value, "clips");
333
+ const hasMemory = recordLike && Object.prototype.hasOwnProperty.call(value, "memory");
334
+ // Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
335
+ // to reproduce, so here the tolerance is the whole of it.
336
+ const hasLegacyRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
329
337
  refuseRetiredStateFields(value);
330
338
  const extraKeys = [
331
339
  ...(hasPrepared ? ["prepared"] : []),
@@ -335,7 +343,9 @@ export function parseActiveContextState(
335
343
  ...(hasPendingMarks ? ["pendingMarks"] : []),
336
344
  ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
337
345
  ...(hasBriefs ? ["briefs"] : []),
338
- ...(hasRider ? ["rider"] : []),
346
+ ...(hasClips ? ["clips"] : []),
347
+ ...(hasMemory ? ["memory"] : []),
348
+ ...(hasLegacyRider ? ["rider"] : []),
339
349
  ];
340
350
  if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
341
351
  const folds = denseOwnArrayValues(ownValue(value, "folds"));
@@ -357,9 +367,6 @@ export function parseActiveContextState(
357
367
  if (hasAdvisory && !validAdvisoryState(ownValue(value, "advisory"))) {
358
368
  throw new Error("Invalid active-context advisory state");
359
369
  }
360
- if (hasRider && !validRiderState(ownValue(value, "rider"))) {
361
- throw new Error("Invalid active-context rider state");
362
- }
363
370
  if (hasTokensSinceToolFold && !validTokensSinceToolFold(ownValue(value, "tokensSinceToolFold"))) {
364
371
  throw new Error("Invalid active-context tool-fold cadence");
365
372
  }
@@ -369,6 +376,8 @@ export function parseActiveContextState(
369
376
  throw new Error("Invalid active-context pending marks");
370
377
  }
371
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")) : [];
372
381
  const source = clone(value) as unknown as ActiveContextState;
373
382
  return {
374
383
  version: 1,
@@ -383,14 +392,56 @@ export function parseActiveContextState(
383
392
  leases,
384
393
  ...(marks.length ? { pendingMarks: marks } : {}),
385
394
  ...(Object.keys(briefs).length ? { briefs } : {}),
395
+ ...(clips.length ? { clips } : {}),
396
+ ...(memory.length ? { memory } : {}),
386
397
  ...(hasAdvisory
387
398
  ? { advisory: clone(source.advisory!) }
388
399
  : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
389
- ...(hasRider ? { rider: clone(source.rider!) } : {}),
390
400
  ...(hasPrepared ? { prepared: clone(source.prepared!) } : {}),
391
401
  };
392
402
  }
393
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
+
394
445
  export function clearPrepared(state: ActiveContextState): ActiveContextState {
395
446
  const next = { ...state };
396
447
  delete next.prepared;
@@ -416,8 +467,24 @@ export function validatePreparedShape(value: unknown): asserts value is Prepared
416
467
  }
417
468
  }
418
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.
419
486
  export function canonicalFoldRecord(fold: ActiveFold): ActiveFold {
420
- return { ...clone(fold), parentId: null };
487
+ return { ...fold, parentId: null };
421
488
  }
422
489
 
423
490
  export function sameFoldRecordIdentity(left: ActiveFold, right: ActiveFold): boolean {
@@ -441,6 +508,29 @@ export function legacyCadenceOmittedStateSha256(state: ActiveContextState): stri
441
508
  return sha256Value(legacy);
442
509
  }
443
510
 
511
+ /**
512
+ * THE DIGEST A RIDER-ERA BUILD WROTE, reproduced without the subsystem.
513
+ *
514
+ * `semanticStateSha256` hashes the materialized state object, and `stableStringify` walks
515
+ * own keys in INSERTION order, so a field that is gone changes the digest even though it
516
+ * changed nothing else. The rider sat immediately before `prepared` in the materialized
517
+ * literal, and last when there was no prepared, so that is where it goes back for the one
518
+ * moment it is needed. It joins the two legacy derivations already here, which exist for
519
+ * exactly this reason: a state stays verifiable across the build that stopped writing it.
520
+ *
521
+ * Consulted ONLY when the wire actually carried a rider, so a current state whose digest
522
+ * has genuinely drifted still fails rather than finding a second way to pass.
523
+ */
524
+ export function legacyRiderStateSha256(state: ActiveContextState, rider: unknown): string {
525
+ const legacy: Record<string, unknown> = {};
526
+ for (const [key, value] of Object.entries(state)) {
527
+ if (key === "prepared") legacy.rider = rider;
528
+ legacy[key] = value;
529
+ }
530
+ if (!Object.prototype.hasOwnProperty.call(legacy, "rider")) legacy.rider = rider;
531
+ return sha256Value(legacy);
532
+ }
533
+
444
534
  export function legacyReplayOrderStateSha256(state: ActiveContextState): string {
445
535
  return sha256Value({
446
536
  version: state.version,
@@ -456,11 +546,18 @@ export function legacyReplayOrderStateSha256(state: ActiveContextState): string
456
546
 
457
547
  export function sameStateProjection(left: ActiveContextState, right: ActiveContextState): boolean {
458
548
  const normalized = (value: ActiveContextState): Partial<ActiveContextState> => {
459
- 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>;
460
555
  if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
461
556
  if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
462
557
  if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
463
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;
464
561
  return normalizedState;
465
562
  };
466
563
  return stableStringify(normalized(left)) === stableStringify(normalized(right));
@@ -503,13 +600,11 @@ export function validateV2ProjectionFields(
503
600
  leasesValue: unknown,
504
601
  pendingMarksValue?: unknown,
505
602
  briefsValue?: unknown,
506
- riderValue?: unknown,
507
603
  ): {
508
604
  expanded: string[];
509
605
  protected: EvidenceRef[];
510
606
  prepared?: PreparedFold;
511
607
  advisory?: NonNullable<ActiveContextState["advisory"]>;
512
- rider?: NonNullable<ActiveContextState["rider"]>;
513
608
  tokensSinceToolFold: number;
514
609
  leases: Record<string, number>;
515
610
  pendingMarks: PendingMark[];
@@ -527,9 +622,6 @@ export function validateV2ProjectionFields(
527
622
  if (advisoryValue !== undefined && !validAdvisoryState(advisoryValue)) {
528
623
  throw new Error("Invalid active-context v2 advisory state");
529
624
  }
530
- if (riderValue !== undefined && !validRiderState(riderValue)) {
531
- throw new Error("Invalid active-context v2 rider state");
532
- }
533
625
  if (tokensSinceToolFoldValue !== undefined && !validTokensSinceToolFold(tokensSinceToolFoldValue)) {
534
626
  throw new Error("Invalid active-context v2 tool-fold cadence");
535
627
  }
@@ -543,9 +635,6 @@ export function validateV2ProjectionFields(
543
635
  ...(advisoryValue === undefined ? {} : {
544
636
  advisory: clone(advisoryValue) as NonNullable<ActiveContextState["advisory"]>,
545
637
  }),
546
- ...(riderValue === undefined ? {} : {
547
- rider: clone(riderValue) as NonNullable<ActiveContextState["rider"]>,
548
- }),
549
638
  tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
550
639
  leases,
551
640
  pendingMarks,
@@ -575,7 +664,15 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
575
664
  Object.prototype.hasOwnProperty.call(value, "addBriefs"));
576
665
  const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
577
666
  Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
578
- const hasRider = Boolean(value && typeof value === "object" &&
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"));
675
+ const hasLegacyRider = Boolean(value && typeof value === "object" &&
579
676
  Object.prototype.hasOwnProperty.call(value, "rider"));
580
677
  refuseRetiredStateFields(value);
581
678
  const optionalKeys = [
@@ -589,7 +686,13 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
589
686
  ...(hasBriefs ? ["briefs"] : []),
590
687
  ...(hasAddBriefs ? ["addBriefs"] : []),
591
688
  ...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
592
- ...(hasRider ? ["rider"] : []),
689
+ ...(hasWireClips ? ["clips"] : []),
690
+ ...(hasClipBase ? ["clipBase"] : []),
691
+ ...(hasAddClips ? ["addClips"] : []),
692
+ ...(hasWireMemory ? ["memory"] : []),
693
+ // Carried no further than the digest check in `materializeStatePersistence`, which is
694
+ // the only thing on a load that the rider's absence can change.
695
+ ...(hasLegacyRider ? ["rider"] : []),
593
696
  ];
594
697
  const checkpoint = kind === "checkpoint" &&
595
698
  exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
@@ -605,8 +708,19 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
605
708
  ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
606
709
  ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
607
710
  ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
608
- ownValue(value, "rider"),
609
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");
610
724
  if (checkpoint) {
611
725
  const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
612
726
  if (!refs || refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Invalid active-context checkpoint refs");
@@ -681,7 +795,25 @@ export function normalizeFoldsForPersistedRecords(
681
795
  return deriveFoldParents(normalized);
682
796
  }
683
797
 
684
- 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[] {
685
817
  const folds = records.map(canonicalFoldRecord);
686
818
  const byId = new Map(folds.map((fold) => [fold.id, fold]));
687
819
  const assigned = new Set<string>();
@@ -693,14 +825,19 @@ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
693
825
  assigned.add(childId);
694
826
  }
695
827
  }
696
- 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));
697
834
  }
698
835
 
699
836
  export function stateFromFoldRefs(
700
837
  wire: Pick<
701
838
  ActiveContextCheckpointV2,
702
839
  "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
703
- "tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "rider"
840
+ "tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips" | "memory"
704
841
  >,
705
842
  refs: FoldRecordRef[],
706
843
  records: Map<string, FoldRecordEntry>,
@@ -715,15 +852,18 @@ export function stateFromFoldRefs(
715
852
  version: 1,
716
853
  sessionId: wire.sessionId,
717
854
  revision: wire.revision,
718
- 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),
719
858
  expanded: clone(wire.expanded),
720
859
  protected: clone(wire.protected),
721
860
  tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
722
861
  leases: clone(wire.leases ?? {}),
723
862
  ...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
724
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) } : {}),
725
866
  ...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
726
- ...(wire.rider === undefined ? {} : { rider: clone(wire.rider) }),
727
867
  ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
728
868
  };
729
869
  return parseActiveContextState(state, wire.sessionId, false);
@@ -793,6 +933,14 @@ export function materializeStatePersistence(
793
933
  },
794
934
  };
795
935
  let stateSha256 = semanticStateSha256(state);
936
+ // WHAT THE LAST ACCEPTED ENTRY ACTUALLY RECORDED, when that is not what this build
937
+ // would compute. A state accepted through one of the legacy derivations below was
938
+ // written by a build that hashed it differently, and the delta that follows it names
939
+ // its base by THAT digest, so a chain of them is unreadable unless the reading carries
940
+ // both: the modern digest for what this build writes next, and the written one for what
941
+ // the next entry will name. Null whenever they agree, which is every state this build
942
+ // wrote itself, so the chain check below is unchanged for them.
943
+ let writtenSha256: string | null = null;
796
944
  let stateStart = -1;
797
945
  let checkpointIndex = -1;
798
946
  let v2Seen = false;
@@ -845,11 +993,21 @@ export function materializeStatePersistence(
845
993
  if (wire.kind === "checkpoint") {
846
994
  state = stateFromFoldRefs(wire, wire.foldRefs, records);
847
995
  } else {
848
- if (wireVersion === 0 || wire.baseRevision !== state.revision || wire.baseStateSha256 !== stateSha256 ||
996
+ if (wireVersion === 0 || wire.baseRevision !== state.revision ||
997
+ (wire.baseStateSha256 !== stateSha256 && wire.baseStateSha256 !== writtenSha256) ||
849
998
  wire.revision <= wire.baseRevision) {
850
999
  throw new Error("Broken active-context delta chain");
851
1000
  }
852
- 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
+ });
853
1011
  const byId = new Map(refs.map((ref) => [ref.id, ref]));
854
1012
  for (const id of wire.removeFoldIds) {
855
1013
  if (!byId.delete(id)) throw new Error(`Unknown active-context delta removal ${id}`);
@@ -905,24 +1063,43 @@ export function materializeStatePersistence(
905
1063
  return mark;
906
1064
  });
907
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
+ }
908
1079
  state = stateFromFoldRefs(
909
1080
  {
910
1081
  ...wire,
911
1082
  briefs: Object.keys(briefs).length ? briefs : undefined,
912
1083
  pendingMarks: marks.length ? marks : undefined,
1084
+ clips: clips.length ? clips : undefined,
1085
+ memory: wire.memory?.length ? clone(wire.memory) : undefined,
913
1086
  },
914
1087
  [...byId.values()],
915
1088
  records,
916
1089
  );
917
1090
  }
918
1091
  const calculated = semanticStateSha256(state);
1092
+ const legacyRider = (wire as { rider?: unknown }).rider;
919
1093
  if (calculated !== wire.stateSha256 &&
920
1094
  legacyCadenceOmittedStateSha256(state) !== wire.stateSha256 &&
921
- legacyReplayOrderStateSha256(state) !== wire.stateSha256) {
1095
+ legacyReplayOrderStateSha256(state) !== wire.stateSha256 &&
1096
+ (legacyRider === undefined ||
1097
+ legacyRiderStateSha256(state, legacyRider) !== wire.stateSha256)) {
922
1098
  throw new Error("Active-context v2 state digest drift");
923
1099
  }
924
1100
  wireVersion = 2;
925
1101
  stateSha256 = calculated;
1102
+ writtenSha256 = wire.stateSha256 === calculated ? null : wire.stateSha256;
926
1103
  rememberProjection();
927
1104
  }
928
1105
  return { state, wireVersion, records, stateSha256, projectionFingerprints };
@@ -965,8 +1142,9 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
965
1142
  leases: clone(state.leases),
966
1143
  ...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
967
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) } : {}),
968
1147
  ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
969
- ...(state.rider ? { rider: clone(state.rider) } : {}),
970
1148
  stateSha256: semanticStateSha256(state),
971
1149
  }, state.sessionId) as ActiveContextCheckpointV2;
972
1150
  }
@@ -1013,6 +1191,23 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
1013
1191
  return !held || sha256Value(held) !== sha256Value(mark);
1014
1192
  });
1015
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;
1016
1211
  // The rest travel whole and stay that way. Measured on the same run, the two that were
1017
1212
  // worth a diff were 20.0 MB of the 21.6 MB ledger; what remained were bounded objects
1018
1213
  // with their own caps rather than collections that grow with the epoch (the largest,
@@ -1038,14 +1233,44 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
1038
1233
  ...(addPendingMarks.length ? { addPendingMarks: clone(addPendingMarks) } : {}),
1039
1234
  ...(Object.keys(addBriefs).length ? { addBriefs } : {}),
1040
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) } : {}),
1041
1240
  ...(next.advisory ? { advisory: clone(next.advisory) } : {}),
1042
- ...(next.rider ? { rider: clone(next.rider) } : {}),
1043
1241
  stateSha256: semanticStateSha256(next),
1044
1242
  }, next.sessionId) as ActiveContextDeltaV2;
1045
1243
  }
1046
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
+
1047
1267
  export function topologySha256(state: Pick<ActiveContextState, "folds" | "expanded">): string {
1048
- 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;
1049
1274
  }
1050
1275
 
1051
1276
  export function protectionSha256(state: Pick<ActiveContextState, "protected">): string {
@@ -1056,12 +1281,13 @@ export const ADVISORY_MILESTONES = Object.freeze(
1056
1281
  ["orientation", "notice", "tools", "chapters", "urgent"] as AdvisoryMilestone[],
1057
1282
  );
1058
1283
 
1059
- export const MAX_RIDER_TEXT_BYTES = 4_096;
1060
-
1061
1284
  /**
1062
1285
  * THE RETIRED STATE FIELDS, REFUSED BY NAME.
1063
1286
  *
1064
- * `lastCall` and `notices` were the occupancy thermostat's two announcements. Nothing
1287
+ * `lastCall` and `notices` were the occupancy thermostat's two announcements, and `rider`
1288
+ * was the post-commit invitation to create marks, deleted 2026-08-23 with the curation
1289
+ * redesign: the runtime cuts and the agent edits, so an invitation to create is a carrier
1290
+ * with nothing left to invite. Nothing
1065
1291
  * writes them any more and nothing reads them, so a durable state that still carries one
1066
1292
  * was written by a build that had them, and the exact-record readers would reject it with
1067
1293
  * "Invalid active-context state keys" -- true, and useless to whoever has to act on it.
@@ -1071,6 +1297,15 @@ export const MAX_RIDER_TEXT_BYTES = 4_096;
1071
1297
  * that comes back is not the state that was written. Gate 17 already states the wire's
1072
1298
  * policy in the other direction -- an older exact-record reader rejects newer fields, and
1073
1299
  * a bump is explicit -- and this is that same policy read from the newer side.
1300
+ *
1301
+ * `rider` IS NOT ON THIS LIST, and the distinction is the whole point of the list. The
1302
+ * refusal's own reason is "a state it cannot reproduce", so a field the build CAN
1303
+ * reproduce is out of its scope: the rider was projection carrier text, it changed no
1304
+ * fold, no ref and no brief, and the only thing it ever touched that survives a load is
1305
+ * the state digest. So it is read, spent on the digest, and dropped, which is what
1306
+ * `legacyRiderStateSha256` below is for. Retiring it by name instead made every state the
1307
+ * sol-20260815-hidden campaign sealed unreadable and cost the hidden-mass result its 102
1308
+ * attributed carriage rows, which is a high price for a field that carried no behaviour.
1074
1309
  */
1075
1310
  const RETIRED_STATE_FIELDS: readonly string[] = Object.freeze(["lastCall", "notices", "surfacing"]);
1076
1311
 
@@ -1086,14 +1321,6 @@ export function refuseRetiredStateFields(value: unknown): void {
1086
1321
  );
1087
1322
  }
1088
1323
 
1089
- export function validRiderState(value: unknown): value is NonNullable<ActiveContextState["rider"]> {
1090
- if (!exactRecord(value, ["epoch", "text"])) return false;
1091
- const epoch = ownValue(value, "epoch");
1092
- const text = ownValue(value, "text");
1093
- return Number.isSafeInteger(epoch) && Number(epoch) >= 0 &&
1094
- typeof text === "string" && text.length > 0 && text.length <= MAX_RIDER_TEXT_BYTES;
1095
- }
1096
-
1097
1324
  export function validAdvisoryState(value: unknown): value is NonNullable<ActiveContextState["advisory"]> {
1098
1325
  const hasArmed = Boolean(value && typeof value === "object" &&
1099
1326
  Object.prototype.hasOwnProperty.call(value, "armed"));