pi-fold 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -7
- package/extensions/active-context.ts +2252 -182
- package/extensions/lib/advisory.ts +171 -14
- package/extensions/lib/canonical.ts +20 -0
- package/extensions/lib/curation.ts +437 -0
- package/extensions/lib/folding.ts +468 -22
- package/extensions/lib/instrumentation.ts +333 -0
- package/extensions/lib/measurement.ts +111 -0
- package/extensions/lib/persistence.ts +158 -2
- package/extensions/lib/policy.ts +380 -4
- package/extensions/lib/scheduling.ts +923 -0
- package/extensions/lib/selection.ts +437 -12
- package/extensions/lib/surfacing.ts +349 -0
- package/extensions/lib/tool-surface.ts +52 -3
- package/extensions/lib/transcript.ts +58 -4
- package/package.json +2 -2
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
denseOwnArrayValues,
|
|
3
|
+
evidenceSha256,
|
|
3
4
|
objectRefKey,
|
|
4
5
|
sha256Value,
|
|
5
6
|
stableStringify,
|
|
6
7
|
} from "./json.ts";
|
|
7
8
|
import {
|
|
8
|
-
advisorySchedule,
|
|
9
|
-
advisoryState,
|
|
10
|
-
clearArmedAdvisory,
|
|
11
|
-
liveAdvisoryText,
|
|
12
|
-
milestoneText,
|
|
13
|
-
updateAdvisoryMilestone,
|
|
14
9
|
} from "./lib/advisory.ts";
|
|
15
10
|
import {
|
|
16
11
|
bytes,
|
|
@@ -22,9 +17,13 @@ import {
|
|
|
22
17
|
import {
|
|
23
18
|
activeContextStatus,
|
|
24
19
|
automaticPreparationId,
|
|
20
|
+
boundStatusPayload,
|
|
25
21
|
commitPreparedFold,
|
|
22
|
+
descendantIds,
|
|
26
23
|
encodedFoldSource,
|
|
27
24
|
foldCandidatesDetail,
|
|
25
|
+
foldTreeDetail,
|
|
26
|
+
peekFoldSource,
|
|
28
27
|
prepareFold,
|
|
29
28
|
preparedFoldError,
|
|
30
29
|
projectActiveContext,
|
|
@@ -37,8 +36,10 @@ import {
|
|
|
37
36
|
withExpandLease,
|
|
38
37
|
} from "./lib/folding.ts";
|
|
39
38
|
import {
|
|
39
|
+
admissionVerdict,
|
|
40
40
|
boundReceiptText,
|
|
41
41
|
boundedInteger,
|
|
42
|
+
capacityAccounting,
|
|
42
43
|
contextUsageRatio,
|
|
43
44
|
contextWindowFor,
|
|
44
45
|
hardFenceRatio,
|
|
@@ -49,7 +50,10 @@ import {
|
|
|
49
50
|
parseProviderContextMeasurementReceipt,
|
|
50
51
|
persistenceProjection,
|
|
51
52
|
providerContextMeasurement,
|
|
53
|
+
providerTokens,
|
|
54
|
+
refsProtected,
|
|
52
55
|
stringIds,
|
|
56
|
+
toolRefsProtected,
|
|
53
57
|
toolPayload,
|
|
54
58
|
} from "./lib/measurement.ts";
|
|
55
59
|
import type {
|
|
@@ -59,7 +63,12 @@ import type {
|
|
|
59
63
|
ProviderMeasurementAnchor,
|
|
60
64
|
} from "./lib/measurement.ts";
|
|
61
65
|
import {
|
|
66
|
+
childFoldIds,
|
|
62
67
|
clearPrepared,
|
|
68
|
+
deriveFoldParents,
|
|
69
|
+
flattenFoldRefs,
|
|
70
|
+
foldBrief,
|
|
71
|
+
foldIdFor,
|
|
63
72
|
makeFoldRecordEntry,
|
|
64
73
|
makeStateCheckpoint,
|
|
65
74
|
makeStateDelta,
|
|
@@ -76,7 +85,6 @@ import type { MaterializedStatePersistence } from "./lib/persistence.ts";
|
|
|
76
85
|
import {
|
|
77
86
|
activeContextSource,
|
|
78
87
|
ACTIVE_CONTEXT_POLICY,
|
|
79
|
-
ACTIVE_CONTEXT_STATUS_KEY,
|
|
80
88
|
ACTIVE_CONTEXT_TOOL_ACTIONS,
|
|
81
89
|
contextBrand,
|
|
82
90
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
@@ -84,8 +92,25 @@ import {
|
|
|
84
92
|
DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
|
|
85
93
|
DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL,
|
|
86
94
|
DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
95
|
+
COMMIT_RECLAIM_FLOOR_SHARE,
|
|
96
|
+
CURATION_OCCUPANCY_SHARE,
|
|
97
|
+
CURATION_STALE_TOOL_SHARE,
|
|
98
|
+
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
99
|
+
CURATION_TARGET_OCCUPANCY_SHARE,
|
|
87
100
|
DEFAULT_CONTEXT_WINDOW,
|
|
101
|
+
DEFAULT_FOLD_SCHEDULING,
|
|
102
|
+
ESTIMATED_BYTES_PER_TOKEN,
|
|
88
103
|
entryTypeNamespace,
|
|
104
|
+
EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS,
|
|
105
|
+
EPOCH_COMMIT_TARGET_WINDOW_SHARE,
|
|
106
|
+
MAX_FOLD_SPAN_CHARS,
|
|
107
|
+
MAX_WEDGE_ABSORB_TOKENS,
|
|
108
|
+
OVERFLOW_RECOVERY_MAX_ATTEMPTS,
|
|
109
|
+
PEEK_DEFAULT_MAX_BYTES,
|
|
110
|
+
PEEK_MIN_SLICE_BYTES,
|
|
111
|
+
PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
112
|
+
FOLD_SCHEDULING_MODES,
|
|
113
|
+
READ_ONLY_CONTEXT_ACTIONS_DEFAULT,
|
|
89
114
|
READ_ONLY_TOOLS_DEFAULT,
|
|
90
115
|
USER_RESCUE_MAX_SOURCE_CHARS,
|
|
91
116
|
} from "./lib/policy.ts";
|
|
@@ -93,36 +118,152 @@ import type {
|
|
|
93
118
|
ActiveContextSnapshot,
|
|
94
119
|
ActiveContextState,
|
|
95
120
|
ActiveContextToolAction,
|
|
96
|
-
AdvisoryMilestone,
|
|
97
121
|
FoldCandidate,
|
|
98
122
|
FoldKind,
|
|
99
123
|
FoldRecordEntry,
|
|
124
|
+
FoldSchedulingMode,
|
|
100
125
|
PreparedFold,
|
|
126
|
+
SuggestionSource,
|
|
101
127
|
} from "./lib/policy.ts";
|
|
128
|
+
import {
|
|
129
|
+
absorbWedgeMarks,
|
|
130
|
+
} from "./lib/scheduling.ts";
|
|
131
|
+
import {
|
|
132
|
+
contextReceipt,
|
|
133
|
+
curationSignals,
|
|
134
|
+
curationTriggerFires,
|
|
135
|
+
markAwarenessText,
|
|
136
|
+
receiptBlockText,
|
|
137
|
+
withReceipt,
|
|
138
|
+
} from "./lib/curation.ts";
|
|
139
|
+
import type {
|
|
140
|
+
ContextReceipt,
|
|
141
|
+
CurationSignals,
|
|
142
|
+
} from "./lib/curation.ts";
|
|
143
|
+
import {
|
|
144
|
+
addPendingMark,
|
|
145
|
+
claimedRefKeys,
|
|
146
|
+
commitPendingMarks,
|
|
147
|
+
ephemeralPeekMarks,
|
|
148
|
+
epochCommitDue,
|
|
149
|
+
estimatedTokens,
|
|
150
|
+
foldMarkFor,
|
|
151
|
+
ladderSelectionMark,
|
|
152
|
+
ladderBrief,
|
|
153
|
+
markAccounting,
|
|
154
|
+
markEligibility,
|
|
155
|
+
markFreedBytes,
|
|
156
|
+
unmarkedRemainder,
|
|
157
|
+
guardWaiverCount,
|
|
158
|
+
markedFoldIds,
|
|
159
|
+
markOrdinal,
|
|
160
|
+
pendingMarks,
|
|
161
|
+
markTouchesCurrentTurn,
|
|
162
|
+
schedulingStatus,
|
|
163
|
+
topUpMarks,
|
|
164
|
+
withPendingMarks,
|
|
165
|
+
} from "./lib/scheduling.ts";
|
|
102
166
|
import {
|
|
103
167
|
automaticToolBrief,
|
|
168
|
+
candidateSpanChars,
|
|
104
169
|
deterministicChapterCandidateBrief,
|
|
105
170
|
deterministicConsolidationBrief,
|
|
106
171
|
manualFoldCandidate,
|
|
107
172
|
selectAutomaticToolBatch,
|
|
108
173
|
selectAutomaticToolForRung,
|
|
174
|
+
snapFoldCandidate,
|
|
175
|
+
snapToFoldBoundaries,
|
|
176
|
+
splitCandidateBySize,
|
|
109
177
|
} from "./lib/selection.ts";
|
|
178
|
+
import type { SpanCorrection } from "./lib/selection.ts";
|
|
179
|
+
import {
|
|
180
|
+
acceptSurfacingSuggestion,
|
|
181
|
+
surfacingOrdinal,
|
|
182
|
+
} from "./lib/surfacing.ts";
|
|
183
|
+
import {
|
|
184
|
+
compareProjections,
|
|
185
|
+
emptyLedger,
|
|
186
|
+
ledgerSummary,
|
|
187
|
+
messageDigests,
|
|
188
|
+
observeCacheUsage,
|
|
189
|
+
prefixDivergence,
|
|
190
|
+
recordContextEvent,
|
|
191
|
+
recordProjection,
|
|
192
|
+
} from "./lib/instrumentation.ts";
|
|
193
|
+
import type {
|
|
194
|
+
ContextEvent,
|
|
195
|
+
ContextEventKind,
|
|
196
|
+
ProjectionChange,
|
|
197
|
+
} from "./lib/instrumentation.ts";
|
|
110
198
|
import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
|
|
111
|
-
import {
|
|
199
|
+
import {
|
|
200
|
+
currentTurnRefKeys,
|
|
201
|
+
mapActiveContext,
|
|
202
|
+
toolFreshIndices,
|
|
203
|
+
} from "./lib/transcript.ts";
|
|
112
204
|
|
|
113
205
|
// Test seam only; the package API is registerPiFold because package.json exports block deep imports.
|
|
114
206
|
export * from "./lib/advisory.ts";
|
|
115
207
|
export * from "./lib/canonical.ts";
|
|
208
|
+
export * from "./lib/curation.ts";
|
|
116
209
|
export * from "./lib/folding.ts";
|
|
210
|
+
export * from "./lib/instrumentation.ts";
|
|
117
211
|
export * from "./lib/measurement.ts";
|
|
118
212
|
export * from "./lib/persistence.ts";
|
|
119
213
|
export * from "./lib/policy.ts";
|
|
214
|
+
export * from "./lib/scheduling.ts";
|
|
120
215
|
export * from "./lib/selection.ts";
|
|
216
|
+
export * from "./lib/surfacing.ts";
|
|
121
217
|
export * from "./lib/transcript.ts";
|
|
122
218
|
|
|
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
|
+
/**
|
|
228
|
+
* The batched fold request: several {span, brief} pairs in one call, with the single
|
|
229
|
+
* `ids` form still accepted because it is the shape a status action hands back.
|
|
230
|
+
*/
|
|
231
|
+
export interface BatchedMarkRequest {
|
|
232
|
+
ids: string[];
|
|
233
|
+
brief?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function batchedMarkRequests(params: Record<string, unknown>): BatchedMarkRequest[] {
|
|
237
|
+
const batched = params.marks;
|
|
238
|
+
if (batched === undefined) {
|
|
239
|
+
return [{
|
|
240
|
+
ids: stringIds(params.ids),
|
|
241
|
+
...(typeof params.brief === "string" && params.brief.trim() ? { brief: params.brief } : {}),
|
|
242
|
+
}];
|
|
243
|
+
}
|
|
244
|
+
const values = denseOwnArrayValues(batched);
|
|
245
|
+
if (!values || !values.length || values.length > 64) {
|
|
246
|
+
throw new Error("marks must be a dense array of 1-64 {ids, brief} objects");
|
|
247
|
+
}
|
|
248
|
+
return values.map((value, index) => {
|
|
249
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
250
|
+
throw new Error(`marks[${index}] must be an object with ids and an optional brief`);
|
|
251
|
+
}
|
|
252
|
+
const brief = ownValue(value, "brief");
|
|
253
|
+
if (brief !== undefined && (typeof brief !== "string" || !brief.trim())) {
|
|
254
|
+
throw new Error(`marks[${index}].brief must be a nonempty string`);
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
ids: stringIds(ownValue(value, "ids")),
|
|
258
|
+
...(typeof brief === "string" && brief.trim() ? { brief } : {}),
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
123
263
|
export function registerActiveContext(pi: any, options: {
|
|
124
264
|
summarizeContextSpan?: (request: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;
|
|
125
265
|
setProjectionProvider?: (provider: (ctx: any) => Array<Record<string, unknown>>) => void;
|
|
266
|
+
setSuggestionSourceRegistrar?: (register: SuggestionSourceRegistrar) => void;
|
|
126
267
|
toolActions?: readonly ActiveContextToolAction[];
|
|
127
268
|
toolName?: string;
|
|
128
269
|
toolLabel?: string;
|
|
@@ -132,13 +273,43 @@ export function registerActiveContext(pi: any, options: {
|
|
|
132
273
|
commandNames?: { status?: string; fold?: string };
|
|
133
274
|
readOnlyTools?: ReadonlySet<string>;
|
|
134
275
|
blockingTools?: readonly string[];
|
|
135
|
-
|
|
276
|
+
foldScheduling?: FoldSchedulingMode;
|
|
277
|
+
foldPeekResults?: boolean;
|
|
278
|
+
providerTotalWindow?: number;
|
|
279
|
+
}): {
|
|
280
|
+
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
281
|
+
registerSuggestionSource: SuggestionSourceRegistrar;
|
|
282
|
+
} {
|
|
136
283
|
const toolName = options.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME;
|
|
137
284
|
const toolLabel = options.toolLabel ?? DEFAULT_ACTIVE_CONTEXT_TOOL_LABEL;
|
|
138
285
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
139
286
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
140
287
|
const commandPrefix = options.commandPrefix ?? "";
|
|
141
288
|
const readOnlyTools = options.readOnlyTools ?? READ_ONLY_TOOLS_DEFAULT;
|
|
289
|
+
const foldScheduling = options.foldScheduling ?? DEFAULT_FOLD_SCHEDULING;
|
|
290
|
+
if (!FOLD_SCHEDULING_MODES.includes(foldScheduling)) {
|
|
291
|
+
throw new Error(`foldScheduling must be one of ${FOLD_SCHEDULING_MODES.join(", ")}`);
|
|
292
|
+
}
|
|
293
|
+
const epochScheduling = foldScheduling === "epoch";
|
|
294
|
+
if (options.foldPeekResults !== undefined && typeof options.foldPeekResults !== "boolean") {
|
|
295
|
+
throw new Error("foldPeekResults must be a boolean");
|
|
296
|
+
}
|
|
297
|
+
// Peek mutates nothing, so its own tool result is a foldable read batch. Epoch mode
|
|
298
|
+
// adopts that classification with its scheduling; immediate mode keeps the narrower
|
|
299
|
+
// one unless the deployment opts in, because a peek copies a fold's stored source
|
|
300
|
+
// back into the window and a window that cannot reclaim it grows without bound.
|
|
301
|
+
const foldPeekResults = options.foldPeekResults ?? epochScheduling;
|
|
302
|
+
if (options.providerTotalWindow !== undefined &&
|
|
303
|
+
(!Number.isSafeInteger(options.providerTotalWindow) || options.providerTotalWindow <= 0)) {
|
|
304
|
+
throw new Error("providerTotalWindow must be a positive integer");
|
|
305
|
+
}
|
|
306
|
+
// The deployment's own fact, and the only capacity knob: declaring it makes every
|
|
307
|
+
// ratio, fence and budget truthful, and leaving it out falls back to the provider
|
|
308
|
+
// descriptor and SAYS "descriptor" in the capacity accounting.
|
|
309
|
+
const providerTotalWindow = options.providerTotalWindow ?? null;
|
|
310
|
+
const readOnlyContextActions = foldPeekResults
|
|
311
|
+
? PEEK_READ_ONLY_CONTEXT_ACTIONS
|
|
312
|
+
: READ_ONLY_CONTEXT_ACTIONS_DEFAULT;
|
|
142
313
|
if (!toolName || !toolLabel || !brandNoun || !entryTypePrefix || typeof commandPrefix !== "string" ||
|
|
143
314
|
(commandPrefix && !/^[a-z0-9-]+$/.test(commandPrefix)) ||
|
|
144
315
|
[...readOnlyTools].some((name) => typeof name !== "string" || !name)) {
|
|
@@ -166,12 +337,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
166
337
|
const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
|
|
167
338
|
const milestoneProjectionType = `${entryTypePrefix}-milestone`;
|
|
168
339
|
const advisoryProjectionType = `${entryTypePrefix}-advisory`;
|
|
340
|
+
const surfacingProjectionType = `${entryTypePrefix}-surfacing`;
|
|
341
|
+
const receiptProjectionType = `${entryTypePrefix}-receipts`;
|
|
342
|
+
const curationProjectionType = `${entryTypePrefix}-curation`;
|
|
169
343
|
const entryNamespace = entryTypeNamespace(entryTypePrefix);
|
|
170
344
|
const providerMeasurementEntryType = `${entryNamespace}-provider-context-measurement`;
|
|
171
345
|
const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
|
|
346
|
+
const contextEventEntryType = `${entryNamespace}-context-event`;
|
|
172
347
|
const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
|
|
348
|
+
// The commit verb only exists where marks exist; immediate mode keeps its exact
|
|
349
|
+
// seven-action surface and description.
|
|
350
|
+
const defaultToolActions = epochScheduling
|
|
351
|
+
? EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS
|
|
352
|
+
: ACTIVE_CONTEXT_TOOL_ACTIONS;
|
|
173
353
|
const configuredToolActions = denseOwnArrayValues(
|
|
174
|
-
options.toolActions ??
|
|
354
|
+
options.toolActions ?? defaultToolActions,
|
|
175
355
|
);
|
|
176
356
|
if (!configuredToolActions || configuredToolActions.length < 1) {
|
|
177
357
|
throw new Error("Active-context tool actions must be one non-empty dense array");
|
|
@@ -180,7 +360,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
180
360
|
const allowedToolActionSet = new Set<string>();
|
|
181
361
|
for (const value of configuredToolActions) {
|
|
182
362
|
if (typeof value !== "string" ||
|
|
183
|
-
!
|
|
363
|
+
!EPOCH_ACTIVE_CONTEXT_TOOL_ACTIONS.includes(value as ActiveContextToolAction) ||
|
|
184
364
|
allowedToolActionSet.has(value)) {
|
|
185
365
|
throw new Error(`Invalid or duplicate active-context tool action '${String(value)}'`);
|
|
186
366
|
}
|
|
@@ -221,6 +401,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
221
401
|
lastSelectionSourceIds: [] as string[],
|
|
222
402
|
pendingContextNote: null as string | null,
|
|
223
403
|
lastAutomaticAction: null as Record<string, unknown> | null,
|
|
404
|
+
/** What the last over-budget projection cost and whether the reduction saved it. */
|
|
405
|
+
overBudgetReduction: null as Record<string, unknown> | null,
|
|
224
406
|
automaticFailure: null as AutomaticFailureState | null,
|
|
225
407
|
failedPreparations: new Set<string>(),
|
|
226
408
|
actionQueue: Promise.resolve<unknown>(undefined),
|
|
@@ -230,6 +412,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
230
412
|
const measurements = {
|
|
231
413
|
latestRatio: null as number | null,
|
|
232
414
|
lastProviderMeasurement: null as ProviderContextMeasurement | null,
|
|
415
|
+
/** What the host/model descriptor last claimed, kept so the gap stays auditable. */
|
|
416
|
+
descriptorWindow: null as number | null,
|
|
417
|
+
/** Serialized size of the projection this process last handed the host. */
|
|
418
|
+
lastProjectedChars: null as number | null,
|
|
419
|
+
/**
|
|
420
|
+
* Ground truth for the transmission fence: recent pairings of a projection's size
|
|
421
|
+
* with the token count the provider reported for it. A fixed bytes-per-token
|
|
422
|
+
* constant is a guess about a tokenizer, and it is wrong by different amounts in
|
|
423
|
+
* different sessions -- measured 2026-08-06, 4.7 chars/token in rep11 and 7.0 in
|
|
424
|
+
* rep12 for the SAME workload. It also DRIFTS within one session: rep13 moved
|
|
425
|
+
* between 5.34 and 6.01 over its last twenty requests. Only the recent window
|
|
426
|
+
* counts, and the fence reads it pessimistically.
|
|
427
|
+
*/
|
|
428
|
+
projectionCalibrations: [] as Array<{ chars: number; tokens: number }>,
|
|
429
|
+
/** What the fence estimated for the projection it last handed the host. */
|
|
430
|
+
lastProjectedEstimate: null as number | null,
|
|
431
|
+
/** Whether that estimate used a measured ratio rather than the bootstrap constant. */
|
|
432
|
+
lastProjectedEstimateCalibrated: false,
|
|
433
|
+
/** Signed estimator error against recent measurements, as a share of measured tokens. */
|
|
434
|
+
estimatorErrors: [] as number[],
|
|
435
|
+
/** Growth in measured tokens between consecutive requests. */
|
|
436
|
+
inflowSteps: [] as number[],
|
|
233
437
|
providerMeasurementQueue: Promise.resolve<void>(undefined),
|
|
234
438
|
providerMeasurementReceipts: new Set<string>(),
|
|
235
439
|
providerMeasurementRevisionByMessageSha: new Map<string, number>(),
|
|
@@ -237,16 +441,107 @@ export function registerActiveContext(pi: any, options: {
|
|
|
237
441
|
providerMeasurementAnchorByMessageSha: new Map<string, ProviderMeasurementAnchor>(),
|
|
238
442
|
};
|
|
239
443
|
|
|
444
|
+
// Owns the projection/cache ledger. Nothing here is durable: it is telemetry about
|
|
445
|
+
// this process's own projections, rebuilt from scratch on every load.
|
|
446
|
+
const instrumentation = {
|
|
447
|
+
ledger: emptyLedger(),
|
|
448
|
+
previousDigests: null as string[] | null,
|
|
449
|
+
/** The previous TRANSMITTED projection, for the byte-level prefix comparison. */
|
|
450
|
+
previousText: null as string | null,
|
|
451
|
+
lastChange: "append" as ProjectionChange,
|
|
452
|
+
/** Share of the PREVIOUS prompt the last projection kept, for miss attribution. */
|
|
453
|
+
lastPreservedShare: null as number | null,
|
|
454
|
+
/** Events emitted since the last handoff, which is the attribution candidate set. */
|
|
455
|
+
sinceHandoff: [] as Array<{ seq: number; kind: string }>,
|
|
456
|
+
/** Structural mutations emitted since the last handoff. The per-generation budget. */
|
|
457
|
+
mutationsSinceHandoff: 0,
|
|
458
|
+
/** Provider responses this session, so event SPACING is readable from the stream. */
|
|
459
|
+
requests: 0,
|
|
460
|
+
lastMutationRequest: 0,
|
|
461
|
+
lastMutationTokens: null as number | null,
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* MECHANISM 1. The frozen surface.
|
|
465
|
+
*
|
|
466
|
+
* Between fold events the projection is byte-frozen: the only change a context action
|
|
467
|
+
* may cause is the append of its own tool result at the tail. Rep 17 measured every
|
|
468
|
+
* single context attempt landing as a projection REWRITE -- status 0.60-0.90 prefix
|
|
469
|
+
* identity, peek 0.96, the held gate down to 0.013 -- because the surface answered
|
|
470
|
+
* each one by re-rendering blocks the agent had already paid to cache. Provider prefix
|
|
471
|
+
* caches are positional, so each of those refreshes was a full or partial cache kill.
|
|
472
|
+
*
|
|
473
|
+
* `body` is the last rebuilt fold projection and `projection` is that body plus the
|
|
474
|
+
* carriers appended to it. While the rebuilt body still starts with `body` verbatim,
|
|
475
|
+
* nothing structural moved, so the previous projection is reused WHOLE and only the
|
|
476
|
+
* newly arrived raw messages are appended after it. A carrier whose key is already in
|
|
477
|
+
* the window is never re-rendered; a fold event, a reveal or an overflow recovery
|
|
478
|
+
* diverges the body and takes the sanctioned rewrite.
|
|
479
|
+
*/
|
|
480
|
+
const freeze = {
|
|
481
|
+
body: null as unknown[] | null,
|
|
482
|
+
bodyText: null as string | null,
|
|
483
|
+
projection: null as unknown[] | null,
|
|
484
|
+
keys: new Set<string>(),
|
|
485
|
+
/** True only while assembling a pass that reused the frozen projection. */
|
|
486
|
+
active: false,
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* A carrier may be built exactly once per freeze window. This is checked BEFORE the
|
|
490
|
+
* builder runs, because building a milestone spends its delivery budget and building
|
|
491
|
+
* a reminder advances the reminder cursor: a carrier that cannot land must not pay.
|
|
492
|
+
*/
|
|
493
|
+
const carrierAdmitted = (key: string): boolean => {
|
|
494
|
+
if (freeze.active && freeze.keys.has(key)) return false;
|
|
495
|
+
freeze.keys.add(key);
|
|
496
|
+
return true;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// Owns the guided-curation gate, the receipt ring, the context-event stream, and the
|
|
500
|
+
// overflow recovery lane. Nothing here is durable session STATE: it describes what
|
|
501
|
+
// this process did to the live window, and the durable copy is the appended
|
|
502
|
+
// instrumentation entry, which is what an external adjudicator reads.
|
|
503
|
+
const curation = {
|
|
504
|
+
receipts: [] as ContextReceipt[],
|
|
505
|
+
/** Every context-management tool call this session. */
|
|
506
|
+
contextCalls: 0,
|
|
507
|
+
/** The last measurement the commit trigger took, reported by status. */
|
|
508
|
+
lastSignals: null as CurationSignals | null,
|
|
509
|
+
/**
|
|
510
|
+
* COMMIT REOPEN HYSTERESIS. The eligible foldable share measured just after the last
|
|
511
|
+
* triggered commit, or null when the trigger is free to fire. Measured 2026-08-07
|
|
512
|
+
* (rep 17): occupancy plateaued at 0.85-0.92 of the truthful budget for the final
|
|
513
|
+
* third of the run, and the trigger fired ten times over a window whose foldable mass
|
|
514
|
+
* had not changed. A plateau is not an event. The trigger re-arms only once occupancy
|
|
515
|
+
* falls back under it and crosses again, or once a reclaim floor of eligible mass is
|
|
516
|
+
* NEW. This outlived the announcement it was built beside: it bounds how often the
|
|
517
|
+
* runtime rewrites the projection, which is the whole cost model.
|
|
518
|
+
*/
|
|
519
|
+
reopenBaselineShare: null as number | null,
|
|
520
|
+
/** Recovery attempts spent on the CURRENT inflow; reset by any accepted request. */
|
|
521
|
+
recoveryAttempts: 0,
|
|
522
|
+
/** A provider rejection observed but not yet recovered from. */
|
|
523
|
+
pendingRejection: null as { status: number; ordinal: number } | null,
|
|
524
|
+
lastRecovery: null as Record<string, unknown> | null,
|
|
525
|
+
instrumentationQueue: Promise.resolve<void>(undefined),
|
|
526
|
+
};
|
|
527
|
+
|
|
240
528
|
// Owns advisory arming and hard-fence delivery; durable effects use persistenceQueue.
|
|
241
529
|
const advisory = {
|
|
242
|
-
historicalGuidanceEntries: 0,
|
|
243
|
-
armedMilestone: null as AdvisoryMilestone | null,
|
|
244
|
-
advisoryScheduleKey: null as string | null,
|
|
245
530
|
hardFenceNoticeKey: null as string | null,
|
|
246
531
|
hardFenceReleaseSessionId: null as string | null,
|
|
247
532
|
hardFenceReleasedProjectionKeys: new Set<string>(),
|
|
248
533
|
};
|
|
249
534
|
|
|
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
|
+
|
|
250
545
|
// Owns native-compaction decisions and completion retry state; nativeReceiptQueue serializes it.
|
|
251
546
|
const nativeCompaction = {
|
|
252
547
|
lastThresholdDecision: null as Record<string, unknown> | null,
|
|
@@ -294,6 +589,62 @@ export function registerActiveContext(pi: any, options: {
|
|
|
294
589
|
receipt.tokens === measurement.tokens && receipt.contextWindow === measurement.contextWindow);
|
|
295
590
|
};
|
|
296
591
|
|
|
592
|
+
const currentOrdinal = (): number =>
|
|
593
|
+
lifecycle.latestSnapshot ? markOrdinal(lifecycle.latestSnapshot) : 0;
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Emit one record into THE context event stream, in the ledger AND as a durable
|
|
597
|
+
* session entry. An in-memory ledger is invisible to an analyst reading session
|
|
598
|
+
* artifacts, and telemetry may never block or fail the action it describes.
|
|
599
|
+
*/
|
|
600
|
+
/**
|
|
601
|
+
* Open a pass's mutation budget. ONE structural mutation per pass is the budget: the
|
|
602
|
+
* rep-15 defect was a second commit inside the SAME pass, after the first had already
|
|
603
|
+
* rebuilt the projection, so the counter is armed wherever a pass begins.
|
|
604
|
+
*/
|
|
605
|
+
const beginMutationPass = (): void => { instrumentation.mutationsSinceHandoff = 0; };
|
|
606
|
+
|
|
607
|
+
const emit = (kind: ContextEventKind, payload: Record<string, unknown> = {}): ContextEvent => {
|
|
608
|
+
const record = recordContextEvent(instrumentation.ledger, kind, {
|
|
609
|
+
session_id: persistence.state?.sessionId ?? "",
|
|
610
|
+
ordinal: currentOrdinal(),
|
|
611
|
+
revision: persistence.state?.revision ?? 0,
|
|
612
|
+
at: Date.now(),
|
|
613
|
+
}, payload);
|
|
614
|
+
instrumentation.sinceHandoff.push({ seq: record.seq, kind });
|
|
615
|
+
const operation = curation.instrumentationQueue.then(async () => {
|
|
616
|
+
try { await pi.appendEntry(contextEventEntryType, record); }
|
|
617
|
+
catch { /* Telemetry is never a lifecycle boundary. */ }
|
|
618
|
+
});
|
|
619
|
+
curation.instrumentationQueue = operation.then(() => undefined, () => undefined);
|
|
620
|
+
return record;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Actions of ours that move bytes at or before a prefix position. Receipts, gate
|
|
625
|
+
* notices and suggestions are appended after the whole projection, so they can only
|
|
626
|
+
* move the tail and are never a prefix cause.
|
|
627
|
+
*/
|
|
628
|
+
const PREFIX_MUTATING_KINDS: ReadonlySet<string> = new Set([
|
|
629
|
+
"context.commit", "context.fold", "context.absorb", "context.split", "context.recovery",
|
|
630
|
+
]);
|
|
631
|
+
|
|
632
|
+
const deliverReceipt = (receipt: ContextReceipt): void => {
|
|
633
|
+
curation.receipts = withReceipt(curation.receipts, receipt);
|
|
634
|
+
emit("context.receipt", {
|
|
635
|
+
receipt_kind: receipt.kind,
|
|
636
|
+
trigger: receipt.trigger,
|
|
637
|
+
folds_committed: receipt.foldsCommitted,
|
|
638
|
+
folds_created: receipt.foldsCreated,
|
|
639
|
+
freed_tokens: receipt.freedTokens,
|
|
640
|
+
split_folds: receipt.splitFolds,
|
|
641
|
+
split_from_chars: receipt.splitFromChars,
|
|
642
|
+
absorbed_wedges: receipt.absorbedWedges,
|
|
643
|
+
recovered: receipt.recovered,
|
|
644
|
+
note: receipt.note,
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
|
|
297
648
|
const safeNotify = (ctx: any, message: string, level: "info" | "warning" | "error"): void => {
|
|
298
649
|
try { ctx.ui?.notify?.(message, level); } catch { /* Presentation cannot block Pi lifecycle progress. */ }
|
|
299
650
|
};
|
|
@@ -319,6 +670,42 @@ export function registerActiveContext(pi: any, options: {
|
|
|
319
670
|
} catch { /* Status presentation is request-ephemeral and never a lifecycle boundary. */ }
|
|
320
671
|
};
|
|
321
672
|
|
|
673
|
+
/**
|
|
674
|
+
* The window every ratio, fence and budget is computed against. With truthful
|
|
675
|
+
* capacity on this is the provider's TOTAL admission window, not the per-request
|
|
676
|
+
* max-input descriptor: the descriptor bakes in a full output reservation the
|
|
677
|
+
* deployment never asked for, and treating it as the ceiling aborts requests inside
|
|
678
|
+
* real headroom. The descriptor is still read, and still reported, so the gap stays
|
|
679
|
+
* auditable rather than assumed.
|
|
680
|
+
*/
|
|
681
|
+
const budgetWindowFor = (ctx: any): number | null => {
|
|
682
|
+
// Remembered so the ctx-free callers (trigger, gate, reminders, advisory) report the
|
|
683
|
+
// same descriptor gap the fence does instead of re-reading a second source.
|
|
684
|
+
measurements.descriptorWindow = contextWindowFor(ctx);
|
|
685
|
+
return providerTotalWindow ?? measurements.descriptorWindow;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* THE serving budget. One resolution, one formula, one value.
|
|
690
|
+
*
|
|
691
|
+
* Every consumer -- the curation trigger, the last-call gate, the sparse reminders,
|
|
692
|
+
* the transmission fence, the projection estimator and every instrumentation record --
|
|
693
|
+
* reads this. Measured 2026-08-06 (rep 15): the trigger and gate ran a whole run
|
|
694
|
+
* against a 272,000-token per-request DESCRIPTOR (budget 255,616) while the fence used
|
|
695
|
+
* the truthful 383,616, because the same arithmetic lived in three places. A second
|
|
696
|
+
* copy of this formula is the defect; there is now only one.
|
|
697
|
+
*/
|
|
698
|
+
const servingCapacity = (window: number | null): ReturnType<typeof capacityAccounting> =>
|
|
699
|
+
capacityAccounting({
|
|
700
|
+
window: window ?? lifecycle.latestSnapshot?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
701
|
+
truthful: providerTotalWindow !== null,
|
|
702
|
+
descriptorWindow: measurements.descriptorWindow,
|
|
703
|
+
usedTokens: measurements.lastProviderMeasurement?.tokens ?? null,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const currentCapacity = (ctx: any): ReturnType<typeof capacityAccounting> =>
|
|
707
|
+
servingCapacity(budgetWindowFor(ctx));
|
|
708
|
+
|
|
322
709
|
const snapshotForEvent = (ctx: any, messages: unknown[]): ActiveContextSnapshot => mapActiveContext({
|
|
323
710
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
324
711
|
eventMessages: messages,
|
|
@@ -327,7 +714,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
327
714
|
brandNoun,
|
|
328
715
|
entryTypePrefix,
|
|
329
716
|
readOnlyTools,
|
|
330
|
-
|
|
717
|
+
readOnlyContextActions,
|
|
718
|
+
contextWindow: budgetWindowFor(ctx) ?? undefined,
|
|
331
719
|
});
|
|
332
720
|
|
|
333
721
|
const authoritativeSnapshotFor = (ctx: any): ActiveContextSnapshot => {
|
|
@@ -344,7 +732,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
344
732
|
brandNoun,
|
|
345
733
|
entryTypePrefix,
|
|
346
734
|
readOnlyTools,
|
|
347
|
-
|
|
735
|
+
readOnlyContextActions,
|
|
736
|
+
contextWindow: budgetWindowFor(ctx) ?? undefined,
|
|
348
737
|
});
|
|
349
738
|
};
|
|
350
739
|
|
|
@@ -402,9 +791,23 @@ export function registerActiveContext(pi: any, options: {
|
|
|
402
791
|
ladder.lastSelectionKind = null;
|
|
403
792
|
ladder.lastSelectionSourceIds = [];
|
|
404
793
|
ladder.pendingContextNote = null;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
794
|
+
instrumentation.ledger = emptyLedger();
|
|
795
|
+
instrumentation.previousDigests = null;
|
|
796
|
+
instrumentation.previousText = null;
|
|
797
|
+
instrumentation.requests = 0;
|
|
798
|
+
instrumentation.lastMutationRequest = 0;
|
|
799
|
+
instrumentation.lastMutationTokens = null;
|
|
800
|
+
instrumentation.lastChange = "append";
|
|
801
|
+
instrumentation.lastPreservedShare = null;
|
|
802
|
+
instrumentation.sinceHandoff = [];
|
|
803
|
+
instrumentation.mutationsSinceHandoff = 0;
|
|
804
|
+
curation.receipts = [];
|
|
805
|
+
curation.contextCalls = 0;
|
|
806
|
+
curation.lastSignals = null;
|
|
807
|
+
curation.reopenBaselineShare = null;
|
|
808
|
+
curation.recoveryAttempts = 0;
|
|
809
|
+
curation.pendingRejection = null;
|
|
810
|
+
curation.lastRecovery = null;
|
|
408
811
|
ladder.lastAutomaticAction = null;
|
|
409
812
|
ladder.automaticFailure = null;
|
|
410
813
|
advisory.hardFenceNoticeKey = null;
|
|
@@ -436,13 +839,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
436
839
|
}
|
|
437
840
|
for (const entry of branchEntries) {
|
|
438
841
|
if (entry?.type !== "custom") continue;
|
|
439
|
-
if (typeof entry.customType === "string" && [
|
|
440
|
-
`${entryTypePrefix}-guidance-`,
|
|
441
|
-
`${ACTIVE_CONTEXT_STATUS_KEY}-guidance-`,
|
|
442
|
-
].some((prefix) => entry.customType.startsWith(prefix))) {
|
|
443
|
-
advisory.historicalGuidanceEntries += 1;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
842
|
if (entry.customType !== providerMeasurementEntryType) continue;
|
|
447
843
|
try {
|
|
448
844
|
const receipt = parseProviderContextMeasurementReceipt(entry.data, sessionId);
|
|
@@ -475,14 +871,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
475
871
|
}
|
|
476
872
|
const durableRestored = restored ?? emptyActiveContextState(sessionId);
|
|
477
873
|
persistence.state = durableRestored.prepared ? clearPrepared(durableRestored) : clone(durableRestored);
|
|
478
|
-
advisory.armedMilestone = advisoryState(persistence.state).armed?.milestone ?? null;
|
|
479
874
|
persistence.persistedWireVersion = restoredPersistence?.wireVersion ?? 0;
|
|
480
875
|
persistence.persistedFoldRecords = restoredPersistence?.records ?? new Map<string, FoldRecordEntry>();
|
|
481
876
|
persistence.persistedStateSha256 = restoredPersistence?.stateSha256 ?? semanticStateSha256(durableRestored);
|
|
482
877
|
const restoredMessages = ctx.sessionManager.buildSessionContext?.()?.messages;
|
|
483
878
|
measurements.lastProviderMeasurement = latestProviderContextMeasurement(
|
|
484
879
|
Array.isArray(restoredMessages) ? restoredMessages : [],
|
|
485
|
-
|
|
880
|
+
budgetWindowFor(ctx),
|
|
486
881
|
ctx.model,
|
|
487
882
|
);
|
|
488
883
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
@@ -763,14 +1158,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
763
1158
|
}
|
|
764
1159
|
};
|
|
765
1160
|
|
|
766
|
-
const markManual = (
|
|
767
|
-
next: ActiveContextState,
|
|
768
|
-
action: Exclude<ActiveContextToolAction, "status">,
|
|
769
|
-
): void => {
|
|
1161
|
+
const markManual = (next: ActiveContextState): void => {
|
|
770
1162
|
cancelPreparation();
|
|
771
1163
|
persistence.state = clearPrepared(next);
|
|
772
1164
|
ladder.pendingManual = true;
|
|
773
|
-
if (action === "fold") advisory.armedMilestone = null;
|
|
774
1165
|
ladder.boundaryFailure = null;
|
|
775
1166
|
};
|
|
776
1167
|
|
|
@@ -782,7 +1173,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
782
1173
|
const stateAtEntry = persistence.state ? clone(persistence.state) : null;
|
|
783
1174
|
const persistedAtEntry = persistence.persisted;
|
|
784
1175
|
const transientAtEntry = captureTransient();
|
|
785
|
-
markManual(next
|
|
1176
|
+
markManual(next);
|
|
786
1177
|
try {
|
|
787
1178
|
await persist(ctx);
|
|
788
1179
|
ladder.pendingManual = false;
|
|
@@ -803,7 +1194,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
803
1194
|
pendingManual: ladder.pendingManual,
|
|
804
1195
|
preparing: ladder.preparing,
|
|
805
1196
|
pendingContextNote: ladder.pendingContextNote,
|
|
806
|
-
armedMilestone: advisory.armedMilestone,
|
|
807
1197
|
lastAutomaticAction: ladder.lastAutomaticAction,
|
|
808
1198
|
automaticFailure: ladder.automaticFailure ? clone(ladder.automaticFailure) : null,
|
|
809
1199
|
boundaryFailure: ladder.boundaryFailure,
|
|
@@ -812,7 +1202,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
812
1202
|
ladder.pendingManual = saved.pendingManual;
|
|
813
1203
|
ladder.preparing = saved.preparing?.controller.signal.aborted ? null : saved.preparing;
|
|
814
1204
|
ladder.pendingContextNote = saved.pendingContextNote;
|
|
815
|
-
advisory.armedMilestone = saved.armedMilestone;
|
|
816
1205
|
ladder.lastAutomaticAction = saved.lastAutomaticAction;
|
|
817
1206
|
ladder.automaticFailure = saved.automaticFailure;
|
|
818
1207
|
ladder.boundaryFailure = saved.boundaryFailure;
|
|
@@ -953,6 +1342,308 @@ export function registerActiveContext(pi: any, options: {
|
|
|
953
1342
|
return true;
|
|
954
1343
|
};
|
|
955
1344
|
|
|
1345
|
+
/**
|
|
1346
|
+
* Bytes per token, measured rather than assumed.
|
|
1347
|
+
*
|
|
1348
|
+
* `ESTIMATED_BYTES_PER_TOKEN` is a constant over RAW text, and the fence weighs a
|
|
1349
|
+
* SERIALIZED projection: roles, ids, custom types and JSON escaping all count toward
|
|
1350
|
+
* the bytes and none of them reach the provider's tokenizer the same way. Measured
|
|
1351
|
+
* 2026-08-06 on one workload: rep11 ran at 4.7 serialized chars per measured token
|
|
1352
|
+
* and rep12 at 7.0. Against the fixed 4 that is a 76% over-estimate in rep12, which
|
|
1353
|
+
* is exactly how a session whose real window was 49% full (187,805 tokens of a
|
|
1354
|
+
* 400,000 window, ratio 0.47) had its request judged over a 383,616-token budget,
|
|
1355
|
+
* reduced, judged over again, and finally ABORTED at stage 37 of 64.
|
|
1356
|
+
*
|
|
1357
|
+
* So the ratio is calibrated per session against ground truth: the size of a
|
|
1358
|
+
* projection this process handed the host, paired with the token count the provider
|
|
1359
|
+
* reported for it. Tiny early requests are ignored -- a 466-char first projection
|
|
1360
|
+
* measured against a system prompt gives a meaningless 0.46 -- and the result is
|
|
1361
|
+
* bounded, so a pathological pairing can neither disable the fence nor trip it.
|
|
1362
|
+
*/
|
|
1363
|
+
const PROJECTION_CALIBRATION_MIN_CHARS = 20_000;
|
|
1364
|
+
const PROJECTION_CALIBRATION_MIN_TOKENS = 5_000;
|
|
1365
|
+
const PROJECTION_CHARS_PER_TOKEN_FLOOR = 2;
|
|
1366
|
+
const PROJECTION_CHARS_PER_TOKEN_CEILING = 12;
|
|
1367
|
+
/** How many recent pairings the ratio, the error window and the inflow window keep. */
|
|
1368
|
+
const PROJECTION_CALIBRATION_WINDOW = 6;
|
|
1369
|
+
const PROJECTION_ERROR_WINDOW = 8;
|
|
1370
|
+
/** Margin floor, as a share of the window, before any error or inflow is measured. */
|
|
1371
|
+
const PROJECTION_MARGIN_FLOOR_SHARE = 0.05;
|
|
1372
|
+
|
|
1373
|
+
const noteProjectionCalibration = (measurement: ProviderContextMeasurement): void => {
|
|
1374
|
+
const previous = measurements.lastProviderMeasurement;
|
|
1375
|
+
if (Number.isFinite(measurement.tokens) && previous && Number.isFinite(previous.tokens)) {
|
|
1376
|
+
const step = measurement.tokens - previous.tokens;
|
|
1377
|
+
// Only GROWTH is an inflow step. A commit shrinks the window; that is the thing
|
|
1378
|
+
// the margin has to survive, not a size the next request can be predicted from.
|
|
1379
|
+
if (step > 0) {
|
|
1380
|
+
measurements.inflowSteps.push(step);
|
|
1381
|
+
if (measurements.inflowSteps.length > PROJECTION_ERROR_WINDOW) measurements.inflowSteps.shift();
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const chars = measurements.lastProjectedChars;
|
|
1385
|
+
if (chars === null || chars < PROJECTION_CALIBRATION_MIN_CHARS) return;
|
|
1386
|
+
if (!Number.isFinite(measurement.tokens) || measurement.tokens < PROJECTION_CALIBRATION_MIN_TOKENS) return;
|
|
1387
|
+
// The signed error of the estimate we made for the projection this measurement
|
|
1388
|
+
// describes. This is the only honest measure of how wrong the fence can be.
|
|
1389
|
+
// Only a CALIBRATED estimate's error belongs in the window. The bootstrap constant
|
|
1390
|
+
// is wrong by design -- 76% high in rep12 -- and letting that one reading set the
|
|
1391
|
+
// margin would keep every later request permanently inside it.
|
|
1392
|
+
if (measurements.lastProjectedEstimate !== null && measurements.lastProjectedEstimateCalibrated &&
|
|
1393
|
+
measurement.tokens > 0) {
|
|
1394
|
+
measurements.estimatorErrors.push(
|
|
1395
|
+
(measurements.lastProjectedEstimate - measurement.tokens) / measurement.tokens,
|
|
1396
|
+
);
|
|
1397
|
+
if (measurements.estimatorErrors.length > PROJECTION_ERROR_WINDOW) measurements.estimatorErrors.shift();
|
|
1398
|
+
}
|
|
1399
|
+
measurements.projectionCalibrations.push({ chars, tokens: measurement.tokens });
|
|
1400
|
+
if (measurements.projectionCalibrations.length > PROJECTION_CALIBRATION_WINDOW) {
|
|
1401
|
+
measurements.projectionCalibrations.shift();
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Recent, and pessimistic.
|
|
1407
|
+
*
|
|
1408
|
+
* A single latest pairing tracks drift but carries its noise; an average lags it.
|
|
1409
|
+
* Near the top of the window neither is acceptable, because the error that matters is
|
|
1410
|
+
* one-sided: a ratio that is too HIGH under-counts tokens and transmits a request the
|
|
1411
|
+
* provider will reject. So the fence takes the smallest ratio in the recent window,
|
|
1412
|
+
* which converges instantly in the dangerous direction and within a window in the
|
|
1413
|
+
* cheap one. Measured 2026-08-06 (rep13): the ratio moved 5.34 to 6.01 and back
|
|
1414
|
+
* across twenty requests while the window sat above 90% full.
|
|
1415
|
+
*/
|
|
1416
|
+
const projectionCharsPerToken = (): number => {
|
|
1417
|
+
const usable = measurements.projectionCalibrations.filter((entry) =>
|
|
1418
|
+
entry.tokens > 0 && Number.isFinite(entry.chars / entry.tokens) && entry.chars / entry.tokens > 0);
|
|
1419
|
+
if (!usable.length) return ESTIMATED_BYTES_PER_TOKEN;
|
|
1420
|
+
const measured = Math.min(...usable.map((entry) => entry.chars / entry.tokens));
|
|
1421
|
+
return Math.min(
|
|
1422
|
+
PROJECTION_CHARS_PER_TOKEN_CEILING,
|
|
1423
|
+
Math.max(PROJECTION_CHARS_PER_TOKEN_FLOOR, measured),
|
|
1424
|
+
);
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
const projectedTokenEstimate = (projected: unknown[]): number =>
|
|
1428
|
+
Math.ceil(bytes(projected) / projectionCharsPerToken());
|
|
1429
|
+
|
|
1430
|
+
/** The largest recent estimator error, as a share. Unmeasured sessions assume none. */
|
|
1431
|
+
const estimatorErrorShare = (): number => measurements.estimatorErrors.length
|
|
1432
|
+
? Math.max(...measurements.estimatorErrors.map((error) => Math.abs(error)))
|
|
1433
|
+
: 0;
|
|
1434
|
+
|
|
1435
|
+
/** The largest recent growth step, which is what one more turn can add. */
|
|
1436
|
+
const expectedInflowTokens = (): number => measurements.inflowSteps.length
|
|
1437
|
+
? Math.max(...measurements.inflowSteps)
|
|
1438
|
+
: 0;
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* The safety margin.
|
|
1442
|
+
*
|
|
1443
|
+
* A fence that fires AT the budget is a fence that fires after the wire: the request
|
|
1444
|
+
* that kills the session is the one built after the last measurement, and it is never
|
|
1445
|
+
* the one being weighed. Measured 2026-08-06 (rep13): the last measurement read
|
|
1446
|
+
* 370,320 tokens against a 383,616 budget -- 13,296 of headroom against a median
|
|
1447
|
+
* inflow of 13,865 and a maximum of 27,815 -- while the estimate for that request was
|
|
1448
|
+
* 366,934, comfortably "under budget". The next request crossed the real limit and
|
|
1449
|
+
* the provider rejected it. Every number in that sentence was known to the runtime.
|
|
1450
|
+
*
|
|
1451
|
+
* So the reduction threshold carries what the runtime knows it does not know: the
|
|
1452
|
+
* worst recent estimator error applied to this estimate, plus one worst recent inflow
|
|
1453
|
+
* step, never less than a floor share of the window.
|
|
1454
|
+
*/
|
|
1455
|
+
const projectionMarginTokens = (estimate: number, windowTokens: number): number => Math.ceil(Math.max(
|
|
1456
|
+
PROJECTION_MARGIN_FLOOR_SHARE * windowTokens,
|
|
1457
|
+
estimatorErrorShare() * estimate + expectedInflowTokens(),
|
|
1458
|
+
));
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* The TRANSMISSION fence.
|
|
1462
|
+
*
|
|
1463
|
+
* `abortUnsafeHardContext` gates on `measurements.latestRatio`, which describes the
|
|
1464
|
+
* request the provider ALREADY answered. It is a lagging indicator: between that
|
|
1465
|
+
* response and the next request an excursion can add a hundred thousand tokens of
|
|
1466
|
+
* tool results, and nothing in the fence looks at the projection actually about to
|
|
1467
|
+
* be sent. Measured 2026-08-06 (rep 11): the last measurement read 359,625 tokens of
|
|
1468
|
+
* a 400,000 window, ratio 0.937 against a 0.959 hard fence, so no abort fired -- and
|
|
1469
|
+
* the projection that went out was 1,831,936 chars, about 458k estimated tokens, 1.2x
|
|
1470
|
+
* the whole window. The provider rejected it twice and the run died. rep4 aborted
|
|
1471
|
+
* correctly only because its 272k DESCRIPTOR window put the stale ratio over the
|
|
1472
|
+
* fence by luck of arithmetic, not because anything measured the request.
|
|
1473
|
+
*
|
|
1474
|
+
* So the projection itself is measured here, against the truthful serving budget the
|
|
1475
|
+
* capacity accounting already computes. Over budget, an emergency reduction runs at
|
|
1476
|
+
* fence pressure -- where every guarded mark is waived -- and the rebuilt projection
|
|
1477
|
+
* is measured again. If it still does not fit, the request is aborted BEFORE
|
|
1478
|
+
* transmission, which is the whole point of having a fence.
|
|
1479
|
+
*/
|
|
1480
|
+
const projectionExceedsBudget = (projected: unknown[], ctx: any): {
|
|
1481
|
+
tokens: number;
|
|
1482
|
+
budgetTokens: number;
|
|
1483
|
+
marginTokens: number;
|
|
1484
|
+
/** Past the wire: this request must not be transmitted at all. */
|
|
1485
|
+
over: boolean;
|
|
1486
|
+
/** Inside the margin: still sendable, but the next one may not be. Reduce NOW. */
|
|
1487
|
+
crowded: boolean;
|
|
1488
|
+
} => {
|
|
1489
|
+
const capacity = currentCapacity(ctx);
|
|
1490
|
+
const budgetTokens = Number.isFinite(capacity.budgetTokens) && capacity.budgetTokens > 0
|
|
1491
|
+
? capacity.budgetTokens
|
|
1492
|
+
: Number.POSITIVE_INFINITY;
|
|
1493
|
+
const tokens = projectedTokenEstimate(projected);
|
|
1494
|
+
const marginTokens = Number.isFinite(budgetTokens)
|
|
1495
|
+
? projectionMarginTokens(tokens, capacity.window)
|
|
1496
|
+
: 0;
|
|
1497
|
+
return {
|
|
1498
|
+
tokens,
|
|
1499
|
+
budgetTokens,
|
|
1500
|
+
marginTokens,
|
|
1501
|
+
over: tokens > budgetTokens,
|
|
1502
|
+
crowded: tokens + marginTokens > budgetTokens,
|
|
1503
|
+
};
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
const abortOverBudgetProjection = (
|
|
1507
|
+
tokens: number,
|
|
1508
|
+
budgetTokens: number,
|
|
1509
|
+
ctx: any,
|
|
1510
|
+
recoveryAttempts = 0,
|
|
1511
|
+
): void => {
|
|
1512
|
+
const key = sha256Value({
|
|
1513
|
+
sessionId: persistence.state?.sessionId ?? null,
|
|
1514
|
+
revision: persistence.state?.revision ?? null,
|
|
1515
|
+
tokens,
|
|
1516
|
+
budgetTokens,
|
|
1517
|
+
phase: "projection-budget-fence",
|
|
1518
|
+
});
|
|
1519
|
+
ladder.pendingContextNote =
|
|
1520
|
+
`The ${brandNoun} projection estimates ${tokens} tokens against a ${budgetTokens}-token serving budget` +
|
|
1521
|
+
(recoveryAttempts
|
|
1522
|
+
? `, after ${recoveryAttempts} recovery attempt(s) that folded everything foldable. The remaining ` +
|
|
1523
|
+
"inflow does not fit at any folding depth, which is an impossibility rather than a recoverable state."
|
|
1524
|
+
: ".") +
|
|
1525
|
+
" The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
|
|
1526
|
+
if (advisory.hardFenceNoticeKey !== key) {
|
|
1527
|
+
advisory.hardFenceNoticeKey = key;
|
|
1528
|
+
safeNotify(
|
|
1529
|
+
ctx,
|
|
1530
|
+
"Provider request aborted: the projection exceeds the serving budget. " +
|
|
1531
|
+
"Run /compact or make an explicit bounded context fold.",
|
|
1532
|
+
"error",
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
if (typeof ctx.abort !== "function") {
|
|
1536
|
+
throw new Error(`Pi projection-budget abort capability is unavailable at ${tokens} estimated tokens`);
|
|
1537
|
+
}
|
|
1538
|
+
ctx.abort();
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Enforce the budget, and RECOVER rather than die.
|
|
1543
|
+
*
|
|
1544
|
+
* A provider context-overflow rejection mutates nothing durable: the assistant
|
|
1545
|
+
* message never lands and this runtime rebuilds the projection from the branch on
|
|
1546
|
+
* every request. So the terminal path is not an abort, it is a rollback: do not
|
|
1547
|
+
* append the failed exchange, fold at fence pressure until the request fits, rebuild,
|
|
1548
|
+
* and let Pi resend. Recovery is capped, and the cap is what makes it safe -- an
|
|
1549
|
+
* inflow that still will not fit after maximal folding is a genuine impossibility,
|
|
1550
|
+
* and that one fails LOUDLY exactly as before.
|
|
1551
|
+
*/
|
|
1552
|
+
const enforceProjectionBudget = async (
|
|
1553
|
+
snapshot: ActiveContextSnapshot,
|
|
1554
|
+
projected: unknown[],
|
|
1555
|
+
ctx: any,
|
|
1556
|
+
): Promise<{ projected: unknown[]; aborted: boolean }> => {
|
|
1557
|
+
let measured = projectionExceedsBudget(projected, ctx);
|
|
1558
|
+
const rejected = curation.pendingRejection !== null;
|
|
1559
|
+
// CROWDED, not over, is the trigger for reducing: the request that kills a session
|
|
1560
|
+
// is the one built after this one. A provider rejection outranks our own estimate:
|
|
1561
|
+
// it is ground truth that the last request did not fit, whatever we measured.
|
|
1562
|
+
if (!measured.crowded && !rejected) return { projected, aborted: false };
|
|
1563
|
+
// Why this fired, recorded from the measurement that TRIGGERED it rather than the
|
|
1564
|
+
// one taken afterwards, which by then describes a projection that fits.
|
|
1565
|
+
const trigger = measured;
|
|
1566
|
+
let reduced = projected;
|
|
1567
|
+
let attempts = 0;
|
|
1568
|
+
let reducedAtLeastOnce = false;
|
|
1569
|
+
// At the fence the only useful action is the fold that keeps the request
|
|
1570
|
+
// transmissible, so every reduction runs with every guarded mark waived.
|
|
1571
|
+
while (attempts < OVERFLOW_RECOVERY_MAX_ATTEMPTS && (measured.crowded || measured.over || rejected)) {
|
|
1572
|
+
attempts += 1;
|
|
1573
|
+
curation.recoveryAttempts += 1;
|
|
1574
|
+
let action: Record<string, unknown> | null = null;
|
|
1575
|
+
try {
|
|
1576
|
+
action = await attemptAutomaticRung(snapshot, 1, ctx, "projection-budget", {
|
|
1577
|
+
waiveToolCadence: true,
|
|
1578
|
+
waiverRatio: 1,
|
|
1579
|
+
});
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
suspendAutomatic(error, "projection-budget", ctx);
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
if (!action || !persistence.state) break;
|
|
1585
|
+
reducedAtLeastOnce = true;
|
|
1586
|
+
reduced = projectWithAdvisory(snapshot);
|
|
1587
|
+
measured = projectionExceedsBudget(reduced, ctx);
|
|
1588
|
+
if (!measured.over && !measured.crowded) break;
|
|
1589
|
+
}
|
|
1590
|
+
if (reducedAtLeastOnce) {
|
|
1591
|
+
ladder.overBudgetReduction = {
|
|
1592
|
+
estimatedTokensBefore: projectedTokenEstimate(projected),
|
|
1593
|
+
estimatedTokensAfter: measured.tokens,
|
|
1594
|
+
budgetTokens: trigger.budgetTokens,
|
|
1595
|
+
marginTokens: trigger.marginTokens,
|
|
1596
|
+
estimatorErrorShare: estimatorErrorShare(),
|
|
1597
|
+
expectedInflowTokens: expectedInflowTokens(),
|
|
1598
|
+
crowded: trigger.crowded,
|
|
1599
|
+
overBeforeReduction: trigger.over,
|
|
1600
|
+
transmitted: !measured.over,
|
|
1601
|
+
recoveryAttempts: attempts,
|
|
1602
|
+
providerRejected: rejected,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
if (rejected) {
|
|
1606
|
+
curation.lastRecovery = {
|
|
1607
|
+
status: curation.pendingRejection?.status ?? null,
|
|
1608
|
+
attempts,
|
|
1609
|
+
estimatedTokensAfter: measured.tokens,
|
|
1610
|
+
budgetTokens: measured.budgetTokens,
|
|
1611
|
+
recovered: !measured.over,
|
|
1612
|
+
};
|
|
1613
|
+
emit("context.recovery", {
|
|
1614
|
+
provider_status: curation.pendingRejection?.status ?? null,
|
|
1615
|
+
attempts,
|
|
1616
|
+
max_attempts: OVERFLOW_RECOVERY_MAX_ATTEMPTS,
|
|
1617
|
+
tokens_before: projectedTokenEstimate(projected),
|
|
1618
|
+
tokens_after: measured.tokens,
|
|
1619
|
+
budget_tokens: measured.budgetTokens,
|
|
1620
|
+
margin_tokens: measured.marginTokens,
|
|
1621
|
+
recovered: !measured.over,
|
|
1622
|
+
});
|
|
1623
|
+
const overflowBefore = projectedTokenEstimate(projected);
|
|
1624
|
+
deliverReceipt(contextReceipt({
|
|
1625
|
+
kind: "overflow-recovery",
|
|
1626
|
+
ordinal: markOrdinal(snapshot),
|
|
1627
|
+
trigger: `provider-rejection:${curation.pendingRejection?.status ?? "unknown"}`,
|
|
1628
|
+
freedTokens: Math.max(0, overflowBefore - measured.tokens),
|
|
1629
|
+
occupancyBefore: overflowBefore,
|
|
1630
|
+
occupancyAfter: measured.tokens,
|
|
1631
|
+
recovered: true,
|
|
1632
|
+
note: measured.over
|
|
1633
|
+
? `The rebuilt request is still ${measured.tokens} tokens against a ${measured.budgetTokens}-token ` +
|
|
1634
|
+
"serving budget, so the run stops here rather than sending a request the provider will reject again."
|
|
1635
|
+
: "A rollback was required: the provider rejected the last request, which overfilled the serving " +
|
|
1636
|
+
`budget at ${overflowBefore} estimated tokens against ${measured.budgetTokens}. ` +
|
|
1637
|
+
`${attempts} reduction(s) landed it at ${measured.tokens} tokens. Nothing durable was written ` +
|
|
1638
|
+
"for it, and the request was rebuilt inside the budget rather than dropped.",
|
|
1639
|
+
}));
|
|
1640
|
+
curation.pendingRejection = null;
|
|
1641
|
+
}
|
|
1642
|
+
if (!measured.over) return { projected: reduced, aborted: false };
|
|
1643
|
+
abortOverBudgetProjection(measured.tokens, measured.budgetTokens, ctx, attempts);
|
|
1644
|
+
return { projected: reduced, aborted: true };
|
|
1645
|
+
};
|
|
1646
|
+
|
|
956
1647
|
const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
|
|
957
1648
|
if (lifecycle.shuttingDown || !persistence.state || ladder.automaticFailure || ratio === null || persistence.state.prepared || ladder.preparing ||
|
|
958
1649
|
ratio < snapshot.policy.warmRatio ||
|
|
@@ -968,6 +1659,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
968
1659
|
? selection.candidate.sourceRefs.slice(0, 8).map((ref) => ref.entryId)
|
|
969
1660
|
: [];
|
|
970
1661
|
if (selection?.kind !== "chapter-prepare") return;
|
|
1662
|
+
// Already bite-sized: the chapter selector never proposes a span past the cap, so a
|
|
1663
|
+
// model brief is never spent on a fold nobody can read back cheaply.
|
|
971
1664
|
const candidate = selection.candidate;
|
|
972
1665
|
const id = automaticPreparationId(candidate, persistence.state);
|
|
973
1666
|
const controller = new AbortController();
|
|
@@ -1018,52 +1711,165 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1018
1711
|
if (ownsSlot && sessionIdentityStillValid(ctx, snapshot.sessionId, capturedGeneration)) updateStatus(ctx);
|
|
1019
1712
|
});
|
|
1020
1713
|
};
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
.flatMap((candidate) => candidate.sourceRefs.at(-1)?.entryId ?? [])
|
|
1038
|
-
.slice(0, 3);
|
|
1039
|
-
const remediationCount = advisoryState(persistence.state!).delivered[armed.milestone] ?? 0;
|
|
1714
|
+
// Appended at the very TAIL, after the stable prefix, so a suggestion never
|
|
1715
|
+
// invalidates a cached prefix and never becomes durable transcript.
|
|
1716
|
+
/**
|
|
1717
|
+
* The receipt block: what the runtime did to this window, as status rather than as
|
|
1718
|
+
* advice. It is the only carrier this runtime builds.
|
|
1719
|
+
*
|
|
1720
|
+
* It is free because it is RETROSPECTIVE. A receipt exists only after a commit has
|
|
1721
|
+
* already rewritten the projection, so it rides a cache break the runtime had to pay
|
|
1722
|
+
* for regardless, and the freeze then closes over it so it is never paid for again.
|
|
1723
|
+
* It is hard-bounded and its ring evicts the oldest entry, because a report about
|
|
1724
|
+
* bloat that becomes bloat has argued against itself.
|
|
1725
|
+
*/
|
|
1726
|
+
const appendReceipts = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1727
|
+
const content = receiptBlockText({ receipts: curation.receipts, toolName, brandNoun });
|
|
1728
|
+
if (!content) return projected;
|
|
1729
|
+
if (!carrierAdmitted("receipts")) return projected;
|
|
1040
1730
|
projected.push({
|
|
1041
1731
|
role: "custom",
|
|
1042
|
-
customType:
|
|
1043
|
-
content
|
|
1732
|
+
customType: receiptProjectionType,
|
|
1733
|
+
content,
|
|
1044
1734
|
display: false,
|
|
1045
|
-
details: {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
content: liveAdvisoryText({
|
|
1052
|
-
milestone: armed.milestone,
|
|
1053
|
-
ratio: measurements.latestRatio,
|
|
1054
|
-
toolEndpoints,
|
|
1055
|
-
chapterEndpoints,
|
|
1056
|
-
remediationCount,
|
|
1057
|
-
brandNoun,
|
|
1058
|
-
}),
|
|
1059
|
-
display: false,
|
|
1060
|
-
details: { source: activeContextSource(entryTypePrefix), ephemeral: true, milestone: armed.milestone },
|
|
1735
|
+
details: {
|
|
1736
|
+
source: activeContextSource(entryTypePrefix),
|
|
1737
|
+
ephemeral: true,
|
|
1738
|
+
receipts: curation.receipts.map((receipt) => ({ ...receipt })),
|
|
1739
|
+
maxBytes: CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
1740
|
+
},
|
|
1061
1741
|
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
1062
1742
|
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
1063
1743
|
: 0,
|
|
1064
1744
|
});
|
|
1065
1745
|
return projected;
|
|
1066
1746
|
};
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Whatever this pass ended up sending becomes the frozen surface for the next one, in
|
|
1750
|
+
* full. Nothing rides after the frozen region.
|
|
1751
|
+
*
|
|
1752
|
+
* The earlier build kept a live suggestion slate outside the freeze, reasoning that
|
|
1753
|
+
* advice goes stale and that a re-rendered block "only diverges the prefix when its
|
|
1754
|
+
* own content changes". That reasoning is wrong, and rep 21 measured the cost. A
|
|
1755
|
+
* positional cache compares bytes from the start: this pass sends [frozen][slate] and
|
|
1756
|
+
* the next sends [frozen][new messages][slate], so at the offset where the slate sat
|
|
1757
|
+
* the next pass has new messages instead. The slate diverges the prefix EVERY pass
|
|
1758
|
+
* whatever it says, because appending anything displaces it. There is no stable
|
|
1759
|
+
* position at a moving tail.
|
|
1760
|
+
*
|
|
1761
|
+
* Measured on rep 21: a 1503-character slate against a 1.5M-character prompt, 99.86%
|
|
1762
|
+
* of the projection preserved by our own accounting, and 16 full cache rebuilds
|
|
1763
|
+
* costing 3.71M tokens -- 21.9% of every input token in the run. Prefix caching is a
|
|
1764
|
+
* step function, not a gradient: mutation cost has no relation to mutation size.
|
|
1765
|
+
*
|
|
1766
|
+
* So the rule this build enforces: the runtime adds to the window only at a moment it
|
|
1767
|
+
* is already rewriting the window, which means at a commit. A carrier landed there is
|
|
1768
|
+
* free, because the break is already paid for, and it stays landed forever because
|
|
1769
|
+
* the freeze closes over it.
|
|
1770
|
+
*/
|
|
1771
|
+
const holdFrozen = (projected: unknown[]): unknown[] => {
|
|
1772
|
+
freeze.projection = [...projected];
|
|
1773
|
+
freeze.active = false;
|
|
1774
|
+
return projected;
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
const projectWithAdvisory = (snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1778
|
+
const body = projectActiveContext(snapshot, persistence.state!).filter((message) => {
|
|
1779
|
+
const customType = ownValue(message, "customType");
|
|
1780
|
+
return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
|
|
1781
|
+
customType !== surfacingProjectionType && customType !== receiptProjectionType &&
|
|
1782
|
+
customType !== curationProjectionType;
|
|
1783
|
+
});
|
|
1784
|
+
// The freeze test, stated as bytes rather than as intent: if the rebuilt body still
|
|
1785
|
+
// OPENS with the frozen body verbatim, nothing structural moved and the previous
|
|
1786
|
+
// projection is reused whole, with only what arrived since appended after it.
|
|
1787
|
+
const held = freeze.body?.length ?? 0;
|
|
1788
|
+
freeze.active = freeze.projection !== null && body.length >= held &&
|
|
1789
|
+
stableStringify(body.slice(0, held)) === freeze.bodyText;
|
|
1790
|
+
const projected = freeze.active
|
|
1791
|
+
? [...freeze.projection!, ...body.slice(held)]
|
|
1792
|
+
: [...body];
|
|
1793
|
+
if (!freeze.active) freeze.keys.clear();
|
|
1794
|
+
freeze.body = body;
|
|
1795
|
+
freeze.bodyText = stableStringify(body);
|
|
1796
|
+
// The one carrier left, and the only one that can ever be free: a statement of what
|
|
1797
|
+
// the runtime just did. It is built only when a commit produced a receipt, it lands
|
|
1798
|
+
// inside the freeze, and it is never rendered twice.
|
|
1799
|
+
//
|
|
1800
|
+
// Everything that used to stand here was ANTICIPATORY -- pressure milestones, the
|
|
1801
|
+
// live advisory, curation reminders, the last-call notice. Anticipatory guidance has
|
|
1802
|
+
// to arrive BEFORE the event it warns about, so it can never ride a break the
|
|
1803
|
+
// runtime was already paying for; it has to create one. Anticipatory guidance and an
|
|
1804
|
+
// append-only projection are mutually exclusive, and eleven runs say the warning
|
|
1805
|
+
// bought nothing anyway: voluntary fold share was 0.00, and the three runs built
|
|
1806
|
+
// specifically to invite curation produced 0, 1 and 0 voluntary folds.
|
|
1807
|
+
appendReceipts(projected, snapshot);
|
|
1808
|
+
return holdFrozen(projected);
|
|
1809
|
+
};
|
|
1810
|
+
/**
|
|
1811
|
+
* Classify the projection we are about to send against the one before it. A rewrite
|
|
1812
|
+
* is ours; a pure append that the provider still re-reads is not, and conflating the
|
|
1813
|
+
* two is what made a scheduling lever look responsible for 12 misses it never caused.
|
|
1814
|
+
*/
|
|
1815
|
+
const noteProjection = (projected: unknown[]): void => {
|
|
1816
|
+
const digests = messageDigests(projected);
|
|
1817
|
+
const comparison = compareProjections(instrumentation.previousDigests, digests);
|
|
1818
|
+
recordProjection(instrumentation.ledger, comparison, digests);
|
|
1819
|
+
instrumentation.previousDigests = digests;
|
|
1820
|
+
instrumentation.lastChange = comparison.change;
|
|
1821
|
+
const text = stableStringify(projected);
|
|
1822
|
+
const divergence = prefixDivergence(instrumentation.previousText, text);
|
|
1823
|
+
const previousChars = instrumentation.previousText?.length ?? 0;
|
|
1824
|
+
// How much of the PREVIOUS prompt this projection still opens with. identicalShare
|
|
1825
|
+
// measures the new projection instead, which grows every pass and would call a
|
|
1826
|
+
// total rewrite "preserved" as soon as enough fresh content landed after it.
|
|
1827
|
+
instrumentation.lastPreservedShare = previousChars > 0
|
|
1828
|
+
? divergence.identicalChars / previousChars
|
|
1829
|
+
: null;
|
|
1830
|
+
const charsPerToken = projectionCharsPerToken();
|
|
1831
|
+
const causes = instrumentation.sinceHandoff.filter((event) =>
|
|
1832
|
+
PREFIX_MUTATING_KINDS.has(event.kind));
|
|
1833
|
+
emit("context.projection", {
|
|
1834
|
+
change: comparison.change,
|
|
1835
|
+
previous_count: comparison.previousCount,
|
|
1836
|
+
next_count: comparison.nextCount,
|
|
1837
|
+
appended_count: comparison.appendedCount,
|
|
1838
|
+
first_divergent_index: comparison.firstDivergentIndex,
|
|
1839
|
+
chars: bytes(projected),
|
|
1840
|
+
estimated_tokens: projectedTokenEstimate(projected),
|
|
1841
|
+
chars_per_token: charsPerToken,
|
|
1842
|
+
});
|
|
1843
|
+
// Emission only. Which of these is a provider-side miss and which is a rewrite we
|
|
1844
|
+
// caused is a join against provider-reported cacheRead, and that join is the
|
|
1845
|
+
// analyst's, not this runtime's.
|
|
1846
|
+
emit("context.prefix", {
|
|
1847
|
+
change: comparison.change,
|
|
1848
|
+
divergent_char: divergence.index,
|
|
1849
|
+
divergent_tokens: divergence.index === null ? null : Math.ceil(divergence.index / charsPerToken),
|
|
1850
|
+
identical_chars: divergence.identicalChars,
|
|
1851
|
+
identical_share: divergence.identicalShare,
|
|
1852
|
+
chars: text.length,
|
|
1853
|
+
previous_chars: instrumentation.previousText?.length ?? 0,
|
|
1854
|
+
estimated_tokens: Math.ceil(text.length / charsPerToken),
|
|
1855
|
+
cause: divergence.index === null
|
|
1856
|
+
? "pure-append"
|
|
1857
|
+
: (causes.length ? causes.map((event) => event.kind).join(",") : "unattributed"),
|
|
1858
|
+
cause_event_seqs: causes.map((event) => event.seq).join(","),
|
|
1859
|
+
events_since_handoff: instrumentation.sinceHandoff.length,
|
|
1860
|
+
// The request class, which falls straight out of the attribution and makes a run
|
|
1861
|
+
// directly comparable to a production cache table. Not new measurement.
|
|
1862
|
+
request_class: divergence.index === null
|
|
1863
|
+
? "steady-state"
|
|
1864
|
+
: (causes.some((event) => event.kind === "context.recovery")
|
|
1865
|
+
? "after-rollback"
|
|
1866
|
+
: (causes.length ? "after-fold" : "after-message")),
|
|
1867
|
+
});
|
|
1868
|
+
instrumentation.previousText = text;
|
|
1869
|
+
instrumentation.sinceHandoff = [];
|
|
1870
|
+
instrumentation.mutationsSinceHandoff = 0;
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1067
1873
|
const commitDeterministicCandidate = async (
|
|
1068
1874
|
snapshot: ActiveContextSnapshot,
|
|
1069
1875
|
candidate: FoldCandidate,
|
|
@@ -1127,22 +1933,603 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1127
1933
|
return { preparedFold, nextState };
|
|
1128
1934
|
};
|
|
1129
1935
|
|
|
1936
|
+
/**
|
|
1937
|
+
* The one mutation of an epoch: every pending mark, plus the automatic additions
|
|
1938
|
+
* a commit is allowed to make, applied through the same machinery as an immediate
|
|
1939
|
+
* fold. Free additions come first (peek reads the agent already discarded), then
|
|
1940
|
+
* the quota top-up that guarantees the commit is worth its rewrite.
|
|
1941
|
+
*/
|
|
1942
|
+
/**
|
|
1943
|
+
* One turn of inflow, as a share of the window: the least a commit at high occupancy
|
|
1944
|
+
* has to free to be worth its rewrite. Measured inflow is used when the session has
|
|
1945
|
+
* any -- rep13 ran at a median 13,865 and a maximum 27,815 tokens per request against
|
|
1946
|
+
* a 400,000 window -- and the floor share stands in until then.
|
|
1947
|
+
*/
|
|
1948
|
+
const commitDepthFloorShare = (snapshot: ActiveContextSnapshot): number => {
|
|
1949
|
+
const window = snapshot.contextWindow;
|
|
1950
|
+
if (!Number.isFinite(window) || window <= 0) return COMMIT_RECLAIM_FLOOR_SHARE;
|
|
1951
|
+
const inflow = expectedInflowTokens();
|
|
1952
|
+
return Math.max(COMMIT_RECLAIM_FLOOR_SHARE, inflow > 0 ? inflow / window : 0);
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
const atOrAboveBackstop = (snapshot: ActiveContextSnapshot, ratio: number | null): boolean =>
|
|
1956
|
+
typeof ratio === "number" && Number.isFinite(ratio) && ratio >= snapshot.policy.refoldRatio;
|
|
1957
|
+
|
|
1958
|
+
/**
|
|
1959
|
+
* A snapshot whose FRESH tail is narrowed.
|
|
1960
|
+
*
|
|
1961
|
+
* Raising the top-up target does nothing once the eligible mass is exhausted, and at
|
|
1962
|
+
* high occupancy that is exactly the state: measured 2026-08-06 (rep13), the big
|
|
1963
|
+
* stale mass was already folded and what remained was the fresh tail and the newest
|
|
1964
|
+
* payloads, so six commits of two to four folds carried the window from 340k to 370k
|
|
1965
|
+
* and into a provider rejection. Depth therefore means reaching into the fresh tail,
|
|
1966
|
+
* not asking harder for evidence that is not there.
|
|
1967
|
+
*
|
|
1968
|
+
* The newest complete turn stays protected and so does everything the current-turn
|
|
1969
|
+
* guard covers: this narrows freshness, it does not abolish it.
|
|
1970
|
+
*/
|
|
1971
|
+
const DEEPENED_FRESH_TURNS = 1;
|
|
1972
|
+
const DEEPENED_FRESH_BYTES_SHARE = 0.25;
|
|
1973
|
+
|
|
1974
|
+
const deepenedFreshnessSnapshot = (snapshot: ActiveContextSnapshot): ActiveContextSnapshot => {
|
|
1975
|
+
const policy = {
|
|
1976
|
+
...snapshot.policy,
|
|
1977
|
+
freshTurns: Math.min(snapshot.policy.freshTurns, DEEPENED_FRESH_TURNS),
|
|
1978
|
+
freshBytes: Math.floor(snapshot.policy.freshBytes * DEEPENED_FRESH_BYTES_SHARE),
|
|
1979
|
+
};
|
|
1980
|
+
const toolProtectedIndices = toolFreshIndices(snapshot.messages, snapshot.completeTurns, policy);
|
|
1981
|
+
// Unmapped messages are protected in every snapshot: nothing can fold what the
|
|
1982
|
+
// durable branch cannot name.
|
|
1983
|
+
for (const item of snapshot.mapped) if (!item.ref) toolProtectedIndices.add(item.index);
|
|
1984
|
+
return { ...snapshot, toolProtectedIndices };
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
const runCommitEpoch = async (
|
|
1988
|
+
snapshot: ActiveContextSnapshot,
|
|
1989
|
+
trigger: string,
|
|
1990
|
+
topUp: boolean,
|
|
1991
|
+
/**
|
|
1992
|
+
* Pressure the waiver is measured against. Defaults to the measured ratio; the
|
|
1993
|
+
* over-budget reduction forces the fence so nothing survivable is held back.
|
|
1994
|
+
*/
|
|
1995
|
+
waiverRatio: number | null = measurements.latestRatio,
|
|
1996
|
+
): Promise<Record<string, unknown> | null> => {
|
|
1997
|
+
const ordinal = markOrdinal(snapshot);
|
|
1998
|
+
let state = persistence.state!;
|
|
1999
|
+
let peekAdded = 0;
|
|
2000
|
+
let topUpAdded = 0;
|
|
2001
|
+
let deepened = false;
|
|
2002
|
+
let deepenedMarks = 0;
|
|
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;
|
|
2007
|
+
for (const mark of ephemeralPeekMarks({ snapshot, state, ordinal })) {
|
|
2008
|
+
const addition = addPendingMark(state, mark);
|
|
2009
|
+
if (addition.added) { state = addition.state; peekAdded += 1; }
|
|
2010
|
+
}
|
|
2011
|
+
const guarded = currentTurnRefKeys(snapshot);
|
|
2012
|
+
// At the fence everything unpinned is fair game, INCLUDING the top-up's reach: a
|
|
2013
|
+
// session whose only foldable evidence is the open excursion has nothing else to
|
|
2014
|
+
// propose, and excluding it there leaves the commit with nothing to apply at the
|
|
2015
|
+
// exact moment reduction is the only thing keeping the request sendable.
|
|
2016
|
+
const fenceLevel = typeof waiverRatio === "number" && Number.isFinite(waiverRatio) &&
|
|
2017
|
+
waiverRatio >= hardFenceRatio(snapshot);
|
|
2018
|
+
// The thermostat. Firing at the trigger line and folding down to the target line is
|
|
2019
|
+
// what makes event SPACING structural rather than hoped for.
|
|
2020
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
2021
|
+
const usedTokens = capacity.usedTokens;
|
|
2022
|
+
const budgetTokens = capacity.budgetTokens;
|
|
2023
|
+
// THE SAFETY EXEMPTION, NARROWED.
|
|
2024
|
+
//
|
|
2025
|
+
// The hard fence is a RATIO prediction. Standing near it is not the same as being
|
|
2026
|
+
// unable to send, and treating the two alike handed the fence path a standing
|
|
2027
|
+
// waiver from both economy guards. Measured 2026-08-07 (rep 17): fence-path
|
|
2028
|
+
// window-pressure commits fired at eligible-freed shares as low as 0.018, under the
|
|
2029
|
+
// 0.02 floor the guided path honors, and re-fired inside a single ordinal. So only
|
|
2030
|
+
// two states outrank the economy: the overflow recovery lane, which runs because a
|
|
2031
|
+
// request already did not fit, and an occupancy genuinely past the serving budget,
|
|
2032
|
+
// where the next request aborts unless something moves. Everything else defers.
|
|
2033
|
+
const overflowExempt = curation.recoveryAttempts > 0 ||
|
|
2034
|
+
(usedTokens !== null && budgetTokens > 0 && usedTokens > budgetTokens);
|
|
2035
|
+
// ONE STRUCTURAL MUTATION PER HANDOFF.
|
|
2036
|
+
//
|
|
2037
|
+
// Measured 2026-08-06 (rep 15): two context.commit records 50ms apart inside one
|
|
2038
|
+
// ordinal, revisions 14 and 15 -- two REAL mutations, not a duplicated record. The
|
|
2039
|
+
// pass had committed at the announced trigger, rebuilt the projection, found it
|
|
2040
|
+
// still CROWDED (inside the fence margin, but transmissible), and committed again.
|
|
2041
|
+
// The second commit bought margin the next pass would have bought anyway and cost a
|
|
2042
|
+
// whole second prefix rewrite. So a second commit in the same handoff defers unless
|
|
2043
|
+
// the request is genuinely untransmittable: the fence and the recovery lane are
|
|
2044
|
+
// safety, and safety outranks the budget.
|
|
2045
|
+
if (instrumentation.mutationsSinceHandoff > 0 && !overflowExempt) {
|
|
2046
|
+
emit("context.commit", {
|
|
2047
|
+
trigger,
|
|
2048
|
+
deferred: true,
|
|
2049
|
+
reason: "mutation-budget-spent",
|
|
2050
|
+
applied_marks: 0,
|
|
2051
|
+
mutations_since_handoff: instrumentation.mutationsSinceHandoff,
|
|
2052
|
+
pending_marks: pendingMarks(state).length,
|
|
2053
|
+
});
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
const hysteresisShare = usedTokens === null || snapshot.contextWindow <= 0
|
|
2057
|
+
? 0
|
|
2058
|
+
: Math.max(0, (usedTokens - CURATION_TARGET_OCCUPANCY_SHARE * budgetTokens) /
|
|
2059
|
+
snapshot.contextWindow);
|
|
2060
|
+
const freeingTarget = Math.max(EPOCH_COMMIT_TARGET_WINDOW_SHARE, hysteresisShare);
|
|
2061
|
+
if (topUp) {
|
|
2062
|
+
// Measuring top-up progress against ELIGIBLE mass is what keeps the pressure
|
|
2063
|
+
// backstop working under a peek-heavy agent: pinned and retained marks are mass
|
|
2064
|
+
// no commit can move, and counting them as progress stops the top-up dead.
|
|
2065
|
+
for (const mark of topUpMarks({
|
|
2066
|
+
snapshot,
|
|
2067
|
+
state,
|
|
2068
|
+
ordinal,
|
|
2069
|
+
excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
|
|
2070
|
+
eligibleOnly: true,
|
|
2071
|
+
// The fence tops up until there is nothing left to propose: a reduction that
|
|
2072
|
+
// stops at the ordinary target can leave the request still untransmittable.
|
|
2073
|
+
targetShare: fenceLevel ? 1 : freeingTarget,
|
|
2074
|
+
})) {
|
|
2075
|
+
const addition = addPendingMark(state, mark);
|
|
2076
|
+
if (addition.added) { state = addition.state; topUpAdded += 1; }
|
|
2077
|
+
}
|
|
2078
|
+
// High-occupancy depth. Near the top of the window a commit that frees less than
|
|
2079
|
+
// one turn of inflow does not reduce anything: it pays a full prefix rewrite,
|
|
2080
|
+
// gives back less than the next stage adds, and the window ratchets UP through
|
|
2081
|
+
// commit after commit. Measured 2026-08-06 (rep13): once the big stale mass was
|
|
2082
|
+
// folded, six commits of two to four folds each carried the window from 340k to
|
|
2083
|
+
// 370k and into a provider rejection, with the backstop firing the whole way.
|
|
2084
|
+
// So a commit at or above the backstop that has not reached one inflow step
|
|
2085
|
+
// reaches further, into everything eligible, instead of billing a rewrite for
|
|
2086
|
+
// crumbs.
|
|
2087
|
+
preDeepenShare = markAccounting(snapshot, state).eligibleFreedWindowShare;
|
|
2088
|
+
// Deepen whenever the ordinary reach fell short of the thermostat's target too:
|
|
2089
|
+
// an event that does not reach the lower line is an event that re-fires soon.
|
|
2090
|
+
const shallow = preDeepenShare < Math.max(commitDepthFloorShare(snapshot), freeingTarget);
|
|
2091
|
+
if (fenceLevel || (shallow && atOrAboveBackstop(snapshot, waiverRatio))) {
|
|
2092
|
+
deepened = true;
|
|
2093
|
+
commitSnapshot = deepenedFreshnessSnapshot(snapshot);
|
|
2094
|
+
for (const mark of topUpMarks({
|
|
2095
|
+
snapshot: commitSnapshot,
|
|
2096
|
+
state,
|
|
2097
|
+
ordinal,
|
|
2098
|
+
excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
|
|
2099
|
+
eligibleOnly: true,
|
|
2100
|
+
targetShare: 1,
|
|
2101
|
+
})) {
|
|
2102
|
+
const addition = addPendingMark(state, mark);
|
|
2103
|
+
if (addition.added) { state = addition.state; topUpAdded += 1; deepenedMarks += 1; }
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
// Boundary slivers ride along in the mutation this commit already pays for; they
|
|
2108
|
+
// are never a mutation of their own, and a gap above the tiny token threshold is
|
|
2109
|
+
// deliberate curation the absorber must not touch.
|
|
2110
|
+
const wedges = absorbWedgeMarks({
|
|
2111
|
+
snapshot,
|
|
2112
|
+
state,
|
|
2113
|
+
charsPerToken: projectionCharsPerToken(),
|
|
2114
|
+
excludeRefKeys: fenceLevel ? new Set<string>() : guarded,
|
|
2115
|
+
});
|
|
2116
|
+
state = wedges.state;
|
|
2117
|
+
const accounting = markAccounting(snapshot, state);
|
|
2118
|
+
if (!accounting.pending) return null;
|
|
2119
|
+
// The reclaim floor. One structural mutation per model call is the budget, so a
|
|
2120
|
+
// commit that would free crumbs spends the whole budget on nothing: it defers and
|
|
2121
|
+
// the marks accumulate. Safety outranks economics, so genuine overflow -- the
|
|
2122
|
+
// recovery lane, or an occupancy already past the serving budget -- fires
|
|
2123
|
+
// regardless of what it frees. Standing at the fence RATIO does not.
|
|
2124
|
+
if (!overflowExempt &&
|
|
2125
|
+
accounting.eligibleFreedWindowShare < COMMIT_RECLAIM_FLOOR_SHARE) {
|
|
2126
|
+
persistence.state = state;
|
|
2127
|
+
emit("context.commit", {
|
|
2128
|
+
trigger,
|
|
2129
|
+
deferred: true,
|
|
2130
|
+
reason: "below-reclaim-floor",
|
|
2131
|
+
applied_marks: 0,
|
|
2132
|
+
pending_marks: accounting.pending,
|
|
2133
|
+
eligible_marks: accounting.eligibleMarks,
|
|
2134
|
+
eligible_freed_share: accounting.eligibleFreedWindowShare,
|
|
2135
|
+
reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2136
|
+
window_tokens: snapshot.contextWindow,
|
|
2137
|
+
});
|
|
2138
|
+
return null;
|
|
2139
|
+
}
|
|
2140
|
+
const bytesBefore = bytes(projectActiveContext(snapshot, state));
|
|
2141
|
+
// The guard protects an in-flight excursion, never at the cost of the session's
|
|
2142
|
+
// ability to send a request at all: above the pressure backstop the oldest guarded
|
|
2143
|
+
// marks are released, and at the fence all of them are.
|
|
2144
|
+
const guardWaiver = guardWaiverCount({
|
|
2145
|
+
snapshot,
|
|
2146
|
+
ratio: waiverRatio,
|
|
2147
|
+
guardedMarks: pendingMarks(state).filter((mark) =>
|
|
2148
|
+
markTouchesCurrentTurn(state, mark, guarded)).length,
|
|
2149
|
+
// Against commitSnapshot, NOT snapshot. The starvation test decides whether the
|
|
2150
|
+
// guard may be waived, so it has to ask the same question the commit will: does
|
|
2151
|
+
// this epoch have work that does not touch the open turn? A DEEPENED epoch tops
|
|
2152
|
+
// up marks that the un-deepened freshness policy still calls protected, so
|
|
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.
|
|
2158
|
+
otherApplicableMarks: pendingMarks(state).filter((mark) =>
|
|
2159
|
+
!markTouchesCurrentTurn(state, mark, guarded) &&
|
|
2160
|
+
markEligibility(commitSnapshot, state, mark) === "eligible").length,
|
|
2161
|
+
});
|
|
2162
|
+
const result = await commitPendingMarks({
|
|
2163
|
+
snapshot: commitSnapshot,
|
|
2164
|
+
state,
|
|
2165
|
+
generation: lifecycle.generation,
|
|
2166
|
+
retainIneligible: true,
|
|
2167
|
+
guardCurrentTurn: true,
|
|
2168
|
+
guardWaiver,
|
|
2169
|
+
});
|
|
2170
|
+
persistence.state = result.state;
|
|
2171
|
+
const bytesAfter = bytes(projectActiveContext(snapshot, result.state));
|
|
2172
|
+
const freedBytes = Math.max(0, bytesBefore - bytesAfter);
|
|
2173
|
+
const commitEvent = emit("context.commit", {
|
|
2174
|
+
trigger,
|
|
2175
|
+
deferred: false,
|
|
2176
|
+
reason: null,
|
|
2177
|
+
eligible_freed_share: accounting.eligibleFreedWindowShare,
|
|
2178
|
+
reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2179
|
+
// The two first-class dials: how much ONE event reclaims, and how long since the
|
|
2180
|
+
// last one. A trigger that frees too little re-fires immediately, and that shape
|
|
2181
|
+
// is only visible with both numbers on the same record.
|
|
2182
|
+
target_freed_share: freeingTarget,
|
|
2183
|
+
hysteresis_target_share: hysteresisShare,
|
|
2184
|
+
target_occupancy_share: CURATION_TARGET_OCCUPANCY_SHARE,
|
|
2185
|
+
shortfall_share: Math.max(0, freeingTarget - accounting.eligibleFreedWindowShare),
|
|
2186
|
+
occupancy_tokens_before: usedTokens,
|
|
2187
|
+
budget_tokens: budgetTokens,
|
|
2188
|
+
requests_since_previous: instrumentation.requests - instrumentation.lastMutationRequest,
|
|
2189
|
+
inflow_tokens_since_previous: usedTokens === null || instrumentation.lastMutationTokens === null
|
|
2190
|
+
? null
|
|
2191
|
+
: usedTokens - instrumentation.lastMutationTokens,
|
|
2192
|
+
applied_marks: result.applied.length,
|
|
2193
|
+
// Refusal is TERMINAL only. A mark whose span was still fresh is deferred, not
|
|
2194
|
+
// refused: it is held and folds at the first commit after the span ages out.
|
|
2195
|
+
refused_marks: result.refused.filter((mark) => !mark.retained).length,
|
|
2196
|
+
deferred_marks: result.retained.length,
|
|
2197
|
+
waived_marks: result.waived.length,
|
|
2198
|
+
pending_marks: accounting.pending,
|
|
2199
|
+
agent_marks: accounting.agentMarks,
|
|
2200
|
+
ladder_marks: accounting.ladderMarks,
|
|
2201
|
+
peek_marks: peekAdded,
|
|
2202
|
+
topup_marks: topUpAdded,
|
|
2203
|
+
deepened: deepened,
|
|
2204
|
+
deepened_marks: deepenedMarks,
|
|
2205
|
+
absorbed_wedges: wedges.absorbed.length,
|
|
2206
|
+
freed_bytes: freedBytes,
|
|
2207
|
+
freed_tokens: estimatedTokens(freedBytes),
|
|
2208
|
+
rewrite_tokens: accounting.rewriteTokens,
|
|
2209
|
+
pinned_bytes: accounting.pinnedBytes,
|
|
2210
|
+
pinned_results: accounting.pinnedResults,
|
|
2211
|
+
window_tokens: snapshot.contextWindow,
|
|
2212
|
+
});
|
|
2213
|
+
instrumentation.mutationsSinceHandoff += 1;
|
|
2214
|
+
instrumentation.lastMutationRequest = instrumentation.requests;
|
|
2215
|
+
instrumentation.lastMutationTokens = usedTokens;
|
|
2216
|
+
let foldedToolResults = 0;
|
|
2217
|
+
let foldedSpans = 0;
|
|
2218
|
+
for (const applied of result.applied) {
|
|
2219
|
+
const fold = persistence.state.folds.find((item) => item.id === applied.foldId);
|
|
2220
|
+
if (fold?.kind === "tool-result") foldedToolResults += 1;
|
|
2221
|
+
else foldedSpans += 1;
|
|
2222
|
+
emit("context.fold", {
|
|
2223
|
+
commit_seq: commitEvent.seq,
|
|
2224
|
+
fold_id: applied.foldId,
|
|
2225
|
+
// The MARK this fold came from. The stream is now the only account of what a
|
|
2226
|
+
// commit applied, so it has to name both ends of that mapping.
|
|
2227
|
+
mark_id: applied.id,
|
|
2228
|
+
fold_kind: fold?.kind ?? applied.mark,
|
|
2229
|
+
origin: applied.origin,
|
|
2230
|
+
source_chars: fold?.sourceChars ?? 0,
|
|
2231
|
+
placeholder_chars: fold?.placeholderChars ?? 0,
|
|
2232
|
+
brief_provenance: fold ? normalizeLegacyProvenance(fold.provenance).kind : null,
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
for (const wedge of wedges.absorbed) {
|
|
2236
|
+
emit("context.absorb", {
|
|
2237
|
+
commit_seq: commitEvent.seq,
|
|
2238
|
+
into_fold_id: wedge.intoMarkId,
|
|
2239
|
+
start_id: wedge.startId,
|
|
2240
|
+
end_id: wedge.endId,
|
|
2241
|
+
entries: wedge.entries,
|
|
2242
|
+
tokens: wedge.tokens,
|
|
2243
|
+
threshold_tokens: MAX_WEDGE_ABSORB_TOKENS,
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
// A bound-out top-up and a silently dropped agent mark are both invisible in an
|
|
2247
|
+
// applied/refused list alone, so the epoch reports its own composition.
|
|
2248
|
+
return {
|
|
2249
|
+
trigger,
|
|
2250
|
+
applied: result.applied,
|
|
2251
|
+
refused: result.refused,
|
|
2252
|
+
pendingMarks: accounting.pending,
|
|
2253
|
+
agentMarks: accounting.agentMarks,
|
|
2254
|
+
ladderMarks: accounting.ladderMarks,
|
|
2255
|
+
peekMarks: peekAdded,
|
|
2256
|
+
topUpMarks: topUpAdded,
|
|
2257
|
+
deepenedTarget: deepened,
|
|
2258
|
+
deepenedMarks,
|
|
2259
|
+
absorbedWedges: wedges.absorbed.length,
|
|
2260
|
+
absorbed: wedges.absorbed,
|
|
2261
|
+
// What the commit actually folded, the way an agent counts it, so the receipt can
|
|
2262
|
+
// report impact instead of an opaque mark total.
|
|
2263
|
+
foldedSpans,
|
|
2264
|
+
foldedToolResults,
|
|
2265
|
+
occupancyTokensBefore: usedTokens,
|
|
2266
|
+
preDeepenFreedShare: preDeepenShare,
|
|
2267
|
+
depthFloorShare: commitDepthFloorShare(snapshot),
|
|
2268
|
+
reclaimFloorShare: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2269
|
+
guardWaived: result.waived.length > 0,
|
|
2270
|
+
waivedMarks: result.waived.length,
|
|
2271
|
+
appliedMarks: result.applied.length,
|
|
2272
|
+
refusedMarks: result.refused.filter((mark) => !mark.retained).length,
|
|
2273
|
+
deferredMarks: result.retained.length,
|
|
2274
|
+
pinnedBytes: accounting.pinnedBytes,
|
|
2275
|
+
pinnedResults: accounting.pinnedResults,
|
|
2276
|
+
retainedMarks: result.retained.length,
|
|
2277
|
+
currentTurnRetained: result.retained.filter((mark) =>
|
|
2278
|
+
markTouchesCurrentTurn(state, mark, guarded)).length,
|
|
2279
|
+
eligibleMarks: accounting.eligibleMarks,
|
|
2280
|
+
estimatedRewriteTokens: accounting.rewriteTokens,
|
|
2281
|
+
estimatedFreedTokens: accounting.freedTokens,
|
|
2282
|
+
freedWindowShare: accounting.freedWindowShare,
|
|
2283
|
+
sourceBytesSaved: freedBytes,
|
|
2284
|
+
actualFreedWindowShare: snapshot.contextWindow > 0
|
|
2285
|
+
? estimatedTokens(freedBytes) / snapshot.contextWindow
|
|
2286
|
+
: 0,
|
|
2287
|
+
targetWindowShare: freeingTarget,
|
|
2288
|
+
hysteresisTargetShare: hysteresisShare,
|
|
2289
|
+
requestsSincePreviousCommit: instrumentation.requests - instrumentation.lastMutationRequest,
|
|
2290
|
+
instrumentation: ledgerSummary(instrumentation.ledger),
|
|
2291
|
+
};
|
|
2292
|
+
};
|
|
2293
|
+
|
|
2294
|
+
/** The two curation signals, measured against the ONE serving budget. */
|
|
2295
|
+
const measuredCurationSignals = (snapshot: ActiveContextSnapshot): CurationSignals => {
|
|
2296
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
2297
|
+
curation.lastSignals = curationSignals({
|
|
2298
|
+
snapshot,
|
|
2299
|
+
state: persistence.state!,
|
|
2300
|
+
usedTokens: capacity.usedTokens,
|
|
2301
|
+
budgetTokens: capacity.budgetTokens,
|
|
2302
|
+
window: capacity.window,
|
|
2303
|
+
charsPerToken: projectionCharsPerToken(),
|
|
2304
|
+
eligibleFolds: markAccounting(snapshot, persistence.state!).eligibleMarks,
|
|
2305
|
+
});
|
|
2306
|
+
return curation.lastSignals;
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
/**
|
|
2310
|
+
* The silent early commit. The two signals decide, the reclaim floor stops a commit
|
|
2311
|
+
* that would free nothing, and the hysteresis stops a plateau from reading as a
|
|
2312
|
+
* stream of events. What is gone is only the announcement in front of it.
|
|
2313
|
+
*/
|
|
2314
|
+
const curationCommitDue = (snapshot: ActiveContextSnapshot): boolean => {
|
|
2315
|
+
if (!persistence.state) return false;
|
|
2316
|
+
if (!curationTriggerFires(measuredCurationSignals(snapshot))) return false;
|
|
2317
|
+
if (curation.reopenBaselineShare !== null) {
|
|
2318
|
+
const eligibleShare = markAccounting(snapshot, persistence.state).eligibleFreedWindowShare;
|
|
2319
|
+
if (eligibleShare - curation.reopenBaselineShare < COMMIT_RECLAIM_FLOOR_SHARE) return false;
|
|
2320
|
+
curation.reopenBaselineShare = null;
|
|
2321
|
+
}
|
|
2322
|
+
return true;
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
/**
|
|
2326
|
+
* Condition (a) of the reopen hysteresis, evaluated on EVERY context pass. A window
|
|
2327
|
+
* that fell back under the trigger has ended its cycle, and the next crossing is a
|
|
2328
|
+
* fresh event. This cannot live inside the trigger: the quiet passes that matter are
|
|
2329
|
+
* exactly the ones too far below it to evaluate it.
|
|
2330
|
+
*/
|
|
2331
|
+
const clearCommitLatchBelowTrigger = (): void => {
|
|
2332
|
+
if (curation.reopenBaselineShare === null) return;
|
|
2333
|
+
const capacity = servingCapacity(lifecycle.latestSnapshot?.contextWindow ?? null);
|
|
2334
|
+
if (capacity.usedTokens === null || capacity.budgetTokens <= 0) return;
|
|
2335
|
+
if (capacity.usedTokens / capacity.budgetTokens < CURATION_OCCUPANCY_SHARE) {
|
|
2336
|
+
curation.reopenBaselineShare = null;
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
/**
|
|
2341
|
+
* One receipt per automatic context action, in the window and in the durable stream.
|
|
2342
|
+
* Informatory, never exhortative: it says what happened, what it freed, and which
|
|
2343
|
+
* verbs correct it. A mark that changed nothing is not an event and gets no receipt.
|
|
2344
|
+
*/
|
|
2345
|
+
const noteAutomaticReceipt = (
|
|
2346
|
+
snapshot: ActiveContextSnapshot,
|
|
2347
|
+
action: Record<string, unknown>,
|
|
2348
|
+
epoch: Record<string, unknown> | null,
|
|
2349
|
+
): void => {
|
|
2350
|
+
const kind = String(action.kind ?? "context-action");
|
|
2351
|
+
if (kind === "mark") return;
|
|
2352
|
+
const savedBytes = Number(action.sourceBytesSaved ?? epoch?.sourceBytesSaved ?? 0);
|
|
2353
|
+
const freedTokens = Number.isFinite(savedBytes) && savedBytes > 0
|
|
2354
|
+
? Math.ceil(savedBytes / projectionCharsPerToken())
|
|
2355
|
+
: 0;
|
|
2356
|
+
const foldIds = Array.isArray(action.foldIds) ? action.foldIds.length : 0;
|
|
2357
|
+
const occupancyBefore = epoch && typeof epoch.occupancyTokensBefore === "number"
|
|
2358
|
+
? epoch.occupancyTokensBefore
|
|
2359
|
+
: measurements.lastProviderMeasurement?.tokens ?? null;
|
|
2360
|
+
deliverReceipt(contextReceipt({
|
|
2361
|
+
kind,
|
|
2362
|
+
ordinal: markOrdinal(snapshot),
|
|
2363
|
+
trigger: epoch ? String(epoch.trigger ?? "") || null : null,
|
|
2364
|
+
foldsCommitted: epoch ? Number(epoch.appliedMarks ?? 0) : 0,
|
|
2365
|
+
foldsCreated: foldIds || (epoch ? Number(epoch.appliedMarks ?? 0) : 0),
|
|
2366
|
+
freedTokens,
|
|
2367
|
+
// The impact, in the same unit the budget is stated in.
|
|
2368
|
+
occupancyBefore,
|
|
2369
|
+
occupancyAfter: occupancyBefore === null ? null : Math.max(0, occupancyBefore - freedTokens),
|
|
2370
|
+
spansFolded: epoch ? Number(epoch.foldedSpans ?? 0) : foldIds,
|
|
2371
|
+
toolResultsFolded: epoch ? Number(epoch.foldedToolResults ?? 0) : 0,
|
|
2372
|
+
splitFolds: Number(action.splitFolds ?? 0),
|
|
2373
|
+
splitFromChars: Number(action.splitFromChars ?? 0),
|
|
2374
|
+
absorbedWedges: Number(epoch?.absorbedWedges ?? 0),
|
|
2375
|
+
recovered: curation.recoveryAttempts > 0,
|
|
2376
|
+
note: epoch && Number(epoch.deepenedMarks ?? 0) > 0
|
|
2377
|
+
? `The commit reached into the fresh tail for ${epoch.deepenedMarks} further span(s), because a ` +
|
|
2378
|
+
"shallower commit would have freed less than one turn of inflow."
|
|
2379
|
+
: null,
|
|
2380
|
+
}));
|
|
2381
|
+
};
|
|
2382
|
+
|
|
1130
2383
|
const applyAutomaticRung = async (
|
|
1131
2384
|
snapshot: ActiveContextSnapshot,
|
|
1132
2385
|
ratio: number,
|
|
1133
|
-
rungOptions: {
|
|
2386
|
+
rungOptions: {
|
|
2387
|
+
waiveToolCadence?: boolean;
|
|
2388
|
+
toolOnly?: boolean;
|
|
2389
|
+
waiverRatio?: number;
|
|
2390
|
+
announcing?: boolean;
|
|
2391
|
+
} = {},
|
|
1134
2392
|
): Promise<Record<string, unknown> | null> => {
|
|
1135
2393
|
if (!persistence.state || ladder.automaticFailure || ladder.preparing) return null;
|
|
1136
|
-
const
|
|
2394
|
+
const rungSelectionOptions = {
|
|
1137
2395
|
waiveToolCadence: rungOptions.waiveToolCadence,
|
|
1138
2396
|
toolOnly: rungOptions.toolOnly,
|
|
1139
2397
|
summarizerAvailable: Boolean(options.summarizeContextSpan),
|
|
1140
2398
|
failedPreparationIds: ladder.failedPreparations,
|
|
1141
|
-
}
|
|
1142
|
-
|
|
2399
|
+
};
|
|
2400
|
+
// The ladder decides but does not move bytes. Marks already pending are decisions
|
|
2401
|
+
// already made: excluding the evidence they cover makes every eligible turn choose
|
|
2402
|
+
// NEW stale content, so marks accumulate with stale growth instead of re-proposing
|
|
2403
|
+
// one stale batch.
|
|
2404
|
+
const markLadderSelection = (): Record<string, unknown> | null => {
|
|
2405
|
+
if (!persistence.state) return null;
|
|
2406
|
+
const decision = selectAutomaticRung(snapshot, persistence.state, ratio, {
|
|
2407
|
+
...rungSelectionOptions,
|
|
2408
|
+
claimed: claimedRefKeys(persistence.state),
|
|
2409
|
+
claimedFoldIds: markedFoldIds(persistence.state),
|
|
2410
|
+
});
|
|
2411
|
+
const mark = decision
|
|
2412
|
+
? ladderSelectionMark({
|
|
2413
|
+
snapshot, state: persistence.state, selection: decision, ordinal: markOrdinal(snapshot),
|
|
2414
|
+
})
|
|
2415
|
+
: null;
|
|
2416
|
+
if (!mark) return null;
|
|
2417
|
+
const addition = addPendingMark(persistence.state, mark);
|
|
2418
|
+
if (!addition.added) return null;
|
|
2419
|
+
persistence.state = addition.state;
|
|
2420
|
+
ladder.pendingContextNote =
|
|
2421
|
+
`Pending ${mark.mark} mark ${mark.id} recorded; no context bytes moved. ` +
|
|
2422
|
+
"It applies at the next commit epoch.";
|
|
2423
|
+
ladder.lastAutomaticAction = {
|
|
2424
|
+
kind: "mark",
|
|
2425
|
+
foldIds: [mark.id],
|
|
2426
|
+
sourceIds: [],
|
|
2427
|
+
markKind: mark.mark,
|
|
2428
|
+
markOrigin: mark.origin,
|
|
2429
|
+
pendingMarks: pendingMarks(persistence.state).length,
|
|
2430
|
+
sourceBytesSaved: 0,
|
|
2431
|
+
};
|
|
2432
|
+
return ladder.lastAutomaticAction;
|
|
2433
|
+
};
|
|
2434
|
+
let epoch: Record<string, unknown> | null = null;
|
|
2435
|
+
let inlineRungs = true;
|
|
2436
|
+
if (epochScheduling) {
|
|
2437
|
+
// QUIET RUNTIME. A commit is an epoch transition: it rewrites the projection, and
|
|
2438
|
+
// every rewrite risks the whole prefix cache, so the cadence has to be the fewest
|
|
2439
|
+
// commits that still keep the window inside its budget.
|
|
2440
|
+
//
|
|
2441
|
+
// The economic eligible-share trigger is DELETED: it fired 164 commits from
|
|
2442
|
+
// ordinal 17 in rep 15, each a fresh cache rebuild bought with marks that had not
|
|
2443
|
+
// yet earned one. What remains is the two-signal curation trigger, which fired 10
|
|
2444
|
+
// to 17 times across reps 17 and 20, over the pressure backstop.
|
|
2445
|
+
//
|
|
2446
|
+
// The trigger stays; only its ANNOUNCEMENT is gone. Nothing warns that a commit
|
|
2447
|
+
// is coming and nothing waits for the agent to react, because a warning has to
|
|
2448
|
+
// arrive before the event it warns about and therefore has to break a prefix
|
|
2449
|
+
// nothing else was breaking. Deleting the trigger with the announcement was the
|
|
2450
|
+
// wrong cut: it left the raw backstop alone in front of the window, and gate 58
|
|
2451
|
+
// caught the projection overrunning its budget before anything reclaimed -- the
|
|
2452
|
+
// rep 13 death. The runtime still decides early. It just no longer says so.
|
|
2453
|
+
const backstopDue = epochCommitDue(snapshot, ratio);
|
|
2454
|
+
// The trigger is evaluated on CONTEXT passes only. A commit is an epoch
|
|
2455
|
+
// transition, and it belongs on the pass that builds the next projection rather
|
|
2456
|
+
// than in the middle of a tool batch: firing it mid-batch makes a MARK rewrite
|
|
2457
|
+
// the window, which is precisely the thing marking is supposed never to do.
|
|
2458
|
+
const curationDue = !backstopDue && rungOptions.announcing === true &&
|
|
2459
|
+
curationCommitDue(snapshot);
|
|
2460
|
+
if (backstopDue || curationDue) {
|
|
2461
|
+
epoch = await runCommitEpoch(
|
|
2462
|
+
snapshot,
|
|
2463
|
+
backstopDue ? "window-pressure" : "curation-trigger",
|
|
2464
|
+
true,
|
|
2465
|
+
rungOptions.waiverRatio ?? ratio,
|
|
2466
|
+
);
|
|
2467
|
+
// Latch the eligible share this commit left behind, so the next one waits for
|
|
2468
|
+
// genuinely new foldable mass rather than for the same window to still be full.
|
|
2469
|
+
if (persistence.state && (curationDue || curation.reopenBaselineShare !== null)) {
|
|
2470
|
+
curation.reopenBaselineShare =
|
|
2471
|
+
markAccounting(snapshot, persistence.state).eligibleFreedWindowShare;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
// An inline rung is free only INSIDE a rewrite the epoch already paid for. Below
|
|
2475
|
+
// the threshold no commit ran, and an epoch that applied NOTHING -- every mark
|
|
2476
|
+
// held back by an open turn or by ineligibility -- paid for no rewrite either,
|
|
2477
|
+
// so folding inline there is a fresh single-fold rewrite of its own. Measured
|
|
2478
|
+
// 2026-08-06 (rep 10): the current-turn guard retained all nine marks every turn
|
|
2479
|
+
// while the inline rung folded one batch per turn anyway, 52 single-fold
|
|
2480
|
+
// rewrites that left the prefix cache share at zero. Below a paid rewrite the
|
|
2481
|
+
// ladder MARKS, so the decisions batch into the first commit that can apply them.
|
|
2482
|
+
inlineRungs = Boolean(epoch) && Number(epoch?.appliedMarks ?? 0) > 0;
|
|
2483
|
+
if (!inlineRungs) {
|
|
2484
|
+
const marked = markLadderSelection();
|
|
2485
|
+
if (marked) {
|
|
2486
|
+
if (epoch) ladder.lastAutomaticAction = { ...marked, epoch };
|
|
2487
|
+
return ladder.lastAutomaticAction;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
// Evidence a pending mark already covers is not the inline rung's to fold: a mark
|
|
2492
|
+
// the commit RETAINED (the open turn's own reads) would otherwise be folded here
|
|
2493
|
+
// by the very pass that just protected it.
|
|
2494
|
+
//
|
|
2495
|
+
// The open turn's keys are excluded DIRECTLY, not just via the marks that claim
|
|
2496
|
+
// them. Claiming is not protection: once a commit applies every pending mark the
|
|
2497
|
+
// claim set is empty, and the inline rung then folds the open turn's oldest read
|
|
2498
|
+
// with nothing left to stop it. That is the rep-8 shape, and byte freshness does
|
|
2499
|
+
// not reach it -- the first read of a long excursion sits outside the fresh tail
|
|
2500
|
+
// while the turn that gathered it is still running. It became reachable when the
|
|
2501
|
+
// quiet cadence started landing a commit on the same context pass, which is what
|
|
2502
|
+
// turns the inline rung on in the first place.
|
|
2503
|
+
const selection = inlineRungs
|
|
2504
|
+
? selectAutomaticRung(snapshot, persistence.state, ratio, epochScheduling
|
|
2505
|
+
? {
|
|
2506
|
+
...rungSelectionOptions,
|
|
2507
|
+
claimed: new Set([
|
|
2508
|
+
...claimedRefKeys(persistence.state),
|
|
2509
|
+
...currentTurnRefKeys(snapshot),
|
|
2510
|
+
]),
|
|
2511
|
+
claimedFoldIds: markedFoldIds(persistence.state),
|
|
2512
|
+
}
|
|
2513
|
+
: rungSelectionOptions)
|
|
2514
|
+
: null;
|
|
2515
|
+
// Above the commit threshold we are already inside the epoch's rewrite turn, so
|
|
2516
|
+
// every rung applies inline: the extra rung costs at most one more invalidation
|
|
2517
|
+
// in a turn that has already paid one. Restricting this to prepared chapters made
|
|
2518
|
+
// the refold and consolidation rungs unreachable in epoch mode, which drove a
|
|
2519
|
+
// session whose reducible bytes sit in expanded folds into the hard fence.
|
|
2520
|
+
const applicable = selection;
|
|
2521
|
+
if (!applicable || applicable.kind === "chapter-prepare") {
|
|
2522
|
+
if (!epoch || !persistence.state) return null;
|
|
2523
|
+
ladder.pendingContextNote =
|
|
2524
|
+
`A commit epoch applied ${(epoch.applied as unknown[]).length} pending mark(s) in one rewrite; ` +
|
|
2525
|
+
"exact evidence remains expandable.";
|
|
2526
|
+
ladder.lastAutomaticAction = { kind: "epoch-commit", foldIds: [], sourceIds: [], epoch };
|
|
2527
|
+
noteAutomaticReceipt(snapshot, ladder.lastAutomaticAction, epoch);
|
|
2528
|
+
return ladder.lastAutomaticAction;
|
|
2529
|
+
}
|
|
1143
2530
|
const projectedBytesBefore = bytes(projectActiveContext(snapshot, persistence.state));
|
|
1144
2531
|
let action: Record<string, unknown> | null = null;
|
|
1145
|
-
if (
|
|
2532
|
+
if (applicable.kind === "prepared-chapter" && persistence.state.prepared) {
|
|
1146
2533
|
const error = preparedFoldError({
|
|
1147
2534
|
prepared: persistence.state.prepared,
|
|
1148
2535
|
snapshot,
|
|
@@ -1164,9 +2551,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1164
2551
|
action = { kind: "chapter-fold", foldIds: [id], sourceIds };
|
|
1165
2552
|
ladder.pendingContextNote = `A coherent stale chapter was folded under ${id}; exact evidence remains expandable.`;
|
|
1166
2553
|
}
|
|
1167
|
-
} else if (
|
|
2554
|
+
} else if (applicable.kind === "tool") {
|
|
1168
2555
|
cancelPreparation();
|
|
1169
|
-
const tool =
|
|
2556
|
+
const tool = applicable.candidate;
|
|
1170
2557
|
const id = await commitDeterministicCandidate(snapshot, tool, automaticToolBrief(snapshot, tool));
|
|
1171
2558
|
action = {
|
|
1172
2559
|
kind: "tool-fold",
|
|
@@ -1174,14 +2561,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1174
2561
|
sourceIds: tool.sourceRefs.map((ref) => ref.entryId),
|
|
1175
2562
|
};
|
|
1176
2563
|
ladder.pendingContextNote = `${tool.sourceRefs.length} stale completed read-only tool result(s) were folded.`;
|
|
1177
|
-
} else if (
|
|
2564
|
+
} else if (applicable.kind === "refold") {
|
|
1178
2565
|
cancelPreparation();
|
|
1179
|
-
persistence.state = setFoldProjectionState(persistence.state,
|
|
1180
|
-
action = { kind: "refold", foldIds: [
|
|
1181
|
-
ladder.pendingContextNote = `Stale expanded fold ${
|
|
1182
|
-
} else if (
|
|
2566
|
+
persistence.state = setFoldProjectionState(persistence.state, applicable.foldId, "folded");
|
|
2567
|
+
action = { kind: "refold", foldIds: [applicable.foldId] };
|
|
2568
|
+
ladder.pendingContextNote = `Stale expanded fold ${applicable.foldId} returned to its identical placeholder.`;
|
|
2569
|
+
} else if (applicable.kind === "consolidation") {
|
|
1183
2570
|
cancelPreparation();
|
|
1184
|
-
const consolidation =
|
|
2571
|
+
const consolidation = applicable.candidate;
|
|
1185
2572
|
const id = await commitDeterministicCandidate(
|
|
1186
2573
|
snapshot,
|
|
1187
2574
|
consolidation,
|
|
@@ -1194,29 +2581,48 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1194
2581
|
};
|
|
1195
2582
|
ladder.pendingContextNote =
|
|
1196
2583
|
`Stale folded chapters were consolidated under ${id}; every child remains expandable.`;
|
|
1197
|
-
} else if (
|
|
1198
|
-
const chapter =
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
2584
|
+
} else if (applicable.kind === "chapter") {
|
|
2585
|
+
const chapter = applicable.candidate;
|
|
2586
|
+
// Bite-sized by construction: an oversized coherent chapter becomes sequential
|
|
2587
|
+
// bounded folds, each with its own brief, split only at closed unit boundaries.
|
|
2588
|
+
const spanChars = candidateSpanChars(snapshot, persistence.state, chapter);
|
|
2589
|
+
const parts = splitCandidateBySize(snapshot, persistence.state, chapter);
|
|
2590
|
+
const foldIds: string[] = [];
|
|
2591
|
+
for (const part of parts) {
|
|
2592
|
+
foldIds.push(await commitDeterministicCandidate(
|
|
2593
|
+
snapshot,
|
|
2594
|
+
part,
|
|
2595
|
+
deterministicChapterCandidateBrief(snapshot, part),
|
|
2596
|
+
));
|
|
2597
|
+
}
|
|
1204
2598
|
action = {
|
|
1205
2599
|
kind: "chapter-fold",
|
|
1206
|
-
foldIds
|
|
2600
|
+
foldIds,
|
|
1207
2601
|
sourceIds: chapter.sourceRefs.map((ref) => ref.entryId),
|
|
2602
|
+
...(parts.length > 1 ? { splitFolds: parts.length, splitFromChars: spanChars } : {}),
|
|
1208
2603
|
};
|
|
1209
|
-
|
|
1210
|
-
|
|
2604
|
+
if (parts.length > 1) {
|
|
2605
|
+
emit("context.split", {
|
|
2606
|
+
source: "ladder",
|
|
2607
|
+
span_chars: spanChars,
|
|
2608
|
+
parts: parts.length,
|
|
2609
|
+
cap_chars: MAX_FOLD_SPAN_CHARS,
|
|
2610
|
+
fold_ids: foldIds.join(","),
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
ladder.pendingContextNote = parts.length > 1
|
|
2614
|
+
? `A ${spanChars}-char stale chapter was folded as ${parts.length} bite-sized folds ` +
|
|
2615
|
+
`(${foldIds.join(", ")}); exact evidence remains expandable.`
|
|
2616
|
+
: `A coherent stale chapter was folded under ${foldIds[0]}; exact evidence remains expandable.`;
|
|
1211
2617
|
}
|
|
1212
|
-
if (!action || !persistence.state) return null;
|
|
2618
|
+
if ((!action && !epoch) || !persistence.state) return null;
|
|
1213
2619
|
const projectedBytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
|
|
1214
|
-
persistence.state = clearArmedAdvisory(persistence.state);
|
|
1215
|
-
advisory.armedMilestone = null;
|
|
1216
2620
|
ladder.lastAutomaticAction = {
|
|
1217
|
-
...action,
|
|
2621
|
+
...(action ?? { kind: "epoch-commit", foldIds: [], sourceIds: [] }),
|
|
2622
|
+
...(epoch ? { epoch } : {}),
|
|
1218
2623
|
sourceBytesSaved: Math.max(0, projectedBytesBefore - projectedBytesAfter),
|
|
1219
2624
|
};
|
|
2625
|
+
noteAutomaticReceipt(snapshot, ladder.lastAutomaticAction, epoch);
|
|
1220
2626
|
return ladder.lastAutomaticAction;
|
|
1221
2627
|
};
|
|
1222
2628
|
const runAutomaticRungTransaction = async (
|
|
@@ -1224,7 +2630,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1224
2630
|
ratio: number,
|
|
1225
2631
|
ctx: any,
|
|
1226
2632
|
phase: string,
|
|
1227
|
-
rungOptions: {
|
|
2633
|
+
rungOptions: {
|
|
2634
|
+
waiveToolCadence?: boolean;
|
|
2635
|
+
toolOnly?: boolean;
|
|
2636
|
+
waiverRatio?: number;
|
|
2637
|
+
announcing?: boolean;
|
|
2638
|
+
} = {},
|
|
1228
2639
|
): Promise<Record<string, unknown> | null> => {
|
|
1229
2640
|
if (!persistence.state || !measurements.lastProviderMeasurement ||
|
|
1230
2641
|
!durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) return null;
|
|
@@ -1269,9 +2680,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1269
2680
|
ratio: number,
|
|
1270
2681
|
ctx: any,
|
|
1271
2682
|
phase: string,
|
|
2683
|
+
rungOptions: {
|
|
2684
|
+
waiveToolCadence?: boolean;
|
|
2685
|
+
toolOnly?: boolean;
|
|
2686
|
+
waiverRatio?: number;
|
|
2687
|
+
announcing?: boolean;
|
|
2688
|
+
} = {},
|
|
1272
2689
|
): Promise<Record<string, unknown> | null> => {
|
|
1273
2690
|
const operation = ladder.actionQueue.then(() =>
|
|
1274
|
-
runAutomaticRungTransaction(snapshot, ratio, ctx, phase));
|
|
2691
|
+
runAutomaticRungTransaction(snapshot, ratio, ctx, phase, rungOptions));
|
|
1275
2692
|
ladder.actionQueue = operation.catch(() => undefined);
|
|
1276
2693
|
return operation;
|
|
1277
2694
|
};
|
|
@@ -1285,6 +2702,38 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1285
2702
|
};
|
|
1286
2703
|
options.setProjectionProvider?.(projectionCandidates);
|
|
1287
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
|
+
|
|
1288
2737
|
// A same-session start/tree reload is a projection-generation mutation.
|
|
1289
2738
|
// Queue it behind every context authority → preparation → commit →
|
|
1290
2739
|
// projection transaction, then serialize the actual load with the action
|
|
@@ -1316,7 +2765,26 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1316
2765
|
}
|
|
1317
2766
|
};
|
|
1318
2767
|
|
|
1319
|
-
|
|
2768
|
+
/**
|
|
2769
|
+
* State the resolved serving budget once, at startup, before anything reads it. A
|
|
2770
|
+
* budget nobody can see in the stream is how rep 15 spent a whole run on a 272,000
|
|
2771
|
+
* descriptor without a single record saying so.
|
|
2772
|
+
*/
|
|
2773
|
+
const recordResolvedCapacity = (ctx: any): void => {
|
|
2774
|
+
const capacity = currentCapacity(ctx);
|
|
2775
|
+
emit("context.capacity", {
|
|
2776
|
+
mode: capacity.mode,
|
|
2777
|
+
window_tokens: capacity.window,
|
|
2778
|
+
budget_tokens: capacity.budgetTokens,
|
|
2779
|
+
output_reservation: capacity.outputReservation,
|
|
2780
|
+
descriptor_window: capacity.descriptorWindow,
|
|
2781
|
+
});
|
|
2782
|
+
};
|
|
2783
|
+
|
|
2784
|
+
pi.on("session_start", async (_event: unknown, ctx: any) => {
|
|
2785
|
+
await safeLifecycleLoad(ctx, "session-start");
|
|
2786
|
+
recordResolvedCapacity(ctx);
|
|
2787
|
+
});
|
|
1320
2788
|
pi.on("session_tree", async (_event: unknown, ctx: any) => { await safeLifecycleLoad(ctx, "session-tree"); });
|
|
1321
2789
|
pi.on("session_compact", async (event: Record<string, unknown>, ctx: any) => {
|
|
1322
2790
|
const reason = boundReceiptText(ownValue(event, "reason"), 64, "unknown");
|
|
@@ -1366,43 +2834,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1366
2834
|
if (persistence.state?.prepared) persistence.state = clearPrepared(persistence.state);
|
|
1367
2835
|
measurements.latestRatio = null;
|
|
1368
2836
|
measurements.lastProviderMeasurement = null;
|
|
1369
|
-
advisory.armedMilestone = persistence.state ? advisoryState(persistence.state).armed?.milestone ?? null : null;
|
|
1370
|
-
advisory.advisoryScheduleKey = "pending-reseed";
|
|
1371
2837
|
nativeCompaction.lastThresholdDecision = null;
|
|
1372
2838
|
updateStatus(ctx);
|
|
1373
2839
|
};
|
|
1374
2840
|
pi.on("model_select", attributionChanged);
|
|
1375
2841
|
pi.on("thinking_level_select", attributionChanged);
|
|
1376
2842
|
|
|
1377
|
-
const armMilestoneForMeasurement = (
|
|
1378
|
-
snapshot: ActiveContextSnapshot,
|
|
1379
|
-
measurement: ProviderContextMeasurement,
|
|
1380
|
-
): boolean => {
|
|
1381
|
-
if (!persistence.state) return false;
|
|
1382
|
-
const ratio = contextUsageRatio(measurement);
|
|
1383
|
-
if (ratio === null) return false;
|
|
1384
|
-
const schedule = advisorySchedule(snapshot);
|
|
1385
|
-
const scheduleKey = sha256Value({
|
|
1386
|
-
schedule: schedule.key,
|
|
1387
|
-
provider: measurement.provider,
|
|
1388
|
-
model: measurement.model,
|
|
1389
|
-
contextWindow: measurement.contextWindow,
|
|
1390
|
-
});
|
|
1391
|
-
const scheduleChanged = advisory.advisoryScheduleKey !== null && advisory.advisoryScheduleKey !== scheduleKey;
|
|
1392
|
-
advisory.advisoryScheduleKey = scheduleKey;
|
|
1393
|
-
const advisoryBefore = stableStringify(advisoryState(persistence.state));
|
|
1394
|
-
const advisoryUpdate = updateAdvisoryMilestone(persistence.state, ratio, schedule, scheduleChanged, scheduleKey);
|
|
1395
|
-
persistence.state = advisoryUpdate.state;
|
|
1396
|
-
const armed = advisoryState(persistence.state).armed;
|
|
1397
|
-
if (armed && ratio < 0.85 * armed.threshold) {
|
|
1398
|
-
persistence.state = clearArmedAdvisory(persistence.state);
|
|
1399
|
-
advisory.armedMilestone = null;
|
|
1400
|
-
} else {
|
|
1401
|
-
advisory.armedMilestone = armed?.milestone ?? null;
|
|
1402
|
-
}
|
|
1403
|
-
return advisoryBefore !== stableStringify(advisoryState(persistence.state));
|
|
1404
|
-
};
|
|
1405
|
-
|
|
1406
2843
|
const accountAnchoredMeasurement = (measurement: ProviderContextMeasurement): boolean => {
|
|
1407
2844
|
if (!persistence.state) return false;
|
|
1408
2845
|
if (!measurements.lastProviderMeasurement) {
|
|
@@ -1423,6 +2860,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1423
2860
|
|
|
1424
2861
|
const handleContext = async (event: { messages: unknown[] }, ctx: any) => {
|
|
1425
2862
|
if (lifecycle.shuttingDown) return { messages: event.messages };
|
|
2863
|
+
beginMutationPass();
|
|
2864
|
+
clearCommitLatchBelowTrigger();
|
|
1426
2865
|
if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
|
|
1427
2866
|
lifecycle.latestSnapshot = null;
|
|
1428
2867
|
lifecycle.latestSnapshotError = null;
|
|
@@ -1443,13 +2882,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1443
2882
|
}
|
|
1444
2883
|
let observedMeasurement = latestProviderContextMeasurement(
|
|
1445
2884
|
snapshot.messages,
|
|
1446
|
-
|
|
2885
|
+
budgetWindowFor(ctx) ?? DEFAULT_CONTEXT_WINDOW,
|
|
1447
2886
|
ctx.model,
|
|
1448
2887
|
);
|
|
1449
2888
|
if (observedMeasurement && providerMeasurementBranchIndex(ctx, observedMeasurement) < 0) {
|
|
1450
2889
|
observedMeasurement = null;
|
|
1451
2890
|
}
|
|
1452
|
-
let advisoryChanged = false;
|
|
1453
2891
|
let measurementStateChanged = false;
|
|
1454
2892
|
if (observedMeasurement) {
|
|
1455
2893
|
const boundRevision = measurements.providerMeasurementRevisionByMessageSha.get(
|
|
@@ -1463,8 +2901,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1463
2901
|
measurements.latestRatio = contextUsageRatio(observedMeasurement);
|
|
1464
2902
|
if (durableProviderMeasurementMatches(observedMeasurement) && measurements.latestRatio !== null) {
|
|
1465
2903
|
measurementStateChanged = accountAnchoredMeasurement(observedMeasurement);
|
|
2904
|
+
// Calibrate BEFORE the new measurement becomes the previous one: the inflow
|
|
2905
|
+
// step is the difference between them.
|
|
2906
|
+
noteProjectionCalibration(observedMeasurement);
|
|
1466
2907
|
measurements.lastProviderMeasurement = observedMeasurement;
|
|
1467
|
-
advisoryChanged = armMilestoneForMeasurement(snapshot, observedMeasurement);
|
|
1468
2908
|
startPreparation(snapshot, measurements.latestRatio, ctx);
|
|
1469
2909
|
if (!ladder.automaticFailure && measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) {
|
|
1470
2910
|
mutationAttempted = true;
|
|
@@ -1474,18 +2914,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1474
2914
|
}
|
|
1475
2915
|
}
|
|
1476
2916
|
if (selectAutomaticToolForRung(snapshot, persistence.state, measurements.latestRatio)) mutationAttempted = true;
|
|
1477
|
-
const action = await attemptAutomaticRung(
|
|
2917
|
+
const action = await attemptAutomaticRung(
|
|
2918
|
+
snapshot, measurements.latestRatio, ctx, "context", { announcing: true },
|
|
2919
|
+
);
|
|
1478
2920
|
if (action) {
|
|
1479
2921
|
mutationAttempted = true;
|
|
1480
2922
|
persistedSucceeded = true;
|
|
1481
|
-
advisoryChanged = false;
|
|
1482
2923
|
}
|
|
1483
2924
|
} else measurements.lastProviderMeasurement = observedMeasurement;
|
|
1484
2925
|
} else {
|
|
1485
2926
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
1486
2927
|
}
|
|
1487
|
-
if (
|
|
1488
|
-
!sameStateProjection(persistence.state, persistence.persisted)) {
|
|
2928
|
+
if (measurementStateChanged && persistence.state &&
|
|
2929
|
+
persistence.persisted && !sameStateProjection(persistence.state, persistence.persisted)) {
|
|
1489
2930
|
mutationAttempted = true;
|
|
1490
2931
|
await persistThroughActionQueue(ctx);
|
|
1491
2932
|
persistedSucceeded = true;
|
|
@@ -1501,6 +2942,29 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1501
2942
|
updateStatus(ctx);
|
|
1502
2943
|
return { messages: event.messages };
|
|
1503
2944
|
}
|
|
2945
|
+
// A projection larger than the serving budget is never transmitted, whatever the
|
|
2946
|
+
// last measured ratio says about the request before it.
|
|
2947
|
+
const budgeted = await enforceProjectionBudget(snapshot, projected, ctx);
|
|
2948
|
+
projected = budgeted.projected;
|
|
2949
|
+
// Every projection this process BUILDS is a size reference for the calibration,
|
|
2950
|
+
// including one the fence aborted. Recording only sent projections locks an
|
|
2951
|
+
// already-large session out of calibrating at all: its first projection aborts
|
|
2952
|
+
// under the uncalibrated constant, so nothing is ever sent, so nothing ever
|
|
2953
|
+
// measures the ratio, so it aborts forever.
|
|
2954
|
+
measurements.lastProjectedChars = bytes(projected);
|
|
2955
|
+
measurements.lastProjectedEstimate = projectedTokenEstimate(projected);
|
|
2956
|
+
measurements.lastProjectedEstimateCalibrated = measurements.projectionCalibrations.length > 0;
|
|
2957
|
+
// An aborted request still returns the PROJECTION. Handing back the raw branch
|
|
2958
|
+
// instead makes the aborted turn the largest message list the session ever
|
|
2959
|
+
// produced -- the corpus, folds and all -- which is the exact inverse of what a
|
|
2960
|
+
// fence is for, and it is what an abort looked like in rep12: 3,952,934 chars
|
|
2961
|
+
// against a 1,322,385-char projection of the same session. It is not noted as a
|
|
2962
|
+
// cache observation, because it was never sent.
|
|
2963
|
+
if (budgeted.aborted) {
|
|
2964
|
+
updateStatus(ctx);
|
|
2965
|
+
return { messages: projected };
|
|
2966
|
+
}
|
|
2967
|
+
noteProjection(projected);
|
|
1504
2968
|
updateStatus(ctx);
|
|
1505
2969
|
return { messages: projected };
|
|
1506
2970
|
} catch (error) {
|
|
@@ -1547,6 +3011,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1547
3011
|
!durableProviderMeasurementMatches(measurement) ||
|
|
1548
3012
|
measuredRatio === null) return;
|
|
1549
3013
|
const measurementStateChanged = accountAnchoredMeasurement(measurement);
|
|
3014
|
+
// The provider accepted a request: whatever the recovery lane spent belongs to an
|
|
3015
|
+
// inflow that is now behind us, and the cap resets for the next one.
|
|
3016
|
+
curation.recoveryAttempts = 0;
|
|
3017
|
+
curation.pendingRejection = null;
|
|
1550
3018
|
measurements.lastProviderMeasurement = measurement;
|
|
1551
3019
|
measurements.latestRatio = measuredRatio;
|
|
1552
3020
|
let snapshot: ActiveContextSnapshot;
|
|
@@ -1555,7 +3023,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1555
3023
|
updateStatus(ctx);
|
|
1556
3024
|
return;
|
|
1557
3025
|
}
|
|
1558
|
-
const advisoryChanged = armMilestoneForMeasurement(snapshot, measurement);
|
|
1559
3026
|
startPreparation(snapshot, measurements.latestRatio, ctx);
|
|
1560
3027
|
if (measurements.latestRatio >= hardFenceRatio(measurement, ctx) && ladder.preparing) await ladder.preparing.promise;
|
|
1561
3028
|
if (!sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration) ||
|
|
@@ -1566,7 +3033,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1566
3033
|
ctx,
|
|
1567
3034
|
"message-end",
|
|
1568
3035
|
);
|
|
1569
|
-
if (!action &&
|
|
3036
|
+
if (!action && measurementStateChanged && persistence.state && persistence.persisted &&
|
|
1570
3037
|
!sameStateProjection(persistence.state, persistence.persisted)) {
|
|
1571
3038
|
await persistThroughActionQueue(ctx);
|
|
1572
3039
|
}
|
|
@@ -1575,11 +3042,31 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1575
3042
|
|
|
1576
3043
|
pi.on("message_end", async (event: Record<string, unknown>, ctx: any) => {
|
|
1577
3044
|
if (lifecycle.shuttingDown) return;
|
|
3045
|
+
beginMutationPass();
|
|
1578
3046
|
try {
|
|
1579
3047
|
const message = ownValue(event, "message");
|
|
3048
|
+
instrumentation.requests += 1;
|
|
3049
|
+
const observation = observeCacheUsage(instrumentation.ledger, {
|
|
3050
|
+
usage: ownValue(message, "usage"),
|
|
3051
|
+
change: instrumentation.lastChange,
|
|
3052
|
+
preservedShare: instrumentation.lastPreservedShare,
|
|
3053
|
+
});
|
|
3054
|
+
if (observation) {
|
|
3055
|
+
// The join key: this is where the stream meets provider-side telemetry.
|
|
3056
|
+
emit("context.usage", {
|
|
3057
|
+
provider: ownValue(message, "provider") ?? null,
|
|
3058
|
+
model: ownValue(message, "model") ?? null,
|
|
3059
|
+
input_tokens: observation.inputTokens,
|
|
3060
|
+
cache_read_tokens: observation.cacheReadTokens,
|
|
3061
|
+
total_tokens: providerTokens(message),
|
|
3062
|
+
projection_change: observation.change,
|
|
3063
|
+
provider_side_miss: observation.providerSideMiss,
|
|
3064
|
+
message_sha256: evidenceSha256(message),
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
1580
3067
|
const measurement = providerContextMeasurement(
|
|
1581
3068
|
message,
|
|
1582
|
-
|
|
3069
|
+
budgetWindowFor(ctx) ?? DEFAULT_CONTEXT_WINDOW,
|
|
1583
3070
|
ctx.model,
|
|
1584
3071
|
);
|
|
1585
3072
|
if (!measurement || !persistence.state) return;
|
|
@@ -1602,11 +3089,25 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1602
3089
|
try { updateStatus(ctx); } catch { /* The provider loop must keep running. */ }
|
|
1603
3090
|
}
|
|
1604
3091
|
});
|
|
3092
|
+
/**
|
|
3093
|
+
* A provider rejection is ground truth that the last request did not fit, and it
|
|
3094
|
+
* mutates nothing durable: no assistant message lands, and the projection is rebuilt
|
|
3095
|
+
* from the branch on the next request. So it is recorded, not fatal -- the next
|
|
3096
|
+
* context pass folds at fence pressure and rebuilds the request rather than dying.
|
|
3097
|
+
* A 429 is a rate limit, not an overflow, and is deliberately not recovered from.
|
|
3098
|
+
*/
|
|
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
|
+
});
|
|
1605
3105
|
pi.on("tool_call", (event: Record<string, unknown>, ctx: any) => {
|
|
1606
3106
|
const calledTool = ownValue(event, "toolName");
|
|
1607
3107
|
if (lifecycle.shuttingDown || lifecycle.blockingToolHarvestedThisTurn || lifecycle.blockingToolHarvestQueuedThisTurn ||
|
|
1608
3108
|
typeof calledTool !== "string" || !blockingTools.has(calledTool)) return;
|
|
1609
3109
|
lifecycle.blockingToolHarvestQueuedThisTurn = true;
|
|
3110
|
+
beginMutationPass();
|
|
1610
3111
|
const operation = ladder.actionQueue.then(async () => {
|
|
1611
3112
|
try {
|
|
1612
3113
|
if (!persistence.state || measurements.latestRatio === null || !measurements.lastProviderMeasurement || ladder.automaticFailure ||
|
|
@@ -1637,6 +3138,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1637
3138
|
ladder.actionQueue = operation.catch(() => undefined);
|
|
1638
3139
|
});
|
|
1639
3140
|
pi.on("turn_end", async (_event: unknown, ctx: any) => {
|
|
3141
|
+
beginMutationPass();
|
|
1640
3142
|
lifecycle.blockingToolHarvestedThisTurn = false;
|
|
1641
3143
|
lifecycle.blockingToolHarvestQueuedThisTurn = false;
|
|
1642
3144
|
if (lifecycle.shuttingDown || !persistence.state || !persistence.persisted) return;
|
|
@@ -1706,6 +3208,51 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1706
3208
|
updateStatus(ctx);
|
|
1707
3209
|
});
|
|
1708
3210
|
|
|
3211
|
+
/**
|
|
3212
|
+
* Admission control. A refusal that names no next action is denial, not governance,
|
|
3213
|
+
* so every refusal carries at least one CONSTRUCTIBLE alternative: a bounded slice
|
|
3214
|
+
* sized to the headroom we actually have, a child fold that is smaller by
|
|
3215
|
+
* construction, and the commit that frees the room. The exact stored size of the
|
|
3216
|
+
* target is known, so this is arithmetic, not a guess.
|
|
3217
|
+
*/
|
|
3218
|
+
const admit = (input: {
|
|
3219
|
+
action: "peek" | "expand";
|
|
3220
|
+
ctx: any;
|
|
3221
|
+
foldId: string;
|
|
3222
|
+
requestedBytes: number;
|
|
3223
|
+
children: string[];
|
|
3224
|
+
}): void => {
|
|
3225
|
+
const capacity = currentCapacity(input.ctx);
|
|
3226
|
+
const verdict = admissionVerdict({
|
|
3227
|
+
requestedBytes: input.requestedBytes,
|
|
3228
|
+
capacity,
|
|
3229
|
+
bytesPerToken: ESTIMATED_BYTES_PER_TOKEN,
|
|
3230
|
+
});
|
|
3231
|
+
if (verdict.admitted) return;
|
|
3232
|
+
const sliceBytes = Math.max(
|
|
3233
|
+
PEEK_MIN_SLICE_BYTES,
|
|
3234
|
+
Math.floor(verdict.affordableBytes * 0.9),
|
|
3235
|
+
);
|
|
3236
|
+
const alternatives: Array<Record<string, unknown>> = [];
|
|
3237
|
+
if (verdict.affordableBytes >= PEEK_MIN_SLICE_BYTES) {
|
|
3238
|
+
alternatives.push({ action: "peek", id: input.foldId, offset: 0, bytes: sliceBytes });
|
|
3239
|
+
}
|
|
3240
|
+
for (const child of input.children.slice(0, 3)) {
|
|
3241
|
+
alternatives.push({ action: "peek", id: child });
|
|
3242
|
+
}
|
|
3243
|
+
if (!alternatives.length) {
|
|
3244
|
+
// The floor: folding is always available, and it is the action that creates
|
|
3245
|
+
// the room this read needs.
|
|
3246
|
+
alternatives.push({ action: "status", detail: "fold_candidates" });
|
|
3247
|
+
}
|
|
3248
|
+
throw new Error(
|
|
3249
|
+
`${input.action} of ${input.foldId} needs ~${verdict.requestedTokens} tokens and only ` +
|
|
3250
|
+
`${verdict.headroomTokens} remain of the ${verdict.budgetTokens}-token budget; it was refused ` +
|
|
3251
|
+
"before it could cross the fence. Free room or read less: " +
|
|
3252
|
+
JSON.stringify(alternatives),
|
|
3253
|
+
);
|
|
3254
|
+
};
|
|
3255
|
+
|
|
1709
3256
|
const executeAction = async (
|
|
1710
3257
|
params: Record<string, unknown>,
|
|
1711
3258
|
signal: AbortSignal | undefined,
|
|
@@ -1729,28 +3276,40 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1729
3276
|
const snapshot = authoritativeSnapshotFor(ctx);
|
|
1730
3277
|
if (action === "status") {
|
|
1731
3278
|
const detail = ownValue(params, "detail");
|
|
1732
|
-
|
|
1733
|
-
|
|
3279
|
+
const details = ["fold_candidates", "tree", "folds", "objects"];
|
|
3280
|
+
if (detail !== undefined && !details.includes(String(detail))) {
|
|
3281
|
+
throw new Error(`status detail must be one of ${details.map((name) => `'${name}'`).join(", ")}`);
|
|
1734
3282
|
}
|
|
1735
|
-
const
|
|
1736
|
-
|
|
3283
|
+
const statusOffset = boundedInteger(params.offset, 0, 0, 1_000_000, "offset");
|
|
3284
|
+
const statusLimit = boundedInteger(params.limit, 40, 1, 100, "limit");
|
|
3285
|
+
// Paging is explicit and agent-driven: the full tree is reachable, it just
|
|
3286
|
+
// stops riding along on every request that wanted a count.
|
|
3287
|
+
const paged = detail === "folds" || detail === "objects";
|
|
3288
|
+
const accounting = markAccounting(snapshot, persistence.state);
|
|
3289
|
+
return toolPayload(boundStatusPayload({
|
|
1737
3290
|
...activeContextStatus(
|
|
1738
3291
|
snapshot,
|
|
1739
3292
|
persistence.state,
|
|
1740
|
-
|
|
1741
|
-
|
|
3293
|
+
statusOffset,
|
|
3294
|
+
statusLimit,
|
|
1742
3295
|
snapshot.policy.maxFoldSourceRefs,
|
|
3296
|
+
{ diet: !paged },
|
|
1743
3297
|
),
|
|
3298
|
+
headroomTokens: currentCapacity(ctx).headroomTokens,
|
|
3299
|
+
budgetTokens: currentCapacity(ctx).budgetTokens,
|
|
3300
|
+
pendingMarks: accounting.pending,
|
|
3301
|
+
eligibleMarks: accounting.eligibleMarks,
|
|
3302
|
+
retainedMarks: accounting.retainedMarks,
|
|
3303
|
+
eligibleMarkedShare: accounting.eligibleFreedWindowShare,
|
|
3304
|
+
markedShare: accounting.freedWindowShare,
|
|
1744
3305
|
available: true,
|
|
1745
3306
|
automatic: {
|
|
1746
3307
|
pressureRatio: measurements.latestRatio,
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
advisory: advisoryState(persistence.state),
|
|
1753
|
-
historicalGuidanceEntries: advisory.historicalGuidanceEntries,
|
|
3308
|
+
surfacing: {
|
|
3309
|
+
rendered: false,
|
|
3310
|
+
sources: surfacing.sources.map((source) => source.id),
|
|
3311
|
+
log: clone(persistence.state.surfacing ?? []),
|
|
3312
|
+
},
|
|
1754
3313
|
warningRatio: snapshot.policy.warningRatio,
|
|
1755
3314
|
toolFoldRatio: snapshot.policy.toolFoldRatio,
|
|
1756
3315
|
refoldRatio: snapshot.policy.refoldRatio,
|
|
@@ -1761,6 +3320,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1761
3320
|
Math.floor(snapshot.contextWindow * 0.1),
|
|
1762
3321
|
),
|
|
1763
3322
|
windowSource: snapshot.windowSource,
|
|
3323
|
+
capacity: {
|
|
3324
|
+
...currentCapacity(ctx),
|
|
3325
|
+
bytesPerToken: projectionCharsPerToken(),
|
|
3326
|
+
},
|
|
1764
3327
|
consolidationRatio: snapshot.policy.consolidationRatio,
|
|
1765
3328
|
providerMeasurement: measurements.lastProviderMeasurement ? {
|
|
1766
3329
|
tokens: measurements.lastProviderMeasurement.tokens,
|
|
@@ -1781,10 +3344,57 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1781
3344
|
lastSelectionKind: ladder.lastSelectionKind,
|
|
1782
3345
|
lastSelectionSourceIds: ladder.lastSelectionSourceIds,
|
|
1783
3346
|
lastAutomaticAction: ladder.lastAutomaticAction,
|
|
3347
|
+
overBudgetReduction: ladder.overBudgetReduction ? clone(ladder.overBudgetReduction) : null,
|
|
3348
|
+
curation: {
|
|
3349
|
+
occupancyThreshold: CURATION_OCCUPANCY_SHARE,
|
|
3350
|
+
staleToolThreshold: CURATION_STALE_TOOL_SHARE,
|
|
3351
|
+
signals: curation.lastSignals ? { ...curation.lastSignals } : null,
|
|
3352
|
+
contextCalls: curation.contextCalls,
|
|
3353
|
+
receipts: curation.receipts.map((receipt) => ({ ...receipt })),
|
|
3354
|
+
},
|
|
3355
|
+
recovery: {
|
|
3356
|
+
attempts: curation.recoveryAttempts,
|
|
3357
|
+
maxAttempts: OVERFLOW_RECOVERY_MAX_ATTEMPTS,
|
|
3358
|
+
pendingRejection: curation.pendingRejection ? { ...curation.pendingRejection } : null,
|
|
3359
|
+
last: curation.lastRecovery ? clone(curation.lastRecovery) : null,
|
|
3360
|
+
},
|
|
3361
|
+
foldSpanCap: MAX_FOLD_SPAN_CHARS,
|
|
3362
|
+
projectionBudgetTokens: currentCapacity(ctx).budgetTokens,
|
|
3363
|
+
projectionCharsPerToken: projectionCharsPerToken(),
|
|
3364
|
+
projectionEstimatorErrorShare: estimatorErrorShare(),
|
|
3365
|
+
projectionExpectedInflowTokens: expectedInflowTokens(),
|
|
3366
|
+
projectionMarginTokens: measurements.lastProjectedChars === null
|
|
3367
|
+
? null
|
|
3368
|
+
: projectionMarginTokens(
|
|
3369
|
+
Math.ceil(measurements.lastProjectedChars / projectionCharsPerToken()),
|
|
3370
|
+
currentCapacity(ctx).window,
|
|
3371
|
+
),
|
|
3372
|
+
projectionEstimatedTokens: measurements.lastProjectedChars === null
|
|
3373
|
+
? null
|
|
3374
|
+
: Math.ceil(measurements.lastProjectedChars / projectionCharsPerToken()),
|
|
1784
3375
|
automaticSuspended: ladder.automaticFailure !== null,
|
|
1785
3376
|
automaticFailure: ladder.automaticFailure ? clone(ladder.automaticFailure) : null,
|
|
1786
3377
|
lastCompactionDecision: nativeCompaction.lastThresholdDecision,
|
|
3378
|
+
scheduling: schedulingStatus({
|
|
3379
|
+
snapshot,
|
|
3380
|
+
state: persistence.state,
|
|
3381
|
+
mode: foldScheduling,
|
|
3382
|
+
ratio: measurements.latestRatio,
|
|
3383
|
+
}),
|
|
1787
3384
|
nativeSummaries: "disabled",
|
|
3385
|
+
instrumentation: {
|
|
3386
|
+
enabled: true,
|
|
3387
|
+
...ledgerSummary(instrumentation.ledger),
|
|
3388
|
+
projectionRecords: clone(instrumentation.ledger.records),
|
|
3389
|
+
cacheObservations: clone(instrumentation.ledger.observations),
|
|
3390
|
+
// THE stream, as the durable entries carry it. One shape, one convention.
|
|
3391
|
+
events: clone(instrumentation.ledger.events),
|
|
3392
|
+
},
|
|
3393
|
+
foldPeekResults,
|
|
3394
|
+
peek: {
|
|
3395
|
+
defaultMaxBytes: PEEK_DEFAULT_MAX_BYTES,
|
|
3396
|
+
lifetime: "append-only",
|
|
3397
|
+
},
|
|
1788
3398
|
freeHarvest: blockingTools.size === 0 ? "disabled" : "enabled",
|
|
1789
3399
|
pressureSource: "last-successful-provider-response-only",
|
|
1790
3400
|
postOverflowCallback: "blocked-while-stock-native-compaction-is-disabled",
|
|
@@ -1801,12 +3411,208 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1801
3411
|
failedPreparationIds: ladder.failedPreparations,
|
|
1802
3412
|
}),
|
|
1803
3413
|
} : {}),
|
|
3414
|
+
...(detail === "tree" ? { tree: foldTreeDetail(snapshot, persistence.state).slice(statusOffset) } : {}),
|
|
3415
|
+
}, typeof detail === "string" ? detail : null, statusOffset));
|
|
3416
|
+
}
|
|
3417
|
+
// Peeking or expanding a suggested fold is the accept signal, recorded for the
|
|
3418
|
+
// retrieval build even though nothing suggests anything today. It stays ephemeral:
|
|
3419
|
+
// the mark rides the next persisted state, never an entry of its own.
|
|
3420
|
+
const noteSurfacingAccept = (id: string): void => {
|
|
3421
|
+
if (!persistence.state) return;
|
|
3422
|
+
persistence.state = acceptSurfacingSuggestion(persistence.state, id, surfacingOrdinal(snapshot));
|
|
3423
|
+
};
|
|
3424
|
+
if (action === "peek") {
|
|
3425
|
+
const id = String(params.id ?? "").trim();
|
|
3426
|
+
if (!id) throw new Error("peek requires id");
|
|
3427
|
+
const offset = boundedInteger(params.offset, 0, 0, 1_000_000_000, "offset");
|
|
3428
|
+
// The DEFAULT is the bounded index view. Widening is an explicit argument, so
|
|
3429
|
+
// the default path cannot overfill a window: measured 2026-08-06, 14 raw peek
|
|
3430
|
+
// results held 1.9M characters, 82 percent of everything still unfolded.
|
|
3431
|
+
const sliceBytes = boundedInteger(
|
|
3432
|
+
params.bytes,
|
|
3433
|
+
PEEK_DEFAULT_MAX_BYTES,
|
|
3434
|
+
PEEK_MIN_SLICE_BYTES,
|
|
3435
|
+
snapshot.policy.maxChapterChars,
|
|
3436
|
+
"bytes",
|
|
3437
|
+
);
|
|
3438
|
+
const target = persistence.state.folds.find((item) => item.id === id);
|
|
3439
|
+
if (!target) throw new Error(`Unknown active-context fold ${id}`);
|
|
3440
|
+
admit({
|
|
3441
|
+
action: "peek",
|
|
3442
|
+
ctx,
|
|
3443
|
+
foldId: id,
|
|
3444
|
+
requestedBytes: Math.max(0, Math.min(sliceBytes, target.sourceChars - offset)),
|
|
3445
|
+
children: childFoldIds(target),
|
|
3446
|
+
});
|
|
3447
|
+
const payload = toolPayload(peekFoldSource({
|
|
3448
|
+
foldId: id,
|
|
3449
|
+
state: persistence.state,
|
|
3450
|
+
entries: ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch(),
|
|
3451
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
3452
|
+
maximumBytes: sliceBytes,
|
|
3453
|
+
offset,
|
|
3454
|
+
toolName,
|
|
3455
|
+
}));
|
|
3456
|
+
noteSurfacingAccept(id);
|
|
3457
|
+
return payload;
|
|
3458
|
+
}
|
|
3459
|
+
if (action === "rebrief") {
|
|
3460
|
+
const id = String(params.id ?? "").trim();
|
|
3461
|
+
if (!id) throw new Error("rebrief requires id");
|
|
3462
|
+
const brief = typeof params.brief === "string" ? params.brief.trim() : "";
|
|
3463
|
+
if (!brief) throw new Error("rebrief requires a nonempty brief");
|
|
3464
|
+
if (brief.length > snapshot.policy.maxBriefChars) {
|
|
3465
|
+
throw new Error(`rebrief brief must be at most ${snapshot.policy.maxBriefChars} characters`);
|
|
3466
|
+
}
|
|
3467
|
+
const fold = requireActiveFold(snapshot, persistence.state, id);
|
|
3468
|
+
const previous = foldBrief(fold, persistence.state);
|
|
3469
|
+
const briefs = { ...(persistence.state.briefs ?? {}), [id]: brief };
|
|
3470
|
+
await persistManual({ ...persistence.state, revision: persistence.state.revision + 1, briefs }, action, ctx);
|
|
3471
|
+
updateStatus(ctx);
|
|
3472
|
+
return toolPayload({
|
|
3473
|
+
version: 1,
|
|
3474
|
+
action,
|
|
3475
|
+
id,
|
|
3476
|
+
brief,
|
|
3477
|
+
previousBrief: previous,
|
|
3478
|
+
durableRevision: persistence.state.revision,
|
|
3479
|
+
activation: "the fold's placeholder, index rows and suggestions now carry your brief; " +
|
|
3480
|
+
"the exact source and its fold record are untouched.",
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
if (action === "reboundary") {
|
|
3484
|
+
// One verb, both directions. `ids` names the span you want to BE one fold: every
|
|
3485
|
+
// fold overlapping it dissolves and the span is re-cut as one, which merges N
|
|
3486
|
+
// adjacent folds when the span covers them and splits one fold when the span sits
|
|
3487
|
+
// inside it. `id` alone is the plain dissolve, which returns a span to raw.
|
|
3488
|
+
if (params.ids !== undefined) {
|
|
3489
|
+
const requestedIds = stringIds(params.ids);
|
|
3490
|
+
const snapped = snapToFoldBoundaries(snapshot, persistence.state, requestedIds);
|
|
3491
|
+
const dissolvedIds = snapped.covered.map((item) => item.id);
|
|
3492
|
+
const briefs = { ...(persistence.state.briefs ?? {}) };
|
|
3493
|
+
for (const dissolvedId of dissolvedIds) delete briefs[dissolvedId];
|
|
3494
|
+
const leases = { ...persistence.state.leases };
|
|
3495
|
+
for (const dissolvedId of dissolvedIds) delete leases[dissolvedId];
|
|
3496
|
+
const dissolvedSet = new Set(dissolvedIds);
|
|
3497
|
+
const flattened: ActiveContextState = {
|
|
3498
|
+
...persistence.state,
|
|
3499
|
+
revision: persistence.state.revision + 1,
|
|
3500
|
+
folds: deriveFoldParents(persistence.state.folds.filter((item) =>
|
|
3501
|
+
!dissolvedSet.has(item.id) && !dissolvedIds.some((rootId) =>
|
|
3502
|
+
descendantIds(persistence.state!, rootId).has(item.id)))),
|
|
3503
|
+
expanded: persistence.state.expanded.filter((expandedId) => !dissolvedSet.has(expandedId)),
|
|
3504
|
+
leases,
|
|
3505
|
+
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
3506
|
+
};
|
|
3507
|
+
if (!Object.keys(briefs).length) delete flattened.briefs;
|
|
3508
|
+
persistence.state = flattened;
|
|
3509
|
+
const supplied = typeof params.brief === "string" && params.brief.trim() ? params.brief : undefined;
|
|
3510
|
+
const recut = manualFoldCandidate(snapshot, persistence.state, snapped.ids, { allowProtected: true });
|
|
3511
|
+
const created: Array<Record<string, unknown>> = [];
|
|
3512
|
+
const correctedBriefs: Record<string, string> = {};
|
|
3513
|
+
for (const part of splitCandidateBySize(snapshot, persistence.state, recut)) {
|
|
3514
|
+
// A re-cut that lands back on a span this session already folded meets its own
|
|
3515
|
+
// immutable record. The record wins -- rewriting it would report a conflicting
|
|
3516
|
+
// durable fold and suspend automatic management -- and the supplied brief
|
|
3517
|
+
// becomes a correction beside it, which is what `rebrief` writes anyway.
|
|
3518
|
+
const durable = persistence.persistedFoldRecords.get(foldIdFor(part.kind, part.parts));
|
|
3519
|
+
const { preparedFold, nextState } = await prepareAndCommitExplicit({
|
|
3520
|
+
snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied, ctx, signal,
|
|
3521
|
+
});
|
|
3522
|
+
persistence.state = nextState;
|
|
3523
|
+
if (durable && supplied) correctedBriefs[preparedFold.id] = supplied;
|
|
3524
|
+
created.push({
|
|
3525
|
+
id: preparedFold.id,
|
|
3526
|
+
kind: preparedFold.fold.kind,
|
|
3527
|
+
brief: supplied ?? preparedFold.fold.brief,
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
if (Object.keys(correctedBriefs).length) {
|
|
3531
|
+
persistence.state = {
|
|
3532
|
+
...persistence.state,
|
|
3533
|
+
briefs: { ...(persistence.state.briefs ?? {}), ...correctedBriefs },
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
await persistManual(persistence.state, action, ctx);
|
|
3537
|
+
updateStatus(ctx);
|
|
3538
|
+
return toolPayload({
|
|
3539
|
+
version: 1,
|
|
3540
|
+
action,
|
|
3541
|
+
mode: dissolvedIds.length > 1 ? "merge" : "recut",
|
|
3542
|
+
dissolved: dissolvedIds,
|
|
3543
|
+
created,
|
|
3544
|
+
corrections: snapped.corrections,
|
|
3545
|
+
startId: snapped.ids[0],
|
|
3546
|
+
endId: snapped.ids[1],
|
|
3547
|
+
durableRevision: persistence.state.revision,
|
|
3548
|
+
activation: "durable immediately; the named span is now exactly one fold per bite-size cap, " +
|
|
3549
|
+
"and every dissolved fold's evidence is inside it or raw again.",
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
const id = String(params.id ?? "").trim();
|
|
3553
|
+
if (!id) throw new Error("reboundary requires id, or ids naming the span to re-cut");
|
|
3554
|
+
const fold = requireActiveFold(snapshot, persistence.state, id);
|
|
3555
|
+
if (fold.parentId) {
|
|
3556
|
+
throw new Error(
|
|
3557
|
+
`Fold ${id} is nested under ${fold.parentId}; dissolve ${fold.parentId} first so ${id} is a root`,
|
|
3558
|
+
);
|
|
3559
|
+
}
|
|
3560
|
+
const refs = flattenFoldRefs(fold, persistence.state);
|
|
3561
|
+
const children = childFoldIds(fold);
|
|
3562
|
+
const briefs = { ...(persistence.state.briefs ?? {}) };
|
|
3563
|
+
delete briefs[id];
|
|
3564
|
+
const next: ActiveContextState = {
|
|
3565
|
+
...persistence.state,
|
|
3566
|
+
revision: persistence.state.revision + 1,
|
|
3567
|
+
folds: deriveFoldParents(persistence.state.folds.filter((item) => item.id !== id)),
|
|
3568
|
+
expanded: persistence.state.expanded.filter((expandedId) => expandedId !== id),
|
|
3569
|
+
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
3570
|
+
};
|
|
3571
|
+
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3572
|
+
const leases = { ...next.leases };
|
|
3573
|
+
delete leases[id];
|
|
3574
|
+
await persistManual({ ...next, leases }, action, ctx);
|
|
3575
|
+
updateStatus(ctx);
|
|
3576
|
+
return toolPayload({
|
|
3577
|
+
version: 1,
|
|
3578
|
+
action,
|
|
3579
|
+
id,
|
|
3580
|
+
dissolved: true,
|
|
3581
|
+
releasedChildren: children,
|
|
3582
|
+
startId: refs[0]?.entryId ?? null,
|
|
3583
|
+
endId: refs.at(-1)?.entryId ?? null,
|
|
3584
|
+
durableRevision: persistence.state.revision,
|
|
3585
|
+
activation: `the span ${id} held is raw again and its boundary is yours to re-cut; ` +
|
|
3586
|
+
"fold the endpoints you meant, or two sub-spans to split it. No evidence moved.",
|
|
1804
3587
|
});
|
|
1805
3588
|
}
|
|
1806
3589
|
if (action === "expand" || action === "refold") {
|
|
1807
3590
|
const id = String(params.id ?? "").trim();
|
|
1808
3591
|
if (!id) throw new Error(`${action} requires id`);
|
|
1809
|
-
|
|
3592
|
+
// An expand with marks pending opens the commit epoch: the restore and the
|
|
3593
|
+
// batch of folds then cost one rewrite between them instead of two.
|
|
3594
|
+
let epochApplied: unknown[] = [];
|
|
3595
|
+
if (epochScheduling && action === "expand" && pendingMarks(persistence.state).length) {
|
|
3596
|
+
const result = await commitPendingMarks({
|
|
3597
|
+
snapshot,
|
|
3598
|
+
state: persistence.state,
|
|
3599
|
+
generation: lifecycle.generation,
|
|
3600
|
+
retainIneligible: true,
|
|
3601
|
+
});
|
|
3602
|
+
persistence.state = result.state;
|
|
3603
|
+
epochApplied = result.applied;
|
|
3604
|
+
}
|
|
3605
|
+
const expanding = requireActiveFold(snapshot, persistence.state, id);
|
|
3606
|
+
if (action === "expand") {
|
|
3607
|
+
admit({
|
|
3608
|
+
action: "expand",
|
|
3609
|
+
ctx,
|
|
3610
|
+
foldId: id,
|
|
3611
|
+
requestedBytes: Math.max(0, expanding.sourceChars - expanding.placeholderChars),
|
|
3612
|
+
children: childFoldIds(expanding),
|
|
3613
|
+
});
|
|
3614
|
+
noteSurfacingAccept(id);
|
|
3615
|
+
}
|
|
1810
3616
|
let next = setFoldProjectionState(persistence.state, id, action === "expand" ? "expanded" : "folded");
|
|
1811
3617
|
if (action === "expand") next = withExpandLease(next, id);
|
|
1812
3618
|
else {
|
|
@@ -1825,6 +3631,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1825
3631
|
action,
|
|
1826
3632
|
id,
|
|
1827
3633
|
state: action === "expand" ? "expanded" : "folded",
|
|
3634
|
+
...(epochApplied.length ? { committedMarks: epochApplied } : {}),
|
|
1828
3635
|
activation: "durable immediately; projected on the next model call in this same turn",
|
|
1829
3636
|
});
|
|
1830
3637
|
}
|
|
@@ -1839,35 +3646,258 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1839
3646
|
activation: "durable immediately; projected on the next model call in this same turn",
|
|
1840
3647
|
});
|
|
1841
3648
|
}
|
|
1842
|
-
if (action === "
|
|
3649
|
+
if (action === "unmark") {
|
|
1843
3650
|
const ids = stringIds(params.ids);
|
|
1844
|
-
const
|
|
1845
|
-
const
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
3651
|
+
const requested = new Set(ids);
|
|
3652
|
+
const before = pendingMarks(persistence.state);
|
|
3653
|
+
const kept = before.filter((mark) => !requested.has(mark.id));
|
|
3654
|
+
const removed = before.filter((mark) => requested.has(mark.id));
|
|
3655
|
+
const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
|
|
3656
|
+
if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
|
|
3657
|
+
await persistManual(withPendingMarks(persistence.state, kept), action, ctx);
|
|
3658
|
+
updateStatus(ctx);
|
|
3659
|
+
const accounting = markAccounting(snapshot, persistence.state);
|
|
3660
|
+
return toolPayload({
|
|
3661
|
+
version: 1,
|
|
3662
|
+
action,
|
|
3663
|
+
unmarked: removed.map((mark) => ({ mark: mark.mark, id: mark.id, origin: mark.origin })),
|
|
3664
|
+
pendingMarks: accounting.pending,
|
|
3665
|
+
eligibleMarks: accounting.eligibleMarks,
|
|
3666
|
+
retainedMarks: accounting.retainedMarks,
|
|
3667
|
+
durableRevision: persistence.state.revision,
|
|
3668
|
+
activation: "the withdrawn marks will never be applied; no context bytes moved",
|
|
1852
3669
|
});
|
|
1853
|
-
|
|
1854
|
-
|
|
3670
|
+
}
|
|
3671
|
+
if (action === "fold") {
|
|
3672
|
+
// One call, several {span, brief} pairs. Marking is free and committing is not,
|
|
3673
|
+
// so the shape that should be cheapest to express is the BATCH: an agent that
|
|
3674
|
+
// must spend one call per span curates less than one that spends one call.
|
|
3675
|
+
const requested = batchedMarkRequests(params);
|
|
3676
|
+
const resolved: Array<{
|
|
3677
|
+
candidate: FoldCandidate;
|
|
3678
|
+
brief?: string;
|
|
3679
|
+
corrections: SpanCorrection[];
|
|
3680
|
+
splitFrom: number;
|
|
3681
|
+
}> = [];
|
|
3682
|
+
for (const request of requested) {
|
|
3683
|
+
const snapped = snapFoldCandidate(snapshot, persistence.state, request.ids, { allowProtected: true });
|
|
3684
|
+
const spanChars = candidateSpanChars(snapshot, persistence.state, snapped.candidate);
|
|
3685
|
+
const parts = splitCandidateBySize(snapshot, persistence.state, snapped.candidate);
|
|
3686
|
+
for (const [index, part] of parts.entries()) {
|
|
3687
|
+
resolved.push({
|
|
3688
|
+
candidate: part,
|
|
3689
|
+
brief: request.brief,
|
|
3690
|
+
corrections: index === 0
|
|
3691
|
+
? [
|
|
3692
|
+
...snapped.corrections,
|
|
3693
|
+
...(parts.length > 1
|
|
3694
|
+
? [{
|
|
3695
|
+
from: request.ids,
|
|
3696
|
+
to: [],
|
|
3697
|
+
reason: `span was ${spanChars} chars, over the ${MAX_FOLD_SPAN_CHARS}-char bite-size cap; ` +
|
|
3698
|
+
`split into ${parts.length} sequential folds, each with its own brief`,
|
|
3699
|
+
}]
|
|
3700
|
+
: []),
|
|
3701
|
+
]
|
|
3702
|
+
: [],
|
|
3703
|
+
splitFrom: parts.length > 1 && index === 0 ? spanChars : 0,
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
if (parts.length > 1) {
|
|
3707
|
+
emit("context.split", {
|
|
3708
|
+
source: "agent",
|
|
3709
|
+
span_chars: spanChars,
|
|
3710
|
+
parts: parts.length,
|
|
3711
|
+
cap_chars: MAX_FOLD_SPAN_CHARS,
|
|
3712
|
+
fold_ids: "",
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
const corrections = resolved.flatMap((item) => item.corrections);
|
|
3717
|
+
if (epochScheduling) {
|
|
3718
|
+
// Resolve every brief now, while the source is in hand, so the commit epoch
|
|
3719
|
+
// itself stays deterministic and free of provider calls. A span that is not
|
|
3720
|
+
// eligible YET cannot go through preparation at all, so it takes the
|
|
3721
|
+
// deterministic brief: the mark is a decision, and refusing to record it
|
|
3722
|
+
// because the span is momentarily fresh is exactly the drop being fixed.
|
|
3723
|
+
const marks: Array<Record<string, unknown>> = [];
|
|
3724
|
+
let staged = persistence.state;
|
|
3725
|
+
for (const item of resolved) {
|
|
3726
|
+
const { candidate } = item;
|
|
3727
|
+
// ACCEPT AND HOLD. A span that is still fresh or protected is not a refusal:
|
|
3728
|
+
// the mark is a standing decision, it is recorded now, and it folds at the
|
|
3729
|
+
// first commit after the span ages out. Refusal is reserved for a mark that
|
|
3730
|
+
// cannot be constructed at all.
|
|
3731
|
+
// A REPEATED DECISION IS INERT. The fold id is derived from the span, so a
|
|
3732
|
+
// span the ladder already prepared carries that exact id in the forest
|
|
3733
|
+
// already, and preparing it a second time throws "Prepared fold already
|
|
3734
|
+
// exists" out of the tool call. Under the old cadence commits were frequent
|
|
3735
|
+
// enough to drain prepared folds before a second decision could land on one;
|
|
3736
|
+
// the quiet runtime commits far less often, so the collision became reachable
|
|
3737
|
+
// in ordinary use -- the ladder marks a span, the agent then folds the same
|
|
3738
|
+
// span, and the agent's call fails. Marking is a standing decision about a
|
|
3739
|
+
// span, and deciding the same thing twice has to be a no-op.
|
|
3740
|
+
const alreadyPrepared = staged.folds.some((fold) =>
|
|
3741
|
+
fold.id === foldIdFor(candidate.kind, candidate.parts));
|
|
3742
|
+
const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
|
|
3743
|
+
(candidate.kind === "tool-result" &&
|
|
3744
|
+
toolRefsProtected(candidate.sourceRefs, staged, snapshot));
|
|
3745
|
+
const briefed = !deferred && !alreadyPrepared
|
|
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({
|
|
3758
|
+
candidate,
|
|
3759
|
+
brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
|
|
3760
|
+
briefProvenance: briefed?.fold.provenance ??
|
|
3761
|
+
(item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
|
|
3762
|
+
origin: "agent",
|
|
3763
|
+
ordinal: markOrdinal(snapshot),
|
|
3764
|
+
});
|
|
3765
|
+
const addition = addPendingMark(staged, mark);
|
|
3766
|
+
if (!addition.added) {
|
|
3767
|
+
marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
|
|
3768
|
+
continue;
|
|
3769
|
+
}
|
|
3770
|
+
staged = addition.state;
|
|
3771
|
+
marks.push({
|
|
3772
|
+
id: mark.id,
|
|
3773
|
+
kind: mark.kind,
|
|
3774
|
+
ok: true,
|
|
3775
|
+
deferred,
|
|
3776
|
+
...(deferred
|
|
3777
|
+
? {
|
|
3778
|
+
scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
|
|
3779
|
+
"first commit after it ages out",
|
|
3780
|
+
}
|
|
3781
|
+
: {}),
|
|
3782
|
+
brief: mark.brief,
|
|
3783
|
+
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
await persistManual(staged, action, ctx);
|
|
3787
|
+
updateStatus(ctx);
|
|
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) => ({
|
|
3793
|
+
id: mark.id,
|
|
3794
|
+
kind: mark.kind,
|
|
3795
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
3796
|
+
}));
|
|
3797
|
+
const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
|
|
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
|
|
3809
|
+
? {
|
|
3810
|
+
id: only.id,
|
|
3811
|
+
kind: only.kind,
|
|
3812
|
+
brief: only.brief,
|
|
3813
|
+
provenance: only.provenance,
|
|
3814
|
+
deferred: only.deferred,
|
|
3815
|
+
...(only.scheduled ? { scheduled: only.scheduled } : {}),
|
|
3816
|
+
}
|
|
3817
|
+
: {}),
|
|
3818
|
+
marks,
|
|
3819
|
+
// Never silently reinterpreted: a span the runtime moved says so, here.
|
|
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 },
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
await persistManual(persistence.state, "fold", ctx);
|
|
1855
3863
|
updateStatus(ctx);
|
|
3864
|
+
const single = applied.length === 1 ? applied[0] : null;
|
|
1856
3865
|
return toolPayload({
|
|
1857
3866
|
version: 1,
|
|
1858
3867
|
action,
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
3868
|
+
scheduling: "immediate",
|
|
3869
|
+
...(single
|
|
3870
|
+
? {
|
|
3871
|
+
id: single.id,
|
|
3872
|
+
kind: single.kind,
|
|
3873
|
+
brief: single.brief,
|
|
3874
|
+
provenance: single.provenance,
|
|
3875
|
+
expand: single.expand,
|
|
3876
|
+
}
|
|
3877
|
+
: {}),
|
|
3878
|
+
folds: applied,
|
|
3879
|
+
corrections,
|
|
1863
3880
|
argumentsSha256: executionArgumentsSha256,
|
|
1864
3881
|
durableRevision: persistence.state.revision,
|
|
1865
3882
|
activation: "durable immediately; projected on the next model call in this same turn",
|
|
1866
|
-
expand: { action: "expand", id: preparedFold.id },
|
|
1867
3883
|
});
|
|
1868
3884
|
}
|
|
1869
3885
|
throw new Error(`Unknown ${toolName} action '${action}'`);
|
|
1870
3886
|
};
|
|
3887
|
+
/**
|
|
3888
|
+
* Every context-management call, accepted AND refused, in the durable stream.
|
|
3889
|
+
*
|
|
3890
|
+
* A rejected call is the more informative of the two: an agent whose spans keep being
|
|
3891
|
+
* refused looks identical, from fold records alone, to an agent that never tried to
|
|
3892
|
+
* curate at all. The exact validation text is recorded verbatim, because "it failed"
|
|
3893
|
+
* is not a finding and "ids must contain 1-64 values" is.
|
|
3894
|
+
*/
|
|
3895
|
+
const requestedMarkCount = (params: Record<string, unknown>): number => {
|
|
3896
|
+
const batched = denseOwnArrayValues(params?.marks);
|
|
3897
|
+
if (batched) return batched.length;
|
|
3898
|
+
return params?.ids === undefined && params?.id === undefined ? 0 : 1;
|
|
3899
|
+
};
|
|
3900
|
+
|
|
1871
3901
|
const toolHandler = async (
|
|
1872
3902
|
_toolCallId: string,
|
|
1873
3903
|
params: Record<string, unknown>,
|
|
@@ -1875,7 +3905,46 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1875
3905
|
_onUpdate: unknown,
|
|
1876
3906
|
ctx: any,
|
|
1877
3907
|
): Promise<unknown> => {
|
|
1878
|
-
const operation = ladder.actionQueue.then(() =>
|
|
3908
|
+
const operation = ladder.actionQueue.then(async () => {
|
|
3909
|
+
const action = String(params?.action ?? "");
|
|
3910
|
+
// The gate's engagement signal, counted BEFORE the call can fail: an agent that
|
|
3911
|
+
// reached for a context verb engaged, whether or not the span was valid.
|
|
3912
|
+
curation.contextCalls += 1;
|
|
3913
|
+
const attempt = (ok: boolean, error: string | null, corrections: Array<Record<string, unknown>>) => {
|
|
3914
|
+
const record = emit("context.attempt", {
|
|
3915
|
+
action,
|
|
3916
|
+
ok,
|
|
3917
|
+
error,
|
|
3918
|
+
marks_requested: requestedMarkCount(params),
|
|
3919
|
+
corrections_applied: corrections.length,
|
|
3920
|
+
arguments_sha256: sha256Value(params ?? {}),
|
|
3921
|
+
});
|
|
3922
|
+
// One record per correction, referencing its attempt: nothing nests, and a
|
|
3923
|
+
// reader counting corrections never has to unpack an array to do it.
|
|
3924
|
+
for (const correction of corrections) {
|
|
3925
|
+
const from = denseOwnArrayValues(ownValue(correction, "from")) ?? [];
|
|
3926
|
+
const to = denseOwnArrayValues(ownValue(correction, "to")) ?? [];
|
|
3927
|
+
emit("context.correction", {
|
|
3928
|
+
attempt_seq: record.seq,
|
|
3929
|
+
action,
|
|
3930
|
+
requested_start_id: from[0] ?? null,
|
|
3931
|
+
requested_end_id: from[1] ?? null,
|
|
3932
|
+
corrected_start_id: to[0] ?? null,
|
|
3933
|
+
corrected_end_id: to[1] ?? null,
|
|
3934
|
+
reason: ownValue(correction, "reason") ?? null,
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
try {
|
|
3939
|
+
const result = await executeAction(params, signal, ctx);
|
|
3940
|
+
const corrections = denseOwnArrayValues(ownValue(ownValue(result, "details"), "corrections"));
|
|
3941
|
+
attempt(true, null, (corrections ?? []) as Array<Record<string, unknown>>);
|
|
3942
|
+
return result;
|
|
3943
|
+
} catch (error) {
|
|
3944
|
+
attempt(false, error instanceof Error ? error.message : String(error), []);
|
|
3945
|
+
throw error;
|
|
3946
|
+
}
|
|
3947
|
+
});
|
|
1879
3948
|
ladder.actionQueue = operation.catch(() => undefined);
|
|
1880
3949
|
return operation;
|
|
1881
3950
|
};
|
|
@@ -1922,8 +3991,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1922
3991
|
if (!sessionIdentityStillValid(ctx, stateBefore.sessionId, generationBefore)) {
|
|
1923
3992
|
throw new Error("Active-context session changed before rescue persistence");
|
|
1924
3993
|
}
|
|
1925
|
-
persistence.state =
|
|
1926
|
-
advisory.armedMilestone = null;
|
|
3994
|
+
persistence.state = nextState;
|
|
1927
3995
|
try { await persist(ctx); }
|
|
1928
3996
|
catch (error) {
|
|
1929
3997
|
if (sessionIdentityStillValid(ctx, stateBefore.sessionId, generationBefore)) {
|
|
@@ -1934,7 +4002,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1934
4002
|
}
|
|
1935
4003
|
ladder.pendingManual = false;
|
|
1936
4004
|
ladder.automaticFailure = null;
|
|
1937
|
-
advisory.armedMilestone = null;
|
|
1938
4005
|
ladder.pendingContextNote =
|
|
1939
4006
|
`User rescue folded stale context under ${preparedFold.id}; exact source remains expandable.`;
|
|
1940
4007
|
ladder.lastAutomaticAction = {
|
|
@@ -1960,8 +4027,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1960
4027
|
name: toolName,
|
|
1961
4028
|
label: toolLabel,
|
|
1962
4029
|
allowedActions: allowedToolActions,
|
|
1963
|
-
fullSurface: allowedToolActions.length ===
|
|
4030
|
+
fullSurface: allowedToolActions.length === defaultToolActions.length,
|
|
1964
4031
|
maxBriefChars: ACTIVE_CONTEXT_POLICY.maxBriefChars,
|
|
4032
|
+
statusDetails: ["fold_candidates", "tree", "folds", "objects"],
|
|
4033
|
+
minPeekSliceBytes: PEEK_MIN_SLICE_BYTES,
|
|
4034
|
+
defaultPeekBytes: PEEK_DEFAULT_MAX_BYTES,
|
|
1965
4035
|
handler: toolHandler,
|
|
1966
4036
|
}));
|
|
1967
4037
|
for (const command of buildActiveContextCommands({
|
|
@@ -1982,9 +4052,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1982
4052
|
persistence.state = null;
|
|
1983
4053
|
persistence.persisted = null;
|
|
1984
4054
|
lifecycle.latestSnapshot = null;
|
|
1985
|
-
|
|
4055
|
+
curation.receipts = [];
|
|
1986
4056
|
try { ctx.ui?.setStatus?.(entryTypePrefix, undefined); } catch { /* Shutdown cannot be blocked by UI. */ }
|
|
1987
4057
|
});
|
|
1988
4058
|
|
|
1989
|
-
return { projectionCandidates };
|
|
4059
|
+
return { projectionCandidates, registerSuggestionSource };
|
|
1990
4060
|
}
|