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 go to a fold store byte for byte, addressed by their SHA-256 hash. The brief carries a handle, the handle resolves to the original, and expansion restores the exact bytes after verifying the hash.
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,
@@ -99,7 +99,8 @@ export type ContextEventKind =
99
99
  | "context.projection"
100
100
  | "context.usage"
101
101
  | "context.prefix"
102
- | "context.anchor";
102
+ | "context.anchor"
103
+ | "context.drift";
103
104
 
104
105
  export function prefixDivergence(
105
106
  previous: string | null,
@@ -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) && item.ref!.sha256 === ref.sha256 &&
56
- evidenceSha256(item.message) === ref.sha256 ? item : null;
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fold",
3
- "version": "4.2.4",
3
+ "version": "4.2.5",
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",