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.
- package/README.md +75 -28
- package/extensions/active-context.ts +1213 -2756
- package/extensions/evidence.js +0 -10
- package/extensions/index.js +25 -32
- package/extensions/json.ts +3 -29
- package/extensions/lib/canonical.ts +0 -8
- package/extensions/lib/curation.ts +52 -313
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +57 -446
- package/extensions/lib/instrumentation.ts +13 -127
- package/extensions/lib/measurement.ts +6 -83
- package/extensions/lib/persistence.ts +272 -229
- package/extensions/lib/policy.ts +165 -612
- package/extensions/lib/rollback.ts +0 -53
- package/extensions/lib/scheduling.ts +352 -450
- package/extensions/lib/selection.ts +212 -390
- package/extensions/lib/tool-surface.ts +29 -46
- package/extensions/lib/transcript.ts +47 -166
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
- package/extensions/lib/surfacing.ts +0 -604
- package/extensions/summarizer.js +0 -291
|
@@ -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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 =
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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 =
|
|
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":"
|
|
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
|
|
752
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|