pi-fold 1.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -28
- package/extensions/active-context.ts +1213 -2756
- package/extensions/evidence.js +0 -10
- package/extensions/index.js +25 -32
- package/extensions/json.ts +3 -29
- package/extensions/lib/canonical.ts +0 -8
- package/extensions/lib/curation.ts +52 -313
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +57 -446
- package/extensions/lib/instrumentation.ts +13 -127
- package/extensions/lib/measurement.ts +6 -83
- package/extensions/lib/persistence.ts +272 -229
- package/extensions/lib/policy.ts +165 -612
- package/extensions/lib/rollback.ts +0 -53
- package/extensions/lib/scheduling.ts +352 -450
- package/extensions/lib/selection.ts +212 -390
- package/extensions/lib/tool-surface.ts +29 -46
- package/extensions/lib/transcript.ts +47 -166
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
- package/extensions/lib/surfacing.ts +0 -604
- package/extensions/summarizer.js +0 -291
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
prepareFold,
|
|
13
13
|
renderFold,
|
|
14
14
|
renderFoldParts,
|
|
15
|
+
selectAutomaticChapter,
|
|
15
16
|
selectAutomaticSpan,
|
|
16
17
|
setFoldProjectionState,
|
|
17
18
|
} from "./folding.ts";
|
|
@@ -19,11 +20,10 @@ import type { AutomaticRungSelection } from "./folding.ts";
|
|
|
19
20
|
import {
|
|
20
21
|
foldInterval,
|
|
21
22
|
budgetOccupancy,
|
|
22
|
-
hardFenceRatio,
|
|
23
23
|
mappedByKey,
|
|
24
24
|
orderedRoots,
|
|
25
|
+
protectedStaleMass,
|
|
25
26
|
refsProtected,
|
|
26
|
-
toolRefsProtected,
|
|
27
27
|
} from "./measurement.ts";
|
|
28
28
|
import {
|
|
29
29
|
childFoldIds,
|
|
@@ -33,12 +33,10 @@ import {
|
|
|
33
33
|
parsePendingMarks,
|
|
34
34
|
pendingMarkKey,
|
|
35
35
|
} from "./persistence.ts";
|
|
36
|
-
import { currentTurnRefKeys } from "./transcript.ts";
|
|
37
36
|
import {
|
|
38
|
-
EPOCH_MAX_TOPUP_MARKS,
|
|
39
37
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
40
|
-
MAX_WEDGE_ABSORB_TOKENS,
|
|
41
38
|
ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES,
|
|
39
|
+
MAX_FRONTIER_CUTS_PER_PASS,
|
|
42
40
|
MAX_PENDING_MARKS,
|
|
43
41
|
MAX_UNMARKED_CANDIDATES,
|
|
44
42
|
} from "./policy.ts";
|
|
@@ -59,66 +57,30 @@ import {
|
|
|
59
57
|
partsForRange,
|
|
60
58
|
peekedSourceFoldIds,
|
|
61
59
|
selectAutomaticConsolidations,
|
|
60
|
+
selectAutomaticRefold,
|
|
62
61
|
spanBytes,
|
|
63
62
|
deterministicChapterCandidateBrief,
|
|
64
63
|
deterministicConsolidationBrief,
|
|
65
64
|
resultCall,
|
|
66
65
|
} from "./selection.ts";
|
|
67
66
|
|
|
68
|
-
/**
|
|
69
|
-
* Two-phase fold scheduling.
|
|
70
|
-
*
|
|
71
|
-
* A provider prefix cache is positional: editing the middle of the window
|
|
72
|
-
* invalidates every byte after the edit, so per-turn folding pays a full uncached
|
|
73
|
-
* prefix over and over. Nothing here changes what a fold MEANS. A fold decision
|
|
74
|
-
* becomes a MARK, which moves no projection byte, and a later COMMIT applies every
|
|
75
|
-
* pending mark through the same preparation and commit machinery an immediate fold
|
|
76
|
-
* uses, in one rewrite.
|
|
77
|
-
*
|
|
78
|
-
* Every function is deterministic: transcript ordinals and positions only, never a
|
|
79
|
-
* wall clock and never randomness.
|
|
80
|
-
*/
|
|
81
|
-
|
|
82
|
-
/** The empty read, shared and frozen, so a state with no marks allocates nothing. */
|
|
83
67
|
const NO_PENDING_MARKS: readonly PendingMark[] = Object.freeze([]);
|
|
84
68
|
|
|
85
|
-
/**
|
|
86
|
-
* The pending marks as a READ VIEW, not a copy.
|
|
87
|
-
*
|
|
88
|
-
* Every caller here reads: membership tests, accounting sums, and filters that build
|
|
89
|
-
* a fresh array anyway. The copy belongs on the WRITE path, and `withPendingMarks`
|
|
90
|
-
* already takes it, so taking a second one on every read bought nothing and cost a
|
|
91
|
-
* great deal: `markSpanRefs` calls this once per mark and is itself called once per
|
|
92
|
-
* mark by the accounting, eligibility, staleness, current-turn and absorption
|
|
93
|
-
* readings, which made a deep clone of the whole array quadratic in the mark count
|
|
94
|
-
* per pass. Profiled at 21 percent of a 120-turn session, the single largest cost in
|
|
95
|
-
* the runtime, ahead of every projection and hash.
|
|
96
|
-
*
|
|
97
|
-
* The view is safe because the state is replaced rather than edited: no code path
|
|
98
|
-
* mutates `state.pendingMarks` in place, and the readonly type is what keeps it so.
|
|
99
|
-
*/
|
|
100
69
|
export function pendingMarks(state: Pick<ActiveContextState, "pendingMarks">): readonly PendingMark[] {
|
|
101
70
|
return state.pendingMarks ?? NO_PENDING_MARKS;
|
|
102
71
|
}
|
|
103
72
|
|
|
104
|
-
/**
|
|
105
|
-
* Property ORDER matters here: the state digest is a stable stringify, so an
|
|
106
|
-
* appended key would replay as digest drift. Rebuild the tail of the record in the
|
|
107
|
-
* same order `parseActiveContextState` produces.
|
|
108
|
-
*/
|
|
109
73
|
export function withPendingMarks(
|
|
110
74
|
state: ActiveContextState,
|
|
111
75
|
marks: readonly PendingMark[],
|
|
112
76
|
): ActiveContextState {
|
|
113
77
|
const next = marks.length ? parsePendingMarks(clone(marks) as PendingMark[]) : [];
|
|
114
78
|
const head = { ...state };
|
|
115
|
-
delete head.surfacing;
|
|
116
79
|
delete head.pendingMarks;
|
|
117
80
|
delete head.advisory;
|
|
118
81
|
delete head.prepared;
|
|
119
82
|
return {
|
|
120
83
|
...head,
|
|
121
|
-
...(state.surfacing?.length ? { surfacing: clone(state.surfacing) } : {}),
|
|
122
84
|
...(next.length ? { pendingMarks: next } : {}),
|
|
123
85
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
124
86
|
...(state.prepared ? { prepared: clone(state.prepared) } : {}),
|
|
@@ -131,7 +93,6 @@ export interface MarkAddition {
|
|
|
131
93
|
reason: string | null;
|
|
132
94
|
}
|
|
133
95
|
|
|
134
|
-
/** Idempotent by mark key, bounded, and never silently dropping the oldest mark. */
|
|
135
96
|
export function addPendingMark(state: ActiveContextState, mark: PendingMark): MarkAddition {
|
|
136
97
|
const marks = pendingMarks(state);
|
|
137
98
|
if (marks.some((existing) => pendingMarkKey(existing) === pendingMarkKey(mark))) {
|
|
@@ -151,43 +112,15 @@ export function estimatedTokens(sizeInBytes: number): number {
|
|
|
151
112
|
return Math.max(0, Math.ceil(sizeInBytes / ESTIMATED_BYTES_PER_TOKEN));
|
|
152
113
|
}
|
|
153
114
|
|
|
154
|
-
/** Transcript-position ordering used for marks; identical to the surfacing ordinal. */
|
|
155
115
|
export function markOrdinal(snapshot: Pick<ActiveContextSnapshot, "mapped">): number {
|
|
156
116
|
return snapshot.mapped.length;
|
|
157
117
|
}
|
|
158
118
|
|
|
159
119
|
export interface MarkSpanRefs {
|
|
160
|
-
/** Every evidence ref the mark's parts resolve to against the state in hand. */
|
|
161
120
|
refs: EvidenceRef[];
|
|
162
|
-
/**
|
|
163
|
-
* The first part naming a fold the state does not hold, or null when the span reads
|
|
164
|
-
* in full. `pending` is the difference between a defer and a drop: a mark still
|
|
165
|
-
* standing carries the id its committed fold will have, so that fold is about to
|
|
166
|
-
* exist, and where no mark names it nothing ever will.
|
|
167
|
-
*/
|
|
168
121
|
unresolved: { foldId: string; pending: boolean } | null;
|
|
169
122
|
}
|
|
170
123
|
|
|
171
|
-
/**
|
|
172
|
-
* THE ONE READING OF A MARK'S SPAN, AND IT ANSWERS RATHER THAN THROWS.
|
|
173
|
-
*
|
|
174
|
-
* A mark's parts name folds by id. Between the mark and the commit the state can stop
|
|
175
|
-
* holding one: `reboundary` dissolves a root while consulting only `fold.parentId`, so
|
|
176
|
-
* nothing there consults the pending marks, and the mark is left naming a fold that is
|
|
177
|
-
* gone. `candidateSourceRefs` answers that with `Missing candidate child`, which is
|
|
178
|
-
* right for a candidate being prepared and wrong for every reading that merely reports
|
|
179
|
-
* on a mark: accounting, eligibility, staleness, the current-turn guard, the claimed
|
|
180
|
-
* keys and wedge absorption all ran that throw out of a status call or a commit pass,
|
|
181
|
-
* on state the runtime itself produced (Shane 2026-08-10).
|
|
182
|
-
*
|
|
183
|
-
* So the readings share this, and it is total. Raw parts resolve; a held child is
|
|
184
|
-
* flattened; an unheld child is REPORTED, with the refs either side of it still
|
|
185
|
-
* collected, because a partial span is the honest reading of a partly resolvable mark
|
|
186
|
-
* and a guard that can see half the evidence is better than one that sees none. A
|
|
187
|
-
* repeated fold on the path reads as unresolved and not pending: a corrupt forest costs
|
|
188
|
-
* one mark the ordinary drop rather than hanging the caller, which is the same stop the
|
|
189
|
-
* apply order's `placing` set carries.
|
|
190
|
-
*/
|
|
191
124
|
export function markSpanRefs(state: ActiveContextState, mark: PendingMark): MarkSpanRefs {
|
|
192
125
|
const byId = new Map(state.folds.map((fold) => [fold.id, fold] as const));
|
|
193
126
|
const marks = pendingMarks(state);
|
|
@@ -230,11 +163,6 @@ export function markSpanRefs(state: ActiveContextState, mark: PendingMark): Mark
|
|
|
230
163
|
return { refs, unresolved };
|
|
231
164
|
}
|
|
232
165
|
|
|
233
|
-
/**
|
|
234
|
-
* Estimated bytes a mark would remove from the projection. Fold marks use a
|
|
235
|
-
* placeholder estimate rather than a trial fold: the number is presentational, so
|
|
236
|
-
* paying a full preparation per mark on every status call is not worth exactness.
|
|
237
|
-
*/
|
|
238
166
|
export function markFreedBytes(
|
|
239
167
|
snapshot: ActiveContextSnapshot,
|
|
240
168
|
state: ActiveContextState,
|
|
@@ -257,41 +185,6 @@ export function markFreedBytes(
|
|
|
257
185
|
return Math.max(0, bytes(source) - placeholder);
|
|
258
186
|
}
|
|
259
187
|
|
|
260
|
-
/**
|
|
261
|
-
* Whether a pending mark covers any evidence the current excursion just gathered.
|
|
262
|
-
*
|
|
263
|
-
* Read off whatever the span resolves to. A mark naming an unheld fold still answers
|
|
264
|
-
* here, on the parts that DO resolve, so a span holding both fresh raw evidence and a
|
|
265
|
-
* child that has not landed is guarded on the fresh evidence instead of throwing the
|
|
266
|
-
* whole commit.
|
|
267
|
-
*/
|
|
268
|
-
export function markTouchesCurrentTurn(
|
|
269
|
-
state: ActiveContextState,
|
|
270
|
-
mark: PendingMark,
|
|
271
|
-
currentTurn: ReadonlySet<string>,
|
|
272
|
-
): boolean {
|
|
273
|
-
if (!currentTurn.size) return false;
|
|
274
|
-
return markSpanRefs(state, mark).refs.some((ref) => currentTurn.has(objectRefKey(ref)));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* The real staleness of a mark: the earliest window index its span covers, in branch
|
|
279
|
-
* order.
|
|
280
|
-
*
|
|
281
|
-
* A mark's `ordinal` is the transcript position at MARK time, so across epochs it
|
|
282
|
-
* carries when the DECISION was made and nothing about the age of what the decision
|
|
283
|
-
* covers, and inside one epoch every mark shares one value. Ordering a release by it
|
|
284
|
-
* therefore fell through to comparing mark ids, which are content hashes. Measured on the
|
|
285
|
-
* sealed rep-3 state 2026-08-10: the bounded release held back the marks covering window
|
|
286
|
-
* indices 101 and 111 and surrendered the one covering 117, the newest material on the
|
|
287
|
-
* table, so the two marks the guard kept were not the two it exists to keep.
|
|
288
|
-
*
|
|
289
|
-
* A span that no longer maps sorts past the newest entry: there is no staleness to read
|
|
290
|
-
* on material that has left the branch, and releasing such a mark frees no window byte.
|
|
291
|
-
* A span that does not resolve in full sorts there too, for the same reason and by the
|
|
292
|
-
* same number: while a named fold is missing there is no start to read, and the apply
|
|
293
|
-
* order places such a mark off its naming rather than off this reading.
|
|
294
|
-
*/
|
|
295
188
|
export function markSpanStart(
|
|
296
189
|
snapshot: ActiveContextSnapshot,
|
|
297
190
|
state: ActiveContextState,
|
|
@@ -308,71 +201,8 @@ export function markSpanStart(
|
|
|
308
201
|
return indices.length ? Math.min(...indices) : snapshot.mapped.length;
|
|
309
202
|
}
|
|
310
203
|
|
|
311
|
-
/**
|
|
312
|
-
* How many of the newest guarded marks the pressure waiver still protects. The agent's
|
|
313
|
-
* MOST RECENT reads are the ones an in-flight excursion is about to work from, so they
|
|
314
|
-
* are the last thing surrendered; everything older is fair game once the window is the
|
|
315
|
-
* bigger risk.
|
|
316
|
-
*/
|
|
317
|
-
export const GUARD_WAIVER_PROTECTED_MARKS = 2;
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* How many marks the guard must be holding before the pressure waiver releases any.
|
|
321
|
-
* Without a floor the waiver degrades into exactly the per-pass folding epoch
|
|
322
|
-
* scheduling exists to prevent: the batch drains, one new mark is guarded on the next
|
|
323
|
-
* pass, and releasing that one mark buys a whole prefix invalidation. Below the fence
|
|
324
|
-
* the waiver only ever releases a BATCH. The fence itself ignores this floor.
|
|
325
|
-
*/
|
|
326
|
-
export const GUARD_WAIVER_MINIMUM_MARKS = 4;
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Survivability outranks the guard.
|
|
330
|
-
*
|
|
331
|
-
* The current-turn guard is correct while there is room to wait, and fatal once there
|
|
332
|
-
* is not: a workload that never emits a terminal assistant message has ONE turn, so
|
|
333
|
-
* the boundary never advances and every mark is guarded forever. Measured 2026-08-06
|
|
334
|
-
* (rep 11): 58 assistant messages, all stopReason "toolUse", ZERO terminal ones, so
|
|
335
|
-
* the automatic commit applied nothing all run; the only marks that ever landed came
|
|
336
|
-
* from the agent's own two explicit commits. The window reached 359,625 tokens and the
|
|
337
|
-
* next request went out at ~458k estimated and was rejected by the provider.
|
|
338
|
-
*
|
|
339
|
-
* So: the guard holds in full while the commit has other work. It is waived only when
|
|
340
|
-
* it would STARVE the commit -- no other mark could be applied -- and pressure has
|
|
341
|
-
* reached the backstop, and then only for the OLDEST guarded marks. At the hard fence
|
|
342
|
-
* every guarded mark is waived, because the only useful action left is the fold that
|
|
343
|
-
* keeps the request transmissible.
|
|
344
|
-
*/
|
|
345
|
-
export function guardWaiverCount(input: {
|
|
346
|
-
snapshot: ActiveContextSnapshot;
|
|
347
|
-
ratio: number | null;
|
|
348
|
-
guardedMarks: number;
|
|
349
|
-
/** Marks this commit can apply WITHOUT the waiver. While any exist, the guard holds. */
|
|
350
|
-
otherApplicableMarks: number;
|
|
351
|
-
}): number {
|
|
352
|
-
const { snapshot, ratio, guardedMarks, otherApplicableMarks } = input;
|
|
353
|
-
if (guardedMarks <= 0 || typeof ratio !== "number" || !Number.isFinite(ratio)) return 0;
|
|
354
|
-
if (ratio >= hardFenceRatio(snapshot)) return guardedMarks;
|
|
355
|
-
const occupancy = budgetOccupancy(snapshot, ratio);
|
|
356
|
-
if (occupancy === null || occupancy < snapshot.policy.refoldRatio || otherApplicableMarks > 0) return 0;
|
|
357
|
-
if (guardedMarks < GUARD_WAIVER_MINIMUM_MARKS) return 0;
|
|
358
|
-
return Math.max(1, guardedMarks - GUARD_WAIVER_PROTECTED_MARKS);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
204
|
export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
|
|
362
205
|
|
|
363
|
-
/**
|
|
364
|
-
* Whether a pending mark can be applied right now.
|
|
365
|
-
*
|
|
366
|
-
* "protected" is a WAITING state, not a failure: the span is fresh or the agent
|
|
367
|
-
* protected it, and both conditions expire. "unfulfillable" is terminal: the evidence
|
|
368
|
-
* or the fold the mark names has left the branch, so no later commit can honour it.
|
|
369
|
-
* Collapsing the two is what made a refusal look like a drop.
|
|
370
|
-
*
|
|
371
|
-
* A mark naming a fold the state does not hold is judged by the same two words, and
|
|
372
|
-
* the classification is the whole answer: a fold another pending mark is about to mint
|
|
373
|
-
* is a span still WAITING to be readable, and a fold nothing will mint has left the
|
|
374
|
-
* branch exactly as lost evidence has. No third word (Shane 2026-08-10).
|
|
375
|
-
*/
|
|
376
206
|
export function markEligibility(
|
|
377
207
|
snapshot: ActiveContextSnapshot,
|
|
378
208
|
state: ActiveContextState,
|
|
@@ -389,9 +219,7 @@ export function markEligibility(
|
|
|
389
219
|
if (!refs.length) return "unfulfillable";
|
|
390
220
|
const mapped = new Set(snapshot.mapped.flatMap((item) => item.ref ? [objectRefKey(item.ref)] : []));
|
|
391
221
|
if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
|
|
392
|
-
const blocked =
|
|
393
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
394
|
-
: refsProtected(refs, state, snapshot);
|
|
222
|
+
const blocked = refsProtected(refs, state, snapshot);
|
|
395
223
|
return blocked ? "protected" : "eligible";
|
|
396
224
|
}
|
|
397
225
|
|
|
@@ -399,29 +227,20 @@ export interface MarkAccounting {
|
|
|
399
227
|
pending: number;
|
|
400
228
|
agentMarks: number;
|
|
401
229
|
ladderMarks: number;
|
|
230
|
+
userMarks: number;
|
|
402
231
|
freedBytes: number;
|
|
403
232
|
freedTokens: number;
|
|
404
233
|
freedBudgetShare: number;
|
|
405
234
|
rewriteTokens: number;
|
|
406
|
-
/** Marks a commit could apply right now. */
|
|
407
235
|
eligibleMarks: number;
|
|
408
|
-
/** Marks whose span is still fresh or protected, waiting rather than lost. */
|
|
409
236
|
retainedMarks: number;
|
|
410
237
|
eligibleFreedBytes: number;
|
|
411
238
|
eligibleFreedTokens: number;
|
|
412
|
-
/** The ROI signal: eligible marked mass as a share of the truthful window. */
|
|
413
239
|
eligibleFreedBudgetShare: number;
|
|
414
|
-
/** Raw peek mass no reclamation can take: pinned reads and protected results. */
|
|
415
240
|
pinnedBytes: number;
|
|
416
|
-
/** How many peek results that mass is spread over. */
|
|
417
241
|
pinnedResults: number;
|
|
418
242
|
}
|
|
419
243
|
|
|
420
|
-
/**
|
|
421
|
-
* What a commit of the current marks would cost and free. `rewriteTokens` is the
|
|
422
|
-
* prefix a commit invalidates: everything from the earliest marked message to the
|
|
423
|
-
* end of the window, which is exactly what a positional cache must re-send.
|
|
424
|
-
*/
|
|
425
244
|
export function markAccounting(
|
|
426
245
|
snapshot: ActiveContextSnapshot,
|
|
427
246
|
state: ActiveContextState,
|
|
@@ -458,6 +277,7 @@ export function markAccounting(
|
|
|
458
277
|
pending: marks.length,
|
|
459
278
|
agentMarks: marks.filter((mark) => mark.origin === "agent").length,
|
|
460
279
|
ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
|
|
280
|
+
userMarks: marks.filter((mark) => mark.origin === "user").length,
|
|
461
281
|
freedBytes,
|
|
462
282
|
freedTokens,
|
|
463
283
|
freedBudgetShare: snapshot.budgetTokens > 0 ? freedTokens / snapshot.budgetTokens : 0,
|
|
@@ -474,22 +294,6 @@ export function markAccounting(
|
|
|
474
294
|
};
|
|
475
295
|
}
|
|
476
296
|
|
|
477
|
-
/**
|
|
478
|
-
* THE commit trigger. There is exactly one.
|
|
479
|
-
*
|
|
480
|
-
* Occupancy of the serving budget reaches maxTarget and a commit fires. Nothing else
|
|
481
|
-
* fires one automatically: the user command and the fence/recovery lane are the only
|
|
482
|
-
* other paths into a commit, and both are authority rather than economics.
|
|
483
|
-
*
|
|
484
|
-
* Two arms used to sit here. The eligible-share arm asked whether accumulated marks
|
|
485
|
-
* were worth a rewrite and fired 164 commits from ordinal 17 in rep 15, each a fresh
|
|
486
|
-
* cache rebuild bought with marks that had not yet earned one; worse, it could fire
|
|
487
|
-
* BELOW maxTarget, which is the quiet-runtime law it was sitting underneath. The
|
|
488
|
-
* pressure arm read a window ratio against the refold rung, which after
|
|
489
|
-
* re-denomination is simply a second line above this one. Both are gone. Marking is
|
|
490
|
-
* doorless below the commit line and costs nothing, so what a pass accumulates is never a
|
|
491
|
-
* reason to commit; crossing the band top is.
|
|
492
|
-
*/
|
|
493
297
|
export function epochCommitDue(snapshot: ActiveContextSnapshot, ratio: number | null): boolean {
|
|
494
298
|
const occupancy = budgetOccupancy(snapshot, ratio);
|
|
495
299
|
return occupancy !== null && occupancy >= snapshot.thresholds.maxTarget;
|
|
@@ -505,8 +309,6 @@ export function foldMarkFor(input: {
|
|
|
505
309
|
const { candidate } = input;
|
|
506
310
|
return {
|
|
507
311
|
mark: "fold",
|
|
508
|
-
// The same identity the committed fold will get, so a repeated decision is
|
|
509
|
-
// inert rather than a second fold of the same span.
|
|
510
312
|
id: foldIdFor(candidate.kind, candidate.parts),
|
|
511
313
|
kind: candidate.kind,
|
|
512
314
|
parts: clone(candidate.parts),
|
|
@@ -517,25 +319,18 @@ export function foldMarkFor(input: {
|
|
|
517
319
|
};
|
|
518
320
|
}
|
|
519
321
|
|
|
520
|
-
/** The deterministic brief a ladder decision would have folded with. */
|
|
521
322
|
export function ladderBrief(
|
|
522
323
|
snapshot: ActiveContextSnapshot,
|
|
523
324
|
state: ActiveContextState,
|
|
524
325
|
candidate: FoldCandidate,
|
|
525
326
|
): string {
|
|
526
327
|
if (candidate.kind === "tool-result") return automaticToolBrief(snapshot, candidate);
|
|
527
|
-
if (candidate.kind === "consolidation")
|
|
328
|
+
if (candidate.kind === "consolidation") {
|
|
329
|
+
return deterministicConsolidationBrief(candidate, state, snapshot.toolName, snapshot);
|
|
330
|
+
}
|
|
528
331
|
return deterministicChapterCandidateBrief(snapshot, candidate);
|
|
529
332
|
}
|
|
530
333
|
|
|
531
|
-
/**
|
|
532
|
-
* The brief and provenance an automatic mark carries.
|
|
533
|
-
*
|
|
534
|
-
* A warmed chapter brief the runtime already paid a model call for is REUSED when the
|
|
535
|
-
* prepared fold is exactly this candidate. Under epoch scheduling every automatic fold
|
|
536
|
-
* arrives through a mark, so a preparation whose brief the mark path ignored would be a
|
|
537
|
-
* model call spent on nothing.
|
|
538
|
-
*/
|
|
539
334
|
export function automaticMarkBrief(
|
|
540
335
|
snapshot: ActiveContextSnapshot,
|
|
541
336
|
state: ActiveContextState,
|
|
@@ -548,11 +343,6 @@ export function automaticMarkBrief(
|
|
|
548
343
|
return { brief: ladderBrief(snapshot, state, candidate), briefProvenance: { kind: "deterministic" } };
|
|
549
344
|
}
|
|
550
345
|
|
|
551
|
-
/**
|
|
552
|
-
* Turn one automatic rung selection into a mark. Prepared chapters are excluded on
|
|
553
|
-
* purpose: that rung only fires at the hard provider fence, which is already inside
|
|
554
|
-
* a commit epoch, and deferring it would strand a model-generated brief.
|
|
555
|
-
*/
|
|
556
346
|
export function ladderSelectionMark(input: {
|
|
557
347
|
snapshot: ActiveContextSnapshot;
|
|
558
348
|
state: ActiveContextState;
|
|
@@ -574,24 +364,6 @@ export function ladderSelectionMark(input: {
|
|
|
574
364
|
});
|
|
575
365
|
}
|
|
576
366
|
|
|
577
|
-
/**
|
|
578
|
-
* PEEK COPIES ARE EPHEMERAL BY CONTRACT.
|
|
579
|
-
*
|
|
580
|
-
* A peek returns a fold's exact stored source, so the copy sits in the window as raw
|
|
581
|
-
* mass beside that fold's own placeholder: the same bytes held twice, with the second
|
|
582
|
-
* copy re-retrievable forever from the id the agent already used. So a completed peek
|
|
583
|
-
* read is reclaimed at the NEXT commit rather than aging through the ladder, and the
|
|
584
|
-
* window being append-only is why the reclaim can only happen at a commit boundary.
|
|
585
|
-
*
|
|
586
|
-
* Two survival paths, and only two. Expanding the fold it peeked means the agent asked
|
|
587
|
-
* for those bytes in place, and pinning is the veto: a pin holds the copy raw through
|
|
588
|
-
* every commit, exactly as it holds any other evidence, and lifting it hands the copy
|
|
589
|
-
* back to the next commit. Both are read here at marking time, and a pin made AFTER the
|
|
590
|
-
* mark exists still vetoes, because a protected mark waits instead of applying.
|
|
591
|
-
*
|
|
592
|
-
* These marks are minted at the last-call exposure as well as at the commit, so the
|
|
593
|
-
* pending disposal is visible during the gated round the pin has to fit inside.
|
|
594
|
-
*/
|
|
595
367
|
export function ephemeralPeekMarks(input: {
|
|
596
368
|
snapshot: ActiveContextSnapshot;
|
|
597
369
|
state: ActiveContextState;
|
|
@@ -619,11 +391,9 @@ export function ephemeralPeekMarks(input: {
|
|
|
619
391
|
.map(({ index }) => snapshot.mapped[index].ref!);
|
|
620
392
|
const peeked = peekedSourceFoldIds(snapshot, refs);
|
|
621
393
|
if (!peeked) continue;
|
|
622
|
-
// The agent committed to what it peeked: leave the read in the raw window.
|
|
623
394
|
if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
|
|
624
395
|
if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
|
|
625
|
-
refs
|
|
626
|
-
toolRefsProtected(refs, state, snapshot)) continue;
|
|
396
|
+
refsProtected(refs, state, snapshot)) continue;
|
|
627
397
|
const candidate: FoldCandidate = {
|
|
628
398
|
kind: "tool-result",
|
|
629
399
|
parts: refs.map((ref) => ({ kind: "raw" as const, ref })),
|
|
@@ -633,8 +403,6 @@ export function ephemeralPeekMarks(input: {
|
|
|
633
403
|
candidate,
|
|
634
404
|
brief: automaticToolBrief(snapshot, candidate),
|
|
635
405
|
briefProvenance: { kind: "deterministic" },
|
|
636
|
-
// A peek is an agent-initiated read; attributing its disposal to the ladder
|
|
637
|
-
// would deflate the agent-vs-ladder mark ratio the adjudication reads.
|
|
638
406
|
origin: "agent",
|
|
639
407
|
ordinal: input.ordinal,
|
|
640
408
|
}));
|
|
@@ -650,12 +418,9 @@ function peekedFoldProtected(
|
|
|
650
418
|
const fold = state.folds.find((item) => item.id === foldId);
|
|
651
419
|
if (!fold) return false;
|
|
652
420
|
const refs = flattenFoldRefs(fold, state);
|
|
653
|
-
return
|
|
654
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
655
|
-
: refsProtected(refs, state, snapshot);
|
|
421
|
+
return refsProtected(refs, state, snapshot);
|
|
656
422
|
}
|
|
657
423
|
|
|
658
|
-
/** Every evidence key already spoken for by a fold or a pending fold mark. */
|
|
659
424
|
export function claimedRefKeys(state: ActiveContextState): Set<string> {
|
|
660
425
|
const keys = new Set<string>();
|
|
661
426
|
for (const fold of state.folds) {
|
|
@@ -668,35 +433,15 @@ export function claimedRefKeys(state: ActiveContextState): Set<string> {
|
|
|
668
433
|
return keys;
|
|
669
434
|
}
|
|
670
435
|
|
|
671
|
-
/** What is still on the table after the marks in hand: the steering number and its parts. */
|
|
672
436
|
export interface UnmarkedRemainder {
|
|
673
|
-
/** Tool results in a MEMBER batch that no fold and no pending mark owns. */
|
|
674
437
|
spans: number;
|
|
675
438
|
tokens: number;
|
|
676
|
-
/**
|
|
439
|
+
/** Raw bytes, which is what the fold frontier measures itself against. */
|
|
440
|
+
chars: number;
|
|
677
441
|
share: number;
|
|
678
|
-
/** The largest few, by reclaim value, so the next batch is one read away. */
|
|
679
442
|
candidates: Array<{ id: string; tokens: number }>;
|
|
680
443
|
}
|
|
681
444
|
|
|
682
|
-
/**
|
|
683
|
-
* The unmarked remainder, as an AGGREGATE plus a bounded head.
|
|
684
|
-
*
|
|
685
|
-
* An exhaustive list is what the status index already is, and re-rendering it is what
|
|
686
|
-
* this build removed from the projection. What an agent needs to decide the NEXT batch
|
|
687
|
-
* is one percentage and the few largest names; everything else is a total.
|
|
688
|
-
*
|
|
689
|
-
* ONE DEFINITION OF STALE, AND IT IS THE SELECTOR'S OWN. The scope is class membership:
|
|
690
|
-
* every result inside a batch `automaticToolBatches` admits, which is the same
|
|
691
|
-
* enumeration `selectAutomaticToolBatch` picks its proposal out of. So the last call and
|
|
692
|
-
* the deferred-commit record announce mass a commit can actually take, by construction
|
|
693
|
-
* rather than by two filters kept in step. Counting whole-window tool results instead
|
|
694
|
-
* announced material no rung could ever propose: rep 3 named 280k tokens while every
|
|
695
|
-
* selector had nothing, which reads as a runtime refusing work it had just described.
|
|
696
|
-
*
|
|
697
|
-
* Membership is not ownership: the denominator is every member result, taken or not, so
|
|
698
|
-
* the share reads as the fraction of reachable tool mass still unspoken for.
|
|
699
|
-
*/
|
|
700
445
|
export function unmarkedRemainder(
|
|
701
446
|
snapshot: ActiveContextSnapshot,
|
|
702
447
|
state: ActiveContextState,
|
|
@@ -709,6 +454,33 @@ export function unmarkedRemainder(
|
|
|
709
454
|
for (const batch of automaticToolBatches(snapshot, state)) {
|
|
710
455
|
for (const index of batch.indices) members.add(index);
|
|
711
456
|
}
|
|
457
|
+
// CHAPTERS ARE FOLDABLE MASS TOO (Shane, 2026-08-23).
|
|
458
|
+
//
|
|
459
|
+
// The population used to be tool batches alone, while `selectAutomaticSpan` proposes a
|
|
460
|
+
// chapter whenever no batch is available, so on a chapter-heavy session this reported a
|
|
461
|
+
// remainder of zero over material automatic folding was about to take. It is the reading
|
|
462
|
+
// a deferred commit uses to say whether its mass is HELD or simply absent, which is the
|
|
463
|
+
// one distinction that record exists to draw, and it was answering "absent" for mass in
|
|
464
|
+
// plain view. It also broke the frontier's first cut, which gated on this number before
|
|
465
|
+
// the threshold moved into the selector where it belongs.
|
|
466
|
+
//
|
|
467
|
+
// Walked with the selector rather than re-derived, so the population is what automatic
|
|
468
|
+
// folding can actually reach and cannot drift from it. Walked from an EMPTY claim set,
|
|
469
|
+
// because this is the DENOMINATOR: a chapter already claimed by a mark is still foldable
|
|
470
|
+
// mass, it is just mass that is already spoken for, and the numerator below is what
|
|
471
|
+
// subtracts it. Bounded by the mapped length, which no terminating walk can exceed since
|
|
472
|
+
// every pass claims at least one ref.
|
|
473
|
+
const indexed = mappedByKey(snapshot);
|
|
474
|
+
const walked = new Set<string>();
|
|
475
|
+
for (let pass = 0; pass < snapshot.mapped.length; pass += 1) {
|
|
476
|
+
const chapter = selectAutomaticChapter(snapshot, state, walked);
|
|
477
|
+
if (!chapter) break;
|
|
478
|
+
for (const ref of chapter.sourceRefs) {
|
|
479
|
+
walked.add(objectRefKey(ref));
|
|
480
|
+
const item = indexed.get(objectRefKey(ref));
|
|
481
|
+
if (item) members.add(item.index);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
712
484
|
const candidates: Array<{ id: string; tokens: number }> = [];
|
|
713
485
|
let unmarkedBytes = 0;
|
|
714
486
|
let memberBytes = 0;
|
|
@@ -724,29 +496,199 @@ export function unmarkedRemainder(
|
|
|
724
496
|
return {
|
|
725
497
|
spans: candidates.length,
|
|
726
498
|
tokens: Math.ceil(unmarkedBytes / perToken),
|
|
499
|
+
chars: unmarkedBytes,
|
|
727
500
|
share: memberBytes > 0 ? unmarkedBytes / memberBytes : 0,
|
|
728
501
|
candidates: candidates.slice(0, Math.max(0, limit)),
|
|
729
502
|
};
|
|
730
503
|
}
|
|
731
504
|
|
|
732
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* THE FOLD FRONTIER: THE RUNTIME CUTS, AND THE AGENT EDITS WHAT IT CUT
|
|
507
|
+
* (Shane, 2026-08-23).
|
|
508
|
+
*
|
|
509
|
+
* The ladder used to stage nothing between commits, and before that it staged a mark on
|
|
510
|
+
* every measured response. Both were answers to the same question, WHEN SHOULD THE AGENT
|
|
511
|
+
* CURATE, and both got it wrong in the same way: they asked the agent to CHOOSE SPANS.
|
|
512
|
+
* Eager staging chose them first and crowded the agent out; standing down left the choice
|
|
513
|
+
* open until the commit, by which time the material was long out of view and the only
|
|
514
|
+
* information available was staleness, which the runtime already has.
|
|
515
|
+
*
|
|
516
|
+
* So the choice moves. As raw material accumulates past `minFoldChars`, the runtime cuts
|
|
517
|
+
* it, stalest first, exactly as a commit would. The cut is a PENDING MARK and pending
|
|
518
|
+
* marks are byte-inert: nothing moves, the projection does not change, the prefix cache is
|
|
519
|
+
* untouched, and the agent keeps seeing the raw material until a commit applies it. What
|
|
520
|
+
* the agent gets is a fold to annotate while the content is still fresh, which is the one
|
|
521
|
+
* thing it knows that the runtime does not.
|
|
522
|
+
*
|
|
523
|
+
* The loop bounds itself twice over. It stops when the unmarked remainder falls under the
|
|
524
|
+
* threshold, and it stops when the selector runs out of members, so a window whose whole
|
|
525
|
+
* remainder is pinned or blacklisted stages nothing and says so by returning empty rather
|
|
526
|
+
* than spinning.
|
|
527
|
+
*/
|
|
528
|
+
export function frontierMarks(input: {
|
|
529
|
+
snapshot: ActiveContextSnapshot;
|
|
530
|
+
state: ActiveContextState;
|
|
531
|
+
ordinal: number;
|
|
532
|
+
}): PendingFoldMark[] {
|
|
533
|
+
const { snapshot } = input;
|
|
534
|
+
const claimed = claimedRefKeys(input.state);
|
|
535
|
+
const marks: PendingFoldMark[] = [];
|
|
536
|
+
let state = input.state;
|
|
537
|
+
while (marks.length < MAX_FRONTIER_CUTS_PER_PASS) {
|
|
538
|
+
// THE THRESHOLD IS THE SELECTOR'S OWN, which is `minFoldChars`, and it is read there
|
|
539
|
+
// rather than counted here. The first cut of this measured the unmarked remainder
|
|
540
|
+
// instead, and that reads TOOL BATCHES ONLY: a chapter-only session had a remainder of
|
|
541
|
+
// zero however much raw prose it held, so the frontier never fired on it at all and
|
|
542
|
+
// gate 7's fixture produced no cut. One threshold, in the one place that knows every
|
|
543
|
+
// span kind.
|
|
544
|
+
const candidate = selectAutomaticSpan(snapshot, state, claimed);
|
|
545
|
+
if (!candidate) break;
|
|
546
|
+
const mark = foldMarkFor({
|
|
547
|
+
candidate,
|
|
548
|
+
...automaticMarkBrief(snapshot, state, candidate),
|
|
549
|
+
origin: "ladder",
|
|
550
|
+
ordinal: input.ordinal,
|
|
551
|
+
});
|
|
552
|
+
const addition = addPendingMark(state, mark);
|
|
553
|
+
if (!addition.added) break;
|
|
554
|
+
state = addition.state;
|
|
555
|
+
marks.push(mark);
|
|
556
|
+
for (const ref of candidate.sourceRefs) claimed.add(objectRefKey(ref));
|
|
557
|
+
}
|
|
558
|
+
return marks;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The pending mark that already holds a piece of this evidence, if any.
|
|
563
|
+
*
|
|
564
|
+
* The commit fills from the stale end and the depth bound retains what it does not
|
|
565
|
+
* reach, so a ladder mark routinely outlives the commit that staged it. An agent
|
|
566
|
+
* marking the same evidence afterwards would stand a second owner over one span and
|
|
567
|
+
* lose at apply time with a forest invariant for a message. Refusing at the tool says
|
|
568
|
+
* so while the agent can still act on it.
|
|
569
|
+
*/
|
|
570
|
+
export function markClaimingRef(
|
|
571
|
+
state: ActiveContextState,
|
|
572
|
+
refs: readonly EvidenceRef[],
|
|
573
|
+
): { markId: string; entryId: string } | null {
|
|
574
|
+
for (const mark of pendingMarks(state)) {
|
|
575
|
+
if (mark.mark !== "fold") continue;
|
|
576
|
+
const claimed = new Set(markSpanRefs(state, mark).refs.map(objectRefKey));
|
|
577
|
+
for (const ref of refs) {
|
|
578
|
+
if (claimed.has(objectRefKey(ref))) return { markId: mark.id, entryId: ref.entryId };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export interface WindowClaims {
|
|
585
|
+
/** Tokens standing marks will free at the next commit. */
|
|
586
|
+
markedTokens: number;
|
|
587
|
+
/** Tokens the agent pinned raw: a claim that frees nothing and is never taken. */
|
|
588
|
+
pinnedTokens: number;
|
|
589
|
+
pinnedRefs: number;
|
|
590
|
+
/** Pinned mass as a share of the serving budget, against its cap. */
|
|
591
|
+
pinnedShare: number;
|
|
592
|
+
/** Completed units neither marked nor pinned: what the ladder takes by age. */
|
|
593
|
+
unclaimedTokens: number;
|
|
594
|
+
unclaimedSpans: number;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* A PIN IS A CLAIM, NOT A FOLD (Shane, 2026-08-22: "if they do a pin on raw span I think
|
|
599
|
+
* we also treat that as a marked fold but a pinned one").
|
|
600
|
+
*
|
|
601
|
+
* Read as accounting, that is exactly right and already half true: pinned evidence is
|
|
602
|
+
* refused to every automatic rung, skipped by wedge absorption, and excluded from the
|
|
603
|
+
* unmarked remainder, so the ladder cannot take it and it is never reported as an
|
|
604
|
+
* oversight. What was missing is that the agent could not SEE it, and the whole point of
|
|
605
|
+
* asking an agent to account for its window is that the gaps become visible.
|
|
606
|
+
*
|
|
607
|
+
* A pin cannot count toward the DROP, and that is not a technicality. Pinned bytes stay
|
|
608
|
+
* in the window, so they free nothing; if they counted, an agent could "cover" a commit
|
|
609
|
+
* with pins, the commit would still be short, and it would fold something else anyway.
|
|
610
|
+
* The number would be a lie at the moment it mattered most. So the three buckets are
|
|
611
|
+
* reported side by side and only one of them pays.
|
|
612
|
+
*/
|
|
613
|
+
export function windowClaims(input: {
|
|
614
|
+
snapshot: ActiveContextSnapshot;
|
|
615
|
+
state: ActiveContextState;
|
|
616
|
+
budgetTokens: number;
|
|
617
|
+
charsPerToken: number;
|
|
618
|
+
}): WindowClaims {
|
|
619
|
+
const { snapshot, state, budgetTokens } = input;
|
|
620
|
+
const accounting = markAccounting(snapshot, state);
|
|
621
|
+
const remainder = unmarkedRemainder(snapshot, state, input.charsPerToken);
|
|
622
|
+
const pinned = protectedStaleMass(snapshot, state);
|
|
623
|
+
const pinnedTokens = estimatedTokens(pinned.bytes);
|
|
624
|
+
return {
|
|
625
|
+
markedTokens: Math.round(accounting.eligibleFreedBudgetShare * budgetTokens),
|
|
626
|
+
pinnedTokens,
|
|
627
|
+
pinnedRefs: pinned.refs,
|
|
628
|
+
pinnedShare: budgetTokens > 0 ? pinnedTokens / budgetTokens : 0,
|
|
629
|
+
unclaimedTokens: remainder.tokens,
|
|
630
|
+
unclaimedSpans: remainder.spans,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export interface CommitCoverage {
|
|
635
|
+
/** The share of the serving budget the next commit is sized to free. */
|
|
636
|
+
targetShare: number;
|
|
637
|
+
targetTokens: number;
|
|
638
|
+
/** What the standing marks will actually free when that commit runs. */
|
|
639
|
+
markedShare: number;
|
|
640
|
+
markedTokens: number;
|
|
641
|
+
/** What the ladder will take by staleness unless the agent marks it first. */
|
|
642
|
+
remainingTokens: number;
|
|
643
|
+
covered: boolean;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* WHAT THE NEXT COMMIT NEEDS, AND HOW MUCH OF IT THE AGENT HAS COVERED.
|
|
648
|
+
*
|
|
649
|
+
* One definition, read by three callers that used to reason about the drop separately:
|
|
650
|
+
* the commit fill that sizes itself to it, the advisory that warns before it, and the
|
|
651
|
+
* mark response that tells the agent where it stands. An agent asked to govern its own
|
|
652
|
+
* context cannot do it against a number nobody tells it.
|
|
653
|
+
*
|
|
654
|
+
* The marked side is the ELIGIBLE share, not every standing mark, because a mark over
|
|
655
|
+
* protected evidence frees nothing at this commit however good it is. That makes
|
|
656
|
+
* `remainingTokens` the honest answer to "what will fold without me".
|
|
657
|
+
*
|
|
658
|
+
* `targetShare` moves as the window grows: it is measured from occupancy NOW, and two
|
|
659
|
+
* large results landing after the reading raise it again. Callers that render it say so.
|
|
660
|
+
*/
|
|
661
|
+
export function commitCoverage(input: {
|
|
662
|
+
snapshot: ActiveContextSnapshot;
|
|
663
|
+
state: ActiveContextState;
|
|
664
|
+
usedTokens: number | null;
|
|
665
|
+
budgetTokens: number;
|
|
666
|
+
}): CommitCoverage {
|
|
667
|
+
const { snapshot, state, usedTokens, budgetTokens } = input;
|
|
668
|
+
const targetShare = usedTokens === null || !(budgetTokens > 0)
|
|
669
|
+
? 0
|
|
670
|
+
: Math.max(0, (usedTokens - snapshot.thresholds.minTarget * budgetTokens) / budgetTokens);
|
|
671
|
+
const accounting = markAccounting(snapshot, state);
|
|
672
|
+
const markedShare = accounting.eligibleFreedBudgetShare;
|
|
673
|
+
const targetTokens = Math.round(targetShare * budgetTokens);
|
|
674
|
+
const markedTokens = Math.round(markedShare * budgetTokens);
|
|
675
|
+
const remainingTokens = Math.max(0, targetTokens - markedTokens);
|
|
676
|
+
return {
|
|
677
|
+
targetShare,
|
|
678
|
+
targetTokens,
|
|
679
|
+
markedShare,
|
|
680
|
+
markedTokens,
|
|
681
|
+
remainingTokens,
|
|
682
|
+
covered: remainingTokens === 0,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
733
686
|
export function markedFoldIds(state: ActiveContextState): Set<string> {
|
|
734
687
|
const ids = new Set<string>();
|
|
735
688
|
for (const mark of pendingMarks(state)) if (mark.mark === "refold") ids.add(mark.id);
|
|
736
689
|
return ids;
|
|
737
690
|
}
|
|
738
691
|
|
|
739
|
-
/**
|
|
740
|
-
* THE COUNT LAW, AS MARKS. Every parent the eligible-root count owes right now, all of
|
|
741
|
-
* them, in the epoch that notices.
|
|
742
|
-
*
|
|
743
|
-
* Separate from the top-up on purpose. The top-up is pressure arithmetic: it stops the
|
|
744
|
-
* moment the marks in hand reach the freeing target, which is the right rule for how
|
|
745
|
-
* DEEP a commit cuts and the wrong rule entirely for a law that says a count of eligible
|
|
746
|
-
* roots at or above the width is a state that must not persist. So this runs at every
|
|
747
|
-
* commit epoch and it runs first, whatever the pressure and whatever the top-up would
|
|
748
|
-
* have proposed (Shane 2026-08-10).
|
|
749
|
-
*/
|
|
750
692
|
export function consolidationMarks(input: {
|
|
751
693
|
snapshot: ActiveContextSnapshot;
|
|
752
694
|
state: ActiveContextState;
|
|
@@ -760,28 +702,16 @@ export function consolidationMarks(input: {
|
|
|
760
702
|
}));
|
|
761
703
|
}
|
|
762
704
|
|
|
763
|
-
/**
|
|
764
|
-
* Agent judgment leads; automation guarantees the floor. If the marks the agent made
|
|
765
|
-
* would free less than the target share of the window, automation adds the stalest
|
|
766
|
-
* unprotected eligible SPANS until they do: completed tool batches and raw narrative
|
|
767
|
-
* chapters. Placeholders are not on that list any more; a placeholder gets a parent from
|
|
768
|
-
* the count law above, which is not pressure arithmetic and does not stop at a target.
|
|
769
|
-
*/
|
|
770
705
|
export function topUpMarks(input: {
|
|
771
706
|
snapshot: ActiveContextSnapshot;
|
|
772
707
|
state: ActiveContextState;
|
|
773
708
|
ordinal: number;
|
|
774
|
-
/** Freeing target as a share of the SERVING BUDGET. Required: there is no second default. */
|
|
775
709
|
targetShare: number;
|
|
776
|
-
/** Evidence keys the top-up may never propose, e.g. the current excursion's reads. */
|
|
777
|
-
excludeRefKeys?: ReadonlySet<string>;
|
|
778
|
-
/** Measure progress against ELIGIBLE mass, so pinned or protected marks never count. */
|
|
779
710
|
eligibleOnly?: boolean;
|
|
780
711
|
}): PendingFoldMark[] {
|
|
781
712
|
const { snapshot } = input;
|
|
782
713
|
const target = input.targetShare;
|
|
783
714
|
const claimed = claimedRefKeys(input.state);
|
|
784
|
-
for (const key of input.excludeRefKeys ?? []) claimed.add(key);
|
|
785
715
|
const marks: PendingFoldMark[] = [];
|
|
786
716
|
let state = input.state;
|
|
787
717
|
const progress = (value: ActiveContextState): number => {
|
|
@@ -789,7 +719,7 @@ export function topUpMarks(input: {
|
|
|
789
719
|
return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
|
|
790
720
|
};
|
|
791
721
|
let share = progress(state);
|
|
792
|
-
|
|
722
|
+
while (share < target) {
|
|
793
723
|
const candidate = selectAutomaticSpan(snapshot, state, claimed);
|
|
794
724
|
if (!candidate) break;
|
|
795
725
|
const mark = foldMarkFor({
|
|
@@ -808,44 +738,64 @@ export function topUpMarks(input: {
|
|
|
808
738
|
return marks;
|
|
809
739
|
}
|
|
810
740
|
|
|
811
|
-
/**
|
|
741
|
+
/**
|
|
742
|
+
* REFOLD AS THE LAST FILL, NOT THE FIRST (Shane, 2026-08-21).
|
|
743
|
+
*
|
|
744
|
+
* An expanded fold is stale mass sitting in the window, so the commit may reclaim it,
|
|
745
|
+
* but only after raw stale material has been spent. An agent that expands a fold is
|
|
746
|
+
* reading it; taking it back while there is still unfolded material to take is hostile
|
|
747
|
+
* for no gain. Pin holds a span past that point, and a live lease holds it for the
|
|
748
|
+
* generations right after the expand. Stalest first, and never past the freeing target.
|
|
749
|
+
*/
|
|
750
|
+
export function refoldMarks(input: {
|
|
751
|
+
snapshot: ActiveContextSnapshot;
|
|
752
|
+
state: ActiveContextState;
|
|
753
|
+
ordinal: number;
|
|
754
|
+
targetShare: number;
|
|
755
|
+
eligibleOnly?: boolean;
|
|
756
|
+
}): PendingMark[] {
|
|
757
|
+
const { snapshot } = input;
|
|
758
|
+
const marks: PendingMark[] = [];
|
|
759
|
+
let state = input.state;
|
|
760
|
+
const claimedFoldIds = markedFoldIds(state);
|
|
761
|
+
const progress = (value: ActiveContextState): number => {
|
|
762
|
+
const accounting = markAccounting(snapshot, value);
|
|
763
|
+
return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
|
|
764
|
+
};
|
|
765
|
+
let share = progress(state);
|
|
766
|
+
while (share < input.targetShare) {
|
|
767
|
+
const foldId = selectAutomaticRefold(snapshot, state, claimedFoldIds);
|
|
768
|
+
if (!foldId) break;
|
|
769
|
+
const mark: PendingMark = {
|
|
770
|
+
mark: "refold",
|
|
771
|
+
id: foldId,
|
|
772
|
+
origin: "ladder",
|
|
773
|
+
ordinal: input.ordinal,
|
|
774
|
+
};
|
|
775
|
+
const addition = addPendingMark(state, mark);
|
|
776
|
+
if (!addition.added) break;
|
|
777
|
+
state = addition.state;
|
|
778
|
+
marks.push(mark);
|
|
779
|
+
claimedFoldIds.add(foldId);
|
|
780
|
+
share = progress(state);
|
|
781
|
+
}
|
|
782
|
+
return marks;
|
|
783
|
+
}
|
|
784
|
+
|
|
812
785
|
export interface AbsorbedWedge {
|
|
813
|
-
/** The mark that grew backward to cover it. */
|
|
814
786
|
intoMarkId: string;
|
|
815
|
-
/**
|
|
816
|
-
* The id that mark carried BEFORE it grew. A mark's id is derived from its span, so
|
|
817
|
-
* growing the span mints a new one; without this the trail from the decision the
|
|
818
|
-
* agent made to the fold that carries it is broken, and a mark appears to vanish.
|
|
819
|
-
*/
|
|
820
787
|
fromMarkId: string;
|
|
821
788
|
startId: string;
|
|
822
789
|
endId: string;
|
|
823
790
|
entries: number;
|
|
824
791
|
tokens: number;
|
|
792
|
+
chars: number;
|
|
825
793
|
}
|
|
826
794
|
|
|
827
|
-
/**
|
|
828
|
-
* Wedge absorption. THE ANTI-LCM PIN LIVES HERE.
|
|
829
|
-
*
|
|
830
|
-
* A sliver of stale raw content hugging a fold boundary is a crumb nothing will ever
|
|
831
|
-
* reclaim: it is below the minimum chapter size, so no chapter rung will take it, and
|
|
832
|
-
* it is not a tool batch, so no tool rung will either. Absorbing it costs nothing
|
|
833
|
-
* because it happens INSIDE a commit the epoch has already paid for -- it grows a
|
|
834
|
-
* pending mark backward rather than performing a mutation of its own.
|
|
835
|
-
*
|
|
836
|
-
* What it must never do is erode deliberate non-sequential curation. An agent holding
|
|
837
|
-
* folds at 10:20, 40:55 and 60:70 with raw spans between them chose that shape, and a
|
|
838
|
-
* mechanism that swallowed those gaps would delete the curation element outright. So
|
|
839
|
-
* the threshold is tiny, measured in the session's own calibrated tokens, and NOT
|
|
840
|
-
* pressure-scaled: above it, a gap stays raw permanently no matter how full the window
|
|
841
|
-
* gets or how ragged the projection looks.
|
|
842
|
-
*/
|
|
843
795
|
export function absorbWedgeMarks(input: {
|
|
844
796
|
snapshot: ActiveContextSnapshot;
|
|
845
797
|
state: ActiveContextState;
|
|
846
|
-
/** The session's measured serialized chars per token; never a fixed constant. */
|
|
847
798
|
charsPerToken: number;
|
|
848
|
-
/** Evidence absorption may never touch, e.g. the current excursion's own reads. */
|
|
849
799
|
excludeRefKeys?: ReadonlySet<string>;
|
|
850
800
|
}): { state: ActiveContextState; absorbed: AbsorbedWedge[] } {
|
|
851
801
|
const { snapshot } = input;
|
|
@@ -863,9 +813,7 @@ export function absorbWedgeMarks(input: {
|
|
|
863
813
|
.filter((index): index is number => index !== undefined);
|
|
864
814
|
return indices.length ? { start: Math.min(...indices), end: Math.max(...indices) } : null;
|
|
865
815
|
};
|
|
866
|
-
|
|
867
|
-
// has already grown is not a candidate to grow again in the same commit.
|
|
868
|
-
for (let guard = 0; guard < MAX_PENDING_MARKS; guard += 1) {
|
|
816
|
+
for (;;) {
|
|
869
817
|
const marks = pendingMarks(state);
|
|
870
818
|
const occupied = [
|
|
871
819
|
...orderedRoots(state, snapshot).map((root) => ({ start: root.start, end: root.end })),
|
|
@@ -885,9 +833,16 @@ export function absorbWedgeMarks(input: {
|
|
|
885
833
|
const gapEnd = interval.start - 1;
|
|
886
834
|
if (gapStart < 0 || gapEnd < gapStart) continue;
|
|
887
835
|
if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
|
|
888
|
-
|
|
889
|
-
//
|
|
890
|
-
|
|
836
|
+
// THE GAP FLOOR IS THE FOLD FLOOR (Shane, 2026-08-21). A gap between two folds
|
|
837
|
+
// that could not stand as a fold on its own has no business staying raw: it costs
|
|
838
|
+
// its own bytes and buys nothing, and the neighbour beside it can hold it for the
|
|
839
|
+
// price of one sentence in the brief. So anything under minFoldChars is absorbed
|
|
840
|
+
// and anything at or over it is left alone, because at that size it is a fold the
|
|
841
|
+
// ladder will take on its own terms. This replaced MAX_WEDGE_ABSORB_TOKENS, a
|
|
842
|
+
// separate 256-token constant that let 1,024-char slivers survive between folds.
|
|
843
|
+
const gapBytes = spanBytes(snapshot, gapStart, gapEnd + 1);
|
|
844
|
+
if (gapBytes >= snapshot.thresholds.minFoldChars) continue;
|
|
845
|
+
const gapTokens = Math.ceil(gapBytes / charsPerToken);
|
|
891
846
|
const gapRefs = [];
|
|
892
847
|
let usable = true;
|
|
893
848
|
for (let index = gapStart; index <= gapEnd; index += 1) {
|
|
@@ -897,15 +852,19 @@ export function absorbWedgeMarks(input: {
|
|
|
897
852
|
}
|
|
898
853
|
if (!usable || !gapRefs.length) continue;
|
|
899
854
|
if (refsProtected(gapRefs, state, snapshot)) continue;
|
|
900
|
-
// Structural safety: the grown span must still be a valid closed chapter, which
|
|
901
|
-
// is what keeps every tool call paired with its result in the projection.
|
|
902
855
|
if (!chapterRangeIsUnitAligned(snapshot, gapStart, interval.end)) continue;
|
|
903
856
|
const parts = partsForRange(snapshot, state, gapStart, interval.end, new Set<FoldKind>(["tool-result"]));
|
|
904
857
|
if (!parts) continue;
|
|
858
|
+
// ELIGIBILITY, not truncation (Shane 2026-08-14): the grown fold's brief
|
|
859
|
+
// must name what it absorbed, and a brief already near the policy cap has
|
|
860
|
+
// no room for that sentence. Slicing dropped exactly the words that made
|
|
861
|
+
// the absorption honest, so a wedge whose truthful suffix does not fit is
|
|
862
|
+
// simply not absorbed; the gap stays raw for a later commit.
|
|
863
|
+
const absorbSuffix = ` It also holds ${gapRefs.length} short adjacent entry(s) absorbed at commit.`;
|
|
864
|
+
if (mark.brief.length + absorbSuffix.length > snapshot.policy.maxBriefChars) continue;
|
|
905
865
|
const grown = foldMarkFor({
|
|
906
866
|
candidate: { kind: "chapter", parts, sourceRefs: candidateSourceRefs(parts, state) },
|
|
907
|
-
brief: `${mark.brief}
|
|
908
|
-
.slice(0, snapshot.policy.maxBriefChars),
|
|
867
|
+
brief: `${mark.brief}${absorbSuffix}`,
|
|
909
868
|
briefProvenance: mark.briefProvenance,
|
|
910
869
|
origin: mark.origin,
|
|
911
870
|
ordinal: mark.ordinal,
|
|
@@ -920,6 +879,7 @@ export function absorbWedgeMarks(input: {
|
|
|
920
879
|
endId: gapRefs.at(-1)!.entryId,
|
|
921
880
|
entries: gapRefs.length,
|
|
922
881
|
tokens: gapTokens,
|
|
882
|
+
chars: gapBytes,
|
|
923
883
|
});
|
|
924
884
|
applied = true;
|
|
925
885
|
break;
|
|
@@ -941,7 +901,6 @@ export interface RefusedMark {
|
|
|
941
901
|
id: string;
|
|
942
902
|
origin: MarkOrigin;
|
|
943
903
|
reason: string;
|
|
944
|
-
/** Whether the mark survives as pending rather than being discarded. */
|
|
945
904
|
retained: boolean;
|
|
946
905
|
}
|
|
947
906
|
|
|
@@ -950,98 +909,51 @@ export interface CommitEpochResult {
|
|
|
950
909
|
applied: AppliedMark[];
|
|
951
910
|
refused: RefusedMark[];
|
|
952
911
|
retained: PendingMark[];
|
|
953
|
-
/** Guarded marks the pressure waiver released into this commit, oldest material first. */
|
|
954
|
-
waived: PendingMark[];
|
|
955
912
|
}
|
|
956
913
|
|
|
957
|
-
/**
|
|
958
|
-
* Apply every pending mark. This is the ONE mutation of the epoch: it reuses
|
|
959
|
-
* prepareFold/commitPreparedFold exactly as the immediate path does, so a committed
|
|
960
|
-
* mark and an immediate fold produce the same fold record.
|
|
961
|
-
*/
|
|
962
914
|
export async function commitPendingMarks(input: {
|
|
963
915
|
snapshot: ActiveContextSnapshot;
|
|
964
916
|
state: ActiveContextState;
|
|
965
917
|
generation: number;
|
|
966
918
|
now?: () => number;
|
|
967
|
-
/**
|
|
968
|
-
* Keep marks the commit could not apply YET. A mark is a standing decision; dropping
|
|
969
|
-
* it because the span happened to be fresh at commit time silently discards the
|
|
970
|
-
* agent's judgment and leaves nothing for the next commit to act on.
|
|
971
|
-
*/
|
|
972
919
|
retainIneligible?: boolean;
|
|
973
920
|
/**
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
921
|
+
* How many bytes this commit needs to free. Marks past it are RETAINED, not applied.
|
|
922
|
+
*
|
|
923
|
+
* This bound did not exist before the frontier (Shane, 2026-08-23), and it did not need
|
|
924
|
+
* to: staging was sized to the drop by `topUpMarks`, so every pending mark was one the
|
|
925
|
+
* commit had already decided it wanted. The frontier stages as material ARRIVES, which
|
|
926
|
+
* is the whole point of it, so by the time a commit fires the pending set is everything
|
|
927
|
+
* the window holds. Applying all of it would cut the window to nothing at the first
|
|
928
|
+
* commit, which is neither what the budget asks for nor what the agent is owed: what it
|
|
929
|
+
* is owed is that material stays raw for as long as there is room for it.
|
|
930
|
+
*
|
|
931
|
+
* Undefined means no bound, which is what the emergency lanes want: a fence or a
|
|
932
|
+
* rollback is not housekeeping and takes everything it can.
|
|
978
933
|
*/
|
|
979
|
-
|
|
980
|
-
/**
|
|
981
|
-
* How many guarded marks the pressure waiver releases anyway, oldest material first. The
|
|
982
|
-
* guard protects an in-flight excursion; it may never cost the session its ability
|
|
983
|
-
* to send a request at all. See `guardWaiverCount`.
|
|
984
|
-
*/
|
|
985
|
-
guardWaiver?: number;
|
|
934
|
+
applyTargetBytes?: number;
|
|
986
935
|
}): Promise<CommitEpochResult> {
|
|
936
|
+
// THE CURRENT-TURN GUARD IS DELETED (Shane, 2026-08-23: "you're using turns as the
|
|
937
|
+
// boundaries, which should not be the case. It should be at the most granular level,
|
|
938
|
+
// which is events"). The guard retained every mark touching a toolResult newer than
|
|
939
|
+
// the last terminal assistant stop, and a terminal stop is a TURN artifact: the first
|
|
940
|
+
// live session (sol-20260823-live rep 1) ran its whole workload as one turn, every
|
|
941
|
+
// assistant response stopping "toolUse", so the guard's "current turn" was the entire
|
|
942
|
+
// window, four band-top commits applied nothing, and the projection fence swept 18
|
|
943
|
+
// marks at 0.958 occupancy down to a 0.031 landing against a 0.40 aim. The waiver
|
|
944
|
+
// machinery existed only to bail the guard out at the fence and the boundary, so it
|
|
945
|
+
// is deleted with it. What protects the working set now is structural and event-level:
|
|
946
|
+
// an incomplete batch is never proposable, the depth bound stops the routine commit at
|
|
947
|
+
// the aim, and the stalest-first cut order leaves the newest events raw.
|
|
987
948
|
const marks = pendingMarks(input.state);
|
|
988
949
|
const retained: PendingMark[] = [];
|
|
989
|
-
const waived: PendingMark[] = [];
|
|
990
|
-
const guardReasons = new Map<string, string>();
|
|
991
950
|
const held = new Set(input.state.folds.map((fold) => fold.id));
|
|
992
951
|
const deferred: PendingMark[] = [];
|
|
993
|
-
/**
|
|
994
|
-
* Whether the mark names a fold some other standing mark is about to mint. Until that
|
|
995
|
-
* fold exists there is no eligibility worth judging: the question is about material
|
|
996
|
-
* the child owns, and the apply loop is where it gets asked, after the child has
|
|
997
|
-
* landed. A mark naming a fold NOBODY will mint is not this case; it is judged
|
|
998
|
-
* normally, and the apply loop drops it by name.
|
|
999
|
-
*/
|
|
1000
952
|
const awaitingMintedChild = (mark: PendingMark): boolean =>
|
|
1001
953
|
markSpanRefs(input.state, mark).unresolved?.pending === true;
|
|
1002
|
-
const currentTurn = input.guardCurrentTurn
|
|
1003
|
-
? currentTurnRefKeys(input.snapshot)
|
|
1004
|
-
: new Set<string>();
|
|
1005
|
-
if (input.guardCurrentTurn) {
|
|
1006
|
-
const applicable: PendingMark[] = [];
|
|
1007
|
-
const guarded: PendingMark[] = [];
|
|
1008
|
-
for (const mark of marks) {
|
|
1009
|
-
if (markTouchesCurrentTurn(input.state, mark, currentTurn)) guarded.push(mark);
|
|
1010
|
-
else applicable.push(mark);
|
|
1011
|
-
}
|
|
1012
|
-
// Oldest MATERIAL first: the newest reads are the ones the excursion is about to
|
|
1013
|
-
// use, so they are the last evidence the waiver surrenders. Staleness is a property
|
|
1014
|
-
// of the span, never of when the mark was proposed; see `markSpanStart`. The id
|
|
1015
|
-
// comparison is the final tiebreak between marks covering the same start, and
|
|
1016
|
-
// nothing else.
|
|
1017
|
-
const guardedStart = new Map(guarded.map((mark) =>
|
|
1018
|
-
[pendingMarkKey(mark), markSpanStart(input.snapshot, input.state, mark)] as const));
|
|
1019
|
-
guarded.sort((left, right) =>
|
|
1020
|
-
guardedStart.get(pendingMarkKey(left))! - guardedStart.get(pendingMarkKey(right))! ||
|
|
1021
|
-
left.id.localeCompare(right.id));
|
|
1022
|
-
const waiverCount = Math.max(0, Math.min(guarded.length, Math.trunc(input.guardWaiver ?? 0)));
|
|
1023
|
-
for (const [index, mark] of guarded.entries()) {
|
|
1024
|
-
if (index < waiverCount) {
|
|
1025
|
-
waived.push(mark);
|
|
1026
|
-
applicable.push(mark);
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
retained.push(mark);
|
|
1030
|
-
guardReasons.set(
|
|
1031
|
-
pendingMarkKey(mark),
|
|
1032
|
-
"evidence was gathered in the current turn; the mark stays pending until the turn closes",
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
marks.length = 0;
|
|
1036
|
-
marks.push(...applicable);
|
|
1037
|
-
}
|
|
1038
954
|
if (input.retainIneligible) {
|
|
1039
955
|
const applicable: PendingMark[] = [];
|
|
1040
956
|
for (const mark of marks) {
|
|
1041
|
-
// A mark awaiting a child another mark will mint is not judged here. Its
|
|
1042
|
-
// eligibility is a question about material the child fold owns, and that fold is
|
|
1043
|
-
// what this commit is about to create. The apply loop asks it once the child has
|
|
1044
|
-
// landed, and answers by name when it does not.
|
|
1045
957
|
if (!awaitingMintedChild(mark) &&
|
|
1046
958
|
markEligibility(input.snapshot, input.state, mark) === "protected") retained.push(mark);
|
|
1047
959
|
else applicable.push(mark);
|
|
@@ -1055,27 +967,9 @@ export async function commitPendingMarks(input: {
|
|
|
1055
967
|
mark: mark.mark,
|
|
1056
968
|
id: mark.id,
|
|
1057
969
|
origin: mark.origin,
|
|
1058
|
-
reason:
|
|
1059
|
-
"span is still fresh or protected; the mark stays pending until it is eligible",
|
|
970
|
+
reason: "span is still fresh or protected; the mark stays pending until it is eligible",
|
|
1060
971
|
retained: true,
|
|
1061
972
|
}));
|
|
1062
|
-
// CHILDREN BEFORE THE SPANS THAT ABSORB THEM, AND TRANSCRIPT ORDER IS NOT THAT ORDER.
|
|
1063
|
-
//
|
|
1064
|
-
// The invariant is real: a span whose parts name another mark's fold must find that
|
|
1065
|
-
// fold in the forest when it applies. `foldMarkFor` gives a mark the id its committed
|
|
1066
|
-
// fold will carry, so the naming is exact and readable off the parts. What it is NOT is
|
|
1067
|
-
// geometry. A parent CONTAINS its child, so the earliest window index a parent covers
|
|
1068
|
-
// is at or before its child's, and ordering oldest material first applies the parent
|
|
1069
|
-
// FIRST in exactly the case the invariant exists for. The cost is not a reordering:
|
|
1070
|
-
// `candidateSourceRefs` throws `Missing candidate child` on the unresolved part, the
|
|
1071
|
-
// apply loop catches it, and the mark is refused with `retained: false`, so the parent
|
|
1072
|
-
// applied one place too early loses the agent's decision outright rather than waiting.
|
|
1073
|
-
//
|
|
1074
|
-
// So the dependency is the law and the sort key is the tiebreak. Among marks with no
|
|
1075
|
-
// such tie, real transcript order: the earliest window index the span covers, oldest
|
|
1076
|
-
// material first, id last. The `ordinal` this used to sort on is the transcript position
|
|
1077
|
-
// at MARK time, one value shared by every mark an epoch proposes, so within an epoch it
|
|
1078
|
-
// decided nothing and the order fell through to comparing content hashes.
|
|
1079
973
|
const minting = new Map(marks.flatMap((mark) =>
|
|
1080
974
|
mark.mark === "fold" && !held.has(mark.id) ? [[mark.id, mark] as const] : []));
|
|
1081
975
|
const applyStart = new Map(marks.map((mark) =>
|
|
@@ -1088,9 +982,6 @@ export async function commitPendingMarks(input: {
|
|
|
1088
982
|
const placing = new Set<string>();
|
|
1089
983
|
const place = (mark: PendingMark): void => {
|
|
1090
984
|
const key = pendingMarkKey(mark);
|
|
1091
|
-
// `placing` is the cycle stop. Fold ids are hashes OF the parts, so a cycle would
|
|
1092
|
-
// need a hash to name its own ancestor; the guard is here so a corrupt state loses
|
|
1093
|
-
// one mark to the ordinary refusal rather than hanging the commit.
|
|
1094
985
|
if (placed.has(key) || placing.has(key)) return;
|
|
1095
986
|
placing.add(key);
|
|
1096
987
|
if (mark.mark === "fold") {
|
|
@@ -1103,6 +994,40 @@ export async function commitPendingMarks(input: {
|
|
|
1103
994
|
placed.add(key);
|
|
1104
995
|
ordered.push(mark);
|
|
1105
996
|
};
|
|
997
|
+
// THE DEPTH CUT, STALEST FIRST. `base` is already in span order, so taking a prefix of
|
|
998
|
+
// it takes the oldest material and leaves the newest raw, which is the ordering the
|
|
999
|
+
// agent would choose if it were asked. A mark left over here is RETAINED rather than
|
|
1000
|
+
// refused: it is still pending, still briefable, still editable, and the next commit
|
|
1001
|
+
// that needs depth will reach it.
|
|
1002
|
+
const bounded: PendingMark[] = [];
|
|
1003
|
+
if (input.applyTargetBytes !== undefined && input.applyTargetBytes > 0) {
|
|
1004
|
+
let freed = 0;
|
|
1005
|
+
for (const mark of base) {
|
|
1006
|
+
if (freed >= input.applyTargetBytes) {
|
|
1007
|
+
bounded.push(mark);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
freed += markFreedBytes(input.snapshot, input.state, mark);
|
|
1011
|
+
}
|
|
1012
|
+
if (bounded.length) {
|
|
1013
|
+
const spare = new Set(bounded.map((mark) => pendingMarkKey(mark)));
|
|
1014
|
+
const kept = base.filter((mark) => !spare.has(pendingMarkKey(mark)));
|
|
1015
|
+
base.length = 0;
|
|
1016
|
+
base.push(...kept);
|
|
1017
|
+
for (const mark of bounded) {
|
|
1018
|
+
retained.push(mark);
|
|
1019
|
+
refused.push({
|
|
1020
|
+
mark: mark.mark,
|
|
1021
|
+
id: mark.id,
|
|
1022
|
+
origin: mark.origin,
|
|
1023
|
+
reason: "the commit reached its target before this mark; it stays pending and " +
|
|
1024
|
+
"the span stays raw until a later commit needs the depth",
|
|
1025
|
+
retained: true,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
state = withPendingMarks(input.state, retained);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1106
1031
|
for (const mark of base) place(mark);
|
|
1107
1032
|
for (const mark of ordered) {
|
|
1108
1033
|
try {
|
|
@@ -1117,15 +1042,6 @@ export async function commitPendingMarks(input: {
|
|
|
1117
1042
|
continue;
|
|
1118
1043
|
}
|
|
1119
1044
|
const parts = clone(mark.parts);
|
|
1120
|
-
// THE ANSWER TO A NAME THE STATE CANNOT RESOLVE, AND IT IS TWO ANSWERS.
|
|
1121
|
-
//
|
|
1122
|
-
// A mark still standing will mint the fold this one names, so the span becomes
|
|
1123
|
-
// readable at the next commit: that is a DEFER, and the mark stays pending. This
|
|
1124
|
-
// is the child a guard held while its parent was applicable, which the epoch used
|
|
1125
|
-
// to discard outright. Nothing standing names it and it is a DROP: `reboundary`
|
|
1126
|
-
// dissolved the fold, or the mark that would have minted it was itself refused,
|
|
1127
|
-
// and no later commit can honour the decision. Same two words the eligibility
|
|
1128
|
-
// reading uses, and the same receipt shape a dropped brief upgrade gets.
|
|
1129
1045
|
const span = markSpanRefs(state, mark);
|
|
1130
1046
|
if (span.unresolved) {
|
|
1131
1047
|
const { foldId, pending } = span.unresolved;
|
|
@@ -1144,13 +1060,11 @@ export async function commitPendingMarks(input: {
|
|
|
1144
1060
|
continue;
|
|
1145
1061
|
}
|
|
1146
1062
|
const sourceRefs = span.refs;
|
|
1147
|
-
const blocked =
|
|
1148
|
-
? toolRefsProtected(sourceRefs, state, input.snapshot)
|
|
1149
|
-
: refsProtected(sourceRefs, state, input.snapshot);
|
|
1063
|
+
const blocked = refsProtected(sourceRefs, state, input.snapshot);
|
|
1150
1064
|
if (blocked) {
|
|
1151
1065
|
throw new Error(
|
|
1152
1066
|
`pending fold mark ${mark.id} covers protected or fresh evidence; ` +
|
|
1153
|
-
"
|
|
1067
|
+
"unpin that evidence and mark it again",
|
|
1154
1068
|
);
|
|
1155
1069
|
}
|
|
1156
1070
|
const prepared = await prepareFold({
|
|
@@ -1169,9 +1083,6 @@ export async function commitPendingMarks(input: {
|
|
|
1169
1083
|
generation: input.generation,
|
|
1170
1084
|
});
|
|
1171
1085
|
applied.push({ mark: "fold", id: mark.id, origin: mark.origin, foldId: prepared.id });
|
|
1172
|
-
// A warmed preparation the commit just materialized through its mark is spent:
|
|
1173
|
-
// leaving it standing would hold a prepared fold whose id already exists in the
|
|
1174
|
-
// forest, and every later drift check would refuse it by name.
|
|
1175
1086
|
if (state.prepared && state.folds.some((fold) => fold.id === state.prepared!.id)) {
|
|
1176
1087
|
state = clearPrepared(state);
|
|
1177
1088
|
}
|
|
@@ -1185,17 +1096,13 @@ export async function commitPendingMarks(input: {
|
|
|
1185
1096
|
});
|
|
1186
1097
|
}
|
|
1187
1098
|
}
|
|
1188
|
-
// Deferrals rejoin the pending set. A mark the loop could not read yet is a standing
|
|
1189
|
-
// decision exactly as a guard-held one is, and `retained` is what the commit record
|
|
1190
|
-
// counts as deferred, so both live in the same list.
|
|
1191
1099
|
if (deferred.length) {
|
|
1192
1100
|
state = withPendingMarks(state, [...pendingMarks(state), ...deferred]);
|
|
1193
1101
|
retained.push(...deferred);
|
|
1194
1102
|
}
|
|
1195
|
-
return { state, applied, refused, retained
|
|
1103
|
+
return { state, applied, refused, retained };
|
|
1196
1104
|
}
|
|
1197
1105
|
|
|
1198
|
-
/** The scheduling block reported by the status action. */
|
|
1199
1106
|
export function schedulingStatus(input: {
|
|
1200
1107
|
snapshot: ActiveContextSnapshot;
|
|
1201
1108
|
state: ActiveContextState;
|
|
@@ -1203,16 +1110,11 @@ export function schedulingStatus(input: {
|
|
|
1203
1110
|
}): Record<string, unknown> {
|
|
1204
1111
|
const accounting = markAccounting(input.snapshot, input.state);
|
|
1205
1112
|
return {
|
|
1206
|
-
// A literal, not a setting: epoch is the only scheduler there is.
|
|
1207
1113
|
mode: "epoch",
|
|
1208
1114
|
...accounting,
|
|
1209
|
-
// The declared policy, reported never mutable.
|
|
1210
1115
|
thresholds: { ...input.snapshot.thresholds },
|
|
1211
1116
|
budgetTokens: input.snapshot.budgetTokens,
|
|
1212
1117
|
commitDue: epochCommitDue(input.snapshot, input.ratio),
|
|
1213
|
-
// One trigger, reported as one line. The eligible share is still reported above in
|
|
1214
|
-
// the accounting because it is what a commit WOULD free; it is no longer a reason
|
|
1215
|
-
// to fire one.
|
|
1216
1118
|
commitTrigger: {
|
|
1217
1119
|
mode: "band-top",
|
|
1218
1120
|
commitOccupancy: input.snapshot.thresholds.maxTarget,
|