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