pi-fold 2.0.0 → 3.1.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 +79 -191
- package/extensions/active-context.ts +1332 -526
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +17 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +94 -61
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +207 -59
- package/extensions/lib/instrumentation.ts +10 -9
- package/extensions/lib/measurement.ts +43 -16
- package/extensions/lib/persistence.ts +273 -46
- package/extensions/lib/policy.ts +251 -95
- package/extensions/lib/scheduling.ts +446 -126
- package/extensions/lib/selection.ts +117 -116
- package/extensions/lib/tool-surface.ts +60 -33
- package/extensions/lib/transcript.ts +82 -74
- package/extensions/settings.ts +456 -0
- package/package.json +4 -1
|
@@ -3,6 +3,7 @@ import type { EvidenceRef } from "../json.ts";
|
|
|
3
3
|
import {
|
|
4
4
|
bytes,
|
|
5
5
|
clone,
|
|
6
|
+
contentText,
|
|
6
7
|
messageRole,
|
|
7
8
|
} from "./canonical.ts";
|
|
8
9
|
import {
|
|
@@ -10,38 +11,39 @@ import {
|
|
|
10
11
|
pinnedPeekMass,
|
|
11
12
|
preparedMatchesCandidate,
|
|
12
13
|
prepareFold,
|
|
14
|
+
projectActiveContext,
|
|
13
15
|
renderFold,
|
|
14
16
|
renderFoldParts,
|
|
17
|
+
selectAutomaticChapter,
|
|
15
18
|
selectAutomaticSpan,
|
|
16
19
|
setFoldProjectionState,
|
|
17
20
|
} from "./folding.ts";
|
|
18
21
|
import type { AutomaticRungSelection } from "./folding.ts";
|
|
22
|
+
import { staleSpanMatureEnd } from "./transcript.ts";
|
|
19
23
|
import {
|
|
20
24
|
foldInterval,
|
|
21
25
|
budgetOccupancy,
|
|
22
|
-
hardFenceRatio,
|
|
23
26
|
mappedByKey,
|
|
24
27
|
orderedRoots,
|
|
28
|
+
protectedStaleMass,
|
|
25
29
|
refsProtected,
|
|
26
|
-
toolRefsProtected,
|
|
27
30
|
} from "./measurement.ts";
|
|
28
31
|
import {
|
|
29
32
|
childFoldIds,
|
|
30
|
-
clearPrepared,
|
|
31
33
|
flattenFoldRefs,
|
|
32
34
|
foldIdFor,
|
|
33
35
|
parsePendingMarks,
|
|
34
36
|
pendingMarkKey,
|
|
35
37
|
} from "./persistence.ts";
|
|
36
|
-
import { currentTurnRefKeys } from "./transcript.ts";
|
|
37
38
|
import {
|
|
38
39
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
39
|
-
MAX_WEDGE_ABSORB_TOKENS,
|
|
40
40
|
ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES,
|
|
41
|
+
MAX_FRONTIER_CUTS_PER_PASS,
|
|
41
42
|
MAX_PENDING_MARKS,
|
|
42
43
|
MAX_UNMARKED_CANDIDATES,
|
|
43
44
|
} from "./policy.ts";
|
|
44
45
|
import type {
|
|
46
|
+
ToolClip,
|
|
45
47
|
ActiveContextSnapshot,
|
|
46
48
|
ActiveContextState,
|
|
47
49
|
FoldCandidate,
|
|
@@ -58,6 +60,7 @@ import {
|
|
|
58
60
|
partsForRange,
|
|
59
61
|
peekedSourceFoldIds,
|
|
60
62
|
selectAutomaticConsolidations,
|
|
63
|
+
selectAutomaticRefold,
|
|
61
64
|
spanBytes,
|
|
62
65
|
deterministicChapterCandidateBrief,
|
|
63
66
|
deterministicConsolidationBrief,
|
|
@@ -185,15 +188,6 @@ export function markFreedBytes(
|
|
|
185
188
|
return Math.max(0, bytes(source) - placeholder);
|
|
186
189
|
}
|
|
187
190
|
|
|
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
191
|
export function markSpanStart(
|
|
198
192
|
snapshot: ActiveContextSnapshot,
|
|
199
193
|
state: ActiveContextState,
|
|
@@ -210,46 +204,6 @@ export function markSpanStart(
|
|
|
210
204
|
return indices.length ? Math.min(...indices) : snapshot.mapped.length;
|
|
211
205
|
}
|
|
212
206
|
|
|
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
207
|
export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
|
|
254
208
|
|
|
255
209
|
export function markEligibility(
|
|
@@ -266,11 +220,16 @@ export function markEligibility(
|
|
|
266
220
|
if (span.unresolved) return span.unresolved.pending ? "protected" : "unfulfillable";
|
|
267
221
|
const refs = span.refs;
|
|
268
222
|
if (!refs.length) return "unfulfillable";
|
|
269
|
-
|
|
223
|
+
// THE MAPPED INDEX IS ALREADY DERIVED (2026-08-28). This built a fresh Set over the whole
|
|
224
|
+
// window to answer a membership question about the handful of refs in one mark's span, and
|
|
225
|
+
// `markAccounting` calls this once per pending mark while the commit fill calls
|
|
226
|
+
// `markAccounting` once per staged mark, so it was O(marks^2 x window). `mappedByKey` is
|
|
227
|
+
// gate 113's memo, keyed on the snapshot OBJECT, and `refsProtected` on the next line
|
|
228
|
+
// already depends on it being fresh: if it could serve stale, this function was already
|
|
229
|
+
// wrong. Measured 177.2ms to 41.6ms at the observed peak of 39 pending marks.
|
|
230
|
+
const mapped = mappedByKey(snapshot);
|
|
270
231
|
if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
|
|
271
|
-
const blocked =
|
|
272
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
273
|
-
: refsProtected(refs, state, snapshot);
|
|
232
|
+
const blocked = refsProtected(refs, state, snapshot);
|
|
274
233
|
return blocked ? "protected" : "eligible";
|
|
275
234
|
}
|
|
276
235
|
|
|
@@ -278,10 +237,10 @@ export interface MarkAccounting {
|
|
|
278
237
|
pending: number;
|
|
279
238
|
agentMarks: number;
|
|
280
239
|
ladderMarks: number;
|
|
240
|
+
userMarks: number;
|
|
281
241
|
freedBytes: number;
|
|
282
242
|
freedTokens: number;
|
|
283
243
|
freedBudgetShare: number;
|
|
284
|
-
rewriteTokens: number;
|
|
285
244
|
eligibleMarks: number;
|
|
286
245
|
retainedMarks: number;
|
|
287
246
|
eligibleFreedBytes: number;
|
|
@@ -296,13 +255,10 @@ export function markAccounting(
|
|
|
296
255
|
state: ActiveContextState,
|
|
297
256
|
): MarkAccounting {
|
|
298
257
|
const marks = pendingMarks(state);
|
|
299
|
-
const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
|
|
300
|
-
item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
|
|
301
258
|
let freedBytes = 0;
|
|
302
259
|
let eligibleFreedBytes = 0;
|
|
303
260
|
let eligibleMarks = 0;
|
|
304
261
|
let retainedMarks = 0;
|
|
305
|
-
let earliest = -1;
|
|
306
262
|
for (const mark of marks) {
|
|
307
263
|
const freed = markFreedBytes(snapshot, state, mark);
|
|
308
264
|
freedBytes += freed;
|
|
@@ -311,15 +267,7 @@ export function markAccounting(
|
|
|
311
267
|
eligibleMarks += 1;
|
|
312
268
|
eligibleFreedBytes += freed;
|
|
313
269
|
} else if (eligibility === "protected") retainedMarks += 1;
|
|
314
|
-
for (const ref of markSpanRefs(state, mark).refs) {
|
|
315
|
-
const index = indexByKey.get(objectRefKey(ref));
|
|
316
|
-
if (index === undefined) continue;
|
|
317
|
-
if (earliest < 0 || index < earliest) earliest = index;
|
|
318
|
-
}
|
|
319
270
|
}
|
|
320
|
-
const rewriteBytes = earliest < 0
|
|
321
|
-
? 0
|
|
322
|
-
: bytes(snapshot.messages.slice(earliest));
|
|
323
271
|
const freedTokens = estimatedTokens(freedBytes);
|
|
324
272
|
const eligibleFreedTokens = estimatedTokens(eligibleFreedBytes);
|
|
325
273
|
const pinned = pinnedPeekMass(snapshot, state);
|
|
@@ -327,10 +275,10 @@ export function markAccounting(
|
|
|
327
275
|
pending: marks.length,
|
|
328
276
|
agentMarks: marks.filter((mark) => mark.origin === "agent").length,
|
|
329
277
|
ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
|
|
278
|
+
userMarks: marks.filter((mark) => mark.origin === "user").length,
|
|
330
279
|
freedBytes,
|
|
331
280
|
freedTokens,
|
|
332
281
|
freedBudgetShare: snapshot.budgetTokens > 0 ? freedTokens / snapshot.budgetTokens : 0,
|
|
333
|
-
rewriteTokens: estimatedTokens(rewriteBytes),
|
|
334
282
|
eligibleMarks,
|
|
335
283
|
retainedMarks,
|
|
336
284
|
eligibleFreedBytes,
|
|
@@ -343,6 +291,40 @@ export function markAccounting(
|
|
|
343
291
|
};
|
|
344
292
|
}
|
|
345
293
|
|
|
294
|
+
/**
|
|
295
|
+
* What one commit would make the provider re-read: the marked tail, from the earliest
|
|
296
|
+
* marked message to the end of the window.
|
|
297
|
+
*
|
|
298
|
+
* IT IS DERIVED WHERE IT IS READ (2026-08-28). This was a field on `MarkAccounting`, which
|
|
299
|
+
* meant `bytes(snapshot.messages.slice(earliest))`, a serialization of the whole marked
|
|
300
|
+
* tail, ran on EVERY accounting call: the commit fill calls one per staged mark, and the
|
|
301
|
+
* `brief` handler calls one to read `pending` alone. Two readers want the number and both
|
|
302
|
+
* already hold the snapshot and the state, so the work moved to them. A memo could not have
|
|
303
|
+
* helped: `snapshot.messages` is `clone(input.eventMessages)` (transcript.ts:531), so its
|
|
304
|
+
* elements are fresh objects on every derivation and nothing keyed on them can ever hit.
|
|
305
|
+
*
|
|
306
|
+
* `schedulingStatus` still publishes `rewriteTokens` and now derives it here, so the status
|
|
307
|
+
* surface is unchanged. It is NOT a getter on the accounting object: `schedulingStatus`
|
|
308
|
+
* spreads the accounting, and a spread evaluates getters, which would put the cost straight
|
|
309
|
+
* back on every reader it was moved away from.
|
|
310
|
+
*/
|
|
311
|
+
export function markRewriteTokens(
|
|
312
|
+
snapshot: ActiveContextSnapshot,
|
|
313
|
+
state: ActiveContextState,
|
|
314
|
+
): number {
|
|
315
|
+
const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
|
|
316
|
+
item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
|
|
317
|
+
let earliest = -1;
|
|
318
|
+
for (const mark of pendingMarks(state)) {
|
|
319
|
+
for (const ref of markSpanRefs(state, mark).refs) {
|
|
320
|
+
const index = indexByKey.get(objectRefKey(ref));
|
|
321
|
+
if (index === undefined) continue;
|
|
322
|
+
if (earliest < 0 || index < earliest) earliest = index;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return earliest < 0 ? 0 : estimatedTokens(bytes(snapshot.messages.slice(earliest)));
|
|
326
|
+
}
|
|
327
|
+
|
|
346
328
|
export function epochCommitDue(snapshot: ActiveContextSnapshot, ratio: number | null): boolean {
|
|
347
329
|
const occupancy = budgetOccupancy(snapshot, ratio);
|
|
348
330
|
return occupancy !== null && occupancy >= snapshot.thresholds.maxTarget;
|
|
@@ -442,8 +424,7 @@ export function ephemeralPeekMarks(input: {
|
|
|
442
424
|
if (!peeked) continue;
|
|
443
425
|
if (peeked.some((id) => state.expanded.includes(id) || peekedFoldProtected(snapshot, state, id))) continue;
|
|
444
426
|
if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
|
|
445
|
-
refs
|
|
446
|
-
toolRefsProtected(refs, state, snapshot)) continue;
|
|
427
|
+
refsProtected(refs, state, snapshot)) continue;
|
|
447
428
|
const candidate: FoldCandidate = {
|
|
448
429
|
kind: "tool-result",
|
|
449
430
|
parts: refs.map((ref) => ({ kind: "raw" as const, ref })),
|
|
@@ -468,9 +449,7 @@ function peekedFoldProtected(
|
|
|
468
449
|
const fold = state.folds.find((item) => item.id === foldId);
|
|
469
450
|
if (!fold) return false;
|
|
470
451
|
const refs = flattenFoldRefs(fold, state);
|
|
471
|
-
return
|
|
472
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
473
|
-
: refsProtected(refs, state, snapshot);
|
|
452
|
+
return refsProtected(refs, state, snapshot);
|
|
474
453
|
}
|
|
475
454
|
|
|
476
455
|
export function claimedRefKeys(state: ActiveContextState): Set<string> {
|
|
@@ -488,6 +467,8 @@ export function claimedRefKeys(state: ActiveContextState): Set<string> {
|
|
|
488
467
|
export interface UnmarkedRemainder {
|
|
489
468
|
spans: number;
|
|
490
469
|
tokens: number;
|
|
470
|
+
/** Raw bytes, which is what the fold frontier measures itself against. */
|
|
471
|
+
chars: number;
|
|
491
472
|
share: number;
|
|
492
473
|
candidates: Array<{ id: string; tokens: number }>;
|
|
493
474
|
}
|
|
@@ -504,6 +485,33 @@ export function unmarkedRemainder(
|
|
|
504
485
|
for (const batch of automaticToolBatches(snapshot, state)) {
|
|
505
486
|
for (const index of batch.indices) members.add(index);
|
|
506
487
|
}
|
|
488
|
+
// CHAPTERS ARE FOLDABLE MASS TOO (Shane, 2026-08-23).
|
|
489
|
+
//
|
|
490
|
+
// The population used to be tool batches alone, while `selectAutomaticSpan` proposes a
|
|
491
|
+
// chapter whenever no batch is available, so on a chapter-heavy session this reported a
|
|
492
|
+
// remainder of zero over material automatic folding was about to take. It is the reading
|
|
493
|
+
// a deferred commit uses to say whether its mass is HELD or simply absent, which is the
|
|
494
|
+
// one distinction that record exists to draw, and it was answering "absent" for mass in
|
|
495
|
+
// plain view. It also broke the frontier's first cut, which gated on this number before
|
|
496
|
+
// the threshold moved into the selector where it belongs.
|
|
497
|
+
//
|
|
498
|
+
// Walked with the selector rather than re-derived, so the population is what automatic
|
|
499
|
+
// folding can actually reach and cannot drift from it. Walked from an EMPTY claim set,
|
|
500
|
+
// because this is the DENOMINATOR: a chapter already claimed by a mark is still foldable
|
|
501
|
+
// mass, it is just mass that is already spoken for, and the numerator below is what
|
|
502
|
+
// subtracts it. Bounded by the mapped length, which no terminating walk can exceed since
|
|
503
|
+
// every pass claims at least one ref.
|
|
504
|
+
const indexed = mappedByKey(snapshot);
|
|
505
|
+
const walked = new Set<string>();
|
|
506
|
+
for (let pass = 0; pass < snapshot.mapped.length; pass += 1) {
|
|
507
|
+
const chapter = selectAutomaticChapter(snapshot, state, walked);
|
|
508
|
+
if (!chapter) break;
|
|
509
|
+
for (const ref of chapter.sourceRefs) {
|
|
510
|
+
walked.add(objectRefKey(ref));
|
|
511
|
+
const item = indexed.get(objectRefKey(ref));
|
|
512
|
+
if (item) members.add(item.index);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
507
515
|
const candidates: Array<{ id: string; tokens: number }> = [];
|
|
508
516
|
let unmarkedBytes = 0;
|
|
509
517
|
let memberBytes = 0;
|
|
@@ -519,11 +527,193 @@ export function unmarkedRemainder(
|
|
|
519
527
|
return {
|
|
520
528
|
spans: candidates.length,
|
|
521
529
|
tokens: Math.ceil(unmarkedBytes / perToken),
|
|
530
|
+
chars: unmarkedBytes,
|
|
522
531
|
share: memberBytes > 0 ? unmarkedBytes / memberBytes : 0,
|
|
523
532
|
candidates: candidates.slice(0, Math.max(0, limit)),
|
|
524
533
|
};
|
|
525
534
|
}
|
|
526
535
|
|
|
536
|
+
/**
|
|
537
|
+
* THE FOLD FRONTIER: THE RUNTIME CUTS, AND THE AGENT EDITS WHAT IT CUT
|
|
538
|
+
* (Shane, 2026-08-23).
|
|
539
|
+
*
|
|
540
|
+
* The ladder used to stage nothing between commits, and before that it staged a mark on
|
|
541
|
+
* every measured response. Both were answers to the same question, WHEN SHOULD THE AGENT
|
|
542
|
+
* CURATE, and both got it wrong in the same way: they asked the agent to CHOOSE SPANS.
|
|
543
|
+
* Eager staging chose them first and crowded the agent out; standing down left the choice
|
|
544
|
+
* open until the commit, by which time the material was long out of view and the only
|
|
545
|
+
* information available was staleness, which the runtime already has.
|
|
546
|
+
*
|
|
547
|
+
* So the choice moves. As raw material accumulates past `minFoldChars`, the runtime cuts
|
|
548
|
+
* it, stalest first, exactly as a commit would. The cut is a PENDING MARK and pending
|
|
549
|
+
* marks are byte-inert: nothing moves, the projection does not change, the prefix cache is
|
|
550
|
+
* untouched, and the agent keeps seeing the raw material until a commit applies it. What
|
|
551
|
+
* the agent gets is a fold to annotate while the content is still fresh, which is the one
|
|
552
|
+
* thing it knows that the runtime does not.
|
|
553
|
+
*
|
|
554
|
+
* The loop bounds itself twice over. It stops when the unmarked remainder falls under the
|
|
555
|
+
* threshold, and it stops when the selector runs out of members, so a window whose whole
|
|
556
|
+
* remainder is pinned or blacklisted stages nothing and says so by returning empty rather
|
|
557
|
+
* than spinning.
|
|
558
|
+
*/
|
|
559
|
+
export function frontierMarks(input: {
|
|
560
|
+
snapshot: ActiveContextSnapshot;
|
|
561
|
+
state: ActiveContextState;
|
|
562
|
+
ordinal: number;
|
|
563
|
+
}): PendingFoldMark[] {
|
|
564
|
+
const { snapshot } = input;
|
|
565
|
+
const claimed = claimedRefKeys(input.state);
|
|
566
|
+
const marks: PendingFoldMark[] = [];
|
|
567
|
+
let state = input.state;
|
|
568
|
+
while (marks.length < MAX_FRONTIER_CUTS_PER_PASS) {
|
|
569
|
+
// THE THRESHOLD IS THE SELECTOR'S OWN, which is `minFoldChars`, and it is read there
|
|
570
|
+
// rather than counted here. The first cut of this measured the unmarked remainder
|
|
571
|
+
// instead, and that reads TOOL BATCHES ONLY: a chapter-only session had a remainder of
|
|
572
|
+
// zero however much raw prose it held, so the frontier never fired on it at all and
|
|
573
|
+
// gate 7's fixture produced no cut. One threshold, in the one place that knows every
|
|
574
|
+
// span kind.
|
|
575
|
+
const candidate = selectAutomaticSpan(snapshot, state, claimed);
|
|
576
|
+
if (!candidate) break;
|
|
577
|
+
const mark = foldMarkFor({
|
|
578
|
+
candidate,
|
|
579
|
+
...automaticMarkBrief(snapshot, state, candidate),
|
|
580
|
+
origin: "ladder",
|
|
581
|
+
ordinal: input.ordinal,
|
|
582
|
+
});
|
|
583
|
+
const addition = addPendingMark(state, mark);
|
|
584
|
+
if (!addition.added) break;
|
|
585
|
+
state = addition.state;
|
|
586
|
+
marks.push(mark);
|
|
587
|
+
for (const ref of candidate.sourceRefs) claimed.add(objectRefKey(ref));
|
|
588
|
+
}
|
|
589
|
+
return marks;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* The pending mark that already holds a piece of this evidence, if any.
|
|
594
|
+
*
|
|
595
|
+
* The commit fills from the stale end and the depth bound retains what it does not
|
|
596
|
+
* reach, so a ladder mark routinely outlives the commit that staged it. An agent
|
|
597
|
+
* marking the same evidence afterwards would stand a second owner over one span and
|
|
598
|
+
* lose at apply time with a forest invariant for a message. Refusing at the tool says
|
|
599
|
+
* so while the agent can still act on it.
|
|
600
|
+
*/
|
|
601
|
+
export function markClaimingRef(
|
|
602
|
+
state: ActiveContextState,
|
|
603
|
+
refs: readonly EvidenceRef[],
|
|
604
|
+
): { markId: string; entryId: string } | null {
|
|
605
|
+
for (const mark of pendingMarks(state)) {
|
|
606
|
+
if (mark.mark !== "fold") continue;
|
|
607
|
+
const claimed = new Set(markSpanRefs(state, mark).refs.map(objectRefKey));
|
|
608
|
+
for (const ref of refs) {
|
|
609
|
+
if (claimed.has(objectRefKey(ref))) return { markId: mark.id, entryId: ref.entryId };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export interface WindowClaims {
|
|
616
|
+
/** Tokens standing marks will free at the next commit. */
|
|
617
|
+
markedTokens: number;
|
|
618
|
+
/** Tokens the agent pinned raw: a claim that frees nothing and is never taken. */
|
|
619
|
+
pinnedTokens: number;
|
|
620
|
+
pinnedRefs: number;
|
|
621
|
+
/** Pinned mass as a share of the serving budget, against its cap. */
|
|
622
|
+
pinnedShare: number;
|
|
623
|
+
/** Completed units neither marked nor pinned: what the ladder takes by age. */
|
|
624
|
+
unclaimedTokens: number;
|
|
625
|
+
unclaimedSpans: number;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* A PIN IS A CLAIM, NOT A FOLD (Shane, 2026-08-22: "if they do a pin on raw span I think
|
|
630
|
+
* we also treat that as a marked fold but a pinned one").
|
|
631
|
+
*
|
|
632
|
+
* Read as accounting, that is exactly right and already half true: pinned evidence is
|
|
633
|
+
* refused to every automatic rung, skipped by wedge absorption, and excluded from the
|
|
634
|
+
* unmarked remainder, so the ladder cannot take it and it is never reported as an
|
|
635
|
+
* oversight. What was missing is that the agent could not SEE it, and the whole point of
|
|
636
|
+
* asking an agent to account for its window is that the gaps become visible.
|
|
637
|
+
*
|
|
638
|
+
* A pin cannot count toward the DROP, and that is not a technicality. Pinned bytes stay
|
|
639
|
+
* in the window, so they free nothing; if they counted, an agent could "cover" a commit
|
|
640
|
+
* with pins, the commit would still be short, and it would fold something else anyway.
|
|
641
|
+
* The number would be a lie at the moment it mattered most. So the three buckets are
|
|
642
|
+
* reported side by side and only one of them pays.
|
|
643
|
+
*/
|
|
644
|
+
export function windowClaims(input: {
|
|
645
|
+
snapshot: ActiveContextSnapshot;
|
|
646
|
+
state: ActiveContextState;
|
|
647
|
+
budgetTokens: number;
|
|
648
|
+
charsPerToken: number;
|
|
649
|
+
}): WindowClaims {
|
|
650
|
+
const { snapshot, state, budgetTokens } = input;
|
|
651
|
+
const accounting = markAccounting(snapshot, state);
|
|
652
|
+
const remainder = unmarkedRemainder(snapshot, state, input.charsPerToken);
|
|
653
|
+
const pinned = protectedStaleMass(snapshot, state);
|
|
654
|
+
const pinnedTokens = estimatedTokens(pinned.bytes);
|
|
655
|
+
return {
|
|
656
|
+
markedTokens: Math.round(accounting.eligibleFreedBudgetShare * budgetTokens),
|
|
657
|
+
pinnedTokens,
|
|
658
|
+
pinnedRefs: pinned.refs,
|
|
659
|
+
pinnedShare: budgetTokens > 0 ? pinnedTokens / budgetTokens : 0,
|
|
660
|
+
unclaimedTokens: remainder.tokens,
|
|
661
|
+
unclaimedSpans: remainder.spans,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export interface CommitCoverage {
|
|
666
|
+
/** The share of the serving budget the next commit is sized to free. */
|
|
667
|
+
targetShare: number;
|
|
668
|
+
targetTokens: number;
|
|
669
|
+
/** What the standing marks will actually free when that commit runs. */
|
|
670
|
+
markedShare: number;
|
|
671
|
+
markedTokens: number;
|
|
672
|
+
/** What the ladder will take by staleness unless the agent marks it first. */
|
|
673
|
+
remainingTokens: number;
|
|
674
|
+
covered: boolean;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* WHAT THE NEXT COMMIT NEEDS, AND HOW MUCH OF IT THE AGENT HAS COVERED.
|
|
679
|
+
*
|
|
680
|
+
* One definition, read by three callers that used to reason about the drop separately:
|
|
681
|
+
* the commit fill that sizes itself to it, the advisory that warns before it, and the
|
|
682
|
+
* mark response that tells the agent where it stands. An agent asked to govern its own
|
|
683
|
+
* context cannot do it against a number nobody tells it.
|
|
684
|
+
*
|
|
685
|
+
* The marked side is the ELIGIBLE share, not every standing mark, because a mark over
|
|
686
|
+
* protected evidence frees nothing at this commit however good it is. That makes
|
|
687
|
+
* `remainingTokens` the honest answer to "what will fold without me".
|
|
688
|
+
*
|
|
689
|
+
* `targetShare` moves as the window grows: it is measured from occupancy NOW, and two
|
|
690
|
+
* large results landing after the reading raise it again. Callers that render it say so.
|
|
691
|
+
*/
|
|
692
|
+
export function commitCoverage(input: {
|
|
693
|
+
snapshot: ActiveContextSnapshot;
|
|
694
|
+
state: ActiveContextState;
|
|
695
|
+
usedTokens: number | null;
|
|
696
|
+
budgetTokens: number;
|
|
697
|
+
}): CommitCoverage {
|
|
698
|
+
const { snapshot, state, usedTokens, budgetTokens } = input;
|
|
699
|
+
const targetShare = usedTokens === null || !(budgetTokens > 0)
|
|
700
|
+
? 0
|
|
701
|
+
: Math.max(0, (usedTokens - snapshot.thresholds.minTarget * budgetTokens) / budgetTokens);
|
|
702
|
+
const accounting = markAccounting(snapshot, state);
|
|
703
|
+
const markedShare = accounting.eligibleFreedBudgetShare;
|
|
704
|
+
const targetTokens = Math.round(targetShare * budgetTokens);
|
|
705
|
+
const markedTokens = Math.round(markedShare * budgetTokens);
|
|
706
|
+
const remainingTokens = Math.max(0, targetTokens - markedTokens);
|
|
707
|
+
return {
|
|
708
|
+
targetShare,
|
|
709
|
+
targetTokens,
|
|
710
|
+
markedShare,
|
|
711
|
+
markedTokens,
|
|
712
|
+
remainingTokens,
|
|
713
|
+
covered: remainingTokens === 0,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
527
717
|
export function markedFoldIds(state: ActiveContextState): Set<string> {
|
|
528
718
|
const ids = new Set<string>();
|
|
529
719
|
for (const mark of pendingMarks(state)) if (mark.mark === "refold") ids.add(mark.id);
|
|
@@ -548,13 +738,11 @@ export function topUpMarks(input: {
|
|
|
548
738
|
state: ActiveContextState;
|
|
549
739
|
ordinal: number;
|
|
550
740
|
targetShare: number;
|
|
551
|
-
excludeRefKeys?: ReadonlySet<string>;
|
|
552
741
|
eligibleOnly?: boolean;
|
|
553
742
|
}): PendingFoldMark[] {
|
|
554
743
|
const { snapshot } = input;
|
|
555
744
|
const target = input.targetShare;
|
|
556
745
|
const claimed = claimedRefKeys(input.state);
|
|
557
|
-
for (const key of input.excludeRefKeys ?? []) claimed.add(key);
|
|
558
746
|
const marks: PendingFoldMark[] = [];
|
|
559
747
|
let state = input.state;
|
|
560
748
|
const progress = (value: ActiveContextState): number => {
|
|
@@ -581,6 +769,50 @@ export function topUpMarks(input: {
|
|
|
581
769
|
return marks;
|
|
582
770
|
}
|
|
583
771
|
|
|
772
|
+
/**
|
|
773
|
+
* REFOLD AS THE LAST FILL, NOT THE FIRST (Shane, 2026-08-21).
|
|
774
|
+
*
|
|
775
|
+
* An expanded fold is stale mass sitting in the window, so the commit may reclaim it,
|
|
776
|
+
* but only after raw stale material has been spent. An agent that expands a fold is
|
|
777
|
+
* reading it; taking it back while there is still unfolded material to take is hostile
|
|
778
|
+
* for no gain. Pin holds a span past that point, and a live lease holds it for the
|
|
779
|
+
* generations right after the expand. Stalest first, and never past the freeing target.
|
|
780
|
+
*/
|
|
781
|
+
export function refoldMarks(input: {
|
|
782
|
+
snapshot: ActiveContextSnapshot;
|
|
783
|
+
state: ActiveContextState;
|
|
784
|
+
ordinal: number;
|
|
785
|
+
targetShare: number;
|
|
786
|
+
eligibleOnly?: boolean;
|
|
787
|
+
}): PendingMark[] {
|
|
788
|
+
const { snapshot } = input;
|
|
789
|
+
const marks: PendingMark[] = [];
|
|
790
|
+
let state = input.state;
|
|
791
|
+
const claimedFoldIds = markedFoldIds(state);
|
|
792
|
+
const progress = (value: ActiveContextState): number => {
|
|
793
|
+
const accounting = markAccounting(snapshot, value);
|
|
794
|
+
return input.eligibleOnly ? accounting.eligibleFreedBudgetShare : accounting.freedBudgetShare;
|
|
795
|
+
};
|
|
796
|
+
let share = progress(state);
|
|
797
|
+
while (share < input.targetShare) {
|
|
798
|
+
const foldId = selectAutomaticRefold(snapshot, state, claimedFoldIds);
|
|
799
|
+
if (!foldId) break;
|
|
800
|
+
const mark: PendingMark = {
|
|
801
|
+
mark: "refold",
|
|
802
|
+
id: foldId,
|
|
803
|
+
origin: "ladder",
|
|
804
|
+
ordinal: input.ordinal,
|
|
805
|
+
};
|
|
806
|
+
const addition = addPendingMark(state, mark);
|
|
807
|
+
if (!addition.added) break;
|
|
808
|
+
state = addition.state;
|
|
809
|
+
marks.push(mark);
|
|
810
|
+
claimedFoldIds.add(foldId);
|
|
811
|
+
share = progress(state);
|
|
812
|
+
}
|
|
813
|
+
return marks;
|
|
814
|
+
}
|
|
815
|
+
|
|
584
816
|
export interface AbsorbedWedge {
|
|
585
817
|
intoMarkId: string;
|
|
586
818
|
fromMarkId: string;
|
|
@@ -588,6 +820,7 @@ export interface AbsorbedWedge {
|
|
|
588
820
|
endId: string;
|
|
589
821
|
entries: number;
|
|
590
822
|
tokens: number;
|
|
823
|
+
chars: number;
|
|
591
824
|
}
|
|
592
825
|
|
|
593
826
|
export function absorbWedgeMarks(input: {
|
|
@@ -631,8 +864,16 @@ export function absorbWedgeMarks(input: {
|
|
|
631
864
|
const gapEnd = interval.start - 1;
|
|
632
865
|
if (gapStart < 0 || gapEnd < gapStart) continue;
|
|
633
866
|
if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
|
|
634
|
-
|
|
635
|
-
|
|
867
|
+
// THE GAP FLOOR IS THE FOLD FLOOR (Shane, 2026-08-21). A gap between two folds
|
|
868
|
+
// that could not stand as a fold on its own has no business staying raw: it costs
|
|
869
|
+
// its own bytes and buys nothing, and the neighbour beside it can hold it for the
|
|
870
|
+
// price of one sentence in the brief. So anything under minFoldChars is absorbed
|
|
871
|
+
// and anything at or over it is left alone, because at that size it is a fold the
|
|
872
|
+
// ladder will take on its own terms. This replaced MAX_WEDGE_ABSORB_TOKENS, a
|
|
873
|
+
// separate 256-token constant that let 1,024-char slivers survive between folds.
|
|
874
|
+
const gapBytes = spanBytes(snapshot, gapStart, gapEnd + 1);
|
|
875
|
+
if (gapBytes >= snapshot.thresholds.minFoldChars) continue;
|
|
876
|
+
const gapTokens = Math.ceil(gapBytes / charsPerToken);
|
|
636
877
|
const gapRefs = [];
|
|
637
878
|
let usable = true;
|
|
638
879
|
for (let index = gapStart; index <= gapEnd; index += 1) {
|
|
@@ -669,6 +910,7 @@ export function absorbWedgeMarks(input: {
|
|
|
669
910
|
endId: gapRefs.at(-1)!.entryId,
|
|
670
911
|
entries: gapRefs.length,
|
|
671
912
|
tokens: gapTokens,
|
|
913
|
+
chars: gapBytes,
|
|
672
914
|
});
|
|
673
915
|
applied = true;
|
|
674
916
|
break;
|
|
@@ -698,7 +940,6 @@ export interface CommitEpochResult {
|
|
|
698
940
|
applied: AppliedMark[];
|
|
699
941
|
refused: RefusedMark[];
|
|
700
942
|
retained: PendingMark[];
|
|
701
|
-
waived: PendingMark[];
|
|
702
943
|
}
|
|
703
944
|
|
|
704
945
|
export async function commitPendingMarks(input: {
|
|
@@ -707,48 +948,40 @@ export async function commitPendingMarks(input: {
|
|
|
707
948
|
generation: number;
|
|
708
949
|
now?: () => number;
|
|
709
950
|
retainIneligible?: boolean;
|
|
710
|
-
|
|
711
|
-
|
|
951
|
+
/**
|
|
952
|
+
* How many bytes this commit needs to free. Marks past it are RETAINED, not applied.
|
|
953
|
+
*
|
|
954
|
+
* This bound did not exist before the frontier (Shane, 2026-08-23), and it did not need
|
|
955
|
+
* to: staging was sized to the drop by `topUpMarks`, so every pending mark was one the
|
|
956
|
+
* commit had already decided it wanted. The frontier stages as material ARRIVES, which
|
|
957
|
+
* is the whole point of it, so by the time a commit fires the pending set is everything
|
|
958
|
+
* the window holds. Applying all of it would cut the window to nothing at the first
|
|
959
|
+
* commit, which is neither what the budget asks for nor what the agent is owed: what it
|
|
960
|
+
* is owed is that material stays raw for as long as there is room for it.
|
|
961
|
+
*
|
|
962
|
+
* Undefined means no bound, which is what the emergency lanes want: a fence or a
|
|
963
|
+
* rollback is not housekeeping and takes everything it can.
|
|
964
|
+
*/
|
|
965
|
+
applyTargetBytes?: number;
|
|
712
966
|
}): Promise<CommitEpochResult> {
|
|
967
|
+
// THE CURRENT-TURN GUARD IS DELETED (Shane, 2026-08-23: "you're using turns as the
|
|
968
|
+
// boundaries, which should not be the case. It should be at the most granular level,
|
|
969
|
+
// which is events"). The guard retained every mark touching a toolResult newer than
|
|
970
|
+
// the last terminal assistant stop, and a terminal stop is a TURN artifact: the first
|
|
971
|
+
// live session (sol-20260823-live rep 1) ran its whole workload as one turn, every
|
|
972
|
+
// assistant response stopping "toolUse", so the guard's "current turn" was the entire
|
|
973
|
+
// window, four band-top commits applied nothing, and the projection fence swept 18
|
|
974
|
+
// marks at 0.958 occupancy down to a 0.031 landing against a 0.40 aim. The waiver
|
|
975
|
+
// machinery existed only to bail the guard out at the fence and the boundary, so it
|
|
976
|
+
// is deleted with it. What protects the working set now is structural and event-level:
|
|
977
|
+
// an incomplete batch is never proposable, the depth bound stops the routine commit at
|
|
978
|
+
// the aim, and the stalest-first cut order leaves the newest events raw.
|
|
713
979
|
const marks = pendingMarks(input.state);
|
|
714
980
|
const retained: PendingMark[] = [];
|
|
715
|
-
const waived: PendingMark[] = [];
|
|
716
|
-
const guardReasons = new Map<string, string>();
|
|
717
981
|
const held = new Set(input.state.folds.map((fold) => fold.id));
|
|
718
982
|
const deferred: PendingMark[] = [];
|
|
719
983
|
const awaitingMintedChild = (mark: PendingMark): boolean =>
|
|
720
984
|
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
985
|
if (input.retainIneligible) {
|
|
753
986
|
const applicable: PendingMark[] = [];
|
|
754
987
|
for (const mark of marks) {
|
|
@@ -765,8 +998,7 @@ export async function commitPendingMarks(input: {
|
|
|
765
998
|
mark: mark.mark,
|
|
766
999
|
id: mark.id,
|
|
767
1000
|
origin: mark.origin,
|
|
768
|
-
reason:
|
|
769
|
-
"span is still fresh or protected; the mark stays pending until it is eligible",
|
|
1001
|
+
reason: "span is still fresh or protected; the mark stays pending until it is eligible",
|
|
770
1002
|
retained: true,
|
|
771
1003
|
}));
|
|
772
1004
|
const minting = new Map(marks.flatMap((mark) =>
|
|
@@ -793,6 +1025,40 @@ export async function commitPendingMarks(input: {
|
|
|
793
1025
|
placed.add(key);
|
|
794
1026
|
ordered.push(mark);
|
|
795
1027
|
};
|
|
1028
|
+
// THE DEPTH CUT, STALEST FIRST. `base` is already in span order, so taking a prefix of
|
|
1029
|
+
// it takes the oldest material and leaves the newest raw, which is the ordering the
|
|
1030
|
+
// agent would choose if it were asked. A mark left over here is RETAINED rather than
|
|
1031
|
+
// refused: it is still pending, still briefable, still editable, and the next commit
|
|
1032
|
+
// that needs depth will reach it.
|
|
1033
|
+
const bounded: PendingMark[] = [];
|
|
1034
|
+
if (input.applyTargetBytes !== undefined && input.applyTargetBytes > 0) {
|
|
1035
|
+
let freed = 0;
|
|
1036
|
+
for (const mark of base) {
|
|
1037
|
+
if (freed >= input.applyTargetBytes) {
|
|
1038
|
+
bounded.push(mark);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
freed += markFreedBytes(input.snapshot, input.state, mark);
|
|
1042
|
+
}
|
|
1043
|
+
if (bounded.length) {
|
|
1044
|
+
const spare = new Set(bounded.map((mark) => pendingMarkKey(mark)));
|
|
1045
|
+
const kept = base.filter((mark) => !spare.has(pendingMarkKey(mark)));
|
|
1046
|
+
base.length = 0;
|
|
1047
|
+
base.push(...kept);
|
|
1048
|
+
for (const mark of bounded) {
|
|
1049
|
+
retained.push(mark);
|
|
1050
|
+
refused.push({
|
|
1051
|
+
mark: mark.mark,
|
|
1052
|
+
id: mark.id,
|
|
1053
|
+
origin: mark.origin,
|
|
1054
|
+
reason: "the commit reached its target before this mark; it stays pending and " +
|
|
1055
|
+
"the span stays raw until a later commit needs the depth",
|
|
1056
|
+
retained: true,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
state = withPendingMarks(input.state, retained);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
796
1062
|
for (const mark of base) place(mark);
|
|
797
1063
|
for (const mark of ordered) {
|
|
798
1064
|
try {
|
|
@@ -825,13 +1091,11 @@ export async function commitPendingMarks(input: {
|
|
|
825
1091
|
continue;
|
|
826
1092
|
}
|
|
827
1093
|
const sourceRefs = span.refs;
|
|
828
|
-
const blocked =
|
|
829
|
-
? toolRefsProtected(sourceRefs, state, input.snapshot)
|
|
830
|
-
: refsProtected(sourceRefs, state, input.snapshot);
|
|
1094
|
+
const blocked = refsProtected(sourceRefs, state, input.snapshot);
|
|
831
1095
|
if (blocked) {
|
|
832
1096
|
throw new Error(
|
|
833
1097
|
`pending fold mark ${mark.id} covers protected or fresh evidence; ` +
|
|
834
|
-
"
|
|
1098
|
+
"unpin that evidence and mark it again",
|
|
835
1099
|
);
|
|
836
1100
|
}
|
|
837
1101
|
const prepared = await prepareFold({
|
|
@@ -850,9 +1114,6 @@ export async function commitPendingMarks(input: {
|
|
|
850
1114
|
generation: input.generation,
|
|
851
1115
|
});
|
|
852
1116
|
applied.push({ mark: "fold", id: mark.id, origin: mark.origin, foldId: prepared.id });
|
|
853
|
-
if (state.prepared && state.folds.some((fold) => fold.id === state.prepared!.id)) {
|
|
854
|
-
state = clearPrepared(state);
|
|
855
|
-
}
|
|
856
1117
|
} catch (error) {
|
|
857
1118
|
refused.push({
|
|
858
1119
|
mark: mark.mark,
|
|
@@ -867,7 +1128,7 @@ export async function commitPendingMarks(input: {
|
|
|
867
1128
|
state = withPendingMarks(state, [...pendingMarks(state), ...deferred]);
|
|
868
1129
|
retained.push(...deferred);
|
|
869
1130
|
}
|
|
870
|
-
return { state, applied, refused, retained
|
|
1131
|
+
return { state, applied, refused, retained };
|
|
871
1132
|
}
|
|
872
1133
|
|
|
873
1134
|
export function schedulingStatus(input: {
|
|
@@ -879,6 +1140,11 @@ export function schedulingStatus(input: {
|
|
|
879
1140
|
return {
|
|
880
1141
|
mode: "epoch",
|
|
881
1142
|
...accounting,
|
|
1143
|
+
// The status surface keeps this field; the accounting no longer carries it. Status is a
|
|
1144
|
+
// read the agent or the human asks for, so paying for the tail serialization HERE is
|
|
1145
|
+
// the point of moving it: the commit fill calls `markAccounting` once per staged mark
|
|
1146
|
+
// and none of those calls wants this number.
|
|
1147
|
+
rewriteTokens: markRewriteTokens(input.snapshot, input.state),
|
|
882
1148
|
thresholds: { ...input.snapshot.thresholds },
|
|
883
1149
|
budgetTokens: input.snapshot.budgetTokens,
|
|
884
1150
|
commitDue: epochCommitDue(input.snapshot, input.ratio),
|
|
@@ -898,3 +1164,57 @@ export function schedulingStatus(input: {
|
|
|
898
1164
|
})),
|
|
899
1165
|
};
|
|
900
1166
|
}
|
|
1167
|
+
|
|
1168
|
+
// THE TOOL-CALL DIET SELECTS AT COMMIT TIME ONLY (2026-08-24, Shane's ToolFoldThreshold).
|
|
1169
|
+
// The zone is the oldest `threshold` share of the projected window measured in bytes,
|
|
1170
|
+
// because "the tool calls prior to the X point" is a statement about what the provider
|
|
1171
|
+
// is being made to re-read, and the projection is exactly that. Eligibility mirrors the
|
|
1172
|
+
// fold lanes' own laws one view over: the result must be MATURE (past
|
|
1173
|
+
// staleSpanMatureEnd, the batch lane's consumption proof), unprotected (a pinned ref is
|
|
1174
|
+
// a durable hold), not blacklisted (a tool whose results must stay raw stays raw in
|
|
1175
|
+
// view too), bigger than the floor (small calls are cheap; the win is the multi-KB
|
|
1176
|
+
// result), not a rendering the projection already substituted, and not already clipped.
|
|
1177
|
+
// The additions are VIEW state: encodedFoldSource reads raw parts, so a later fold of a
|
|
1178
|
+
// clipped span still encodes the full bytes and losslessness is untouched.
|
|
1179
|
+
export function toolClipAdditions(input: {
|
|
1180
|
+
snapshot: ActiveContextSnapshot;
|
|
1181
|
+
state: ActiveContextState;
|
|
1182
|
+
threshold: number;
|
|
1183
|
+
blacklist: ReadonlySet<string>;
|
|
1184
|
+
}): ToolClip[] {
|
|
1185
|
+
const { snapshot, state, threshold, blacklist } = input;
|
|
1186
|
+
const projected = projectActiveContext(snapshot, state);
|
|
1187
|
+
const perMessage = projected.map((message) => bytes([message]));
|
|
1188
|
+
const total = perMessage.reduce((sum, size) => sum + size, 0);
|
|
1189
|
+
const zoneEnd = threshold * total;
|
|
1190
|
+
const matureEnd = staleSpanMatureEnd(snapshot.messages);
|
|
1191
|
+
const snapshotIndexByCallId = new Map<string, number>();
|
|
1192
|
+
for (let index = 0; index < snapshot.messages.length; index += 1) {
|
|
1193
|
+
const message = snapshot.messages[index] as { role?: unknown; toolCallId?: unknown };
|
|
1194
|
+
if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
|
|
1195
|
+
snapshotIndexByCallId.set(message.toolCallId, index);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const already = new Set((state.clips ?? []).map((clip) => clip.callId));
|
|
1199
|
+
const additions: ToolClip[] = [];
|
|
1200
|
+
let offset = 0;
|
|
1201
|
+
for (let index = 0; index < projected.length; index += 1) {
|
|
1202
|
+
const start = offset;
|
|
1203
|
+
offset += perMessage[index];
|
|
1204
|
+
if (start >= zoneEnd) break;
|
|
1205
|
+
const message = projected[index] as {
|
|
1206
|
+
role?: unknown; toolCallId?: unknown; toolName?: unknown; details?: unknown;
|
|
1207
|
+
};
|
|
1208
|
+
if (message?.role !== "toolResult" || typeof message.toolCallId !== "string") continue;
|
|
1209
|
+
if (already.has(message.toolCallId)) continue;
|
|
1210
|
+
if (message.details && typeof (message.details as { projection?: unknown }).projection === "string") continue;
|
|
1211
|
+
if (typeof message.toolName === "string" && blacklist.has(message.toolName)) continue;
|
|
1212
|
+
const snapshotIndex = snapshotIndexByCallId.get(message.toolCallId);
|
|
1213
|
+
if (snapshotIndex === undefined || snapshotIndex >= matureEnd) continue;
|
|
1214
|
+
const ref = snapshot.mapped.find((item) => item.index === snapshotIndex)?.ref;
|
|
1215
|
+
if (!ref || refsProtected([ref], state, snapshot)) continue;
|
|
1216
|
+
if (contentText(message).length <= snapshot.policy.toolClipFloorChars) continue;
|
|
1217
|
+
additions.push({ callId: message.toolCallId, entryId: ref.entryId });
|
|
1218
|
+
}
|
|
1219
|
+
return additions;
|
|
1220
|
+
}
|