pi-fold 2.0.0 → 3.1.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.
@@ -5,8 +5,8 @@ export const DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX = "pi-fold-active-context"
5
5
  export const DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL = "pi-fold Active Context";
6
6
  export const DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN = "pi-fold";
7
7
  export const DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES = Object.freeze({
8
- status: "pi-fold-context",
9
- fold: "fold-context",
8
+ status: "fold-status",
9
+ fold: "fold",
10
10
  });
11
11
 
12
12
  export function entryTypeNamespace(entryTypePrefix: string): string {
@@ -33,11 +33,67 @@ const DEFAULT_ENTRY_NAMESPACE = entryTypeNamespace(DEFAULT_ACTIVE_CONTEXT_ENTRY_
33
33
  export const ACTIVE_CONTEXT_STATE_ENTRY = `${DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX}-state`;
34
34
  export const ACTIVE_CONTEXT_FOLD_RECORD_ENTRY = `${DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX}-fold-record`;
35
35
  export const PROVIDER_CONTEXT_MEASUREMENT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-provider-context-measurement`;
36
+ /**
37
+ * How many unbriefed pending folds stand before the runtime says so.
38
+ *
39
+ * A CONSTANT, not a threshold the deployment can turn (Shane's KISS rule): it is a
40
+ * batching size, and the only thing a knob here would buy is a deployment that never
41
+ * speaks. Three is the smallest batch that reads as a batch rather than as a running
42
+ * commentary on every cut.
43
+ */
44
+ export const UNBRIEFED_FOLDS_BEFORE_NOTICE = 3;
45
+
46
+ /**
47
+ * How many spans the frontier cuts on one projection pass.
48
+ *
49
+ * A CONSTANT, and a cost bound rather than a policy. Each cut rescans the window for the
50
+ * next stalest span, so an unbounded loop is quadratic in the number of spans a window
51
+ * holds: on a 300-turn fixture it took a gate from 2.5 seconds to 152. A frontier does not
52
+ * need to catch up in one pass, because a pass costs nothing to repeat and the material it
53
+ * has not reached yet is still raw and still in front of the agent, which is where the
54
+ * design wants it. Eight is comfortably ahead of any real arrival rate.
55
+ */
56
+ export const MAX_FRONTIER_CUTS_PER_PASS = 8;
57
+
36
58
  export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
37
- "status", "peek", "fold", "expand", "refold", "protect", "unprotect",
38
- "rebrief", "reboundary", "unmark",
59
+ "status", "peek", "brief", "expand", "refold", "pin", "unpin",
60
+ "reboundary", "unmark",
39
61
  ] as const);
40
- export type ActiveContextToolAction = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
62
+ /**
63
+ * FOLD IS DELETED AS AN AGENT VERB (Shane, 2026-08-23), and `brief` is what replaced it.
64
+ *
65
+ * The agent used to choose spans from nothing. Measured against real compaction
66
+ * boundaries on 2026-08-23, that produced a projection byte-identical to the same arm
67
+ * with no agent at all: six marks, 9,450 characters of briefs, same visible count, same
68
+ * fold count, same aborts. It marks what the ladder would have taken anyway, because at
69
+ * the moment it is asked the only information available is staleness and the runtime
70
+ * already has that.
71
+ *
72
+ * So the runtime cuts at the frontier and the agent annotates what it cut, while the
73
+ * material is still in front of it. `brief` attaches or replaces the brief on a PENDING
74
+ * fold and refuses on a standing one, which is the rebrief deletion above read from the
75
+ * other side: a pending mark is not in the window, so writing its brief moves no bytes
76
+ * and costs no cache, and a standing fold's placeholder is in the window, so writing that
77
+ * one costs the whole prefix from there on. The name `fold` stays spent.
78
+ */
79
+ /**
80
+ * REBRIEF IS DELETED (Shane, 2026-08-21). Every rebrief rewrites the projection at its
81
+ * fold's placeholder, which breaks the prefix cache from that point on: sol-20260820
82
+ * rep 7's first rebrief hit a front-of-projection fold, identical_share collapsed to
83
+ * 0.0007, and nine ~185K-token requests ran cacheRead 0 for $8.36 in one burst. The
84
+ * end block read 10 of 10 in every pifold rep with or without rebriefs, because
85
+ * lossless peek already carries recall, so the action was paying a full cache break
86
+ * for something the runtime did not need. The name stays spent.
87
+ */
88
+ export const RETIRED_TOOL_ACTIONS: Readonly<Record<string, string>> = Object.freeze({
89
+ rebrief: "rebrief is deleted: rewriting a standing fold's brief rewrites the projection " +
90
+ "at that fold's placeholder and breaks the prefix cache from there on, for recall that " +
91
+ "peek already carries losslessly. Write the brief you want when you mark the span, or " +
92
+ "use reboundary to re-cut it.",
93
+ });
94
+ export type ActiveContextToolAction =
95
+ | typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number]
96
+ | typeof WORKING_MEMORY_TOOL_ACTIONS[number];
41
97
  export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
42
98
  export const DEFAULT_CONTEXT_WINDOW = 272_000;
43
99
  export const EXPAND_LEASE_GENERATIONS = 8;
@@ -45,38 +101,99 @@ export const MAX_EXPAND_LEASES = 64;
45
101
 
46
102
  export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
47
103
 
104
+ /**
105
+ * How far below the commit trigger the agent is told a commit is coming (Shane,
106
+ * 2026-08-22: "around 10% from the commit threshold"). A share of the serving budget
107
+ * rather than a token count, because a token count means a different fraction of every
108
+ * deployment's window. The advisory takes the wider of this and one worst recent inflow
109
+ * step, so a single large result cannot step over the whole band unasked.
110
+ */
48
111
  export const MAX_PINNED_SHARE = 0.25;
49
112
 
113
+ /**
114
+ * THE TOOL-CALL DIET's default share (Shane 2026-08-28, on the sol-20260826-full2
115
+ * verdict). Every fold repetition from 3 onward ran 0.50 and the campaign's headline
116
+ * results are all from that shape, so the shipped default is the measured value rather
117
+ * than the absence of one. A deployment turns the diet off with an explicit 0, which is
118
+ * why the option's range is [0, 1) instead of the open interval it shipped with: one
119
+ * meaning per value, and no absent-means-off special case.
120
+ */
121
+ export const DEFAULT_TOOL_FOLD_THRESHOLD = 0.50;
122
+
50
123
  export interface ActiveContextThresholds {
51
124
  maxTarget: number;
52
125
  minTarget: number;
53
- freshTail: number;
54
126
  consolidateAfter: number;
127
+ minFoldChars: number;
55
128
  }
56
129
 
130
+ /**
131
+ * The floor a fold has to clear to be worth making, in CHARACTERS (Shane, 2026-08-21).
132
+ *
133
+ * Characters rather than tokens because characters are provider-agnostic: a token count
134
+ * means a different amount of text on every wire, and this number exists so a person can
135
+ * reason about it. It replaces two constants nobody could tell apart, minToolChars at
136
+ * 2,000 and minChapterChars at 4,000, and it is not specific to tool results or to
137
+ * chapters: everything in the window costs tokens, so everything counts toward the floor.
138
+ *
139
+ * It is a floor, not a cut. The automatic span accumulates whole messages and stops at
140
+ * the first one that carries it OVER the floor, so a fold is always at least this big
141
+ * and never cuts a message in half to hit the number exactly. The same floor is what a
142
+ * gap between two folds has to clear to survive: anything smaller is absorbed by the
143
+ * fold beside it rather than left as a raw sliver, because a sliver costs its bytes and
144
+ * buys nothing.
145
+ *
146
+ * 8,000 was chosen against the sealed corpus. Rep 1 of sol-20260814-traps folded 89
147
+ * spans at a median of 57,808 source chars, but nine of them came in under 8,000 and
148
+ * TWO were net negative, 2,726 chars of source replaced by 5,341 of placeholder (196
149
+ * percent) and 6,384 replaced by 10,531 (165 percent). A fold that grows the window is
150
+ * not a fold. The floor sits above every observed inversion with room to spare.
151
+ */
152
+ export const MINIMUM_FOLD_CHARS_FLOOR = 2_000;
153
+
57
154
  export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
58
155
  maxTarget: 0.80,
59
- // 0.20, down from 0.35 (Shane 2026-08-14). The cut depth is what sets the epoch
60
- // cadence: every commit rewrites the prefix from the earliest folded byte, so the
61
- // cost of an epoch is nearly flat in how deep it cuts, while the relief it buys is
62
- // the whole distance to the trigger. sol-20260814-traps rep 1 ran six commits, each
63
- // freeing to 0.35 and refilling in two to three stages; at 0.20 the same run shape
64
- // owes roughly a quarter fewer epochs, which is a quarter fewer full-prefix
65
- // rewrites, the dominant self-inflicted cache break. The floor is a target the
66
- // class law outranks, not a guarantee: a session pinned to the 0.25 ceiling
67
- // beside the 0.02 fresh tail legally holds 0.27 and lands above this number,
68
- // and the epoch receipt reports targetBudgetShare beside actualFreedBudgetShare
69
- // so the shortfall is a readable fact per epoch rather than a silent miss. The
70
- // validation laws below bind exactly as before.
156
+ // 0.20 (Shane 2026-08-28, on the sol-20260826-full2 verdict). This number went 0.20 ->
157
+ // 0.40 on 2026-08-23 and came back on the campaign that was still running when it
158
+ // moved, so both readings are recorded here rather than one quietly replacing the
159
+ // other. The top of the band is not in question and never was.
160
+ //
161
+ // Read against the 19 mature sealed pifold runs that share a 251,520-token budget,
162
+ // where the trigger sat at 0.80 of it, 201,216 tokens. Peak occupancy per run came in
163
+ // at a median 237,399, a 0.944 share, p90 247,551, max 257,117: a session runs roughly
164
+ // 36,000 tokens PAST its own trigger before the commit it fired takes effect, because
165
+ // the trigger fires at a measurement and inflow continues while the epoch is computed.
166
+ // So 0.80 already delivers a 250k top, and raising it toward 250k directly would put
167
+ // the median peak over the budget and into the fence.
168
+ //
169
+ // The 2026-08-23 case for 0.40 was that it seats the floor near where commits actually
170
+ // land: across 206 commit landings the session came to rest at a median 79,034 tokens,
171
+ // 153 of the 206 below 100,000, and landings sit ABOVE the aim rather than at it, since
172
+ // the class law and the available material both outrank it. That was bought knowing the
173
+ // price, about a fifth more epochs and so a fifth more full-prefix rewrites, to hold
174
+ // 100k of raw context across the whole cycle.
175
+ //
176
+ // The campaign priced the trade and it went the other way. The commit rewrite tax is
177
+ // minTarget / (maxTarget - minTarget), because every commit re-reads roughly
178
+ // minTarget x budget of prefix uncached and a shallower cut buys less runway for nearly
179
+ // the same bill: 1.0 at 0.80/0.40 against 0.33 at 0.80/0.20. Every sol-20260826-full2
180
+ // fold repetition from 3 onward ran 0.80/0.20, including the fold-plus-canon rep 5 that
181
+ // answered 14 of 16 with nothing wrong at $100.85 against native-plus-canon's $201.70.
182
+ // The shipped default is now the configuration that was measured.
183
+ //
184
+ // THE KNOWN LIMIT, unchanged and still true: MAX_PINNED_SHARE is 0.25, so a fully
185
+ // pinned session legally holds more than a 0.20 aim asks for, and there the commit
186
+ // stops short of its target by construction rather than by fault. The epoch receipt
187
+ // reports targetBudgetShare beside actualFreedBudgetShare, so the shortfall stays a
188
+ // readable fact per epoch rather than a silent miss. The validation laws below bind
189
+ // exactly as before.
71
190
  minTarget: 0.20,
72
- freshTail: 0.02,
73
191
  consolidateAfter: 10,
192
+ minFoldChars: 8_000,
74
193
  });
75
194
 
76
195
  export const MINIMUM_SUPPORTED_BUDGET_TOKENS = 10_000;
77
196
 
78
- export const MINIMUM_FRESH_TAIL_TOKENS = 500;
79
-
80
197
  export class ThresholdPolicyError extends Error {
81
198
  readonly invariant: string;
82
199
  constructor(invariant: string, message: string) {
@@ -99,7 +216,7 @@ export function resolveThresholds(value: unknown): ActiveContextThresholds {
99
216
  throw new ThresholdPolicyError("shape", "thresholds must be an object with all four fields");
100
217
  }
101
218
  const supplied = value as Record<string, unknown>;
102
- const fields = ["maxTarget", "minTarget", "freshTail", "consolidateAfter"] as const;
219
+ const fields = ["maxTarget", "minTarget", "consolidateAfter", "minFoldChars"] as const;
103
220
  for (const key of Object.keys(supplied)) {
104
221
  if (!(fields as readonly string[]).includes(key)) {
105
222
  throw new ThresholdPolicyError("shape", `thresholds has no ${key} field`);
@@ -112,51 +229,31 @@ export function resolveThresholds(value: unknown): ActiveContextThresholds {
112
229
  }
113
230
  const maxTarget = thresholdProportion(supplied.maxTarget, "maxTarget");
114
231
  const minTarget = thresholdProportion(supplied.minTarget, "minTarget");
115
- const freshTail = thresholdProportion(supplied.freshTail, "freshTail");
116
232
  const consolidateAfter = supplied.consolidateAfter;
117
233
  if (typeof consolidateAfter !== "number" || !Number.isInteger(consolidateAfter) || consolidateAfter < 1) {
118
234
  throw new ThresholdPolicyError("consolidateAfter", "thresholds.consolidateAfter must be a positive integer count");
119
235
  }
236
+ const minFoldChars = supplied.minFoldChars;
237
+ if (typeof minFoldChars !== "number" || !Number.isInteger(minFoldChars) ||
238
+ minFoldChars < MINIMUM_FOLD_CHARS_FLOOR) {
239
+ throw new ThresholdPolicyError("minFoldChars",
240
+ "thresholds.minFoldChars must be an integer character count of at least " +
241
+ `${MINIMUM_FOLD_CHARS_FLOOR}; below that a fold can cost more placeholder than the ` +
242
+ "source it replaces");
243
+ }
120
244
  if (!(minTarget < maxTarget)) {
121
245
  throw new ThresholdPolicyError("minTarget<maxTarget",
122
246
  "thresholds.minTarget must sit below thresholds.maxTarget, or a commit has no depth to reach");
123
247
  }
124
- if (!(freshTail < maxTarget)) {
125
- throw new ThresholdPolicyError("freshTail<maxTarget",
126
- "thresholds.freshTail must sit below thresholds.maxTarget, or the trigger fires on protected mass alone");
127
- }
128
- if (!(maxTarget - minTarget >= freshTail)) {
129
- throw new ThresholdPolicyError("gap>=freshTail",
130
- "the hysteresis gap (maxTarget - minTarget) must be at least thresholds.freshTail, " +
131
- "or refilling the protected tail alone re-fires the trigger");
132
- }
133
- const pinned = Math.ceil(MAX_PINNED_SHARE * MINIMUM_SUPPORTED_BUDGET_TOKENS);
134
- const fresh = Math.ceil(freshTail * MINIMUM_SUPPORTED_BUDGET_TOKENS);
135
- if (!(pinned + fresh < Math.floor(maxTarget * MINIMUM_SUPPORTED_BUDGET_TOKENS))) {
136
- throw new ThresholdPolicyError("pinnedPlusFreshTail<maxTarget",
137
- `the pin ceiling (${MAX_PINNED_SHARE}) plus thresholds.freshTail must stay below ` +
138
- `thresholds.maxTarget at the ${MINIMUM_SUPPORTED_BUDGET_TOKENS}-token minimum supported budget`);
139
- }
140
- return { maxTarget, minTarget, freshTail, consolidateAfter };
248
+ return { maxTarget, minTarget, consolidateAfter, minFoldChars };
141
249
  }
142
250
 
143
- export function assertThresholdsServable(thresholds: ActiveContextThresholds, budgetTokens: number): void {
251
+ export function assertThresholdsServable(_thresholds: ActiveContextThresholds, budgetTokens: number): void {
144
252
  if (!Number.isFinite(budgetTokens) || budgetTokens < MINIMUM_SUPPORTED_BUDGET_TOKENS) {
145
253
  throw new ThresholdPolicyError("minimumBudget",
146
254
  `a ${Math.floor(budgetTokens)}-token serving budget is below the ` +
147
255
  `${MINIMUM_SUPPORTED_BUDGET_TOKENS}-token minimum this package supports`);
148
256
  }
149
- const freshTokens = Math.floor(thresholds.freshTail * budgetTokens);
150
- if (freshTokens < MINIMUM_FRESH_TAIL_TOKENS) {
151
- throw new ThresholdPolicyError("freshTailTokens",
152
- `thresholds.freshTail is ${freshTokens} tokens of a ${Math.floor(budgetTokens)}-token serving budget, ` +
153
- `below the ${MINIMUM_FRESH_TAIL_TOKENS}-token minimum one foldable unit needs`);
154
- }
155
- }
156
-
157
- export function zoneBytes(share: number, budgetTokens: number): number {
158
- if (!Number.isFinite(share) || share <= 0 || !Number.isFinite(budgetTokens) || budgetTokens <= 0) return 0;
159
- return Math.floor(share * budgetTokens * ESTIMATED_BYTES_PER_TOKEN);
160
257
  }
161
258
 
162
259
  export function servingBudgetTokens(window: number): number {
@@ -172,54 +269,79 @@ export const PEEK_MIN_SLICE_BYTES = 1_024;
172
269
 
173
270
  export const STATUS_DIET_INDEX_ROWS = 5;
174
271
 
175
- export interface ActiveContextGuidance {
176
- actionResponses: boolean;
177
- }
178
-
179
- export const DEFAULT_GUIDANCE: ActiveContextGuidance = Object.freeze({
180
- actionResponses: true,
181
- });
182
-
183
- const GUIDANCE_KEYS: readonly (keyof ActiveContextGuidance)[] = Object.freeze([
184
- "actionResponses",
185
- ]);
186
-
187
- export function resolveGuidance(value: unknown): ActiveContextGuidance {
188
- if (value === undefined) return DEFAULT_GUIDANCE;
189
- if (!value || typeof value !== "object" || Array.isArray(value)) {
190
- throw new Error("guidance must be an object of { actionResponses } booleans");
191
- }
192
- for (const key of Object.keys(value)) {
193
- if (!GUIDANCE_KEYS.includes(key as keyof ActiveContextGuidance)) {
194
- throw new Error(`guidance has no ${key} setting: the only keys are ` +
195
- `${GUIDANCE_KEYS.join(", ")}, and they are booleans`);
196
- }
197
- }
198
- const resolved = { ...DEFAULT_GUIDANCE } as ActiveContextGuidance;
199
- for (const key of GUIDANCE_KEYS) {
200
- if (!Object.hasOwn(value, key)) continue;
201
- const setting = (value as Record<string, unknown>)[key];
202
- if (typeof setting !== "boolean") throw new Error(`guidance.${key} must be a boolean`);
203
- resolved[key] = setting;
204
- }
205
- return Object.freeze(resolved);
206
- }
207
-
208
272
  export const MAX_CONTEXT_RECEIPTS = 3;
209
273
  export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
210
274
 
211
- export const CONTEXT_MARK_RESPONSE_BYTES = 900;
275
+ /**
276
+ * The post-fold notice's bound, and its own rather than the receipt block's.
277
+ *
278
+ * It is larger because it carries a variable-length list the receipt does not: three
279
+ * fixed sentences of instruction plus one entry per unbriefed fold, and the pending set
280
+ * grows past MAX_FRONTIER_CUTS_PER_PASS whenever the frontier stages faster than the
281
+ * agent answers. 1,200 seats the instruction and roughly eight folds; past that the list
282
+ * gives way and states how many it could not name.
283
+ */
284
+ export const FOLD_NOTICE_BYTES = 2_400;
212
285
  export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
213
286
  export const MAX_UNMARKED_CANDIDATES = 3;
214
287
 
215
288
  export const MAX_FOLD_SPAN_CHARS = 16_000;
216
- export const MAX_WEDGE_ABSORB_TOKENS = 256;
217
289
  export const PEEK_DEFAULT_MAX_BYTES = 16_000;
218
290
  export const PEEK_HEAD_SHARE = 0.6;
219
291
 
220
- export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
292
+ /**
293
+ * A PEEK IS EPHEMERAL UNLESS THE AGENT SAYS OTHERWISE (Shane, 2026-08-22).
294
+ *
295
+ * The default follows the measured choice rather than the cautious one. On sealed run
296
+ * rep 7, six of seven peeks asked for ephemeral at first organic exposure, and the one
297
+ * durable read was the stage-64 probe, exactly where the bytes were wanted standing.
298
+ * The economics agree: a peek is a pure append and costs the cache nothing, while the
299
+ * withdrawal is a tail operation at the withdrawn result's own index, so the cheap read
300
+ * should be the one you get without asking.
301
+ *
302
+ * Only an explicit false is durable. A missing value, and anything the validator has
303
+ * already rejected, reads as ephemeral, so a host or model that never learned the
304
+ * argument still gets the cheap behaviour and can still recover the bytes by peeking
305
+ * again, which is lossless.
306
+ */
307
+ export function peekIsEphemeral(params: Record<string, unknown> | undefined | null): boolean {
308
+ return params?.ephemeral !== false;
309
+ }
310
+
311
+ export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek", "recall"]);
312
+
313
+ /**
314
+ * THE WORKING MEMORY (Shane, 2026-08-26). A session-scoped ordered dictionary the agent
315
+ * maintains beside the fold index: keys list in a table of contents the projection
316
+ * carries, bodies live in state and are read on demand with `recall`. It exists because
317
+ * the campaign measured a structural gap, not a tuning one: a maintained digest (running
318
+ * tallies, current totals) is a different operation from a lossless index, and blending
319
+ * digest prose into the index's briefs was measured making BOTH worse (sol-20260826-full2
320
+ * rep 4: nine confident wrong end-block answers against the deterministic arm's zero).
321
+ * The digest gets its own channel and the index stays mechanical.
322
+ *
323
+ * Three constants, not knobs. The key cap keeps the table of contents a table of
324
+ * contents; the body cap is deliberately larger than a brief because a digest is
325
+ * maintained prose, not an index row; the entry cap bounds the whole store at a size
326
+ * whose table of contents stays one glance.
327
+ */
328
+ export const MEMORY_KEY_CHARS = 64;
329
+ export const MEMORY_BODY_CHARS = 4_000;
330
+ export const MEMORY_KEYS_MAX = 24;
331
+
332
+ export const WORKING_MEMORY_TOOL_ACTIONS = Object.freeze(["remember", "recall"] as const);
333
+
334
+ export interface WorkingMemoryEntry {
335
+ key: string;
336
+ body: string;
337
+ /** Write counter at last update; the table of contents lists freshest first. */
338
+ ordinal: number;
339
+ }
221
340
 
222
- export type MarkOrigin = "agent" | "ladder";
341
+ /** "user" is a mark laid down by the human through /fold-editor; it stages and
342
+ * commits through the same validated path as an agent mark and counts toward the
343
+ * commit's coverage the same way. */
344
+ export type MarkOrigin = "agent" | "ladder" | "user";
223
345
 
224
346
  export interface PendingFoldMark {
225
347
  mark: "fold";
@@ -245,22 +367,37 @@ export const ACTIVE_CONTEXT_POLICY = Object.freeze({
245
367
  refoldRatio: 0.85,
246
368
  prepareRatio: 0.90,
247
369
  responseReserve: 16_384,
248
- minToolChars: 2_000,
249
- minChapterChars: 4_000,
250
- maxChapterChars: 128_000,
251
- maxChapterTurns: 4,
370
+ // The most exact source bytes ONE read may put back in the window, whichever verb
371
+ // asks: an expand collapses its biggest children until the render fits, and a peek
372
+ // clamps its `bytes` to it. Was two numbers until 2026-08-22, when the peek ceiling
373
+ // was `maxChapterChars` 128,000 and that constant also claimed to cap a chapter.
252
374
  maxSourceChars: 200_000,
253
- maxFoldSourceRefs: 256,
375
+ // THE TOOL-CALL DIET (2026-08-24): at COMMIT TIME ONLY, a stale tool result inside
376
+ // the configured toolFoldThreshold share of the projected window is clipped IN VIEW
377
+ // to an identified head, the full bytes staying peek-recoverable behind its entry id.
378
+ // The head keeps the leading paragraph up to its cap (gate 134's rule); results at or
379
+ // under the floor stay whole, because clipping a small result buys almost nothing and
380
+ // adds a peek indirection. Constants, not options, for v1: the share point is the one
381
+ // public knob.
382
+ toolClipHeadChars: 500,
383
+ toolClipFloorChars: 2_000,
254
384
  maxBriefChars: 2_000,
385
+ // The agent's share of maxBriefChars when a supplied brief augments the deterministic
386
+ // head (sol-20260823-live rep 7: supplied briefs REPLACED the fact-carrying head and
387
+ // 74 of 95 peeks recovered values the run's own briefs dropped). The head keeps the
388
+ // remainder, so identification can never be displaced by a long agent clause.
389
+ agentBriefReserve: 600,
255
390
  orientationMessages: 2,
256
391
  maxOrientationChars: 12_000,
257
392
  });
258
393
 
259
394
  export type FoldKind = "tool-result" | "chapter" | "consolidation";
395
+ export type ToolClip = { callId: string; entryId: string };
260
396
  export type FoldPart = { kind: "raw"; ref: EvidenceRef } | { kind: "fold"; foldId: string };
261
397
  export type BriefProvenance =
262
398
  | { kind: "supplied" }
263
399
  | { kind: "deterministic" }
400
+ | { kind: "augmented" }
264
401
  | {
265
402
  kind: "model" | "luna";
266
403
  provider: string;
@@ -312,6 +449,7 @@ export interface ActiveContextState {
312
449
  pendingMarks?: PendingMark[];
313
450
  pinnedPeeks?: never;
314
451
  briefs?: Record<string, BriefOverride>;
452
+ clips?: ToolClip[];
315
453
  prepared?: PreparedFold;
316
454
  advisory?: {
317
455
  highWater: number;
@@ -319,6 +457,8 @@ export interface ActiveContextState {
319
457
  armed?: { milestone: AdvisoryMilestone; threshold: number; scheduleKey: string };
320
458
  };
321
459
  rider?: { epoch: number; text: string };
460
+ /** The working memory's entries, in write order; absent when empty. */
461
+ memory?: WorkingMemoryEntry[];
322
462
  }
323
463
 
324
464
  export interface FoldRecordRef {
@@ -347,6 +487,8 @@ export interface ActiveContextCheckpointV2 {
347
487
  leases?: Record<string, number>;
348
488
  pendingMarks?: PendingMark[];
349
489
  briefs?: Record<string, BriefOverride>;
490
+ clips?: ToolClip[];
491
+ memory?: WorkingMemoryEntry[];
350
492
  advisory?: NonNullable<ActiveContextState["advisory"]>;
351
493
  rider?: NonNullable<ActiveContextState["rider"]>;
352
494
  lastCall?: NonNullable<ActiveContextState["lastCall"]>;
@@ -378,6 +520,21 @@ export interface ActiveContextDeltaV2 {
378
520
  briefs?: Record<string, BriefOverride>;
379
521
  addBriefs?: Record<string, BriefOverride>;
380
522
  removeBriefIds?: string[];
523
+ // `clips` on a delta is the WHOLE array: what every delta written before 2026-08-24
524
+ // carries, and what sealed sessions replay. New deltas carry the change instead. Clips
525
+ // are appended and never edited, so the change is a count of what the base already holds
526
+ // plus the ones that are new; a rollback that shortens the array states a lower count.
527
+ // The order list the marks use would be WORSE than the bug here, because a callId is
528
+ // about a hundred characters and re-listing thirty of them on every delta costs more
529
+ // than the array it replaces.
530
+ clips?: ToolClip[];
531
+ clipBase?: number;
532
+ addClips?: ToolClip[];
533
+ // `memory` on a delta is the WHOLE array. It is bounded by MEMORY_KEYS_MAX entries and
534
+ // edited in place by key, so a change form would have to restate the key order the way
535
+ // the marks do while saving at most a handful of bounded bodies: the whole array is the
536
+ // cheaper wire. Absent means empty, by the same discriminator rule as the marks.
537
+ memory?: WorkingMemoryEntry[];
381
538
  advisory?: NonNullable<ActiveContextState["advisory"]>;
382
539
  rider?: NonNullable<ActiveContextState["rider"]>;
383
540
  lastCall?: NonNullable<ActiveContextState["lastCall"]>;
@@ -414,7 +571,6 @@ export interface ActiveContextSnapshot {
414
571
  thresholds: ActiveContextThresholds;
415
572
  budgetTokens: number;
416
573
  protectedIndices: Set<number>;
417
- toolProtectedIndices: Set<number>;
418
574
  policy: typeof ACTIVE_CONTEXT_POLICY;
419
575
  toolName: string;
420
576
  brandNoun: string;