pi-fold 4.2.6 → 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
|
};
|
|
@@ -1184,6 +1192,146 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1184
1192
|
thresholds,
|
|
1185
1193
|
}));
|
|
1186
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
|
+
|
|
1187
1335
|
const authoritativeSnapshotFor = (ctx: any): ActiveContextSnapshot => {
|
|
1188
1336
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1189
1337
|
if (!lifecycle.latestSnapshot || lifecycle.latestSnapshot.sessionId !== sessionId) {
|
|
@@ -1251,7 +1399,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1251
1399
|
model: measurement.model,
|
|
1252
1400
|
});
|
|
1253
1401
|
|
|
1254
|
-
const load = (ctx: any, preserveThresholdDecision = false): void => {
|
|
1402
|
+
const load = (ctx: any, preserveThresholdDecision = false, forkedSessionHint = false): void => {
|
|
1255
1403
|
lifecycle.generation += 1;
|
|
1256
1404
|
lifecycle.shuttingDown = false;
|
|
1257
1405
|
cancelPreparation();
|
|
@@ -1291,6 +1439,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1291
1439
|
curation.contextCalls = 0;
|
|
1292
1440
|
curation.recoveryAttempts = 0;
|
|
1293
1441
|
curation.pendingRejection = null;
|
|
1442
|
+
curation.restoreRevisionGap = false;
|
|
1294
1443
|
curation.lastRecovery = null;
|
|
1295
1444
|
ladder.lastAutomaticAction = null;
|
|
1296
1445
|
ladder.automaticFailure = null;
|
|
@@ -1299,14 +1448,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1299
1448
|
measurements.providerMeasurementByMessageSha.clear();
|
|
1300
1449
|
measurements.providerMeasurementAnchorByMessageSha.clear();
|
|
1301
1450
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1451
|
+
const forkedSession = forkedSessionHint || Boolean(ctx.sessionManager.getHeader?.()?.parentSession);
|
|
1302
1452
|
let restored: ActiveContextState | null = null;
|
|
1303
1453
|
let restoreError: unknown = null;
|
|
1304
1454
|
let restoredPersistence: MaterializedStatePersistence | null = null;
|
|
1305
1455
|
let measurementRestoreError: unknown = null;
|
|
1456
|
+
let forkBaseline = false;
|
|
1457
|
+
let startupSnapshot: ActiveContextSnapshot | null = null;
|
|
1458
|
+
let aheadReceiptRevision: number | null = null;
|
|
1306
1459
|
const branchEntries = [...ctx.sessionManager.getBranch()];
|
|
1460
|
+
const restoredMessages = piContextMessages(ctx);
|
|
1307
1461
|
try {
|
|
1308
1462
|
restoredPersistence = materializeStatePersistence(
|
|
1309
|
-
branchEntries,
|
|
1463
|
+
forkedSession ? forkEntriesForSession(branchEntries, sessionId) : branchEntries,
|
|
1310
1464
|
sessionId,
|
|
1311
1465
|
stateEntryType,
|
|
1312
1466
|
foldRecordEntryType,
|
|
@@ -1314,10 +1468,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1314
1468
|
restored = restoredPersistence.state;
|
|
1315
1469
|
} catch (error) {
|
|
1316
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
|
+
}
|
|
1317
1483
|
}
|
|
1318
1484
|
for (const entry of branchEntries) {
|
|
1319
1485
|
if (entry?.type !== "custom") continue;
|
|
1320
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
|
+
}
|
|
1321
1493
|
try {
|
|
1322
1494
|
const receipt = parseProviderContextMeasurementReceipt(entry.data, sessionId);
|
|
1323
1495
|
const boundRevision = measurements.providerMeasurementRevisionByMessageSha.get(receipt.messageSha256);
|
|
@@ -1330,6 +1502,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1330
1502
|
throw new Error("One provider response has conflicting durable measurement receipts");
|
|
1331
1503
|
}
|
|
1332
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
|
+
}
|
|
1333
1511
|
const fingerprint = restoredPersistence?.projectionFingerprints.get(receipt.projectionRevision);
|
|
1334
1512
|
if (fingerprint) {
|
|
1335
1513
|
measurements.providerMeasurementAnchorByMessageSha.set(receipt.messageSha256, {
|
|
@@ -1348,6 +1526,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1348
1526
|
}
|
|
1349
1527
|
}
|
|
1350
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
|
+
}
|
|
1351
1535
|
persistence.state = durableRestored.prepared ? clearPrepared(durableRestored) : clone(durableRestored);
|
|
1352
1536
|
// AN UNREADABLE LINEAGE IS NOT AN EMPTY ONE (2026-09-07). `persistedWireVersion` reads
|
|
1353
1537
|
// 0 for both, and `persist` writes a CHECKPOINT whenever it is not 2, so a restore that
|
|
@@ -1360,23 +1544,25 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1360
1544
|
// the same ids with drifted bytes, 48 of 85 ids left conflicting. The two states are
|
|
1361
1545
|
// told apart here, once, rather than at each of `persist`'s callers.
|
|
1362
1546
|
persistence.lineageUnreadable = restoreError !== null;
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
persistence.
|
|
1366
|
-
persistence.
|
|
1367
|
-
|
|
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;
|
|
1368
1555
|
measurements.lastProviderMeasurement = latestProviderContextMeasurement(
|
|
1369
1556
|
Array.isArray(restoredMessages) ? restoredMessages : [],
|
|
1370
1557
|
budgetWindowFor(ctx),
|
|
1371
1558
|
ctx.model,
|
|
1372
1559
|
);
|
|
1373
1560
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
1374
|
-
persistence.persisted = clone(durableRestored);
|
|
1375
1561
|
// Reuse Pi's own reconstructed messages, including custom messages and branch
|
|
1376
1562
|
// selection. This is ONLY a view for the bar: do not populate latestSnapshot,
|
|
1377
1563
|
// advance the frontier, persist a projection, or pretend a provider event occurred.
|
|
1378
1564
|
if (typeof ctx.ui?.setWidget === "function" && Array.isArray(restoredMessages)) {
|
|
1379
|
-
try { foldBar.startupSnapshot = snapshotForEvent(ctx, restoredMessages); }
|
|
1565
|
+
try { foldBar.startupSnapshot = startupSnapshot ?? snapshotForEvent(ctx, restoredMessages); }
|
|
1380
1566
|
catch { /* Keep the honest mapping fallback if the host cannot supply a view. */ }
|
|
1381
1567
|
}
|
|
1382
1568
|
// The warning names the CONSEQUENCE and not only the cause: "state was ignored" read as
|
|
@@ -1424,7 +1610,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1424
1610
|
next = persistenceProjection(next, projectionSnapshot);
|
|
1425
1611
|
}
|
|
1426
1612
|
next.folds = normalizeFoldsForPersistedRecords(next.folds, persistence.persistedFoldRecords);
|
|
1427
|
-
if (sameStateProjection(next, persistence.persisted)) {
|
|
1613
|
+
if (sameStateProjection(next, persistence.persisted) && !persistence.forkCheckpointPending) {
|
|
1428
1614
|
if (arrivingFoldIds.length) {
|
|
1429
1615
|
const lost = clone(persistence.state).folds.find((fold) => fold.id === arrivingFoldIds[0]);
|
|
1430
1616
|
const lostRefs = lost ? flattenFoldRefs(lost, persistence.state) : [];
|
|
@@ -1503,6 +1689,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1503
1689
|
await pi.appendEntry(stateEntryType, wire);
|
|
1504
1690
|
persistence.persisted = clone(next);
|
|
1505
1691
|
persistence.persistedWireVersion = 2;
|
|
1692
|
+
persistence.forkCheckpointPending = false;
|
|
1506
1693
|
persistence.deltasSinceCheckpoint = wire.kind === "checkpoint" ? 0 : persistence.deltasSinceCheckpoint + 1;
|
|
1507
1694
|
persistence.persistedStateSha256 = wire.stateSha256;
|
|
1508
1695
|
persistence.state = lifecycle.shuttingDown ? null : clone(next);
|
|
@@ -2466,7 +2653,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2466
2653
|
recovered,
|
|
2467
2654
|
note: measured.over
|
|
2468
2655
|
? `The rebuilt request is still ${measured.tokens} tokens against a ${measured.budgetTokens}-token ` +
|
|
2469
|
-
"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."
|
|
2470
2658
|
: recovered
|
|
2471
2659
|
? "A rollback was required: the provider rejected the last request, which overfilled the serving " +
|
|
2472
2660
|
`budget at ${rejectedTokens} estimated tokens against ${measured.budgetTokens}. This pass ` +
|
|
@@ -2843,7 +3031,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2843
3031
|
if (addition.added) { state = addition.state; consolidationAdded += 1; }
|
|
2844
3032
|
}
|
|
2845
3033
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
2846
|
-
const usedTokens = capacity.usedTokens;
|
|
3034
|
+
const usedTokens = curation.restoreRevisionGap ? null : capacity.usedTokens;
|
|
2847
3035
|
const budgetTokens = capacity.budgetTokens;
|
|
2848
3036
|
const overflowExempt = userRequested || curation.recoveryAttempts > 0 ||
|
|
2849
3037
|
(usedTokens !== null && budgetTokens > 0 && usedTokens > budgetTokens);
|
|
@@ -3231,8 +3419,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3231
3419
|
phase: string,
|
|
3232
3420
|
operation: () => Promise<Record<string, unknown> | null>,
|
|
3233
3421
|
): Promise<Record<string, unknown> | null> => {
|
|
3234
|
-
|
|
3235
|
-
|
|
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;
|
|
3236
3426
|
if (ladder.automaticFailure) {
|
|
3237
3427
|
ladder.automaticFailure.suppressedCallbacks = Math.min(
|
|
3238
3428
|
Number.MAX_SAFE_INTEGER,
|
|
@@ -3292,10 +3482,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3292
3482
|
return [];
|
|
3293
3483
|
}
|
|
3294
3484
|
};
|
|
3295
|
-
const enqueueLifecycleLoad = async (ctx: any): Promise<void> => {
|
|
3485
|
+
const enqueueLifecycleLoad = async (ctx: any, forkedSessionHint = false): Promise<void> => {
|
|
3296
3486
|
const operation = lifecycle.contextQueue.then(() => {
|
|
3297
3487
|
const loadOperation = ladder.actionQueue.then(async () => {
|
|
3298
|
-
load(ctx);
|
|
3488
|
+
load(ctx, false, forkedSessionHint);
|
|
3489
|
+
if (persistence.forkCheckpointPending) await persist(ctx);
|
|
3299
3490
|
await recoverNativeReceipts(ctx);
|
|
3300
3491
|
});
|
|
3301
3492
|
ladder.actionQueue = loadOperation.catch(() => undefined);
|
|
@@ -3305,8 +3496,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3305
3496
|
await operation;
|
|
3306
3497
|
};
|
|
3307
3498
|
|
|
3308
|
-
const safeLifecycleLoad = async (
|
|
3309
|
-
|
|
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); }
|
|
3310
3505
|
catch (error) {
|
|
3311
3506
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
3312
3507
|
if (!persistence.state || persistence.state.sessionId !== sessionId) persistence.state = emptyActiveContextState(sessionId);
|
|
@@ -3328,12 +3523,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3328
3523
|
});
|
|
3329
3524
|
};
|
|
3330
3525
|
|
|
3331
|
-
pi.on("session_start", async (
|
|
3332
|
-
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");
|
|
3333
3528
|
recordResolvedCapacity(ctx);
|
|
3334
3529
|
await armRollbackLane(ctx);
|
|
3335
3530
|
});
|
|
3336
|
-
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
|
+
});
|
|
3337
3534
|
pi.on("session_compact", async (event: Record<string, unknown>, ctx: any) => {
|
|
3338
3535
|
const reason = boundReceiptText(ownValue(event, "reason"), 64, "unknown");
|
|
3339
3536
|
const decision = makeNativeDecision(
|
|
@@ -3508,6 +3705,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3508
3705
|
// neither of their inputs. What it changes is WHEN the agent learns a fold exists,
|
|
3509
3706
|
// which is now while the material it covers is still in front of it.
|
|
3510
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
|
+
}
|
|
3511
3717
|
// BEFORE the fence, so an ordinary crossing is an ordinary commit and the fence is
|
|
3512
3718
|
// left holding only the requests that genuinely will not fit.
|
|
3513
3719
|
if (await enforceBandTop(snapshot, ctx)) {
|
|
@@ -3760,7 +3966,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3760
3966
|
rollback.attempts += 1;
|
|
3761
3967
|
refuseRollback(
|
|
3762
3968
|
"the overflow arrived without a compaction event, so pi never stripped the failed message from agent " +
|
|
3763
|
-
"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`,
|
|
3764
3971
|
"message_end",
|
|
3765
3972
|
ctx,
|
|
3766
3973
|
);
|
|
@@ -3828,7 +4035,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3828
4035
|
"turn after an unsatisfied tool call is a malformed transcript for every provider";
|
|
3829
4036
|
}
|
|
3830
4037
|
const occupancyBefore = measurements.lastProviderMeasurement?.tokens ?? null;
|
|
3831
|
-
|
|
4038
|
+
let notice = rollbackNoticeText({
|
|
3832
4039
|
brandNoun,
|
|
3833
4040
|
toolName,
|
|
3834
4041
|
tokensRolledBack: estimatedTokens(rolledBackBytes),
|
|
@@ -3852,7 +4059,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3852
4059
|
}
|
|
3853
4060
|
}
|
|
3854
4061
|
}
|
|
3855
|
-
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
|
+
}
|
|
3856
4069
|
const record = emit("context.rollback", {
|
|
3857
4070
|
trigger: "session_before_compact",
|
|
3858
4071
|
armed: true,
|
|
@@ -4839,14 +5052,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4839
5052
|
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
4840
5053
|
? capacity.usedTokens / capacity.budgetTokens
|
|
4841
5054
|
: null;
|
|
4842
|
-
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;
|
|
4843
5059
|
const committed = await runCommitEpoch(snapshot, "user-command", topUp, measurements.latestRatio, true);
|
|
4844
5060
|
// Until this lands the epoch's folds are in the event stream and nowhere else.
|
|
4845
5061
|
// Swallowing the failure here announced a commit that does not exist and left the
|
|
4846
5062
|
// session computing folds it would keep throwing away, which is the dead session
|
|
4847
5063
|
// gate 122 was built for, reached through the user's own command instead.
|
|
4848
|
-
try {
|
|
4849
|
-
|
|
5064
|
+
try {
|
|
5065
|
+
await persist(ctx);
|
|
5066
|
+
if (committed) curation.restoreRevisionGap = false;
|
|
5067
|
+
} catch (error) {
|
|
4850
5068
|
suspendAutomatic(error, "user-command", ctx);
|
|
4851
5069
|
updateStatus(ctx);
|
|
4852
5070
|
throw error;
|
|
@@ -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);
|