pi-fold 4.2.4 → 4.2.5
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.
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Compaction replaces the transcript with a summary and discards the originals. Fo
|
|
|
27
27
|
<img alt="Compaction turns the transcript into a summary and discards the originals, so the session starts over. pi-fold turns it into a brief and keeps the exact originals on disk, one peek or expand away, so the session keeps going." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/fold-vs-compaction-light.svg">
|
|
28
28
|
</picture>
|
|
29
29
|
|
|
30
|
-
A fold takes a contiguous span of session entries and replaces it, in the window only, with a short brief. The entries
|
|
30
|
+
A fold takes a contiguous span of session entries and replaces it, in the window only, with a short brief. The entries stay in Pi's session file byte for byte; the fold finds them by Pi's own entry ids and records each one's SHA-256 hash when it folds. The brief carries a handle, the handle resolves to the originals, and expansion restores them as Pi holds them. If an entry's bytes no longer match the recorded hash, the fold still stands and the change is logged as a `context.drift` event.
|
|
31
31
|
|
|
32
32
|
The package registers one tool, `pi_fold_context`, with nine actions over the transcript: `status`, `peek`, `brief`, `expand`, `refold`, `pin`, `unpin`, `reboundary` and `unmark`. A session that never calls the tool still folds; it degrades into lossless hierarchical compaction.
|
|
33
33
|
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
latestProviderContextMeasurement,
|
|
53
53
|
orderedRoots,
|
|
54
54
|
parseNativeCompactionCompletion,
|
|
55
|
+
contentDrifted,
|
|
55
56
|
exactMapped,
|
|
56
57
|
foldInterval,
|
|
57
58
|
parseNativeCompactionDecision,
|
|
@@ -3396,6 +3397,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3396
3397
|
return changed;
|
|
3397
3398
|
};
|
|
3398
3399
|
|
|
3400
|
+
// A FOLDED ENTRY WHOSE BYTES CHANGED (2026-10-05). The fold still stands over it, by Pi's
|
|
3401
|
+
// identity (lib/measurement exactMapped); what changed is recorded once per entry per
|
|
3402
|
+
// process, so the drift is on the record instead of silently unseating the fold.
|
|
3403
|
+
const reportedDrift = new Set<string>();
|
|
3404
|
+
const recordContentDrift = (snapshot: ActiveContextSnapshot): void => {
|
|
3405
|
+
const state = persistence.state;
|
|
3406
|
+
if (!state) return;
|
|
3407
|
+
const drifted: string[] = [];
|
|
3408
|
+
for (const fold of state.folds) {
|
|
3409
|
+
for (const part of fold.parts) {
|
|
3410
|
+
if (part.kind !== "raw" || reportedDrift.has(part.ref.entryId)) continue;
|
|
3411
|
+
if (!contentDrifted(snapshot, part.ref)) continue;
|
|
3412
|
+
reportedDrift.add(part.ref.entryId);
|
|
3413
|
+
drifted.push(part.ref.entryId);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
if (drifted.length) emit("context.drift", { entries: drifted.length, entry_ids: drifted.slice(0, 64) });
|
|
3417
|
+
};
|
|
3399
3418
|
const handleContext = async (event: { messages: unknown[] }, ctx: any) => {
|
|
3400
3419
|
if (lifecycle.shuttingDown) return { messages: event.messages };
|
|
3401
3420
|
beginMutationPass();
|
|
@@ -3412,6 +3431,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3412
3431
|
const snapshot = snapshotForEvent(ctx, event.messages);
|
|
3413
3432
|
lifecycle.latestSnapshot = snapshot;
|
|
3414
3433
|
foldBar.startupSnapshot = null;
|
|
3434
|
+
recordContentDrift(snapshot);
|
|
3415
3435
|
if (ladder.automaticFailure) {
|
|
3416
3436
|
ladder.automaticFailure.suppressedCallbacks = Math.min(
|
|
3417
3437
|
Number.MAX_SAFE_INTEGER,
|
|
@@ -50,10 +50,23 @@ export function mappedByKey(snapshot: ActiveContextSnapshot): Map<string, Mapped
|
|
|
50
50
|
return indexed;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// A FOLDED ENTRY IS FOUND BY PI'S IDENTITY, NOT BY ITS BYTES (2026-10-05). Pi names every
|
|
54
|
+
// entry (session, entry id, role) and the transcript it hands back is the truth for that
|
|
55
|
+
// entry. This also required the content digest recorded at fold time to match, and one
|
|
56
|
+
// assistant message in session 01a10834 hashed differently live than as persisted (1 of
|
|
57
|
+
// 3,686 refs, cause unidentified): on resume its root could not be seated, the commit
|
|
58
|
+
// that followed erased the root's 377 folds, and the request went out at ~1.49M tokens
|
|
59
|
+
// against a 1M window. A fold over an entry whose bytes changed now stands over Pi's
|
|
60
|
+
// current bytes; `contentDrifted` names the case so it is recorded rather than silent.
|
|
53
61
|
export function exactMapped(snapshot: ActiveContextSnapshot, ref: EvidenceRef): MappedMessage | null {
|
|
54
62
|
const item = mappedByKey(snapshot).get(objectRefKey(ref));
|
|
55
|
-
return item && sameObjectIdentity(item.ref!, ref)
|
|
56
|
-
|
|
63
|
+
return item && sameObjectIdentity(item.ref!, ref) ? item : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The entry is where the ref says, but its bytes are not the ones it was folded with. */
|
|
67
|
+
export function contentDrifted(snapshot: ActiveContextSnapshot, ref: EvidenceRef): boolean {
|
|
68
|
+
const item = exactMapped(snapshot, ref);
|
|
69
|
+
return item !== null && item.ref!.sha256 !== ref.sha256;
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
export function refsInOrder(snapshot: ActiveContextSnapshot, refs: EvidenceRef[]): number[] | null {
|