pi-fold 4.1.2 → 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 {
@@ -225,6 +226,7 @@ export * from "./lib/persistence.ts";
225
226
  export * from "./lib/policy.ts";
226
227
  export * from "./lib/rollback.ts";
227
228
  export * from "./lib/scheduling.ts";
229
+ export * from "./lib/status-widget.ts";
228
230
  export * from "./lib/selection.ts";
229
231
  export * from "./lib/transcript.ts";
230
232
 
@@ -746,11 +748,18 @@ export function registerActiveContext(pi: any, options: {
746
748
  };
747
749
  }, { placement: "belowEditor" });
748
750
  };
749
- const foldBarModel = (input: {
751
+ const foldBarModel = (ctx: any, input: {
750
752
  share: number | null; budgetTokens: number; staged: number; roots: number; weighed: boolean;
751
753
  }): FoldBarModel => {
752
754
  const state = persistence.state;
753
- const snapshot = lifecycle.latestSnapshot;
755
+ // THE SAME SNAPSHOT /fold-status PRICES AGAINST. `markFreedBytes` answers ZERO, silently,
756
+ // for a span it cannot locate contiguously in the snapshot it is handed, and the last
757
+ // context event's snapshot can trail the branch the marks were cut from; the bar read
758
+ // "5 staged, 0 to free" beside a status command pricing the same marks at 41,877
759
+ // tokens (Shane, 2026-09-02, session 01a06272). The authoritative snapshot is memoized
760
+ // on the branch length, so reading it here costs nothing while nothing has changed.
761
+ let snapshot = lifecycle.latestSnapshot;
762
+ try { snapshot = authoritativeSnapshotFor(ctx); } catch { }
754
763
  let stagedTokens = 0;
755
764
  let placeholderChars = 0;
756
765
  let hiddenChars = 0;
@@ -801,7 +810,7 @@ export function registerActiveContext(pi: any, options: {
801
810
  try {
802
811
  installFoldBar(ctx);
803
812
  if (foldBar.installed) {
804
- foldBar.model = foldBarModel({
813
+ foldBar.model = foldBarModel(ctx, {
805
814
  share, budgetTokens: capacity.budgetTokens, staged, roots,
806
815
  weighed: ladder.bandTopMeasurement !== null &&
807
816
  ladder.bandTopMeasurement === measurements.lastProviderMeasurement,
@@ -2520,6 +2529,11 @@ export function registerActiveContext(pi: any, options: {
2520
2529
  state: persistence.state!,
2521
2530
  generation: generationAtStart,
2522
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
+ }
2523
2537
  return { preparedFold, nextState };
2524
2538
  };
2525
2539
 
@@ -2761,6 +2775,16 @@ export function registerActiveContext(pi: any, options: {
2761
2775
  clips: [...(result.state.clips ?? []), ...clipAdditions] } };
2762
2776
  }
2763
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
+ }
2764
2788
  persistence.state = result.state;
2765
2789
  const bytesAfter = pricedBytes(projectActiveContext(snapshot, persistence.state));
2766
2790
  const freedBytes = Math.max(0, bytesBefore - bytesAfter);
@@ -2796,6 +2820,7 @@ export function registerActiveContext(pi: any, options: {
2796
2820
  closing_consolidation_marks: closingAdded,
2797
2821
  absorbed_wedges: wedges.absorbed.length,
2798
2822
  clipped_results: clippedResults,
2823
+ adopted_records: adoption.adopted.length,
2799
2824
  freed_bytes: freedBytes,
2800
2825
  freed_tokens: estimatedTokens(freedBytes),
2801
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)) {
@@ -66,11 +66,33 @@ export interface FoldBarTheme {
66
66
  fg(color: "dim" | "muted" | "text" | "warning" | "error", text: string): string;
67
67
  bold(text: string): string;
68
68
  getColorMode?(): "truecolor" | "256color";
69
+ getFgAnsi?(color: "text"): string;
70
+ name?: string;
69
71
  }
70
72
 
71
- // Crameri batlow10, classes 3, 6 and 8 (0-indexed). The two darkest classes are unusable
72
- // as fill on a dark terminal, and the lightest washes out on a light one.
73
- const BATLOW = { raw: "#3C6D56", staged: "#D29343", folded: "#FDB7BC" } as const;
73
+ // Crameri batlow10, three classes per background. Classes 3, 6 and 8 (0-indexed) read on
74
+ // a dark terminal; the lightest of them is 1.7:1 against white, so a light terminal steps
75
+ // two classes darker (2, 5 and 6), the same triple the README's light figure uses.
76
+ const BATLOW_ON_DARK = { raw: "#3C6D56", staged: "#D29343", folded: "#FDB7BC" } as const;
77
+ const BATLOW_ON_LIGHT = { raw: "#1C5A62", staged: "#9D892B", folded: "#D29343" } as const;
78
+
79
+ /**
80
+ * WHICH WAY THE BACKGROUND GOES, read off the theme's own text colour rather than its
81
+ * name: a theme whose body text is dark is drawn on a light background. pi hands the
82
+ * text colour out as a truecolor escape, and a theme that does not (256-colour mode,
83
+ * or a host without the accessor) falls back to the name, then to dark.
84
+ */
85
+ export function lightBackground(theme: FoldBarTheme): boolean {
86
+ try {
87
+ const ansi = theme.getFgAnsi?.("text") ?? "";
88
+ const match = /38;2;(\d+);(\d+);(\d+)/.exec(ansi);
89
+ if (match) {
90
+ const [r, g, b] = match.slice(1).map((v) => Number(v) / 255);
91
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.5;
92
+ }
93
+ } catch { }
94
+ return /light/i.test(theme.name ?? "");
95
+ }
74
96
 
75
97
  const truecolor = (hex: string, text: string): string => {
76
98
  const r = parseInt(hex.slice(1, 3), 16);
@@ -140,6 +162,7 @@ export function renderFoldBar(model: FoldBarModel, width: number, theme: FoldBar
140
162
  width, theme.fg("dim", "..."));
141
163
  }
142
164
  const rich = theme.getColorMode?.() === "truecolor";
165
+ const BATLOW = lightBackground(theme) ? BATLOW_ON_LIGHT : BATLOW_ON_DARK;
143
166
  const ink: Record<Cell, (text: string) => string> = {
144
167
  raw: (t) => rich ? truecolor(BATLOW.raw, t) : theme.fg("muted", t),
145
168
  staged: (t) => rich ? truecolor(BATLOW.staged, t) : theme.fg("text", t),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fold",
3
- "version": "4.1.2",
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",