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.
- package/README.md +79 -191
- package/extensions/active-context.ts +1332 -526
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +17 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +94 -61
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +207 -59
- package/extensions/lib/instrumentation.ts +10 -9
- package/extensions/lib/measurement.ts +43 -16
- package/extensions/lib/persistence.ts +273 -46
- package/extensions/lib/policy.ts +251 -95
- package/extensions/lib/scheduling.ts +446 -126
- package/extensions/lib/selection.ts +117 -116
- package/extensions/lib/tool-surface.ts +60 -33
- package/extensions/lib/transcript.ts +82 -74
- package/extensions/settings.ts +456 -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,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", "
|
|
38
|
-
"
|
|
59
|
+
"status", "peek", "brief", "expand", "refold", "pin", "unpin",
|
|
60
|
+
"reboundary", "unmark",
|
|
39
61
|
] as const);
|
|
40
|
-
|
|
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
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// and
|
|
69
|
-
//
|
|
70
|
-
//
|
|
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", "
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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;
|