pi-fold 1.0.4 → 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 +75 -28
- package/extensions/active-context.ts +1213 -2756
- package/extensions/evidence.js +0 -10
- package/extensions/index.js +25 -32
- package/extensions/json.ts +3 -29
- package/extensions/lib/canonical.ts +0 -8
- package/extensions/lib/curation.ts +52 -313
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +57 -446
- package/extensions/lib/instrumentation.ts +13 -127
- package/extensions/lib/measurement.ts +6 -83
- package/extensions/lib/persistence.ts +272 -229
- package/extensions/lib/policy.ts +165 -612
- package/extensions/lib/rollback.ts +0 -53
- package/extensions/lib/scheduling.ts +352 -450
- package/extensions/lib/selection.ts +212 -390
- package/extensions/lib/tool-surface.ts +29 -46
- package/extensions/lib/transcript.ts +47 -166
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
- package/extensions/lib/surfacing.ts +0 -604
- package/extensions/summarizer.js +0 -291
package/extensions/lib/policy.ts
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import type { EvidenceRef } from "../json.ts";
|
|
2
2
|
|
|
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
3
|
export const DEFAULT_ACTIVE_CONTEXT_TOOL_NAME = "pi_fold_context";
|
|
16
4
|
export const DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX = "pi-fold-active-context";
|
|
17
5
|
export const DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL = "pi-fold Active Context";
|
|
18
6
|
export const DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN = "pi-fold";
|
|
19
7
|
export const DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES = Object.freeze({
|
|
20
|
-
status: "
|
|
21
|
-
fold: "fold
|
|
8
|
+
status: "fold-status",
|
|
9
|
+
fold: "fold",
|
|
22
10
|
});
|
|
23
11
|
|
|
24
12
|
export function entryTypeNamespace(entryTypePrefix: string): string {
|
|
@@ -44,206 +32,153 @@ export function contextBrand(brandNoun: string): string {
|
|
|
44
32
|
const DEFAULT_ENTRY_NAMESPACE = entryTypeNamespace(DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX);
|
|
45
33
|
export const ACTIVE_CONTEXT_STATE_ENTRY = `${DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX}-state`;
|
|
46
34
|
export const ACTIVE_CONTEXT_FOLD_RECORD_ENTRY = `${DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX}-fold-record`;
|
|
47
|
-
export const NATIVE_COMPACTION_RECEIPT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-native-compaction-receipt`;
|
|
48
|
-
export const NATIVE_COMPACTION_DECISION_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-native-compaction-decision`;
|
|
49
35
|
export const PROVIDER_CONTEXT_MEASUREMENT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-provider-context-measurement`;
|
|
50
|
-
export const ACTIVE_CONTEXT_STATUS_KEY = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
51
36
|
/**
|
|
52
|
-
*
|
|
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.
|
|
37
|
+
* How many unbriefed pending folds stand before the runtime says so.
|
|
58
38
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* the gated commit, the fence and the recovery lane -- are unchanged and unexposed.
|
|
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.
|
|
64
43
|
*/
|
|
65
|
-
export const
|
|
66
|
-
"status", "peek", "fold", "expand", "refold", "protect", "unprotect",
|
|
67
|
-
"rebrief", "reboundary", "unmark",
|
|
68
|
-
] as const);
|
|
69
|
-
export type ActiveContextToolAction = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
|
|
70
|
-
export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
|
|
71
|
-
export const DEFAULT_CONTEXT_WINDOW = 272_000;
|
|
72
|
-
export const EXPAND_LEASE_GENERATIONS = 8;
|
|
73
|
-
export const MAX_EXPAND_LEASES = 64;
|
|
74
|
-
export const MAX_ADVISORY_DELIVERIES_PER_MILESTONE = 16;
|
|
44
|
+
export const UNBRIEFED_FOLDS_BEFORE_NOTICE = 3;
|
|
75
45
|
|
|
76
|
-
// Surfacing SELECTOR structure. These stay INTERNAL constants rather than public
|
|
77
|
-
// options: they are the knobs the surfacing experiment is meant to settle, and an
|
|
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
46
|
/**
|
|
86
|
-
*
|
|
47
|
+
* How many spans the frontier cuts on one projection pass.
|
|
87
48
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* is
|
|
93
|
-
*
|
|
94
|
-
* The MARGIN keeps the pair from meeting in the middle, where both readings are weak
|
|
95
|
-
* and neither says anything.
|
|
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.
|
|
96
55
|
*/
|
|
97
|
-
export const
|
|
98
|
-
|
|
99
|
-
export const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
export const SURFACING_OUTCOME_WINDOW_ORDINALS = 12;
|
|
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;
|
|
117
|
-
|
|
118
|
-
// Conservative LOWER bound on UTF-8 bytes per provider token, used only to cap
|
|
119
|
-
// the protected byte tail as a share of a small window; never to estimate usage.
|
|
120
|
-
export const BYTES_PER_TOKEN_FLOOR = 2;
|
|
121
|
-
|
|
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.
|
|
129
|
-
|
|
130
|
-
// Epoch knobs stay INTERNAL constants for the same reason the surfacing knobs do:
|
|
131
|
-
// they are what the round-2 cost measurement exists to settle, and an option surface
|
|
132
|
-
// fixed before that data would freeze a guess.
|
|
56
|
+
export const MAX_FRONTIER_CUTS_PER_PASS = 8;
|
|
57
|
+
|
|
58
|
+
export const ACTIVE_CONTEXT_TOOL_ACTIONS = Object.freeze([
|
|
59
|
+
"status", "peek", "brief", "expand", "refold", "pin", "unpin",
|
|
60
|
+
"reboundary", "unmark",
|
|
61
|
+
] as const);
|
|
133
62
|
/**
|
|
134
|
-
*
|
|
135
|
-
* firing at all.
|
|
63
|
+
* FOLD IS DELETED AS AN AGENT VERB (Shane, 2026-08-23), and `brief` is what replaced it.
|
|
136
64
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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.
|
|
143
71
|
*
|
|
144
|
-
* So
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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.
|
|
148
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 = typeof ACTIVE_CONTEXT_TOOL_ACTIONS[number];
|
|
95
|
+
export const USER_RESCUE_MAX_SOURCE_CHARS = 512_000;
|
|
96
|
+
export const DEFAULT_CONTEXT_WINDOW = 272_000;
|
|
97
|
+
export const EXPAND_LEASE_GENERATIONS = 8;
|
|
98
|
+
export const MAX_EXPAND_LEASES = 64;
|
|
99
|
+
|
|
149
100
|
export const COMMIT_RECLAIM_FLOOR_SHARE = 0.02;
|
|
150
101
|
|
|
151
102
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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.
|
|
157
108
|
*/
|
|
158
109
|
export const MAX_PINNED_SHARE = 0.25;
|
|
159
110
|
|
|
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
111
|
export interface ActiveContextThresholds {
|
|
184
|
-
/** Occupancy of the serving budget that fires an automatic commit. */
|
|
185
112
|
maxTarget: number;
|
|
186
|
-
/** Occupancy an automatic commit folds down toward. The hysteresis gap is maxTarget - minTarget. */
|
|
187
113
|
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
114
|
consolidateAfter: number;
|
|
115
|
+
minFoldChars: number;
|
|
192
116
|
}
|
|
193
117
|
|
|
194
118
|
/**
|
|
195
|
-
* The
|
|
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.
|
|
119
|
+
* The floor a fold has to clear to be worth making, in CHARACTERS (Shane, 2026-08-21).
|
|
200
120
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* about 7 points, so six worst-case steps land between events.
|
|
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.
|
|
207
126
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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.
|
|
213
133
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* thing this object exists to make singular.
|
|
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.
|
|
220
139
|
*/
|
|
140
|
+
export const MINIMUM_FOLD_CHARS_FLOOR = 2_000;
|
|
141
|
+
|
|
221
142
|
export const DEFAULT_THRESHOLDS: Readonly<ActiveContextThresholds> = Object.freeze({
|
|
222
143
|
maxTarget: 0.80,
|
|
223
|
-
|
|
224
|
-
|
|
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,
|
|
225
176
|
consolidateAfter: 10,
|
|
177
|
+
minFoldChars: 8_000,
|
|
226
178
|
});
|
|
227
179
|
|
|
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
180
|
export const MINIMUM_SUPPORTED_BUDGET_TOKENS = 10_000;
|
|
239
181
|
|
|
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
182
|
export class ThresholdPolicyError extends Error {
|
|
248
183
|
readonly invariant: string;
|
|
249
184
|
constructor(invariant: string, message: string) {
|
|
@@ -260,21 +195,13 @@ function thresholdProportion(value: unknown, field: string): number {
|
|
|
260
195
|
return value;
|
|
261
196
|
}
|
|
262
197
|
|
|
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
198
|
export function resolveThresholds(value: unknown): ActiveContextThresholds {
|
|
272
199
|
if (value === undefined) return { ...DEFAULT_THRESHOLDS };
|
|
273
200
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
274
201
|
throw new ThresholdPolicyError("shape", "thresholds must be an object with all four fields");
|
|
275
202
|
}
|
|
276
203
|
const supplied = value as Record<string, unknown>;
|
|
277
|
-
const fields = ["maxTarget", "minTarget", "
|
|
204
|
+
const fields = ["maxTarget", "minTarget", "consolidateAfter", "minFoldChars"] as const;
|
|
278
205
|
for (const key of Object.keys(supplied)) {
|
|
279
206
|
if (!(fields as readonly string[]).includes(key)) {
|
|
280
207
|
throw new ThresholdPolicyError("shape", `thresholds has no ${key} field`);
|
|
@@ -287,73 +214,33 @@ export function resolveThresholds(value: unknown): ActiveContextThresholds {
|
|
|
287
214
|
}
|
|
288
215
|
const maxTarget = thresholdProportion(supplied.maxTarget, "maxTarget");
|
|
289
216
|
const minTarget = thresholdProportion(supplied.minTarget, "minTarget");
|
|
290
|
-
const freshTail = thresholdProportion(supplied.freshTail, "freshTail");
|
|
291
217
|
const consolidateAfter = supplied.consolidateAfter;
|
|
292
218
|
if (typeof consolidateAfter !== "number" || !Number.isInteger(consolidateAfter) || consolidateAfter < 1) {
|
|
293
219
|
throw new ThresholdPolicyError("consolidateAfter", "thresholds.consolidateAfter must be a positive integer count");
|
|
294
220
|
}
|
|
295
|
-
|
|
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
|
+
}
|
|
296
229
|
if (!(minTarget < maxTarget)) {
|
|
297
230
|
throw new ThresholdPolicyError("minTarget<maxTarget",
|
|
298
231
|
"thresholds.minTarget must sit below thresholds.maxTarget, or a commit has no depth to reach");
|
|
299
232
|
}
|
|
300
|
-
|
|
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 };
|
|
233
|
+
return { maxTarget, minTarget, consolidateAfter, minFoldChars };
|
|
322
234
|
}
|
|
323
235
|
|
|
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 {
|
|
236
|
+
export function assertThresholdsServable(_thresholds: ActiveContextThresholds, budgetTokens: number): void {
|
|
330
237
|
if (!Number.isFinite(budgetTokens) || budgetTokens < MINIMUM_SUPPORTED_BUDGET_TOKENS) {
|
|
331
238
|
throw new ThresholdPolicyError("minimumBudget",
|
|
332
239
|
`a ${Math.floor(budgetTokens)}-token serving budget is below the ` +
|
|
333
240
|
`${MINIMUM_SUPPORTED_BUDGET_TOKENS}-token minimum this package supports`);
|
|
334
241
|
}
|
|
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
242
|
}
|
|
342
243
|
|
|
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.
|
|
352
|
-
*
|
|
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.
|
|
356
|
-
*/
|
|
357
244
|
export function zoneBytes(share: number, budgetTokens: number): number {
|
|
358
245
|
if (!Number.isFinite(share) || share <= 0 || !Number.isFinite(budgetTokens) || budgetTokens <= 0) return 0;
|
|
359
246
|
return Math.floor(share * budgetTokens * ESTIMATED_BYTES_PER_TOKEN);
|
|
@@ -365,281 +252,59 @@ export function servingBudgetTokens(window: number): number {
|
|
|
365
252
|
}
|
|
366
253
|
|
|
367
254
|
export const MAX_PENDING_MARKS = 256;
|
|
368
|
-
/** Enough batches to actually reach the floor on a wide window before the loop exits. */
|
|
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;
|
|
378
|
-
/** Estimate only; provider token accounting always comes from a measured response. */
|
|
379
255
|
export const ESTIMATED_BYTES_PER_TOKEN = 4;
|
|
380
|
-
/** Rendered navigation/topology overhead assumed around a brief when estimating a placeholder. */
|
|
381
256
|
export const ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES = 240;
|
|
382
257
|
|
|
383
|
-
// ---------------------------------------------------------------------------
|
|
384
|
-
// The reliability spine. Every behavior below shipped as an off-by-default lever
|
|
385
|
-
// through iterations 2 and 3 and was sealed ON by rep 14 (64/64), so it is now
|
|
386
|
-
// simply how pi-fold works: the flags and their conditional twins are gone, and
|
|
387
|
-
// git history is the lineage. What stays configurable is only what is genuinely a
|
|
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.
|
|
392
|
-
//
|
|
393
|
-
// Sealed unconditional: admission control, retained pending marks, the eligible-share
|
|
394
|
-
// commit trigger, stage-identified briefs, the current-turn commit guard, the
|
|
395
|
-
// pinned-mass backstop, the status index diet, delivery-counted advisories, and
|
|
396
|
-
// projection instrumentation.
|
|
397
|
-
//
|
|
398
|
-
// NOT sealed, deleted: ephemeral peek and its per-call `ephemeral` override. It rewrote
|
|
399
|
-
// a consumed peek result in place on the theory the edit was tail-local. It is not: the
|
|
400
|
-
// rewrite waits for a later assistant message to exist, by which point the window has
|
|
401
|
-
// grown over it and the edit lands mid-prefix. Rep 22 measured two of them costing 100k
|
|
402
|
-
// fresh tokens. A peek is append-only; the tool-fold rung reclaims the duplicate bytes
|
|
403
|
-
// at the next commit, which is the one moment a rewrite is already being paid for.
|
|
404
|
-
// ---------------------------------------------------------------------------
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* The provider serving window is the TOTAL admission budget minus whatever output
|
|
408
|
-
* reservation the deployment actually asked for; the per-request max-input descriptor
|
|
409
|
-
* assumes a full output reservation and understates the real ceiling. Measured
|
|
410
|
-
* 2026-08-06: a run aborted at ~297k projected tokens against a 272k descriptor while
|
|
411
|
-
* the same provider had just accepted 339,689 tokens. Declaring the total window is
|
|
412
|
-
* the deployment's own fact, so it is the option; there is no separate boolean, and
|
|
413
|
-
* an undeclared window falls back to the descriptor and SAYS so in the accounting.
|
|
414
|
-
*/
|
|
415
|
-
|
|
416
|
-
/** Narrowing is what makes a refusal governance instead of denial. */
|
|
417
258
|
export const PEEK_MIN_SLICE_BYTES = 1_024;
|
|
418
259
|
|
|
419
|
-
|
|
420
|
-
export const STATUS_DIET_SUGGESTIONS = 5;
|
|
260
|
+
export const STATUS_DIET_INDEX_ROWS = 5;
|
|
421
261
|
|
|
422
|
-
export const MAX_PROJECTION_HASH_RECORDS = 64;
|
|
423
|
-
|
|
424
|
-
// ---------------------------------------------------------------------------
|
|
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.
|
|
428
|
-
// ---------------------------------------------------------------------------
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* The three occupancy waypoints, as shares of the truthful serving budget.
|
|
432
|
-
*
|
|
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.
|
|
438
|
-
*/
|
|
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;
|
|
442
|
-
/**
|
|
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.
|
|
445
|
-
*/
|
|
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
|
-
]);
|
|
461
|
-
|
|
462
|
-
/**
|
|
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.
|
|
469
|
-
*/
|
|
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;
|
|
494
|
-
/**
|
|
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.
|
|
499
|
-
*/
|
|
500
|
-
export const MAX_SURFACING_LINE_BYTES = 384;
|
|
501
|
-
|
|
502
|
-
/** How many automatic-action receipts stay in the window; the oldest ages out. */
|
|
503
262
|
export const MAX_CONTEXT_RECEIPTS = 3;
|
|
504
|
-
/** Hard byte cap on the rendered receipt block, so a receipt can never itself bloat. */
|
|
505
263
|
export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
|
|
506
|
-
|
|
507
|
-
export const MAX_CONTEXT_ATTEMPT_RECORDS = 128;
|
|
508
|
-
|
|
509
|
-
// ---------------------------------------------------------------------------
|
|
510
|
-
// The mark response.
|
|
511
|
-
//
|
|
512
|
-
// Measured 2026-08-07 (rep 17): every context action was followed by a projection
|
|
513
|
-
// REWRITE, never an append, because the surface answered a mark by refreshing blocks
|
|
514
|
-
// the agent had already paid to cache. A tail-appended tool result is the one place
|
|
515
|
-
// left that can inform the agent for free, so the awareness moved THERE and the
|
|
516
|
-
// projection went byte-frozen between fold events. The result is bounded on the same
|
|
517
|
-
// principle as the receipt block: a report about bloat that becomes bloat has argued
|
|
518
|
-
// against itself.
|
|
519
|
-
// ---------------------------------------------------------------------------
|
|
520
|
-
|
|
521
|
-
/** Hard byte cap on the rendered awareness block a mark call answers with. */
|
|
522
|
-
export const CONTEXT_MARK_RESPONSE_BYTES = 900;
|
|
264
|
+
|
|
523
265
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
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.
|
|
530
273
|
*/
|
|
274
|
+
export const FOLD_NOTICE_BYTES = 1_200;
|
|
531
275
|
export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
|
|
532
|
-
/** How many unmarked candidates the awareness block names; the remainder is an aggregate. */
|
|
533
276
|
export const MAX_UNMARKED_CANDIDATES = 3;
|
|
534
277
|
|
|
535
|
-
// ---------------------------------------------------------------------------
|
|
536
|
-
// Bite-sized folds and bounded peeks. These two constants are one decision read
|
|
537
|
-
// from two ends, so they are stated together and kept equal.
|
|
538
|
-
//
|
|
539
|
-
// Measured 2026-08-06 (rep 6): a single 60,432-byte chapter fold hid the needed
|
|
540
|
-
// fact in its tail, and every peek of it was either truncated short of the answer
|
|
541
|
-
// or too expensive to widen. A fold is only navigable if reading one back is cheap,
|
|
542
|
-
// so a fold's source is capped and an oversized span is SPLIT into sequential
|
|
543
|
-
// bounded folds, each with its own brief. The peek bound is the same number, so a
|
|
544
|
-
// default peek of one bite-sized fold returns it whole -- about 4,000 tokens, a
|
|
545
|
-
// read any session can afford -- and only a legacy oversized fold is ever truncated.
|
|
546
|
-
// ---------------------------------------------------------------------------
|
|
547
|
-
|
|
548
|
-
/** A fold whose exact source exceeds this is split into sequential bounded folds. */
|
|
549
278
|
export const MAX_FOLD_SPAN_CHARS = 16_000;
|
|
550
|
-
/**
|
|
551
|
-
* The other bound: a SLIVER of stale raw content hugging a fold boundary is absorbed
|
|
552
|
-
* into its later neighbour inside a commit the epoch already paid for.
|
|
553
|
-
*
|
|
554
|
-
* The threshold is deliberately tiny, measured in the session's own calibrated TOKENS,
|
|
555
|
-
* and it is not pressure-scaled. Non-sequential curation is a first-class state: an
|
|
556
|
-
* agent may hold folds at 10:20, 40:55 and 60:70 with raw spans between them ON
|
|
557
|
-
* PURPOSE, and a mechanism that swallowed those gaps would quietly turn pi-fold into
|
|
558
|
-
* system-controlled compaction with the curation element deleted. 256 tokens is about
|
|
559
|
-
* one short prompt or one one-line result -- nobody deliberately keeps a sliver that
|
|
560
|
-
* small as standing context -- and it sits an order of magnitude under the minimum
|
|
561
|
-
* chapter size the ladder itself will fold. Anything larger stays raw permanently,
|
|
562
|
-
* however ragged the projection looks; the ladder's ordinary fold eligibility is the
|
|
563
|
-
* only thing that ever touches it, and absorption never becomes a second folding path.
|
|
564
|
-
*
|
|
565
|
-
* Later, not earlier, because extending the LATER fold backward mutates at a shallower
|
|
566
|
-
* prefix position and preserves more of the cache. There is no content-affinity logic,
|
|
567
|
-
* every absorption is receipted, and a wrong grouping is one `reboundary` away.
|
|
568
|
-
*/
|
|
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;
|
|
608
|
-
/** What a peek returns without an explicit widening argument. */
|
|
609
279
|
export const PEEK_DEFAULT_MAX_BYTES = 16_000;
|
|
610
|
-
/** Head share of a truncated peek; the remainder is the tail, where conclusions live. */
|
|
611
280
|
export const PEEK_HEAD_SHARE = 0.6;
|
|
612
281
|
|
|
613
|
-
// ---------------------------------------------------------------------------
|
|
614
|
-
// Overflow rollback and recovery.
|
|
615
|
-
// ---------------------------------------------------------------------------
|
|
616
|
-
|
|
617
282
|
/**
|
|
618
|
-
*
|
|
283
|
+
* A PEEK IS EPHEMERAL UNLESS THE AGENT SAYS OTHERWISE (Shane, 2026-08-22).
|
|
619
284
|
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
* folding is a genuine impossibility rather than a recoverable state.
|
|
627
|
-
*/
|
|
628
|
-
export const OVERFLOW_RECOVERY_MAX_ATTEMPTS = 2;
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Active-context tool actions that read without mutating, so their results may fold.
|
|
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.
|
|
632
291
|
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
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.
|
|
637
296
|
*/
|
|
297
|
+
export function peekIsEphemeral(params: Record<string, unknown> | undefined | null): boolean {
|
|
298
|
+
return params?.ephemeral !== false;
|
|
299
|
+
}
|
|
300
|
+
|
|
638
301
|
export const PEEK_READ_ONLY_CONTEXT_ACTIONS: ReadonlySet<string> = new Set(["status", "peek"]);
|
|
639
302
|
|
|
640
|
-
|
|
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";
|
|
641
307
|
|
|
642
|
-
/** A fold decided but not yet applied: no projection byte has moved for it. */
|
|
643
308
|
export interface PendingFoldMark {
|
|
644
309
|
mark: "fold";
|
|
645
310
|
id: string;
|
|
@@ -648,11 +313,9 @@ export interface PendingFoldMark {
|
|
|
648
313
|
brief: string;
|
|
649
314
|
briefProvenance: BriefProvenance;
|
|
650
315
|
origin: MarkOrigin;
|
|
651
|
-
/** Transcript ordinal at mark time; orders the commit. Never a wall clock. */
|
|
652
316
|
ordinal: number;
|
|
653
317
|
}
|
|
654
318
|
|
|
655
|
-
/** An expanded fold decided to return to its placeholder at the next commit. */
|
|
656
319
|
export interface PendingRefoldMark {
|
|
657
320
|
mark: "refold";
|
|
658
321
|
id: string;
|
|
@@ -665,33 +328,13 @@ export type PendingMark = PendingFoldMark | PendingRefoldMark;
|
|
|
665
328
|
export const ACTIVE_CONTEXT_POLICY = Object.freeze({
|
|
666
329
|
refoldRatio: 0.85,
|
|
667
330
|
prepareRatio: 0.90,
|
|
668
|
-
warmRatio: 0.55,
|
|
669
331
|
responseReserve: 16_384,
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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.
|
|
674
336
|
maxSourceChars: 200_000,
|
|
675
|
-
maxFoldSourceRefs: 256,
|
|
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
337
|
maxBriefChars: 2_000,
|
|
694
|
-
briefTimeoutMs: 120_000,
|
|
695
338
|
orientationMessages: 2,
|
|
696
339
|
maxOrientationChars: 12_000,
|
|
697
340
|
});
|
|
@@ -709,10 +352,6 @@ export type BriefProvenance =
|
|
|
709
352
|
launchContractDigest?: string;
|
|
710
353
|
};
|
|
711
354
|
|
|
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
355
|
export type BriefOverride = string | { brief: string; provenance: BriefProvenance };
|
|
717
356
|
|
|
718
357
|
export interface ActiveFold {
|
|
@@ -744,57 +383,6 @@ export interface PreparedFold {
|
|
|
744
383
|
fold: ActiveFold;
|
|
745
384
|
}
|
|
746
385
|
|
|
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";
|
|
754
|
-
|
|
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
|
-
*/
|
|
760
|
-
export interface SurfacingRecord {
|
|
761
|
-
id: string;
|
|
762
|
-
surfaced: number;
|
|
763
|
-
taken: number;
|
|
764
|
-
/** Transcript ordinal of the last state change; the cooldown and the window read it. */
|
|
765
|
-
ordinal: number;
|
|
766
|
-
outcome: SurfacingOutcome;
|
|
767
|
-
}
|
|
768
|
-
|
|
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. */
|
|
778
|
-
export interface SurfacingCandidate {
|
|
779
|
-
id: 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;
|
|
784
|
-
route: string;
|
|
785
|
-
/** Transcript position of the fold's last entry; higher is more recent. Never a clock. */
|
|
786
|
-
position: number;
|
|
787
|
-
depth: number;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
export interface SurfacingSuggestion extends 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;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
386
|
export interface ActiveContextState {
|
|
799
387
|
version: 1;
|
|
800
388
|
sessionId: string;
|
|
@@ -804,17 +392,8 @@ export interface ActiveContextState {
|
|
|
804
392
|
protected: EvidenceRef[];
|
|
805
393
|
tokensSinceToolFold: number;
|
|
806
394
|
leases: Record<string, number>;
|
|
807
|
-
surfacing?: SurfacingRecord[];
|
|
808
|
-
/** Epoch scheduling only; omitted when empty so immediate-mode state digests never move. */
|
|
809
395
|
pendingMarks?: PendingMark[];
|
|
810
|
-
/** Retired with peek reclamation; carried only so pre-cut state still parses. */
|
|
811
396
|
pinnedPeeks?: never;
|
|
812
|
-
/**
|
|
813
|
-
* Corrected and upgraded fold briefs, by fold id. A fold RECORD is content-addressed and
|
|
814
|
-
* immutable -- rewriting its brief in place would report a conflicting durable fold
|
|
815
|
-
* and suspend automatic management -- so a re-brief is durable state beside the
|
|
816
|
-
* record rather than a mutation of it. Omitted when empty.
|
|
817
|
-
*/
|
|
818
397
|
briefs?: Record<string, BriefOverride>;
|
|
819
398
|
prepared?: PreparedFold;
|
|
820
399
|
advisory?: {
|
|
@@ -822,26 +401,7 @@ export interface ActiveContextState {
|
|
|
822
401
|
delivered: Record<string, number>;
|
|
823
402
|
armed?: { milestone: AdvisoryMilestone; threshold: number; scheduleKey: string };
|
|
824
403
|
};
|
|
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
404
|
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 }> };
|
|
845
405
|
}
|
|
846
406
|
|
|
847
407
|
export interface FoldRecordRef {
|
|
@@ -868,7 +428,6 @@ export interface ActiveContextCheckpointV2 {
|
|
|
868
428
|
prepared: PreparedFold | null;
|
|
869
429
|
tokensSinceToolFold?: number;
|
|
870
430
|
leases?: Record<string, number>;
|
|
871
|
-
surfacing?: SurfacingRecord[];
|
|
872
431
|
pendingMarks?: PendingMark[];
|
|
873
432
|
briefs?: Record<string, BriefOverride>;
|
|
874
433
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
@@ -892,9 +451,16 @@ export interface ActiveContextDeltaV2 {
|
|
|
892
451
|
prepared: PreparedFold | null;
|
|
893
452
|
tokensSinceToolFold?: number;
|
|
894
453
|
leases?: Record<string, number>;
|
|
895
|
-
|
|
454
|
+
// `pendingMarks` and `briefs` on a delta are the WHOLE value: what every delta written
|
|
455
|
+
// before 2026-08-13 carries, and what sealed sessions replay. New deltas carry the change
|
|
456
|
+
// instead. The marks carry an ORDER as well as a membership, so the change is the marks
|
|
457
|
+
// that are new or rewritten plus the whole key order, which states the removals too.
|
|
896
458
|
pendingMarks?: PendingMark[];
|
|
459
|
+
addPendingMarks?: PendingMark[];
|
|
460
|
+
pendingMarkOrder?: string[];
|
|
897
461
|
briefs?: Record<string, BriefOverride>;
|
|
462
|
+
addBriefs?: Record<string, BriefOverride>;
|
|
463
|
+
removeBriefIds?: string[];
|
|
898
464
|
advisory?: NonNullable<ActiveContextState["advisory"]>;
|
|
899
465
|
rider?: NonNullable<ActiveContextState["rider"]>;
|
|
900
466
|
lastCall?: NonNullable<ActiveContextState["lastCall"]>;
|
|
@@ -928,18 +494,14 @@ export interface ActiveContextSnapshot {
|
|
|
928
494
|
branchObjects: BranchObject[];
|
|
929
495
|
completeTurns: CompleteTurn[];
|
|
930
496
|
freshBoundary: number;
|
|
931
|
-
/** The four thermostat numbers in force for this session. */
|
|
932
497
|
thresholds: ActiveContextThresholds;
|
|
933
|
-
/** The serving budget the thresholds are proportions of. One denominator. */
|
|
934
498
|
budgetTokens: number;
|
|
935
499
|
protectedIndices: Set<number>;
|
|
936
|
-
toolProtectedIndices: Set<number>;
|
|
937
500
|
policy: typeof ACTIVE_CONTEXT_POLICY;
|
|
938
501
|
toolName: string;
|
|
939
502
|
brandNoun: string;
|
|
940
503
|
entryTypePrefix: string;
|
|
941
504
|
blacklistAutoFoldTools: ReadonlySet<string>;
|
|
942
|
-
/** Active-context tool actions treated as read-only when classifying tool batches. */
|
|
943
505
|
readOnlyContextActions: ReadonlySet<string>;
|
|
944
506
|
contextWindow: number;
|
|
945
507
|
windowSource: "reported" | "fallback";
|
|
@@ -951,15 +513,6 @@ export interface FoldCandidate {
|
|
|
951
513
|
sourceRefs: EvidenceRef[];
|
|
952
514
|
}
|
|
953
515
|
|
|
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
516
|
export const AUTO_FOLD_BLACKLIST_DEFAULT: ReadonlySet<string> = new Set();
|
|
964
517
|
|
|
965
518
|
export type AdvisoryMilestone = "orientation" | "notice" | "tools" | "chapters" | "urgent";
|