pi-fold 4.2.5 → 4.2.7
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.
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
objectRefKey,
|
|
5
5
|
sha256Value,
|
|
6
6
|
stableStringify,
|
|
7
|
+
type EvidenceRef,
|
|
7
8
|
} from "./json.ts";
|
|
8
9
|
import {
|
|
9
10
|
contentText,
|
|
@@ -77,6 +78,7 @@ import type {
|
|
|
77
78
|
ProviderMeasurementAnchor,
|
|
78
79
|
} from "./lib/measurement.ts";
|
|
79
80
|
import {
|
|
81
|
+
assignFoldParents,
|
|
80
82
|
childFoldIds,
|
|
81
83
|
clearPrepared,
|
|
82
84
|
deriveFoldParents,
|
|
@@ -86,6 +88,8 @@ import {
|
|
|
86
88
|
makeFoldRecordEntry,
|
|
87
89
|
makeStateCheckpoint,
|
|
88
90
|
makeStateDelta,
|
|
91
|
+
parseActiveContextState,
|
|
92
|
+
validateFoldForest,
|
|
89
93
|
MAX_ACTIVE_FOLD_RECORDS,
|
|
90
94
|
MAX_DELTAS_BETWEEN_CHECKPOINTS,
|
|
91
95
|
materializeStatePersistence,
|
|
@@ -136,10 +140,12 @@ import type {
|
|
|
136
140
|
ActiveContextSnapshot,
|
|
137
141
|
ActiveContextState,
|
|
138
142
|
ActiveContextThresholds,
|
|
143
|
+
ActiveFold,
|
|
139
144
|
ActiveContextToolAction,
|
|
140
145
|
BriefProvenance,
|
|
141
146
|
FoldCandidate,
|
|
142
147
|
FoldKind,
|
|
148
|
+
FoldPart,
|
|
143
149
|
FoldRecordEntry,
|
|
144
150
|
PreparedFold,
|
|
145
151
|
} from "./lib/policy.ts";
|
|
@@ -445,6 +451,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
445
451
|
* session that never had any, which is what `persistedWireVersion` 0 also means and
|
|
446
452
|
* which legitimately writes the first checkpoint. See `persist`. */
|
|
447
453
|
lineageUnreadable: false,
|
|
454
|
+
forkCheckpointPending: false,
|
|
448
455
|
/** Deltas standing after the newest checkpoint in the branch. See
|
|
449
456
|
* `MAX_DELTAS_BETWEEN_CHECKPOINTS`: at the bound, `persist` writes a checkpoint instead
|
|
450
457
|
* of a delta, which is what keeps a load's replay bounded. */
|
|
@@ -574,6 +581,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
574
581
|
wallEpisodeOpen: false,
|
|
575
582
|
recoveryAttempts: 0,
|
|
576
583
|
pendingRejection: null as { status: number; ordinal: number } | null,
|
|
584
|
+
restoreRevisionGap: false,
|
|
577
585
|
lastRecovery: null as Record<string, unknown> | null,
|
|
578
586
|
instrumentationQueue: Promise.resolve<void>(undefined),
|
|
579
587
|
};
|
|
@@ -1157,6 +1165,18 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1157
1165
|
}
|
|
1158
1166
|
};
|
|
1159
1167
|
|
|
1168
|
+
// WHAT PI'S CONTEXT EVENT WOULD HAND OVER, BEFORE ONE HAS ARRIVED (2026-10-05). Pi's
|
|
1169
|
+
// ExtensionRunner.emitContext filters role "system" before any handler sees a message;
|
|
1170
|
+
// buildSessionContext does not. A `/fold` before the first context event cut folds from
|
|
1171
|
+
// the unfiltered view in session 01a10161: spans that crossed a system-prompt change
|
|
1172
|
+
// looked broken, and that commit erased 739 of 920 folds.
|
|
1173
|
+
const piContextMessages = (ctx: any): unknown[] | null => {
|
|
1174
|
+
const messages = ctx.sessionManager.buildSessionContext?.()?.messages;
|
|
1175
|
+
return Array.isArray(messages)
|
|
1176
|
+
? messages.filter((message: any) => message?.role !== "system")
|
|
1177
|
+
: null;
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1160
1180
|
const snapshotForEvent = (ctx: any, messages: unknown[]): ActiveContextSnapshot =>
|
|
1161
1181
|
timedDerivation(() => mapActiveContext({
|
|
1162
1182
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -1172,6 +1192,146 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1172
1192
|
thresholds,
|
|
1173
1193
|
}));
|
|
1174
1194
|
|
|
1195
|
+
const forkEntriesForSession = (entries: any[], sessionId: string): any[] => {
|
|
1196
|
+
// A child checkpoint supersedes inherited records; keep child records and validate anything newer strictly.
|
|
1197
|
+
const checkpointIndex = entries.findLastIndex((entry) => entry?.type === "custom" &&
|
|
1198
|
+
entry.customType === stateEntryType && ownValue(entry.data, "sessionId") === sessionId &&
|
|
1199
|
+
ownValue(entry.data, "version") === 2 && ownValue(entry.data, "kind") === "checkpoint");
|
|
1200
|
+
if (checkpointIndex < 0) return entries;
|
|
1201
|
+
const inheritedOwners = new Set(entries.slice(0, checkpointIndex).flatMap((entry) => {
|
|
1202
|
+
if (entry?.type !== "custom" || entry.customType !== stateEntryType) return [];
|
|
1203
|
+
const owner = ownValue(entry.data, "sessionId");
|
|
1204
|
+
return typeof owner === "string" && owner !== sessionId ? [owner] : [];
|
|
1205
|
+
}));
|
|
1206
|
+
return entries.filter((entry, index) => {
|
|
1207
|
+
if (entry?.type !== "custom" || entry.customType !== foldRecordEntryType || index >= checkpointIndex) return true;
|
|
1208
|
+
const owner = ownValue(entry.data, "sessionId");
|
|
1209
|
+
return owner === sessionId || typeof owner !== "string" || !inheritedOwners.has(owner);
|
|
1210
|
+
});
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
const forkEntriesForOwner = (entries: any[], sessionId: string): any[] => entries.filter((entry) =>
|
|
1214
|
+
entry?.type !== "custom" || entry.customType !== foldRecordEntryType ||
|
|
1215
|
+
ownValue(entry.data, "sessionId") === sessionId,
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
const rebaseForkState = (
|
|
1219
|
+
state: ActiveContextState,
|
|
1220
|
+
snapshot: ActiveContextSnapshot,
|
|
1221
|
+
sessionId: string,
|
|
1222
|
+
): ActiveContextState => {
|
|
1223
|
+
const refsByEntryId = new Map<string, ActiveContextSnapshot["branchObjects"]>();
|
|
1224
|
+
for (const item of snapshot.branchObjects) {
|
|
1225
|
+
const refs = refsByEntryId.get(item.ref.entryId) ?? [];
|
|
1226
|
+
refs.push(item);
|
|
1227
|
+
refsByEntryId.set(item.ref.entryId, refs);
|
|
1228
|
+
}
|
|
1229
|
+
const rebaseRef = (ref: ActiveContextState["protected"][number]) => {
|
|
1230
|
+
const matches = refsByEntryId.get(ref.entryId)?.filter((item) =>
|
|
1231
|
+
item.ref.role === ref.role,
|
|
1232
|
+
) ?? [];
|
|
1233
|
+
if (matches.length !== 1) {
|
|
1234
|
+
throw new Error(`Inherited active-context evidence ${ref.entryId} is missing or ambiguous in the fork branch`);
|
|
1235
|
+
}
|
|
1236
|
+
// Pi's entry identity locates the source; preserve its recorded digest so the
|
|
1237
|
+
// runtime can report content drift instead of silently erasing its provenance.
|
|
1238
|
+
return { ...clone(ref), sessionId };
|
|
1239
|
+
};
|
|
1240
|
+
const rebaseParts = (parts: FoldPart[], resolveFoldId: (id: string) => string): FoldPart[] =>
|
|
1241
|
+
parts.map((part) => {
|
|
1242
|
+
if (part.kind === "raw") return { kind: "raw", ref: rebaseRef(part.ref) };
|
|
1243
|
+
return { kind: "fold", foldId: resolveFoldId(part.foldId) };
|
|
1244
|
+
});
|
|
1245
|
+
const sourceFolds = new Map(state.folds.map((fold) => [fold.id, fold]));
|
|
1246
|
+
const rebasedFolds = new Map<string, { fold: ActiveFold; refs: EvidenceRef[] }>();
|
|
1247
|
+
const visiting = new Set<string>();
|
|
1248
|
+
const rebaseFold = (id: string): { fold: ActiveFold; refs: EvidenceRef[] } => {
|
|
1249
|
+
const existing = rebasedFolds.get(id);
|
|
1250
|
+
if (existing) return existing;
|
|
1251
|
+
const source = sourceFolds.get(id);
|
|
1252
|
+
if (!source) throw new Error(`Inherited active-context fold ${id} is missing`);
|
|
1253
|
+
if (visiting.has(id)) throw new Error(`Inherited active-context fold cycle at ${id}`);
|
|
1254
|
+
visiting.add(id);
|
|
1255
|
+
const parts = rebaseParts(source.parts, (foldId) => rebaseFold(foldId).fold.id);
|
|
1256
|
+
const refs = source.parts.flatMap((part) => {
|
|
1257
|
+
if (part.kind === "raw") return [rebaseRef(part.ref)];
|
|
1258
|
+
return rebaseFold(part.foldId).refs;
|
|
1259
|
+
});
|
|
1260
|
+
const fold: ActiveFold = {
|
|
1261
|
+
...source,
|
|
1262
|
+
id: foldIdFor(source.kind, parts),
|
|
1263
|
+
parentId: null,
|
|
1264
|
+
parts,
|
|
1265
|
+
sourceSha256: sha256Value(refs),
|
|
1266
|
+
};
|
|
1267
|
+
const rebased = { fold, refs };
|
|
1268
|
+
visiting.delete(id);
|
|
1269
|
+
rebasedFolds.set(id, rebased);
|
|
1270
|
+
return rebased;
|
|
1271
|
+
};
|
|
1272
|
+
const folds = assignFoldParents(state.folds.map((fold) => rebaseFold(fold.id).fold));
|
|
1273
|
+
const rebasedFoldId = (id: string): string => {
|
|
1274
|
+
const rebased = rebasedFolds.get(id);
|
|
1275
|
+
if (!rebased) throw new Error(`Inherited active-context state references missing fold ${id}`);
|
|
1276
|
+
return rebased.fold.id;
|
|
1277
|
+
};
|
|
1278
|
+
let rebased: ActiveContextState;
|
|
1279
|
+
try {
|
|
1280
|
+
const expanded = state.expanded.map(rebasedFoldId);
|
|
1281
|
+
const leases = Object.fromEntries(Object.entries(state.leases).map(([id, value]) => [rebasedFoldId(id), value]));
|
|
1282
|
+
const briefs = Object.fromEntries(Object.entries(state.briefs ?? {}).map(([id, brief]) => [rebasedFoldId(id), brief]));
|
|
1283
|
+
const pendingMarks = (state.pendingMarks ?? []).map((mark) => {
|
|
1284
|
+
if (mark.mark === "refold") return { ...mark, id: rebasedFoldId(mark.id) };
|
|
1285
|
+
const parts = rebaseParts(mark.parts, rebasedFoldId);
|
|
1286
|
+
return { ...mark, id: foldIdFor(mark.kind, parts), parts };
|
|
1287
|
+
});
|
|
1288
|
+
rebased = clearPrepared({
|
|
1289
|
+
...state,
|
|
1290
|
+
sessionId,
|
|
1291
|
+
folds,
|
|
1292
|
+
expanded,
|
|
1293
|
+
protected: state.protected.map(rebaseRef),
|
|
1294
|
+
leases,
|
|
1295
|
+
...(state.pendingMarks ? { pendingMarks } : {}),
|
|
1296
|
+
...(state.briefs ? { briefs } : {}),
|
|
1297
|
+
});
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
// Keep forest errors ahead of curation errors without validating successful rebases twice.
|
|
1300
|
+
validateFoldForest(folds);
|
|
1301
|
+
throw error;
|
|
1302
|
+
}
|
|
1303
|
+
return parseActiveContextState(rebased, sessionId);
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
const prepareForkRecovery = (
|
|
1307
|
+
ctx: any,
|
|
1308
|
+
branchEntries: any[],
|
|
1309
|
+
sessionId: string,
|
|
1310
|
+
restoredMessages: unknown,
|
|
1311
|
+
restoreError: unknown,
|
|
1312
|
+
): { inherited: MaterializedStatePersistence; snapshot: ActiveContextSnapshot } => {
|
|
1313
|
+
const inheritedStateEntry = branchEntries.findLast((entry) =>
|
|
1314
|
+
entry?.type === "custom" && entry.customType === stateEntryType,
|
|
1315
|
+
);
|
|
1316
|
+
const inheritedSessionId = ownValue(inheritedStateEntry?.data, "sessionId");
|
|
1317
|
+
if (typeof inheritedSessionId !== "string" || !inheritedSessionId || inheritedSessionId === sessionId) {
|
|
1318
|
+
throw new Error("No distinct inherited session ID is available for fork recovery", { cause: restoreError });
|
|
1319
|
+
}
|
|
1320
|
+
const inherited = materializeStatePersistence(
|
|
1321
|
+
forkEntriesForOwner(branchEntries, inheritedSessionId),
|
|
1322
|
+
inheritedSessionId,
|
|
1323
|
+
stateEntryType,
|
|
1324
|
+
foldRecordEntryType,
|
|
1325
|
+
);
|
|
1326
|
+
if (!Array.isArray(restoredMessages)) {
|
|
1327
|
+
throw new Error("Pi did not provide the fork branch messages needed to rebase active-context state", {
|
|
1328
|
+
cause: restoreError,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
const snapshot = snapshotForEvent(ctx, restoredMessages);
|
|
1332
|
+
return { inherited, snapshot };
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1175
1335
|
const authoritativeSnapshotFor = (ctx: any): ActiveContextSnapshot => {
|
|
1176
1336
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1177
1337
|
if (!lifecycle.latestSnapshot || lifecycle.latestSnapshot.sessionId !== sessionId) {
|
|
@@ -1239,7 +1399,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1239
1399
|
model: measurement.model,
|
|
1240
1400
|
});
|
|
1241
1401
|
|
|
1242
|
-
const load = (ctx: any, preserveThresholdDecision = false): void => {
|
|
1402
|
+
const load = (ctx: any, preserveThresholdDecision = false, forkedSessionHint = false): void => {
|
|
1243
1403
|
lifecycle.generation += 1;
|
|
1244
1404
|
lifecycle.shuttingDown = false;
|
|
1245
1405
|
cancelPreparation();
|
|
@@ -1279,6 +1439,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1279
1439
|
curation.contextCalls = 0;
|
|
1280
1440
|
curation.recoveryAttempts = 0;
|
|
1281
1441
|
curation.pendingRejection = null;
|
|
1442
|
+
curation.restoreRevisionGap = false;
|
|
1282
1443
|
curation.lastRecovery = null;
|
|
1283
1444
|
ladder.lastAutomaticAction = null;
|
|
1284
1445
|
ladder.automaticFailure = null;
|
|
@@ -1287,14 +1448,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1287
1448
|
measurements.providerMeasurementByMessageSha.clear();
|
|
1288
1449
|
measurements.providerMeasurementAnchorByMessageSha.clear();
|
|
1289
1450
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1451
|
+
const forkedSession = forkedSessionHint || Boolean(ctx.sessionManager.getHeader?.()?.parentSession);
|
|
1290
1452
|
let restored: ActiveContextState | null = null;
|
|
1291
1453
|
let restoreError: unknown = null;
|
|
1292
1454
|
let restoredPersistence: MaterializedStatePersistence | null = null;
|
|
1293
1455
|
let measurementRestoreError: unknown = null;
|
|
1456
|
+
let forkBaseline = false;
|
|
1457
|
+
let startupSnapshot: ActiveContextSnapshot | null = null;
|
|
1458
|
+
let aheadReceiptRevision: number | null = null;
|
|
1294
1459
|
const branchEntries = [...ctx.sessionManager.getBranch()];
|
|
1460
|
+
const restoredMessages = piContextMessages(ctx);
|
|
1295
1461
|
try {
|
|
1296
1462
|
restoredPersistence = materializeStatePersistence(
|
|
1297
|
-
branchEntries,
|
|
1463
|
+
forkedSession ? forkEntriesForSession(branchEntries, sessionId) : branchEntries,
|
|
1298
1464
|
sessionId,
|
|
1299
1465
|
stateEntryType,
|
|
1300
1466
|
foldRecordEntryType,
|
|
@@ -1302,10 +1468,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1302
1468
|
restored = restoredPersistence.state;
|
|
1303
1469
|
} catch (error) {
|
|
1304
1470
|
restoreError = error;
|
|
1471
|
+
if (forkedSession) {
|
|
1472
|
+
try {
|
|
1473
|
+
const recovery = prepareForkRecovery(ctx, branchEntries, sessionId, restoredMessages, error);
|
|
1474
|
+
startupSnapshot = recovery.snapshot;
|
|
1475
|
+
restored = rebaseForkState(recovery.inherited.state, startupSnapshot, sessionId);
|
|
1476
|
+
restoredPersistence = recovery.inherited;
|
|
1477
|
+
forkBaseline = true;
|
|
1478
|
+
restoreError = null;
|
|
1479
|
+
} catch (forkError) {
|
|
1480
|
+
restoreError = forkError;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1305
1483
|
}
|
|
1306
1484
|
for (const entry of branchEntries) {
|
|
1307
1485
|
if (entry?.type !== "custom") continue;
|
|
1308
1486
|
if (entry.customType !== providerMeasurementEntryType) continue;
|
|
1487
|
+
const receiptSessionId = ownValue(entry.data, "sessionId");
|
|
1488
|
+
if (forkedSession && typeof receiptSessionId === "string" && receiptSessionId !== sessionId) {
|
|
1489
|
+
try { parseProviderContextMeasurementReceipt(entry.data, receiptSessionId); }
|
|
1490
|
+
catch (error) { measurementRestoreError = error; }
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1309
1493
|
try {
|
|
1310
1494
|
const receipt = parseProviderContextMeasurementReceipt(entry.data, sessionId);
|
|
1311
1495
|
const boundRevision = measurements.providerMeasurementRevisionByMessageSha.get(receipt.messageSha256);
|
|
@@ -1318,6 +1502,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1318
1502
|
throw new Error("One provider response has conflicting durable measurement receipts");
|
|
1319
1503
|
}
|
|
1320
1504
|
measurements.providerMeasurementByMessageSha.set(receipt.messageSha256, receipt);
|
|
1505
|
+
if (restoreError === null && receipt.projectionRevision > (restored?.revision ?? 0)) {
|
|
1506
|
+
aheadReceiptRevision = Math.max(aheadReceiptRevision ?? 0, receipt.projectionRevision);
|
|
1507
|
+
// This receipt describes a projection whose state is absent from this branch.
|
|
1508
|
+
// Keep the original receipt, but never anchor the restored window to it.
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1321
1511
|
const fingerprint = restoredPersistence?.projectionFingerprints.get(receipt.projectionRevision);
|
|
1322
1512
|
if (fingerprint) {
|
|
1323
1513
|
measurements.providerMeasurementAnchorByMessageSha.set(receipt.messageSha256, {
|
|
@@ -1336,6 +1526,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1336
1526
|
}
|
|
1337
1527
|
}
|
|
1338
1528
|
const durableRestored = restored ?? emptyActiveContextState(sessionId);
|
|
1529
|
+
if (restoreError === null && aheadReceiptRevision !== null) {
|
|
1530
|
+
curation.restoreRevisionGap = true;
|
|
1531
|
+
safeNotify(ctx, `A provider receipt names revision ${aheadReceiptRevision}, ahead of the restored ` +
|
|
1532
|
+
`${brandNoun} ledger at revision ${durableRestored.revision}. Existing folds and raw history are retained; ` +
|
|
1533
|
+
"the next context pass will rebuild eligible folds from that history. Missing ledger changes cannot be replayed.", "warning");
|
|
1534
|
+
}
|
|
1339
1535
|
persistence.state = durableRestored.prepared ? clearPrepared(durableRestored) : clone(durableRestored);
|
|
1340
1536
|
// AN UNREADABLE LINEAGE IS NOT AN EMPTY ONE (2026-09-07). `persistedWireVersion` reads
|
|
1341
1537
|
// 0 for both, and `persist` writes a CHECKPOINT whenever it is not 2, so a restore that
|
|
@@ -1348,23 +1544,25 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1348
1544
|
// the same ids with drifted bytes, 48 of 85 ids left conflicting. The two states are
|
|
1349
1545
|
// told apart here, once, rather than at each of `persist`'s callers.
|
|
1350
1546
|
persistence.lineageUnreadable = restoreError !== null;
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
persistence.
|
|
1354
|
-
persistence.
|
|
1355
|
-
|
|
1547
|
+
const persistedRestore = forkBaseline ? null : restoredPersistence;
|
|
1548
|
+
const persistedState = forkBaseline ? emptyActiveContextState(sessionId) : durableRestored;
|
|
1549
|
+
persistence.persistedWireVersion = persistedRestore?.wireVersion ?? 0;
|
|
1550
|
+
persistence.deltasSinceCheckpoint = persistedRestore?.deltasSinceCheckpoint ?? 0;
|
|
1551
|
+
persistence.persistedFoldRecords = persistedRestore?.records ?? new Map<string, FoldRecordEntry>();
|
|
1552
|
+
persistence.persistedStateSha256 = persistedRestore?.stateSha256 ?? semanticStateSha256(persistedState);
|
|
1553
|
+
persistence.persisted = clone(persistedState);
|
|
1554
|
+
persistence.forkCheckpointPending = forkBaseline;
|
|
1356
1555
|
measurements.lastProviderMeasurement = latestProviderContextMeasurement(
|
|
1357
1556
|
Array.isArray(restoredMessages) ? restoredMessages : [],
|
|
1358
1557
|
budgetWindowFor(ctx),
|
|
1359
1558
|
ctx.model,
|
|
1360
1559
|
);
|
|
1361
1560
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
1362
|
-
persistence.persisted = clone(durableRestored);
|
|
1363
1561
|
// Reuse Pi's own reconstructed messages, including custom messages and branch
|
|
1364
1562
|
// selection. This is ONLY a view for the bar: do not populate latestSnapshot,
|
|
1365
1563
|
// advance the frontier, persist a projection, or pretend a provider event occurred.
|
|
1366
1564
|
if (typeof ctx.ui?.setWidget === "function" && Array.isArray(restoredMessages)) {
|
|
1367
|
-
try { foldBar.startupSnapshot = snapshotForEvent(ctx, restoredMessages); }
|
|
1565
|
+
try { foldBar.startupSnapshot = startupSnapshot ?? snapshotForEvent(ctx, restoredMessages); }
|
|
1368
1566
|
catch { /* Keep the honest mapping fallback if the host cannot supply a view. */ }
|
|
1369
1567
|
}
|
|
1370
1568
|
// The warning names the CONSEQUENCE and not only the cause: "state was ignored" read as
|
|
@@ -1412,7 +1610,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1412
1610
|
next = persistenceProjection(next, projectionSnapshot);
|
|
1413
1611
|
}
|
|
1414
1612
|
next.folds = normalizeFoldsForPersistedRecords(next.folds, persistence.persistedFoldRecords);
|
|
1415
|
-
if (sameStateProjection(next, persistence.persisted)) {
|
|
1613
|
+
if (sameStateProjection(next, persistence.persisted) && !persistence.forkCheckpointPending) {
|
|
1416
1614
|
if (arrivingFoldIds.length) {
|
|
1417
1615
|
const lost = clone(persistence.state).folds.find((fold) => fold.id === arrivingFoldIds[0]);
|
|
1418
1616
|
const lostRefs = lost ? flattenFoldRefs(lost, persistence.state) : [];
|
|
@@ -1491,6 +1689,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1491
1689
|
await pi.appendEntry(stateEntryType, wire);
|
|
1492
1690
|
persistence.persisted = clone(next);
|
|
1493
1691
|
persistence.persistedWireVersion = 2;
|
|
1692
|
+
persistence.forkCheckpointPending = false;
|
|
1494
1693
|
persistence.deltasSinceCheckpoint = wire.kind === "checkpoint" ? 0 : persistence.deltasSinceCheckpoint + 1;
|
|
1495
1694
|
persistence.persistedStateSha256 = wire.stateSha256;
|
|
1496
1695
|
persistence.state = lifecycle.shuttingDown ? null : clone(next);
|
|
@@ -2454,7 +2653,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2454
2653
|
recovered,
|
|
2455
2654
|
note: measured.over
|
|
2456
2655
|
? `The rebuilt request is still ${measured.tokens} tokens against a ${measured.budgetTokens}-token ` +
|
|
2457
|
-
"serving budget
|
|
2656
|
+
"serving budget by our estimate. Available folds have been attempted; the retry is being sent, " +
|
|
2657
|
+
"and the provider will determine whether it fits. Recovery is not yet established."
|
|
2458
2658
|
: recovered
|
|
2459
2659
|
? "A rollback was required: the provider rejected the last request, which overfilled the serving " +
|
|
2460
2660
|
`budget at ${rejectedTokens} estimated tokens against ${measured.budgetTokens}. This pass ` +
|
|
@@ -2831,7 +3031,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2831
3031
|
if (addition.added) { state = addition.state; consolidationAdded += 1; }
|
|
2832
3032
|
}
|
|
2833
3033
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
2834
|
-
const usedTokens = capacity.usedTokens;
|
|
3034
|
+
const usedTokens = curation.restoreRevisionGap ? null : capacity.usedTokens;
|
|
2835
3035
|
const budgetTokens = capacity.budgetTokens;
|
|
2836
3036
|
const overflowExempt = userRequested || curation.recoveryAttempts > 0 ||
|
|
2837
3037
|
(usedTokens !== null && budgetTokens > 0 && usedTokens > budgetTokens);
|
|
@@ -3219,8 +3419,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3219
3419
|
phase: string,
|
|
3220
3420
|
operation: () => Promise<Record<string, unknown> | null>,
|
|
3221
3421
|
): Promise<Record<string, unknown> | null> => {
|
|
3222
|
-
|
|
3223
|
-
|
|
3422
|
+
const unmeasuredRecovery = phase === "restore-revision-gap" ||
|
|
3423
|
+
(phase === "projection-budget" && curation.pendingRejection !== null && !measurements.lastProviderMeasurement);
|
|
3424
|
+
if (!persistence.state || (!unmeasuredRecovery && (!measurements.lastProviderMeasurement ||
|
|
3425
|
+
!durableProviderMeasurementMatches(measurements.lastProviderMeasurement)))) return null;
|
|
3224
3426
|
if (ladder.automaticFailure) {
|
|
3225
3427
|
ladder.automaticFailure.suppressedCallbacks = Math.min(
|
|
3226
3428
|
Number.MAX_SAFE_INTEGER,
|
|
@@ -3280,10 +3482,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3280
3482
|
return [];
|
|
3281
3483
|
}
|
|
3282
3484
|
};
|
|
3283
|
-
const enqueueLifecycleLoad = async (ctx: any): Promise<void> => {
|
|
3485
|
+
const enqueueLifecycleLoad = async (ctx: any, forkedSessionHint = false): Promise<void> => {
|
|
3284
3486
|
const operation = lifecycle.contextQueue.then(() => {
|
|
3285
3487
|
const loadOperation = ladder.actionQueue.then(async () => {
|
|
3286
|
-
load(ctx);
|
|
3488
|
+
load(ctx, false, forkedSessionHint);
|
|
3489
|
+
if (persistence.forkCheckpointPending) await persist(ctx);
|
|
3287
3490
|
await recoverNativeReceipts(ctx);
|
|
3288
3491
|
});
|
|
3289
3492
|
ladder.actionQueue = loadOperation.catch(() => undefined);
|
|
@@ -3293,8 +3496,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3293
3496
|
await operation;
|
|
3294
3497
|
};
|
|
3295
3498
|
|
|
3296
|
-
const safeLifecycleLoad = async (
|
|
3297
|
-
|
|
3499
|
+
const safeLifecycleLoad = async (
|
|
3500
|
+
ctx: any,
|
|
3501
|
+
phase: "session-start" | "session-tree",
|
|
3502
|
+
forkedSessionHint = false,
|
|
3503
|
+
): Promise<void> => {
|
|
3504
|
+
try { await enqueueLifecycleLoad(ctx, forkedSessionHint); }
|
|
3298
3505
|
catch (error) {
|
|
3299
3506
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
3300
3507
|
if (!persistence.state || persistence.state.sessionId !== sessionId) persistence.state = emptyActiveContextState(sessionId);
|
|
@@ -3316,12 +3523,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3316
3523
|
});
|
|
3317
3524
|
};
|
|
3318
3525
|
|
|
3319
|
-
pi.on("session_start", async (
|
|
3320
|
-
await safeLifecycleLoad(ctx, "session-start");
|
|
3526
|
+
pi.on("session_start", async (event: unknown, ctx: any) => {
|
|
3527
|
+
await safeLifecycleLoad(ctx, "session-start", ownValue(event, "reason") === "fork");
|
|
3321
3528
|
recordResolvedCapacity(ctx);
|
|
3322
3529
|
await armRollbackLane(ctx);
|
|
3323
3530
|
});
|
|
3324
|
-
pi.on("session_tree", async (_event: unknown, ctx: any) => {
|
|
3531
|
+
pi.on("session_tree", async (_event: unknown, ctx: any) => {
|
|
3532
|
+
await safeLifecycleLoad(ctx, "session-tree", Boolean(ctx.sessionManager.getHeader?.()?.parentSession));
|
|
3533
|
+
});
|
|
3325
3534
|
pi.on("session_compact", async (event: Record<string, unknown>, ctx: any) => {
|
|
3326
3535
|
const reason = boundReceiptText(ownValue(event, "reason"), 64, "unknown");
|
|
3327
3536
|
const decision = makeNativeDecision(
|
|
@@ -3496,6 +3705,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3496
3705
|
// neither of their inputs. What it changes is WHEN the agent learns a fold exists,
|
|
3497
3706
|
// which is now while the material it covers is still in front of it.
|
|
3498
3707
|
advanceFoldFrontier(snapshot);
|
|
3708
|
+
if (curation.restoreRevisionGap && !ladder.automaticFailure) {
|
|
3709
|
+
mutationAttempted = true;
|
|
3710
|
+
const action = await attemptAutomaticCommit(snapshot, ctx, "restore-revision-gap", 1);
|
|
3711
|
+
if (action) {
|
|
3712
|
+
persistedSucceeded = true;
|
|
3713
|
+
projected = projectWithAdvisory(snapshot);
|
|
3714
|
+
}
|
|
3715
|
+
curation.restoreRevisionGap = false;
|
|
3716
|
+
}
|
|
3499
3717
|
// BEFORE the fence, so an ordinary crossing is an ordinary commit and the fence is
|
|
3500
3718
|
// left holding only the requests that genuinely will not fit.
|
|
3501
3719
|
if (await enforceBandTop(snapshot, ctx)) {
|
|
@@ -3748,7 +3966,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3748
3966
|
rollback.attempts += 1;
|
|
3749
3967
|
refuseRollback(
|
|
3750
3968
|
"the overflow arrived without a compaction event, so pi never stripped the failed message from agent " +
|
|
3751
|
-
"state and a tree rollback would leave the tree and agent state disagreeing (auto-compaction is off)"
|
|
3969
|
+
"state and a tree rollback would leave the tree and agent state disagreeing (auto-compaction is off); " +
|
|
3970
|
+
`use /${commandNames.fold} to commit eligible stale context, then retry explicitly`,
|
|
3752
3971
|
"message_end",
|
|
3753
3972
|
ctx,
|
|
3754
3973
|
);
|
|
@@ -3816,7 +4035,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3816
4035
|
"turn after an unsatisfied tool call is a malformed transcript for every provider";
|
|
3817
4036
|
}
|
|
3818
4037
|
const occupancyBefore = measurements.lastProviderMeasurement?.tokens ?? null;
|
|
3819
|
-
|
|
4038
|
+
let notice = rollbackNoticeText({
|
|
3820
4039
|
brandNoun,
|
|
3821
4040
|
toolName,
|
|
3822
4041
|
tokensRolledBack: estimatedTokens(rolledBackBytes),
|
|
@@ -3840,7 +4059,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3840
4059
|
}
|
|
3841
4060
|
}
|
|
3842
4061
|
}
|
|
3843
|
-
if (!replayed)
|
|
4062
|
+
if (!replayed) {
|
|
4063
|
+
notice = rollbackNoticeText({
|
|
4064
|
+
brandNoun, toolName, tokensRolledBack: estimatedTokens(rolledBackBytes),
|
|
4065
|
+
entriesAbandoned: abandoned.length, replayed: false, replaySkipReason,
|
|
4066
|
+
});
|
|
4067
|
+
safeNotify(ctx, notice, "warning");
|
|
4068
|
+
}
|
|
3844
4069
|
const record = emit("context.rollback", {
|
|
3845
4070
|
trigger: "session_before_compact",
|
|
3846
4071
|
armed: true,
|
|
@@ -3897,6 +4122,26 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3897
4122
|
try { updateStatus(ctx); } catch { }
|
|
3898
4123
|
return undefined;
|
|
3899
4124
|
}
|
|
4125
|
+
// PI CHECKS BEFORE THE FIRST PROMPT, BEFORE ANY CONTEXT EVENT (2026-10-05). After a
|
|
4126
|
+
// context_edit (Pi's own retry recovery writes one per failed attempt) Pi stops trusting
|
|
4127
|
+
// the last provider count and estimates the whole raw transcript, folds unseen: 134% on
|
|
4128
|
+
// session 01a10834, 260% on 01a10161. This used to throw for want of a snapshot, suspend
|
|
4129
|
+
// folding and let native compaction run, which ran Pi out of memory on a 272MB session.
|
|
4130
|
+
// The context event that follows projects and commits on the projection itself, so the
|
|
4131
|
+
// boundary is cancelled and folding stays on.
|
|
4132
|
+
const currentSnapshot = lifecycle.latestSnapshot;
|
|
4133
|
+
if (!currentSnapshot || currentSnapshot.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
4134
|
+
nativeCompaction.lastThresholdDecision = {
|
|
4135
|
+
handled: true,
|
|
4136
|
+
retry: false,
|
|
4137
|
+
reason: `${contextBrand(brandNoun)} blocked stock automatic compaction before the first context event; ` +
|
|
4138
|
+
"the projection is measured when the request is built",
|
|
4139
|
+
compactionReason: reason,
|
|
4140
|
+
nativeCompactionCompleted: false,
|
|
4141
|
+
};
|
|
4142
|
+
try { updateStatus(ctx); } catch { }
|
|
4143
|
+
return { cancel: true };
|
|
4144
|
+
}
|
|
3900
4145
|
let handoff: Record<string, unknown> | null = null;
|
|
3901
4146
|
try {
|
|
3902
4147
|
const snapshot = authoritativeSnapshotFor(ctx);
|
|
@@ -4794,7 +5039,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4794
5039
|
if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
|
|
4795
5040
|
const snapshot = lifecycle.latestSnapshot
|
|
4796
5041
|
? authoritativeSnapshotFor(ctx)
|
|
4797
|
-
: snapshotForEvent(ctx, ctx
|
|
5042
|
+
: snapshotForEvent(ctx, piContextMessages(ctx) ?? []);
|
|
4798
5043
|
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
4799
5044
|
const divider = args.indexOf(" -- ");
|
|
4800
5045
|
const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
|
|
@@ -4807,14 +5052,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4807
5052
|
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
4808
5053
|
? capacity.usedTokens / capacity.budgetTokens
|
|
4809
5054
|
: null;
|
|
4810
|
-
const
|
|
5055
|
+
const estimatedOccupancy = occupancy ?? (capacity.budgetTokens > 0
|
|
5056
|
+
? estimatedTokens(pricedBytes(projectActiveContext(snapshot, persistence.state!))) / capacity.budgetTokens
|
|
5057
|
+
: 0);
|
|
5058
|
+
const topUp = Boolean(curation.restoreRevisionGap) || estimatedOccupancy >= thresholds.maxTarget;
|
|
4811
5059
|
const committed = await runCommitEpoch(snapshot, "user-command", topUp, measurements.latestRatio, true);
|
|
4812
5060
|
// Until this lands the epoch's folds are in the event stream and nowhere else.
|
|
4813
5061
|
// Swallowing the failure here announced a commit that does not exist and left the
|
|
4814
5062
|
// session computing folds it would keep throwing away, which is the dead session
|
|
4815
5063
|
// gate 122 was built for, reached through the user's own command instead.
|
|
4816
|
-
try {
|
|
4817
|
-
|
|
5064
|
+
try {
|
|
5065
|
+
await persist(ctx);
|
|
5066
|
+
if (committed) curation.restoreRevisionGap = false;
|
|
5067
|
+
} catch (error) {
|
|
4818
5068
|
suspendAutomatic(error, "user-command", ctx);
|
|
4819
5069
|
updateStatus(ctx);
|
|
4820
5070
|
throw error;
|
|
@@ -4904,7 +5154,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4904
5154
|
}
|
|
4905
5155
|
const snapshot = lifecycle.latestSnapshot
|
|
4906
5156
|
? authoritativeSnapshotFor(ctx)
|
|
4907
|
-
: snapshotForEvent(ctx, ctx
|
|
5157
|
+
: snapshotForEvent(ctx, piContextMessages(ctx) ?? []);
|
|
4908
5158
|
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
4909
5159
|
if (!persistence.state) throw new Error("The context window is empty; there is nothing to show yet");
|
|
4910
5160
|
|
|
@@ -69,8 +69,15 @@ export function contentDrifted(snapshot: ActiveContextSnapshot, ref: EvidenceRef
|
|
|
69
69
|
return item !== null && item.ref!.sha256 !== ref.sha256;
|
|
70
70
|
}
|
|
71
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.
|
|
72
77
|
export function refsInOrder(snapshot: ActiveContextSnapshot, refs: EvidenceRef[]): number[] | null {
|
|
73
|
-
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);
|
|
74
81
|
if (indices.some((index) => index < 0) || new Set(indices).size !== indices.length) return null;
|
|
75
82
|
for (let index = 1; index < indices.length; index += 1) if (indices[index] !== indices[index - 1] + 1) return null;
|
|
76
83
|
return indices;
|
|
@@ -152,11 +152,11 @@ export function rollbackNoticeText(input: {
|
|
|
152
152
|
}): string {
|
|
153
153
|
const head = "The provider rejected the last request because the context exceeded its input limit. " +
|
|
154
154
|
`${input.brandNoun} rolled the session back past that request (${input.entriesAbandoned} entry(s), ` +
|
|
155
|
-
`about ${input.tokensRolledBack} tokens, kept in the tree as an abandoned branch)
|
|
156
|
-
"stale window to make room.";
|
|
155
|
+
`about ${input.tokensRolledBack} tokens, kept in the tree as an abandoned branch).`;
|
|
157
156
|
if (input.replayed) {
|
|
158
|
-
return `${head} The request is being reissued now
|
|
159
|
-
|
|
157
|
+
return `${head} The request is being reissued now. Its context pass will attempt to fold eligible stale ` +
|
|
158
|
+
"material; the recovery receipt will report the result. Original entries remain in the session tree, " +
|
|
159
|
+
`and folded material is still readable through ${input.toolName}.`;
|
|
160
160
|
}
|
|
161
161
|
return `${head} The request was NOT reissued: ${input.replaySkipReason ?? "the rolled-back tail is not replayable"}. ` +
|
|
162
162
|
"Reissue the work yourself from the state you can see.";
|
|
@@ -699,9 +699,12 @@ export function commitCoverage(input: {
|
|
|
699
699
|
budgetTokens: number;
|
|
700
700
|
}): CommitCoverage {
|
|
701
701
|
const { snapshot, state, usedTokens, budgetTokens } = input;
|
|
702
|
-
|
|
702
|
+
// Before the first response, or after a restore, an estimate can size a commit.
|
|
703
|
+
// It never decides whether the resulting request is allowed to reach the provider.
|
|
704
|
+
const occupancyTokens = usedTokens ?? estimatedTokens(pricedBytes(projectActiveContext(snapshot, state)));
|
|
705
|
+
const targetShare = !(budgetTokens > 0)
|
|
703
706
|
? 0
|
|
704
|
-
: Math.max(0, (
|
|
707
|
+
: Math.max(0, (occupancyTokens - snapshot.thresholds.minTarget * budgetTokens) / budgetTokens);
|
|
705
708
|
const accounting = markAccounting(snapshot, state);
|
|
706
709
|
const markedShare = accounting.eligibleFreedBudgetShare;
|
|
707
710
|
const targetTokens = Math.round(targetShare * budgetTokens);
|