pi-fold 1.0.2 → 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 +29 -40
- package/extensions/active-context.ts +2374 -600
- 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 +414 -106
- package/extensions/lib/selection.ts +528 -144
- package/extensions/lib/surfacing.ts +479 -224
- 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
|
@@ -29,16 +29,28 @@ 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
|
|
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 stays in the window like any other tool result. Expand is the durable in-place restoration.`
|
|
33
33
|
: "";
|
|
34
34
|
const correctionGuidance = input.allowedActions.includes("rebrief")
|
|
35
35
|
? " Curation is correctable: rebrief replaces a fold's brief, and 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."
|
|
36
36
|
: "";
|
|
37
|
+
// The pin. Protection predates this sentence; the sentence exists because a verb
|
|
38
|
+
// nobody can discover is a verb nobody uses.
|
|
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."
|
|
41
|
+
: "";
|
|
37
42
|
// Marking is yours, folding is the runtime's. The epoch guidance says how marks
|
|
38
43
|
// behave and stops there: there is no agent-callable commit verb to describe.
|
|
39
44
|
const epochGuidance = input.allowedActions.includes("unmark")
|
|
40
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."
|
|
41
46
|
: "";
|
|
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
|
+
const recoveryGuidance = input.allowedActions.includes("expand")
|
|
52
|
+
? " 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
|
+
: "";
|
|
42
54
|
// The standing facts about how this runtime behaves. They belong HERE, in the tool
|
|
43
55
|
// surface, because the tool surface sits in the stable prefix: it is cached from the
|
|
44
56
|
// first request and costs nothing per request thereafter, forever. The same sentences
|
|
@@ -49,8 +61,8 @@ export function buildActiveContextTool(input: {
|
|
|
49
61
|
name: input.name,
|
|
50
62
|
label: input.label,
|
|
51
63
|
description: input.fullSurface
|
|
52
|
-
? `Page, peek, fold, expand, refold, protect, rebrief, or re-cut exact Pi active-context evidence.${peekGuidance}${correctionGuidance}${epochGuidance}${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 1200-character maximum.`
|
|
53
|
-
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${epochGuidance}${standingGuidance} Call fold only by copying the exact eligibleChapter.action returned by status; if status has no eligibleChapter, continue the task without folding. Supplied fold briefs have a hard 1200-character maximum.`,
|
|
64
|
+
? `Page, peek, fold, expand, refold, protect, rebrief, 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 1200-character maximum.`
|
|
65
|
+
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Call fold only by copying the exact eligibleChapter.action returned by status; if status has no eligibleChapter, continue the task without folding. Supplied fold briefs have a hard 1200-character maximum.`,
|
|
54
66
|
parameters: {
|
|
55
67
|
type: "object",
|
|
56
68
|
additionalProperties: false,
|
|
@@ -70,7 +82,14 @@ export function buildActiveContextTool(input: {
|
|
|
70
82
|
required: ["ids"],
|
|
71
83
|
properties: {
|
|
72
84
|
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
73
|
-
brief: {
|
|
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
|
+
},
|
|
74
93
|
},
|
|
75
94
|
},
|
|
76
95
|
},
|
|
@@ -108,7 +127,8 @@ export function buildActiveContextCommands(input: {
|
|
|
108
127
|
},
|
|
109
128
|
{
|
|
110
129
|
name: input.foldName,
|
|
111
|
-
description: "Losslessly fold a stale context span
|
|
130
|
+
description: "Losslessly fold a stale context span, or \"commit\" to apply every eligible mark now; " +
|
|
131
|
+
"works without a main-model request",
|
|
112
132
|
handler: input.foldHandler,
|
|
113
133
|
},
|
|
114
134
|
];
|
|
@@ -16,16 +16,19 @@ import {
|
|
|
16
16
|
} from "./canonical.ts";
|
|
17
17
|
import {
|
|
18
18
|
ACTIVE_CONTEXT_POLICY,
|
|
19
|
-
|
|
19
|
+
AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
20
20
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
21
21
|
DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
|
|
22
22
|
DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
23
23
|
DEFAULT_CONTEXT_WINDOW,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
DEFAULT_THRESHOLDS,
|
|
25
|
+
PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
26
|
+
servingBudgetTokens,
|
|
27
|
+
zoneBytes,
|
|
26
28
|
} from "./policy.ts";
|
|
27
29
|
import type {
|
|
28
30
|
ActiveContextSnapshot,
|
|
31
|
+
ActiveContextThresholds,
|
|
29
32
|
BranchObject,
|
|
30
33
|
CompleteTurn,
|
|
31
34
|
MappedMessage,
|
|
@@ -72,23 +75,33 @@ export function leadingCompactionContinuation(messages: unknown[]): CompleteTurn
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/**
|
|
75
|
-
* The exact argument surface each read-only active-context action may carry. A
|
|
76
|
-
* outside this surface is not classified read-only, so its batch never folds
|
|
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.
|
|
77
81
|
*/
|
|
78
82
|
export const READ_ONLY_CONTEXT_ACTION_ARGUMENTS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
79
83
|
status: Object.freeze(["action", "detail", "offset", "limit"]),
|
|
80
84
|
peek: Object.freeze(["action", "id"]),
|
|
81
85
|
});
|
|
82
86
|
|
|
83
|
-
|
|
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
|
+
export function isAutoFoldableToolCall(
|
|
84
96
|
name: string,
|
|
85
97
|
args?: unknown,
|
|
86
98
|
toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
87
|
-
|
|
88
|
-
readOnlyContextActions: ReadonlySet<string> =
|
|
99
|
+
blacklistAutoFoldTools: ReadonlySet<string> = AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
100
|
+
readOnlyContextActions: ReadonlySet<string> = PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
89
101
|
): boolean {
|
|
90
|
-
if (
|
|
91
|
-
if (name !== toolName
|
|
102
|
+
if (blacklistAutoFoldTools.has(name)) return false;
|
|
103
|
+
if (name !== toolName) return true;
|
|
104
|
+
if (!isPlainRecord(args)) return false;
|
|
92
105
|
const action = ownValue(args, "action");
|
|
93
106
|
if (typeof action !== "string" || !readOnlyContextActions.has(action)) return false;
|
|
94
107
|
const permitted = ownValue(READ_ONLY_CONTEXT_ACTION_ARGUMENTS, action);
|
|
@@ -125,8 +138,8 @@ export function scanTurnToolBatches(
|
|
|
125
138
|
turn: CompleteTurn,
|
|
126
139
|
allowIncomplete = false,
|
|
127
140
|
toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
128
|
-
|
|
129
|
-
readOnlyContextActions: ReadonlySet<string> =
|
|
141
|
+
blacklistAutoFoldTools: ReadonlySet<string> = AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
142
|
+
readOnlyContextActions: ReadonlySet<string> = PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
130
143
|
): ScannedToolBatches | null {
|
|
131
144
|
if (!Number.isSafeInteger(turn.start) || !Number.isSafeInteger(turn.end) ||
|
|
132
145
|
turn.start < 0 || turn.end > messages.length || turn.start >= turn.end) return null;
|
|
@@ -163,8 +176,8 @@ export function scanTurnToolBatches(
|
|
|
163
176
|
const id = ownValue(part, "id");
|
|
164
177
|
const name = ownValue(part, "name");
|
|
165
178
|
if (typeof id !== "string" || !id || typeof name !== "string" || !name ||
|
|
166
|
-
calls.has(id) || !
|
|
167
|
-
name, ownValue(part, "arguments"), toolName,
|
|
179
|
+
calls.has(id) || !isAutoFoldableToolCall(
|
|
180
|
+
name, ownValue(part, "arguments"), toolName, blacklistAutoFoldTools, readOnlyContextActions,
|
|
168
181
|
)) safe = false;
|
|
169
182
|
else calls.set(id, { id, name, assistantIndex: index });
|
|
170
183
|
}
|
|
@@ -207,16 +220,16 @@ export function scanTurnToolBatches(
|
|
|
207
220
|
return { calls, unsafeIndices };
|
|
208
221
|
}
|
|
209
222
|
|
|
210
|
-
/** Validate a complete turn as wholly
|
|
223
|
+
/** Validate a complete turn as wholly auto-foldable. Granular callers may still use its safe batches. */
|
|
211
224
|
export function validateTurnToolBatch(
|
|
212
225
|
messages: unknown[],
|
|
213
226
|
turn: CompleteTurn,
|
|
214
227
|
toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
215
|
-
|
|
216
|
-
readOnlyContextActions: ReadonlySet<string> =
|
|
228
|
+
blacklistAutoFoldTools: ReadonlySet<string> = AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
229
|
+
readOnlyContextActions: ReadonlySet<string> = PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
217
230
|
): ValidatedToolBatch | null {
|
|
218
231
|
const scanned = scanTurnToolBatches(
|
|
219
|
-
messages, turn, false, toolName,
|
|
232
|
+
messages, turn, false, toolName, blacklistAutoFoldTools, readOnlyContextActions,
|
|
220
233
|
);
|
|
221
234
|
return scanned && scanned.unsafeIndices.size === 0 ? { calls: scanned.calls } : null;
|
|
222
235
|
}
|
|
@@ -340,7 +353,18 @@ export function unsafeChapterIndices(messages: unknown[]): Set<number> {
|
|
|
340
353
|
return unsafe;
|
|
341
354
|
}
|
|
342
355
|
|
|
343
|
-
|
|
356
|
+
/**
|
|
357
|
+
* The newest end of the window, where nothing folds.
|
|
358
|
+
*
|
|
359
|
+
* Two things decide it, and only two. The STRUCTURAL invariant comes first: an
|
|
360
|
+
* unfinished turn is never foldable at any share, because the excursion in progress is
|
|
361
|
+
* still using the evidence it gathered. Then the freshTail share, walked back turn by
|
|
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.
|
|
366
|
+
*/
|
|
367
|
+
export function freshBoundary(messages: unknown[], turns: CompleteTurn[], freshBytes: number): number {
|
|
344
368
|
if (!messages.length) return 0;
|
|
345
369
|
let unfinishedUser = messages.length;
|
|
346
370
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
@@ -353,19 +377,16 @@ export function freshBoundary(messages: unknown[], turns: CompleteTurn[], policy
|
|
|
353
377
|
break;
|
|
354
378
|
}
|
|
355
379
|
const leading = leadingCompactionContinuation(messages);
|
|
356
|
-
let boundary =
|
|
357
|
-
? turns[turns.length - policy.freshTurns].start
|
|
358
|
-
: leading?.end ?? 0;
|
|
359
|
-
boundary = Math.min(boundary, unfinishedUser);
|
|
380
|
+
let boundary = Math.min(messages.length, unfinishedUser);
|
|
360
381
|
while (boundary > 0) {
|
|
361
382
|
const suffix: unknown[] = [];
|
|
362
383
|
for (let index = boundary; index < messages.length; index += 1) suffix.push(messages[index]);
|
|
363
|
-
if (bytes(suffix) >=
|
|
384
|
+
if (bytes(suffix) >= freshBytes) break;
|
|
364
385
|
let previous: CompleteTurn | null = null;
|
|
365
386
|
for (let turnIndex = 0; turnIndex < turns.length; turnIndex += 1) {
|
|
366
387
|
if (turns[turnIndex].start < boundary) previous = turns[turnIndex];
|
|
367
388
|
}
|
|
368
|
-
if (!previous) { boundary = 0; break; }
|
|
389
|
+
if (!previous) { boundary = leading?.end ?? 0; break; }
|
|
369
390
|
boundary = previous.start;
|
|
370
391
|
}
|
|
371
392
|
return boundary;
|
|
@@ -373,18 +394,13 @@ export function freshBoundary(messages: unknown[], turns: CompleteTurn[], policy
|
|
|
373
394
|
|
|
374
395
|
export function toolFreshIndices(
|
|
375
396
|
messages: unknown[],
|
|
376
|
-
|
|
377
|
-
|
|
397
|
+
_turns: CompleteTurn[],
|
|
398
|
+
freshBytes: number,
|
|
378
399
|
): Set<number> {
|
|
379
400
|
const protectedIndices = new Set<number>();
|
|
380
|
-
const firstFreshTurn = Math.max(0, turns.length - policy.freshTurns);
|
|
381
|
-
for (let turnIndex = firstFreshTurn; turnIndex < turns.length; turnIndex += 1) {
|
|
382
|
-
const turn = turns[turnIndex];
|
|
383
|
-
for (let index = turn.start; index < turn.end; index += 1) protectedIndices.add(index);
|
|
384
|
-
}
|
|
385
401
|
let tailBytes = 0;
|
|
386
402
|
let boundary = messages.length;
|
|
387
|
-
while (boundary > 0 && tailBytes <
|
|
403
|
+
while (boundary > 0 && tailBytes < freshBytes) {
|
|
388
404
|
boundary -= 1;
|
|
389
405
|
tailBytes += bytes(messages[boundary]);
|
|
390
406
|
}
|
|
@@ -392,6 +408,57 @@ export function toolFreshIndices(
|
|
|
392
408
|
return protectedIndices;
|
|
393
409
|
}
|
|
394
410
|
|
|
411
|
+
type EntryEvidence = { message: unknown; ref: BranchObject["ref"] };
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Evidence for ONE session entry, derived once and kept.
|
|
415
|
+
*
|
|
416
|
+
* A session is append-only, and `buildContextEntries` hands back the session manager's own
|
|
417
|
+
* stored entry objects rather than copies, so an entry mapped once cannot change shape
|
|
418
|
+
* underneath us. Keying on the entry OBJECT rather than its id means a replaced entry
|
|
419
|
+
* misses and is recomputed instead of served stale: there is no invalidation rule here to
|
|
420
|
+
* get wrong, and a branch switch is simply a different path over the same stable objects.
|
|
421
|
+
*
|
|
422
|
+
* What this removes is not loop overhead. It is a `stableStringify` and a `sha256` per
|
|
423
|
+
* message over the WHOLE history on every call, while the window that history projects
|
|
424
|
+
* into stays the same size. That is superlinear in total session bytes, and it spent 135
|
|
425
|
+
* of the 258 minutes of the 2026-08-11 rep, more than the run spent waiting on the model.
|
|
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.
|
|
429
|
+
*/
|
|
430
|
+
const ENTRY_EVIDENCE = new WeakMap<object, {
|
|
431
|
+
sessionId: string;
|
|
432
|
+
projectEntry: (entry: Record<string, unknown>) => unknown[];
|
|
433
|
+
items: EntryEvidence[];
|
|
434
|
+
}>();
|
|
435
|
+
|
|
436
|
+
function entryEvidence(
|
|
437
|
+
entry: Record<string, unknown>,
|
|
438
|
+
entryId: string,
|
|
439
|
+
sessionId: string,
|
|
440
|
+
projectEntry: (entry: Record<string, unknown>) => unknown[],
|
|
441
|
+
): EntryEvidence[] {
|
|
442
|
+
const cached = ENTRY_EVIDENCE.get(entry);
|
|
443
|
+
if (cached && cached.sessionId === sessionId && cached.projectEntry === projectEntry) {
|
|
444
|
+
return cached.items;
|
|
445
|
+
}
|
|
446
|
+
const items: EntryEvidence[] = [];
|
|
447
|
+
const projected = denseOwnArrayValues(projectEntry(entry));
|
|
448
|
+
if (projected) {
|
|
449
|
+
for (let messageIndex = 0; messageIndex < projected.length; messageIndex += 1) {
|
|
450
|
+
const message = projected[messageIndex];
|
|
451
|
+
try {
|
|
452
|
+
items.push({ message, ref: evidenceRef(sessionId, entryId, message) });
|
|
453
|
+
} catch {
|
|
454
|
+
// Unknown persisted shapes stay unavailable rather than weakening exact refs.
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
ENTRY_EVIDENCE.set(entry, { sessionId, projectEntry, items });
|
|
459
|
+
return items;
|
|
460
|
+
}
|
|
461
|
+
|
|
395
462
|
export function mapActiveContext(input: {
|
|
396
463
|
sessionId: string;
|
|
397
464
|
eventMessages: unknown[];
|
|
@@ -401,9 +468,12 @@ export function mapActiveContext(input: {
|
|
|
401
468
|
toolName?: string;
|
|
402
469
|
brandNoun?: string;
|
|
403
470
|
entryTypePrefix?: string;
|
|
404
|
-
|
|
471
|
+
blacklistAutoFoldTools?: ReadonlySet<string>;
|
|
405
472
|
readOnlyContextActions?: ReadonlySet<string>;
|
|
406
473
|
contextWindow?: number;
|
|
474
|
+
/** True when `contextWindow` is a declared serving budget rather than a raw window. */
|
|
475
|
+
netBudget?: boolean;
|
|
476
|
+
thresholds?: ActiveContextThresholds;
|
|
407
477
|
}): ActiveContextSnapshot {
|
|
408
478
|
const policy = Object.freeze({ ...ACTIVE_CONTEXT_POLICY, ...(input.policy ?? {}) }) as typeof ACTIVE_CONTEXT_POLICY;
|
|
409
479
|
const projectEntry = input.projectEntry ?? sessionEntryMessages;
|
|
@@ -412,19 +482,8 @@ export function mapActiveContext(input: {
|
|
|
412
482
|
const entry = input.contextEntries[entryIndex];
|
|
413
483
|
const entryId = typeof entry?.id === "string" ? entry.id : "";
|
|
414
484
|
if (!entryId) continue;
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
for (let messageIndex = 0; messageIndex < projected.length; messageIndex += 1) {
|
|
418
|
-
const message = projected[messageIndex];
|
|
419
|
-
try {
|
|
420
|
-
branchObjects.push({
|
|
421
|
-
branchIndex: branchObjects.length,
|
|
422
|
-
message,
|
|
423
|
-
ref: evidenceRef(input.sessionId, entryId, message),
|
|
424
|
-
});
|
|
425
|
-
} catch {
|
|
426
|
-
// Unknown persisted shapes stay unavailable rather than weakening exact refs.
|
|
427
|
-
}
|
|
485
|
+
for (const item of entryEvidence(entry, entryId, input.sessionId, projectEntry)) {
|
|
486
|
+
branchObjects.push({ branchIndex: branchObjects.length, message: item.message, ref: item.ref });
|
|
428
487
|
}
|
|
429
488
|
}
|
|
430
489
|
|
|
@@ -460,19 +519,41 @@ export function mapActiveContext(input: {
|
|
|
460
519
|
Number.isFinite(input.contextWindow) && input.contextWindow > 0
|
|
461
520
|
? input.contextWindow
|
|
462
521
|
: null;
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
const thresholds = input.thresholds ?? DEFAULT_THRESHOLDS;
|
|
529
|
+
const budgetTokens = input.netBudget === true
|
|
530
|
+
? (reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW)
|
|
531
|
+
: servingBudgetTokens(reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW);
|
|
532
|
+
const freshBytes = zoneBytes(thresholds.freshTail, budgetTokens);
|
|
472
533
|
const turns = completeTurns(input.eventMessages);
|
|
473
|
-
const boundary = freshBoundary(input.eventMessages, turns,
|
|
534
|
+
const boundary = freshBoundary(input.eventMessages, turns, freshBytes);
|
|
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.
|
|
474
555
|
const protectedIndices = unsafeChapterIndices(input.eventMessages);
|
|
475
|
-
const toolProtectedIndices = toolFreshIndices(input.eventMessages, turns,
|
|
556
|
+
const toolProtectedIndices = toolFreshIndices(input.eventMessages, turns, freshBytes);
|
|
476
557
|
// Chapter protection is set-shaped: preserve the newest complete turns and
|
|
477
558
|
// raw byte tail, while allowing an older closed prefix of the current turn.
|
|
478
559
|
for (const index of toolProtectedIndices) protectedIndices.add(index);
|
|
@@ -482,24 +563,27 @@ export function mapActiveContext(input: {
|
|
|
482
563
|
protectedIndices.add(item.index);
|
|
483
564
|
toolProtectedIndices.add(item.index);
|
|
484
565
|
}
|
|
485
|
-
|
|
566
|
+
const snapshot: ActiveContextSnapshot = {
|
|
486
567
|
sessionId: input.sessionId,
|
|
487
568
|
messages: clone(input.eventMessages),
|
|
488
569
|
mapped,
|
|
489
570
|
branchObjects,
|
|
490
571
|
completeTurns: turns,
|
|
491
572
|
freshBoundary: boundary,
|
|
573
|
+
thresholds,
|
|
574
|
+
budgetTokens,
|
|
492
575
|
protectedIndices,
|
|
493
576
|
toolProtectedIndices,
|
|
494
|
-
policy
|
|
577
|
+
policy,
|
|
495
578
|
toolName: input.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
496
579
|
brandNoun: input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
497
580
|
entryTypePrefix: input.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
|
|
498
|
-
|
|
499
|
-
readOnlyContextActions: input.readOnlyContextActions ??
|
|
581
|
+
blacklistAutoFoldTools: input.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
582
|
+
readOnlyContextActions: input.readOnlyContextActions ?? PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
500
583
|
contextWindow: reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
501
584
|
windowSource: reportedContextWindow === null ? "fallback" : "reported",
|
|
502
585
|
};
|
|
586
|
+
return snapshot;
|
|
503
587
|
}
|
|
504
588
|
|
|
505
589
|
/**
|