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
|
-
|
|
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)
|
|
72
|
-
//
|
|
73
|
-
|
|
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),
|