pi-fold 4.1.6 → 4.2.0
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.
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
makeStateCheckpoint,
|
|
86
86
|
makeStateDelta,
|
|
87
87
|
MAX_ACTIVE_FOLD_RECORDS,
|
|
88
|
+
MAX_DELTAS_BETWEEN_CHECKPOINTS,
|
|
88
89
|
materializeStatePersistence,
|
|
89
90
|
foldProvenance,
|
|
90
91
|
normalizeFoldsForPersistedRecords,
|
|
@@ -118,7 +119,7 @@ import {
|
|
|
118
119
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
119
120
|
IMAGE_ESTIMATED_TOKENS,
|
|
120
121
|
entryTypeNamespace,
|
|
121
|
-
|
|
122
|
+
chapterSpanCap,
|
|
122
123
|
DEFAULT_TOOL_FOLD_THRESHOLD,
|
|
123
124
|
MAX_PINNED_SHARE,
|
|
124
125
|
PEEK_DEFAULT_MAX_BYTES,
|
|
@@ -432,6 +433,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
432
433
|
state: null as ActiveContextState | null,
|
|
433
434
|
persisted: null as ActiveContextState | null,
|
|
434
435
|
persistedWireVersion: 0 as 0 | 1 | 2,
|
|
436
|
+
/** Set when the branch HELD durable state this build could not read. Distinct from a
|
|
437
|
+
* session that never had any, which is what `persistedWireVersion` 0 also means and
|
|
438
|
+
* which legitimately writes the first checkpoint. See `persist`. */
|
|
439
|
+
lineageUnreadable: false,
|
|
440
|
+
/** Deltas standing after the newest checkpoint in the branch. See
|
|
441
|
+
* `MAX_DELTAS_BETWEEN_CHECKPOINTS`: at the bound, `persist` writes a checkpoint instead
|
|
442
|
+
* of a delta, which is what keeps a load's replay bounded. */
|
|
443
|
+
deltasSinceCheckpoint: 0,
|
|
435
444
|
persistedStateSha256: "",
|
|
436
445
|
persistedFoldRecords: new Map<string, FoldRecordEntry>(),
|
|
437
446
|
persistenceQueue: Promise.resolve<void>(undefined),
|
|
@@ -1187,7 +1196,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1187
1196
|
}
|
|
1188
1197
|
const durableRestored = restored ?? emptyActiveContextState(sessionId);
|
|
1189
1198
|
persistence.state = durableRestored.prepared ? clearPrepared(durableRestored) : clone(durableRestored);
|
|
1199
|
+
// AN UNREADABLE LINEAGE IS NOT AN EMPTY ONE (2026-09-07). `persistedWireVersion` reads
|
|
1200
|
+
// 0 for both, and `persist` writes a CHECKPOINT whenever it is not 2, so a restore that
|
|
1201
|
+
// threw put a second checkpoint into a branch that already had one. The reader refuses
|
|
1202
|
+
// exactly that ("Duplicate active-context v2 checkpoint"), so the next load threw too,
|
|
1203
|
+
// and wrote a third. ONE TRANSIENT FAILURE OF ANY CAUSE LATCHED THE SESSION DEAD AND
|
|
1204
|
+
// EVERY RELOAD MADE IT STRICTLY WORSE: a real session reached five checkpoints, four of
|
|
1205
|
+
// them from four /reload presses inside eleven minutes, and grew from 859 entries to
|
|
1206
|
+
// 4,393 because each restart also re-cut the same spans and wrote fold records under
|
|
1207
|
+
// the same ids with drifted bytes, 48 of 85 ids left conflicting. The two states are
|
|
1208
|
+
// told apart here, once, rather than at each of `persist`'s callers.
|
|
1209
|
+
persistence.lineageUnreadable = restoreError !== null;
|
|
1190
1210
|
persistence.persistedWireVersion = restoredPersistence?.wireVersion ?? 0;
|
|
1211
|
+
persistence.deltasSinceCheckpoint = restoredPersistence?.deltasSinceCheckpoint ?? 0;
|
|
1191
1212
|
persistence.persistedFoldRecords = restoredPersistence?.records ?? new Map<string, FoldRecordEntry>();
|
|
1192
1213
|
persistence.persistedStateSha256 = restoredPersistence?.stateSha256 ?? semanticStateSha256(durableRestored);
|
|
1193
1214
|
const restoredMessages = ctx.sessionManager.buildSessionContext?.()?.messages;
|
|
@@ -1205,11 +1226,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1205
1226
|
try { foldBar.startupSnapshot = snapshotForEvent(ctx, restoredMessages); }
|
|
1206
1227
|
catch { /* Keep the honest mapping fallback if the host cannot supply a view. */ }
|
|
1207
1228
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
)
|
|
1229
|
+
// The warning names the CONSEQUENCE and not only the cause: "state was ignored" read as
|
|
1230
|
+
// graceful degradation, and the session it described could no longer fold at all. The
|
|
1231
|
+
// suspension carries no ctx of its own so the canonical `context.suspend` record is
|
|
1232
|
+
// emitted for a headless host without a second UI line saying the same thing twice.
|
|
1233
|
+
if (restoreError) {
|
|
1234
|
+
safeNotify(
|
|
1235
|
+
ctx,
|
|
1236
|
+
"Active-context state could not be read, so folding is suspended for this session and " +
|
|
1237
|
+
`Pi native context remains authoritative: ${String(restoreError)}`,
|
|
1238
|
+
"warning",
|
|
1239
|
+
);
|
|
1240
|
+
suspendAutomatic(restoreError, "restore", undefined);
|
|
1241
|
+
}
|
|
1213
1242
|
if (measurementRestoreError) safeNotify(
|
|
1214
1243
|
ctx,
|
|
1215
1244
|
`Malformed provider measurement receipt was ignored; automatic context remains unmeasured: ${String(measurementRestoreError)}`,
|
|
@@ -1220,6 +1249,18 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1220
1249
|
const persist = (ctx?: any): Promise<void> => {
|
|
1221
1250
|
const operation = persistence.persistenceQueue.then(async () => {
|
|
1222
1251
|
if (!persistence.state || !persistence.persisted) return;
|
|
1252
|
+
// THE ONE WRITE THAT MUST NOT HAPPEN. Folding is suspended the moment a restore fails,
|
|
1253
|
+
// so nothing automatic reaches this; a manual action still can, and one appended state
|
|
1254
|
+
// entry is all it takes to latch the session. Raising rather than returning keeps the
|
|
1255
|
+
// law of the discarded commit: a write that cannot land says so instead of vanishing,
|
|
1256
|
+
// and the manual path already restores the state it entered with and reports.
|
|
1257
|
+
if (persistence.lineageUnreadable) {
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
"Active-context durable state is unreadable for this session, so no state may be " +
|
|
1260
|
+
"written: a checkpoint here would be the second in this branch and would make every " +
|
|
1261
|
+
"later load fail",
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1223
1264
|
let next = clone(persistence.state);
|
|
1224
1265
|
const persistedFoldIds = new Set(persistence.persisted.folds.map((fold) => fold.id));
|
|
1225
1266
|
const arrivingFoldIds = next.folds.filter((fold) => !persistedFoldIds.has(fold.id)).map((fold) => fold.id);
|
|
@@ -1287,7 +1328,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1287
1328
|
throw new Error("Active-context session changed after fold-record persistence");
|
|
1288
1329
|
}
|
|
1289
1330
|
}
|
|
1290
|
-
|
|
1331
|
+
// A CHECKPOINT ENDS THE RUN THE READER WOULD OTHERWISE WALK (2026-09-20). A delta is
|
|
1332
|
+
// the cheap write and stays the common one; at the bound the run is closed instead, so
|
|
1333
|
+
// the next load replays at most `MAX_DELTAS_BETWEEN_CHECKPOINTS` deltas however long the
|
|
1334
|
+
// session runs. A session that is already past the bound when this build first reads it
|
|
1335
|
+
// closes its run on this write, so it pays the long load once and never again.
|
|
1336
|
+
const runIsLong = persistence.deltasSinceCheckpoint >= MAX_DELTAS_BETWEEN_CHECKPOINTS;
|
|
1337
|
+
const wire = persistence.persistedWireVersion === 2 && !runIsLong
|
|
1338
|
+
? makeStateDelta(persistence.persisted, next)
|
|
1339
|
+
: makeStateCheckpoint(next);
|
|
1291
1340
|
// THE WIRE CARRIES BOTH DIGESTS IT JUST DERIVED (2026-08-28). `makeStateDelta` writes
|
|
1292
1341
|
// `baseStateSha256` from `persistence.persisted` and `stateSha256` from `next`, and
|
|
1293
1342
|
// nothing mutates either state between that call and here, so re-hashing two whole
|
|
@@ -1301,6 +1350,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1301
1350
|
await pi.appendEntry(stateEntryType, wire);
|
|
1302
1351
|
persistence.persisted = clone(next);
|
|
1303
1352
|
persistence.persistedWireVersion = 2;
|
|
1353
|
+
persistence.deltasSinceCheckpoint = wire.kind === "checkpoint" ? 0 : persistence.deltasSinceCheckpoint + 1;
|
|
1304
1354
|
persistence.persistedStateSha256 = wire.stateSha256;
|
|
1305
1355
|
persistence.state = lifecycle.shuttingDown ? null : clone(next);
|
|
1306
1356
|
if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
|
|
@@ -4147,7 +4197,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4147
4197
|
last: rollback.last ? clone(rollback.last) : null,
|
|
4148
4198
|
},
|
|
4149
4199
|
},
|
|
4150
|
-
foldSpanCap:
|
|
4200
|
+
foldSpanCap: chapterSpanCap(thresholds.minFoldChars),
|
|
4151
4201
|
projectionBudgetTokens: currentCapacity(ctx).budgetTokens,
|
|
4152
4202
|
projectionCharsPerToken: projectionCharsPerToken(),
|
|
4153
4203
|
projectionEstimatorErrorShare: estimatorErrorShare(),
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
CONTEXT_STATUS_RESPONSE_BYTES,
|
|
52
52
|
EXPAND_LEASE_GENERATIONS,
|
|
53
53
|
MAX_EXPAND_LEASES,
|
|
54
|
-
|
|
54
|
+
chapterSpanCap,
|
|
55
55
|
PEEK_DEFAULT_MAX_BYTES,
|
|
56
56
|
PEEK_HEAD_SHARE,
|
|
57
57
|
STATUS_DIET_INDEX_ROWS,
|
|
@@ -95,6 +95,10 @@ export function selectAutomaticChapter(
|
|
|
95
95
|
// The maturity floor: a stale span may end only where the batch lane has already had
|
|
96
96
|
// its chance, so a chapter can never claim a tool batch one turn before it qualifies.
|
|
97
97
|
const matureEnd = staleSpanMatureEnd(snapshot.messages);
|
|
98
|
+
// THE CAP FOLLOWS THE FLOOR: a multi-unit span may not pass it, and it sits at least
|
|
99
|
+
// one floor above `minFoldChars` so the accumulate-until-over walk below can always
|
|
100
|
+
// land between the two (policy.ts, chapterSpanCap).
|
|
101
|
+
const spanCap = chapterSpanCap(snapshot.thresholds.minFoldChars);
|
|
98
102
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
|
|
99
103
|
const first = units[unitIndex];
|
|
100
104
|
if (first.end > matureEnd) break;
|
|
@@ -131,12 +135,12 @@ export function selectAutomaticChapter(
|
|
|
131
135
|
if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref)))) break;
|
|
132
136
|
if (refsProtected(refs, state, snapshot)) break;
|
|
133
137
|
const size = bytes(encodedFoldSource(snapshot, state, parts, "chapter"));
|
|
134
|
-
const biteSized = size <=
|
|
138
|
+
const biteSized = size <= spanCap || endIndex === unitIndex;
|
|
135
139
|
// ACCUMULATE UNTIL OVER, never cut to hit the number. The walk grows the span one
|
|
136
140
|
// whole unit at a time and records the first one that carries it past the floor,
|
|
137
141
|
// so a fold is always at least minFoldChars and always ends on a message boundary.
|
|
138
142
|
if (size >= snapshot.thresholds.minFoldChars && biteSized) best = { kind: "chapter", parts, sourceRefs: refs };
|
|
139
|
-
if (size >
|
|
143
|
+
if (size > spanCap) break;
|
|
140
144
|
}
|
|
141
145
|
if (best) return best;
|
|
142
146
|
}
|
|
@@ -68,6 +68,53 @@ export const STATE_DELTA_V2_KEYS = [
|
|
|
68
68
|
export const MAX_ACTIVE_FOLD_RECORD_BYTES = 256 * 1024;
|
|
69
69
|
export const MAX_ACTIVE_STATE_EVENT_BYTES = 512 * 1024;
|
|
70
70
|
export const MAX_ACTIVE_FOLD_RECORDS = 4_096;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* HOW MANY DELTAS MAY STAND BETWEEN CHECKPOINTS (2026-09-20).
|
|
74
|
+
*
|
|
75
|
+
* The v2 ledger was one checkpoint and then deltas for the life of the session, and
|
|
76
|
+
* `materializeStatePersistence` replays every one of them on every load. Each delta rebuilds
|
|
77
|
+
* the WHOLE state, so a load costs the delta count times the state size: quadratic in
|
|
78
|
+
* session length. Shane's noloss session 01a0ad40 reached 2,207 deltas and took 129.6s to
|
|
79
|
+
* restore, against 0.6s for pi to parse the same 93MB of entries; 01a0ba4e took 56.1s on
|
|
80
|
+
* 1,147 deltas. Doubling the entries quadrupled the time (3,987 entries 9.3s, 7,974 entries
|
|
81
|
+
* 38.8s) and the per-delta cost rose with it (7.5ms to 57.0ms), because what each delta
|
|
82
|
+
* re-walks is the state, not the delta. The load is also not paid once: `session_start`,
|
|
83
|
+
* `session_tree`, `session_compact` and a branch switch each pay it again, and
|
|
84
|
+
* `doubleEscapeAction: "tree"` puts it behind a keypress.
|
|
85
|
+
*
|
|
86
|
+
* A CHECKPOINT STATES THE WHOLE STATE, so a replay may start at the LAST one rather than the
|
|
87
|
+
* only one and never walk the run before it. That bounds a load at this many deltas instead
|
|
88
|
+
* of at the session's lifetime. Nothing is weakened by starting later: a checkpoint carries
|
|
89
|
+
* its own digest and the deltas after it still chain, so the final state is proven by the
|
|
90
|
+
* same equalities as before. What a later start gives up is re-proving revisions that were
|
|
91
|
+
* already proven when they were written, and the fingerprint map loses the revisions before
|
|
92
|
+
* the checkpoint, which its one reader already treats as absent (see `projectionFingerprints`).
|
|
93
|
+
*
|
|
94
|
+
* THE BOUND IS A TRADE, AND 50 IS WHERE IT WAS MEASURED. A checkpoint names every standing
|
|
95
|
+
* fold by id and digest, so it costs far more bytes than a delta: 237.9KB against a 2.55KB
|
|
96
|
+
* delta at 375 folds, 248.9KB against 3.34KB at 213. The load after a checkpoint is 0.34s and
|
|
97
|
+
* 0.20s for those two sessions, and each replayed delta adds about 85ms at 375 folds, so the
|
|
98
|
+
* bound sets both the worst load and the write overhead:
|
|
99
|
+
*
|
|
100
|
+
* K=100 8.8s worst load +93% state-ledger bytes (+5.6% of the session file)
|
|
101
|
+
* K= 50 4.4s worst load +187% state-ledger bytes (+11% of the session file)
|
|
102
|
+
* K= 25 2.2s worst load +374% state-ledger bytes (+22% of the session file)
|
|
103
|
+
*
|
|
104
|
+
* The state ledger is a small part of a session (5.9MB of 01a0ad40's 93MB), which is why the
|
|
105
|
+
* percentages look worse against the ledger than against the file. 50 takes the worst load
|
|
106
|
+
* under five seconds and the average near two, for a tenth of the file, and a session already
|
|
107
|
+
* past the bound heals itself on its next durable write.
|
|
108
|
+
*
|
|
109
|
+
* WHAT WOULD MAKE THE BOUND STOP MATTERING is the per-delta cost itself, which is still the
|
|
110
|
+
* whole state: 23.8% of a profiled load is `structuredClone` and 34% is `stableStringify`
|
|
111
|
+
* under `semanticStateSha256`, both walking every standing fold on every delta. Neither is
|
|
112
|
+
* reachable without giving the reader one canonical fold object per id for the life of a
|
|
113
|
+
* materialization, so that the clone stops being per-delta and a serializer memo can hit at
|
|
114
|
+
* all. That is a change to the aliasing barrier gate 158 guards and is deliberately NOT made
|
|
115
|
+
* here; until it is, this constant is what bounds the load.
|
|
116
|
+
*/
|
|
117
|
+
export const MAX_DELTAS_BETWEEN_CHECKPOINTS = 50;
|
|
71
118
|
export const MAX_ACTIVE_FOLD_PARTS = 1_024;
|
|
72
119
|
export const MAX_ACTIVE_EXPANDED = 1_024;
|
|
73
120
|
export const MAX_ACTIVE_PROTECTED = 1_024;
|
|
@@ -269,15 +316,54 @@ export function directFoldOwners(folds: readonly ActiveFold[]): Map<string, stri
|
|
|
269
316
|
return owner;
|
|
270
317
|
}
|
|
271
318
|
|
|
319
|
+
/**
|
|
320
|
+
* A FOLD THIS MATERIALIZATION HAS ALREADY PROVEN, AND WILL NOT PROVE AGAIN (2026-09-20).
|
|
321
|
+
*
|
|
322
|
+
* The replay's cost was never the deltas, it was re-proving the whole forest at each one.
|
|
323
|
+
* `stateFromFoldRefs` runs per delta and ends in `parseActiveContextState`, so a session with
|
|
324
|
+
* 2,207 deltas and 400 standing folds recomputed 400 content addresses and flattened and
|
|
325
|
+
* hashed 400 fold subtrees 2,207 times over. Profiling a 1,147-delta load put 48.2% of all
|
|
326
|
+
* time inside `validateFoldForest` and 14.8% on `foldIdFor` alone.
|
|
327
|
+
*
|
|
328
|
+
* WHAT THE MARK ASSERTS, and why each half of it is already true:
|
|
329
|
+
*
|
|
330
|
+
* The content address. `parseFoldRecordEntry` refuses a record unless
|
|
331
|
+
* `fold.id === foldIdFor(fold.kind, fold.parts)`, and every fold reaching a marked forest
|
|
332
|
+
* came out of `records` past that check, matched to its ref on `recordSha256`. The only
|
|
333
|
+
* field `assignFoldParents` writes is `parentId`, which `foldIdFor` does not read. So the
|
|
334
|
+
* recomputation could not reach a different verdict. This is the argument the 2026-08-28
|
|
335
|
+
* `foldRecordRef` change already made in `persist`, applied on the reading side.
|
|
336
|
+
*
|
|
337
|
+
* The source digest. This one is NOT implied by the record, so it is not skipped: it is
|
|
338
|
+
* paid ONCE PER DISTINCT FOLD PER MATERIALIZATION instead of once per delta. A fold's
|
|
339
|
+
* flattened refs are a pure function of its id, because the id pins kind and parts and each
|
|
340
|
+
* child id pins its own subtree in turn, so a second computation over the same id is
|
|
341
|
+
* arithmetic already done. `stateFromFoldRefs` marks only ids already proven against THIS
|
|
342
|
+
* records map and leaves a newly arriving fold unmarked, so its digest is still checked, and
|
|
343
|
+
* still inside `validateFoldForest` after the ownership loop that proves its children are
|
|
344
|
+
* present. Error ordering is unchanged.
|
|
345
|
+
*
|
|
346
|
+
* KEYED ON OBJECT IDENTITY, like gate 160's VALIDATED_FORESTS, so nothing outside this module
|
|
347
|
+
* can present itself as proven. It carries none of gate 160's stated cost: a fold mutated in
|
|
348
|
+
* place is never marked, because the mark is applied to the objects `assignFoldParents` has
|
|
349
|
+
* just built and is read before `clone` hands the forest on.
|
|
350
|
+
*/
|
|
351
|
+
const RECORD_PROVEN_FOLDS = new WeakSet<ActiveFold>();
|
|
352
|
+
const PROVEN_SOURCE_DIGESTS = new WeakMap<Map<string, FoldRecordEntry>, Set<string>>();
|
|
353
|
+
|
|
272
354
|
export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
|
|
273
355
|
const rawValues = denseOwnArrayValues(folds);
|
|
274
356
|
if (!rawValues || rawValues.some((fold) => !validFoldShape(fold))) {
|
|
275
357
|
throw new Error("Invalid active-context fold forest shape");
|
|
276
358
|
}
|
|
277
359
|
const values = clone(rawValues) as ActiveFold[];
|
|
360
|
+
// READ OFF THE CALLER'S OBJECTS, BEFORE THE CLONE ABOVE COSTS US THE IDENTITY. `clone`
|
|
361
|
+
// preserves order, so the verdict for `values[i]` is the mark on `rawValues[i]`.
|
|
362
|
+
const proven = rawValues.map((fold) => RECORD_PROVEN_FOLDS.has(fold as ActiveFold));
|
|
278
363
|
const byId = new Map<string, ActiveFold>();
|
|
279
|
-
for (
|
|
280
|
-
|
|
364
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
365
|
+
const fold = values[index];
|
|
366
|
+
if ((!proven[index] && fold.id !== foldIdFor(fold.kind, fold.parts)) || byId.has(fold.id)) {
|
|
281
367
|
throw new Error("Invalid active-context fold");
|
|
282
368
|
}
|
|
283
369
|
if (fold.kind === "tool-result" &&
|
|
@@ -304,7 +390,9 @@ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
|
|
|
304
390
|
for (const fold of values) {
|
|
305
391
|
if ((parent.get(fold.id) ?? null) !== fold.parentId) throw new Error(`Fold ${fold.id} parent drift`);
|
|
306
392
|
}
|
|
307
|
-
for (
|
|
393
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
394
|
+
if (proven[index]) continue;
|
|
395
|
+
const fold = values[index];
|
|
308
396
|
const refs = flattenFoldRefs(fold, { folds: values });
|
|
309
397
|
if (fold.sourceSha256 !== sha256Value(refs)) throw new Error(`Fold ${fold.id} source digest drift`);
|
|
310
398
|
}
|
|
@@ -890,13 +978,28 @@ export function stateFromFoldRefs(
|
|
|
890
978
|
if (!record || record.recordSha256 !== ref.sha256) throw new Error(`Missing active-context fold record ${ref.id}`);
|
|
891
979
|
return record.fold;
|
|
892
980
|
});
|
|
981
|
+
const parented = assignFoldParents(folds);
|
|
982
|
+
// WHAT THIS MATERIALIZATION HAS ALREADY PROVEN IS MARKED; THE REST IS STILL PROVEN BELOW.
|
|
983
|
+
// See RECORD_PROVEN_FOLDS. A fold standing across deltas is marked from its second
|
|
984
|
+
// appearance on, so its content address and source digest are computed once for the whole
|
|
985
|
+
// replay rather than once per delta. A fold arriving now is deliberately left unmarked: its
|
|
986
|
+
// digest is checked by `validateFoldForest`, after the loop that proves its children are
|
|
987
|
+
// present, exactly where it was checked before.
|
|
988
|
+
let provenIds = PROVEN_SOURCE_DIGESTS.get(records);
|
|
989
|
+
if (!provenIds) {
|
|
990
|
+
provenIds = new Set<string>();
|
|
991
|
+
PROVEN_SOURCE_DIGESTS.set(records, provenIds);
|
|
992
|
+
}
|
|
993
|
+
for (const fold of parented) {
|
|
994
|
+
if (provenIds.has(fold.id)) RECORD_PROVEN_FOLDS.add(fold);
|
|
995
|
+
}
|
|
893
996
|
const state: ActiveContextState = {
|
|
894
997
|
version: 1,
|
|
895
998
|
sessionId: wire.sessionId,
|
|
896
999
|
revision: wire.revision,
|
|
897
1000
|
// `parseActiveContextState` at the end of this function validates the forest, so
|
|
898
1001
|
// validating it here as well was the same walk twice on one persist.
|
|
899
|
-
folds:
|
|
1002
|
+
folds: parented,
|
|
900
1003
|
expanded: clone(wire.expanded),
|
|
901
1004
|
protected: clone(wire.protected),
|
|
902
1005
|
tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
|
|
@@ -907,7 +1010,10 @@ export function stateFromFoldRefs(
|
|
|
907
1010
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
908
1011
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
909
1012
|
};
|
|
910
|
-
|
|
1013
|
+
const parsed = parseActiveContextState(state, wire.sessionId, false);
|
|
1014
|
+
// RECORDED ONLY ONCE THE FOREST HAS PASSED, so a throw leaves nothing marked as proven.
|
|
1015
|
+
for (const fold of parented) provenIds.add(fold.id);
|
|
1016
|
+
return parsed;
|
|
911
1017
|
}
|
|
912
1018
|
|
|
913
1019
|
export interface ProjectionFingerprint {
|
|
@@ -933,6 +1039,12 @@ export interface MaterializedStatePersistence {
|
|
|
933
1039
|
records: Map<string, FoldRecordEntry>;
|
|
934
1040
|
stateSha256: string;
|
|
935
1041
|
projectionFingerprints: ProjectionFingerprints;
|
|
1042
|
+
/**
|
|
1043
|
+
* HOW MANY DELTAS STAND AFTER THE LAST CHECKPOINT in the branch just read. `persist` writes
|
|
1044
|
+
* a checkpoint instead of a delta once this reaches `MAX_DELTAS_BETWEEN_CHECKPOINTS`, which
|
|
1045
|
+
* is what stops the run, and therefore the load, from growing without bound.
|
|
1046
|
+
*/
|
|
1047
|
+
deltasSinceCheckpoint: number;
|
|
936
1048
|
}
|
|
937
1049
|
|
|
938
1050
|
export function materializeStatePersistence(
|
|
@@ -985,6 +1097,7 @@ export function materializeStatePersistence(
|
|
|
985
1097
|
let stateStart = -1;
|
|
986
1098
|
let checkpointIndex = -1;
|
|
987
1099
|
let v2Seen = false;
|
|
1100
|
+
let deltasSinceCheckpoint = 0;
|
|
988
1101
|
const rememberProjection = (): void => {
|
|
989
1102
|
fingerprintSources.set(state.revision, state);
|
|
990
1103
|
};
|
|
@@ -1001,11 +1114,25 @@ export function materializeStatePersistence(
|
|
|
1001
1114
|
if (version !== 2) throw new Error("Invalid active-context persisted state version");
|
|
1002
1115
|
const kind = ownValue(entry.data, "kind");
|
|
1003
1116
|
if (kind === "checkpoint") {
|
|
1004
|
-
|
|
1117
|
+
// THE LAST CHECKPOINT WINS, AND A SECOND ONE IS NO LONGER FATAL (2026-09-20). This
|
|
1118
|
+
// raised "Duplicate active-context v2 checkpoint" on sight of a second, which made the
|
|
1119
|
+
// ledger append-only in the strongest sense: one checkpoint, then deltas forever, and a
|
|
1120
|
+
// replay that walked all of them on every load. Restarting the replay at the newest
|
|
1121
|
+
// checkpoint is what makes `MAX_DELTAS_BETWEEN_CHECKPOINTS` possible, and it reaches
|
|
1122
|
+
// the same final state by the same digest equalities.
|
|
1123
|
+
//
|
|
1124
|
+
// It also un-latches the failure gate 173 describes. A branch that took a second
|
|
1125
|
+
// checkpoint from a failed restore used to be unreadable forever, and every reload made
|
|
1126
|
+
// it worse; such a branch now reads from its newest checkpoint. That is a second line of
|
|
1127
|
+
// defence, not the first: the `lineageUnreadable` guard in `persist` still refuses to
|
|
1128
|
+
// write the state entry that starts the damage, and gate 173 still pins it.
|
|
1005
1129
|
checkpointIndex = index;
|
|
1006
1130
|
stateStart = index;
|
|
1131
|
+
deltasSinceCheckpoint = 0;
|
|
1007
1132
|
} else if (checkpointIndex < 0) {
|
|
1008
1133
|
throw new Error("Active-context v2 delta precedes migration checkpoint");
|
|
1134
|
+
} else {
|
|
1135
|
+
deltasSinceCheckpoint += 1;
|
|
1009
1136
|
}
|
|
1010
1137
|
v2Seen = true;
|
|
1011
1138
|
}
|
|
@@ -1142,7 +1269,7 @@ export function materializeStatePersistence(
|
|
|
1142
1269
|
writtenSha256 = wire.stateSha256 === calculated ? null : wire.stateSha256;
|
|
1143
1270
|
rememberProjection();
|
|
1144
1271
|
}
|
|
1145
|
-
return { state, wireVersion, records, stateSha256, projectionFingerprints };
|
|
1272
|
+
return { state, wireVersion, records, stateSha256, projectionFingerprints, deltasSinceCheckpoint };
|
|
1146
1273
|
}
|
|
1147
1274
|
|
|
1148
1275
|
export function materializeActiveContextState(
|
package/extensions/lib/policy.ts
CHANGED
|
@@ -343,6 +343,26 @@ export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
|
|
|
343
343
|
export const MAX_UNMARKED_CANDIDATES = 3;
|
|
344
344
|
|
|
345
345
|
export const MAX_FOLD_SPAN_CHARS = 16_000;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* THE LADDER'S CHAPTER CAP FOLLOWS THE FLOOR (2026-09-07).
|
|
349
|
+
*
|
|
350
|
+
* `selectAutomaticChapter` accumulates whole units until a span reaches `minFoldChars` and
|
|
351
|
+
* refuses a multi-unit span past this cap, so the cap has to sit at least one unit above
|
|
352
|
+
* the floor or the walk can never satisfy both. At the shipped 8,000 the 16,000 constant
|
|
353
|
+
* does. /fold-settings offers floors up to 32,000, and at any floor past the constant every
|
|
354
|
+
* multi-unit chapter was refused by arithmetic: only a single oversized unit or a tool batch
|
|
355
|
+
* could fold, and a prose session with the floor at 32,000 held its raw material forever
|
|
356
|
+
* while the status row announced a commit (session 01a07498, 2026-09-07: fourteen sizable
|
|
357
|
+
* units in view, none over 32,000, the frontier staging nothing). Twice the floor is the
|
|
358
|
+
* smallest cap under which a walk that crosses the floor by one unit shorter than the floor
|
|
359
|
+
* always lands inside it; the constant stays as the cap's own floor so every default reads
|
|
360
|
+
* exactly as before, and a manual span is never bounded by either (Shane, 2026-08-22).
|
|
361
|
+
*/
|
|
362
|
+
export function chapterSpanCap(minFoldChars: number): number {
|
|
363
|
+
return Math.max(MAX_FOLD_SPAN_CHARS, 2 * minFoldChars);
|
|
364
|
+
}
|
|
365
|
+
|
|
346
366
|
export const PEEK_DEFAULT_MAX_BYTES = 16_000;
|
|
347
367
|
export const PEEK_HEAD_SHARE = 0.6;
|
|
348
368
|
|