pi-fold 1.0.4 → 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.
@@ -20,13 +20,8 @@ import {
20
20
  ACTIVE_CONTEXT_POLICY,
21
21
  ACTIVE_CONTEXT_STATE_ENTRY,
22
22
  EXPAND_LEASE_GENERATIONS,
23
- MAX_ADVISORY_DELIVERIES_PER_MILESTONE,
24
23
  MAX_EXPAND_LEASES,
25
- MAX_LAST_CALL_TEXT_BYTES,
26
24
  MAX_PENDING_MARKS,
27
- MAX_THRESHOLD_NOTICE_TEXT_BYTES,
28
- MAX_THRESHOLD_NOTICES,
29
- SURFACING_MAX_LEDGER_RECORDS,
30
25
  } from "./policy.ts";
31
26
  import type {
32
27
  ActiveContextCheckpointV2,
@@ -44,7 +39,6 @@ import type {
44
39
  MarkOrigin,
45
40
  PendingMark,
46
41
  PreparedFold,
47
- SurfacingRecord,
48
42
  } from "./policy.ts";
49
43
 
50
44
  export const ACTIVE_FOLD_KEYS = [
@@ -58,13 +52,11 @@ export const PREPARED_FOLD_KEYS = [
58
52
  export const ACTIVE_STATE_KEYS = ["version", "sessionId", "revision", "folds", "expanded", "protected"] as const;
59
53
  export const FOLD_RECORD_REF_KEYS = ["id", "sha256"] as const;
60
54
  export const FOLD_RECORD_ENTRY_KEYS = ["version", "sessionId", "foldId", "recordSha256", "fold"] as const;
61
- export const SURFACING_RECORD_KEYS = ["id", "surfaced", "taken", "ordinal", "outcome"] as const;
62
- export const SURFACING_OUTCOMES = Object.freeze(["shown", "acted", "used", "ignored"] as const);
63
55
  export const PENDING_FOLD_MARK_KEYS = [
64
56
  "mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
65
57
  ] as const;
66
58
  export const PENDING_REFOLD_MARK_KEYS = ["mark", "id", "origin", "ordinal"] as const;
67
- export const MARK_ORIGINS = Object.freeze(["agent", "ladder"] as const);
59
+ export const MARK_ORIGINS = Object.freeze(["agent", "ladder", "user"] as const);
68
60
  export const STATE_CHECKPOINT_V2_KEYS = [
69
61
  "version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
70
62
  ] as const;
@@ -83,28 +75,6 @@ export function validTokensSinceToolFold(value: unknown): value is number {
83
75
  return Number.isSafeInteger(value) && Number(value) >= 0;
84
76
  }
85
77
 
86
- export function validSurfacingRecord(value: unknown): value is SurfacingRecord {
87
- if (!exactRecord(value, SURFACING_RECORD_KEYS)) return false;
88
- const surfaced = ownValue(value, "surfaced");
89
- const taken = ownValue(value, "taken");
90
- return typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
91
- Number.isSafeInteger(surfaced) && Number(surfaced) > 0 &&
92
- Number.isSafeInteger(taken) && Number(taken) >= 0 && Number(taken) <= Number(surfaced) &&
93
- Number.isSafeInteger(ownValue(value, "ordinal")) && Number(ownValue(value, "ordinal")) >= 0 &&
94
- SURFACING_OUTCOMES.includes(ownValue(value, "outcome") as SurfacingRecord["outcome"]);
95
- }
96
-
97
- /** One record per fold: a duplicate id is a ledger that forgot an ignore. */
98
- export function parseSurfacingLog(value: unknown): SurfacingRecord[] {
99
- const records = denseOwnArrayValues(value);
100
- if (!records || records.length > SURFACING_MAX_LEDGER_RECORDS || !records.every(validSurfacingRecord) ||
101
- new Set(records.map((record) => (record as SurfacingRecord).id)).size !== records.length) {
102
- throw new Error("Invalid active-context surfacing ledger");
103
- }
104
- return clone(records) as SurfacingRecord[];
105
- }
106
-
107
- /** Stable identity of one pending mark: its phase plus the fold it names. */
108
78
  export function pendingMarkKey(mark: Pick<PendingMark, "mark" | "id">): string {
109
79
  return `${mark.mark}:${mark.id}`;
110
80
  }
@@ -121,23 +91,11 @@ export function validPendingMark(value: unknown): value is PendingMark {
121
91
  const kind = ownValue(value, "kind");
122
92
  const parts = denseOwnArrayValues(ownValue(value, "parts"));
123
93
  return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
124
- Boolean(parts?.length) && parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
125
- parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
94
+ Boolean(parts?.length) && parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
126
95
  validProvenance(ownValue(value, "briefProvenance")) &&
127
96
  id === foldIdFor(kind as FoldKind, parts as FoldPart[]);
128
97
  }
129
98
 
130
- /**
131
- * Agent-corrected fold briefs, keyed by fold id. Bounded by the same brief cap a
132
- * supplied brief obeys, and refused for a fold that is not in the forest, exactly
133
- * like a pin or a refold mark naming a missing fold.
134
- */
135
- /**
136
- * A model brief written after the fold committed carries its generator with it, so an
137
- * override is either the bare corrected string the agent wrote or that string beside the
138
- * attribution. Two shapes, one map: the string form is what every state written before
139
- * the upgrade lane existed holds, and it must keep parsing and keep its exact digest.
140
- */
141
99
  export function briefOverrideText(override: unknown): string | null {
142
100
  if (typeof override === "string") return override;
143
101
  const brief = ownValue(override, "brief");
@@ -169,7 +127,6 @@ export function parseBriefOverrides(
169
127
  ) as Record<string, BriefOverride>;
170
128
  }
171
129
 
172
- /** The brief a fold PRESENTS: the agent's correction when it made one, else the record's. */
173
130
  export function foldBrief(
174
131
  fold: Pick<ActiveFold, "id" | "brief">,
175
132
  state: Pick<ActiveContextState, "briefs">,
@@ -177,11 +134,6 @@ export function foldBrief(
177
134
  return briefOverrideText(state.briefs?.[fold.id]) ?? fold.brief;
178
135
  }
179
136
 
180
- /**
181
- * The provenance a fold PRESENTS. A fold RECORD is immutable, so a brief written after
182
- * the commit lives in the override map and its attribution travels with it; without this
183
- * lens a model-written upgrade would report the provenance of the brief it replaced.
184
- */
185
137
  export function foldProvenance(
186
138
  fold: Pick<ActiveFold, "id" | "provenance">,
187
139
  state: Pick<ActiveContextState, "briefs">,
@@ -218,7 +170,6 @@ export function validProvenance(value: unknown): value is BriefProvenance {
218
170
  (ownValue(value, "kind") === "supplied" || ownValue(value, "kind") === "deterministic")) return true;
219
171
  const hasDigest = Boolean(value && typeof value === "object" &&
220
172
  Object.prototype.hasOwnProperty.call(value, "launchContractDigest"));
221
- // legacy provenance kind written by pre-release sessions; normalized to the model kind at presentation
222
173
  if (!exactRecord(value, ["kind", "provider", "model", "effort", ...(hasDigest ? ["launchContractDigest"] : [])]) ||
223
174
  (ownValue(value, "kind") !== "model" && ownValue(value, "kind") !== "luna") ||
224
175
  typeof ownValue(value, "provider") !== "string" || !ownValue(value, "provider") ||
@@ -258,7 +209,6 @@ export function validFoldShape(value: unknown): value is ActiveFold {
258
209
  return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
259
210
  typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
260
211
  (parentId === null || typeof parentId === "string") && Boolean(parts?.length) &&
261
- parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
262
212
  parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
263
213
  validProvenance(ownValue(value, "provenance")) && typeof sourceSha256 === "string" &&
264
214
  /^[a-f0-9]{64}$/.test(sourceSha256) && Number.isSafeInteger(ownValue(value, "sourceChars")) &&
@@ -281,11 +231,6 @@ export function foldMap(state: Pick<ActiveContextState, "folds">): Map<string, A
281
231
  return new Map(state.folds.map((fold) => [fold.id, fold]));
282
232
  }
283
233
 
284
- /**
285
- * The folds a span names as children. Read off the parts alone, so a PENDING fold mark
286
- * answers it too: a mark carries the id its committed fold will get, so the ids here are
287
- * exactly the folds that must already exist when this span applies.
288
- */
289
234
  export function childFoldIds(fold: Pick<ActiveFold, "parts">): string[] {
290
235
  return fold.parts.filter((part): part is Extract<FoldPart, { kind: "fold" }> => part.kind === "fold")
291
236
  .map((part) => part.foldId);
@@ -309,20 +254,6 @@ export function flattenFoldRefs(fold: ActiveFold, state: Pick<ActiveContextState
309
254
  return visit(fold);
310
255
  }
311
256
 
312
- /**
313
- * Which fold DIRECTLY owns each raw evidence key.
314
- *
315
- * One derivation, two readers (Shane 2026-08-11). The forest invariant below reads it to
316
- * reject a second owner, and the agent-facing candidate builder reads it BEFORE it builds
317
- * anything, so a span naming evidence some fold already holds comes back as a refusal
318
- * that names the owner instead of this invariant escaping the tool call as an exception.
319
- *
320
- * The two readings stay distinguishable because the input differs. Called on the folds
321
- * already in state it can only throw when the stored forest is itself corrupt, which no
322
- * agent input can produce: every state reaches storage through `validateFoldForest`.
323
- * Called on a forest that includes a candidate the caller just built, a duplicate means
324
- * the REQUEST collided with an existing fold, and that is the refusal case.
325
- */
326
257
  export function directFoldOwners(folds: readonly ActiveFold[]): Map<string, string> {
327
258
  const owner = new Map<string, string>();
328
259
  for (const fold of folds) {
@@ -351,18 +282,6 @@ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
351
282
  (!fold.parts.length || fold.parts.some((part) => part.kind !== "raw" || part.ref.role !== "toolResult"))) {
352
283
  throw new Error("Tool-result fold must own one validated assistant batch of tool results");
353
284
  }
354
- // WHAT A PARENT IS, RE-DERIVED UNDER THE COUNT LAW (Shane 2026-08-10).
355
- //
356
- // Two shape rules stood here and both described the old approximation. "Only child
357
- // folds" forbade the raw gaps between children, and a parent's span runs from its
358
- // first child to its last one, so the gap material inside it is the parent's or the
359
- // span is not a span. "Only chapter or consolidation children" forbade tool-result
360
- // children, which was true while a bare tool batch nested through a chapter span;
361
- // automatic chapters no longer take placeholders at any count, so that rule would
362
- // now leave every tool fold as an obstacle no parent could ever reclaim.
363
- //
364
- // What survives is the one thing that makes a consolidation a consolidation: it
365
- // groups folds. A parent with no child fold is a chapter wearing the wrong name.
366
285
  if (fold.kind === "consolidation" && !fold.parts.some((part) => part.kind === "fold")) {
367
286
  throw new Error("Consolidation folds must contain at least one child fold");
368
287
  }
@@ -401,29 +320,22 @@ export function parseActiveContextState(
401
320
  const hasTokensSinceToolFold = recordLike &&
402
321
  Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold");
403
322
  const hasLeases = recordLike && Object.prototype.hasOwnProperty.call(value, "leases");
404
- const hasSurfacing = recordLike && Object.prototype.hasOwnProperty.call(value, "surfacing");
405
323
  const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
406
- // Accepted, never read, never written. `pinnedPeeks` pinned a peek result against
407
- // per-projection reclamation, which was deleted after rep 22 measured it rewriting
408
- // the prefix mid-window. State written before that cut still carries the key, and
409
- // rejecting it here would suspend automatic management on upgrade.
410
324
  const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
411
325
  const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
412
- const hasRider = recordLike && Object.prototype.hasOwnProperty.call(value, "rider");
413
- const hasLastCall = recordLike && Object.prototype.hasOwnProperty.call(value, "lastCall");
414
- const hasNotices = recordLike && Object.prototype.hasOwnProperty.call(value, "notices");
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
+ refuseRetiredStateFields(value);
415
330
  const extraKeys = [
416
331
  ...(hasPrepared ? ["prepared"] : []),
417
332
  ...(hasAdvisory ? ["advisory"] : []),
418
333
  ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
419
334
  ...(hasLeases ? ["leases"] : []),
420
- ...(hasSurfacing ? ["surfacing"] : []),
421
335
  ...(hasPendingMarks ? ["pendingMarks"] : []),
422
336
  ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
423
337
  ...(hasBriefs ? ["briefs"] : []),
424
- ...(hasRider ? ["rider"] : []),
425
- ...(hasLastCall ? ["lastCall"] : []),
426
- ...(hasNotices ? ["notices"] : []),
338
+ ...(hasLegacyRider ? ["rider"] : []),
427
339
  ];
428
340
  if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
429
341
  const folds = denseOwnArrayValues(ownValue(value, "folds"));
@@ -445,29 +357,16 @@ export function parseActiveContextState(
445
357
  if (hasAdvisory && !validAdvisoryState(ownValue(value, "advisory"))) {
446
358
  throw new Error("Invalid active-context advisory state");
447
359
  }
448
- if (hasRider && !validRiderState(ownValue(value, "rider"))) {
449
- throw new Error("Invalid active-context rider state");
450
- }
451
- if (hasLastCall && !validLastCallState(ownValue(value, "lastCall"))) {
452
- throw new Error("Invalid active-context last-call state");
453
- }
454
- if (hasNotices && !validNoticesState(ownValue(value, "notices"))) {
455
- throw new Error("Invalid active-context notices state");
456
- }
457
360
  if (hasTokensSinceToolFold && !validTokensSinceToolFold(ownValue(value, "tokensSinceToolFold"))) {
458
361
  throw new Error("Invalid active-context tool-fold cadence");
459
362
  }
460
363
  const leases = hasLeases ? parseLeases(ownValue(value, "leases"), ids) : {};
461
- const surfacing = hasSurfacing ? parseSurfacingLog(ownValue(value, "surfacing")) : [];
462
364
  const marks = hasPendingMarks ? parsePendingMarks(ownValue(value, "pendingMarks")) : [];
463
365
  if (marks.some((mark) => mark.mark === "refold" && !ids.has(mark.id))) {
464
366
  throw new Error("Invalid active-context pending marks");
465
367
  }
466
368
  const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
467
369
  const source = clone(value) as unknown as ActiveContextState;
468
- // Provenance normalization is presentation-only; never mutate a durable
469
- // content-addressed fold record: changing its bytes causes the next re-persist
470
- // to report a conflicting durable fold and suspend automatic management.
471
370
  return {
472
371
  version: 1,
473
372
  sessionId: source.sessionId,
@@ -479,17 +378,11 @@ export function parseActiveContextState(
479
378
  ? Number(ownValue(value, "tokensSinceToolFold"))
480
379
  : 0,
481
380
  leases,
482
- // Omitted when empty so every state written before surfacing existed keeps its
483
- // exact digest; the replay digest is order- and presence-sensitive by design.
484
- ...(surfacing.length ? { surfacing } : {}),
485
381
  ...(marks.length ? { pendingMarks: marks } : {}),
486
382
  ...(Object.keys(briefs).length ? { briefs } : {}),
487
383
  ...(hasAdvisory
488
384
  ? { advisory: clone(source.advisory!) }
489
385
  : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
490
- ...(hasRider ? { rider: clone(source.rider!) } : {}),
491
- ...(hasLastCall ? { lastCall: clone(source.lastCall!) } : {}),
492
- ...(hasNotices ? { notices: clone(source.notices!) } : {}),
493
386
  ...(hasPrepared ? { prepared: clone(source.prepared!) } : {}),
494
387
  };
495
388
  }
@@ -544,6 +437,29 @@ export function legacyCadenceOmittedStateSha256(state: ActiveContextState): stri
544
437
  return sha256Value(legacy);
545
438
  }
546
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
+
547
463
  export function legacyReplayOrderStateSha256(state: ActiveContextState): string {
548
464
  return sha256Value({
549
465
  version: state.version,
@@ -562,7 +478,6 @@ export function sameStateProjection(left: ActiveContextState, right: ActiveConte
562
478
  const normalizedState = { ...clone(value), revision: 0 } as Partial<ActiveContextState>;
563
479
  if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
564
480
  if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
565
- if (normalizedState.surfacing && normalizedState.surfacing.length === 0) delete normalizedState.surfacing;
566
481
  if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
567
482
  if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
568
483
  return normalizedState;
@@ -605,23 +520,15 @@ export function validateV2ProjectionFields(
605
520
  advisoryValue: unknown,
606
521
  tokensSinceToolFoldValue: unknown,
607
522
  leasesValue: unknown,
608
- surfacingValue?: unknown,
609
523
  pendingMarksValue?: unknown,
610
524
  briefsValue?: unknown,
611
- riderValue?: unknown,
612
- lastCallValue?: unknown,
613
- noticesValue?: unknown,
614
525
  ): {
615
526
  expanded: string[];
616
527
  protected: EvidenceRef[];
617
528
  prepared?: PreparedFold;
618
529
  advisory?: NonNullable<ActiveContextState["advisory"]>;
619
- rider?: NonNullable<ActiveContextState["rider"]>;
620
- lastCall?: NonNullable<ActiveContextState["lastCall"]>;
621
- notices?: NonNullable<ActiveContextState["notices"]>;
622
530
  tokensSinceToolFold: number;
623
531
  leases: Record<string, number>;
624
- surfacing: SurfacingRecord[];
625
532
  pendingMarks: PendingMark[];
626
533
  briefs: Record<string, string>;
627
534
  } {
@@ -637,20 +544,10 @@ export function validateV2ProjectionFields(
637
544
  if (advisoryValue !== undefined && !validAdvisoryState(advisoryValue)) {
638
545
  throw new Error("Invalid active-context v2 advisory state");
639
546
  }
640
- if (riderValue !== undefined && !validRiderState(riderValue)) {
641
- throw new Error("Invalid active-context v2 rider state");
642
- }
643
- if (lastCallValue !== undefined && !validLastCallState(lastCallValue)) {
644
- throw new Error("Invalid active-context v2 last-call state");
645
- }
646
- if (noticesValue !== undefined && !validNoticesState(noticesValue)) {
647
- throw new Error("Invalid active-context v2 notices state");
648
- }
649
547
  if (tokensSinceToolFoldValue !== undefined && !validTokensSinceToolFold(tokensSinceToolFoldValue)) {
650
548
  throw new Error("Invalid active-context v2 tool-fold cadence");
651
549
  }
652
550
  const leases = leasesValue === undefined ? {} : parseLeases(leasesValue);
653
- const surfacing = surfacingValue === undefined ? [] : parseSurfacingLog(surfacingValue);
654
551
  const pendingMarks = pendingMarksValue === undefined ? [] : parsePendingMarks(pendingMarksValue);
655
552
  const briefs = briefsValue === undefined ? {} : parseBriefOverrides(briefsValue);
656
553
  return {
@@ -660,18 +557,8 @@ export function validateV2ProjectionFields(
660
557
  ...(advisoryValue === undefined ? {} : {
661
558
  advisory: clone(advisoryValue) as NonNullable<ActiveContextState["advisory"]>,
662
559
  }),
663
- ...(riderValue === undefined ? {} : {
664
- rider: clone(riderValue) as NonNullable<ActiveContextState["rider"]>,
665
- }),
666
- ...(lastCallValue === undefined ? {} : {
667
- lastCall: clone(lastCallValue) as NonNullable<ActiveContextState["lastCall"]>,
668
- }),
669
- ...(noticesValue === undefined ? {} : {
670
- notices: clone(noticesValue) as NonNullable<ActiveContextState["notices"]>,
671
- }),
672
560
  tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
673
561
  leases,
674
- surfacing,
675
562
  pendingMarks,
676
563
  briefs,
677
564
  };
@@ -685,35 +572,37 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
685
572
  Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold"));
686
573
  const hasLeases = Boolean(value && typeof value === "object" &&
687
574
  Object.prototype.hasOwnProperty.call(value, "leases"));
688
- const hasSurfacing = Boolean(value && typeof value === "object" &&
689
- Object.prototype.hasOwnProperty.call(value, "surfacing"));
690
575
  const hasPendingMarks = Boolean(value && typeof value === "object" &&
691
576
  Object.prototype.hasOwnProperty.call(value, "pendingMarks"));
692
- // Accepted, never read, never written. `pinnedPeeks` pinned a peek result against
693
- // per-projection reclamation, which was deleted after rep 22 measured it rewriting
694
- // the prefix mid-window. State written before that cut still carries the key, and
695
- // rejecting it here would suspend automatic management on upgrade.
577
+ const hasAddPendingMarks = Boolean(value && typeof value === "object" &&
578
+ Object.prototype.hasOwnProperty.call(value, "addPendingMarks"));
579
+ const hasPendingMarkOrder = Boolean(value && typeof value === "object" &&
580
+ Object.prototype.hasOwnProperty.call(value, "pendingMarkOrder"));
696
581
  const hasPinnedPeeks = Boolean(value && typeof value === "object" &&
697
582
  Object.prototype.hasOwnProperty.call(value, "pinnedPeeks"));
698
583
  const hasBriefs = Boolean(value && typeof value === "object" &&
699
584
  Object.prototype.hasOwnProperty.call(value, "briefs"));
700
- const hasRider = Boolean(value && typeof value === "object" &&
585
+ const hasAddBriefs = Boolean(value && typeof value === "object" &&
586
+ Object.prototype.hasOwnProperty.call(value, "addBriefs"));
587
+ const hasRemoveBriefIds = Boolean(value && typeof value === "object" &&
588
+ Object.prototype.hasOwnProperty.call(value, "removeBriefIds"));
589
+ const hasLegacyRider = Boolean(value && typeof value === "object" &&
701
590
  Object.prototype.hasOwnProperty.call(value, "rider"));
702
- const hasLastCall = Boolean(value && typeof value === "object" &&
703
- Object.prototype.hasOwnProperty.call(value, "lastCall"));
704
- const hasNotices = Boolean(value && typeof value === "object" &&
705
- Object.prototype.hasOwnProperty.call(value, "notices"));
591
+ refuseRetiredStateFields(value);
706
592
  const optionalKeys = [
707
593
  ...(hasAdvisory ? ["advisory"] : []),
708
594
  ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
709
595
  ...(hasLeases ? ["leases"] : []),
710
- ...(hasSurfacing ? ["surfacing"] : []),
711
596
  ...(hasPendingMarks ? ["pendingMarks"] : []),
597
+ ...(hasAddPendingMarks ? ["addPendingMarks"] : []),
598
+ ...(hasPendingMarkOrder ? ["pendingMarkOrder"] : []),
712
599
  ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
713
600
  ...(hasBriefs ? ["briefs"] : []),
714
- ...(hasRider ? ["rider"] : []),
715
- ...(hasLastCall ? ["lastCall"] : []),
716
- ...(hasNotices ? ["notices"] : []),
601
+ ...(hasAddBriefs ? ["addBriefs"] : []),
602
+ ...(hasRemoveBriefIds ? ["removeBriefIds"] : []),
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"] : []),
717
606
  ];
718
607
  const checkpoint = kind === "checkpoint" &&
719
608
  exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
@@ -728,8 +617,7 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
728
617
  validateV2ProjectionFields(
729
618
  ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
730
619
  ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
731
- ownValue(value, "surfacing"), ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
732
- ownValue(value, "rider"), ownValue(value, "lastCall"), ownValue(value, "notices"),
620
+ ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
733
621
  );
734
622
  if (checkpoint) {
735
623
  const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
@@ -754,6 +642,36 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
754
642
  if (new Set(parsed.map((ref) => ref.id)).size !== parsed.length || new Set(removeIds).size !== removeIds.length) {
755
643
  throw new Error("Duplicate active-context delta change");
756
644
  }
645
+ if (hasAddBriefs) parseBriefOverrides(ownValue(value, "addBriefs"));
646
+ if (hasRemoveBriefIds) {
647
+ const removeBriefs = denseOwnArrayValues(ownValue(value, "removeBriefIds"));
648
+ if (!removeBriefs || removeBriefs.length > MAX_ACTIVE_FOLD_RECORDS ||
649
+ removeBriefs.some((id) => typeof id !== "string" || !id) ||
650
+ new Set(removeBriefs).size !== removeBriefs.length) {
651
+ throw new Error("Invalid active-context delta brief removals");
652
+ }
653
+ }
654
+ if (hasBriefs && (hasAddBriefs || hasRemoveBriefIds)) {
655
+ throw new Error("Active-context delta states the whole brief map and a change to it");
656
+ }
657
+ if (hasAddPendingMarks) parsePendingMarks(ownValue(value, "addPendingMarks"));
658
+ if (hasPendingMarkOrder) {
659
+ const order = denseOwnArrayValues(ownValue(value, "pendingMarkOrder"));
660
+ if (!order || order.length > MAX_PENDING_MARKS ||
661
+ order.some((key) => typeof key !== "string" || !key) ||
662
+ new Set(order).size !== order.length) {
663
+ throw new Error("Invalid active-context delta pending mark order");
664
+ }
665
+ }
666
+ // The order is what the replay is BUILT from, so additions without it name marks
667
+ // nothing will place. Refused rather than absorbed, because absorbing it would drop
668
+ // the marks silently and a dropped mark is evidence that never gets folded.
669
+ if (hasAddPendingMarks && !hasPendingMarkOrder) {
670
+ throw new Error("Active-context delta adds pending marks and states no order for them");
671
+ }
672
+ if (hasPendingMarks && (hasAddPendingMarks || hasPendingMarkOrder)) {
673
+ throw new Error("Active-context delta states the whole mark array and a change to it");
674
+ }
757
675
  }
758
676
  const parsed = clone(value) as unknown as ActiveContextStateWireV2;
759
677
  if (bytes(parsed) > MAX_ACTIVE_STATE_EVENT_BYTES) throw new Error("Active-context state event exceeds wire limit");
@@ -794,8 +712,7 @@ export function stateFromFoldRefs(
794
712
  wire: Pick<
795
713
  ActiveContextCheckpointV2,
796
714
  "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
797
- "tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs" | "rider" |
798
- "lastCall" | "notices"
715
+ "tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs"
799
716
  >,
800
717
  refs: FoldRecordRef[],
801
718
  records: Map<string, FoldRecordEntry>,
@@ -815,24 +732,37 @@ export function stateFromFoldRefs(
815
732
  protected: clone(wire.protected),
816
733
  tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
817
734
  leases: clone(wire.leases ?? {}),
818
- ...(wire.surfacing?.length ? { surfacing: clone(wire.surfacing) } : {}),
819
735
  ...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
820
736
  ...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
821
737
  ...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
822
- ...(wire.rider === undefined ? {} : { rider: clone(wire.rider) }),
823
- ...(wire.lastCall === undefined ? {} : { lastCall: clone(wire.lastCall) }),
824
- ...(wire.notices === undefined ? {} : { notices: clone(wire.notices) }),
825
738
  ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
826
739
  };
827
740
  return parseActiveContextState(state, wire.sessionId, false);
828
741
  }
829
742
 
743
+ export interface ProjectionFingerprint {
744
+ topologySha256: string;
745
+ protectionSha256: string;
746
+ }
747
+
748
+ /**
749
+ * A REVISION'S FINGERPRINTS, COMPUTED WHEN ONE IS ASKED FOR.
750
+ *
751
+ * A `Map` satisfies this, which is what it was until 2026-08-13. The narrower shape is the
752
+ * point: the only reader takes ONE revision on the restore path, and the eager map hashed
753
+ * the fold forest twice for every revision the ledger ever held, on every materialization,
754
+ * including the ones that discard the map entirely.
755
+ */
756
+ export interface ProjectionFingerprints {
757
+ get(revision: number): ProjectionFingerprint | undefined;
758
+ }
759
+
830
760
  export interface MaterializedStatePersistence {
831
761
  state: ActiveContextState;
832
762
  wireVersion: 0 | 1 | 2;
833
763
  records: Map<string, FoldRecordEntry>;
834
764
  stateSha256: string;
835
- projectionFingerprints: Map<number, { topologySha256: string; protectionSha256: string }>;
765
+ projectionFingerprints: ProjectionFingerprints;
836
766
  }
837
767
 
838
768
  export function materializeStatePersistence(
@@ -844,16 +774,49 @@ export function materializeStatePersistence(
844
774
  let state = emptyActiveContextState(sessionId);
845
775
  let wireVersion: 0 | 1 | 2 = 0;
846
776
  const records = new Map<string, FoldRecordEntry>();
847
- const projectionFingerprints = new Map<number, { topologySha256: string; protectionSha256: string }>();
777
+ // WHAT A FINGERPRINT IS MADE OF, HELD BY REFERENCE UNTIL ONE IS ASKED FOR.
778
+ //
779
+ // Replaying a ledger hashed the whole fold forest twice per revision, and one revision is
780
+ // ever read, by one caller, on the restore path. `materializeActiveContextState` takes
781
+ // `.state` and drops the map, so every request on a folding session paid for hundreds of
782
+ // digests it could not reach. Profiled against sol-20260813-paired rep 1 at its full
783
+ // 1,276 entries, materialization was 98% of a derivation, and canonicalizing and hashing
784
+ // was 72% of that.
785
+ //
786
+ // Holding the fields costs no copy: `state` is REPLACED on every delta rather than
787
+ // mutated (see `stateFromFoldRefs` below), so a reference taken here keeps that
788
+ // revision's own values, and the fold objects inside are shared with `records`.
789
+ const fingerprintSources = new Map<number, Pick<ActiveContextState,
790
+ "folds" | "expanded" | "protected">>();
791
+ const fingerprintCache = new Map<number, ProjectionFingerprint>();
792
+ const projectionFingerprints: ProjectionFingerprints = {
793
+ get(revision: number): ProjectionFingerprint | undefined {
794
+ const cached = fingerprintCache.get(revision);
795
+ if (cached) return cached;
796
+ const source = fingerprintSources.get(revision);
797
+ if (!source) return undefined;
798
+ const computed = {
799
+ topologySha256: topologySha256(source),
800
+ protectionSha256: protectionSha256(source),
801
+ };
802
+ fingerprintCache.set(revision, computed);
803
+ return computed;
804
+ },
805
+ };
848
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;
849
815
  let stateStart = -1;
850
816
  let checkpointIndex = -1;
851
817
  let v2Seen = false;
852
818
  const rememberProjection = (): void => {
853
- projectionFingerprints.set(state.revision, {
854
- topologySha256: topologySha256(state),
855
- protectionSha256: protectionSha256(state),
856
- });
819
+ fingerprintSources.set(state.revision, state);
857
820
  };
858
821
  rememberProjection();
859
822
  for (let index = 0; index < entries.length; index += 1) {
@@ -901,7 +864,8 @@ export function materializeStatePersistence(
901
864
  if (wire.kind === "checkpoint") {
902
865
  state = stateFromFoldRefs(wire, wire.foldRefs, records);
903
866
  } else {
904
- if (wireVersion === 0 || wire.baseRevision !== state.revision || wire.baseStateSha256 !== stateSha256 ||
867
+ if (wireVersion === 0 || wire.baseRevision !== state.revision ||
868
+ (wire.baseStateSha256 !== stateSha256 && wire.baseStateSha256 !== writtenSha256) ||
905
869
  wire.revision <= wire.baseRevision) {
906
870
  throw new Error("Broken active-context delta chain");
907
871
  }
@@ -916,20 +880,73 @@ export function materializeStatePersistence(
916
880
  if (existing) throw new Error(`Duplicate active-context delta addition ${ref.id}`);
917
881
  byId.set(ref.id, ref);
918
882
  }
919
- state = stateFromFoldRefs(wire, [...byId.values()], records);
883
+ // A delta written before 2026-08-13 carries the whole brief map; a newer one carries
884
+ // only what changed. The presence of `briefs` is the discriminator, not a fallback:
885
+ // a wire that states the whole map is replayed as the whole map.
886
+ const briefs = wire.briefs !== undefined ? clone(wire.briefs) : { ...(state.briefs ?? {}) };
887
+ if (wire.briefs === undefined) {
888
+ for (const id of wire.removeBriefIds ?? []) {
889
+ if (!Object.prototype.hasOwnProperty.call(briefs, id)) {
890
+ throw new Error(`Unknown active-context brief removal ${id}`);
891
+ }
892
+ delete briefs[id];
893
+ }
894
+ for (const [id, brief] of Object.entries(wire.addBriefs ?? {})) {
895
+ if (Object.prototype.hasOwnProperty.call(briefs, id) &&
896
+ sha256Value(briefs[id]) === sha256Value(brief)) {
897
+ throw new Error(`Redundant active-context brief ${id}`);
898
+ }
899
+ briefs[id] = clone(brief);
900
+ }
901
+ }
902
+ // Same rule for the marks, but the discriminator is the ORDER's presence rather than
903
+ // the array's absence. A delta written before 2026-08-13 states the whole array when
904
+ // it has one and states nothing when it does not, so "no array" means "no marks"
905
+ // there; reading it as "keep the base's marks" would resurrect marks a sealed session
906
+ // had cleared. The order list is what a NEW delta's replay is built from, so a key it
907
+ // does not name is removed by not being named.
908
+ let marks: PendingMark[];
909
+ if (wire.pendingMarkOrder === undefined) {
910
+ marks = wire.pendingMarks !== undefined ? clone(wire.pendingMarks) : [];
911
+ } else {
912
+ const held = new Map((state.pendingMarks ?? []).map((mark) =>
913
+ [pendingMarkKey(mark), mark] as const));
914
+ for (const mark of wire.addPendingMarks ?? []) {
915
+ const key = pendingMarkKey(mark);
916
+ const standing = held.get(key);
917
+ if (standing && sha256Value(standing) === sha256Value(mark)) {
918
+ throw new Error(`Redundant active-context pending mark ${key}`);
919
+ }
920
+ held.set(key, clone(mark));
921
+ }
922
+ marks = wire.pendingMarkOrder.map((key) => {
923
+ const mark = held.get(key);
924
+ if (!mark) throw new Error(`Unknown active-context pending mark ${key}`);
925
+ return mark;
926
+ });
927
+ }
928
+ state = stateFromFoldRefs(
929
+ {
930
+ ...wire,
931
+ briefs: Object.keys(briefs).length ? briefs : undefined,
932
+ pendingMarks: marks.length ? marks : undefined,
933
+ },
934
+ [...byId.values()],
935
+ records,
936
+ );
920
937
  }
921
938
  const calculated = semanticStateSha256(state);
922
- // Compatibility window: accept the current digest plus two legacy shapes: one
923
- // omitted zero cadence/empty leases; the other used a different replay-property
924
- // order. These shims keep already-written v2 events readable and can be removed
925
- // at the next wire-version bump.
939
+ const legacyRider = (wire as { rider?: unknown }).rider;
926
940
  if (calculated !== wire.stateSha256 &&
927
941
  legacyCadenceOmittedStateSha256(state) !== wire.stateSha256 &&
928
- legacyReplayOrderStateSha256(state) !== wire.stateSha256) {
942
+ legacyReplayOrderStateSha256(state) !== wire.stateSha256 &&
943
+ (legacyRider === undefined ||
944
+ legacyRiderStateSha256(state, legacyRider) !== wire.stateSha256)) {
929
945
  throw new Error("Active-context v2 state digest drift");
930
946
  }
931
947
  wireVersion = 2;
932
948
  stateSha256 = calculated;
949
+ writtenSha256 = wire.stateSha256 === calculated ? null : wire.stateSha256;
933
950
  rememberProjection();
934
951
  }
935
952
  return { state, wireVersion, records, stateSha256, projectionFingerprints };
@@ -970,13 +987,9 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
970
987
  prepared: state.prepared ? clone(state.prepared) : null,
971
988
  tokensSinceToolFold: state.tokensSinceToolFold,
972
989
  leases: clone(state.leases),
973
- ...(state.surfacing?.length ? { surfacing: clone(state.surfacing) } : {}),
974
990
  ...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
975
991
  ...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
976
992
  ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
977
- ...(state.rider ? { rider: clone(state.rider) } : {}),
978
- ...(state.lastCall ? { lastCall: clone(state.lastCall) } : {}),
979
- ...(state.notices ? { notices: clone(state.notices) } : {}),
980
993
  stateSha256: semanticStateSha256(state),
981
994
  }, state.sessionId) as ActiveContextCheckpointV2;
982
995
  }
@@ -996,6 +1009,40 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
996
1009
  throw new Error(`Active-context fold record changed for ${id}`);
997
1010
  }
998
1011
  }
1012
+ // A delta carries the CHANGE, never a value its base already holds. The brief map used
1013
+ // to ship whole on every write: 81% of all state bytes on sol-20260812 rep 9, 17.7 MB of
1014
+ // it unchanged, which then made every later turn slower because the session file is what
1015
+ // the runtime derives over.
1016
+ const previousBriefs = previous.briefs ?? {};
1017
+ const nextBriefs = next.briefs ?? {};
1018
+ const addBriefs: Record<string, BriefOverride> = {};
1019
+ for (const [id, brief] of Object.entries(nextBriefs)) {
1020
+ if (!Object.prototype.hasOwnProperty.call(previousBriefs, id)) { addBriefs[id] = clone(brief); continue; }
1021
+ if (sha256Value(previousBriefs[id]) !== sha256Value(brief)) addBriefs[id] = clone(brief);
1022
+ }
1023
+ const removeBriefIds = Object.keys(previousBriefs)
1024
+ .filter((id) => !Object.prototype.hasOwnProperty.call(nextBriefs, id));
1025
+ // The marks are the same rule one shape along, and the next largest field after the
1026
+ // briefs: 2.55 MB of rep 9's 21.6 MB ledger, which a diff takes to 0.16 MB. They carry an
1027
+ // ORDER as well as a membership, and appending after removing does not always reproduce
1028
+ // it (four of that run's 309 writes), so the key order travels whole and states the
1029
+ // removals with it. It is short next to the marks themselves, so there is no case to
1030
+ // split and no branch that can pick the wrong one.
1031
+ const previousMarks = previous.pendingMarks ?? [];
1032
+ const nextMarks = next.pendingMarks ?? [];
1033
+ const previousByKey = new Map(previousMarks.map((mark) => [pendingMarkKey(mark), mark] as const));
1034
+ const addPendingMarks = nextMarks.filter((mark) => {
1035
+ const held = previousByKey.get(pendingMarkKey(mark));
1036
+ return !held || sha256Value(held) !== sha256Value(mark);
1037
+ });
1038
+ const marksTravel = previousMarks.length > 0 || nextMarks.length > 0;
1039
+ // The rest travel whole and stay that way. Measured on the same run, the two that were
1040
+ // worth a diff were 20.0 MB of the 21.6 MB ledger; what remained were bounded objects
1041
+ // with their own caps rather than collections that grow with the epoch (the largest,
1042
+ // the since-retired `notices`, was 0.93 MB). A diff for a capped
1043
+ // value is a second encoding to keep honest for a few hundred kilobytes a session, and
1044
+ // the reader's own compatibility rule is the part that is easy to get wrong: this build
1045
+ // shipped one such bug and the gate caught it.
999
1046
  return parseActiveContextStateV2({
1000
1047
  version: 2,
1001
1048
  kind: "delta",
@@ -1010,13 +1057,11 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
1010
1057
  prepared: next.prepared ? clone(next.prepared) : null,
1011
1058
  tokensSinceToolFold: next.tokensSinceToolFold,
1012
1059
  leases: clone(next.leases),
1013
- ...(next.surfacing?.length ? { surfacing: clone(next.surfacing) } : {}),
1014
- ...(next.pendingMarks?.length ? { pendingMarks: clone(next.pendingMarks) } : {}),
1015
- ...(next.briefs && Object.keys(next.briefs).length ? { briefs: clone(next.briefs) } : {}),
1060
+ ...(marksTravel ? { pendingMarkOrder: nextMarks.map(pendingMarkKey) } : {}),
1061
+ ...(addPendingMarks.length ? { addPendingMarks: clone(addPendingMarks) } : {}),
1062
+ ...(Object.keys(addBriefs).length ? { addBriefs } : {}),
1063
+ ...(removeBriefIds.length ? { removeBriefIds } : {}),
1016
1064
  ...(next.advisory ? { advisory: clone(next.advisory) } : {}),
1017
- ...(next.rider ? { rider: clone(next.rider) } : {}),
1018
- ...(next.lastCall ? { lastCall: clone(next.lastCall) } : {}),
1019
- ...(next.notices ? { notices: clone(next.notices) } : {}),
1020
1065
  stateSha256: semanticStateSha256(next),
1021
1066
  }, next.sessionId) as ActiveContextDeltaV2;
1022
1067
  }
@@ -1029,49 +1074,48 @@ export function protectionSha256(state: Pick<ActiveContextState, "protected">):
1029
1074
  return sha256Value([...state.protected].sort((left, right) => objectRefKey(left).localeCompare(objectRefKey(right))));
1030
1075
  }
1031
1076
 
1032
- /**
1033
- * The milestone vocabulary a persisted `advisory` state field may carry. The delivery
1034
- * schedule that spent a per-milestone budget is deleted; this list stays because the
1035
- * state field is written into every state and covered by the state digest, so sealed
1036
- * runs have to keep parsing. It is its own literal now rather than the key set of the
1037
- * deleted budget table.
1038
- */
1039
1077
  export const ADVISORY_MILESTONES = Object.freeze(
1040
1078
  ["orientation", "notice", "tools", "chapters", "urgent"] as AdvisoryMilestone[],
1041
1079
  );
1042
1080
 
1043
- /** The rider is LITERAL persisted bytes, so the bound is on the stored text itself. */
1044
- export const MAX_RIDER_TEXT_BYTES = 4_096;
1045
-
1046
- export function validRiderState(value: unknown): value is NonNullable<ActiveContextState["rider"]> {
1047
- if (!exactRecord(value, ["epoch", "text"])) return false;
1048
- const epoch = ownValue(value, "epoch");
1049
- const text = ownValue(value, "text");
1050
- return Number.isSafeInteger(epoch) && Number(epoch) >= 0 &&
1051
- typeof text === "string" && text.length > 0 && text.length <= MAX_RIDER_TEXT_BYTES;
1052
- }
1053
-
1054
- export function validLastCallState(value: unknown): value is NonNullable<ActiveContextState["lastCall"]> {
1055
- if (!exactRecord(value, ["exposure", "ordinal", "contextCalls", "agentMarks", "text"])) return false;
1056
- const text = ownValue(value, "text");
1057
- return ["exposure", "ordinal", "contextCalls", "agentMarks"].every((field) =>
1058
- Number.isSafeInteger(ownValue(value, field)) && Number(ownValue(value, field)) >= 0) &&
1059
- typeof text === "string" && text.length > 0 && text.length <= MAX_LAST_CALL_TEXT_BYTES;
1060
- }
1081
+ /**
1082
+ * THE RETIRED STATE FIELDS, REFUSED BY NAME.
1083
+ *
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
1088
+ * writes them any more and nothing reads them, so a durable state that still carries one
1089
+ * was written by a build that had them, and the exact-record readers would reject it with
1090
+ * "Invalid active-context state keys" -- true, and useless to whoever has to act on it.
1091
+ *
1092
+ * Refusing by name is the whole behaviour. Tolerating the field and dropping it is the
1093
+ * silent path: it makes a version boundary look like a successful load, and the state
1094
+ * that comes back is not the state that was written. Gate 17 already states the wire's
1095
+ * policy in the other direction -- an older exact-record reader rejects newer fields, and
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.
1106
+ */
1107
+ const RETIRED_STATE_FIELDS: readonly string[] = Object.freeze(["lastCall", "notices", "surfacing"]);
1061
1108
 
1062
- export function validNoticesState(value: unknown): value is NonNullable<ActiveContextState["notices"]> {
1063
- if (!exactRecord(value, ["fired", "ring"])) return false;
1064
- const fired = denseOwnArrayValues(ownValue(value, "fired"));
1065
- const ring = denseOwnArrayValues(ownValue(value, "ring"));
1066
- if (!fired || !ring || ring.length > MAX_THRESHOLD_NOTICES) return false;
1067
- if (fired.some((share) => typeof share !== "number" || !Number.isFinite(share) || share <= 0 || share >= 1) ||
1068
- new Set(fired).size !== fired.length) return false;
1069
- return ring.every((notice) => exactRecord(notice, ["share", "ordinal", "text"]) &&
1070
- typeof ownValue(notice, "share") === "number" && Number.isFinite(ownValue(notice, "share")) &&
1071
- Number(ownValue(notice, "share")) > 0 && Number(ownValue(notice, "share")) < 1 &&
1072
- Number.isSafeInteger(ownValue(notice, "ordinal")) && Number(ownValue(notice, "ordinal")) >= 0 &&
1073
- typeof ownValue(notice, "text") === "string" && String(ownValue(notice, "text")).length > 0 &&
1074
- String(ownValue(notice, "text")).length <= MAX_THRESHOLD_NOTICE_TEXT_BYTES);
1109
+ export function refuseRetiredStateFields(value: unknown): void {
1110
+ if (!value || typeof value !== "object") return;
1111
+ const carried = RETIRED_STATE_FIELDS.filter((field) =>
1112
+ Object.prototype.hasOwnProperty.call(value, field));
1113
+ if (!carried.length) return;
1114
+ throw new Error(
1115
+ `Active-context state carries the retired field(s) ${carried.join(", ")}: it was written ` +
1116
+ "by a build that still carried the subsystem behind it. Nothing writes or reads them " +
1117
+ "now, and this build will not load a state it cannot reproduce.",
1118
+ );
1075
1119
  }
1076
1120
 
1077
1121
  export function validAdvisoryState(value: unknown): value is NonNullable<ActiveContextState["advisory"]> {
@@ -1094,6 +1138,5 @@ export function validAdvisoryState(value: unknown): value is NonNullable<ActiveC
1094
1138
  const delivered = ownValue(value, "delivered") as Record<string, unknown>;
1095
1139
  return Reflect.ownKeys(delivered).every((key) => typeof key === "string" &&
1096
1140
  ADVISORY_MILESTONES.includes(key as AdvisoryMilestone) &&
1097
- Number.isSafeInteger(ownValue(delivered, key)) && Number(ownValue(delivered, key)) >= 0 &&
1098
- Number(ownValue(delivered, key)) <= MAX_ADVISORY_DELIVERIES_PER_MILESTONE);
1141
+ Number.isSafeInteger(ownValue(delivered, key)) && Number(ownValue(delivered, key)) >= 0);
1099
1142
  }