pi-fold 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -42
- package/extensions/active-context.ts +2375 -601
- package/extensions/evidence.js +15 -2
- package/extensions/index.js +69 -21
- package/extensions/json.ts +33 -1
- package/extensions/lib/curation.ts +141 -139
- package/extensions/lib/folding.ts +418 -74
- package/extensions/lib/instrumentation.ts +24 -4
- package/extensions/lib/measurement.ts +92 -16
- package/extensions/lib/persistence.ts +212 -42
- package/extensions/lib/policy.ts +511 -155
- package/extensions/lib/rollback.ts +216 -0
- package/extensions/lib/scheduling.ts +415 -107
- package/extensions/lib/selection.ts +528 -144
- package/extensions/lib/surfacing.ts +480 -225
- package/extensions/lib/tool-surface.ts +25 -5
- package/extensions/lib/transcript.ts +146 -62
- package/extensions/summarizer.js +206 -10
- package/package.json +1 -1
- package/extensions/lib/advisory.ts +0 -309
|
@@ -28,9 +28,7 @@ import {
|
|
|
28
28
|
orderedRoots,
|
|
29
29
|
refsInOrder,
|
|
30
30
|
refsProtected,
|
|
31
|
-
toolFoldCadence,
|
|
32
31
|
toolRefsProtected,
|
|
33
|
-
visibleCollapsedRoots,
|
|
34
32
|
} from "./measurement.ts";
|
|
35
33
|
import {
|
|
36
34
|
childFoldIds,
|
|
@@ -39,6 +37,7 @@ import {
|
|
|
39
37
|
foldBrief,
|
|
40
38
|
foldIdFor,
|
|
41
39
|
foldMap,
|
|
40
|
+
MAX_ACTIVE_PROTECTED,
|
|
42
41
|
normalizedPart,
|
|
43
42
|
protectionSha256,
|
|
44
43
|
topologySha256,
|
|
@@ -48,7 +47,6 @@ import {
|
|
|
48
47
|
ACTIVE_CONTEXT_POLICY,
|
|
49
48
|
activeContextBrand,
|
|
50
49
|
activeContextSource,
|
|
51
|
-
CONSOLIDATION_WIDTH_THRESHOLD,
|
|
52
50
|
CONTEXT_STATUS_RESPONSE_BYTES,
|
|
53
51
|
EXPAND_LEASE_GENERATIONS,
|
|
54
52
|
MAX_EXPAND_LEASES,
|
|
@@ -56,6 +54,7 @@ import {
|
|
|
56
54
|
PEEK_DEFAULT_MAX_BYTES,
|
|
57
55
|
PEEK_HEAD_SHARE,
|
|
58
56
|
STATUS_DIET_SUGGESTIONS,
|
|
57
|
+
zoneBytes,
|
|
59
58
|
} from "./policy.ts";
|
|
60
59
|
import type {
|
|
61
60
|
ActiveContextSnapshot,
|
|
@@ -72,12 +71,15 @@ import {
|
|
|
72
71
|
automaticToolBrief,
|
|
73
72
|
candidateSourceRefs,
|
|
74
73
|
chapterUnits,
|
|
74
|
+
unpinnedStaleFolds,
|
|
75
75
|
deterministicChapterCandidateBrief,
|
|
76
|
+
deterministicConsolidationBrief,
|
|
77
|
+
NO_FOLD_KINDS,
|
|
76
78
|
partsForRange,
|
|
77
79
|
resultCall,
|
|
78
|
-
|
|
80
|
+
selectAutomaticConsolidations,
|
|
79
81
|
selectAutomaticRefold,
|
|
80
|
-
|
|
82
|
+
selectAutomaticToolBatch,
|
|
81
83
|
} from "./selection.ts";
|
|
82
84
|
|
|
83
85
|
export function selectAutomaticChapter(
|
|
@@ -86,8 +88,17 @@ export function selectAutomaticChapter(
|
|
|
86
88
|
maximumSourceRefs: number = snapshot.policy.maxFoldSourceRefs,
|
|
87
89
|
claimed: ReadonlySet<string> = new Set<string>(),
|
|
88
90
|
): FoldCandidate | null {
|
|
91
|
+
// Every structurally closed unit is a candidate. What keeps a chapter off the newest
|
|
92
|
+
// material is `refsProtected` below -- the fresh byte tail and the agent's pins -- and
|
|
93
|
+
// what keeps it off the running excursion's own evidence is the guard adjudicated at
|
|
94
|
+
// the commit. There is no position filter here: a unit is composable or it is not.
|
|
89
95
|
const units = chapterUnits(snapshot);
|
|
90
|
-
|
|
96
|
+
// An automatic chapter is raw material, always. Letting a chapter swallow placeholders
|
|
97
|
+
// once the root count reached `consolidateAfter` was the old approximation of
|
|
98
|
+
// consolidation, and the count law replaces it outright: a placeholder gets a parent
|
|
99
|
+
// when the count says it must, under a fold that says what it is, not as a side effect
|
|
100
|
+
// of which chapter happened to be composable that pass (Shane 2026-08-10).
|
|
101
|
+
const allowedChildren = NO_FOLD_KINDS;
|
|
91
102
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
|
|
92
103
|
const first = units[unitIndex];
|
|
93
104
|
let best: FoldCandidate | null = null;
|
|
@@ -124,6 +135,46 @@ export function selectAutomaticChapter(
|
|
|
124
135
|
return null;
|
|
125
136
|
}
|
|
126
137
|
|
|
138
|
+
/**
|
|
139
|
+
* ONE automatic selection law for stale material.
|
|
140
|
+
*
|
|
141
|
+
* There is no chapter rung and no consolidation rung any more, each with its own
|
|
142
|
+
* trigger: automation proposes the stalest eligible SPAN, and what the span contains
|
|
143
|
+
* decides the fold's kind. Raw narrative folds as a chapter; a parent the root count
|
|
144
|
+
* requires folds as a consolidation. Nothing here is pressure-scaled: the epoch's own
|
|
145
|
+
* commit trigger decides WHEN, and this decides WHAT.
|
|
146
|
+
*
|
|
147
|
+
* One candidate, because that is what a single inline rung can apply. The count law
|
|
148
|
+
* itself is not rationed this way: the commit epoch takes EVERY group the count requires
|
|
149
|
+
* in one pass, and this is the leftover route by which the stalest of them can also ride
|
|
150
|
+
* an epoch's own rewrite.
|
|
151
|
+
*/
|
|
152
|
+
export function selectAutomaticStaleSpan(
|
|
153
|
+
snapshot: ActiveContextSnapshot,
|
|
154
|
+
state: ActiveContextState,
|
|
155
|
+
claimed: ReadonlySet<string> = new Set<string>(),
|
|
156
|
+
): FoldCandidate | null {
|
|
157
|
+
const chapter = selectAutomaticChapter(snapshot, state, snapshot.policy.maxFoldSourceRefs, claimed);
|
|
158
|
+
const folds = selectAutomaticConsolidations(snapshot, state)[0] ?? null;
|
|
159
|
+
if (!chapter || !folds) return chapter ?? folds;
|
|
160
|
+
const at = (candidate: FoldCandidate): number =>
|
|
161
|
+
exactMapped(snapshot, candidate.sourceRefs[0])?.index ?? Number.MAX_SAFE_INTEGER;
|
|
162
|
+
return at(folds) < at(chapter) ? folds : chapter;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* What the commit epoch proposes: a completed read-only tool batch if one is stale, and
|
|
167
|
+
* otherwise the stalest span the one law composes. This is the whole automatic path.
|
|
168
|
+
*/
|
|
169
|
+
export function selectAutomaticSpan(
|
|
170
|
+
snapshot: ActiveContextSnapshot,
|
|
171
|
+
state: ActiveContextState,
|
|
172
|
+
claimed: ReadonlySet<string> = new Set<string>(),
|
|
173
|
+
): FoldCandidate | null {
|
|
174
|
+
return selectAutomaticToolBatch(snapshot, state, claimed)[0] ??
|
|
175
|
+
selectAutomaticStaleSpan(snapshot, state, claimed);
|
|
176
|
+
}
|
|
177
|
+
|
|
127
178
|
export type AutomaticRungSelection =
|
|
128
179
|
| { kind: "prepared-chapter"; candidate: FoldCandidate }
|
|
129
180
|
| { kind: "tool"; candidate: FoldCandidate }
|
|
@@ -133,7 +184,6 @@ export type AutomaticRungSelection =
|
|
|
133
184
|
| { kind: "chapter-prepare"; candidate: FoldCandidate };
|
|
134
185
|
|
|
135
186
|
export interface AutomaticRungSelectionOptions {
|
|
136
|
-
waiveToolCadence?: boolean;
|
|
137
187
|
toolOnly?: boolean;
|
|
138
188
|
summarizerAvailable?: boolean;
|
|
139
189
|
failedPreparationIds?: ReadonlySet<string>;
|
|
@@ -169,14 +219,16 @@ export function selectAutomaticRung(
|
|
|
169
219
|
const candidate = selectAutomaticChapter(snapshot, state, snapshot.policy.maxFoldSourceRefs, claimed);
|
|
170
220
|
return candidate ? { kind: "prepared-chapter", candidate } : null;
|
|
171
221
|
}
|
|
172
|
-
const tool =
|
|
222
|
+
const tool = selectAutomaticToolBatch(snapshot, state, claimed)[0] ?? null;
|
|
173
223
|
if (tool) return { kind: "tool", candidate: tool };
|
|
174
224
|
if (options.toolOnly || !Number.isFinite(ratio)) return null;
|
|
175
|
-
const refold = selectAutomaticRefold(snapshot, state,
|
|
225
|
+
const refold = selectAutomaticRefold(snapshot, state, claimedFoldIds);
|
|
176
226
|
if (refold) return { kind: "refold", foldId: refold };
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
227
|
+
// One law decides the span; only the CHAPTER kind still has a preparation path in
|
|
228
|
+
// front of it, because only a chapter brief is worth a model call.
|
|
229
|
+
const span = selectAutomaticStaleSpan(snapshot, state, claimed);
|
|
230
|
+
if (span?.kind === "consolidation") return { kind: "consolidation", candidate: span };
|
|
231
|
+
const chapter = span;
|
|
180
232
|
if (!chapter) return null;
|
|
181
233
|
const preparationFailed = options.failedPreparationIds?.has(
|
|
182
234
|
automaticPreparationId(chapter, state),
|
|
@@ -207,10 +259,11 @@ export function foldCandidatesDetail(
|
|
|
207
259
|
} = {},
|
|
208
260
|
): Record<string, unknown> {
|
|
209
261
|
const measuredRatio = ratio !== null && Number.isFinite(ratio) ? ratio : Number.NaN;
|
|
210
|
-
const tool =
|
|
211
|
-
const refold = selectAutomaticRefold(snapshot, state
|
|
212
|
-
const
|
|
213
|
-
const
|
|
262
|
+
const tool = selectAutomaticToolBatch(snapshot, state)[0] ?? null;
|
|
263
|
+
const refold = selectAutomaticRefold(snapshot, state);
|
|
264
|
+
const staleSpan = selectAutomaticStaleSpan(snapshot, state);
|
|
265
|
+
const consolidation = staleSpan?.kind === "consolidation" ? staleSpan : null;
|
|
266
|
+
const chapter = staleSpan?.kind === "chapter" ? staleSpan : null;
|
|
214
267
|
const selection = selectAutomaticRung(snapshot, state, measuredRatio, {
|
|
215
268
|
summarizerAvailable: options.summarizerAvailable,
|
|
216
269
|
failedPreparationIds: options.failedPreparationIds,
|
|
@@ -243,13 +296,17 @@ export function foldCandidatesDetail(
|
|
|
243
296
|
} : null,
|
|
244
297
|
wouldFireNow,
|
|
245
298
|
blockedBy,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
299
|
+
// The one position that still cuts anything, plus the count law read as the
|
|
300
|
+
// arithmetic it is: the roots it counts, the width it divides by, and how many
|
|
301
|
+
// parents that count owes right now.
|
|
302
|
+
zones: {
|
|
303
|
+
freshBoundary: snapshot.freshBoundary,
|
|
304
|
+
budgetTokens: snapshot.budgetTokens,
|
|
249
305
|
},
|
|
250
306
|
width: {
|
|
251
|
-
visibleRoots:
|
|
252
|
-
threshold:
|
|
307
|
+
visibleRoots: unpinnedStaleFolds(snapshot, state).length,
|
|
308
|
+
threshold: snapshot.thresholds.consolidateAfter,
|
|
309
|
+
groupsDue: selectAutomaticConsolidations(snapshot, state).length,
|
|
253
310
|
},
|
|
254
311
|
};
|
|
255
312
|
}
|
|
@@ -312,6 +369,69 @@ export function encodedFoldSource(
|
|
|
312
369
|
}));
|
|
313
370
|
}
|
|
314
371
|
|
|
372
|
+
/**
|
|
373
|
+
* What a generator reads to brief a PARENT: its children at depth one.
|
|
374
|
+
*
|
|
375
|
+
* `encodedFoldSource` renders a parent's children through `renderFold`, which yields their
|
|
376
|
+
* placeholders, so a parent's source was the children's briefs. Briefing that produces a
|
|
377
|
+
* summary of summaries, and by the second rung the material itself is three descriptions
|
|
378
|
+
* away. So each child is opened here instead: its own brief for orientation, then its
|
|
379
|
+
* contents, with ITS children left as placeholders. Depth one is the whole rule. It bounds
|
|
380
|
+
* the read at one level per rung whatever the ladder's height, and it is the same view an
|
|
381
|
+
* agent gets from expanding the parent, so a brief written from it describes what expanding
|
|
382
|
+
* would actually return.
|
|
383
|
+
*
|
|
384
|
+
* When the opened children do not fit, the LARGEST collapses back to brief-only first, and
|
|
385
|
+
* again until the rest fit. Size is what decides, not position: dropping in span order
|
|
386
|
+
* would spend the whole budget on an early giant and close every child behind it, while
|
|
387
|
+
* taking the giant out first buys back the most room for the fewest subjects lost. A
|
|
388
|
+
* collapsed child still appears, in span order, carrying its brief and saying it is
|
|
389
|
+
* collapsed, so the generator can tell a child it read from a child it only heard about.
|
|
390
|
+
*/
|
|
391
|
+
export function consolidationSourceText(
|
|
392
|
+
snapshot: ActiveContextSnapshot,
|
|
393
|
+
state: ActiveContextState,
|
|
394
|
+
parts: FoldPart[],
|
|
395
|
+
): string {
|
|
396
|
+
const byId = foldMap(state);
|
|
397
|
+
const entries = parts.map((part, index) => {
|
|
398
|
+
if (part.kind === "raw") {
|
|
399
|
+
const item = exactMapped(snapshot, part.ref);
|
|
400
|
+
if (!item) throw new Error(`Exact Pi evidence drift for ${part.ref.entryId}`);
|
|
401
|
+
return { index, foldId: null, kind: "raw" as const, brief: null, messages: [clone(item.message)] };
|
|
402
|
+
}
|
|
403
|
+
const child = byId.get(part.foldId);
|
|
404
|
+
if (!child) throw new Error(`Consolidation source lost child fold ${part.foldId}`);
|
|
405
|
+
const messages = renderFoldParts(child.parts, state, snapshot);
|
|
406
|
+
if (!messages) throw new Error(`Consolidation source projection drifted at ${child.id}`);
|
|
407
|
+
return { index, foldId: child.id, kind: child.kind, brief: foldBrief(child, state), messages };
|
|
408
|
+
});
|
|
409
|
+
const collapsed = new Set<number>();
|
|
410
|
+
const render = (): string => stableStringify(entries.map((entry) => ({
|
|
411
|
+
child: entry.index + 1,
|
|
412
|
+
of: entries.length,
|
|
413
|
+
...(entry.foldId === null ? {} : { foldId: entry.foldId }),
|
|
414
|
+
kind: entry.kind,
|
|
415
|
+
...(entry.brief === null ? {} : { brief: entry.brief }),
|
|
416
|
+
...(collapsed.has(entry.index)
|
|
417
|
+
? { collapsed: "brief only: this child did not fit expanded" }
|
|
418
|
+
: { contents: entry.messages }),
|
|
419
|
+
})));
|
|
420
|
+
// Largest first, and only as many as the budget forces: each collapse is a subject the
|
|
421
|
+
// brief must describe from one sentence instead of from its contents.
|
|
422
|
+
const bySize = entries
|
|
423
|
+
.filter((entry) => entry.foldId !== null)
|
|
424
|
+
.map((entry) => ({ index: entry.index, size: bytes(entry.messages) }))
|
|
425
|
+
.sort((a, b) => b.size - a.size || a.index - b.index);
|
|
426
|
+
let text = render();
|
|
427
|
+
for (const entry of bySize) {
|
|
428
|
+
if (bytes(text) <= snapshot.policy.maxSourceChars) break;
|
|
429
|
+
collapsed.add(entry.index);
|
|
430
|
+
text = render();
|
|
431
|
+
}
|
|
432
|
+
return text;
|
|
433
|
+
}
|
|
434
|
+
|
|
315
435
|
export function boundedOrientation(
|
|
316
436
|
snapshot: ActiveContextSnapshot,
|
|
317
437
|
sourceRefs: EvidenceRef[],
|
|
@@ -339,6 +459,155 @@ export function boundedOrientation(
|
|
|
339
459
|
return { beforeRefs: before, beforeText, afterRefs: after.refs, afterText: after.text };
|
|
340
460
|
}
|
|
341
461
|
|
|
462
|
+
/**
|
|
463
|
+
* What is wrong with a generated brief, said in words its author can act on, or null.
|
|
464
|
+
*
|
|
465
|
+
* Length is a criterion we set, so a brief that misses it is answered rather than cut.
|
|
466
|
+
* Truncating a long brief silently loses whatever the generator put last, which on a group
|
|
467
|
+
* is the last children it covered; the cure is to hand the complaint back and let the
|
|
468
|
+
* generator spend the budget itself. Coverage cannot be checked the same way: no test tells
|
|
469
|
+
* whether a sentence stands for a child, so coverage is instructed in the request and
|
|
470
|
+
* defended by the deterministic index underneath, and what is checked here is the pair of
|
|
471
|
+
* failures that were silent before.
|
|
472
|
+
*/
|
|
473
|
+
export function briefContractComplaint(
|
|
474
|
+
brief: string,
|
|
475
|
+
maxBriefChars: number,
|
|
476
|
+
toolName: string,
|
|
477
|
+
): string | null {
|
|
478
|
+
if (!brief) return "You returned no brief text. Reply with the brief itself and nothing else.";
|
|
479
|
+
if (brief.length > maxBriefChars) {
|
|
480
|
+
return `Your brief was ${brief.length} characters and the limit is ${maxBriefChars}. ` +
|
|
481
|
+
"Rewrite it within the limit. Cut detail, not subjects: every subject you named must " +
|
|
482
|
+
"still be named in the shorter brief.";
|
|
483
|
+
}
|
|
484
|
+
if (!usefulBrief(brief, maxBriefChars, toolName)) {
|
|
485
|
+
return "Your brief carried no factual content once fold navigation lines were dropped. " +
|
|
486
|
+
"Name concrete things from the span: files, identifiers, decisions, results, errors.";
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* One brief from the generator, with one chance to cure a criterion it missed.
|
|
493
|
+
*
|
|
494
|
+
* The no-retry rule this lane was built with holds for what it was written about: a
|
|
495
|
+
* generator that fails on a span fails on it again, and looping would spend the session's
|
|
496
|
+
* calls proving it. A contract complaint is the other case. The request stated a limit, the
|
|
497
|
+
* answer missed it, and the complaint names exactly what to change, so the second ask is
|
|
498
|
+
* new information rather than the same ask twice. It is bounded at one, and only a
|
|
499
|
+
* complaint earns it: transport faults, timeouts and attribution drift still fail on the
|
|
500
|
+
* first answer. Both attempts sit inside the caller's timeout, so a brief costs no more
|
|
501
|
+
* wall clock than it did.
|
|
502
|
+
*/
|
|
503
|
+
export async function generatedBrief(input: {
|
|
504
|
+
summarize: (request: Record<string, unknown>, ctx?: unknown) => Promise<Record<string, unknown>>;
|
|
505
|
+
request: Record<string, unknown>;
|
|
506
|
+
ctx?: unknown;
|
|
507
|
+
maxBriefChars: number;
|
|
508
|
+
toolName: string;
|
|
509
|
+
}): Promise<{ brief: string; provenance: BriefProvenance; cured: boolean }> {
|
|
510
|
+
let complaint: string | null = null;
|
|
511
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
512
|
+
const result = await input.summarize(
|
|
513
|
+
complaint === null ? input.request : { ...input.request, cure: complaint },
|
|
514
|
+
input.ctx,
|
|
515
|
+
);
|
|
516
|
+
const brief = typeof result?.brief === "string" ? result.brief.trim() : "";
|
|
517
|
+
const provenance = generatorAttribution(result);
|
|
518
|
+
complaint = briefContractComplaint(brief, input.maxBriefChars, input.toolName);
|
|
519
|
+
if (complaint === null) return { brief, provenance, cured: attempt > 0 };
|
|
520
|
+
}
|
|
521
|
+
throw new Error(`Model context brief usefulness contract drift after one cure: ${complaint}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Attribution is the harness's own wiring, not the generator's judgment, so it is never
|
|
526
|
+
* handed back to be cured: a summarizer that cannot say which model wrote a brief is broken
|
|
527
|
+
* in a way a second ask cannot mend. Shared by the single and batched paths so the two
|
|
528
|
+
* cannot drift.
|
|
529
|
+
*/
|
|
530
|
+
function generatorAttribution(result: Record<string, unknown> | null | undefined): BriefProvenance {
|
|
531
|
+
const digest = result?.launchContractDigest;
|
|
532
|
+
if (typeof result?.provider !== "string" || !result.provider ||
|
|
533
|
+
typeof result?.model !== "string" || !result.model ||
|
|
534
|
+
typeof result?.effort !== "string" || !result.effort || result.toolCalls !== 0 ||
|
|
535
|
+
(digest !== undefined && (typeof digest !== "string" || !/^[a-f0-9]{64}$/.test(digest)))) {
|
|
536
|
+
throw new Error("Model context brief attribution, zero-tool, or digest contract drift");
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
kind: "model",
|
|
540
|
+
provider: result.provider,
|
|
541
|
+
model: result.model,
|
|
542
|
+
effort: result.effort,
|
|
543
|
+
...(typeof digest === "string" ? { launchContractDigest: digest } : {}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Many briefs from one call, with one chance to cure the ones that missed.
|
|
549
|
+
*
|
|
550
|
+
* The lane used to send one request per fold, two at a time, which meant most folds never
|
|
551
|
+
* reached a generator at all: the 2026-08-11 rep committed 87 folds and applied 14 model
|
|
552
|
+
* briefs, the rest keeping the deterministic sentence because the queue was full. A commit
|
|
553
|
+
* folds its spans together, so they are briefed together, and coverage stops competing with
|
|
554
|
+
* throughput.
|
|
555
|
+
*
|
|
556
|
+
* The cure is per-span and the retry carries only the spans that failed, so one bad brief
|
|
557
|
+
* does not make the model rewrite eleven good ones. A span still failing after its cure
|
|
558
|
+
* keeps its deterministic brief and is named in the error, which is the same outcome a
|
|
559
|
+
* single-span failure has always had; the difference is that its neighbours still land.
|
|
560
|
+
*/
|
|
561
|
+
export async function generatedBriefs(input: {
|
|
562
|
+
summarize: (request: Record<string, unknown>, ctx?: unknown) => Promise<Record<string, unknown>>;
|
|
563
|
+
request: Record<string, unknown>;
|
|
564
|
+
spans: Array<Record<string, unknown>>;
|
|
565
|
+
ctx?: unknown;
|
|
566
|
+
maxBriefChars: number;
|
|
567
|
+
toolName: string;
|
|
568
|
+
}): Promise<{
|
|
569
|
+
briefs: Array<{ brief: string; provenance: BriefProvenance } | null>;
|
|
570
|
+
cured: number;
|
|
571
|
+
complaints: string[];
|
|
572
|
+
}> {
|
|
573
|
+
const settled = new Array<{ brief: string; provenance: BriefProvenance } | null>(input.spans.length)
|
|
574
|
+
.fill(null);
|
|
575
|
+
let pending = input.spans.map((span, index) => ({ span, index }));
|
|
576
|
+
let cured = 0;
|
|
577
|
+
let complaints: string[] = [];
|
|
578
|
+
for (let attempt = 0; attempt < 2 && pending.length; attempt += 1) {
|
|
579
|
+
const asked = pending;
|
|
580
|
+
const result = await input.summarize({
|
|
581
|
+
...input.request,
|
|
582
|
+
spans: asked.map((item) => item.span),
|
|
583
|
+
...(complaints.length
|
|
584
|
+
? {
|
|
585
|
+
cure: asked.map((item, at) =>
|
|
586
|
+
`Brief ${at + 1}: ${complaints[at] ?? "did not meet the stated criteria."}`).join(" "),
|
|
587
|
+
}
|
|
588
|
+
: {}),
|
|
589
|
+
}, input.ctx);
|
|
590
|
+
const provenance = generatorAttribution(result);
|
|
591
|
+
const written = Array.isArray(result?.briefs) ? result.briefs : [];
|
|
592
|
+
const failed: typeof pending = [];
|
|
593
|
+
const next: string[] = [];
|
|
594
|
+
for (let at = 0; at < asked.length; at += 1) {
|
|
595
|
+
const brief = typeof written[at] === "string" ? (written[at] as string).trim() : "";
|
|
596
|
+
const complaint = briefContractComplaint(brief, input.maxBriefChars, input.toolName);
|
|
597
|
+
if (complaint === null) {
|
|
598
|
+
settled[asked[at].index] = { brief, provenance };
|
|
599
|
+
if (attempt > 0) cured += 1;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
failed.push(asked[at]);
|
|
603
|
+
next.push(complaint);
|
|
604
|
+
}
|
|
605
|
+
pending = failed;
|
|
606
|
+
complaints = next;
|
|
607
|
+
}
|
|
608
|
+
return { briefs: settled, cured, complaints };
|
|
609
|
+
}
|
|
610
|
+
|
|
342
611
|
export async function prepareFold(input: {
|
|
343
612
|
candidate: FoldCandidate;
|
|
344
613
|
snapshot: ActiveContextSnapshot;
|
|
@@ -366,6 +635,10 @@ export async function prepareFold(input: {
|
|
|
366
635
|
throw new Error("Fold source is not exact, stale, and unprotected");
|
|
367
636
|
}
|
|
368
637
|
const sourceText = encodedFoldSource(snapshot, state, candidate.parts, candidate.kind);
|
|
638
|
+
const groupChildren = candidate.parts.filter((part) => part.kind === "fold").length;
|
|
639
|
+
const readableSource = groupChildren
|
|
640
|
+
? consolidationSourceText(snapshot, state, candidate.parts)
|
|
641
|
+
: sourceText;
|
|
369
642
|
const orientation = boundedOrientation(snapshot, candidate.sourceRefs);
|
|
370
643
|
const sourceSha256 = sha256Text(sourceText);
|
|
371
644
|
const beforeSha256 = sha256Text(orientation.beforeText);
|
|
@@ -396,7 +669,7 @@ export async function prepareFold(input: {
|
|
|
396
669
|
input.signal?.addEventListener("abort", relayAbort, { once: true });
|
|
397
670
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
398
671
|
try {
|
|
399
|
-
if (bytes(
|
|
672
|
+
if (bytes(readableSource) > snapshot.policy.maxSourceChars) {
|
|
400
673
|
throw new Error("Fold source exceeds the bounded model-summary request");
|
|
401
674
|
}
|
|
402
675
|
const timed = new Promise<never>((_resolve, reject) => {
|
|
@@ -405,39 +678,31 @@ export async function prepareFold(input: {
|
|
|
405
678
|
reject(new Error(`Context brief exceeded ${snapshot.policy.briefTimeoutMs}ms`));
|
|
406
679
|
}, snapshot.policy.briefTimeoutMs);
|
|
407
680
|
});
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
typeof result?.effort !== "string" || !result.effort || result.toolCalls !== 0 ||
|
|
428
|
-
(digest !== undefined && (typeof digest !== "string" || !/^[a-f0-9]{64}$/.test(digest)))) {
|
|
429
|
-
throw new Error("Model context brief attribution, zero-tool, digest, or usefulness contract drift");
|
|
430
|
-
}
|
|
431
|
-
modelBrief = {
|
|
432
|
-
brief: generated,
|
|
433
|
-
provenance: {
|
|
434
|
-
kind: "model",
|
|
435
|
-
provider: result.provider,
|
|
436
|
-
model: result.model,
|
|
437
|
-
effort: result.effort,
|
|
438
|
-
...(typeof digest === "string" ? { launchContractDigest: digest } : {}),
|
|
681
|
+
const generated = await Promise.race([generatedBrief({
|
|
682
|
+
summarize: input.summarize,
|
|
683
|
+
request: {
|
|
684
|
+
candidateId,
|
|
685
|
+
sourceRefs: clone(candidate.sourceRefs),
|
|
686
|
+
// What the generator READS, which for a group is its children opened one level
|
|
687
|
+
// rather than the placeholders `sourceText` renders. The fold's own identity
|
|
688
|
+
// stays `sourceSha256` above: what a span IS does not change with how it is read.
|
|
689
|
+
sourceText: readableSource,
|
|
690
|
+
sourceSha256: sha256Text(readableSource),
|
|
691
|
+
children: groupChildren,
|
|
692
|
+
beforeRefs: clone(orientation.beforeRefs),
|
|
693
|
+
beforeText: orientation.beforeText,
|
|
694
|
+
beforeSha256,
|
|
695
|
+
afterRefs: clone(orientation.afterRefs),
|
|
696
|
+
afterText: orientation.afterText,
|
|
697
|
+
afterSha256,
|
|
698
|
+
maxBriefChars: snapshot.policy.maxBriefChars,
|
|
699
|
+
signal: controller.signal,
|
|
439
700
|
},
|
|
440
|
-
|
|
701
|
+
ctx: input.ctx,
|
|
702
|
+
maxBriefChars: snapshot.policy.maxBriefChars,
|
|
703
|
+
toolName: snapshot.toolName,
|
|
704
|
+
}), timed]);
|
|
705
|
+
modelBrief = { brief: generated.brief, provenance: generated.provenance };
|
|
441
706
|
} catch (error) {
|
|
442
707
|
if (input.signal?.aborted) throw error;
|
|
443
708
|
input.onSummarizerFailure?.(error);
|
|
@@ -450,7 +715,11 @@ export async function prepareFold(input: {
|
|
|
450
715
|
brief = modelBrief.brief;
|
|
451
716
|
provenance = modelBrief.provenance;
|
|
452
717
|
} else {
|
|
453
|
-
|
|
718
|
+
// By kind, not by assumption: a group that loses its generator falls back to the
|
|
719
|
+
// index of its children, and only a chapter falls back to a chapter's brief.
|
|
720
|
+
brief = candidate.kind === "consolidation"
|
|
721
|
+
? deterministicConsolidationBrief(candidate, state)
|
|
722
|
+
: deterministicChapterCandidateBrief(snapshot, candidate);
|
|
454
723
|
provenance = { kind: "deterministic" };
|
|
455
724
|
}
|
|
456
725
|
}
|
|
@@ -706,6 +975,14 @@ export function protectEvidence(
|
|
|
706
975
|
});
|
|
707
976
|
const byKey = new Map(state.protected.map((ref) => [objectRefKey(ref), ref]));
|
|
708
977
|
for (const ref of refs) protect ? byKey.set(objectRefKey(ref), ref) : byKey.delete(objectRefKey(ref));
|
|
978
|
+
// Atomic: the whole resolved set pins or nothing does. A partial pin that reports
|
|
979
|
+
// success would leave part of the requested span silently reclaimable.
|
|
980
|
+
if (byKey.size > MAX_ACTIVE_PROTECTED) {
|
|
981
|
+
throw new Error(
|
|
982
|
+
`Protecting ${ids.join(", ")} would hold ${byKey.size} refs, over the ${MAX_ACTIVE_PROTECTED}-ref cap; ` +
|
|
983
|
+
"unprotect something first",
|
|
984
|
+
);
|
|
985
|
+
}
|
|
709
986
|
return clearPrepared({
|
|
710
987
|
...state,
|
|
711
988
|
revision: state.revision + 1,
|
|
@@ -1114,7 +1391,7 @@ export function activeContextStatus(
|
|
|
1114
1391
|
endId: eligibleSourceIds.at(-1),
|
|
1115
1392
|
action: { action: "fold", ids: eligibleEndpoints, brief: "<factual brief, at most 1000 characters>" },
|
|
1116
1393
|
} : null,
|
|
1117
|
-
rawTailMinimumBytes: snapshot.
|
|
1394
|
+
rawTailMinimumBytes: zoneBytes(snapshot.thresholds.freshTail, snapshot.budgetTokens),
|
|
1118
1395
|
currentTurnRequiresBoundary: false,
|
|
1119
1396
|
actions: {
|
|
1120
1397
|
status: { action: "status", offset, limit },
|
|
@@ -1274,15 +1551,16 @@ export function projectionSlateCandidates(
|
|
|
1274
1551
|
});
|
|
1275
1552
|
}
|
|
1276
1553
|
|
|
1277
|
-
export
|
|
1554
|
+
export interface FoldSourceRequest {
|
|
1278
1555
|
foldId: string;
|
|
1279
1556
|
state: ActiveContextState;
|
|
1280
1557
|
entries: Array<Record<string, unknown>>;
|
|
1281
1558
|
sessionId: string;
|
|
1282
1559
|
projectEntry?: (entry: Record<string, unknown>) => unknown[];
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/** One exact message per ref identity and digest, read off the immutable session entries. */
|
|
1563
|
+
function exactSessionMessages(input: FoldSourceRequest): Map<string, unknown> {
|
|
1286
1564
|
const projectEntry = input.projectEntry ?? sessionEntryMessages;
|
|
1287
1565
|
const exact = new Map<string, unknown>();
|
|
1288
1566
|
for (const entry of input.entries) {
|
|
@@ -1292,20 +1570,81 @@ export function recoverFoldMessages(input: {
|
|
|
1292
1570
|
exact.set(`${objectRefKey(ref)}:${ref.sha256}`, message);
|
|
1293
1571
|
}
|
|
1294
1572
|
}
|
|
1295
|
-
return
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1573
|
+
return exact;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function exactMessageFor(
|
|
1577
|
+
exact: Map<string, unknown>,
|
|
1578
|
+
ref: EvidenceRef,
|
|
1579
|
+
sessionId: string,
|
|
1580
|
+
): unknown {
|
|
1581
|
+
if (ref.sessionId !== sessionId) throw new Error(`Fold source ${ref.entryId} belongs to another session`);
|
|
1582
|
+
const message = exact.get(`${objectRefKey(ref)}:${ref.sha256}`);
|
|
1583
|
+
if (!message || evidenceSha256(message) !== ref.sha256) {
|
|
1584
|
+
throw new Error(`Exact recovery failed for ${ref.entryId}`);
|
|
1585
|
+
}
|
|
1586
|
+
return clone(message);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* One fold's ENTIRE original source, at every depth.
|
|
1591
|
+
*
|
|
1592
|
+
* This is the rescue read: restoration and the recovery audit both need the bytes as the
|
|
1593
|
+
* session first wrote them, with nothing standing in for anything. Retrieval inside a
|
|
1594
|
+
* session is the other function, and it serves one level.
|
|
1595
|
+
*/
|
|
1596
|
+
export function recoverFoldMessages(input: FoldSourceRequest): unknown[] {
|
|
1597
|
+
const fold = input.state.folds.find((item) => item.id === input.foldId);
|
|
1598
|
+
if (!fold) throw new Error(`Unknown active-context fold ${input.foldId}`);
|
|
1599
|
+
const exact = exactSessionMessages(input);
|
|
1600
|
+
return flattenFoldRefs(fold, input.state).map((ref) => exactMessageFor(exact, ref, input.sessionId));
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/** A child fold as its parent's stored span holds it: the placeholder, not the bytes under it. */
|
|
1604
|
+
export function storedChildPlaceholder(child: ActiveFold, state: ActiveContextState): Record<string, unknown> {
|
|
1605
|
+
return {
|
|
1606
|
+
placeholder: "fold",
|
|
1607
|
+
id: child.id,
|
|
1608
|
+
kind: child.kind,
|
|
1609
|
+
brief: foldBrief(child, state),
|
|
1610
|
+
sourceSha256: child.sourceSha256,
|
|
1611
|
+
sourceBytes: child.sourceChars,
|
|
1612
|
+
children: childFoldIds(child).length,
|
|
1613
|
+
peek: { action: "peek", id: child.id },
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* One fold's stored span, AS STORED: one level.
|
|
1619
|
+
*
|
|
1620
|
+
* A fold's span is what it took in, and what it took in was already whatever the window
|
|
1621
|
+
* held: raw entries, and placeholders for folds that were there first. So the honest read
|
|
1622
|
+
* of a parent is that same span, children still placeheld. Reading it as the original
|
|
1623
|
+
* bytes at every depth would hand back a whole session's transcript for one call and
|
|
1624
|
+
* undo the nesting the fold performed.
|
|
1625
|
+
*
|
|
1626
|
+
* The verbatim floor is untouched by this. A leaf's stored span IS its original bytes, so
|
|
1627
|
+
* the bytes are always exactly one peek away from any id the read hands back.
|
|
1628
|
+
*/
|
|
1629
|
+
export function foldStoredSpan(input: FoldSourceRequest): unknown[] {
|
|
1630
|
+
const fold = input.state.folds.find((item) => item.id === input.foldId);
|
|
1631
|
+
if (!fold) throw new Error(`Unknown active-context fold ${input.foldId}`);
|
|
1632
|
+
const exact = exactSessionMessages(input);
|
|
1633
|
+
const byId = foldMap(input.state);
|
|
1634
|
+
return fold.parts.map((part) => {
|
|
1635
|
+
if (part.kind === "raw") return exactMessageFor(exact, part.ref, input.sessionId);
|
|
1636
|
+
const child = byId.get(part.foldId);
|
|
1637
|
+
if (!child) throw new Error(`Missing active-context child ${part.foldId}`);
|
|
1638
|
+
return storedChildPlaceholder(child, input.state);
|
|
1302
1639
|
});
|
|
1303
1640
|
}
|
|
1304
1641
|
|
|
1305
1642
|
/**
|
|
1306
|
-
* Ephemeral point read of one fold's
|
|
1307
|
-
*
|
|
1308
|
-
* projection: the fold stays collapsed and
|
|
1643
|
+
* Ephemeral point read of one fold's stored span. It returns the same SHA-256-verified
|
|
1644
|
+
* span expansion restores, ONE level: raw entries verbatim, child folds still placeheld.
|
|
1645
|
+
* It is bounded for one model call and changes no projection: the fold stays collapsed and
|
|
1646
|
+
* the durable state is untouched. Any id in the result peeks in one hop, so the verbatim
|
|
1647
|
+
* floor is a hop away at every depth rather than a payload nobody asked for.
|
|
1309
1648
|
*/
|
|
1310
1649
|
export function peekFoldSource(input: {
|
|
1311
1650
|
foldId: string;
|
|
@@ -1321,7 +1660,7 @@ export function peekFoldSource(input: {
|
|
|
1321
1660
|
}): Record<string, unknown> {
|
|
1322
1661
|
const fold = input.state.folds.find((item) => item.id === input.foldId);
|
|
1323
1662
|
if (!fold) throw new Error(`Unknown active-context fold ${input.foldId}`);
|
|
1324
|
-
const messages =
|
|
1663
|
+
const messages = foldStoredSpan({
|
|
1325
1664
|
foldId: input.foldId,
|
|
1326
1665
|
state: input.state,
|
|
1327
1666
|
entries: input.entries,
|
|
@@ -1377,12 +1716,17 @@ export function peekFoldSource(input: {
|
|
|
1377
1716
|
...(truncated
|
|
1378
1717
|
? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxChapterChars) } }
|
|
1379
1718
|
: {}),
|
|
1380
|
-
lifetime: "these bytes stay in the window exactly as returned, like any other tool " +
|
|
1381
|
-
|
|
1719
|
+
lifetime: "these bytes stay in the window exactly as returned, like any other tool result, and " +
|
|
1720
|
+
`nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s stored source, so the ` +
|
|
1721
|
+
`next commit reclaims it behind a placeholder naming ${fold.id}; peek ${fold.id} again for the ` +
|
|
1722
|
+
"same verbatim bytes, or pin this result to keep the copy raw.",
|
|
1382
1723
|
note: truncated
|
|
1383
1724
|
? `Bounded read: ${returnedBytes} of ${sourceBytes} exact source bytes, ${view.omittedBytes} omitted ` +
|
|
1384
1725
|
`from the middle. Widen with bytes, page with offset, or expand ${fold.id} to restore it in place.`
|
|
1385
|
-
:
|
|
1726
|
+
: children.length
|
|
1727
|
+
? `Complete stored span, one level: raw entries exactly, and ${children.length} child fold(s) still ` +
|
|
1728
|
+
"placeheld. Peek a child id to read its own span; the fold stayed collapsed and no projection changed."
|
|
1729
|
+
: "Complete exact source; the fold stayed collapsed and no projection changed.",
|
|
1386
1730
|
source: returned,
|
|
1387
1731
|
// Serialized AFTER the source on purpose: a reader that just consumed a bounded slice
|
|
1388
1732
|
// decides its next action at the end of the payload, and a notice buried before ten
|