pi-fold 1.0.2 → 1.0.4
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 +2408 -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
|
@@ -2,25 +2,29 @@ import {
|
|
|
2
2
|
denseOwnArrayValues,
|
|
3
3
|
evidenceSha256,
|
|
4
4
|
objectRefKey,
|
|
5
|
+
sha256Text,
|
|
5
6
|
sha256Value,
|
|
6
7
|
stableStringify,
|
|
7
8
|
} from "./json.ts";
|
|
8
|
-
import {
|
|
9
|
-
} from "./lib/advisory.ts";
|
|
9
|
+
import type { EvidenceRef } from "./json.ts";
|
|
10
10
|
import {
|
|
11
11
|
bytes,
|
|
12
12
|
clone,
|
|
13
13
|
emptyActiveContextState,
|
|
14
14
|
ownValue,
|
|
15
|
+
sessionEntryMessages,
|
|
15
16
|
uniqueMessageDigestAnchor,
|
|
16
17
|
} from "./lib/canonical.ts";
|
|
17
18
|
import {
|
|
18
19
|
activeContextStatus,
|
|
19
20
|
automaticPreparationId,
|
|
21
|
+
boundedOrientation,
|
|
20
22
|
boundStatusPayload,
|
|
21
23
|
commitPreparedFold,
|
|
24
|
+
consolidationSourceText,
|
|
22
25
|
descendantIds,
|
|
23
26
|
encodedFoldSource,
|
|
27
|
+
generatedBriefs,
|
|
24
28
|
foldCandidatesDetail,
|
|
25
29
|
foldTreeDetail,
|
|
26
30
|
peekFoldSource,
|
|
@@ -42,6 +46,8 @@ import {
|
|
|
42
46
|
capacityAccounting,
|
|
43
47
|
contextUsageRatio,
|
|
44
48
|
contextWindowFor,
|
|
49
|
+
explicitProtectedMass,
|
|
50
|
+
budgetOccupancy,
|
|
45
51
|
hardFenceRatio,
|
|
46
52
|
latestProviderContextMeasurement,
|
|
47
53
|
orderedRoots,
|
|
@@ -49,6 +55,7 @@ import {
|
|
|
49
55
|
parseNativeCompactionDecision,
|
|
50
56
|
parseProviderContextMeasurementReceipt,
|
|
51
57
|
persistenceProjection,
|
|
58
|
+
protectedStaleMass,
|
|
52
59
|
providerContextMeasurement,
|
|
53
60
|
providerTokens,
|
|
54
61
|
refsProtected,
|
|
@@ -74,6 +81,7 @@ import {
|
|
|
74
81
|
makeStateDelta,
|
|
75
82
|
MAX_ACTIVE_FOLD_RECORDS,
|
|
76
83
|
materializeStatePersistence,
|
|
84
|
+
foldProvenance,
|
|
77
85
|
normalizeFoldsForPersistedRecords,
|
|
78
86
|
normalizeLegacyProvenance,
|
|
79
87
|
protectionSha256,
|
|
@@ -93,47 +101,58 @@ import {
|
|
|
93
101
|
DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL,
|
|
94
102
|
DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
95
103
|
COMMIT_RECLAIM_FLOOR_SHARE,
|
|
96
|
-
CURATION_OCCUPANCY_SHARE,
|
|
97
|
-
CURATION_STALE_TOOL_SHARE,
|
|
98
104
|
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
99
|
-
CURATION_TARGET_OCCUPANCY_SHARE,
|
|
100
105
|
DEFAULT_CONTEXT_WINDOW,
|
|
101
|
-
|
|
106
|
+
EPOCH_CONSOLIDATION_ROUNDS,
|
|
107
|
+
MAX_THRESHOLD_NOTICES,
|
|
108
|
+
THRESHOLD_NOTICE_SHARES,
|
|
109
|
+
resolveGuidance,
|
|
110
|
+
assertThresholdsServable,
|
|
111
|
+
resolveThresholds,
|
|
112
|
+
servingBudgetTokens,
|
|
102
113
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
103
114
|
entryTypeNamespace,
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
MAX_BRIEF_BATCH_SPANS,
|
|
116
|
+
MAX_BRIEF_UPGRADE_QUEUE,
|
|
117
|
+
MAX_BRIEF_UPGRADES_IN_FLIGHT,
|
|
106
118
|
MAX_FOLD_SPAN_CHARS,
|
|
119
|
+
MAX_PINNED_SHARE,
|
|
107
120
|
MAX_WEDGE_ABSORB_TOKENS,
|
|
108
121
|
OVERFLOW_RECOVERY_MAX_ATTEMPTS,
|
|
109
122
|
PEEK_DEFAULT_MAX_BYTES,
|
|
110
123
|
PEEK_MIN_SLICE_BYTES,
|
|
111
124
|
PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
126
|
+
SURFACING_BRIEF_HIT,
|
|
127
|
+
SURFACING_CONTENT_HIT,
|
|
128
|
+
SURFACING_DIVERGENCE_MARGIN,
|
|
129
|
+
SURFACING_OUTCOME_WINDOW_ORDINALS,
|
|
130
|
+
SURFACING_SLATE_SIZE,
|
|
115
131
|
USER_RESCUE_MAX_SOURCE_CHARS,
|
|
116
132
|
} from "./lib/policy.ts";
|
|
117
133
|
import type {
|
|
134
|
+
ActiveContextGuidance,
|
|
118
135
|
ActiveContextSnapshot,
|
|
119
136
|
ActiveContextState,
|
|
137
|
+
ActiveContextThresholds,
|
|
120
138
|
ActiveContextToolAction,
|
|
139
|
+
BriefProvenance,
|
|
121
140
|
FoldCandidate,
|
|
122
141
|
FoldKind,
|
|
123
142
|
FoldRecordEntry,
|
|
124
|
-
FoldSchedulingMode,
|
|
125
143
|
PreparedFold,
|
|
126
|
-
SuggestionSource,
|
|
127
144
|
} from "./lib/policy.ts";
|
|
128
145
|
import {
|
|
129
146
|
absorbWedgeMarks,
|
|
130
147
|
} from "./lib/scheduling.ts";
|
|
131
148
|
import {
|
|
132
149
|
contextReceipt,
|
|
150
|
+
contextRiderText,
|
|
133
151
|
curationSignals,
|
|
134
|
-
|
|
152
|
+
lastCallText,
|
|
135
153
|
markAwarenessText,
|
|
136
154
|
receiptBlockText,
|
|
155
|
+
thresholdNoticeText,
|
|
137
156
|
withReceipt,
|
|
138
157
|
} from "./lib/curation.ts";
|
|
139
158
|
import type {
|
|
@@ -144,6 +163,7 @@ import {
|
|
|
144
163
|
addPendingMark,
|
|
145
164
|
claimedRefKeys,
|
|
146
165
|
commitPendingMarks,
|
|
166
|
+
consolidationMarks,
|
|
147
167
|
ephemeralPeekMarks,
|
|
148
168
|
epochCommitDue,
|
|
149
169
|
estimatedTokens,
|
|
@@ -169,16 +189,21 @@ import {
|
|
|
169
189
|
deterministicChapterCandidateBrief,
|
|
170
190
|
deterministicConsolidationBrief,
|
|
171
191
|
manualFoldCandidate,
|
|
192
|
+
peekedSourceFoldIds,
|
|
172
193
|
selectAutomaticToolBatch,
|
|
173
|
-
selectAutomaticToolForRung,
|
|
174
194
|
snapFoldCandidate,
|
|
175
195
|
snapToFoldBoundaries,
|
|
176
196
|
splitCandidateBySize,
|
|
177
197
|
} from "./lib/selection.ts";
|
|
178
198
|
import type { SpanCorrection } from "./lib/selection.ts";
|
|
179
199
|
import {
|
|
180
|
-
|
|
181
|
-
|
|
200
|
+
issueSurfacing,
|
|
201
|
+
noteSurfacingAction,
|
|
202
|
+
resolveSurfacing,
|
|
203
|
+
selectSurfacingSlate,
|
|
204
|
+
surfacingLedger,
|
|
205
|
+
surfacingSilenced,
|
|
206
|
+
surfacingSlateText,
|
|
182
207
|
} from "./lib/surfacing.ts";
|
|
183
208
|
import {
|
|
184
209
|
compareProjections,
|
|
@@ -199,11 +224,27 @@ import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-s
|
|
|
199
224
|
import {
|
|
200
225
|
currentTurnRefKeys,
|
|
201
226
|
mapActiveContext,
|
|
202
|
-
toolFreshIndices,
|
|
203
227
|
} from "./lib/transcript.ts";
|
|
228
|
+
import {
|
|
229
|
+
findOverflowErrorEntry,
|
|
230
|
+
overflowEventShape,
|
|
231
|
+
preStripHolds,
|
|
232
|
+
probeRollbackSurfaces,
|
|
233
|
+
rollbackNoticeText,
|
|
234
|
+
unansweredToolCalls,
|
|
235
|
+
} from "./lib/rollback.ts";
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* How a projection's token reading was reached.
|
|
239
|
+
*
|
|
240
|
+
* "anchored": the provider's own count for the projection this one extends, plus a rate
|
|
241
|
+
* estimate of the bytes appended after it. "rewritten": the current projection does not
|
|
242
|
+
* begin with the measured one, so there is nothing to add a delta to and the whole
|
|
243
|
+
* projection is estimated. "unmeasured": no accepted provider count yet.
|
|
244
|
+
*/
|
|
245
|
+
export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured";
|
|
204
246
|
|
|
205
247
|
// Test seam only; the package API is registerPiFold because package.json exports block deep imports.
|
|
206
|
-
export * from "./lib/advisory.ts";
|
|
207
248
|
export * from "./lib/canonical.ts";
|
|
208
249
|
export * from "./lib/curation.ts";
|
|
209
250
|
export * from "./lib/folding.ts";
|
|
@@ -211,19 +252,12 @@ export * from "./lib/instrumentation.ts";
|
|
|
211
252
|
export * from "./lib/measurement.ts";
|
|
212
253
|
export * from "./lib/persistence.ts";
|
|
213
254
|
export * from "./lib/policy.ts";
|
|
255
|
+
export * from "./lib/rollback.ts";
|
|
214
256
|
export * from "./lib/scheduling.ts";
|
|
215
257
|
export * from "./lib/selection.ts";
|
|
216
258
|
export * from "./lib/surfacing.ts";
|
|
217
259
|
export * from "./lib/transcript.ts";
|
|
218
260
|
|
|
219
|
-
/** Handle a suggestion source keeps: withdraw itself, and report that its item was acted on. */
|
|
220
|
-
export interface SuggestionSourceHandle {
|
|
221
|
-
unregister: () => void;
|
|
222
|
-
accepted: (id: string) => void;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export type SuggestionSourceRegistrar = (source: SuggestionSource) => SuggestionSourceHandle;
|
|
226
|
-
|
|
227
261
|
/**
|
|
228
262
|
* The batched fold request: several {span, brief} pairs in one call, with the single
|
|
229
263
|
* `ids` form still accepted because it is the shape a status action hands back.
|
|
@@ -261,113 +295,139 @@ export function batchedMarkRequests(params: Record<string, unknown>): BatchedMar
|
|
|
261
295
|
}
|
|
262
296
|
|
|
263
297
|
export function registerActiveContext(pi: any, options: {
|
|
298
|
+
/**
|
|
299
|
+
* INTERNAL brief-generator seam, not a public option: `registerPiFold` refuses the
|
|
300
|
+
* name and builds the generator from `summarizer`. It stays a parameter because it
|
|
301
|
+
* is the runtime's only brief-generator interface.
|
|
302
|
+
*/
|
|
264
303
|
summarizeContextSpan?: (request: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
304
|
+
/**
|
|
305
|
+
* INTERNAL deployment-identity seam, not public options: the package entry refuses all
|
|
306
|
+
* five by name and the shipped identity is the one in `policy.ts`. They survive here
|
|
307
|
+
* because the neutrality gate has to register a synthetic brand to prove none of it
|
|
308
|
+
* reaches the defaults, and the experiment harness registers the pi-fold identity
|
|
309
|
+
* explicitly so its sealed runs keep their entry types.
|
|
310
|
+
*/
|
|
268
311
|
toolName?: string;
|
|
269
312
|
toolLabel?: string;
|
|
270
313
|
brandNoun?: string;
|
|
271
314
|
entryTypePrefix?: string;
|
|
272
|
-
commandPrefix?: string;
|
|
273
315
|
commandNames?: { status?: string; fold?: string };
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
316
|
+
/**
|
|
317
|
+
* The auto-fold EXCEPTION list, empty by default: every completed tool batch folds
|
|
318
|
+
* unmarked, and this names the tools whose results must stay raw.
|
|
319
|
+
*/
|
|
320
|
+
blacklistAutoFoldTools?: ReadonlySet<string>;
|
|
321
|
+
/** The serving budget itself, ALREADY NET of the deployment's output reservation. */
|
|
322
|
+
providerInputBudget?: number;
|
|
323
|
+
/**
|
|
324
|
+
* The thermostat, set whole or not at all. USER policy: no agent action reads it back
|
|
325
|
+
* as a mutable surface, and status reports the values without offering to change them.
|
|
326
|
+
*/
|
|
327
|
+
thresholds?: ActiveContextThresholds;
|
|
328
|
+
/** Both guidance surfaces, set together, defaulting on. */
|
|
329
|
+
guidance?: Partial<ActiveContextGuidance>;
|
|
279
330
|
}): {
|
|
280
331
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
281
|
-
registerSuggestionSource: SuggestionSourceRegistrar;
|
|
282
332
|
} {
|
|
283
333
|
const toolName = options.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME;
|
|
284
334
|
const toolLabel = options.toolLabel ?? DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL;
|
|
285
335
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
286
336
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
337
|
+
const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
|
|
338
|
+
// Deleted options are REFUSED by name, never ignored. A deployment still passing one
|
|
339
|
+
// believes it asked for something, and silence would hand it the opposite behavior:
|
|
340
|
+
// `foldScheduling` chose between epoch and the deleted immediate scheduler,
|
|
341
|
+
// `foldPeekResults` opted peek results out of the foldable classification, and
|
|
342
|
+
// `toolActions` narrowed the action surface. All three are unconditional now.
|
|
343
|
+
for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
|
|
344
|
+
if (Object.hasOwn(options, removed)) {
|
|
345
|
+
throw new Error(`${removed} is no longer an option: epoch scheduling, peek foldability ` +
|
|
346
|
+
"and the whole action surface are unconditional");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (Object.hasOwn(options, "blockingTools")) {
|
|
350
|
+
throw new Error("blockingTools is no longer an option: a tool call never causes a rewrite of " +
|
|
351
|
+
"its own, and the commit epoch is the only path that mutates the projection");
|
|
352
|
+
}
|
|
353
|
+
// The per-request surfacing channel. `setProjectionProvider` was a second delivery
|
|
354
|
+
// path for the value this call already returns, and `setSuggestionSourceRegistrar`
|
|
355
|
+
// handed out registration into a carrier that no longer exists: the slate rode a
|
|
356
|
+
// per-request ephemeral tail, which moved the prefix on every single pass. The
|
|
357
|
+
// selector survives and returns at the commit boundary; the channel does not.
|
|
358
|
+
for (const removed of ["setProjectionProvider", "setSuggestionSourceRegistrar"]) {
|
|
359
|
+
if (Object.hasOwn(options, removed)) {
|
|
360
|
+
throw new Error(`${removed} is no longer an option: projection candidates are returned by ` +
|
|
361
|
+
"registration, and external suggestion sources have no carrier to render into");
|
|
362
|
+
}
|
|
292
363
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
364
|
+
// The renamed options are refused by their OLD names, each pointing at the new one.
|
|
365
|
+
// `readOnlyTools` and then `autoFoldableTools` were both ALLOW-lists, and the list runs
|
|
366
|
+
// the other way now, so neither may be forwarded silently: an allow-list read as a
|
|
367
|
+
// blacklist bars exactly the tools it meant to permit. `commandPrefix` derived two
|
|
368
|
+
// command names from a stem the full-name override then overrode anyway;
|
|
369
|
+
// `providerTotalWindow` was a gross window the runtime netted down with a GUESSED
|
|
370
|
+
// reservation, and the guess is what the reshape deleted.
|
|
371
|
+
for (const inverted of ["readOnlyTools", "autoFoldableTools"]) {
|
|
372
|
+
if (Object.hasOwn(options, inverted)) {
|
|
373
|
+
throw new Error(`${inverted} is now blacklistAutoFoldTools, and the sense is INVERTED: ` +
|
|
374
|
+
"every completed tool batch folds unmarked, and the list names the exceptions whose " +
|
|
375
|
+
"results must stay raw. An allow-list moved across verbatim would bar exactly the " +
|
|
376
|
+
"tools it meant to permit");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (Object.hasOwn(options, "commandPrefix")) {
|
|
380
|
+
throw new Error("commandPrefix is no longer an option: give whole names with commandNames");
|
|
296
381
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (options.
|
|
303
|
-
(!Number.isSafeInteger(options.
|
|
304
|
-
throw new Error("
|
|
382
|
+
if (Object.hasOwn(options, "providerTotalWindow")) {
|
|
383
|
+
throw new Error("providerTotalWindow is now providerInputBudget, and it is ALREADY NET: " +
|
|
384
|
+
"pass the tokens the deployment may actually fill, not the total window the runtime " +
|
|
385
|
+
"then subtracts a guessed output reservation from");
|
|
386
|
+
}
|
|
387
|
+
if (options.providerInputBudget !== undefined &&
|
|
388
|
+
(!Number.isSafeInteger(options.providerInputBudget) || options.providerInputBudget <= 0)) {
|
|
389
|
+
throw new Error("providerInputBudget must be a positive integer");
|
|
305
390
|
}
|
|
306
391
|
// The deployment's own fact, and the only capacity knob: declaring it makes every
|
|
307
392
|
// ratio, fence and budget truthful, and leaving it out falls back to the provider
|
|
308
393
|
// descriptor and SAYS "descriptor" in the capacity accounting.
|
|
309
|
-
const
|
|
310
|
-
const readOnlyContextActions =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
(commandPrefix && !/^[a-z0-9-]+$/.test(commandPrefix)) ||
|
|
315
|
-
[...readOnlyTools].some((name) => typeof name !== "string" || !name)) {
|
|
316
|
-
throw new Error("Active-context names and read-only tools must be nonempty strings");
|
|
394
|
+
const providerInputBudget = options.providerInputBudget ?? null;
|
|
395
|
+
const readOnlyContextActions = PEEK_READ_ONLY_CONTEXT_ACTIONS;
|
|
396
|
+
if (!toolName || !toolLabel || !brandNoun || !entryTypePrefix ||
|
|
397
|
+
[...blacklistAutoFoldTools].some((name) => typeof name !== "string" || !name)) {
|
|
398
|
+
throw new Error("Active-context names and blacklisted auto-fold tools must be nonempty strings");
|
|
317
399
|
}
|
|
318
|
-
const commandStem = commandPrefix ? `${commandPrefix.replace(/-+$/, "")}-` : "";
|
|
319
|
-
// Full-name override for hosts that need non-default command names (e.g. the
|
|
320
|
-
// pi-fold package's neutral "context"); commandPrefix remains the derived form.
|
|
321
400
|
const commandNames = {
|
|
322
|
-
status: options.commandNames?.status ??
|
|
323
|
-
fold: options.commandNames?.fold ??
|
|
401
|
+
status: options.commandNames?.status ?? DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.status,
|
|
402
|
+
fold: options.commandNames?.fold ?? DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES.fold,
|
|
324
403
|
};
|
|
325
404
|
if (![commandNames.status, commandNames.fold].every((name) =>
|
|
326
405
|
typeof name === "string" && /^[a-z0-9][a-z0-9-]*$/.test(name)) ||
|
|
327
406
|
commandNames.status === commandNames.fold) {
|
|
328
407
|
throw new Error("Active-context command names must be distinct kebab-case strings");
|
|
329
408
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const blockingTools = new Set(configuredBlockingTools as string[]);
|
|
409
|
+
// The thermostat, validated atomically before anything else reads a threshold, and
|
|
410
|
+
// then checked against the budget this deployment can actually serve.
|
|
411
|
+
const thresholds = resolveThresholds(options.thresholds);
|
|
412
|
+
assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
413
|
+
const guidance = resolveGuidance(options.guidance);
|
|
336
414
|
const stateEntryType = `${entryTypePrefix}-state`;
|
|
337
415
|
const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
|
|
338
416
|
const milestoneProjectionType = `${entryTypePrefix}-milestone`;
|
|
339
417
|
const advisoryProjectionType = `${entryTypePrefix}-advisory`;
|
|
340
418
|
const surfacingProjectionType = `${entryTypePrefix}-surfacing`;
|
|
341
419
|
const receiptProjectionType = `${entryTypePrefix}-receipts`;
|
|
420
|
+
const riderProjectionType = `${entryTypePrefix}-rider`;
|
|
421
|
+
const lastCallProjectionType = `${entryTypePrefix}-lastcall`;
|
|
422
|
+
const noticeProjectionType = `${entryTypePrefix}-notice`;
|
|
342
423
|
const curationProjectionType = `${entryTypePrefix}-curation`;
|
|
343
424
|
const entryNamespace = entryTypeNamespace(entryTypePrefix);
|
|
344
425
|
const providerMeasurementEntryType = `${entryNamespace}-provider-context-measurement`;
|
|
345
426
|
const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
|
|
346
427
|
const contextEventEntryType = `${entryNamespace}-context-event`;
|
|
347
428
|
const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const defaultToolActions = epochScheduling
|
|
351
|
-
? EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS
|
|
352
|
-
: ACTIVE_CONTEXT_TOOL_ACTIONS;
|
|
353
|
-
const configuredToolActions = denseOwnArrayValues(
|
|
354
|
-
options.toolActions ?? defaultToolActions,
|
|
355
|
-
);
|
|
356
|
-
if (!configuredToolActions || configuredToolActions.length < 1) {
|
|
357
|
-
throw new Error("Active-context tool actions must be one non-empty dense array");
|
|
358
|
-
}
|
|
359
|
-
const allowedToolActions: ActiveContextToolAction[] = [];
|
|
360
|
-
const allowedToolActionSet = new Set<string>();
|
|
361
|
-
for (const value of configuredToolActions) {
|
|
362
|
-
if (typeof value !== "string" ||
|
|
363
|
-
!EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS.includes(value as ActiveContextToolAction) ||
|
|
364
|
-
allowedToolActionSet.has(value)) {
|
|
365
|
-
throw new Error(`Invalid or duplicate active-context tool action '${String(value)}'`);
|
|
366
|
-
}
|
|
367
|
-
allowedToolActions.push(value as ActiveContextToolAction);
|
|
368
|
-
allowedToolActionSet.add(value);
|
|
369
|
-
}
|
|
370
|
-
Object.freeze(allowedToolActions);
|
|
429
|
+
const allowedToolActions: readonly ActiveContextToolAction[] = ACTIVE_CONTEXT_TOOL_ACTIONS;
|
|
430
|
+
const allowedToolActionSet = new Set<string>(allowedToolActions);
|
|
371
431
|
|
|
372
432
|
type AutomaticFailureState = {
|
|
373
433
|
key: string;
|
|
@@ -408,6 +468,62 @@ export function registerActiveContext(pi: any, options: {
|
|
|
408
468
|
actionQueue: Promise.resolve<unknown>(undefined),
|
|
409
469
|
};
|
|
410
470
|
|
|
471
|
+
/**
|
|
472
|
+
* THE BRIEF UPGRADE LANE.
|
|
473
|
+
*
|
|
474
|
+
* A fold brief is model-written from this campaign forward; the deterministic brief is
|
|
475
|
+
* what a failure leaves behind. The automatic ladder cannot wait for a generator: the
|
|
476
|
+
* commit path is synchronous with a measured projection and a fold deferred until a
|
|
477
|
+
* provider answers is a fold that does not happen. So the ladder commits deterministic,
|
|
478
|
+
* a generator runs between boundaries, and the brief it writes lands as an UPGRADE
|
|
479
|
+
* riding the next commit: the same law wedge absorption and the surfacing slate follow,
|
|
480
|
+
* mutations only inside a rewrite the session already paid for.
|
|
481
|
+
*
|
|
482
|
+
* Everything here is process-local. An upgrade lost to a reload is a brief that stays
|
|
483
|
+
* deterministic, which is exactly the failure the fallback exists for, and the state
|
|
484
|
+
* wire keeps one shape instead of carrying work in progress.
|
|
485
|
+
*/
|
|
486
|
+
const upgrades = {
|
|
487
|
+
/**
|
|
488
|
+
* BATCHES of folds committed deterministic, each holding the exact source its briefs
|
|
489
|
+
* will cover. One batch is one generator call: a commit folds its spans together, so
|
|
490
|
+
* they are briefed together, and the spans of one batch orient each other. Sending them
|
|
491
|
+
* one at a time is what left the 2026-08-11 rep with 14 model briefs across 87 folds.
|
|
492
|
+
*/
|
|
493
|
+
queue: [] as Array<{
|
|
494
|
+
members: Array<{ foldId: string; sourceSha256: string }>;
|
|
495
|
+
request: Record<string, unknown>;
|
|
496
|
+
}>,
|
|
497
|
+
/**
|
|
498
|
+
* Generator calls in flight, keyed by every fold each one briefs, bounded by a constant
|
|
499
|
+
* and cancel-safe on session change or shutdown. Keyed rather than counted because
|
|
500
|
+
* the queue has to know a fold is already being briefed.
|
|
501
|
+
*/
|
|
502
|
+
running: new Map<string, { controller: AbortController; promise: Promise<void> }>(),
|
|
503
|
+
/** Written briefs waiting for a boundary. They never apply anywhere else. */
|
|
504
|
+
ready: [] as Array<{ foldId: string; sourceSha256: string; brief: string; provenance: BriefProvenance }>,
|
|
505
|
+
/** A generator that failed a fold does not get it back: one attempt, then deterministic stands. */
|
|
506
|
+
failed: new Set<string>(),
|
|
507
|
+
/**
|
|
508
|
+
* Parents whose turn has not come: a group is read at depth one, so a child still
|
|
509
|
+
* waiting on its own brief would be read through the sentence that brief is about to
|
|
510
|
+
* replace. They are retried at every boundary and never dropped, because the wait ends
|
|
511
|
+
* the moment the lane drains and a parent that was skipped once is a parent no
|
|
512
|
+
* generator ever writes.
|
|
513
|
+
*/
|
|
514
|
+
deferred: new Set<string>(),
|
|
515
|
+
/** Failures since the last commit record carried them, so none of this is silent. */
|
|
516
|
+
failures: 0,
|
|
517
|
+
/**
|
|
518
|
+
* Briefs that took a second ask before they met the contract. Counted because the cure
|
|
519
|
+
* is the mechanism that lets the brief cap be a real limit rather than a truncation
|
|
520
|
+
* point, so how often it fires is how we learn whether the cap is set anywhere near
|
|
521
|
+
* right (Shane 2026-08-11).
|
|
522
|
+
*/
|
|
523
|
+
cures: 0,
|
|
524
|
+
lastError: null as string | null,
|
|
525
|
+
};
|
|
526
|
+
|
|
411
527
|
// Owns provider receipts, anchors, and usage; providerMeasurementQueue serializes receipt writes.
|
|
412
528
|
const measurements = {
|
|
413
529
|
latestRatio: null as number | null,
|
|
@@ -430,10 +546,31 @@ export function registerActiveContext(pi: any, options: {
|
|
|
430
546
|
lastProjectedEstimate: null as number | null,
|
|
431
547
|
/** Whether that estimate used a measured ratio rather than the bootstrap constant. */
|
|
432
548
|
lastProjectedEstimateCalibrated: false,
|
|
549
|
+
/** How that reading was reached, so an audit can tell an anchor from a fallback. */
|
|
550
|
+
lastProjectedEstimateBasis: "unmeasured" as ProjectionReadingBasis,
|
|
551
|
+
/** That projection's own size at one rate, the baseline a reduction is measured from. */
|
|
552
|
+
lastProjectedSizeTokens: null as number | null,
|
|
553
|
+
/**
|
|
554
|
+
* The provider's own count for the last projection this process TRANSMITTED, held
|
|
555
|
+
* with that projection's serialized text so a later projection can be tested against
|
|
556
|
+
* it by construction. `head` is the serialization minus its closing bracket, which is
|
|
557
|
+
* what makes "the same projection with more appended after it" a byte-exact prefix
|
|
558
|
+
* test rather than a size comparison.
|
|
559
|
+
*/
|
|
560
|
+
projectionAnchor: null as {
|
|
561
|
+
tokens: number;
|
|
562
|
+
chars: number;
|
|
563
|
+
head: string;
|
|
564
|
+
messageSha256: string;
|
|
565
|
+
sessionId: string;
|
|
566
|
+
generation: number;
|
|
567
|
+
} | null,
|
|
433
568
|
/** Signed estimator error against recent measurements, as a share of measured tokens. */
|
|
434
569
|
estimatorErrors: [] as number[],
|
|
435
570
|
/** Growth in measured tokens between consecutive requests. */
|
|
436
571
|
inflowSteps: [] as number[],
|
|
572
|
+
/** Growth steps the OUTPUT WALL reads. Separate from `inflowSteps` on purpose: see noteWallInflow. */
|
|
573
|
+
wallInflowSteps: [] as number[],
|
|
437
574
|
providerMeasurementQueue: Promise.resolve<void>(undefined),
|
|
438
575
|
providerMeasurementReceipts: new Set<string>(),
|
|
439
576
|
providerMeasurementRevisionByMessageSha: new Map<string, number>(),
|
|
@@ -517,6 +654,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
517
654
|
* runtime rewrites the projection, which is the whole cost model.
|
|
518
655
|
*/
|
|
519
656
|
reopenBaselineShare: null as number | null,
|
|
657
|
+
/** Whether the output wall's crossing has already spent its one forced commit. */
|
|
658
|
+
wallEpisodeOpen: false,
|
|
659
|
+
/**
|
|
660
|
+
* Where the armed last-call actually LANDED: the exposure it rendered and the
|
|
661
|
+
* ordinal of the pass that rendered it. In-memory on purpose: after a reload the
|
|
662
|
+
* carrier re-renders on the first projection, this re-arms, and the commit waits
|
|
663
|
+
* one more round rather than firing on a prompt nobody saw.
|
|
664
|
+
*/
|
|
665
|
+
lastCallDelivery: null as { exposure: number; ordinal: number } | null,
|
|
520
666
|
/** Recovery attempts spent on the CURRENT inflow; reset by any accepted request. */
|
|
521
667
|
recoveryAttempts: 0,
|
|
522
668
|
/** A provider rejection observed but not yet recovered from. */
|
|
@@ -525,6 +671,29 @@ export function registerActiveContext(pi: any, options: {
|
|
|
525
671
|
instrumentationQueue: Promise.resolve<void>(undefined),
|
|
526
672
|
};
|
|
527
673
|
|
|
674
|
+
/**
|
|
675
|
+
* The tree-rollback lane.
|
|
676
|
+
*
|
|
677
|
+
* Armed once per session against the pi surfaces it calls, because every one of them
|
|
678
|
+
* is an internal the extension contract does not promise. Disarmed, the lane is OFF
|
|
679
|
+
* and says so on the record: an overflow still emits `context.rollback` with
|
|
680
|
+
* `armed: false` and the reason, the user is notified, and the session behaves
|
|
681
|
+
* exactly as it did before this build -- the projection-budget fence aborts an
|
|
682
|
+
* over-budget request rather than half-performing a recovery.
|
|
683
|
+
*/
|
|
684
|
+
const rollback = {
|
|
685
|
+
probes: null as ReturnType<typeof probeRollbackSurfaces> | null,
|
|
686
|
+
armed: false,
|
|
687
|
+
/** pi's own overflow classifier, resolved from the host; null when unreachable. */
|
|
688
|
+
classifier: null as ((message: unknown, contextWindow: number) => boolean) | null,
|
|
689
|
+
classifierSource: null as string | null,
|
|
690
|
+
/** Ordinals of episodes handled, so the one-shot and the ledger agree. */
|
|
691
|
+
attempts: 0,
|
|
692
|
+
last: null as Record<string, unknown> | null,
|
|
693
|
+
/** An overflow seen at message_end and not yet claimed by the compaction event. */
|
|
694
|
+
pendingOverflow: null as { at: number; entryId: string | null } | null,
|
|
695
|
+
};
|
|
696
|
+
|
|
528
697
|
// Owns advisory arming and hard-fence delivery; durable effects use persistenceQueue.
|
|
529
698
|
const advisory = {
|
|
530
699
|
hardFenceNoticeKey: null as string | null,
|
|
@@ -532,16 +701,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
532
701
|
hardFenceReleasedProjectionKeys: new Set<string>(),
|
|
533
702
|
};
|
|
534
703
|
|
|
535
|
-
// Registered suggestion sources. RETRIEVAL IS UNWIRED FROM THE PROJECTION as of this
|
|
536
|
-
// build: the slate that rendered these was a per-pass tail carrier, and a tail carrier
|
|
537
|
-
// is displaced by every append, so it diverged the prefix on every single pass. The
|
|
538
|
-
// selector in lib/surfacing.ts is kept and still gated, because proactive retrieval is
|
|
539
|
-
// the right idea in the wrong place: when it returns it must ride a commit like every
|
|
540
|
-
// other carrier. Until then nothing renders a suggestion, so nothing selects one.
|
|
541
|
-
const surfacing = {
|
|
542
|
-
sources: [] as SuggestionSource[],
|
|
543
|
-
};
|
|
544
|
-
|
|
545
704
|
// Owns native-compaction decisions and completion retry state; nativeReceiptQueue serializes it.
|
|
546
705
|
const nativeCompaction = {
|
|
547
706
|
lastThresholdDecision: null as Record<string, unknown> | null,
|
|
@@ -560,8 +719,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
560
719
|
shuttingDown: false,
|
|
561
720
|
latestSnapshot: null as ActiveContextSnapshot | null,
|
|
562
721
|
latestSnapshotError: null as string | null,
|
|
563
|
-
blockingToolHarvestedThisTurn: false,
|
|
564
|
-
blockingToolHarvestQueuedThisTurn: false,
|
|
565
722
|
contextQueue: Promise.resolve<void>(undefined),
|
|
566
723
|
};
|
|
567
724
|
|
|
@@ -620,6 +777,103 @@ export function registerActiveContext(pi: any, options: {
|
|
|
620
777
|
return record;
|
|
621
778
|
};
|
|
622
779
|
|
|
780
|
+
/**
|
|
781
|
+
* The generator, wrapped once so every brief it writes is on the record.
|
|
782
|
+
*
|
|
783
|
+
* Wrapping HERE rather than instrumenting the two call sites is deliberate. The inline
|
|
784
|
+
* path in `prepareFold` and the async upgrade lane both receive this function, so one
|
|
785
|
+
* wrapper covers both, no signature changes and no way to add a third caller that
|
|
786
|
+
* quietly escapes the ledger. It also covers a deployment's OWN summarizer, which is
|
|
787
|
+
* the case a call-site patch would have missed.
|
|
788
|
+
*
|
|
789
|
+
* A generator call is invisible everywhere else: it runs outside the session, so it
|
|
790
|
+
* lands in no provider ledger, and it runs off the turn, so it lands in no turn timing.
|
|
791
|
+
* Duration, queue wait and usage are therefore recorded here or nowhere.
|
|
792
|
+
*
|
|
793
|
+
* A failure is emitted and rethrown unchanged. This observes; it decides nothing, and
|
|
794
|
+
* the fallback to the deterministic brief stays exactly where it was.
|
|
795
|
+
*/
|
|
796
|
+
const observedSummarize = options.summarizeContextSpan
|
|
797
|
+
? async (request: Record<string, unknown>, summarizerCtx: unknown): Promise<Record<string, unknown>> => {
|
|
798
|
+
const startedAt = Date.now();
|
|
799
|
+
const queuedAt = typeof request.queuedAtMs === "number" ? request.queuedAtMs : null;
|
|
800
|
+
const spans = Array.isArray(request.spans)
|
|
801
|
+
? request.spans as Array<Record<string, unknown>>
|
|
802
|
+
: null;
|
|
803
|
+
const sourceText = typeof request.sourceText === "string" ? request.sourceText : "";
|
|
804
|
+
// A span is a CONSOLIDATION when it is a group of folds, and an automatic fold
|
|
805
|
+
// otherwise. Both kinds are counted and both kinds' source is measured, because a
|
|
806
|
+
// call can carry a mix and the honest way to attribute its tokens across kinds is by
|
|
807
|
+
// the share of source each kind contributed, never by pretending the call was one
|
|
808
|
+
// kind (Shane 2026-08-11: track cures and tokens by fold type).
|
|
809
|
+
const groupSpan = (span: Record<string, unknown>): boolean =>
|
|
810
|
+
Number.isInteger(span.children) && (span.children as number) > 1;
|
|
811
|
+
const charsOf = (span: Record<string, unknown>): number =>
|
|
812
|
+
typeof span.sourceText === "string" ? span.sourceText.length : 0;
|
|
813
|
+
const kindCounts = spans
|
|
814
|
+
? {
|
|
815
|
+
spans: spans.length,
|
|
816
|
+
group_spans: spans.filter(groupSpan).length,
|
|
817
|
+
leaf_spans: spans.filter((span) => !groupSpan(span)).length,
|
|
818
|
+
group_source_chars: spans.filter(groupSpan).reduce((sum, span) => sum + charsOf(span), 0),
|
|
819
|
+
leaf_source_chars: spans.filter((span) => !groupSpan(span))
|
|
820
|
+
.reduce((sum, span) => sum + charsOf(span), 0),
|
|
821
|
+
fold_ids: spans.map((span) => typeof span.candidateId === "string" ? span.candidateId : ""),
|
|
822
|
+
}
|
|
823
|
+
: {
|
|
824
|
+
spans: 1,
|
|
825
|
+
group_spans: groupSpan(request) ? 1 : 0,
|
|
826
|
+
leaf_spans: groupSpan(request) ? 0 : 1,
|
|
827
|
+
group_source_chars: groupSpan(request) ? sourceText.length : 0,
|
|
828
|
+
leaf_source_chars: groupSpan(request) ? 0 : sourceText.length,
|
|
829
|
+
};
|
|
830
|
+
const base = {
|
|
831
|
+
fold_id: typeof request.candidateId === "string" ? request.candidateId : "",
|
|
832
|
+
source_chars: spans ? spans.reduce((sum, span) => sum + charsOf(span), 0) : sourceText.length,
|
|
833
|
+
source_sha256: typeof request.sourceSha256 === "string" ? request.sourceSha256 : sha256Text(sourceText),
|
|
834
|
+
...kindCounts,
|
|
835
|
+
// The second ask, not the first. How often this fires is how we learn whether the
|
|
836
|
+
// brief cap is anywhere near right, so it is a field rather than an inference.
|
|
837
|
+
cure: typeof request.cure === "string" && request.cure.length > 0,
|
|
838
|
+
...(queuedAt === null ? {} : { queued_ms: Math.max(0, startedAt - queuedAt) }),
|
|
839
|
+
};
|
|
840
|
+
try {
|
|
841
|
+
const result = await options.summarizeContextSpan!(request, summarizerCtx);
|
|
842
|
+
const written = Array.isArray(result?.briefs)
|
|
843
|
+
? (result.briefs as unknown[]).map((item) => typeof item === "string" ? item.trim() : "")
|
|
844
|
+
: [typeof result?.brief === "string" ? result.brief.trim() : ""];
|
|
845
|
+
emit("context.brief", {
|
|
846
|
+
...base,
|
|
847
|
+
outcome: "ok",
|
|
848
|
+
duration_ms: Date.now() - startedAt,
|
|
849
|
+
provider: typeof result?.provider === "string" ? result.provider : "",
|
|
850
|
+
model: typeof result?.model === "string" ? result.model : "",
|
|
851
|
+
effort: typeof result?.effort === "string" ? result.effort : "",
|
|
852
|
+
brief_chars: written.reduce((sum, item) => sum + item.length, 0),
|
|
853
|
+
// Per span, so a batch where one brief came back empty is visible as that rather
|
|
854
|
+
// than as a slightly short total.
|
|
855
|
+
brief_chars_each: written.map((item) => item.length),
|
|
856
|
+
brief_sha256: written.length === 1 && written[0] ? sha256Text(written[0]) : "",
|
|
857
|
+
// Absent when the provider reported none. Never defaulted: a call whose cost we
|
|
858
|
+
// do not know and a call that cost nothing are different facts.
|
|
859
|
+
usage: result?.usage && typeof result.usage === "object" ? clone(result.usage) : null,
|
|
860
|
+
});
|
|
861
|
+
return result;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
864
|
+
emit("context.brief", {
|
|
865
|
+
...base,
|
|
866
|
+
// A timeout is not a refusal and not a provider fault, and telling them apart is
|
|
867
|
+
// the difference between raising the bound and changing the generator.
|
|
868
|
+
outcome: /exceeded \d+ms/.test(message) ? "timeout" : "error",
|
|
869
|
+
duration_ms: Date.now() - startedAt,
|
|
870
|
+
error: boundReceiptText(message, 240, "brief generator"),
|
|
871
|
+
});
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
: undefined;
|
|
876
|
+
|
|
623
877
|
/**
|
|
624
878
|
* Actions of ours that move bytes at or before a prefix position. Receipts, gate
|
|
625
879
|
* notices and suggestions are appended after the whole projection, so they can only
|
|
@@ -641,6 +895,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
641
895
|
split_from_chars: receipt.splitFromChars,
|
|
642
896
|
absorbed_wedges: receipt.absorbedWedges,
|
|
643
897
|
recovered: receipt.recovered,
|
|
898
|
+
protected_bytes: receipt.protectedBytes,
|
|
644
899
|
note: receipt.note,
|
|
645
900
|
});
|
|
646
901
|
};
|
|
@@ -671,18 +926,18 @@ export function registerActiveContext(pi: any, options: {
|
|
|
671
926
|
};
|
|
672
927
|
|
|
673
928
|
/**
|
|
674
|
-
* The
|
|
675
|
-
*
|
|
676
|
-
* max-input descriptor
|
|
677
|
-
* deployment never asked for
|
|
678
|
-
*
|
|
929
|
+
* The number every ratio, fence and budget is computed against. Declared, it is the
|
|
930
|
+
* deployment's own serving budget, already net of whatever output reservation it holds
|
|
931
|
+
* back; undeclared, it is the per-request max-input descriptor, which bakes in a full
|
|
932
|
+
* output reservation the deployment never asked for and so aborts requests inside real
|
|
933
|
+
* headroom. The descriptor is still read, and still reported, so the gap stays
|
|
679
934
|
* auditable rather than assumed.
|
|
680
935
|
*/
|
|
681
936
|
const budgetWindowFor = (ctx: any): number | null => {
|
|
682
937
|
// Remembered so the ctx-free callers (trigger, gate, reminders, advisory) report the
|
|
683
938
|
// same descriptor gap the fence does instead of re-reading a second source.
|
|
684
939
|
measurements.descriptorWindow = contextWindowFor(ctx);
|
|
685
|
-
return
|
|
940
|
+
return providerInputBudget ?? measurements.descriptorWindow;
|
|
686
941
|
};
|
|
687
942
|
|
|
688
943
|
/**
|
|
@@ -698,7 +953,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
698
953
|
const servingCapacity = (window: number | null): ReturnType<typeof capacityAccounting> =>
|
|
699
954
|
capacityAccounting({
|
|
700
955
|
window: window ?? lifecycle.latestSnapshot?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
701
|
-
truthful:
|
|
956
|
+
truthful: providerInputBudget !== null,
|
|
702
957
|
descriptorWindow: measurements.descriptorWindow,
|
|
703
958
|
usedTokens: measurements.lastProviderMeasurement?.tokens ?? null,
|
|
704
959
|
});
|
|
@@ -713,9 +968,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
713
968
|
toolName,
|
|
714
969
|
brandNoun,
|
|
715
970
|
entryTypePrefix,
|
|
716
|
-
|
|
971
|
+
blacklistAutoFoldTools,
|
|
717
972
|
readOnlyContextActions,
|
|
718
973
|
contextWindow: budgetWindowFor(ctx) ?? undefined,
|
|
974
|
+
netBudget: providerInputBudget !== null,
|
|
975
|
+
thresholds,
|
|
719
976
|
});
|
|
720
977
|
|
|
721
978
|
const authoritativeSnapshotFor = (ctx: any): ActiveContextSnapshot => {
|
|
@@ -731,9 +988,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
731
988
|
toolName,
|
|
732
989
|
brandNoun,
|
|
733
990
|
entryTypePrefix,
|
|
734
|
-
|
|
991
|
+
blacklistAutoFoldTools,
|
|
735
992
|
readOnlyContextActions,
|
|
736
993
|
contextWindow: budgetWindowFor(ctx) ?? undefined,
|
|
994
|
+
netBudget: providerInputBudget !== null,
|
|
995
|
+
thresholds,
|
|
737
996
|
});
|
|
738
997
|
};
|
|
739
998
|
|
|
@@ -777,13 +1036,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
777
1036
|
lifecycle.generation += 1;
|
|
778
1037
|
lifecycle.shuttingDown = false;
|
|
779
1038
|
cancelPreparation();
|
|
1039
|
+
cancelBriefUpgrades();
|
|
780
1040
|
lifecycle.latestSnapshot = null;
|
|
781
1041
|
lifecycle.latestSnapshotError = null;
|
|
782
1042
|
measurements.latestRatio = null;
|
|
783
1043
|
measurements.lastProviderMeasurement = null;
|
|
1044
|
+
measurements.wallInflowSteps.length = 0;
|
|
1045
|
+
measurements.projectionAnchor = null;
|
|
1046
|
+
measurements.lastProjectedEstimateBasis = "unmeasured";
|
|
784
1047
|
ladder.pendingManual = false;
|
|
785
|
-
lifecycle.blockingToolHarvestedThisTurn = false;
|
|
786
|
-
lifecycle.blockingToolHarvestQueuedThisTurn = false;
|
|
787
1048
|
if (!preserveThresholdDecision) nativeCompaction.lastThresholdDecision = null;
|
|
788
1049
|
ladder.lastPreparationError = null;
|
|
789
1050
|
ladder.boundaryFailure = null;
|
|
@@ -898,11 +1159,45 @@ export function registerActiveContext(pi: any, options: {
|
|
|
898
1159
|
const operation = persistence.persistenceQueue.then(async () => {
|
|
899
1160
|
if (!persistence.state || !persistence.persisted) return;
|
|
900
1161
|
let next = clone(persistence.state);
|
|
1162
|
+
// Which folds this write is TRYING to add, read before the projection can drop them.
|
|
1163
|
+
const persistedFoldIds = new Set(persistence.persisted.folds.map((fold) => fold.id));
|
|
1164
|
+
const arrivingFoldIds = next.folds.filter((fold) => !persistedFoldIds.has(fold.id)).map((fold) => fold.id);
|
|
1165
|
+
const foldsInMemory = next.folds.length;
|
|
901
1166
|
if (ctx && lifecycle.latestSnapshot?.sessionId === next.sessionId) {
|
|
902
1167
|
next = persistenceProjection(next, authoritativeSnapshotFor(ctx));
|
|
903
1168
|
}
|
|
904
1169
|
next.folds = normalizeFoldsForPersistedRecords(next.folds, persistence.persistedFoldRecords);
|
|
905
1170
|
if (sameStateProjection(next, persistence.persisted)) {
|
|
1171
|
+
// A COMMIT THAT VANISHES IS NOT A NO-OP.
|
|
1172
|
+
//
|
|
1173
|
+
// `persistenceProjection` keeps only folds whose refs still resolve against the
|
|
1174
|
+
// snapshot, which is correct when evidence has genuinely left the branch. But when
|
|
1175
|
+
// a write ARRIVES with new folds and projects down to something byte-equal to what
|
|
1176
|
+
// is already durable, every one of those folds was dropped, and returning quietly
|
|
1177
|
+
// here also resets `persistence.state` BACKWARDS to the persisted revision. The
|
|
1178
|
+
// marks that produced them go back to pending, the same commit is attempted again
|
|
1179
|
+
// on the next boundary, and it vanishes again.
|
|
1180
|
+
//
|
|
1181
|
+
// Measured, sol-20260812 rep 3: a band-top commit ran at revision 143 and folded
|
|
1182
|
+
// 579,489 source chars into 85,463 of placeholder. Durable state never passed 134.
|
|
1183
|
+
// None of its 11 folds reached a record, no receipt was written, occupancy climbed
|
|
1184
|
+
// 237,682 -> 253,423 against a 251,520 budget unopposed, and the next request was
|
|
1185
|
+
// refused. Nothing was reported, because a silent return is not a failure. Rep 2
|
|
1186
|
+
// ran 15 commits on the same plan with no gap at all, so this is rare rather than
|
|
1187
|
+
// systemic, which is exactly why it has to announce itself the first time.
|
|
1188
|
+
//
|
|
1189
|
+
// Raising it routes through the automatic-failure path, which restores the state
|
|
1190
|
+
// this call was about to discard, suspends automatic folding and tells the user.
|
|
1191
|
+
// Suspending is the safe direction: continuing to compute folds that are thrown
|
|
1192
|
+
// away is what turned one dropped commit into a dead session.
|
|
1193
|
+
if (arrivingFoldIds.length) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`Active-context commit discarded at persistence: ${arrivingFoldIds.length} arriving fold(s) ` +
|
|
1196
|
+
`did not survive the persistence projection (${foldsInMemory} in memory, ` +
|
|
1197
|
+
`${next.folds.length} durable) at revision ${persistence.state.revision}; ` +
|
|
1198
|
+
`first ${arrivingFoldIds[0]}`,
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
906
1201
|
persistence.state = clone(persistence.persisted);
|
|
907
1202
|
return;
|
|
908
1203
|
}
|
|
@@ -1240,11 +1535,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1240
1535
|
protection: persistence.state ? protectionSha256(persistence.state) : null,
|
|
1241
1536
|
selection,
|
|
1242
1537
|
policy: snapshot ? {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1538
|
+
maxTarget: snapshot.thresholds.maxTarget,
|
|
1539
|
+
minTarget: snapshot.thresholds.minTarget,
|
|
1540
|
+
freshTail: snapshot.thresholds.freshTail,
|
|
1246
1541
|
hardFenceRatio: hardFenceRatio(snapshot),
|
|
1247
|
-
consolidationRatio: snapshot.policy.consolidationRatio,
|
|
1248
1542
|
} : null,
|
|
1249
1543
|
});
|
|
1250
1544
|
};
|
|
@@ -1424,8 +1718,126 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1424
1718
|
);
|
|
1425
1719
|
};
|
|
1426
1720
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1721
|
+
/**
|
|
1722
|
+
* Bind the provider's count to the projection it counted.
|
|
1723
|
+
*
|
|
1724
|
+
* The anchor is the number the provider reported for a request this process actually
|
|
1725
|
+
* TRANSMITTED, held with that projection's serialized bytes. Identity is carried three
|
|
1726
|
+
* ways so a stale anchor cannot pass for a fresh one: the session it belongs to, the
|
|
1727
|
+
* attribution generation (a model or thinking-level change is a different tokenizer, so
|
|
1728
|
+
* its counts describe nothing here), and the projection text itself, which the reading
|
|
1729
|
+
* re-checks byte for byte before adding anything to it.
|
|
1730
|
+
*/
|
|
1731
|
+
const noteProviderProjectionAnchor = (measurement: ProviderContextMeasurement): void => {
|
|
1732
|
+
// ONE ANCHOR PER MEASURED RESPONSE. A pass that re-observes the same assistant
|
|
1733
|
+
// message -- the retry after a provider rejection, or any pass the host runs without
|
|
1734
|
+
// a new response -- would otherwise re-bind that count to a newer projection the
|
|
1735
|
+
// provider never counted, and the count would silently describe the wrong bytes.
|
|
1736
|
+
if (measurements.projectionAnchor?.messageSha256 === measurement.messageSha256) return;
|
|
1737
|
+
const text = instrumentation.previousText;
|
|
1738
|
+
// Only a TRANSMITTED projection can be anchored: an aborted pass built a projection
|
|
1739
|
+
// the provider never saw, so no reported count describes it.
|
|
1740
|
+
if (!text || text.length < 2 || !text.endsWith("]") || !persistence.state) {
|
|
1741
|
+
measurements.projectionAnchor = null;
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const chars = bytes(text);
|
|
1745
|
+
// AND THE COUNT HAS TO FIT THE BYTES IT IS BOUND TO.
|
|
1746
|
+
//
|
|
1747
|
+
// An anchor is exact for the bytes it covers only if it actually describes them. A
|
|
1748
|
+
// pairing that implies fewer serialized chars per token than any tokenizer produces
|
|
1749
|
+
// is not describing this projection, and taken at face value it would put the fence
|
|
1750
|
+
// under permanent pressure a projection of that size cannot explain. The calibration
|
|
1751
|
+
// window already refuses such a ratio through the same floor; the anchor refuses it
|
|
1752
|
+
// outright and falls back to the estimate. Only the FLOOR applies: a rate ABOVE the
|
|
1753
|
+
// calibration ceiling is the signature-heavy composition this mechanism exists to
|
|
1754
|
+
// read, and clamping it would throw away the measurement that motivated the build.
|
|
1755
|
+
if (!(measurement.tokens > 0) || chars / measurement.tokens < PROJECTION_CHARS_PER_TOKEN_FLOOR) {
|
|
1756
|
+
measurements.projectionAnchor = null;
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
measurements.projectionAnchor = {
|
|
1760
|
+
tokens: measurement.tokens,
|
|
1761
|
+
chars,
|
|
1762
|
+
head: text.slice(0, -1),
|
|
1763
|
+
messageSha256: measurement.messageSha256,
|
|
1764
|
+
sessionId: persistence.state.sessionId,
|
|
1765
|
+
generation: lifecycle.generation,
|
|
1766
|
+
};
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* THE OCCUPANCY READING: what the provider counted, plus only what arrived after it.
|
|
1771
|
+
*
|
|
1772
|
+
* A whole-projection estimate divides every byte in the window by one rate, and one
|
|
1773
|
+
* rate does not fit the window's contents. Measured 2026-08-10 on the sealed rep-23
|
|
1774
|
+
* run, byte exact against that run's own projection records: the estimate is unbiased
|
|
1775
|
+
* at the pre-commit PEAKS (provider over estimate, mean 1.004 across six) and over-reads
|
|
1776
|
+
* the post-fold TROUGHS by 22 to 35 percent (mean ratio 0.784 across six). It is a phase
|
|
1777
|
+
* effect, not drift: encrypted reasoning-signature blobs on assistant messages grew from
|
|
1778
|
+
* 14.6 to 51.3 percent of projection bytes over that run and price at a fitted 24.8 chars
|
|
1779
|
+
* per token against 3.957 for ordinary content, so a projection's rate depends on what it
|
|
1780
|
+
* is made of. The runtime therefore believed its floor climbed 0.298 to 0.490 of budget
|
|
1781
|
+
* while the provider read 0.276 to 0.382: roughly 45 percent of the apparent floor rise
|
|
1782
|
+
* was measurement error, five of six true troughs sat at or below the 0.35 minTarget, and
|
|
1783
|
+
* the ladder was folding past its own floor without seeing it.
|
|
1784
|
+
*
|
|
1785
|
+
* So the rate is applied to the DELTA only. The provider's count for the projection this
|
|
1786
|
+
* one extends is exact for every byte it covers, whatever those bytes are made of, and
|
|
1787
|
+
* only the material appended since is estimated. Measured on the same run: mean absolute
|
|
1788
|
+
* occupancy error across all requests falls from 8.38 to 3.08 percent, and inside every
|
|
1789
|
+
* post-fold refill window it drops from the +9 to +27 percent band to under 4 percent from
|
|
1790
|
+
* the second request onward.
|
|
1791
|
+
*
|
|
1792
|
+
* Two things this deliberately does NOT do. It does not move the commit trigger: at all
|
|
1793
|
+
* twelve commit points of that run the delta since the last provider count was exactly
|
|
1794
|
+
* zero, because the commit hook runs after the response and its usage record have landed.
|
|
1795
|
+
* And it does not fix the one pass per cycle where the projection is REWRITTEN rather than
|
|
1796
|
+
* appended, right after a commit: the anchor describes a projection this one does not
|
|
1797
|
+
* begin with, so there is nothing to add a delta to and the whole-projection estimate
|
|
1798
|
+
* stands, error and all (+27 to +35 percent, measured). That case is detected by
|
|
1799
|
+
* construction, not by the sign of the delta: the current serialization must begin with
|
|
1800
|
+
* the anchored one verbatim, up to the array separator.
|
|
1801
|
+
*
|
|
1802
|
+
* The anchor is the provider's TOTAL for that request, which includes the output tokens
|
|
1803
|
+
* of the response the next projection appends and then prices again as bytes. That is one
|
|
1804
|
+
* response's worth of double count, it runs HIGH rather than low, and it keeps a single
|
|
1805
|
+
* definition of "the provider's number" across occupancy, the ratio and the calibration
|
|
1806
|
+
* instead of a second one that only this reading would use.
|
|
1807
|
+
*/
|
|
1808
|
+
const projectedTokenReading = (projected: unknown[]): {
|
|
1809
|
+
tokens: number;
|
|
1810
|
+
basis: ProjectionReadingBasis;
|
|
1811
|
+
chars: number;
|
|
1812
|
+
anchorTokens: number | null;
|
|
1813
|
+
deltaChars: number | null;
|
|
1814
|
+
} => {
|
|
1815
|
+
const text = stableStringify(projected);
|
|
1816
|
+
const chars = Buffer.byteLength(text, "utf8");
|
|
1817
|
+
const charsPerToken = projectionCharsPerToken();
|
|
1818
|
+
const estimate = Math.ceil(chars / charsPerToken);
|
|
1819
|
+
const anchor = measurements.projectionAnchor;
|
|
1820
|
+
if (!anchor || anchor.generation !== lifecycle.generation ||
|
|
1821
|
+
anchor.sessionId !== persistence.state?.sessionId) {
|
|
1822
|
+
return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null };
|
|
1823
|
+
}
|
|
1824
|
+
// By construction: the anchored serialization minus its closing bracket must be a
|
|
1825
|
+
// byte-exact prefix, and the byte after it must be the array separator or the close.
|
|
1826
|
+
// A message that merely GREW in place fails this, because the anchored head ends with
|
|
1827
|
+
// that message's own closing brace.
|
|
1828
|
+
const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
|
|
1829
|
+
if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
|
|
1830
|
+
return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null };
|
|
1831
|
+
}
|
|
1832
|
+
const deltaChars = chars - anchor.chars;
|
|
1833
|
+
return {
|
|
1834
|
+
tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken)),
|
|
1835
|
+
basis: "anchored",
|
|
1836
|
+
chars,
|
|
1837
|
+
anchorTokens: anchor.tokens,
|
|
1838
|
+
deltaChars,
|
|
1839
|
+
};
|
|
1840
|
+
};
|
|
1429
1841
|
|
|
1430
1842
|
/** The largest recent estimator error, as a share. Unmeasured sessions assume none. */
|
|
1431
1843
|
const estimatorErrorShare = (): number => measurements.estimatorErrors.length
|
|
@@ -1437,6 +1849,38 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1437
1849
|
? Math.max(...measurements.inflowSteps)
|
|
1438
1850
|
: 0;
|
|
1439
1851
|
|
|
1852
|
+
/**
|
|
1853
|
+
* THE OUTPUT WALL'S OWN FORWARD LOOK, AND WHY IT IS NOT THE ONE ABOVE.
|
|
1854
|
+
*
|
|
1855
|
+
* `inflowSteps` is filled by `noteProjectionCalibration`, which runs on the context pass.
|
|
1856
|
+
* By then the message-end path has already advanced `lastProviderMeasurement` to the
|
|
1857
|
+
* count being calibrated, so the difference is a measurement against itself and the step
|
|
1858
|
+
* is zero. That window has therefore been empty in every session ever run, which means
|
|
1859
|
+
* the transmission fence margin and the commit depth floor, both of which read it, have
|
|
1860
|
+
* been running on their floor shares alone. Reviving it moves both, and both are
|
|
1861
|
+
* calibrated against reps recorded while it was empty, so that is its own build with its
|
|
1862
|
+
* own gates rather than a rider on this one.
|
|
1863
|
+
*
|
|
1864
|
+
* This window is filled where the previous count is still readable, and nothing but the
|
|
1865
|
+
* wall reads it. When a count reaches only the context pass the window stays empty and
|
|
1866
|
+
* the wall degrades to the condition it replaced, which is the safe direction.
|
|
1867
|
+
*/
|
|
1868
|
+
const WALL_INFLOW_WINDOW = 8;
|
|
1869
|
+
const noteWallInflow = (measurement: ProviderContextMeasurement): void => {
|
|
1870
|
+
const previous = measurements.lastProviderMeasurement;
|
|
1871
|
+
if (!previous || !Number.isFinite(measurement.tokens) || !Number.isFinite(previous.tokens)) return;
|
|
1872
|
+
const step = measurement.tokens - previous.tokens;
|
|
1873
|
+
// Only GROWTH. A commit shrinks the window; that is what the wall has to survive, not
|
|
1874
|
+
// a size the next request can be predicted from.
|
|
1875
|
+
if (step <= 0) return;
|
|
1876
|
+
measurements.wallInflowSteps.push(step);
|
|
1877
|
+
if (measurements.wallInflowSteps.length > WALL_INFLOW_WINDOW) measurements.wallInflowSteps.shift();
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const expectedWallInflowTokens = (): number => measurements.wallInflowSteps.length
|
|
1881
|
+
? Math.max(...measurements.wallInflowSteps)
|
|
1882
|
+
: 0;
|
|
1883
|
+
|
|
1440
1884
|
/**
|
|
1441
1885
|
* The safety margin.
|
|
1442
1886
|
*
|
|
@@ -1478,7 +1922,18 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1478
1922
|
* transmission, which is the whole point of having a fence.
|
|
1479
1923
|
*/
|
|
1480
1924
|
const projectionExceedsBudget = (projected: unknown[], ctx: any): {
|
|
1925
|
+
/** Occupancy: what the provider counted, plus what was appended after it. */
|
|
1481
1926
|
tokens: number;
|
|
1927
|
+
/**
|
|
1928
|
+
* This projection's OWN size, every byte of it at one rate. Occupancy answers "how
|
|
1929
|
+
* full is the window"; a before-and-after comparison between two projections answers
|
|
1930
|
+
* "did this pass make the request smaller", and those are different questions. A
|
|
1931
|
+
* reduction is measured in size space on both sides, because the projection the pass
|
|
1932
|
+
* rebuilds is a rewrite the anchor does not describe, and subtracting an anchored
|
|
1933
|
+
* reading from an unanchored one reports a change that no folding caused.
|
|
1934
|
+
*/
|
|
1935
|
+
sizeTokens: number;
|
|
1936
|
+
basis: ProjectionReadingBasis;
|
|
1482
1937
|
budgetTokens: number;
|
|
1483
1938
|
marginTokens: number;
|
|
1484
1939
|
/** Past the wire: this request must not be transmitted at all. */
|
|
@@ -1490,12 +1945,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1490
1945
|
const budgetTokens = Number.isFinite(capacity.budgetTokens) && capacity.budgetTokens > 0
|
|
1491
1946
|
? capacity.budgetTokens
|
|
1492
1947
|
: Number.POSITIVE_INFINITY;
|
|
1493
|
-
const
|
|
1948
|
+
const reading = projectedTokenReading(projected);
|
|
1949
|
+
const tokens = reading.tokens;
|
|
1950
|
+
const sizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
|
|
1494
1951
|
const marginTokens = Number.isFinite(budgetTokens)
|
|
1495
1952
|
? projectionMarginTokens(tokens, capacity.window)
|
|
1496
1953
|
: 0;
|
|
1497
1954
|
return {
|
|
1498
1955
|
tokens,
|
|
1956
|
+
sizeTokens,
|
|
1957
|
+
basis: reading.basis,
|
|
1499
1958
|
budgetTokens,
|
|
1500
1959
|
marginTokens,
|
|
1501
1960
|
over: tokens > budgetTokens,
|
|
@@ -1560,6 +2019,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1560
2019
|
// is the one built after this one. A provider rejection outranks our own estimate:
|
|
1561
2020
|
// it is ground truth that the last request did not fit, whatever we measured.
|
|
1562
2021
|
if (!measured.crowded && !rejected) return { projected, aborted: false };
|
|
2022
|
+
// Crowded is a MARGIN PREDICTION, not an overflow. While a last-call round is
|
|
2023
|
+
// genuinely OPEN, exposure rendered this pass or the agent's response still
|
|
2024
|
+
// outstanding, the prediction waits for it: this request still fits and the
|
|
2025
|
+
// round ends on the very next context pass. Once the round has elapsed the
|
|
2026
|
+
// margin lane acts freely, because a stale exposure parked behind the reopen
|
|
2027
|
+
// latch must never muzzle the one reducer that can still act (the 20k-window
|
|
2028
|
+
// probe climbed from crowded to the wire in exactly that state). Genuine
|
|
2029
|
+
// untransmissibility, an over-budget projection or a provider rejection, never
|
|
2030
|
+
// waits: a request whose projection exceeds the provider input budget is rejected
|
|
2031
|
+
// outright, so recovery must produce a window that fits. Economy does not outrank
|
|
2032
|
+
// the one round.
|
|
2033
|
+
const lastCall = persistence.state?.lastCall;
|
|
2034
|
+
const delivery = curation.lastCallDelivery;
|
|
2035
|
+
const roundOpen = Boolean(lastCall) && (!delivery || delivery.exposure !== lastCall.exposure ||
|
|
2036
|
+
markOrdinal(snapshot) <= delivery.ordinal);
|
|
2037
|
+
if (!measured.over && !rejected && roundOpen) {
|
|
2038
|
+
return { projected, aborted: false };
|
|
2039
|
+
}
|
|
1563
2040
|
// Why this fired, recorded from the measurement that TRIGGERED it rather than the
|
|
1564
2041
|
// one taken afterwards, which by then describes a projection that fits.
|
|
1565
2042
|
const trigger = measured;
|
|
@@ -1574,7 +2051,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1574
2051
|
let action: Record<string, unknown> | null = null;
|
|
1575
2052
|
try {
|
|
1576
2053
|
action = await attemptAutomaticRung(snapshot, 1, ctx, "projection-budget", {
|
|
1577
|
-
waiveToolCadence: true,
|
|
1578
2054
|
waiverRatio: 1,
|
|
1579
2055
|
});
|
|
1580
2056
|
} catch (error) {
|
|
@@ -1589,8 +2065,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1589
2065
|
}
|
|
1590
2066
|
if (reducedAtLeastOnce) {
|
|
1591
2067
|
ladder.overBudgetReduction = {
|
|
1592
|
-
|
|
1593
|
-
|
|
2068
|
+
// Size space on both sides: what this pass CHANGED, not how full the window is.
|
|
2069
|
+
estimatedTokensBefore: trigger.sizeTokens,
|
|
2070
|
+
estimatedTokensAfter: measured.sizeTokens,
|
|
2071
|
+
occupancyTokensAfter: measured.tokens,
|
|
2072
|
+
occupancyBasis: measured.basis,
|
|
1594
2073
|
budgetTokens: trigger.budgetTokens,
|
|
1595
2074
|
marginTokens: trigger.marginTokens,
|
|
1596
2075
|
estimatorErrorShare: estimatorErrorShare(),
|
|
@@ -1603,39 +2082,87 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1603
2082
|
};
|
|
1604
2083
|
}
|
|
1605
2084
|
if (rejected) {
|
|
2085
|
+
const overflowBefore = trigger.sizeTokens;
|
|
2086
|
+
// RECOVERY IS A CHANGE, NOT AN OPINION.
|
|
2087
|
+
//
|
|
2088
|
+
// This read `!measured.over`: our own estimator answering the question the
|
|
2089
|
+
// provider had just answered differently. A rejection is ground truth that the
|
|
2090
|
+
// estimate was wrong, so the estimate cannot also be the evidence that the retry
|
|
2091
|
+
// will work. And a rejection mutates nothing durable -- the rolled-back branch
|
|
2092
|
+
// rebuilds the same projection -- so a pass that changed nothing hands the
|
|
2093
|
+
// provider a byte-identical request and calls it a rescue. Measured 2026-08-10
|
|
2094
|
+
// (luna-20260810 pifold rep 2): two rejections at stage 25, not one byte folded
|
|
2095
|
+
// either time, both recorded recovered true, and the run died anyway.
|
|
2096
|
+
//
|
|
2097
|
+
// The baseline is the request that was actually REJECTED, which is the last
|
|
2098
|
+
// projection this process built: it is written at the end of every pass that
|
|
2099
|
+
// builds one, and this lane runs before the current pass overwrites it. Measuring
|
|
2100
|
+
// against the projection handed to this function instead would credit only the
|
|
2101
|
+
// recovery loop's own folds and miss the ordinary commit that already ran earlier
|
|
2102
|
+
// in the same pass, which is the usual way a retried request gets smaller.
|
|
2103
|
+
//
|
|
2104
|
+
// Size space on both sides. Occupancy is anchored to the provider's count for the
|
|
2105
|
+
// projection it measured, and the projection this lane rebuilds is a rewrite that
|
|
2106
|
+
// anchor does not describe, so a rejected-minus-retried subtraction across the two
|
|
2107
|
+
// bases reports a change no folding caused.
|
|
2108
|
+
const rejectedTokens = measurements.lastProjectedSizeTokens;
|
|
2109
|
+
const freedTokens = typeof rejectedTokens === "number"
|
|
2110
|
+
? Math.max(0, rejectedTokens - measured.sizeTokens)
|
|
2111
|
+
: 0;
|
|
2112
|
+
// Smaller than what the provider refused, AND inside the budget. Either half alone
|
|
2113
|
+
// is a claim this runtime has already been wrong about once.
|
|
2114
|
+
const recovered = freedTokens > 0 && !measured.over;
|
|
1606
2115
|
curation.lastRecovery = {
|
|
1607
2116
|
status: curation.pendingRejection?.status ?? null,
|
|
1608
2117
|
attempts,
|
|
1609
|
-
estimatedTokensAfter: measured.
|
|
2118
|
+
estimatedTokensAfter: measured.sizeTokens,
|
|
1610
2119
|
budgetTokens: measured.budgetTokens,
|
|
1611
|
-
recovered
|
|
2120
|
+
recovered,
|
|
1612
2121
|
};
|
|
1613
2122
|
emit("context.recovery", {
|
|
1614
2123
|
provider_status: curation.pendingRejection?.status ?? null,
|
|
1615
2124
|
attempts,
|
|
1616
2125
|
max_attempts: OVERFLOW_RECOVERY_MAX_ATTEMPTS,
|
|
1617
|
-
tokens_before:
|
|
1618
|
-
tokens_after: measured.
|
|
2126
|
+
tokens_before: overflowBefore,
|
|
2127
|
+
tokens_after: measured.sizeTokens,
|
|
2128
|
+
occupancy_tokens_after: measured.tokens,
|
|
2129
|
+
occupancy_basis: measured.basis,
|
|
1619
2130
|
budget_tokens: measured.budgetTokens,
|
|
1620
2131
|
margin_tokens: measured.marginTokens,
|
|
1621
|
-
recovered
|
|
2132
|
+
recovered,
|
|
2133
|
+
// What the verdict rests on, so a false claim cannot be read back as a true one:
|
|
2134
|
+
// the size of the request the provider refused, and how much smaller this one is.
|
|
2135
|
+
rejected_tokens: typeof rejectedTokens === "number" ? rejectedTokens : null,
|
|
2136
|
+
freed_tokens: freedTokens,
|
|
2137
|
+
// Whether the recovery loop itself folded, as distinct from the pass as a whole.
|
|
2138
|
+
loop_reduced: reducedAtLeastOnce,
|
|
2139
|
+
// The join. `context.rollback` records what left the branch; this records what
|
|
2140
|
+
// the retried pass folded to make the shorter window fit, and the two are one
|
|
2141
|
+
// episode. Null when a rejection was recorded without a tree rollback.
|
|
2142
|
+
rollback_seq: typeof rollback.last?.seq === "number" ? rollback.last.seq : null,
|
|
1622
2143
|
});
|
|
1623
|
-
const
|
|
2144
|
+
const unchangedNote = "The provider rejected the last request and this pass made it no smaller, so the " +
|
|
2145
|
+
`rebuilt request is ${measured.tokens} estimated tokens against a ${measured.budgetTokens}-token ` +
|
|
2146
|
+
"serving budget. Our own estimate says it fits; the provider already said otherwise about a request " +
|
|
2147
|
+
"this size, so this is not a recovery. Fold or protect deliberately, or the next request meets the " +
|
|
2148
|
+
"same rejection.";
|
|
1624
2149
|
deliverReceipt(contextReceipt({
|
|
1625
2150
|
kind: "overflow-recovery",
|
|
1626
2151
|
ordinal: markOrdinal(snapshot),
|
|
1627
2152
|
trigger: `provider-rejection:${curation.pendingRejection?.status ?? "unknown"}`,
|
|
1628
|
-
freedTokens
|
|
1629
|
-
occupancyBefore: overflowBefore,
|
|
1630
|
-
occupancyAfter: measured.
|
|
1631
|
-
recovered
|
|
2153
|
+
freedTokens,
|
|
2154
|
+
occupancyBefore: typeof rejectedTokens === "number" ? rejectedTokens : overflowBefore,
|
|
2155
|
+
occupancyAfter: measured.sizeTokens,
|
|
2156
|
+
recovered,
|
|
1632
2157
|
note: measured.over
|
|
1633
2158
|
? `The rebuilt request is still ${measured.tokens} tokens against a ${measured.budgetTokens}-token ` +
|
|
1634
2159
|
"serving budget, so the run stops here rather than sending a request the provider will reject again."
|
|
1635
|
-
:
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
2160
|
+
: recovered
|
|
2161
|
+
? "A rollback was required: the provider rejected the last request, which overfilled the serving " +
|
|
2162
|
+
`budget at ${rejectedTokens} estimated tokens against ${measured.budgetTokens}. This pass ` +
|
|
2163
|
+
`landed it at ${measured.sizeTokens} tokens. Nothing durable was written for it, and the request ` +
|
|
2164
|
+
"was rebuilt inside the budget rather than dropped."
|
|
2165
|
+
: unchangedNote,
|
|
1639
2166
|
}));
|
|
1640
2167
|
curation.pendingRejection = null;
|
|
1641
2168
|
}
|
|
@@ -1675,7 +2202,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1675
2202
|
snapshot,
|
|
1676
2203
|
state: capturedState,
|
|
1677
2204
|
generation: capturedGeneration,
|
|
1678
|
-
summarize: ladder.failedPreparations.has(id) ? undefined :
|
|
2205
|
+
summarize: ladder.failedPreparations.has(id) ? undefined : observedSummarize,
|
|
1679
2206
|
onSummarizerFailure: (error) => {
|
|
1680
2207
|
ladder.lastPreparationError = error instanceof Error ? error.message : String(error);
|
|
1681
2208
|
ladder.failedPreparations.add(id);
|
|
@@ -1745,6 +2272,96 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1745
2272
|
return projected;
|
|
1746
2273
|
};
|
|
1747
2274
|
|
|
2275
|
+
/**
|
|
2276
|
+
* The rider rides the SAME admission as the receipt block: it renders from the
|
|
2277
|
+
* persisted literal bytes, lands once per freeze cycle, and the freeze closes over
|
|
2278
|
+
* it, so it can never diverge a prefix on its own. It is never regenerated: the
|
|
2279
|
+
* bytes the epoch persisted are the bytes every re-render carries.
|
|
2280
|
+
*/
|
|
2281
|
+
const appendRider = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
2282
|
+
const rider = persistence.state?.rider;
|
|
2283
|
+
if (!rider) return projected;
|
|
2284
|
+
if (!carrierAdmitted("rider")) return projected;
|
|
2285
|
+
projected.push({
|
|
2286
|
+
role: "custom",
|
|
2287
|
+
customType: riderProjectionType,
|
|
2288
|
+
content: rider.text,
|
|
2289
|
+
display: false,
|
|
2290
|
+
details: {
|
|
2291
|
+
source: activeContextSource(entryTypePrefix),
|
|
2292
|
+
ephemeral: true,
|
|
2293
|
+
riderEpoch: rider.epoch,
|
|
2294
|
+
},
|
|
2295
|
+
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
2296
|
+
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
2297
|
+
: 0,
|
|
2298
|
+
});
|
|
2299
|
+
return projected;
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
/**
|
|
2303
|
+
* Threshold notices are APPEND-ONCE: each delivered notice is its own carrier with
|
|
2304
|
+
* its own key, so a notice that fires mid-freeze lands at the tail as a pure append,
|
|
2305
|
+
* the freeze closes over it, and it then persists in the window the way a tool
|
|
2306
|
+
* result does. A freeze break re-renders every retained notice from its persisted
|
|
2307
|
+
* literal bytes inside the rewrite that broke the freeze.
|
|
2308
|
+
*/
|
|
2309
|
+
const appendNotices = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
2310
|
+
const notices = persistence.state?.notices;
|
|
2311
|
+
if (!notices?.ring.length) return projected;
|
|
2312
|
+
for (const notice of notices.ring) {
|
|
2313
|
+
if (!carrierAdmitted(`notice-${notice.share}-${notice.ordinal}`)) continue;
|
|
2314
|
+
projected.push({
|
|
2315
|
+
role: "custom",
|
|
2316
|
+
customType: noticeProjectionType,
|
|
2317
|
+
content: notice.text,
|
|
2318
|
+
display: false,
|
|
2319
|
+
details: {
|
|
2320
|
+
source: activeContextSource(entryTypePrefix),
|
|
2321
|
+
ephemeral: true,
|
|
2322
|
+
share: notice.share,
|
|
2323
|
+
noticeOrdinal: notice.ordinal,
|
|
2324
|
+
},
|
|
2325
|
+
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
2326
|
+
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
2327
|
+
: 0,
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
return projected;
|
|
2331
|
+
};
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* The pre-commit last-call rides the commit BOUNDARY: it is armed when the band-top
|
|
2335
|
+
* trigger fires, appended at the tail (a pure append) for exactly one gated round,
|
|
2336
|
+
* and the commit that consumes it clears the state, so the carrier's disappearance
|
|
2337
|
+
* rides the rewrite that commit already pays for. Rendering records the delivery,
|
|
2338
|
+
* which is what the one-round clock is measured from.
|
|
2339
|
+
*/
|
|
2340
|
+
const appendLastCall = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
2341
|
+
const lastCall = persistence.state?.lastCall;
|
|
2342
|
+
if (!lastCall) return projected;
|
|
2343
|
+
if (carrierAdmitted(`lastcall-${lastCall.exposure}`)) {
|
|
2344
|
+
projected.push({
|
|
2345
|
+
role: "custom",
|
|
2346
|
+
customType: lastCallProjectionType,
|
|
2347
|
+
content: lastCall.text,
|
|
2348
|
+
display: false,
|
|
2349
|
+
details: {
|
|
2350
|
+
source: activeContextSource(entryTypePrefix),
|
|
2351
|
+
ephemeral: true,
|
|
2352
|
+
exposure: lastCall.exposure,
|
|
2353
|
+
},
|
|
2354
|
+
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
2355
|
+
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
2356
|
+
: 0,
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
if (curation.lastCallDelivery?.exposure !== lastCall.exposure) {
|
|
2360
|
+
curation.lastCallDelivery = { exposure: lastCall.exposure, ordinal: markOrdinal(snapshot) };
|
|
2361
|
+
}
|
|
2362
|
+
return projected;
|
|
2363
|
+
};
|
|
2364
|
+
|
|
1748
2365
|
/**
|
|
1749
2366
|
* Whatever this pass ended up sending becomes the frozen surface for the next one, in
|
|
1750
2367
|
* full. Nothing rides after the frozen region.
|
|
@@ -1779,7 +2396,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1779
2396
|
const customType = ownValue(message, "customType");
|
|
1780
2397
|
return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
|
|
1781
2398
|
customType !== surfacingProjectionType && customType !== receiptProjectionType &&
|
|
1782
|
-
customType !== curationProjectionType
|
|
2399
|
+
customType !== riderProjectionType && customType !== curationProjectionType &&
|
|
2400
|
+
customType !== lastCallProjectionType && customType !== noticeProjectionType;
|
|
1783
2401
|
});
|
|
1784
2402
|
// The freeze test, stated as bytes rather than as intent: if the rebuilt body still
|
|
1785
2403
|
// OPENS with the frozen body verbatim, nothing structural moved and the previous
|
|
@@ -1805,6 +2423,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1805
2423
|
// bought nothing anyway: voluntary fold share was 0.00, and the three runs built
|
|
1806
2424
|
// specifically to invite curation produced 0, 1 and 0 voluntary folds.
|
|
1807
2425
|
appendReceipts(projected, snapshot);
|
|
2426
|
+
appendRider(projected, snapshot);
|
|
2427
|
+
appendNotices(projected, snapshot);
|
|
2428
|
+
appendLastCall(projected, snapshot);
|
|
1808
2429
|
return holdFrozen(projected);
|
|
1809
2430
|
};
|
|
1810
2431
|
/**
|
|
@@ -1830,14 +2451,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1830
2451
|
const charsPerToken = projectionCharsPerToken();
|
|
1831
2452
|
const causes = instrumentation.sinceHandoff.filter((event) =>
|
|
1832
2453
|
PREFIX_MUTATING_KINDS.has(event.kind));
|
|
2454
|
+
// The reading this projection was judged by, with the pieces it was built from, so
|
|
2455
|
+
// the anchored series is readable from the stream instead of reconstructed.
|
|
2456
|
+
const reading = projectedTokenReading(projected);
|
|
1833
2457
|
emit("context.projection", {
|
|
1834
2458
|
change: comparison.change,
|
|
1835
2459
|
previous_count: comparison.previousCount,
|
|
1836
2460
|
next_count: comparison.nextCount,
|
|
1837
2461
|
appended_count: comparison.appendedCount,
|
|
1838
2462
|
first_divergent_index: comparison.firstDivergentIndex,
|
|
1839
|
-
chars:
|
|
1840
|
-
estimated_tokens:
|
|
2463
|
+
chars: reading.chars,
|
|
2464
|
+
estimated_tokens: reading.tokens,
|
|
2465
|
+
estimate_basis: reading.basis,
|
|
2466
|
+
anchor_tokens: reading.anchorTokens,
|
|
2467
|
+
delta_chars: reading.deltaChars,
|
|
1841
2468
|
chars_per_token: charsPerToken,
|
|
1842
2469
|
});
|
|
1843
2470
|
// Emission only. Which of these is a provider-side miss and which is a rewrite we
|
|
@@ -1870,15 +2497,354 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1870
2497
|
instrumentation.mutationsSinceHandoff = 0;
|
|
1871
2498
|
};
|
|
1872
2499
|
|
|
2500
|
+
/**
|
|
2501
|
+
* Enqueue the folds this commit created with a deterministic brief.
|
|
2502
|
+
*
|
|
2503
|
+
* The request is built HERE, against the snapshot and the pre-commit state, because
|
|
2504
|
+
* this is the last moment the fold's source is still exact active evidence: after the
|
|
2505
|
+
* commit its span is a placeholder and the orientation either side of it is gone.
|
|
2506
|
+
* These are the bytes `prepareFold` already hashed into the fold's identity, so the
|
|
2507
|
+
* generator briefs the span that was folded and nothing else.
|
|
2508
|
+
*
|
|
2509
|
+
* A PARENT is queued too, and reads its children at depth one rather than through their
|
|
2510
|
+
* briefs: `consolidationSourceText` opens each child and leaves the grandchildren folded,
|
|
2511
|
+
* so the group is briefed from material instead of from a summary of summaries. The one
|
|
2512
|
+
* thing it must not race is a child's own upgrade, so a parent whose children are still
|
|
2513
|
+
* in this lane is deferred to the next boundary instead of reading briefs about to be
|
|
2514
|
+
* replaced. Unlike a leaf, a parent's source keeps: its children are folds, and folds
|
|
2515
|
+
* stay resolvable after the commit that nested them.
|
|
2516
|
+
*/
|
|
2517
|
+
const queueBriefUpgrades = (
|
|
2518
|
+
snapshot: ActiveContextSnapshot,
|
|
2519
|
+
stateBeforeCommit: ActiveContextState,
|
|
2520
|
+
foldIds: readonly string[],
|
|
2521
|
+
): void => {
|
|
2522
|
+
if (!observedSummarize || !persistence.state) return;
|
|
2523
|
+
const queued = (id: string): boolean =>
|
|
2524
|
+
upgrades.queue.some((entry) => entry.members.some((member) => member.foldId === id));
|
|
2525
|
+
const pending = (id: string): boolean => upgrades.running.has(id) || queued(id) ||
|
|
2526
|
+
upgrades.ready.some((entry) => entry.foldId === id) ||
|
|
2527
|
+
upgrades.deferred.has(id);
|
|
2528
|
+
// Gathered before any packing, because a batch is a property of the whole commit and
|
|
2529
|
+
// not of the fold that happens to be read first.
|
|
2530
|
+
const gathered: Array<{
|
|
2531
|
+
foldId: string;
|
|
2532
|
+
sourceSha256: string;
|
|
2533
|
+
refs: EvidenceRef[];
|
|
2534
|
+
span: Record<string, unknown>;
|
|
2535
|
+
chars: number;
|
|
2536
|
+
parent: boolean;
|
|
2537
|
+
}> = [];
|
|
2538
|
+
// Parents that waited are considered first and at every boundary, so the wait ends as
|
|
2539
|
+
// soon as the children settle rather than whenever the ladder happens to build another.
|
|
2540
|
+
for (const foldId of [...upgrades.deferred, ...foldIds]) {
|
|
2541
|
+
const full = upgrades.queue.length >= MAX_BRIEF_UPGRADE_QUEUE;
|
|
2542
|
+
if (upgrades.failed.has(foldId) || upgrades.running.has(foldId) || queued(foldId) ||
|
|
2543
|
+
upgrades.ready.some((entry) => entry.foldId === foldId)) continue;
|
|
2544
|
+
const fold = persistence.state.folds.find((item) => item.id === foldId);
|
|
2545
|
+
// A supplied brief is agent judgment and outranks the generator; a model brief is
|
|
2546
|
+
// already what this lane exists to produce. A fold that is no longer there is no
|
|
2547
|
+
// longer owed anything, so a rolled-back parent leaves the wait rather than sitting
|
|
2548
|
+
// in it.
|
|
2549
|
+
if (!fold || foldProvenance(fold, persistence.state).kind !== "deterministic") {
|
|
2550
|
+
upgrades.deferred.delete(foldId);
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
const parent = fold.parts.some((part) => part.kind === "fold");
|
|
2554
|
+
// A parent waits rather than being shed: its children are folds, so its source is
|
|
2555
|
+
// still there whenever the lane has room, and a group that loses its turn at a busy
|
|
2556
|
+
// boundary would otherwise keep the index its children could not write. A leaf is the
|
|
2557
|
+
// other way round. Its source is exact active evidence at this commit and a
|
|
2558
|
+
// placeholder after it, so a full queue drops it here and the receipt says so.
|
|
2559
|
+
if (parent && (full || childFoldIds(fold).some(pending))) {
|
|
2560
|
+
upgrades.deferred.add(foldId);
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
if (full) continue;
|
|
2564
|
+
upgrades.deferred.delete(foldId);
|
|
2565
|
+
const refs = parent
|
|
2566
|
+
? flattenFoldRefs(fold, persistence.state)
|
|
2567
|
+
: fold.parts.every((part) => part.kind === "raw")
|
|
2568
|
+
? fold.parts.map((part) => (part as { kind: "raw"; ref: EvidenceRef }).ref)
|
|
2569
|
+
: null;
|
|
2570
|
+
if (!refs) continue;
|
|
2571
|
+
try {
|
|
2572
|
+
// The parent reads the state that holds its children and their upgraded briefs;
|
|
2573
|
+
// a leaf reads the state where its own span was still raw active evidence.
|
|
2574
|
+
const sourceText = parent
|
|
2575
|
+
? consolidationSourceText(snapshot, persistence.state, fold.parts)
|
|
2576
|
+
: encodedFoldSource(snapshot, stateBeforeCommit, fold.parts, fold.kind);
|
|
2577
|
+
const chars = bytes(sourceText);
|
|
2578
|
+
if (chars > snapshot.policy.maxSourceChars) continue;
|
|
2579
|
+
gathered.push({
|
|
2580
|
+
foldId,
|
|
2581
|
+
sourceSha256: fold.sourceSha256,
|
|
2582
|
+
refs,
|
|
2583
|
+
chars,
|
|
2584
|
+
parent,
|
|
2585
|
+
span: {
|
|
2586
|
+
candidateId: fold.id,
|
|
2587
|
+
sourceRefs: clone(refs),
|
|
2588
|
+
sourceText,
|
|
2589
|
+
sourceSha256: sha256Text(sourceText),
|
|
2590
|
+
// What the PAYLOAD holds, which is one entry per part: a consolidation can
|
|
2591
|
+
// carry absorbed raw evidence beside its folded children, and the coverage
|
|
2592
|
+
// rule counts the things the model is actually shown. Counting only the fold
|
|
2593
|
+
// children told it to write fewer clauses than the payload has subjects.
|
|
2594
|
+
children: parent ? fold.parts.length : 0,
|
|
2595
|
+
},
|
|
2596
|
+
});
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
// Evidence drift between the commit and this read leaves the deterministic brief
|
|
2599
|
+
// standing, and the next commit record reports the attempt rather than hiding it.
|
|
2600
|
+
upgrades.failed.add(foldId);
|
|
2601
|
+
upgrades.failures += 1;
|
|
2602
|
+
upgrades.lastError = boundReceiptText(
|
|
2603
|
+
error instanceof Error ? error.message : String(error), 240, "brief upgrade",
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
// Pack the commit's spans into as few calls as the source budget allows. Order is the
|
|
2608
|
+
// order they were gathered, which is span order within the commit, so the numbered
|
|
2609
|
+
// spans a batch shows the generator run the way the session did and one really is the
|
|
2610
|
+
// orientation for the next.
|
|
2611
|
+
for (let at = 0; at < gathered.length;) {
|
|
2612
|
+
if (upgrades.queue.length >= MAX_BRIEF_UPGRADE_QUEUE) {
|
|
2613
|
+
// No room for another call. A parent goes back to waiting, because its children are
|
|
2614
|
+
// folds and its source will still be there whenever the lane frees up. A leaf
|
|
2615
|
+
// cannot wait: its source is exact active evidence at THIS commit and a placeholder
|
|
2616
|
+
// after it, so it keeps the deterministic brief it committed with.
|
|
2617
|
+
for (const item of gathered.slice(at)) {
|
|
2618
|
+
if (item.parent) upgrades.deferred.add(item.foldId);
|
|
2619
|
+
}
|
|
2620
|
+
break;
|
|
2621
|
+
}
|
|
2622
|
+
const members: typeof gathered = [];
|
|
2623
|
+
let chars = 0;
|
|
2624
|
+
while (at < gathered.length && members.length < MAX_BRIEF_BATCH_SPANS &&
|
|
2625
|
+
(!members.length || chars + gathered[at].chars <= snapshot.policy.maxSourceChars)) {
|
|
2626
|
+
chars += gathered[at].chars;
|
|
2627
|
+
members.push(gathered[at]);
|
|
2628
|
+
at += 1;
|
|
2629
|
+
}
|
|
2630
|
+
// Orientation is the run's, not each span's: where this batch of consecutive spans
|
|
2631
|
+
// sits in the wider conversation. Taken from the ends rather than from the union,
|
|
2632
|
+
// because a commit can fold spans with protected material still raw between them and
|
|
2633
|
+
// the union of those refs is not one contiguous range.
|
|
2634
|
+
const before = boundedOrientation(snapshot, members[0].refs);
|
|
2635
|
+
const after = boundedOrientation(snapshot, members[members.length - 1].refs);
|
|
2636
|
+
upgrades.queue.push({
|
|
2637
|
+
members: members.map((item) => ({ foldId: item.foldId, sourceSha256: item.sourceSha256 })),
|
|
2638
|
+
request: {
|
|
2639
|
+
spans: members.map((item) => item.span),
|
|
2640
|
+
beforeRefs: clone(before.beforeRefs),
|
|
2641
|
+
beforeText: before.beforeText,
|
|
2642
|
+
beforeSha256: sha256Text(before.beforeText),
|
|
2643
|
+
afterRefs: clone(after.afterRefs),
|
|
2644
|
+
afterText: after.afterText,
|
|
2645
|
+
afterSha256: sha256Text(after.afterText),
|
|
2646
|
+
maxBriefChars: snapshot.policy.maxBriefChars,
|
|
2647
|
+
// Read by the observed wrapper and by nothing else, so a queue that backs up is
|
|
2648
|
+
// visible as waiting rather than as a slow generator. The summarizer contract is
|
|
2649
|
+
// on the RESULT, so an extra request field reaches it harmlessly.
|
|
2650
|
+
queuedAtMs: Date.now(),
|
|
2651
|
+
},
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* One generator call, between boundaries, cancel-safe. It writes nothing durable: a
|
|
2658
|
+
* finished brief waits in `ready` for a commit to carry it, and an aborted or failed
|
|
2659
|
+
* call leaves the fold exactly as it committed.
|
|
2660
|
+
*
|
|
2661
|
+
* Returns whether a call was started, which is what lets the drain below stop.
|
|
2662
|
+
*/
|
|
2663
|
+
const startBriefUpgrade = (snapshot: ActiveContextSnapshot, ctx: any): boolean => {
|
|
2664
|
+
const summarize = observedSummarize;
|
|
2665
|
+
// In CALLS, not folds. `running` is keyed by every fold a call briefs so the queue can
|
|
2666
|
+
// ask whether one is already being written, so its size is a fold count and reading it
|
|
2667
|
+
// as the in-flight bound would stop the lane after a single batch.
|
|
2668
|
+
const inFlight = new Set(upgrades.running.values()).size;
|
|
2669
|
+
if (!summarize || lifecycle.shuttingDown || !upgrades.queue.length ||
|
|
2670
|
+
inFlight >= MAX_BRIEF_UPGRADES_IN_FLIGHT) return false;
|
|
2671
|
+
const entry = upgrades.queue.shift()!;
|
|
2672
|
+
const controller = new AbortController();
|
|
2673
|
+
const slot = { controller, promise: Promise.resolve() };
|
|
2674
|
+
// Keyed by EVERY fold the call briefs, so the queue's "already being briefed" check and
|
|
2675
|
+
// the in-flight bound both read one call as the one thing it is.
|
|
2676
|
+
for (const member of entry.members) upgrades.running.set(member.foldId, slot);
|
|
2677
|
+
const capturedSessionId = snapshot.sessionId;
|
|
2678
|
+
const capturedGeneration = lifecycle.generation;
|
|
2679
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
2680
|
+
slot.promise = (async () => {
|
|
2681
|
+
const timed = new Promise<never>((_resolve, reject) => {
|
|
2682
|
+
timeout = setTimeout(() => {
|
|
2683
|
+
controller.abort();
|
|
2684
|
+
reject(new Error(`Brief upgrade exceeded ${snapshot.policy.briefTimeoutMs}ms`));
|
|
2685
|
+
}, snapshot.policy.briefTimeoutMs);
|
|
2686
|
+
});
|
|
2687
|
+
// The same contract `prepareFold` holds a generated brief to, through the same
|
|
2688
|
+
// function: an upgrade with its own copy of the check would drift from the fold
|
|
2689
|
+
// path's, and the cure a missed criterion earns belongs to both or to neither.
|
|
2690
|
+
const generated = await Promise.race([
|
|
2691
|
+
generatedBriefs({
|
|
2692
|
+
summarize: (request, callCtx) => summarize({ ...request, signal: controller.signal }, callCtx),
|
|
2693
|
+
request: entry.request,
|
|
2694
|
+
spans: entry.request.spans as Array<Record<string, unknown>>,
|
|
2695
|
+
ctx,
|
|
2696
|
+
maxBriefChars: snapshot.policy.maxBriefChars,
|
|
2697
|
+
toolName: snapshot.toolName,
|
|
2698
|
+
}),
|
|
2699
|
+
timed,
|
|
2700
|
+
]);
|
|
2701
|
+
if (controller.signal.aborted ||
|
|
2702
|
+
!sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration)) return;
|
|
2703
|
+
// Per span, not per call. A batch is one request and many answers, so one span that
|
|
2704
|
+
// could not be cured is one fold keeping its deterministic brief while its neighbours
|
|
2705
|
+
// take theirs; the whole call failing on it would be the coverage problem batching
|
|
2706
|
+
// exists to solve, wearing a different hat.
|
|
2707
|
+
entry.members.forEach((member, at) => {
|
|
2708
|
+
const written = generated.briefs[at];
|
|
2709
|
+
if (!written) {
|
|
2710
|
+
upgrades.failed.add(member.foldId);
|
|
2711
|
+
upgrades.failures += 1;
|
|
2712
|
+
upgrades.lastError = boundReceiptText(
|
|
2713
|
+
`span ${at + 1} of ${entry.members.length}: ${generated.complaints[0] ?? "no brief"}`,
|
|
2714
|
+
240, "brief upgrade",
|
|
2715
|
+
);
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
upgrades.ready.push({
|
|
2719
|
+
foldId: member.foldId,
|
|
2720
|
+
sourceSha256: member.sourceSha256,
|
|
2721
|
+
brief: written.brief,
|
|
2722
|
+
provenance: written.provenance,
|
|
2723
|
+
});
|
|
2724
|
+
});
|
|
2725
|
+
upgrades.cures += generated.cured;
|
|
2726
|
+
})().catch((error) => {
|
|
2727
|
+
if (controller.signal.aborted) return;
|
|
2728
|
+
// LOUD, and once, for every fold the call carried. They keep their deterministic
|
|
2729
|
+
// briefs, the failure is counted onto the next commit record, and none of them is
|
|
2730
|
+
// ever sent again: a generator that fails on a span fails on it again, and a retry
|
|
2731
|
+
// loop would spend the session's provider calls proving that.
|
|
2732
|
+
for (const member of entry.members) upgrades.failed.add(member.foldId);
|
|
2733
|
+
upgrades.failures += 1;
|
|
2734
|
+
upgrades.lastError = boundReceiptText(
|
|
2735
|
+
error instanceof Error ? error.message : String(error), 240, "brief upgrade",
|
|
2736
|
+
);
|
|
2737
|
+
}).finally(() => {
|
|
2738
|
+
clearTimeout(timeout);
|
|
2739
|
+
for (const member of entry.members) {
|
|
2740
|
+
if (upgrades.running.get(member.foldId) === slot) upgrades.running.delete(member.foldId);
|
|
2741
|
+
}
|
|
2742
|
+
// A finished call starts the next one, so the drain rate is the generator's latency
|
|
2743
|
+
// rather than however often a ladder pass happens to look. Still between
|
|
2744
|
+
// boundaries: nothing here touches the projection, and the generation check is the
|
|
2745
|
+
// same fence `cancelBriefUpgrades` is called behind, so a session that moved on
|
|
2746
|
+
// cannot be restarted by a call that outlived it.
|
|
2747
|
+
if (!lifecycle.shuttingDown && lifecycle.generation === capturedGeneration) {
|
|
2748
|
+
resumeBriefUpgrades(snapshot, ctx);
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
return true;
|
|
2752
|
+
};
|
|
2753
|
+
|
|
2754
|
+
/**
|
|
2755
|
+
* Fill the in-flight slots from the queue. The bound is a constant, not a pace: the
|
|
2756
|
+
* lane starts everything it is allowed to start the moment there is work, because a
|
|
2757
|
+
* fold whose brief is written late is a fold the session read deterministic in the
|
|
2758
|
+
* meantime.
|
|
2759
|
+
*/
|
|
2760
|
+
const startBriefUpgrades = (snapshot: ActiveContextSnapshot, ctx: any): void => {
|
|
2761
|
+
while (startBriefUpgrade(snapshot, ctx)) { /* Bounded by the in-flight constant. */ }
|
|
2762
|
+
};
|
|
2763
|
+
|
|
2764
|
+
/**
|
|
2765
|
+
* Take up the waiting parents, then fill the slots.
|
|
2766
|
+
*
|
|
2767
|
+
* A deferred parent was waiting on the lane, so the lane finishing is the news, not the
|
|
2768
|
+
* next commit. Boundaries are where briefs APPLY; hanging the retry on them too would
|
|
2769
|
+
* leave a session that stops folding holding parents whose children were briefed long
|
|
2770
|
+
* ago, and the last group the ladder built is exactly the one a session ends beside.
|
|
2771
|
+
*/
|
|
2772
|
+
const resumeBriefUpgrades = (snapshot: ActiveContextSnapshot, ctx: any): void => {
|
|
2773
|
+
if (upgrades.deferred.size && persistence.state) {
|
|
2774
|
+
queueBriefUpgrades(snapshot, persistence.state, []);
|
|
2775
|
+
}
|
|
2776
|
+
startBriefUpgrades(snapshot, ctx);
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2779
|
+
const cancelBriefUpgrades = (): void => {
|
|
2780
|
+
for (const slot of upgrades.running.values()) slot.controller.abort();
|
|
2781
|
+
upgrades.running.clear();
|
|
2782
|
+
upgrades.queue = [];
|
|
2783
|
+
upgrades.ready = [];
|
|
2784
|
+
upgrades.deferred.clear();
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
/**
|
|
2788
|
+
* Apply finished upgrades INSIDE the commit's own mutation.
|
|
2789
|
+
*
|
|
2790
|
+
* Called between `commitPendingMarks` and the byte accounting, so an upgraded brief is
|
|
2791
|
+
* part of what this commit is measured to have done rather than a rewrite nobody
|
|
2792
|
+
* charged for. Nothing else may call this: an upgrade that finishes between boundaries
|
|
2793
|
+
* waits, however long the wait is.
|
|
2794
|
+
*
|
|
2795
|
+
* Identity is revalidated the way `preparedFoldError` revalidates a prepared fold. A
|
|
2796
|
+
* fold that was expanded, reclaimed, re-briefed by the agent, or rebuilt over a
|
|
2797
|
+
* different span is a different object than the one the generator read, and its upgrade
|
|
2798
|
+
* is DROPPED rather than deferred.
|
|
2799
|
+
*
|
|
2800
|
+
* It lands in the brief OVERRIDE map, the same channel the agent's own `rebrief` uses,
|
|
2801
|
+
* and carries its generator beside the text. A fold record is content-addressed and
|
|
2802
|
+
* immutable: rewriting one in place reports a conflicting durable fold at the next
|
|
2803
|
+
* persist and suspends automatic management, which is precisely why the override map
|
|
2804
|
+
* exists.
|
|
2805
|
+
*
|
|
2806
|
+
* EVERY finished brief lands, and there is no per-boundary cap. A prefix cache is
|
|
2807
|
+
* invalidated from the FIRST byte that differs, so the commit's mutation point is the
|
|
2808
|
+
* position of the earliest fold it upgrades and every later upgrade in the same commit
|
|
2809
|
+
* sits inside a suffix that break already re-sent: the second and third upgrade on one
|
|
2810
|
+
* boundary cost nothing the first did not already cost. Holding one back does not save
|
|
2811
|
+
* that cost, it defers the same break to a later commit and pays it there on top of
|
|
2812
|
+
* whatever that commit does. What bounds the count is the work that can be outstanding,
|
|
2813
|
+
* MAX_BRIEF_UPGRADE_QUEUE waiting plus MAX_BRIEF_UPGRADES_IN_FLIGHT running, and the
|
|
2814
|
+
* boundary that clears them.
|
|
2815
|
+
*/
|
|
2816
|
+
const applyBriefUpgrades = (): string[] => {
|
|
2817
|
+
if (!upgrades.ready.length || !persistence.state) return [];
|
|
2818
|
+
const applied: string[] = [];
|
|
2819
|
+
for (const entry of upgrades.ready) {
|
|
2820
|
+
const state = persistence.state;
|
|
2821
|
+
const fold = state.folds.find((item) => item.id === entry.foldId);
|
|
2822
|
+
if (!fold || fold.sourceSha256 !== entry.sourceSha256 ||
|
|
2823
|
+
foldProvenance(fold, state).kind !== "deterministic" ||
|
|
2824
|
+
state.expanded.includes(fold.id) || state.briefs?.[fold.id] !== undefined) continue;
|
|
2825
|
+
persistence.state = {
|
|
2826
|
+
...state,
|
|
2827
|
+
briefs: {
|
|
2828
|
+
...(state.briefs ?? {}),
|
|
2829
|
+
[fold.id]: { brief: entry.brief, provenance: clone(entry.provenance) },
|
|
2830
|
+
},
|
|
2831
|
+
};
|
|
2832
|
+
applied.push(fold.id);
|
|
2833
|
+
}
|
|
2834
|
+
upgrades.ready = [];
|
|
2835
|
+
return applied;
|
|
2836
|
+
};
|
|
2837
|
+
|
|
1873
2838
|
const commitDeterministicCandidate = async (
|
|
1874
2839
|
snapshot: ActiveContextSnapshot,
|
|
1875
2840
|
candidate: FoldCandidate,
|
|
1876
2841
|
brief: string,
|
|
1877
2842
|
): Promise<string> => {
|
|
2843
|
+
const stateBeforeCommit = persistence.state!;
|
|
1878
2844
|
const preparedFold = await prepareFold({
|
|
1879
2845
|
candidate,
|
|
1880
2846
|
snapshot,
|
|
1881
|
-
state:
|
|
2847
|
+
state: stateBeforeCommit,
|
|
1882
2848
|
generation: lifecycle.generation,
|
|
1883
2849
|
brief,
|
|
1884
2850
|
briefProvenance: "deterministic",
|
|
@@ -1886,9 +2852,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1886
2852
|
persistence.state = commitPreparedFold({
|
|
1887
2853
|
prepared: preparedFold,
|
|
1888
2854
|
snapshot,
|
|
1889
|
-
state:
|
|
2855
|
+
state: stateBeforeCommit,
|
|
1890
2856
|
generation: lifecycle.generation,
|
|
1891
2857
|
});
|
|
2858
|
+
queueBriefUpgrades(snapshot, stateBeforeCommit, [preparedFold.id]);
|
|
1892
2859
|
return preparedFold.id;
|
|
1893
2860
|
};
|
|
1894
2861
|
|
|
@@ -1913,7 +2880,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1913
2880
|
state: baseState,
|
|
1914
2881
|
generation: generationAtStart,
|
|
1915
2882
|
brief: input.brief,
|
|
1916
|
-
summarize:
|
|
2883
|
+
summarize: observedSummarize,
|
|
1917
2884
|
ctx: input.ctx,
|
|
1918
2885
|
signal: input.signal,
|
|
1919
2886
|
});
|
|
@@ -1952,36 +2919,77 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1952
2919
|
return Math.max(COMMIT_RECLAIM_FLOOR_SHARE, inflow > 0 ? inflow / window : 0);
|
|
1953
2920
|
};
|
|
1954
2921
|
|
|
1955
|
-
const atOrAboveBackstop = (snapshot: ActiveContextSnapshot, ratio: number | null): boolean =>
|
|
1956
|
-
|
|
2922
|
+
const atOrAboveBackstop = (snapshot: ActiveContextSnapshot, ratio: number | null): boolean => {
|
|
2923
|
+
const occupancy = budgetOccupancy(snapshot, ratio);
|
|
2924
|
+
return occupancy !== null && occupancy >= snapshot.policy.refoldRatio;
|
|
2925
|
+
};
|
|
1957
2926
|
|
|
1958
2927
|
/**
|
|
1959
|
-
*
|
|
2928
|
+
* THE OUTPUT WALL.
|
|
2929
|
+
*
|
|
2930
|
+
* A declared serving budget has already netted out the reservation the answer needs,
|
|
2931
|
+
* so the runtime treats the whole budget as spendable and fences only when it is
|
|
2932
|
+
* spent. That fence is the input one, and the input is not what fails first. The host
|
|
2933
|
+
* derives the answer's ceiling per request as its DESCRIPTOR window less its own
|
|
2934
|
+
* estimate of the projection less a fixed safety margin, so filling the declared
|
|
2935
|
+
* budget to the brim leaves the answer exactly the reservation and nothing else.
|
|
1960
2936
|
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
*
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
2937
|
+
* Which is a fence that fires after the wire, the same defect the transmission fence
|
|
2938
|
+
* was given a forward look to fix in rep 13. The commit trigger never got one.
|
|
2939
|
+
* Measured 2026-08-12 (sol-20260812 rep 1): occupancy walked 218,713 -> 236,595 over
|
|
2940
|
+
* twelve ordinals against a 251,520 budget with no commit, and the request built after
|
|
2941
|
+
* the last measurement was served 16 output tokens and aborted. Every number was known
|
|
2942
|
+
* to the runtime; nothing was weighing the request that had not been built yet.
|
|
1967
2943
|
*
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
2944
|
+
* So the trigger carries what the transmission fence already carries: one worst recent
|
|
2945
|
+
* inflow step. With no measured inflow this is exactly the old `used > budget`
|
|
2946
|
+
* condition, which is the state it degrades to on a session's first turns.
|
|
2947
|
+
*/
|
|
2948
|
+
const outputWallDue = (snapshot: ActiveContextSnapshot): boolean => {
|
|
2949
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
2950
|
+
if (capacity.usedTokens === null || !(capacity.budgetTokens > 0)) return false;
|
|
2951
|
+
// THE EARLY ZONE ONLY, so this is strictly additive.
|
|
2952
|
+
//
|
|
2953
|
+
// Past the budget the pre-existing overflow paths already own the emergency: the
|
|
2954
|
+
// commit-path exemption, the last-call gate and the fence all read `used > budget`
|
|
2955
|
+
// and have since before this existed. A second trigger in that territory changes the
|
|
2956
|
+
// cadence of sessions that were already being handled, and gate 107 measured what
|
|
2957
|
+
// that costs: a fixture parked at 88,000 against an 83,616 budget put four generator
|
|
2958
|
+
// calls in flight against a bound of two. So the wall speaks only where nothing spoke
|
|
2959
|
+
// before, in the band one inflow step wide below the budget.
|
|
2960
|
+
if (capacity.usedTokens > capacity.budgetTokens) return false;
|
|
2961
|
+
return capacity.usedTokens + expectedWallInflowTokens() > capacity.budgetTokens;
|
|
2962
|
+
};
|
|
2963
|
+
|
|
2964
|
+
/**
|
|
2965
|
+
* What the gated round did, measured against the arming snapshot. Pins and unpins
|
|
2966
|
+
* are read from the stream itself (context.protect records after the exposure); the
|
|
2967
|
+
* call and mark deltas are clamped at zero because contextCalls is process-local and
|
|
2968
|
+
* a reload inside a round must not report a negative response.
|
|
1970
2969
|
*/
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2970
|
+
const lastCallAttribution = (
|
|
2971
|
+
lastCall: NonNullable<ActiveContextState["lastCall"]>,
|
|
2972
|
+
agentMarksNow: number,
|
|
2973
|
+
): { responded: boolean; context_calls: number; marks_added: number; protects: number; unprotects: number } => {
|
|
2974
|
+
const contextCalls = Math.max(0, curation.contextCalls - lastCall.contextCalls);
|
|
2975
|
+
const protectRecords = instrumentation.ledger.events.filter((record) =>
|
|
2976
|
+
record.seq > lastCall.exposure && record.kind === "context.protect");
|
|
2977
|
+
const protects = protectRecords.filter((record) => record.protect === true).length;
|
|
2978
|
+
return {
|
|
2979
|
+
responded: contextCalls > 0,
|
|
2980
|
+
context_calls: contextCalls,
|
|
2981
|
+
marks_added: Math.max(0, agentMarksNow - lastCall.agentMarks),
|
|
2982
|
+
protects,
|
|
2983
|
+
unprotects: protectRecords.length - protects,
|
|
1979
2984
|
};
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2985
|
+
};
|
|
2986
|
+
|
|
2987
|
+
const clearLastCall = (): void => {
|
|
2988
|
+
if (!persistence.state?.lastCall) return;
|
|
2989
|
+
const next = { ...persistence.state };
|
|
2990
|
+
delete next.lastCall;
|
|
2991
|
+
persistence.state = next;
|
|
2992
|
+
curation.lastCallDelivery = null;
|
|
1985
2993
|
};
|
|
1986
2994
|
|
|
1987
2995
|
const runCommitEpoch = async (
|
|
@@ -1993,44 +3001,85 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1993
3001
|
* over-budget reduction forces the fence so nothing survivable is held back.
|
|
1994
3002
|
*/
|
|
1995
3003
|
waiverRatio: number | null = measurements.latestRatio,
|
|
3004
|
+
/**
|
|
3005
|
+
* A user asked for this commit. It outranks the two ECONOMIC guards -- the reclaim
|
|
3006
|
+
* floor and the one-mutation-per-handoff budget -- because those exist to spend the
|
|
3007
|
+
* session's rewrites well and the user just said where to spend one. It outranks
|
|
3008
|
+
* nothing else: freshness, pins, exactness and the provider fence are unchanged.
|
|
3009
|
+
*/
|
|
3010
|
+
userRequested = false,
|
|
1996
3011
|
): Promise<Record<string, unknown> | null> => {
|
|
1997
3012
|
const ordinal = markOrdinal(snapshot);
|
|
1998
3013
|
let state = persistence.state!;
|
|
1999
3014
|
let peekAdded = 0;
|
|
2000
3015
|
let topUpAdded = 0;
|
|
2001
|
-
let
|
|
2002
|
-
let
|
|
2003
|
-
let preDeepenShare: number | null = null;
|
|
2004
|
-
// The snapshot the commit adjudicates against: the deepened one narrows freshness
|
|
2005
|
-
// so the marks the deep top-up just made are not refused as fresh.
|
|
2006
|
-
let commitSnapshot = snapshot;
|
|
3016
|
+
let consolidationAdded = 0;
|
|
3017
|
+
let closingAdded = 0;
|
|
2007
3018
|
for (const mark of ephemeralPeekMarks({ snapshot, state, ordinal })) {
|
|
2008
3019
|
const addition = addPendingMark(state, mark);
|
|
2009
3020
|
if (addition.added) { state = addition.state; peekAdded += 1; }
|
|
2010
3021
|
}
|
|
3022
|
+
// THE COUNT LAW RUNS AT EVERY COMMIT EPOCH, BEFORE ANYTHING ELSE PROPOSES.
|
|
3023
|
+
//
|
|
3024
|
+
// The number of parents the eligible-root count owes is arithmetic, not a judgment
|
|
3025
|
+
// about pressure, so it is not the top-up's to make and not the user commit's to
|
|
3026
|
+
// skip: at any point the count allows k groups, k parents form. Running first is
|
|
3027
|
+
// what makes the rest coherent -- the groups claim their children before the top-up
|
|
3028
|
+
// reads the claim set, so nothing proposes a span over a fold a parent is taking.
|
|
3029
|
+
for (const mark of consolidationMarks({ snapshot, state, ordinal })) {
|
|
3030
|
+
const addition = addPendingMark(state, mark);
|
|
3031
|
+
if (addition.added) { state = addition.state; consolidationAdded += 1; }
|
|
3032
|
+
}
|
|
2011
3033
|
const guarded = currentTurnRefKeys(snapshot);
|
|
2012
|
-
//
|
|
2013
|
-
//
|
|
2014
|
-
//
|
|
2015
|
-
//
|
|
2016
|
-
|
|
2017
|
-
|
|
3034
|
+
// THE GUARD IS ADJUDICATED AT COMMIT, NEVER AT PROPOSAL.
|
|
3035
|
+
//
|
|
3036
|
+
// `guarded` is every tool result since the last terminal assistant message. When no
|
|
3037
|
+
// turn has ever closed, `currentTurnBoundary` is -1 and that set is the WHOLE
|
|
3038
|
+
// window. Handing it to `topUpMarks` as `excludeRefKeys` therefore proposed nothing
|
|
3039
|
+
// at all on that shape, so `guardWaiverCount` below counted zero guarded marks and
|
|
3040
|
+
// the waiver it exists to grant could never fire. Measured 2026-08-10
|
|
3041
|
+
// (luna-20260810 pifold rep 2): 274,173 tokens of unmarked stale spans, zero marks
|
|
3042
|
+
// proposed, zero commits, two provider rejections.
|
|
3043
|
+
//
|
|
3044
|
+
// So the top-up proposes freely and `commitPendingMarks({ guardCurrentTurn: true,
|
|
3045
|
+
// guardWaiver })` decides. That is the only place the guard has ever had a waiver,
|
|
3046
|
+
// and one adjudicator is why the starvation case is reachable at all.
|
|
3047
|
+
//
|
|
3048
|
+
// Wedge absorption keeps the exclusion (below): it grows a mark backward over a
|
|
3049
|
+
// gap without any waiver of its own, so the guard there has no second chance.
|
|
3050
|
+
// THE CLASS LAW IS UNCONDITIONAL.
|
|
3051
|
+
//
|
|
3052
|
+
// There was a fence-only snapshot here: at high occupancy it narrowed the fresh
|
|
3053
|
+
// tail to a quarter and extended the automatic reach over the middle, so a reduction
|
|
3054
|
+
// that had to make a rejected request sendable had mass to reach. That existed
|
|
3055
|
+
// because the runtime had exactly one answer to a provider rejection, folding
|
|
3056
|
+
// harder, and folding harder inside a positional region runs out of legal material.
|
|
3057
|
+
// The rollback lane is the answer now: an overflow rolls the leaf back past the
|
|
3058
|
+
// request that failed and the ordinary commit runs on the shorter window. So the
|
|
3059
|
+
// law holds in EVERY snapshot at every occupancy -- membership decides what folds,
|
|
3060
|
+
// the fresh tail never folds, pins are exempt -- and there is one set of rules to
|
|
3061
|
+
// reason about instead of two.
|
|
2018
3062
|
// The thermostat. Firing at the trigger line and folding down to the target line is
|
|
2019
3063
|
// what makes event SPACING structural rather than hoped for.
|
|
2020
3064
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
2021
3065
|
const usedTokens = capacity.usedTokens;
|
|
2022
3066
|
const budgetTokens = capacity.budgetTokens;
|
|
2023
|
-
// THE
|
|
3067
|
+
// THE UNTRANSMISSIBILITY EXEMPTION, NARROWED.
|
|
2024
3068
|
//
|
|
2025
|
-
//
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
3069
|
+
// A request whose projection exceeds the provider input budget is rejected outright,
|
|
3070
|
+
// so recovery must produce a window that fits. The hard fence, though, is a RATIO
|
|
3071
|
+
// prediction. Standing near it is not the same as being unable to send, and treating
|
|
3072
|
+
// the two alike handed the fence path a standing waiver from both economy guards. Measured 2026-08-07 (rep 17): fence-path
|
|
2028
3073
|
// window-pressure commits fired at eligible-freed shares as low as 0.018, under the
|
|
2029
3074
|
// 0.02 floor the guided path honors, and re-fired inside a single ordinal. So only
|
|
2030
3075
|
// two states outrank the economy: the overflow recovery lane, which runs because a
|
|
2031
3076
|
// request already did not fit, and an occupancy genuinely past the serving budget,
|
|
2032
3077
|
// where the next request aborts unless something moves. Everything else defers.
|
|
2033
|
-
|
|
3078
|
+
// The output wall deliberately does NOT join this list. It decides WHEN the one
|
|
3079
|
+
// structural mutation of a handoff fires, not how many fire: exempting it here let a
|
|
3080
|
+
// window parked one inflow step under the budget rebuild its prefix on every pass,
|
|
3081
|
+
// which is the cost the mutation budget exists to refuse and what gate 58 caught.
|
|
3082
|
+
const overflowExempt = userRequested || curation.recoveryAttempts > 0 ||
|
|
2034
3083
|
(usedTokens !== null && budgetTokens > 0 && usedTokens > budgetTokens);
|
|
2035
3084
|
// ONE STRUCTURAL MUTATION PER HANDOFF.
|
|
2036
3085
|
//
|
|
@@ -2040,8 +3089,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2040
3089
|
// still CROWDED (inside the fence margin, but transmissible), and committed again.
|
|
2041
3090
|
// The second commit bought margin the next pass would have bought anyway and cost a
|
|
2042
3091
|
// whole second prefix rewrite. So a second commit in the same handoff defers unless
|
|
2043
|
-
// the request is genuinely untransmittable:
|
|
2044
|
-
//
|
|
3092
|
+
// the request is genuinely untransmittable: a request whose projection exceeds the
|
|
3093
|
+
// provider input budget is rejected outright, so recovery must produce a window that
|
|
3094
|
+
// fits, and the fence and the recovery lane spend the budget to get one.
|
|
2045
3095
|
if (instrumentation.mutationsSinceHandoff > 0 && !overflowExempt) {
|
|
2046
3096
|
emit("context.commit", {
|
|
2047
3097
|
trigger,
|
|
@@ -2053,11 +3103,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2053
3103
|
});
|
|
2054
3104
|
return null;
|
|
2055
3105
|
}
|
|
2056
|
-
|
|
3106
|
+
// THE HYSTERESIS, AND THE WHOLE FREEING TARGET.
|
|
3107
|
+
//
|
|
3108
|
+
// How deep this event cuts is one subtraction on one denominator: what is used,
|
|
3109
|
+
// less where the thermostat wants to land, over the serving budget. The separate
|
|
3110
|
+
// 0.40 floor is gone -- it was a share of the WINDOW under a share of the BUDGET,
|
|
3111
|
+
// so it never once bound (the hysteresis share at these thresholds bottoms out at
|
|
3112
|
+
// 0.405 of window), and a floor that cannot fire is a number that only drifts.
|
|
3113
|
+
const freeingTarget = usedTokens === null || budgetTokens <= 0
|
|
2057
3114
|
? 0
|
|
2058
|
-
: Math.max(0, (usedTokens -
|
|
2059
|
-
snapshot.contextWindow);
|
|
2060
|
-
const freeingTarget = Math.max(EPOCH_COMMIT_TARGET_WINDOW_SHARE, hysteresisShare);
|
|
3115
|
+
: Math.max(0, (usedTokens - thresholds.minTarget * budgetTokens) / budgetTokens);
|
|
2061
3116
|
if (topUp) {
|
|
2062
3117
|
// Measuring top-up progress against ELIGIBLE mass is what keeps the pressure
|
|
2063
3118
|
// backstop working under a peek-heavy agent: pinned and retained marks are mass
|
|
@@ -2066,41 +3121,35 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2066
3121
|
snapshot,
|
|
2067
3122
|
state,
|
|
2068
3123
|
ordinal,
|
|
2069
|
-
excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
|
|
2070
3124
|
eligibleOnly: true,
|
|
2071
|
-
|
|
2072
|
-
// stops at the ordinary target can leave the request still untransmittable.
|
|
2073
|
-
targetShare: fenceLevel ? 1 : freeingTarget,
|
|
3125
|
+
targetShare: freeingTarget,
|
|
2074
3126
|
})) {
|
|
2075
3127
|
const addition = addPendingMark(state, mark);
|
|
2076
3128
|
if (addition.added) { state = addition.state; topUpAdded += 1; }
|
|
2077
3129
|
}
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
//
|
|
2085
|
-
//
|
|
2086
|
-
//
|
|
2087
|
-
|
|
2088
|
-
//
|
|
2089
|
-
|
|
2090
|
-
const shallow =
|
|
2091
|
-
if (
|
|
2092
|
-
deepened = true;
|
|
2093
|
-
commitSnapshot = deepenedFreshnessSnapshot(snapshot);
|
|
3130
|
+
// DEPTH, inside the zones.
|
|
3131
|
+
//
|
|
3132
|
+
// Near the top of the window a commit that frees less than one turn of inflow
|
|
3133
|
+
// does not reduce anything: it pays a full prefix rewrite, gives back less than
|
|
3134
|
+
// the next stage adds, and the window ratchets UP through commit after commit.
|
|
3135
|
+
// Measured 2026-08-06 (rep13): once the big stale mass was folded, six commits of
|
|
3136
|
+
// two to four folds each carried the window from 340k to 370k and into a provider
|
|
3137
|
+
// rejection, with the backstop firing the whole way. So a commit at or above the
|
|
3138
|
+
// backstop that has not reached one inflow step tops up against everything
|
|
3139
|
+
// ELIGIBLE instead of billing a rewrite for crumbs. Eligible is the operative
|
|
3140
|
+
// word: this reaches harder inside what the class law admits, never outside it.
|
|
3141
|
+
const reachedShare = markAccounting(snapshot, state).eligibleFreedBudgetShare;
|
|
3142
|
+
const shallow = reachedShare < Math.max(commitDepthFloorShare(snapshot), freeingTarget);
|
|
3143
|
+
if (shallow && atOrAboveBackstop(snapshot, waiverRatio)) {
|
|
2094
3144
|
for (const mark of topUpMarks({
|
|
2095
|
-
snapshot
|
|
3145
|
+
snapshot,
|
|
2096
3146
|
state,
|
|
2097
3147
|
ordinal,
|
|
2098
|
-
excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
|
|
2099
3148
|
eligibleOnly: true,
|
|
2100
3149
|
targetShare: 1,
|
|
2101
3150
|
})) {
|
|
2102
3151
|
const addition = addPendingMark(state, mark);
|
|
2103
|
-
if (addition.added) { state = addition.state; topUpAdded += 1;
|
|
3152
|
+
if (addition.added) { state = addition.state; topUpAdded += 1; }
|
|
2104
3153
|
}
|
|
2105
3154
|
}
|
|
2106
3155
|
}
|
|
@@ -2111,18 +3160,44 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2111
3160
|
snapshot,
|
|
2112
3161
|
state,
|
|
2113
3162
|
charsPerToken: projectionCharsPerToken(),
|
|
2114
|
-
excludeRefKeys:
|
|
3163
|
+
excludeRefKeys: guarded,
|
|
2115
3164
|
});
|
|
2116
3165
|
state = wedges.state;
|
|
2117
3166
|
const accounting = markAccounting(snapshot, state);
|
|
2118
|
-
|
|
3167
|
+
// A COMMIT PASS THAT FINDS NOTHING SAYS SO.
|
|
3168
|
+
//
|
|
3169
|
+
// This was a bare `return null`, and it is how the rep-2 starvation stayed invisible
|
|
3170
|
+
// for 25 stages: the last call announced 274,173 tokens of unmarked stale spans, the
|
|
3171
|
+
// pass that answered it proposed nothing, and the stream carried no record that a
|
|
3172
|
+
// commit had even been attempted. Every other deferral on this path already names
|
|
3173
|
+
// itself; this one is the same shape with the reason that matters, and it reports
|
|
3174
|
+
// the remainder beside the emptiness so the contradiction -- mass on the table, no
|
|
3175
|
+
// proposal -- is readable on one record instead of inferred across two.
|
|
3176
|
+
//
|
|
3177
|
+
// Stream record, not a `context.receipt`: receipts are window carriers, and a pass
|
|
3178
|
+
// that changed nothing must not add bytes to the projection to announce it.
|
|
3179
|
+
if (!accounting.pending) {
|
|
3180
|
+
const remainder = unmarkedRemainder(snapshot, state, projectionCharsPerToken());
|
|
3181
|
+
emit("context.commit", {
|
|
3182
|
+
trigger,
|
|
3183
|
+
deferred: true,
|
|
3184
|
+
reason: "nothing-proposable",
|
|
3185
|
+
applied_marks: 0,
|
|
3186
|
+
pending_marks: 0,
|
|
3187
|
+
unmarked_stale_spans: remainder.spans,
|
|
3188
|
+
unmarked_stale_tokens: remainder.tokens,
|
|
3189
|
+
window_tokens: snapshot.contextWindow,
|
|
3190
|
+
});
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
2119
3193
|
// The reclaim floor. One structural mutation per model call is the budget, so a
|
|
2120
3194
|
// commit that would free crumbs spends the whole budget on nothing: it defers and
|
|
2121
|
-
// the marks accumulate.
|
|
2122
|
-
//
|
|
3195
|
+
// the marks accumulate. A request whose projection exceeds the provider input budget
|
|
3196
|
+
// is rejected outright, so recovery must produce a window that fits: genuine overflow
|
|
3197
|
+
// -- the recovery lane, or an occupancy already past the serving budget -- fires
|
|
2123
3198
|
// regardless of what it frees. Standing at the fence RATIO does not.
|
|
2124
3199
|
if (!overflowExempt &&
|
|
2125
|
-
accounting.
|
|
3200
|
+
accounting.eligibleFreedBudgetShare < COMMIT_RECLAIM_FLOOR_SHARE) {
|
|
2126
3201
|
persistence.state = state;
|
|
2127
3202
|
emit("context.commit", {
|
|
2128
3203
|
trigger,
|
|
@@ -2131,7 +3206,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2131
3206
|
applied_marks: 0,
|
|
2132
3207
|
pending_marks: accounting.pending,
|
|
2133
3208
|
eligible_marks: accounting.eligibleMarks,
|
|
2134
|
-
eligible_freed_share: accounting.
|
|
3209
|
+
eligible_freed_share: accounting.eligibleFreedBudgetShare,
|
|
2135
3210
|
reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2136
3211
|
window_tokens: snapshot.contextWindow,
|
|
2137
3212
|
});
|
|
@@ -2140,51 +3215,101 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2140
3215
|
const bytesBefore = bytes(projectActiveContext(snapshot, state));
|
|
2141
3216
|
// The guard protects an in-flight excursion, never at the cost of the session's
|
|
2142
3217
|
// ability to send a request at all: above the pressure backstop the oldest guarded
|
|
2143
|
-
// marks are released
|
|
3218
|
+
// marks are released. The guard is a protection with a WAIVER, which is why it
|
|
3219
|
+
// survived the fence-snapshot deletion: one protection, one owner, one waiver.
|
|
2144
3220
|
const guardWaiver = guardWaiverCount({
|
|
2145
3221
|
snapshot,
|
|
2146
3222
|
ratio: waiverRatio,
|
|
2147
3223
|
guardedMarks: pendingMarks(state).filter((mark) =>
|
|
2148
3224
|
markTouchesCurrentTurn(state, mark, guarded)).length,
|
|
2149
|
-
//
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2152
|
-
//
|
|
2153
|
-
// weighing them under the shallow snapshot reported zero applicable marks while
|
|
2154
|
-
// the commit went on to apply two. The waiver then fired on a false starvation
|
|
2155
|
-
// reading and released eight open-turn marks, six of which folded evidence the
|
|
2156
|
-
// turn was still using. Two snapshots, two answers, and the one that decides the
|
|
2157
|
-
// commit was not the one being asked.
|
|
3225
|
+
// The starvation test asks the same question the commit will, against the same
|
|
3226
|
+
// snapshot the commit adjudicates against: does this epoch have work that does
|
|
3227
|
+
// not touch the open turn? Two snapshots gave two answers, and the one that
|
|
3228
|
+
// decided the commit was not the one being asked. There is one snapshot now.
|
|
2158
3229
|
otherApplicableMarks: pendingMarks(state).filter((mark) =>
|
|
2159
3230
|
!markTouchesCurrentTurn(state, mark, guarded) &&
|
|
2160
|
-
markEligibility(
|
|
3231
|
+
markEligibility(snapshot, state, mark) === "eligible").length,
|
|
2161
3232
|
});
|
|
2162
|
-
|
|
2163
|
-
snapshot
|
|
3233
|
+
let result = await commitPendingMarks({
|
|
3234
|
+
snapshot,
|
|
2164
3235
|
state,
|
|
2165
3236
|
generation: lifecycle.generation,
|
|
2166
3237
|
retainIneligible: true,
|
|
2167
3238
|
guardCurrentTurn: true,
|
|
2168
3239
|
guardWaiver,
|
|
2169
3240
|
});
|
|
3241
|
+
// THE EPOCH CLOSES ITS OWN COUNT.
|
|
3242
|
+
//
|
|
3243
|
+
// The folds this commit just landed are eligible roots the moment they exist, and
|
|
3244
|
+
// the law is about the count, not about which pass noticed it. Leaving them for the
|
|
3245
|
+
// next epoch is what let the pile stand between commits: measured 2026-08-10 (PT-3),
|
|
3246
|
+
// a commit that landed 26 tool folds left 32 visible roots up until the next band
|
|
3247
|
+
// crossing four cycles later. So the epoch re-reads its own count and parents what
|
|
3248
|
+
// it just made, inside the rewrite it has already paid for. No waiver here: a parent
|
|
3249
|
+
// this round cannot apply is held by the ordinary guard and waits, exactly like any
|
|
3250
|
+
// other fold. It converges because a parent is excluded from the count it came from.
|
|
3251
|
+
for (let round = 0; round < EPOCH_CONSOLIDATION_ROUNDS; round += 1) {
|
|
3252
|
+
let closing = result.state;
|
|
3253
|
+
for (const mark of consolidationMarks({ snapshot, state: closing, ordinal })) {
|
|
3254
|
+
const addition = addPendingMark(closing, mark);
|
|
3255
|
+
if (addition.added) { closing = addition.state; closingAdded += 1; }
|
|
3256
|
+
}
|
|
3257
|
+
if (closing === result.state) break;
|
|
3258
|
+
const closed = await commitPendingMarks({
|
|
3259
|
+
snapshot,
|
|
3260
|
+
state: closing,
|
|
3261
|
+
generation: lifecycle.generation,
|
|
3262
|
+
retainIneligible: true,
|
|
3263
|
+
guardCurrentTurn: true,
|
|
3264
|
+
});
|
|
3265
|
+
if (!closed.applied.length) break;
|
|
3266
|
+
// A mark the guard RETAINED is still pending, so the closing round sees it again
|
|
3267
|
+
// and reports it again; only the terminal refusals accumulate, and the last round
|
|
3268
|
+
// is the truth about what is still held.
|
|
3269
|
+
result = {
|
|
3270
|
+
...closed,
|
|
3271
|
+
applied: [...result.applied, ...closed.applied],
|
|
3272
|
+
refused: [...result.refused.filter((mark) => !mark.retained), ...closed.refused],
|
|
3273
|
+
waived: [...result.waived, ...closed.waived],
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
2170
3276
|
persistence.state = result.state;
|
|
2171
|
-
|
|
3277
|
+
// The upgrade wedge: model briefs written since the last boundary replace their
|
|
3278
|
+
// deterministic ones HERE, inside the mutation this commit already pays for, before
|
|
3279
|
+
// anything measures what the commit did.
|
|
3280
|
+
const upgradedFolds = applyBriefUpgrades();
|
|
3281
|
+
const upgradeFailures = upgrades.failures;
|
|
3282
|
+
const upgradeError = upgrades.lastError;
|
|
3283
|
+
const upgradeCures = upgrades.cures;
|
|
3284
|
+
upgrades.failures = 0;
|
|
3285
|
+
upgrades.cures = 0;
|
|
3286
|
+
upgrades.lastError = null;
|
|
3287
|
+
const bytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
|
|
2172
3288
|
const freedBytes = Math.max(0, bytesBefore - bytesAfter);
|
|
3289
|
+
// The cost of the agent's pins, measured at the moment it matters: mass this
|
|
3290
|
+
// commit could not touch because protect holds it.
|
|
3291
|
+
const pinHeld = protectedStaleMass(snapshot, result.state);
|
|
2173
3292
|
const commitEvent = emit("context.commit", {
|
|
2174
3293
|
trigger,
|
|
2175
3294
|
deferred: false,
|
|
2176
3295
|
reason: null,
|
|
2177
|
-
eligible_freed_share: accounting.
|
|
3296
|
+
eligible_freed_share: accounting.eligibleFreedBudgetShare,
|
|
2178
3297
|
reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2179
3298
|
// The two first-class dials: how much ONE event reclaims, and how long since the
|
|
2180
3299
|
// last one. A trigger that frees too little re-fires immediately, and that shape
|
|
2181
3300
|
// is only visible with both numbers on the same record.
|
|
2182
3301
|
target_freed_share: freeingTarget,
|
|
2183
|
-
hysteresis_target_share:
|
|
2184
|
-
target_occupancy_share:
|
|
2185
|
-
shortfall_share: Math.max(0, freeingTarget - accounting.
|
|
3302
|
+
hysteresis_target_share: freeingTarget,
|
|
3303
|
+
target_occupancy_share: thresholds.minTarget,
|
|
3304
|
+
shortfall_share: Math.max(0, freeingTarget - accounting.eligibleFreedBudgetShare),
|
|
2186
3305
|
occupancy_tokens_before: usedTokens,
|
|
2187
3306
|
budget_tokens: budgetTokens,
|
|
3307
|
+
// The forward look, on the record beside the occupancy it was added to. A commit
|
|
3308
|
+
// that fired because one more turn would have spent the answer's reservation reads
|
|
3309
|
+
// differently from one that fired at the band top, and the difference is only
|
|
3310
|
+
// legible with the allowance printed.
|
|
3311
|
+
expected_inflow_tokens: expectedWallInflowTokens(),
|
|
3312
|
+
output_wall: outputWallDue(snapshot),
|
|
2188
3313
|
requests_since_previous: instrumentation.requests - instrumentation.lastMutationRequest,
|
|
2189
3314
|
inflow_tokens_since_previous: usedTokens === null || instrumentation.lastMutationTokens === null
|
|
2190
3315
|
? null
|
|
@@ -2200,25 +3325,119 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2200
3325
|
ladder_marks: accounting.ladderMarks,
|
|
2201
3326
|
peek_marks: peekAdded,
|
|
2202
3327
|
topup_marks: topUpAdded,
|
|
2203
|
-
|
|
2204
|
-
|
|
3328
|
+
consolidation_marks: consolidationAdded,
|
|
3329
|
+
closing_consolidation_marks: closingAdded,
|
|
2205
3330
|
absorbed_wedges: wedges.absorbed.length,
|
|
3331
|
+
// The upgrade is a mutation of a fold whose `context.fold` record was written at
|
|
3332
|
+
// creation, so the stream would otherwise show a brief that silently changed. The
|
|
3333
|
+
// count, the ids, and the failures ride the boundary that carried them.
|
|
3334
|
+
brief_upgrades: upgradedFolds.length,
|
|
3335
|
+
brief_upgrade_ids: upgradedFolds.join(","),
|
|
3336
|
+
brief_upgrade_failures: upgradeFailures,
|
|
3337
|
+
// Briefs that needed a second ask before they met the contract. Beside the failures
|
|
3338
|
+
// rather than inside them: a cured brief is a brief that landed, and the two together
|
|
3339
|
+
// are how the brief cap gets judged against something other than intuition.
|
|
3340
|
+
brief_upgrade_cures: upgradeCures,
|
|
3341
|
+
brief_upgrade_error: upgradeError,
|
|
3342
|
+
// Everything still owed to a FOLD: queued, in flight, and (always zero once the
|
|
3343
|
+
// wedge above ran) written but uncarried. In flight is counted because the lane
|
|
3344
|
+
// holds several calls open and work nobody can see is work nobody bounds. Counted in
|
|
3345
|
+
// folds rather than calls because a call now carries a whole commit's spans, and the
|
|
3346
|
+
// question this answers is how many folds are still reading deterministic.
|
|
3347
|
+
brief_upgrades_waiting: upgrades.ready.length + upgrades.running.size +
|
|
3348
|
+
upgrades.queue.reduce((total, entry) => total + entry.members.length, 0),
|
|
3349
|
+
// The same work in the unit the lane's two constants actually bound: calls, queued
|
|
3350
|
+
// plus in flight. Reported beside the fold count so a backed-up lane cannot read as
|
|
3351
|
+
// a busy one, or the reverse.
|
|
3352
|
+
brief_upgrade_calls: upgrades.queue.length + new Set(upgrades.running.values()).size,
|
|
2206
3353
|
freed_bytes: freedBytes,
|
|
2207
3354
|
freed_tokens: estimatedTokens(freedBytes),
|
|
2208
3355
|
rewrite_tokens: accounting.rewriteTokens,
|
|
2209
3356
|
pinned_bytes: accounting.pinnedBytes,
|
|
2210
3357
|
pinned_results: accounting.pinnedResults,
|
|
3358
|
+
protected_stale_bytes: pinHeld.bytes,
|
|
3359
|
+
protected_stale_refs: pinHeld.refs,
|
|
2211
3360
|
window_tokens: snapshot.contextWindow,
|
|
2212
3361
|
});
|
|
2213
3362
|
instrumentation.mutationsSinceHandoff += 1;
|
|
2214
3363
|
instrumentation.lastMutationRequest = instrumentation.requests;
|
|
2215
3364
|
instrumentation.lastMutationTokens = usedTokens;
|
|
3365
|
+
// THE RESPONSE ATTRIBUTION. Whatever the gated round did, or that it did nothing,
|
|
3366
|
+
// is linked to the exposure it answered, and the exposure is consumed by the
|
|
3367
|
+
// commit whatever its trigger: a fence or user commit landing mid-round is still
|
|
3368
|
+
// the commit boundary the last-call announced. The carrier's disappearance rides
|
|
3369
|
+
// this commit's own rewrite.
|
|
3370
|
+
if (persistence.state.lastCall) {
|
|
3371
|
+
const lastCall = persistence.state.lastCall;
|
|
3372
|
+
const attribution = lastCallAttribution(lastCall, accounting.agentMarks);
|
|
3373
|
+
emit("context.response", {
|
|
3374
|
+
exposure_seq: lastCall.exposure,
|
|
3375
|
+
commit_seq: commitEvent.seq,
|
|
3376
|
+
trigger,
|
|
3377
|
+
outcome: attribution.responded ? "responded" : "silent",
|
|
3378
|
+
...attribution,
|
|
3379
|
+
});
|
|
3380
|
+
clearLastCall();
|
|
3381
|
+
}
|
|
3382
|
+
// A commit that drops occupancy back under a waypoint re-arms its notice: the next
|
|
3383
|
+
// upward crossing is a new event and earns a new one. The estimate is the same one
|
|
3384
|
+
// the receipt reports: measured occupancy before, less what this commit freed.
|
|
3385
|
+
if (persistence.state.notices && usedTokens !== null && budgetTokens > 0) {
|
|
3386
|
+
const postOccupancy = Math.max(0, usedTokens - estimatedTokens(freedBytes)) / budgetTokens;
|
|
3387
|
+
const fired = persistence.state.notices.fired.filter((share) => postOccupancy >= share);
|
|
3388
|
+
if (fired.length !== persistence.state.notices.fired.length) {
|
|
3389
|
+
persistence.state = { ...persistence.state, notices: { ...persistence.state.notices, fired } };
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
// The rider: at most ONE action prompt per fold epoch, composed from post-commit
|
|
3393
|
+
// numbers, persisted as literal bytes, delivered beside the receipt inside the
|
|
3394
|
+
// rewrite this commit already paid for. The epoch key is the commit event's own
|
|
3395
|
+
// stream sequence: strictly monotone, one per applied commit, so refold-only
|
|
3396
|
+
// epochs get their rider too and no two epochs can ever share a key. The next
|
|
3397
|
+
// epoch REPLACES the text; nothing stacks.
|
|
3398
|
+
const riderEpoch = commitEvent.seq;
|
|
3399
|
+
if (persistence.state.rider?.epoch !== riderEpoch) {
|
|
3400
|
+
const postAccounting = markAccounting(snapshot, persistence.state);
|
|
3401
|
+
const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
|
|
3402
|
+
const pinnedMass = explicitProtectedMass(snapshot, persistence.state);
|
|
3403
|
+
const pinnedShare = budgetTokens > 0
|
|
3404
|
+
? estimatedTokens(pinnedMass.bytes) / budgetTokens
|
|
3405
|
+
: 0;
|
|
3406
|
+
const riderText = contextRiderText({
|
|
3407
|
+
toolName,
|
|
3408
|
+
brandNoun,
|
|
3409
|
+
suggestion: deliverSurfacing(snapshot, "rider"),
|
|
3410
|
+
pendingAgentMarks: postAccounting.agentMarks,
|
|
3411
|
+
eligibleMarks: postAccounting.eligibleMarks,
|
|
3412
|
+
freedTokens: postAccounting.freedTokens,
|
|
3413
|
+
eligibleFreedTokens: postAccounting.eligibleFreedTokens,
|
|
3414
|
+
anchors: remainder.candidates.slice(0, 3).map((candidate) => candidate.id),
|
|
3415
|
+
pinnedShare,
|
|
3416
|
+
maxPinnedShare: MAX_PINNED_SHARE,
|
|
3417
|
+
});
|
|
3418
|
+
persistence.state = { ...persistence.state, rider: { epoch: riderEpoch, text: riderText } };
|
|
3419
|
+
emit("context.rider", {
|
|
3420
|
+
epoch: riderEpoch,
|
|
3421
|
+
chars: riderText.length,
|
|
3422
|
+
anchors: Math.min(3, remainder.candidates.length),
|
|
3423
|
+
pending_agent_marks: postAccounting.agentMarks,
|
|
3424
|
+
eligible_marks: postAccounting.eligibleMarks,
|
|
3425
|
+
pinned_share: pinnedShare,
|
|
3426
|
+
max_pinned_share: MAX_PINNED_SHARE,
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
2216
3429
|
let foldedToolResults = 0;
|
|
2217
3430
|
let foldedSpans = 0;
|
|
2218
3431
|
for (const applied of result.applied) {
|
|
2219
3432
|
const fold = persistence.state.folds.find((item) => item.id === applied.foldId);
|
|
2220
3433
|
if (fold?.kind === "tool-result") foldedToolResults += 1;
|
|
2221
3434
|
else foldedSpans += 1;
|
|
3435
|
+
// A reclaimed peek copy names what it copied. Derived from the transcript rather
|
|
3436
|
+
// than carried on the mark, so the pointer is the same whichever mechanism claimed
|
|
3437
|
+
// the copy: the exposure's reclaim marking, the commit's, or the doorless ladder.
|
|
3438
|
+
const peekedSources = fold?.kind === "tool-result"
|
|
3439
|
+
? peekedSourceFoldIds(snapshot, flattenFoldRefs(fold, persistence.state))
|
|
3440
|
+
: null;
|
|
2222
3441
|
emit("context.fold", {
|
|
2223
3442
|
commit_seq: commitEvent.seq,
|
|
2224
3443
|
fold_id: applied.foldId,
|
|
@@ -2227,15 +3446,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2227
3446
|
mark_id: applied.id,
|
|
2228
3447
|
fold_kind: fold?.kind ?? applied.mark,
|
|
2229
3448
|
origin: applied.origin,
|
|
3449
|
+
peek_of: peekedSources ? peekedSources.join(",") : null,
|
|
2230
3450
|
source_chars: fold?.sourceChars ?? 0,
|
|
2231
3451
|
placeholder_chars: fold?.placeholderChars ?? 0,
|
|
2232
|
-
brief_provenance: fold ?
|
|
3452
|
+
brief_provenance: fold ? foldProvenance(fold, persistence.state).kind : null,
|
|
2233
3453
|
});
|
|
2234
3454
|
}
|
|
3455
|
+
// Queued AFTER the applications above, so no fold can be committed and upgraded on
|
|
3456
|
+
// one boundary: the generator has not run yet, and the wait is the mechanism.
|
|
3457
|
+
queueBriefUpgrades(snapshot, state, result.applied.map((applied) => applied.foldId));
|
|
2235
3458
|
for (const wedge of wedges.absorbed) {
|
|
2236
3459
|
emit("context.absorb", {
|
|
2237
3460
|
commit_seq: commitEvent.seq,
|
|
2238
3461
|
into_fold_id: wedge.intoMarkId,
|
|
3462
|
+
from_mark_id: wedge.fromMarkId,
|
|
2239
3463
|
start_id: wedge.startId,
|
|
2240
3464
|
end_id: wedge.endId,
|
|
2241
3465
|
entries: wedge.entries,
|
|
@@ -2254,8 +3478,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2254
3478
|
ladderMarks: accounting.ladderMarks,
|
|
2255
3479
|
peekMarks: peekAdded,
|
|
2256
3480
|
topUpMarks: topUpAdded,
|
|
2257
|
-
|
|
2258
|
-
|
|
3481
|
+
consolidationMarks: consolidationAdded,
|
|
3482
|
+
// Parents formed over the folds THIS commit landed. They were never in
|
|
3483
|
+
// `pendingMarks`, so an epoch's arithmetic is applied = pending + closing.
|
|
3484
|
+
closingMarks: closingAdded,
|
|
2259
3485
|
absorbedWedges: wedges.absorbed.length,
|
|
2260
3486
|
absorbed: wedges.absorbed,
|
|
2261
3487
|
// What the commit actually folded, the way an agent counts it, so the receipt can
|
|
@@ -2263,7 +3489,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2263
3489
|
foldedSpans,
|
|
2264
3490
|
foldedToolResults,
|
|
2265
3491
|
occupancyTokensBefore: usedTokens,
|
|
2266
|
-
preDeepenFreedShare: preDeepenShare,
|
|
2267
3492
|
depthFloorShare: commitDepthFloorShare(snapshot),
|
|
2268
3493
|
reclaimFloorShare: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2269
3494
|
guardWaived: result.waived.length > 0,
|
|
@@ -2273,19 +3498,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2273
3498
|
deferredMarks: result.retained.length,
|
|
2274
3499
|
pinnedBytes: accounting.pinnedBytes,
|
|
2275
3500
|
pinnedResults: accounting.pinnedResults,
|
|
3501
|
+
protectedStaleBytes: pinHeld.bytes,
|
|
3502
|
+
protectedStaleRefs: pinHeld.refs,
|
|
2276
3503
|
retainedMarks: result.retained.length,
|
|
2277
3504
|
currentTurnRetained: result.retained.filter((mark) =>
|
|
2278
3505
|
markTouchesCurrentTurn(state, mark, guarded)).length,
|
|
2279
3506
|
eligibleMarks: accounting.eligibleMarks,
|
|
2280
3507
|
estimatedRewriteTokens: accounting.rewriteTokens,
|
|
2281
3508
|
estimatedFreedTokens: accounting.freedTokens,
|
|
2282
|
-
|
|
3509
|
+
freedBudgetShare: accounting.freedBudgetShare,
|
|
2283
3510
|
sourceBytesSaved: freedBytes,
|
|
2284
|
-
|
|
2285
|
-
? estimatedTokens(freedBytes) / snapshot.
|
|
3511
|
+
actualFreedBudgetShare: snapshot.budgetTokens > 0
|
|
3512
|
+
? estimatedTokens(freedBytes) / snapshot.budgetTokens
|
|
2286
3513
|
: 0,
|
|
2287
|
-
|
|
2288
|
-
hysteresisTargetShare:
|
|
3514
|
+
targetBudgetShare: freeingTarget,
|
|
3515
|
+
hysteresisTargetShare: freeingTarget,
|
|
2289
3516
|
requestsSincePreviousCommit: instrumentation.requests - instrumentation.lastMutationRequest,
|
|
2290
3517
|
instrumentation: ledgerSummary(instrumentation.ledger),
|
|
2291
3518
|
};
|
|
@@ -2311,12 +3538,78 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2311
3538
|
* that would free nothing, and the hysteresis stops a plateau from reading as a
|
|
2312
3539
|
* stream of events. What is gone is only the announcement in front of it.
|
|
2313
3540
|
*/
|
|
2314
|
-
|
|
3541
|
+
/**
|
|
3542
|
+
* THE trigger, with its one piece of hysteresis.
|
|
3543
|
+
*
|
|
3544
|
+
* Occupancy at or above maxTarget, and then the reopen latch: a window that is still
|
|
3545
|
+
* full because the last commit could not reach the target is the SAME event, not a
|
|
3546
|
+
* new one, so a second commit waits for at least a reclaim floor of genuinely new
|
|
3547
|
+
* eligible mass. Without it a parked window fires a commit per pass and every one of
|
|
3548
|
+
* them rebuilds the prefix for crumbs.
|
|
3549
|
+
*/
|
|
3550
|
+
const commitTriggerDue = (snapshot: ActiveContextSnapshot, ratio: number | null): boolean => {
|
|
2315
3551
|
if (!persistence.state) return false;
|
|
2316
|
-
|
|
3552
|
+
// The output wall outranks the quiet-runtime line rather than sitting under it. A
|
|
3553
|
+
// window one inflow step from spending the answer's whole reservation is past
|
|
3554
|
+
// economising, and the band top is not the thing it is about to cross.
|
|
3555
|
+
//
|
|
3556
|
+
// ONE COMMIT PER CROSSING, NOT ONE PER PASS. The wall is a level, and a window that
|
|
3557
|
+
// sits on it sits there for many passes: releasing the reopen latch on each of them
|
|
3558
|
+
// fires a commit per pass, which is the prefix-rewrite churn the latch exists to
|
|
3559
|
+
// prevent and which the quiet cadence is built around. Measured on gate 52's fixture:
|
|
3560
|
+
// the first wall commit applied 44 marks and correctly retained all ten of the open
|
|
3561
|
+
// turn, and the two that followed it applied nothing, rewrote the prefix anyway, and
|
|
3562
|
+
// dragged the guard's starvation waiver in on a session that had just made progress.
|
|
3563
|
+
// So the crossing is the event. While the wall stays due the ordinary hysteresis
|
|
3564
|
+
// decides, which is what makes a second commit wait for genuinely new foldable mass.
|
|
3565
|
+
const wall = outputWallDue(snapshot);
|
|
3566
|
+
if (!wall) curation.wallEpisodeOpen = false;
|
|
3567
|
+
if (!wall && !epochCommitDue(snapshot, ratio)) return false;
|
|
3568
|
+
measuredCurationSignals(snapshot);
|
|
3569
|
+
if (wall && !curation.wallEpisodeOpen) {
|
|
3570
|
+
curation.wallEpisodeOpen = true;
|
|
3571
|
+
curation.reopenBaselineShare = null;
|
|
3572
|
+
return true;
|
|
3573
|
+
}
|
|
3574
|
+
// The latch is an ECONOMY rule, and the fence is not latched: a request whose
|
|
3575
|
+
// projection exceeds the provider input budget is rejected outright, so recovery
|
|
3576
|
+
// must produce a window that fits.
|
|
3577
|
+
// A request that does not fit gets its commit whether or not the previous one left
|
|
3578
|
+
// new eligible mass behind; holding it back would be spending the session's
|
|
3579
|
+
// transmissibility to save a prefix rewrite.
|
|
3580
|
+
if (typeof ratio === "number" && Number.isFinite(ratio) && ratio >= hardFenceRatio(snapshot)) {
|
|
3581
|
+
curation.reopenBaselineShare = null;
|
|
3582
|
+
return true;
|
|
3583
|
+
}
|
|
2317
3584
|
if (curation.reopenBaselineShare !== null) {
|
|
2318
|
-
const eligibleShare = markAccounting(snapshot, persistence.state).
|
|
2319
|
-
if (eligibleShare - curation.reopenBaselineShare < COMMIT_RECLAIM_FLOOR_SHARE)
|
|
3585
|
+
const eligibleShare = markAccounting(snapshot, persistence.state).eligibleFreedBudgetShare;
|
|
3586
|
+
if (eligibleShare - curation.reopenBaselineShare < COMMIT_RECLAIM_FLOOR_SHARE) {
|
|
3587
|
+
// A HOLD THAT SAYS SO.
|
|
3588
|
+
//
|
|
3589
|
+
// This was a bare `return false`, and it is the last silent deferral on the
|
|
3590
|
+
// commit path: every other one names itself and reports its numbers. In
|
|
3591
|
+
// sol-20260812 rep 1 the trigger was due for twelve straight ordinals while
|
|
3592
|
+
// occupancy climbed to 94 percent of budget, and the stream carries no record of
|
|
3593
|
+
// what held it, so the stall can be proven but not explained. Emitted on every
|
|
3594
|
+
// held pass rather than once per episode: under-reporting is the defect being
|
|
3595
|
+
// fixed, the record is a stream record that never reaches the projection, and
|
|
3596
|
+
// the volume is bounded by passes above the band top.
|
|
3597
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
3598
|
+
emit("context.commit", {
|
|
3599
|
+
trigger: "band-top",
|
|
3600
|
+
deferred: true,
|
|
3601
|
+
reason: "reopen-latch",
|
|
3602
|
+
applied_marks: 0,
|
|
3603
|
+
eligible_freed_share: eligibleShare,
|
|
3604
|
+
reopen_baseline_share: curation.reopenBaselineShare,
|
|
3605
|
+
reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
3606
|
+
occupancy_tokens: capacity.usedTokens,
|
|
3607
|
+
expected_inflow_tokens: expectedWallInflowTokens(),
|
|
3608
|
+
budget_tokens: capacity.budgetTokens,
|
|
3609
|
+
window_tokens: snapshot.contextWindow,
|
|
3610
|
+
});
|
|
3611
|
+
return false;
|
|
3612
|
+
}
|
|
2320
3613
|
curation.reopenBaselineShare = null;
|
|
2321
3614
|
}
|
|
2322
3615
|
return true;
|
|
@@ -2332,7 +3625,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2332
3625
|
if (curation.reopenBaselineShare === null) return;
|
|
2333
3626
|
const capacity = servingCapacity(lifecycle.latestSnapshot?.contextWindow ?? null);
|
|
2334
3627
|
if (capacity.usedTokens === null || capacity.budgetTokens <= 0) return;
|
|
2335
|
-
if (capacity.usedTokens / capacity.budgetTokens <
|
|
3628
|
+
if (capacity.usedTokens / capacity.budgetTokens < thresholds.maxTarget) {
|
|
2336
3629
|
curation.reopenBaselineShare = null;
|
|
2337
3630
|
}
|
|
2338
3631
|
};
|
|
@@ -2373,26 +3666,250 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2373
3666
|
splitFromChars: Number(action.splitFromChars ?? 0),
|
|
2374
3667
|
absorbedWedges: Number(epoch?.absorbedWedges ?? 0),
|
|
2375
3668
|
recovered: curation.recoveryAttempts > 0,
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
"shallower commit would have freed less than one turn of inflow."
|
|
2379
|
-
: null,
|
|
3669
|
+
protectedBytes: Number(epoch?.protectedStaleBytes ?? 0),
|
|
3670
|
+
note: null,
|
|
2380
3671
|
}));
|
|
2381
3672
|
};
|
|
2382
3673
|
|
|
3674
|
+
/**
|
|
3675
|
+
* Threshold notices, evaluated on every pass the ladder runs on. One waypoint fires
|
|
3676
|
+
* once per upward crossing of the measured occupancy; delivery is an append-once
|
|
3677
|
+
* carrier plus its stream record, and re-arming belongs to the commit that drops
|
|
3678
|
+
* occupancy back under the line. Default on; `guidance.thresholdNotices: false`
|
|
3679
|
+
* registers the runtime with the waypoints silent.
|
|
3680
|
+
*/
|
|
3681
|
+
const deliverThresholdNotices = (snapshot: ActiveContextSnapshot): boolean => {
|
|
3682
|
+
if (!guidance.thresholdNotices || !persistence.state) return false;
|
|
3683
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
3684
|
+
if (capacity.usedTokens === null || capacity.budgetTokens <= 0) return false;
|
|
3685
|
+
const occupancy = capacity.usedTokens / capacity.budgetTokens;
|
|
3686
|
+
const current = persistence.state.notices ?? { fired: [], ring: [] };
|
|
3687
|
+
let fired = current.fired;
|
|
3688
|
+
let ring = current.ring;
|
|
3689
|
+
let changed = false;
|
|
3690
|
+
for (const share of THRESHOLD_NOTICE_SHARES) {
|
|
3691
|
+
if (occupancy < share || fired.includes(share)) continue;
|
|
3692
|
+
const text = thresholdNoticeText({
|
|
3693
|
+
share,
|
|
3694
|
+
occupancyTokens: capacity.usedTokens,
|
|
3695
|
+
budgetTokens: capacity.budgetTokens,
|
|
3696
|
+
maxTarget: thresholds.maxTarget,
|
|
3697
|
+
toolName,
|
|
3698
|
+
brandNoun,
|
|
3699
|
+
});
|
|
3700
|
+
fired = [...fired, share];
|
|
3701
|
+
ring = [...ring, { share, ordinal: markOrdinal(snapshot), text }];
|
|
3702
|
+
if (ring.length > MAX_THRESHOLD_NOTICES) ring = ring.slice(ring.length - MAX_THRESHOLD_NOTICES);
|
|
3703
|
+
changed = true;
|
|
3704
|
+
emit("context.notice", {
|
|
3705
|
+
share,
|
|
3706
|
+
occupancy,
|
|
3707
|
+
occupancy_tokens: capacity.usedTokens,
|
|
3708
|
+
budget_tokens: capacity.budgetTokens,
|
|
3709
|
+
max_target: thresholds.maxTarget,
|
|
3710
|
+
chars: text.length,
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
if (changed) persistence.state = { ...persistence.state, notices: { fired, ring } };
|
|
3714
|
+
return changed;
|
|
3715
|
+
};
|
|
3716
|
+
|
|
3717
|
+
/**
|
|
3718
|
+
* The same selection, read rather than delivered: what the next carrier WOULD say,
|
|
3719
|
+
* with the ledger that decides whether it says anything at all.
|
|
3720
|
+
*/
|
|
3721
|
+
const statusSurfacing = (snapshot: ActiveContextSnapshot): Record<string, unknown> => {
|
|
3722
|
+
const ledger = persistence.state ? surfacingLedger(persistence.state) : [];
|
|
3723
|
+
if (!persistence.state) return { slate: [], line: null, ledger, silenced: [] };
|
|
3724
|
+
const selection = selectSurfacingSlate({
|
|
3725
|
+
state: persistence.state,
|
|
3726
|
+
snapshot,
|
|
3727
|
+
toolName,
|
|
3728
|
+
ordinal: markOrdinal(snapshot),
|
|
3729
|
+
});
|
|
3730
|
+
return {
|
|
3731
|
+
slate: selection.slate.map((suggestion) => ({
|
|
3732
|
+
id: suggestion.id,
|
|
3733
|
+
route: suggestion.route,
|
|
3734
|
+
contentScore: suggestion.contentScore,
|
|
3735
|
+
briefScore: suggestion.briefScore,
|
|
3736
|
+
margin: suggestion.margin,
|
|
3737
|
+
slot: suggestion.slot,
|
|
3738
|
+
})),
|
|
3739
|
+
line: surfacingSlateText({ slate: selection.slate, queryTerms: selection.queryTerms, brandNoun }),
|
|
3740
|
+
considered: selection.considered,
|
|
3741
|
+
divergent: selection.divergent,
|
|
3742
|
+
suppressed: selection.suppressed,
|
|
3743
|
+
intentTerms: selection.queryTerms.size,
|
|
3744
|
+
contentHit: SURFACING_CONTENT_HIT,
|
|
3745
|
+
briefHit: SURFACING_BRIEF_HIT,
|
|
3746
|
+
divergenceMargin: SURFACING_DIVERGENCE_MARGIN,
|
|
3747
|
+
slateSize: SURFACING_SLATE_SIZE,
|
|
3748
|
+
silenced: [...surfacingSilenced(ledger)],
|
|
3749
|
+
ledger,
|
|
3750
|
+
};
|
|
3751
|
+
};
|
|
3752
|
+
|
|
3753
|
+
/**
|
|
3754
|
+
* THE SURFACING SLATE, delivered.
|
|
3755
|
+
*
|
|
3756
|
+
* The agent cannot peek a fold it does not know to want, so one line names the fold
|
|
3757
|
+
* whose stored content matches the task in hand when its brief does not. Where it
|
|
3758
|
+
* lands is the whole economics: only carriers that ride a rewrite the runtime is
|
|
3759
|
+
* already paying for (the pre-commit last call and the post-commit rider) plus the
|
|
3760
|
+
* on-demand status surface, which the agent asked for. The per-request ephemeral
|
|
3761
|
+
* carrier stays dead; it cost 21.9% of every input token in rep 21 and nothing else
|
|
3762
|
+
* about this build changes that verdict.
|
|
3763
|
+
*
|
|
3764
|
+
* One suggestion per delivery point, and most delivery points get none. Every
|
|
3765
|
+
* delivery first resolves whatever offers have run out of window, so the ignore that
|
|
3766
|
+
* silences a fold is recorded before the next selection reads the ledger.
|
|
3767
|
+
*/
|
|
3768
|
+
const deliverSurfacing = (snapshot: ActiveContextSnapshot, carrier: string): string | null => {
|
|
3769
|
+
if (!persistence.state) return null;
|
|
3770
|
+
const ordinal = markOrdinal(snapshot);
|
|
3771
|
+
const resolved = resolveSurfacing({ state: persistence.state, snapshot, ordinal });
|
|
3772
|
+
persistence.state = resolved.state;
|
|
3773
|
+
for (const transition of resolved.transitions) {
|
|
3774
|
+
emit("context.outcome", {
|
|
3775
|
+
fold_id: transition.id,
|
|
3776
|
+
from_outcome: transition.from,
|
|
3777
|
+
outcome: transition.to,
|
|
3778
|
+
outcome_ordinal: transition.ordinal,
|
|
3779
|
+
window_ordinals: SURFACING_OUTCOME_WINDOW_ORDINALS,
|
|
3780
|
+
});
|
|
3781
|
+
}
|
|
3782
|
+
const selection = selectSurfacingSlate({ state: persistence.state, snapshot, toolName, ordinal });
|
|
3783
|
+
const suggestion = selection.slate[0];
|
|
3784
|
+
if (!suggestion) return null;
|
|
3785
|
+
const text = surfacingSlateText({
|
|
3786
|
+
slate: selection.slate,
|
|
3787
|
+
queryTerms: selection.queryTerms,
|
|
3788
|
+
brandNoun,
|
|
3789
|
+
});
|
|
3790
|
+
if (!text) return null;
|
|
3791
|
+
persistence.state = issueSurfacing(persistence.state, suggestion.id, ordinal);
|
|
3792
|
+
emit("context.suggestion", {
|
|
3793
|
+
carrier,
|
|
3794
|
+
fold_id: suggestion.id,
|
|
3795
|
+
content_score: suggestion.contentScore,
|
|
3796
|
+
brief_score: suggestion.briefScore,
|
|
3797
|
+
margin: suggestion.margin,
|
|
3798
|
+
content_hit: SURFACING_CONTENT_HIT,
|
|
3799
|
+
brief_hit: SURFACING_BRIEF_HIT,
|
|
3800
|
+
divergence_margin: SURFACING_DIVERGENCE_MARGIN,
|
|
3801
|
+
slot: suggestion.slot,
|
|
3802
|
+
slate_size: SURFACING_SLATE_SIZE,
|
|
3803
|
+
fold_depth: suggestion.depth,
|
|
3804
|
+
considered: selection.considered,
|
|
3805
|
+
divergent: selection.divergent,
|
|
3806
|
+
suppressed: selection.suppressed,
|
|
3807
|
+
intent_terms: selection.queryTerms.size,
|
|
3808
|
+
chars: text.length,
|
|
3809
|
+
});
|
|
3810
|
+
return text;
|
|
3811
|
+
};
|
|
3812
|
+
|
|
3813
|
+
/**
|
|
3814
|
+
* THE PRE-COMMIT LAST-CALL GATE. When the band-top trigger fires, the commit defers
|
|
3815
|
+
* exactly one gated round behind one exposure of the ruled prompt; then it proceeds
|
|
3816
|
+
* with whatever marks exist. A request whose projection exceeds the provider input
|
|
3817
|
+
* budget is rejected outright, so recovery must produce a window that fits: fence
|
|
3818
|
+
* pressure, the recovery lane, or an occupancy already past the serving budget
|
|
3819
|
+
* commit NOW, and the user
|
|
3820
|
+
* command never reaches this gate because explicit intent is its own answer. The
|
|
3821
|
+
* round is measured from DELIVERY: the commit proceeds only on a context pass whose
|
|
3822
|
+
* ordinal is past the pass that rendered the exposure, which is exactly one agent
|
|
3823
|
+
* response between prompt and rewrite.
|
|
3824
|
+
*/
|
|
3825
|
+
const lastCallVerdict = (
|
|
3826
|
+
snapshot: ActiveContextSnapshot,
|
|
3827
|
+
pressure: number | null,
|
|
3828
|
+
phase: string,
|
|
3829
|
+
): "expose" | "hold" | "proceed" => {
|
|
3830
|
+
const state = persistence.state!;
|
|
3831
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
3832
|
+
const fenceLevel = typeof pressure === "number" && Number.isFinite(pressure) &&
|
|
3833
|
+
pressure >= hardFenceRatio(snapshot);
|
|
3834
|
+
// The output wall deliberately does NOT proceed here. It buys the crossing one inflow
|
|
3835
|
+
// step of lead, and the gated round is what that lead is for: the agent still gets its
|
|
3836
|
+
// one bounded turn to curate before a rewrite, which is a contract the wall has no
|
|
3837
|
+
// business cancelling. If the round overruns and occupancy passes the budget outright,
|
|
3838
|
+
// the condition below is what proceeds, on the pass that builds the next request.
|
|
3839
|
+
const overBudget = capacity.usedTokens !== null && capacity.budgetTokens > 0 &&
|
|
3840
|
+
capacity.usedTokens > capacity.budgetTokens;
|
|
3841
|
+
if (fenceLevel || overBudget || curation.recoveryAttempts > 0 || curation.pendingRejection) {
|
|
3842
|
+
return "proceed";
|
|
3843
|
+
}
|
|
3844
|
+
if (!state.lastCall) return "expose";
|
|
3845
|
+
if (phase !== "context") return "hold";
|
|
3846
|
+
const delivery = curation.lastCallDelivery;
|
|
3847
|
+
if (!delivery || delivery.exposure !== state.lastCall.exposure) return "hold";
|
|
3848
|
+
return markOrdinal(snapshot) > delivery.ordinal ? "proceed" : "hold";
|
|
3849
|
+
};
|
|
3850
|
+
|
|
3851
|
+
const exposeLastCall = (snapshot: ActiveContextSnapshot): void => {
|
|
3852
|
+
if (!persistence.state) return;
|
|
3853
|
+
// THE PEEK RECLAIM IS MARKED HERE, NOT ONLY AT THE COMMIT.
|
|
3854
|
+
//
|
|
3855
|
+
// A peek copy is reclaimed at the next commit by contract, and a contract the agent
|
|
3856
|
+
// cannot see until after it executes is not one it can veto. The exposure is the one
|
|
3857
|
+
// bounded round before the rewrite, so minting the reclaim marks now puts the pending
|
|
3858
|
+
// disposal in front of the agent while a pin still changes the outcome: a pinned mark
|
|
3859
|
+
// waits instead of applying, and lifting the pin hands the copy to a later commit.
|
|
3860
|
+
const exposureOrdinal = markOrdinal(snapshot);
|
|
3861
|
+
let peekReclaims = 0;
|
|
3862
|
+
for (const mark of ephemeralPeekMarks({ snapshot, state: persistence.state, ordinal: exposureOrdinal })) {
|
|
3863
|
+
const addition = addPendingMark(persistence.state, mark);
|
|
3864
|
+
if (addition.added) { persistence.state = addition.state; peekReclaims += 1; }
|
|
3865
|
+
}
|
|
3866
|
+
const signals = measuredCurationSignals(snapshot);
|
|
3867
|
+
const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
|
|
3868
|
+
const accounting = markAccounting(snapshot, persistence.state);
|
|
3869
|
+
const text = lastCallText({
|
|
3870
|
+
signals,
|
|
3871
|
+
unmarked: { spans: remainder.spans, tokens: remainder.tokens },
|
|
3872
|
+
pendingMarks: accounting.pending,
|
|
3873
|
+
peekReclaims,
|
|
3874
|
+
suggestion: deliverSurfacing(snapshot, "lastcall"),
|
|
3875
|
+
toolName,
|
|
3876
|
+
brandNoun,
|
|
3877
|
+
});
|
|
3878
|
+
const record = emit("context.lastcall", {
|
|
3879
|
+
occupancy: signals.occupancy,
|
|
3880
|
+
max_target: thresholds.maxTarget,
|
|
3881
|
+
occupancy_tokens: signals.occupancyTokens,
|
|
3882
|
+
budget_tokens: signals.budgetTokens,
|
|
3883
|
+
unmarked_stale_spans: remainder.spans,
|
|
3884
|
+
unmarked_stale_tokens: remainder.tokens,
|
|
3885
|
+
pending_marks: accounting.pending,
|
|
3886
|
+
pending_agent_marks: accounting.agentMarks,
|
|
3887
|
+
peek_marks: peekReclaims,
|
|
3888
|
+
chars: text.length,
|
|
3889
|
+
});
|
|
3890
|
+
persistence.state = {
|
|
3891
|
+
...persistence.state,
|
|
3892
|
+
lastCall: {
|
|
3893
|
+
exposure: record.seq,
|
|
3894
|
+
ordinal: markOrdinal(snapshot),
|
|
3895
|
+
contextCalls: curation.contextCalls,
|
|
3896
|
+
agentMarks: accounting.agentMarks,
|
|
3897
|
+
text,
|
|
3898
|
+
},
|
|
3899
|
+
};
|
|
3900
|
+
};
|
|
3901
|
+
|
|
2383
3902
|
const applyAutomaticRung = async (
|
|
2384
3903
|
snapshot: ActiveContextSnapshot,
|
|
2385
3904
|
ratio: number,
|
|
2386
3905
|
rungOptions: {
|
|
2387
|
-
waiveToolCadence?: boolean;
|
|
2388
3906
|
toolOnly?: boolean;
|
|
2389
3907
|
waiverRatio?: number;
|
|
2390
|
-
announcing?: boolean;
|
|
2391
3908
|
} = {},
|
|
3909
|
+
phase = "context",
|
|
2392
3910
|
): Promise<Record<string, unknown> | null> => {
|
|
2393
3911
|
if (!persistence.state || ladder.automaticFailure || ladder.preparing) return null;
|
|
2394
3912
|
const rungSelectionOptions = {
|
|
2395
|
-
waiveToolCadence: rungOptions.waiveToolCadence,
|
|
2396
3913
|
toolOnly: rungOptions.toolOnly,
|
|
2397
3914
|
summarizerAvailable: Boolean(options.summarizeContextSpan),
|
|
2398
3915
|
failedPreparationIds: ladder.failedPreparations,
|
|
@@ -2433,59 +3950,98 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2433
3950
|
};
|
|
2434
3951
|
let epoch: Record<string, unknown> | null = null;
|
|
2435
3952
|
let inlineRungs = true;
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
3953
|
+
// QUIET RUNTIME, AND THE ONE TRIGGER.
|
|
3954
|
+
//
|
|
3955
|
+
// A commit is an epoch transition: it rewrites the projection, every rewrite risks
|
|
3956
|
+
// the whole prefix cache, so the cadence has to be the fewest commits that still
|
|
3957
|
+
// keep the window inside its budget. Occupancy reaching maxTarget is the whole
|
|
3958
|
+
// automatic condition. Below it the runtime is quiet and marks accumulate free;
|
|
3959
|
+
// at it, one commit folds down toward minTarget, and the gap between the two lines
|
|
3960
|
+
// is what makes the spacing of events structural rather than hoped for.
|
|
3961
|
+
//
|
|
3962
|
+
// Two other arms used to reach this line and both are deleted. The eligible-share
|
|
3963
|
+
// arm fired 164 commits from ordinal 17 in rep 15 on marks that had not earned a
|
|
3964
|
+
// rewrite, and it could fire below maxTarget, which is the quiet law it sat under.
|
|
3965
|
+
// The stale-mass AND-condition guarded an ANNOUNCEMENT that no longer exists: a
|
|
3966
|
+
// commit is never announced, so a commit with nothing eligible now applies nothing
|
|
3967
|
+
// and reports it rather than being suppressed in advance.
|
|
3968
|
+
//
|
|
3969
|
+
// The trigger decides early and says nothing. Nothing warns that a commit is
|
|
3970
|
+
// coming, because a warning has to arrive before the event it warns about and
|
|
3971
|
+
// therefore has to break a prefix nothing else was breaking.
|
|
3972
|
+
// Evaluated on EVERY pass the ladder runs on, which is where the pressure backstop
|
|
3973
|
+
// always sat. Crossing the band top is a property of occupancy, not of which
|
|
3974
|
+
// lifecycle hook happened to notice it, and a trigger that waits for the next
|
|
3975
|
+
// projection pass is a window that keeps climbing while it waits.
|
|
3976
|
+
const noticesChanged = deliverThresholdNotices(snapshot);
|
|
3977
|
+
let lastCallChanged = false;
|
|
3978
|
+
const commitDue = commitTriggerDue(snapshot, ratio);
|
|
3979
|
+
if (commitDue) {
|
|
3980
|
+
const verdict = lastCallVerdict(snapshot, rungOptions.waiverRatio ?? ratio, phase);
|
|
3981
|
+
if (verdict === "expose") {
|
|
3982
|
+
exposeLastCall(snapshot);
|
|
3983
|
+
lastCallChanged = true;
|
|
3984
|
+
} else if (verdict === "proceed") {
|
|
2461
3985
|
epoch = await runCommitEpoch(
|
|
2462
3986
|
snapshot,
|
|
2463
|
-
|
|
3987
|
+
"band-top",
|
|
2464
3988
|
true,
|
|
2465
3989
|
rungOptions.waiverRatio ?? ratio,
|
|
2466
3990
|
);
|
|
2467
|
-
// Latch the eligible share this commit left behind, so the next
|
|
2468
|
-
// genuinely new foldable mass rather than for the same window to still be
|
|
2469
|
-
|
|
3991
|
+
// Latch the eligible share this commit left behind, so the next crossing waits
|
|
3992
|
+
// for genuinely new foldable mass rather than for the same window to still be
|
|
3993
|
+
// full. The band top is a LINE, and a window parked on it is one event.
|
|
3994
|
+
if (persistence.state) {
|
|
2470
3995
|
curation.reopenBaselineShare =
|
|
2471
|
-
markAccounting(snapshot, persistence.state).
|
|
3996
|
+
markAccounting(snapshot, persistence.state).eligibleFreedBudgetShare;
|
|
2472
3997
|
}
|
|
2473
3998
|
}
|
|
2474
|
-
//
|
|
2475
|
-
//
|
|
2476
|
-
|
|
2477
|
-
//
|
|
2478
|
-
//
|
|
2479
|
-
//
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
const
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
3999
|
+
// "hold": the exposure is out and the round is still the agent's. Marks keep
|
|
4000
|
+
// accumulating below; nothing commits and nothing re-exposes.
|
|
4001
|
+
} else if (persistence.state.lastCall) {
|
|
4002
|
+
// The crossing died without its commit (a user rescue or expiry dropped
|
|
4003
|
+
// occupancy back under the trigger), so the exposure lapses: attributed,
|
|
4004
|
+
// cleared, and the next crossing is a fresh event with a fresh exposure.
|
|
4005
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
4006
|
+
if (capacity.usedTokens !== null && capacity.budgetTokens > 0 &&
|
|
4007
|
+
capacity.usedTokens / capacity.budgetTokens < thresholds.maxTarget) {
|
|
4008
|
+
const lastCall = persistence.state.lastCall;
|
|
4009
|
+
const attribution = lastCallAttribution(
|
|
4010
|
+
lastCall,
|
|
4011
|
+
markAccounting(snapshot, persistence.state).agentMarks,
|
|
4012
|
+
);
|
|
4013
|
+
emit("context.response", {
|
|
4014
|
+
exposure_seq: lastCall.exposure,
|
|
4015
|
+
commit_seq: null,
|
|
4016
|
+
trigger: null,
|
|
4017
|
+
outcome: "lapsed",
|
|
4018
|
+
...attribution,
|
|
4019
|
+
});
|
|
4020
|
+
clearLastCall();
|
|
4021
|
+
lastCallChanged = true;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
// An inline rung is free only INSIDE a rewrite the epoch already paid for. Below
|
|
4025
|
+
// the threshold no commit ran, and an epoch that applied NOTHING -- every mark
|
|
4026
|
+
// held back by an open turn or by ineligibility -- paid for no rewrite either,
|
|
4027
|
+
// so folding inline there is a fresh single-fold rewrite of its own. Measured
|
|
4028
|
+
// 2026-08-06 (rep 10): the current-turn guard retained all nine marks every turn
|
|
4029
|
+
// while the inline rung folded one batch per turn anyway, 52 single-fold
|
|
4030
|
+
// rewrites that left the prefix cache share at zero. Below a paid rewrite the
|
|
4031
|
+
// ladder MARKS, so the decisions batch into the first commit that can apply them.
|
|
4032
|
+
inlineRungs = Boolean(epoch) && Number(epoch?.appliedMarks ?? 0) > 0;
|
|
4033
|
+
if (!inlineRungs) {
|
|
4034
|
+
const marked = markLadderSelection();
|
|
4035
|
+
if (marked) {
|
|
4036
|
+
if (epoch) ladder.lastAutomaticAction = { ...marked, epoch };
|
|
4037
|
+
return ladder.lastAutomaticAction;
|
|
4038
|
+
}
|
|
4039
|
+
// A carrier change is durable state even when no mark and no epoch moved: the
|
|
4040
|
+
// exposure and the notices must survive a reload, so the pass returns an action
|
|
4041
|
+
// for the transaction to persist. Never a receipt: a carrier reports itself.
|
|
4042
|
+
if (!epoch && (lastCallChanged || noticesChanged)) {
|
|
4043
|
+
ladder.lastAutomaticAction = { kind: "carrier", foldIds: [], sourceIds: [], sourceBytesSaved: 0 };
|
|
4044
|
+
return ladder.lastAutomaticAction;
|
|
2489
4045
|
}
|
|
2490
4046
|
}
|
|
2491
4047
|
// Evidence a pending mark already covers is not the inline rung's to fold: a mark
|
|
@@ -2501,16 +4057,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2501
4057
|
// quiet cadence started landing a commit on the same context pass, which is what
|
|
2502
4058
|
// turns the inline rung on in the first place.
|
|
2503
4059
|
const selection = inlineRungs
|
|
2504
|
-
? selectAutomaticRung(snapshot, persistence.state, ratio,
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
}
|
|
2513
|
-
: rungSelectionOptions)
|
|
4060
|
+
? selectAutomaticRung(snapshot, persistence.state, ratio, {
|
|
4061
|
+
...rungSelectionOptions,
|
|
4062
|
+
claimed: new Set([
|
|
4063
|
+
...claimedRefKeys(persistence.state),
|
|
4064
|
+
...currentTurnRefKeys(snapshot),
|
|
4065
|
+
]),
|
|
4066
|
+
claimedFoldIds: markedFoldIds(persistence.state),
|
|
4067
|
+
})
|
|
2514
4068
|
: null;
|
|
2515
4069
|
// Above the commit threshold we are already inside the epoch's rewrite turn, so
|
|
2516
4070
|
// every rung applies inline: the extra rung costs at most one more invalidation
|
|
@@ -2631,10 +4185,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2631
4185
|
ctx: any,
|
|
2632
4186
|
phase: string,
|
|
2633
4187
|
rungOptions: {
|
|
2634
|
-
waiveToolCadence?: boolean;
|
|
2635
4188
|
toolOnly?: boolean;
|
|
2636
4189
|
waiverRatio?: number;
|
|
2637
|
-
announcing?: boolean;
|
|
2638
4190
|
} = {},
|
|
2639
4191
|
): Promise<Record<string, unknown> | null> => {
|
|
2640
4192
|
if (!persistence.state || !measurements.lastProviderMeasurement ||
|
|
@@ -2653,7 +4205,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2653
4205
|
const transientAtEntry = captureTransient();
|
|
2654
4206
|
let action: Record<string, unknown> | null = null;
|
|
2655
4207
|
try {
|
|
2656
|
-
action = await applyAutomaticRung(snapshot, ratio, rungOptions);
|
|
4208
|
+
action = await applyAutomaticRung(snapshot, ratio, rungOptions, phase);
|
|
2657
4209
|
if (action) await persist(ctx);
|
|
2658
4210
|
if (action) ladder.boundaryFailure = null;
|
|
2659
4211
|
return action;
|
|
@@ -2681,14 +4233,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2681
4233
|
ctx: any,
|
|
2682
4234
|
phase: string,
|
|
2683
4235
|
rungOptions: {
|
|
2684
|
-
waiveToolCadence?: boolean;
|
|
2685
4236
|
toolOnly?: boolean;
|
|
2686
4237
|
waiverRatio?: number;
|
|
2687
|
-
announcing?: boolean;
|
|
2688
4238
|
} = {},
|
|
2689
4239
|
): Promise<Record<string, unknown> | null> => {
|
|
2690
|
-
const operation = ladder.actionQueue.then(() =>
|
|
2691
|
-
runAutomaticRungTransaction(snapshot, ratio, ctx, phase, rungOptions)
|
|
4240
|
+
const operation = ladder.actionQueue.then(async () => {
|
|
4241
|
+
const action = await runAutomaticRungTransaction(snapshot, ratio, ctx, phase, rungOptions);
|
|
4242
|
+
// Where the generator is started from a pass: after the transaction, so the calls
|
|
4243
|
+
// sit BETWEEN boundaries. Folds queued by the commit this pass just made are
|
|
4244
|
+
// briefed now and land on the next commit, which is the whole shape of the lane.
|
|
4245
|
+
// Afterwards each finishing call starts the next, so this is the lane's ignition
|
|
4246
|
+
// rather than its pace. Parents waiting on their children are taken up here too:
|
|
4247
|
+
// a pass is the other place we know the lane may have room.
|
|
4248
|
+
resumeBriefUpgrades(snapshot, ctx);
|
|
4249
|
+
return action;
|
|
4250
|
+
});
|
|
2692
4251
|
ladder.actionQueue = operation.catch(() => undefined);
|
|
2693
4252
|
return operation;
|
|
2694
4253
|
};
|
|
@@ -2700,40 +4259,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2700
4259
|
return [];
|
|
2701
4260
|
}
|
|
2702
4261
|
};
|
|
2703
|
-
options.setProjectionProvider?.(projectionCandidates);
|
|
2704
|
-
|
|
2705
|
-
/**
|
|
2706
|
-
* The suggestion-source hook. The built-in fold-brief source registers through this
|
|
2707
|
-
* same call, so an external memory system's items reach the identical carrier,
|
|
2708
|
-
* budget, ranking, and accept/reject log; pi-fold owns the channel, not the content.
|
|
2709
|
-
*/
|
|
2710
|
-
const registerSuggestionSource: SuggestionSourceRegistrar = (source: SuggestionSource) => {
|
|
2711
|
-
if (!source || typeof source !== "object" || typeof source.id !== "string" || !source.id ||
|
|
2712
|
-
typeof source.candidates !== "function") {
|
|
2713
|
-
throw new Error("A suggestion source needs a nonempty id and a candidates function");
|
|
2714
|
-
}
|
|
2715
|
-
if (surfacing.sources.some((registered) => registered.id === source.id)) {
|
|
2716
|
-
throw new Error(`Suggestion source '${source.id}' is already registered`);
|
|
2717
|
-
}
|
|
2718
|
-
surfacing.sources.push(source);
|
|
2719
|
-
return {
|
|
2720
|
-
unregister: () => {
|
|
2721
|
-
surfacing.sources = surfacing.sources.filter((registered) => registered !== source);
|
|
2722
|
-
},
|
|
2723
|
-
accepted: (id: string) => {
|
|
2724
|
-
if (!persistence.state || typeof id !== "string" || !id) return;
|
|
2725
|
-
// In-memory only: the next persisted state carries it. Reporting an outcome
|
|
2726
|
-
// must never write a durable entry of its own.
|
|
2727
|
-
persistence.state = acceptSurfacingSuggestion(
|
|
2728
|
-
persistence.state,
|
|
2729
|
-
id,
|
|
2730
|
-
lifecycle.latestSnapshot ? surfacingOrdinal(lifecycle.latestSnapshot) : 0,
|
|
2731
|
-
);
|
|
2732
|
-
},
|
|
2733
|
-
};
|
|
2734
|
-
};
|
|
2735
|
-
options.setSuggestionSourceRegistrar?.(registerSuggestionSource);
|
|
2736
|
-
|
|
2737
4262
|
// A same-session start/tree reload is a projection-generation mutation.
|
|
2738
4263
|
// Queue it behind every context authority → preparation → commit →
|
|
2739
4264
|
// projection transaction, then serialize the actual load with the action
|
|
@@ -2784,6 +4309,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2784
4309
|
pi.on("session_start", async (_event: unknown, ctx: any) => {
|
|
2785
4310
|
await safeLifecycleLoad(ctx, "session-start");
|
|
2786
4311
|
recordResolvedCapacity(ctx);
|
|
4312
|
+
await armRollbackLane(ctx);
|
|
2787
4313
|
});
|
|
2788
4314
|
pi.on("session_tree", async (_event: unknown, ctx: any) => { await safeLifecycleLoad(ctx, "session-tree"); });
|
|
2789
4315
|
pi.on("session_compact", async (event: Record<string, unknown>, ctx: any) => {
|
|
@@ -2831,9 +4357,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2831
4357
|
const attributionChanged = async (_event: unknown, ctx: any): Promise<void> => {
|
|
2832
4358
|
lifecycle.generation += 1;
|
|
2833
4359
|
cancelPreparation();
|
|
4360
|
+
cancelBriefUpgrades();
|
|
2834
4361
|
if (persistence.state?.prepared) persistence.state = clearPrepared(persistence.state);
|
|
2835
4362
|
measurements.latestRatio = null;
|
|
2836
4363
|
measurements.lastProviderMeasurement = null;
|
|
4364
|
+
measurements.wallInflowSteps.length = 0;
|
|
4365
|
+
// A different model or thinking level is a different tokenizer, so counts taken
|
|
4366
|
+
// under the old one describe nothing here. The generation the anchor carries would
|
|
4367
|
+
// already refuse it; dropping it releases the projection text it held as well.
|
|
4368
|
+
measurements.projectionAnchor = null;
|
|
2837
4369
|
nativeCompaction.lastThresholdDecision = null;
|
|
2838
4370
|
updateStatus(ctx);
|
|
2839
4371
|
};
|
|
@@ -2904,6 +4436,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2904
4436
|
// Calibrate BEFORE the new measurement becomes the previous one: the inflow
|
|
2905
4437
|
// step is the difference between them.
|
|
2906
4438
|
noteProjectionCalibration(observedMeasurement);
|
|
4439
|
+
// Anchor before this pass builds anything: `previousText` is still the
|
|
4440
|
+
// projection the provider just counted, and the projection built below is the
|
|
4441
|
+
// one that either extends it or does not.
|
|
4442
|
+
noteProviderProjectionAnchor(observedMeasurement);
|
|
2907
4443
|
measurements.lastProviderMeasurement = observedMeasurement;
|
|
2908
4444
|
startPreparation(snapshot, measurements.latestRatio, ctx);
|
|
2909
4445
|
if (!ladder.automaticFailure && measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) {
|
|
@@ -2913,9 +4449,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2913
4449
|
return { messages: event.messages };
|
|
2914
4450
|
}
|
|
2915
4451
|
}
|
|
2916
|
-
if (
|
|
4452
|
+
if (selectAutomaticToolBatch(snapshot, persistence.state)[0]) mutationAttempted = true;
|
|
2917
4453
|
const action = await attemptAutomaticRung(
|
|
2918
|
-
snapshot, measurements.latestRatio, ctx, "context",
|
|
4454
|
+
snapshot, measurements.latestRatio, ctx, "context",
|
|
2919
4455
|
);
|
|
2920
4456
|
if (action) {
|
|
2921
4457
|
mutationAttempted = true;
|
|
@@ -2951,8 +4487,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2951
4487
|
// already-large session out of calibrating at all: its first projection aborts
|
|
2952
4488
|
// under the uncalibrated constant, so nothing is ever sent, so nothing ever
|
|
2953
4489
|
// measures the ratio, so it aborts forever.
|
|
2954
|
-
|
|
2955
|
-
measurements.
|
|
4490
|
+
const reading = projectedTokenReading(projected);
|
|
4491
|
+
measurements.lastProjectedChars = reading.chars;
|
|
4492
|
+
measurements.lastProjectedEstimate = reading.tokens;
|
|
4493
|
+
measurements.lastProjectedEstimateBasis = reading.basis;
|
|
4494
|
+
measurements.lastProjectedSizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
|
|
2956
4495
|
measurements.lastProjectedEstimateCalibrated = measurements.projectionCalibrations.length > 0;
|
|
2957
4496
|
// An aborted request still returns the PROJECTION. Handing back the raw branch
|
|
2958
4497
|
// instead makes the aborted turn the largest message list the session ever
|
|
@@ -3015,6 +4554,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3015
4554
|
// inflow that is now behind us, and the cap resets for the next one.
|
|
3016
4555
|
curation.recoveryAttempts = 0;
|
|
3017
4556
|
curation.pendingRejection = null;
|
|
4557
|
+
noteWallInflow(measurement);
|
|
3018
4558
|
measurements.lastProviderMeasurement = measurement;
|
|
3019
4559
|
measurements.latestRatio = measuredRatio;
|
|
3020
4560
|
let snapshot: ActiveContextSnapshot;
|
|
@@ -3045,6 +4585,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3045
4585
|
beginMutationPass();
|
|
3046
4586
|
try {
|
|
3047
4587
|
const message = ownValue(event, "message");
|
|
4588
|
+
noteOverflowAtMessageEnd(message, ctx);
|
|
3048
4589
|
instrumentation.requests += 1;
|
|
3049
4590
|
const observation = observeCacheUsage(instrumentation.ledger, {
|
|
3050
4591
|
usage: ownValue(message, "usage"),
|
|
@@ -3089,58 +4630,17 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3089
4630
|
try { updateStatus(ctx); } catch { /* The provider loop must keep running. */ }
|
|
3090
4631
|
}
|
|
3091
4632
|
});
|
|
3092
|
-
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
3095
|
-
*
|
|
3096
|
-
*
|
|
3097
|
-
*
|
|
4633
|
+
/*
|
|
4634
|
+
* There was an `after_provider_response` handler here that treated `status >= 400` as
|
|
4635
|
+
* the rejection signal. It could never fire on the case it was written for. Every
|
|
4636
|
+
* provider adapter calls `onResponse` exactly once and only AFTER the request promise
|
|
4637
|
+
* resolves, so a 4xx rejects the promise and control never reaches that call at all
|
|
4638
|
+
* (verified across all seven adapters in pi-ai 0.83.0). `pendingRejection` was
|
|
4639
|
+
* therefore never set in production and the recovery branch of the projection budget
|
|
4640
|
+
* never ran. It is set by the rollback lane now, from an event that does fire.
|
|
3098
4641
|
*/
|
|
3099
|
-
pi.on("after_provider_response", (event: Record<string, unknown>) => {
|
|
3100
|
-
const status = ownValue(event, "status");
|
|
3101
|
-
if (typeof status !== "number" || status < 400 || status === 429) return;
|
|
3102
|
-
if (curation.recoveryAttempts >= OVERFLOW_RECOVERY_MAX_ATTEMPTS) return;
|
|
3103
|
-
curation.pendingRejection = { status, ordinal: currentOrdinal() };
|
|
3104
|
-
});
|
|
3105
|
-
pi.on("tool_call", (event: Record<string, unknown>, ctx: any) => {
|
|
3106
|
-
const calledTool = ownValue(event, "toolName");
|
|
3107
|
-
if (lifecycle.shuttingDown || lifecycle.blockingToolHarvestedThisTurn || lifecycle.blockingToolHarvestQueuedThisTurn ||
|
|
3108
|
-
typeof calledTool !== "string" || !blockingTools.has(calledTool)) return;
|
|
3109
|
-
lifecycle.blockingToolHarvestQueuedThisTurn = true;
|
|
3110
|
-
beginMutationPass();
|
|
3111
|
-
const operation = ladder.actionQueue.then(async () => {
|
|
3112
|
-
try {
|
|
3113
|
-
if (!persistence.state || measurements.latestRatio === null || !measurements.lastProviderMeasurement || ladder.automaticFailure ||
|
|
3114
|
-
!durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) return null;
|
|
3115
|
-
cancelPreparation();
|
|
3116
|
-
let snapshot: ActiveContextSnapshot;
|
|
3117
|
-
try { snapshot = authoritativeSnapshotFor(ctx); }
|
|
3118
|
-
catch { return null; }
|
|
3119
|
-
const candidate = selectAutomaticToolForRung(snapshot, persistence.state, measurements.latestRatio, true);
|
|
3120
|
-
if (!candidate) {
|
|
3121
|
-
lifecycle.blockingToolHarvestedThisTurn = true;
|
|
3122
|
-
return null;
|
|
3123
|
-
}
|
|
3124
|
-
const action = await runAutomaticRungTransaction(
|
|
3125
|
-
snapshot,
|
|
3126
|
-
measurements.latestRatio,
|
|
3127
|
-
ctx,
|
|
3128
|
-
"tool-call",
|
|
3129
|
-
{ waiveToolCadence: true, toolOnly: true },
|
|
3130
|
-
);
|
|
3131
|
-
if (action) lifecycle.blockingToolHarvestedThisTurn = true;
|
|
3132
|
-
return action;
|
|
3133
|
-
} finally {
|
|
3134
|
-
lifecycle.blockingToolHarvestQueuedThisTurn = false;
|
|
3135
|
-
try { updateStatus(ctx); } catch { /* A blocking tool call must never wait on presentation. */ }
|
|
3136
|
-
}
|
|
3137
|
-
});
|
|
3138
|
-
ladder.actionQueue = operation.catch(() => undefined);
|
|
3139
|
-
});
|
|
3140
4642
|
pi.on("turn_end", async (_event: unknown, ctx: any) => {
|
|
3141
4643
|
beginMutationPass();
|
|
3142
|
-
lifecycle.blockingToolHarvestedThisTurn = false;
|
|
3143
|
-
lifecycle.blockingToolHarvestQueuedThisTurn = false;
|
|
3144
4644
|
if (lifecycle.shuttingDown || !persistence.state || !persistence.persisted) return;
|
|
3145
4645
|
const stateAtEntry = clone(persistence.state);
|
|
3146
4646
|
const persistedAtEntry = persistence.persisted;
|
|
@@ -3170,8 +4670,287 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3170
4670
|
return ownValue(preparation, "userWantsSummary") === true ? { cancel: true } : undefined;
|
|
3171
4671
|
});
|
|
3172
4672
|
|
|
4673
|
+
/**
|
|
4674
|
+
* pi's own overflow classifier, borrowed rather than re-implemented.
|
|
4675
|
+
*
|
|
4676
|
+
* `isContextOverflow` owns the provider-message patterns AND the silent cases (usage
|
|
4677
|
+
* past the window with a clean stop, the truncating `length` stop). Re-stating that
|
|
4678
|
+
* pattern set here would drift on the first provider that reworded its 400. The
|
|
4679
|
+
* bare specifier is tried first, for a deployment where pi-ai is hoisted; the second
|
|
4680
|
+
* form reaches pi's own nested copy, which is where a default install puts it.
|
|
4681
|
+
*/
|
|
4682
|
+
const resolveOverflowClassifier = async (): Promise<void> => {
|
|
4683
|
+
if (rollback.classifier || rollback.classifierSource === "unresolved") return;
|
|
4684
|
+
const host = "@earendil-works/pi-coding-agent";
|
|
4685
|
+
const marker = `/${host}/`;
|
|
4686
|
+
const specifiers = ["@earendil-works/pi-ai/compat"];
|
|
4687
|
+
try {
|
|
4688
|
+
const entry = import.meta.resolve(host);
|
|
4689
|
+
const at = entry.lastIndexOf(marker);
|
|
4690
|
+
if (at >= 0) {
|
|
4691
|
+
specifiers.push(`${entry.slice(0, at + marker.length)}node_modules/@earendil-works/pi-ai/dist/compat.js`);
|
|
4692
|
+
}
|
|
4693
|
+
} catch { /* An unresolvable host is the same answer as an unresolvable classifier. */ }
|
|
4694
|
+
for (const specifier of specifiers) {
|
|
4695
|
+
try {
|
|
4696
|
+
const module = await import(specifier) as Record<string, unknown>;
|
|
4697
|
+
if (typeof module.isContextOverflow === "function") {
|
|
4698
|
+
rollback.classifier = module.isContextOverflow as (message: unknown, contextWindow: number) => boolean;
|
|
4699
|
+
rollback.classifierSource = specifier;
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4702
|
+
} catch { /* Try the next form; the last failure is the answer. */ }
|
|
4703
|
+
}
|
|
4704
|
+
rollback.classifierSource = "unresolved";
|
|
4705
|
+
};
|
|
4706
|
+
|
|
4707
|
+
const armRollbackLane = async (ctx: any): Promise<void> => {
|
|
4708
|
+
const probes = probeRollbackSurfaces(ctx?.sessionManager);
|
|
4709
|
+
rollback.probes = probes;
|
|
4710
|
+
rollback.armed = probes.armed;
|
|
4711
|
+
await resolveOverflowClassifier();
|
|
4712
|
+
if (!probes.armed) {
|
|
4713
|
+
safeNotify(
|
|
4714
|
+
ctx,
|
|
4715
|
+
`${brandNoun} overflow rollback is DISARMED for this session (${probes.failures.join("; ")}). ` +
|
|
4716
|
+
"An input-overflow rejection will abort the request instead of rolling the session back.",
|
|
4717
|
+
"warning",
|
|
4718
|
+
);
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
|
|
4722
|
+
/**
|
|
4723
|
+
* The disarmed answer, and the only one: say so on the record and leave the session
|
|
4724
|
+
* behaving exactly as it did before the lane existed.
|
|
4725
|
+
*/
|
|
4726
|
+
const refuseRollback = (reason: string, trigger: string, ctx: any): void => {
|
|
4727
|
+
emit("context.rollback", {
|
|
4728
|
+
trigger,
|
|
4729
|
+
armed: false,
|
|
4730
|
+
disarm_reason: reason,
|
|
4731
|
+
error_entry_id: null,
|
|
4732
|
+
old_leaf_id: null,
|
|
4733
|
+
new_leaf_id: null,
|
|
4734
|
+
entries_abandoned: 0,
|
|
4735
|
+
occupancy_tokens_before: measurements.lastProviderMeasurement?.tokens ?? null,
|
|
4736
|
+
tokens_rolled_back: 0,
|
|
4737
|
+
replayed: false,
|
|
4738
|
+
replay_skip_reason: "lane-disarmed",
|
|
4739
|
+
notice_chars: 0,
|
|
4740
|
+
attempt_ordinal: rollback.attempts,
|
|
4741
|
+
probes_passed: rollback.probes ? rollback.probes.failures.length === 0 : false,
|
|
4742
|
+
});
|
|
4743
|
+
safeNotify(
|
|
4744
|
+
ctx,
|
|
4745
|
+
`${brandNoun} could not roll back after a provider input-overflow rejection: ${reason}. ` +
|
|
4746
|
+
"The request was not retried.",
|
|
4747
|
+
"error",
|
|
4748
|
+
);
|
|
4749
|
+
};
|
|
4750
|
+
|
|
4751
|
+
/**
|
|
4752
|
+
* The fallback trigger, and the honest limit of it.
|
|
4753
|
+
*
|
|
4754
|
+
* `session_before_compact` never fires for a deployment that turned auto-compaction
|
|
4755
|
+
* off, so the primary trigger is deaf there. This is the second detector: pi's own
|
|
4756
|
+
* classifier over the assistant message at `message_end`, which is where the error
|
|
4757
|
+
* message first becomes visible. It records the episode and, if no compaction event
|
|
4758
|
+
* claims it, reports it.
|
|
4759
|
+
*
|
|
4760
|
+
* It does NOT roll back, and the reason is mechanical rather than cautious. The
|
|
4761
|
+
* pre-strip this lane depends on happens INSIDE `_checkCompaction`, which is exactly
|
|
4762
|
+
* the code an auto-compaction-disabled deployment never reaches, so on this path
|
|
4763
|
+
* agent state still carries the oversized window plus the error message. A bare
|
|
4764
|
+
* `branch()` there would leave the tree and agent state disagreeing, which is the
|
|
4765
|
+
* failure mode the whole design exists to avoid. Held, and reported as held.
|
|
4766
|
+
*/
|
|
4767
|
+
const noteOverflowAtMessageEnd = (message: unknown, ctx: any): void => {
|
|
4768
|
+
if (!rollback.classifier || !message || typeof message !== "object") return;
|
|
4769
|
+
const window = budgetWindowFor(ctx) ?? DEFAULT_CONTEXT_WINDOW;
|
|
4770
|
+
let overflow = false;
|
|
4771
|
+
try { overflow = rollback.classifier(message, window) === true; }
|
|
4772
|
+
catch { return; }
|
|
4773
|
+
if (!overflow) return;
|
|
4774
|
+
rollback.pendingOverflow = { at: currentOrdinal(), entryId: null };
|
|
4775
|
+
};
|
|
4776
|
+
|
|
4777
|
+
/**
|
|
4778
|
+
* An overflow that no compaction event claimed: the auto-compaction-disabled shape.
|
|
4779
|
+
* Loud, terminal, and never silent.
|
|
4780
|
+
*/
|
|
4781
|
+
const reportUnclaimedOverflow = (ctx: any): void => {
|
|
4782
|
+
if (!rollback.pendingOverflow) return;
|
|
4783
|
+
rollback.pendingOverflow = null;
|
|
4784
|
+
rollback.attempts += 1;
|
|
4785
|
+
refuseRollback(
|
|
4786
|
+
"the overflow arrived without a compaction event, so pi never stripped the failed message from agent " +
|
|
4787
|
+
"state and a tree rollback would leave the tree and agent state disagreeing (auto-compaction is off)",
|
|
4788
|
+
"message_end",
|
|
4789
|
+
ctx,
|
|
4790
|
+
);
|
|
4791
|
+
};
|
|
4792
|
+
|
|
4793
|
+
/**
|
|
4794
|
+
* ROLLBACK, COMMIT, REPLAY.
|
|
4795
|
+
*
|
|
4796
|
+
* pi has already stripped the trailing error message from agent state by the time
|
|
4797
|
+
* this fires, so agent state IS the rolled-back window and all that remains is to
|
|
4798
|
+
* move the session leaf to match. The label goes on FIRST, because a label appends at
|
|
4799
|
+
* the current leaf and advances it, so applied after the branch it would mark the
|
|
4800
|
+
* surviving path instead of the abandoned one.
|
|
4801
|
+
*
|
|
4802
|
+
* The commit deliberately does NOT happen here. The retried request runs its own
|
|
4803
|
+
* `context` pass, and that pass folds and commits before the payload is built. A
|
|
4804
|
+
* commit here would adjudicate a snapshot taken outside a projection pass and then be
|
|
4805
|
+
* evaluated again by the retry, which is two mutations for one episode.
|
|
4806
|
+
*/
|
|
4807
|
+
const rebuiltMessageCount = (manager: Record<string, any>): number | null => {
|
|
4808
|
+
try {
|
|
4809
|
+
const rebuilt = manager.buildSessionContext?.();
|
|
4810
|
+
return Array.isArray(rebuilt?.messages) ? rebuilt.messages.length : null;
|
|
4811
|
+
} catch { return null; }
|
|
4812
|
+
};
|
|
4813
|
+
|
|
4814
|
+
const recoverFromOverflow = (event: Record<string, unknown>, ctx: any): void => {
|
|
4815
|
+
rollback.attempts += 1;
|
|
4816
|
+
rollback.pendingOverflow = null;
|
|
4817
|
+
if (!rollback.armed) {
|
|
4818
|
+
refuseRollback(
|
|
4819
|
+
rollback.probes?.failures.join("; ") ?? "the rollback probes never ran",
|
|
4820
|
+
"session_before_compact",
|
|
4821
|
+
ctx,
|
|
4822
|
+
);
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4825
|
+
const shape = overflowEventShape(event);
|
|
4826
|
+
if (!shape.ok) {
|
|
4827
|
+
refuseRollback(`the overflow event is missing ${shape.missing.join(", ")}`, "session_before_compact", ctx);
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4830
|
+
const branchEntries = ownValue(event, "branchEntries");
|
|
4831
|
+
// Copied, not aliased. `getBranch()` hands back the live array and the branch call
|
|
4832
|
+
// below truncates it in place, so a handler holding the original reference watches
|
|
4833
|
+
// the entries it is still reasoning about disappear underneath it.
|
|
4834
|
+
const entries = Array.isArray(branchEntries) ? [...branchEntries] : [];
|
|
4835
|
+
const errorEntry = findOverflowErrorEntry(entries);
|
|
4836
|
+
if (!errorEntry) {
|
|
4837
|
+
refuseRollback("the rejected assistant entry is not the branch tail", "session_before_compact", ctx);
|
|
4838
|
+
return;
|
|
4839
|
+
}
|
|
4840
|
+
const manager = ctx.sessionManager as Record<string, any>;
|
|
4841
|
+
const oldLeafId = manager.getLeafId();
|
|
4842
|
+
const sessionMessagesBefore = rebuiltMessageCount(manager);
|
|
4843
|
+
const abandoned = entries.slice(errorEntry.index);
|
|
4844
|
+
const rolledBackBytes = bytes(abandoned.flatMap((entry: any) => sessionEntryMessages(entry) ?? []));
|
|
4845
|
+
// Lineage first. The abandoned path stays in the tree, and it says why it was left.
|
|
4846
|
+
try {
|
|
4847
|
+
manager.appendLabelChange(errorEntry.id, `${entryTypePrefix} overflow rollback`);
|
|
4848
|
+
} catch (error) {
|
|
4849
|
+
refuseRollback(`the lineage label failed (${String(error)})`, "session_before_compact", ctx);
|
|
4850
|
+
return;
|
|
4851
|
+
}
|
|
4852
|
+
try {
|
|
4853
|
+
if (errorEntry.parentId === null) manager.resetLeaf();
|
|
4854
|
+
else manager.branch(errorEntry.parentId);
|
|
4855
|
+
} catch (error) {
|
|
4856
|
+
refuseRollback(`the branch failed (${String(error)})`, "session_before_compact", ctx);
|
|
4857
|
+
return;
|
|
4858
|
+
}
|
|
4859
|
+
// The invariant, measured rather than assumed: the rolled-back window has to be
|
|
4860
|
+
// exactly the failed entry shorter, or the retry sends something nobody built.
|
|
4861
|
+
const errorEntryMessages = (sessionEntryMessages(entries[errorEntry.index]) ?? []).length;
|
|
4862
|
+
const sessionMessagesAfter = rebuiltMessageCount(manager);
|
|
4863
|
+
const holds = sessionMessagesBefore === null || sessionMessagesAfter === null ||
|
|
4864
|
+
preStripHolds({ sessionMessagesBefore, sessionMessagesAfter, errorEntryMessages });
|
|
4865
|
+
const unanswered = unansweredToolCalls(entries.slice(0, errorEntry.index));
|
|
4866
|
+
let replaySkipReason: string | null = null;
|
|
4867
|
+
if (!holds) {
|
|
4868
|
+
replaySkipReason = `the rolled-back window came back ${sessionMessagesAfter} messages against the ` +
|
|
4869
|
+
`${(sessionMessagesBefore ?? 0) - errorEntryMessages} the rollback should have left`;
|
|
4870
|
+
} else if (unanswered.length) {
|
|
4871
|
+
replaySkipReason = `the rolled-back tail leaves ${unanswered.length} tool call(s) unanswered, and a user ` +
|
|
4872
|
+
"turn after an unsatisfied tool call is a malformed transcript for every provider";
|
|
4873
|
+
}
|
|
4874
|
+
const occupancyBefore = measurements.lastProviderMeasurement?.tokens ?? null;
|
|
4875
|
+
const notice = rollbackNoticeText({
|
|
4876
|
+
brandNoun,
|
|
4877
|
+
toolName,
|
|
4878
|
+
tokensRolledBack: estimatedTokens(rolledBackBytes),
|
|
4879
|
+
entriesAbandoned: abandoned.length,
|
|
4880
|
+
replayed: replaySkipReason === null,
|
|
4881
|
+
replaySkipReason,
|
|
4882
|
+
});
|
|
4883
|
+
// Exactly ONE steered message. Steering drains one at a time at the top of the
|
|
4884
|
+
// retried loop, so a second would be silently held to a turn that never comes.
|
|
4885
|
+
let replayed = false;
|
|
4886
|
+
if (replaySkipReason === null) {
|
|
4887
|
+
if (typeof pi.sendMessage !== "function") {
|
|
4888
|
+
replaySkipReason = "this pi build exposes no message verb, so the retry cannot be steered";
|
|
4889
|
+
} else {
|
|
4890
|
+
try {
|
|
4891
|
+
pi.sendMessage(
|
|
4892
|
+
{ customType: `${entryTypePrefix}-overflow-recovery`, content: notice },
|
|
4893
|
+
{ deliverAs: "steer" },
|
|
4894
|
+
);
|
|
4895
|
+
replayed = true;
|
|
4896
|
+
} catch (error) {
|
|
4897
|
+
replaySkipReason = `the recovery notice could not be queued (${String(error)})`;
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
if (!replayed) safeNotify(ctx, notice, "warning");
|
|
4902
|
+
const record = emit("context.rollback", {
|
|
4903
|
+
trigger: "session_before_compact",
|
|
4904
|
+
armed: true,
|
|
4905
|
+
disarm_reason: null,
|
|
4906
|
+
error_entry_id: errorEntry.id,
|
|
4907
|
+
old_leaf_id: typeof oldLeafId === "string" ? oldLeafId : null,
|
|
4908
|
+
new_leaf_id: errorEntry.parentId,
|
|
4909
|
+
entries_abandoned: abandoned.length,
|
|
4910
|
+
occupancy_tokens_before: occupancyBefore,
|
|
4911
|
+
tokens_rolled_back: estimatedTokens(rolledBackBytes),
|
|
4912
|
+
replayed,
|
|
4913
|
+
replay_skip_reason: replaySkipReason,
|
|
4914
|
+
notice_chars: notice.length,
|
|
4915
|
+
attempt_ordinal: rollback.attempts,
|
|
4916
|
+
probes_passed: true,
|
|
4917
|
+
});
|
|
4918
|
+
rollback.last = {
|
|
4919
|
+
seq: record.seq,
|
|
4920
|
+
errorEntryId: errorEntry.id,
|
|
4921
|
+
newLeafId: errorEntry.parentId,
|
|
4922
|
+
entriesAbandoned: abandoned.length,
|
|
4923
|
+
tokensRolledBack: estimatedTokens(rolledBackBytes),
|
|
4924
|
+
replayed,
|
|
4925
|
+
replaySkipReason,
|
|
4926
|
+
noticeChars: notice.length,
|
|
4927
|
+
attemptOrdinal: rollback.attempts,
|
|
4928
|
+
};
|
|
4929
|
+
// The tree moved, so the mapping is rebuilt from it. THEN the fold is armed for the
|
|
4930
|
+
// retried pass, in that order: a reload clears the recovery flags, and arming before
|
|
4931
|
+
// it would hand the retry a cleared one. The guards are waived in that pass because
|
|
4932
|
+
// a request that already did not fit has nothing left to economize.
|
|
4933
|
+
load(ctx, true);
|
|
4934
|
+
curation.pendingRejection = { status: 400, ordinal: currentOrdinal() };
|
|
4935
|
+
};
|
|
4936
|
+
|
|
3173
4937
|
pi.on("session_before_compact", (event: Record<string, unknown>, ctx: any) => {
|
|
3174
4938
|
const reason = ownValue(event, "reason");
|
|
4939
|
+
// The overflow reason is a provider rejection pi is prepared to retry, and it is
|
|
4940
|
+
// the rollback lane's trigger. Threshold and manual compaction are unchanged.
|
|
4941
|
+
if (reason === "overflow" && ownValue(event, "willRetry") === true) {
|
|
4942
|
+
nativeCompaction.lastThresholdDecision = {
|
|
4943
|
+
handled: true,
|
|
4944
|
+
retry: true,
|
|
4945
|
+
reason: `provider input overflow; ${contextBrand(brandNoun)} rolled the session tree back instead`,
|
|
4946
|
+
compactionReason: reason,
|
|
4947
|
+
nativeCompactionCompleted: false,
|
|
4948
|
+
};
|
|
4949
|
+
try { recoverFromOverflow(event, ctx); }
|
|
4950
|
+
catch (error) { suspendAutomatic(error, "overflow-rollback", ctx); }
|
|
4951
|
+
try { updateStatus(ctx); } catch { /* Recovery must survive presentation failure. */ }
|
|
4952
|
+
return { cancel: true };
|
|
4953
|
+
}
|
|
3175
4954
|
if (reason === "manual") {
|
|
3176
4955
|
nativeCompaction.lastThresholdDecision = {
|
|
3177
4956
|
handled: false,
|
|
@@ -3192,6 +4971,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3192
4971
|
return { cancel: true };
|
|
3193
4972
|
});
|
|
3194
4973
|
pi.on("agent_settled", async (_event: unknown, ctx: any) => {
|
|
4974
|
+
reportUnclaimedOverflow(ctx);
|
|
3195
4975
|
if (ladder.pendingManual && persistence.persisted && ladder.boundaryFailure === null) {
|
|
3196
4976
|
cancelPreparation();
|
|
3197
4977
|
persistence.state = clone(persistence.persisted);
|
|
@@ -3300,31 +5080,34 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3300
5080
|
pendingMarks: accounting.pending,
|
|
3301
5081
|
eligibleMarks: accounting.eligibleMarks,
|
|
3302
5082
|
retainedMarks: accounting.retainedMarks,
|
|
3303
|
-
eligibleMarkedShare: accounting.
|
|
3304
|
-
markedShare: accounting.
|
|
5083
|
+
eligibleMarkedShare: accounting.eligibleFreedBudgetShare,
|
|
5084
|
+
markedShare: accounting.freedBudgetShare,
|
|
3305
5085
|
available: true,
|
|
3306
5086
|
automatic: {
|
|
3307
5087
|
pressureRatio: measurements.latestRatio,
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
toolFoldRatio: snapshot.policy.toolFoldRatio,
|
|
5088
|
+
// Status is a READ of the slate, never a delivery of one. `status` is a
|
|
5089
|
+
// read-only context action and the tool-batch safety scan is built on
|
|
5090
|
+
// read-only actions not writing durable state, so asking must not issue a
|
|
5091
|
+
// suggestion, spend the precision budget, or move a suppression counter. An
|
|
5092
|
+
// agent that asks gets the answer; only the push carriers are metered.
|
|
5093
|
+
surfacing: statusSurfacing(snapshot),
|
|
3315
5094
|
refoldRatio: snapshot.policy.refoldRatio,
|
|
3316
5095
|
chapterPrepareRatio: snapshot.policy.prepareRatio,
|
|
3317
5096
|
hardFenceRatio: hardFenceRatio(snapshot),
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
5097
|
+
// The declared policy, reported. No action reads it back as a setting.
|
|
5098
|
+
thresholds: { ...snapshot.thresholds },
|
|
5099
|
+
zones: {
|
|
5100
|
+
freshBoundary: snapshot.freshBoundary,
|
|
5101
|
+
budgetTokens: snapshot.budgetTokens,
|
|
5102
|
+
},
|
|
5103
|
+
// What THIS runtime withheld, which is nothing once the deployment declared an
|
|
5104
|
+
// already-net budget. One number, from the same accounting the fence divides by.
|
|
5105
|
+
responseReserve: currentCapacity(ctx).outputReservation,
|
|
3322
5106
|
windowSource: snapshot.windowSource,
|
|
3323
5107
|
capacity: {
|
|
3324
5108
|
...currentCapacity(ctx),
|
|
3325
5109
|
bytesPerToken: projectionCharsPerToken(),
|
|
3326
5110
|
},
|
|
3327
|
-
consolidationRatio: snapshot.policy.consolidationRatio,
|
|
3328
5111
|
providerMeasurement: measurements.lastProviderMeasurement ? {
|
|
3329
5112
|
tokens: measurements.lastProviderMeasurement.tokens,
|
|
3330
5113
|
contextWindow: measurements.lastProviderMeasurement.contextWindow,
|
|
@@ -3346,8 +5129,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3346
5129
|
lastAutomaticAction: ladder.lastAutomaticAction,
|
|
3347
5130
|
overBudgetReduction: ladder.overBudgetReduction ? clone(ladder.overBudgetReduction) : null,
|
|
3348
5131
|
curation: {
|
|
3349
|
-
occupancyThreshold:
|
|
3350
|
-
staleToolThreshold: CURATION_STALE_TOOL_SHARE,
|
|
5132
|
+
occupancyThreshold: thresholds.maxTarget,
|
|
3351
5133
|
signals: curation.lastSignals ? { ...curation.lastSignals } : null,
|
|
3352
5134
|
contextCalls: curation.contextCalls,
|
|
3353
5135
|
receipts: curation.receipts.map((receipt) => ({ ...receipt })),
|
|
@@ -3357,28 +5139,34 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3357
5139
|
maxAttempts: OVERFLOW_RECOVERY_MAX_ATTEMPTS,
|
|
3358
5140
|
pendingRejection: curation.pendingRejection ? { ...curation.pendingRejection } : null,
|
|
3359
5141
|
last: curation.lastRecovery ? clone(curation.lastRecovery) : null,
|
|
5142
|
+
rollback: {
|
|
5143
|
+
armed: rollback.armed,
|
|
5144
|
+
probeFailures: rollback.probes ? [...rollback.probes.failures] : null,
|
|
5145
|
+
classifier: rollback.classifierSource,
|
|
5146
|
+
attempts: rollback.attempts,
|
|
5147
|
+
last: rollback.last ? clone(rollback.last) : null,
|
|
5148
|
+
},
|
|
3360
5149
|
},
|
|
3361
5150
|
foldSpanCap: MAX_FOLD_SPAN_CHARS,
|
|
3362
5151
|
projectionBudgetTokens: currentCapacity(ctx).budgetTokens,
|
|
3363
5152
|
projectionCharsPerToken: projectionCharsPerToken(),
|
|
3364
5153
|
projectionEstimatorErrorShare: estimatorErrorShare(),
|
|
3365
5154
|
projectionExpectedInflowTokens: expectedInflowTokens(),
|
|
3366
|
-
|
|
5155
|
+
// What the fence actually weighed, not a second arithmetic over the same bytes:
|
|
5156
|
+
// once occupancy is anchored, re-deriving it here from chars alone would report
|
|
5157
|
+
// a number no decision was made on.
|
|
5158
|
+
projectionMarginTokens: measurements.lastProjectedEstimate === null
|
|
3367
5159
|
? null
|
|
3368
|
-
: projectionMarginTokens(
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
projectionEstimatedTokens: measurements.lastProjectedChars === null
|
|
3373
|
-
? null
|
|
3374
|
-
: Math.ceil(measurements.lastProjectedChars / projectionCharsPerToken()),
|
|
5160
|
+
: projectionMarginTokens(measurements.lastProjectedEstimate, currentCapacity(ctx).window),
|
|
5161
|
+
projectionEstimatedTokens: measurements.lastProjectedEstimate,
|
|
5162
|
+
projectionEstimateBasis: measurements.lastProjectedEstimateBasis,
|
|
5163
|
+
projectionAnchorTokens: measurements.projectionAnchor?.tokens ?? null,
|
|
3375
5164
|
automaticSuspended: ladder.automaticFailure !== null,
|
|
3376
5165
|
automaticFailure: ladder.automaticFailure ? clone(ladder.automaticFailure) : null,
|
|
3377
5166
|
lastCompactionDecision: nativeCompaction.lastThresholdDecision,
|
|
3378
5167
|
scheduling: schedulingStatus({
|
|
3379
5168
|
snapshot,
|
|
3380
5169
|
state: persistence.state,
|
|
3381
|
-
mode: foldScheduling,
|
|
3382
5170
|
ratio: measurements.latestRatio,
|
|
3383
5171
|
}),
|
|
3384
5172
|
nativeSummaries: "disabled",
|
|
@@ -3390,12 +5178,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3390
5178
|
// THE stream, as the durable entries carry it. One shape, one convention.
|
|
3391
5179
|
events: clone(instrumentation.ledger.events),
|
|
3392
5180
|
},
|
|
3393
|
-
foldPeekResults,
|
|
3394
5181
|
peek: {
|
|
3395
5182
|
defaultMaxBytes: PEEK_DEFAULT_MAX_BYTES,
|
|
3396
|
-
lifetime: "append-only",
|
|
5183
|
+
lifetime: "append-only; the copy is reclaimed at the next commit unless pinned",
|
|
3397
5184
|
},
|
|
3398
|
-
freeHarvest: blockingTools.size === 0 ? "disabled" : "enabled",
|
|
3399
5185
|
pressureSource: "last-successful-provider-response-only",
|
|
3400
5186
|
postOverflowCallback: "blocked-while-stock-native-compaction-is-disabled",
|
|
3401
5187
|
sameOperationRetry: false,
|
|
@@ -3414,12 +5200,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3414
5200
|
...(detail === "tree" ? { tree: foldTreeDetail(snapshot, persistence.state).slice(statusOffset) } : {}),
|
|
3415
5201
|
}, typeof detail === "string" ? detail : null, statusOffset));
|
|
3416
5202
|
}
|
|
3417
|
-
// Peeking or expanding a
|
|
3418
|
-
//
|
|
3419
|
-
//
|
|
5203
|
+
// Peeking or expanding a surfaced fold is the ACTED label, joined to the offer that
|
|
5204
|
+
// named it. It stays ephemeral: the ledger rides the next persisted state, never an
|
|
5205
|
+
// entry of its own, and the used/ignored grades land when the window closes.
|
|
3420
5206
|
const noteSurfacingAccept = (id: string): void => {
|
|
3421
5207
|
if (!persistence.state) return;
|
|
3422
|
-
persistence.state =
|
|
5208
|
+
persistence.state = noteSurfacingAction(persistence.state, id, markOrdinal(snapshot));
|
|
3423
5209
|
};
|
|
3424
5210
|
if (action === "peek") {
|
|
3425
5211
|
const id = String(params.id ?? "").trim();
|
|
@@ -3592,7 +5378,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3592
5378
|
// An expand with marks pending opens the commit epoch: the restore and the
|
|
3593
5379
|
// batch of folds then cost one rewrite between them instead of two.
|
|
3594
5380
|
let epochApplied: unknown[] = [];
|
|
3595
|
-
if (
|
|
5381
|
+
if (action === "expand" && pendingMarks(persistence.state).length) {
|
|
3596
5382
|
const result = await commitPendingMarks({
|
|
3597
5383
|
snapshot,
|
|
3598
5384
|
state: persistence.state,
|
|
@@ -3637,12 +5423,39 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3637
5423
|
}
|
|
3638
5424
|
if (action === "protect" || action === "unprotect") {
|
|
3639
5425
|
const ids = stringIds(params.ids);
|
|
3640
|
-
|
|
5426
|
+
const refsBefore = persistence.state.protected.length;
|
|
5427
|
+
const nextProtection = protectEvidence(snapshot, persistence.state, ids, action === "protect");
|
|
5428
|
+
if (action === "protect") {
|
|
5429
|
+
// The pin ceiling: protect is a promise to hold bytes raw through every fold,
|
|
5430
|
+
// and a promise with no mass bound can pin the window solid. The refusal is
|
|
5431
|
+
// corrective, not denial: it names the cap and the release valve.
|
|
5432
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
5433
|
+
const prospectiveShare = capacity.budgetTokens > 0
|
|
5434
|
+
? estimatedTokens(explicitProtectedMass(snapshot, nextProtection).bytes) / capacity.budgetTokens
|
|
5435
|
+
: 0;
|
|
5436
|
+
if (prospectiveShare > MAX_PINNED_SHARE) {
|
|
5437
|
+
throw new Error(
|
|
5438
|
+
`protect refused: these pins would hold ${Math.round(prospectiveShare * 100)}% of the ` +
|
|
5439
|
+
`working window raw, past the ${Math.round(MAX_PINNED_SHARE * 100)}% pinned-share cap. ` +
|
|
5440
|
+
'Release earlier pins with {"action":"unprotect","ids":["<entry-id>"]} first; ' +
|
|
5441
|
+
"folding keeps entries exactly recoverable without pinning them.");
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
await persistManual(nextProtection, action, ctx);
|
|
5445
|
+
const refsAfter = persistence.state.protected.length;
|
|
5446
|
+
emit("context.protect", {
|
|
5447
|
+
protect: action === "protect",
|
|
5448
|
+
ids: ids.join(","),
|
|
5449
|
+
id_count: ids.length,
|
|
5450
|
+
protected_refs_before: refsBefore,
|
|
5451
|
+
protected_refs_after: refsAfter,
|
|
5452
|
+
});
|
|
3641
5453
|
updateStatus(ctx);
|
|
3642
5454
|
return toolPayload({
|
|
3643
5455
|
version: 1,
|
|
3644
5456
|
action,
|
|
3645
5457
|
ids,
|
|
5458
|
+
protectedRefs: refsAfter,
|
|
3646
5459
|
activation: "durable immediately; projected on the next model call in this same turn",
|
|
3647
5460
|
});
|
|
3648
5461
|
}
|
|
@@ -3714,172 +5527,135 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3714
5527
|
}
|
|
3715
5528
|
}
|
|
3716
5529
|
const corrections = resolved.flatMap((item) => item.corrections);
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
(candidate.
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
? await prepareFold({
|
|
3747
|
-
candidate,
|
|
3748
|
-
snapshot,
|
|
3749
|
-
state: staged,
|
|
3750
|
-
generation: lifecycle.generation,
|
|
3751
|
-
brief: item.brief,
|
|
3752
|
-
summarize: options.summarizeContextSpan,
|
|
3753
|
-
ctx,
|
|
3754
|
-
signal,
|
|
3755
|
-
})
|
|
3756
|
-
: null;
|
|
3757
|
-
const mark = foldMarkFor({
|
|
5530
|
+
// Resolve every brief now, while the source is in hand, so the commit epoch
|
|
5531
|
+
// itself stays deterministic and free of provider calls. A span that is not
|
|
5532
|
+
// eligible YET cannot go through preparation at all, so it takes the
|
|
5533
|
+
// deterministic brief: the mark is a decision, and refusing to record it
|
|
5534
|
+
// because the span is momentarily fresh is exactly the drop being fixed.
|
|
5535
|
+
const marks: Array<Record<string, unknown>> = [];
|
|
5536
|
+
let staged = persistence.state;
|
|
5537
|
+
for (const item of resolved) {
|
|
5538
|
+
const { candidate } = item;
|
|
5539
|
+
// ACCEPT AND HOLD. A span that is still fresh or protected is not a refusal:
|
|
5540
|
+
// the mark is a standing decision, it is recorded now, and it folds at the
|
|
5541
|
+
// first commit after the span ages out. Refusal is reserved for a mark that
|
|
5542
|
+
// cannot be constructed at all.
|
|
5543
|
+
// A REPEATED DECISION IS INERT. The fold id is derived from the span, so a
|
|
5544
|
+
// span the ladder already prepared carries that exact id in the forest
|
|
5545
|
+
// already, and preparing it a second time throws "Prepared fold already
|
|
5546
|
+
// exists" out of the tool call. Under the old cadence commits were frequent
|
|
5547
|
+
// enough to drain prepared folds before a second decision could land on one;
|
|
5548
|
+
// the quiet runtime commits far less often, so the collision became reachable
|
|
5549
|
+
// in ordinary use -- the ladder marks a span, the agent then folds the same
|
|
5550
|
+
// span, and the agent's call fails. Marking is a standing decision about a
|
|
5551
|
+
// span, and deciding the same thing twice has to be a no-op.
|
|
5552
|
+
const alreadyPrepared = staged.folds.some((fold) =>
|
|
5553
|
+
fold.id === foldIdFor(candidate.kind, candidate.parts));
|
|
5554
|
+
const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
|
|
5555
|
+
(candidate.kind === "tool-result" &&
|
|
5556
|
+
toolRefsProtected(candidate.sourceRefs, staged, snapshot));
|
|
5557
|
+
const briefed = !deferred && !alreadyPrepared
|
|
5558
|
+
? await prepareFold({
|
|
3758
5559
|
candidate,
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
}
|
|
3781
|
-
: {}),
|
|
3782
|
-
brief: mark.brief,
|
|
3783
|
-
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3784
|
-
});
|
|
5560
|
+
snapshot,
|
|
5561
|
+
state: staged,
|
|
5562
|
+
generation: lifecycle.generation,
|
|
5563
|
+
brief: item.brief,
|
|
5564
|
+
summarize: observedSummarize,
|
|
5565
|
+
ctx,
|
|
5566
|
+
signal,
|
|
5567
|
+
})
|
|
5568
|
+
: null;
|
|
5569
|
+
const mark = foldMarkFor({
|
|
5570
|
+
candidate,
|
|
5571
|
+
brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
|
|
5572
|
+
briefProvenance: briefed?.fold.provenance ??
|
|
5573
|
+
(item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
|
|
5574
|
+
origin: "agent",
|
|
5575
|
+
ordinal: markOrdinal(snapshot),
|
|
5576
|
+
});
|
|
5577
|
+
const addition = addPendingMark(staged, mark);
|
|
5578
|
+
if (!addition.added) {
|
|
5579
|
+
marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
|
|
5580
|
+
continue;
|
|
3785
5581
|
}
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
const accounting = markAccounting(snapshot, persistence.state);
|
|
3789
|
-
// MECHANISM 3. The projection is byte-frozen between fold events, so this reply
|
|
3790
|
-
// is the only cache-free channel left: it carries the whole picture, not just
|
|
3791
|
-
// an acknowledgement of the spans this call named.
|
|
3792
|
-
const held = pendingMarks(persistence.state).map((mark) => ({
|
|
5582
|
+
staged = addition.state;
|
|
5583
|
+
marks.push({
|
|
3793
5584
|
id: mark.id,
|
|
3794
5585
|
kind: mark.kind,
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
// The single-span shape is the head of the batched one: one call carrying one
|
|
3799
|
-
// span answers exactly as it always did, and a batch adds fields rather than
|
|
3800
|
-
// replacing them.
|
|
3801
|
-
const only = marks.length === 1 && marks[0].ok === true ? marks[0] : null;
|
|
3802
|
-
return toolPayload({
|
|
3803
|
-
version: 1,
|
|
3804
|
-
action,
|
|
3805
|
-
scheduling: "epoch",
|
|
3806
|
-
ok: marks.some((mark) => mark.ok === true),
|
|
3807
|
-
deferredMarks: marks.filter((mark) => mark.deferred === true).length,
|
|
3808
|
-
...(only
|
|
5586
|
+
ok: true,
|
|
5587
|
+
deferred,
|
|
5588
|
+
...(deferred
|
|
3809
5589
|
? {
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
brief: only.brief,
|
|
3813
|
-
provenance: only.provenance,
|
|
3814
|
-
deferred: only.deferred,
|
|
3815
|
-
...(only.scheduled ? { scheduled: only.scheduled } : {}),
|
|
5590
|
+
scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
|
|
5591
|
+
"first commit after it ages out",
|
|
3816
5592
|
}
|
|
3817
5593
|
: {}),
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
corrections,
|
|
3821
|
-
argumentsSha256: executionArgumentsSha256,
|
|
3822
|
-
durableRevision: persistence.state.revision,
|
|
3823
|
-
pendingMarks: accounting.pending,
|
|
3824
|
-
eligibleMarks: accounting.eligibleMarks,
|
|
3825
|
-
retainedMarks: accounting.retainedMarks,
|
|
3826
|
-
estimatedFreedWindowShare: accounting.freedWindowShare,
|
|
3827
|
-
estimatedEligibleWindowShare: accounting.eligibleFreedWindowShare,
|
|
3828
|
-
estimatedRewriteTokens: accounting.rewriteTokens,
|
|
3829
|
-
held,
|
|
3830
|
-
heldNote: "held until it ages out or the next fold event",
|
|
3831
|
-
unmarkedSpans: remainder.spans,
|
|
3832
|
-
unmarkedTokens: remainder.tokens,
|
|
3833
|
-
unmarkedShare: remainder.share,
|
|
3834
|
-
unmarkedCandidates: remainder.candidates,
|
|
3835
|
-
awareness: markAwarenessText({ held, remainder, toolName, brandNoun }),
|
|
3836
|
-
activation: "accepted as pending marks; no context bytes moved, and nothing else in your " +
|
|
3837
|
-
"context changed either. They apply together at the next commit epoch, which the runtime " +
|
|
3838
|
-
"opens at the fold event. " +
|
|
3839
|
-
"A mark over a still-fresh span is held, not refused: it is scheduled, and it folds at the " +
|
|
3840
|
-
"first commit after that span ages out of the fresh window. " +
|
|
3841
|
-
"Mark several spans in one call: this whole picture comes back with each one.",
|
|
3842
|
-
});
|
|
3843
|
-
}
|
|
3844
|
-
const applied: Array<Record<string, unknown>> = [];
|
|
3845
|
-
for (const item of resolved) {
|
|
3846
|
-
const { preparedFold, nextState } = await prepareAndCommitExplicit({
|
|
3847
|
-
snapshot,
|
|
3848
|
-
candidate: item.candidate,
|
|
3849
|
-
brief: item.brief,
|
|
3850
|
-
ctx,
|
|
3851
|
-
signal,
|
|
3852
|
-
});
|
|
3853
|
-
persistence.state = nextState;
|
|
3854
|
-
applied.push({
|
|
3855
|
-
id: preparedFold.id,
|
|
3856
|
-
kind: preparedFold.fold.kind,
|
|
3857
|
-
brief: preparedFold.fold.brief,
|
|
3858
|
-
provenance: normalizeLegacyProvenance(preparedFold.fold.provenance),
|
|
3859
|
-
expand: { action: "expand", id: preparedFold.id },
|
|
5594
|
+
brief: mark.brief,
|
|
5595
|
+
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3860
5596
|
});
|
|
3861
5597
|
}
|
|
3862
|
-
await persistManual(
|
|
5598
|
+
await persistManual(staged, action, ctx);
|
|
3863
5599
|
updateStatus(ctx);
|
|
3864
|
-
const
|
|
5600
|
+
const accounting = markAccounting(snapshot, persistence.state);
|
|
5601
|
+
// MECHANISM 3. The projection is byte-frozen between fold events, so this reply
|
|
5602
|
+
// is the only cache-free channel left: it carries the whole picture, not just
|
|
5603
|
+
// an acknowledgement of the spans this call named.
|
|
5604
|
+
const held = pendingMarks(persistence.state).map((mark) => ({
|
|
5605
|
+
id: mark.id,
|
|
5606
|
+
kind: mark.kind,
|
|
5607
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
5608
|
+
}));
|
|
5609
|
+
const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
|
|
5610
|
+
// The single-span shape is the head of the batched one: one call carrying one
|
|
5611
|
+
// span answers exactly as it always did, and a batch adds fields rather than
|
|
5612
|
+
// replacing them.
|
|
5613
|
+
const only = marks.length === 1 && marks[0].ok === true ? marks[0] : null;
|
|
3865
5614
|
return toolPayload({
|
|
3866
5615
|
version: 1,
|
|
3867
5616
|
action,
|
|
3868
|
-
scheduling: "
|
|
3869
|
-
|
|
5617
|
+
scheduling: "epoch",
|
|
5618
|
+
ok: marks.some((mark) => mark.ok === true),
|
|
5619
|
+
deferredMarks: marks.filter((mark) => mark.deferred === true).length,
|
|
5620
|
+
...(only
|
|
3870
5621
|
? {
|
|
3871
|
-
id:
|
|
3872
|
-
kind:
|
|
3873
|
-
brief:
|
|
3874
|
-
provenance:
|
|
3875
|
-
|
|
5622
|
+
id: only.id,
|
|
5623
|
+
kind: only.kind,
|
|
5624
|
+
brief: only.brief,
|
|
5625
|
+
provenance: only.provenance,
|
|
5626
|
+
deferred: only.deferred,
|
|
5627
|
+
...(only.scheduled ? { scheduled: only.scheduled } : {}),
|
|
3876
5628
|
}
|
|
3877
5629
|
: {}),
|
|
3878
|
-
|
|
5630
|
+
marks,
|
|
5631
|
+
// Never silently reinterpreted: a span the runtime moved says so, here.
|
|
3879
5632
|
corrections,
|
|
3880
5633
|
argumentsSha256: executionArgumentsSha256,
|
|
3881
5634
|
durableRevision: persistence.state.revision,
|
|
3882
|
-
|
|
5635
|
+
pendingMarks: accounting.pending,
|
|
5636
|
+
eligibleMarks: accounting.eligibleMarks,
|
|
5637
|
+
retainedMarks: accounting.retainedMarks,
|
|
5638
|
+
estimatedFreedWindowShare: accounting.freedBudgetShare,
|
|
5639
|
+
estimatedEligibleWindowShare: accounting.eligibleFreedBudgetShare,
|
|
5640
|
+
estimatedRewriteTokens: accounting.rewriteTokens,
|
|
5641
|
+
held,
|
|
5642
|
+
heldNote: "held until it ages out or the next fold event",
|
|
5643
|
+
unmarkedSpans: remainder.spans,
|
|
5644
|
+
unmarkedTokens: remainder.tokens,
|
|
5645
|
+
unmarkedShare: remainder.share,
|
|
5646
|
+
unmarkedCandidates: remainder.candidates,
|
|
5647
|
+
// The acknowledgement rides the tool result, so it PERSISTS in context the way
|
|
5648
|
+
// every tool result does. Default on;
|
|
5649
|
+
// `guidance.actionResponses: false` returns the accounting without the prose.
|
|
5650
|
+
...(guidance.actionResponses ? {
|
|
5651
|
+
awareness: markAwarenessText({ held, remainder, toolName, brandNoun }),
|
|
5652
|
+
activation: "accepted as pending marks; no context bytes moved, and nothing else in your " +
|
|
5653
|
+
"context changed either. They apply together at the next commit epoch, which the runtime " +
|
|
5654
|
+
"opens at the fold event. " +
|
|
5655
|
+
"A mark over a still-fresh span is held, not refused: it is scheduled, and it folds at the " +
|
|
5656
|
+
"first commit after that span ages out of the fresh window. " +
|
|
5657
|
+
"Mark several spans in one call: this whole picture comes back with each one.",
|
|
5658
|
+
} : {}),
|
|
3883
5659
|
});
|
|
3884
5660
|
}
|
|
3885
5661
|
throw new Error(`Unknown ${toolName} action '${action}'`);
|
|
@@ -3899,7 +5675,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3899
5675
|
};
|
|
3900
5676
|
|
|
3901
5677
|
const toolHandler = async (
|
|
3902
|
-
|
|
5678
|
+
toolCallId: string,
|
|
3903
5679
|
params: Record<string, unknown>,
|
|
3904
5680
|
signal: AbortSignal,
|
|
3905
5681
|
_onUpdate: unknown,
|
|
@@ -3915,6 +5691,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3915
5691
|
action,
|
|
3916
5692
|
ok,
|
|
3917
5693
|
error,
|
|
5694
|
+
// The join key against the worker's own tool-call log: one logical row per
|
|
5695
|
+
// model-emitted invocation, matched by id and never by order or clock.
|
|
5696
|
+
tool_call_id: toolCallId,
|
|
3918
5697
|
marks_requested: requestedMarkCount(params),
|
|
3919
5698
|
corrections_applied: corrections.length,
|
|
3920
5699
|
arguments_sha256: sha256Value(params ?? {}),
|
|
@@ -3972,11 +5751,39 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3972
5751
|
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
3973
5752
|
const divider = args.indexOf(" -- ");
|
|
3974
5753
|
const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
|
|
5754
|
+
// THE USER COMMIT. One word, and the only commit verb any caller has: the model
|
|
5755
|
+
// tool still has no commit action, because a verb the runtime is entitled to
|
|
5756
|
+
// overrule is surface without function. A user is not overruled.
|
|
5757
|
+
//
|
|
5758
|
+
// Below maxTarget it applies the eligible marks in hand and stops -- no automatic
|
|
5759
|
+
// top-up, because the user asked to bank the curation they made, not to have the
|
|
5760
|
+
// runtime pick more. At or above maxTarget it is the ordinary event: the planner
|
|
5761
|
+
// may top up toward minTarget. Neither path touches freshness, pins, exactness or
|
|
5762
|
+
// the provider fence.
|
|
5763
|
+
if (selector === "commit") {
|
|
5764
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
5765
|
+
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
5766
|
+
? capacity.usedTokens / capacity.budgetTokens
|
|
5767
|
+
: null;
|
|
5768
|
+
const topUp = occupancy !== null && occupancy >= thresholds.maxTarget;
|
|
5769
|
+
const committed = await runCommitEpoch(snapshot, "user-command", topUp, measurements.latestRatio, true);
|
|
5770
|
+
try { await persist(ctx); } catch { /* The commit already reported its own outcome. */ }
|
|
5771
|
+
updateStatus(ctx);
|
|
5772
|
+
safeNotify(
|
|
5773
|
+
ctx,
|
|
5774
|
+
committed
|
|
5775
|
+
? `Committed ${ownValue(committed, "applied_marks") ?? 0} mark(s)${topUp ? " with automatic top-up" : ""}. ` +
|
|
5776
|
+
"Exact source remains expandable."
|
|
5777
|
+
: "Nothing eligible to commit: no pending mark is outside the fresh tail.",
|
|
5778
|
+
"info",
|
|
5779
|
+
);
|
|
5780
|
+
return;
|
|
5781
|
+
}
|
|
3975
5782
|
const supplied = (divider >= 0 ? args.slice(divider + 4) : "").trim() || undefined;
|
|
3976
5783
|
const ids = selector ? selector.replace(/\.\./g, " ").split(/[\s,]+/).filter(Boolean) : [];
|
|
3977
5784
|
const candidate = ids.length
|
|
3978
5785
|
? manualFoldCandidate(snapshot, persistence.state!, ids)
|
|
3979
|
-
: selectAutomaticChapter(snapshot, persistence.state!) ?? selectAutomaticToolBatch(snapshot, persistence.state
|
|
5786
|
+
: selectAutomaticChapter(snapshot, persistence.state!) ?? selectAutomaticToolBatch(snapshot, persistence.state!)[0] ?? null;
|
|
3980
5787
|
if (!candidate) throw new Error("No exact stale rescue span is currently eligible");
|
|
3981
5788
|
const stateBefore = persistence.state!;
|
|
3982
5789
|
const persistedBefore = persistence.persisted ? clone(persistence.persisted) : null;
|
|
@@ -4027,7 +5834,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4027
5834
|
name: toolName,
|
|
4028
5835
|
label: toolLabel,
|
|
4029
5836
|
allowedActions: allowedToolActions,
|
|
4030
|
-
fullSurface:
|
|
5837
|
+
fullSurface: true,
|
|
4031
5838
|
maxBriefChars: ACTIVE_CONTEXT_POLICY.maxBriefChars,
|
|
4032
5839
|
statusDetails: ["fold_candidates", "tree", "folds", "objects"],
|
|
4033
5840
|
minPeekSliceBytes: PEEK_MIN_SLICE_BYTES,
|
|
@@ -4049,6 +5856,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4049
5856
|
lifecycle.generation += 1;
|
|
4050
5857
|
lifecycle.shuttingDown = true;
|
|
4051
5858
|
cancelPreparation();
|
|
5859
|
+
cancelBriefUpgrades();
|
|
4052
5860
|
persistence.state = null;
|
|
4053
5861
|
persistence.persisted = null;
|
|
4054
5862
|
lifecycle.latestSnapshot = null;
|
|
@@ -4056,5 +5864,5 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4056
5864
|
try { ctx.ui?.setStatus?.(entryTypePrefix, undefined); } catch { /* Shutdown cannot be blocked by UI. */ }
|
|
4057
5865
|
});
|
|
4058
5866
|
|
|
4059
|
-
return { projectionCandidates
|
|
5867
|
+
return { projectionCandidates };
|
|
4060
5868
|
}
|