pi-fold 3.0.0 → 4.0.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 +76 -212
- package/extensions/active-context.ts +457 -362
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +8 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +141 -9
- package/extensions/lib/editor-ui.ts +133 -25
- package/extensions/lib/folding.ts +181 -17
- package/extensions/lib/instrumentation.ts +0 -6
- package/extensions/lib/measurement.ts +84 -4
- package/extensions/lib/persistence.ts +181 -9
- package/extensions/lib/policy.ts +80 -28
- package/extensions/lib/scheduling.ts +105 -18
- package/extensions/lib/selection.ts +112 -39
- package/extensions/lib/tool-surface.ts +6 -4
- package/extensions/lib/transcript.ts +65 -14
- package/extensions/settings.ts +123 -54
- 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,7 @@ import type {
|
|
|
32
35
|
AdvisoryMilestone,
|
|
33
36
|
BriefOverride,
|
|
34
37
|
BriefProvenance,
|
|
38
|
+
ToolClip,
|
|
35
39
|
FoldKind,
|
|
36
40
|
FoldPart,
|
|
37
41
|
FoldRecordEntry,
|
|
@@ -167,7 +171,8 @@ export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Reco
|
|
|
167
171
|
|
|
168
172
|
export function validProvenance(value: unknown): value is BriefProvenance {
|
|
169
173
|
if (exactRecord(value, ["kind"]) &&
|
|
170
|
-
(ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic"
|
|
174
|
+
(ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic" ||
|
|
175
|
+
ownValue(value, "kind") === "augmented")) return true;
|
|
171
176
|
const hasDigest = Boolean(value && typeof value === "object" &&
|
|
172
177
|
Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
|
|
173
178
|
if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
|
|
@@ -323,6 +328,12 @@ export function parseActiveContextState(
|
|
|
323
328
|
const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
|
|
324
329
|
const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
|
|
325
330
|
const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
|
|
331
|
+
const hasClips = recordLike && Object.prototype.hasOwnProperty.call(value, "clips");
|
|
332
|
+
// Read and dropped, never stored (2026-08-28): the working memory is deleted, and a
|
|
333
|
+
// sealed state that carries it must still load. Tolerating the key while discarding the
|
|
334
|
+
// value is the legacy-rider treatment, not the retired-field refusal: sol-20260826-full2
|
|
335
|
+
// reps 4 and 6 ran with it, and a campaign this package still cites has to stay readable.
|
|
336
|
+
const hasMemory = recordLike && Object.prototype.hasOwnProperty.call(value, "memory");
|
|
326
337
|
// Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
|
|
327
338
|
// to reproduce, so here the tolerance is the whole of it.
|
|
328
339
|
const hasLegacyRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
|
|
@@ -335,6 +346,8 @@ export function parseActiveContextState(
|
|
|
335
346
|
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
336
347
|
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
337
348
|
...(hasBriefs ? ["briefs"] : []),
|
|
349
|
+
...(hasClips ? ["clips"] : []),
|
|
350
|
+
...(hasMemory ? ["memory"] : []),
|
|
338
351
|
...(hasLegacyRider ? ["rider"] : []),
|
|
339
352
|
];
|
|
340
353
|
if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
|
|
@@ -366,6 +379,7 @@ export function parseActiveContextState(
|
|
|
366
379
|
throw new Error("Invalid active-context pending marks");
|
|
367
380
|
}
|
|
368
381
|
const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
|
|
382
|
+
const clips = hasClips ? parseToolClips(ownValue(value, "clips")) : [];
|
|
369
383
|
const source = clone(value) as unknown as ActiveContextState;
|
|
370
384
|
return {
|
|
371
385
|
version: 1,
|
|
@@ -380,6 +394,7 @@ export function parseActiveContextState(
|
|
|
380
394
|
leases,
|
|
381
395
|
...(marks.length ? { pendingMarks: marks } : {}),
|
|
382
396
|
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
397
|
+
...(clips.length ? { clips } : {}),
|
|
383
398
|
...(hasAdvisory
|
|
384
399
|
? { advisory: clone(source.advisory!) }
|
|
385
400
|
: defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
|
|
@@ -387,6 +402,23 @@ export function parseActiveContextState(
|
|
|
387
402
|
};
|
|
388
403
|
}
|
|
389
404
|
|
|
405
|
+
function parseToolClips(value: unknown): ToolClip[] {
|
|
406
|
+
const entries = denseOwnArrayValues(value);
|
|
407
|
+
if (!entries) throw new Error("Invalid active-context clips");
|
|
408
|
+
const clips = entries.map((entry) => {
|
|
409
|
+
if (!exactRecord(entry, ["callId", "entryId"]) ||
|
|
410
|
+
typeof ownValue(entry, "callId") !== "string" || !ownValue(entry, "callId") ||
|
|
411
|
+
typeof ownValue(entry, "entryId") !== "string" || !ownValue(entry, "entryId")) {
|
|
412
|
+
throw new Error("Invalid active-context clip entry");
|
|
413
|
+
}
|
|
414
|
+
return { callId: String(ownValue(entry, "callId")), entryId: String(ownValue(entry, "entryId")) };
|
|
415
|
+
});
|
|
416
|
+
if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
|
|
417
|
+
throw new Error("Invalid active-context clips: duplicate call id");
|
|
418
|
+
}
|
|
419
|
+
return clips;
|
|
420
|
+
}
|
|
421
|
+
|
|
390
422
|
export function clearPrepared(state: ActiveContextState): ActiveContextState {
|
|
391
423
|
const next = { ...state };
|
|
392
424
|
delete next.prepared;
|
|
@@ -412,8 +444,24 @@ export function validatePreparedShape(value: unknown): asserts value is Prepared
|
|
|
412
444
|
}
|
|
413
445
|
}
|
|
414
446
|
|
|
447
|
+
// THE SPREAD IS THE COPY (2026-08-28). Roughly six calls per fold per persist, and only
|
|
448
|
+
// `deriveFoldParents` wants a record it can write to, where it writes the TOP-LEVEL
|
|
449
|
+
// `parentId` that the spread already gives it a fresh slot for. Every other caller hashes
|
|
450
|
+
// and discards: `foldRecordRef`, `sameFoldRecordIdentity`, and `makeFoldRecordEntry`, whose
|
|
451
|
+
// `parseFoldRecordEntry` deep-clones twice on its own.
|
|
452
|
+
//
|
|
453
|
+
// The digest cannot move. `safeJson` emits in `Reflect.ownKeys` order, a spread preserves
|
|
454
|
+
// insertion order, and overwriting an existing key keeps it in its original position, so
|
|
455
|
+
// `{ ...fold, parentId: null }` serializes to the same bytes as the cloned form.
|
|
456
|
+
//
|
|
457
|
+
// THE COST, STATED BECAUSE IT IS NOW LOAD-BEARING: the returned record shares every nested
|
|
458
|
+
// object with its input, so "no caller mutates a canonical record's nested fields" is a rule
|
|
459
|
+
// this function relies on rather than enforces. It holds today (`validateFoldForest` clones
|
|
460
|
+
// at :281, `parseFoldRecordEntry` twice at :567-568, the hashing callers never write), and a
|
|
461
|
+
// future caller that writes through `record.parts` would corrupt the fold it was built from.
|
|
462
|
+
// Write `{ ...record, field: value }` rather than assigning into a nested one.
|
|
415
463
|
export function canonicalFoldRecord(fold: ActiveFold): ActiveFold {
|
|
416
|
-
return { ...
|
|
464
|
+
return { ...fold, parentId: null };
|
|
417
465
|
}
|
|
418
466
|
|
|
419
467
|
export function sameFoldRecordIdentity(left: ActiveFold, right: ActiveFold): boolean {
|
|
@@ -475,11 +523,17 @@ export function legacyReplayOrderStateSha256(state: ActiveContextState): string
|
|
|
475
523
|
|
|
476
524
|
export function sameStateProjection(left: ActiveContextState, right: ActiveContextState): boolean {
|
|
477
525
|
const normalized = (value: ActiveContextState): Partial<ActiveContextState> => {
|
|
478
|
-
|
|
526
|
+
// A SHALLOW COPY IS ENOUGH TO DELETE TOP-LEVEL KEYS (2026-08-28). `delete` on a shallow
|
|
527
|
+
// copy cannot reach the original, the normalized object never escapes this function, and
|
|
528
|
+
// the runtime already stableStringifies live un-cloned state (`semanticStateSha256`), so
|
|
529
|
+
// un-cloned state is already required to be serializer-safe. Measured 97.6ms to 82.6ms
|
|
530
|
+
// on the largest real state in the corpus, at about two calls per provider request.
|
|
531
|
+
const normalizedState = { ...value, revision: 0 } as Partial<ActiveContextState>;
|
|
479
532
|
if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
|
|
480
533
|
if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
|
|
481
534
|
if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
|
|
482
535
|
if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
|
|
536
|
+
if (normalizedState.clips && normalizedState.clips.length === 0) delete normalizedState.clips;
|
|
483
537
|
return normalizedState;
|
|
484
538
|
};
|
|
485
539
|
return stableStringify(normalized(left)) === stableStringify(normalized(right));
|
|
@@ -586,6 +640,14 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
586
640
|
Object.prototype.hasOwnProperty.call(value, "addBriefs"));
|
|
587
641
|
const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
|
|
588
642
|
Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
|
|
643
|
+
const hasWireClips = Boolean(value && typeof value === "object" &&
|
|
644
|
+
Object.prototype.hasOwnProperty.call(value, "clips"));
|
|
645
|
+
const hasClipBase = Boolean(value && typeof value === "object" &&
|
|
646
|
+
Object.prototype.hasOwnProperty.call(value, "clipBase"));
|
|
647
|
+
const hasAddClips = Boolean(value && typeof value === "object" &&
|
|
648
|
+
Object.prototype.hasOwnProperty.call(value, "addClips"));
|
|
649
|
+
const hasWireMemory = Boolean(value && typeof value === "object" &&
|
|
650
|
+
Object.prototype.hasOwnProperty.call(value, "memory"));
|
|
589
651
|
const hasLegacyRider = Boolean(value && typeof value === "object" &&
|
|
590
652
|
Object.prototype.hasOwnProperty.call(value, "rider"));
|
|
591
653
|
refuseRetiredStateFields(value);
|
|
@@ -600,6 +662,10 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
600
662
|
...(hasBriefs ? ["briefs"] : []),
|
|
601
663
|
...(hasAddBriefs ? ["addBriefs"] : []),
|
|
602
664
|
...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
|
|
665
|
+
...(hasWireClips ? ["clips"] : []),
|
|
666
|
+
...(hasClipBase ? ["clipBase"] : []),
|
|
667
|
+
...(hasAddClips ? ["addClips"] : []),
|
|
668
|
+
...(hasWireMemory ? ["memory"] : []),
|
|
603
669
|
// Carried no further than the digest check in `materializeStatePersistence`, which is
|
|
604
670
|
// the only thing on a load that the rider's absence can change.
|
|
605
671
|
...(hasLegacyRider ? ["rider"] : []),
|
|
@@ -619,6 +685,17 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
619
685
|
ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
|
|
620
686
|
ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
|
|
621
687
|
);
|
|
688
|
+
if (hasWireClips) parseToolClips(ownValue(value, "clips"));
|
|
689
|
+
if (hasAddClips) parseToolClips(ownValue(value, "addClips"));
|
|
690
|
+
if (hasClipBase) {
|
|
691
|
+
const base = ownValue(value, "clipBase");
|
|
692
|
+
if (!Number.isSafeInteger(base) || Number(base) < 0) throw new Error("Invalid active-context clip base");
|
|
693
|
+
}
|
|
694
|
+
// A whole array and a change cannot both be the truth on one wire.
|
|
695
|
+
if (hasWireClips && (hasClipBase || hasAddClips)) {
|
|
696
|
+
throw new Error("Invalid active-context clips: whole array beside a change");
|
|
697
|
+
}
|
|
698
|
+
if (hasAddClips && !hasClipBase) throw new Error("Invalid active-context clips: change without a base");
|
|
622
699
|
if (checkpoint) {
|
|
623
700
|
const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
|
|
624
701
|
if (!refs || refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Invalid active-context checkpoint refs");
|
|
@@ -693,7 +770,25 @@ export function normalizeFoldsForPersistedRecords(
|
|
|
693
770
|
return deriveFoldParents(normalized);
|
|
694
771
|
}
|
|
695
772
|
|
|
696
|
-
|
|
773
|
+
/**
|
|
774
|
+
* Rebuild the parent links a fold forest's records do not carry.
|
|
775
|
+
*
|
|
776
|
+
* THIS DOES NOT VALIDATE THE FOREST, and the split is deliberate. `validateFoldForest` deep
|
|
777
|
+
* clones the forest, recomputes `foldIdFor` per fold, and calls `flattenFoldRefs` per fold,
|
|
778
|
+
* which rebuilds `foldMap` over every fold; two of the four callers handed the result
|
|
779
|
+
* straight to `parseActiveContextState`, whose FIRST act is to validate it again. So a
|
|
780
|
+
* persist validated the same forest three times. Roughly 12ms saved at 88 folds, 28 at 178,
|
|
781
|
+
* 55 at 267, plus about 1.6x on the session-start load path.
|
|
782
|
+
*
|
|
783
|
+
* The child-record check above stays here, because it is about the ASSIGNMENT rather than
|
|
784
|
+
* the forest: a child claimed twice, or claimed by a parent that does not hold it, is a
|
|
785
|
+
* contradiction this loop is the only thing positioned to see.
|
|
786
|
+
*
|
|
787
|
+
* Callers that do not parse immediately afterwards want `deriveFoldParents` below. Reaching
|
|
788
|
+
* for this one is a claim that the very next statement validates the same array; nothing
|
|
789
|
+
* else makes it safe, and it reads at review as removing validation, which it is not.
|
|
790
|
+
*/
|
|
791
|
+
export function assignFoldParents(records: ActiveFold[]): ActiveFold[] {
|
|
697
792
|
const folds = records.map(canonicalFoldRecord);
|
|
698
793
|
const byId = new Map(folds.map((fold) => [fold.id, fold]));
|
|
699
794
|
const assigned = new Set<string>();
|
|
@@ -705,14 +800,19 @@ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
|
|
|
705
800
|
assigned.add(childId);
|
|
706
801
|
}
|
|
707
802
|
}
|
|
708
|
-
return
|
|
803
|
+
return folds;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Assign the parents and validate the forest, for callers that do not parse next. */
|
|
807
|
+
export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
|
|
808
|
+
return validateFoldForest(assignFoldParents(records));
|
|
709
809
|
}
|
|
710
810
|
|
|
711
811
|
export function stateFromFoldRefs(
|
|
712
812
|
wire: Pick<
|
|
713
813
|
ActiveContextCheckpointV2,
|
|
714
814
|
"sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
|
|
715
|
-
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs"
|
|
815
|
+
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips"
|
|
716
816
|
>,
|
|
717
817
|
refs: FoldRecordRef[],
|
|
718
818
|
records: Map<string, FoldRecordEntry>,
|
|
@@ -727,13 +827,16 @@ export function stateFromFoldRefs(
|
|
|
727
827
|
version: 1,
|
|
728
828
|
sessionId: wire.sessionId,
|
|
729
829
|
revision: wire.revision,
|
|
730
|
-
|
|
830
|
+
// `parseActiveContextState` at the end of this function validates the forest, so
|
|
831
|
+
// validating it here as well was the same walk twice on one persist.
|
|
832
|
+
folds: assignFoldParents(folds),
|
|
731
833
|
expanded: clone(wire.expanded),
|
|
732
834
|
protected: clone(wire.protected),
|
|
733
835
|
tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
|
|
734
836
|
leases: clone(wire.leases ?? {}),
|
|
735
837
|
...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
|
|
736
838
|
...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
|
|
839
|
+
...(wire.clips?.length ? { clips: clone(wire.clips) } : {}),
|
|
737
840
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
738
841
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
739
842
|
};
|
|
@@ -869,7 +972,16 @@ export function materializeStatePersistence(
|
|
|
869
972
|
wire.revision <= wire.baseRevision) {
|
|
870
973
|
throw new Error("Broken active-context delta chain");
|
|
871
974
|
}
|
|
872
|
-
|
|
975
|
+
// THE RECORD MAP ALREADY HOLDS THIS DIGEST (2026-08-28). `foldRecordRef` canonicalizes
|
|
976
|
+
// and hashes a whole fold, and this ran once per delta entry over every standing fold:
|
|
977
|
+
// 48,715 calls in ONE load of a real sealed session, 2.6 to 3.7s of a 24 to 33s load.
|
|
978
|
+
// `records` is built by the same replay and carries the same digest verbatim. The
|
|
979
|
+
// fallback is unreachable, because the pre-scan raises on a delta before a checkpoint;
|
|
980
|
+
// it is kept so the change is exactly behaviour-preserving rather than nearly so.
|
|
981
|
+
const refs = state.folds.map((fold) => {
|
|
982
|
+
const record = records.get(fold.id);
|
|
983
|
+
return record ? { id: fold.id, sha256: record.recordSha256 } : foldRecordRef(fold);
|
|
984
|
+
});
|
|
873
985
|
const byId = new Map(refs.map((ref) => [ref.id, ref]));
|
|
874
986
|
for (const id of wire.removeFoldIds) {
|
|
875
987
|
if (!byId.delete(id)) throw new Error(`Unknown active-context delta removal ${id}`);
|
|
@@ -925,11 +1037,25 @@ export function materializeStatePersistence(
|
|
|
925
1037
|
return mark;
|
|
926
1038
|
});
|
|
927
1039
|
}
|
|
1040
|
+
// The clips replay by the same discriminator rule as the briefs and the marks: the
|
|
1041
|
+
// presence of the CHANGE is what says a wire carries one. A delta written before
|
|
1042
|
+
// 2026-08-24 states the whole array when it has one and states nothing when it does
|
|
1043
|
+
// not, so "no array" means "no clips" there, exactly as it does for the marks.
|
|
1044
|
+
const clips = wire.clipBase === undefined
|
|
1045
|
+
? (wire.clips !== undefined ? clone(wire.clips) : [])
|
|
1046
|
+
: [...(state.clips ?? []).slice(0, wire.clipBase), ...clone(wire.addClips ?? [])];
|
|
1047
|
+
if (wire.clipBase !== undefined && wire.clipBase > (state.clips ?? []).length) {
|
|
1048
|
+
throw new Error(`Unknown active-context clip base ${wire.clipBase}`);
|
|
1049
|
+
}
|
|
1050
|
+
if (new Set(clips.map((clip) => clip.callId)).size !== clips.length) {
|
|
1051
|
+
throw new Error("Invalid active-context clips: duplicate call id");
|
|
1052
|
+
}
|
|
928
1053
|
state = stateFromFoldRefs(
|
|
929
1054
|
{
|
|
930
1055
|
...wire,
|
|
931
1056
|
briefs: Object.keys(briefs).length ? briefs : undefined,
|
|
932
1057
|
pendingMarks: marks.length ? marks : undefined,
|
|
1058
|
+
clips: clips.length ? clips : undefined,
|
|
933
1059
|
},
|
|
934
1060
|
[...byId.values()],
|
|
935
1061
|
records,
|
|
@@ -989,6 +1115,7 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
|
|
|
989
1115
|
leases: clone(state.leases),
|
|
990
1116
|
...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
|
|
991
1117
|
...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
|
|
1118
|
+
...(state.clips?.length ? { clips: clone(state.clips) } : {}),
|
|
992
1119
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
993
1120
|
stateSha256: semanticStateSha256(state),
|
|
994
1121
|
}, state.sessionId) as ActiveContextCheckpointV2;
|
|
@@ -1036,6 +1163,23 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1036
1163
|
return !held || sha256Value(held) !== sha256Value(mark);
|
|
1037
1164
|
});
|
|
1038
1165
|
const marksTravel = previousMarks.length > 0 || nextMarks.length > 0;
|
|
1166
|
+
// THE CLIPS ARE THE SAME RULE A THIRD TIME (2026-08-24). They shipped whole on every
|
|
1167
|
+
// delta, which was 50.1 percent of all state bytes on sol-20260823-live rep 11, 472,750
|
|
1168
|
+
// of 942,902, for an array that never held more than 34 entries: the same defect the two
|
|
1169
|
+
// comments above were written about, arriving with a feature that did not inherit their
|
|
1170
|
+
// encoding. Clips are APPENDED and never edited, so the change is a count of what the
|
|
1171
|
+
// base already holds plus the ones that are new, and a rollback that shortens the array
|
|
1172
|
+
// states a lower count. The marks' order list would be worse than the bug here: a callId
|
|
1173
|
+
// is about a hundred characters, so re-listing them all on every delta costs more than
|
|
1174
|
+
// the array it replaces.
|
|
1175
|
+
const previousClips = previous.clips ?? [];
|
|
1176
|
+
const nextClips = next.clips ?? [];
|
|
1177
|
+
let clipBase = 0;
|
|
1178
|
+
while (clipBase < previousClips.length && clipBase < nextClips.length &&
|
|
1179
|
+
previousClips[clipBase].callId === nextClips[clipBase].callId &&
|
|
1180
|
+
previousClips[clipBase].entryId === nextClips[clipBase].entryId) clipBase += 1;
|
|
1181
|
+
const addClips = nextClips.slice(clipBase);
|
|
1182
|
+
const clipsTravel = previousClips.length > 0 || nextClips.length > 0;
|
|
1039
1183
|
// The rest travel whole and stay that way. Measured on the same run, the two that were
|
|
1040
1184
|
// worth a diff were 20.0 MB of the 21.6 MB ledger; what remained were bounded objects
|
|
1041
1185
|
// with their own caps rather than collections that grow with the epoch (the largest,
|
|
@@ -1061,13 +1205,41 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1061
1205
|
...(addPendingMarks.length ? { addPendingMarks: clone(addPendingMarks) } : {}),
|
|
1062
1206
|
...(Object.keys(addBriefs).length ? { addBriefs } : {}),
|
|
1063
1207
|
...(removeBriefIds.length ? { removeBriefIds } : {}),
|
|
1208
|
+
...(clipsTravel ? { clipBase: clipBase, ...(addClips.length ? { addClips: clone(addClips) } : {}) } : {}),
|
|
1064
1209
|
...(next.advisory ? { advisory: clone(next.advisory) } : {}),
|
|
1065
1210
|
stateSha256: semanticStateSha256(next),
|
|
1066
1211
|
}, next.sessionId) as ActiveContextDeltaV2;
|
|
1067
1212
|
}
|
|
1068
1213
|
|
|
1214
|
+
// THE FOREST IS HASHED ONCE PER STATE OBJECT (2026-08-28). Nine call-site pairs reach this,
|
|
1215
|
+
// and the one that scales is the per-mark loop in `commitPendingMarks`: `prepareFold` hashes
|
|
1216
|
+
// `state`, and the `commitPreparedFold` on the very next statement re-hashes the SAME object
|
|
1217
|
+
// through `preparedFoldError`. So every applied mark paid one avoidable whole-forest hash,
|
|
1218
|
+
// and `state` grows by a fold each iteration, making a commit epoch O(marks x folds).
|
|
1219
|
+
// Instrumented across the suite: 6,363 calls, 63.8 percent of them repeats on an object
|
|
1220
|
+
// already hashed, peaking at 198 in a single context event.
|
|
1221
|
+
//
|
|
1222
|
+
// It is gate 113's rule, gate 120's and gate 121's, at a fourth address: kept against the
|
|
1223
|
+
// state OBJECT rather than its contents, because state is REPLACED on every change rather
|
|
1224
|
+
// than mutated, so a replaced one misses and hashes again instead of serving something
|
|
1225
|
+
// stale. Every transition in the runtime is a spread; nothing writes `state.folds`,
|
|
1226
|
+
// `state.expanded` or a fold in place.
|
|
1227
|
+
//
|
|
1228
|
+
// The accepted cost, which is the same one gate 121 accepted: a state mutated in place keeps
|
|
1229
|
+
// its first digest, and this digest is persisted into `PreparedFold.topologySha256`.
|
|
1230
|
+
//
|
|
1231
|
+
// NOT memoized beside it: `protectionSha256`, measured at 0.037 to 0.061ms on real states
|
|
1232
|
+
// because `state.protected` is empty in every sealed run, so a memo there buys about 3
|
|
1233
|
+
// percent and adds a second retained map for nothing.
|
|
1234
|
+
const TOPOLOGY_SHA256 = new WeakMap<object, string>();
|
|
1235
|
+
|
|
1069
1236
|
export function topologySha256(state: Pick<ActiveContextState, "folds" | "expanded">): string {
|
|
1070
|
-
|
|
1237
|
+
const key = state as unknown as object;
|
|
1238
|
+
const memoized = TOPOLOGY_SHA256.get(key);
|
|
1239
|
+
if (memoized !== undefined) return memoized;
|
|
1240
|
+
const digest = sha256Value({ folds: state.folds, expanded: [...state.expanded].sort() });
|
|
1241
|
+
TOPOLOGY_SHA256.set(key, digest);
|
|
1242
|
+
return digest;
|
|
1071
1243
|
}
|
|
1072
1244
|
|
|
1073
1245
|
export function protectionSha256(state: Pick<ActiveContextState, "protected">): string {
|
package/extensions/lib/policy.ts
CHANGED
|
@@ -108,6 +108,16 @@ export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
|
|
|
108
108
|
*/
|
|
109
109
|
export const MAX_PINNED_SHARE = 0.25;
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* THE TOOL-CALL DIET's default share (Shane 2026-08-28, on the sol-20260826-full2
|
|
113
|
+
* verdict). Every fold repetition from 3 onward ran 0.50 and the campaign's headline
|
|
114
|
+
* results are all from that shape, so the shipped default is the measured value rather
|
|
115
|
+
* than the absence of one. A deployment turns the diet off with an explicit 0, which is
|
|
116
|
+
* why the option's range is [0, 1) instead of the open interval it shipped with: one
|
|
117
|
+
* meaning per value, and no absent-means-off special case.
|
|
118
|
+
*/
|
|
119
|
+
export const DEFAULT_TOOL_FOLD_THRESHOLD = 0.50;
|
|
120
|
+
|
|
111
121
|
export interface ActiveContextThresholds {
|
|
112
122
|
maxTarget: number;
|
|
113
123
|
minTarget: number;
|
|
@@ -141,9 +151,10 @@ export const MINIMUM_FOLD_CHARS_FLOOR = 2_000;
|
|
|
141
151
|
|
|
142
152
|
export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
|
|
143
153
|
maxTarget: 0.80,
|
|
144
|
-
// 0.
|
|
145
|
-
//
|
|
146
|
-
//
|
|
154
|
+
// 0.20 (Shane 2026-08-28, on the sol-20260826-full2 verdict). This number went 0.20 ->
|
|
155
|
+
// 0.40 on 2026-08-23 and came back on the campaign that was still running when it
|
|
156
|
+
// moved, so both readings are recorded here rather than one quietly replacing the
|
|
157
|
+
// other. The top of the band is not in question and never was.
|
|
147
158
|
//
|
|
148
159
|
// Read against the 19 mature sealed pifold runs that share a 251,520-token budget,
|
|
149
160
|
// where the trigger sat at 0.80 of it, 201,216 tokens. Peak occupancy per run came in
|
|
@@ -153,26 +164,28 @@ export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.free
|
|
|
153
164
|
// So 0.80 already delivers a 250k top, and raising it toward 250k directly would put
|
|
154
165
|
// the median peak over the budget and into the fence.
|
|
155
166
|
//
|
|
156
|
-
// The
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
167
|
+
// The 2026-08-23 case for 0.40 was that it seats the floor near where commits actually
|
|
168
|
+
// land: across 206 commit landings the session came to rest at a median 79,034 tokens,
|
|
169
|
+
// 153 of the 206 below 100,000, and landings sit ABOVE the aim rather than at it, since
|
|
170
|
+
// the class law and the available material both outrank it. That was bought knowing the
|
|
171
|
+
// price, about a fifth more epochs and so a fifth more full-prefix rewrites, to hold
|
|
172
|
+
// 100k of raw context across the whole cycle.
|
|
162
173
|
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
174
|
+
// The campaign priced the trade and it went the other way. The commit rewrite tax is
|
|
175
|
+
// minTarget / (maxTarget - minTarget), because every commit re-reads roughly
|
|
176
|
+
// minTarget x budget of prefix uncached and a shallower cut buys less runway for nearly
|
|
177
|
+
// the same bill: 1.0 at 0.80/0.40 against 0.33 at 0.80/0.20. Every sol-20260826-full2
|
|
178
|
+
// fold repetition from 3 onward ran 0.80/0.20, including the fold-plus-canon rep 5 that
|
|
179
|
+
// answered 14 of 16 with nothing wrong at $100.85 against native-plus-canon's $201.70.
|
|
180
|
+
// The shipped default is now the configuration that was measured.
|
|
168
181
|
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
minTarget: 0.
|
|
182
|
+
// THE KNOWN LIMIT, unchanged and still true: MAX_PINNED_SHARE is 0.25, so a fully
|
|
183
|
+
// pinned session legally holds more than a 0.20 aim asks for, and there the commit
|
|
184
|
+
// stops short of its target by construction rather than by fault. The epoch receipt
|
|
185
|
+
// reports targetBudgetShare beside actualFreedBudgetShare, so the shortfall stays a
|
|
186
|
+
// readable fact per epoch rather than a silent miss. The validation laws below bind
|
|
187
|
+
// exactly as before.
|
|
188
|
+
minTarget: 0.20,
|
|
176
189
|
consolidateAfter: 10,
|
|
177
190
|
minFoldChars: 8_000,
|
|
178
191
|
});
|
|
@@ -241,11 +254,6 @@ export function assertThresholdsServable(_thresholds: ActiveContextThresholds, b
|
|
|
241
254
|
}
|
|
242
255
|
}
|
|
243
256
|
|
|
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
257
|
export function servingBudgetTokens(window: number): number {
|
|
250
258
|
const resolved = Number.isFinite(window) && window > 0 ? window : DEFAULT_CONTEXT_WINDOW;
|
|
251
259
|
return resolved - Math.min(ACTIVE_CONTEXT_POLICY.responseReserve, Math.floor(resolved * 0.1));
|
|
@@ -253,6 +261,22 @@ export function servingBudgetTokens(window: number): number {
|
|
|
253
261
|
|
|
254
262
|
export const MAX_PENDING_MARKS = 256;
|
|
255
263
|
export const ESTIMATED_BYTES_PER_TOKEN = 4;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* What one image in the window is worth, in tokens, for estimation only.
|
|
267
|
+
*
|
|
268
|
+
* A provider prices an image from its DIMENSIONS, not its byte size: Anthropic bills
|
|
269
|
+
* about width x height / 750, which puts a 2000x563 screenshot near 1,500 tokens, and a
|
|
270
|
+
* large image caps close to this number once the long edge is scaled to the 1568px limit.
|
|
271
|
+
* The exact figure differs per provider and cannot be known from the base64 alone.
|
|
272
|
+
*
|
|
273
|
+
* It does not need to be exact. The value it replaces was the image's base64 length
|
|
274
|
+
* divided by a prose chars-per-token ratio, which read one screenshot as 110,000 tokens
|
|
275
|
+
* against a true cost near 1,500. Being within an order of magnitude ends the failure;
|
|
276
|
+
* the provider anchor corrects the rest as soon as a real measurement lands. A constant
|
|
277
|
+
* rather than an option, because nobody can tune this better than the anchor can.
|
|
278
|
+
*/
|
|
279
|
+
export const IMAGE_ESTIMATED_TOKENS = 1_600;
|
|
256
280
|
export const ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES = 240;
|
|
257
281
|
|
|
258
282
|
export const PEEK_MIN_SLICE_BYTES = 1_024;
|
|
@@ -271,7 +295,7 @@ export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
|
|
|
271
295
|
* agent answers. 1,200 seats the instruction and roughly eight folds; past that the list
|
|
272
296
|
* gives way and states how many it could not name.
|
|
273
297
|
*/
|
|
274
|
-
export const FOLD_NOTICE_BYTES =
|
|
298
|
+
export const FOLD_NOTICE_BYTES = 2_400;
|
|
275
299
|
export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
|
|
276
300
|
export const MAX_UNMARKED_CANDIDATES = 3;
|
|
277
301
|
|
|
@@ -298,7 +322,7 @@ export function peekIsEphemeral(params: Record<string, unknown> | undefined | nu
|
|
|
298
322
|
return params?.ephemeral !== false;
|
|
299
323
|
}
|
|
300
324
|
|
|
301
|
-
export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
|
|
325
|
+
export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek", "recall"]);
|
|
302
326
|
|
|
303
327
|
/** "user" is a mark laid down by the human through /fold-editor; it stages and
|
|
304
328
|
* commits through the same validated path as an agent mark and counts toward the
|
|
@@ -334,16 +358,32 @@ export const ACTIVE_CONTEXT_POLICY = Object.freeze({
|
|
|
334
358
|
// clamps its `bytes` to it. Was two numbers until 2026-08-22, when the peek ceiling
|
|
335
359
|
// was `maxChapterChars` 128,000 and that constant also claimed to cap a chapter.
|
|
336
360
|
maxSourceChars: 200_000,
|
|
361
|
+
// THE TOOL-CALL DIET (2026-08-24): at COMMIT TIME ONLY, a stale tool result inside
|
|
362
|
+
// the configured toolFoldThreshold share of the projected window is clipped IN VIEW
|
|
363
|
+
// to an identified head, the full bytes staying peek-recoverable behind its entry id.
|
|
364
|
+
// The head keeps the leading paragraph up to its cap (gate 134's rule); results at or
|
|
365
|
+
// under the floor stay whole, because clipping a small result buys almost nothing and
|
|
366
|
+
// adds a peek indirection. Constants, not options, for v1: the share point is the one
|
|
367
|
+
// public knob.
|
|
368
|
+
toolClipHeadChars: 500,
|
|
369
|
+
toolClipFloorChars: 2_000,
|
|
337
370
|
maxBriefChars: 2_000,
|
|
371
|
+
// The agent's share of maxBriefChars when a supplied brief augments the deterministic
|
|
372
|
+
// head (sol-20260823-live rep 7: supplied briefs REPLACED the fact-carrying head and
|
|
373
|
+
// 74 of 95 peeks recovered values the run's own briefs dropped). The head keeps the
|
|
374
|
+
// remainder, so identification can never be displaced by a long agent clause.
|
|
375
|
+
agentBriefReserve: 600,
|
|
338
376
|
orientationMessages: 2,
|
|
339
377
|
maxOrientationChars: 12_000,
|
|
340
378
|
});
|
|
341
379
|
|
|
342
380
|
export type FoldKind = "tool-result" | "chapter" | "consolidation";
|
|
381
|
+
export type ToolClip = { callId: string; entryId: string };
|
|
343
382
|
export type FoldPart = { kind: "raw"; ref: EvidenceRef } | { kind: "fold"; foldId: string };
|
|
344
383
|
export type BriefProvenance =
|
|
345
384
|
| { kind: "supplied" }
|
|
346
385
|
| { kind: "deterministic" }
|
|
386
|
+
| { kind: "augmented" }
|
|
347
387
|
| {
|
|
348
388
|
kind: "model" | "luna";
|
|
349
389
|
provider: string;
|
|
@@ -395,6 +435,7 @@ export interface ActiveContextState {
|
|
|
395
435
|
pendingMarks?: PendingMark[];
|
|
396
436
|
pinnedPeeks?: never;
|
|
397
437
|
briefs?: Record<string, BriefOverride>;
|
|
438
|
+
clips?: ToolClip[];
|
|
398
439
|
prepared?: PreparedFold;
|
|
399
440
|
advisory?: {
|
|
400
441
|
highWater: number;
|
|
@@ -430,6 +471,7 @@ export interface ActiveContextCheckpointV2 {
|
|
|
430
471
|
leases?: Record<string, number>;
|
|
431
472
|
pendingMarks?: PendingMark[];
|
|
432
473
|
briefs?: Record<string, BriefOverride>;
|
|
474
|
+
clips?: ToolClip[];
|
|
433
475
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
434
476
|
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
435
477
|
lastCall?: NonNullable<ActiveContextState["lastCall"]>;
|
|
@@ -461,6 +503,16 @@ export interface ActiveContextDeltaV2 {
|
|
|
461
503
|
briefs?: Record<string, BriefOverride>;
|
|
462
504
|
addBriefs?: Record<string, BriefOverride>;
|
|
463
505
|
removeBriefIds?: string[];
|
|
506
|
+
// `clips` on a delta is the WHOLE array: what every delta written before 2026-08-24
|
|
507
|
+
// carries, and what sealed sessions replay. New deltas carry the change instead. Clips
|
|
508
|
+
// are appended and never edited, so the change is a count of what the base already holds
|
|
509
|
+
// plus the ones that are new; a rollback that shortens the array states a lower count.
|
|
510
|
+
// The order list the marks use would be WORSE than the bug here, because a callId is
|
|
511
|
+
// about a hundred characters and re-listing thirty of them on every delta costs more
|
|
512
|
+
// than the array it replaces.
|
|
513
|
+
clips?: ToolClip[];
|
|
514
|
+
clipBase?: number;
|
|
515
|
+
addClips?: ToolClip[];
|
|
464
516
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
465
517
|
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
466
518
|
lastCall?: NonNullable<ActiveContextState["lastCall"]>;
|