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.
@@ -1,16 +1,25 @@
1
1
  import type { EvidenceRef } from "../json.ts";
2
2
 
3
- export const DEFAULT_ACTIVE_CONTEXT_TOOL_NAME = "active_context";
3
+ /**
4
+ * THE deployment identity. One package, one brand, no constructor options.
5
+ *
6
+ * These were five public knobs through 1.0.2, on the theory that the runtime was a
7
+ * generic framework a host would brand. It is not: it is a branded package with one
8
+ * deployment, and the knobs bought nothing but a namespace a consumer could strand its
9
+ * own durable state under. They are hardwired here and REFUSED by name at the package
10
+ * entry. An internal seam survives on `registerActiveContext`, because the neutrality
11
+ * gate has to register a synthetic brand to prove none of it leaks into these defaults,
12
+ * and the experiment harness registers this same identity explicitly so the sealed runs
13
+ * keep their entry types.
14
+ */
15
+ export const DEFAULT_ACTIVE_CONTEXT_TOOL_NAME = "pi_fold_context";
4
16
  export const DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX = "pi-fold-active-context";
5
- export const DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL = "Active Context";
6
- export const DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN = "active-context";
17
+ export const DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL = "pi-fold Active Context";
18
+ export const DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN = "pi-fold";
7
19
  export const DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES = Object.freeze({
8
- status: "context",
20
+ status: "pi-fold-context",
9
21
  fold: "fold-context",
10
22
  });
11
- export const GUIDANCE_PROFILES = Object.freeze(["pressure", "curation", "minimal"] as const);
12
- export type GuidanceProfile = typeof GUIDANCE_PROFILES[number];
13
- export const DEFAULT_GUIDANCE_PROFILE: GuidanceProfile = "pressure";
14
23
 
15
24
  export function entryTypeNamespace(entryTypePrefix: string): string {
16
25
  const suffix = "-active-context";
@@ -39,16 +48,13 @@ export const NATIVE_COMPACTION_RECEIPT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-nativ
39
48
  export const NATIVE_COMPACTION_DECISION_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-native-compaction-decision`;
40
49
  export const PROVIDER_CONTEXT_MEASUREMENT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-provider-context-measurement`;
41
50
  export const ACTIVE_CONTEXT_STATUS_KEY = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
42
- export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
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",
48
- ] as const);
49
51
  /**
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
+ * The action surface, whole. There was a second, narrower list while immediate
53
+ * scheduling existed; epoch is the only scheduler now, so marks always exist and
54
+ * `unmark` is an ordinary verb: a mark is a standing decision rather than a one-shot
55
+ * attempt, so withdrawing one needs a verb of its own. The correction verbs are here
56
+ * for the same reason: curation the agent cannot fix afterwards is curation it will
57
+ * not risk making.
52
58
  *
53
59
  * There is deliberately NO agent-callable commit verb. Marking is the agent's job and
54
60
  * folding is the runtime's, and a verb the runtime is entitled to overrule is surface
@@ -56,65 +62,74 @@ export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
56
62
  * runtime correctly held it both times. The internal commit paths -- the epoch commit,
57
63
  * the gated commit, the fence and the recovery lane -- are unchanged and unexposed.
58
64
  */
59
- export const EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
60
- ...ACTIVE_CONTEXT_TOOL_ACTIONS, "unmark",
65
+ export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
66
+ "status", "peek", "fold", "expand", "refold", "protect", "unprotect",
67
+ "rebrief", "reboundary", "unmark",
61
68
  ] as const);
62
- export type ActiveContextToolAction = typeof EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS[number];
69
+ export type ActiveContextToolAction = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
63
70
  export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
64
71
  export const DEFAULT_CONTEXT_WINDOW = 272_000;
65
- export const TOOL_FOLD_CADENCE_MIN_TOKENS = 12_000;
66
- export const TOOL_FOLD_CADENCE_WINDOW_FRACTION = 0.06;
67
72
  export const EXPAND_LEASE_GENERATIONS = 8;
68
73
  export const MAX_EXPAND_LEASES = 64;
69
- export const CONSOLIDATION_WIDTH_THRESHOLD = 10;
70
74
  export const MAX_ADVISORY_DELIVERIES_PER_MILESTONE = 16;
71
75
 
72
- // Ephemeral surfacing structure. These stay INTERNAL constants rather than public
76
+ // Surfacing SELECTOR structure. These stay INTERNAL constants rather than public
73
77
  // options: they are the knobs the surfacing experiment is meant to settle, and an
74
- // option surface fixed before the accept/reject data exists would freeze a guess.
75
- export const DEFAULT_SURFACING_ENABLED = true;
76
- export const SURFACING_TOP_K = 3;
77
- export const SURFACING_MIN_SCORE = 0.30;
78
- export const SURFACING_CHAR_BUDGET = 1_000;
79
- export const SURFACING_COOLDOWN_ORDINALS = 8;
78
+ // option surface fixed before the acted/used/ignored data exists would freeze a guess.
79
+ // The per-request ephemeral carrier that once rendered the slate is deleted, and its
80
+ // enable flag with it; the slate rides the commit-boundary carriers and status only.
81
+
82
+ /** BM25's canonical parameters, and memex's fold lane runs on exactly these. */
83
+ export const SURFACING_BM25_K1 = 1.5;
84
+ export const SURFACING_BM25_B = 0.75;
85
+ /**
86
+ * The divergence trigger, as three numbers on one 0..1 scale.
87
+ *
88
+ * The scale is the share of the query's own saturation ceiling a document reaches, so
89
+ * 1.0 would be every query term saturated in one document and a strong real match lands
90
+ * in the third of the range these numbers sit in. CONTENT_HIT is the cry-wolf guard:
91
+ * below it the fold does not match the task at all and nothing else matters. BRIEF_HIT
92
+ * is the visibility line: at or above it the placeholder already says what the fold
93
+ * holds, so the agent can see it and a suggestion repeats the window back to itself.
94
+ * The MARGIN keeps the pair from meeting in the middle, where both readings are weak
95
+ * and neither says anything.
96
+ */
97
+ export const SURFACING_CONTENT_HIT = 0.25;
98
+ export const SURFACING_BRIEF_HIT = 0.15;
99
+ export const SURFACING_DIVERGENCE_MARGIN = 0.10;
100
+ /** Precision budget, not a rate limit: at most one suggestion per delivery point. */
101
+ export const SURFACING_SLATE_SIZE = 1;
102
+ /** One clock. It is the outcome window AND the cooldown: no re-offer before an answer. */
80
103
  export const SURFACING_OUTCOME_WINDOW_ORDINALS = 12;
81
- export const SURFACING_RECENT_TASK_SPANS = 6;
82
- export const SURFACING_MAX_TASK_CHARS = 12_000;
83
- export const SURFACING_MAX_LOG_RECORDS = 128;
84
- export const SURFACING_LEXICAL_WEIGHT = 0.80;
85
- export const SURFACING_RECENCY_WEIGHT = 0.14;
86
- export const SURFACING_DEPTH_WEIGHT = 0.06;
87
- export const SURFACING_MAX_DEPTH = 4;
88
- export const SURFACING_MAX_TEXT_CHARS = 220;
89
- export const SURFACING_SOURCE_ID = "fold-brief";
104
+ /** Offered this many times and never taken, and the fold leaves the candidate set. */
105
+ export const SURFACING_IGNORE_LIMIT = 2;
106
+ /** Distinct content-only terms downstream that grade an acted suggestion as used. */
107
+ export const SURFACING_PROVENANCE_TERMS = 3;
108
+ export const SURFACING_INTENT_CHARS = 1_200;
109
+ export const SURFACING_INTENT_ARGUMENT_CHARS = 120;
110
+ export const SURFACING_INTENT_RECENCY_SHARE = 0.5;
111
+ export const SURFACING_INTENT_ARGUMENT_KEYS: readonly string[] = Object.freeze([
112
+ "path", "file_path", "query", "pattern", "command", "brief", "id",
113
+ ]);
114
+ export const SURFACING_MAX_CONTENT_CHARS = 20_000;
115
+ export const SURFACING_MAX_LEDGER_RECORDS = 256;
116
+ export const SURFACING_HOOK_CHARS = 160;
90
117
 
91
118
  // Conservative LOWER bound on UTF-8 bytes per provider token, used only to cap
92
119
  // the protected byte tail as a share of a small window; never to estimate usage.
93
120
  export const BYTES_PER_TOKEN_FLOOR = 2;
94
121
 
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";
122
+ // Two-phase fold scheduling, and the only scheduler. A provider prefix cache is
123
+ // positional: any mid-window edit invalidates every byte after it, so the cost of
124
+ // folding is dominated by how OFTEN the projection changes, not by how much it saves.
125
+ // A fold decision is therefore a free MARK, and a later COMMIT applies every pending
126
+ // mark in one rewrite. The alternative shipped as an option through 1.0.2 and was
127
+ // deleted once measured: applying each fold where it was made cost 54 prefix rewrites
128
+ // and a 0.193 pooled cache share against epoch's 0.919 on the same task.
103
129
 
104
130
  // Epoch knobs stay INTERNAL constants for the same reason the surfacing knobs do:
105
131
  // they are what the round-2 cost measurement exists to settle, and an option surface
106
132
  // 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
133
  /**
119
134
  * The reclaim floor, and the single constant that decides whether a commit is worth
120
135
  * firing at all.
@@ -127,32 +142,239 @@ export const EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD = 0.30;
127
142
  * per-mutation penalty is the same on both.
128
143
  *
129
144
  * 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.
145
+ * fire: it defers and accumulates until it is worth a full prefix. A request whose
146
+ * projection exceeds the provider input budget is rejected outright, so recovery must
147
+ * produce a window that fits; the hard fence and overflow recovery fire regardless.
132
148
  */
133
149
  export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
134
150
 
135
151
  /**
136
- * The thermostat's lower line.
152
+ * The pin ceiling (Shane 2026-08-09): protect may hold at most this share of the
153
+ * truthful serving budget raw. Protect was a per-entry promise with no mass bound,
154
+ * so a protect-happy agent could pin the window solid and leave every commit
155
+ * nothing to reclaim; past the cap, protect refuses with the cap named and
156
+ * unprotect is the release valve. A constant, not a knob.
157
+ */
158
+ export const MAX_PINNED_SHARE = 0.25;
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // THE THERMOSTAT. One declared object, one denominator, four numbers.
162
+ //
163
+ // Occupancy used to be a cluster: a trigger share of the budget, a post-commit target
164
+ // share of the budget, a freeing floor stated as a share of the WINDOW, a fresh tail
165
+ // stated in turns and bytes with a small-window byte cap, and a consolidation count.
166
+ // Three of those divided by a different denominator than the other two, so the same
167
+ // English word meant two numbers five to ten points apart at large windows. They are
168
+ // one decision, so they are one object, and every one of them is now a proportion of
169
+ // `capacityAccounting.budgetTokens` -- the truthful serving budget, the same value the
170
+ // fence, the estimator and the trigger already read.
171
+ //
172
+ // There was a fifth, `staleTail`, that declared how far back automation could reach as a
173
+ // byte prefix of the window. It is deleted (Shane 2026-08-10): automatic foldability is a
174
+ // CLASS a span belongs to, not a position it occupies, so there is no width to declare.
175
+ //
176
+ // User-set, never agent-set (Sol 2026-08-09): an agent that can widen freshTail or move
177
+ // maxTarget to the fence neutralizes the governor without ever calling a verb it is not
178
+ // entitled to. Agent authority stays where it already is -- mark, unmark, protect,
179
+ // unprotect -- and status may REPORT these values.
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /** The four numbers that decide when a commit fires, how deep it cuts, and what stays raw. */
183
+ export interface ActiveContextThresholds {
184
+ /** Occupancy of the serving budget that fires an automatic commit. */
185
+ maxTarget: number;
186
+ /** Occupancy an automatic commit folds down toward. The hysteresis gap is maxTarget - minTarget. */
187
+ minTarget: number;
188
+ /** Newest share of the serving budget where nothing folds, marked or not. */
189
+ freshTail: number;
190
+ /** Unpinned member folds at or above which placeholders become span material. */
191
+ consolidateAfter: number;
192
+ }
193
+
194
+ /**
195
+ * The proven values, and the reason each one is the number it is.
196
+ *
197
+ * maxTarget 0.80: Shane's RULING after rep 15. Below this line the runtime is quiet --
198
+ * nothing folds automatically at all -- so the line is where the fold event should
199
+ * happen, and the remaining fifth of the budget is the runway the commit itself spends.
137
200
  *
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.
201
+ * minTarget 0.35: the thermostat's lower line, and the reason event spacing is
202
+ * structural rather than hoped for. Rep 13's crumb ratchet fired, freed a little and
203
+ * re-fired, so the window climbed a staircase of mutations; folding down to a lower
204
+ * line means the next event cannot arrive until inflow has refilled a 45-point gap.
205
+ * Rep 13's largest measured inflow step was 27,815 tokens against a 400,000 window,
206
+ * about 7 points, so six worst-case steps land between events.
207
+ *
208
+ * freshTail 0.02: today's protected tail, re-denominated. The proven tail was 24,000
209
+ * serialized bytes; at the estimator's 4 bytes per token that is 6,000 tokens, and
210
+ * against the reference serving budget of 383,616 tokens (a 400,000-token window minus
211
+ * the 16,384-token response reserve) that is 1.564%, rounded UP to 0.02 so the
212
+ * re-denomination never narrows protection at the reference deployment.
213
+ *
214
+ * consolidateAfter 10: the proven count (the retired width rung counted the same roots
215
+ * against the same line). Placeholder briefs are about 300 tokens, so ten siblings cost
216
+ * under 1% of budget, and ten keeps an overnight session within two hops of any
217
+ * verbatim byte. A count, not a proportion: table-of-contents readability does not
218
+ * scale with window size, and a proportion would add a second denominator to the one
219
+ * thing this object exists to make singular.
220
+ */
221
+ export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
222
+ maxTarget: 0.80,
223
+ minTarget: 0.35,
224
+ freshTail: 0.02,
225
+ consolidateAfter: 10,
226
+ });
227
+
228
+ /**
229
+ * The smallest serving budget this package supports, in tokens.
230
+ *
231
+ * Derived from the package's own two largest bounded replies: one status payload
232
+ * (CONTEXT_STATUS_RESPONSE_BYTES, 24,000) plus one default peek
233
+ * (PEEK_DEFAULT_MAX_BYTES, 16,000) is 40,000 bytes, which at the estimator's 4 bytes
234
+ * per token is 10,000 tokens. A budget that cannot hold the largest pair of answers
235
+ * this package can emit back to back cannot serve the surface it ships, so registration
236
+ * refuses it by name rather than folding its way into an unservable session.
237
+ */
238
+ export const MINIMUM_SUPPORTED_BUDGET_TOKENS = 10_000;
239
+
240
+ /**
241
+ * The narrowest fresh tail that still protects anything, in tokens: one minimum tool
242
+ * result (`minToolChars` 2,000 bytes) at the estimator's 4 bytes per token. A tail
243
+ * thinner than the smallest thing automation will ever fold protects nothing.
244
+ */
245
+ export const MINIMUM_FRESH_TAIL_TOKENS = 500;
246
+
247
+ export class ThresholdPolicyError extends Error {
248
+ readonly invariant: string;
249
+ constructor(invariant: string, message: string) {
250
+ super(message);
251
+ this.name = "ThresholdPolicyError";
252
+ this.invariant = invariant;
253
+ }
254
+ }
255
+
256
+ function thresholdProportion(value: unknown, field: string): number {
257
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value >= 1) {
258
+ throw new ThresholdPolicyError(field, `thresholds.${field} must be a proportion above 0 and below 1`);
259
+ }
260
+ return value;
261
+ }
262
+
263
+ /**
264
+ * The whole object, validated atomically, or an error naming the invariant that failed.
265
+ *
266
+ * Atomic because these are four quarters of one decision: validating them one at a time
267
+ * is how a trigger moved from 0.50 to 0.80 while its target stayed at 0.35 and the
268
+ * docstring kept arguing a 15-point gap that had become 45. Never clamped: a policy that
269
+ * cannot be served is a registration error, not a value to quietly rewrite (Sol).
270
+ */
271
+ export function resolveThresholds(value: unknown): ActiveContextThresholds {
272
+ if (value === undefined) return { ...DEFAULT_THRESHOLDS };
273
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
274
+ throw new ThresholdPolicyError("shape", "thresholds must be an object with all four fields");
275
+ }
276
+ const supplied = value as Record<string, unknown>;
277
+ const fields = ["maxTarget", "minTarget", "freshTail", "consolidateAfter"] as const;
278
+ for (const key of Object.keys(supplied)) {
279
+ if (!(fields as readonly string[]).includes(key)) {
280
+ throw new ThresholdPolicyError("shape", `thresholds has no ${key} field`);
281
+ }
282
+ }
283
+ for (const field of fields) {
284
+ if (!Object.prototype.hasOwnProperty.call(supplied, field)) {
285
+ throw new ThresholdPolicyError("shape", `thresholds must declare ${field}; the object is set whole or not at all`);
286
+ }
287
+ }
288
+ const maxTarget = thresholdProportion(supplied.maxTarget, "maxTarget");
289
+ const minTarget = thresholdProportion(supplied.minTarget, "minTarget");
290
+ const freshTail = thresholdProportion(supplied.freshTail, "freshTail");
291
+ const consolidateAfter = supplied.consolidateAfter;
292
+ if (typeof consolidateAfter !== "number" || !Number.isInteger(consolidateAfter) || consolidateAfter < 1) {
293
+ throw new ThresholdPolicyError("consolidateAfter", "thresholds.consolidateAfter must be a positive integer count");
294
+ }
295
+ // L < M: the thermostat needs a gap to fold down into.
296
+ if (!(minTarget < maxTarget)) {
297
+ throw new ThresholdPolicyError("minTarget<maxTarget",
298
+ "thresholds.minTarget must sit below thresholds.maxTarget, or a commit has no depth to reach");
299
+ }
300
+ // F < M: a fresh tail wider than the trigger is a window that triggers on protection.
301
+ if (!(freshTail < maxTarget)) {
302
+ throw new ThresholdPolicyError("freshTail<maxTarget",
303
+ "thresholds.freshTail must sit below thresholds.maxTarget, or the trigger fires on protected mass alone");
304
+ }
305
+ // G >= F: one refill of the protected tail must not re-arm the trigger by itself.
306
+ if (!(maxTarget - minTarget >= freshTail)) {
307
+ throw new ThresholdPolicyError("gap>=freshTail",
308
+ "the hysteresis gap (maxTarget - minTarget) must be at least thresholds.freshTail, " +
309
+ "or refilling the protected tail alone re-fires the trigger");
310
+ }
311
+ // P + F < M, in TOKENS at the smallest supported budget, so rounding cannot slip past:
312
+ // the pin ceiling plus the structurally fresh tail is the floor a commit can never get
313
+ // under, and a trigger at or below that floor announces commits with nothing to reclaim.
314
+ const pinned = Math.ceil(MAX_PINNED_SHARE * MINIMUM_SUPPORTED_BUDGET_TOKENS);
315
+ const fresh = Math.ceil(freshTail * MINIMUM_SUPPORTED_BUDGET_TOKENS);
316
+ if (!(pinned + fresh < Math.floor(maxTarget * MINIMUM_SUPPORTED_BUDGET_TOKENS))) {
317
+ throw new ThresholdPolicyError("pinnedPlusFreshTail<maxTarget",
318
+ `the pin ceiling (${MAX_PINNED_SHARE}) plus thresholds.freshTail must stay below ` +
319
+ `thresholds.maxTarget at the ${MINIMUM_SUPPORTED_BUDGET_TOKENS}-token minimum supported budget`);
320
+ }
321
+ return { maxTarget, minTarget, freshTail, consolidateAfter };
322
+ }
323
+
324
+ /**
325
+ * The tiny-window refusal, evaluated once at registration against the declared budget.
326
+ * Reject, never clamp: a deployment whose window cannot carry the policy it asked for
327
+ * gets told so with both numbers rather than a silently different governor.
328
+ */
329
+ export function assertThresholdsServable(thresholds: ActiveContextThresholds, budgetTokens: number): void {
330
+ if (!Number.isFinite(budgetTokens) || budgetTokens < MINIMUM_SUPPORTED_BUDGET_TOKENS) {
331
+ throw new ThresholdPolicyError("minimumBudget",
332
+ `a ${Math.floor(budgetTokens)}-token serving budget is below the ` +
333
+ `${MINIMUM_SUPPORTED_BUDGET_TOKENS}-token minimum this package supports`);
334
+ }
335
+ const freshTokens = Math.floor(thresholds.freshTail * budgetTokens);
336
+ if (freshTokens < MINIMUM_FRESH_TAIL_TOKENS) {
337
+ throw new ThresholdPolicyError("freshTailTokens",
338
+ `thresholds.freshTail is ${freshTokens} tokens of a ${Math.floor(budgetTokens)}-token serving budget, ` +
339
+ `below the ${MINIMUM_FRESH_TAIL_TOKENS}-token minimum one foldable unit needs`);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * THE serving budget, from one window, by one formula.
345
+ *
346
+ * The output reservation is what makes the budget differ from the window, so it is
347
+ * subtracted exactly once and everything downstream -- the fence, the estimator, the
348
+ * trigger, and every threshold proportion -- divides by the same number.
349
+ */
350
+ /**
351
+ * A zone width in serialized BYTES, from a share of the serving budget.
144
352
  *
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.
353
+ * The thresholds are proportions of one denominator and a transcript position is a byte
354
+ * count, so exactly one conversion stands between them: the estimator's bytes per token.
355
+ * It is stated here, once, so every share of the budget is cut by one ruler.
151
356
  */
152
- export const CURATION_TARGET_OCCUPANCY_SHARE = 0.35;
357
+ export function zoneBytes(share: number, budgetTokens: number): number {
358
+ if (!Number.isFinite(share) || share <= 0 || !Number.isFinite(budgetTokens) || budgetTokens <= 0) return 0;
359
+ return Math.floor(share * budgetTokens * ESTIMATED_BYTES_PER_TOKEN);
360
+ }
361
+
362
+ export function servingBudgetTokens(window: number): number {
363
+ const resolved = Number.isFinite(window) && window > 0 ? window : DEFAULT_CONTEXT_WINDOW;
364
+ return resolved - Math.min(ACTIVE_CONTEXT_POLICY.responseReserve, Math.floor(resolved * 0.1));
365
+ }
366
+
153
367
  export const MAX_PENDING_MARKS = 256;
154
368
  /** Enough batches to actually reach the floor on a wide window before the loop exits. */
155
369
  export const EPOCH_MAX_TOPUP_MARKS = 64;
370
+ /**
371
+ * How many times one commit re-reads its own eligible-root count.
372
+ *
373
+ * A termination bound, not a cadence: a parent is excluded from the count that produced
374
+ * it, so each round divides the standing roots by `consolidateAfter` and the sequence
375
+ * closes in two rounds at any window a session can carry.
376
+ */
377
+ export const EPOCH_CONSOLIDATION_ROUNDS = 4;
156
378
  /** Estimate only; provider token accounting always comes from a measured response. */
157
379
  export const ESTIMATED_BYTES_PER_TOKEN = 4;
158
380
  /** Rendered navigation/topology overhead assumed around a brief when estimating a placeholder. */
@@ -163,8 +385,10 @@ export const ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES = 240;
163
385
  // through iterations 2 and 3 and was sealed ON by rep 14 (64/64), so it is now
164
386
  // simply how pi-fold works: the flags and their conditional twins are gone, and
165
387
  // 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`).
388
+ // deployment fact (`providerTotalWindow`). The experiment conditions that used to sit
389
+ // beside it (`foldScheduling`, `foldPeekResults`, `guidedCuration`) are gone too: epoch
390
+ // scheduling, peek foldability and guided curation are simply how pi-fold works, and
391
+ // the harness keeps reading the keys only so sealed runs still validate.
168
392
  //
169
393
  // Sealed unconditional: admission control, retained pending marks, the eligible-share
170
394
  // commit trigger, stage-identified briefs, the current-turn commit guard, the
@@ -198,53 +422,82 @@ export const STATUS_DIET_SUGGESTIONS = 5;
198
422
  export const MAX_PROJECTION_HASH_RECORDS = 64;
199
423
 
200
424
  // ---------------------------------------------------------------------------
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.
425
+ // Guided curation: the two-signal curation trigger, its bounded last-call gate,
426
+ // and the reactive receipt block. It shipped as the one iteration-4 experiment
427
+ // condition and is unconditional now, so only the constants below remain.
203
428
  // ---------------------------------------------------------------------------
204
429
 
205
- export const DEFAULT_GUIDED_CURATION = false;
206
-
207
430
  /**
208
- * Signal one: measured occupancy of the truthful serving budget.
431
+ * The three occupancy waypoints, as shares of the truthful serving budget.
209
432
  *
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.
433
+ * Each one lands exactly once per upward crossing as an append-once notice that then
434
+ * persists in the window the way a tool result does: appended at the tail, closed over
435
+ * by the freeze, never re-rendered mid-cycle, so it can never move a prefix byte. A
436
+ * commit that drops occupancy back under a waypoint re-arms it; the next crossing is a
437
+ * new event and gets a new notice.
215
438
  */
216
- export const CURATION_OCCUPANCY_SHARE = 0.80;
217
-
439
+ export const THRESHOLD_NOTICE_SHARES: readonly number[] = Object.freeze([0.25, 0.50, 0.75]);
440
+ /** Delivered notices kept rendered in the window; the oldest leaves the carrier, never the stream. */
441
+ export const MAX_THRESHOLD_NOTICES = 9;
218
442
  /**
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.
443
+ * The two guidance surfaces, as ONE public option, both switches defaulting on.
444
+ * Shane's ruling: notices and action responses are optional, and the default is yes.
226
445
  */
227
- export const CURATION_REMINDER_SHARES: readonly number[] = Object.freeze([0.45, 0.65]);
446
+ export interface ActiveContextGuidance {
447
+ /** Append-once occupancy waypoints at 25/50/75 percent of the serving budget. */
448
+ thresholdNotices: boolean;
449
+ /** Persistent acknowledgements for the agent's own context actions. */
450
+ actionResponses: boolean;
451
+ }
452
+
453
+ export const DEFAULT_GUIDANCE: ActiveContextGuidance = Object.freeze({
454
+ thresholdNotices: true,
455
+ actionResponses: true,
456
+ });
457
+
458
+ const GUIDANCE_KEYS: readonly (keyof ActiveContextGuidance)[] = Object.freeze([
459
+ "thresholdNotices", "actionResponses",
460
+ ]);
228
461
 
229
462
  /**
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.
463
+ * Validated whole, like the thresholds: an unknown key is a typo, not a silent no-op.
464
+ *
465
+ * Booleans and nothing else. The dosage families that used to parameterize guidance
466
+ * (profiles, reminder shares, milestone budgets) were deleted for measuring nothing
467
+ * across eleven runs, and shares or per-notice keys here would rebuild them by another
468
+ * name. An operator either wants the runtime to speak at the boundary or does not.
238
469
  */
239
- export const CURATION_STALE_TOOL_SHARE = 0.20;
240
-
470
+ export function resolveGuidance(value: unknown): ActiveContextGuidance {
471
+ if (value === undefined) return DEFAULT_GUIDANCE;
472
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
473
+ throw new Error("guidance must be an object of { thresholdNotices, actionResponses } booleans");
474
+ }
475
+ for (const key of Object.keys(value)) {
476
+ if (!GUIDANCE_KEYS.includes(key as keyof ActiveContextGuidance)) {
477
+ throw new Error(`guidance has no ${key} setting: the only keys are ` +
478
+ `${GUIDANCE_KEYS.join(", ")}, and both are booleans`);
479
+ }
480
+ }
481
+ const resolved = { ...DEFAULT_GUIDANCE } as ActiveContextGuidance;
482
+ for (const key of GUIDANCE_KEYS) {
483
+ if (!Object.hasOwn(value, key)) continue;
484
+ const setting = (value as Record<string, unknown>)[key];
485
+ if (typeof setting !== "boolean") throw new Error(`guidance.${key} must be a boolean`);
486
+ resolved[key] = setting;
487
+ }
488
+ return Object.freeze(resolved);
489
+ }
490
+ /** The last-call is LITERAL persisted bytes; the bound is on the stored text itself. */
491
+ export const MAX_LAST_CALL_TEXT_BYTES = 2_048;
492
+ /** One notice is one waypoint line; a waypoint that becomes a paragraph is bloat. */
493
+ export const MAX_THRESHOLD_NOTICE_TEXT_BYTES = 512;
241
494
  /**
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.
495
+ * The surfacing line's own bound, applied to the line and not to the carrier that
496
+ * carries it, so a carrier's total overhead is its own bound plus this one and stays a
497
+ * number a gate can pin. The 21.9% ephemeral-slate tax is what this is measured
498
+ * against: one bounded line landing inside a rewrite the commit already paid for.
246
499
  */
247
- export const CURATION_GATE_MAX_ROUNDS = 2;
500
+ export const MAX_SURFACING_LINE_BYTES = 384;
248
501
 
249
502
  /** How many automatic-action receipts stay in the window; the oldest ages out. */
250
503
  export const MAX_CONTEXT_RECEIPTS = 3;
@@ -314,6 +567,44 @@ export const MAX_FOLD_SPAN_CHARS = 16_000;
314
567
  * every absorption is receipted, and a wrong grouping is one `reboundary` away.
315
568
  */
316
569
  export const MAX_WEDGE_ABSORB_TOKENS = 256;
570
+ /**
571
+ * How many generator calls the upgrade lane may hold open at once.
572
+ *
573
+ * The lane has to write briefs at the rate the ladder makes folds, or the folds that
574
+ * carry a model brief are a shrinking fraction of the folds a long session holds. One
575
+ * call in flight, started once per ladder pass, is not that rate: the live pressure run
576
+ * of 2026-08-10 measured completions at 6.9s, 9.4s and 14.3s while passes came about
577
+ * half a minute apart, so the lane spent most of its time idle and produced exactly one
578
+ * brief per commit boundary while four folds sat waiting. Two in flight, with a finished
579
+ * call starting the next one itself, makes production a function of generator latency
580
+ * instead of pass cadence. Two rather than more because provider calls are real money
581
+ * and real concurrency: the ladder makes about one leaf fold per boundary, and a lane
582
+ * that outruns the ladder by a wider margin only buys idle capacity.
583
+ */
584
+ export const MAX_BRIEF_UPGRADES_IN_FLIGHT = 2;
585
+ /**
586
+ * How many folds may be waiting on a generator at once. The queue holds each fold's
587
+ * exact source, so it is a memory bound as much as a work bound, and a fold dropped for
588
+ * a full queue simply keeps the deterministic brief it committed with. With the queue
589
+ * capped here and calls capped above, the work a session can have outstanding is a
590
+ * constant, and so is the number of briefs any one boundary can be asked to carry.
591
+ */
592
+ export const MAX_BRIEF_UPGRADE_QUEUE = 4;
593
+ /**
594
+ * How many spans one generator call may brief at once.
595
+ *
596
+ * A commit folds its spans together, so they are briefed together: one call, one answer,
597
+ * one brief per span. Sending them one at a time is what left the 2026-08-11 rep with 14
598
+ * model briefs across 87 committed folds, because the ladder makes about a dozen folds per
599
+ * boundary and the lane could hold four.
600
+ *
601
+ * Ten because that is the ladder's own group width (`consolidateAfter`), so a batch is
602
+ * never asked for more briefs than a consolidation would produce, and the answer stays
603
+ * bounded at ten brief caps. The batch's SOURCE is bounded by `maxSourceChars`, the same
604
+ * budget a single span was always held to, so the queue's memory bound does not move: it
605
+ * is still MAX_BRIEF_UPGRADE_QUEUE requests of at most one source budget each.
606
+ */
607
+ export const MAX_BRIEF_BATCH_SPANS = 10;
317
608
  /** What a peek returns without an explicit widening argument. */
318
609
  export const PEEK_DEFAULT_MAX_BYTES = 16_000;
319
610
  /** Head share of a truncated peek; the remainder is the tail, where conclusions live. */
@@ -336,13 +627,13 @@ export const PEEK_HEAD_SHARE = 0.6;
336
627
  */
337
628
  export const OVERFLOW_RECOVERY_MAX_ATTEMPTS = 2;
338
629
 
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
630
  /**
342
- * The classification that also lets a `peek` result be reclaimed by the tool-fold rung.
631
+ * Active-context tool actions that read without mutating, so their results may fold.
632
+ *
343
633
  * 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`.
634
+ * pure duplicate of evidence the runtime already holds and the tool-fold rung reclaims
635
+ * it. There was a narrower set while immediate scheduling existed, reached by opting
636
+ * out; epoch is the only scheduler now and this classification is unconditional.
346
637
  */
347
638
  export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
348
639
 
@@ -372,25 +663,34 @@ export interface PendingRefoldMark {
372
663
  export type PendingMark = PendingFoldMark | PendingRefoldMark;
373
664
 
374
665
  export const ACTIVE_CONTEXT_POLICY = Object.freeze({
375
- freshTurns: 3,
376
- freshBytes: 24_000,
377
- freshWindowShare: 0.25,
378
- warningRatio: 0.65,
379
- toolFoldRatio: 0.75,
380
666
  refoldRatio: 0.85,
381
667
  prepareRatio: 0.90,
382
668
  warmRatio: 0.55,
383
669
  responseReserve: 16_384,
384
- consolidationRatio: 0.85,
385
- consolidationChildren: 5,
386
- maxConsolidationChildren: 8,
387
670
  minToolChars: 2_000,
388
671
  minChapterChars: 4_000,
389
672
  maxChapterChars: 128_000,
390
673
  maxChapterTurns: 4,
391
674
  maxSourceChars: 200_000,
392
675
  maxFoldSourceRefs: 256,
393
- maxBriefChars: 1_200,
676
+ /**
677
+ * 2,000 (Shane 2026-08-11), raised from 1,200.
678
+ *
679
+ * The cap is a limit the generator is told and can be cured against, not a truncation
680
+ * point, so it should sit where a good brief stops rather than where a cheap one does.
681
+ * Measured against what a stop-the-world compaction hands forward: this harness's own
682
+ * native-arm summaries ran 8,000 to 20,000 characters for a whole session, and a folded
683
+ * window carries about ten briefs, so a per-fold budget of roughly a tenth of a handoff
684
+ * is 800 to 2,000. The ceiling of that range is the right end to sit at, because ten
685
+ * briefs at the cap is 20,000 characters against a serving budget measured in hundreds
686
+ * of thousands, and because the cost of a brief that is too short is a fold the agent
687
+ * cannot decide whether to expand.
688
+ *
689
+ * It binds rarely by design: the run before this one averaged well under 1,200 on the
690
+ * calls that were not being truncated by a token ceiling. `context.brief` records every
691
+ * cure so the next run says how often it binds at all.
692
+ */
693
+ maxBriefChars: 2_000,
394
694
  briefTimeoutMs: 120_000,
395
695
  orientationMessages: 2,
396
696
  maxOrientationChars: 12_000,
@@ -409,6 +709,12 @@ export type BriefProvenance =
409
709
  launchContractDigest?: string;
410
710
  };
411
711
 
712
+ /**
713
+ * A brief written after its fold committed: the agent's bare correction, or a model
714
+ * brief carrying the generator that wrote it.
715
+ */
716
+ export type BriefOverride = string | { brief: string; provenance: BriefProvenance };
717
+
412
718
  export interface ActiveFold {
413
719
  id: string;
414
720
  kind: FoldKind;
@@ -438,42 +744,55 @@ export interface PreparedFold {
438
744
  fold: ActiveFold;
439
745
  }
440
746
 
441
- export type SurfacingOutcome = "shown" | "accept" | "reject";
747
+ /**
748
+ * The graded success labels. `shown` is the open offer; the other three are terminal.
749
+ * `acted` is a context verb on the surfaced fold inside the window, `used` is that plus
750
+ * the retrieved content showing up in what the agent went on to say or call, and
751
+ * `ignored` is a closed window with neither.
752
+ */
753
+ export type SurfacingOutcome = "shown" | "acted" | "used" | "ignored";
442
754
 
443
- /** One durable surfacing observation: what was shown, how it scored, and what the agent did. */
755
+ /**
756
+ * The durable SUPPRESSION ledger: one record per fold, not one per showing. The
757
+ * per-suggestion detail a bandit trains on is the event stream, which is durable and
758
+ * unbounded; this is only what the next selection pass must not forget.
759
+ */
444
760
  export interface SurfacingRecord {
445
- source: string;
446
761
  id: string;
447
- score: number;
762
+ surfaced: number;
763
+ taken: number;
764
+ /** Transcript ordinal of the last state change; the cooldown and the window read it. */
448
765
  ordinal: number;
449
766
  outcome: SurfacingOutcome;
450
767
  }
451
768
 
452
- /** One item a suggestion source offers for the shared carrier. */
769
+ /** One suppression-state change, returned so the caller emits it rather than inferring it. */
770
+ export interface SurfacingTransition {
771
+ id: string;
772
+ from: SurfacingOutcome;
773
+ to: SurfacingOutcome;
774
+ ordinal: number;
775
+ }
776
+
777
+ /** One collapsed fold, scored on both channels. */
453
778
  export interface SurfacingCandidate {
454
- source: string;
455
779
  id: string;
456
- text: string;
780
+ /** What the placeholder shows: the brief channel. */
781
+ brief: string;
782
+ /** What the fold stores, descendants included: the content channel. */
783
+ content: string;
457
784
  route: string;
458
- alternateRoute?: string;
459
- /** Transcript position used for recency; higher is more recent. Never a wall clock. */
460
- position?: number;
461
- depth?: number;
785
+ /** Transcript position of the fold's last entry; higher is more recent. Never a clock. */
786
+ position: number;
787
+ depth: number;
462
788
  }
463
789
 
464
790
  export interface SurfacingSuggestion extends SurfacingCandidate {
465
- score: number;
466
- }
467
-
468
- export interface SuggestionSourceInput {
469
- state: ActiveContextState;
470
- snapshot: ActiveContextSnapshot;
471
- toolName: string;
472
- }
473
-
474
- export interface SuggestionSource {
475
- id: string;
476
- candidates: (input: SuggestionSourceInput) => SurfacingCandidate[];
791
+ contentScore: number;
792
+ briefScore: number;
793
+ margin: number;
794
+ /** Slate position, from 0. One suggestion per delivery point, so today it is always 0. */
795
+ slot: number;
477
796
  }
478
797
 
479
798
  export interface ActiveContextState {
@@ -491,18 +810,38 @@ export interface ActiveContextState {
491
810
  /** Retired with peek reclamation; carried only so pre-cut state still parses. */
492
811
  pinnedPeeks?: never;
493
812
  /**
494
- * Agent-corrected fold briefs, by fold id. A fold RECORD is content-addressed and
813
+ * Corrected and upgraded fold briefs, by fold id. A fold RECORD is content-addressed and
495
814
  * immutable -- rewriting its brief in place would report a conflicting durable fold
496
815
  * and suspend automatic management -- so a re-brief is durable state beside the
497
816
  * record rather than a mutation of it. Omitted when empty.
498
817
  */
499
- briefs?: Record<string, string>;
818
+ briefs?: Record<string, BriefOverride>;
500
819
  prepared?: PreparedFold;
501
820
  advisory?: {
502
821
  highWater: number;
503
822
  delivered: Record<string, number>;
504
823
  armed?: { milestone: AdvisoryMilestone; threshold: number; scheduleKey: string };
505
824
  };
825
+ /**
826
+ * The one action-prompt carrier: computed at a fold commit, persisted as LITERAL
827
+ * bytes so every re-render of the same epoch is byte-identical, replaced only by
828
+ * the next epoch's rider. Omitted when absent so pre-rider state digests never move.
829
+ */
830
+ rider?: { epoch: number; text: string };
831
+ /**
832
+ * The armed pre-commit last-call: one exposure per band-top crossing, persisted as
833
+ * LITERAL bytes like the rider. `exposure` is the context.lastcall event's stream
834
+ * seq; `contextCalls` and `agentMarks` are the arming snapshot the response
835
+ * attribution is measured against. Cleared by the commit that consumes it. Omitted
836
+ * when absent so pre-last-call state digests never move.
837
+ */
838
+ lastCall?: { exposure: number; ordinal: number; contextCalls: number; agentMarks: number; text: string };
839
+ /**
840
+ * Threshold notices: `fired` is the waypoint shares spent this occupancy cycle
841
+ * (re-armed by a commit that drops back under them), `ring` the delivered notices
842
+ * still rendered in the window, literal bytes, oldest evicted. Omitted when absent.
843
+ */
844
+ notices?: { fired: number[]; ring: Array<{ share: number; ordinal: number; text: string }> };
506
845
  }
507
846
 
508
847
  export interface FoldRecordRef {
@@ -531,8 +870,11 @@ export interface ActiveContextCheckpointV2 {
531
870
  leases?: Record<string, number>;
532
871
  surfacing?: SurfacingRecord[];
533
872
  pendingMarks?: PendingMark[];
534
- briefs?: Record<string, string>;
873
+ briefs?: Record<string, BriefOverride>;
535
874
  advisory?: NonNullable<ActiveContextState["advisory"]>;
875
+ rider?: NonNullable<ActiveContextState["rider"]>;
876
+ lastCall?: NonNullable<ActiveContextState["lastCall"]>;
877
+ notices?: NonNullable<ActiveContextState["notices"]>;
536
878
  stateSha256: string;
537
879
  }
538
880
 
@@ -552,8 +894,11 @@ export interface ActiveContextDeltaV2 {
552
894
  leases?: Record<string, number>;
553
895
  surfacing?: SurfacingRecord[];
554
896
  pendingMarks?: PendingMark[];
555
- briefs?: Record<string, string>;
897
+ briefs?: Record<string, BriefOverride>;
556
898
  advisory?: NonNullable<ActiveContextState["advisory"]>;
899
+ rider?: NonNullable<ActiveContextState["rider"]>;
900
+ lastCall?: NonNullable<ActiveContextState["lastCall"]>;
901
+ notices?: NonNullable<ActiveContextState["notices"]>;
557
902
  stateSha256: string;
558
903
  }
559
904
 
@@ -583,13 +928,17 @@ export interface ActiveContextSnapshot {
583
928
  branchObjects: BranchObject[];
584
929
  completeTurns: CompleteTurn[];
585
930
  freshBoundary: number;
931
+ /** The four thermostat numbers in force for this session. */
932
+ thresholds: ActiveContextThresholds;
933
+ /** The serving budget the thresholds are proportions of. One denominator. */
934
+ budgetTokens: number;
586
935
  protectedIndices: Set<number>;
587
936
  toolProtectedIndices: Set<number>;
588
937
  policy: typeof ACTIVE_CONTEXT_POLICY;
589
938
  toolName: string;
590
939
  brandNoun: string;
591
940
  entryTypePrefix: string;
592
- readOnlyTools: ReadonlySet<string>;
941
+ blacklistAutoFoldTools: ReadonlySet<string>;
593
942
  /** Active-context tool actions treated as read-only when classifying tool batches. */
594
943
  readOnlyContextActions: ReadonlySet<string>;
595
944
  contextWindow: number;
@@ -602,8 +951,15 @@ export interface FoldCandidate {
602
951
  sourceRefs: EvidenceRef[];
603
952
  }
604
953
 
605
- export const READ_ONLY_TOOLS_DEFAULT = new Set([
606
- "read", "grep", "find", "ls",
607
- ]);
954
+ /**
955
+ * The auto-fold exception list, EMPTY by default: every completed tool batch is foldable
956
+ * by the ladder without an agent mark, and this names the tools whose results must stay
957
+ * raw. The list ran the other way until the 2026-08-10 surface, as an allow-list seeded
958
+ * with pi's four built-in readers, which meant a deployment's own tools were unfoldable
959
+ * until someone remembered to name them and the ladder starved on exactly the results
960
+ * that filled the window. Foldability was never the protection: pins, the fresh tail and
961
+ * the commit guard are, and they apply to a blacklisted tool and an ordinary one alike.
962
+ */
963
+ export const AUTO_FOLD_BLACKLIST_DEFAULT: ReadonlySet<string> = new Set();
608
964
 
609
965
  export type AdvisoryMilestone = "orientation" | "notice" | "tools" | "chapters" | "urgent";