pi-fold 2.0.0 → 3.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.
- package/README.md +49 -28
- package/extensions/active-context.ts +1011 -444
- package/extensions/index.js +16 -3
- package/extensions/lib/curation.ts +56 -60
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +26 -42
- package/extensions/lib/instrumentation.ts +10 -3
- package/extensions/lib/measurement.ts +6 -12
- package/extensions/lib/persistence.ts +62 -38
- package/extensions/lib/policy.ts +171 -89
- package/extensions/lib/scheduling.ts +341 -108
- package/extensions/lib/selection.ts +6 -78
- package/extensions/lib/tool-surface.ts +29 -33
- package/extensions/lib/transcript.ts +17 -60
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
package/extensions/lib/policy.ts
CHANGED
|
@@ -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: "
|
|
9
|
-
fold: "fold
|
|
8
|
+
status: "fold-status",
|
|
9
|
+
fold: "fold",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
export function entryTypeNamespace(entryTypePrefix: string): string {
|
|
@@ -33,10 +33,64 @@ 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", "
|
|
38
|
-
"
|
|
59
|
+
"status", "peek", "brief", "expand", "refold", "pin", "unpin",
|
|
60
|
+
"reboundary", "unmark",
|
|
39
61
|
] as const);
|
|
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
|
+
});
|
|
40
94
|
export type ActiveContextToolAction = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
|
|
41
95
|
export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
|
|
42
96
|
export const DEFAULT_CONTEXT_WINDOW = 272_000;
|
|
@@ -45,38 +99,86 @@ export const MAX_EXPAND_LEASES = 64;
|
|
|
45
99
|
|
|
46
100
|
export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
|
|
47
101
|
|
|
102
|
+
/**
|
|
103
|
+
* How far below the commit trigger the agent is told a commit is coming (Shane,
|
|
104
|
+
* 2026-08-22: "around 10% from the commit threshold"). A share of the serving budget
|
|
105
|
+
* rather than a token count, because a token count means a different fraction of every
|
|
106
|
+
* deployment's window. The advisory takes the wider of this and one worst recent inflow
|
|
107
|
+
* step, so a single large result cannot step over the whole band unasked.
|
|
108
|
+
*/
|
|
48
109
|
export const MAX_PINNED_SHARE = 0.25;
|
|
49
110
|
|
|
50
111
|
export interface ActiveContextThresholds {
|
|
51
112
|
maxTarget: number;
|
|
52
113
|
minTarget: number;
|
|
53
|
-
freshTail: number;
|
|
54
114
|
consolidateAfter: number;
|
|
115
|
+
minFoldChars: number;
|
|
55
116
|
}
|
|
56
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The floor a fold has to clear to be worth making, in CHARACTERS (Shane, 2026-08-21).
|
|
120
|
+
*
|
|
121
|
+
* Characters rather than tokens because characters are provider-agnostic: a token count
|
|
122
|
+
* means a different amount of text on every wire, and this number exists so a person can
|
|
123
|
+
* reason about it. It replaces two constants nobody could tell apart, minToolChars at
|
|
124
|
+
* 2,000 and minChapterChars at 4,000, and it is not specific to tool results or to
|
|
125
|
+
* chapters: everything in the window costs tokens, so everything counts toward the floor.
|
|
126
|
+
*
|
|
127
|
+
* It is a floor, not a cut. The automatic span accumulates whole messages and stops at
|
|
128
|
+
* the first one that carries it OVER the floor, so a fold is always at least this big
|
|
129
|
+
* and never cuts a message in half to hit the number exactly. The same floor is what a
|
|
130
|
+
* gap between two folds has to clear to survive: anything smaller is absorbed by the
|
|
131
|
+
* fold beside it rather than left as a raw sliver, because a sliver costs its bytes and
|
|
132
|
+
* buys nothing.
|
|
133
|
+
*
|
|
134
|
+
* 8,000 was chosen against the sealed corpus. Rep 1 of sol-20260814-traps folded 89
|
|
135
|
+
* spans at a median of 57,808 source chars, but nine of them came in under 8,000 and
|
|
136
|
+
* TWO were net negative, 2,726 chars of source replaced by 5,341 of placeholder (196
|
|
137
|
+
* percent) and 6,384 replaced by 10,531 (165 percent). A fold that grows the window is
|
|
138
|
+
* not a fold. The floor sits above every observed inversion with room to spare.
|
|
139
|
+
*/
|
|
140
|
+
export const MINIMUM_FOLD_CHARS_FLOOR = 2_000;
|
|
141
|
+
|
|
57
142
|
export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
|
|
58
143
|
maxTarget: 0.80,
|
|
59
|
-
// 0.
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// and
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
144
|
+
// 0.40, up from 0.20 (Shane 2026-08-23: "we change the thresholds to what hits that
|
|
145
|
+
// 100k/250k or so"). This is the only threshold that moved, because the corpus says
|
|
146
|
+
// the top of the band was already where he wants it and only the floor was wrong.
|
|
147
|
+
//
|
|
148
|
+
// Read against the 19 mature sealed pifold runs that share a 251,520-token budget,
|
|
149
|
+
// where the trigger sat at 0.80 of it, 201,216 tokens. Peak occupancy per run came in
|
|
150
|
+
// at a median 237,399, a 0.944 share, p90 247,551, max 257,117: a session runs roughly
|
|
151
|
+
// 36,000 tokens PAST its own trigger before the commit it fired takes effect, because
|
|
152
|
+
// the trigger fires at a measurement and inflow continues while the epoch is computed.
|
|
153
|
+
// So 0.80 already delivers a 250k top, and raising it toward 250k directly would put
|
|
154
|
+
// the median peak over the budget and into the fence.
|
|
155
|
+
//
|
|
156
|
+
// The floor was the miss. Across 206 commit landings the session came to rest at a
|
|
157
|
+
// median 79,034 tokens, and 153 of the 206 landed below 100,000. Landings sit ABOVE
|
|
158
|
+
// the aim rather than at it, since the class law and the available material both
|
|
159
|
+
// outrank it, so moving the aim to 0.40 sets a floor near where it lands rather than
|
|
160
|
+
// far under it: 102,246 tokens against a default 272,000-token descriptor, 100,608
|
|
161
|
+
// against the harness budget.
|
|
162
|
+
//
|
|
163
|
+
// 0.40 also makes the aim reachable for the first time. MAX_PINNED_SHARE is 0.25, so a
|
|
164
|
+
// fully pinned session legally holds more than a 0.20 aim asks for and the target was
|
|
165
|
+
// a number the class law outranked by construction. The epoch receipt still reports
|
|
166
|
+
// targetBudgetShare beside actualFreedBudgetShare, so any remaining shortfall stays a
|
|
167
|
+
// readable fact per epoch rather than a silent miss.
|
|
168
|
+
//
|
|
169
|
+
// The cost is paid in cadence and it is the 2026-08-14 argument running backwards: a
|
|
170
|
+
// shallower cut buys less runway for nearly the same commit, so the refill distance
|
|
171
|
+
// goes from the observed 0.314 -> 0.80 to 0.40 -> 0.80 and the same session shape owes
|
|
172
|
+
// about a fifth more epochs, which is a fifth more full-prefix rewrites. Shane bought
|
|
173
|
+
// that deliberately: 100k of raw context held across the whole cycle is what he is
|
|
174
|
+
// paying for. The validation laws below bind exactly as before.
|
|
175
|
+
minTarget: 0.40,
|
|
73
176
|
consolidateAfter: 10,
|
|
177
|
+
minFoldChars: 8_000,
|
|
74
178
|
});
|
|
75
179
|
|
|
76
180
|
export const MINIMUM_SUPPORTED_BUDGET_TOKENS = 10_000;
|
|
77
181
|
|
|
78
|
-
export const MINIMUM_FRESH_TAIL_TOKENS = 500;
|
|
79
|
-
|
|
80
182
|
export class ThresholdPolicyError extends Error {
|
|
81
183
|
readonly invariant: string;
|
|
82
184
|
constructor(invariant: string, message: string) {
|
|
@@ -99,7 +201,7 @@ export function resolveThresholds(value: unknown): ActiveContextThresholds {
|
|
|
99
201
|
throw new ThresholdPolicyError("shape", "thresholds must be an object with all four fields");
|
|
100
202
|
}
|
|
101
203
|
const supplied = value as Record<string, unknown>;
|
|
102
|
-
const fields = ["maxTarget", "minTarget", "
|
|
204
|
+
const fields = ["maxTarget", "minTarget", "consolidateAfter", "minFoldChars"] as const;
|
|
103
205
|
for (const key of Object.keys(supplied)) {
|
|
104
206
|
if (!(fields as readonly string[]).includes(key)) {
|
|
105
207
|
throw new ThresholdPolicyError("shape", `thresholds has no ${key} field`);
|
|
@@ -112,46 +214,31 @@ export function resolveThresholds(value: unknown): ActiveContextThresholds {
|
|
|
112
214
|
}
|
|
113
215
|
const maxTarget = thresholdProportion(supplied.maxTarget, "maxTarget");
|
|
114
216
|
const minTarget = thresholdProportion(supplied.minTarget, "minTarget");
|
|
115
|
-
const freshTail = thresholdProportion(supplied.freshTail, "freshTail");
|
|
116
217
|
const consolidateAfter = supplied.consolidateAfter;
|
|
117
218
|
if (typeof consolidateAfter !== "number" || !Number.isInteger(consolidateAfter) || consolidateAfter < 1) {
|
|
118
219
|
throw new ThresholdPolicyError("consolidateAfter", "thresholds.consolidateAfter must be a positive integer count");
|
|
119
220
|
}
|
|
221
|
+
const minFoldChars = supplied.minFoldChars;
|
|
222
|
+
if (typeof minFoldChars !== "number" || !Number.isInteger(minFoldChars) ||
|
|
223
|
+
minFoldChars < MINIMUM_FOLD_CHARS_FLOOR) {
|
|
224
|
+
throw new ThresholdPolicyError("minFoldChars",
|
|
225
|
+
"thresholds.minFoldChars must be an integer character count of at least " +
|
|
226
|
+
`${MINIMUM_FOLD_CHARS_FLOOR}; below that a fold can cost more placeholder than the ` +
|
|
227
|
+
"source it replaces");
|
|
228
|
+
}
|
|
120
229
|
if (!(minTarget < maxTarget)) {
|
|
121
230
|
throw new ThresholdPolicyError("minTarget<maxTarget",
|
|
122
231
|
"thresholds.minTarget must sit below thresholds.maxTarget, or a commit has no depth to reach");
|
|
123
232
|
}
|
|
124
|
-
|
|
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 };
|
|
233
|
+
return { maxTarget, minTarget, consolidateAfter, minFoldChars };
|
|
141
234
|
}
|
|
142
235
|
|
|
143
|
-
export function assertThresholdsServable(
|
|
236
|
+
export function assertThresholdsServable(_thresholds: ActiveContextThresholds, budgetTokens: number): void {
|
|
144
237
|
if (!Number.isFinite(budgetTokens) || budgetTokens < MINIMUM_SUPPORTED_BUDGET_TOKENS) {
|
|
145
238
|
throw new ThresholdPolicyError("minimumBudget",
|
|
146
239
|
`a ${Math.floor(budgetTokens)}-token serving budget is below the ` +
|
|
147
240
|
`${MINIMUM_SUPPORTED_BUDGET_TOKENS}-token minimum this package supports`);
|
|
148
241
|
}
|
|
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
242
|
}
|
|
156
243
|
|
|
157
244
|
export function zoneBytes(share: number, budgetTokens: number): number {
|
|
@@ -172,54 +259,51 @@ export const PEEK_MIN_SLICE_BYTES = 1_024;
|
|
|
172
259
|
|
|
173
260
|
export const STATUS_DIET_INDEX_ROWS = 5;
|
|
174
261
|
|
|
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
262
|
export const MAX_CONTEXT_RECEIPTS = 3;
|
|
209
263
|
export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
|
|
210
264
|
|
|
211
|
-
|
|
265
|
+
/**
|
|
266
|
+
* The post-fold notice's bound, and its own rather than the receipt block's.
|
|
267
|
+
*
|
|
268
|
+
* It is larger because it carries a variable-length list the receipt does not: three
|
|
269
|
+
* fixed sentences of instruction plus one entry per unbriefed fold, and the pending set
|
|
270
|
+
* grows past MAX_FRONTIER_CUTS_PER_PASS whenever the frontier stages faster than the
|
|
271
|
+
* agent answers. 1,200 seats the instruction and roughly eight folds; past that the list
|
|
272
|
+
* gives way and states how many it could not name.
|
|
273
|
+
*/
|
|
274
|
+
export const FOLD_NOTICE_BYTES = 1_200;
|
|
212
275
|
export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
|
|
213
276
|
export const MAX_UNMARKED_CANDIDATES = 3;
|
|
214
277
|
|
|
215
278
|
export const MAX_FOLD_SPAN_CHARS = 16_000;
|
|
216
|
-
export const MAX_WEDGE_ABSORB_TOKENS = 256;
|
|
217
279
|
export const PEEK_DEFAULT_MAX_BYTES = 16_000;
|
|
218
280
|
export const PEEK_HEAD_SHARE = 0.6;
|
|
219
281
|
|
|
282
|
+
/**
|
|
283
|
+
* A PEEK IS EPHEMERAL UNLESS THE AGENT SAYS OTHERWISE (Shane, 2026-08-22).
|
|
284
|
+
*
|
|
285
|
+
* The default follows the measured choice rather than the cautious one. On sealed run
|
|
286
|
+
* rep 7, six of seven peeks asked for ephemeral at first organic exposure, and the one
|
|
287
|
+
* durable read was the stage-64 probe, exactly where the bytes were wanted standing.
|
|
288
|
+
* The economics agree: a peek is a pure append and costs the cache nothing, while the
|
|
289
|
+
* withdrawal is a tail operation at the withdrawn result's own index, so the cheap read
|
|
290
|
+
* should be the one you get without asking.
|
|
291
|
+
*
|
|
292
|
+
* Only an explicit false is durable. A missing value, and anything the validator has
|
|
293
|
+
* already rejected, reads as ephemeral, so a host or model that never learned the
|
|
294
|
+
* argument still gets the cheap behaviour and can still recover the bytes by peeking
|
|
295
|
+
* again, which is lossless.
|
|
296
|
+
*/
|
|
297
|
+
export function peekIsEphemeral(params: Record<string, unknown> | undefined | null): boolean {
|
|
298
|
+
return params?.ephemeral !== false;
|
|
299
|
+
}
|
|
300
|
+
|
|
220
301
|
export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
|
|
221
302
|
|
|
222
|
-
|
|
303
|
+
/** "user" is a mark laid down by the human through /fold-editor; it stages and
|
|
304
|
+
* commits through the same validated path as an agent mark and counts toward the
|
|
305
|
+
* commit's coverage the same way. */
|
|
306
|
+
export type MarkOrigin = "agent" | "ladder" | "user";
|
|
223
307
|
|
|
224
308
|
export interface PendingFoldMark {
|
|
225
309
|
mark: "fold";
|
|
@@ -245,12 +329,11 @@ export const ACTIVE_CONTEXT_POLICY = Object.freeze({
|
|
|
245
329
|
refoldRatio: 0.85,
|
|
246
330
|
prepareRatio: 0.90,
|
|
247
331
|
responseReserve: 16_384,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
332
|
+
// The most exact source bytes ONE read may put back in the window, whichever verb
|
|
333
|
+
// asks: an expand collapses its biggest children until the render fits, and a peek
|
|
334
|
+
// clamps its `bytes` to it. Was two numbers until 2026-08-22, when the peek ceiling
|
|
335
|
+
// was `maxChapterChars` 128,000 and that constant also claimed to cap a chapter.
|
|
252
336
|
maxSourceChars: 200_000,
|
|
253
|
-
maxFoldSourceRefs: 256,
|
|
254
337
|
maxBriefChars: 2_000,
|
|
255
338
|
orientationMessages: 2,
|
|
256
339
|
maxOrientationChars: 12_000,
|
|
@@ -414,7 +497,6 @@ export interface ActiveContextSnapshot {
|
|
|
414
497
|
thresholds: ActiveContextThresholds;
|
|
415
498
|
budgetTokens: number;
|
|
416
499
|
protectedIndices: Set<number>;
|
|
417
|
-
toolProtectedIndices: Set<number>;
|
|
418
500
|
policy: typeof ACTIVE_CONTEXT_POLICY;
|
|
419
501
|
toolName: string;
|
|
420
502
|
brandNoun: string;
|