pi-fold 1.0.4 → 2.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 +34 -8
- package/extensions/active-context.ts +245 -2355
- package/extensions/evidence.js +0 -10
- package/extensions/index.js +10 -30
- package/extensions/json.ts +3 -29
- package/extensions/lib/canonical.ts +0 -8
- package/extensions/lib/curation.ts +1 -258
- package/extensions/lib/folding.ts +33 -406
- package/extensions/lib/instrumentation.ts +3 -124
- package/extensions/lib/measurement.ts +0 -71
- package/extensions/lib/persistence.ts +216 -197
- package/extensions/lib/policy.ts +29 -558
- package/extensions/lib/rollback.ts +0 -53
- package/extensions/lib/scheduling.ts +36 -367
- package/extensions/lib/selection.ts +206 -312
- package/extensions/lib/tool-surface.ts +5 -18
- package/extensions/lib/transcript.ts +32 -108
- package/package.json +1 -1
- package/extensions/lib/surfacing.ts +0 -604
- package/extensions/summarizer.js +0 -291
|
@@ -152,19 +152,23 @@ export function toolCallArguments(snapshot: ActiveContextSnapshot, assistantInde
|
|
|
152
152
|
return calls.get(id);
|
|
153
153
|
}
|
|
154
154
|
|
|
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
155
|
export const IDENTIFIED_BRIEF_ARGUMENT_CHARS = 240;
|
|
162
156
|
export const IDENTIFIED_BRIEF_VALUE_CHARS = 96;
|
|
157
|
+
// A composed stage-identified brief is held to the ONE policy cap, maxBriefChars
|
|
158
|
+
// (Shane 2026-08-14: "2k brief is more appropriate"). The head keeps the
|
|
159
|
+
// result's leading paragraph and gets exactly what remains of the cap after the
|
|
160
|
+
// parts that identify the fold (call signatures, tail anchor, fixed text), so
|
|
161
|
+
// the paragraph takes every character the contract can give it. The 1,100
|
|
162
|
+
// sub-cap this replaced held 100 below a separate 1,200 supplied maximum as
|
|
163
|
+
// headroom for commit-time gap absorption's appended sentence; the absorption
|
|
164
|
+
// eligibility law (gate 136) removed that hazard, and both smaller numbers went
|
|
165
|
+
// with it. Gate 134 pins the paragraph surviving.
|
|
166
|
+
// The head bound for a result with NO terminated opening paragraph: its first
|
|
167
|
+
// line, exactly the pre-paragraph behaviour.
|
|
163
168
|
export const IDENTIFIED_BRIEF_HEAD_CHARS = 160;
|
|
164
169
|
export const IDENTIFIED_BRIEF_TAIL_CHARS = 120;
|
|
165
170
|
export const IDENTIFIED_BRIEF_CALLS_CHARS = 400;
|
|
166
171
|
|
|
167
|
-
/** Every scalar argument of a call, in stable key order, bounded per value and in total. */
|
|
168
172
|
export function identifiedCallArguments(
|
|
169
173
|
args: unknown,
|
|
170
174
|
factual: (value: string, maximum: number) => string,
|
|
@@ -182,7 +186,6 @@ export function identifiedCallArguments(
|
|
|
182
186
|
return oneLine(pairs.join(", "), IDENTIFIED_BRIEF_ARGUMENT_CHARS);
|
|
183
187
|
}
|
|
184
188
|
|
|
185
|
-
/** The last characters of a result, which is the region a truncated read never shows. */
|
|
186
189
|
export function identifiedResultTail(
|
|
187
190
|
message: unknown,
|
|
188
191
|
factual: (value: string, maximum: number) => string,
|
|
@@ -192,13 +195,6 @@ export function identifiedResultTail(
|
|
|
192
195
|
return factual(text.slice(Math.max(0, text.length - IDENTIFIED_BRIEF_TAIL_CHARS)), IDENTIFIED_BRIEF_TAIL_CHARS);
|
|
193
196
|
}
|
|
194
197
|
|
|
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
198
|
export function stageIdentifiedToolBrief(input: {
|
|
203
199
|
snapshot: ActiveContextSnapshot;
|
|
204
200
|
refs: EvidenceRef[];
|
|
@@ -216,31 +212,92 @@ export function stageIdentifiedToolBrief(input: {
|
|
|
216
212
|
);
|
|
217
213
|
return args ? `${factualToolName(call.name)}(${args})` : factualToolName(call.name);
|
|
218
214
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
// THE LEADING PARAGRAPH, not the first line (Shane 2026-08-14). A tool result
|
|
216
|
+
// that opens with a header line and then states, in prose, what the caller must
|
|
217
|
+
// do or know had everything after that header dropped: the head kept ONE line
|
|
218
|
+
// and the header was it. sol-20260814-traps rep 1 is what that costs. A stage
|
|
219
|
+
// result opens "STAGE 09 / read" and carries its audit note, including a code
|
|
220
|
+
// word and a correction WITHDRAWING an earlier one, on the next line. The fold
|
|
221
|
+
// committed at entry 121 with a head of "STAGE 09 / read", the model brief that
|
|
222
|
+
// did carry both facts landed at entry 311, and the answer at entry 185 in
|
|
223
|
+
// between was a fabricated code word present nowhere in the run.
|
|
224
|
+
//
|
|
225
|
+
// Reading to the first blank line is the generic form of that: a result whose
|
|
226
|
+
// payload follows a blank line contributes its whole opening paragraph, and a
|
|
227
|
+
// result that is bulk from line one contributes exactly what it did before. The
|
|
228
|
+
// deterministic brief is what a fold is COMMITTED with, so it has to stand alone
|
|
229
|
+
// the moment the fold appears, and never be the thing an async upgrade rescues.
|
|
230
|
+
// Paragraph-shaped means TERMINATED: an opening block is only kept whole when
|
|
231
|
+
// a blank line ends it before the bulk. A result that is bulk from its first
|
|
232
|
+
// line has no opening prose to keep, and handing its first thousand characters
|
|
233
|
+
// to the brief is noise that inflates every status page and shrinks every
|
|
234
|
+
// fold's reclaimable mass; that result contributes its first line, exactly as
|
|
235
|
+
// before.
|
|
236
|
+
const leadingParagraph = (message: unknown): string | null => {
|
|
237
|
+
const lines = String(contentText(message) ?? "").split(/\r?\n/);
|
|
238
|
+
const kept: string[] = [];
|
|
239
|
+
for (const line of lines) {
|
|
240
|
+
if (line.trim()) kept.push(line.trim());
|
|
241
|
+
else if (kept.length) return kept.join(" ");
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
};
|
|
223
245
|
const tail = identifiedResultTail(messages.at(-1), factualValue);
|
|
224
|
-
|
|
246
|
+
// THE AGENT'S OWN CLOSING NOTES (Shane 2026-08-14). The batch the fold absorbs
|
|
247
|
+
// runs past its results: the assistant text between the last result and the
|
|
248
|
+
// next batch's work is the agent recording what it just derived, and
|
|
249
|
+
// sol-20260814-traps rep 2 lost trace-a-02 and trace-d-05 because those lines
|
|
250
|
+
// lived nowhere else. Each assistant message contributes its leading
|
|
251
|
+
// paragraph, INCLUDING the one that also opens the next batch: in a
|
|
252
|
+
// pull-shaped session the agent closes batch N and calls batch N+1 in one
|
|
253
|
+
// message (36 of rep 3's 37 assistant messages carry text and the next call
|
|
254
|
+
// together), so the text is collected before the tool-calling break, and
|
|
255
|
+
// ownership stays with batch N because batch N+1's own walk starts after
|
|
256
|
+
// its own results. The walk stops at the first user turn or tool result,
|
|
257
|
+
// and after consuming one tool-calling assistant.
|
|
258
|
+
const resultIndices = refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
|
|
259
|
+
const noteTexts: string[] = [];
|
|
260
|
+
for (let index = Math.max(...resultIndices) + 1; index < snapshot.mapped.length; index += 1) {
|
|
261
|
+
const message = snapshot.mapped[index]?.message;
|
|
262
|
+
const role = messageRole(message);
|
|
263
|
+
if (role !== "assistant") break;
|
|
264
|
+
const paragraphText = assistantNoteText(message);
|
|
265
|
+
if (paragraphText) noteTexts.push(paragraphText);
|
|
266
|
+
const callsTools = (denseOwnArrayValues(ownValue(message, "content")) ?? [])
|
|
267
|
+
.some((part) => ownValue(part, "type") === "toolCall");
|
|
268
|
+
if (callsTools) break;
|
|
269
|
+
}
|
|
270
|
+
// Compose the identifying parts FIRST, then hand the paragraph what is left of
|
|
271
|
+
// the bound. A fixed head allowance either starves the paragraph or outgrows
|
|
272
|
+
// the composed contract; the remainder does neither. The head is seated before
|
|
273
|
+
// the notes: the instruction prose won gate 134 on sealed evidence, so when
|
|
274
|
+
// both are long the head keeps its allowance and the notes take the remainder.
|
|
275
|
+
const assemble = (label: string, noted: string): string => [
|
|
225
276
|
`Read ${oneLine(signatures.join("; "), IDENTIFIED_BRIEF_CALLS_CHARS)}`,
|
|
226
277
|
label ? `opens "${label}"` : "",
|
|
227
278
|
tail ? `ends "${tail}"` : "",
|
|
279
|
+
noted ? `agent noted "${noted}"` : "",
|
|
228
280
|
calls.length > 1 ? `${calls.length} exact results here` : "exact source here",
|
|
229
281
|
].filter(Boolean).join(" · ");
|
|
230
|
-
|
|
282
|
+
// A present label costs its own wrapper on top of the fixed parts: the
|
|
283
|
+
// ` · opens "` prefix and closing quote are 11 characters that assemble("")
|
|
284
|
+
// does not count, because an empty label drops the whole term.
|
|
285
|
+
const headAllowance = Math.max(0, ACTIVE_CONTEXT_POLICY.maxBriefChars - assemble("", "").length - 11);
|
|
286
|
+
const paragraph = leadingParagraph(messages[0]);
|
|
287
|
+
const label = paragraph !== null
|
|
288
|
+
? factualValue(paragraph, headAllowance)
|
|
289
|
+
: factualValue(
|
|
290
|
+
String(contentText(messages[0]) ?? "").split(/\r?\n/).find((line) => line.trim()) ?? "",
|
|
291
|
+
IDENTIFIED_BRIEF_HEAD_CHARS,
|
|
292
|
+
);
|
|
293
|
+
// The notes wrapper costs 18 characters the empty form does not count, the
|
|
294
|
+
// same accounting as the label's 11.
|
|
295
|
+
const notesAllowance = Math.max(0, ACTIVE_CONTEXT_POLICY.maxBriefChars - assemble(label, "").length - 18);
|
|
296
|
+
const noted = noteTexts.length ? factualValue(noteTexts.join(" · "), notesAllowance) : "";
|
|
297
|
+
const bounded = oneLine(assemble(label, noted), ACTIVE_CONTEXT_POLICY.maxBriefChars);
|
|
231
298
|
return usefulBrief(bounded, ACTIVE_CONTEXT_POLICY.maxBriefChars, snapshot.toolName) ? bounded : null;
|
|
232
299
|
}
|
|
233
300
|
|
|
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
301
|
export function peekedSourceFoldIds(
|
|
245
302
|
snapshot: ActiveContextSnapshot,
|
|
246
303
|
refs: readonly EvidenceRef[],
|
|
@@ -262,14 +319,6 @@ export function peekedSourceFoldIds(
|
|
|
262
319
|
return ids.length ? ids : null;
|
|
263
320
|
}
|
|
264
321
|
|
|
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
322
|
export function peekReclaimBrief(sourceFoldIds: readonly string[]): string {
|
|
274
323
|
const named = sourceFoldIds.join(", ");
|
|
275
324
|
return sourceFoldIds.length === 1
|
|
@@ -293,26 +342,19 @@ export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: F
|
|
|
293
342
|
throw new Error("Automatic tool brief crossed a validated assistant batch");
|
|
294
343
|
}
|
|
295
344
|
const first = calls[0];
|
|
296
|
-
const factualValue = (value: string, maximum: number): string =>
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
const factualValue = (value: string, maximum: number): string => oneLine(
|
|
346
|
+
value
|
|
347
|
+
.replace(new RegExp(snapshot.toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "active-context service")
|
|
348
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " "),
|
|
349
|
+
maximum,
|
|
350
|
+
);
|
|
302
351
|
const factualBriefValue = (value: string): string => factualValue(value, 120);
|
|
303
352
|
const factualToolName = (name: string): string => name.toLowerCase() === snapshot.toolName.toLowerCase()
|
|
304
353
|
? "active-context status inspection"
|
|
305
354
|
: factualBriefValue(name);
|
|
306
355
|
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
356
|
const peeked = peekedSourceFoldIds(snapshot, refs);
|
|
313
357
|
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
358
|
const identified = stageIdentifiedToolBrief({ snapshot, refs, calls, factualValue, factualToolName });
|
|
317
359
|
if (identified) return identified;
|
|
318
360
|
const targets: string[] = [];
|
|
@@ -328,10 +370,28 @@ export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: F
|
|
|
328
370
|
return `Completed one read-only ${names} batch with ${calls.length} exact results${target}; every stale output remains recoverable from this fold.`;
|
|
329
371
|
}
|
|
330
372
|
|
|
331
|
-
/** The narrowest entry in a parent's index that still names a subject rather than a stub. */
|
|
332
373
|
const MIN_SUBJECT_CHARS = 24;
|
|
374
|
+
const CHAPTER_NOTE_CHARS = 160;
|
|
375
|
+
|
|
376
|
+
// The leading paragraph of an assistant message read as the agent's own note.
|
|
377
|
+
// Unlike a tool result's opening prose, an unterminated block is KEPT: the
|
|
378
|
+
// whole message is the agent's words, so a note with no blank line is a
|
|
379
|
+
// one-paragraph note, not bulk to refuse.
|
|
380
|
+
function assistantNoteText(message: unknown): string {
|
|
381
|
+
const lines = String(contentText(message) ?? "").split(/\r?\n/);
|
|
382
|
+
const kept: string[] = [];
|
|
383
|
+
for (const line of lines) {
|
|
384
|
+
if (line.trim()) kept.push(line.trim());
|
|
385
|
+
else if (kept.length) break;
|
|
386
|
+
}
|
|
387
|
+
return kept.join(" ");
|
|
388
|
+
}
|
|
333
389
|
|
|
334
|
-
|
|
390
|
+
// THE one bounding primitive (Shane 2026-08-14). Every deterministic brief cut
|
|
391
|
+
// runs through here so a bound always leaves its mark: "..." means exactly
|
|
392
|
+
// "content continues in the exact source", and a reader never mistakes a cut
|
|
393
|
+
// value for a complete one. oneLine collapses whitespace then delegates;
|
|
394
|
+
// factualValue sanitizes then delegates through oneLine. No other site slices.
|
|
335
395
|
function boundedSubject(text: string, budget: number): string {
|
|
336
396
|
if (text.length <= budget) return text;
|
|
337
397
|
let kept = text.slice(0, Math.max(0, budget - 3)).trimEnd();
|
|
@@ -340,48 +400,80 @@ function boundedSubject(text: string, budget: number): string {
|
|
|
340
400
|
return `${kept}...`;
|
|
341
401
|
}
|
|
342
402
|
|
|
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 {
|
|
403
|
+
export function deterministicConsolidationBrief(
|
|
404
|
+
candidate: FoldCandidate,
|
|
405
|
+
state: ActiveContextState,
|
|
406
|
+
toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
407
|
+
snapshot?: ActiveContextSnapshot,
|
|
408
|
+
): string {
|
|
360
409
|
const byId = foldMap(state);
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
410
|
+
const escapedToolName = toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
411
|
+
const sanitize = (value: string): string => value
|
|
412
|
+
.replace(new RegExp(escapedToolName, "gi"), "active-context service")
|
|
413
|
+
.replace(/\s+/g, " ")
|
|
414
|
+
.trim();
|
|
415
|
+
let foldCount = 0;
|
|
416
|
+
let spanStart = Number.MAX_SAFE_INTEGER;
|
|
417
|
+
let spanEnd = -1;
|
|
418
|
+
const seat = (ref: EvidenceRef): void => {
|
|
419
|
+
const item = snapshot ? exactMapped(snapshot, ref) : null;
|
|
420
|
+
if (!item) return;
|
|
421
|
+
spanStart = Math.min(spanStart, item.index);
|
|
422
|
+
spanEnd = Math.max(spanEnd, item.index);
|
|
423
|
+
};
|
|
424
|
+
const childSubjects = candidate.parts.flatMap((part) => {
|
|
425
|
+
if (part.kind === "fold") {
|
|
426
|
+
const child = byId.get(part.foldId);
|
|
427
|
+
if (!child) return [];
|
|
428
|
+
foldCount += 1;
|
|
429
|
+
for (const ref of flattenFoldRefs(child, state)) seat(ref);
|
|
430
|
+
return [sanitize(foldBrief(child, state))];
|
|
431
|
+
}
|
|
432
|
+
seat(part.ref);
|
|
433
|
+
return [];
|
|
368
434
|
});
|
|
435
|
+
// The agent's own recorded words are the ONLY carrier a derived fact ever
|
|
436
|
+
// has (sol-20260814-traps rep 2 lost trace-a-02 and trace-d-05 exactly here:
|
|
437
|
+
// each lived in one assistant turn between two tool batches, the parent
|
|
438
|
+
// indexed only its children's briefs, and the value reached zero briefs), so
|
|
439
|
+
// every assistant message the group hides contributes its leading paragraph
|
|
440
|
+
// as its OWN subject, sharing the division with the child briefs. The child
|
|
441
|
+
// briefs are not trusted to carry the notes for it: a child's subject is
|
|
442
|
+
// truncated to its division share, and a note past the cut is the rep-2
|
|
443
|
+
// loss again one level up, so a note rides whole or not at all, which the
|
|
444
|
+
// ascending-length division guarantees for anything shorter than its fair
|
|
445
|
+
// share. The span is WALKED rather than read off the candidate's raw parts,
|
|
446
|
+
// because absorption geometry varies: rep 2 held its notes as unclaimed raw
|
|
447
|
+
// parts while denser sessions claim the same assistant text into a child's
|
|
448
|
+
// interval, and both spellings hide the same words. Non-assistant gaps
|
|
449
|
+
// contribute nothing: their facts live in the folds beside them. Without a
|
|
450
|
+
// snapshot the exact text is unreadable and the output is the
|
|
451
|
+
// pre-2026-08-14 bytes exactly.
|
|
452
|
+
const notes: string[] = [];
|
|
453
|
+
if (snapshot && spanEnd >= 0) {
|
|
454
|
+
for (let index = spanStart; index <= spanEnd; index += 1) {
|
|
455
|
+
const item = snapshot.mapped[index];
|
|
456
|
+
if (!item?.message) continue;
|
|
457
|
+
if (messageRole(item.message) !== "assistant") continue;
|
|
458
|
+
const noteText = assistantNoteText(item.message);
|
|
459
|
+
if (noteText) notes.push(sanitize(boundedSubject(noteText, CHAPTER_NOTE_CHARS)));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const noteCount = notes.length;
|
|
463
|
+
const subjects = [...childSubjects, ...notes];
|
|
369
464
|
if (!subjects.length) return "Grouped completed context covering no readable folds.";
|
|
370
465
|
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.
|
|
466
|
+
const lead = noteCount
|
|
467
|
+
? `Grouped completed context covering ${foldCount} folds and ${noteCount} agent notes: `
|
|
468
|
+
: `Grouped completed context covering ${subjects.length} folds: `;
|
|
376
469
|
const room = ACTIVE_CONTEXT_POLICY.maxBriefChars - lead.length - 1;
|
|
377
470
|
const named = Math.max(1, Math.min(subjects.length,
|
|
378
471
|
Math.floor((room + separator.length) / (MIN_SUBJECT_CHARS + separator.length))));
|
|
379
472
|
const omitted = subjects.length - named;
|
|
380
|
-
|
|
473
|
+
// With notes in the division, what the slice drops may be a note rather than
|
|
474
|
+
// a fold, so the tail only claims folds when folds are all it holds.
|
|
475
|
+
const tail = omitted ? `${separator}${omitted} more ${noteCount ? "in this group" : "folds in this group"}` : "";
|
|
381
476
|
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
477
|
const order = subjects.slice(0, named).map((text, index) => ({ text, index })).sort((a, b) =>
|
|
386
478
|
a.text.length - b.text.length || a.index - b.index);
|
|
387
479
|
const bounded = new Array<string>(named);
|
|
@@ -395,21 +487,11 @@ export function deterministicConsolidationBrief(candidate: FoldCandidate, state:
|
|
|
395
487
|
return `${lead}${bounded.join(separator)}${tail}.`.replace(/\s+/g, " ").trim();
|
|
396
488
|
}
|
|
397
489
|
|
|
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
490
|
export const ALL_FOLD_KINDS: ReadonlySet<FoldKind> =
|
|
407
491
|
new Set<FoldKind>(["tool-result", "chapter", "consolidation"]);
|
|
408
492
|
|
|
409
|
-
/** No placeholder is span material. What an automatic chapter composes is raw entries. */
|
|
410
493
|
export const NO_FOLD_KINDS: ReadonlySet<FoldKind> = new Set<FoldKind>();
|
|
411
494
|
|
|
412
|
-
/** The first pinned fold a span would nest, or null when the span swallows no pin. */
|
|
413
495
|
export function pinnedChildFold(
|
|
414
496
|
parts: FoldPart[],
|
|
415
497
|
state: ActiveContextState,
|
|
@@ -429,33 +511,12 @@ export function pinnedChildFold(
|
|
|
429
511
|
return null;
|
|
430
512
|
}
|
|
431
513
|
|
|
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
514
|
export function pinnedNestingRefusal(pinned: ActiveFold, toolName: string): string {
|
|
441
515
|
return `fold refused: ${pinned.id} is pinned, and nesting it would hide context you asked to keep. ` +
|
|
442
516
|
`Release it with ${toolName} {"action":"unprotect","ids":["${pinned.id}"]} first, ` +
|
|
443
517
|
"or name a span that stops at its boundary.";
|
|
444
518
|
}
|
|
445
519
|
|
|
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
520
|
export function foldedSpanRefusal(ownerId: string, subject: string, toolName: string): string {
|
|
460
521
|
return `fold refused: ${subject} is already folded inside ${ownerId}, so there is nothing ` +
|
|
461
522
|
`left to fold there. Read it with ${toolName} {"action":"peek","id":"${ownerId}"}, restore it ` +
|
|
@@ -509,22 +570,6 @@ export function chapterUnits(snapshot: ActiveContextSnapshot): ChapterUnit[] {
|
|
|
509
570
|
.flatMap((segment) => structurallyClosedChapterUnits(snapshot.messages, segment));
|
|
510
571
|
}
|
|
511
572
|
|
|
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
573
|
export function unpinnedStaleFolds(
|
|
529
574
|
snapshot: ActiveContextSnapshot,
|
|
530
575
|
state: ActiveContextState,
|
|
@@ -533,7 +578,6 @@ export function unpinnedStaleFolds(
|
|
|
533
578
|
.filter(({ fold }) => !refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
534
579
|
}
|
|
535
580
|
|
|
536
|
-
/** Everything a pending mark already speaks for: whole folds by id, raw evidence by key. */
|
|
537
581
|
export function pendingMarkClaims(
|
|
538
582
|
state: ActiveContextState,
|
|
539
583
|
): { foldIds: Set<string>; refKeys: Set<string> } {
|
|
@@ -552,7 +596,6 @@ export function pendingMarkClaims(
|
|
|
552
596
|
return { foldIds, refKeys };
|
|
553
597
|
}
|
|
554
598
|
|
|
555
|
-
/** Whether the raw material between two roots is material a parent may swallow. */
|
|
556
599
|
function absorbableGap(
|
|
557
600
|
snapshot: ActiveContextSnapshot,
|
|
558
601
|
state: ActiveContextState,
|
|
@@ -561,9 +604,6 @@ function absorbableGap(
|
|
|
561
604
|
claims: { refKeys: Set<string> },
|
|
562
605
|
): boolean {
|
|
563
606
|
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
607
|
const parts = partsForRange(snapshot, state, from, to, NO_FOLD_KINDS);
|
|
568
608
|
if (!parts) return false;
|
|
569
609
|
const refs = parts.flatMap((part) => part.kind === "raw" ? [part.ref] : []);
|
|
@@ -571,7 +611,6 @@ function absorbableGap(
|
|
|
571
611
|
return !refs.some((ref) => claims.refKeys.has(objectRefKey(ref)));
|
|
572
612
|
}
|
|
573
613
|
|
|
574
|
-
/** Every tool call in the window, by id, with the assistant and the result that close it. */
|
|
575
614
|
function windowToolLinkage(snapshot: ActiveContextSnapshot): Map<string, { call: number; result: number }> {
|
|
576
615
|
const linkage = new Map<string, { call: number; result: number }>();
|
|
577
616
|
const at = (id: string): { call: number; result: number } => {
|
|
@@ -598,15 +637,6 @@ function windowToolLinkage(snapshot: ActiveContextSnapshot): Map<string, { call:
|
|
|
598
637
|
return linkage;
|
|
599
638
|
}
|
|
600
639
|
|
|
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
640
|
function linkageClosedSpan(
|
|
611
641
|
snapshot: ActiveContextSnapshot,
|
|
612
642
|
start: number,
|
|
@@ -630,7 +660,6 @@ function linkageClosedSpan(
|
|
|
630
660
|
return { start: from, end: to };
|
|
631
661
|
}
|
|
632
662
|
|
|
633
|
-
/** One group of exactly `consolidateAfter` roots, read as the parent it must become. */
|
|
634
663
|
function consolidationCandidate(
|
|
635
664
|
snapshot: ActiveContextSnapshot,
|
|
636
665
|
state: ActiveContextState,
|
|
@@ -647,34 +676,10 @@ function consolidationCandidate(
|
|
|
647
676
|
if (refsProtected(raw, state, snapshot)) return null;
|
|
648
677
|
if (raw.some((ref) => claims.refKeys.has(objectRefKey(ref)))) return null;
|
|
649
678
|
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
679
|
if (sourceRefs.length > snapshot.policy.maxFoldSourceRefs) return null;
|
|
655
680
|
return { kind: "consolidation", parts, sourceRefs };
|
|
656
681
|
}
|
|
657
682
|
|
|
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
683
|
export function selectAutomaticConsolidations(
|
|
679
684
|
snapshot: ActiveContextSnapshot,
|
|
680
685
|
state: ActiveContextState,
|
|
@@ -702,30 +707,12 @@ export function selectAutomaticConsolidations(
|
|
|
702
707
|
return groups;
|
|
703
708
|
}
|
|
704
709
|
|
|
705
|
-
/** One completed batch the automatic law may take, with the indices it covers. */
|
|
706
710
|
export interface AutomaticToolBatch {
|
|
707
711
|
refs: EvidenceRef[];
|
|
708
712
|
indices: number[];
|
|
709
713
|
bytes: number;
|
|
710
714
|
}
|
|
711
715
|
|
|
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
716
|
export function automaticToolBatches(
|
|
730
717
|
snapshot: ActiveContextSnapshot,
|
|
731
718
|
state: ActiveContextState,
|
|
@@ -756,11 +743,6 @@ export function automaticToolBatches(
|
|
|
756
743
|
return batches;
|
|
757
744
|
}
|
|
758
745
|
|
|
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
746
|
export function selectAutomaticToolBatch(
|
|
765
747
|
snapshot: ActiveContextSnapshot,
|
|
766
748
|
state: ActiveContextState,
|
|
@@ -779,14 +761,6 @@ export function selectAutomaticToolBatch(
|
|
|
779
761
|
return [];
|
|
780
762
|
}
|
|
781
763
|
|
|
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
764
|
export function selectAutomaticRefold(
|
|
791
765
|
snapshot: ActiveContextSnapshot,
|
|
792
766
|
state: ActiveContextState,
|
|
@@ -835,27 +809,12 @@ export function chapterRangeIsUnitAligned(snapshot: ActiveContextSnapshot, start
|
|
|
835
809
|
return units.every((unit, index) => index === 0 || unit.start === units[index - 1].end);
|
|
836
810
|
}
|
|
837
811
|
|
|
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
812
|
export function splitCandidateBySize(
|
|
852
813
|
snapshot: ActiveContextSnapshot,
|
|
853
814
|
state: ActiveContextState,
|
|
854
815
|
candidate: FoldCandidate,
|
|
855
816
|
maxChars: number = MAX_FOLD_SPAN_CHARS,
|
|
856
817
|
): 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
818
|
if (candidate.kind !== "chapter") return [candidate];
|
|
860
819
|
const refs = candidate.sourceRefs.length ? candidate.sourceRefs : candidateSourceRefs(candidate.parts, state);
|
|
861
820
|
const indices = refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
|
|
@@ -887,7 +846,6 @@ export function splitCandidateBySize(
|
|
|
887
846
|
return split.length ? split : [candidate];
|
|
888
847
|
}
|
|
889
848
|
|
|
890
|
-
/** Exact serialized size of a candidate's whole span, folds and raw entries alike. */
|
|
891
849
|
export function candidateSpanChars(
|
|
892
850
|
snapshot: ActiveContextSnapshot,
|
|
893
851
|
state: ActiveContextState,
|
|
@@ -899,12 +857,10 @@ export function candidateSpanChars(
|
|
|
899
857
|
return spanBytes(snapshot, Math.min(...indices), Math.max(...indices) + 1);
|
|
900
858
|
}
|
|
901
859
|
|
|
902
|
-
/** Exact serialized size of a half-open mapped message range. */
|
|
903
860
|
export function spanBytes(snapshot: ActiveContextSnapshot, start: number, end: number): number {
|
|
904
861
|
return bytes(snapshot.messages.slice(start, end));
|
|
905
862
|
}
|
|
906
863
|
|
|
907
|
-
/** One auto-snap the runtime applied to a requested span, reported never inferred. */
|
|
908
864
|
export interface SpanCorrection {
|
|
909
865
|
from: string[];
|
|
910
866
|
to: string[];
|
|
@@ -916,15 +872,6 @@ export interface SnappedFoldSpan {
|
|
|
916
872
|
corrections: SpanCorrection[];
|
|
917
873
|
}
|
|
918
874
|
|
|
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
875
|
export function snapFoldCandidate(
|
|
929
876
|
snapshot: ActiveContextSnapshot,
|
|
930
877
|
state: ActiveContextState,
|
|
@@ -937,11 +884,6 @@ export function snapFoldCandidate(
|
|
|
937
884
|
} catch (error) {
|
|
938
885
|
directError = error instanceof Error ? error : new Error(String(error));
|
|
939
886
|
}
|
|
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
887
|
const alternatives = snapSpanAlternatives(snapshot, state, ids);
|
|
946
888
|
let lastError: Error = directError;
|
|
947
889
|
for (const snapped of alternatives) {
|
|
@@ -954,23 +896,12 @@ export function snapFoldCandidate(
|
|
|
954
896
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
955
897
|
}
|
|
956
898
|
}
|
|
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
899
|
throw new Error(
|
|
961
900
|
`${directError.message}. No corrected reading of that span is constructible`,
|
|
962
901
|
{ cause: lastError },
|
|
963
902
|
);
|
|
964
903
|
}
|
|
965
904
|
|
|
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
905
|
export function snapSpanAlternatives(
|
|
975
906
|
snapshot: ActiveContextSnapshot,
|
|
976
907
|
state: ActiveContextState,
|
|
@@ -991,13 +922,6 @@ export function snapSpanAlternatives(
|
|
|
991
922
|
});
|
|
992
923
|
}
|
|
993
924
|
|
|
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
925
|
export function snapSpanToWholeFolds(
|
|
1002
926
|
snapshot: ActiveContextSnapshot,
|
|
1003
927
|
state: ActiveContextState,
|
|
@@ -1023,19 +947,13 @@ export function snapSpanToWholeFolds(
|
|
|
1023
947
|
};
|
|
1024
948
|
}
|
|
1025
949
|
|
|
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
950
|
export function snapSpanIds(
|
|
1031
951
|
snapshot: ActiveContextSnapshot,
|
|
1032
952
|
state: ActiveContextState,
|
|
1033
953
|
ids: string[],
|
|
1034
|
-
/** How an endpoint inside a fold is read: by distance, or always one way. */
|
|
1035
954
|
mode: "nearest" | "absorb" | "exclude" = "nearest",
|
|
1036
955
|
): { ids: string[]; corrections: SpanCorrection[] } | null {
|
|
1037
956
|
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
957
|
try { resolved = resolveFoldInputIds(snapshot, state, ids); }
|
|
1040
958
|
catch { return null; }
|
|
1041
959
|
const requested = [resolved[0].start, resolved.at(-1)!.end] as [number, number];
|
|
@@ -1049,11 +967,6 @@ export function snapSpanIds(
|
|
|
1049
967
|
});
|
|
1050
968
|
};
|
|
1051
969
|
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
970
|
for (let pass = 0; pass < state.folds.length + 2; pass += 1) {
|
|
1058
971
|
const passStart = start;
|
|
1059
972
|
const passEnd = end;
|
|
@@ -1088,9 +1001,6 @@ export function snapSpanIds(
|
|
|
1088
1001
|
end = endUnit.end - 1;
|
|
1089
1002
|
note("span ended mid-unit; corrected to the end of its closed user/assistant/tool unit");
|
|
1090
1003
|
}
|
|
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
1004
|
const covered = units.filter((unit) => unit.start >= start && unit.end <= end + 1);
|
|
1095
1005
|
if (covered.length) {
|
|
1096
1006
|
const turns: number[] = [];
|
|
@@ -1115,14 +1025,6 @@ export function snapSpanIds(
|
|
|
1115
1025
|
return { ids: [entryIdAt(snapshot, start), entryIdAt(snapshot, end)], corrections };
|
|
1116
1026
|
}
|
|
1117
1027
|
|
|
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
1028
|
export function snapToFoldBoundaries(
|
|
1127
1029
|
snapshot: ActiveContextSnapshot,
|
|
1128
1030
|
state: ActiveContextState,
|
|
@@ -1132,7 +1034,6 @@ export function snapToFoldBoundaries(
|
|
|
1132
1034
|
const requested: [number, number] = [resolved[0].start, resolved.at(-1)!.end];
|
|
1133
1035
|
let [start, end] = requested;
|
|
1134
1036
|
const corrections: SpanCorrection[] = [];
|
|
1135
|
-
// Fixed point: absorbing one fold can pull the span across the next.
|
|
1136
1037
|
for (let pass = 0; pass < state.folds.length + 1; pass += 1) {
|
|
1137
1038
|
let moved = false;
|
|
1138
1039
|
for (const root of orderedRoots(state, snapshot)) {
|
|
@@ -1163,39 +1064,22 @@ export function manualFoldCandidate(
|
|
|
1163
1064
|
snapshot: ActiveContextSnapshot,
|
|
1164
1065
|
state: ActiveContextState,
|
|
1165
1066
|
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
1067
|
options: { allowProtected?: boolean } = {},
|
|
1173
1068
|
): FoldCandidate {
|
|
1174
1069
|
const blockedTool = (refs: EvidenceRef[]): boolean =>
|
|
1175
1070
|
!options.allowProtected && toolRefsProtected(refs, state, snapshot);
|
|
1176
1071
|
const blocked = (refs: EvidenceRef[]): boolean =>
|
|
1177
1072
|
!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
1073
|
const owners = directFoldOwners(state.folds);
|
|
1182
1074
|
const bounded = (candidate: FoldCandidate): FoldCandidate => {
|
|
1183
1075
|
if (candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs) {
|
|
1184
1076
|
throw new Error(`Folds may include at most ${snapshot.policy.maxFoldSourceRefs} exact source references`);
|
|
1185
1077
|
}
|
|
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
1078
|
for (const part of candidate.parts) {
|
|
1191
1079
|
if (part.kind !== "raw") continue;
|
|
1192
1080
|
const ownerId = owners.get(objectRefKey(part.ref));
|
|
1193
1081
|
if (ownerId) throw new Error(foldedSpanRefusal(ownerId, part.ref.entryId, snapshot.toolName));
|
|
1194
1082
|
}
|
|
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
1083
|
const only = candidate.parts.length === 1 ? candidate.parts[0] : null;
|
|
1200
1084
|
if (only?.kind === "fold") {
|
|
1201
1085
|
throw new Error(foldedSpanRefusal(only.foldId, "that span", snapshot.toolName));
|
|
@@ -1236,10 +1120,6 @@ export function manualFoldCandidate(
|
|
|
1236
1120
|
}
|
|
1237
1121
|
const parts = partsForRange(snapshot, state, start, end, ALL_FOLD_KINDS);
|
|
1238
1122
|
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
1123
|
const cut = orderedRoots(state, snapshot).filter((root) => root.start <= end && start <= root.end);
|
|
1244
1124
|
const container = cut.find((root) => root.start <= start && root.end >= end);
|
|
1245
1125
|
throw new Error(container
|
|
@@ -1254,12 +1134,7 @@ export function manualFoldCandidate(
|
|
|
1254
1134
|
}
|
|
1255
1135
|
|
|
1256
1136
|
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;
|
|
1137
|
+
return boundedSubject(value.replace(/\s+/g, " ").trim(), maximum);
|
|
1263
1138
|
}
|
|
1264
1139
|
|
|
1265
1140
|
export function deterministicChapterBrief(
|
|
@@ -1271,8 +1146,6 @@ export function deterministicChapterBrief(
|
|
|
1271
1146
|
throw new Error("Deterministic chapter brief requires aligned exact evidence");
|
|
1272
1147
|
}
|
|
1273
1148
|
const firstUser = messages.find((message) => messageRole(message) === "user");
|
|
1274
|
-
const firstAssistant = messages.find((message) =>
|
|
1275
|
-
messageRole(message) === "assistant" && contentText(message).trim());
|
|
1276
1149
|
const toolCounts = new Map<string, number>();
|
|
1277
1150
|
for (const message of messages) {
|
|
1278
1151
|
if (messageRole(message) !== "assistant") continue;
|
|
@@ -1282,18 +1155,39 @@ export function deterministicChapterBrief(
|
|
|
1282
1155
|
if (typeof name === "string" && name) toolCounts.set(name, (toolCounts.get(name) ?? 0) + 1);
|
|
1283
1156
|
}
|
|
1284
1157
|
}
|
|
1158
|
+
// EVERY assistant note in the span, not the first line of the first one
|
|
1159
|
+
// (Shane 2026-08-14). A chapter is the first fold that hides its span, and a
|
|
1160
|
+
// value the agent recorded once on a later turn died with the old single
|
|
1161
|
+
// line: sol-20260814-traps rep 2 lost trace-a-02 and trace-d-05 to exactly
|
|
1162
|
+
// that shape, an assistant message between two tool batches whose words
|
|
1163
|
+
// reached zero briefs. Each note is the message's leading paragraph, capped
|
|
1164
|
+
// per note, seated whole in span order until the brief's own bound refuses
|
|
1165
|
+
// the next one, with the count of what did not seat stated rather than cut.
|
|
1166
|
+
const notes = messages.flatMap((message) => {
|
|
1167
|
+
if (messageRole(message) !== "assistant") return [];
|
|
1168
|
+
const note = assistantNoteText(message);
|
|
1169
|
+
return note ? [boundedSubject(note, CHAPTER_NOTE_CHARS)] : [];
|
|
1170
|
+
});
|
|
1285
1171
|
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
1172
|
const tools = oneLine([...toolCounts.entries()]
|
|
1289
1173
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
1290
1174
|
.map(([name, count]) => `${count}×${name}`)
|
|
1291
1175
|
.join(" ") || "no tools", 500);
|
|
1176
|
+
const separator = " | ";
|
|
1177
|
+
const compose = (kept: number): string => {
|
|
1178
|
+
const omitted = notes.length - kept;
|
|
1179
|
+
const shown = kept ? notes.slice(0, kept).join(separator)
|
|
1180
|
+
: (notes.length ? "" : "No assistant text in this span");
|
|
1181
|
+
const tail = omitted ? `${kept ? separator : ""}${omitted} more notes in this span` : "";
|
|
1182
|
+
return `User: ${ask} · Tools: ${tools} · Assistant: ${shown}${tail}`;
|
|
1183
|
+
};
|
|
1184
|
+
let composed = compose(notes.length);
|
|
1185
|
+
for (let kept = notes.length; kept > 0 && composed.length > ACTIVE_CONTEXT_POLICY.maxBriefChars; kept -= 1) {
|
|
1186
|
+
composed = compose(kept - 1);
|
|
1187
|
+
}
|
|
1292
1188
|
const escapeName = (name: string) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1293
|
-
|
|
1294
|
-
.replace(new RegExp(escapeName(toolName), "gi"), "active-context service");
|
|
1189
|
+
composed = composed.replace(new RegExp(escapeName(toolName), "gi"), "active-context service");
|
|
1295
1190
|
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
1191
|
return `Folded ${refs.length} exact messages from this span's complete turns.`;
|
|
1298
1192
|
}
|
|
1299
1193
|
|