pi-fold 1.0.2 → 1.0.3

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.
@@ -22,8 +22,11 @@ import {
22
22
  EXPAND_LEASE_GENERATIONS,
23
23
  MAX_ADVISORY_DELIVERIES_PER_MILESTONE,
24
24
  MAX_EXPAND_LEASES,
25
+ MAX_LAST_CALL_TEXT_BYTES,
25
26
  MAX_PENDING_MARKS,
26
- SURFACING_MAX_LOG_RECORDS,
27
+ MAX_THRESHOLD_NOTICE_TEXT_BYTES,
28
+ MAX_THRESHOLD_NOTICES,
29
+ SURFACING_MAX_LEDGER_RECORDS,
27
30
  } from "./policy.ts";
28
31
  import type {
29
32
  ActiveContextCheckpointV2,
@@ -32,6 +35,7 @@ import type {
32
35
  ActiveContextStateWireV2,
33
36
  ActiveFold,
34
37
  AdvisoryMilestone,
38
+ BriefOverride,
35
39
  BriefProvenance,
36
40
  FoldKind,
37
41
  FoldPart,
@@ -54,8 +58,8 @@ export const PREPARED_FOLD_KEYS = [
54
58
  export const ACTIVE_STATE_KEYS = ["version", "sessionId", "revision", "folds", "expanded", "protected"] as const;
55
59
  export const FOLD_RECORD_REF_KEYS = ["id", "sha256"] as const;
56
60
  export const FOLD_RECORD_ENTRY_KEYS = ["version", "sessionId", "foldId", "recordSha256", "fold"] as const;
57
- export const SURFACING_RECORD_KEYS = ["source", "id", "score", "ordinal", "outcome"] as const;
58
- export const SURFACING_OUTCOMES = Object.freeze(["shown", "accept", "reject"] 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);
59
63
  export const PENDING_FOLD_MARK_KEYS = [
60
64
  "mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
61
65
  ] as const;
@@ -81,18 +85,21 @@ export function validTokensSinceToolFold(value: unknown): value is number {
81
85
 
82
86
  export function validSurfacingRecord(value: unknown): value is SurfacingRecord {
83
87
  if (!exactRecord(value, SURFACING_RECORD_KEYS)) return false;
84
- const score = ownValue(value, "score");
85
- return typeof ownValue(value, "source") === "string" && Boolean(ownValue(value, "source")) &&
86
- typeof ownValue(value, "id") === "string" && Boolean(ownValue(value, "id")) &&
87
- typeof score === "number" && Number.isFinite(score) && Number(score) >= 0 && Number(score) <= 1 &&
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) &&
88
93
  Number.isSafeInteger(ownValue(value, "ordinal")) && Number(ownValue(value, "ordinal")) >= 0 &&
89
94
  SURFACING_OUTCOMES.includes(ownValue(value, "outcome") as SurfacingRecord["outcome"]);
90
95
  }
91
96
 
97
+ /** One record per fold: a duplicate id is a ledger that forgot an ignore. */
92
98
  export function parseSurfacingLog(value: unknown): SurfacingRecord[] {
93
99
  const records = denseOwnArrayValues(value);
94
- if (!records || records.length > SURFACING_MAX_LOG_RECORDS || !records.every(validSurfacingRecord)) {
95
- throw new Error("Invalid active-context surfacing log");
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");
96
103
  }
97
104
  return clone(records) as SurfacingRecord[];
98
105
  }
@@ -125,20 +132,41 @@ export function validPendingMark(value: unknown): value is PendingMark {
125
132
  * supplied brief obeys, and refused for a fold that is not in the forest, exactly
126
133
  * like a pin or a refold mark naming a missing fold.
127
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
+ export function briefOverrideText(override: unknown): string | null {
142
+ if (typeof override === "string") return override;
143
+ const brief = ownValue(override, "brief");
144
+ return typeof brief === "string" ? brief : null;
145
+ }
146
+
128
147
  export function parseBriefOverrides(
129
148
  value: unknown,
130
149
  ids?: ReadonlySet<string>,
131
- ): Record<string, string> {
150
+ ): Record<string, BriefOverride> {
132
151
  if (!value || typeof value !== "object" || Array.isArray(value)) {
133
152
  throw new Error("Invalid active-context brief overrides");
134
153
  }
135
154
  const entries = Object.entries(value as Record<string, unknown>);
155
+ const validOverride = (override: unknown): boolean => {
156
+ const brief = briefOverrideText(override);
157
+ if (typeof brief !== "string" || !brief.trim() ||
158
+ brief.length > ACTIVE_CONTEXT_POLICY.maxBriefChars) return false;
159
+ if (typeof override === "string") return true;
160
+ return exactRecord(override, ["brief", "provenance"]) &&
161
+ validProvenance(ownValue(override, "provenance"));
162
+ };
136
163
  if (entries.length > MAX_ACTIVE_FOLD_RECORDS ||
137
- entries.some(([id, brief]) => !id || typeof brief !== "string" || !brief.trim() ||
138
- brief.length > ACTIVE_CONTEXT_POLICY.maxBriefChars || (ids && !ids.has(id)))) {
164
+ entries.some(([id, override]) => !id || !validOverride(override) || (ids && !ids.has(id)))) {
139
165
  throw new Error("Invalid active-context brief overrides");
140
166
  }
141
- return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right))) as Record<string, string>;
167
+ return Object.fromEntries(
168
+ entries.sort(([left], [right]) => left.localeCompare(right)),
169
+ ) as Record<string, BriefOverride>;
142
170
  }
143
171
 
144
172
  /** The brief a fold PRESENTS: the agent's correction when it made one, else the record's. */
@@ -146,7 +174,23 @@ export function foldBrief(
146
174
  fold: Pick<ActiveFold, "id" | "brief">,
147
175
  state: Pick<ActiveContextState, "briefs">,
148
176
  ): string {
149
- return state.briefs?.[fold.id] ?? fold.brief;
177
+ return briefOverrideText(state.briefs?.[fold.id]) ?? fold.brief;
178
+ }
179
+
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
+ export function foldProvenance(
186
+ fold: Pick<ActiveFold, "id" | "provenance">,
187
+ state: Pick<ActiveContextState, "briefs">,
188
+ ): BriefProvenance {
189
+ const override = state.briefs?.[fold.id];
190
+ const provenance = typeof override === "object" && override
191
+ ? ownValue(override, "provenance")
192
+ : undefined;
193
+ return normalizeLegacyProvenance(provenance ?? fold.provenance);
150
194
  }
151
195
 
152
196
  export function parsePendingMarks(value: unknown): PendingMark[] {
@@ -237,7 +281,12 @@ export function foldMap(state: Pick<ActiveContextState, "folds">): Map<string, A
237
281
  return new Map(state.folds.map((fold) => [fold.id, fold]));
238
282
  }
239
283
 
240
- export function childFoldIds(fold: ActiveFold): string[] {
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
+ export function childFoldIds(fold: Pick<ActiveFold, "parts">): string[] {
241
290
  return fold.parts.filter((part): part is Extract<FoldPart, { kind: "fold" }> => part.kind === "fold")
242
291
  .map((part) => part.foldId);
243
292
  }
@@ -260,6 +309,33 @@ export function flattenFoldRefs(fold: ActiveFold, state: Pick<ActiveContextState
260
309
  return visit(fold);
261
310
  }
262
311
 
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
+ export function directFoldOwners(folds: readonly ActiveFold[]): Map<string, string> {
327
+ const owner = new Map<string, string>();
328
+ for (const fold of folds) {
329
+ for (const part of fold.parts) {
330
+ if (part.kind !== "raw") continue;
331
+ const key = objectRefKey(part.ref);
332
+ if (owner.has(key)) throw new Error(`Evidence ${part.ref.entryId} has multiple direct fold owners`);
333
+ owner.set(key, fold.id);
334
+ }
335
+ }
336
+ return owner;
337
+ }
338
+
263
339
  export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
264
340
  const rawValues = denseOwnArrayValues(folds);
265
341
  if (!rawValues || rawValues.some((fold) => !validFoldShape(fold))) {
@@ -275,20 +351,28 @@ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
275
351
  (!fold.parts.length || fold.parts.some((part) => part.kind !== "raw" || part.ref.role !== "toolResult"))) {
276
352
  throw new Error("Tool-result fold must own one validated assistant batch of tool results");
277
353
  }
278
- if (fold.kind === "consolidation" && fold.parts.some((part) => part.kind !== "fold")) {
279
- throw new Error("Consolidation folds may contain only child folds");
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
+ if (fold.kind === "consolidation" && !fold.parts.some((part) => part.kind === "fold")) {
367
+ throw new Error("Consolidation folds must contain at least one child fold");
280
368
  }
281
369
  byId.set(fold.id, fold);
282
370
  }
371
+ directFoldOwners(values);
283
372
  const parent = new Map<string, string>();
284
- const rawOwner = new Map<string, string>();
285
373
  for (const fold of values) {
286
374
  for (const part of fold.parts) {
287
- if (part.kind === "raw") {
288
- const key = objectRefKey(part.ref);
289
- if (rawOwner.has(key)) throw new Error(`Evidence ${part.ref.entryId} has multiple direct fold owners`);
290
- rawOwner.set(key, fold.id);
291
- } else {
375
+ if (part.kind === "fold") {
292
376
  if (!byId.has(part.foldId) || part.foldId === fold.id || parent.has(part.foldId)) {
293
377
  throw new Error(`Invalid or multiply owned child fold ${part.foldId}`);
294
378
  }
@@ -298,15 +382,6 @@ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
298
382
  }
299
383
  for (const fold of values) {
300
384
  if ((parent.get(fold.id) ?? null) !== fold.parentId) throw new Error(`Fold ${fold.id} parent drift`);
301
- for (const childId of childFoldIds(fold)) {
302
- const child = byId.get(childId)!;
303
- if (fold.kind === "chapter" && child.kind !== "tool-result") {
304
- throw new Error(`Chapter ${fold.id} may contain only tool-result folds`);
305
- }
306
- if (fold.kind === "consolidation" && child.kind === "tool-result") {
307
- throw new Error(`Consolidation ${fold.id} may contain only chapter or consolidation folds`);
308
- }
309
- }
310
385
  }
311
386
  for (const fold of values) {
312
387
  const refs = flattenFoldRefs(fold, { folds: values });
@@ -334,6 +409,9 @@ export function parseActiveContextState(
334
409
  // rejecting it here would suspend automatic management on upgrade.
335
410
  const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
336
411
  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");
337
415
  const extraKeys = [
338
416
  ...(hasPrepared ? ["prepared"] : []),
339
417
  ...(hasAdvisory ? ["advisory"] : []),
@@ -343,6 +421,9 @@ export function parseActiveContextState(
343
421
  ...(hasPendingMarks ? ["pendingMarks"] : []),
344
422
  ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
345
423
  ...(hasBriefs ? ["briefs"] : []),
424
+ ...(hasRider ? ["rider"] : []),
425
+ ...(hasLastCall ? ["lastCall"] : []),
426
+ ...(hasNotices ? ["notices"] : []),
346
427
  ];
347
428
  if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
348
429
  const folds = denseOwnArrayValues(ownValue(value, "folds"));
@@ -364,6 +445,15 @@ export function parseActiveContextState(
364
445
  if (hasAdvisory && !validAdvisoryState(ownValue(value, "advisory"))) {
365
446
  throw new Error("Invalid active-context advisory state");
366
447
  }
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
+ }
367
457
  if (hasTokensSinceToolFold && !validTokensSinceToolFold(ownValue(value, "tokensSinceToolFold"))) {
368
458
  throw new Error("Invalid active-context tool-fold cadence");
369
459
  }
@@ -397,6 +487,9 @@ export function parseActiveContextState(
397
487
  ...(hasAdvisory
398
488
  ? { advisory: clone(source.advisory!) }
399
489
  : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
490
+ ...(hasRider ? { rider: clone(source.rider!) } : {}),
491
+ ...(hasLastCall ? { lastCall: clone(source.lastCall!) } : {}),
492
+ ...(hasNotices ? { notices: clone(source.notices!) } : {}),
400
493
  ...(hasPrepared ? { prepared: clone(source.prepared!) } : {}),
401
494
  };
402
495
  }
@@ -515,11 +608,17 @@ export function validateV2ProjectionFields(
515
608
  surfacingValue?: unknown,
516
609
  pendingMarksValue?: unknown,
517
610
  briefsValue?: unknown,
611
+ riderValue?: unknown,
612
+ lastCallValue?: unknown,
613
+ noticesValue?: unknown,
518
614
  ): {
519
615
  expanded: string[];
520
616
  protected: EvidenceRef[];
521
617
  prepared?: PreparedFold;
522
618
  advisory?: NonNullable<ActiveContextState["advisory"]>;
619
+ rider?: NonNullable<ActiveContextState["rider"]>;
620
+ lastCall?: NonNullable<ActiveContextState["lastCall"]>;
621
+ notices?: NonNullable<ActiveContextState["notices"]>;
523
622
  tokensSinceToolFold: number;
524
623
  leases: Record<string, number>;
525
624
  surfacing: SurfacingRecord[];
@@ -538,6 +637,15 @@ export function validateV2ProjectionFields(
538
637
  if (advisoryValue !== undefined && !validAdvisoryState(advisoryValue)) {
539
638
  throw new Error("Invalid active-context v2 advisory state");
540
639
  }
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
+ }
541
649
  if (tokensSinceToolFoldValue !== undefined && !validTokensSinceToolFold(tokensSinceToolFoldValue)) {
542
650
  throw new Error("Invalid active-context v2 tool-fold cadence");
543
651
  }
@@ -552,6 +660,15 @@ export function validateV2ProjectionFields(
552
660
  ...(advisoryValue === undefined ? {} : {
553
661
  advisory: clone(advisoryValue) as NonNullable<ActiveContextState["advisory"]>,
554
662
  }),
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
+ }),
555
672
  tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
556
673
  leases,
557
674
  surfacing,
@@ -580,6 +697,12 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
580
697
  Object.prototype.hasOwnProperty.call(value, "pinnedPeeks"));
581
698
  const hasBriefs = Boolean(value && typeof value === "object" &&
582
699
  Object.prototype.hasOwnProperty.call(value, "briefs"));
700
+ const hasRider = Boolean(value && typeof value === "object" &&
701
+ 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"));
583
706
  const optionalKeys = [
584
707
  ...(hasAdvisory ? ["advisory"] : []),
585
708
  ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
@@ -588,6 +711,9 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
588
711
  ...(hasPendingMarks ? ["pendingMarks"] : []),
589
712
  ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
590
713
  ...(hasBriefs ? ["briefs"] : []),
714
+ ...(hasRider ? ["rider"] : []),
715
+ ...(hasLastCall ? ["lastCall"] : []),
716
+ ...(hasNotices ? ["notices"] : []),
591
717
  ];
592
718
  const checkpoint = kind === "checkpoint" &&
593
719
  exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
@@ -603,6 +729,7 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
603
729
  ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
604
730
  ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
605
731
  ownValue(value, "surfacing"), ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
732
+ ownValue(value, "rider"), ownValue(value, "lastCall"), ownValue(value, "notices"),
606
733
  );
607
734
  if (checkpoint) {
608
735
  const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
@@ -667,7 +794,8 @@ export function stateFromFoldRefs(
667
794
  wire: Pick<
668
795
  ActiveContextCheckpointV2,
669
796
  "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
670
- "tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs"
797
+ "tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs" | "rider" |
798
+ "lastCall" | "notices"
671
799
  >,
672
800
  refs: FoldRecordRef[],
673
801
  records: Map<string, FoldRecordEntry>,
@@ -691,6 +819,9 @@ export function stateFromFoldRefs(
691
819
  ...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
692
820
  ...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
693
821
  ...(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) }),
694
825
  ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
695
826
  };
696
827
  return parseActiveContextState(state, wire.sessionId, false);
@@ -843,6 +974,9 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
843
974
  ...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
844
975
  ...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
845
976
  ...(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) } : {}),
846
980
  stateSha256: semanticStateSha256(state),
847
981
  }, state.sessionId) as ActiveContextCheckpointV2;
848
982
  }
@@ -880,6 +1014,9 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
880
1014
  ...(next.pendingMarks?.length ? { pendingMarks: clone(next.pendingMarks) } : {}),
881
1015
  ...(next.briefs && Object.keys(next.briefs).length ? { briefs: clone(next.briefs) } : {}),
882
1016
  ...(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) } : {}),
883
1020
  stateSha256: semanticStateSha256(next),
884
1021
  }, next.sessionId) as ActiveContextDeltaV2;
885
1022
  }
@@ -892,18 +1029,51 @@ export function protectionSha256(state: Pick<ActiveContextState, "protected">):
892
1029
  return sha256Value([...state.protected].sort((left, right) => objectRefKey(left).localeCompare(objectRefKey(right))));
893
1030
  }
894
1031
 
895
- export const ADVISORY_BUDGETS: Readonly<Record<AdvisoryMilestone, number>> = Object.freeze({
896
- orientation: 1,
897
- notice: 2,
898
- tools: 2,
899
- chapters: 2,
900
- urgent: 3,
901
- });
902
-
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
+ */
903
1039
  export const ADVISORY_MILESTONES = Object.freeze(
904
- Object.keys(ADVISORY_BUDGETS) as AdvisoryMilestone[],
1040
+ ["orientation", "notice", "tools", "chapters", "urgent"] as AdvisoryMilestone[],
905
1041
  );
906
1042
 
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
+ }
1061
+
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);
1075
+ }
1076
+
907
1077
  export function validAdvisoryState(value: unknown): value is NonNullable<ActiveContextState["advisory"]> {
908
1078
  const hasArmed = Boolean(value && typeof value === "object" &&
909
1079
  Object.prototype.hasOwnProperty.call(value, "armed"));