pi-fold 0.1.0 → 1.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.
@@ -22,6 +22,8 @@ import {
22
22
  EXPAND_LEASE_GENERATIONS,
23
23
  MAX_ADVISORY_DELIVERIES_PER_MILESTONE,
24
24
  MAX_EXPAND_LEASES,
25
+ MAX_PENDING_MARKS,
26
+ SURFACING_MAX_LOG_RECORDS,
25
27
  } from "./policy.ts";
26
28
  import type {
27
29
  ActiveContextCheckpointV2,
@@ -35,7 +37,10 @@ import type {
35
37
  FoldPart,
36
38
  FoldRecordEntry,
37
39
  FoldRecordRef,
40
+ MarkOrigin,
41
+ PendingMark,
38
42
  PreparedFold,
43
+ SurfacingRecord,
39
44
  } from "./policy.ts";
40
45
 
41
46
  export const ACTIVE_FOLD_KEYS = [
@@ -49,6 +54,13 @@ export const PREPARED_FOLD_KEYS = [
49
54
  export const ACTIVE_STATE_KEYS = ["version", "sessionId", "revision", "folds", "expanded", "protected"] as const;
50
55
  export const FOLD_RECORD_REF_KEYS = ["id", "sha256"] as const;
51
56
  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);
59
+ export const PENDING_FOLD_MARK_KEYS = [
60
+ "mark", "id", "kind", "parts", "brief", "briefProvenance", "origin", "ordinal",
61
+ ] as const;
62
+ export const PENDING_REFOLD_MARK_KEYS = ["mark", "id", "origin", "ordinal"] as const;
63
+ export const MARK_ORIGINS = Object.freeze(["agent", "ladder"] as const);
52
64
  export const STATE_CHECKPOINT_V2_KEYS = [
53
65
  "version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
54
66
  ] as const;
@@ -67,6 +79,85 @@ export function validTokensSinceToolFold(value: unknown): value is number {
67
79
  return Number.isSafeInteger(value) && Number(value) >= 0;
68
80
  }
69
81
 
82
+ export function validSurfacingRecord(value: unknown): value is SurfacingRecord {
83
+ 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
+ Number.isSafeInteger(ownValue(value, "ordinal")) && Number(ownValue(value, "ordinal")) >= 0 &&
89
+ SURFACING_OUTCOMES.includes(ownValue(value, "outcome") as SurfacingRecord["outcome"]);
90
+ }
91
+
92
+ export function parseSurfacingLog(value: unknown): SurfacingRecord[] {
93
+ 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");
96
+ }
97
+ return clone(records) as SurfacingRecord[];
98
+ }
99
+
100
+ /** Stable identity of one pending mark: its phase plus the fold it names. */
101
+ export function pendingMarkKey(mark: Pick<PendingMark, "mark" | "id">): string {
102
+ return `${mark.mark}:${mark.id}`;
103
+ }
104
+
105
+ export function validPendingMark(value: unknown): value is PendingMark {
106
+ const id = ownValue(value, "id");
107
+ const ordinal = ownValue(value, "ordinal");
108
+ if (typeof id !== "string" || !id ||
109
+ !MARK_ORIGINS.includes(ownValue(value, "origin") as MarkOrigin) ||
110
+ !Number.isSafeInteger(ordinal) || Number(ordinal) < 0) return false;
111
+ const mark = ownValue(value, "mark");
112
+ if (mark === "refold") return exactRecord(value, PENDING_REFOLD_MARK_KEYS);
113
+ if (mark !== "fold" || !exactRecord(value, PENDING_FOLD_MARK_KEYS)) return false;
114
+ const kind = ownValue(value, "kind");
115
+ const parts = denseOwnArrayValues(ownValue(value, "parts"));
116
+ return (kind === "tool-result" || kind === "chapter" || kind === "consolidation") &&
117
+ Boolean(parts?.length) && parts!.length <= ACTIVE_CONTEXT_POLICY.maxFoldSourceRefs &&
118
+ parts!.every(validFoldPart) && structurallyValidBrief(ownValue(value, "brief")) &&
119
+ validProvenance(ownValue(value, "briefProvenance")) &&
120
+ id === foldIdFor(kind as FoldKind, parts as FoldPart[]);
121
+ }
122
+
123
+ /**
124
+ * Agent-corrected fold briefs, keyed by fold id. Bounded by the same brief cap a
125
+ * supplied brief obeys, and refused for a fold that is not in the forest, exactly
126
+ * like a pin or a refold mark naming a missing fold.
127
+ */
128
+ export function parseBriefOverrides(
129
+ value: unknown,
130
+ ids?: ReadonlySet<string>,
131
+ ): Record<string, string> {
132
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
133
+ throw new Error("Invalid active-context brief overrides");
134
+ }
135
+ const entries = Object.entries(value as Record<string, unknown>);
136
+ 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)))) {
139
+ throw new Error("Invalid active-context brief overrides");
140
+ }
141
+ return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right))) as Record<string, string>;
142
+ }
143
+
144
+ /** The brief a fold PRESENTS: the agent's correction when it made one, else the record's. */
145
+ export function foldBrief(
146
+ fold: Pick<ActiveFold, "id" | "brief">,
147
+ state: Pick<ActiveContextState, "briefs">,
148
+ ): string {
149
+ return state.briefs?.[fold.id] ?? fold.brief;
150
+ }
151
+
152
+ export function parsePendingMarks(value: unknown): PendingMark[] {
153
+ const marks = denseOwnArrayValues(value);
154
+ if (!marks || marks.length > MAX_PENDING_MARKS || !marks.every(validPendingMark) ||
155
+ new Set((marks as PendingMark[]).map(pendingMarkKey)).size !== marks.length) {
156
+ throw new Error("Invalid active-context pending marks");
157
+ }
158
+ return clone(marks) as PendingMark[];
159
+ }
160
+
70
161
  export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Record<string, number> {
71
162
  if (!isPlainRecord(value)) throw new Error("Invalid active-context expand leases");
72
163
  const entries = Object.entries(value);
@@ -235,11 +326,23 @@ export function parseActiveContextState(
235
326
  const hasTokensSinceToolFold = recordLike &&
236
327
  Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold");
237
328
  const hasLeases = recordLike && Object.prototype.hasOwnProperty.call(value, "leases");
329
+ const hasSurfacing = recordLike && Object.prototype.hasOwnProperty.call(value, "surfacing");
330
+ const hasPendingMarks = recordLike && Object.prototype.hasOwnProperty.call(value, "pendingMarks");
331
+ // Accepted, never read, never written. `pinnedPeeks` pinned a peek result against
332
+ // per-projection reclamation, which was deleted after rep 22 measured it rewriting
333
+ // the prefix mid-window. State written before that cut still carries the key, and
334
+ // rejecting it here would suspend automatic management on upgrade.
335
+ const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
336
+ const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
238
337
  const extraKeys = [
239
338
  ...(hasPrepared ? ["prepared"] : []),
240
339
  ...(hasAdvisory ? ["advisory"] : []),
241
340
  ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
242
341
  ...(hasLeases ? ["leases"] : []),
342
+ ...(hasSurfacing ? ["surfacing"] : []),
343
+ ...(hasPendingMarks ? ["pendingMarks"] : []),
344
+ ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
345
+ ...(hasBriefs ? ["briefs"] : []),
243
346
  ];
244
347
  if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
245
348
  const folds = denseOwnArrayValues(ownValue(value, "folds"));
@@ -265,8 +368,14 @@ export function parseActiveContextState(
265
368
  throw new Error("Invalid active-context tool-fold cadence");
266
369
  }
267
370
  const leases = hasLeases ? parseLeases(ownValue(value, "leases"), ids) : {};
371
+ const surfacing = hasSurfacing ? parseSurfacingLog(ownValue(value, "surfacing")) : [];
372
+ const marks = hasPendingMarks ? parsePendingMarks(ownValue(value, "pendingMarks")) : [];
373
+ if (marks.some((mark) => mark.mark === "refold" && !ids.has(mark.id))) {
374
+ throw new Error("Invalid active-context pending marks");
375
+ }
376
+ const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
268
377
  const source = clone(value) as unknown as ActiveContextState;
269
- // Provenance normalization is presentation-only — never mutate a durable
378
+ // Provenance normalization is presentation-only; never mutate a durable
270
379
  // content-addressed fold record: changing its bytes causes the next re-persist
271
380
  // to report a conflicting durable fold and suspend automatic management.
272
381
  return {
@@ -280,6 +389,11 @@ export function parseActiveContextState(
280
389
  ? Number(ownValue(value, "tokensSinceToolFold"))
281
390
  : 0,
282
391
  leases,
392
+ // Omitted when empty so every state written before surfacing existed keeps its
393
+ // exact digest; the replay digest is order- and presence-sensitive by design.
394
+ ...(surfacing.length ? { surfacing } : {}),
395
+ ...(marks.length ? { pendingMarks: marks } : {}),
396
+ ...(Object.keys(briefs).length ? { briefs } : {}),
283
397
  ...(hasAdvisory
284
398
  ? { advisory: clone(source.advisory!) }
285
399
  : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
@@ -355,6 +469,9 @@ export function sameStateProjection(left: ActiveContextState, right: ActiveConte
355
469
  const normalizedState = { ...clone(value), revision: 0 } as Partial<ActiveContextState>;
356
470
  if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
357
471
  if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
472
+ if (normalizedState.surfacing && normalizedState.surfacing.length === 0) delete normalizedState.surfacing;
473
+ if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
474
+ if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
358
475
  return normalizedState;
359
476
  };
360
477
  return stableStringify(normalized(left)) === stableStringify(normalized(right));
@@ -395,6 +512,9 @@ export function validateV2ProjectionFields(
395
512
  advisoryValue: unknown,
396
513
  tokensSinceToolFoldValue: unknown,
397
514
  leasesValue: unknown,
515
+ surfacingValue?: unknown,
516
+ pendingMarksValue?: unknown,
517
+ briefsValue?: unknown,
398
518
  ): {
399
519
  expanded: string[];
400
520
  protected: EvidenceRef[];
@@ -402,6 +522,9 @@ export function validateV2ProjectionFields(
402
522
  advisory?: NonNullable<ActiveContextState["advisory"]>;
403
523
  tokensSinceToolFold: number;
404
524
  leases: Record<string, number>;
525
+ surfacing: SurfacingRecord[];
526
+ pendingMarks: PendingMark[];
527
+ briefs: Record<string, string>;
405
528
  } {
406
529
  const expanded = denseOwnArrayValues(expandedValue);
407
530
  const protectedRefs = denseOwnArrayValues(protectedValue);
@@ -419,6 +542,9 @@ export function validateV2ProjectionFields(
419
542
  throw new Error("Invalid active-context v2 tool-fold cadence");
420
543
  }
421
544
  const leases = leasesValue === undefined ? {} : parseLeases(leasesValue);
545
+ const surfacing = surfacingValue === undefined ? [] : parseSurfacingLog(surfacingValue);
546
+ const pendingMarks = pendingMarksValue === undefined ? [] : parsePendingMarks(pendingMarksValue);
547
+ const briefs = briefsValue === undefined ? {} : parseBriefOverrides(briefsValue);
422
548
  return {
423
549
  expanded: clone(expanded) as string[],
424
550
  protected: clone(protectedRefs) as EvidenceRef[],
@@ -428,6 +554,9 @@ export function validateV2ProjectionFields(
428
554
  }),
429
555
  tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
430
556
  leases,
557
+ surfacing,
558
+ pendingMarks,
559
+ briefs,
431
560
  };
432
561
  }
433
562
 
@@ -439,10 +568,26 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
439
568
  Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold"));
440
569
  const hasLeases = Boolean(value && typeof value === "object" &&
441
570
  Object.prototype.hasOwnProperty.call(value, "leases"));
571
+ const hasSurfacing = Boolean(value && typeof value === "object" &&
572
+ Object.prototype.hasOwnProperty.call(value, "surfacing"));
573
+ const hasPendingMarks = Boolean(value && typeof value === "object" &&
574
+ Object.prototype.hasOwnProperty.call(value, "pendingMarks"));
575
+ // Accepted, never read, never written. `pinnedPeeks` pinned a peek result against
576
+ // per-projection reclamation, which was deleted after rep 22 measured it rewriting
577
+ // the prefix mid-window. State written before that cut still carries the key, and
578
+ // rejecting it here would suspend automatic management on upgrade.
579
+ const hasPinnedPeeks = Boolean(value && typeof value === "object" &&
580
+ Object.prototype.hasOwnProperty.call(value, "pinnedPeeks"));
581
+ const hasBriefs = Boolean(value && typeof value === "object" &&
582
+ Object.prototype.hasOwnProperty.call(value, "briefs"));
442
583
  const optionalKeys = [
443
584
  ...(hasAdvisory ? ["advisory"] : []),
444
585
  ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
445
586
  ...(hasLeases ? ["leases"] : []),
587
+ ...(hasSurfacing ? ["surfacing"] : []),
588
+ ...(hasPendingMarks ? ["pendingMarks"] : []),
589
+ ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
590
+ ...(hasBriefs ? ["briefs"] : []),
446
591
  ];
447
592
  const checkpoint = kind === "checkpoint" &&
448
593
  exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
@@ -457,6 +602,7 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
457
602
  validateV2ProjectionFields(
458
603
  ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
459
604
  ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
605
+ ownValue(value, "surfacing"), ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
460
606
  );
461
607
  if (checkpoint) {
462
608
  const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
@@ -521,7 +667,7 @@ export function stateFromFoldRefs(
521
667
  wire: Pick<
522
668
  ActiveContextCheckpointV2,
523
669
  "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
524
- "tokensSinceToolFold" | "leases"
670
+ "tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs"
525
671
  >,
526
672
  refs: FoldRecordRef[],
527
673
  records: Map<string, FoldRecordEntry>,
@@ -541,6 +687,9 @@ export function stateFromFoldRefs(
541
687
  protected: clone(wire.protected),
542
688
  tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
543
689
  leases: clone(wire.leases ?? {}),
690
+ ...(wire.surfacing?.length ? { surfacing: clone(wire.surfacing) } : {}),
691
+ ...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
692
+ ...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
544
693
  ...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
545
694
  ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
546
695
  };
@@ -690,6 +839,9 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
690
839
  prepared: state.prepared ? clone(state.prepared) : null,
691
840
  tokensSinceToolFold: state.tokensSinceToolFold,
692
841
  leases: clone(state.leases),
842
+ ...(state.surfacing?.length ? { surfacing: clone(state.surfacing) } : {}),
843
+ ...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
844
+ ...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
693
845
  ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
694
846
  stateSha256: semanticStateSha256(state),
695
847
  }, state.sessionId) as ActiveContextCheckpointV2;
@@ -724,6 +876,9 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
724
876
  prepared: next.prepared ? clone(next.prepared) : null,
725
877
  tokensSinceToolFold: next.tokensSinceToolFold,
726
878
  leases: clone(next.leases),
879
+ ...(next.surfacing?.length ? { surfacing: clone(next.surfacing) } : {}),
880
+ ...(next.pendingMarks?.length ? { pendingMarks: clone(next.pendingMarks) } : {}),
881
+ ...(next.briefs && Object.keys(next.briefs).length ? { briefs: clone(next.briefs) } : {}),
727
882
  ...(next.advisory ? { advisory: clone(next.advisory) } : {}),
728
883
  stateSha256: semanticStateSha256(next),
729
884
  }, next.sessionId) as ActiveContextDeltaV2;
@@ -738,6 +893,7 @@ export function protectionSha256(state: Pick<ActiveContextState, "protected">):
738
893
  }
739
894
 
740
895
  export const ADVISORY_BUDGETS: Readonly<Record<AdvisoryMilestone, number>> = Object.freeze({
896
+ orientation: 1,
741
897
  notice: 2,
742
898
  tools: 2,
743
899
  chapters: 2,