pi-fold 0.1.1 → 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,7 @@ import {
22
22
  EXPAND_LEASE_GENERATIONS,
23
23
  MAX_ADVISORY_DELIVERIES_PER_MILESTONE,
24
24
  MAX_EXPAND_LEASES,
25
+ MAX_PENDING_MARKS,
25
26
  SURFACING_MAX_LOG_RECORDS,
26
27
  } from "./policy.ts";
27
28
  import type {
@@ -36,6 +37,8 @@ import type {
36
37
  FoldPart,
37
38
  FoldRecordEntry,
38
39
  FoldRecordRef,
40
+ MarkOrigin,
41
+ PendingMark,
39
42
  PreparedFold,
40
43
  SurfacingRecord,
41
44
  } from "./policy.ts";
@@ -53,6 +56,11 @@ export const FOLD_RECORD_REF_KEYS = ["id", "sha256"] as const;
53
56
  export const FOLD_RECORD_ENTRY_KEYS = ["version", "sessionId", "foldId", "recordSha256", "fold"] as const;
54
57
  export const SURFACING_RECORD_KEYS = ["source", "id", "score", "ordinal", "outcome"] as const;
55
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);
56
64
  export const STATE_CHECKPOINT_V2_KEYS = [
57
65
  "version", "kind", "sessionId", "revision", "foldRefs", "expanded", "protected", "prepared", "stateSha256",
58
66
  ] as const;
@@ -89,6 +97,67 @@ export function parseSurfacingLog(value: unknown): SurfacingRecord[] {
89
97
  return clone(records) as SurfacingRecord[];
90
98
  }
91
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
+
92
161
  export function parseLeases(value: unknown, foldIds?: ReadonlySet<string>): Record<string, number> {
93
162
  if (!isPlainRecord(value)) throw new Error("Invalid active-context expand leases");
94
163
  const entries = Object.entries(value);
@@ -258,12 +327,22 @@ export function parseActiveContextState(
258
327
  Object.prototype.hasOwnProperty.call(value, "tokensSinceToolFold");
259
328
  const hasLeases = recordLike && Object.prototype.hasOwnProperty.call(value, "leases");
260
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");
261
337
  const extraKeys = [
262
338
  ...(hasPrepared ? ["prepared"] : []),
263
339
  ...(hasAdvisory ? ["advisory"] : []),
264
340
  ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
265
341
  ...(hasLeases ? ["leases"] : []),
266
342
  ...(hasSurfacing ? ["surfacing"] : []),
343
+ ...(hasPendingMarks ? ["pendingMarks"] : []),
344
+ ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
345
+ ...(hasBriefs ? ["briefs"] : []),
267
346
  ];
268
347
  if (!exactRecord(value, [...ACTIVE_STATE_KEYS, ...extraKeys])) throw new Error("Invalid active-context state keys");
269
348
  const folds = denseOwnArrayValues(ownValue(value, "folds"));
@@ -290,6 +369,11 @@ export function parseActiveContextState(
290
369
  }
291
370
  const leases = hasLeases ? parseLeases(ownValue(value, "leases"), ids) : {};
292
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) : {};
293
377
  const source = clone(value) as unknown as ActiveContextState;
294
378
  // Provenance normalization is presentation-only; never mutate a durable
295
379
  // content-addressed fold record: changing its bytes causes the next re-persist
@@ -308,6 +392,8 @@ export function parseActiveContextState(
308
392
  // Omitted when empty so every state written before surfacing existed keeps its
309
393
  // exact digest; the replay digest is order- and presence-sensitive by design.
310
394
  ...(surfacing.length ? { surfacing } : {}),
395
+ ...(marks.length ? { pendingMarks: marks } : {}),
396
+ ...(Object.keys(briefs).length ? { briefs } : {}),
311
397
  ...(hasAdvisory
312
398
  ? { advisory: clone(source.advisory!) }
313
399
  : defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
@@ -384,6 +470,8 @@ export function sameStateProjection(left: ActiveContextState, right: ActiveConte
384
470
  if (normalizedState.tokensSinceToolFold === 0) delete normalizedState.tokensSinceToolFold;
385
471
  if (normalizedState.leases && Object.keys(normalizedState.leases).length === 0) delete normalizedState.leases;
386
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;
387
475
  return normalizedState;
388
476
  };
389
477
  return stableStringify(normalized(left)) === stableStringify(normalized(right));
@@ -425,6 +513,8 @@ export function validateV2ProjectionFields(
425
513
  tokensSinceToolFoldValue: unknown,
426
514
  leasesValue: unknown,
427
515
  surfacingValue?: unknown,
516
+ pendingMarksValue?: unknown,
517
+ briefsValue?: unknown,
428
518
  ): {
429
519
  expanded: string[];
430
520
  protected: EvidenceRef[];
@@ -433,6 +523,8 @@ export function validateV2ProjectionFields(
433
523
  tokensSinceToolFold: number;
434
524
  leases: Record<string, number>;
435
525
  surfacing: SurfacingRecord[];
526
+ pendingMarks: PendingMark[];
527
+ briefs: Record<string, string>;
436
528
  } {
437
529
  const expanded = denseOwnArrayValues(expandedValue);
438
530
  const protectedRefs = denseOwnArrayValues(protectedValue);
@@ -451,6 +543,8 @@ export function validateV2ProjectionFields(
451
543
  }
452
544
  const leases = leasesValue === undefined ? {} : parseLeases(leasesValue);
453
545
  const surfacing = surfacingValue === undefined ? [] : parseSurfacingLog(surfacingValue);
546
+ const pendingMarks = pendingMarksValue === undefined ? [] : parsePendingMarks(pendingMarksValue);
547
+ const briefs = briefsValue === undefined ? {} : parseBriefOverrides(briefsValue);
454
548
  return {
455
549
  expanded: clone(expanded) as string[],
456
550
  protected: clone(protectedRefs) as EvidenceRef[],
@@ -461,6 +555,8 @@ export function validateV2ProjectionFields(
461
555
  tokensSinceToolFold: tokensSinceToolFoldValue === undefined ? 0 : Number(tokensSinceToolFoldValue),
462
556
  leases,
463
557
  surfacing,
558
+ pendingMarks,
559
+ briefs,
464
560
  };
465
561
  }
466
562
 
@@ -474,11 +570,24 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
474
570
  Object.prototype.hasOwnProperty.call(value, "leases"));
475
571
  const hasSurfacing = Boolean(value && typeof value === "object" &&
476
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"));
477
583
  const optionalKeys = [
478
584
  ...(hasAdvisory ? ["advisory"] : []),
479
585
  ...(hasTokensSinceToolFold ? ["tokensSinceToolFold"] : []),
480
586
  ...(hasLeases ? ["leases"] : []),
481
587
  ...(hasSurfacing ? ["surfacing"] : []),
588
+ ...(hasPendingMarks ? ["pendingMarks"] : []),
589
+ ...(hasPinnedPeeks ? ["pinnedPeeks"] : []),
590
+ ...(hasBriefs ? ["briefs"] : []),
482
591
  ];
483
592
  const checkpoint = kind === "checkpoint" &&
484
593
  exactRecord(value, [...STATE_CHECKPOINT_V2_KEYS, ...optionalKeys]);
@@ -493,7 +602,7 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
493
602
  validateV2ProjectionFields(
494
603
  ownValue(value, "expanded"), ownValue(value, "protected"), ownValue(value, "prepared"),
495
604
  ownValue(value, "advisory"), ownValue(value, "tokensSinceToolFold"), ownValue(value, "leases"),
496
- ownValue(value, "surfacing"),
605
+ ownValue(value, "surfacing"), ownValue(value, "pendingMarks"), ownValue(value, "briefs"),
497
606
  );
498
607
  if (checkpoint) {
499
608
  const refs = denseOwnArrayValues(ownValue(value, "foldRefs"));
@@ -558,7 +667,7 @@ export function stateFromFoldRefs(
558
667
  wire: Pick<
559
668
  ActiveContextCheckpointV2,
560
669
  "sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
561
- "tokensSinceToolFold" | "leases" | "surfacing"
670
+ "tokensSinceToolFold" | "leases" | "surfacing" | "pendingMarks" | "briefs"
562
671
  >,
563
672
  refs: FoldRecordRef[],
564
673
  records: Map<string, FoldRecordEntry>,
@@ -579,6 +688,8 @@ export function stateFromFoldRefs(
579
688
  tokensSinceToolFold: wire.tokensSinceToolFold ?? 0,
580
689
  leases: clone(wire.leases ?? {}),
581
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) } : {}),
582
693
  ...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
583
694
  ...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
584
695
  };
@@ -729,6 +840,8 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
729
840
  tokensSinceToolFold: state.tokensSinceToolFold,
730
841
  leases: clone(state.leases),
731
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) } : {}),
732
845
  ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
733
846
  stateSha256: semanticStateSha256(state),
734
847
  }, state.sessionId) as ActiveContextCheckpointV2;
@@ -764,6 +877,8 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
764
877
  tokensSinceToolFold: next.tokensSinceToolFold,
765
878
  leases: clone(next.leases),
766
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) } : {}),
767
882
  ...(next.advisory ? { advisory: clone(next.advisory) } : {}),
768
883
  stateSha256: semanticStateSha256(next),
769
884
  }, next.sessionId) as ActiveContextDeltaV2;
@@ -41,8 +41,25 @@ export const PROVIDER_CONTEXT_MEASUREMENT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-pr
41
41
  export const ACTIVE_CONTEXT_STATUS_KEY = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
42
42
  export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
43
43
  "status", "peek", "fold", "expand", "refold", "protect", "unprotect",
44
+ // The correction verbs. Curation the agent cannot fix afterwards is curation it
45
+ // will not risk making, so re-briefing a fold and dissolving a mis-cut boundary
46
+ // are part of the ordinary surface, not an epoch-only extra.
47
+ "rebrief", "reboundary",
44
48
  ] as const);
45
- export type ActiveContextToolAction = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
49
+ /**
50
+ * The epoch surface: the immediate actions plus `unmark`. A mark is a standing decision
51
+ * rather than a one-shot attempt, so withdrawing one needs a verb of its own.
52
+ *
53
+ * There is deliberately NO agent-callable commit verb. Marking is the agent's job and
54
+ * folding is the runtime's, and a verb the runtime is entitled to overrule is surface
55
+ * without function: measured 2026-08-07 (rep 17), the agent called commit twice and the
56
+ * runtime correctly held it both times. The internal commit paths -- the epoch commit,
57
+ * the gated commit, the fence and the recovery lane -- are unchanged and unexposed.
58
+ */
59
+ export const EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
60
+ ...ACTIVE_CONTEXT_TOOL_ACTIONS, "unmark",
61
+ ] as const);
62
+ export type ActiveContextToolAction = typeof EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS[number];
46
63
  export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
47
64
  export const DEFAULT_CONTEXT_WINDOW = 272_000;
48
65
  export const TOOL_FOLD_CADENCE_MIN_TOKENS = 12_000;
@@ -75,6 +92,285 @@ export const SURFACING_SOURCE_ID = "fold-brief";
75
92
  // the protected byte tail as a share of a small window; never to estimate usage.
76
93
  export const BYTES_PER_TOKEN_FLOOR = 2;
77
94
 
95
+ // Two-phase fold scheduling. A provider prefix cache is positional: any mid-window
96
+ // edit invalidates every byte after it, so the cost of folding is dominated by how
97
+ // OFTEN the projection changes, not by how much it saves. In "epoch" mode a fold
98
+ // decision becomes a free MARK and a later COMMIT applies every pending mark in one
99
+ // rewrite. "immediate" is the default and is byte-identical to pre-0.1.2 behavior.
100
+ export const FOLD_SCHEDULING_MODES = Object.freeze(["immediate", "epoch"] as const);
101
+ export type FoldSchedulingMode = typeof FOLD_SCHEDULING_MODES[number];
102
+ export const DEFAULT_FOLD_SCHEDULING: FoldSchedulingMode = "immediate";
103
+
104
+ // Epoch knobs stay INTERNAL constants for the same reason the surfacing knobs do:
105
+ // they are what the round-2 cost measurement exists to settle, and an option surface
106
+ // fixed before that data would freeze a guess.
107
+ /**
108
+ * An automatic commit tops up with stale tool batches until it would free this share
109
+ * of the window. It is a FLOOR under an epoch's accumulated marks, not the driver of
110
+ * one, so it is set where a commit is unambiguously worth its single rewrite.
111
+ */
112
+ export const EPOCH_COMMIT_TARGET_WINDOW_SHARE = 0.40;
113
+ /**
114
+ * Commit when the ELIGIBLE marked mass is worth its one rewrite. Window pressure stays
115
+ * underneath as the safety backstop; this is the economic trigger above it.
116
+ */
117
+ export const EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD = 0.30;
118
+ /**
119
+ * The reclaim floor, and the single constant that decides whether a commit is worth
120
+ * firing at all.
121
+ *
122
+ * Measured on memex (19,082 calls, 254 sessions): a token-weighted pooled cache share
123
+ * of 89.9% across two provider wires, with steady-state requests at 91.6% and any
124
+ * request following a mutation at 15.7%. A fold costs exactly what one user message
125
+ * costs. Their mutations are 5.3% of requests; rep 14's were about 34%, and that
126
+ * FREQUENCY difference is the entire gap between their 90% and our 64% -- the
127
+ * per-mutation penalty is the same on both.
128
+ *
129
+ * So a commit that would free less than this share of the truthful budget does not
130
+ * fire: it defers and accumulates until it is worth a full prefix. Safety outranks
131
+ * economics, so the hard fence and overflow recovery fire regardless.
132
+ */
133
+ export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
134
+
135
+ /**
136
+ * The thermostat's lower line.
137
+ *
138
+ * The floor above says what is worth firing; this says how DEEP a firing goes. The
139
+ * failure shape it removes is rep 13's crumb ratchet: a trigger that fires, frees a
140
+ * little, and re-fires almost immediately, so the window climbs a staircase of
141
+ * mutations. Firing at the trigger line and folding down to a target line makes the
142
+ * spacing between events structural -- the next event cannot arrive until inflow has
143
+ * refilled the gap -- so the window saws instead of climbing.
144
+ *
145
+ * The gap here is 15 points of a truthful budget. Rep 13's largest measured inflow step
146
+ * was 27,815 tokens against a 400,000 window, about 7 points, so at least two full
147
+ * worst-case steps must land before the trigger can re-arm, and the agent keeps roughly
148
+ * two thirds of its budget as working room after every event. Eligibility still binds:
149
+ * if the eligible mass cannot reach the target, the commit takes what it may and
150
+ * records the shortfall rather than loosening a protection to hit a number.
151
+ */
152
+ export const CURATION_TARGET_OCCUPANCY_SHARE = 0.35;
153
+ export const MAX_PENDING_MARKS = 256;
154
+ /** Enough batches to actually reach the floor on a wide window before the loop exits. */
155
+ export const EPOCH_MAX_TOPUP_MARKS = 64;
156
+ /** Estimate only; provider token accounting always comes from a measured response. */
157
+ export const ESTIMATED_BYTES_PER_TOKEN = 4;
158
+ /** Rendered navigation/topology overhead assumed around a brief when estimating a placeholder. */
159
+ export const ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES = 240;
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // The reliability spine. Every behavior below shipped as an off-by-default lever
163
+ // through iterations 2 and 3 and was sealed ON by rep 14 (64/64), so it is now
164
+ // simply how pi-fold works: the flags and their conditional twins are gone, and
165
+ // git history is the lineage. What stays configurable is only what is genuinely a
166
+ // deployment fact (`providerTotalWindow`) or an experiment condition (`guidance`,
167
+ // `foldScheduling`, `foldPeekResults`, `guidedCuration`).
168
+ //
169
+ // Sealed unconditional: admission control, retained pending marks, the eligible-share
170
+ // commit trigger, stage-identified briefs, the current-turn commit guard, the
171
+ // pinned-mass backstop, the status index diet, delivery-counted advisories, and
172
+ // projection instrumentation.
173
+ //
174
+ // NOT sealed, deleted: ephemeral peek and its per-call `ephemeral` override. It rewrote
175
+ // a consumed peek result in place on the theory the edit was tail-local. It is not: the
176
+ // rewrite waits for a later assistant message to exist, by which point the window has
177
+ // grown over it and the edit lands mid-prefix. Rep 22 measured two of them costing 100k
178
+ // fresh tokens. A peek is append-only; the tool-fold rung reclaims the duplicate bytes
179
+ // at the next commit, which is the one moment a rewrite is already being paid for.
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /**
183
+ * The provider serving window is the TOTAL admission budget minus whatever output
184
+ * reservation the deployment actually asked for; the per-request max-input descriptor
185
+ * assumes a full output reservation and understates the real ceiling. Measured
186
+ * 2026-08-06: a run aborted at ~297k projected tokens against a 272k descriptor while
187
+ * the same provider had just accepted 339,689 tokens. Declaring the total window is
188
+ * the deployment's own fact, so it is the option; there is no separate boolean, and
189
+ * an undeclared window falls back to the descriptor and SAYS so in the accounting.
190
+ */
191
+
192
+ /** Narrowing is what makes a refusal governance instead of denial. */
193
+ export const PEEK_MIN_SLICE_BYTES = 1_024;
194
+
195
+ /** How many folds the dieted status payload ranks by what they would reclaim. */
196
+ export const STATUS_DIET_SUGGESTIONS = 5;
197
+
198
+ export const MAX_PROJECTION_HASH_RECORDS = 64;
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Guided curation. The one iteration-4 experiment condition: the two-signal
202
+ // curation trigger, its bounded last-call gate, and the reactive receipt block.
203
+ // ---------------------------------------------------------------------------
204
+
205
+ export const DEFAULT_GUIDED_CURATION = false;
206
+
207
+ /**
208
+ * Signal one: measured occupancy of the truthful serving budget.
209
+ *
210
+ * Four fifths, not half (Shane RULING 2026-08-06, after rep 15): below this line the
211
+ * runtime is QUIET -- nothing folds automatically at all -- so the threshold is where
212
+ * the fold event should happen, and the remaining fifth of the budget is the runway the
213
+ * gate rounds and the commit itself spend. An earlier line buys announcement room by
214
+ * spending window the agent still wants for the task.
215
+ */
216
+ export const CURATION_OCCUPANCY_SHARE = 0.80;
217
+
218
+ /**
219
+ * The two sparse reminders, as shares of the truthful serving budget.
220
+ *
221
+ * Exactly two per window cycle, one line each, informatory: mark chapters as you go so
222
+ * the folds are neat when the fold event triggers. They are the ONLY pre-gate guidance
223
+ * in guided curation -- recurring pressure nags trained rep 15's agent to ignore the
224
+ * one message that mattered -- and they re-arm after each commit cycle, so a window that
225
+ * climbs twice is reminded twice.
226
+ */
227
+ export const CURATION_REMINDER_SHARES: readonly number[] = Object.freeze([0.45, 0.65]);
228
+
229
+ /**
230
+ * Signal two: tool-result mass OUTSIDE the fresh tail, as a share of the window.
231
+ * Occupancy alone announces commits a commit cannot serve -- a window full of
232
+ * conversation has nothing stale to fold. Rep 14 ran 138 tool results over ~4.2MB of
233
+ * payload against a 400k window, so a fifth of the window (~80k tokens, ~320KB) is
234
+ * roughly a dozen ordinary results: reached early in a tool-heavy run, and never
235
+ * reached by a run whose window is not actually made of stale tool output. It is half
236
+ * the commit's own top-up target, so the announcement always precedes a commit that
237
+ * has at least half a target of real mass to work with.
238
+ */
239
+ export const CURATION_STALE_TOOL_SHARE = 0.20;
240
+
241
+ /**
242
+ * The gate's hard round cap. Every context pass with the gate open consumes one round,
243
+ * so the commit proceeds after at most this many passes NO MATTER WHAT the agent does;
244
+ * a pass whose response was not a context-tool call proceeds immediately. Two is a
245
+ * bounded conversation, not a negotiation, and it makes a stall non-constructible.
246
+ */
247
+ export const CURATION_GATE_MAX_ROUNDS = 2;
248
+
249
+ /** How many automatic-action receipts stay in the window; the oldest ages out. */
250
+ export const MAX_CONTEXT_RECEIPTS = 3;
251
+ /** Hard byte cap on the rendered receipt block, so a receipt can never itself bloat. */
252
+ export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
253
+ /** Durable instrumentation records kept in the in-memory ledger for status. */
254
+ export const MAX_CONTEXT_ATTEMPT_RECORDS = 128;
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // The mark response.
258
+ //
259
+ // Measured 2026-08-07 (rep 17): every context action was followed by a projection
260
+ // REWRITE, never an append, because the surface answered a mark by refreshing blocks
261
+ // the agent had already paid to cache. A tail-appended tool result is the one place
262
+ // left that can inform the agent for free, so the awareness moved THERE and the
263
+ // projection went byte-frozen between fold events. The result is bounded on the same
264
+ // principle as the receipt block: a report about bloat that becomes bloat has argued
265
+ // against itself.
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /** Hard byte cap on the rendered awareness block a mark call answers with. */
269
+ export const CONTEXT_MARK_RESPONSE_BYTES = 900;
270
+ /**
271
+ * Hard byte cap on one serialized status payload, every detail variant included.
272
+ * Measured 2026-08-07 (rep 19): six paged status results accumulated to ~254k tokens
273
+ * and the seventh answered with 526KB, 6.2x the remaining headroom, aborting the
274
+ * request. Paging already existed; nothing FORCED it. Above the cap the listing is
275
+ * truncated at a unit boundary and a continuation names the next offset, so the
276
+ * biggest possible status answer is about 6k tokens and the rest is another page.
277
+ */
278
+ export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
279
+ /** How many unmarked candidates the awareness block names; the remainder is an aggregate. */
280
+ export const MAX_UNMARKED_CANDIDATES = 3;
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Bite-sized folds and bounded peeks. These two constants are one decision read
284
+ // from two ends, so they are stated together and kept equal.
285
+ //
286
+ // Measured 2026-08-06 (rep 6): a single 60,432-byte chapter fold hid the needed
287
+ // fact in its tail, and every peek of it was either truncated short of the answer
288
+ // or too expensive to widen. A fold is only navigable if reading one back is cheap,
289
+ // so a fold's source is capped and an oversized span is SPLIT into sequential
290
+ // bounded folds, each with its own brief. The peek bound is the same number, so a
291
+ // default peek of one bite-sized fold returns it whole -- about 4,000 tokens, a
292
+ // read any session can afford -- and only a legacy oversized fold is ever truncated.
293
+ // ---------------------------------------------------------------------------
294
+
295
+ /** A fold whose exact source exceeds this is split into sequential bounded folds. */
296
+ export const MAX_FOLD_SPAN_CHARS = 16_000;
297
+ /**
298
+ * The other bound: a SLIVER of stale raw content hugging a fold boundary is absorbed
299
+ * into its later neighbour inside a commit the epoch already paid for.
300
+ *
301
+ * The threshold is deliberately tiny, measured in the session's own calibrated TOKENS,
302
+ * and it is not pressure-scaled. Non-sequential curation is a first-class state: an
303
+ * agent may hold folds at 10:20, 40:55 and 60:70 with raw spans between them ON
304
+ * PURPOSE, and a mechanism that swallowed those gaps would quietly turn pi-fold into
305
+ * system-controlled compaction with the curation element deleted. 256 tokens is about
306
+ * one short prompt or one one-line result -- nobody deliberately keeps a sliver that
307
+ * small as standing context -- and it sits an order of magnitude under the minimum
308
+ * chapter size the ladder itself will fold. Anything larger stays raw permanently,
309
+ * however ragged the projection looks; the ladder's ordinary fold eligibility is the
310
+ * only thing that ever touches it, and absorption never becomes a second folding path.
311
+ *
312
+ * Later, not earlier, because extending the LATER fold backward mutates at a shallower
313
+ * prefix position and preserves more of the cache. There is no content-affinity logic,
314
+ * every absorption is receipted, and a wrong grouping is one `reboundary` away.
315
+ */
316
+ export const MAX_WEDGE_ABSORB_TOKENS = 256;
317
+ /** What a peek returns without an explicit widening argument. */
318
+ export const PEEK_DEFAULT_MAX_BYTES = 16_000;
319
+ /** Head share of a truncated peek; the remainder is the tail, where conclusions live. */
320
+ export const PEEK_HEAD_SHARE = 0.6;
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Overflow rollback and recovery.
324
+ // ---------------------------------------------------------------------------
325
+
326
+ /**
327
+ * How many times one inflow may be recovered before the runtime gives up.
328
+ *
329
+ * A provider context-overflow rejection mutates nothing durable -- the assistant
330
+ * message never lands and the projection is rebuilt per request -- so recovery is a
331
+ * reduction plus a rebuild, not a branch rewind. Two attempts is the cap: the first
332
+ * folds at fence pressure with the guard fully waived, the second reaches into the
333
+ * fresh tail, and a third would be asking the same question of the same evidence.
334
+ * Past it the run fails LOUDLY, because an inflow that cannot fit after maximal
335
+ * folding is a genuine impossibility rather than a recoverable state.
336
+ */
337
+ export const OVERFLOW_RECOVERY_MAX_ATTEMPTS = 2;
338
+
339
+ /** Active-context tool actions that read without mutating, so their results may fold. */
340
+ export const READ_ONLY_CONTEXT_ACTIONS_DEFAULT: ReadonlySet<string> = new Set(["status"]);
341
+ /**
342
+ * The classification that also lets a `peek` result be reclaimed by the tool-fold rung.
343
+ * A peek copies a fold's own stored source back into the window, so leaving it raw is a
344
+ * pure duplicate of evidence the runtime already holds; epoch scheduling adopts this set
345
+ * unconditionally, and immediate scheduling reaches it through `foldPeekResults`.
346
+ */
347
+ export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
348
+
349
+ export type MarkOrigin = "agent" | "ladder";
350
+
351
+ /** A fold decided but not yet applied: no projection byte has moved for it. */
352
+ export interface PendingFoldMark {
353
+ mark: "fold";
354
+ id: string;
355
+ kind: FoldKind;
356
+ parts: FoldPart[];
357
+ brief: string;
358
+ briefProvenance: BriefProvenance;
359
+ origin: MarkOrigin;
360
+ /** Transcript ordinal at mark time; orders the commit. Never a wall clock. */
361
+ ordinal: number;
362
+ }
363
+
364
+ /** An expanded fold decided to return to its placeholder at the next commit. */
365
+ export interface PendingRefoldMark {
366
+ mark: "refold";
367
+ id: string;
368
+ origin: MarkOrigin;
369
+ ordinal: number;
370
+ }
371
+
372
+ export type PendingMark = PendingFoldMark | PendingRefoldMark;
373
+
78
374
  export const ACTIVE_CONTEXT_POLICY = Object.freeze({
79
375
  freshTurns: 3,
80
376
  freshBytes: 24_000,
@@ -190,6 +486,17 @@ export interface ActiveContextState {
190
486
  tokensSinceToolFold: number;
191
487
  leases: Record<string, number>;
192
488
  surfacing?: SurfacingRecord[];
489
+ /** Epoch scheduling only; omitted when empty so immediate-mode state digests never move. */
490
+ pendingMarks?: PendingMark[];
491
+ /** Retired with peek reclamation; carried only so pre-cut state still parses. */
492
+ pinnedPeeks?: never;
493
+ /**
494
+ * Agent-corrected fold briefs, by fold id. A fold RECORD is content-addressed and
495
+ * immutable -- rewriting its brief in place would report a conflicting durable fold
496
+ * and suspend automatic management -- so a re-brief is durable state beside the
497
+ * record rather than a mutation of it. Omitted when empty.
498
+ */
499
+ briefs?: Record<string, string>;
193
500
  prepared?: PreparedFold;
194
501
  advisory?: {
195
502
  highWater: number;
@@ -223,6 +530,8 @@ export interface ActiveContextCheckpointV2 {
223
530
  tokensSinceToolFold?: number;
224
531
  leases?: Record<string, number>;
225
532
  surfacing?: SurfacingRecord[];
533
+ pendingMarks?: PendingMark[];
534
+ briefs?: Record<string, string>;
226
535
  advisory?: NonNullable<ActiveContextState["advisory"]>;
227
536
  stateSha256: string;
228
537
  }
@@ -242,6 +551,8 @@ export interface ActiveContextDeltaV2 {
242
551
  tokensSinceToolFold?: number;
243
552
  leases?: Record<string, number>;
244
553
  surfacing?: SurfacingRecord[];
554
+ pendingMarks?: PendingMark[];
555
+ briefs?: Record<string, string>;
245
556
  advisory?: NonNullable<ActiveContextState["advisory"]>;
246
557
  stateSha256: string;
247
558
  }
@@ -279,6 +590,8 @@ export interface ActiveContextSnapshot {
279
590
  brandNoun: string;
280
591
  entryTypePrefix: string;
281
592
  readOnlyTools: ReadonlySet<string>;
593
+ /** Active-context tool actions treated as read-only when classifying tool batches. */
594
+ readOnlyContextActions: ReadonlySet<string>;
282
595
  contextWindow: number;
283
596
  windowSource: "reported" | "fallback";
284
597
  }