pi-fold 0.0.1 → 0.1.1

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.
@@ -0,0 +1,814 @@
1
+ import type { EvidenceRef } from "../json.ts";
2
+ import {
3
+ denseOwnArrayValues,
4
+ isObjectRef,
5
+ isPlainRecord,
6
+ objectRefKey,
7
+ sha256Value,
8
+ stableStringify,
9
+ } from "../json.ts";
10
+ import {
11
+ bytes,
12
+ clone,
13
+ emptyActiveContextState,
14
+ exactRecord,
15
+ ownValue,
16
+ structurallyValidBrief,
17
+ } from "./canonical.ts";
18
+ import {
19
+ ACTIVE_CONTEXT_FOLD_RECORD_ENTRY,
20
+ ACTIVE_CONTEXT_POLICY,
21
+ ACTIVE_CONTEXT_STATE_ENTRY,
22
+ EXPAND_LEASE_GENERATIONS,
23
+ MAX_ADVISORY_DELIVERIES_PER_MILESTONE,
24
+ MAX_EXPAND_LEASES,
25
+ SURFACING_MAX_LOG_RECORDS,
26
+ } from "./policy.ts";
27
+ import type {
28
+ ActiveContextCheckpointV2,
29
+ ActiveContextDeltaV2,
30
+ ActiveContextState,
31
+ ActiveContextStateWireV2,
32
+ ActiveFold,
33
+ AdvisoryMilestone,
34
+ BriefProvenance,
35
+ FoldKind,
36
+ FoldPart,
37
+ FoldRecordEntry,
38
+ FoldRecordRef,
39
+ PreparedFold,
40
+ SurfacingRecord,
41
+ } from "./policy.ts";
42
+
43
+ export const ACTIVE_FOLD_KEYS = [
44
+ "id", "kind", "parentId", "parts", "brief", "provenance", "sourceSha256",
45
+ "sourceChars", "placeholderChars", "createdAt",
46
+ ] as const;
47
+ export const PREPARED_FOLD_KEYS = [
48
+ "id", "sessionId", "generation", "branchSha256", "topologySha256", "protectionSha256",
49
+ "sourceRefs", "sourceSha256", "beforeRefs", "beforeSha256", "afterRefs", "afterSha256", "fold",
50
+ ] as const;
51
+ export const ACTIVE_STATE_KEYS = ["version", "sessionId", "revision", "folds", "expanded", "protected"] as const;
52
+ export const FOLD_RECORD_REF_KEYS = ["id", "sha256"] as const;
53
+ export const FOLD_RECORD_ENTRY_KEYS = ["version", "sessionId", "foldId", "recordSha256", "fold"] as const;
54
+ export const SURFACING_RECORD_KEYS = ["source", "id", "score", "ordinal", "outcome"] as const;
55
+ export const SURFACING_OUTCOMES = Object.freeze(["shown", "accept", "reject"] as const);
56
+ export const STATE_CHECKPOINT_V2_KEYS = [
57
+ "version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
58
+ ] as const;
59
+ export const STATE_DELTA_V2_KEYS = [
60
+ "version", "kind", "sessionId", "revision", "baseRevision", "baseStateSha256", "addFoldRefs",
61
+ "removeFoldIds", "expanded", "protected", "prepared", "stateSha256",
62
+ ] as const;
63
+ export const MAX_ACTIVE_FOLD_RECORD_BYTES = 256 * 1024;
64
+ export const MAX_ACTIVE_STATE_EVENT_BYTES = 512 * 1024;
65
+ export const MAX_ACTIVE_FOLD_RECORDS = 4_096;
66
+ export const MAX_ACTIVE_FOLD_PARTS = 1_024;
67
+ export const MAX_ACTIVE_EXPANDED = 1_024;
68
+ export const MAX_ACTIVE_PROTECTED = 1_024;
69
+
70
+ export function validTokensSinceToolFold(value: unknown): value is number {
71
+ return Number.isSafeInteger(value) && Number(value) >= 0;
72
+ }
73
+
74
+ export function validSurfacingRecord(value: unknown): value is SurfacingRecord {
75
+ if (!exactRecord(value, SURFACING_RECORD_KEYS)) return false;
76
+ const score = ownValue(value, "score");
77
+ return typeof ownValue(value, "source") === "string" && Boolean(ownValue(value, "source")) &&
78
+ typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
79
+ typeof score === "number" && Number.isFinite(score) && Number(score) >= 0 && Number(score) <= 1 &&
80
+ Number.isSafeInteger(ownValue(value, "ordinal")) && Number(ownValue(value, "ordinal")) >= 0 &&
81
+ SURFACING_OUTCOMES.includes(ownValue(value, "outcome") as SurfacingRecord["outcome"]);
82
+ }
83
+
84
+ export function parseSurfacingLog(value: unknown): SurfacingRecord[] {
85
+ const records = denseOwnArrayValues(value);
86
+ if (!records || records.length > SURFACING_MAX_LOG_RECORDS || !records.every(validSurfacingRecord)) {
87
+ throw new Error("Invalid active-context surfacing log");
88
+ }
89
+ return clone(records) as SurfacingRecord[];
90
+ }
91
+
92
+ export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Record<string, number> {
93
+ if (!isPlainRecord(value)) throw new Error("Invalid active-context expand leases");
94
+ const entries = Object.entries(value);
95
+ if (entries.length > MAX_EXPAND_LEASES || entries.some(([id, remaining]) =>
96
+ !id || (foldIds && !foldIds.has(id)) || !Number.isSafeInteger(remaining) ||
97
+ Number(remaining) < 1 || Number(remaining) > EXPAND_LEASE_GENERATIONS)) {
98
+ throw new Error("Invalid active-context expand leases");
99
+ }
100
+ return Object.fromEntries(entries.map(([id, remaining]) => [id, Number(remaining)]));
101
+ }
102
+
103
+ export function validProvenance(value: unknown): value is BriefProvenance {
104
+ if (exactRecord(value, ["kind"]) &&
105
+ (ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic")) return true;
106
+ const hasDigest = Boolean(value && typeof value === "object" &&
107
+ Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
108
+ // legacy provenance kind written by pre-release sessions; normalized to the model kind at presentation
109
+ if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
110
+ (ownValue(value, "kind") !== "model" && ownValue(value, "kind") !== "luna") ||
111
+ typeof ownValue(value, "provider") !== "string" || !ownValue(value, "provider") ||
112
+ typeof ownValue(value, "model") !== "string" || !ownValue(value, "model") ||
113
+ typeof ownValue(value, "effort") !== "string" || !ownValue(value, "effort")) return false;
114
+ return !hasDigest || (typeof ownValue(value, "launchContractDigest") === "string" &&
115
+ /^[a-f0-9]{64}$/.test(String(ownValue(value, "launchContractDigest"))));
116
+ }
117
+
118
+ export function normalizeLegacyProvenance(provenance: unknown): BriefProvenance {
119
+ if (ownValue(provenance, "kind") !== "luna") return clone(provenance) as BriefProvenance;
120
+ return {
121
+ kind: "model",
122
+ provider: String(ownValue(provenance, "provider")),
123
+ model: String(ownValue(provenance, "model")),
124
+ effort: String(ownValue(provenance, "effort")),
125
+ ...(typeof ownValue(provenance, "launchContractDigest") === "string"
126
+ ? { launchContractDigest: String(ownValue(provenance, "launchContractDigest")) }
127
+ : {}),
128
+ };
129
+ }
130
+
131
+ export function validFoldPart(value: unknown): value is FoldPart {
132
+ if (exactRecord(value, ["kind", "ref"]) && ownValue(value, "kind") === "raw") {
133
+ return isObjectRef(ownValue(value, "ref"));
134
+ }
135
+ return exactRecord(value, ["kind", "foldId"]) && ownValue(value, "kind") === "fold" &&
136
+ typeof ownValue(value, "foldId") === "string" && Boolean(ownValue(value, "foldId"));
137
+ }
138
+
139
+ export function validFoldShape(value: unknown): value is ActiveFold {
140
+ if (!exactRecord(value, ACTIVE_FOLD_KEYS)) return false;
141
+ const kind = ownValue(value, "kind");
142
+ const parts = denseOwnArrayValues(ownValue(value, "parts"));
143
+ const parentId = ownValue(value, "parentId");
144
+ const sourceSha256 = ownValue(value, "sourceSha256");
145
+ return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
146
+ typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
147
+ (parentId === null || typeof parentId === "string") && Boolean(parts?.length) &&
148
+ parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
149
+ parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
150
+ validProvenance(ownValue(value, "provenance")) && typeof sourceSha256 === "string" &&
151
+ /^[a-f0-9]{64}$/.test(sourceSha256) && Number.isSafeInteger(ownValue(value, "sourceChars")) &&
152
+ Number(ownValue(value, "sourceChars")) > 0 && Number.isSafeInteger(ownValue(value, "placeholderChars")) &&
153
+ Number(ownValue(value, "placeholderChars")) > 0 && Number.isSafeInteger(ownValue(value, "createdAt")) &&
154
+ Number(ownValue(value, "createdAt")) >= 0;
155
+ }
156
+
157
+ export function normalizedPart(part: FoldPart): unknown {
158
+ return part.kind === "fold"
159
+ ? { kind: "fold", foldId: part.foldId }
160
+ : { kind: "raw", ref: { ...part.ref } };
161
+ }
162
+
163
+ export function foldIdFor(kind: FoldKind, parts: FoldPart[]): string {
164
+ return `fold_${sha256Value({ kind, parts: parts.map(normalizedPart) }).slice(0, 24)}`;
165
+ }
166
+
167
+ export function foldMap(state: Pick<ActiveContextState, "folds">): Map<string, ActiveFold> {
168
+ return new Map(state.folds.map((fold) => [fold.id, fold]));
169
+ }
170
+
171
+ export function childFoldIds(fold: ActiveFold): string[] {
172
+ return fold.parts.filter((part): part is Extract<FoldPart, { kind: "fold" }> => part.kind === "fold")
173
+ .map((part) => part.foldId);
174
+ }
175
+
176
+ export function flattenFoldRefs(fold: ActiveFold, state: Pick<ActiveContextState, "folds">): EvidenceRef[] {
177
+ const byId = foldMap(state);
178
+ const visiting = new Set<string>();
179
+ const visit = (current: ActiveFold): EvidenceRef[] => {
180
+ if (visiting.has(current.id)) throw new Error(`Active-context fold cycle at ${current.id}`);
181
+ visiting.add(current.id);
182
+ const refs = current.parts.flatMap((part) => {
183
+ if (part.kind === "raw") return [part.ref];
184
+ const child = byId.get(part.foldId);
185
+ if (!child) throw new Error(`Missing active-context child ${part.foldId}`);
186
+ return visit(child);
187
+ });
188
+ visiting.delete(current.id);
189
+ return refs;
190
+ };
191
+ return visit(fold);
192
+ }
193
+
194
+ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
195
+ const rawValues = denseOwnArrayValues(folds);
196
+ if (!rawValues || rawValues.some((fold) => !validFoldShape(fold))) {
197
+ throw new Error("Invalid active-context fold forest shape");
198
+ }
199
+ const values = clone(rawValues) as ActiveFold[];
200
+ const byId = new Map<string, ActiveFold>();
201
+ for (const fold of values) {
202
+ if (fold.id !== foldIdFor(fold.kind, fold.parts) || byId.has(fold.id)) {
203
+ throw new Error("Invalid active-context fold");
204
+ }
205
+ if (fold.kind === "tool-result" &&
206
+ (!fold.parts.length || fold.parts.some((part) => part.kind !== "raw" || part.ref.role !== "toolResult"))) {
207
+ throw new Error("Tool-result fold must own one validated assistant batch of tool results");
208
+ }
209
+ if (fold.kind === "consolidation" && fold.parts.some((part) => part.kind !== "fold")) {
210
+ throw new Error("Consolidation folds may contain only child folds");
211
+ }
212
+ byId.set(fold.id, fold);
213
+ }
214
+ const parent = new Map<string, string>();
215
+ const rawOwner = new Map<string, string>();
216
+ for (const fold of values) {
217
+ for (const part of fold.parts) {
218
+ if (part.kind === "raw") {
219
+ const key = objectRefKey(part.ref);
220
+ if (rawOwner.has(key)) throw new Error(`Evidence ${part.ref.entryId} has multiple direct fold owners`);
221
+ rawOwner.set(key, fold.id);
222
+ } else {
223
+ if (!byId.has(part.foldId) || part.foldId === fold.id || parent.has(part.foldId)) {
224
+ throw new Error(`Invalid or multiply owned child fold ${part.foldId}`);
225
+ }
226
+ parent.set(part.foldId, fold.id);
227
+ }
228
+ }
229
+ }
230
+ for (const fold of values) {
231
+ if ((parent.get(fold.id) ?? null) !== fold.parentId) throw new Error(`Fold ${fold.id} parent drift`);
232
+ for (const childId of childFoldIds(fold)) {
233
+ const child = byId.get(childId)!;
234
+ if (fold.kind === "chapter" && child.kind !== "tool-result") {
235
+ throw new Error(`Chapter ${fold.id} may contain only tool-result folds`);
236
+ }
237
+ if (fold.kind === "consolidation" && child.kind === "tool-result") {
238
+ throw new Error(`Consolidation ${fold.id} may contain only chapter or consolidation folds`);
239
+ }
240
+ }
241
+ }
242
+ for (const fold of values) {
243
+ const refs = flattenFoldRefs(fold, { folds: values });
244
+ if (fold.sourceSha256 !== sha256Value(refs)) throw new Error(`Fold ${fold.id} source digest drift`);
245
+ }
246
+ return values;
247
+ }
248
+
249
+ export function parseActiveContextState(
250
+ value: unknown,
251
+ sessionId: string,
252
+ defaultAdvisory = true,
253
+ ): ActiveContextState {
254
+ const recordLike = Boolean(value && typeof value === "object" && !Array.isArray(value));
255
+ const hasPrepared = recordLike && Object.prototype.hasOwnProperty.call(value, "prepared");
256
+ const hasAdvisory = recordLike && Object.prototype.hasOwnProperty.call(value, "advisory");
257
+ const hasTokensSinceToolFold = recordLike &&
258
+ Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold");
259
+ const hasLeases = recordLike && Object.prototype.hasOwnProperty.call(value, "leases");
260
+ const hasSurfacing = recordLike && Object.prototype.hasOwnProperty.call(value, "surfacing");
261
+ const extraKeys = [
262
+ ...(hasPrepared ? ["prepared"] : []),
263
+ ...(hasAdvisory ? ["advisory"] : []),
264
+ ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
265
+ ...(hasLeases ? ["leases"] : []),
266
+ ...(hasSurfacing ? ["surfacing"] : []),
267
+ ];
268
+ if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
269
+ const folds = denseOwnArrayValues(ownValue(value, "folds"));
270
+ const expanded = denseOwnArrayValues(ownValue(value, "expanded"));
271
+ const protectedRefs = denseOwnArrayValues(ownValue(value, "protected"));
272
+ if (ownValue(value, "version") !== 1 || ownValue(value, "sessionId") !== sessionId ||
273
+ !Number.isSafeInteger(ownValue(value, "revision")) || Number(ownValue(value, "revision")) < 0 ||
274
+ !folds || !expanded || !protectedRefs || expanded.some((id) => typeof id !== "string") ||
275
+ protectedRefs.some((ref) => !isObjectRef(ref))) throw new Error("Invalid active-context state");
276
+ const validatedFolds = validateFoldForest(folds as ActiveFold[]);
277
+ const ids = new Set(validatedFolds.map((fold) => fold.id));
278
+ if (new Set(expanded).size !== expanded.length || expanded.some((id) => !ids.has(String(id)))) {
279
+ throw new Error("Invalid active-context expanded set");
280
+ }
281
+ if (new Set((protectedRefs as EvidenceRef[]).map(objectRefKey)).size !== protectedRefs.length) {
282
+ throw new Error("Invalid active-context protection set");
283
+ }
284
+ if (hasPrepared) validatePreparedShape(ownValue(value, "prepared"));
285
+ if (hasAdvisory && !validAdvisoryState(ownValue(value, "advisory"))) {
286
+ throw new Error("Invalid active-context advisory state");
287
+ }
288
+ if (hasTokensSinceToolFold && !validTokensSinceToolFold(ownValue(value, "tokensSinceToolFold"))) {
289
+ throw new Error("Invalid active-context tool-fold cadence");
290
+ }
291
+ const leases = hasLeases ? parseLeases(ownValue(value, "leases"), ids) : {};
292
+ const surfacing = hasSurfacing ? parseSurfacingLog(ownValue(value, "surfacing")) : [];
293
+ const source = clone(value) as unknown as ActiveContextState;
294
+ // Provenance normalization is presentation-only; never mutate a durable
295
+ // content-addressed fold record: changing its bytes causes the next re-persist
296
+ // to report a conflicting durable fold and suspend automatic management.
297
+ return {
298
+ version: 1,
299
+ sessionId: source.sessionId,
300
+ revision: source.revision,
301
+ folds: validatedFolds,
302
+ expanded: clone(source.expanded),
303
+ protected: clone(source.protected),
304
+ tokensSinceToolFold: hasTokensSinceToolFold
305
+ ? Number(ownValue(value, "tokensSinceToolFold"))
306
+ : 0,
307
+ leases,
308
+ // Omitted when empty so every state written before surfacing existed keeps its
309
+ // exact digest; the replay digest is order- and presence-sensitive by design.
310
+ ...(surfacing.length ? { surfacing } : {}),
311
+ ...(hasAdvisory
312
+ ? { advisory: clone(source.advisory!) }
313
+ : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
314
+ ...(hasPrepared ? { prepared: clone(source.prepared!) } : {}),
315
+ };
316
+ }
317
+
318
+ export function clearPrepared(state: ActiveContextState): ActiveContextState {
319
+ const next = { ...state };
320
+ delete next.prepared;
321
+ return next;
322
+ }
323
+
324
+ export function validatePreparedShape(value: unknown): asserts value is PreparedFold {
325
+ if (!exactRecord(value, PREPARED_FOLD_KEYS)) throw new Error("Invalid prepared active-context fold keys");
326
+ const prepared = value as unknown as PreparedFold;
327
+ const sourceRefs = denseOwnArrayValues(ownValue(value, "sourceRefs"));
328
+ const beforeRefs = denseOwnArrayValues(ownValue(value, "beforeRefs"));
329
+ const afterRefs = denseOwnArrayValues(ownValue(value, "afterRefs"));
330
+ if (typeof prepared.id !== "string" || !prepared.id || !validFoldShape(prepared.fold) ||
331
+ prepared.id !== prepared.fold.id || prepared.fold.parentId !== null || !prepared.sessionId ||
332
+ !Number.isSafeInteger(prepared.generation) || prepared.generation < 0 ||
333
+ ![prepared.branchSha256, prepared.topologySha256, prepared.protectionSha256, prepared.sourceSha256,
334
+ prepared.beforeSha256, prepared.afterSha256].every((digest) => /^[a-f0-9]{64}$/.test(digest)) ||
335
+ !sourceRefs?.length || !beforeRefs || !afterRefs ||
336
+ [...sourceRefs, ...beforeRefs, ...afterRefs].some((ref) => !isObjectRef(ref)) ||
337
+ prepared.fold.id !== foldIdFor(prepared.fold.kind, prepared.fold.parts) ||
338
+ prepared.fold.sourceSha256 !== sha256Value(sourceRefs)) {
339
+ throw new Error("Invalid prepared active-context fold");
340
+ }
341
+ }
342
+
343
+ export function canonicalFoldRecord(fold: ActiveFold): ActiveFold {
344
+ return { ...clone(fold), parentId: null };
345
+ }
346
+
347
+ export function sameFoldRecordIdentity(left: ActiveFold, right: ActiveFold): boolean {
348
+ return sha256Value({ ...canonicalFoldRecord(left), createdAt: 0 }) ===
349
+ sha256Value({ ...canonicalFoldRecord(right), createdAt: 0 });
350
+ }
351
+
352
+ export function foldRecordRef(fold: ActiveFold): FoldRecordRef {
353
+ const canonical = canonicalFoldRecord(fold);
354
+ return { id: canonical.id, sha256: sha256Value(canonical) };
355
+ }
356
+
357
+ export function semanticStateSha256(state: ActiveContextState): string {
358
+ return sha256Value(state);
359
+ }
360
+
361
+ export function legacyCadenceOmittedStateSha256(state: ActiveContextState): string {
362
+ const legacy = clone(state) as Partial<ActiveContextState>;
363
+ if (legacy.tokensSinceToolFold === 0) delete legacy.tokensSinceToolFold;
364
+ if (legacy.leases && Object.keys(legacy.leases).length === 0) delete legacy.leases;
365
+ return sha256Value(legacy);
366
+ }
367
+
368
+ export function legacyReplayOrderStateSha256(state: ActiveContextState): string {
369
+ return sha256Value({
370
+ version: state.version,
371
+ sessionId: state.sessionId,
372
+ revision: state.revision,
373
+ folds: state.folds,
374
+ expanded: state.expanded,
375
+ protected: state.protected,
376
+ ...(state.prepared ? { prepared: state.prepared } : {}),
377
+ ...(state.advisory ? { advisory: state.advisory } : {}),
378
+ });
379
+ }
380
+
381
+ export function sameStateProjection(left: ActiveContextState, right: ActiveContextState): boolean {
382
+ const normalized = (value: ActiveContextState): Partial<ActiveContextState> => {
383
+ const normalizedState = { ...clone(value), revision: 0 } as Partial<ActiveContextState>;
384
+ if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
385
+ if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
386
+ if (normalizedState.surfacing && normalizedState.surfacing.length === 0) delete normalizedState.surfacing;
387
+ return normalizedState;
388
+ };
389
+ return stableStringify(normalized(left)) === stableStringify(normalized(right));
390
+ }
391
+
392
+ export function parseFoldRecordRef(value: unknown): FoldRecordRef {
393
+ if (!exactRecord(value, FOLD_RECORD_REF_KEYS) ||
394
+ typeof ownValue(value, "id") !== "string" || !ownValue(value, "id") ||
395
+ typeof ownValue(value, "sha256") !== "string" ||
396
+ !/^[a-f0-9]{64}$/.test(String(ownValue(value, "sha256")))) {
397
+ throw new Error("Invalid active-context fold record reference");
398
+ }
399
+ return clone(value) as unknown as FoldRecordRef;
400
+ }
401
+
402
+ export function parseFoldRecordEntry(value: unknown, sessionId: string): FoldRecordEntry {
403
+ if (!exactRecord(value, FOLD_RECORD_ENTRY_KEYS) || ownValue(value, "version") !== 1 ||
404
+ ownValue(value, "sessionId") !== sessionId || typeof ownValue(value, "foldId") !== "string" ||
405
+ typeof ownValue(value, "recordSha256") !== "string") {
406
+ throw new Error("Invalid active-context fold record");
407
+ }
408
+ const foldValue = ownValue(value, "fold");
409
+ if (!validFoldShape(foldValue)) throw new Error("Invalid active-context fold record payload");
410
+ const fold = clone(foldValue);
411
+ if (fold.parentId !== null || fold.parts.length > MAX_ACTIVE_FOLD_PARTS || fold.id !== ownValue(value, "foldId") ||
412
+ fold.id !== foldIdFor(fold.kind, fold.parts) || sha256Value(fold) !== ownValue(value, "recordSha256")) {
413
+ throw new Error("Active-context fold record identity drift");
414
+ }
415
+ const record = clone(value) as unknown as FoldRecordEntry;
416
+ if (bytes(record) > MAX_ACTIVE_FOLD_RECORD_BYTES) throw new Error("Active-context fold record exceeds wire limit");
417
+ return record;
418
+ }
419
+
420
+ export function validateV2ProjectionFields(
421
+ expandedValue: unknown,
422
+ protectedValue: unknown,
423
+ preparedValue: unknown,
424
+ advisoryValue: unknown,
425
+ tokensSinceToolFoldValue: unknown,
426
+ leasesValue: unknown,
427
+ surfacingValue?: unknown,
428
+ ): {
429
+ expanded: string[];
430
+ protected: EvidenceRef[];
431
+ prepared?: PreparedFold;
432
+ advisory?: NonNullable<ActiveContextState["advisory"]>;
433
+ tokensSinceToolFold: number;
434
+ leases: Record<string, number>;
435
+ surfacing: SurfacingRecord[];
436
+ } {
437
+ const expanded = denseOwnArrayValues(expandedValue);
438
+ const protectedRefs = denseOwnArrayValues(protectedValue);
439
+ if (!expanded || expanded.length > MAX_ACTIVE_EXPANDED || expanded.some((id) => typeof id !== "string" || !id) ||
440
+ new Set(expanded).size !== expanded.length || !protectedRefs || protectedRefs.length > MAX_ACTIVE_PROTECTED ||
441
+ protectedRefs.some((ref) => !isObjectRef(ref)) ||
442
+ new Set((protectedRefs as EvidenceRef[]).map(objectRefKey)).size !== protectedRefs.length) {
443
+ throw new Error("Invalid active-context v2 projection fields");
444
+ }
445
+ if (preparedValue !== null) validatePreparedShape(preparedValue);
446
+ if (advisoryValue !== undefined && !validAdvisoryState(advisoryValue)) {
447
+ throw new Error("Invalid active-context v2 advisory state");
448
+ }
449
+ if (tokensSinceToolFoldValue !== undefined && !validTokensSinceToolFold(tokensSinceToolFoldValue)) {
450
+ throw new Error("Invalid active-context v2 tool-fold cadence");
451
+ }
452
+ const leases = leasesValue === undefined ? {} : parseLeases(leasesValue);
453
+ const surfacing = surfacingValue === undefined ? [] : parseSurfacingLog(surfacingValue);
454
+ return {
455
+ expanded: clone(expanded) as string[],
456
+ protected: clone(protectedRefs) as EvidenceRef[],
457
+ ...(preparedValue === null ? {} : { prepared: clone(preparedValue) as PreparedFold }),
458
+ ...(advisoryValue === undefined ? {} : {
459
+ advisory: clone(advisoryValue) as NonNullable<ActiveContextState["advisory"]>,
460
+ }),
461
+ tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
462
+ leases,
463
+ surfacing,
464
+ };
465
+ }
466
+
467
+ export function parseActiveContextStateV2(value: unknown, sessionId: string): ActiveContextStateWireV2 {
468
+ const kind = ownValue(value, "kind");
469
+ const hasAdvisory = Boolean(value && typeof value === "object" &&
470
+ Object.prototype.hasOwnProperty.call(value, "advisory"));
471
+ const hasTokensSinceToolFold = Boolean(value && typeof value === "object" &&
472
+ Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold"));
473
+ const hasLeases = Boolean(value && typeof value === "object" &&
474
+ Object.prototype.hasOwnProperty.call(value, "leases"));
475
+ const hasSurfacing = Boolean(value && typeof value === "object" &&
476
+ Object.prototype.hasOwnProperty.call(value, "surfacing"));
477
+ const optionalKeys = [
478
+ ...(hasAdvisory ? ["advisory"] : []),
479
+ ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
480
+ ...(hasLeases ? ["leases"] : []),
481
+ ...(hasSurfacing ? ["surfacing"] : []),
482
+ ];
483
+ const checkpoint = kind === "checkpoint" &&
484
+ exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
485
+ const delta = kind === "delta" &&
486
+ exactRecord(value, [...STATE_DELTA_V2_KEYS, ...optionalKeys]);
487
+ if ((!checkpoint && !delta) || ownValue(value, "version") !== 2 || ownValue(value, "sessionId") !== sessionId ||
488
+ !Number.isSafeInteger(ownValue(value, "revision")) || Number(ownValue(value, "revision")) < 0 ||
489
+ typeof ownValue(value, "stateSha256") !== "string" ||
490
+ !/^[a-f0-9]{64}$/.test(String(ownValue(value, "stateSha256")))) {
491
+ throw new Error("Invalid active-context v2 state");
492
+ }
493
+ validateV2ProjectionFields(
494
+ ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
495
+ ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
496
+ ownValue(value, "surfacing"),
497
+ );
498
+ if (checkpoint) {
499
+ const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
500
+ if (!refs || refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Invalid active-context checkpoint refs");
501
+ const parsed = refs.map(parseFoldRecordRef);
502
+ if (new Set(parsed.map((ref) => ref.id)).size !== parsed.length) {
503
+ throw new Error("Duplicate active-context checkpoint ref");
504
+ }
505
+ } else {
506
+ if (!Number.isSafeInteger(ownValue(value, "baseRevision")) || Number(ownValue(value, "baseRevision")) < 0 ||
507
+ typeof ownValue(value, "baseStateSha256") !== "string" ||
508
+ !/^[a-f0-9]{64}$/.test(String(ownValue(value, "baseStateSha256")))) {
509
+ throw new Error("Invalid active-context delta base");
510
+ }
511
+ const addRefs = denseOwnArrayValues(ownValue(value, "addFoldRefs"));
512
+ const removeIds = denseOwnArrayValues(ownValue(value, "removeFoldIds"));
513
+ if (!addRefs || addRefs.length > MAX_ACTIVE_FOLD_RECORDS || !removeIds ||
514
+ removeIds.length > MAX_ACTIVE_FOLD_RECORDS || removeIds.some((id) => typeof id !== "string" || !id)) {
515
+ throw new Error("Invalid active-context delta changes");
516
+ }
517
+ const parsed = addRefs.map(parseFoldRecordRef);
518
+ if (new Set(parsed.map((ref) => ref.id)).size !== parsed.length || new Set(removeIds).size !== removeIds.length) {
519
+ throw new Error("Duplicate active-context delta change");
520
+ }
521
+ }
522
+ const parsed = clone(value) as unknown as ActiveContextStateWireV2;
523
+ if (bytes(parsed) > MAX_ACTIVE_STATE_EVENT_BYTES) throw new Error("Active-context state event exceeds wire limit");
524
+ return parsed;
525
+ }
526
+
527
+ export function normalizeFoldsForPersistedRecords(
528
+ folds: ActiveFold[],
529
+ records: Map<string, FoldRecordEntry>,
530
+ ): ActiveFold[] {
531
+ const normalized = folds.map((fold) => {
532
+ const record = records.get(fold.id);
533
+ if (!record) return clone(fold);
534
+ if (!sameFoldRecordIdentity(fold, record.fold)) {
535
+ throw new Error(`Conflicting durable active-context fold ${fold.id}`);
536
+ }
537
+ return { ...clone(record.fold), parentId: fold.parentId };
538
+ });
539
+ return deriveFoldParents(normalized);
540
+ }
541
+
542
+ export function deriveFoldParents(records: ActiveFold[]): ActiveFold[] {
543
+ const folds = records.map(canonicalFoldRecord);
544
+ const byId = new Map(folds.map((fold) => [fold.id, fold]));
545
+ const assigned = new Set<string>();
546
+ for (const parent of folds) {
547
+ for (const childId of childFoldIds(parent)) {
548
+ const child = byId.get(childId);
549
+ if (!child || assigned.has(childId)) throw new Error(`Invalid active-context child record ${childId}`);
550
+ child.parentId = parent.id;
551
+ assigned.add(childId);
552
+ }
553
+ }
554
+ return validateFoldForest(folds);
555
+ }
556
+
557
+ export function stateFromFoldRefs(
558
+ wire: Pick<
559
+ ActiveContextCheckpointV2,
560
+ "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
561
+ "tokensSinceToolFold" | "leases" | "surfacing"
562
+ >,
563
+ refs: FoldRecordRef[],
564
+ records: Map<string, FoldRecordEntry>,
565
+ ): ActiveContextState {
566
+ if (refs.length > MAX_ACTIVE_FOLD_RECORDS) throw new Error("Active-context fold closure exceeds limit");
567
+ const folds = refs.map((ref) => {
568
+ const record = records.get(ref.id);
569
+ if (!record || record.recordSha256 !== ref.sha256) throw new Error(`Missing active-context fold record ${ref.id}`);
570
+ return record.fold;
571
+ });
572
+ const state: ActiveContextState = {
573
+ version: 1,
574
+ sessionId: wire.sessionId,
575
+ revision: wire.revision,
576
+ folds: deriveFoldParents(folds),
577
+ expanded: clone(wire.expanded),
578
+ protected: clone(wire.protected),
579
+ tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
580
+ leases: clone(wire.leases ?? {}),
581
+ ...(wire.surfacing?.length ? { surfacing: clone(wire.surfacing) } : {}),
582
+ ...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
583
+ ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
584
+ };
585
+ return parseActiveContextState(state, wire.sessionId, false);
586
+ }
587
+
588
+ export interface MaterializedStatePersistence {
589
+ state: ActiveContextState;
590
+ wireVersion: 0 | 1 | 2;
591
+ records: Map<string, FoldRecordEntry>;
592
+ stateSha256: string;
593
+ projectionFingerprints: Map<number, { topologySha256: string; protectionSha256: string }>;
594
+ }
595
+
596
+ export function materializeStatePersistence(
597
+ entries: any[],
598
+ sessionId: string,
599
+ stateEntryType = ACTIVE_CONTEXT_STATE_ENTRY,
600
+ foldRecordEntryType = ACTIVE_CONTEXT_FOLD_RECORD_ENTRY,
601
+ ): MaterializedStatePersistence {
602
+ let state = emptyActiveContextState(sessionId);
603
+ let wireVersion: 0 | 1 | 2 = 0;
604
+ const records = new Map<string, FoldRecordEntry>();
605
+ const projectionFingerprints = new Map<number, { topologySha256: string; protectionSha256: string }>();
606
+ let stateSha256 = semanticStateSha256(state);
607
+ let stateStart = -1;
608
+ let checkpointIndex = -1;
609
+ let v2Seen = false;
610
+ const rememberProjection = (): void => {
611
+ projectionFingerprints.set(state.revision, {
612
+ topologySha256: topologySha256(state),
613
+ protectionSha256: protectionSha256(state),
614
+ });
615
+ };
616
+ rememberProjection();
617
+ for (let index = 0; index < entries.length; index += 1) {
618
+ const entry = entries[index];
619
+ if (entry?.type !== "custom" || entry.customType !== stateEntryType) continue;
620
+ const version = ownValue(entry.data, "version");
621
+ if (version === 1) {
622
+ if (v2Seen) throw new Error("Active-context v1 state follows v2 state");
623
+ stateStart = index;
624
+ continue;
625
+ }
626
+ if (version !== 2) throw new Error("Invalid active-context persisted state version");
627
+ const kind = ownValue(entry.data, "kind");
628
+ if (kind === "checkpoint") {
629
+ if (v2Seen || checkpointIndex >= 0) throw new Error("Duplicate active-context v2 checkpoint");
630
+ checkpointIndex = index;
631
+ stateStart = index;
632
+ } else if (checkpointIndex < 0) {
633
+ throw new Error("Active-context v2 delta precedes migration checkpoint");
634
+ }
635
+ v2Seen = true;
636
+ }
637
+ for (let index = 0; index < entries.length; index += 1) {
638
+ const entry = entries[index];
639
+ if (entry?.type !== "custom") continue;
640
+ if (entry.customType === foldRecordEntryType) {
641
+ const record = parseFoldRecordEntry(entry.data, sessionId);
642
+ const previous = records.get(record.foldId);
643
+ if (previous && previous.recordSha256 !== record.recordSha256) {
644
+ throw new Error(`Conflicting active-context fold record ${record.foldId}`);
645
+ }
646
+ records.set(record.foldId, record);
647
+ continue;
648
+ }
649
+ if (entry.customType !== stateEntryType || index < stateStart) continue;
650
+ if (ownValue(entry.data, "version") === 1) {
651
+ if (wireVersion === 2) throw new Error("Active-context v1 state follows v2 state");
652
+ state = parseActiveContextState(entry.data, sessionId);
653
+ wireVersion = 1;
654
+ stateSha256 = semanticStateSha256(state);
655
+ rememberProjection();
656
+ continue;
657
+ }
658
+ const wire = parseActiveContextStateV2(entry.data, sessionId);
659
+ if (wire.kind === "checkpoint") {
660
+ state = stateFromFoldRefs(wire, wire.foldRefs, records);
661
+ } else {
662
+ if (wireVersion === 0 || wire.baseRevision !== state.revision || wire.baseStateSha256 !== stateSha256 ||
663
+ wire.revision <= wire.baseRevision) {
664
+ throw new Error("Broken active-context delta chain");
665
+ }
666
+ const refs = state.folds.map(foldRecordRef);
667
+ const byId = new Map(refs.map((ref) => [ref.id, ref]));
668
+ for (const id of wire.removeFoldIds) {
669
+ if (!byId.delete(id)) throw new Error(`Unknown active-context delta removal ${id}`);
670
+ }
671
+ for (const ref of wire.addFoldRefs) {
672
+ const existing = byId.get(ref.id);
673
+ if (existing && existing.sha256 !== ref.sha256) throw new Error(`Conflicting active-context delta ref ${ref.id}`);
674
+ if (existing) throw new Error(`Duplicate active-context delta addition ${ref.id}`);
675
+ byId.set(ref.id, ref);
676
+ }
677
+ state = stateFromFoldRefs(wire, [...byId.values()], records);
678
+ }
679
+ const calculated = semanticStateSha256(state);
680
+ // Compatibility window: accept the current digest plus two legacy shapes: one
681
+ // omitted zero cadence/empty leases; the other used a different replay-property
682
+ // order. These shims keep already-written v2 events readable and can be removed
683
+ // at the next wire-version bump.
684
+ if (calculated !== wire.stateSha256 &&
685
+ legacyCadenceOmittedStateSha256(state) !== wire.stateSha256 &&
686
+ legacyReplayOrderStateSha256(state) !== wire.stateSha256) {
687
+ throw new Error("Active-context v2 state digest drift");
688
+ }
689
+ wireVersion = 2;
690
+ stateSha256 = calculated;
691
+ rememberProjection();
692
+ }
693
+ return { state, wireVersion, records, stateSha256, projectionFingerprints };
694
+ }
695
+
696
+ export function materializeActiveContextState(
697
+ entries: any[],
698
+ sessionId: string,
699
+ stateEntryType = ACTIVE_CONTEXT_STATE_ENTRY,
700
+ foldRecordEntryType = ACTIVE_CONTEXT_FOLD_RECORD_ENTRY,
701
+ ): ActiveContextState {
702
+ return parseActiveContextState(
703
+ materializeStatePersistence(entries, sessionId, stateEntryType, foldRecordEntryType).state,
704
+ sessionId,
705
+ );
706
+ }
707
+
708
+ export function makeFoldRecordEntry(fold: ActiveFold, sessionId: string): FoldRecordEntry {
709
+ const canonical = canonicalFoldRecord(fold);
710
+ return parseFoldRecordEntry({
711
+ version: 1,
712
+ sessionId,
713
+ foldId: canonical.id,
714
+ recordSha256: sha256Value(canonical),
715
+ fold: canonical,
716
+ }, sessionId);
717
+ }
718
+
719
+ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextCheckpointV2 {
720
+ return parseActiveContextStateV2({
721
+ version: 2,
722
+ kind: "checkpoint",
723
+ sessionId: state.sessionId,
724
+ revision: state.revision,
725
+ foldRefs: state.folds.map(foldRecordRef),
726
+ expanded: clone(state.expanded),
727
+ protected: clone(state.protected),
728
+ prepared: state.prepared ? clone(state.prepared) : null,
729
+ tokensSinceToolFold: state.tokensSinceToolFold,
730
+ leases: clone(state.leases),
731
+ ...(state.surfacing?.length ? { surfacing: clone(state.surfacing) } : {}),
732
+ ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
733
+ stateSha256: semanticStateSha256(state),
734
+ }, state.sessionId) as ActiveContextCheckpointV2;
735
+ }
736
+
737
+ export function makeStateDelta(previous: ActiveContextState, next: ActiveContextState): ActiveContextDeltaV2 {
738
+ const previousRefs = new Map(previous.folds.map((fold) => {
739
+ const ref = foldRecordRef(fold);
740
+ return [ref.id, ref] as const;
741
+ }));
742
+ const nextRefs = new Map(next.folds.map((fold) => {
743
+ const ref = foldRecordRef(fold);
744
+ return [ref.id, ref] as const;
745
+ }));
746
+ for (const [id, ref] of nextRefs) {
747
+ const previousRef = previousRefs.get(id);
748
+ if (previousRef && previousRef.sha256 !== ref.sha256) {
749
+ throw new Error(`Active-context fold record changed for ${id}`);
750
+ }
751
+ }
752
+ return parseActiveContextStateV2({
753
+ version: 2,
754
+ kind: "delta",
755
+ sessionId: next.sessionId,
756
+ revision: next.revision,
757
+ baseRevision: previous.revision,
758
+ baseStateSha256: semanticStateSha256(previous),
759
+ addFoldRefs: [...nextRefs.values()].filter((ref) => !previousRefs.has(ref.id)),
760
+ removeFoldIds: [...previousRefs.keys()].filter((id) => !nextRefs.has(id)),
761
+ expanded: clone(next.expanded),
762
+ protected: clone(next.protected),
763
+ prepared: next.prepared ? clone(next.prepared) : null,
764
+ tokensSinceToolFold: next.tokensSinceToolFold,
765
+ leases: clone(next.leases),
766
+ ...(next.surfacing?.length ? { surfacing: clone(next.surfacing) } : {}),
767
+ ...(next.advisory ? { advisory: clone(next.advisory) } : {}),
768
+ stateSha256: semanticStateSha256(next),
769
+ }, next.sessionId) as ActiveContextDeltaV2;
770
+ }
771
+
772
+ export function topologySha256(state: Pick<ActiveContextState, "folds" | "expanded">): string {
773
+ return sha256Value({ folds: state.folds, expanded: [...state.expanded].sort() });
774
+ }
775
+
776
+ export function protectionSha256(state: Pick<ActiveContextState, "protected">): string {
777
+ return sha256Value([...state.protected].sort((left, right) => objectRefKey(left).localeCompare(objectRefKey(right))));
778
+ }
779
+
780
+ export const ADVISORY_BUDGETS: Readonly<Record<AdvisoryMilestone, number>> = Object.freeze({
781
+ orientation: 1,
782
+ notice: 2,
783
+ tools: 2,
784
+ chapters: 2,
785
+ urgent: 3,
786
+ });
787
+
788
+ export const ADVISORY_MILESTONES = Object.freeze(
789
+ Object.keys(ADVISORY_BUDGETS) as AdvisoryMilestone[],
790
+ );
791
+
792
+ export function validAdvisoryState(value: unknown): value is NonNullable<ActiveContextState["advisory"]> {
793
+ const hasArmed = Boolean(value && typeof value === "object" &&
794
+ Object.prototype.hasOwnProperty.call(value, "armed"));
795
+ if (!exactRecord(value, ["highWater", "delivered", ...(hasArmed ? ["armed"] : [])]) ||
796
+ typeof ownValue(value, "highWater") !== "number" ||
797
+ !Number.isFinite(ownValue(value, "highWater")) || Number(ownValue(value, "highWater")) < 0 ||
798
+ Number(ownValue(value, "highWater")) > 1 || !isPlainRecord(ownValue(value, "delivered"))) return false;
799
+ if (hasArmed) {
800
+ const armed = ownValue(value, "armed");
801
+ if (!exactRecord(armed, ["milestone", "threshold", "scheduleKey"]) ||
802
+ !ADVISORY_MILESTONES.includes(ownValue(armed, "milestone") as AdvisoryMilestone) ||
803
+ typeof ownValue(armed, "threshold") !== "number" ||
804
+ !Number.isFinite(ownValue(armed, "threshold")) || Number(ownValue(armed, "threshold")) < 0 ||
805
+ Number(ownValue(armed, "threshold")) > 1 ||
806
+ typeof ownValue(armed, "scheduleKey") !== "string" ||
807
+ !/^[a-f0-9]{64}$/.test(String(ownValue(armed, "scheduleKey")))) return false;
808
+ }
809
+ const delivered = ownValue(value, "delivered") as Record<string, unknown>;
810
+ return Reflect.ownKeys(delivered).every((key) => typeof key === "string" &&
811
+ ADVISORY_MILESTONES.includes(key as AdvisoryMilestone) &&
812
+ Number.isSafeInteger(ownValue(delivered, key)) && Number(ownValue(delivered, key)) >= 0 &&
813
+ Number(ownValue(delivered, key)) <= MAX_ADVISORY_DELIVERIES_PER_MILESTONE);
814
+ }