pi-fold 2.0.0 → 3.1.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.
- package/README.md +79 -191
- package/extensions/active-context.ts +1332 -526
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +17 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +94 -61
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +207 -59
- package/extensions/lib/instrumentation.ts +10 -9
- package/extensions/lib/measurement.ts +43 -16
- package/extensions/lib/persistence.ts +273 -46
- package/extensions/lib/policy.ts +251 -95
- package/extensions/lib/scheduling.ts +446 -126
- package/extensions/lib/selection.ts +117 -116
- package/extensions/lib/tool-surface.ts +60 -33
- package/extensions/lib/transcript.ts +82 -74
- package/extensions/settings.ts +456 -0
- package/package.json +4 -1
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
EXPAND_LEASE_GENERATIONS,
|
|
23
23
|
MAX_EXPAND_LEASES,
|
|
24
24
|
MAX_PENDING_MARKS,
|
|
25
|
+
MEMORY_BODY_CHARS,
|
|
26
|
+
MEMORY_KEY_CHARS,
|
|
27
|
+
MEMORY_KEYS_MAX,
|
|
25
28
|
} from "./policy.ts";
|
|
26
29
|
import type {
|
|
27
30
|
ActiveContextCheckpointV2,
|
|
@@ -32,6 +35,8 @@ import type {
|
|
|
32
35
|
AdvisoryMilestone,
|
|
33
36
|
BriefOverride,
|
|
34
37
|
BriefProvenance,
|
|
38
|
+
ToolClip,
|
|
39
|
+
WorkingMemoryEntry,
|
|
35
40
|
FoldKind,
|
|
36
41
|
FoldPart,
|
|
37
42
|
FoldRecordEntry,
|
|
@@ -56,7 +61,7 @@ export const PENDING_FOLD_MARK_KEYS = [
|
|
|
56
61
|
"mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
|
|
57
62
|
] as const;
|
|
58
63
|
export const PENDING_REFOLD_MARK_KEYS = ["mark", "id", "origin", "ordinal"] as const;
|
|
59
|
-
export const MARK_ORIGINS = Object.freeze(["agent", "ladder"] as const);
|
|
64
|
+
export const MARK_ORIGINS = Object.freeze(["agent", "ladder", "user"] as const);
|
|
60
65
|
export const STATE_CHECKPOINT_V2_KEYS = [
|
|
61
66
|
"version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
|
|
62
67
|
] as const;
|
|
@@ -91,8 +96,7 @@ export function validPendingMark(value: unknown): value is PendingMark {
|
|
|
91
96
|
const kind = ownValue(value, "kind");
|
|
92
97
|
const parts = denseOwnArrayValues(ownValue(value, "parts"));
|
|
93
98
|
return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
|
|
94
|
-
Boolean(parts?.length) && parts!.
|
|
95
|
-
parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
|
|
99
|
+
Boolean(parts?.length) && parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
|
|
96
100
|
validProvenance(ownValue(value, "briefProvenance")) &&
|
|
97
101
|
id === foldIdFor(kind as FoldKind, parts as FoldPart[]);
|
|
98
102
|
}
|
|
@@ -168,7 +172,8 @@ export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Reco
|
|
|
168
172
|
|
|
169
173
|
export function validProvenance(value: unknown): value is BriefProvenance {
|
|
170
174
|
if (exactRecord(value, ["kind"]) &&
|
|
171
|
-
(ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic"
|
|
175
|
+
(ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic" ||
|
|
176
|
+
ownValue(value, "kind") === "augmented")) return true;
|
|
172
177
|
const hasDigest = Boolean(value && typeof value === "object" &&
|
|
173
178
|
Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
|
|
174
179
|
if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
|
|
@@ -210,7 +215,6 @@ export function validFoldShape(value: unknown): value is ActiveFold {
|
|
|
210
215
|
return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
|
|
211
216
|
typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
|
|
212
217
|
(parentId === null || typeof parentId === "string") && Boolean(parts?.length) &&
|
|
213
|
-
parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
|
|
214
218
|
parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
|
|
215
219
|
validProvenance(ownValue(value, "provenance")) && typeof sourceSha256 === "string" &&
|
|
216
220
|
/^[a-f0-9]{64}$/.test(sourceSha256) && Number.isSafeInteger(ownValue(value, "sourceChars")) &&
|
|
@@ -325,7 +329,11 @@ export function parseActiveContextState(
|
|
|
325
329
|
const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
|
|
326
330
|
const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
|
|
327
331
|
const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
|
|
328
|
-
const
|
|
332
|
+
const hasClips = recordLike && Object.prototype.hasOwnProperty.call(value, "clips");
|
|
333
|
+
const hasMemory = recordLike && Object.prototype.hasOwnProperty.call(value, "memory");
|
|
334
|
+
// Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
|
|
335
|
+
// to reproduce, so here the tolerance is the whole of it.
|
|
336
|
+
const hasLegacyRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
|
|
329
337
|
refuseRetiredStateFields(value);
|
|
330
338
|
const extraKeys = [
|
|
331
339
|
...(hasPrepared ? ["prepared"] : []),
|
|
@@ -335,7 +343,9 @@ export function parseActiveContextState(
|
|
|
335
343
|
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
336
344
|
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
337
345
|
...(hasBriefs ? ["briefs"] : []),
|
|
338
|
-
...(
|
|
346
|
+
...(hasClips ? ["clips"] : []),
|
|
347
|
+
...(hasMemory ? ["memory"] : []),
|
|
348
|
+
...(hasLegacyRider ? ["rider"] : []),
|
|
339
349
|
];
|
|
340
350
|
if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
|
|
341
351
|
const folds = denseOwnArrayValues(ownValue(value, "folds"));
|
|
@@ -357,9 +367,6 @@ export function parseActiveContextState(
|
|
|
357
367
|
if (hasAdvisory && !validAdvisoryState(ownValue(value, "advisory"))) {
|
|
358
368
|
throw new Error("Invalid active-context advisory state");
|
|
359
369
|
}
|
|
360
|
-
if (hasRider && !validRiderState(ownValue(value, "rider"))) {
|
|
361
|
-
throw new Error("Invalid active-context rider state");
|
|
362
|
-
}
|
|
363
370
|
if (hasTokensSinceToolFold && !validTokensSinceToolFold(ownValue(value, "tokensSinceToolFold"))) {
|
|
364
371
|
throw new Error("Invalid active-context tool-fold cadence");
|
|
365
372
|
}
|
|
@@ -369,6 +376,8 @@ export function parseActiveContextState(
|
|
|
369
376
|
throw new Error("Invalid active-context pending marks");
|
|
370
377
|
}
|
|
371
378
|
const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
|
|
379
|
+
const clips = hasClips ? parseToolClips(ownValue(value, "clips")) : [];
|
|
380
|
+
const memory = hasMemory ? parseWorkingMemory(ownValue(value, "memory")) : [];
|
|
372
381
|
const source = clone(value) as unknown as ActiveContextState;
|
|
373
382
|
return {
|
|
374
383
|
version: 1,
|
|
@@ -383,14 +392,56 @@ export function parseActiveContextState(
|
|
|
383
392
|
leases,
|
|
384
393
|
...(marks.length ? { pendingMarks: marks } : {}),
|
|
385
394
|
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
395
|
+
...(clips.length ? { clips } : {}),
|
|
396
|
+
...(memory.length ? { memory } : {}),
|
|
386
397
|
...(hasAdvisory
|
|
387
398
|
? { advisory: clone(source.advisory!) }
|
|
388
399
|
: defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
|
|
389
|
-
...(hasRider ? { rider: clone(source.rider!) } : {}),
|
|
390
400
|
...(hasPrepared ? { prepared: clone(source.prepared!) } : {}),
|
|
391
401
|
};
|
|
392
402
|
}
|
|
393
403
|
|
|
404
|
+
function parseToolClips(value: unknown): ToolClip[] {
|
|
405
|
+
const entries = denseOwnArrayValues(value);
|
|
406
|
+
if (!entries) throw new Error("Invalid active-context clips");
|
|
407
|
+
const clips = entries.map((entry) => {
|
|
408
|
+
if (!exactRecord(entry, ["callId", "entryId"]) ||
|
|
409
|
+
typeof ownValue(entry, "callId") !== "string" || !ownValue(entry, "callId") ||
|
|
410
|
+
typeof ownValue(entry, "entryId") !== "string" || !ownValue(entry, "entryId")) {
|
|
411
|
+
throw new Error("Invalid active-context clip entry");
|
|
412
|
+
}
|
|
413
|
+
return { callId: String(ownValue(entry, "callId")), entryId: String(ownValue(entry, "entryId")) };
|
|
414
|
+
});
|
|
415
|
+
if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
|
|
416
|
+
throw new Error("Invalid active-context clips: duplicate call id");
|
|
417
|
+
}
|
|
418
|
+
return clips;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function parseWorkingMemory(value: unknown): WorkingMemoryEntry[] {
|
|
422
|
+
const entries = denseOwnArrayValues(value);
|
|
423
|
+
if (!entries) throw new Error("Invalid active-context working memory");
|
|
424
|
+
const memory = entries.map((entry) => {
|
|
425
|
+
const key = ownValue(entry, "key");
|
|
426
|
+
const body = ownValue(entry, "body");
|
|
427
|
+
const ordinal = ownValue(entry, "ordinal");
|
|
428
|
+
if (!exactRecord(entry, ["key", "body", "ordinal"]) ||
|
|
429
|
+
typeof key !== "string" || !key || key.length > MEMORY_KEY_CHARS ||
|
|
430
|
+
typeof body !== "string" || !body || body.length > MEMORY_BODY_CHARS ||
|
|
431
|
+
!Number.isSafeInteger(ordinal) || Number(ordinal) < 0) {
|
|
432
|
+
throw new Error("Invalid active-context working-memory entry");
|
|
433
|
+
}
|
|
434
|
+
return { key, body, ordinal: Number(ordinal) };
|
|
435
|
+
});
|
|
436
|
+
if (memory.length > MEMORY_KEYS_MAX) {
|
|
437
|
+
throw new Error(`Invalid active-context working memory: over ${MEMORY_KEYS_MAX} entries`);
|
|
438
|
+
}
|
|
439
|
+
if (new Set(memory.map((entry) => entry.key)).size !== memory.length) {
|
|
440
|
+
throw new Error("Invalid active-context working memory: duplicate key");
|
|
441
|
+
}
|
|
442
|
+
return memory;
|
|
443
|
+
}
|
|
444
|
+
|
|
394
445
|
export function clearPrepared(state: ActiveContextState): ActiveContextState {
|
|
395
446
|
const next = { ...state };
|
|
396
447
|
delete next.prepared;
|
|
@@ -416,8 +467,24 @@ export function validatePreparedShape(value: unknown): asserts value is Prepared
|
|
|
416
467
|
}
|
|
417
468
|
}
|
|
418
469
|
|
|
470
|
+
// THE SPREAD IS THE COPY (2026-08-28). Roughly six calls per fold per persist, and only
|
|
471
|
+
// `deriveFoldParents` wants a record it can write to, where it writes the TOP-LEVEL
|
|
472
|
+
// `parentId` that the spread already gives it a fresh slot for. Every other caller hashes
|
|
473
|
+
// and discards: `foldRecordRef`, `sameFoldRecordIdentity`, and `makeFoldRecordEntry`, whose
|
|
474
|
+
// `parseFoldRecordEntry` deep-clones twice on its own.
|
|
475
|
+
//
|
|
476
|
+
// The digest cannot move. `safeJson` emits in `Reflect.ownKeys` order, a spread preserves
|
|
477
|
+
// insertion order, and overwriting an existing key keeps it in its original position, so
|
|
478
|
+
// `{ ...fold, parentId: null }` serializes to the same bytes as the cloned form.
|
|
479
|
+
//
|
|
480
|
+
// THE COST, STATED BECAUSE IT IS NOW LOAD-BEARING: the returned record shares every nested
|
|
481
|
+
// object with its input, so "no caller mutates a canonical record's nested fields" is a rule
|
|
482
|
+
// this function relies on rather than enforces. It holds today (`validateFoldForest` clones
|
|
483
|
+
// at :281, `parseFoldRecordEntry` twice at :567-568, the hashing callers never write), and a
|
|
484
|
+
// future caller that writes through `record.parts` would corrupt the fold it was built from.
|
|
485
|
+
// Write `{ ...record, field: value }` rather than assigning into a nested one.
|
|
419
486
|
export function canonicalFoldRecord(fold: ActiveFold): ActiveFold {
|
|
420
|
-
return { ...
|
|
487
|
+
return { ...fold, parentId: null };
|
|
421
488
|
}
|
|
422
489
|
|
|
423
490
|
export function sameFoldRecordIdentity(left: ActiveFold, right: ActiveFold): boolean {
|
|
@@ -441,6 +508,29 @@ export function legacyCadenceOmittedStateSha256(state: ActiveContextState): stri
|
|
|
441
508
|
return sha256Value(legacy);
|
|
442
509
|
}
|
|
443
510
|
|
|
511
|
+
/**
|
|
512
|
+
* THE DIGEST A RIDER-ERA BUILD WROTE, reproduced without the subsystem.
|
|
513
|
+
*
|
|
514
|
+
* `semanticStateSha256` hashes the materialized state object, and `stableStringify` walks
|
|
515
|
+
* own keys in INSERTION order, so a field that is gone changes the digest even though it
|
|
516
|
+
* changed nothing else. The rider sat immediately before `prepared` in the materialized
|
|
517
|
+
* literal, and last when there was no prepared, so that is where it goes back for the one
|
|
518
|
+
* moment it is needed. It joins the two legacy derivations already here, which exist for
|
|
519
|
+
* exactly this reason: a state stays verifiable across the build that stopped writing it.
|
|
520
|
+
*
|
|
521
|
+
* Consulted ONLY when the wire actually carried a rider, so a current state whose digest
|
|
522
|
+
* has genuinely drifted still fails rather than finding a second way to pass.
|
|
523
|
+
*/
|
|
524
|
+
export function legacyRiderStateSha256(state: ActiveContextState, rider: unknown): string {
|
|
525
|
+
const legacy: Record<string, unknown> = {};
|
|
526
|
+
for (const [key, value] of Object.entries(state)) {
|
|
527
|
+
if (key === "prepared") legacy.rider = rider;
|
|
528
|
+
legacy[key] = value;
|
|
529
|
+
}
|
|
530
|
+
if (!Object.prototype.hasOwnProperty.call(legacy, "rider")) legacy.rider = rider;
|
|
531
|
+
return sha256Value(legacy);
|
|
532
|
+
}
|
|
533
|
+
|
|
444
534
|
export function legacyReplayOrderStateSha256(state: ActiveContextState): string {
|
|
445
535
|
return sha256Value({
|
|
446
536
|
version: state.version,
|
|
@@ -456,11 +546,18 @@ export function legacyReplayOrderStateSha256(state: ActiveContextState): string
|
|
|
456
546
|
|
|
457
547
|
export function sameStateProjection(left: ActiveContextState, right: ActiveContextState): boolean {
|
|
458
548
|
const normalized = (value: ActiveContextState): Partial<ActiveContextState> => {
|
|
459
|
-
|
|
549
|
+
// A SHALLOW COPY IS ENOUGH TO DELETE TOP-LEVEL KEYS (2026-08-28). `delete` on a shallow
|
|
550
|
+
// copy cannot reach the original, the normalized object never escapes this function, and
|
|
551
|
+
// the runtime already stableStringifies live un-cloned state (`semanticStateSha256`), so
|
|
552
|
+
// un-cloned state is already required to be serializer-safe. Measured 97.6ms to 82.6ms
|
|
553
|
+
// on the largest real state in the corpus, at about two calls per provider request.
|
|
554
|
+
const normalizedState = { ...value, revision: 0 } as Partial<ActiveContextState>;
|
|
460
555
|
if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
|
|
461
556
|
if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
|
|
462
557
|
if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
|
|
463
558
|
if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
|
|
559
|
+
if (normalizedState.clips && normalizedState.clips.length === 0) delete normalizedState.clips;
|
|
560
|
+
if (normalizedState.memory && normalizedState.memory.length === 0) delete normalizedState.memory;
|
|
464
561
|
return normalizedState;
|
|
465
562
|
};
|
|
466
563
|
return stableStringify(normalized(left)) === stableStringify(normalized(right));
|
|
@@ -503,13 +600,11 @@ export function validateV2ProjectionFields(
|
|
|
503
600
|
leasesValue: unknown,
|
|
504
601
|
pendingMarksValue?: unknown,
|
|
505
602
|
briefsValue?: unknown,
|
|
506
|
-
riderValue?: unknown,
|
|
507
603
|
): {
|
|
508
604
|
expanded: string[];
|
|
509
605
|
protected: EvidenceRef[];
|
|
510
606
|
prepared?: PreparedFold;
|
|
511
607
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
512
|
-
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
513
608
|
tokensSinceToolFold: number;
|
|
514
609
|
leases: Record<string, number>;
|
|
515
610
|
pendingMarks: PendingMark[];
|
|
@@ -527,9 +622,6 @@ export function validateV2ProjectionFields(
|
|
|
527
622
|
if (advisoryValue !== undefined && !validAdvisoryState(advisoryValue)) {
|
|
528
623
|
throw new Error("Invalid active-context v2 advisory state");
|
|
529
624
|
}
|
|
530
|
-
if (riderValue !== undefined && !validRiderState(riderValue)) {
|
|
531
|
-
throw new Error("Invalid active-context v2 rider state");
|
|
532
|
-
}
|
|
533
625
|
if (tokensSinceToolFoldValue !== undefined && !validTokensSinceToolFold(tokensSinceToolFoldValue)) {
|
|
534
626
|
throw new Error("Invalid active-context v2 tool-fold cadence");
|
|
535
627
|
}
|
|
@@ -543,9 +635,6 @@ export function validateV2ProjectionFields(
|
|
|
543
635
|
...(advisoryValue === undefined ? {} : {
|
|
544
636
|
advisory: clone(advisoryValue) as NonNullable<ActiveContextState["advisory"]>,
|
|
545
637
|
}),
|
|
546
|
-
...(riderValue === undefined ? {} : {
|
|
547
|
-
rider: clone(riderValue) as NonNullable<ActiveContextState["rider"]>,
|
|
548
|
-
}),
|
|
549
638
|
tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
|
|
550
639
|
leases,
|
|
551
640
|
pendingMarks,
|
|
@@ -575,7 +664,15 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
575
664
|
Object.prototype.hasOwnProperty.call(value, "addBriefs"));
|
|
576
665
|
const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
|
|
577
666
|
Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
|
|
578
|
-
const
|
|
667
|
+
const hasWireClips = Boolean(value && typeof value === "object" &&
|
|
668
|
+
Object.prototype.hasOwnProperty.call(value, "clips"));
|
|
669
|
+
const hasClipBase = Boolean(value && typeof value === "object" &&
|
|
670
|
+
Object.prototype.hasOwnProperty.call(value, "clipBase"));
|
|
671
|
+
const hasAddClips = Boolean(value && typeof value === "object" &&
|
|
672
|
+
Object.prototype.hasOwnProperty.call(value, "addClips"));
|
|
673
|
+
const hasWireMemory = Boolean(value && typeof value === "object" &&
|
|
674
|
+
Object.prototype.hasOwnProperty.call(value, "memory"));
|
|
675
|
+
const hasLegacyRider = Boolean(value && typeof value === "object" &&
|
|
579
676
|
Object.prototype.hasOwnProperty.call(value, "rider"));
|
|
580
677
|
refuseRetiredStateFields(value);
|
|
581
678
|
const optionalKeys = [
|
|
@@ -589,7 +686,13 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
589
686
|
...(hasBriefs ? ["briefs"] : []),
|
|
590
687
|
...(hasAddBriefs ? ["addBriefs"] : []),
|
|
591
688
|
...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
|
|
592
|
-
...(
|
|
689
|
+
...(hasWireClips ? ["clips"] : []),
|
|
690
|
+
...(hasClipBase ? ["clipBase"] : []),
|
|
691
|
+
...(hasAddClips ? ["addClips"] : []),
|
|
692
|
+
...(hasWireMemory ? ["memory"] : []),
|
|
693
|
+
// Carried no further than the digest check in `materializeStatePersistence`, which is
|
|
694
|
+
// the only thing on a load that the rider's absence can change.
|
|
695
|
+
...(hasLegacyRider ? ["rider"] : []),
|
|
593
696
|
];
|
|
594
697
|
const checkpoint = kind === "checkpoint" &&
|
|
595
698
|
exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
|
|
@@ -605,8 +708,19 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
605
708
|
ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
|
|
606
709
|
ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
|
|
607
710
|
ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
|
|
608
|
-
ownValue(value, "rider"),
|
|
609
711
|
);
|
|
712
|
+
if (hasWireClips) parseToolClips(ownValue(value, "clips"));
|
|
713
|
+
if (hasAddClips) parseToolClips(ownValue(value, "addClips"));
|
|
714
|
+
if (hasWireMemory) parseWorkingMemory(ownValue(value, "memory"));
|
|
715
|
+
if (hasClipBase) {
|
|
716
|
+
const base = ownValue(value, "clipBase");
|
|
717
|
+
if (!Number.isSafeInteger(base) || Number(base) < 0) throw new Error("Invalid active-context clip base");
|
|
718
|
+
}
|
|
719
|
+
// A whole array and a change cannot both be the truth on one wire.
|
|
720
|
+
if (hasWireClips && (hasClipBase || hasAddClips)) {
|
|
721
|
+
throw new Error("Invalid active-context clips: whole array beside a change");
|
|
722
|
+
}
|
|
723
|
+
if (hasAddClips && !hasClipBase) throw new Error("Invalid active-context clips: change without a base");
|
|
610
724
|
if (checkpoint) {
|
|
611
725
|
const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
|
|
612
726
|
if (!refs || refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Invalid active-context checkpoint refs");
|
|
@@ -681,7 +795,25 @@ export function normalizeFoldsForPersistedRecords(
|
|
|
681
795
|
return deriveFoldParents(normalized);
|
|
682
796
|
}
|
|
683
797
|
|
|
684
|
-
|
|
798
|
+
/**
|
|
799
|
+
* Rebuild the parent links a fold forest's records do not carry.
|
|
800
|
+
*
|
|
801
|
+
* THIS DOES NOT VALIDATE THE FOREST, and the split is deliberate. `validateFoldForest` deep
|
|
802
|
+
* clones the forest, recomputes `foldIdFor` per fold, and calls `flattenFoldRefs` per fold,
|
|
803
|
+
* which rebuilds `foldMap` over every fold; two of the four callers handed the result
|
|
804
|
+
* straight to `parseActiveContextState`, whose FIRST act is to validate it again. So a
|
|
805
|
+
* persist validated the same forest three times. Roughly 12ms saved at 88 folds, 28 at 178,
|
|
806
|
+
* 55 at 267, plus about 1.6x on the session-start load path.
|
|
807
|
+
*
|
|
808
|
+
* The child-record check above stays here, because it is about the ASSIGNMENT rather than
|
|
809
|
+
* the forest: a child claimed twice, or claimed by a parent that does not hold it, is a
|
|
810
|
+
* contradiction this loop is the only thing positioned to see.
|
|
811
|
+
*
|
|
812
|
+
* Callers that do not parse immediately afterwards want `deriveFoldParents` below. Reaching
|
|
813
|
+
* for this one is a claim that the very next statement validates the same array; nothing
|
|
814
|
+
* else makes it safe, and it reads at review as removing validation, which it is not.
|
|
815
|
+
*/
|
|
816
|
+
export function assignFoldParents(records: ActiveFold[]): ActiveFold[] {
|
|
685
817
|
const folds = records.map(canonicalFoldRecord);
|
|
686
818
|
const byId = new Map(folds.map((fold) => [fold.id, fold]));
|
|
687
819
|
const assigned = new Set<string>();
|
|
@@ -693,14 +825,19 @@ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
|
|
|
693
825
|
assigned.add(childId);
|
|
694
826
|
}
|
|
695
827
|
}
|
|
696
|
-
return
|
|
828
|
+
return folds;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** Assign the parents and validate the forest, for callers that do not parse next. */
|
|
832
|
+
export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
|
|
833
|
+
return validateFoldForest(assignFoldParents(records));
|
|
697
834
|
}
|
|
698
835
|
|
|
699
836
|
export function stateFromFoldRefs(
|
|
700
837
|
wire: Pick<
|
|
701
838
|
ActiveContextCheckpointV2,
|
|
702
839
|
"sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
|
|
703
|
-
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "
|
|
840
|
+
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips" | "memory"
|
|
704
841
|
>,
|
|
705
842
|
refs: FoldRecordRef[],
|
|
706
843
|
records: Map<string, FoldRecordEntry>,
|
|
@@ -715,15 +852,18 @@ export function stateFromFoldRefs(
|
|
|
715
852
|
version: 1,
|
|
716
853
|
sessionId: wire.sessionId,
|
|
717
854
|
revision: wire.revision,
|
|
718
|
-
|
|
855
|
+
// `parseActiveContextState` at the end of this function validates the forest, so
|
|
856
|
+
// validating it here as well was the same walk twice on one persist.
|
|
857
|
+
folds: assignFoldParents(folds),
|
|
719
858
|
expanded: clone(wire.expanded),
|
|
720
859
|
protected: clone(wire.protected),
|
|
721
860
|
tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
|
|
722
861
|
leases: clone(wire.leases ?? {}),
|
|
723
862
|
...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
|
|
724
863
|
...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
|
|
864
|
+
...(wire.clips?.length ? { clips: clone(wire.clips) } : {}),
|
|
865
|
+
...(wire.memory?.length ? { memory: clone(wire.memory) } : {}),
|
|
725
866
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
726
|
-
...(wire.rider === undefined ? {} : { rider: clone(wire.rider) }),
|
|
727
867
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
728
868
|
};
|
|
729
869
|
return parseActiveContextState(state, wire.sessionId, false);
|
|
@@ -793,6 +933,14 @@ export function materializeStatePersistence(
|
|
|
793
933
|
},
|
|
794
934
|
};
|
|
795
935
|
let stateSha256 = semanticStateSha256(state);
|
|
936
|
+
// WHAT THE LAST ACCEPTED ENTRY ACTUALLY RECORDED, when that is not what this build
|
|
937
|
+
// would compute. A state accepted through one of the legacy derivations below was
|
|
938
|
+
// written by a build that hashed it differently, and the delta that follows it names
|
|
939
|
+
// its base by THAT digest, so a chain of them is unreadable unless the reading carries
|
|
940
|
+
// both: the modern digest for what this build writes next, and the written one for what
|
|
941
|
+
// the next entry will name. Null whenever they agree, which is every state this build
|
|
942
|
+
// wrote itself, so the chain check below is unchanged for them.
|
|
943
|
+
let writtenSha256: string | null = null;
|
|
796
944
|
let stateStart = -1;
|
|
797
945
|
let checkpointIndex = -1;
|
|
798
946
|
let v2Seen = false;
|
|
@@ -845,11 +993,21 @@ export function materializeStatePersistence(
|
|
|
845
993
|
if (wire.kind === "checkpoint") {
|
|
846
994
|
state = stateFromFoldRefs(wire, wire.foldRefs, records);
|
|
847
995
|
} else {
|
|
848
|
-
if (wireVersion === 0 || wire.baseRevision !== state.revision ||
|
|
996
|
+
if (wireVersion === 0 || wire.baseRevision !== state.revision ||
|
|
997
|
+
(wire.baseStateSha256 !== stateSha256 && wire.baseStateSha256 !== writtenSha256) ||
|
|
849
998
|
wire.revision <= wire.baseRevision) {
|
|
850
999
|
throw new Error("Broken active-context delta chain");
|
|
851
1000
|
}
|
|
852
|
-
|
|
1001
|
+
// THE RECORD MAP ALREADY HOLDS THIS DIGEST (2026-08-28). `foldRecordRef` canonicalizes
|
|
1002
|
+
// and hashes a whole fold, and this ran once per delta entry over every standing fold:
|
|
1003
|
+
// 48,715 calls in ONE load of a real sealed session, 2.6 to 3.7s of a 24 to 33s load.
|
|
1004
|
+
// `records` is built by the same replay and carries the same digest verbatim. The
|
|
1005
|
+
// fallback is unreachable, because the pre-scan raises on a delta before a checkpoint;
|
|
1006
|
+
// it is kept so the change is exactly behaviour-preserving rather than nearly so.
|
|
1007
|
+
const refs = state.folds.map((fold) => {
|
|
1008
|
+
const record = records.get(fold.id);
|
|
1009
|
+
return record ? { id: fold.id, sha256: record.recordSha256 } : foldRecordRef(fold);
|
|
1010
|
+
});
|
|
853
1011
|
const byId = new Map(refs.map((ref) => [ref.id, ref]));
|
|
854
1012
|
for (const id of wire.removeFoldIds) {
|
|
855
1013
|
if (!byId.delete(id)) throw new Error(`Unknown active-context delta removal ${id}`);
|
|
@@ -905,24 +1063,43 @@ export function materializeStatePersistence(
|
|
|
905
1063
|
return mark;
|
|
906
1064
|
});
|
|
907
1065
|
}
|
|
1066
|
+
// The clips replay by the same discriminator rule as the briefs and the marks: the
|
|
1067
|
+
// presence of the CHANGE is what says a wire carries one. A delta written before
|
|
1068
|
+
// 2026-08-24 states the whole array when it has one and states nothing when it does
|
|
1069
|
+
// not, so "no array" means "no clips" there, exactly as it does for the marks.
|
|
1070
|
+
const clips = wire.clipBase === undefined
|
|
1071
|
+
? (wire.clips !== undefined ? clone(wire.clips) : [])
|
|
1072
|
+
: [...(state.clips ?? []).slice(0, wire.clipBase), ...clone(wire.addClips ?? [])];
|
|
1073
|
+
if (wire.clipBase !== undefined && wire.clipBase > (state.clips ?? []).length) {
|
|
1074
|
+
throw new Error(`Unknown active-context clip base ${wire.clipBase}`);
|
|
1075
|
+
}
|
|
1076
|
+
if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
|
|
1077
|
+
throw new Error("Invalid active-context clips: duplicate call id");
|
|
1078
|
+
}
|
|
908
1079
|
state = stateFromFoldRefs(
|
|
909
1080
|
{
|
|
910
1081
|
...wire,
|
|
911
1082
|
briefs: Object.keys(briefs).length ? briefs : undefined,
|
|
912
1083
|
pendingMarks: marks.length ? marks : undefined,
|
|
1084
|
+
clips: clips.length ? clips : undefined,
|
|
1085
|
+
memory: wire.memory?.length ? clone(wire.memory) : undefined,
|
|
913
1086
|
},
|
|
914
1087
|
[...byId.values()],
|
|
915
1088
|
records,
|
|
916
1089
|
);
|
|
917
1090
|
}
|
|
918
1091
|
const calculated = semanticStateSha256(state);
|
|
1092
|
+
const legacyRider = (wire as { rider?: unknown }).rider;
|
|
919
1093
|
if (calculated !== wire.stateSha256 &&
|
|
920
1094
|
legacyCadenceOmittedStateSha256(state) !== wire.stateSha256 &&
|
|
921
|
-
legacyReplayOrderStateSha256(state) !== wire.stateSha256
|
|
1095
|
+
legacyReplayOrderStateSha256(state) !== wire.stateSha256 &&
|
|
1096
|
+
(legacyRider === undefined ||
|
|
1097
|
+
legacyRiderStateSha256(state, legacyRider) !== wire.stateSha256)) {
|
|
922
1098
|
throw new Error("Active-context v2 state digest drift");
|
|
923
1099
|
}
|
|
924
1100
|
wireVersion = 2;
|
|
925
1101
|
stateSha256 = calculated;
|
|
1102
|
+
writtenSha256 = wire.stateSha256 === calculated ? null : wire.stateSha256;
|
|
926
1103
|
rememberProjection();
|
|
927
1104
|
}
|
|
928
1105
|
return { state, wireVersion, records, stateSha256, projectionFingerprints };
|
|
@@ -965,8 +1142,9 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
|
|
|
965
1142
|
leases: clone(state.leases),
|
|
966
1143
|
...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
|
|
967
1144
|
...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
|
|
1145
|
+
...(state.clips?.length ? { clips: clone(state.clips) } : {}),
|
|
1146
|
+
...(state.memory?.length ? { memory: clone(state.memory) } : {}),
|
|
968
1147
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
969
|
-
...(state.rider ? { rider: clone(state.rider) } : {}),
|
|
970
1148
|
stateSha256: semanticStateSha256(state),
|
|
971
1149
|
}, state.sessionId) as ActiveContextCheckpointV2;
|
|
972
1150
|
}
|
|
@@ -1013,6 +1191,23 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1013
1191
|
return !held || sha256Value(held) !== sha256Value(mark);
|
|
1014
1192
|
});
|
|
1015
1193
|
const marksTravel = previousMarks.length > 0 || nextMarks.length > 0;
|
|
1194
|
+
// THE CLIPS ARE THE SAME RULE A THIRD TIME (2026-08-24). They shipped whole on every
|
|
1195
|
+
// delta, which was 50.1 percent of all state bytes on sol-20260823-live rep 11, 472,750
|
|
1196
|
+
// of 942,902, for an array that never held more than 34 entries: the same defect the two
|
|
1197
|
+
// comments above were written about, arriving with a feature that did not inherit their
|
|
1198
|
+
// encoding. Clips are APPENDED and never edited, so the change is a count of what the
|
|
1199
|
+
// base already holds plus the ones that are new, and a rollback that shortens the array
|
|
1200
|
+
// states a lower count. The marks' order list would be worse than the bug here: a callId
|
|
1201
|
+
// is about a hundred characters, so re-listing them all on every delta costs more than
|
|
1202
|
+
// the array it replaces.
|
|
1203
|
+
const previousClips = previous.clips ?? [];
|
|
1204
|
+
const nextClips = next.clips ?? [];
|
|
1205
|
+
let clipBase = 0;
|
|
1206
|
+
while (clipBase < previousClips.length && clipBase < nextClips.length &&
|
|
1207
|
+
previousClips[clipBase].callId === nextClips[clipBase].callId &&
|
|
1208
|
+
previousClips[clipBase].entryId === nextClips[clipBase].entryId) clipBase += 1;
|
|
1209
|
+
const addClips = nextClips.slice(clipBase);
|
|
1210
|
+
const clipsTravel = previousClips.length > 0 || nextClips.length > 0;
|
|
1016
1211
|
// The rest travel whole and stay that way. Measured on the same run, the two that were
|
|
1017
1212
|
// worth a diff were 20.0 MB of the 21.6 MB ledger; what remained were bounded objects
|
|
1018
1213
|
// with their own caps rather than collections that grow with the epoch (the largest,
|
|
@@ -1038,14 +1233,44 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1038
1233
|
...(addPendingMarks.length ? { addPendingMarks: clone(addPendingMarks) } : {}),
|
|
1039
1234
|
...(Object.keys(addBriefs).length ? { addBriefs } : {}),
|
|
1040
1235
|
...(removeBriefIds.length ? { removeBriefIds } : {}),
|
|
1236
|
+
...(clipsTravel ? { clipBase: clipBase, ...(addClips.length ? { addClips: clone(addClips) } : {}) } : {}),
|
|
1237
|
+
// The memory travels whole when it exists: bounded at MEMORY_KEYS_MAX entries and
|
|
1238
|
+
// edited in place, so the change encodings above have nothing to save here.
|
|
1239
|
+
...(next.memory?.length ? { memory: clone(next.memory) } : {}),
|
|
1041
1240
|
...(next.advisory ? { advisory: clone(next.advisory) } : {}),
|
|
1042
|
-
...(next.rider ? { rider: clone(next.rider) } : {}),
|
|
1043
1241
|
stateSha256: semanticStateSha256(next),
|
|
1044
1242
|
}, next.sessionId) as ActiveContextDeltaV2;
|
|
1045
1243
|
}
|
|
1046
1244
|
|
|
1245
|
+
// THE FOREST IS HASHED ONCE PER STATE OBJECT (2026-08-28). Nine call-site pairs reach this,
|
|
1246
|
+
// and the one that scales is the per-mark loop in `commitPendingMarks`: `prepareFold` hashes
|
|
1247
|
+
// `state`, and the `commitPreparedFold` on the very next statement re-hashes the SAME object
|
|
1248
|
+
// through `preparedFoldError`. So every applied mark paid one avoidable whole-forest hash,
|
|
1249
|
+
// and `state` grows by a fold each iteration, making a commit epoch O(marks x folds).
|
|
1250
|
+
// Instrumented across the suite: 6,363 calls, 63.8 percent of them repeats on an object
|
|
1251
|
+
// already hashed, peaking at 198 in a single context event.
|
|
1252
|
+
//
|
|
1253
|
+
// It is gate 113's rule, gate 120's and gate 121's, at a fourth address: kept against the
|
|
1254
|
+
// state OBJECT rather than its contents, because state is REPLACED on every change rather
|
|
1255
|
+
// than mutated, so a replaced one misses and hashes again instead of serving something
|
|
1256
|
+
// stale. Every transition in the runtime is a spread; nothing writes `state.folds`,
|
|
1257
|
+
// `state.expanded` or a fold in place.
|
|
1258
|
+
//
|
|
1259
|
+
// The accepted cost, which is the same one gate 121 accepted: a state mutated in place keeps
|
|
1260
|
+
// its first digest, and this digest is persisted into `PreparedFold.topologySha256`.
|
|
1261
|
+
//
|
|
1262
|
+
// NOT memoized beside it: `protectionSha256`, measured at 0.037 to 0.061ms on real states
|
|
1263
|
+
// because `state.protected` is empty in every sealed run, so a memo there buys about 3
|
|
1264
|
+
// percent and adds a second retained map for nothing.
|
|
1265
|
+
const TOPOLOGY_SHA256 = new WeakMap<object, string>();
|
|
1266
|
+
|
|
1047
1267
|
export function topologySha256(state: Pick<ActiveContextState, "folds" | "expanded">): string {
|
|
1048
|
-
|
|
1268
|
+
const key = state as unknown as object;
|
|
1269
|
+
const memoized = TOPOLOGY_SHA256.get(key);
|
|
1270
|
+
if (memoized !== undefined) return memoized;
|
|
1271
|
+
const digest = sha256Value({ folds: state.folds, expanded: [...state.expanded].sort() });
|
|
1272
|
+
TOPOLOGY_SHA256.set(key, digest);
|
|
1273
|
+
return digest;
|
|
1049
1274
|
}
|
|
1050
1275
|
|
|
1051
1276
|
export function protectionSha256(state: Pick<ActiveContextState, "protected">): string {
|
|
@@ -1056,12 +1281,13 @@ export const ADVISORY_MILESTONES = Object.freeze(
|
|
|
1056
1281
|
["orientation", "notice", "tools", "chapters", "urgent"] as AdvisoryMilestone[],
|
|
1057
1282
|
);
|
|
1058
1283
|
|
|
1059
|
-
export const MAX_RIDER_TEXT_BYTES = 4_096;
|
|
1060
|
-
|
|
1061
1284
|
/**
|
|
1062
1285
|
* THE RETIRED STATE FIELDS, REFUSED BY NAME.
|
|
1063
1286
|
*
|
|
1064
|
-
* `lastCall` and `notices` were the occupancy thermostat's two announcements
|
|
1287
|
+
* `lastCall` and `notices` were the occupancy thermostat's two announcements, and `rider`
|
|
1288
|
+
* was the post-commit invitation to create marks, deleted 2026-08-23 with the curation
|
|
1289
|
+
* redesign: the runtime cuts and the agent edits, so an invitation to create is a carrier
|
|
1290
|
+
* with nothing left to invite. Nothing
|
|
1065
1291
|
* writes them any more and nothing reads them, so a durable state that still carries one
|
|
1066
1292
|
* was written by a build that had them, and the exact-record readers would reject it with
|
|
1067
1293
|
* "Invalid active-context state keys" -- true, and useless to whoever has to act on it.
|
|
@@ -1071,6 +1297,15 @@ export const MAX_RIDER_TEXT_BYTES = 4_096;
|
|
|
1071
1297
|
* that comes back is not the state that was written. Gate 17 already states the wire's
|
|
1072
1298
|
* policy in the other direction -- an older exact-record reader rejects newer fields, and
|
|
1073
1299
|
* a bump is explicit -- and this is that same policy read from the newer side.
|
|
1300
|
+
*
|
|
1301
|
+
* `rider` IS NOT ON THIS LIST, and the distinction is the whole point of the list. The
|
|
1302
|
+
* refusal's own reason is "a state it cannot reproduce", so a field the build CAN
|
|
1303
|
+
* reproduce is out of its scope: the rider was projection carrier text, it changed no
|
|
1304
|
+
* fold, no ref and no brief, and the only thing it ever touched that survives a load is
|
|
1305
|
+
* the state digest. So it is read, spent on the digest, and dropped, which is what
|
|
1306
|
+
* `legacyRiderStateSha256` below is for. Retiring it by name instead made every state the
|
|
1307
|
+
* sol-20260815-hidden campaign sealed unreadable and cost the hidden-mass result its 102
|
|
1308
|
+
* attributed carriage rows, which is a high price for a field that carried no behaviour.
|
|
1074
1309
|
*/
|
|
1075
1310
|
const RETIRED_STATE_FIELDS: readonly string[] = Object.freeze(["lastCall", "notices", "surfacing"]);
|
|
1076
1311
|
|
|
@@ -1086,14 +1321,6 @@ export function refuseRetiredStateFields(value: unknown): void {
|
|
|
1086
1321
|
);
|
|
1087
1322
|
}
|
|
1088
1323
|
|
|
1089
|
-
export function validRiderState(value: unknown): value is NonNullable<ActiveContextState["rider"]> {
|
|
1090
|
-
if (!exactRecord(value, ["epoch", "text"])) return false;
|
|
1091
|
-
const epoch = ownValue(value, "epoch");
|
|
1092
|
-
const text = ownValue(value, "text");
|
|
1093
|
-
return Number.isSafeInteger(epoch) && Number(epoch) >= 0 &&
|
|
1094
|
-
typeof text === "string" && text.length > 0 && text.length <= MAX_RIDER_TEXT_BYTES;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
1324
|
export function validAdvisoryState(value: unknown): value is NonNullable<ActiveContextState["advisory"]> {
|
|
1098
1325
|
const hasArmed = Boolean(value && typeof value === "object" &&
|
|
1099
1326
|
Object.prototype.hasOwnProperty.call(value, "armed"));
|