pi-fold 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -40
- package/extensions/active-context.ts +2408 -600
- package/extensions/evidence.js +15 -2
- package/extensions/index.js +69 -21
- package/extensions/json.ts +33 -1
- package/extensions/lib/curation.ts +141 -139
- package/extensions/lib/folding.ts +418 -74
- package/extensions/lib/instrumentation.ts +24 -4
- package/extensions/lib/measurement.ts +92 -16
- package/extensions/lib/persistence.ts +212 -42
- package/extensions/lib/policy.ts +511 -155
- package/extensions/lib/rollback.ts +216 -0
- package/extensions/lib/scheduling.ts +414 -106
- package/extensions/lib/selection.ts +528 -144
- package/extensions/lib/surfacing.ts +479 -224
- package/extensions/lib/tool-surface.ts +25 -5
- package/extensions/lib/transcript.ts +146 -62
- package/extensions/summarizer.js +206 -10
- package/package.json +1 -1
- package/extensions/lib/advisory.ts +0 -309
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { objectRefKey } from "../json.ts";
|
|
2
|
+
import type { EvidenceRef } from "../json.ts";
|
|
2
3
|
import {
|
|
3
4
|
bytes,
|
|
4
5
|
clone,
|
|
@@ -7,20 +8,26 @@ import {
|
|
|
7
8
|
import {
|
|
8
9
|
commitPreparedFold,
|
|
9
10
|
pinnedPeekMass,
|
|
11
|
+
preparedMatchesCandidate,
|
|
10
12
|
prepareFold,
|
|
11
13
|
renderFold,
|
|
12
14
|
renderFoldParts,
|
|
15
|
+
selectAutomaticSpan,
|
|
13
16
|
setFoldProjectionState,
|
|
14
17
|
} from "./folding.ts";
|
|
15
18
|
import type { AutomaticRungSelection } from "./folding.ts";
|
|
16
19
|
import {
|
|
17
20
|
foldInterval,
|
|
21
|
+
budgetOccupancy,
|
|
18
22
|
hardFenceRatio,
|
|
23
|
+
mappedByKey,
|
|
19
24
|
orderedRoots,
|
|
20
25
|
refsProtected,
|
|
21
26
|
toolRefsProtected,
|
|
22
27
|
} from "./measurement.ts";
|
|
23
28
|
import {
|
|
29
|
+
childFoldIds,
|
|
30
|
+
clearPrepared,
|
|
24
31
|
flattenFoldRefs,
|
|
25
32
|
foldIdFor,
|
|
26
33
|
parsePendingMarks,
|
|
@@ -28,8 +35,6 @@ import {
|
|
|
28
35
|
} from "./persistence.ts";
|
|
29
36
|
import { currentTurnRefKeys } from "./transcript.ts";
|
|
30
37
|
import {
|
|
31
|
-
EPOCH_COMMIT_TARGET_WINDOW_SHARE,
|
|
32
|
-
EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD,
|
|
33
38
|
EPOCH_MAX_TOPUP_MARKS,
|
|
34
39
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
35
40
|
MAX_WEDGE_ABSORB_TOKENS,
|
|
@@ -41,21 +46,23 @@ import type {
|
|
|
41
46
|
ActiveContextSnapshot,
|
|
42
47
|
ActiveContextState,
|
|
43
48
|
FoldCandidate,
|
|
49
|
+
FoldPart,
|
|
44
50
|
MarkOrigin,
|
|
45
51
|
PendingFoldMark,
|
|
46
52
|
PendingMark,
|
|
47
53
|
} from "./policy.ts";
|
|
48
54
|
import {
|
|
55
|
+
automaticToolBatches,
|
|
49
56
|
automaticToolBrief,
|
|
50
57
|
candidateSourceRefs,
|
|
51
58
|
chapterRangeIsUnitAligned,
|
|
52
59
|
partsForRange,
|
|
60
|
+
peekedSourceFoldIds,
|
|
61
|
+
selectAutomaticConsolidations,
|
|
53
62
|
spanBytes,
|
|
54
63
|
deterministicChapterCandidateBrief,
|
|
55
64
|
deterministicConsolidationBrief,
|
|
56
65
|
resultCall,
|
|
57
|
-
selectAutomaticToolBatch,
|
|
58
|
-
toolCallArguments,
|
|
59
66
|
} from "./selection.ts";
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -72,8 +79,26 @@ import {
|
|
|
72
79
|
* wall clock and never randomness.
|
|
73
80
|
*/
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
/** The empty read, shared and frozen, so a state with no marks allocates nothing. */
|
|
83
|
+
const NO_PENDING_MARKS: readonly PendingMark[] = Object.freeze([]);
|
|
84
|
+
|
|
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
|
+
export function pendingMarks(state: Pick<ActiveContextState, "pendingMarks">): readonly PendingMark[] {
|
|
101
|
+
return state.pendingMarks ?? NO_PENDING_MARKS;
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
/**
|
|
@@ -131,6 +156,80 @@ export function markOrdinal(snapshot: Pick<ActiveContextSnapshot, "mapped">): nu
|
|
|
131
156
|
return snapshot.mapped.length;
|
|
132
157
|
}
|
|
133
158
|
|
|
159
|
+
export interface MarkSpanRefs {
|
|
160
|
+
/** Every evidence ref the mark's parts resolve to against the state in hand. */
|
|
161
|
+
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
|
+
unresolved: { foldId: string; pending: boolean } | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
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
|
+
export function markSpanRefs(state: ActiveContextState, mark: PendingMark): MarkSpanRefs {
|
|
192
|
+
const byId = new Map(state.folds.map((fold) => [fold.id, fold] as const));
|
|
193
|
+
const marks = pendingMarks(state);
|
|
194
|
+
const refs: EvidenceRef[] = [];
|
|
195
|
+
let unresolved: MarkSpanRefs["unresolved"] = null;
|
|
196
|
+
const path = new Set<string>();
|
|
197
|
+
const miss = (foldId: string, pending: boolean): void => {
|
|
198
|
+
if (!unresolved) unresolved = { foldId, pending };
|
|
199
|
+
};
|
|
200
|
+
const collect = (parts: readonly FoldPart[]): void => {
|
|
201
|
+
for (const part of parts) {
|
|
202
|
+
if (part.kind === "raw") {
|
|
203
|
+
refs.push(part.ref);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const child = byId.get(part.foldId);
|
|
207
|
+
if (!child) {
|
|
208
|
+
miss(part.foldId, marks.some((item) => item.mark === "fold" && item.id === part.foldId));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (path.has(child.id)) {
|
|
212
|
+
miss(child.id, false);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
path.add(child.id);
|
|
216
|
+
collect(child.parts);
|
|
217
|
+
path.delete(child.id);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
if (mark.mark === "refold") {
|
|
221
|
+
const fold = byId.get(mark.id);
|
|
222
|
+
if (!fold) {
|
|
223
|
+
miss(mark.id, marks.some((item) => item.mark === "fold" && item.id === mark.id));
|
|
224
|
+
} else {
|
|
225
|
+
path.add(fold.id);
|
|
226
|
+
collect(fold.parts);
|
|
227
|
+
path.delete(fold.id);
|
|
228
|
+
}
|
|
229
|
+
} else collect(mark.parts);
|
|
230
|
+
return { refs, unresolved };
|
|
231
|
+
}
|
|
232
|
+
|
|
134
233
|
/**
|
|
135
234
|
* Estimated bytes a mark would remove from the projection. Fold marks use a
|
|
136
235
|
* placeholder estimate rather than a trial fold: the number is presentational, so
|
|
@@ -158,20 +257,55 @@ export function markFreedBytes(
|
|
|
158
257
|
return Math.max(0, bytes(source) - placeholder);
|
|
159
258
|
}
|
|
160
259
|
|
|
161
|
-
/**
|
|
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
|
+
*/
|
|
162
268
|
export function markTouchesCurrentTurn(
|
|
163
269
|
state: ActiveContextState,
|
|
164
270
|
mark: PendingMark,
|
|
165
271
|
currentTurn: ReadonlySet<string>,
|
|
166
272
|
): boolean {
|
|
167
273
|
if (!currentTurn.size) return false;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
export function markSpanStart(
|
|
296
|
+
snapshot: ActiveContextSnapshot,
|
|
297
|
+
state: ActiveContextState,
|
|
298
|
+
mark: PendingMark,
|
|
299
|
+
): number {
|
|
300
|
+
const span = markSpanRefs(state, mark);
|
|
301
|
+
if (span.unresolved) return snapshot.mapped.length;
|
|
302
|
+
const refs = span.refs;
|
|
303
|
+
const indexed = mappedByKey(snapshot);
|
|
304
|
+
const indices = refs.flatMap((ref) => {
|
|
305
|
+
const item = indexed.get(objectRefKey(ref));
|
|
306
|
+
return item ? [item.index] : [];
|
|
307
|
+
});
|
|
308
|
+
return indices.length ? Math.min(...indices) : snapshot.mapped.length;
|
|
175
309
|
}
|
|
176
310
|
|
|
177
311
|
/**
|
|
@@ -218,7 +352,8 @@ export function guardWaiverCount(input: {
|
|
|
218
352
|
const { snapshot, ratio, guardedMarks, otherApplicableMarks } = input;
|
|
219
353
|
if (guardedMarks <= 0 || typeof ratio !== "number" || !Number.isFinite(ratio)) return 0;
|
|
220
354
|
if (ratio >= hardFenceRatio(snapshot)) return guardedMarks;
|
|
221
|
-
|
|
355
|
+
const occupancy = budgetOccupancy(snapshot, ratio);
|
|
356
|
+
if (occupancy === null || occupancy < snapshot.policy.refoldRatio || otherApplicableMarks > 0) return 0;
|
|
222
357
|
if (guardedMarks < GUARD_WAIVER_MINIMUM_MARKS) return 0;
|
|
223
358
|
return Math.max(1, guardedMarks - GUARD_WAIVER_PROTECTED_MARKS);
|
|
224
359
|
}
|
|
@@ -232,6 +367,11 @@ export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
|
|
|
232
367
|
* protected it, and both conditions expire. "unfulfillable" is terminal: the evidence
|
|
233
368
|
* or the fold the mark names has left the branch, so no later commit can honour it.
|
|
234
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).
|
|
235
375
|
*/
|
|
236
376
|
export function markEligibility(
|
|
237
377
|
snapshot: ActiveContextSnapshot,
|
|
@@ -243,7 +383,9 @@ export function markEligibility(
|
|
|
243
383
|
if (!fold || !foldInterval(fold, state, snapshot)) return "unfulfillable";
|
|
244
384
|
return state.expanded.includes(mark.id) ? "eligible" : "unfulfillable";
|
|
245
385
|
}
|
|
246
|
-
const
|
|
386
|
+
const span = markSpanRefs(state, mark);
|
|
387
|
+
if (span.unresolved) return span.unresolved.pending ? "protected" : "unfulfillable";
|
|
388
|
+
const refs = span.refs;
|
|
247
389
|
if (!refs.length) return "unfulfillable";
|
|
248
390
|
const mapped = new Set(snapshot.mapped.flatMap((item) => item.ref ? [objectRefKey(item.ref)] : []));
|
|
249
391
|
if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
|
|
@@ -259,7 +401,7 @@ export interface MarkAccounting {
|
|
|
259
401
|
ladderMarks: number;
|
|
260
402
|
freedBytes: number;
|
|
261
403
|
freedTokens: number;
|
|
262
|
-
|
|
404
|
+
freedBudgetShare: number;
|
|
263
405
|
rewriteTokens: number;
|
|
264
406
|
/** Marks a commit could apply right now. */
|
|
265
407
|
eligibleMarks: number;
|
|
@@ -268,7 +410,7 @@ export interface MarkAccounting {
|
|
|
268
410
|
eligibleFreedBytes: number;
|
|
269
411
|
eligibleFreedTokens: number;
|
|
270
412
|
/** The ROI signal: eligible marked mass as a share of the truthful window. */
|
|
271
|
-
|
|
413
|
+
eligibleFreedBudgetShare: number;
|
|
272
414
|
/** Raw peek mass no reclamation can take: pinned reads and protected results. */
|
|
273
415
|
pinnedBytes: number;
|
|
274
416
|
/** How many peek results that mass is spread over. */
|
|
@@ -300,11 +442,7 @@ export function markAccounting(
|
|
|
300
442
|
eligibleMarks += 1;
|
|
301
443
|
eligibleFreedBytes += freed;
|
|
302
444
|
} else if (eligibility === "protected") retainedMarks += 1;
|
|
303
|
-
const
|
|
304
|
-
const refs = mark.mark === "refold"
|
|
305
|
-
? (fold ? flattenFoldRefs(fold, state) : [])
|
|
306
|
-
: candidateSourceRefs(mark.parts, state);
|
|
307
|
-
for (const ref of refs) {
|
|
445
|
+
for (const ref of markSpanRefs(state, mark).refs) {
|
|
308
446
|
const index = indexByKey.get(objectRefKey(ref));
|
|
309
447
|
if (index === undefined) continue;
|
|
310
448
|
if (earliest < 0 || index < earliest) earliest = index;
|
|
@@ -322,14 +460,14 @@ export function markAccounting(
|
|
|
322
460
|
ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
|
|
323
461
|
freedBytes,
|
|
324
462
|
freedTokens,
|
|
325
|
-
|
|
463
|
+
freedBudgetShare: snapshot.budgetTokens > 0 ? freedTokens / snapshot.budgetTokens : 0,
|
|
326
464
|
rewriteTokens: estimatedTokens(rewriteBytes),
|
|
327
465
|
eligibleMarks,
|
|
328
466
|
retainedMarks,
|
|
329
467
|
eligibleFreedBytes,
|
|
330
468
|
eligibleFreedTokens,
|
|
331
|
-
|
|
332
|
-
? eligibleFreedTokens / snapshot.
|
|
469
|
+
eligibleFreedBudgetShare: snapshot.budgetTokens > 0
|
|
470
|
+
? eligibleFreedTokens / snapshot.budgetTokens
|
|
333
471
|
: 0,
|
|
334
472
|
pinnedBytes: pinned.bytes,
|
|
335
473
|
pinnedResults: pinned.results,
|
|
@@ -337,23 +475,24 @@ export function markAccounting(
|
|
|
337
475
|
}
|
|
338
476
|
|
|
339
477
|
/**
|
|
340
|
-
*
|
|
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.
|
|
341
483
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
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.
|
|
348
492
|
*/
|
|
349
|
-
export function epochCommitDue(
|
|
350
|
-
snapshot
|
|
351
|
-
|
|
352
|
-
eligibleShare: number | null = null,
|
|
353
|
-
): boolean {
|
|
354
|
-
if (typeof eligibleShare === "number" && Number.isFinite(eligibleShare) &&
|
|
355
|
-
eligibleShare >= EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD) return true;
|
|
356
|
-
return typeof ratio === "number" && Number.isFinite(ratio) && ratio >= snapshot.policy.refoldRatio;
|
|
493
|
+
export function epochCommitDue(snapshot: ActiveContextSnapshot, ratio: number | null): boolean {
|
|
494
|
+
const occupancy = budgetOccupancy(snapshot, ratio);
|
|
495
|
+
return occupancy !== null && occupancy >= snapshot.thresholds.maxTarget;
|
|
357
496
|
}
|
|
358
497
|
|
|
359
498
|
export function foldMarkFor(input: {
|
|
@@ -389,6 +528,26 @@ export function ladderBrief(
|
|
|
389
528
|
return deterministicChapterCandidateBrief(snapshot, candidate);
|
|
390
529
|
}
|
|
391
530
|
|
|
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
|
+
export function automaticMarkBrief(
|
|
540
|
+
snapshot: ActiveContextSnapshot,
|
|
541
|
+
state: ActiveContextState,
|
|
542
|
+
candidate: FoldCandidate,
|
|
543
|
+
): { brief: string; briefProvenance: PendingFoldMark["briefProvenance"] } {
|
|
544
|
+
const prepared = state.prepared;
|
|
545
|
+
if (prepared && preparedMatchesCandidate(prepared, candidate)) {
|
|
546
|
+
return { brief: prepared.fold.brief, briefProvenance: clone(prepared.fold.provenance) };
|
|
547
|
+
}
|
|
548
|
+
return { brief: ladderBrief(snapshot, state, candidate), briefProvenance: { kind: "deterministic" } };
|
|
549
|
+
}
|
|
550
|
+
|
|
392
551
|
/**
|
|
393
552
|
* Turn one automatic rung selection into a mark. Prepared chapters are excluded on
|
|
394
553
|
* purpose: that rung only fires at the hard provider fence, which is already inside
|
|
@@ -409,17 +568,29 @@ export function ladderSelectionMark(input: {
|
|
|
409
568
|
}
|
|
410
569
|
return foldMarkFor({
|
|
411
570
|
candidate: selection.candidate,
|
|
412
|
-
|
|
413
|
-
briefProvenance: { kind: "deterministic" },
|
|
571
|
+
...automaticMarkBrief(input.snapshot, input.state, selection.candidate),
|
|
414
572
|
origin: "ladder",
|
|
415
573
|
ordinal: input.ordinal,
|
|
416
574
|
});
|
|
417
575
|
}
|
|
418
576
|
|
|
419
577
|
/**
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
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.
|
|
423
594
|
*/
|
|
424
595
|
export function ephemeralPeekMarks(input: {
|
|
425
596
|
snapshot: ActiveContextSnapshot;
|
|
@@ -443,25 +614,13 @@ export function ephemeralPeekMarks(input: {
|
|
|
443
614
|
const expected = entries[0].call.batch;
|
|
444
615
|
const ids = new Set(entries.map(({ call }) => call.id));
|
|
445
616
|
if (ids.size !== expected.length || expected.some((id) => !ids.has(id))) continue;
|
|
446
|
-
const peeked: string[] = [];
|
|
447
|
-
let everyCallIsPeek = true;
|
|
448
|
-
for (const { call } of entries) {
|
|
449
|
-
const args = toolCallArguments(snapshot, call.assistantIndex, call.id);
|
|
450
|
-
const action = args && typeof args === "object" && !Array.isArray(args)
|
|
451
|
-
? (args as Record<string, unknown>).action
|
|
452
|
-
: undefined;
|
|
453
|
-
const id = args && typeof args === "object" && !Array.isArray(args)
|
|
454
|
-
? (args as Record<string, unknown>).id
|
|
455
|
-
: undefined;
|
|
456
|
-
if (action !== "peek" || typeof id !== "string" || !id) { everyCallIsPeek = false; break; }
|
|
457
|
-
peeked.push(id);
|
|
458
|
-
}
|
|
459
|
-
if (!everyCallIsPeek) continue;
|
|
460
|
-
// The agent committed to what it peeked: leave the read in the raw window.
|
|
461
|
-
if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
|
|
462
617
|
const refs = entries
|
|
463
618
|
.sort((left, right) => left.index - right.index)
|
|
464
619
|
.map(({ index }) => snapshot.mapped[index].ref!);
|
|
620
|
+
const peeked = peekedSourceFoldIds(snapshot, refs);
|
|
621
|
+
if (!peeked) continue;
|
|
622
|
+
// The agent committed to what it peeked: leave the read in the raw window.
|
|
623
|
+
if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
|
|
465
624
|
if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
|
|
466
625
|
refs.length > snapshot.policy.maxFoldSourceRefs ||
|
|
467
626
|
toolRefsProtected(refs, state, snapshot)) continue;
|
|
@@ -504,17 +663,17 @@ export function claimedRefKeys(state: ActiveContextState): Set<string> {
|
|
|
504
663
|
}
|
|
505
664
|
for (const mark of pendingMarks(state)) {
|
|
506
665
|
if (mark.mark !== "fold") continue;
|
|
507
|
-
for (const ref of
|
|
666
|
+
for (const ref of markSpanRefs(state, mark).refs) keys.add(objectRefKey(ref));
|
|
508
667
|
}
|
|
509
668
|
return keys;
|
|
510
669
|
}
|
|
511
670
|
|
|
512
671
|
/** What is still on the table after the marks in hand: the steering number and its parts. */
|
|
513
672
|
export interface UnmarkedRemainder {
|
|
514
|
-
/**
|
|
673
|
+
/** Tool results in a MEMBER batch that no fold and no pending mark owns. */
|
|
515
674
|
spans: number;
|
|
516
675
|
tokens: number;
|
|
517
|
-
/** Unmarked
|
|
676
|
+
/** Unmarked member tokens as a share of the member tool mass. The steering number. */
|
|
518
677
|
share: number;
|
|
519
678
|
/** The largest few, by reclaim value, so the next batch is one read away. */
|
|
520
679
|
candidates: Array<{ id: string; tokens: number }>;
|
|
@@ -526,6 +685,17 @@ export interface UnmarkedRemainder {
|
|
|
526
685
|
* An exhaustive list is what the status index already is, and re-rendering it is what
|
|
527
686
|
* this build removed from the projection. What an agent needs to decide the NEXT batch
|
|
528
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.
|
|
529
699
|
*/
|
|
530
700
|
export function unmarkedRemainder(
|
|
531
701
|
snapshot: ActiveContextSnapshot,
|
|
@@ -535,14 +705,17 @@ export function unmarkedRemainder(
|
|
|
535
705
|
): UnmarkedRemainder {
|
|
536
706
|
const perToken = Number.isFinite(charsPerToken) && charsPerToken > 0 ? charsPerToken : ESTIMATED_BYTES_PER_TOKEN;
|
|
537
707
|
const claimed = claimedRefKeys(state);
|
|
708
|
+
const members = new Set<number>();
|
|
709
|
+
for (const batch of automaticToolBatches(snapshot, state)) {
|
|
710
|
+
for (const index of batch.indices) members.add(index);
|
|
711
|
+
}
|
|
538
712
|
const candidates: Array<{ id: string; tokens: number }> = [];
|
|
539
713
|
let unmarkedBytes = 0;
|
|
540
|
-
let
|
|
714
|
+
let memberBytes = 0;
|
|
541
715
|
for (const item of snapshot.mapped) {
|
|
542
|
-
if (!item.ref ||
|
|
543
|
-
if (snapshot.toolProtectedIndices.has(item.index)) continue;
|
|
716
|
+
if (!item.ref || !members.has(item.index)) continue;
|
|
544
717
|
const size = bytes(item.message);
|
|
545
|
-
|
|
718
|
+
memberBytes += size;
|
|
546
719
|
if (claimed.has(objectRefKey(item.ref))) continue;
|
|
547
720
|
unmarkedBytes += size;
|
|
548
721
|
candidates.push({ id: item.ref.entryId, tokens: Math.ceil(size / perToken) });
|
|
@@ -551,7 +724,7 @@ export function unmarkedRemainder(
|
|
|
551
724
|
return {
|
|
552
725
|
spans: candidates.length,
|
|
553
726
|
tokens: Math.ceil(unmarkedBytes / perToken),
|
|
554
|
-
share:
|
|
727
|
+
share: memberBytes > 0 ? unmarkedBytes / memberBytes : 0,
|
|
555
728
|
candidates: candidates.slice(0, Math.max(0, limit)),
|
|
556
729
|
};
|
|
557
730
|
}
|
|
@@ -564,38 +737,64 @@ export function markedFoldIds(state: ActiveContextState): Set<string> {
|
|
|
564
737
|
}
|
|
565
738
|
|
|
566
739
|
/**
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
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
|
+
export function consolidationMarks(input: {
|
|
751
|
+
snapshot: ActiveContextSnapshot;
|
|
752
|
+
state: ActiveContextState;
|
|
753
|
+
ordinal: number;
|
|
754
|
+
}): PendingFoldMark[] {
|
|
755
|
+
return selectAutomaticConsolidations(input.snapshot, input.state).map((candidate) => foldMarkFor({
|
|
756
|
+
candidate,
|
|
757
|
+
...automaticMarkBrief(input.snapshot, input.state, candidate),
|
|
758
|
+
origin: "ladder",
|
|
759
|
+
ordinal: input.ordinal,
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
|
|
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.
|
|
570
769
|
*/
|
|
571
770
|
export function topUpMarks(input: {
|
|
572
771
|
snapshot: ActiveContextSnapshot;
|
|
573
772
|
state: ActiveContextState;
|
|
574
773
|
ordinal: number;
|
|
575
|
-
|
|
774
|
+
/** Freeing target as a share of the SERVING BUDGET. Required: there is no second default. */
|
|
775
|
+
targetShare: number;
|
|
576
776
|
/** Evidence keys the top-up may never propose, e.g. the current excursion's reads. */
|
|
577
777
|
excludeRefKeys?: ReadonlySet<string>;
|
|
578
778
|
/** Measure progress against ELIGIBLE mass, so pinned or protected marks never count. */
|
|
579
779
|
eligibleOnly?: boolean;
|
|
580
780
|
}): PendingFoldMark[] {
|
|
581
781
|
const { snapshot } = input;
|
|
582
|
-
const target = input.targetShare
|
|
782
|
+
const target = input.targetShare;
|
|
583
783
|
const claimed = claimedRefKeys(input.state);
|
|
584
784
|
for (const key of input.excludeRefKeys ?? []) claimed.add(key);
|
|
585
785
|
const marks: PendingFoldMark[] = [];
|
|
586
786
|
let state = input.state;
|
|
587
787
|
const progress = (value: ActiveContextState): number => {
|
|
588
788
|
const accounting = markAccounting(snapshot, value);
|
|
589
|
-
return input.eligibleOnly ? accounting.
|
|
789
|
+
return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
|
|
590
790
|
};
|
|
591
791
|
let share = progress(state);
|
|
592
792
|
for (let attempt = 0; attempt < EPOCH_MAX_TOPUP_MARKS && share < target; attempt += 1) {
|
|
593
|
-
const candidate =
|
|
793
|
+
const candidate = selectAutomaticSpan(snapshot, state, claimed);
|
|
594
794
|
if (!candidate) break;
|
|
595
795
|
const mark = foldMarkFor({
|
|
596
796
|
candidate,
|
|
597
|
-
|
|
598
|
-
briefProvenance: { kind: "deterministic" },
|
|
797
|
+
...automaticMarkBrief(snapshot, state, candidate),
|
|
599
798
|
origin: "ladder",
|
|
600
799
|
ordinal: input.ordinal,
|
|
601
800
|
});
|
|
@@ -613,6 +812,12 @@ export function topUpMarks(input: {
|
|
|
613
812
|
export interface AbsorbedWedge {
|
|
614
813
|
/** The mark that grew backward to cover it. */
|
|
615
814
|
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
|
+
fromMarkId: string;
|
|
616
821
|
startId: string;
|
|
617
822
|
endId: string;
|
|
618
823
|
entries: number;
|
|
@@ -642,10 +847,8 @@ export function absorbWedgeMarks(input: {
|
|
|
642
847
|
charsPerToken: number;
|
|
643
848
|
/** Evidence absorption may never touch, e.g. the current excursion's own reads. */
|
|
644
849
|
excludeRefKeys?: ReadonlySet<string>;
|
|
645
|
-
maxTokens?: number;
|
|
646
850
|
}): { state: ActiveContextState; absorbed: AbsorbedWedge[] } {
|
|
647
851
|
const { snapshot } = input;
|
|
648
|
-
const maxTokens = input.maxTokens ?? MAX_WEDGE_ABSORB_TOKENS;
|
|
649
852
|
const charsPerToken = Number.isFinite(input.charsPerToken) && input.charsPerToken > 0
|
|
650
853
|
? input.charsPerToken
|
|
651
854
|
: ESTIMATED_BYTES_PER_TOKEN;
|
|
@@ -655,7 +858,7 @@ export function absorbWedgeMarks(input: {
|
|
|
655
858
|
const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
|
|
656
859
|
item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
|
|
657
860
|
const markInterval = (mark: PendingFoldMark): { start: number; end: number } | null => {
|
|
658
|
-
const indices =
|
|
861
|
+
const indices = markSpanRefs(state, mark).refs
|
|
659
862
|
.map((ref) => indexByKey.get(objectRefKey(ref)))
|
|
660
863
|
.filter((index): index is number => index !== undefined);
|
|
661
864
|
return indices.length ? { start: Math.min(...indices), end: Math.max(...indices) } : null;
|
|
@@ -684,7 +887,7 @@ export function absorbWedgeMarks(input: {
|
|
|
684
887
|
if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
|
|
685
888
|
const gapTokens = Math.ceil(spanBytes(snapshot, gapStart, gapEnd + 1) / charsPerToken);
|
|
686
889
|
// The whole discriminator, in one line: above this, the gap is the agent's.
|
|
687
|
-
if (gapTokens >
|
|
890
|
+
if (gapTokens > MAX_WEDGE_ABSORB_TOKENS) continue;
|
|
688
891
|
const gapRefs = [];
|
|
689
892
|
let usable = true;
|
|
690
893
|
for (let index = gapStart; index <= gapEnd; index += 1) {
|
|
@@ -712,6 +915,7 @@ export function absorbWedgeMarks(input: {
|
|
|
712
915
|
state = withPendingMarks(state, [...kept, grown]);
|
|
713
916
|
absorbed.push({
|
|
714
917
|
intoMarkId: grown.id,
|
|
918
|
+
fromMarkId: mark.id,
|
|
715
919
|
startId: gapRefs[0].entryId,
|
|
716
920
|
endId: gapRefs.at(-1)!.entryId,
|
|
717
921
|
entries: gapRefs.length,
|
|
@@ -746,7 +950,7 @@ export interface CommitEpochResult {
|
|
|
746
950
|
applied: AppliedMark[];
|
|
747
951
|
refused: RefusedMark[];
|
|
748
952
|
retained: PendingMark[];
|
|
749
|
-
/** Guarded marks the pressure waiver released into this commit, oldest first. */
|
|
953
|
+
/** Guarded marks the pressure waiver released into this commit, oldest material first. */
|
|
750
954
|
waived: PendingMark[];
|
|
751
955
|
}
|
|
752
956
|
|
|
@@ -774,7 +978,7 @@ export async function commitPendingMarks(input: {
|
|
|
774
978
|
*/
|
|
775
979
|
guardCurrentTurn?: boolean;
|
|
776
980
|
/**
|
|
777
|
-
* How many guarded marks the pressure waiver releases anyway, oldest first. The
|
|
981
|
+
* How many guarded marks the pressure waiver releases anyway, oldest material first. The
|
|
778
982
|
* guard protects an in-flight excursion; it may never cost the session its ability
|
|
779
983
|
* to send a request at all. See `guardWaiverCount`.
|
|
780
984
|
*/
|
|
@@ -784,6 +988,17 @@ export async function commitPendingMarks(input: {
|
|
|
784
988
|
const retained: PendingMark[] = [];
|
|
785
989
|
const waived: PendingMark[] = [];
|
|
786
990
|
const guardReasons = new Map<string, string>();
|
|
991
|
+
const held = new Set(input.state.folds.map((fold) => fold.id));
|
|
992
|
+
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
|
+
const awaitingMintedChild = (mark: PendingMark): boolean =>
|
|
1001
|
+
markSpanRefs(input.state, mark).unresolved?.pending === true;
|
|
787
1002
|
const currentTurn = input.guardCurrentTurn
|
|
788
1003
|
? currentTurnRefKeys(input.snapshot)
|
|
789
1004
|
: new Set<string>();
|
|
@@ -794,9 +1009,16 @@ export async function commitPendingMarks(input: {
|
|
|
794
1009
|
if (markTouchesCurrentTurn(input.state, mark, currentTurn)) guarded.push(mark);
|
|
795
1010
|
else applicable.push(mark);
|
|
796
1011
|
}
|
|
797
|
-
// Oldest first: the newest reads are the ones the excursion is about to
|
|
798
|
-
// they are the last evidence the waiver surrenders.
|
|
799
|
-
|
|
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));
|
|
800
1022
|
const waiverCount = Math.max(0, Math.min(guarded.length, Math.trunc(input.guardWaiver ?? 0)));
|
|
801
1023
|
for (const [index, mark] of guarded.entries()) {
|
|
802
1024
|
if (index < waiverCount) {
|
|
@@ -816,7 +1038,12 @@ export async function commitPendingMarks(input: {
|
|
|
816
1038
|
if (input.retainIneligible) {
|
|
817
1039
|
const applicable: PendingMark[] = [];
|
|
818
1040
|
for (const mark of marks) {
|
|
819
|
-
|
|
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
|
+
if (!awaitingMintedChild(mark) &&
|
|
1046
|
+
markEligibility(input.snapshot, input.state, mark) === "protected") retained.push(mark);
|
|
820
1047
|
else applicable.push(mark);
|
|
821
1048
|
}
|
|
822
1049
|
marks.length = 0;
|
|
@@ -832,10 +1059,51 @@ export async function commitPendingMarks(input: {
|
|
|
832
1059
|
"span is still fresh or protected; the mark stays pending until it is eligible",
|
|
833
1060
|
retained: true,
|
|
834
1061
|
}));
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
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
|
+
const minting = new Map(marks.flatMap((mark) =>
|
|
1080
|
+
mark.mark === "fold" && !held.has(mark.id) ? [[mark.id, mark] as const] : []));
|
|
1081
|
+
const applyStart = new Map(marks.map((mark) =>
|
|
1082
|
+
[pendingMarkKey(mark), markSpanStart(input.snapshot, input.state, mark)] as const));
|
|
1083
|
+
const base = [...marks].sort((left, right) =>
|
|
1084
|
+
applyStart.get(pendingMarkKey(left))! - applyStart.get(pendingMarkKey(right))! ||
|
|
1085
|
+
left.id.localeCompare(right.id));
|
|
1086
|
+
const ordered: PendingMark[] = [];
|
|
1087
|
+
const placed = new Set<string>();
|
|
1088
|
+
const placing = new Set<string>();
|
|
1089
|
+
const place = (mark: PendingMark): void => {
|
|
1090
|
+
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
|
+
if (placed.has(key) || placing.has(key)) return;
|
|
1095
|
+
placing.add(key);
|
|
1096
|
+
if (mark.mark === "fold") {
|
|
1097
|
+
for (const childId of childFoldIds(mark)) {
|
|
1098
|
+
const child = minting.get(childId);
|
|
1099
|
+
if (child) place(child);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
placing.delete(key);
|
|
1103
|
+
placed.add(key);
|
|
1104
|
+
ordered.push(mark);
|
|
1105
|
+
};
|
|
1106
|
+
for (const mark of base) place(mark);
|
|
839
1107
|
for (const mark of ordered) {
|
|
840
1108
|
try {
|
|
841
1109
|
if (mark.mark === "refold") {
|
|
@@ -849,7 +1117,33 @@ export async function commitPendingMarks(input: {
|
|
|
849
1117
|
continue;
|
|
850
1118
|
}
|
|
851
1119
|
const parts = clone(mark.parts);
|
|
852
|
-
|
|
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
|
+
const span = markSpanRefs(state, mark);
|
|
1130
|
+
if (span.unresolved) {
|
|
1131
|
+
const { foldId, pending } = span.unresolved;
|
|
1132
|
+
if (pending) deferred.push(mark);
|
|
1133
|
+
refused.push({
|
|
1134
|
+
mark: mark.mark,
|
|
1135
|
+
id: mark.id,
|
|
1136
|
+
origin: mark.origin,
|
|
1137
|
+
reason: pending
|
|
1138
|
+
? `pending fold mark ${mark.id} names fold ${foldId}, which a still-pending mark ` +
|
|
1139
|
+
"has not minted yet; the mark stays pending until that fold lands"
|
|
1140
|
+
: `pending fold mark ${mark.id} names fold ${foldId}, which the context no longer ` +
|
|
1141
|
+
"holds and no pending mark will mint; the decision cannot be honoured",
|
|
1142
|
+
retained: pending,
|
|
1143
|
+
});
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
const sourceRefs = span.refs;
|
|
853
1147
|
const blocked = mark.kind === "tool-result"
|
|
854
1148
|
? toolRefsProtected(sourceRefs, state, input.snapshot)
|
|
855
1149
|
: refsProtected(sourceRefs, state, input.snapshot);
|
|
@@ -875,6 +1169,12 @@ export async function commitPendingMarks(input: {
|
|
|
875
1169
|
generation: input.generation,
|
|
876
1170
|
});
|
|
877
1171
|
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
|
+
if (state.prepared && state.folds.some((fold) => fold.id === state.prepared!.id)) {
|
|
1176
|
+
state = clearPrepared(state);
|
|
1177
|
+
}
|
|
878
1178
|
} catch (error) {
|
|
879
1179
|
refused.push({
|
|
880
1180
|
mark: mark.mark,
|
|
@@ -885,6 +1185,13 @@ export async function commitPendingMarks(input: {
|
|
|
885
1185
|
});
|
|
886
1186
|
}
|
|
887
1187
|
}
|
|
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
|
+
if (deferred.length) {
|
|
1192
|
+
state = withPendingMarks(state, [...pendingMarks(state), ...deferred]);
|
|
1193
|
+
retained.push(...deferred);
|
|
1194
|
+
}
|
|
888
1195
|
return { state, applied, refused, retained, waived };
|
|
889
1196
|
}
|
|
890
1197
|
|
|
@@ -892,25 +1199,26 @@ export async function commitPendingMarks(input: {
|
|
|
892
1199
|
export function schedulingStatus(input: {
|
|
893
1200
|
snapshot: ActiveContextSnapshot;
|
|
894
1201
|
state: ActiveContextState;
|
|
895
|
-
mode: string;
|
|
896
1202
|
ratio: number | null;
|
|
897
1203
|
}): Record<string, unknown> {
|
|
898
1204
|
const accounting = markAccounting(input.snapshot, input.state);
|
|
899
|
-
const threshold = EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD;
|
|
900
1205
|
return {
|
|
901
|
-
|
|
1206
|
+
// A literal, not a setting: epoch is the only scheduler there is.
|
|
1207
|
+
mode: "epoch",
|
|
902
1208
|
...accounting,
|
|
903
|
-
|
|
904
|
-
|
|
1209
|
+
// The declared policy, reported never mutable.
|
|
1210
|
+
thresholds: { ...input.snapshot.thresholds },
|
|
1211
|
+
budgetTokens: input.snapshot.budgetTokens,
|
|
1212
|
+
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.
|
|
905
1216
|
commitTrigger: {
|
|
906
|
-
mode: "
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
pressureDue: epochCommitDue(input.snapshot, input.ratio),
|
|
911
|
-
roiDue: accounting.eligibleFreedWindowShare >= threshold,
|
|
1217
|
+
mode: "band-top",
|
|
1218
|
+
commitOccupancy: input.snapshot.thresholds.maxTarget,
|
|
1219
|
+
occupancy: budgetOccupancy(input.snapshot, input.ratio),
|
|
1220
|
+
due: epochCommitDue(input.snapshot, input.ratio),
|
|
912
1221
|
},
|
|
913
|
-
commitRatio: input.snapshot.policy.refoldRatio,
|
|
914
1222
|
marks: pendingMarks(input.state).map((mark) => ({
|
|
915
1223
|
mark: mark.mark,
|
|
916
1224
|
id: mark.id,
|