pi-fold 2.0.0 → 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 +49 -28
- package/extensions/active-context.ts +1011 -444
- package/extensions/index.js +16 -3
- package/extensions/lib/curation.ts +56 -60
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +26 -42
- package/extensions/lib/instrumentation.ts +10 -3
- package/extensions/lib/measurement.ts +6 -12
- package/extensions/lib/persistence.ts +62 -38
- package/extensions/lib/policy.ts +171 -89
- package/extensions/lib/scheduling.ts +341 -108
- package/extensions/lib/selection.ts +6 -78
- package/extensions/lib/tool-surface.ts +29 -33
- package/extensions/lib/transcript.ts +17 -60
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
|
@@ -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,11 +33,10 @@ import {
|
|
|
33
33
|
parsePendingMarks,
|
|
34
34
|
pendingMarkKey,
|
|
35
35
|
} from "./persistence.ts";
|
|
36
|
-
import { currentTurnRefKeys } from "./transcript.ts";
|
|
37
36
|
import {
|
|
38
37
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
39
|
-
MAX_WEDGE_ABSORB_TOKENS,
|
|
40
38
|
ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES,
|
|
39
|
+
MAX_FRONTIER_CUTS_PER_PASS,
|
|
41
40
|
MAX_PENDING_MARKS,
|
|
42
41
|
MAX_UNMARKED_CANDIDATES,
|
|
43
42
|
} from "./policy.ts";
|
|
@@ -58,6 +57,7 @@ import {
|
|
|
58
57
|
partsForRange,
|
|
59
58
|
peekedSourceFoldIds,
|
|
60
59
|
selectAutomaticConsolidations,
|
|
60
|
+
selectAutomaticRefold,
|
|
61
61
|
spanBytes,
|
|
62
62
|
deterministicChapterCandidateBrief,
|
|
63
63
|
deterministicConsolidationBrief,
|
|
@@ -185,15 +185,6 @@ export function markFreedBytes(
|
|
|
185
185
|
return Math.max(0, bytes(source) - placeholder);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
export function markTouchesCurrentTurn(
|
|
189
|
-
state: ActiveContextState,
|
|
190
|
-
mark: PendingMark,
|
|
191
|
-
currentTurn: ReadonlySet<string>,
|
|
192
|
-
): boolean {
|
|
193
|
-
if (!currentTurn.size) return false;
|
|
194
|
-
return markSpanRefs(state, mark).refs.some((ref) => currentTurn.has(objectRefKey(ref)));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
188
|
export function markSpanStart(
|
|
198
189
|
snapshot: ActiveContextSnapshot,
|
|
199
190
|
state: ActiveContextState,
|
|
@@ -210,46 +201,6 @@ export function markSpanStart(
|
|
|
210
201
|
return indices.length ? Math.min(...indices) : snapshot.mapped.length;
|
|
211
202
|
}
|
|
212
203
|
|
|
213
|
-
export const GUARD_WAIVER_PROTECTED_MARKS = 2;
|
|
214
|
-
|
|
215
|
-
export const GUARD_WAIVER_MINIMUM_MARKS = 4;
|
|
216
|
-
|
|
217
|
-
export function guardWaiverCount(input: {
|
|
218
|
-
snapshot: ActiveContextSnapshot;
|
|
219
|
-
ratio: number | null;
|
|
220
|
-
guardedMarks: number;
|
|
221
|
-
otherApplicableMarks: number;
|
|
222
|
-
/**
|
|
223
|
-
* THE BOUNDARY IS ITS OWN SIGNAL, and a better one than the occupancy anchor.
|
|
224
|
-
*
|
|
225
|
-
* The anchor exists to answer "is this session in enough trouble to spend the open
|
|
226
|
-
* turn's own evidence": a proxy, because nothing else was asking. At a compaction
|
|
227
|
-
* boundary Pi has already answered it. Cancelling the compaction and then applying
|
|
228
|
-
* nothing hands Pi's own threshold a byte-identical projection, which is worse than
|
|
229
|
-
* letting it compact, because compaction would at least have shed something.
|
|
230
|
-
*
|
|
231
|
-
* Measured on the sol-20260813 smoke: the boundary fired, cancelled the compaction and
|
|
232
|
-
* retained all seven marks at 0.20 occupancy, freeing nothing. The full run reaches the
|
|
233
|
-
* anchor 12,576 tokens past its own trigger, so it self-corrects there and this is
|
|
234
|
-
* about what happens in between; a smoke, or any session whose budget it never
|
|
235
|
-
* approaches, never corrects at all.
|
|
236
|
-
*
|
|
237
|
-
* Everything else about the waiver is unchanged, and deliberately: the newest marks
|
|
238
|
-
* still stay raw, a small guarded set still holds whole, and other applicable work
|
|
239
|
-
* still takes precedence over spending the open turn.
|
|
240
|
-
*/
|
|
241
|
-
boundary?: boolean;
|
|
242
|
-
}): number {
|
|
243
|
-
const { snapshot, ratio, guardedMarks, otherApplicableMarks, boundary } = input;
|
|
244
|
-
if (guardedMarks <= 0 || typeof ratio !== "number" || !Number.isFinite(ratio)) return 0;
|
|
245
|
-
if (ratio >= hardFenceRatio(snapshot)) return guardedMarks;
|
|
246
|
-
const occupancy = budgetOccupancy(snapshot, ratio);
|
|
247
|
-
if (occupancy === null || otherApplicableMarks > 0) return 0;
|
|
248
|
-
if (occupancy < snapshot.policy.refoldRatio && !boundary) return 0;
|
|
249
|
-
if (guardedMarks < GUARD_WAIVER_MINIMUM_MARKS) return 0;
|
|
250
|
-
return Math.max(1, guardedMarks - GUARD_WAIVER_PROTECTED_MARKS);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
204
|
export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
|
|
254
205
|
|
|
255
206
|
export function markEligibility(
|
|
@@ -268,9 +219,7 @@ export function markEligibility(
|
|
|
268
219
|
if (!refs.length) return "unfulfillable";
|
|
269
220
|
const mapped = new Set(snapshot.mapped.flatMap((item) => item.ref ? [objectRefKey(item.ref)] : []));
|
|
270
221
|
if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
|
|
271
|
-
const blocked =
|
|
272
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
273
|
-
: refsProtected(refs, state, snapshot);
|
|
222
|
+
const blocked = refsProtected(refs, state, snapshot);
|
|
274
223
|
return blocked ? "protected" : "eligible";
|
|
275
224
|
}
|
|
276
225
|
|
|
@@ -278,6 +227,7 @@ export interface MarkAccounting {
|
|
|
278
227
|
pending: number;
|
|
279
228
|
agentMarks: number;
|
|
280
229
|
ladderMarks: number;
|
|
230
|
+
userMarks: number;
|
|
281
231
|
freedBytes: number;
|
|
282
232
|
freedTokens: number;
|
|
283
233
|
freedBudgetShare: number;
|
|
@@ -327,6 +277,7 @@ export function markAccounting(
|
|
|
327
277
|
pending: marks.length,
|
|
328
278
|
agentMarks: marks.filter((mark) => mark.origin === "agent").length,
|
|
329
279
|
ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
|
|
280
|
+
userMarks: marks.filter((mark) => mark.origin === "user").length,
|
|
330
281
|
freedBytes,
|
|
331
282
|
freedTokens,
|
|
332
283
|
freedBudgetShare: snapshot.budgetTokens > 0 ? freedTokens / snapshot.budgetTokens : 0,
|
|
@@ -442,8 +393,7 @@ export function ephemeralPeekMarks(input: {
|
|
|
442
393
|
if (!peeked) continue;
|
|
443
394
|
if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
|
|
444
395
|
if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
|
|
445
|
-
refs
|
|
446
|
-
toolRefsProtected(refs, state, snapshot)) continue;
|
|
396
|
+
refsProtected(refs, state, snapshot)) continue;
|
|
447
397
|
const candidate: FoldCandidate = {
|
|
448
398
|
kind: "tool-result",
|
|
449
399
|
parts: refs.map((ref) => ({ kind: "raw" as const, ref })),
|
|
@@ -468,9 +418,7 @@ function peekedFoldProtected(
|
|
|
468
418
|
const fold = state.folds.find((item) => item.id === foldId);
|
|
469
419
|
if (!fold) return false;
|
|
470
420
|
const refs = flattenFoldRefs(fold, state);
|
|
471
|
-
return
|
|
472
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
473
|
-
: refsProtected(refs, state, snapshot);
|
|
421
|
+
return refsProtected(refs, state, snapshot);
|
|
474
422
|
}
|
|
475
423
|
|
|
476
424
|
export function claimedRefKeys(state: ActiveContextState): Set<string> {
|
|
@@ -488,6 +436,8 @@ export function claimedRefKeys(state: ActiveContextState): Set<string> {
|
|
|
488
436
|
export interface UnmarkedRemainder {
|
|
489
437
|
spans: number;
|
|
490
438
|
tokens: number;
|
|
439
|
+
/** Raw bytes, which is what the fold frontier measures itself against. */
|
|
440
|
+
chars: number;
|
|
491
441
|
share: number;
|
|
492
442
|
candidates: Array<{ id: string; tokens: number }>;
|
|
493
443
|
}
|
|
@@ -504,6 +454,33 @@ export function unmarkedRemainder(
|
|
|
504
454
|
for (const batch of automaticToolBatches(snapshot, state)) {
|
|
505
455
|
for (const index of batch.indices) members.add(index);
|
|
506
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
|
+
}
|
|
507
484
|
const candidates: Array<{ id: string; tokens: number }> = [];
|
|
508
485
|
let unmarkedBytes = 0;
|
|
509
486
|
let memberBytes = 0;
|
|
@@ -519,11 +496,193 @@ export function unmarkedRemainder(
|
|
|
519
496
|
return {
|
|
520
497
|
spans: candidates.length,
|
|
521
498
|
tokens: Math.ceil(unmarkedBytes / perToken),
|
|
499
|
+
chars: unmarkedBytes,
|
|
522
500
|
share: memberBytes > 0 ? unmarkedBytes / memberBytes : 0,
|
|
523
501
|
candidates: candidates.slice(0, Math.max(0, limit)),
|
|
524
502
|
};
|
|
525
503
|
}
|
|
526
504
|
|
|
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
|
+
|
|
527
686
|
export function markedFoldIds(state: ActiveContextState): Set<string> {
|
|
528
687
|
const ids = new Set<string>();
|
|
529
688
|
for (const mark of pendingMarks(state)) if (mark.mark === "refold") ids.add(mark.id);
|
|
@@ -548,13 +707,11 @@ export function topUpMarks(input: {
|
|
|
548
707
|
state: ActiveContextState;
|
|
549
708
|
ordinal: number;
|
|
550
709
|
targetShare: number;
|
|
551
|
-
excludeRefKeys?: ReadonlySet<string>;
|
|
552
710
|
eligibleOnly?: boolean;
|
|
553
711
|
}): PendingFoldMark[] {
|
|
554
712
|
const { snapshot } = input;
|
|
555
713
|
const target = input.targetShare;
|
|
556
714
|
const claimed = claimedRefKeys(input.state);
|
|
557
|
-
for (const key of input.excludeRefKeys ?? []) claimed.add(key);
|
|
558
715
|
const marks: PendingFoldMark[] = [];
|
|
559
716
|
let state = input.state;
|
|
560
717
|
const progress = (value: ActiveContextState): number => {
|
|
@@ -581,6 +738,50 @@ export function topUpMarks(input: {
|
|
|
581
738
|
return marks;
|
|
582
739
|
}
|
|
583
740
|
|
|
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
|
+
|
|
584
785
|
export interface AbsorbedWedge {
|
|
585
786
|
intoMarkId: string;
|
|
586
787
|
fromMarkId: string;
|
|
@@ -588,6 +789,7 @@ export interface AbsorbedWedge {
|
|
|
588
789
|
endId: string;
|
|
589
790
|
entries: number;
|
|
590
791
|
tokens: number;
|
|
792
|
+
chars: number;
|
|
591
793
|
}
|
|
592
794
|
|
|
593
795
|
export function absorbWedgeMarks(input: {
|
|
@@ -631,8 +833,16 @@ export function absorbWedgeMarks(input: {
|
|
|
631
833
|
const gapEnd = interval.start - 1;
|
|
632
834
|
if (gapStart < 0 || gapEnd < gapStart) continue;
|
|
633
835
|
if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
|
|
634
|
-
|
|
635
|
-
|
|
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);
|
|
636
846
|
const gapRefs = [];
|
|
637
847
|
let usable = true;
|
|
638
848
|
for (let index = gapStart; index <= gapEnd; index += 1) {
|
|
@@ -669,6 +879,7 @@ export function absorbWedgeMarks(input: {
|
|
|
669
879
|
endId: gapRefs.at(-1)!.entryId,
|
|
670
880
|
entries: gapRefs.length,
|
|
671
881
|
tokens: gapTokens,
|
|
882
|
+
chars: gapBytes,
|
|
672
883
|
});
|
|
673
884
|
applied = true;
|
|
674
885
|
break;
|
|
@@ -698,7 +909,6 @@ export interface CommitEpochResult {
|
|
|
698
909
|
applied: AppliedMark[];
|
|
699
910
|
refused: RefusedMark[];
|
|
700
911
|
retained: PendingMark[];
|
|
701
|
-
waived: PendingMark[];
|
|
702
912
|
}
|
|
703
913
|
|
|
704
914
|
export async function commitPendingMarks(input: {
|
|
@@ -707,48 +917,40 @@ export async function commitPendingMarks(input: {
|
|
|
707
917
|
generation: number;
|
|
708
918
|
now?: () => number;
|
|
709
919
|
retainIneligible?: boolean;
|
|
710
|
-
|
|
711
|
-
|
|
920
|
+
/**
|
|
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.
|
|
933
|
+
*/
|
|
934
|
+
applyTargetBytes?: number;
|
|
712
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.
|
|
713
948
|
const marks = pendingMarks(input.state);
|
|
714
949
|
const retained: PendingMark[] = [];
|
|
715
|
-
const waived: PendingMark[] = [];
|
|
716
|
-
const guardReasons = new Map<string, string>();
|
|
717
950
|
const held = new Set(input.state.folds.map((fold) => fold.id));
|
|
718
951
|
const deferred: PendingMark[] = [];
|
|
719
952
|
const awaitingMintedChild = (mark: PendingMark): boolean =>
|
|
720
953
|
markSpanRefs(input.state, mark).unresolved?.pending === true;
|
|
721
|
-
const currentTurn = input.guardCurrentTurn
|
|
722
|
-
? currentTurnRefKeys(input.snapshot)
|
|
723
|
-
: new Set<string>();
|
|
724
|
-
if (input.guardCurrentTurn) {
|
|
725
|
-
const applicable: PendingMark[] = [];
|
|
726
|
-
const guarded: PendingMark[] = [];
|
|
727
|
-
for (const mark of marks) {
|
|
728
|
-
if (markTouchesCurrentTurn(input.state, mark, currentTurn)) guarded.push(mark);
|
|
729
|
-
else applicable.push(mark);
|
|
730
|
-
}
|
|
731
|
-
const guardedStart = new Map(guarded.map((mark) =>
|
|
732
|
-
[pendingMarkKey(mark), markSpanStart(input.snapshot, input.state, mark)] as const));
|
|
733
|
-
guarded.sort((left, right) =>
|
|
734
|
-
guardedStart.get(pendingMarkKey(left))! - guardedStart.get(pendingMarkKey(right))! ||
|
|
735
|
-
left.id.localeCompare(right.id));
|
|
736
|
-
const waiverCount = Math.max(0, Math.min(guarded.length, Math.trunc(input.guardWaiver ?? 0)));
|
|
737
|
-
for (const [index, mark] of guarded.entries()) {
|
|
738
|
-
if (index < waiverCount) {
|
|
739
|
-
waived.push(mark);
|
|
740
|
-
applicable.push(mark);
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
retained.push(mark);
|
|
744
|
-
guardReasons.set(
|
|
745
|
-
pendingMarkKey(mark),
|
|
746
|
-
"evidence was gathered in the current turn; the mark stays pending until the turn closes",
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
marks.length = 0;
|
|
750
|
-
marks.push(...applicable);
|
|
751
|
-
}
|
|
752
954
|
if (input.retainIneligible) {
|
|
753
955
|
const applicable: PendingMark[] = [];
|
|
754
956
|
for (const mark of marks) {
|
|
@@ -765,8 +967,7 @@ export async function commitPendingMarks(input: {
|
|
|
765
967
|
mark: mark.mark,
|
|
766
968
|
id: mark.id,
|
|
767
969
|
origin: mark.origin,
|
|
768
|
-
reason:
|
|
769
|
-
"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",
|
|
770
971
|
retained: true,
|
|
771
972
|
}));
|
|
772
973
|
const minting = new Map(marks.flatMap((mark) =>
|
|
@@ -793,6 +994,40 @@ export async function commitPendingMarks(input: {
|
|
|
793
994
|
placed.add(key);
|
|
794
995
|
ordered.push(mark);
|
|
795
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
|
+
}
|
|
796
1031
|
for (const mark of base) place(mark);
|
|
797
1032
|
for (const mark of ordered) {
|
|
798
1033
|
try {
|
|
@@ -825,13 +1060,11 @@ export async function commitPendingMarks(input: {
|
|
|
825
1060
|
continue;
|
|
826
1061
|
}
|
|
827
1062
|
const sourceRefs = span.refs;
|
|
828
|
-
const blocked =
|
|
829
|
-
? toolRefsProtected(sourceRefs, state, input.snapshot)
|
|
830
|
-
: refsProtected(sourceRefs, state, input.snapshot);
|
|
1063
|
+
const blocked = refsProtected(sourceRefs, state, input.snapshot);
|
|
831
1064
|
if (blocked) {
|
|
832
1065
|
throw new Error(
|
|
833
1066
|
`pending fold mark ${mark.id} covers protected or fresh evidence; ` +
|
|
834
|
-
"
|
|
1067
|
+
"unpin that evidence and mark it again",
|
|
835
1068
|
);
|
|
836
1069
|
}
|
|
837
1070
|
const prepared = await prepareFold({
|
|
@@ -867,7 +1100,7 @@ export async function commitPendingMarks(input: {
|
|
|
867
1100
|
state = withPendingMarks(state, [...pendingMarks(state), ...deferred]);
|
|
868
1101
|
retained.push(...deferred);
|
|
869
1102
|
}
|
|
870
|
-
return { state, applied, refused, retained
|
|
1103
|
+
return { state, applied, refused, retained };
|
|
871
1104
|
}
|
|
872
1105
|
|
|
873
1106
|
export function schedulingStatus(input: {
|