pi-fold 4.1.3 → 4.1.4

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.
@@ -89,6 +89,7 @@ import {
89
89
  sameStateProjection,
90
90
  semanticStateSha256,
91
91
  topologySha256,
92
+ adoptDurableFoldRecords,
92
93
  } from "./lib/persistence.ts";
93
94
  import type { MaterializedStatePersistence } from "./lib/persistence.ts";
94
95
  import {
@@ -2528,6 +2529,11 @@ export function registerActiveContext(pi: any, options: {
2528
2529
  state: persistence.state!,
2529
2530
  generation: generationAtStart,
2530
2531
  });
2532
+ const adoption = adoptDurableFoldRecords(nextState.folds, persistence.persistedFoldRecords);
2533
+ if (adoption.adopted.length) {
2534
+ emit("context.adopt", { adopted: adoption.adopted.length, fold_ids: adoption.adopted.slice(0, 64) });
2535
+ return { preparedFold, nextState: { ...nextState, folds: adoption.folds } };
2536
+ }
2531
2537
  return { preparedFold, nextState };
2532
2538
  };
2533
2539
 
@@ -2769,6 +2775,16 @@ export function registerActiveContext(pi: any, options: {
2769
2775
  clips: [...(result.state.clips ?? []), ...clipAdditions] } };
2770
2776
  }
2771
2777
  }
2778
+ // A RE-CUT SPAN ADOPTS ITS DURABLE RECORD (persistence.ts, adoptDurableFoldRecords):
2779
+ // a fold minted here under an id the store already holds, with the same source, takes
2780
+ // the record's bytes before the projection is measured, so the persist path finds
2781
+ // equal digests where session 01a06746 found "Conflicting durable active-context
2782
+ // fold" and stopped folding for good.
2783
+ const adoption = adoptDurableFoldRecords(result.state.folds, persistence.persistedFoldRecords);
2784
+ if (adoption.adopted.length) {
2785
+ result = { ...result, state: { ...result.state, folds: adoption.folds } };
2786
+ emit("context.adopt", { adopted: adoption.adopted.length, fold_ids: adoption.adopted.slice(0, 64) });
2787
+ }
2772
2788
  persistence.state = result.state;
2773
2789
  const bytesAfter = pricedBytes(projectActiveContext(snapshot, persistence.state));
2774
2790
  const freedBytes = Math.max(0, bytesBefore - bytesAfter);
@@ -2804,6 +2820,7 @@ export function registerActiveContext(pi: any, options: {
2804
2820
  closing_consolidation_marks: closingAdded,
2805
2821
  absorbed_wedges: wedges.absorbed.length,
2806
2822
  clipped_results: clippedResults,
2823
+ adopted_records: adoption.adopted.length,
2807
2824
  freed_bytes: freedBytes,
2808
2825
  freed_tokens: estimatedTokens(freedBytes),
2809
2826
  rewrite_tokens: rewriteTokens,
@@ -789,11 +789,44 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
789
789
  return parsed;
790
790
  }
791
791
 
792
+ /**
793
+ * A RE-CUT SPAN ADOPTS ITS DURABLE RECORD (2026-09-04).
794
+ *
795
+ * A fold's id is derived from its parts, so the same span cut twice mints the same id,
796
+ * and a durable record under an id is immutable by design. Those two facts met in
797
+ * session 01a06746: a persistence projection on the first pass after a restart could
798
+ * not seat one consolidation root and dropped it with its 30 children, the spans read
799
+ * as raw, the frontier cut them again, and the re-cut folds differed from their records
800
+ * by 25 bytes of placeholder text. `normalizeFoldsForPersistedRecords` and the persist
801
+ * path both refused the conflict, folding suspended, and every restart walked into the
802
+ * same refusal because the durable state had already lost the folds.
803
+ *
804
+ * The record is the canonical fold for its source. A fold whose id has a record with
805
+ * the SAME kind and SAME source hash takes the record's bytes (brief, provenance,
806
+ * placeholder size, createdAt) and keeps only its own parent link, so the persist path
807
+ * finds equal digests and the session goes on. A record under the same id with a
808
+ * DIFFERENT source is a corrupt store and still refuses by name.
809
+ */
810
+ export function adoptDurableFoldRecords(
811
+ folds: ActiveFold[],
812
+ records: Map<string, FoldRecordEntry>,
813
+ ): { folds: ActiveFold[]; adopted: string[] } {
814
+ const adopted: string[] = [];
815
+ const next = folds.map((fold) => {
816
+ const record = records.get(fold.id);
817
+ if (!record || sameFoldRecordIdentity(fold, record.fold)) return fold;
818
+ if (record.fold.kind !== fold.kind || record.fold.sourceSha256 !== fold.sourceSha256) return fold;
819
+ adopted.push(fold.id);
820
+ return { ...clone(record.fold), parentId: fold.parentId };
821
+ });
822
+ return { folds: next, adopted };
823
+ }
824
+
792
825
  export function normalizeFoldsForPersistedRecords(
793
826
  folds: ActiveFold[],
794
827
  records: Map<string, FoldRecordEntry>,
795
828
  ): ActiveFold[] {
796
- const normalized = folds.map((fold) => {
829
+ const normalized = adoptDurableFoldRecords(folds, records).folds.map((fold) => {
797
830
  const record = records.get(fold.id);
798
831
  if (!record) return clone(fold);
799
832
  if (!sameFoldRecordIdentity(fold, record.fold)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fold",
3
- "version": "4.1.3",
3
+ "version": "4.1.4",
4
4
  "description": "Lossless context folding for the Pi coding agent: only the oldest context changes; the fresh window stays untouched.",
5
5
  "keywords": [
6
6
  "pi-package",