pi-fold 1.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -28
- package/extensions/active-context.ts +1213 -2756
- package/extensions/evidence.js +0 -10
- package/extensions/index.js +25 -32
- package/extensions/json.ts +3 -29
- package/extensions/lib/canonical.ts +0 -8
- package/extensions/lib/curation.ts +52 -313
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +57 -446
- package/extensions/lib/instrumentation.ts +13 -127
- package/extensions/lib/measurement.ts +6 -83
- package/extensions/lib/persistence.ts +272 -229
- package/extensions/lib/policy.ts +165 -612
- package/extensions/lib/rollback.ts +0 -53
- package/extensions/lib/scheduling.ts +352 -450
- package/extensions/lib/selection.ts +212 -390
- package/extensions/lib/tool-surface.ts +29 -46
- package/extensions/lib/transcript.ts +47 -166
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
- package/extensions/lib/surfacing.ts +0 -604
- package/extensions/summarizer.js +0 -291
|
@@ -29,40 +29,27 @@ export function buildActiveContextTool(input: {
|
|
|
29
29
|
handler: ToolHandler;
|
|
30
30
|
}) {
|
|
31
31
|
const peekGuidance = input.allowedActions.includes("peek")
|
|
32
|
-
? ` Peek is the point read: it returns one fold's index view -- every nested fold's brief in full, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back exactly, folds nested inside stay placeheld, and peeking a child id you see there returns that child's own bytes, so anything folded is one hop away at any depth. It returns at most ${input.defaultPeekBytes} bytes unless you widen it with bytes, and it
|
|
32
|
+
? ` Peek is the point read: it returns one fold's index view -- every nested fold's brief in full, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back exactly, folds nested inside stay placeheld, and peeking a child id you see there returns that child's own bytes, so anything folded is one hop away at any depth. It returns at most ${input.defaultPeekBytes} bytes unless you widen it with bytes, and it is EPHEMERAL by default: the bytes ride only until your next message, your reply then the surviving trace, so extract what your current task needs from them in that reply. Peeking again is lossless and costs one append, so a read you turn out to need twice is cheap; pass ephemeral false when you need the bytes standing across several turns. Expand is the durable in-place restoration.`
|
|
33
33
|
: "";
|
|
34
|
-
const correctionGuidance = input.allowedActions.includes("
|
|
35
|
-
? " Curation is correctable:
|
|
34
|
+
const correctionGuidance = input.allowedActions.includes("reboundary")
|
|
35
|
+
? " Curation is correctable: reboundary with ids re-cuts a span into exactly one fold, which merges adjacent folds when the span covers several and splits one when the span sits inside it. Reboundary with a single id returns that fold's span to raw. A brief is written when you mark the span; a standing fold's brief is not rewritten, because rewriting it rewrites the projection at that fold's placeholder and costs the whole prefix cache from there on."
|
|
36
36
|
: "";
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const pinGuidance = input.allowedActions.includes("protect")
|
|
40
|
-
? " Protect is the pin: protect with ids holds folds or entries against automatic folding, consolidation and refolding until unprotect releases them, so an expanded span you still need stays expanded. Protect reveals nothing on its own: a folded span stays folded, just held. Pinning moves no context bytes, and fold receipts report how much mass you have pinned."
|
|
37
|
+
const pinGuidance = input.allowedActions.includes("pin")
|
|
38
|
+
? " Pin with ids holds folds or entries against automatic folding, consolidation and refolding until unpin releases them, so an expanded span you still need stays expanded. Pin reveals nothing on its own: a folded span stays folded, just held. Pinning moves no context bytes, and fold receipts report how much mass you have pinned."
|
|
41
39
|
: "";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const epochGuidance = input.allowedActions.includes("unmark")
|
|
45
|
-
? " Fold scheduling is epoch mode: fold records pending marks and moves no context bytes, and the runtime applies every pending mark in one rewrite when the fold event fires. Mark SEVERAL spans in one call: marks carries several {ids, brief} pairs, and one call answers with your whole picture, every span now held plus the unmarked remainder and what share of the non-fresh window it is. Between fold events nothing else in your context changes, so that result is where the picture lives. Mark freely as you work; when to fold is the runtime's to decide, and unmark withdraws a mark you no longer want."
|
|
40
|
+
const epochGuidance = input.allowedActions.includes("brief")
|
|
41
|
+
? " THE RUNTIME CUTS THE FOLDS AND YOU ANNOTATE THEM. As raw material piles up it is cut into pending folds behind you, stalest first. A pending fold moves NO context bytes: you keep seeing the raw material exactly as it is, and the fold reaches your window only when a commit applies it. You are told when folds are cut and which ones carry no brief yet. Answer with brief, one id and one sentence: you know why the span mattered and what you will want back from it, and the deterministic brief that fills the gap reads the span alone and does not. Writing a brief on a pending fold is FREE, because the fold is not in your window yet; briefing a standing fold is refused, because its placeholder is in your window and rewriting it rewrites your context from that point on. reboundary re-cuts a span the runtime got wrong, unmark withdraws a cut you do not want taken, and when to commit stays the runtime's to decide."
|
|
46
42
|
: "";
|
|
47
|
-
// The recovery norm. Every placeholder already states the exact expand call, and a
|
|
48
|
-
// full run answered questions about folded material from memory anyway, inventing
|
|
49
|
-
// plausible values instead of reopening the fold. The syntax was discoverable; the
|
|
50
|
-
// norm was not, so the norm is stated where the syntax lives.
|
|
51
43
|
const recoveryGuidance = input.allowedActions.includes("expand")
|
|
52
44
|
? " A fold brief is an index entry, not the source: when a question or task depends on material that is folded, peek or expand that fold and answer from the exact bytes, never from memory of them. Recalling folded detail without reopening it is how specifics get misremembered."
|
|
53
45
|
: "";
|
|
54
|
-
// The standing facts about how this runtime behaves. They belong HERE, in the tool
|
|
55
|
-
// surface, because the tool surface sits in the stable prefix: it is cached from the
|
|
56
|
-
// first request and costs nothing per request thereafter, forever. The same sentences
|
|
57
|
-
// delivered as in-window carriers cost a cache break on the pass that renders them
|
|
58
|
-
// and every pass that displaces them. Anything always true is said once, here.
|
|
59
46
|
const standingGuidance = " Folding is automatic, lossless and recoverable: the runtime folds stale spans on its own when the window needs room, nothing is ever discarded, and every folded span keeps its exact source. You are never asked to make room and no fold is ever announced in advance. Work normally and let it happen.";
|
|
60
47
|
return {
|
|
61
48
|
name: input.name,
|
|
62
49
|
label: input.label,
|
|
63
50
|
description: input.fullSurface
|
|
64
|
-
? `Page, peek,
|
|
65
|
-
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance}
|
|
51
|
+
? `Page, peek, brief, expand, refold, pin, or re-cut exact Pi active-context evidence.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Mutations persist immediately and affect the next model call inside the same continuing turn; no turn boundary is required. Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`
|
|
52
|
+
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`,
|
|
66
53
|
parameters: {
|
|
67
54
|
type: "object",
|
|
68
55
|
additionalProperties: false,
|
|
@@ -71,35 +58,24 @@ export function buildActiveContextTool(input: {
|
|
|
71
58
|
action: { type: "string", enum: input.allowedActions },
|
|
72
59
|
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
73
60
|
id: { type: "string", minLength: 1 },
|
|
74
|
-
marks: {
|
|
75
|
-
type: "array",
|
|
76
|
-
minItems: 1,
|
|
77
|
-
maxItems: 64,
|
|
78
|
-
description: "Fold only: several spans with their own briefs in one call, which is the shape to prefer. An invalid or overlapping span is corrected to the nearest valid edge and the correction is reported back.",
|
|
79
|
-
items: {
|
|
80
|
-
type: "object",
|
|
81
|
-
additionalProperties: false,
|
|
82
|
-
required: ["ids"],
|
|
83
|
-
properties: {
|
|
84
|
-
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
85
|
-
brief: {
|
|
86
|
-
type: "string",
|
|
87
|
-
minLength: 1,
|
|
88
|
-
maxLength: input.maxBriefChars,
|
|
89
|
-
description: "Write one for every span you mark. You know why the span mattered and " +
|
|
90
|
-
"what you will want back from it; a generator reading the span alone does not. " +
|
|
91
|
-
"Leaving it out hands the brief to the summarizer rather than skipping it.",
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
61
|
brief: {
|
|
97
62
|
type: "string",
|
|
98
63
|
minLength: 1,
|
|
99
64
|
maxLength: input.maxBriefChars,
|
|
100
|
-
description: "
|
|
65
|
+
description: "Brief only: what this pending fold covers and what you will want " +
|
|
66
|
+
`back from it, at most ${input.maxBriefChars} characters. Free to write, ` +
|
|
67
|
+
"because the fold it names is not in your window yet.",
|
|
101
68
|
},
|
|
102
69
|
offset: { type: "integer", minimum: 0 },
|
|
70
|
+
ephemeral: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Peek only. Default TRUE: the returned bytes ride your context " +
|
|
73
|
+
"exactly until your next message, then their place holds a one-line " +
|
|
74
|
+
"placeholder and your reply is the surviving trace, so extract what you need " +
|
|
75
|
+
"into that reply. Peeking again is lossless. Pass false to keep the result " +
|
|
76
|
+
"standing like any other tool result, which is what you want when the bytes " +
|
|
77
|
+
"have to survive several turns of work.",
|
|
78
|
+
},
|
|
103
79
|
bytes: {
|
|
104
80
|
type: "integer",
|
|
105
81
|
minimum: input.minPeekSliceBytes,
|
|
@@ -116,8 +92,10 @@ export function buildActiveContextTool(input: {
|
|
|
116
92
|
export function buildActiveContextCommands(input: {
|
|
117
93
|
statusName: string;
|
|
118
94
|
foldName: string;
|
|
95
|
+
editorName: string;
|
|
119
96
|
statusHandler: CommandHandler;
|
|
120
97
|
foldHandler: CommandHandler;
|
|
98
|
+
editorHandler: CommandHandler;
|
|
121
99
|
}) {
|
|
122
100
|
return [
|
|
123
101
|
{
|
|
@@ -127,9 +105,14 @@ export function buildActiveContextCommands(input: {
|
|
|
127
105
|
},
|
|
128
106
|
{
|
|
129
107
|
name: input.foldName,
|
|
130
|
-
description: "
|
|
108
|
+
description: "Commit every staged fold in one epoch; with explicit ids, folds exactly that span now; " +
|
|
131
109
|
"works without a main-model request",
|
|
132
110
|
handler: input.foldHandler,
|
|
133
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: input.editorName,
|
|
114
|
+
description: "Open the context window editor: occupancy, fold roots and briefs, staged marks, pins",
|
|
115
|
+
handler: input.editorHandler,
|
|
116
|
+
},
|
|
134
117
|
];
|
|
135
118
|
}
|
|
@@ -3,12 +3,10 @@ import {
|
|
|
3
3
|
evidenceRef,
|
|
4
4
|
evidenceValue,
|
|
5
5
|
isPlainRecord,
|
|
6
|
-
objectRefKey,
|
|
7
6
|
sha256Text,
|
|
8
7
|
stableStringify,
|
|
9
8
|
} from "../json.ts";
|
|
10
9
|
import {
|
|
11
|
-
bytes,
|
|
12
10
|
clone,
|
|
13
11
|
messageRole,
|
|
14
12
|
ownValue,
|
|
@@ -24,7 +22,6 @@ import {
|
|
|
24
22
|
DEFAULT_THRESHOLDS,
|
|
25
23
|
PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
26
24
|
servingBudgetTokens,
|
|
27
|
-
zoneBytes,
|
|
28
25
|
} from "./policy.ts";
|
|
29
26
|
import type {
|
|
30
27
|
ActiveContextSnapshot,
|
|
@@ -55,12 +52,6 @@ export function completeTurns(messages: unknown[]): CompleteTurn[] {
|
|
|
55
52
|
return turns;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
/**
|
|
59
|
-
* Native compaction may resume the assistant after a summary without replaying
|
|
60
|
-
* the compacted user message. Once a later user message exists, that closed
|
|
61
|
-
* assistant/tool prefix is stale evidence, not part of the fresh user tail.
|
|
62
|
-
* Keep the native summary itself raw and require a terminal assistant boundary.
|
|
63
|
-
*/
|
|
64
55
|
export function leadingCompactionContinuation(messages: unknown[]): CompleteTurn | null {
|
|
65
56
|
if (messageRole(messages[0]) !== "compactionSummary") return null;
|
|
66
57
|
let firstUser = -1;
|
|
@@ -74,24 +65,17 @@ export function leadingCompactionContinuation(messages: unknown[]): CompleteTurn
|
|
|
74
65
|
return end > 1 && terminalAssistant(messages[end - 1]) ? { start: 1, end } : null;
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
/**
|
|
78
|
-
* The exact argument surface each read-only active-context action may carry. A context
|
|
79
|
-
* call outside this surface is not classified read-only, so its batch never folds
|
|
80
|
-
* unmarked.
|
|
81
|
-
*/
|
|
82
68
|
export const READ_ONLY_CONTEXT_ACTION_ARGUMENTS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
83
69
|
status: Object.freeze(["action", "detail", "offset", "limit"]),
|
|
84
|
-
|
|
70
|
+
// offset and bytes are the tool surface's own narrowing parameters: a paged or
|
|
71
|
+
// narrowed peek is as byte-inert as a bare one, and excluding them made the
|
|
72
|
+
// read built to SAVE window mass linger as raw mass past the reclaimer.
|
|
73
|
+
// ephemeral joins them for the same reason: it changes when the RESULT leaves
|
|
74
|
+
// the projection, never what the call moves, and a call the reclaimer cannot
|
|
75
|
+
// claim would linger as exactly the mass the parameter exists to shed.
|
|
76
|
+
peek: Object.freeze(["action", "id", "offset", "bytes", "ephemeral"]),
|
|
85
77
|
});
|
|
86
78
|
|
|
87
|
-
/**
|
|
88
|
-
* May the ladder fold this completed call's batch without an agent mark?
|
|
89
|
-
*
|
|
90
|
-
* Yes for every tool, unless the deployment blacklisted it. The one carve-out is the
|
|
91
|
-
* active-context tool itself: a mutating context call folding its own batch would be a
|
|
92
|
-
* rewrite caused by a tool call, so only its read-only actions inside the exact argument
|
|
93
|
-
* surface above are foldable.
|
|
94
|
-
*/
|
|
95
79
|
export function isAutoFoldableToolCall(
|
|
96
80
|
name: string,
|
|
97
81
|
args?: unknown,
|
|
@@ -132,7 +116,6 @@ export interface ScannedToolBatches extends ValidatedToolBatch {
|
|
|
132
116
|
unsafeIndices: Set<number>;
|
|
133
117
|
}
|
|
134
118
|
|
|
135
|
-
/** Validate each same-assistant tool batch inside one complete turn; results never cross a batch. */
|
|
136
119
|
export function scanTurnToolBatches(
|
|
137
120
|
messages: unknown[],
|
|
138
121
|
turn: CompleteTurn,
|
|
@@ -220,7 +203,6 @@ export function scanTurnToolBatches(
|
|
|
220
203
|
return { calls, unsafeIndices };
|
|
221
204
|
}
|
|
222
205
|
|
|
223
|
-
/** Validate a complete turn as wholly auto-foldable. Granular callers may still use its safe batches. */
|
|
224
206
|
export function validateTurnToolBatch(
|
|
225
207
|
messages: unknown[],
|
|
226
208
|
turn: CompleteTurn,
|
|
@@ -240,12 +222,6 @@ export interface ChapterUnit {
|
|
|
240
222
|
turnStart: number;
|
|
241
223
|
}
|
|
242
224
|
|
|
243
|
-
/**
|
|
244
|
-
* Structural chapter segments are deliberately independent of terminal turns.
|
|
245
|
-
* A long-running user turn is one segment even while the coordinator continues
|
|
246
|
-
* issuing tools; a later user message closes the preceding segment without
|
|
247
|
-
* making that boundary either necessary or sufficient for folding.
|
|
248
|
-
*/
|
|
249
225
|
export function chapterSegments(messages: unknown[]): CompleteTurn[] {
|
|
250
226
|
const starts: number[] = [];
|
|
251
227
|
for (let index = 0; index < messages.length; index += 1) {
|
|
@@ -266,12 +242,6 @@ export function chapterSegments(messages: unknown[]): CompleteTurn[] {
|
|
|
266
242
|
return segments;
|
|
267
243
|
}
|
|
268
244
|
|
|
269
|
-
/**
|
|
270
|
-
* Build indivisible structurally closed units. Chapters may summarize completed
|
|
271
|
-
* mutating and failed work, including an old prefix of the currently running
|
|
272
|
-
* turn. Every tool-use assistant must own one complete, unique result batch;
|
|
273
|
-
* any pending, unmatched, or unknown shape protects its entire segment.
|
|
274
|
-
*/
|
|
275
245
|
export function structurallyClosedChapterUnits(messages: unknown[], segment: CompleteTurn): ChapterUnit[] {
|
|
276
246
|
const units: ChapterUnit[] = [];
|
|
277
247
|
const ownedCalls = new Set<string>();
|
|
@@ -344,9 +314,6 @@ export function unsafeChapterIndices(messages: unknown[]): Set<number> {
|
|
|
344
314
|
}
|
|
345
315
|
const unsafe = new Set<number>();
|
|
346
316
|
for (let index = 0; index < messages.length; index += 1) if (!eligible.has(index)) unsafe.add(index);
|
|
347
|
-
// The initiating request of the still-running segment remains raw even after
|
|
348
|
-
// its older completed work has moved beyond the byte tail. It carries the
|
|
349
|
-
// live marathon objective; subsequent structurally closed batches may fold.
|
|
350
317
|
const current = segments.length ? segments[segments.length - 1] : undefined;
|
|
351
318
|
if (current?.end === messages.length && !terminalAssistant(messages[current.end - 1]) &&
|
|
352
319
|
messageRole(messages[current.start]) === "user") unsafe.add(current.start);
|
|
@@ -354,17 +321,13 @@ export function unsafeChapterIndices(messages: unknown[]): Set<number> {
|
|
|
354
321
|
}
|
|
355
322
|
|
|
356
323
|
/**
|
|
357
|
-
* The
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* turn until the suffix carries its bytes. The old hybrid rule -- three complete turns
|
|
363
|
-
* OR 24,000 bytes, capped at a quarter of a small window -- is gone: it was three
|
|
364
|
-
* numbers in two units answering one question, and at small windows the turn leg and
|
|
365
|
-
* the byte leg disagreed about which was the protection.
|
|
324
|
+
* The first index of an unfinished user turn, or the end of the transcript when the last
|
|
325
|
+
* turn completed. Folding into an unfinished turn leaves a user message whose tool calls are
|
|
326
|
+
* unanswered, which is malformed for every provider, so this is a validity boundary rather
|
|
327
|
+
* than a curation preference. The fresh-tail byte walk that used to sit here is deleted:
|
|
328
|
+
* nothing decided on it, and stale-first ordering already leaves recent material last.
|
|
366
329
|
*/
|
|
367
|
-
export function freshBoundary(messages: unknown[], turns: CompleteTurn[]
|
|
330
|
+
export function freshBoundary(messages: unknown[], turns: CompleteTurn[]): number {
|
|
368
331
|
if (!messages.length) return 0;
|
|
369
332
|
let unfinishedUser = messages.length;
|
|
370
333
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
@@ -376,57 +339,43 @@ export function freshBoundary(messages: unknown[], turns: CompleteTurn[], freshB
|
|
|
376
339
|
unfinishedUser = complete ? messages.length : index;
|
|
377
340
|
break;
|
|
378
341
|
}
|
|
379
|
-
|
|
380
|
-
let boundary = Math.min(messages.length, unfinishedUser);
|
|
381
|
-
while (boundary > 0) {
|
|
382
|
-
const suffix: unknown[] = [];
|
|
383
|
-
for (let index = boundary; index < messages.length; index += 1) suffix.push(messages[index]);
|
|
384
|
-
if (bytes(suffix) >= freshBytes) break;
|
|
385
|
-
let previous: CompleteTurn | null = null;
|
|
386
|
-
for (let turnIndex = 0; turnIndex < turns.length; turnIndex += 1) {
|
|
387
|
-
if (turns[turnIndex].start < boundary) previous = turns[turnIndex];
|
|
388
|
-
}
|
|
389
|
-
if (!previous) { boundary = leading?.end ?? 0; break; }
|
|
390
|
-
boundary = previous.start;
|
|
391
|
-
}
|
|
392
|
-
return boundary;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export function toolFreshIndices(
|
|
396
|
-
messages: unknown[],
|
|
397
|
-
_turns: CompleteTurn[],
|
|
398
|
-
freshBytes: number,
|
|
399
|
-
): Set<number> {
|
|
400
|
-
const protectedIndices = new Set<number>();
|
|
401
|
-
let tailBytes = 0;
|
|
402
|
-
let boundary = messages.length;
|
|
403
|
-
while (boundary > 0 && tailBytes < freshBytes) {
|
|
404
|
-
boundary -= 1;
|
|
405
|
-
tailBytes += bytes(messages[boundary]);
|
|
406
|
-
}
|
|
407
|
-
for (let index = boundary; index < messages.length; index += 1) protectedIndices.add(index);
|
|
408
|
-
return protectedIndices;
|
|
342
|
+
return Math.min(messages.length, unfinishedUser);
|
|
409
343
|
}
|
|
410
344
|
|
|
411
345
|
type EntryEvidence = { message: unknown; ref: BranchObject["ref"] };
|
|
412
346
|
|
|
413
347
|
/**
|
|
414
|
-
*
|
|
348
|
+
* ONE CANONICALIZATION PER MESSAGE OBJECT.
|
|
415
349
|
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
350
|
+
* Mapping the branch canonicalizes and hashes EVERY event message, then canonicalizes a
|
|
351
|
+
* branch candidate again on each digest match. That is roughly twice the session's bytes
|
|
352
|
+
* of stringify-and-sha per derivation, and a derivation runs many times per request: on
|
|
353
|
+
* sol-20260813-paired rep 2 a single one cost 18.5 seconds against a 16.7 MB session,
|
|
354
|
+
* where rep 1 cost 3.5 against 7.1 MB. The work was never the mapping, it was rebuilding
|
|
355
|
+
* the same strings for messages that had not changed since the last pass.
|
|
421
356
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* `branchIndex` is deliberately NOT cached. It is a position in the branch being mapped
|
|
428
|
-
* now, and a different leaf yields a different path across the same entries.
|
|
357
|
+
* This is gate 121's rule at the layer above it, and it carries gate 121's tradeoff
|
|
358
|
+
* unchanged and on purpose: the memo is keyed on the message OBJECT, so a replaced object
|
|
359
|
+
* misses and recomputes rather than serving stale, and a message MUTATED IN PLACE keeps
|
|
360
|
+
* its first canonical form. The session is append-only, so nothing mutates a message that
|
|
361
|
+
* has already been mapped; the cost is stated rather than guarded against.
|
|
429
362
|
*/
|
|
363
|
+
const MESSAGE_WIRE = new WeakMap<object, { wire: string; digest: string }>();
|
|
364
|
+
|
|
365
|
+
function messageWire(message: unknown): { wire: string; digest: string } | null {
|
|
366
|
+
const key = message && typeof message === "object" ? message as object : null;
|
|
367
|
+
if (key) {
|
|
368
|
+
const cached = MESSAGE_WIRE.get(key);
|
|
369
|
+
if (cached) return cached;
|
|
370
|
+
}
|
|
371
|
+
let wire: string;
|
|
372
|
+
try { wire = stableStringify(evidenceValue(message)); }
|
|
373
|
+
catch { return null; }
|
|
374
|
+
const canonical = { wire, digest: sha256Text(wire) };
|
|
375
|
+
if (key) MESSAGE_WIRE.set(key, canonical);
|
|
376
|
+
return canonical;
|
|
377
|
+
}
|
|
378
|
+
|
|
430
379
|
const ENTRY_EVIDENCE = new WeakMap<object, {
|
|
431
380
|
sessionId: string;
|
|
432
381
|
projectEntry: (entry: Record<string, unknown>) => unknown[];
|
|
@@ -451,7 +400,6 @@ function entryEvidence(
|
|
|
451
400
|
try {
|
|
452
401
|
items.push({ message, ref: evidenceRef(sessionId, entryId, message) });
|
|
453
402
|
} catch {
|
|
454
|
-
// Unknown persisted shapes stay unavailable rather than weakening exact refs.
|
|
455
403
|
}
|
|
456
404
|
}
|
|
457
405
|
}
|
|
@@ -471,7 +419,6 @@ export function mapActiveContext(input: {
|
|
|
471
419
|
blacklistAutoFoldTools?: ReadonlySet<string>;
|
|
472
420
|
readOnlyContextActions?: ReadonlySet<string>;
|
|
473
421
|
contextWindow?: number;
|
|
474
|
-
/** True when `contextWindow` is a declared serving budget rather than a raw window. */
|
|
475
422
|
netBudget?: boolean;
|
|
476
423
|
thresholds?: ActiveContextThresholds;
|
|
477
424
|
}): ActiveContextSnapshot {
|
|
@@ -491,19 +438,16 @@ export function mapActiveContext(input: {
|
|
|
491
438
|
let cursor = 0;
|
|
492
439
|
for (let index = 0; index < input.eventMessages.length; index += 1) {
|
|
493
440
|
const message = input.eventMessages[index];
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
try {
|
|
497
|
-
wire = stableStringify(evidenceValue(message));
|
|
498
|
-
digest = sha256Text(wire);
|
|
499
|
-
} catch {
|
|
441
|
+
const canonical = messageWire(message);
|
|
442
|
+
if (!canonical) {
|
|
500
443
|
mapped.push({ index, message, ref: null });
|
|
501
444
|
continue;
|
|
502
445
|
}
|
|
446
|
+
const { wire, digest } = canonical;
|
|
503
447
|
let match = -1;
|
|
504
448
|
for (let branchIndex = cursor; branchIndex < branchObjects.length; branchIndex += 1) {
|
|
505
449
|
const candidate = branchObjects[branchIndex];
|
|
506
|
-
if (candidate.ref.sha256 === digest &&
|
|
450
|
+
if (candidate.ref.sha256 === digest && messageWire(candidate.message)?.wire === wire) {
|
|
507
451
|
match = branchIndex;
|
|
508
452
|
break;
|
|
509
453
|
}
|
|
@@ -519,49 +463,17 @@ export function mapActiveContext(input: {
|
|
|
519
463
|
Number.isFinite(input.contextWindow) && input.contextWindow > 0
|
|
520
464
|
? input.contextWindow
|
|
521
465
|
: null;
|
|
522
|
-
// One denominator: both zone widths are shares of the serving budget the declared
|
|
523
|
-
// window yields, so a smaller window narrows the tails in proportion instead of
|
|
524
|
-
// meeting a fixed byte floor that only a second constant could rescue.
|
|
525
|
-
// A deployment that declared `providerInputBudget` passes its window in ALREADY NET of
|
|
526
|
-
// the reservation it holds back, so the zone widths divide by it as given. Only the
|
|
527
|
-
// descriptor fallback estimates a reservation out of the raw window.
|
|
528
466
|
const thresholds = input.thresholds ?? DEFAULT_THRESHOLDS;
|
|
529
467
|
const budgetTokens = input.netBudget === true
|
|
530
468
|
? (reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW)
|
|
531
469
|
: servingBudgetTokens(reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW);
|
|
532
|
-
const freshBytes = zoneBytes(thresholds.freshTail, budgetTokens);
|
|
533
470
|
const turns = completeTurns(input.eventMessages);
|
|
534
|
-
const boundary = freshBoundary(input.eventMessages, turns
|
|
535
|
-
// THERE IS NO POSITIONAL REACH HERE ANY MORE.
|
|
536
|
-
//
|
|
537
|
-
// This function used to compute a second boundary, a byte prefix of the window called
|
|
538
|
-
// the stale zone, and the selectors would take nothing above it. Two reps died of it.
|
|
539
|
-
// Rep 2 clamped it to the last CLOSED turn, so a marathon session that never closed
|
|
540
|
-
// one had a zero-width zone: the last call announced 19 unmarked spans worth 274,173
|
|
541
|
-
// tokens while the selector could propose nothing, no commit ever fired, and the
|
|
542
|
-
// window was rejected twice at 375,830 estimated tokens. Rep 3 unclamped it and walked
|
|
543
|
-
// raw bytes, so a folded prefix was charged for evidence it had already replaced: 17
|
|
544
|
-
// folded results cost 1,002,801 raw bytes of a 1,072,081-byte allowance, the reach
|
|
545
|
-
// froze at index 36 of 133, and the run died against the fence at 0.972 occupancy with
|
|
546
|
-
// 25 takeable batches sitting above the line.
|
|
547
|
-
//
|
|
548
|
-
// Shane's ruling (2026-08-10): stale is a CLASS, not a position. A span is
|
|
549
|
-
// automatically foldable because of what it IS -- a completed tool batch the deployment
|
|
550
|
-
// did not blacklist, or older material a chapter or consolidation can compose -- and
|
|
551
|
-
// never because of where it sits. What this function still produces is the protection
|
|
552
|
-
// that is genuinely positional: the fresh byte tail, set-shaped, in `protectedIndices`
|
|
553
|
-
// and `toolProtectedIndices`. The rest of the law is membership plus the agent's pins
|
|
554
|
-
// plus the guard-with-waiver adjudicated once at commit.
|
|
471
|
+
const boundary = freshBoundary(input.eventMessages, turns);
|
|
555
472
|
const protectedIndices = unsafeChapterIndices(input.eventMessages);
|
|
556
|
-
const toolProtectedIndices = toolFreshIndices(input.eventMessages, turns, freshBytes);
|
|
557
|
-
// Chapter protection is set-shaped: preserve the newest complete turns and
|
|
558
|
-
// raw byte tail, while allowing an older closed prefix of the current turn.
|
|
559
|
-
for (const index of toolProtectedIndices) protectedIndices.add(index);
|
|
560
473
|
for (let mappedIndex = 0; mappedIndex < mapped.length; mappedIndex += 1) {
|
|
561
474
|
const item = mapped[mappedIndex];
|
|
562
475
|
if (item.ref) continue;
|
|
563
476
|
protectedIndices.add(item.index);
|
|
564
|
-
toolProtectedIndices.add(item.index);
|
|
565
477
|
}
|
|
566
478
|
const snapshot: ActiveContextSnapshot = {
|
|
567
479
|
sessionId: input.sessionId,
|
|
@@ -573,7 +485,6 @@ export function mapActiveContext(input: {
|
|
|
573
485
|
thresholds,
|
|
574
486
|
budgetTokens,
|
|
575
487
|
protectedIndices,
|
|
576
|
-
toolProtectedIndices,
|
|
577
488
|
policy,
|
|
578
489
|
toolName: input.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
579
490
|
brandNoun: input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
@@ -585,33 +496,3 @@ export function mapActiveContext(input: {
|
|
|
585
496
|
};
|
|
586
497
|
return snapshot;
|
|
587
498
|
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* The evidence boundary of the CURRENT turn: the position of the last terminal
|
|
591
|
-
* assistant message. Everything after it was gathered by the excursion still in
|
|
592
|
-
* progress, which is exactly the evidence a commit must not fold.
|
|
593
|
-
*
|
|
594
|
-
* Measured 2026-08-06 (rep 8): nineteen folds landed between the last read result and
|
|
595
|
-
* the agent's next reply, rewriting the projection from 938k to 487k chars, so the
|
|
596
|
-
* agent answered from a window where its own just-gathered evidence had become
|
|
597
|
-
* placeholders. Freshness in bytes did not catch it; the boundary is a TURN.
|
|
598
|
-
*/
|
|
599
|
-
export function currentTurnBoundary(snapshot: Pick<ActiveContextSnapshot, "messages">): number {
|
|
600
|
-
let boundary = -1;
|
|
601
|
-
for (let index = 0; index < snapshot.messages.length; index += 1) {
|
|
602
|
-
if (terminalAssistant(snapshot.messages[index])) boundary = index;
|
|
603
|
-
}
|
|
604
|
-
return boundary;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/** Every tool-result evidence key produced since that boundary. */
|
|
608
|
-
export function currentTurnRefKeys(snapshot: ActiveContextSnapshot): Set<string> {
|
|
609
|
-
const boundary = currentTurnBoundary(snapshot);
|
|
610
|
-
const keys = new Set<string>();
|
|
611
|
-
for (const item of snapshot.mapped) {
|
|
612
|
-
if (item.index <= boundary || !item.ref) continue;
|
|
613
|
-
if (messageRole(item.message) !== "toolResult") continue;
|
|
614
|
-
keys.add(objectRefKey(item.ref));
|
|
615
|
-
}
|
|
616
|
-
return keys;
|
|
617
|
-
}
|