pi-fold 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -40
- package/extensions/active-context.ts +2374 -600
- package/extensions/evidence.js +15 -2
- package/extensions/index.js +69 -21
- package/extensions/json.ts +33 -1
- package/extensions/lib/curation.ts +141 -139
- package/extensions/lib/folding.ts +418 -74
- package/extensions/lib/instrumentation.ts +24 -4
- package/extensions/lib/measurement.ts +92 -16
- package/extensions/lib/persistence.ts +212 -42
- package/extensions/lib/policy.ts +511 -155
- package/extensions/lib/rollback.ts +216 -0
- package/extensions/lib/scheduling.ts +414 -106
- package/extensions/lib/selection.ts +528 -144
- package/extensions/lib/surfacing.ts +479 -224
- package/extensions/lib/tool-surface.ts +25 -5
- package/extensions/lib/transcript.ts +146 -62
- package/extensions/summarizer.js +206 -10
- package/package.json +1 -1
- package/extensions/lib/advisory.ts +0 -309
|
@@ -16,17 +16,17 @@ import {
|
|
|
16
16
|
orderedRoots,
|
|
17
17
|
refsInOrder,
|
|
18
18
|
refsProtected,
|
|
19
|
-
toolFoldCadence,
|
|
20
19
|
toolRefsProtected,
|
|
21
20
|
visibleCollapsedRoots,
|
|
22
21
|
} from "./measurement.ts";
|
|
23
22
|
import {
|
|
23
|
+
directFoldOwners,
|
|
24
24
|
flattenFoldRefs,
|
|
25
|
+
foldBrief,
|
|
25
26
|
foldMap,
|
|
26
27
|
} from "./persistence.ts";
|
|
27
28
|
import {
|
|
28
29
|
ACTIVE_CONTEXT_POLICY,
|
|
29
|
-
CONSOLIDATION_WIDTH_THRESHOLD,
|
|
30
30
|
DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
31
31
|
MAX_FOLD_SPAN_CHARS,
|
|
32
32
|
} from "./policy.ts";
|
|
@@ -88,7 +88,7 @@ export function resultCallIndex(snapshot: ActiveContextSnapshot, allowConsumedIn
|
|
|
88
88
|
turn,
|
|
89
89
|
allowConsumedIncomplete,
|
|
90
90
|
snapshot.toolName,
|
|
91
|
-
snapshot.
|
|
91
|
+
snapshot.blacklistAutoFoldTools,
|
|
92
92
|
snapshot.readOnlyContextActions,
|
|
93
93
|
);
|
|
94
94
|
if (!validated) continue;
|
|
@@ -231,6 +231,54 @@ export function stageIdentifiedToolBrief(input: {
|
|
|
231
231
|
return usefulBrief(bounded, ACTIVE_CONTEXT_POLICY.maxBriefChars, snapshot.toolName) ? bounded : null;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The fold ids a batch of tool results PEEKED, or null if the batch is not one.
|
|
236
|
+
*
|
|
237
|
+
* A peek result is the one tool output whose bytes the runtime already holds: it is a
|
|
238
|
+
* copy of a fold's own stored source. Reclaiming it therefore has an identity the
|
|
239
|
+
* ordinary tool-fold path does not have, and that identity has to be derivable from the
|
|
240
|
+
* transcript alone, because whichever mechanism claims the copy (the epoch's own
|
|
241
|
+
* reclaim marking or the doorless ladder) the record it leaves behind must still point
|
|
242
|
+
* back at the source.
|
|
243
|
+
*/
|
|
244
|
+
export function peekedSourceFoldIds(
|
|
245
|
+
snapshot: ActiveContextSnapshot,
|
|
246
|
+
refs: readonly EvidenceRef[],
|
|
247
|
+
): string[] | null {
|
|
248
|
+
if (!refs.length) return null;
|
|
249
|
+
const ids: string[] = [];
|
|
250
|
+
for (const ref of refs) {
|
|
251
|
+
const item = exactMapped(snapshot, ref);
|
|
252
|
+
if (!item) return null;
|
|
253
|
+
const call = resultCall(snapshot, item.index, true);
|
|
254
|
+
if (!call || call.name !== snapshot.toolName) return null;
|
|
255
|
+
const args = toolCallArguments(snapshot, call.assistantIndex, call.id);
|
|
256
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return null;
|
|
257
|
+
const record = args as Record<string, unknown>;
|
|
258
|
+
const id = record.id;
|
|
259
|
+
if (record.action !== "peek" || typeof id !== "string" || !id) return null;
|
|
260
|
+
if (!ids.includes(id)) ids.push(id);
|
|
261
|
+
}
|
|
262
|
+
return ids.length ? ids : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The brief a reclaimed peek copy leaves behind: a pointer, not a summary.
|
|
267
|
+
*
|
|
268
|
+
* The copy's content is the source fold's content, so summarizing it again would mint
|
|
269
|
+
* a second description of bytes that already have one. What the agent needs from the
|
|
270
|
+
* placeholder is the id it already used once, so re-reading the verbatim content is the
|
|
271
|
+
* same one-hop peek it was before the reclaim.
|
|
272
|
+
*/
|
|
273
|
+
export function peekReclaimBrief(sourceFoldIds: readonly string[]): string {
|
|
274
|
+
const named = sourceFoldIds.join(", ");
|
|
275
|
+
return sourceFoldIds.length === 1
|
|
276
|
+
? `Peek copy of fold ${named}, reclaimed: the copy was a duplicate of that fold's own stored ` +
|
|
277
|
+
`source, which stays verbatim and one hop away by peeking ${named} again.`
|
|
278
|
+
: `Peek copies of folds ${named}, reclaimed: the copies duplicated those folds' own stored ` +
|
|
279
|
+
"sources, which stay verbatim and one hop away by peeking each id again.";
|
|
280
|
+
}
|
|
281
|
+
|
|
234
282
|
export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: FoldCandidate): string {
|
|
235
283
|
const refs = candidate.kind === "tool-result" && candidate.parts.every((part) => part.kind === "raw")
|
|
236
284
|
? candidate.parts.map((part) => (part as Extract<FoldPart, { kind: "raw" }>).ref)
|
|
@@ -256,6 +304,13 @@ export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: F
|
|
|
256
304
|
? "active-context status inspection"
|
|
257
305
|
: factualBriefValue(name);
|
|
258
306
|
const args = toolCallArguments(snapshot, first.assistantIndex, first.id);
|
|
307
|
+
// A peek copy is named by what it copied, before anything else. The stage-identified
|
|
308
|
+
// brief below describes a result by its own head and tail, which for a peek copy is a
|
|
309
|
+
// second description of a fold that already has one; the pointer is what makes the
|
|
310
|
+
// reclaim recoverable, so it comes first and it is derived here rather than at the one
|
|
311
|
+
// call site that happens to know a reclaim is under way.
|
|
312
|
+
const peeked = peekedSourceFoldIds(snapshot, refs);
|
|
313
|
+
if (peeked) return peekReclaimBrief(peeked);
|
|
259
314
|
// Exact anchors before generic prose: one sentence per tool made every fold of that
|
|
260
315
|
// tool carry the SAME brief, so no index could answer which fold holds a given stage.
|
|
261
316
|
const identified = stageIdentifiedToolBrief({ snapshot, refs, calls, factualValue, factualToolName });
|
|
@@ -273,19 +328,138 @@ export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: F
|
|
|
273
328
|
return `Completed one read-only ${names} batch with ${calls.length} exact results${target}; every stale output remains recoverable from this fold.`;
|
|
274
329
|
}
|
|
275
330
|
|
|
331
|
+
/** The narrowest entry in a parent's index that still names a subject rather than a stub. */
|
|
332
|
+
const MIN_SUBJECT_CHARS = 24;
|
|
333
|
+
|
|
334
|
+
/** Cut to a character budget without splitting a surrogate pair, marked where it cut. */
|
|
335
|
+
function boundedSubject(text: string, budget: number): string {
|
|
336
|
+
if (text.length <= budget) return text;
|
|
337
|
+
let kept = text.slice(0, Math.max(0, budget - 3)).trimEnd();
|
|
338
|
+
const finalCode = kept.charCodeAt(kept.length - 1);
|
|
339
|
+
if (finalCode >= 0xd800 && finalCode <= 0xdbff) kept = kept.slice(0, -1);
|
|
340
|
+
return `${kept}...`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A parent's fallback brief: an index of its children, one entry each.
|
|
345
|
+
*
|
|
346
|
+
* This used to concatenate the children's briefs and slice the result at the cap, which
|
|
347
|
+
* cannot represent them. Ten children whose briefs may each run to the cap need ten times
|
|
348
|
+
* the cap, so the slice landed inside the first child or two and the rest of the group
|
|
349
|
+
* left no trace at all. In the 2026-08-11 rep every parent came out at exactly the cap,
|
|
350
|
+
* the bottom rung showed five of ten children and every rung above it showed ONE: at rung
|
|
351
|
+
* two the first child was itself a capped parent, so it spent the whole budget alone.
|
|
352
|
+
*
|
|
353
|
+
* The budget is divided instead. Children shorter than an even share hand their remainder
|
|
354
|
+
* to the longer ones, so the cut falls on the children that are actually long rather than
|
|
355
|
+
* on whoever sorts last, and every child appears whatever its neighbours cost. Coverage is
|
|
356
|
+
* the property being defended: this is the brief a group carries when no generator wrote
|
|
357
|
+
* it one, and a group that cannot say what a member holds cannot be navigated back into.
|
|
358
|
+
*/
|
|
276
359
|
export function deterministicConsolidationBrief(candidate: FoldCandidate, state: ActiveContextState): string {
|
|
277
360
|
const byId = foldMap(state);
|
|
278
361
|
const subjects = candidate.parts.flatMap((part) => {
|
|
279
362
|
if (part.kind !== "fold") return [];
|
|
280
363
|
const child = byId.get(part.foldId);
|
|
281
|
-
|
|
364
|
+
// Through the override map, never off the record: a child upgraded since it committed
|
|
365
|
+
// presents the model's brief, and reading the record here would index the sentence
|
|
366
|
+
// that brief replaced.
|
|
367
|
+
return child ? [foldBrief(child, state).replace(/\s+/g, " ").trim()] : [];
|
|
282
368
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
369
|
+
if (!subjects.length) return "Grouped completed context covering no readable folds.";
|
|
370
|
+
const separator = " | ";
|
|
371
|
+
const lead = `Grouped completed context covering ${subjects.length} folds: `;
|
|
372
|
+
// Below this width an entry says nothing a reader could act on, so a group too wide to
|
|
373
|
+
// give every child that much names as many as it can and then says, in the brief itself,
|
|
374
|
+
// how many it could not name. A count is a poor substitute for a subject and a truthful
|
|
375
|
+
// one; a slice through the middle of the third child is neither.
|
|
376
|
+
const room = ACTIVE_CONTEXT_POLICY.maxBriefChars - lead.length - 1;
|
|
377
|
+
const named = Math.max(1, Math.min(subjects.length,
|
|
378
|
+
Math.floor((room + separator.length) / (MIN_SUBJECT_CHARS + separator.length))));
|
|
379
|
+
const omitted = subjects.length - named;
|
|
380
|
+
const tail = omitted ? `${separator}${omitted} more folds in this group` : "";
|
|
381
|
+
const budget = room - tail.length - separator.length * (named - 1);
|
|
382
|
+
// Shortest first, so every subject that fits whole releases what it did not use to the
|
|
383
|
+
// subjects still to be measured. `owed` is always the even share of what is left among
|
|
384
|
+
// those that remain, which is the largest floor that still leaves room for all of them.
|
|
385
|
+
const order = subjects.slice(0, named).map((text, index) => ({ text, index })).sort((a, b) =>
|
|
386
|
+
a.text.length - b.text.length || a.index - b.index);
|
|
387
|
+
const bounded = new Array<string>(named);
|
|
388
|
+
let left = budget;
|
|
389
|
+
for (let taken = 0; taken < order.length; taken += 1) {
|
|
390
|
+
const owed = Math.max(1, Math.floor(left / (order.length - taken)));
|
|
391
|
+
const kept = boundedSubject(order[taken].text, owed);
|
|
392
|
+
bounded[order[taken].index] = kept;
|
|
393
|
+
left -= kept.length;
|
|
394
|
+
}
|
|
395
|
+
return `${lead}${bounded.join(separator)}${tail}.`.replace(/\s+/g, " ").trim();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Every fold kind, as a span may contain it.
|
|
400
|
+
*
|
|
401
|
+
* The agent path composes with this set, and so does the count law's parent: a fold is a
|
|
402
|
+
* fold, whatever kind made it, and a parent may take any of them. What an automatic
|
|
403
|
+
* CHAPTER may take is nothing of the sort, at any count: a placeholder reaches an
|
|
404
|
+
* automatic fold through exactly one route now, which is the count (Shane 2026-08-10).
|
|
405
|
+
*/
|
|
406
|
+
export const ALL_FOLD_KINDS: ReadonlySet<FoldKind> =
|
|
407
|
+
new Set<FoldKind>(["tool-result", "chapter", "consolidation"]);
|
|
408
|
+
|
|
409
|
+
/** No placeholder is span material. What an automatic chapter composes is raw entries. */
|
|
410
|
+
export const NO_FOLD_KINDS: ReadonlySet<FoldKind> = new Set<FoldKind>();
|
|
411
|
+
|
|
412
|
+
/** The first pinned fold a span would nest, or null when the span swallows no pin. */
|
|
413
|
+
export function pinnedChildFold(
|
|
414
|
+
parts: FoldPart[],
|
|
415
|
+
state: ActiveContextState,
|
|
416
|
+
snapshot: ActiveContextSnapshot,
|
|
417
|
+
): ActiveFold | null {
|
|
418
|
+
const byId = foldMap(state);
|
|
419
|
+
for (const part of parts) {
|
|
420
|
+
if (part.kind !== "fold") continue;
|
|
421
|
+
const child = byId.get(part.foldId);
|
|
422
|
+
if (!child) continue;
|
|
423
|
+
const refs = flattenFoldRefs(child, state);
|
|
424
|
+
const pinned = child.kind === "tool-result"
|
|
425
|
+
? toolRefsProtected(refs, state, snapshot)
|
|
426
|
+
: refsProtected(refs, state, snapshot);
|
|
427
|
+
if (pinned) return child;
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The one refusal nesting keeps.
|
|
434
|
+
*
|
|
435
|
+
* A span may take in any fold, so the pin is the sole thing that can stop it, and a pin
|
|
436
|
+
* is a promise to hold bytes raw. Nesting one would put a fold the agent asked to keep
|
|
437
|
+
* visible behind a placeholder, so the span is refused by name with the release valve
|
|
438
|
+
* in it, the same texture the pinned-share cap uses.
|
|
439
|
+
*/
|
|
440
|
+
export function pinnedNestingRefusal(pinned: ActiveFold, toolName: string): string {
|
|
441
|
+
return `fold refused: ${pinned.id} is pinned, and nesting it would hide context you asked to keep. ` +
|
|
442
|
+
`Release it with ${toolName} {"action":"unprotect","ids":["${pinned.id}"]} first, ` +
|
|
443
|
+
"or name a span that stops at its boundary.";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The span you named is already folded, said the way every other collision here is said.
|
|
448
|
+
*
|
|
449
|
+
* A fold that already owned the evidence used to reach the agent as
|
|
450
|
+
* "Evidence <id> has multiple direct fold owners", an invariant message thrown out of the
|
|
451
|
+
* tool call with no next move in it. The collision is ordinary: automatic folding and an
|
|
452
|
+
* agent curate the same window, so the ladder taking a span the agent was about to name
|
|
453
|
+
* is the campaign working, not a race. So it reads like the pin refusal above: name the
|
|
454
|
+
* fold holding it, give the two verbs that open it, and name the span that would work.
|
|
455
|
+
*
|
|
456
|
+
* One sentence covers the three ways a caller can point at folded material: an entry a
|
|
457
|
+
* fold directly owns, a span sitting inside one, and a span whose only reading IS one.
|
|
458
|
+
*/
|
|
459
|
+
export function foldedSpanRefusal(ownerId: string, subject: string, toolName: string): string {
|
|
460
|
+
return `fold refused: ${subject} is already folded inside ${ownerId}, so there is nothing ` +
|
|
461
|
+
`left to fold there. Read it with ${toolName} {"action":"peek","id":"${ownerId}"}, restore it ` +
|
|
462
|
+
`with ${toolName} {"action":"expand","id":"${ownerId}"}, or name a span that reaches past it.`;
|
|
289
463
|
}
|
|
290
464
|
|
|
291
465
|
export function partsForRange(
|
|
@@ -335,77 +509,227 @@ export function chapterUnits(snapshot: ActiveContextSnapshot): ChapterUnit[] {
|
|
|
335
509
|
.flatMap((segment) => structurallyClosedChapterUnits(snapshot.messages, segment));
|
|
336
510
|
}
|
|
337
511
|
|
|
338
|
-
|
|
512
|
+
/**
|
|
513
|
+
* The automatic jurisdiction, as a list of roots: THE ROOTS THE COUNT COUNTS.
|
|
514
|
+
*
|
|
515
|
+
* Membership, not position, and not kind. Every visible collapsed root belongs unless the
|
|
516
|
+
* agent pinned it or the fresh tail covers it, and an EXPANDED fold is not a placeholder
|
|
517
|
+
* at all. One predicate answers all of it, because that is the same question
|
|
518
|
+
* `refsProtected` already asks of a span's evidence.
|
|
519
|
+
*
|
|
520
|
+
* A consolidation counts here like anything else. A consolidated fold is not a different
|
|
521
|
+
* species of span, and depth is the point: ten visible parents make a grandparent, and
|
|
522
|
+
* under gradual accrual the stalest parent is simply the stalest member of the next group
|
|
523
|
+
* of ten, so the oldest context sits a few layers down after a long session. The only
|
|
524
|
+
* folds outside the count are the held ones, because a counted fold must be groupable and
|
|
525
|
+
* a held one is exactly the fold that cannot be (Shane 2026-08-10). A fold already nested
|
|
526
|
+
* inside a parent is not a root and is never counted separately.
|
|
527
|
+
*/
|
|
528
|
+
export function unpinnedStaleFolds(
|
|
339
529
|
snapshot: ActiveContextSnapshot,
|
|
340
530
|
state: ActiveContextState,
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
531
|
+
): Array<{ fold: ActiveFold; start: number; end: number }> {
|
|
532
|
+
return visibleCollapsedRoots(state, snapshot)
|
|
533
|
+
.filter(({ fold }) => !refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Everything a pending mark already speaks for: whole folds by id, raw evidence by key. */
|
|
537
|
+
export function pendingMarkClaims(
|
|
538
|
+
state: ActiveContextState,
|
|
539
|
+
): { foldIds: Set<string>; refKeys: Set<string> } {
|
|
540
|
+
const foldIds = new Set<string>();
|
|
541
|
+
const refKeys = new Set<string>();
|
|
542
|
+
for (const mark of state.pendingMarks ?? []) {
|
|
543
|
+
if (mark.mark === "refold") {
|
|
544
|
+
foldIds.add(mark.id);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
for (const part of mark.parts) {
|
|
548
|
+
if (part.kind === "fold") foldIds.add(part.foldId);
|
|
549
|
+
else refKeys.add(objectRefKey(part.ref));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return { foldIds, refKeys };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Whether the raw material between two roots is material a parent may swallow. */
|
|
556
|
+
function absorbableGap(
|
|
557
|
+
snapshot: ActiveContextSnapshot,
|
|
558
|
+
state: ActiveContextState,
|
|
559
|
+
from: number,
|
|
560
|
+
to: number,
|
|
561
|
+
claims: { refKeys: Set<string> },
|
|
562
|
+
): boolean {
|
|
563
|
+
if (from > to) return true;
|
|
564
|
+
// No fold kind is admitted, so any root sitting in the gap -- a parent the count
|
|
565
|
+
// already made, a fold the agent expanded, a fold the agent pinned -- refuses the
|
|
566
|
+
// range outright and splits the run there.
|
|
567
|
+
const parts = partsForRange(snapshot, state, from, to, NO_FOLD_KINDS);
|
|
568
|
+
if (!parts) return false;
|
|
569
|
+
const refs = parts.flatMap((part) => part.kind === "raw" ? [part.ref] : []);
|
|
570
|
+
if (refs.length !== parts.length || refsProtected(refs, state, snapshot)) return false;
|
|
571
|
+
return !refs.some((ref) => claims.refKeys.has(objectRefKey(ref)));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Every tool call in the window, by id, with the assistant and the result that close it. */
|
|
575
|
+
function windowToolLinkage(snapshot: ActiveContextSnapshot): Map<string, { call: number; result: number }> {
|
|
576
|
+
const linkage = new Map<string, { call: number; result: number }>();
|
|
577
|
+
const at = (id: string): { call: number; result: number } => {
|
|
578
|
+
const existing = linkage.get(id);
|
|
579
|
+
if (existing) return existing;
|
|
580
|
+
const created = { call: -1, result: -1 };
|
|
581
|
+
linkage.set(id, created);
|
|
582
|
+
return created;
|
|
360
583
|
};
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
run = [root];
|
|
584
|
+
for (let index = 0; index < snapshot.messages.length; index += 1) {
|
|
585
|
+
const message = snapshot.messages[index];
|
|
586
|
+
const role = messageRole(message);
|
|
587
|
+
if (role === "assistant") {
|
|
588
|
+
for (const part of denseOwnArrayValues(ownValue(message, "content")) ?? []) {
|
|
589
|
+
if (ownValue(part, "type") !== "toolCall") continue;
|
|
590
|
+
const id = ownValue(part, "id");
|
|
591
|
+
if (typeof id === "string" && id) at(id).call = index;
|
|
370
592
|
}
|
|
593
|
+
} else if (role === "toolResult") {
|
|
594
|
+
const id = ownValue(message, "toolCallId");
|
|
595
|
+
if (typeof id === "string" && id) at(id).result = index;
|
|
371
596
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
597
|
+
}
|
|
598
|
+
return linkage;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* The parent's span, widened until hiding it keeps every tool call with its result.
|
|
603
|
+
*
|
|
604
|
+
* A collapsed parent renders as ONE entry, so a span that opens on a tool-result fold
|
|
605
|
+
* would hide the result while its call stayed visible, and the projection guard rejects
|
|
606
|
+
* that split outright. The widening is the same raw absorption the law already grants
|
|
607
|
+
* between children, applied at the edges: it takes the calling assistant in, never a
|
|
608
|
+
* fold, and if the material it needs is not absorbable the group does not form.
|
|
609
|
+
*/
|
|
610
|
+
function linkageClosedSpan(
|
|
611
|
+
snapshot: ActiveContextSnapshot,
|
|
612
|
+
start: number,
|
|
613
|
+
end: number,
|
|
614
|
+
): { start: number; end: number } {
|
|
615
|
+
const linkage = windowToolLinkage(snapshot);
|
|
616
|
+
let from = start;
|
|
617
|
+
let to = end;
|
|
618
|
+
for (let pass = 0; pass <= linkage.size; pass += 1) {
|
|
619
|
+
let moved = false;
|
|
620
|
+
for (const { call, result } of linkage.values()) {
|
|
621
|
+
if (call < 0 || result < 0) continue;
|
|
622
|
+
const callInside = call >= from && call <= to;
|
|
623
|
+
const resultInside = result >= from && result <= to;
|
|
624
|
+
if (callInside === resultInside) continue;
|
|
625
|
+
if (call < from) { from = call; moved = true; }
|
|
626
|
+
if (result > to) { to = result; moved = true; }
|
|
390
627
|
}
|
|
628
|
+
if (!moved) break;
|
|
391
629
|
}
|
|
392
|
-
return
|
|
630
|
+
return { start: from, end: to };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** One group of exactly `consolidateAfter` roots, read as the parent it must become. */
|
|
634
|
+
function consolidationCandidate(
|
|
635
|
+
snapshot: ActiveContextSnapshot,
|
|
636
|
+
state: ActiveContextState,
|
|
637
|
+
members: Array<{ fold: ActiveFold; start: number; end: number }>,
|
|
638
|
+
claims: { refKeys: Set<string> },
|
|
639
|
+
): FoldCandidate | null {
|
|
640
|
+
const span = linkageClosedSpan(snapshot, members[0].start, members.at(-1)!.end);
|
|
641
|
+
const parts = partsForRange(snapshot, state, span.start, span.end, ALL_FOLD_KINDS);
|
|
642
|
+
if (!parts) return null;
|
|
643
|
+
const ids = new Set(members.map(({ fold }) => fold.id));
|
|
644
|
+
const children = parts.flatMap((part) => part.kind === "fold" ? [part.foldId] : []);
|
|
645
|
+
if (children.length !== ids.size || children.some((id) => !ids.has(id))) return null;
|
|
646
|
+
const raw = parts.flatMap((part) => part.kind === "raw" ? [part.ref] : []);
|
|
647
|
+
if (refsProtected(raw, state, snapshot)) return null;
|
|
648
|
+
if (raw.some((ref) => claims.refKeys.has(objectRefKey(ref)))) return null;
|
|
649
|
+
const sourceRefs = candidateSourceRefs(parts, state);
|
|
650
|
+
// THE ONE PHYSICAL BOUND, HANDLED LOUDLY. A record carries at most
|
|
651
|
+
// `maxFoldSourceRefs` exact references. A group over that bound is left UNFORMED
|
|
652
|
+
// rather than quietly shrunk: shrinking would make the parent a function of the
|
|
653
|
+
// record format instead of the count, and the law is the count.
|
|
654
|
+
if (sourceRefs.length > snapshot.policy.maxFoldSourceRefs) return null;
|
|
655
|
+
return { kind: "consolidation", parts, sourceRefs };
|
|
393
656
|
}
|
|
394
657
|
|
|
395
658
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
659
|
+
* CONSOLIDATION IS A PURE FUNCTION OF THE COUNT (Shane 2026-08-10).
|
|
660
|
+
*
|
|
661
|
+
* Not a crossing event, not a paced rung, and not one candidate per pass. Count the
|
|
662
|
+
* visible unheld roots of every kind; each `consolidateAfter` consecutive ones, stalest
|
|
663
|
+
* first, MUST be under a parent, and the remainder is left alone. From a count of n that
|
|
664
|
+
* is `n / consolidateAfter` parents, and all of them form in the epoch that notices,
|
|
665
|
+
* because a rule that formed one per commit would leave a count the rule says is
|
|
666
|
+
* impossible standing for as many commits as it takes to drain.
|
|
667
|
+
*
|
|
668
|
+
* A parent it made is a root like any other, so the epoch applies the rule to a FIXPOINT:
|
|
669
|
+
* ten parents are a group, and the grandparent that follows is how the oldest context
|
|
670
|
+
* ends up several layers down after a long session.
|
|
671
|
+
*
|
|
672
|
+
* The run is where the count is taken. A hold -- a pin, the fresh tail, an expanded fold,
|
|
673
|
+
* evidence a pending mark spoke for -- cannot be swallowed, so it splits the run and each
|
|
674
|
+
* side counts on its own; a run shorter than the width waits as remainder. Gaps of raw
|
|
675
|
+
* context between children fall into the parent, which is what makes the span one
|
|
676
|
+
* contiguous range instead of a list of islands.
|
|
399
677
|
*/
|
|
400
|
-
export function
|
|
678
|
+
export function selectAutomaticConsolidations(
|
|
401
679
|
snapshot: ActiveContextSnapshot,
|
|
402
680
|
state: ActiveContextState,
|
|
403
|
-
ratio: number,
|
|
404
|
-
claimed: ReadonlySet<string> = new Set<string>(),
|
|
405
681
|
): FoldCandidate[] {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
682
|
+
const width = snapshot.thresholds.consolidateAfter;
|
|
683
|
+
if (!Number.isInteger(width) || width < 1) return [];
|
|
684
|
+
const claims = pendingMarkClaims(state);
|
|
685
|
+
const eligible = unpinnedStaleFolds(snapshot, state)
|
|
686
|
+
.filter(({ fold }) => !claims.foldIds.has(fold.id));
|
|
687
|
+
const groups: FoldCandidate[] = [];
|
|
688
|
+
let run: typeof eligible = [];
|
|
689
|
+
const close = (): void => {
|
|
690
|
+
for (let at = 0; at + width <= run.length; at += width) {
|
|
691
|
+
const candidate = consolidationCandidate(snapshot, state, run.slice(at, at + width), claims);
|
|
692
|
+
if (candidate) groups.push(candidate);
|
|
693
|
+
}
|
|
694
|
+
run = [];
|
|
695
|
+
};
|
|
696
|
+
for (const root of eligible) {
|
|
697
|
+
const previous = run.at(-1);
|
|
698
|
+
if (previous && !absorbableGap(snapshot, state, previous.end + 1, root.start - 1, claims)) close();
|
|
699
|
+
run.push(root);
|
|
700
|
+
}
|
|
701
|
+
close();
|
|
702
|
+
return groups;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** One completed batch the automatic law may take, with the indices it covers. */
|
|
706
|
+
export interface AutomaticToolBatch {
|
|
707
|
+
refs: EvidenceRef[];
|
|
708
|
+
indices: number[];
|
|
709
|
+
bytes: number;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* MEMBERSHIP, WRITTEN ONCE: every completed tool batch the automatic law may take,
|
|
714
|
+
* stalest first, whether or not anything has claimed it yet.
|
|
715
|
+
*
|
|
716
|
+
* A batch is a member when it is complete and validated, its tool is not blacklisted
|
|
717
|
+
* (`resultCall` runs the batch scan with the deployment's list), its evidence carries
|
|
718
|
+
* exact refs in branch order, it is inside the record's reference bound, it is large
|
|
719
|
+
* enough to be worth a placeholder, and neither an agent pin nor the fresh byte tail
|
|
720
|
+
* covers any part of it. There is no position in that list. A prefix of the window is
|
|
721
|
+
* not more foldable than the middle of it, and the middle is not a wall: it is whatever
|
|
722
|
+
* the ladder has not needed yet plus whatever the agent pinned (Shane 2026-08-10).
|
|
723
|
+
*
|
|
724
|
+
* Ownership is deliberately NOT membership. A batch some fold or pending mark already
|
|
725
|
+
* speaks for is still a member of the class; it is simply taken. The selector filters on
|
|
726
|
+
* ownership because it must propose something new, and the announcement counts the class
|
|
727
|
+
* because that is the mass a commit can reach. One definition, two readings of it.
|
|
728
|
+
*/
|
|
729
|
+
export function automaticToolBatches(
|
|
730
|
+
snapshot: ActiveContextSnapshot,
|
|
731
|
+
state: ActiveContextState,
|
|
732
|
+
): AutomaticToolBatch[] {
|
|
409
733
|
const groups = new Map<number, Array<{ item: MappedMessage; call: NonNullable<ReturnType<typeof resultCall>> }>>();
|
|
410
734
|
for (const item of snapshot.mapped) {
|
|
411
735
|
if (messageRole(item.message) !== "toolResult") continue;
|
|
@@ -415,6 +739,7 @@ export function selectAutomaticToolBatch(
|
|
|
415
739
|
group.push({ item, call });
|
|
416
740
|
groups.set(call.assistantIndex, group);
|
|
417
741
|
}
|
|
742
|
+
const batches: AutomaticToolBatch[] = [];
|
|
418
743
|
for (const group of groups.values()) {
|
|
419
744
|
const expected = group[0].call.batch;
|
|
420
745
|
const ids = new Set(group.map(({ call }) => call.id));
|
|
@@ -422,26 +747,51 @@ export function selectAutomaticToolBatch(
|
|
|
422
747
|
if (ids.size !== expected.length || expected.some((id) => !ids.has(id)) ||
|
|
423
748
|
refs.some((ref) => !ref || ref.role !== "toolResult")) continue;
|
|
424
749
|
const exactRefs = refs as EvidenceRef[];
|
|
750
|
+
const size = group.reduce((total, { item }) => total + bytes(item.message), 0);
|
|
425
751
|
if (exactRefs.length > snapshot.policy.maxFoldSourceRefs ||
|
|
426
|
-
exactRefs.some((ref) => owned.has(objectRefKey(ref))) ||
|
|
427
752
|
toolRefsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
|
|
428
|
-
|
|
753
|
+
size < snapshot.policy.minToolChars) continue;
|
|
754
|
+
batches.push({ refs: exactRefs, indices: group.map(({ item }) => item.index), bytes: size });
|
|
755
|
+
}
|
|
756
|
+
return batches;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* The stalest member batch nothing has claimed. `claimed` lets a caller exclude evidence
|
|
761
|
+
* already spoken for by something that is not yet a fold (a pending mark), so repeated
|
|
762
|
+
* calls walk forward instead of returning the same batch.
|
|
763
|
+
*/
|
|
764
|
+
export function selectAutomaticToolBatch(
|
|
765
|
+
snapshot: ActiveContextSnapshot,
|
|
766
|
+
state: ActiveContextState,
|
|
767
|
+
claimed: ReadonlySet<string> = new Set<string>(),
|
|
768
|
+
): FoldCandidate[] {
|
|
769
|
+
const owned = new Set([...claimed, ...state.folds.flatMap((fold) => fold.parts.flatMap((part) =>
|
|
770
|
+
part.kind === "raw" ? [objectRefKey(part.ref)] : []))]);
|
|
771
|
+
for (const batch of automaticToolBatches(snapshot, state)) {
|
|
772
|
+
if (batch.refs.some((ref) => owned.has(objectRefKey(ref)))) continue;
|
|
429
773
|
return [{
|
|
430
774
|
kind: "tool-result",
|
|
431
|
-
parts:
|
|
432
|
-
sourceRefs:
|
|
775
|
+
parts: batch.refs.map((ref) => ({ kind: "raw", ref })),
|
|
776
|
+
sourceRefs: batch.refs,
|
|
433
777
|
}];
|
|
434
778
|
}
|
|
435
779
|
return [];
|
|
436
780
|
}
|
|
437
781
|
|
|
782
|
+
/**
|
|
783
|
+
* The re-fold path.
|
|
784
|
+
*
|
|
785
|
+
* An expansion is a deliberate act, and the way to make it permanent is a pin: a pinned
|
|
786
|
+
* fold is never re-folded, and a leased one is held for the term of its lease. Anything
|
|
787
|
+
* else the agent opened is an ordinary member of the foldable class again, so automation
|
|
788
|
+
* may return it to its placeholder, stalest and narrowest first.
|
|
789
|
+
*/
|
|
438
790
|
export function selectAutomaticRefold(
|
|
439
791
|
snapshot: ActiveContextSnapshot,
|
|
440
792
|
state: ActiveContextState,
|
|
441
|
-
ratio: number,
|
|
442
793
|
claimedFoldIds: ReadonlySet<string> = new Set<string>(),
|
|
443
794
|
): string | null {
|
|
444
|
-
if (!Number.isFinite(ratio) || ratio < snapshot.policy.refoldRatio) return null;
|
|
445
795
|
const candidates = state.expanded.flatMap((id) => {
|
|
446
796
|
if (claimedFoldIds.has(id)) return [];
|
|
447
797
|
const fold = state.folds.find((item) => item.id === id);
|
|
@@ -449,24 +799,13 @@ export function selectAutomaticRefold(
|
|
|
449
799
|
const protectedSource = fold && (fold.kind === "tool-result"
|
|
450
800
|
? toolRefsProtected(flattenFoldRefs(fold, state), state, snapshot)
|
|
451
801
|
: refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
452
|
-
return fold && interval && !protectedSource && !state.leases[id]
|
|
802
|
+
return fold && interval && !protectedSource && !state.leases[id]
|
|
803
|
+
? [{ id, ...interval }]
|
|
804
|
+
: [];
|
|
453
805
|
}).sort((left, right) => left.end - right.end || (left.end - left.start) - (right.end - right.start));
|
|
454
806
|
return candidates[0]?.id ?? null;
|
|
455
807
|
}
|
|
456
808
|
|
|
457
|
-
export function selectAutomaticToolForRung(
|
|
458
|
-
snapshot: ActiveContextSnapshot,
|
|
459
|
-
state: ActiveContextState,
|
|
460
|
-
ratio: number,
|
|
461
|
-
cadenceWaived = false,
|
|
462
|
-
claimed: ReadonlySet<string> = new Set<string>(),
|
|
463
|
-
): FoldCandidate | null {
|
|
464
|
-
const cadenceSatisfied = state.tokensSinceToolFold >= toolFoldCadence(snapshot.contextWindow);
|
|
465
|
-
if (!cadenceWaived && (!Number.isFinite(ratio) ||
|
|
466
|
-
(ratio < snapshot.policy.toolFoldRatio && !cadenceSatisfied))) return null;
|
|
467
|
-
return selectAutomaticToolBatch(snapshot, state, 1, claimed)[0] ?? null;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
809
|
export function resolveFoldInputIds(
|
|
471
810
|
snapshot: ActiveContextSnapshot,
|
|
472
811
|
state: ActiveContextState,
|
|
@@ -600,9 +939,9 @@ export function snapFoldCandidate(
|
|
|
600
939
|
}
|
|
601
940
|
// Nearest first, then the other readings of the same intent. A span that cuts into one
|
|
602
941
|
// fold has exactly two endpoint corrections -- absorb it, or step past it -- and which
|
|
603
|
-
// one survives depends on structure the caller cannot see. A
|
|
604
|
-
//
|
|
605
|
-
//
|
|
942
|
+
// one survives depends on structure the caller cannot see. A whole fold left inside the
|
|
943
|
+
// corrected span is no longer a reason to fail: the span nests it. What still fails is
|
|
944
|
+
// a span with no valid frame at all, and a pin the span would swallow.
|
|
606
945
|
const alternatives = snapSpanAlternatives(snapshot, state, ids);
|
|
607
946
|
let lastError: Error = directError;
|
|
608
947
|
for (const snapped of alternatives) {
|
|
@@ -627,10 +966,10 @@ export function snapFoldCandidate(
|
|
|
627
966
|
/**
|
|
628
967
|
* Every constructible reading of a requested span, nearest intent first.
|
|
629
968
|
*
|
|
630
|
-
* The three endpoint modes move the EDGES; the fourth reading moves the FRAME
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
969
|
+
* The three endpoint modes move the EDGES; the fourth reading moves the FRAME, reading
|
|
970
|
+
* the span as the whole folds it cuts into. The frame reading survives the nesting law
|
|
971
|
+
* because it is still the faithful one where the endpoints cannot snap to closed units:
|
|
972
|
+
* the folds themselves already tile the range exactly.
|
|
634
973
|
*/
|
|
635
974
|
export function snapSpanAlternatives(
|
|
636
975
|
snapshot: ActiveContextSnapshot,
|
|
@@ -678,7 +1017,7 @@ export function snapSpanToWholeFolds(
|
|
|
678
1017
|
ids: tiling.map((root) => root.fold.id),
|
|
679
1018
|
corrections: [{
|
|
680
1019
|
...outward.corrections[0],
|
|
681
|
-
reason: `span cut into ${tiling.length} folds
|
|
1020
|
+
reason: `span cut into ${tiling.length} folds; corrected outward to their whole ` +
|
|
682
1021
|
`boundaries and read as a consolidation of ${tiling.map((root) => root.fold.id).join(", ")}`,
|
|
683
1022
|
}],
|
|
684
1023
|
};
|
|
@@ -709,57 +1048,67 @@ export function snapSpanIds(
|
|
|
709
1048
|
reason,
|
|
710
1049
|
});
|
|
711
1050
|
};
|
|
712
|
-
for (const root of orderedRoots(state, snapshot)) {
|
|
713
|
-
if (start > root.start && start <= root.end) {
|
|
714
|
-
const absorb = mode === "absorb" ||
|
|
715
|
-
(mode === "nearest" && start - root.start <= root.end + 1 - start);
|
|
716
|
-
start = absorb ? root.start : root.end + 1;
|
|
717
|
-
note(`span started inside ${root.fold.id}; corrected to its ` +
|
|
718
|
-
`${absorb ? "start" : "far"} boundary`);
|
|
719
|
-
}
|
|
720
|
-
if (end >= root.start && end < root.end) {
|
|
721
|
-
const absorb = mode === "absorb" ||
|
|
722
|
-
(mode === "nearest" && root.end - end <= end - (root.start - 1));
|
|
723
|
-
end = absorb ? root.end : root.start - 1;
|
|
724
|
-
note(`span ended inside ${root.fold.id}; corrected to its ` +
|
|
725
|
-
`${absorb ? "end" : "near"} boundary`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
const boundary = currentTurnBoundary(snapshot);
|
|
729
|
-
if (boundary >= 0 && end > boundary) {
|
|
730
|
-
end = boundary;
|
|
731
|
-
note("span reached into the turn still in flight; corrected back to the last closed turn");
|
|
732
|
-
}
|
|
733
1051
|
const units = chapterUnits(snapshot);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const turns: number[] = [];
|
|
750
|
-
let clamped = end;
|
|
751
|
-
for (const unit of covered) {
|
|
752
|
-
if (!turns.includes(unit.turnStart)) {
|
|
753
|
-
if (turns.length >= snapshot.policy.maxChapterTurns) break;
|
|
754
|
-
turns.push(unit.turnStart);
|
|
1052
|
+
// Each correction can undo another: pulling an endpoint onto a unit boundary can put
|
|
1053
|
+
// it back inside a fold, and clamping the width can cut one in half. So the passes run
|
|
1054
|
+
// to a FIXED POINT rather than once each. After the first pass an endpoint inside a
|
|
1055
|
+
// fold always absorbs it, which is outward and therefore terminating: stepping past a
|
|
1056
|
+
// fold here would drop material the agent named, and nesting no longer loses anything.
|
|
1057
|
+
for (let pass = 0; pass < state.folds.length + 2; pass += 1) {
|
|
1058
|
+
const passStart = start;
|
|
1059
|
+
const passEnd = end;
|
|
1060
|
+
for (const root of orderedRoots(state, snapshot)) {
|
|
1061
|
+
if (start > root.start && start <= root.end) {
|
|
1062
|
+
const absorb = pass > 0 || mode === "absorb" ||
|
|
1063
|
+
(mode === "nearest" && start - root.start <= root.end + 1 - start);
|
|
1064
|
+
start = absorb ? root.start : root.end + 1;
|
|
1065
|
+
note(`span started inside ${root.fold.id}; corrected to its ` +
|
|
1066
|
+
`${absorb ? "start" : "far"} boundary`);
|
|
755
1067
|
}
|
|
756
|
-
|
|
1068
|
+
if (end >= root.start && end < root.end) {
|
|
1069
|
+
const absorb = pass > 0 || mode === "absorb" ||
|
|
1070
|
+
(mode === "nearest" && root.end - end <= end - (root.start - 1));
|
|
1071
|
+
end = absorb ? root.end : root.start - 1;
|
|
1072
|
+
note(`span ended inside ${root.fold.id}; corrected to its ` +
|
|
1073
|
+
`${absorb ? "end" : "near"} boundary`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const boundary = currentTurnBoundary(snapshot);
|
|
1077
|
+
if (boundary >= 0 && end > boundary) {
|
|
1078
|
+
end = boundary;
|
|
1079
|
+
note("span reached into the turn still in flight; corrected back to the last closed turn");
|
|
757
1080
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1081
|
+
const startUnit = units.find((unit) => start >= unit.start && start < unit.end);
|
|
1082
|
+
if (startUnit && startUnit.start !== start) {
|
|
1083
|
+
start = startUnit.start;
|
|
1084
|
+
note("span started mid-unit; corrected to the start of its closed user/assistant/tool unit");
|
|
762
1085
|
}
|
|
1086
|
+
const endUnit = units.find((unit) => end >= unit.start && end < unit.end);
|
|
1087
|
+
if (endUnit && endUnit.end - 1 !== end) {
|
|
1088
|
+
end = endUnit.end - 1;
|
|
1089
|
+
note("span ended mid-unit; corrected to the end of its closed user/assistant/tool unit");
|
|
1090
|
+
}
|
|
1091
|
+
// A chapter spans at most a few closed turns. A longer request is a span the agent
|
|
1092
|
+
// meant, so it is CLAMPED to what one fold may hold rather than refused; the
|
|
1093
|
+
// remainder stays raw and is the agent's to fold next.
|
|
1094
|
+
const covered = units.filter((unit) => unit.start >= start && unit.end <= end + 1);
|
|
1095
|
+
if (covered.length) {
|
|
1096
|
+
const turns: number[] = [];
|
|
1097
|
+
let clamped = end;
|
|
1098
|
+
for (const unit of covered) {
|
|
1099
|
+
if (!turns.includes(unit.turnStart)) {
|
|
1100
|
+
if (turns.length >= snapshot.policy.maxChapterTurns) break;
|
|
1101
|
+
turns.push(unit.turnStart);
|
|
1102
|
+
}
|
|
1103
|
+
clamped = unit.end - 1;
|
|
1104
|
+
}
|
|
1105
|
+
if (clamped < end) {
|
|
1106
|
+
end = clamped;
|
|
1107
|
+
note(`span covered more than the ${snapshot.policy.maxChapterTurns}-turn chapter limit; ` +
|
|
1108
|
+
"corrected to the last turn that fits");
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (start === passStart && end === passEnd) break;
|
|
763
1112
|
}
|
|
764
1113
|
if (!corrections.length || end < start ||
|
|
765
1114
|
!snapshot.mapped[start]?.ref || !snapshot.mapped[end]?.ref) return null;
|
|
@@ -826,10 +1175,31 @@ export function manualFoldCandidate(
|
|
|
826
1175
|
!options.allowProtected && toolRefsProtected(refs, state, snapshot);
|
|
827
1176
|
const blocked = (refs: EvidenceRef[]): boolean =>
|
|
828
1177
|
!options.allowProtected && refsProtected(refs, state, snapshot);
|
|
1178
|
+
// The forest as it stands, read once. Building it here can only throw when the STORED
|
|
1179
|
+
// forest is already corrupt, which is the loud case and stays loud; every collision
|
|
1180
|
+
// between this request and a fold that exists is answered below by name.
|
|
1181
|
+
const owners = directFoldOwners(state.folds);
|
|
829
1182
|
const bounded = (candidate: FoldCandidate): FoldCandidate => {
|
|
830
1183
|
if (candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs) {
|
|
831
1184
|
throw new Error(`Folds may include at most ${snapshot.policy.maxFoldSourceRefs} exact source references`);
|
|
832
1185
|
}
|
|
1186
|
+
// Every reading of a requested span leaves through here, so one check covers the
|
|
1187
|
+
// tool-batch, consolidation and chapter readings alike. It runs before anything is
|
|
1188
|
+
// prepared or persisted: no brief is generated, no state is touched, and the caller
|
|
1189
|
+
// gets a span it can act on instead of an invariant it cannot.
|
|
1190
|
+
for (const part of candidate.parts) {
|
|
1191
|
+
if (part.kind !== "raw") continue;
|
|
1192
|
+
const ownerId = owners.get(objectRefKey(part.ref));
|
|
1193
|
+
if (ownerId) throw new Error(foldedSpanRefusal(ownerId, part.ref.entryId, snapshot.toolName));
|
|
1194
|
+
}
|
|
1195
|
+
// A reading that is exactly one fold that already exists is not a fold to make. It
|
|
1196
|
+
// wraps a placeholder in a placeholder, reclaims nothing, and buries the fold the
|
|
1197
|
+
// caller was pointing at one level deeper, so it is the same refusal: the span is
|
|
1198
|
+
// already folded, and here is what opens it.
|
|
1199
|
+
const only = candidate.parts.length === 1 ? candidate.parts[0] : null;
|
|
1200
|
+
if (only?.kind === "fold") {
|
|
1201
|
+
throw new Error(foldedSpanRefusal(only.foldId, "that span", snapshot.toolName));
|
|
1202
|
+
}
|
|
833
1203
|
return candidate;
|
|
834
1204
|
};
|
|
835
1205
|
const selected = resolveFoldInputIds(snapshot, state, ids);
|
|
@@ -856,14 +1226,28 @@ export function manualFoldCandidate(
|
|
|
856
1226
|
if (exactFolds && selected.length >= 2) {
|
|
857
1227
|
const parts: FoldPart[] = selected.map((item) => ({ kind: "fold", foldId: item.fold!.id }));
|
|
858
1228
|
const refs = candidateSourceRefs(parts, state);
|
|
1229
|
+
const pinnedChild = pinnedChildFold(parts, state, snapshot);
|
|
1230
|
+
if (pinnedChild) throw new Error(pinnedNestingRefusal(pinnedChild, snapshot.toolName));
|
|
859
1231
|
if (blocked(refs)) throw new Error("Manual consolidation contains protected evidence");
|
|
860
1232
|
return bounded({ kind: "consolidation", parts, sourceRefs: refs });
|
|
861
1233
|
}
|
|
862
1234
|
if (!chapterRangeIsUnitAligned(snapshot, start, end)) {
|
|
863
1235
|
throw new Error("Chapter folds must align to a contiguous structurally closed user/assistant/tool-batch range");
|
|
864
1236
|
}
|
|
865
|
-
const parts = partsForRange(snapshot, state, start, end,
|
|
866
|
-
if (!parts)
|
|
1237
|
+
const parts = partsForRange(snapshot, state, start, end, ALL_FOLD_KINDS);
|
|
1238
|
+
if (!parts) {
|
|
1239
|
+
// Two shapes reach here, and they are different asks. A span INSIDE one fold is
|
|
1240
|
+
// material that is already folded, so there is no fold to make and the useful answer
|
|
1241
|
+
// names the fold holding it. A span that merely cuts one is a boundary the snap
|
|
1242
|
+
// corrects, and it says which fold moved the edge.
|
|
1243
|
+
const cut = orderedRoots(state, snapshot).filter((root) => root.start <= end && start <= root.end);
|
|
1244
|
+
const container = cut.find((root) => root.start <= start && root.end >= end);
|
|
1245
|
+
throw new Error(container
|
|
1246
|
+
? foldedSpanRefusal(container.fold.id, "that span", snapshot.toolName)
|
|
1247
|
+
: `Fold span partially overlaps ${cut.map((root) => root.fold.id).join(", ")}`);
|
|
1248
|
+
}
|
|
1249
|
+
const swallowedPin = pinnedChildFold(parts, state, snapshot);
|
|
1250
|
+
if (swallowedPin) throw new Error(pinnedNestingRefusal(swallowedPin, snapshot.toolName));
|
|
867
1251
|
const refs = candidateSourceRefs(parts, state);
|
|
868
1252
|
if (blocked(refs)) throw new Error("Manual chapter contains fresh, unfinished, unmatched, unmapped, or protected evidence");
|
|
869
1253
|
return bounded({ kind: "chapter", parts, sourceRefs: refs });
|