pi-fold 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,923 @@
1
+ import { objectRefKey } from "../json.ts";
2
+ import {
3
+ bytes,
4
+ clone,
5
+ messageRole,
6
+ } from "./canonical.ts";
7
+ import {
8
+ commitPreparedFold,
9
+ pinnedPeekMass,
10
+ prepareFold,
11
+ renderFold,
12
+ renderFoldParts,
13
+ setFoldProjectionState,
14
+ } from "./folding.ts";
15
+ import type { AutomaticRungSelection } from "./folding.ts";
16
+ import {
17
+ foldInterval,
18
+ hardFenceRatio,
19
+ orderedRoots,
20
+ refsProtected,
21
+ toolRefsProtected,
22
+ } from "./measurement.ts";
23
+ import {
24
+ flattenFoldRefs,
25
+ foldIdFor,
26
+ parsePendingMarks,
27
+ pendingMarkKey,
28
+ } from "./persistence.ts";
29
+ import { currentTurnRefKeys } from "./transcript.ts";
30
+ import {
31
+ EPOCH_COMMIT_TARGET_WINDOW_SHARE,
32
+ EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD,
33
+ EPOCH_MAX_TOPUP_MARKS,
34
+ ESTIMATED_BYTES_PER_TOKEN,
35
+ MAX_WEDGE_ABSORB_TOKENS,
36
+ ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES,
37
+ MAX_PENDING_MARKS,
38
+ MAX_UNMARKED_CANDIDATES,
39
+ } from "./policy.ts";
40
+ import type {
41
+ ActiveContextSnapshot,
42
+ ActiveContextState,
43
+ FoldCandidate,
44
+ MarkOrigin,
45
+ PendingFoldMark,
46
+ PendingMark,
47
+ } from "./policy.ts";
48
+ import {
49
+ automaticToolBrief,
50
+ candidateSourceRefs,
51
+ chapterRangeIsUnitAligned,
52
+ partsForRange,
53
+ spanBytes,
54
+ deterministicChapterCandidateBrief,
55
+ deterministicConsolidationBrief,
56
+ resultCall,
57
+ selectAutomaticToolBatch,
58
+ toolCallArguments,
59
+ } from "./selection.ts";
60
+
61
+ /**
62
+ * Two-phase fold scheduling.
63
+ *
64
+ * A provider prefix cache is positional: editing the middle of the window
65
+ * invalidates every byte after the edit, so per-turn folding pays a full uncached
66
+ * prefix over and over. Nothing here changes what a fold MEANS. A fold decision
67
+ * becomes a MARK, which moves no projection byte, and a later COMMIT applies every
68
+ * pending mark through the same preparation and commit machinery an immediate fold
69
+ * uses, in one rewrite.
70
+ *
71
+ * Every function is deterministic: transcript ordinals and positions only, never a
72
+ * wall clock and never randomness.
73
+ */
74
+
75
+ export function pendingMarks(state: Pick<ActiveContextState, "pendingMarks">): PendingMark[] {
76
+ return state.pendingMarks ? clone(state.pendingMarks) : [];
77
+ }
78
+
79
+ /**
80
+ * Property ORDER is load-bearing: the state digest is a stable stringify, so an
81
+ * appended key would replay as digest drift. Rebuild the tail of the record in the
82
+ * same order `parseActiveContextState` produces.
83
+ */
84
+ export function withPendingMarks(
85
+ state: ActiveContextState,
86
+ marks: readonly PendingMark[],
87
+ ): ActiveContextState {
88
+ const next = marks.length ? parsePendingMarks(clone(marks) as PendingMark[]) : [];
89
+ const head = { ...state };
90
+ delete head.surfacing;
91
+ delete head.pendingMarks;
92
+ delete head.advisory;
93
+ delete head.prepared;
94
+ return {
95
+ ...head,
96
+ ...(state.surfacing?.length ? { surfacing: clone(state.surfacing) } : {}),
97
+ ...(next.length ? { pendingMarks: next } : {}),
98
+ ...(state.advisory ? { advisory: clone(state.advisory) } : {}),
99
+ ...(state.prepared ? { prepared: clone(state.prepared) } : {}),
100
+ };
101
+ }
102
+
103
+ export interface MarkAddition {
104
+ state: ActiveContextState;
105
+ added: boolean;
106
+ reason: string | null;
107
+ }
108
+
109
+ /** Idempotent by mark key, bounded, and never silently dropping the oldest mark. */
110
+ export function addPendingMark(state: ActiveContextState, mark: PendingMark): MarkAddition {
111
+ const marks = pendingMarks(state);
112
+ if (marks.some((existing) => pendingMarkKey(existing) === pendingMarkKey(mark))) {
113
+ return { state, added: false, reason: `${mark.mark} mark ${mark.id} is already pending` };
114
+ }
115
+ if (marks.length >= MAX_PENDING_MARKS) {
116
+ return {
117
+ state,
118
+ added: false,
119
+ reason: `at most ${MAX_PENDING_MARKS} marks may be pending; commit the epoch first`,
120
+ };
121
+ }
122
+ return { state: withPendingMarks(state, [...marks, mark]), added: true, reason: null };
123
+ }
124
+
125
+ export function estimatedTokens(sizeInBytes: number): number {
126
+ return Math.max(0, Math.ceil(sizeInBytes / ESTIMATED_BYTES_PER_TOKEN));
127
+ }
128
+
129
+ /** Transcript-position ordering used for marks; identical to the surfacing ordinal. */
130
+ export function markOrdinal(snapshot: Pick<ActiveContextSnapshot, "mapped">): number {
131
+ return snapshot.mapped.length;
132
+ }
133
+
134
+ /**
135
+ * Estimated bytes a mark would remove from the projection. Fold marks use a
136
+ * placeholder estimate rather than a trial fold: the number is presentational, so
137
+ * paying a full preparation per mark on every status call is not worth exactness.
138
+ */
139
+ export function markFreedBytes(
140
+ snapshot: ActiveContextSnapshot,
141
+ state: ActiveContextState,
142
+ mark: PendingMark,
143
+ ): number {
144
+ if (mark.mark === "refold") {
145
+ const fold = state.folds.find((item) => item.id === mark.id);
146
+ if (!fold || !state.expanded.includes(mark.id)) return 0;
147
+ const expanded = renderFold(fold, state, snapshot);
148
+ const folded = renderFold(fold, {
149
+ ...state,
150
+ expanded: state.expanded.filter((id) => id !== mark.id),
151
+ }, snapshot);
152
+ return expanded && folded ? Math.max(0, bytes(expanded) - bytes(folded)) : 0;
153
+ }
154
+ const source = renderFoldParts(mark.parts, state, snapshot);
155
+ if (!source) return 0;
156
+ const placeholders = mark.kind === "tool-result" ? mark.parts.length : 1;
157
+ const placeholder = placeholders * (mark.brief.length + ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES);
158
+ return Math.max(0, bytes(source) - placeholder);
159
+ }
160
+
161
+ /** Whether a pending mark covers any evidence the current excursion just gathered. */
162
+ export function markTouchesCurrentTurn(
163
+ state: ActiveContextState,
164
+ mark: PendingMark,
165
+ currentTurn: ReadonlySet<string>,
166
+ ): boolean {
167
+ if (!currentTurn.size) return false;
168
+ const refs = mark.mark === "refold"
169
+ ? (() => {
170
+ const fold = state.folds.find((item) => item.id === mark.id);
171
+ return fold ? flattenFoldRefs(fold, state) : [];
172
+ })()
173
+ : candidateSourceRefs(mark.parts, state);
174
+ return refs.some((ref) => currentTurn.has(objectRefKey(ref)));
175
+ }
176
+
177
+ /**
178
+ * How many of the newest guarded marks the pressure waiver still protects. The agent's
179
+ * MOST RECENT reads are the ones an in-flight excursion is about to work from, so they
180
+ * are the last thing surrendered; everything older is fair game once the window is the
181
+ * bigger risk.
182
+ */
183
+ export const GUARD_WAIVER_PROTECTED_MARKS = 2;
184
+
185
+ /**
186
+ * How many marks the guard must be holding before the pressure waiver releases any.
187
+ * Without a floor the waiver degrades into exactly the per-pass folding epoch
188
+ * scheduling exists to prevent: the batch drains, one new mark is guarded on the next
189
+ * pass, and releasing that one mark buys a whole prefix invalidation. Below the fence
190
+ * the waiver only ever releases a BATCH. The fence itself ignores this floor.
191
+ */
192
+ export const GUARD_WAIVER_MINIMUM_MARKS = 4;
193
+
194
+ /**
195
+ * Survivability outranks the guard.
196
+ *
197
+ * The current-turn guard is correct while there is room to wait, and fatal once there
198
+ * is not: a workload that never emits a terminal assistant message has ONE turn, so
199
+ * the boundary never advances and every mark is guarded forever. Measured 2026-08-06
200
+ * (rep 11): 58 assistant messages, all stopReason "toolUse", ZERO terminal ones, so
201
+ * the automatic commit applied nothing all run; the only marks that ever landed came
202
+ * from the agent's own two explicit commits. The window reached 359,625 tokens and the
203
+ * next request went out at ~458k estimated and was rejected by the provider.
204
+ *
205
+ * So: the guard holds in full while the commit has other work. It is waived only when
206
+ * it would STARVE the commit -- no other mark could be applied -- and pressure has
207
+ * reached the backstop, and then only for the OLDEST guarded marks. At the hard fence
208
+ * every guarded mark is waived, because the only useful action left is the fold that
209
+ * keeps the request transmissible.
210
+ */
211
+ export function guardWaiverCount(input: {
212
+ snapshot: ActiveContextSnapshot;
213
+ ratio: number | null;
214
+ guardedMarks: number;
215
+ /** Marks this commit can apply WITHOUT the waiver. While any exist, the guard holds. */
216
+ otherApplicableMarks: number;
217
+ }): number {
218
+ const { snapshot, ratio, guardedMarks, otherApplicableMarks } = input;
219
+ if (guardedMarks <= 0 || typeof ratio !== "number" || !Number.isFinite(ratio)) return 0;
220
+ if (ratio >= hardFenceRatio(snapshot)) return guardedMarks;
221
+ if (ratio < snapshot.policy.refoldRatio || otherApplicableMarks > 0) return 0;
222
+ if (guardedMarks < GUARD_WAIVER_MINIMUM_MARKS) return 0;
223
+ return Math.max(1, guardedMarks - GUARD_WAIVER_PROTECTED_MARKS);
224
+ }
225
+
226
+ export type MarkEligibility = "eligible" | "protected" | "unfulfillable";
227
+
228
+ /**
229
+ * Whether a pending mark can be applied right now.
230
+ *
231
+ * "protected" is a WAITING state, not a failure: the span is fresh or the agent
232
+ * protected it, and both conditions expire. "unfulfillable" is terminal: the evidence
233
+ * or the fold the mark names has left the branch, so no later commit can honour it.
234
+ * Collapsing the two is what made a refusal look like a drop.
235
+ */
236
+ export function markEligibility(
237
+ snapshot: ActiveContextSnapshot,
238
+ state: ActiveContextState,
239
+ mark: PendingMark,
240
+ ): MarkEligibility {
241
+ if (mark.mark === "refold") {
242
+ const fold = state.folds.find((item) => item.id === mark.id);
243
+ if (!fold || !foldInterval(fold, state, snapshot)) return "unfulfillable";
244
+ return state.expanded.includes(mark.id) ? "eligible" : "unfulfillable";
245
+ }
246
+ const refs = candidateSourceRefs(mark.parts, state);
247
+ if (!refs.length) return "unfulfillable";
248
+ const mapped = new Set(snapshot.mapped.flatMap((item) => item.ref ? [objectRefKey(item.ref)] : []));
249
+ if (refs.some((ref) => !mapped.has(objectRefKey(ref)))) return "unfulfillable";
250
+ const blocked = mark.kind === "tool-result"
251
+ ? toolRefsProtected(refs, state, snapshot)
252
+ : refsProtected(refs, state, snapshot);
253
+ return blocked ? "protected" : "eligible";
254
+ }
255
+
256
+ export interface MarkAccounting {
257
+ pending: number;
258
+ agentMarks: number;
259
+ ladderMarks: number;
260
+ freedBytes: number;
261
+ freedTokens: number;
262
+ freedWindowShare: number;
263
+ rewriteTokens: number;
264
+ /** Marks a commit could apply right now. */
265
+ eligibleMarks: number;
266
+ /** Marks whose span is still fresh or protected, waiting rather than lost. */
267
+ retainedMarks: number;
268
+ eligibleFreedBytes: number;
269
+ eligibleFreedTokens: number;
270
+ /** The ROI signal: eligible marked mass as a share of the truthful window. */
271
+ eligibleFreedWindowShare: number;
272
+ /** Raw peek mass no reclamation can take: pinned reads and protected results. */
273
+ pinnedBytes: number;
274
+ /** How many peek results that mass is spread over. */
275
+ pinnedResults: number;
276
+ }
277
+
278
+ /**
279
+ * What a commit of the current marks would cost and free. `rewriteTokens` is the
280
+ * prefix a commit invalidates: everything from the earliest marked message to the
281
+ * end of the window, which is exactly what a positional cache must re-send.
282
+ */
283
+ export function markAccounting(
284
+ snapshot: ActiveContextSnapshot,
285
+ state: ActiveContextState,
286
+ ): MarkAccounting {
287
+ const marks = pendingMarks(state);
288
+ const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
289
+ item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
290
+ let freedBytes = 0;
291
+ let eligibleFreedBytes = 0;
292
+ let eligibleMarks = 0;
293
+ let retainedMarks = 0;
294
+ let earliest = -1;
295
+ for (const mark of marks) {
296
+ const freed = markFreedBytes(snapshot, state, mark);
297
+ freedBytes += freed;
298
+ const eligibility = markEligibility(snapshot, state, mark);
299
+ if (eligibility === "eligible") {
300
+ eligibleMarks += 1;
301
+ eligibleFreedBytes += freed;
302
+ } else if (eligibility === "protected") retainedMarks += 1;
303
+ const fold = mark.mark === "refold" ? state.folds.find((item) => item.id === mark.id) : null;
304
+ const refs = mark.mark === "refold"
305
+ ? (fold ? flattenFoldRefs(fold, state) : [])
306
+ : candidateSourceRefs(mark.parts, state);
307
+ for (const ref of refs) {
308
+ const index = indexByKey.get(objectRefKey(ref));
309
+ if (index === undefined) continue;
310
+ if (earliest < 0 || index < earliest) earliest = index;
311
+ }
312
+ }
313
+ const rewriteBytes = earliest < 0
314
+ ? 0
315
+ : bytes(snapshot.messages.slice(earliest));
316
+ const freedTokens = estimatedTokens(freedBytes);
317
+ const eligibleFreedTokens = estimatedTokens(eligibleFreedBytes);
318
+ const pinned = pinnedPeekMass(snapshot, state);
319
+ return {
320
+ pending: marks.length,
321
+ agentMarks: marks.filter((mark) => mark.origin === "agent").length,
322
+ ladderMarks: marks.filter((mark) => mark.origin === "ladder").length,
323
+ freedBytes,
324
+ freedTokens,
325
+ freedWindowShare: snapshot.contextWindow > 0 ? freedTokens / snapshot.contextWindow : 0,
326
+ rewriteTokens: estimatedTokens(rewriteBytes),
327
+ eligibleMarks,
328
+ retainedMarks,
329
+ eligibleFreedBytes,
330
+ eligibleFreedTokens,
331
+ eligibleFreedWindowShare: snapshot.contextWindow > 0
332
+ ? eligibleFreedTokens / snapshot.contextWindow
333
+ : 0,
334
+ pinnedBytes: pinned.bytes,
335
+ pinnedResults: pinned.results,
336
+ };
337
+ }
338
+
339
+ /**
340
+ * The commit trigger.
341
+ *
342
+ * The ROI half asks the economic question: commit when the marks that could apply
343
+ * RIGHT NOW would free enough to pay for the rewrite. The pressure half reuses the
344
+ * ladder's own refold threshold and stays underneath as the SAFETY backstop -- it
345
+ * fires when the window is nearly full, which is the worst moment to discover the
346
+ * marks are worth nothing (the rep4 abort fired there with zero pending marks). The
347
+ * agent's own commit is authoritative and immediate regardless of either.
348
+ */
349
+ export function epochCommitDue(
350
+ snapshot: ActiveContextSnapshot,
351
+ ratio: number | null,
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;
357
+ }
358
+
359
+ export function foldMarkFor(input: {
360
+ candidate: FoldCandidate;
361
+ brief: string;
362
+ briefProvenance: PendingFoldMark["briefProvenance"];
363
+ origin: MarkOrigin;
364
+ ordinal: number;
365
+ }): PendingFoldMark {
366
+ const { candidate } = input;
367
+ return {
368
+ mark: "fold",
369
+ // The same identity the committed fold will get, so a repeated decision is
370
+ // inert rather than a second fold of the same span.
371
+ id: foldIdFor(candidate.kind, candidate.parts),
372
+ kind: candidate.kind,
373
+ parts: clone(candidate.parts),
374
+ brief: input.brief,
375
+ briefProvenance: clone(input.briefProvenance),
376
+ origin: input.origin,
377
+ ordinal: input.ordinal,
378
+ };
379
+ }
380
+
381
+ /** The deterministic brief a ladder decision would have folded with. */
382
+ export function ladderBrief(
383
+ snapshot: ActiveContextSnapshot,
384
+ state: ActiveContextState,
385
+ candidate: FoldCandidate,
386
+ ): string {
387
+ if (candidate.kind === "tool-result") return automaticToolBrief(snapshot, candidate);
388
+ if (candidate.kind === "consolidation") return deterministicConsolidationBrief(candidate, state);
389
+ return deterministicChapterCandidateBrief(snapshot, candidate);
390
+ }
391
+
392
+ /**
393
+ * Turn one automatic rung selection into a mark. Prepared chapters are excluded on
394
+ * purpose: that rung only fires at the hard provider fence, which is already inside
395
+ * a commit epoch, and deferring it would strand a model-generated brief.
396
+ */
397
+ export function ladderSelectionMark(input: {
398
+ snapshot: ActiveContextSnapshot;
399
+ state: ActiveContextState;
400
+ selection: AutomaticRungSelection;
401
+ ordinal: number;
402
+ }): PendingMark | null {
403
+ const { selection } = input;
404
+ if (selection.kind === "refold") {
405
+ return { mark: "refold", id: selection.foldId, origin: "ladder", ordinal: input.ordinal };
406
+ }
407
+ if (selection.kind !== "tool" && selection.kind !== "chapter" && selection.kind !== "consolidation") {
408
+ return null;
409
+ }
410
+ return foldMarkFor({
411
+ candidate: selection.candidate,
412
+ brief: ladderBrief(input.snapshot, input.state, selection.candidate),
413
+ briefProvenance: { kind: "deterministic" },
414
+ origin: "ladder",
415
+ ordinal: input.ordinal,
416
+ });
417
+ }
418
+
419
+ /**
420
+ * Peek is the ephemeral read: look, decide, discard. Its own tool result is a
421
+ * completed read batch, so at the next commit it folds away automatically, unless
422
+ * the agent committed to the fold it peeked by expanding or protecting it.
423
+ */
424
+ export function ephemeralPeekMarks(input: {
425
+ snapshot: ActiveContextSnapshot;
426
+ state: ActiveContextState;
427
+ ordinal: number;
428
+ }): PendingFoldMark[] {
429
+ const { snapshot, state } = input;
430
+ const claimed = claimedRefKeys(state);
431
+ const groups = new Map<number, Array<{ index: number; call: NonNullable<ReturnType<typeof resultCall>> }>>();
432
+ for (const item of snapshot.mapped) {
433
+ if (messageRole(item.message) !== "toolResult" || !item.ref) continue;
434
+ const call = resultCall(snapshot, item.index, true);
435
+ if (!call || call.name !== snapshot.toolName) continue;
436
+ const group = groups.get(call.assistantIndex) ?? [];
437
+ group.push({ index: item.index, call });
438
+ groups.set(call.assistantIndex, group);
439
+ }
440
+ const marks: PendingFoldMark[] = [];
441
+ for (const group of [...groups.entries()].sort(([left], [right]) => left - right)) {
442
+ const entries = group[1];
443
+ const expected = entries[0].call.batch;
444
+ const ids = new Set(entries.map(({ call }) => call.id));
445
+ 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
+ const refs = entries
463
+ .sort((left, right) => left.index - right.index)
464
+ .map(({ index }) => snapshot.mapped[index].ref!);
465
+ if (refs.some((ref) => claimed.has(objectRefKey(ref))) ||
466
+ refs.length > snapshot.policy.maxFoldSourceRefs ||
467
+ toolRefsProtected(refs, state, snapshot)) continue;
468
+ const candidate: FoldCandidate = {
469
+ kind: "tool-result",
470
+ parts: refs.map((ref) => ({ kind: "raw" as const, ref })),
471
+ sourceRefs: refs,
472
+ };
473
+ marks.push(foldMarkFor({
474
+ candidate,
475
+ brief: automaticToolBrief(snapshot, candidate),
476
+ briefProvenance: { kind: "deterministic" },
477
+ // A peek is an agent-initiated read; attributing its disposal to the ladder
478
+ // would deflate the agent-vs-ladder mark ratio the adjudication reads.
479
+ origin: "agent",
480
+ ordinal: input.ordinal,
481
+ }));
482
+ }
483
+ return marks;
484
+ }
485
+
486
+ function peekedFoldProtected(
487
+ snapshot: ActiveContextSnapshot,
488
+ state: ActiveContextState,
489
+ foldId: string,
490
+ ): boolean {
491
+ const fold = state.folds.find((item) => item.id === foldId);
492
+ if (!fold) return false;
493
+ const refs = flattenFoldRefs(fold, state);
494
+ return fold.kind === "tool-result"
495
+ ? toolRefsProtected(refs, state, snapshot)
496
+ : refsProtected(refs, state, snapshot);
497
+ }
498
+
499
+ /** Every evidence key already spoken for by a fold or a pending fold mark. */
500
+ export function claimedRefKeys(state: ActiveContextState): Set<string> {
501
+ const keys = new Set<string>();
502
+ for (const fold of state.folds) {
503
+ for (const part of fold.parts) if (part.kind === "raw") keys.add(objectRefKey(part.ref));
504
+ }
505
+ for (const mark of pendingMarks(state)) {
506
+ if (mark.mark !== "fold") continue;
507
+ for (const ref of candidateSourceRefs(mark.parts, state)) keys.add(objectRefKey(ref));
508
+ }
509
+ return keys;
510
+ }
511
+
512
+ /** What is still on the table after the marks in hand: the steering number and its parts. */
513
+ export interface UnmarkedRemainder {
514
+ /** Stale tool results outside the fresh tail that no fold and no pending mark owns. */
515
+ spans: number;
516
+ tokens: number;
517
+ /** Unmarked stale tokens as a share of the non-fresh window. The steering number. */
518
+ share: number;
519
+ /** The largest few, by reclaim value, so the next batch is one read away. */
520
+ candidates: Array<{ id: string; tokens: number }>;
521
+ }
522
+
523
+ /**
524
+ * The unmarked remainder, as an AGGREGATE plus a bounded head.
525
+ *
526
+ * An exhaustive list is what the status index already is, and re-rendering it is what
527
+ * this build removed from the projection. What an agent needs to decide the NEXT batch
528
+ * is one percentage and the few largest names; everything else is a total.
529
+ */
530
+ export function unmarkedRemainder(
531
+ snapshot: ActiveContextSnapshot,
532
+ state: ActiveContextState,
533
+ charsPerToken: number,
534
+ limit = MAX_UNMARKED_CANDIDATES,
535
+ ): UnmarkedRemainder {
536
+ const perToken = Number.isFinite(charsPerToken) && charsPerToken > 0 ? charsPerToken : ESTIMATED_BYTES_PER_TOKEN;
537
+ const claimed = claimedRefKeys(state);
538
+ const candidates: Array<{ id: string; tokens: number }> = [];
539
+ let unmarkedBytes = 0;
540
+ let staleBytes = 0;
541
+ for (const item of snapshot.mapped) {
542
+ if (!item.ref || messageRole(item.message) !== "toolResult") continue;
543
+ if (snapshot.toolProtectedIndices.has(item.index)) continue;
544
+ const size = bytes(item.message);
545
+ staleBytes += size;
546
+ if (claimed.has(objectRefKey(item.ref))) continue;
547
+ unmarkedBytes += size;
548
+ candidates.push({ id: item.ref.entryId, tokens: Math.ceil(size / perToken) });
549
+ }
550
+ candidates.sort((left, right) => right.tokens - left.tokens || (left.id < right.id ? -1 : 1));
551
+ return {
552
+ spans: candidates.length,
553
+ tokens: Math.ceil(unmarkedBytes / perToken),
554
+ share: staleBytes > 0 ? unmarkedBytes / staleBytes : 0,
555
+ candidates: candidates.slice(0, Math.max(0, limit)),
556
+ };
557
+ }
558
+
559
+ /** Every fold id already spoken for by a pending refold mark. */
560
+ export function markedFoldIds(state: ActiveContextState): Set<string> {
561
+ const ids = new Set<string>();
562
+ for (const mark of pendingMarks(state)) if (mark.mark === "refold") ids.add(mark.id);
563
+ return ids;
564
+ }
565
+
566
+ /**
567
+ * Agent judgment leads; automation guarantees the floor. If the marks the agent
568
+ * made would free less than the target share of the window, the ladder adds the
569
+ * stalest unprotected eligible tool batches until they do.
570
+ */
571
+ export function topUpMarks(input: {
572
+ snapshot: ActiveContextSnapshot;
573
+ state: ActiveContextState;
574
+ ordinal: number;
575
+ targetShare?: number;
576
+ /** Evidence keys the top-up may never propose, e.g. the current excursion's reads. */
577
+ excludeRefKeys?: ReadonlySet<string>;
578
+ /** Measure progress against ELIGIBLE mass, so pinned or protected marks never count. */
579
+ eligibleOnly?: boolean;
580
+ }): PendingFoldMark[] {
581
+ const { snapshot } = input;
582
+ const target = input.targetShare ?? EPOCH_COMMIT_TARGET_WINDOW_SHARE;
583
+ const claimed = claimedRefKeys(input.state);
584
+ for (const key of input.excludeRefKeys ?? []) claimed.add(key);
585
+ const marks: PendingFoldMark[] = [];
586
+ let state = input.state;
587
+ const progress = (value: ActiveContextState): number => {
588
+ const accounting = markAccounting(snapshot, value);
589
+ return input.eligibleOnly ? accounting.eligibleFreedWindowShare : accounting.freedWindowShare;
590
+ };
591
+ let share = progress(state);
592
+ for (let attempt = 0; attempt < EPOCH_MAX_TOPUP_MARKS && share < target; attempt += 1) {
593
+ const candidate = selectAutomaticToolBatch(snapshot, state, 1, claimed)[0];
594
+ if (!candidate) break;
595
+ const mark = foldMarkFor({
596
+ candidate,
597
+ brief: automaticToolBrief(snapshot, candidate),
598
+ briefProvenance: { kind: "deterministic" },
599
+ origin: "ladder",
600
+ ordinal: input.ordinal,
601
+ });
602
+ const addition = addPendingMark(state, mark);
603
+ if (!addition.added) break;
604
+ state = addition.state;
605
+ marks.push(mark);
606
+ for (const ref of candidate.sourceRefs) claimed.add(objectRefKey(ref));
607
+ share = progress(state);
608
+ }
609
+ return marks;
610
+ }
611
+
612
+ /** One sliver a commit swallowed into its later neighbour, reported never inferred. */
613
+ export interface AbsorbedWedge {
614
+ /** The mark that grew backward to cover it. */
615
+ intoMarkId: string;
616
+ startId: string;
617
+ endId: string;
618
+ entries: number;
619
+ tokens: number;
620
+ }
621
+
622
+ /**
623
+ * Wedge absorption. THE ANTI-LCM PIN LIVES HERE.
624
+ *
625
+ * A sliver of stale raw content hugging a fold boundary is a crumb nothing will ever
626
+ * reclaim: it is below the minimum chapter size, so no chapter rung will take it, and
627
+ * it is not a tool batch, so no tool rung will either. Absorbing it costs nothing
628
+ * because it happens INSIDE a commit the epoch has already paid for -- it grows a
629
+ * pending mark backward rather than performing a mutation of its own.
630
+ *
631
+ * What it must never do is erode deliberate non-sequential curation. An agent holding
632
+ * folds at 10:20, 40:55 and 60:70 with raw spans between them chose that shape, and a
633
+ * mechanism that swallowed those gaps would delete the curation element outright. So
634
+ * the threshold is tiny, measured in the session's own calibrated tokens, and NOT
635
+ * pressure-scaled: above it, a gap stays raw permanently no matter how full the window
636
+ * gets or how ragged the projection looks.
637
+ */
638
+ export function absorbWedgeMarks(input: {
639
+ snapshot: ActiveContextSnapshot;
640
+ state: ActiveContextState;
641
+ /** The session's measured serialized chars per token; never a fixed constant. */
642
+ charsPerToken: number;
643
+ /** Evidence absorption may never touch, e.g. the current excursion's own reads. */
644
+ excludeRefKeys?: ReadonlySet<string>;
645
+ maxTokens?: number;
646
+ }): { state: ActiveContextState; absorbed: AbsorbedWedge[] } {
647
+ const { snapshot } = input;
648
+ const maxTokens = input.maxTokens ?? MAX_WEDGE_ABSORB_TOKENS;
649
+ const charsPerToken = Number.isFinite(input.charsPerToken) && input.charsPerToken > 0
650
+ ? input.charsPerToken
651
+ : ESTIMATED_BYTES_PER_TOKEN;
652
+ const exclude = input.excludeRefKeys ?? new Set<string>();
653
+ let state = input.state;
654
+ const absorbed: AbsorbedWedge[] = [];
655
+ const indexByKey = new Map(snapshot.mapped.flatMap((item) =>
656
+ item.ref ? [[objectRefKey(item.ref), item.index] as const] : []));
657
+ const markInterval = (mark: PendingFoldMark): { start: number; end: number } | null => {
658
+ const indices = candidateSourceRefs(mark.parts, state)
659
+ .map((ref) => indexByKey.get(objectRefKey(ref)))
660
+ .filter((index): index is number => index !== undefined);
661
+ return indices.length ? { start: Math.min(...indices), end: Math.max(...indices) } : null;
662
+ };
663
+ // One pass, oldest first. Each absorption rewrites exactly one mark, and a mark that
664
+ // has already grown is not a candidate to grow again in the same commit.
665
+ for (let guard = 0; guard < MAX_PENDING_MARKS; guard += 1) {
666
+ const marks = pendingMarks(state);
667
+ const occupied = [
668
+ ...orderedRoots(state, snapshot).map((root) => ({ start: root.start, end: root.end })),
669
+ ...marks.flatMap((mark) => {
670
+ if (mark.mark !== "fold") return [];
671
+ const interval = markInterval(mark);
672
+ return interval ? [interval] : [];
673
+ }),
674
+ ].sort((left, right) => left.start - right.start);
675
+ let applied = false;
676
+ for (const mark of marks) {
677
+ if (mark.mark !== "fold") continue;
678
+ const interval = markInterval(mark);
679
+ if (!interval) continue;
680
+ const previous = occupied.filter((item) => item.end < interval.start).at(-1);
681
+ const gapStart = previous ? previous.end + 1 : -1;
682
+ const gapEnd = interval.start - 1;
683
+ if (gapStart < 0 || gapEnd < gapStart) continue;
684
+ if (absorbed.some((item) => item.intoMarkId === mark.id)) continue;
685
+ const gapTokens = Math.ceil(spanBytes(snapshot, gapStart, gapEnd + 1) / charsPerToken);
686
+ // The whole discriminator, in one line: above this, the gap is the agent's.
687
+ if (gapTokens > maxTokens) continue;
688
+ const gapRefs = [];
689
+ let usable = true;
690
+ for (let index = gapStart; index <= gapEnd; index += 1) {
691
+ const ref = snapshot.mapped[index]?.ref;
692
+ if (!ref || exclude.has(objectRefKey(ref))) { usable = false; break; }
693
+ gapRefs.push(ref);
694
+ }
695
+ if (!usable || !gapRefs.length) continue;
696
+ if (refsProtected(gapRefs, state, snapshot)) continue;
697
+ // Structural safety: the grown span must still be a valid closed chapter, which
698
+ // is what keeps every tool call paired with its result in the projection.
699
+ if (!chapterRangeIsUnitAligned(snapshot, gapStart, interval.end)) continue;
700
+ const parts = partsForRange(snapshot, state, gapStart, interval.end, new Set<FoldKind>(["tool-result"]));
701
+ if (!parts) continue;
702
+ const grown = foldMarkFor({
703
+ candidate: { kind: "chapter", parts, sourceRefs: candidateSourceRefs(parts, state) },
704
+ brief: `${mark.brief} It also holds ${gapRefs.length} short adjacent entry(s) absorbed at commit.`
705
+ .slice(0, snapshot.policy.maxBriefChars),
706
+ briefProvenance: mark.briefProvenance,
707
+ origin: mark.origin,
708
+ ordinal: mark.ordinal,
709
+ });
710
+ const kept = pendingMarks(state).filter((item) => pendingMarkKey(item) !== pendingMarkKey(mark));
711
+ if (kept.some((item) => pendingMarkKey(item) === pendingMarkKey(grown))) continue;
712
+ state = withPendingMarks(state, [...kept, grown]);
713
+ absorbed.push({
714
+ intoMarkId: grown.id,
715
+ startId: gapRefs[0].entryId,
716
+ endId: gapRefs.at(-1)!.entryId,
717
+ entries: gapRefs.length,
718
+ tokens: gapTokens,
719
+ });
720
+ applied = true;
721
+ break;
722
+ }
723
+ if (!applied) break;
724
+ }
725
+ return { state, absorbed };
726
+ }
727
+
728
+ export interface AppliedMark {
729
+ mark: PendingMark["mark"];
730
+ id: string;
731
+ origin: MarkOrigin;
732
+ foldId: string;
733
+ }
734
+
735
+ export interface RefusedMark {
736
+ mark: PendingMark["mark"];
737
+ id: string;
738
+ origin: MarkOrigin;
739
+ reason: string;
740
+ /** Whether the mark survives as pending rather than being discarded. */
741
+ retained: boolean;
742
+ }
743
+
744
+ export interface CommitEpochResult {
745
+ state: ActiveContextState;
746
+ applied: AppliedMark[];
747
+ refused: RefusedMark[];
748
+ retained: PendingMark[];
749
+ /** Guarded marks the pressure waiver released into this commit, oldest first. */
750
+ waived: PendingMark[];
751
+ }
752
+
753
+ /**
754
+ * Apply every pending mark. This is the ONE mutation of the epoch: it reuses
755
+ * prepareFold/commitPreparedFold exactly as the immediate path does, so a committed
756
+ * mark and an immediate fold produce the same fold record.
757
+ */
758
+ export async function commitPendingMarks(input: {
759
+ snapshot: ActiveContextSnapshot;
760
+ state: ActiveContextState;
761
+ generation: number;
762
+ now?: () => number;
763
+ /**
764
+ * Keep marks the commit could not apply YET. A mark is a standing decision; dropping
765
+ * it because the span happened to be fresh at commit time silently discards the
766
+ * agent's judgment and leaves nothing for the next commit to act on.
767
+ */
768
+ retainIneligible?: boolean;
769
+ /**
770
+ * Never fold evidence the CURRENT excursion gathered. A commit that runs between
771
+ * an agent's read and its use of that read hands the agent a window where its own
772
+ * just-gathered evidence is a placeholder. Such marks stay pending for the next
773
+ * commit rather than being applied or dropped.
774
+ */
775
+ guardCurrentTurn?: boolean;
776
+ /**
777
+ * How many guarded marks the pressure waiver releases anyway, oldest first. The
778
+ * guard protects an in-flight excursion; it may never cost the session its ability
779
+ * to send a request at all. See `guardWaiverCount`.
780
+ */
781
+ guardWaiver?: number;
782
+ }): Promise<CommitEpochResult> {
783
+ const marks = pendingMarks(input.state);
784
+ const retained: PendingMark[] = [];
785
+ const waived: PendingMark[] = [];
786
+ const guardReasons = new Map<string, string>();
787
+ const currentTurn = input.guardCurrentTurn
788
+ ? currentTurnRefKeys(input.snapshot)
789
+ : new Set<string>();
790
+ if (input.guardCurrentTurn) {
791
+ const applicable: PendingMark[] = [];
792
+ const guarded: PendingMark[] = [];
793
+ for (const mark of marks) {
794
+ if (markTouchesCurrentTurn(input.state, mark, currentTurn)) guarded.push(mark);
795
+ else applicable.push(mark);
796
+ }
797
+ // Oldest first: the newest reads are the ones the excursion is about to use, so
798
+ // they are the last evidence the waiver surrenders.
799
+ guarded.sort((left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id));
800
+ const waiverCount = Math.max(0, Math.min(guarded.length, Math.trunc(input.guardWaiver ?? 0)));
801
+ for (const [index, mark] of guarded.entries()) {
802
+ if (index < waiverCount) {
803
+ waived.push(mark);
804
+ applicable.push(mark);
805
+ continue;
806
+ }
807
+ retained.push(mark);
808
+ guardReasons.set(
809
+ pendingMarkKey(mark),
810
+ "evidence was gathered in the current turn; the mark stays pending until the turn closes",
811
+ );
812
+ }
813
+ marks.length = 0;
814
+ marks.push(...applicable);
815
+ }
816
+ if (input.retainIneligible) {
817
+ const applicable: PendingMark[] = [];
818
+ for (const mark of marks) {
819
+ if (markEligibility(input.snapshot, input.state, mark) === "protected") retained.push(mark);
820
+ else applicable.push(mark);
821
+ }
822
+ marks.length = 0;
823
+ marks.push(...applicable);
824
+ }
825
+ let state = withPendingMarks(input.state, retained);
826
+ const applied: AppliedMark[] = [];
827
+ const refused: RefusedMark[] = retained.map((mark) => ({
828
+ mark: mark.mark,
829
+ id: mark.id,
830
+ origin: mark.origin,
831
+ reason: guardReasons.get(pendingMarkKey(mark)) ??
832
+ "span is still fresh or protected; the mark stays pending until it is eligible",
833
+ retained: true,
834
+ }));
835
+ // Oldest first, then by id: a chapter mark that absorbs an earlier tool-result
836
+ // mark must find that child already folded, which is transcript order.
837
+ const ordered = [...marks].sort((left, right) =>
838
+ left.ordinal - right.ordinal || left.id.localeCompare(right.id));
839
+ for (const mark of ordered) {
840
+ try {
841
+ if (mark.mark === "refold") {
842
+ const fold = state.folds.find((item) => item.id === mark.id);
843
+ if (!fold || !foldInterval(fold, state, input.snapshot)) {
844
+ throw new Error(`fold ${mark.id} is no longer active in the context event`);
845
+ }
846
+ if (!state.expanded.includes(mark.id)) throw new Error(`fold ${mark.id} is already folded`);
847
+ state = setFoldProjectionState(state, mark.id, "folded");
848
+ applied.push({ mark: "refold", id: mark.id, origin: mark.origin, foldId: mark.id });
849
+ continue;
850
+ }
851
+ const parts = clone(mark.parts);
852
+ const sourceRefs = candidateSourceRefs(parts, state);
853
+ const blocked = mark.kind === "tool-result"
854
+ ? toolRefsProtected(sourceRefs, state, input.snapshot)
855
+ : refsProtected(sourceRefs, state, input.snapshot);
856
+ if (blocked) {
857
+ throw new Error(
858
+ `pending fold mark ${mark.id} covers protected or fresh evidence; ` +
859
+ "unprotect that evidence and mark it again",
860
+ );
861
+ }
862
+ const prepared = await prepareFold({
863
+ candidate: { kind: mark.kind, parts, sourceRefs },
864
+ snapshot: input.snapshot,
865
+ state,
866
+ generation: input.generation,
867
+ brief: mark.brief,
868
+ briefProvenance: mark.briefProvenance,
869
+ now: input.now,
870
+ });
871
+ state = commitPreparedFold({
872
+ prepared,
873
+ snapshot: input.snapshot,
874
+ state,
875
+ generation: input.generation,
876
+ });
877
+ applied.push({ mark: "fold", id: mark.id, origin: mark.origin, foldId: prepared.id });
878
+ } catch (error) {
879
+ refused.push({
880
+ mark: mark.mark,
881
+ id: mark.id,
882
+ origin: mark.origin,
883
+ reason: error instanceof Error ? error.message : String(error),
884
+ retained: false,
885
+ });
886
+ }
887
+ }
888
+ return { state, applied, refused, retained, waived };
889
+ }
890
+
891
+ /** The scheduling block reported by the status action. */
892
+ export function schedulingStatus(input: {
893
+ snapshot: ActiveContextSnapshot;
894
+ state: ActiveContextState;
895
+ mode: string;
896
+ ratio: number | null;
897
+ }): Record<string, unknown> {
898
+ const accounting = markAccounting(input.snapshot, input.state);
899
+ const threshold = EPOCH_ELIGIBLE_SHARE_COMMIT_THRESHOLD;
900
+ return {
901
+ mode: input.mode,
902
+ ...accounting,
903
+ targetWindowShare: EPOCH_COMMIT_TARGET_WINDOW_SHARE,
904
+ commitDue: epochCommitDue(input.snapshot, input.ratio, accounting.eligibleFreedWindowShare),
905
+ commitTrigger: {
906
+ mode: "eligible-share",
907
+ eligibleShareThreshold: threshold,
908
+ eligibleShare: accounting.eligibleFreedWindowShare,
909
+ backstopRatio: input.snapshot.policy.refoldRatio,
910
+ pressureDue: epochCommitDue(input.snapshot, input.ratio),
911
+ roiDue: accounting.eligibleFreedWindowShare >= threshold,
912
+ },
913
+ commitRatio: input.snapshot.policy.refoldRatio,
914
+ marks: pendingMarks(input.state).map((mark) => ({
915
+ mark: mark.mark,
916
+ id: mark.id,
917
+ origin: mark.origin,
918
+ ordinal: mark.ordinal,
919
+ eligibility: markEligibility(input.snapshot, input.state, mark),
920
+ ...(mark.mark === "fold" ? { kind: mark.kind, brief: mark.brief } : {}),
921
+ })),
922
+ };
923
+ }