pi-fold 3.0.0 → 3.1.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 +78 -211
- package/extensions/active-context.ts +483 -244
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +4 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +46 -9
- package/extensions/lib/folding.ts +181 -17
- package/extensions/lib/instrumentation.ts +0 -6
- package/extensions/lib/measurement.ts +37 -4
- package/extensions/lib/persistence.ts +212 -9
- package/extensions/lib/policy.ts +103 -29
- package/extensions/lib/scheduling.ts +105 -18
- package/extensions/lib/selection.ts +112 -39
- package/extensions/lib/tool-surface.ts +37 -6
- package/extensions/lib/transcript.ts +65 -14
- package/extensions/settings.ts +11 -36
- package/package.json +1 -1
package/extensions/evidence.js
CHANGED
|
@@ -562,10 +562,6 @@ function mcpServerForTool(toolName, fallback) {
|
|
|
562
562
|
: fallback;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
function conventionalMcpTool(toolName) {
|
|
566
|
-
return mcpServerForTool(toolName, null) !== null;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
565
|
export function registerEvidenceIngestion(pi, {
|
|
570
566
|
entryTypePrefix = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
|
|
571
567
|
} = {}) {
|
|
@@ -626,7 +622,7 @@ export function registerEvidenceIngestion(pi, {
|
|
|
626
622
|
return projectEvidenceText(text, descriptor, "utf8-head", truncation ? { truncation } : undefined);
|
|
627
623
|
}
|
|
628
624
|
|
|
629
|
-
if (
|
|
625
|
+
if (mcpServerForTool(toolName, null) !== null) {
|
|
630
626
|
const envelope = eventEnvelope(event);
|
|
631
627
|
const serialized = `${stableStringify(envelope)}\n`;
|
|
632
628
|
if (utf8Bytes(serialized) <= TOOL_RESULT_PROJECTION_BYTES) return;
|
package/extensions/index.js
CHANGED
|
@@ -10,7 +10,8 @@ export {
|
|
|
10
10
|
} from "./settings.ts";
|
|
11
11
|
|
|
12
12
|
const PUBLIC_OPTIONS = Object.freeze([
|
|
13
|
-
"thresholds", "providerInputBudget", "blacklistAutoFoldTools",
|
|
13
|
+
"thresholds", "providerInputBudget", "blacklistAutoFoldTools", "toolFoldThreshold",
|
|
14
|
+
"workingMemory", "postFoldNotice",
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
17
|
const GENERATOR_DELETED = "the model brief generator is deleted (2026-08-14): across 15 sealed " +
|
|
@@ -27,8 +28,8 @@ const REFUSED_OPTIONS = Object.freeze({
|
|
|
27
28
|
guidance: "deleted 2026-08-23: it switched the copy that taught the agent to " +
|
|
28
29
|
"CHOOSE SPANS, and the agent does not choose spans. The runtime cuts at the frontier and " +
|
|
29
30
|
"the agent annotates what it cut, so the guidance it switched is replaced wholesale by " +
|
|
30
|
-
"the post-fold notice
|
|
31
|
-
"
|
|
31
|
+
"the post-fold notice. That notice has its own option, postFoldNotice, which defaults " +
|
|
32
|
+
"false: the deterministic shape is what the campaign measured",
|
|
32
33
|
toolName: "the deployment identity is hardwired to pi-fold: the tool is pi_fold_context",
|
|
33
34
|
toolLabel: "the deployment identity is hardwired to pi-fold",
|
|
34
35
|
brandNoun: "the deployment identity is hardwired to pi-fold",
|
package/extensions/json.ts
CHANGED
|
@@ -194,8 +194,30 @@ export function isObjectRef(value: unknown): value is EvidenceRef {
|
|
|
194
194
|
typeof sha256 === "string" && reflectApply(regexpTest, sha256Pattern, [sha256]);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// THE HOT PATH (issue #2, 2026-08-28). This is the most-called function in the package:
|
|
198
|
+
// every mark, span, claim, eligibility and measurement pass keys its refs through it,
|
|
199
|
+
// and a reporter profiling a 467KB window found the serializer under it at the top of a
|
|
200
|
+
// V8 profile with the process pegged at 80 percent for forty minutes. The general
|
|
201
|
+
// serializer earns its cost on arbitrary values, checking for proxies, cycles, accessors
|
|
202
|
+
// and non-plain prototypes; none of that is in question for three strings off a ref.
|
|
203
|
+
//
|
|
204
|
+
// The output is IDENTICAL, not merely equivalent, and that is the whole design of this
|
|
205
|
+
// shape: safeJson emits object keys in own-property order rather than sorted, so the
|
|
206
|
+
// literal below is the same byte sequence its object form produced, and string escaping
|
|
207
|
+
// is native JSON.stringify on both paths. It matters because these keys order
|
|
208
|
+
// `protectionSha256`'s sort and reach `branchSha256`'s missing-digest, so a format that
|
|
209
|
+
// merely round-trips would move digests that sealed sessions carry. Anything that is not
|
|
210
|
+
// three strings falls through to the serializer, which owns every remaining case.
|
|
211
|
+
// Measured 8.5x, 4.27us to 0.50us per call, over 2,000 refs. Gate 150 pins the identity.
|
|
197
212
|
export function objectRefKey(ref: EvidenceRef): string {
|
|
198
|
-
|
|
213
|
+
const sessionId = ref.sessionId;
|
|
214
|
+
const entryId = ref.entryId;
|
|
215
|
+
const role = ref.role;
|
|
216
|
+
if (typeof sessionId === "string" && typeof entryId === "string" && typeof role === "string") {
|
|
217
|
+
return `{"sessionId":${jsonStringify(sessionId)},"entryId":${jsonStringify(entryId)},` +
|
|
218
|
+
`"role":${jsonStringify(role)}}`;
|
|
219
|
+
}
|
|
220
|
+
return stableStringify({ sessionId, entryId, role });
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
export function sameObjectIdentity(left: EvidenceRef, right: EvidenceRef): boolean {
|
|
@@ -15,6 +15,40 @@ export function bytes(value: unknown): number {
|
|
|
15
15
|
return Buffer.byteLength(typeof value === "string" ? value : stableStringify(value), "utf8");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Does `heldText`, the serialization of the first `held` elements, stand as a prefix of
|
|
20
|
+
* `bodyText`, the serialization of the whole array?
|
|
21
|
+
*
|
|
22
|
+
* ARRAY SERIALIZATION IS PREFIX-FREE BY CONSTRUCTION, which is the whole reason this can be
|
|
23
|
+
* answered from a string the caller already has instead of by serializing a slice. `safeJson`
|
|
24
|
+
* emits `[`, then the elements separated by `,`, then `]`: no whitespace, no trailing comma.
|
|
25
|
+
* So the serialization of the first `held` elements is exactly the head of the whole one with
|
|
26
|
+
* `]` in place of the `,` that follows element `held - 1`.
|
|
27
|
+
*
|
|
28
|
+
* The two ends break that rule and are branched on rather than argued away. At `held` 0 the
|
|
29
|
+
* prefix is `[]` and there is no comma to match, so testing `"[," ` would be nonsense. At
|
|
30
|
+
* `held === length` there is no following element and the two strings are simply equal.
|
|
31
|
+
*
|
|
32
|
+
* The comma is the reason this is not a bare `startsWith`, and it is worth being exact about
|
|
33
|
+
* when it earns its place. JSON's object and string forms SELF-TERMINATE, with `}` and a
|
|
34
|
+
* closing quote, so for the message arrays this has one caller for, a bare `startsWith` would
|
|
35
|
+
* happen to agree. Numbers do not self-terminate: `[1]` is a leading substring of `[12]` and
|
|
36
|
+
* a bare `startsWith` accepts it. The separator is what makes the predicate correct for any
|
|
37
|
+
* array rather than for today's caller, and a false positive in that caller splices a stale
|
|
38
|
+
* head in front of a live tail. Gate 154 drives the numeric case for exactly this reason.
|
|
39
|
+
*/
|
|
40
|
+
export function serializedPrefixHolds(
|
|
41
|
+
bodyText: string,
|
|
42
|
+
heldText: string | null | undefined,
|
|
43
|
+
held: number,
|
|
44
|
+
length: number,
|
|
45
|
+
): boolean {
|
|
46
|
+
if (heldText === null || heldText === undefined || length < held) return false;
|
|
47
|
+
if (held === 0) return heldText === "[]";
|
|
48
|
+
if (held === length) return bodyText === heldText;
|
|
49
|
+
return bodyText.startsWith(`${heldText.slice(0, -1)},`);
|
|
50
|
+
}
|
|
51
|
+
|
|
18
52
|
export function boundedUtf8(text: string, maximumBytes: number): string {
|
|
19
53
|
const buffer = Buffer.from(text, "utf8");
|
|
20
54
|
if (buffer.length <= maximumBytes) return text;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boundReceiptText } from "./measurement.ts";
|
|
2
|
+
import { oneLine } from "./selection.ts";
|
|
2
3
|
import {
|
|
3
4
|
contextBrand,
|
|
4
5
|
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
* worth a turn's attention, and it is ephemeral, so it never accumulates.
|
|
23
24
|
*/
|
|
24
25
|
export function foldNoticeText(input: {
|
|
25
|
-
unbriefed: ReadonlyArray<{ id: string; kind: string; tokens: number }>;
|
|
26
|
+
unbriefed: ReadonlyArray<{ id: string; kind: string; tokens: number; brief?: string }>;
|
|
26
27
|
pending: number;
|
|
27
28
|
toolName: string;
|
|
28
29
|
brandNoun?: string;
|
|
@@ -49,8 +50,15 @@ export function foldNoticeText(input: {
|
|
|
49
50
|
].join("\n");
|
|
50
51
|
// WHAT IT COULD NOT NAME, IT COUNTS (gate 136's law, and gate 115's shape one level
|
|
51
52
|
// out): a list that stops early and says nothing reads as a complete list.
|
|
53
|
+
// EACH ROW IDENTIFIES ITS SPAN. The pending mark carries the deterministic brief the
|
|
54
|
+
// runtime cut it with, and a notice that withholds it makes the agent guess: the
|
|
55
|
+
// dogfooded 1M session answered "I need to brief three pending folds. What are they?
|
|
56
|
+
// Likely: ..." from memory, and a wrong guess writes an accurate-sounding brief onto
|
|
57
|
+
// the wrong fold. The head is the identification; the agent adds what it cannot know.
|
|
58
|
+
const NOTICE_ROW_CHARS = 160;
|
|
52
59
|
const entries = input.unbriefed
|
|
53
|
-
.map((fold) => `${fold.id} (${fold.kind}, ~${fold.tokens} tokens)`
|
|
60
|
+
.map((fold) => `${fold.id} (${fold.kind}, ~${fold.tokens} tokens)` +
|
|
61
|
+
(fold.brief ? `: ${oneLine(fold.brief, NOTICE_ROW_CHARS)}` : ""));
|
|
54
62
|
const overflowText = (seated: number) =>
|
|
55
63
|
`${input.toolName} {"action":"status"} lists the other ${input.unbriefed.length - seated}.`;
|
|
56
64
|
let seated = entries.length;
|
|
@@ -65,6 +73,42 @@ export function foldNoticeText(input: {
|
|
|
65
73
|
`[${brand} folds] Pending folds are waiting for briefs; the list is unavailable this pass.`);
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* THE WORKING MEMORY'S TABLE OF CONTENTS (Shane, 2026-08-26). One copy rides the
|
|
78
|
+
* projection, refreshed at each commit by the carrier freeze: keys only, freshest first,
|
|
79
|
+
* because the bodies live in state and are read on demand. The instruction is fixed and
|
|
80
|
+
* the key list is the variable part, exactly as the fold notice above, and a list that
|
|
81
|
+
* stops early counts what it dropped (gate 136's law). It shares FOLD_NOTICE_BYTES: both
|
|
82
|
+
* are one-glance carriers and a second bound would be a knob with no second job.
|
|
83
|
+
*/
|
|
84
|
+
export function memoryTocText(input: {
|
|
85
|
+
entries: ReadonlyArray<{ key: string; chars: number }>;
|
|
86
|
+
toolName: string;
|
|
87
|
+
brandNoun?: string;
|
|
88
|
+
}): string {
|
|
89
|
+
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
90
|
+
const head = [
|
|
91
|
+
`[${brand} memory] Your working memory holds ${input.entries.length} ` +
|
|
92
|
+
`entr${input.entries.length === 1 ? "y" : "ies"}, freshest first.`,
|
|
93
|
+
`Read any of them with ${input.toolName} {"action":"recall","keys":["<key>"]}. ` +
|
|
94
|
+
`Update an entry whose facts have changed, or add one, with ${input.toolName} ` +
|
|
95
|
+
'{"action":"remember","key":"<key>","body":"..."}; an empty body removes the entry.',
|
|
96
|
+
].join("\n");
|
|
97
|
+
const rows = input.entries.map((entry) => `${entry.key} (${entry.chars} chars)`);
|
|
98
|
+
const overflowText = (seated: number) =>
|
|
99
|
+
`${input.toolName} {"action":"recall"} lists the other ${input.entries.length - seated}.`;
|
|
100
|
+
let seated = rows.length;
|
|
101
|
+
let list = `Entries: ${rows.join("; ")}.`;
|
|
102
|
+
while (seated > 0 && Buffer.byteLength(`${head}\n${list}`, "utf8") > FOLD_NOTICE_BYTES) {
|
|
103
|
+
seated -= 1;
|
|
104
|
+
list = seated === 0
|
|
105
|
+
? overflowText(0)
|
|
106
|
+
: `Entries: ${rows.slice(0, seated).join("; ")}. ${overflowText(seated)}`;
|
|
107
|
+
}
|
|
108
|
+
return boundReceiptText(`${head}\n${list}`, FOLD_NOTICE_BYTES,
|
|
109
|
+
`[${brand} memory] Working memory has entries; the list is unavailable this pass.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
68
112
|
export interface ContextReceipt {
|
|
69
113
|
kind: string;
|
|
70
114
|
ordinal: number;
|
|
@@ -76,8 +120,6 @@ export interface ContextReceipt {
|
|
|
76
120
|
occupancyAfter: number | null;
|
|
77
121
|
spansFolded: number;
|
|
78
122
|
toolResultsFolded: number;
|
|
79
|
-
splitFolds: number;
|
|
80
|
-
splitFromChars: number;
|
|
81
123
|
absorbedWedges: number;
|
|
82
124
|
recovered: boolean;
|
|
83
125
|
protectedBytes: number;
|
|
@@ -96,8 +138,6 @@ export function contextReceipt(input: Partial<ContextReceipt> & { kind: string;
|
|
|
96
138
|
occupancyAfter: input.occupancyAfter ?? null,
|
|
97
139
|
spansFolded: input.spansFolded ?? 0,
|
|
98
140
|
toolResultsFolded: input.toolResultsFolded ?? 0,
|
|
99
|
-
splitFolds: input.splitFolds ?? 0,
|
|
100
|
-
splitFromChars: input.splitFromChars ?? 0,
|
|
101
141
|
absorbedWedges: input.absorbedWedges ?? 0,
|
|
102
142
|
recovered: input.recovered ?? false,
|
|
103
143
|
protectedBytes: input.protectedBytes ?? 0,
|
|
@@ -128,9 +168,6 @@ export function receiptLine(receipt: ContextReceipt): string {
|
|
|
128
168
|
`about ${receipt.freedTokens} tokens freed.`,
|
|
129
169
|
occupancy,
|
|
130
170
|
folded,
|
|
131
|
-
receipt.splitFolds
|
|
132
|
-
? `A ${receipt.splitFromChars}-char span was split into ${receipt.splitFolds} bite-sized folds.`
|
|
133
|
-
: "",
|
|
134
171
|
receipt.absorbedWedges
|
|
135
172
|
? `${receipt.absorbedWedges} short stale span(s) wedged between folds were absorbed into their ` +
|
|
136
173
|
"later neighbour rather than left as crumbs."
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
stableStringify,
|
|
11
11
|
} from "../json.ts";
|
|
12
12
|
import {
|
|
13
|
+
contentText,
|
|
13
14
|
boundedUtf8,
|
|
14
15
|
bytes,
|
|
15
16
|
clone,
|
|
@@ -71,7 +72,9 @@ import {
|
|
|
71
72
|
chapterUnits,
|
|
72
73
|
unpinnedStaleFolds,
|
|
73
74
|
deterministicChapterCandidateBrief,
|
|
75
|
+
toolClipHead,
|
|
74
76
|
deterministicConsolidationBrief,
|
|
77
|
+
oneLine,
|
|
75
78
|
NO_FOLD_KINDS,
|
|
76
79
|
partsForRange,
|
|
77
80
|
resultCall,
|
|
@@ -79,6 +82,7 @@ import {
|
|
|
79
82
|
selectAutomaticRefold,
|
|
80
83
|
selectAutomaticToolBatch,
|
|
81
84
|
} from "./selection.ts";
|
|
85
|
+
import { staleSpanMatureEnd } from "./transcript.ts";
|
|
82
86
|
|
|
83
87
|
export function selectAutomaticChapter(
|
|
84
88
|
snapshot: ActiveContextSnapshot,
|
|
@@ -87,19 +91,44 @@ export function selectAutomaticChapter(
|
|
|
87
91
|
): FoldCandidate | null {
|
|
88
92
|
const units = chapterUnits(snapshot);
|
|
89
93
|
const allowedChildren = NO_FOLD_KINDS;
|
|
94
|
+
// The maturity floor: a stale span may end only where the batch lane has already had
|
|
95
|
+
// its chance, so a chapter can never claim a tool batch one turn before it qualifies.
|
|
96
|
+
const matureEnd = staleSpanMatureEnd(snapshot.messages);
|
|
90
97
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
|
|
91
98
|
const first = units[unitIndex];
|
|
99
|
+
if (first.end > matureEnd) break;
|
|
92
100
|
let best: FoldCandidate | null = null;
|
|
93
101
|
for (let endIndex = unitIndex; endIndex < units.length; endIndex += 1) {
|
|
94
102
|
const unit = units[endIndex];
|
|
103
|
+
if (unit.end > matureEnd) break;
|
|
95
104
|
if (endIndex > unitIndex && unit.start !== units[endIndex - 1].end) break;
|
|
96
105
|
const coherentSegment = endIndex > unitIndex || first.end - first.start > 1;
|
|
97
106
|
if (!coherentSegment) continue;
|
|
107
|
+
// A DISQUALIFIED RANGE DISQUALIFIES EVERY LARGER ONE, so the walk stops here rather
|
|
108
|
+
// than rediscovering the same obstruction at every remaining endIndex (issue #2,
|
|
109
|
+
// 2026-08-28). The inner loop only ever EXTENDS the range to the right, and all
|
|
110
|
+
// three conditions below are monotone under that extension: an unmapped index or an
|
|
111
|
+
// overlapping root stays inside a larger range, a claimed ref stays claimed because
|
|
112
|
+
// `claimed` is fixed for the whole walk, and a protected ref stays protected. Only
|
|
113
|
+
// `size > MAX_FOLD_SPAN_CHARS` bounded this loop, and these three `continue`s skipped
|
|
114
|
+
// the sizing that would have hit it, so an obstruction near the start of the window
|
|
115
|
+
// let the walk run to the end of `units` for every start: O(units^2) iterations doing
|
|
116
|
+
// O(span) work each. Instrumented on a 549KB window, ONE context event ran 457,986
|
|
117
|
+
// inner iterations and allocated 98,811,691 FoldPart objects, of which 456,724
|
|
118
|
+
// iterations (99.7 percent) died on the `claimed` check and only 72 ever reached the
|
|
119
|
+
// sizing. That is 42 seconds of CPU at 824KB, ungated by occupancy, on every event.
|
|
120
|
+
// With the walk stopped at the first obstruction the same event costs 377ms.
|
|
121
|
+
//
|
|
122
|
+
// This is 00dfeb3's defect one layer out. That commit memoized `orderedRoots` to kill
|
|
123
|
+
// the O(roots x folds) rebuild INSIDE each iteration and its own comment named "a walk
|
|
124
|
+
// that is quadratic in units" as the standing cost; this is that walk. Nothing about
|
|
125
|
+
// WHICH candidate is selected changes: every iteration this skips would have been
|
|
126
|
+
// skipped by the same condition.
|
|
98
127
|
const parts = partsForRange(snapshot, state, first.start, unit.end - 1, allowedChildren);
|
|
99
|
-
if (!parts || parts.some((part) => part.kind === "fold" && state.expanded.includes(part.foldId)))
|
|
128
|
+
if (!parts || parts.some((part) => part.kind === "fold" && state.expanded.includes(part.foldId))) break;
|
|
100
129
|
const refs = candidateSourceRefs(parts, state);
|
|
101
|
-
if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref))))
|
|
102
|
-
if (refsProtected(refs, state, snapshot))
|
|
130
|
+
if (claimed.size && refs.some((ref) => claimed.has(objectRefKey(ref)))) break;
|
|
131
|
+
if (refsProtected(refs, state, snapshot)) break;
|
|
103
132
|
const size = bytes(encodedFoldSource(snapshot, state, parts, "chapter"));
|
|
104
133
|
const biteSized = size <= MAX_FOLD_SPAN_CHARS || endIndex === unitIndex;
|
|
105
134
|
// ACCUMULATE UNTIL OVER, never cut to hit the number. The walk grows the span one
|
|
@@ -414,13 +443,43 @@ export async function prepareFold(input: {
|
|
|
414
443
|
let brief: string;
|
|
415
444
|
let provenance: BriefProvenance;
|
|
416
445
|
if (input.brief !== undefined) {
|
|
417
|
-
const
|
|
418
|
-
input.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
446
|
+
const declaredKind = typeof input.briefProvenance === "object"
|
|
447
|
+
? input.briefProvenance.kind
|
|
448
|
+
: input.briefProvenance ?? "supplied";
|
|
449
|
+
if (declaredKind === "supplied") {
|
|
450
|
+
// A SUPPLIED BRIEF AUGMENTS THE HEAD, NEVER REPLACES IT (rep 7 of
|
|
451
|
+
// sol-20260823-live: replacement deleted the fact-carrying identification and 74
|
|
452
|
+
// of 95 peeks recovered values the run's own briefs dropped). The agent's clause
|
|
453
|
+
// is judged against its own reserve so the refusal names the budget the agent
|
|
454
|
+
// actually has, and the deterministic head keeps the remainder of the one policy
|
|
455
|
+
// cap. Only the agent's OWN words take this path: a deterministic brief arriving
|
|
456
|
+
// through the same door IS the head, and a re-prepared augmented brief is already
|
|
457
|
+
// composed, so each passes through under the whole-brief cap instead.
|
|
458
|
+
const complaint = briefContractComplaint(
|
|
459
|
+
input.brief.trim(), snapshot.policy.agentBriefReserve, snapshot.toolName);
|
|
460
|
+
if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
|
|
461
|
+
const clause = input.brief.trim();
|
|
462
|
+
const head = candidate.kind === "consolidation"
|
|
463
|
+
? deterministicConsolidationBrief(candidate, state, snapshot.toolName, snapshot)
|
|
464
|
+
: deterministicChapterCandidateBrief(snapshot, candidate);
|
|
465
|
+
if (head) {
|
|
466
|
+
const seam = " · Agent: ";
|
|
467
|
+
const headRoom = Math.max(0, snapshot.policy.maxBriefChars - seam.length - clause.length);
|
|
468
|
+
brief = `${oneLine(head, headRoom)}${seam}${clause}`;
|
|
469
|
+
provenance = { kind: "augmented" };
|
|
470
|
+
} else {
|
|
471
|
+
brief = clause;
|
|
472
|
+
provenance = { kind: "supplied" };
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
const complaint = briefContractComplaint(
|
|
476
|
+
input.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
|
|
477
|
+
if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
|
|
478
|
+
brief = input.brief.trim();
|
|
479
|
+
provenance = typeof input.briefProvenance === "object"
|
|
480
|
+
? clone(input.briefProvenance)
|
|
481
|
+
: { kind: declaredKind } as BriefProvenance;
|
|
482
|
+
}
|
|
424
483
|
} else {
|
|
425
484
|
brief = candidate.kind === "consolidation"
|
|
426
485
|
? deterministicConsolidationBrief(candidate, state, snapshot.toolName, snapshot)
|
|
@@ -806,6 +865,13 @@ export function pinnedPeekMass(
|
|
|
806
865
|
state: ActiveContextState,
|
|
807
866
|
): { bytes: number; results: number } {
|
|
808
867
|
const explicitProtected = explicitProtectedKeys(state);
|
|
868
|
+
// ITS TWO SIBLINGS ALREADY RETURN HERE (2026-08-28). `protectedStaleMass` and
|
|
869
|
+
// `explicitProtectedMass` both open with this guard; this one did the whole quadratic walk
|
|
870
|
+
// and then returned zero, because every increment in its final loop is gated on
|
|
871
|
+
// `explicitProtected.has(...)`. So the guard is exact, not an approximation. It is not
|
|
872
|
+
// hypothetical either: `state.protected` is empty in all 10,521 persisted state records
|
|
873
|
+
// across 36 sealed runs, the corpus carrying no `context.pin` event at all.
|
|
874
|
+
if (!explicitProtected.size) return { bytes: 0, results: 0 };
|
|
809
875
|
const folded = new Set<string>();
|
|
810
876
|
for (const fold of state.folds) {
|
|
811
877
|
for (const ref of flattenFoldRefs(fold, state)) folded.add(objectRefKey(ref));
|
|
@@ -859,10 +925,55 @@ export function projectActiveContext(
|
|
|
859
925
|
index += 1;
|
|
860
926
|
}
|
|
861
927
|
}
|
|
928
|
+
applyToolClips(output, state, snapshot);
|
|
862
929
|
assertProjectionPreservesToolLinkage(snapshot.messages, output);
|
|
863
930
|
return output;
|
|
864
931
|
}
|
|
865
932
|
|
|
933
|
+
// THE TOOL-CALL DIET'S VIEW (2026-08-24). A clipped result renders in place as its
|
|
934
|
+
// identified head with the cut stated and the recovery named, matched BY CALL ID and
|
|
935
|
+
// identity-preserving on role, call id, tool name, error flag and timestamp, exactly
|
|
936
|
+
// the ephemeral-peek withdrawal's shape one mechanism over. Clips are only ever ADDED
|
|
937
|
+
// inside a commit transaction, and a commit already rewrites the projection, so a
|
|
938
|
+
// non-commit pass with unchanged clips renders byte-identically and the frozen prefix
|
|
939
|
+
// never moves. Every reader shares this pass because every reader shares
|
|
940
|
+
// projectActiveContext: the live projection, the persistence readers and the
|
|
941
|
+
// attribution lens all see the same clipped view, and the full bytes stay in the
|
|
942
|
+
// transcript behind the entry id the marker names.
|
|
943
|
+
function applyToolClips(
|
|
944
|
+
output: unknown[],
|
|
945
|
+
state: ActiveContextState,
|
|
946
|
+
snapshot: ActiveContextSnapshot,
|
|
947
|
+
): void {
|
|
948
|
+
if (!state.clips?.length) return;
|
|
949
|
+
const byCallId = new Map(state.clips.map((clip) => [clip.callId, clip]));
|
|
950
|
+
for (let index = 0; index < output.length; index += 1) {
|
|
951
|
+
const message = output[index] as Record<string, unknown> | null;
|
|
952
|
+
if (!message || (message as { role?: unknown }).role !== "toolResult") continue;
|
|
953
|
+
const callId = (message as { toolCallId?: unknown }).toolCallId;
|
|
954
|
+
if (typeof callId !== "string") continue;
|
|
955
|
+
const clip = byCallId.get(callId);
|
|
956
|
+
if (!clip) continue;
|
|
957
|
+
const details = (message as { details?: unknown }).details;
|
|
958
|
+
if (details && typeof (details as { projection?: unknown }).projection === "string") continue;
|
|
959
|
+
const full = contentText(message);
|
|
960
|
+
if (full.length <= snapshot.policy.toolClipFloorChars) continue;
|
|
961
|
+
const head = toolClipHead(full, snapshot.policy.toolClipHeadChars);
|
|
962
|
+
const hidden = full.length - head.length;
|
|
963
|
+
if (hidden <= 0) continue;
|
|
964
|
+
output[index] = {
|
|
965
|
+
role: "toolResult",
|
|
966
|
+
toolCallId: callId,
|
|
967
|
+
toolName: (message as { toolName?: unknown }).toolName,
|
|
968
|
+
content: [{ type: "text", text: `${head}\n... [${hidden} more chars clipped; ` +
|
|
969
|
+
`{"action":"peek","id":"${clip.entryId}"} returns the full result]` }],
|
|
970
|
+
isError: (message as { isError?: unknown }).isError ?? false,
|
|
971
|
+
details: { projection: "tool-clip" },
|
|
972
|
+
timestamp: (message as { timestamp?: unknown }).timestamp,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
866
977
|
export function foldStatusRow(fold: ActiveFold, state: ActiveContextState, snapshot: ActiveContextSnapshot): Record<string, unknown> {
|
|
867
978
|
const navigation = foldNavigation(fold, state, snapshot);
|
|
868
979
|
const interval = foldInterval(fold, state, snapshot);
|
|
@@ -1299,11 +1410,26 @@ export function peekFoldSource(input: {
|
|
|
1299
1410
|
const source = stableStringify(messages);
|
|
1300
1411
|
const sourceBytes = bytes(source);
|
|
1301
1412
|
const offset = Math.min(Math.max(0, input.offset ?? 0), sourceBytes);
|
|
1302
|
-
|
|
1413
|
+
// The widest read anyone may ask for is the policy's own source cap, the same ceiling
|
|
1414
|
+
// the `wider` hint has always offered; an unclamped `bytes` was the one door left to a
|
|
1415
|
+
// single result larger than any fold the runtime would cut.
|
|
1416
|
+
const budget = Math.min(
|
|
1417
|
+
input.maximumBytes ?? PEEK_DEFAULT_MAX_BYTES,
|
|
1418
|
+
ACTIVE_CONTEXT_POLICY.maxSourceChars,
|
|
1419
|
+
);
|
|
1420
|
+
// ONE BUDGET GOVERNS THE WHOLE RESULT (sol-20260826-full2, pifold rep 1). The byte
|
|
1421
|
+
// bound used to govern `source` alone while the descendant index rode free, so peeking
|
|
1422
|
+
// the root of a deep consolidation tree returned 320 nested briefs, 423KB against a
|
|
1423
|
+
// stated 16KB bound, durably into a window already near its budget; that one result is
|
|
1424
|
+
// what the projection fence then aborted thirty-five requests over. The index seats
|
|
1425
|
+
// first, into at most half the budget, because a parent's index is the point read; the
|
|
1426
|
+
// source view takes everything the seated rows leave.
|
|
1427
|
+
const catalog = boundedDescendantIndex(fold, input.state, Math.floor(budget / 2));
|
|
1428
|
+
const sourceBudget = budget - catalog.seatedBytes;
|
|
1303
1429
|
const window = offset > 0 ? utf8Slice(source, offset) : source;
|
|
1304
1430
|
const view = offset > 0
|
|
1305
|
-
? { text: boundedUtf8(window,
|
|
1306
|
-
: boundedHeadTail(window,
|
|
1431
|
+
? { text: boundedUtf8(window, sourceBudget), omittedBytes: 0, contiguous: true }
|
|
1432
|
+
: boundedHeadTail(window, sourceBudget);
|
|
1307
1433
|
const returned = view.text;
|
|
1308
1434
|
const returnedBytes = bytes(returned) - bytes(headTailMarker(view.omittedBytes));
|
|
1309
1435
|
const nextOffset = view.contiguous && offset + returnedBytes < sourceBytes
|
|
@@ -1311,7 +1437,7 @@ export function peekFoldSource(input: {
|
|
|
1311
1437
|
: null;
|
|
1312
1438
|
const truncated = view.omittedBytes > 0 || nextOffset !== null;
|
|
1313
1439
|
const children = childFoldIds(fold);
|
|
1314
|
-
const index =
|
|
1440
|
+
const index = catalog.rows;
|
|
1315
1441
|
return {
|
|
1316
1442
|
version: 1,
|
|
1317
1443
|
action: "peek",
|
|
@@ -1330,25 +1456,31 @@ export function peekFoldSource(input: {
|
|
|
1330
1456
|
truncated,
|
|
1331
1457
|
view: offset > 0 ? "slice" : (view.omittedBytes > 0 ? "index" : "complete"),
|
|
1332
1458
|
...(index.length ? { index } : {}),
|
|
1459
|
+
...(catalog.omitted ? { indexOmitted: catalog.omitted } : {}),
|
|
1333
1460
|
children,
|
|
1334
1461
|
narrower: {
|
|
1335
1462
|
...(nextOffset === null ? {} : { slice: { action: "peek", id: fold.id, offset: nextOffset, bytes: returnedBytes } }),
|
|
1336
1463
|
...(children.length ? { child: { action: "peek", id: children[0] } } : {}),
|
|
1337
1464
|
},
|
|
1338
|
-
...(truncated
|
|
1465
|
+
...(truncated || catalog.omitted
|
|
1339
1466
|
? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars) } }
|
|
1340
1467
|
: {}),
|
|
1341
1468
|
lifetime: "these bytes stay in the window exactly as returned, like any other tool result, and " +
|
|
1342
1469
|
`nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s stored source, so the ` +
|
|
1343
1470
|
`next commit reclaims it behind a placeholder naming ${fold.id}; peek ${fold.id} again for the ` +
|
|
1344
1471
|
"same verbatim bytes, or pin this result to keep the copy raw.",
|
|
1345
|
-
note: truncated
|
|
1472
|
+
note: (truncated
|
|
1346
1473
|
? `Bounded read: ${returnedBytes} of ${sourceBytes} exact source bytes, ${view.omittedBytes} omitted ` +
|
|
1347
1474
|
`from the middle. Widen with bytes, page with offset, or expand ${fold.id} to restore it in place.`
|
|
1348
1475
|
: children.length
|
|
1349
1476
|
? `Complete stored span, one level: raw entries exactly, and ${children.length} child fold(s) still ` +
|
|
1350
1477
|
"placeheld. Peek a child id to read its own span; the fold stayed collapsed and no projection changed."
|
|
1351
|
-
: "Complete exact source; the fold stayed collapsed and no projection changed."
|
|
1478
|
+
: "Complete exact source; the fold stayed collapsed and no projection changed.") +
|
|
1479
|
+
(catalog.omitted
|
|
1480
|
+
? ` The nested index seats the shallowest ${index.length} of ${index.length + catalog.omitted} ` +
|
|
1481
|
+
"descendant briefs inside the byte budget; the deeper rows are one hop away by peeking a child id, " +
|
|
1482
|
+
"or widen with bytes."
|
|
1483
|
+
: ""),
|
|
1352
1484
|
source: returned,
|
|
1353
1485
|
...(truncated
|
|
1354
1486
|
? {
|
|
@@ -1388,6 +1520,38 @@ export function boundedHeadTail(
|
|
|
1388
1520
|
};
|
|
1389
1521
|
}
|
|
1390
1522
|
|
|
1523
|
+
/**
|
|
1524
|
+
* The descendant index, seated inside a byte budget: shallowest rows first, whole rows
|
|
1525
|
+
* only, the omitted counted rather than dropped. Depth order is the navigational order,
|
|
1526
|
+
* because a row's own children are reachable by peeking it, so the rows worth seating
|
|
1527
|
+
* first are the ones every deeper row is reachable THROUGH. Walk order breaks depth
|
|
1528
|
+
* ties, which keeps siblings in span order. The seating stops at the first row that
|
|
1529
|
+
* does not fit, so what is omitted is always a contiguous deep tail of the depth order
|
|
1530
|
+
* and the note can state it as one count.
|
|
1531
|
+
*/
|
|
1532
|
+
export function boundedDescendantIndex(
|
|
1533
|
+
fold: ActiveFold,
|
|
1534
|
+
state: ActiveContextState,
|
|
1535
|
+
budget: number,
|
|
1536
|
+
): { rows: Array<Record<string, unknown>>; omitted: number; seatedBytes: number } {
|
|
1537
|
+
const walked = descendantIndexRows(fold, state)
|
|
1538
|
+
.map((row, walk) => ({ row, walk }))
|
|
1539
|
+
.sort((left, right) =>
|
|
1540
|
+
Number(left.row.depth) - Number(right.row.depth) || left.walk - right.walk);
|
|
1541
|
+
const rows: Array<Record<string, unknown>> = [];
|
|
1542
|
+
// Counted as the array actually serializes, brackets and joining commas included, so
|
|
1543
|
+
// "seated bytes" is the bytes the result carries and not an under-read of them.
|
|
1544
|
+
let seatedBytes = 2;
|
|
1545
|
+
for (const { row } of walked) {
|
|
1546
|
+
const cost = bytes(stableStringify(row)) + (rows.length ? 1 : 0);
|
|
1547
|
+
if (seatedBytes + cost > budget) break;
|
|
1548
|
+
seatedBytes += cost;
|
|
1549
|
+
rows.push(row);
|
|
1550
|
+
}
|
|
1551
|
+
if (!rows.length) seatedBytes = 0;
|
|
1552
|
+
return { rows, omitted: walked.length - rows.length, seatedBytes };
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1391
1555
|
export function descendantIndexRows(
|
|
1392
1556
|
fold: ActiveFold,
|
|
1393
1557
|
state: ActiveContextState,
|
|
@@ -87,17 +87,11 @@ export type ContextEventKind =
|
|
|
87
87
|
| "context.pin"
|
|
88
88
|
| "context.capacity"
|
|
89
89
|
| "context.receipt"
|
|
90
|
-
| "context.lastcall"
|
|
91
|
-
| "context.response"
|
|
92
|
-
| "context.suggestion"
|
|
93
|
-
| "context.outcome"
|
|
94
|
-
| "context.notice"
|
|
95
90
|
| "context.frontier"
|
|
96
91
|
| "context.commit"
|
|
97
92
|
| "context.fold"
|
|
98
93
|
| "context.brief"
|
|
99
94
|
| "context.absorb"
|
|
100
|
-
| "context.split"
|
|
101
95
|
| "context.recovery"
|
|
102
96
|
| "context.suspend"
|
|
103
97
|
| "context.rollback"
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./canonical.ts";
|
|
16
16
|
import {
|
|
17
17
|
childFoldIds,
|
|
18
|
-
|
|
18
|
+
assignFoldParents,
|
|
19
19
|
flattenFoldRefs,
|
|
20
20
|
foldMap,
|
|
21
21
|
parseActiveContextState,
|
|
@@ -270,11 +270,42 @@ export function foldInterval(fold: ActiveFold, state: ActiveContextState, snapsh
|
|
|
270
270
|
return indices ? { start: indices[0], end: indices.at(-1)! } : null;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
export
|
|
274
|
-
|
|
273
|
+
export type OrderedRoot = { fold: ActiveFold; start: number; end: number };
|
|
274
|
+
|
|
275
|
+
// ROOT INTERVALS ARE DERIVED ONCE PER (STATE, SNAPSHOT) PAIR (2026-08-24). This derivation
|
|
276
|
+
// is O(roots x folds) on its own, because `foldInterval` calls `flattenFoldRefs`, which
|
|
277
|
+
// rebuilds `foldMap` for every root it walks. `partsForRange` opens with it, and
|
|
278
|
+
// `selectAutomaticChapter` calls `partsForRange` from a walk that is quadratic in units,
|
|
279
|
+
// so the cost was O(units^2 x roots x folds) for an answer that cannot change inside a
|
|
280
|
+
// pass. Replaying sealed sol-20260823-live rep 3 at 125 folds, ONE frontier walk made
|
|
281
|
+
// 12,333 calls and spent 22.5 seconds, and 12,328 of them returned null without reading a
|
|
282
|
+
// root, because the automatic chapter passes an EMPTY allowed-child set and any overlap
|
|
283
|
+
// refuses. At 140 folds the walk was 98.8 percent of the runtime's whole per-request cost;
|
|
284
|
+
// the per-turn gap between a response and the next projection ran 3s at zero folds to 164s
|
|
285
|
+
// past 150, and five per-turn call sites each pay a walk.
|
|
286
|
+
//
|
|
287
|
+
// The memo is gate 113's rule and gate 121's rule at a third address: kept against the
|
|
288
|
+
// state and snapshot OBJECTS rather than their contents, because both are REPLACED on
|
|
289
|
+
// every change rather than mutated, so a replaced one misses and derives again instead of
|
|
290
|
+
// serving something stale. It hands every caller the SAME array, which is safe only while
|
|
291
|
+
// no reading mutates it; gate 149 drives every call site against a frozen one so a reading
|
|
292
|
+
// that starts mutating throws rather than quietly poisoning the memo.
|
|
293
|
+
export const ORDERED_ROOTS = new WeakMap<ActiveContextState, WeakMap<ActiveContextSnapshot, OrderedRoot[]>>();
|
|
294
|
+
|
|
295
|
+
export function orderedRoots(state: ActiveContextState, snapshot: ActiveContextSnapshot): OrderedRoot[] {
|
|
296
|
+
let bySnapshot = ORDERED_ROOTS.get(state);
|
|
297
|
+
if (!bySnapshot) {
|
|
298
|
+
bySnapshot = new WeakMap<ActiveContextSnapshot, OrderedRoot[]>();
|
|
299
|
+
ORDERED_ROOTS.set(state, bySnapshot);
|
|
300
|
+
}
|
|
301
|
+
const memoized = bySnapshot.get(snapshot);
|
|
302
|
+
if (memoized) return memoized;
|
|
303
|
+
const roots = rootFolds(state).flatMap((fold) => {
|
|
275
304
|
const interval = foldInterval(fold, state, snapshot);
|
|
276
305
|
return interval ? [{ fold, ...interval }] : [];
|
|
277
306
|
}).sort((left, right) => left.start - right.start);
|
|
307
|
+
bySnapshot.set(snapshot, roots);
|
|
308
|
+
return roots;
|
|
278
309
|
}
|
|
279
310
|
|
|
280
311
|
export function visibleCollapsedRoots(state: ActiveContextState, snapshot: ActiveContextSnapshot) {
|
|
@@ -317,7 +348,9 @@ export function persistenceProjection(state: ActiveContextState, snapshot: Activ
|
|
|
317
348
|
if (projected.prepared && projected.prepared.sourceRefs.some((ref) => !mapped.has(objectRefKey(ref)))) {
|
|
318
349
|
delete projected.prepared;
|
|
319
350
|
}
|
|
320
|
-
|
|
351
|
+
// The parse on the next line validates this forest, so `assignFoldParents` is the whole
|
|
352
|
+
// job here; `deriveFoldParents` would walk it twice.
|
|
353
|
+
projected.folds = assignFoldParents(projected.folds);
|
|
321
354
|
return parseActiveContextState(projected, state.sessionId);
|
|
322
355
|
}
|
|
323
356
|
|