pi-fold 4.2.4 → 4.2.6
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,
|
|
@@ -1156,6 +1157,18 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1156
1157
|
}
|
|
1157
1158
|
};
|
|
1158
1159
|
|
|
1160
|
+
// WHAT PI'S CONTEXT EVENT WOULD HAND OVER, BEFORE ONE HAS ARRIVED (2026-10-05). Pi's
|
|
1161
|
+
// ExtensionRunner.emitContext filters role "system" before any handler sees a message;
|
|
1162
|
+
// buildSessionContext does not. A `/fold` before the first context event cut folds from
|
|
1163
|
+
// the unfiltered view in session 01a10161: spans that crossed a system-prompt change
|
|
1164
|
+
// looked broken, and that commit erased 739 of 920 folds.
|
|
1165
|
+
const piContextMessages = (ctx: any): unknown[] | null => {
|
|
1166
|
+
const messages = ctx.sessionManager.buildSessionContext?.()?.messages;
|
|
1167
|
+
return Array.isArray(messages)
|
|
1168
|
+
? messages.filter((message: any) => message?.role !== "system")
|
|
1169
|
+
: null;
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1159
1172
|
const snapshotForEvent = (ctx: any, messages: unknown[]): ActiveContextSnapshot =>
|
|
1160
1173
|
timedDerivation(() => mapActiveContext({
|
|
1161
1174
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -1351,7 +1364,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1351
1364
|
persistence.deltasSinceCheckpoint = restoredPersistence?.deltasSinceCheckpoint ?? 0;
|
|
1352
1365
|
persistence.persistedFoldRecords = restoredPersistence?.records ?? new Map<string, FoldRecordEntry>();
|
|
1353
1366
|
persistence.persistedStateSha256 = restoredPersistence?.stateSha256 ?? semanticStateSha256(durableRestored);
|
|
1354
|
-
const restoredMessages = ctx
|
|
1367
|
+
const restoredMessages = piContextMessages(ctx);
|
|
1355
1368
|
measurements.lastProviderMeasurement = latestProviderContextMeasurement(
|
|
1356
1369
|
Array.isArray(restoredMessages) ? restoredMessages : [],
|
|
1357
1370
|
budgetWindowFor(ctx),
|
|
@@ -3396,6 +3409,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3396
3409
|
return changed;
|
|
3397
3410
|
};
|
|
3398
3411
|
|
|
3412
|
+
// A FOLDED ENTRY WHOSE BYTES CHANGED (2026-10-05). The fold still stands over it, by Pi's
|
|
3413
|
+
// identity (lib/measurement exactMapped); what changed is recorded once per entry per
|
|
3414
|
+
// process, so the drift is on the record instead of silently unseating the fold.
|
|
3415
|
+
const reportedDrift = new Set<string>();
|
|
3416
|
+
const recordContentDrift = (snapshot: ActiveContextSnapshot): void => {
|
|
3417
|
+
const state = persistence.state;
|
|
3418
|
+
if (!state) return;
|
|
3419
|
+
const drifted: string[] = [];
|
|
3420
|
+
for (const fold of state.folds) {
|
|
3421
|
+
for (const part of fold.parts) {
|
|
3422
|
+
if (part.kind !== "raw" || reportedDrift.has(part.ref.entryId)) continue;
|
|
3423
|
+
if (!contentDrifted(snapshot, part.ref)) continue;
|
|
3424
|
+
reportedDrift.add(part.ref.entryId);
|
|
3425
|
+
drifted.push(part.ref.entryId);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
if (drifted.length) emit("context.drift", { entries: drifted.length, entry_ids: drifted.slice(0, 64) });
|
|
3429
|
+
};
|
|
3399
3430
|
const handleContext = async (event: { messages: unknown[] }, ctx: any) => {
|
|
3400
3431
|
if (lifecycle.shuttingDown) return { messages: event.messages };
|
|
3401
3432
|
beginMutationPass();
|
|
@@ -3412,6 +3443,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3412
3443
|
const snapshot = snapshotForEvent(ctx, event.messages);
|
|
3413
3444
|
lifecycle.latestSnapshot = snapshot;
|
|
3414
3445
|
foldBar.startupSnapshot = null;
|
|
3446
|
+
recordContentDrift(snapshot);
|
|
3415
3447
|
if (ladder.automaticFailure) {
|
|
3416
3448
|
ladder.automaticFailure.suppressedCallbacks = Math.min(
|
|
3417
3449
|
Number.MAX_SAFE_INTEGER,
|
|
@@ -3877,6 +3909,26 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3877
3909
|
try { updateStatus(ctx); } catch { }
|
|
3878
3910
|
return undefined;
|
|
3879
3911
|
}
|
|
3912
|
+
// PI CHECKS BEFORE THE FIRST PROMPT, BEFORE ANY CONTEXT EVENT (2026-10-05). After a
|
|
3913
|
+
// context_edit (Pi's own retry recovery writes one per failed attempt) Pi stops trusting
|
|
3914
|
+
// the last provider count and estimates the whole raw transcript, folds unseen: 134% on
|
|
3915
|
+
// session 01a10834, 260% on 01a10161. This used to throw for want of a snapshot, suspend
|
|
3916
|
+
// folding and let native compaction run, which ran Pi out of memory on a 272MB session.
|
|
3917
|
+
// The context event that follows projects and commits on the projection itself, so the
|
|
3918
|
+
// boundary is cancelled and folding stays on.
|
|
3919
|
+
const currentSnapshot = lifecycle.latestSnapshot;
|
|
3920
|
+
if (!currentSnapshot || currentSnapshot.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
3921
|
+
nativeCompaction.lastThresholdDecision = {
|
|
3922
|
+
handled: true,
|
|
3923
|
+
retry: false,
|
|
3924
|
+
reason: `${contextBrand(brandNoun)} blocked stock automatic compaction before the first context event; ` +
|
|
3925
|
+
"the projection is measured when the request is built",
|
|
3926
|
+
compactionReason: reason,
|
|
3927
|
+
nativeCompactionCompleted: false,
|
|
3928
|
+
};
|
|
3929
|
+
try { updateStatus(ctx); } catch { }
|
|
3930
|
+
return { cancel: true };
|
|
3931
|
+
}
|
|
3880
3932
|
let handoff: Record<string, unknown> | null = null;
|
|
3881
3933
|
try {
|
|
3882
3934
|
const snapshot = authoritativeSnapshotFor(ctx);
|
|
@@ -4774,7 +4826,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4774
4826
|
if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
|
|
4775
4827
|
const snapshot = lifecycle.latestSnapshot
|
|
4776
4828
|
? authoritativeSnapshotFor(ctx)
|
|
4777
|
-
: snapshotForEvent(ctx, ctx
|
|
4829
|
+
: snapshotForEvent(ctx, piContextMessages(ctx) ?? []);
|
|
4778
4830
|
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
4779
4831
|
const divider = args.indexOf(" -- ");
|
|
4780
4832
|
const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
|
|
@@ -4884,7 +4936,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4884
4936
|
}
|
|
4885
4937
|
const snapshot = lifecycle.latestSnapshot
|
|
4886
4938
|
? authoritativeSnapshotFor(ctx)
|
|
4887
|
-
: snapshotForEvent(ctx, ctx
|
|
4939
|
+
: snapshotForEvent(ctx, piContextMessages(ctx) ?? []);
|
|
4888
4940
|
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
4889
4941
|
if (!persistence.state) throw new Error("The context window is empty; there is nothing to show yet");
|
|
4890
4942
|
|
|
@@ -50,14 +50,34 @@ 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
|
-
evidenceSha256(item.message) === ref.sha256 ? item : null;
|
|
63
|
+
return item && sameObjectIdentity(item.ref!, ref) ? item : null;
|
|
57
64
|
}
|
|
58
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;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// PI NEVER SHOWS AN EXTENSION ITS SYSTEM MESSAGES (2026-10-05). The context event filters
|
|
73
|
+
// role "system" (ExtensionRunner.emitContext), so a system-prompt-change entry has no
|
|
74
|
+
// place in the window and cannot break a span. Folds cut from a view that did include them
|
|
75
|
+
// (a `/fold` before the first context event, session 01a10161) carry such refs; they are
|
|
76
|
+
// skipped here rather than unseating the fold and, at the next commit, its whole subtree.
|
|
59
77
|
export function refsInOrder(snapshot: ActiveContextSnapshot, refs: EvidenceRef[]): number[] | null {
|
|
60
|
-
const
|
|
78
|
+
const placed = refs.filter((ref) => ref.role !== "system");
|
|
79
|
+
if (!placed.length) return null;
|
|
80
|
+
const indices = placed.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
|
|
61
81
|
if (indices.some((index) => index < 0) || new Set(indices).size !== indices.length) return null;
|
|
62
82
|
for (let index = 1; index < indices.length; index += 1) if (indices[index] !== indices[index - 1] + 1) return null;
|
|
63
83
|
return indices;
|