pi-fold 3.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 +78 -211
- package/extensions/active-context.ts +483 -244
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +4 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +46 -9
- package/extensions/lib/folding.ts +181 -17
- package/extensions/lib/instrumentation.ts +0 -6
- package/extensions/lib/measurement.ts +37 -4
- package/extensions/lib/persistence.ts +212 -9
- package/extensions/lib/policy.ts +103 -29
- package/extensions/lib/scheduling.ts +105 -18
- package/extensions/lib/selection.ts +112 -39
- package/extensions/lib/tool-surface.ts +37 -6
- package/extensions/lib/transcript.ts +65 -14
- package/extensions/settings.ts +11 -36
- package/package.json +1 -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,
|
|
@@ -167,7 +172,8 @@ export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Reco
|
|
|
167
172
|
|
|
168
173
|
export function validProvenance(value: unknown): value is BriefProvenance {
|
|
169
174
|
if (exactRecord(value, ["kind"]) &&
|
|
170
|
-
(ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic"
|
|
175
|
+
(ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic" ||
|
|
176
|
+
ownValue(value, "kind") === "augmented")) return true;
|
|
171
177
|
const hasDigest = Boolean(value && typeof value === "object" &&
|
|
172
178
|
Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
|
|
173
179
|
if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
|
|
@@ -323,6 +329,8 @@ export function parseActiveContextState(
|
|
|
323
329
|
const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
|
|
324
330
|
const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
|
|
325
331
|
const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
|
|
332
|
+
const hasClips = recordLike && Object.prototype.hasOwnProperty.call(value, "clips");
|
|
333
|
+
const hasMemory = recordLike && Object.prototype.hasOwnProperty.call(value, "memory");
|
|
326
334
|
// Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
|
|
327
335
|
// to reproduce, so here the tolerance is the whole of it.
|
|
328
336
|
const hasLegacyRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
|
|
@@ -335,6 +343,8 @@ export function parseActiveContextState(
|
|
|
335
343
|
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
336
344
|
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
337
345
|
...(hasBriefs ? ["briefs"] : []),
|
|
346
|
+
...(hasClips ? ["clips"] : []),
|
|
347
|
+
...(hasMemory ? ["memory"] : []),
|
|
338
348
|
...(hasLegacyRider ? ["rider"] : []),
|
|
339
349
|
];
|
|
340
350
|
if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
|
|
@@ -366,6 +376,8 @@ export function parseActiveContextState(
|
|
|
366
376
|
throw new Error("Invalid active-context pending marks");
|
|
367
377
|
}
|
|
368
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")) : [];
|
|
369
381
|
const source = clone(value) as unknown as ActiveContextState;
|
|
370
382
|
return {
|
|
371
383
|
version: 1,
|
|
@@ -380,6 +392,8 @@ export function parseActiveContextState(
|
|
|
380
392
|
leases,
|
|
381
393
|
...(marks.length ? { pendingMarks: marks } : {}),
|
|
382
394
|
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
395
|
+
...(clips.length ? { clips } : {}),
|
|
396
|
+
...(memory.length ? { memory } : {}),
|
|
383
397
|
...(hasAdvisory
|
|
384
398
|
? { advisory: clone(source.advisory!) }
|
|
385
399
|
: defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
|
|
@@ -387,6 +401,47 @@ export function parseActiveContextState(
|
|
|
387
401
|
};
|
|
388
402
|
}
|
|
389
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
|
+
|
|
390
445
|
export function clearPrepared(state: ActiveContextState): ActiveContextState {
|
|
391
446
|
const next = { ...state };
|
|
392
447
|
delete next.prepared;
|
|
@@ -412,8 +467,24 @@ export function validatePreparedShape(value: unknown): asserts value is Prepared
|
|
|
412
467
|
}
|
|
413
468
|
}
|
|
414
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.
|
|
415
486
|
export function canonicalFoldRecord(fold: ActiveFold): ActiveFold {
|
|
416
|
-
return { ...
|
|
487
|
+
return { ...fold, parentId: null };
|
|
417
488
|
}
|
|
418
489
|
|
|
419
490
|
export function sameFoldRecordIdentity(left: ActiveFold, right: ActiveFold): boolean {
|
|
@@ -475,11 +546,18 @@ export function legacyReplayOrderStateSha256(state: ActiveContextState): string
|
|
|
475
546
|
|
|
476
547
|
export function sameStateProjection(left: ActiveContextState, right: ActiveContextState): boolean {
|
|
477
548
|
const normalized = (value: ActiveContextState): Partial<ActiveContextState> => {
|
|
478
|
-
|
|
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>;
|
|
479
555
|
if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
|
|
480
556
|
if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
|
|
481
557
|
if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
|
|
482
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;
|
|
483
561
|
return normalizedState;
|
|
484
562
|
};
|
|
485
563
|
return stableStringify(normalized(left)) === stableStringify(normalized(right));
|
|
@@ -586,6 +664,14 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
586
664
|
Object.prototype.hasOwnProperty.call(value, "addBriefs"));
|
|
587
665
|
const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
|
|
588
666
|
Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
|
|
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"));
|
|
589
675
|
const hasLegacyRider = Boolean(value && typeof value === "object" &&
|
|
590
676
|
Object.prototype.hasOwnProperty.call(value, "rider"));
|
|
591
677
|
refuseRetiredStateFields(value);
|
|
@@ -600,6 +686,10 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
600
686
|
...(hasBriefs ? ["briefs"] : []),
|
|
601
687
|
...(hasAddBriefs ? ["addBriefs"] : []),
|
|
602
688
|
...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
|
|
689
|
+
...(hasWireClips ? ["clips"] : []),
|
|
690
|
+
...(hasClipBase ? ["clipBase"] : []),
|
|
691
|
+
...(hasAddClips ? ["addClips"] : []),
|
|
692
|
+
...(hasWireMemory ? ["memory"] : []),
|
|
603
693
|
// Carried no further than the digest check in `materializeStatePersistence`, which is
|
|
604
694
|
// the only thing on a load that the rider's absence can change.
|
|
605
695
|
...(hasLegacyRider ? ["rider"] : []),
|
|
@@ -619,6 +709,18 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
619
709
|
ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
|
|
620
710
|
ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
|
|
621
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");
|
|
622
724
|
if (checkpoint) {
|
|
623
725
|
const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
|
|
624
726
|
if (!refs || refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Invalid active-context checkpoint refs");
|
|
@@ -693,7 +795,25 @@ export function normalizeFoldsForPersistedRecords(
|
|
|
693
795
|
return deriveFoldParents(normalized);
|
|
694
796
|
}
|
|
695
797
|
|
|
696
|
-
|
|
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[] {
|
|
697
817
|
const folds = records.map(canonicalFoldRecord);
|
|
698
818
|
const byId = new Map(folds.map((fold) => [fold.id, fold]));
|
|
699
819
|
const assigned = new Set<string>();
|
|
@@ -705,14 +825,19 @@ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
|
|
|
705
825
|
assigned.add(childId);
|
|
706
826
|
}
|
|
707
827
|
}
|
|
708
|
-
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));
|
|
709
834
|
}
|
|
710
835
|
|
|
711
836
|
export function stateFromFoldRefs(
|
|
712
837
|
wire: Pick<
|
|
713
838
|
ActiveContextCheckpointV2,
|
|
714
839
|
"sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
|
|
715
|
-
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs"
|
|
840
|
+
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips" | "memory"
|
|
716
841
|
>,
|
|
717
842
|
refs: FoldRecordRef[],
|
|
718
843
|
records: Map<string, FoldRecordEntry>,
|
|
@@ -727,13 +852,17 @@ export function stateFromFoldRefs(
|
|
|
727
852
|
version: 1,
|
|
728
853
|
sessionId: wire.sessionId,
|
|
729
854
|
revision: wire.revision,
|
|
730
|
-
|
|
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),
|
|
731
858
|
expanded: clone(wire.expanded),
|
|
732
859
|
protected: clone(wire.protected),
|
|
733
860
|
tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
|
|
734
861
|
leases: clone(wire.leases ?? {}),
|
|
735
862
|
...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
|
|
736
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) } : {}),
|
|
737
866
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
738
867
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
739
868
|
};
|
|
@@ -869,7 +998,16 @@ export function materializeStatePersistence(
|
|
|
869
998
|
wire.revision <= wire.baseRevision) {
|
|
870
999
|
throw new Error("Broken active-context delta chain");
|
|
871
1000
|
}
|
|
872
|
-
|
|
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
|
+
});
|
|
873
1011
|
const byId = new Map(refs.map((ref) => [ref.id, ref]));
|
|
874
1012
|
for (const id of wire.removeFoldIds) {
|
|
875
1013
|
if (!byId.delete(id)) throw new Error(`Unknown active-context delta removal ${id}`);
|
|
@@ -925,11 +1063,26 @@ export function materializeStatePersistence(
|
|
|
925
1063
|
return mark;
|
|
926
1064
|
});
|
|
927
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
|
+
}
|
|
928
1079
|
state = stateFromFoldRefs(
|
|
929
1080
|
{
|
|
930
1081
|
...wire,
|
|
931
1082
|
briefs: Object.keys(briefs).length ? briefs : undefined,
|
|
932
1083
|
pendingMarks: marks.length ? marks : undefined,
|
|
1084
|
+
clips: clips.length ? clips : undefined,
|
|
1085
|
+
memory: wire.memory?.length ? clone(wire.memory) : undefined,
|
|
933
1086
|
},
|
|
934
1087
|
[...byId.values()],
|
|
935
1088
|
records,
|
|
@@ -989,6 +1142,8 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
|
|
|
989
1142
|
leases: clone(state.leases),
|
|
990
1143
|
...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
|
|
991
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) } : {}),
|
|
992
1147
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
993
1148
|
stateSha256: semanticStateSha256(state),
|
|
994
1149
|
}, state.sessionId) as ActiveContextCheckpointV2;
|
|
@@ -1036,6 +1191,23 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1036
1191
|
return !held || sha256Value(held) !== sha256Value(mark);
|
|
1037
1192
|
});
|
|
1038
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;
|
|
1039
1211
|
// The rest travel whole and stay that way. Measured on the same run, the two that were
|
|
1040
1212
|
// worth a diff were 20.0 MB of the 21.6 MB ledger; what remained were bounded objects
|
|
1041
1213
|
// with their own caps rather than collections that grow with the epoch (the largest,
|
|
@@ -1061,13 +1233,44 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1061
1233
|
...(addPendingMarks.length ? { addPendingMarks: clone(addPendingMarks) } : {}),
|
|
1062
1234
|
...(Object.keys(addBriefs).length ? { addBriefs } : {}),
|
|
1063
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) } : {}),
|
|
1064
1240
|
...(next.advisory ? { advisory: clone(next.advisory) } : {}),
|
|
1065
1241
|
stateSha256: semanticStateSha256(next),
|
|
1066
1242
|
}, next.sessionId) as ActiveContextDeltaV2;
|
|
1067
1243
|
}
|
|
1068
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
|
+
|
|
1069
1267
|
export function topologySha256(state: Pick<ActiveContextState, "folds" | "expanded">): string {
|
|
1070
|
-
|
|
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;
|
|
1071
1274
|
}
|
|
1072
1275
|
|
|
1073
1276
|
export function protectionSha256(state: Pick<ActiveContextState, "protected">): string {
|
package/extensions/lib/policy.ts
CHANGED
|
@@ -91,7 +91,9 @@ export const RETIRED_TOOL_ACTIONS: Readonly<Record<string, string>> = Object.fre
|
|
|
91
91
|
"peek already carries losslessly. Write the brief you want when you mark the span, or " +
|
|
92
92
|
"use reboundary to re-cut it.",
|
|
93
93
|
});
|
|
94
|
-
export type ActiveContextToolAction =
|
|
94
|
+
export type ActiveContextToolAction =
|
|
95
|
+
| typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number]
|
|
96
|
+
| typeof WORKING_MEMORY_TOOL_ACTIONS[number];
|
|
95
97
|
export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
|
|
96
98
|
export const DEFAULT_CONTEXT_WINDOW = 272_000;
|
|
97
99
|
export const EXPAND_LEASE_GENERATIONS = 8;
|
|
@@ -108,6 +110,16 @@ export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
|
|
|
108
110
|
*/
|
|
109
111
|
export const MAX_PINNED_SHARE = 0.25;
|
|
110
112
|
|
|
113
|
+
/**
|
|
114
|
+
* THE TOOL-CALL DIET's default share (Shane 2026-08-28, on the sol-20260826-full2
|
|
115
|
+
* verdict). Every fold repetition from 3 onward ran 0.50 and the campaign's headline
|
|
116
|
+
* results are all from that shape, so the shipped default is the measured value rather
|
|
117
|
+
* than the absence of one. A deployment turns the diet off with an explicit 0, which is
|
|
118
|
+
* why the option's range is [0, 1) instead of the open interval it shipped with: one
|
|
119
|
+
* meaning per value, and no absent-means-off special case.
|
|
120
|
+
*/
|
|
121
|
+
export const DEFAULT_TOOL_FOLD_THRESHOLD = 0.50;
|
|
122
|
+
|
|
111
123
|
export interface ActiveContextThresholds {
|
|
112
124
|
maxTarget: number;
|
|
113
125
|
minTarget: number;
|
|
@@ -141,9 +153,10 @@ export const MINIMUM_FOLD_CHARS_FLOOR = 2_000;
|
|
|
141
153
|
|
|
142
154
|
export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
|
|
143
155
|
maxTarget: 0.80,
|
|
144
|
-
// 0.
|
|
145
|
-
//
|
|
146
|
-
//
|
|
156
|
+
// 0.20 (Shane 2026-08-28, on the sol-20260826-full2 verdict). This number went 0.20 ->
|
|
157
|
+
// 0.40 on 2026-08-23 and came back on the campaign that was still running when it
|
|
158
|
+
// moved, so both readings are recorded here rather than one quietly replacing the
|
|
159
|
+
// other. The top of the band is not in question and never was.
|
|
147
160
|
//
|
|
148
161
|
// Read against the 19 mature sealed pifold runs that share a 251,520-token budget,
|
|
149
162
|
// where the trigger sat at 0.80 of it, 201,216 tokens. Peak occupancy per run came in
|
|
@@ -153,26 +166,28 @@ export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.free
|
|
|
153
166
|
// So 0.80 already delivers a 250k top, and raising it toward 250k directly would put
|
|
154
167
|
// the median peak over the budget and into the fence.
|
|
155
168
|
//
|
|
156
|
-
// The
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
169
|
+
// The 2026-08-23 case for 0.40 was that it seats the floor near where commits actually
|
|
170
|
+
// land: across 206 commit landings the session came to rest at a median 79,034 tokens,
|
|
171
|
+
// 153 of the 206 below 100,000, and landings sit ABOVE the aim rather than at it, since
|
|
172
|
+
// the class law and the available material both outrank it. That was bought knowing the
|
|
173
|
+
// price, about a fifth more epochs and so a fifth more full-prefix rewrites, to hold
|
|
174
|
+
// 100k of raw context across the whole cycle.
|
|
162
175
|
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
176
|
+
// The campaign priced the trade and it went the other way. The commit rewrite tax is
|
|
177
|
+
// minTarget / (maxTarget - minTarget), because every commit re-reads roughly
|
|
178
|
+
// minTarget x budget of prefix uncached and a shallower cut buys less runway for nearly
|
|
179
|
+
// the same bill: 1.0 at 0.80/0.40 against 0.33 at 0.80/0.20. Every sol-20260826-full2
|
|
180
|
+
// fold repetition from 3 onward ran 0.80/0.20, including the fold-plus-canon rep 5 that
|
|
181
|
+
// answered 14 of 16 with nothing wrong at $100.85 against native-plus-canon's $201.70.
|
|
182
|
+
// The shipped default is now the configuration that was measured.
|
|
168
183
|
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
minTarget: 0.
|
|
184
|
+
// THE KNOWN LIMIT, unchanged and still true: MAX_PINNED_SHARE is 0.25, so a fully
|
|
185
|
+
// pinned session legally holds more than a 0.20 aim asks for, and there the commit
|
|
186
|
+
// stops short of its target by construction rather than by fault. The epoch receipt
|
|
187
|
+
// reports targetBudgetShare beside actualFreedBudgetShare, so the shortfall stays a
|
|
188
|
+
// readable fact per epoch rather than a silent miss. The validation laws below bind
|
|
189
|
+
// exactly as before.
|
|
190
|
+
minTarget: 0.20,
|
|
176
191
|
consolidateAfter: 10,
|
|
177
192
|
minFoldChars: 8_000,
|
|
178
193
|
});
|
|
@@ -241,11 +256,6 @@ export function assertThresholdsServable(_thresholds: ActiveContextThresholds, b
|
|
|
241
256
|
}
|
|
242
257
|
}
|
|
243
258
|
|
|
244
|
-
export function zoneBytes(share: number, budgetTokens: number): number {
|
|
245
|
-
if (!Number.isFinite(share) || share <= 0 || !Number.isFinite(budgetTokens) || budgetTokens <= 0) return 0;
|
|
246
|
-
return Math.floor(share * budgetTokens * ESTIMATED_BYTES_PER_TOKEN);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
259
|
export function servingBudgetTokens(window: number): number {
|
|
250
260
|
const resolved = Number.isFinite(window) && window > 0 ? window : DEFAULT_CONTEXT_WINDOW;
|
|
251
261
|
return resolved - Math.min(ACTIVE_CONTEXT_POLICY.responseReserve, Math.floor(resolved * 0.1));
|
|
@@ -271,7 +281,7 @@ export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
|
|
|
271
281
|
* agent answers. 1,200 seats the instruction and roughly eight folds; past that the list
|
|
272
282
|
* gives way and states how many it could not name.
|
|
273
283
|
*/
|
|
274
|
-
export const FOLD_NOTICE_BYTES =
|
|
284
|
+
export const FOLD_NOTICE_BYTES = 2_400;
|
|
275
285
|
export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
|
|
276
286
|
export const MAX_UNMARKED_CANDIDATES = 3;
|
|
277
287
|
|
|
@@ -298,7 +308,35 @@ export function peekIsEphemeral(params: Record<string, unknown> | undefined | nu
|
|
|
298
308
|
return params?.ephemeral !== false;
|
|
299
309
|
}
|
|
300
310
|
|
|
301
|
-
export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
|
|
311
|
+
export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek", "recall"]);
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* THE WORKING MEMORY (Shane, 2026-08-26). A session-scoped ordered dictionary the agent
|
|
315
|
+
* maintains beside the fold index: keys list in a table of contents the projection
|
|
316
|
+
* carries, bodies live in state and are read on demand with `recall`. It exists because
|
|
317
|
+
* the campaign measured a structural gap, not a tuning one: a maintained digest (running
|
|
318
|
+
* tallies, current totals) is a different operation from a lossless index, and blending
|
|
319
|
+
* digest prose into the index's briefs was measured making BOTH worse (sol-20260826-full2
|
|
320
|
+
* rep 4: nine confident wrong end-block answers against the deterministic arm's zero).
|
|
321
|
+
* The digest gets its own channel and the index stays mechanical.
|
|
322
|
+
*
|
|
323
|
+
* Three constants, not knobs. The key cap keeps the table of contents a table of
|
|
324
|
+
* contents; the body cap is deliberately larger than a brief because a digest is
|
|
325
|
+
* maintained prose, not an index row; the entry cap bounds the whole store at a size
|
|
326
|
+
* whose table of contents stays one glance.
|
|
327
|
+
*/
|
|
328
|
+
export const MEMORY_KEY_CHARS = 64;
|
|
329
|
+
export const MEMORY_BODY_CHARS = 4_000;
|
|
330
|
+
export const MEMORY_KEYS_MAX = 24;
|
|
331
|
+
|
|
332
|
+
export const WORKING_MEMORY_TOOL_ACTIONS = Object.freeze(["remember", "recall"] as const);
|
|
333
|
+
|
|
334
|
+
export interface WorkingMemoryEntry {
|
|
335
|
+
key: string;
|
|
336
|
+
body: string;
|
|
337
|
+
/** Write counter at last update; the table of contents lists freshest first. */
|
|
338
|
+
ordinal: number;
|
|
339
|
+
}
|
|
302
340
|
|
|
303
341
|
/** "user" is a mark laid down by the human through /fold-editor; it stages and
|
|
304
342
|
* commits through the same validated path as an agent mark and counts toward the
|
|
@@ -334,16 +372,32 @@ export const ACTIVE_CONTEXT_POLICY = Object.freeze({
|
|
|
334
372
|
// clamps its `bytes` to it. Was two numbers until 2026-08-22, when the peek ceiling
|
|
335
373
|
// was `maxChapterChars` 128,000 and that constant also claimed to cap a chapter.
|
|
336
374
|
maxSourceChars: 200_000,
|
|
375
|
+
// THE TOOL-CALL DIET (2026-08-24): at COMMIT TIME ONLY, a stale tool result inside
|
|
376
|
+
// the configured toolFoldThreshold share of the projected window is clipped IN VIEW
|
|
377
|
+
// to an identified head, the full bytes staying peek-recoverable behind its entry id.
|
|
378
|
+
// The head keeps the leading paragraph up to its cap (gate 134's rule); results at or
|
|
379
|
+
// under the floor stay whole, because clipping a small result buys almost nothing and
|
|
380
|
+
// adds a peek indirection. Constants, not options, for v1: the share point is the one
|
|
381
|
+
// public knob.
|
|
382
|
+
toolClipHeadChars: 500,
|
|
383
|
+
toolClipFloorChars: 2_000,
|
|
337
384
|
maxBriefChars: 2_000,
|
|
385
|
+
// The agent's share of maxBriefChars when a supplied brief augments the deterministic
|
|
386
|
+
// head (sol-20260823-live rep 7: supplied briefs REPLACED the fact-carrying head and
|
|
387
|
+
// 74 of 95 peeks recovered values the run's own briefs dropped). The head keeps the
|
|
388
|
+
// remainder, so identification can never be displaced by a long agent clause.
|
|
389
|
+
agentBriefReserve: 600,
|
|
338
390
|
orientationMessages: 2,
|
|
339
391
|
maxOrientationChars: 12_000,
|
|
340
392
|
});
|
|
341
393
|
|
|
342
394
|
export type FoldKind = "tool-result" | "chapter" | "consolidation";
|
|
395
|
+
export type ToolClip = { callId: string; entryId: string };
|
|
343
396
|
export type FoldPart = { kind: "raw"; ref: EvidenceRef } | { kind: "fold"; foldId: string };
|
|
344
397
|
export type BriefProvenance =
|
|
345
398
|
| { kind: "supplied" }
|
|
346
399
|
| { kind: "deterministic" }
|
|
400
|
+
| { kind: "augmented" }
|
|
347
401
|
| {
|
|
348
402
|
kind: "model" | "luna";
|
|
349
403
|
provider: string;
|
|
@@ -395,6 +449,7 @@ export interface ActiveContextState {
|
|
|
395
449
|
pendingMarks?: PendingMark[];
|
|
396
450
|
pinnedPeeks?: never;
|
|
397
451
|
briefs?: Record<string, BriefOverride>;
|
|
452
|
+
clips?: ToolClip[];
|
|
398
453
|
prepared?: PreparedFold;
|
|
399
454
|
advisory?: {
|
|
400
455
|
highWater: number;
|
|
@@ -402,6 +457,8 @@ export interface ActiveContextState {
|
|
|
402
457
|
armed?: { milestone: AdvisoryMilestone; threshold: number; scheduleKey: string };
|
|
403
458
|
};
|
|
404
459
|
rider?: { epoch: number; text: string };
|
|
460
|
+
/** The working memory's entries, in write order; absent when empty. */
|
|
461
|
+
memory?: WorkingMemoryEntry[];
|
|
405
462
|
}
|
|
406
463
|
|
|
407
464
|
export interface FoldRecordRef {
|
|
@@ -430,6 +487,8 @@ export interface ActiveContextCheckpointV2 {
|
|
|
430
487
|
leases?: Record<string, number>;
|
|
431
488
|
pendingMarks?: PendingMark[];
|
|
432
489
|
briefs?: Record<string, BriefOverride>;
|
|
490
|
+
clips?: ToolClip[];
|
|
491
|
+
memory?: WorkingMemoryEntry[];
|
|
433
492
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
434
493
|
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
435
494
|
lastCall?: NonNullable<ActiveContextState["lastCall"]>;
|
|
@@ -461,6 +520,21 @@ export interface ActiveContextDeltaV2 {
|
|
|
461
520
|
briefs?: Record<string, BriefOverride>;
|
|
462
521
|
addBriefs?: Record<string, BriefOverride>;
|
|
463
522
|
removeBriefIds?: string[];
|
|
523
|
+
// `clips` on a delta is the WHOLE array: what every delta written before 2026-08-24
|
|
524
|
+
// carries, and what sealed sessions replay. New deltas carry the change instead. Clips
|
|
525
|
+
// are appended and never edited, so the change is a count of what the base already holds
|
|
526
|
+
// plus the ones that are new; a rollback that shortens the array states a lower count.
|
|
527
|
+
// The order list the marks use would be WORSE than the bug here, because a callId is
|
|
528
|
+
// about a hundred characters and re-listing thirty of them on every delta costs more
|
|
529
|
+
// than the array it replaces.
|
|
530
|
+
clips?: ToolClip[];
|
|
531
|
+
clipBase?: number;
|
|
532
|
+
addClips?: ToolClip[];
|
|
533
|
+
// `memory` on a delta is the WHOLE array. It is bounded by MEMORY_KEYS_MAX entries and
|
|
534
|
+
// edited in place by key, so a change form would have to restate the key order the way
|
|
535
|
+
// the marks do while saving at most a handful of bounded bodies: the whole array is the
|
|
536
|
+
// cheaper wire. Absent means empty, by the same discriminator rule as the marks.
|
|
537
|
+
memory?: WorkingMemoryEntry[];
|
|
464
538
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
465
539
|
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
466
540
|
lastCall?: NonNullable<ActiveContextState["lastCall"]>;
|