pi-fold 2.0.0 → 3.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 +49 -28
- package/extensions/active-context.ts +1011 -444
- package/extensions/index.js +16 -3
- package/extensions/lib/curation.ts +56 -60
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +26 -42
- package/extensions/lib/instrumentation.ts +10 -3
- package/extensions/lib/measurement.ts +6 -12
- package/extensions/lib/persistence.ts +62 -38
- package/extensions/lib/policy.ts +171 -89
- package/extensions/lib/scheduling.ts +341 -108
- package/extensions/lib/selection.ts +6 -78
- package/extensions/lib/tool-surface.ts +29 -33
- package/extensions/lib/transcript.ts +17 -60
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
orderedRoots,
|
|
29
29
|
refsInOrder,
|
|
30
30
|
refsProtected,
|
|
31
|
-
toolRefsProtected,
|
|
32
31
|
} from "./measurement.ts";
|
|
33
32
|
import {
|
|
34
33
|
childFoldIds,
|
|
@@ -54,7 +53,6 @@ import {
|
|
|
54
53
|
PEEK_DEFAULT_MAX_BYTES,
|
|
55
54
|
PEEK_HEAD_SHARE,
|
|
56
55
|
STATUS_DIET_INDEX_ROWS,
|
|
57
|
-
zoneBytes,
|
|
58
56
|
} from "./policy.ts";
|
|
59
57
|
import type {
|
|
60
58
|
ActiveContextSnapshot,
|
|
@@ -85,7 +83,6 @@ import {
|
|
|
85
83
|
export function selectAutomaticChapter(
|
|
86
84
|
snapshot: ActiveContextSnapshot,
|
|
87
85
|
state: ActiveContextState,
|
|
88
|
-
maximumSourceRefs: number = snapshot.policy.maxFoldSourceRefs,
|
|
89
86
|
claimed: ReadonlySet<string> = new Set<string>(),
|
|
90
87
|
): FoldCandidate | null {
|
|
91
88
|
const units = chapterUnits(snapshot);
|
|
@@ -93,24 +90,22 @@ export function selectAutomaticChapter(
|
|
|
93
90
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
|
|
94
91
|
const first = units[unitIndex];
|
|
95
92
|
let best: FoldCandidate | null = null;
|
|
96
|
-
const turnStarts = new Set<number>();
|
|
97
93
|
for (let endIndex = unitIndex; endIndex < units.length; endIndex += 1) {
|
|
98
94
|
const unit = units[endIndex];
|
|
99
95
|
if (endIndex > unitIndex && unit.start !== units[endIndex - 1].end) break;
|
|
100
|
-
turnStarts.add(unit.turnStart);
|
|
101
|
-
if (turnStarts.size > snapshot.policy.maxChapterTurns) break;
|
|
102
96
|
const coherentSegment = endIndex > unitIndex || first.end - first.start > 1;
|
|
103
97
|
if (!coherentSegment) continue;
|
|
104
98
|
const parts = partsForRange(snapshot, state, first.start, unit.end - 1, allowedChildren);
|
|
105
99
|
if (!parts || parts.some((part) => part.kind === "fold" && state.expanded.includes(part.foldId))) continue;
|
|
106
100
|
const refs = candidateSourceRefs(parts, state);
|
|
107
|
-
if (refs.length > maximumSourceRefs) break;
|
|
108
101
|
if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref)))) continue;
|
|
109
102
|
if (refsProtected(refs, state, snapshot)) continue;
|
|
110
103
|
const size = bytes(encodedFoldSource(snapshot, state, parts, "chapter"));
|
|
111
|
-
if (size > snapshot.policy.maxChapterChars && endIndex > unitIndex) break;
|
|
112
104
|
const biteSized = size <= MAX_FOLD_SPAN_CHARS || endIndex === unitIndex;
|
|
113
|
-
|
|
105
|
+
// ACCUMULATE UNTIL OVER, never cut to hit the number. The walk grows the span one
|
|
106
|
+
// whole unit at a time and records the first one that carries it past the floor,
|
|
107
|
+
// so a fold is always at least minFoldChars and always ends on a message boundary.
|
|
108
|
+
if (size >= snapshot.thresholds.minFoldChars && biteSized) best = { kind: "chapter", parts, sourceRefs: refs };
|
|
114
109
|
if (size > MAX_FOLD_SPAN_CHARS) break;
|
|
115
110
|
}
|
|
116
111
|
if (best) return best;
|
|
@@ -123,7 +118,7 @@ export function selectAutomaticStaleSpan(
|
|
|
123
118
|
state: ActiveContextState,
|
|
124
119
|
claimed: ReadonlySet<string> = new Set<string>(),
|
|
125
120
|
): FoldCandidate | null {
|
|
126
|
-
const chapter = selectAutomaticChapter(snapshot, state,
|
|
121
|
+
const chapter = selectAutomaticChapter(snapshot, state, claimed);
|
|
127
122
|
const folds = selectAutomaticConsolidations(snapshot, state)[0] ?? null;
|
|
128
123
|
if (!chapter || !folds) return chapter ?? folds;
|
|
129
124
|
const at = (candidate: FoldCandidate): number =>
|
|
@@ -173,7 +168,7 @@ export function selectAutomaticRung(
|
|
|
173
168
|
const claimedFoldIds = options.claimedFoldIds ?? new Set<string>();
|
|
174
169
|
if (!options.toolOnly && state.prepared) {
|
|
175
170
|
if (!Number.isFinite(ratio) || ratio < hardFenceRatio(snapshot)) return null;
|
|
176
|
-
const candidate = selectAutomaticChapter(snapshot, state,
|
|
171
|
+
const candidate = selectAutomaticChapter(snapshot, state, claimed);
|
|
177
172
|
return candidate ? { kind: "prepared-chapter", candidate } : null;
|
|
178
173
|
}
|
|
179
174
|
const tool = selectAutomaticToolBatch(snapshot, state, claimed)[0] ?? null;
|
|
@@ -405,10 +400,8 @@ export async function prepareFold(input: {
|
|
|
405
400
|
now?: () => number;
|
|
406
401
|
}): Promise<PreparedFold> {
|
|
407
402
|
const { candidate, snapshot, state } = input;
|
|
408
|
-
const protectedSource = candidate.
|
|
409
|
-
|
|
410
|
-
: refsProtected(candidate.sourceRefs, state, snapshot);
|
|
411
|
-
if (!candidate.sourceRefs.length || candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs ||
|
|
403
|
+
const protectedSource = refsProtected(candidate.sourceRefs, state, snapshot);
|
|
404
|
+
if (!candidate.sourceRefs.length ||
|
|
412
405
|
refsInOrder(snapshot, candidate.sourceRefs) === null || protectedSource) {
|
|
413
406
|
throw new Error("Fold source is not exact, stale, and unprotected");
|
|
414
407
|
}
|
|
@@ -580,13 +573,12 @@ export function preparedFoldError(input: {
|
|
|
580
573
|
if (prepared.branchSha256 !== branchSha256(snapshot, [
|
|
581
574
|
...prepared.beforeRefs, ...prepared.sourceRefs, ...prepared.afterRefs,
|
|
582
575
|
])) return "branch drift";
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
: refsProtected(prepared.sourceRefs, state, snapshot)) return "source became protected";
|
|
576
|
+
// One protected set now: an entry is unfoldable because its turn has not completed or
|
|
577
|
+
// because it carries no evidence ref, and both are validity, not recency. The branch on
|
|
578
|
+
// fold kind went with fresh-tail protection: it read `toolRefsProtected` on one side,
|
|
579
|
+
// and once that collapsed onto `refsProtected` both arms were the same call.
|
|
580
|
+
if (indices.some((index) => snapshot.protectedIndices.has(index))) return "protection drift";
|
|
581
|
+
if (refsProtected(prepared.sourceRefs, state, snapshot)) return "source became protected";
|
|
590
582
|
if (Object.prototype.hasOwnProperty.call(input, "ratio")) {
|
|
591
583
|
if (input.ratio === null || typeof input.ratio !== "number" || !Number.isFinite(input.ratio)) {
|
|
592
584
|
return "current pressure unavailable";
|
|
@@ -686,7 +678,7 @@ export function protectEvidence(
|
|
|
686
678
|
if (byKey.size > MAX_ACTIVE_PROTECTED) {
|
|
687
679
|
throw new Error(
|
|
688
680
|
`Protecting ${ids.join(", ")} would hold ${byKey.size} refs, over the ${MAX_ACTIVE_PROTECTED}-ref cap; ` +
|
|
689
|
-
"
|
|
681
|
+
"unpin something first",
|
|
690
682
|
);
|
|
691
683
|
}
|
|
692
684
|
return clearPrepared({
|
|
@@ -737,9 +729,7 @@ export function renderFold(
|
|
|
737
729
|
const refs = flattenFoldRefs(fold, state);
|
|
738
730
|
const indices = refsInOrder(snapshot, refs);
|
|
739
731
|
if (!indices) return null;
|
|
740
|
-
const mustReveal = state.expanded.includes(fold.id) || (
|
|
741
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
742
|
-
: refsProtected(refs, state, snapshot));
|
|
732
|
+
const mustReveal = state.expanded.includes(fold.id) || (refsProtected(refs, state, snapshot));
|
|
743
733
|
if (!mustReveal) {
|
|
744
734
|
const first = snapshot.messages[indices[0]] as Record<string, unknown>;
|
|
745
735
|
const text = foldPlaceholder(fold, state, snapshot);
|
|
@@ -879,9 +869,7 @@ export function foldStatusRow(fold: ActiveFold, state: ActiveContextState, snaps
|
|
|
879
869
|
const refs = flattenFoldRefs(fold, state);
|
|
880
870
|
const allSourceIds = refs.map((ref) => ref.entryId);
|
|
881
871
|
const sourceIds = allSourceIds.slice(0, 64);
|
|
882
|
-
const blocked =
|
|
883
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
884
|
-
: refsProtected(refs, state, snapshot);
|
|
872
|
+
const blocked = refsProtected(refs, state, snapshot);
|
|
885
873
|
const projection = state.expanded.includes(fold.id) ? "expanded" : "folded";
|
|
886
874
|
return {
|
|
887
875
|
id: fold.id,
|
|
@@ -905,7 +893,7 @@ export function foldStatusRow(fold: ActiveFold, state: ActiveContextState, snaps
|
|
|
905
893
|
: { action: "refold", id: fold.id },
|
|
906
894
|
expand: { action: "expand", id: fold.id },
|
|
907
895
|
refold: { action: "refold", id: fold.id },
|
|
908
|
-
|
|
896
|
+
pin: { action: "pin", ids: [fold.id] },
|
|
909
897
|
},
|
|
910
898
|
};
|
|
911
899
|
}
|
|
@@ -995,7 +983,6 @@ export function activeContextStatus(
|
|
|
995
983
|
state: ActiveContextState,
|
|
996
984
|
offset = 0,
|
|
997
985
|
limit = 40,
|
|
998
|
-
maximumChapterSourceRefs = Number.MAX_SAFE_INTEGER,
|
|
999
986
|
statusOptions: { diet?: boolean; indexRows?: number } = {},
|
|
1000
987
|
): Record<string, unknown> {
|
|
1001
988
|
const roots = orderedRoots(state, snapshot).map((item) => item.fold.id);
|
|
@@ -1010,11 +997,11 @@ export function activeContextStatus(
|
|
|
1010
997
|
stale: !snapshot.protectedIndices.has(item.index),
|
|
1011
998
|
protected: snapshot.protectedIndices.has(item.index) || protectedKeys.has(objectRefKey(item.ref)),
|
|
1012
999
|
staleToolResult: item.ref.role === "toolResult" &&
|
|
1013
|
-
!snapshot.
|
|
1000
|
+
!snapshot.protectedIndices.has(item.index) && !protectedKeys.has(objectRefKey(item.ref)) &&
|
|
1014
1001
|
resultCall(snapshot, item.index, true) !== null,
|
|
1015
1002
|
}] : []);
|
|
1016
1003
|
const selectedObjects = objects.slice(offset, offset + limit);
|
|
1017
|
-
const eligibleChapter = selectAutomaticChapter(snapshot, state
|
|
1004
|
+
const eligibleChapter = selectAutomaticChapter(snapshot, state);
|
|
1018
1005
|
const eligibleSourceIds = eligibleChapter?.sourceRefs.map((ref) => ref.entryId) ?? [];
|
|
1019
1006
|
const eligibleEndpoints = eligibleSourceIds.length
|
|
1020
1007
|
? [...new Set([eligibleSourceIds[0], eligibleSourceIds.at(-1)!])]
|
|
@@ -1067,12 +1054,11 @@ export function activeContextStatus(
|
|
|
1067
1054
|
endId: eligibleSourceIds.at(-1),
|
|
1068
1055
|
action: { action: "fold", ids: eligibleEndpoints, brief: "<factual brief, at most 1000 characters>" },
|
|
1069
1056
|
} : null,
|
|
1070
|
-
rawTailMinimumBytes: zoneBytes(snapshot.thresholds.freshTail, snapshot.budgetTokens),
|
|
1071
1057
|
currentTurnRequiresBoundary: false,
|
|
1072
1058
|
actions: {
|
|
1073
1059
|
status: { action: "status", offset, limit },
|
|
1074
1060
|
fold: { action: "fold", ids: ["<source-or-fold-id>"], brief: "<optional factual brief, at most 1000 characters>" },
|
|
1075
|
-
|
|
1061
|
+
pin: { action: "pin", ids: ["<source-or-fold-id>"] },
|
|
1076
1062
|
},
|
|
1077
1063
|
};
|
|
1078
1064
|
}
|
|
@@ -1120,8 +1106,8 @@ export function boundStatusPayload(
|
|
|
1120
1106
|
shrink(instrumentation, "events", "context event(s)", true);
|
|
1121
1107
|
if (shrink(payload, "sourceMap", "source map row(s)") > 0) payload.sourceMapTruncated = true;
|
|
1122
1108
|
shrink(payload, "topFolds", "top fold row(s)");
|
|
1123
|
-
// The last epoch's receipt lists grow with commit depth (a
|
|
1124
|
-
//
|
|
1109
|
+
// The last epoch's receipt lists grow with commit depth (a deep epoch applies dozens
|
|
1110
|
+
// of marks). They are the commit's own receipt, so they outlast
|
|
1125
1111
|
// every other diagnostic, but they still page away before any listing row the
|
|
1126
1112
|
// caller actually asked for: on the detail pages, where none of the diagnostics
|
|
1127
1113
|
// above exist, they are the mass that made one fold row miss the cap.
|
|
@@ -1170,9 +1156,7 @@ export function visibleCollapsedFolds(
|
|
|
1170
1156
|
const visit = (fold: ActiveFold): void => {
|
|
1171
1157
|
const refs = flattenFoldRefs(fold, state);
|
|
1172
1158
|
if (!foldInterval(fold, state, snapshot)) return;
|
|
1173
|
-
const protectedSource =
|
|
1174
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
1175
|
-
: refsProtected(refs, state, snapshot);
|
|
1159
|
+
const protectedSource = refsProtected(refs, state, snapshot);
|
|
1176
1160
|
if (!state.expanded.includes(fold.id) && !protectedSource) {
|
|
1177
1161
|
output.push(fold);
|
|
1178
1162
|
return;
|
|
@@ -1352,7 +1336,7 @@ export function peekFoldSource(input: {
|
|
|
1352
1336
|
...(children.length ? { child: { action: "peek", id: children[0] } } : {}),
|
|
1353
1337
|
},
|
|
1354
1338
|
...(truncated
|
|
1355
|
-
? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.
|
|
1339
|
+
? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars) } }
|
|
1356
1340
|
: {}),
|
|
1357
1341
|
lifetime: "these bytes stay in the window exactly as returned, like any other tool result, and " +
|
|
1358
1342
|
`nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s stored source, so the ` +
|
|
@@ -1372,7 +1356,7 @@ export function peekFoldSource(input: {
|
|
|
1372
1356
|
"bytes were NOT shown" +
|
|
1373
1357
|
(view.omittedBytes ? " (the middle; the head and tail are both above)" : " (everything after this slice)") +
|
|
1374
1358
|
`. Widen with {"action":"peek","id":"${fold.id}","bytes":${
|
|
1375
|
-
Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.
|
|
1359
|
+
Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars)}}` +
|
|
1376
1360
|
(nextOffset === null ? "" : ` or read on with {"action":"peek","id":"${fold.id}","offset":${nextOffset}}`) +
|
|
1377
1361
|
".",
|
|
1378
1362
|
}
|
|
@@ -64,6 +64,7 @@ export interface CacheObservation {
|
|
|
64
64
|
change: ProjectionChange;
|
|
65
65
|
inputTokens: number;
|
|
66
66
|
cacheReadTokens: number;
|
|
67
|
+
cacheWriteTokens: number;
|
|
67
68
|
providerSideMiss: boolean;
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -83,15 +84,15 @@ export interface ContextEvent {
|
|
|
83
84
|
export type ContextEventKind =
|
|
84
85
|
| "context.attempt"
|
|
85
86
|
| "context.correction"
|
|
86
|
-
| "context.
|
|
87
|
+
| "context.pin"
|
|
87
88
|
| "context.capacity"
|
|
88
89
|
| "context.receipt"
|
|
89
|
-
| "context.rider"
|
|
90
90
|
| "context.lastcall"
|
|
91
91
|
| "context.response"
|
|
92
92
|
| "context.suggestion"
|
|
93
93
|
| "context.outcome"
|
|
94
94
|
| "context.notice"
|
|
95
|
+
| "context.frontier"
|
|
95
96
|
| "context.commit"
|
|
96
97
|
| "context.fold"
|
|
97
98
|
| "context.brief"
|
|
@@ -197,9 +198,14 @@ export function observeCacheUsage(
|
|
|
197
198
|
const usage = input.usage;
|
|
198
199
|
const inputTokens = ownValue(usage, "input");
|
|
199
200
|
const cacheReadTokens = ownValue(usage, "cacheRead");
|
|
201
|
+
const rawCacheWriteTokens = ownValue(usage, "cacheWrite");
|
|
200
202
|
if (typeof inputTokens !== "number" || !Number.isFinite(inputTokens) || inputTokens <= 0 ||
|
|
201
203
|
typeof cacheReadTokens !== "number" || !Number.isFinite(cacheReadTokens) ||
|
|
202
204
|
cacheReadTokens < 0) return null;
|
|
205
|
+
const cacheWriteTokens = typeof rawCacheWriteTokens === "number" &&
|
|
206
|
+
Number.isFinite(rawCacheWriteTokens) && rawCacheWriteTokens >= 0
|
|
207
|
+
? rawCacheWriteTokens
|
|
208
|
+
: 0;
|
|
203
209
|
const missed = cacheReadTokens === 0;
|
|
204
210
|
const preserved = typeof input.preservedShare === "number" && Number.isFinite(input.preservedShare)
|
|
205
211
|
? input.preservedShare
|
|
@@ -209,7 +215,8 @@ export function observeCacheUsage(
|
|
|
209
215
|
change: input.change,
|
|
210
216
|
inputTokens,
|
|
211
217
|
cacheReadTokens,
|
|
212
|
-
|
|
218
|
+
cacheWriteTokens,
|
|
219
|
+
providerSideMiss: missed && cacheWriteTokens === 0 && (input.change !== "rewrite" ||
|
|
213
220
|
(preserved !== null && preserved >= PROVIDER_SIDE_MISS_PRESERVED_SHARE)),
|
|
214
221
|
};
|
|
215
222
|
if (missed) ledger.observedMisses += 1;
|
|
@@ -325,6 +325,12 @@ export function explicitProtectedKeys(state: ActiveContextState): Set<string> {
|
|
|
325
325
|
return new Set(state.protected.map(objectRefKey));
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Held against folding: pinned by name, or unfoldable for validity (an unfinished turn, an
|
|
330
|
+
* entry with no evidence ref). There used to be a second reading of this for tool spans,
|
|
331
|
+
* differing only in which protected set it consulted; the two sets collapsed into one when
|
|
332
|
+
* fresh-tail was deleted, and the two functions were byte-identical after that.
|
|
333
|
+
*/
|
|
328
334
|
export function refsProtected(
|
|
329
335
|
refs: EvidenceRef[],
|
|
330
336
|
state: ActiveContextState,
|
|
@@ -337,18 +343,6 @@ export function refsProtected(
|
|
|
337
343
|
});
|
|
338
344
|
}
|
|
339
345
|
|
|
340
|
-
export function toolRefsProtected(
|
|
341
|
-
refs: EvidenceRef[],
|
|
342
|
-
state: ActiveContextState,
|
|
343
|
-
snapshot: ActiveContextSnapshot,
|
|
344
|
-
): boolean {
|
|
345
|
-
const explicit = explicitProtectedKeys(state);
|
|
346
|
-
return refs.some((ref) => {
|
|
347
|
-
const item = exactMapped(snapshot, ref);
|
|
348
|
-
return !item || explicit.has(objectRefKey(ref)) || snapshot.toolProtectedIndices.has(item.index);
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
346
|
export function protectedStaleMass(
|
|
353
347
|
snapshot: ActiveContextSnapshot,
|
|
354
348
|
state: ActiveContextState,
|
|
@@ -56,7 +56,7 @@ export const PENDING_FOLD_MARK_KEYS = [
|
|
|
56
56
|
"mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
|
|
57
57
|
] as const;
|
|
58
58
|
export const PENDING_REFOLD_MARK_KEYS = ["mark", "id", "origin", "ordinal"] as const;
|
|
59
|
-
export const MARK_ORIGINS = Object.freeze(["agent", "ladder"] as const);
|
|
59
|
+
export const MARK_ORIGINS = Object.freeze(["agent", "ladder", "user"] as const);
|
|
60
60
|
export const STATE_CHECKPOINT_V2_KEYS = [
|
|
61
61
|
"version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
|
|
62
62
|
] as const;
|
|
@@ -91,8 +91,7 @@ export function validPendingMark(value: unknown): value is PendingMark {
|
|
|
91
91
|
const kind = ownValue(value, "kind");
|
|
92
92
|
const parts = denseOwnArrayValues(ownValue(value, "parts"));
|
|
93
93
|
return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
|
|
94
|
-
Boolean(parts?.length) && parts!.
|
|
95
|
-
parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
|
|
94
|
+
Boolean(parts?.length) && parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
|
|
96
95
|
validProvenance(ownValue(value, "briefProvenance")) &&
|
|
97
96
|
id === foldIdFor(kind as FoldKind, parts as FoldPart[]);
|
|
98
97
|
}
|
|
@@ -210,7 +209,6 @@ export function validFoldShape(value: unknown): value is ActiveFold {
|
|
|
210
209
|
return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
|
|
211
210
|
typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
|
|
212
211
|
(parentId === null || typeof parentId === "string") && Boolean(parts?.length) &&
|
|
213
|
-
parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
|
|
214
212
|
parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
|
|
215
213
|
validProvenance(ownValue(value, "provenance")) && typeof sourceSha256 === "string" &&
|
|
216
214
|
/^[a-f0-9]{64}$/.test(sourceSha256) && Number.isSafeInteger(ownValue(value, "sourceChars")) &&
|
|
@@ -325,7 +323,9 @@ export function parseActiveContextState(
|
|
|
325
323
|
const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
|
|
326
324
|
const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
|
|
327
325
|
const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
|
|
328
|
-
|
|
326
|
+
// Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
|
|
327
|
+
// to reproduce, so here the tolerance is the whole of it.
|
|
328
|
+
const hasLegacyRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
|
|
329
329
|
refuseRetiredStateFields(value);
|
|
330
330
|
const extraKeys = [
|
|
331
331
|
...(hasPrepared ? ["prepared"] : []),
|
|
@@ -335,7 +335,7 @@ export function parseActiveContextState(
|
|
|
335
335
|
...(hasPendingMarks ? ["pendingMarks"] : []),
|
|
336
336
|
...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
|
|
337
337
|
...(hasBriefs ? ["briefs"] : []),
|
|
338
|
-
...(
|
|
338
|
+
...(hasLegacyRider ? ["rider"] : []),
|
|
339
339
|
];
|
|
340
340
|
if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
|
|
341
341
|
const folds = denseOwnArrayValues(ownValue(value, "folds"));
|
|
@@ -357,9 +357,6 @@ export function parseActiveContextState(
|
|
|
357
357
|
if (hasAdvisory && !validAdvisoryState(ownValue(value, "advisory"))) {
|
|
358
358
|
throw new Error("Invalid active-context advisory state");
|
|
359
359
|
}
|
|
360
|
-
if (hasRider && !validRiderState(ownValue(value, "rider"))) {
|
|
361
|
-
throw new Error("Invalid active-context rider state");
|
|
362
|
-
}
|
|
363
360
|
if (hasTokensSinceToolFold && !validTokensSinceToolFold(ownValue(value, "tokensSinceToolFold"))) {
|
|
364
361
|
throw new Error("Invalid active-context tool-fold cadence");
|
|
365
362
|
}
|
|
@@ -386,7 +383,6 @@ export function parseActiveContextState(
|
|
|
386
383
|
...(hasAdvisory
|
|
387
384
|
? { advisory: clone(source.advisory!) }
|
|
388
385
|
: defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
|
|
389
|
-
...(hasRider ? { rider: clone(source.rider!) } : {}),
|
|
390
386
|
...(hasPrepared ? { prepared: clone(source.prepared!) } : {}),
|
|
391
387
|
};
|
|
392
388
|
}
|
|
@@ -441,6 +437,29 @@ export function legacyCadenceOmittedStateSha256(state: ActiveContextState): stri
|
|
|
441
437
|
return sha256Value(legacy);
|
|
442
438
|
}
|
|
443
439
|
|
|
440
|
+
/**
|
|
441
|
+
* THE DIGEST A RIDER-ERA BUILD WROTE, reproduced without the subsystem.
|
|
442
|
+
*
|
|
443
|
+
* `semanticStateSha256` hashes the materialized state object, and `stableStringify` walks
|
|
444
|
+
* own keys in INSERTION order, so a field that is gone changes the digest even though it
|
|
445
|
+
* changed nothing else. The rider sat immediately before `prepared` in the materialized
|
|
446
|
+
* literal, and last when there was no prepared, so that is where it goes back for the one
|
|
447
|
+
* moment it is needed. It joins the two legacy derivations already here, which exist for
|
|
448
|
+
* exactly this reason: a state stays verifiable across the build that stopped writing it.
|
|
449
|
+
*
|
|
450
|
+
* Consulted ONLY when the wire actually carried a rider, so a current state whose digest
|
|
451
|
+
* has genuinely drifted still fails rather than finding a second way to pass.
|
|
452
|
+
*/
|
|
453
|
+
export function legacyRiderStateSha256(state: ActiveContextState, rider: unknown): string {
|
|
454
|
+
const legacy: Record<string, unknown> = {};
|
|
455
|
+
for (const [key, value] of Object.entries(state)) {
|
|
456
|
+
if (key === "prepared") legacy.rider = rider;
|
|
457
|
+
legacy[key] = value;
|
|
458
|
+
}
|
|
459
|
+
if (!Object.prototype.hasOwnProperty.call(legacy, "rider")) legacy.rider = rider;
|
|
460
|
+
return sha256Value(legacy);
|
|
461
|
+
}
|
|
462
|
+
|
|
444
463
|
export function legacyReplayOrderStateSha256(state: ActiveContextState): string {
|
|
445
464
|
return sha256Value({
|
|
446
465
|
version: state.version,
|
|
@@ -503,13 +522,11 @@ export function validateV2ProjectionFields(
|
|
|
503
522
|
leasesValue: unknown,
|
|
504
523
|
pendingMarksValue?: unknown,
|
|
505
524
|
briefsValue?: unknown,
|
|
506
|
-
riderValue?: unknown,
|
|
507
525
|
): {
|
|
508
526
|
expanded: string[];
|
|
509
527
|
protected: EvidenceRef[];
|
|
510
528
|
prepared?: PreparedFold;
|
|
511
529
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
512
|
-
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
513
530
|
tokensSinceToolFold: number;
|
|
514
531
|
leases: Record<string, number>;
|
|
515
532
|
pendingMarks: PendingMark[];
|
|
@@ -527,9 +544,6 @@ export function validateV2ProjectionFields(
|
|
|
527
544
|
if (advisoryValue !== undefined && !validAdvisoryState(advisoryValue)) {
|
|
528
545
|
throw new Error("Invalid active-context v2 advisory state");
|
|
529
546
|
}
|
|
530
|
-
if (riderValue !== undefined && !validRiderState(riderValue)) {
|
|
531
|
-
throw new Error("Invalid active-context v2 rider state");
|
|
532
|
-
}
|
|
533
547
|
if (tokensSinceToolFoldValue !== undefined && !validTokensSinceToolFold(tokensSinceToolFoldValue)) {
|
|
534
548
|
throw new Error("Invalid active-context v2 tool-fold cadence");
|
|
535
549
|
}
|
|
@@ -543,9 +557,6 @@ export function validateV2ProjectionFields(
|
|
|
543
557
|
...(advisoryValue === undefined ? {} : {
|
|
544
558
|
advisory: clone(advisoryValue) as NonNullable<ActiveContextState["advisory"]>,
|
|
545
559
|
}),
|
|
546
|
-
...(riderValue === undefined ? {} : {
|
|
547
|
-
rider: clone(riderValue) as NonNullable<ActiveContextState["rider"]>,
|
|
548
|
-
}),
|
|
549
560
|
tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
|
|
550
561
|
leases,
|
|
551
562
|
pendingMarks,
|
|
@@ -575,7 +586,7 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
575
586
|
Object.prototype.hasOwnProperty.call(value, "addBriefs"));
|
|
576
587
|
const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
|
|
577
588
|
Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
|
|
578
|
-
const
|
|
589
|
+
const hasLegacyRider = Boolean(value && typeof value === "object" &&
|
|
579
590
|
Object.prototype.hasOwnProperty.call(value, "rider"));
|
|
580
591
|
refuseRetiredStateFields(value);
|
|
581
592
|
const optionalKeys = [
|
|
@@ -589,7 +600,9 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
589
600
|
...(hasBriefs ? ["briefs"] : []),
|
|
590
601
|
...(hasAddBriefs ? ["addBriefs"] : []),
|
|
591
602
|
...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
|
|
592
|
-
|
|
603
|
+
// Carried no further than the digest check in `materializeStatePersistence`, which is
|
|
604
|
+
// the only thing on a load that the rider's absence can change.
|
|
605
|
+
...(hasLegacyRider ? ["rider"] : []),
|
|
593
606
|
];
|
|
594
607
|
const checkpoint = kind === "checkpoint" &&
|
|
595
608
|
exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
|
|
@@ -605,7 +618,6 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
605
618
|
ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
|
|
606
619
|
ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
|
|
607
620
|
ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
|
|
608
|
-
ownValue(value, "rider"),
|
|
609
621
|
);
|
|
610
622
|
if (checkpoint) {
|
|
611
623
|
const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
|
|
@@ -700,7 +712,7 @@ export function stateFromFoldRefs(
|
|
|
700
712
|
wire: Pick<
|
|
701
713
|
ActiveContextCheckpointV2,
|
|
702
714
|
"sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
|
|
703
|
-
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs"
|
|
715
|
+
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs"
|
|
704
716
|
>,
|
|
705
717
|
refs: FoldRecordRef[],
|
|
706
718
|
records: Map<string, FoldRecordEntry>,
|
|
@@ -723,7 +735,6 @@ export function stateFromFoldRefs(
|
|
|
723
735
|
...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
|
|
724
736
|
...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
|
|
725
737
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
726
|
-
...(wire.rider === undefined ? {} : { rider: clone(wire.rider) }),
|
|
727
738
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
728
739
|
};
|
|
729
740
|
return parseActiveContextState(state, wire.sessionId, false);
|
|
@@ -793,6 +804,14 @@ export function materializeStatePersistence(
|
|
|
793
804
|
},
|
|
794
805
|
};
|
|
795
806
|
let stateSha256 = semanticStateSha256(state);
|
|
807
|
+
// WHAT THE LAST ACCEPTED ENTRY ACTUALLY RECORDED, when that is not what this build
|
|
808
|
+
// would compute. A state accepted through one of the legacy derivations below was
|
|
809
|
+
// written by a build that hashed it differently, and the delta that follows it names
|
|
810
|
+
// its base by THAT digest, so a chain of them is unreadable unless the reading carries
|
|
811
|
+
// both: the modern digest for what this build writes next, and the written one for what
|
|
812
|
+
// the next entry will name. Null whenever they agree, which is every state this build
|
|
813
|
+
// wrote itself, so the chain check below is unchanged for them.
|
|
814
|
+
let writtenSha256: string | null = null;
|
|
796
815
|
let stateStart = -1;
|
|
797
816
|
let checkpointIndex = -1;
|
|
798
817
|
let v2Seen = false;
|
|
@@ -845,7 +864,8 @@ export function materializeStatePersistence(
|
|
|
845
864
|
if (wire.kind === "checkpoint") {
|
|
846
865
|
state = stateFromFoldRefs(wire, wire.foldRefs, records);
|
|
847
866
|
} else {
|
|
848
|
-
if (wireVersion === 0 || wire.baseRevision !== state.revision ||
|
|
867
|
+
if (wireVersion === 0 || wire.baseRevision !== state.revision ||
|
|
868
|
+
(wire.baseStateSha256 !== stateSha256 && wire.baseStateSha256 !== writtenSha256) ||
|
|
849
869
|
wire.revision <= wire.baseRevision) {
|
|
850
870
|
throw new Error("Broken active-context delta chain");
|
|
851
871
|
}
|
|
@@ -916,13 +936,17 @@ export function materializeStatePersistence(
|
|
|
916
936
|
);
|
|
917
937
|
}
|
|
918
938
|
const calculated = semanticStateSha256(state);
|
|
939
|
+
const legacyRider = (wire as { rider?: unknown }).rider;
|
|
919
940
|
if (calculated !== wire.stateSha256 &&
|
|
920
941
|
legacyCadenceOmittedStateSha256(state) !== wire.stateSha256 &&
|
|
921
|
-
legacyReplayOrderStateSha256(state) !== wire.stateSha256
|
|
942
|
+
legacyReplayOrderStateSha256(state) !== wire.stateSha256 &&
|
|
943
|
+
(legacyRider === undefined ||
|
|
944
|
+
legacyRiderStateSha256(state, legacyRider) !== wire.stateSha256)) {
|
|
922
945
|
throw new Error("Active-context v2 state digest drift");
|
|
923
946
|
}
|
|
924
947
|
wireVersion = 2;
|
|
925
948
|
stateSha256 = calculated;
|
|
949
|
+
writtenSha256 = wire.stateSha256 === calculated ? null : wire.stateSha256;
|
|
926
950
|
rememberProjection();
|
|
927
951
|
}
|
|
928
952
|
return { state, wireVersion, records, stateSha256, projectionFingerprints };
|
|
@@ -966,7 +990,6 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
|
|
|
966
990
|
...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
|
|
967
991
|
...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
|
|
968
992
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
969
|
-
...(state.rider ? { rider: clone(state.rider) } : {}),
|
|
970
993
|
stateSha256: semanticStateSha256(state),
|
|
971
994
|
}, state.sessionId) as ActiveContextCheckpointV2;
|
|
972
995
|
}
|
|
@@ -1039,7 +1062,6 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1039
1062
|
...(Object.keys(addBriefs).length ? { addBriefs } : {}),
|
|
1040
1063
|
...(removeBriefIds.length ? { removeBriefIds } : {}),
|
|
1041
1064
|
...(next.advisory ? { advisory: clone(next.advisory) } : {}),
|
|
1042
|
-
...(next.rider ? { rider: clone(next.rider) } : {}),
|
|
1043
1065
|
stateSha256: semanticStateSha256(next),
|
|
1044
1066
|
}, next.sessionId) as ActiveContextDeltaV2;
|
|
1045
1067
|
}
|
|
@@ -1056,12 +1078,13 @@ export const ADVISORY_MILESTONES = Object.freeze(
|
|
|
1056
1078
|
["orientation", "notice", "tools", "chapters", "urgent"] as AdvisoryMilestone[],
|
|
1057
1079
|
);
|
|
1058
1080
|
|
|
1059
|
-
export const MAX_RIDER_TEXT_BYTES = 4_096;
|
|
1060
|
-
|
|
1061
1081
|
/**
|
|
1062
1082
|
* THE RETIRED STATE FIELDS, REFUSED BY NAME.
|
|
1063
1083
|
*
|
|
1064
|
-
* `lastCall` and `notices` were the occupancy thermostat's two announcements
|
|
1084
|
+
* `lastCall` and `notices` were the occupancy thermostat's two announcements, and `rider`
|
|
1085
|
+
* was the post-commit invitation to create marks, deleted 2026-08-23 with the curation
|
|
1086
|
+
* redesign: the runtime cuts and the agent edits, so an invitation to create is a carrier
|
|
1087
|
+
* with nothing left to invite. Nothing
|
|
1065
1088
|
* writes them any more and nothing reads them, so a durable state that still carries one
|
|
1066
1089
|
* was written by a build that had them, and the exact-record readers would reject it with
|
|
1067
1090
|
* "Invalid active-context state keys" -- true, and useless to whoever has to act on it.
|
|
@@ -1071,6 +1094,15 @@ export const MAX_RIDER_TEXT_BYTES = 4_096;
|
|
|
1071
1094
|
* that comes back is not the state that was written. Gate 17 already states the wire's
|
|
1072
1095
|
* policy in the other direction -- an older exact-record reader rejects newer fields, and
|
|
1073
1096
|
* a bump is explicit -- and this is that same policy read from the newer side.
|
|
1097
|
+
*
|
|
1098
|
+
* `rider` IS NOT ON THIS LIST, and the distinction is the whole point of the list. The
|
|
1099
|
+
* refusal's own reason is "a state it cannot reproduce", so a field the build CAN
|
|
1100
|
+
* reproduce is out of its scope: the rider was projection carrier text, it changed no
|
|
1101
|
+
* fold, no ref and no brief, and the only thing it ever touched that survives a load is
|
|
1102
|
+
* the state digest. So it is read, spent on the digest, and dropped, which is what
|
|
1103
|
+
* `legacyRiderStateSha256` below is for. Retiring it by name instead made every state the
|
|
1104
|
+
* sol-20260815-hidden campaign sealed unreadable and cost the hidden-mass result its 102
|
|
1105
|
+
* attributed carriage rows, which is a high price for a field that carried no behaviour.
|
|
1074
1106
|
*/
|
|
1075
1107
|
const RETIRED_STATE_FIELDS: readonly string[] = Object.freeze(["lastCall", "notices", "surfacing"]);
|
|
1076
1108
|
|
|
@@ -1086,14 +1118,6 @@ export function refuseRetiredStateFields(value: unknown): void {
|
|
|
1086
1118
|
);
|
|
1087
1119
|
}
|
|
1088
1120
|
|
|
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
1121
|
export function validAdvisoryState(value: unknown): value is NonNullable<ActiveContextState["advisory"]> {
|
|
1098
1122
|
const hasArmed = Boolean(value && typeof value === "object" &&
|
|
1099
1123
|
Object.prototype.hasOwnProperty.call(value, "armed"));
|