pi-fold 1.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,6 @@ import {
16
16
  orderedRoots,
17
17
  refsInOrder,
18
18
  refsProtected,
19
- toolRefsProtected,
20
19
  visibleCollapsedRoots,
21
20
  } from "./measurement.ts";
22
21
  import {
@@ -28,7 +27,6 @@ import {
28
27
  import {
29
28
  ACTIVE_CONTEXT_POLICY,
30
29
  DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
31
- MAX_FOLD_SPAN_CHARS,
32
30
  } from "./policy.ts";
33
31
  import type {
34
32
  ActiveContextSnapshot,
@@ -42,7 +40,6 @@ import type {
42
40
  } from "./policy.ts";
43
41
  import {
44
42
  chapterSegments,
45
- currentTurnBoundary,
46
43
  scanTurnToolBatches,
47
44
  structurallyClosedChapterUnits,
48
45
  terminalAssistant,
@@ -152,19 +149,23 @@ export function toolCallArguments(snapshot: ActiveContextSnapshot, assistantInde
152
149
  return calls.get(id);
153
150
  }
154
151
 
155
- /**
156
- * Anchor budgets for a stage-identified brief. They exist to keep the composed brief
157
- * inside the hard 1200-character brief cap with the generic prose trimmed away: an
158
- * argument list, a leading label from the result head, and the result TAIL, which is
159
- * where a staged chain puts its keys and a report puts its conclusion.
160
- */
161
152
  export const IDENTIFIED_BRIEF_ARGUMENT_CHARS = 240;
162
153
  export const IDENTIFIED_BRIEF_VALUE_CHARS = 96;
154
+ // A composed stage-identified brief is held to the ONE policy cap, maxBriefChars
155
+ // (Shane 2026-08-14: "2k brief is more appropriate"). The head keeps the
156
+ // result's leading paragraph and gets exactly what remains of the cap after the
157
+ // parts that identify the fold (call signatures, tail anchor, fixed text), so
158
+ // the paragraph takes every character the contract can give it. The 1,100
159
+ // sub-cap this replaced held 100 below a separate 1,200 supplied maximum as
160
+ // headroom for commit-time gap absorption's appended sentence; the absorption
161
+ // eligibility law (gate 136) removed that hazard, and both smaller numbers went
162
+ // with it. Gate 134 pins the paragraph surviving.
163
+ // The head bound for a result with NO terminated opening paragraph: its first
164
+ // line, exactly the pre-paragraph behaviour.
163
165
  export const IDENTIFIED_BRIEF_HEAD_CHARS = 160;
164
166
  export const IDENTIFIED_BRIEF_TAIL_CHARS = 120;
165
167
  export const IDENTIFIED_BRIEF_CALLS_CHARS = 400;
166
168
 
167
- /** Every scalar argument of a call, in stable key order, bounded per value and in total. */
168
169
  export function identifiedCallArguments(
169
170
  args: unknown,
170
171
  factual: (value: string, maximum: number) => string,
@@ -182,7 +183,6 @@ export function identifiedCallArguments(
182
183
  return oneLine(pairs.join(", "), IDENTIFIED_BRIEF_ARGUMENT_CHARS);
183
184
  }
184
185
 
185
- /** The last characters of a result, which is the region a truncated read never shows. */
186
186
  export function identifiedResultTail(
187
187
  message: unknown,
188
188
  factual: (value: string, maximum: number) => string,
@@ -192,13 +192,6 @@ export function identifiedResultTail(
192
192
  return factual(text.slice(Math.max(0, text.length - IDENTIFIED_BRIEF_TAIL_CHARS)), IDENTIFIED_BRIEF_TAIL_CHARS);
193
193
  }
194
194
 
195
- /**
196
- * The stage-identified brief. The generic prose is trimmed to a few words so the
197
- * character budget buys ANCHORS instead: which call this was, with which arguments,
198
- * what the result opened with, and what it ENDED with. The tail anchor is the point
199
- * of the whole change: a reader deciding which fold holds a chain key can only tell
200
- * from the tail, and a generic sentence never carried one.
201
- */
202
195
  export function stageIdentifiedToolBrief(input: {
203
196
  snapshot: ActiveContextSnapshot;
204
197
  refs: EvidenceRef[];
@@ -216,31 +209,92 @@ export function stageIdentifiedToolBrief(input: {
216
209
  );
217
210
  return args ? `${factualToolName(call.name)}(${args})` : factualToolName(call.name);
218
211
  });
219
- const label = factualValue(
220
- String(contentText(messages[0]) ?? "").split(/\r?\n/).find((line) => line.trim()) ?? "",
221
- IDENTIFIED_BRIEF_HEAD_CHARS,
222
- );
212
+ // THE LEADING PARAGRAPH, not the first line (Shane 2026-08-14). A tool result
213
+ // that opens with a header line and then states, in prose, what the caller must
214
+ // do or know had everything after that header dropped: the head kept ONE line
215
+ // and the header was it. sol-20260814-traps rep 1 is what that costs. A stage
216
+ // result opens "STAGE 09 / read" and carries its audit note, including a code
217
+ // word and a correction WITHDRAWING an earlier one, on the next line. The fold
218
+ // committed at entry 121 with a head of "STAGE 09 / read", the model brief that
219
+ // did carry both facts landed at entry 311, and the answer at entry 185 in
220
+ // between was a fabricated code word present nowhere in the run.
221
+ //
222
+ // Reading to the first blank line is the generic form of that: a result whose
223
+ // payload follows a blank line contributes its whole opening paragraph, and a
224
+ // result that is bulk from line one contributes exactly what it did before. The
225
+ // deterministic brief is what a fold is COMMITTED with, so it has to stand alone
226
+ // the moment the fold appears, and never be the thing an async upgrade rescues.
227
+ // Paragraph-shaped means TERMINATED: an opening block is only kept whole when
228
+ // a blank line ends it before the bulk. A result that is bulk from its first
229
+ // line has no opening prose to keep, and handing its first thousand characters
230
+ // to the brief is noise that inflates every status page and shrinks every
231
+ // fold's reclaimable mass; that result contributes its first line, exactly as
232
+ // before.
233
+ const leadingParagraph = (message: unknown): string | null => {
234
+ const lines = String(contentText(message) ?? "").split(/\r?\n/);
235
+ const kept: string[] = [];
236
+ for (const line of lines) {
237
+ if (line.trim()) kept.push(line.trim());
238
+ else if (kept.length) return kept.join(" ");
239
+ }
240
+ return null;
241
+ };
223
242
  const tail = identifiedResultTail(messages.at(-1), factualValue);
224
- const composed = [
243
+ // THE AGENT'S OWN CLOSING NOTES (Shane 2026-08-14). The batch the fold absorbs
244
+ // runs past its results: the assistant text between the last result and the
245
+ // next batch's work is the agent recording what it just derived, and
246
+ // sol-20260814-traps rep 2 lost trace-a-02 and trace-d-05 because those lines
247
+ // lived nowhere else. Each assistant message contributes its leading
248
+ // paragraph, INCLUDING the one that also opens the next batch: in a
249
+ // pull-shaped session the agent closes batch N and calls batch N+1 in one
250
+ // message (36 of rep 3's 37 assistant messages carry text and the next call
251
+ // together), so the text is collected before the tool-calling break, and
252
+ // ownership stays with batch N because batch N+1's own walk starts after
253
+ // its own results. The walk stops at the first user turn or tool result,
254
+ // and after consuming one tool-calling assistant.
255
+ const resultIndices = refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
256
+ const noteTexts: string[] = [];
257
+ for (let index = Math.max(...resultIndices) + 1; index < snapshot.mapped.length; index += 1) {
258
+ const message = snapshot.mapped[index]?.message;
259
+ const role = messageRole(message);
260
+ if (role !== "assistant") break;
261
+ const paragraphText = assistantNoteText(message);
262
+ if (paragraphText) noteTexts.push(paragraphText);
263
+ const callsTools = (denseOwnArrayValues(ownValue(message, "content")) ?? [])
264
+ .some((part) => ownValue(part, "type") === "toolCall");
265
+ if (callsTools) break;
266
+ }
267
+ // Compose the identifying parts FIRST, then hand the paragraph what is left of
268
+ // the bound. A fixed head allowance either starves the paragraph or outgrows
269
+ // the composed contract; the remainder does neither. The head is seated before
270
+ // the notes: the instruction prose won gate 134 on sealed evidence, so when
271
+ // both are long the head keeps its allowance and the notes take the remainder.
272
+ const assemble = (label: string, noted: string): string => [
225
273
  `Read ${oneLine(signatures.join("; "), IDENTIFIED_BRIEF_CALLS_CHARS)}`,
226
274
  label ? `opens "${label}"` : "",
227
275
  tail ? `ends "${tail}"` : "",
276
+ noted ? `agent noted "${noted}"` : "",
228
277
  calls.length > 1 ? `${calls.length} exact results here` : "exact source here",
229
278
  ].filter(Boolean).join(" · ");
230
- const bounded = oneLine(composed, ACTIVE_CONTEXT_POLICY.maxBriefChars);
279
+ // A present label costs its own wrapper on top of the fixed parts: the
280
+ // ` · opens "` prefix and closing quote are 11 characters that assemble("")
281
+ // does not count, because an empty label drops the whole term.
282
+ const headAllowance = Math.max(0, ACTIVE_CONTEXT_POLICY.maxBriefChars - assemble("", "").length - 11);
283
+ const paragraph = leadingParagraph(messages[0]);
284
+ const label = paragraph !== null
285
+ ? factualValue(paragraph, headAllowance)
286
+ : factualValue(
287
+ String(contentText(messages[0]) ?? "").split(/\r?\n/).find((line) => line.trim()) ?? "",
288
+ IDENTIFIED_BRIEF_HEAD_CHARS,
289
+ );
290
+ // The notes wrapper costs 18 characters the empty form does not count, the
291
+ // same accounting as the label's 11.
292
+ const notesAllowance = Math.max(0, ACTIVE_CONTEXT_POLICY.maxBriefChars - assemble(label, "").length - 18);
293
+ const noted = noteTexts.length ? factualValue(noteTexts.join(" · "), notesAllowance) : "";
294
+ const bounded = oneLine(assemble(label, noted), ACTIVE_CONTEXT_POLICY.maxBriefChars);
231
295
  return usefulBrief(bounded, ACTIVE_CONTEXT_POLICY.maxBriefChars, snapshot.toolName) ? bounded : null;
232
296
  }
233
297
 
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
298
  export function peekedSourceFoldIds(
245
299
  snapshot: ActiveContextSnapshot,
246
300
  refs: readonly EvidenceRef[],
@@ -262,14 +316,6 @@ export function peekedSourceFoldIds(
262
316
  return ids.length ? ids : null;
263
317
  }
264
318
 
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
319
  export function peekReclaimBrief(sourceFoldIds: readonly string[]): string {
274
320
  const named = sourceFoldIds.join(", ");
275
321
  return sourceFoldIds.length === 1
@@ -293,26 +339,19 @@ export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: F
293
339
  throw new Error("Automatic tool brief crossed a validated assistant batch");
294
340
  }
295
341
  const first = calls[0];
296
- const factualValue = (value: string, maximum: number): string => value
297
- .replace(new RegExp(snapshot.toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "active-context service")
298
- .replace(/[\u0000-\u001f\u007f]+/g, " ")
299
- .replace(/\s+/g, " ")
300
- .trim()
301
- .slice(0, maximum);
342
+ const factualValue = (value: string, maximum: number): string => oneLine(
343
+ value
344
+ .replace(new RegExp(snapshot.toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "active-context service")
345
+ .replace(/[\u0000-\u001f\u007f]+/g, " "),
346
+ maximum,
347
+ );
302
348
  const factualBriefValue = (value: string): string => factualValue(value, 120);
303
349
  const factualToolName = (name: string): string => name.toLowerCase() === snapshot.toolName.toLowerCase()
304
350
  ? "active-context status inspection"
305
351
  : factualBriefValue(name);
306
352
  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
353
  const peeked = peekedSourceFoldIds(snapshot, refs);
313
354
  if (peeked) return peekReclaimBrief(peeked);
314
- // Exact anchors before generic prose: one sentence per tool made every fold of that
315
- // tool carry the SAME brief, so no index could answer which fold holds a given stage.
316
355
  const identified = stageIdentifiedToolBrief({ snapshot, refs, calls, factualValue, factualToolName });
317
356
  if (identified) return identified;
318
357
  const targets: string[] = [];
@@ -328,10 +367,28 @@ export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: F
328
367
  return `Completed one read-only ${names} batch with ${calls.length} exact results${target}; every stale output remains recoverable from this fold.`;
329
368
  }
330
369
 
331
- /** The narrowest entry in a parent's index that still names a subject rather than a stub. */
332
370
  const MIN_SUBJECT_CHARS = 24;
371
+ const CHAPTER_NOTE_CHARS = 160;
372
+
373
+ // The leading paragraph of an assistant message read as the agent's own note.
374
+ // Unlike a tool result's opening prose, an unterminated block is KEPT: the
375
+ // whole message is the agent's words, so a note with no blank line is a
376
+ // one-paragraph note, not bulk to refuse.
377
+ function assistantNoteText(message: unknown): string {
378
+ const lines = String(contentText(message) ?? "").split(/\r?\n/);
379
+ const kept: string[] = [];
380
+ for (const line of lines) {
381
+ if (line.trim()) kept.push(line.trim());
382
+ else if (kept.length) break;
383
+ }
384
+ return kept.join(" ");
385
+ }
333
386
 
334
- /** Cut to a character budget without splitting a surrogate pair, marked where it cut. */
387
+ // THE one bounding primitive (Shane 2026-08-14). Every deterministic brief cut
388
+ // runs through here so a bound always leaves its mark: "..." means exactly
389
+ // "content continues in the exact source", and a reader never mistakes a cut
390
+ // value for a complete one. oneLine collapses whitespace then delegates;
391
+ // factualValue sanitizes then delegates through oneLine. No other site slices.
335
392
  function boundedSubject(text: string, budget: number): string {
336
393
  if (text.length <= budget) return text;
337
394
  let kept = text.slice(0, Math.max(0, budget - 3)).trimEnd();
@@ -340,48 +397,80 @@ function boundedSubject(text: string, budget: number): string {
340
397
  return `${kept}...`;
341
398
  }
342
399
 
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
- */
359
- export function deterministicConsolidationBrief(candidate: FoldCandidate, state: ActiveContextState): string {
400
+ export function deterministicConsolidationBrief(
401
+ candidate: FoldCandidate,
402
+ state: ActiveContextState,
403
+ toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
404
+ snapshot?: ActiveContextSnapshot,
405
+ ): string {
360
406
  const byId = foldMap(state);
361
- const subjects = candidate.parts.flatMap((part) => {
362
- if (part.kind !== "fold") return [];
363
- const child = byId.get(part.foldId);
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()] : [];
407
+ const escapedToolName = toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
408
+ const sanitize = (value: string): string => value
409
+ .replace(new RegExp(escapedToolName, "gi"), "active-context service")
410
+ .replace(/\s+/g, " ")
411
+ .trim();
412
+ let foldCount = 0;
413
+ let spanStart = Number.MAX_SAFE_INTEGER;
414
+ let spanEnd = -1;
415
+ const seat = (ref: EvidenceRef): void => {
416
+ const item = snapshot ? exactMapped(snapshot, ref) : null;
417
+ if (!item) return;
418
+ spanStart = Math.min(spanStart, item.index);
419
+ spanEnd = Math.max(spanEnd, item.index);
420
+ };
421
+ const childSubjects = candidate.parts.flatMap((part) => {
422
+ if (part.kind === "fold") {
423
+ const child = byId.get(part.foldId);
424
+ if (!child) return [];
425
+ foldCount += 1;
426
+ for (const ref of flattenFoldRefs(child, state)) seat(ref);
427
+ return [sanitize(foldBrief(child, state))];
428
+ }
429
+ seat(part.ref);
430
+ return [];
368
431
  });
432
+ // The agent's own recorded words are the ONLY carrier a derived fact ever
433
+ // has (sol-20260814-traps rep 2 lost trace-a-02 and trace-d-05 exactly here:
434
+ // each lived in one assistant turn between two tool batches, the parent
435
+ // indexed only its children's briefs, and the value reached zero briefs), so
436
+ // every assistant message the group hides contributes its leading paragraph
437
+ // as its OWN subject, sharing the division with the child briefs. The child
438
+ // briefs are not trusted to carry the notes for it: a child's subject is
439
+ // truncated to its division share, and a note past the cut is the rep-2
440
+ // loss again one level up, so a note rides whole or not at all, which the
441
+ // ascending-length division guarantees for anything shorter than its fair
442
+ // share. The span is WALKED rather than read off the candidate's raw parts,
443
+ // because absorption geometry varies: rep 2 held its notes as unclaimed raw
444
+ // parts while denser sessions claim the same assistant text into a child's
445
+ // interval, and both spellings hide the same words. Non-assistant gaps
446
+ // contribute nothing: their facts live in the folds beside them. Without a
447
+ // snapshot the exact text is unreadable and the output is the
448
+ // pre-2026-08-14 bytes exactly.
449
+ const notes: string[] = [];
450
+ if (snapshot && spanEnd >= 0) {
451
+ for (let index = spanStart; index <= spanEnd; index += 1) {
452
+ const item = snapshot.mapped[index];
453
+ if (!item?.message) continue;
454
+ if (messageRole(item.message) !== "assistant") continue;
455
+ const noteText = assistantNoteText(item.message);
456
+ if (noteText) notes.push(sanitize(boundedSubject(noteText, CHAPTER_NOTE_CHARS)));
457
+ }
458
+ }
459
+ const noteCount = notes.length;
460
+ const subjects = [...childSubjects, ...notes];
369
461
  if (!subjects.length) return "Grouped completed context covering no readable folds.";
370
462
  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.
463
+ const lead = noteCount
464
+ ? `Grouped completed context covering ${foldCount} folds and ${noteCount} agent notes: `
465
+ : `Grouped completed context covering ${subjects.length} folds: `;
376
466
  const room = ACTIVE_CONTEXT_POLICY.maxBriefChars - lead.length - 1;
377
467
  const named = Math.max(1, Math.min(subjects.length,
378
468
  Math.floor((room + separator.length) / (MIN_SUBJECT_CHARS + separator.length))));
379
469
  const omitted = subjects.length - named;
380
- const tail = omitted ? `${separator}${omitted} more folds in this group` : "";
470
+ // With notes in the division, what the slice drops may be a note rather than
471
+ // a fold, so the tail only claims folds when folds are all it holds.
472
+ const tail = omitted ? `${separator}${omitted} more ${noteCount ? "in this group" : "folds in this group"}` : "";
381
473
  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
474
  const order = subjects.slice(0, named).map((text, index) => ({ text, index })).sort((a, b) =>
386
475
  a.text.length - b.text.length || a.index - b.index);
387
476
  const bounded = new Array<string>(named);
@@ -395,21 +484,11 @@ export function deterministicConsolidationBrief(candidate: FoldCandidate, state:
395
484
  return `${lead}${bounded.join(separator)}${tail}.`.replace(/\s+/g, " ").trim();
396
485
  }
397
486
 
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
487
  export const ALL_FOLD_KINDS: ReadonlySet<FoldKind> =
407
488
  new Set<FoldKind>(["tool-result", "chapter", "consolidation"]);
408
489
 
409
- /** No placeholder is span material. What an automatic chapter composes is raw entries. */
410
490
  export const NO_FOLD_KINDS: ReadonlySet<FoldKind> = new Set<FoldKind>();
411
491
 
412
- /** The first pinned fold a span would nest, or null when the span swallows no pin. */
413
492
  export function pinnedChildFold(
414
493
  parts: FoldPart[],
415
494
  state: ActiveContextState,
@@ -421,41 +500,18 @@ export function pinnedChildFold(
421
500
  const child = byId.get(part.foldId);
422
501
  if (!child) continue;
423
502
  const refs = flattenFoldRefs(child, state);
424
- const pinned = child.kind === "tool-result"
425
- ? toolRefsProtected(refs, state, snapshot)
426
- : refsProtected(refs, state, snapshot);
503
+ const pinned = refsProtected(refs, state, snapshot);
427
504
  if (pinned) return child;
428
505
  }
429
506
  return null;
430
507
  }
431
508
 
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
509
  export function pinnedNestingRefusal(pinned: ActiveFold, toolName: string): string {
441
510
  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, ` +
511
+ `Release it with ${toolName} {"action":"unpin","ids":["${pinned.id}"]} first, ` +
443
512
  "or name a span that stops at its boundary.";
444
513
  }
445
514
 
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
515
  export function foldedSpanRefusal(ownerId: string, subject: string, toolName: string): string {
460
516
  return `fold refused: ${subject} is already folded inside ${ownerId}, so there is nothing ` +
461
517
  `left to fold there. Read it with ${toolName} {"action":"peek","id":"${ownerId}"}, restore it ` +
@@ -509,22 +565,6 @@ export function chapterUnits(snapshot: ActiveContextSnapshot): ChapterUnit[] {
509
565
  .flatMap((segment) => structurallyClosedChapterUnits(snapshot.messages, segment));
510
566
  }
511
567
 
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
568
  export function unpinnedStaleFolds(
529
569
  snapshot: ActiveContextSnapshot,
530
570
  state: ActiveContextState,
@@ -533,7 +573,6 @@ export function unpinnedStaleFolds(
533
573
  .filter(({ fold }) => !refsProtected(flattenFoldRefs(fold, state), state, snapshot));
534
574
  }
535
575
 
536
- /** Everything a pending mark already speaks for: whole folds by id, raw evidence by key. */
537
576
  export function pendingMarkClaims(
538
577
  state: ActiveContextState,
539
578
  ): { foldIds: Set<string>; refKeys: Set<string> } {
@@ -552,7 +591,6 @@ export function pendingMarkClaims(
552
591
  return { foldIds, refKeys };
553
592
  }
554
593
 
555
- /** Whether the raw material between two roots is material a parent may swallow. */
556
594
  function absorbableGap(
557
595
  snapshot: ActiveContextSnapshot,
558
596
  state: ActiveContextState,
@@ -561,9 +599,6 @@ function absorbableGap(
561
599
  claims: { refKeys: Set<string> },
562
600
  ): boolean {
563
601
  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
602
  const parts = partsForRange(snapshot, state, from, to, NO_FOLD_KINDS);
568
603
  if (!parts) return false;
569
604
  const refs = parts.flatMap((part) => part.kind === "raw" ? [part.ref] : []);
@@ -571,7 +606,6 @@ function absorbableGap(
571
606
  return !refs.some((ref) => claims.refKeys.has(objectRefKey(ref)));
572
607
  }
573
608
 
574
- /** Every tool call in the window, by id, with the assistant and the result that close it. */
575
609
  function windowToolLinkage(snapshot: ActiveContextSnapshot): Map<string, { call: number; result: number }> {
576
610
  const linkage = new Map<string, { call: number; result: number }>();
577
611
  const at = (id: string): { call: number; result: number } => {
@@ -598,15 +632,6 @@ function windowToolLinkage(snapshot: ActiveContextSnapshot): Map<string, { call:
598
632
  return linkage;
599
633
  }
600
634
 
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
635
  function linkageClosedSpan(
611
636
  snapshot: ActiveContextSnapshot,
612
637
  start: number,
@@ -630,7 +655,6 @@ function linkageClosedSpan(
630
655
  return { start: from, end: to };
631
656
  }
632
657
 
633
- /** One group of exactly `consolidateAfter` roots, read as the parent it must become. */
634
658
  function consolidationCandidate(
635
659
  snapshot: ActiveContextSnapshot,
636
660
  state: ActiveContextState,
@@ -647,34 +671,9 @@ function consolidationCandidate(
647
671
  if (refsProtected(raw, state, snapshot)) return null;
648
672
  if (raw.some((ref) => claims.refKeys.has(objectRefKey(ref)))) return null;
649
673
  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
674
  return { kind: "consolidation", parts, sourceRefs };
656
675
  }
657
676
 
658
- /**
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.
677
- */
678
677
  export function selectAutomaticConsolidations(
679
678
  snapshot: ActiveContextSnapshot,
680
679
  state: ActiveContextState,
@@ -702,30 +701,12 @@ export function selectAutomaticConsolidations(
702
701
  return groups;
703
702
  }
704
703
 
705
- /** One completed batch the automatic law may take, with the indices it covers. */
706
704
  export interface AutomaticToolBatch {
707
705
  refs: EvidenceRef[];
708
706
  indices: number[];
709
707
  bytes: number;
710
708
  }
711
709
 
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
710
  export function automaticToolBatches(
730
711
  snapshot: ActiveContextSnapshot,
731
712
  state: ActiveContextState,
@@ -748,19 +729,13 @@ export function automaticToolBatches(
748
729
  refs.some((ref) => !ref || ref.role !== "toolResult")) continue;
749
730
  const exactRefs = refs as EvidenceRef[];
750
731
  const size = group.reduce((total, { item }) => total + bytes(item.message), 0);
751
- if (exactRefs.length > snapshot.policy.maxFoldSourceRefs ||
752
- toolRefsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
753
- size < snapshot.policy.minToolChars) continue;
732
+ if (refsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
733
+ size < snapshot.thresholds.minFoldChars) continue;
754
734
  batches.push({ refs: exactRefs, indices: group.map(({ item }) => item.index), bytes: size });
755
735
  }
756
736
  return batches;
757
737
  }
758
738
 
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
739
  export function selectAutomaticToolBatch(
765
740
  snapshot: ActiveContextSnapshot,
766
741
  state: ActiveContextState,
@@ -779,14 +754,6 @@ export function selectAutomaticToolBatch(
779
754
  return [];
780
755
  }
781
756
 
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
- */
790
757
  export function selectAutomaticRefold(
791
758
  snapshot: ActiveContextSnapshot,
792
759
  state: ActiveContextState,
@@ -796,9 +763,7 @@ export function selectAutomaticRefold(
796
763
  if (claimedFoldIds.has(id)) return [];
797
764
  const fold = state.folds.find((item) => item.id === id);
798
765
  const interval = fold ? foldInterval(fold, state, snapshot) : null;
799
- const protectedSource = fold && (fold.kind === "tool-result"
800
- ? toolRefsProtected(flattenFoldRefs(fold, state), state, snapshot)
801
- : refsProtected(flattenFoldRefs(fold, state), state, snapshot));
766
+ const protectedSource = fold && (refsProtected(flattenFoldRefs(fold, state), state, snapshot));
802
767
  return fold && interval && !protectedSource && !state.leases[id]
803
768
  ? [{ id, ...interval }]
804
769
  : [];
@@ -831,63 +796,9 @@ export function resolveFoldInputIds(
831
796
  export function chapterRangeIsUnitAligned(snapshot: ActiveContextSnapshot, start: number, end: number): boolean {
832
797
  const units = chapterUnits(snapshot).filter((unit) => unit.end > start && unit.start <= end);
833
798
  if (!units.length || units[0].start !== start || units.at(-1)!.end !== end + 1) return false;
834
- if (new Set(units.map((unit) => unit.turnStart)).size > snapshot.policy.maxChapterTurns) return false;
835
799
  return units.every((unit, index) => index === 0 || unit.start === units[index - 1].end);
836
800
  }
837
801
 
838
- /**
839
- * Bite-sized folds.
840
- *
841
- * A fold is only navigable if reading one back is cheap. Measured 2026-08-06 (rep 6):
842
- * one 60,432-byte chapter fold hid the fact the run needed in its tail, and every peek
843
- * of it was either truncated short of the answer or too expensive to widen. So a span
844
- * whose content exceeds the cap becomes SEQUENTIAL folds, each with its own brief,
845
- * split only at structurally closed unit boundaries -- never mid-entry.
846
- *
847
- * A single unit that alone exceeds the cap is returned whole: there is no valid
848
- * interior boundary, and refusing to fold it would leave the biggest span in the
849
- * window as the one thing nothing may reclaim. The caller reports the split.
850
- */
851
- export function splitCandidateBySize(
852
- snapshot: ActiveContextSnapshot,
853
- state: ActiveContextState,
854
- candidate: FoldCandidate,
855
- maxChars: number = MAX_FOLD_SPAN_CHARS,
856
- ): FoldCandidate[] {
857
- // A tool-result fold owns exactly one validated assistant batch and a consolidation
858
- // owns whole child folds; neither has an interior boundary to split on.
859
- if (candidate.kind !== "chapter") return [candidate];
860
- const refs = candidate.sourceRefs.length ? candidate.sourceRefs : candidateSourceRefs(candidate.parts, state);
861
- const indices = refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
862
- if (!indices.length || indices.some((index) => index < 0)) return [candidate];
863
- const start = Math.min(...indices);
864
- const end = Math.max(...indices);
865
- if (spanBytes(snapshot, start, end + 1) <= maxChars) return [candidate];
866
- const units = chapterUnits(snapshot).filter((unit) => unit.start >= start && unit.end <= end + 1);
867
- if (units.length < 2) return [candidate];
868
- const split: FoldCandidate[] = [];
869
- const emit = (from: number, to: number): boolean => {
870
- const parts = partsForRange(snapshot, state, from, to, new Set<FoldKind>(["tool-result"]));
871
- if (!parts) return false;
872
- split.push({ kind: "chapter", parts, sourceRefs: candidateSourceRefs(parts, state) });
873
- return true;
874
- };
875
- let runStart = units[0].start;
876
- let runBytes = 0;
877
- for (const unit of units) {
878
- const size = spanBytes(snapshot, unit.start, unit.end);
879
- if (runBytes > 0 && runBytes + size > maxChars) {
880
- if (!emit(runStart, unit.start - 1)) return [candidate];
881
- runStart = unit.start;
882
- runBytes = 0;
883
- }
884
- runBytes += size;
885
- }
886
- if (runBytes > 0 && !emit(runStart, units.at(-1)!.end - 1)) return [candidate];
887
- return split.length ? split : [candidate];
888
- }
889
-
890
- /** Exact serialized size of a candidate's whole span, folds and raw entries alike. */
891
802
  export function candidateSpanChars(
892
803
  snapshot: ActiveContextSnapshot,
893
804
  state: ActiveContextState,
@@ -899,12 +810,10 @@ export function candidateSpanChars(
899
810
  return spanBytes(snapshot, Math.min(...indices), Math.max(...indices) + 1);
900
811
  }
901
812
 
902
- /** Exact serialized size of a half-open mapped message range. */
903
813
  export function spanBytes(snapshot: ActiveContextSnapshot, start: number, end: number): number {
904
814
  return bytes(snapshot.messages.slice(start, end));
905
815
  }
906
816
 
907
- /** One auto-snap the runtime applied to a requested span, reported never inferred. */
908
817
  export interface SpanCorrection {
909
818
  from: string[];
910
819
  to: string[];
@@ -916,15 +825,6 @@ export interface SnappedFoldSpan {
916
825
  corrections: SpanCorrection[];
917
826
  }
918
827
 
919
- /**
920
- * Resolve a requested span, correcting it where a correction exists.
921
- *
922
- * A span the agent got slightly wrong -- crossing a fold it forgot about, reaching
923
- * into the turn still in flight, landing mid-unit -- is a span it MEANT, and refusing
924
- * it teaches the agent that curating is a coin flip. So an invalid span SNAPS to the
925
- * nearest valid edge and the correction is stated in the result. Rejection is reserved
926
- * for a request with no valid interpretation at all, and it says exactly what failed.
927
- */
928
828
  export function snapFoldCandidate(
929
829
  snapshot: ActiveContextSnapshot,
930
830
  state: ActiveContextState,
@@ -937,11 +837,6 @@ export function snapFoldCandidate(
937
837
  } catch (error) {
938
838
  directError = error instanceof Error ? error : new Error(String(error));
939
839
  }
940
- // Nearest first, then the other readings of the same intent. A span that cuts into one
941
- // fold has exactly two endpoint corrections -- absorb it, or step past it -- and which
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.
945
840
  const alternatives = snapSpanAlternatives(snapshot, state, ids);
946
841
  let lastError: Error = directError;
947
842
  for (const snapped of alternatives) {
@@ -954,23 +849,12 @@ export function snapFoldCandidate(
954
849
  lastError = error instanceof Error ? error : new Error(String(error));
955
850
  }
956
851
  }
957
- // A suggestion the caller cannot act on is worse than no suggestion: never name a
958
- // "nearest valid span" this same validation just refused. Nothing snapped, so the
959
- // refusal is the direct one, stated once.
960
852
  throw new Error(
961
853
  `${directError.message}. No corrected reading of that span is constructible`,
962
854
  { cause: lastError },
963
855
  );
964
856
  }
965
857
 
966
- /**
967
- * Every constructible reading of a requested span, nearest intent first.
968
- *
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.
973
- */
974
858
  export function snapSpanAlternatives(
975
859
  snapshot: ActiveContextSnapshot,
976
860
  state: ActiveContextState,
@@ -991,13 +875,6 @@ export function snapSpanAlternatives(
991
875
  });
992
876
  }
993
877
 
994
- /**
995
- * The requested span read as the whole folds it cuts into.
996
- *
997
- * Constructible only when the outward snap lands on a contiguous tiling of two or more
998
- * foldable roots: then the span the agent named IS those folds, and consolidating them
999
- * is the one reading that both covers what was asked for and validates.
1000
- */
1001
878
  export function snapSpanToWholeFolds(
1002
879
  snapshot: ActiveContextSnapshot,
1003
880
  state: ActiveContextState,
@@ -1023,19 +900,13 @@ export function snapSpanToWholeFolds(
1023
900
  };
1024
901
  }
1025
902
 
1026
- /**
1027
- * The correction itself: pull the endpoints out of any fold they cut through, out of
1028
- * the turn still in flight, and onto structurally closed unit boundaries.
1029
- */
1030
903
  export function snapSpanIds(
1031
904
  snapshot: ActiveContextSnapshot,
1032
905
  state: ActiveContextState,
1033
906
  ids: string[],
1034
- /** How an endpoint inside a fold is read: by distance, or always one way. */
1035
907
  mode: "nearest" | "absorb" | "exclude" = "nearest",
1036
908
  ): { ids: string[]; corrections: SpanCorrection[] } | null {
1037
909
  let resolved: Array<{ start: number; end: number; fold?: ActiveFold }>;
1038
- // An unknown id has no nearest valid edge: there is nothing to snap TO.
1039
910
  try { resolved = resolveFoldInputIds(snapshot, state, ids); }
1040
911
  catch { return null; }
1041
912
  const requested = [resolved[0].start, resolved.at(-1)!.end] as [number, number];
@@ -1049,11 +920,6 @@ export function snapSpanIds(
1049
920
  });
1050
921
  };
1051
922
  const units = chapterUnits(snapshot);
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
923
  for (let pass = 0; pass < state.folds.length + 2; pass += 1) {
1058
924
  const passStart = start;
1059
925
  const passEnd = end;
@@ -1073,11 +939,6 @@ export function snapSpanIds(
1073
939
  `${absorb ? "end" : "near"} boundary`);
1074
940
  }
1075
941
  }
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");
1080
- }
1081
942
  const startUnit = units.find((unit) => start >= unit.start && start < unit.end);
1082
943
  if (startUnit && startUnit.start !== start) {
1083
944
  start = startUnit.start;
@@ -1088,26 +949,6 @@ export function snapSpanIds(
1088
949
  end = endUnit.end - 1;
1089
950
  note("span ended mid-unit; corrected to the end of its closed user/assistant/tool unit");
1090
951
  }
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
952
  if (start === passStart && end === passEnd) break;
1112
953
  }
1113
954
  if (!corrections.length || end < start ||
@@ -1115,14 +956,6 @@ export function snapSpanIds(
1115
956
  return { ids: [entryIdAt(snapshot, start), entryIdAt(snapshot, end)], corrections };
1116
957
  }
1117
958
 
1118
- /**
1119
- * Snap a span OUTWARD to whole-fold boundaries.
1120
- *
1121
- * The re-boundary verb operates on folds, so a span that cuts one in half has exactly
1122
- * one sane reading: the agent meant that fold. Outward rather than nearest, because
1123
- * losing a fold the agent did not name is the surprising outcome and re-cutting a
1124
- * slightly larger span is not.
1125
- */
1126
959
  export function snapToFoldBoundaries(
1127
960
  snapshot: ActiveContextSnapshot,
1128
961
  state: ActiveContextState,
@@ -1132,7 +965,6 @@ export function snapToFoldBoundaries(
1132
965
  const requested: [number, number] = [resolved[0].start, resolved.at(-1)!.end];
1133
966
  let [start, end] = requested;
1134
967
  const corrections: SpanCorrection[] = [];
1135
- // Fixed point: absorbing one fold can pull the span across the next.
1136
968
  for (let pass = 0; pass < state.folds.length + 1; pass += 1) {
1137
969
  let moved = false;
1138
970
  for (const root of orderedRoots(state, snapshot)) {
@@ -1163,39 +995,19 @@ export function manualFoldCandidate(
1163
995
  snapshot: ActiveContextSnapshot,
1164
996
  state: ActiveContextState,
1165
997
  ids: string[],
1166
- /**
1167
- * Accept a span that is still fresh or protected. Only a caller that will DEFER the
1168
- * fold may pass this: it relaxes eligibility, never structure, so contiguity, unit
1169
- * alignment and batch completeness are still enforced and the commit still refuses
1170
- * to apply a mark whose span is protected at commit time.
1171
- */
1172
998
  options: { allowProtected?: boolean } = {},
1173
999
  ): FoldCandidate {
1174
1000
  const blockedTool = (refs: EvidenceRef[]): boolean =>
1175
- !options.allowProtected && toolRefsProtected(refs, state, snapshot);
1001
+ !options.allowProtected && refsProtected(refs, state, snapshot);
1176
1002
  const blocked = (refs: EvidenceRef[]): boolean =>
1177
1003
  !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
1004
  const owners = directFoldOwners(state.folds);
1182
1005
  const bounded = (candidate: FoldCandidate): FoldCandidate => {
1183
- if (candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs) {
1184
- throw new Error(`Folds may include at most ${snapshot.policy.maxFoldSourceRefs} exact source references`);
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
1006
  for (const part of candidate.parts) {
1191
1007
  if (part.kind !== "raw") continue;
1192
1008
  const ownerId = owners.get(objectRefKey(part.ref));
1193
1009
  if (ownerId) throw new Error(foldedSpanRefusal(ownerId, part.ref.entryId, snapshot.toolName));
1194
1010
  }
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
1011
  const only = candidate.parts.length === 1 ? candidate.parts[0] : null;
1200
1012
  if (only?.kind === "fold") {
1201
1013
  throw new Error(foldedSpanRefusal(only.foldId, "that span", snapshot.toolName));
@@ -1236,10 +1048,6 @@ export function manualFoldCandidate(
1236
1048
  }
1237
1049
  const parts = partsForRange(snapshot, state, start, end, ALL_FOLD_KINDS);
1238
1050
  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
1051
  const cut = orderedRoots(state, snapshot).filter((root) => root.start <= end && start <= root.end);
1244
1052
  const container = cut.find((root) => root.start <= start && root.end >= end);
1245
1053
  throw new Error(container
@@ -1254,12 +1062,7 @@ export function manualFoldCandidate(
1254
1062
  }
1255
1063
 
1256
1064
  export function oneLine(value: string, maximum: number): string {
1257
- const text = value.replace(/\s+/g, " ").trim();
1258
- if (text.length <= maximum) return text;
1259
- let bounded = text.slice(0, maximum).trimEnd();
1260
- const finalCode = bounded.charCodeAt(bounded.length - 1);
1261
- if (finalCode >= 0xd800 && finalCode <= 0xdbff) bounded = bounded.slice(0, -1);
1262
- return bounded;
1065
+ return boundedSubject(value.replace(/\s+/g, " ").trim(), maximum);
1263
1066
  }
1264
1067
 
1265
1068
  export function deterministicChapterBrief(
@@ -1271,8 +1074,6 @@ export function deterministicChapterBrief(
1271
1074
  throw new Error("Deterministic chapter brief requires aligned exact evidence");
1272
1075
  }
1273
1076
  const firstUser = messages.find((message) => messageRole(message) === "user");
1274
- const firstAssistant = messages.find((message) =>
1275
- messageRole(message) === "assistant" && contentText(message).trim());
1276
1077
  const toolCounts = new Map<string, number>();
1277
1078
  for (const message of messages) {
1278
1079
  if (messageRole(message) !== "assistant") continue;
@@ -1282,18 +1083,39 @@ export function deterministicChapterBrief(
1282
1083
  if (typeof name === "string" && name) toolCounts.set(name, (toolCounts.get(name) ?? 0) + 1);
1283
1084
  }
1284
1085
  }
1086
+ // EVERY assistant note in the span, not the first line of the first one
1087
+ // (Shane 2026-08-14). A chapter is the first fold that hides its span, and a
1088
+ // value the agent recorded once on a later turn died with the old single
1089
+ // line: sol-20260814-traps rep 2 lost trace-a-02 and trace-d-05 to exactly
1090
+ // that shape, an assistant message between two tool batches whose words
1091
+ // reached zero briefs. Each note is the message's leading paragraph, capped
1092
+ // per note, seated whole in span order until the brief's own bound refuses
1093
+ // the next one, with the count of what did not seat stated rather than cut.
1094
+ const notes = messages.flatMap((message) => {
1095
+ if (messageRole(message) !== "assistant") return [];
1096
+ const note = assistantNoteText(message);
1097
+ return note ? [boundedSubject(note, CHAPTER_NOTE_CHARS)] : [];
1098
+ });
1285
1099
  const ask = oneLine(firstUser ? contentText(firstUser) : "No user ask in this span", 90);
1286
- const assistant = oneLine(firstAssistant ? contentText(firstAssistant).split(/\r?\n/)[0] :
1287
- "No assistant text in this span", 110);
1288
1100
  const tools = oneLine([...toolCounts.entries()]
1289
1101
  .sort(([left], [right]) => left.localeCompare(right))
1290
1102
  .map(([name, count]) => `${count}×${name}`)
1291
1103
  .join(" ") || "no tools", 500);
1104
+ const separator = " | ";
1105
+ const compose = (kept: number): string => {
1106
+ const omitted = notes.length - kept;
1107
+ const shown = kept ? notes.slice(0, kept).join(separator)
1108
+ : (notes.length ? "" : "No assistant text in this span");
1109
+ const tail = omitted ? `${kept ? separator : ""}${omitted} more notes in this span` : "";
1110
+ return `User: ${ask} · Tools: ${tools} · Assistant: ${shown}${tail}`;
1111
+ };
1112
+ let composed = compose(notes.length);
1113
+ for (let kept = notes.length; kept > 0 && composed.length > ACTIVE_CONTEXT_POLICY.maxBriefChars; kept -= 1) {
1114
+ composed = compose(kept - 1);
1115
+ }
1292
1116
  const escapeName = (name: string) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1293
- const composed = `User: ${ask} · Tools: ${tools} · Assistant: ${assistant}`
1294
- .replace(new RegExp(escapeName(toolName), "gi"), "active-context service");
1117
+ composed = composed.replace(new RegExp(escapeName(toolName), "gi"), "active-context service");
1295
1118
  if (usefulBrief(composed, ACTIVE_CONTEXT_POLICY.maxBriefChars, toolName)) return composed;
1296
- // Constant floor-of-the-floor: provably passes usefulBrief (no tool name, no structural pattern).
1297
1119
  return `Folded ${refs.length} exact messages from this span's complete turns.`;
1298
1120
  }
1299
1121