pi-fold 2.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 +79 -191
- package/extensions/active-context.ts +1332 -526
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +17 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +94 -61
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +207 -59
- package/extensions/lib/instrumentation.ts +10 -9
- package/extensions/lib/measurement.ts +43 -16
- package/extensions/lib/persistence.ts +273 -46
- package/extensions/lib/policy.ts +251 -95
- package/extensions/lib/scheduling.ts +446 -126
- package/extensions/lib/selection.ts +117 -116
- package/extensions/lib/tool-surface.ts +60 -33
- package/extensions/lib/transcript.ts +82 -74
- package/extensions/settings.ts +456 -0
- package/package.json +4 -1
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
stableStringify,
|
|
7
7
|
} from "./json.ts";
|
|
8
8
|
import {
|
|
9
|
+
contentText,
|
|
9
10
|
bytes,
|
|
10
11
|
clone,
|
|
11
12
|
emptyActiveContextState,
|
|
12
13
|
ownValue,
|
|
14
|
+
serializedPrefixHolds,
|
|
13
15
|
sessionEntryMessages,
|
|
14
16
|
uniqueMessageDigestAnchor,
|
|
15
17
|
} from "./lib/canonical.ts";
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
descendantIds,
|
|
23
25
|
encodedFoldSource,
|
|
24
26
|
foldCandidatesDetail,
|
|
27
|
+
foldIndexRow,
|
|
25
28
|
foldTreeDetail,
|
|
26
29
|
peekFoldSource,
|
|
27
30
|
prepareFold,
|
|
@@ -29,7 +32,6 @@ import {
|
|
|
29
32
|
projectionSlateCandidates,
|
|
30
33
|
protectEvidence,
|
|
31
34
|
requireActiveFold,
|
|
32
|
-
selectAutomaticChapter,
|
|
33
35
|
selectAutomaticRung,
|
|
34
36
|
setFoldProjectionState,
|
|
35
37
|
withExpandLease,
|
|
@@ -56,7 +58,6 @@ import {
|
|
|
56
58
|
providerTokens,
|
|
57
59
|
refsProtected,
|
|
58
60
|
stringIds,
|
|
59
|
-
toolRefsProtected,
|
|
60
61
|
toolPayload,
|
|
61
62
|
} from "./lib/measurement.ts";
|
|
62
63
|
import type {
|
|
@@ -70,8 +71,8 @@ import {
|
|
|
70
71
|
clearPrepared,
|
|
71
72
|
deriveFoldParents,
|
|
72
73
|
flattenFoldRefs,
|
|
73
|
-
foldBrief,
|
|
74
74
|
foldIdFor,
|
|
75
|
+
foldRecordRef,
|
|
75
76
|
makeFoldRecordEntry,
|
|
76
77
|
makeStateCheckpoint,
|
|
77
78
|
makeStateDelta,
|
|
@@ -90,6 +91,7 @@ import {
|
|
|
90
91
|
activeContextSource,
|
|
91
92
|
ACTIVE_CONTEXT_POLICY,
|
|
92
93
|
ACTIVE_CONTEXT_TOOL_ACTIONS,
|
|
94
|
+
UNBRIEFED_FOLDS_BEFORE_NOTICE,
|
|
93
95
|
contextBrand,
|
|
94
96
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
95
97
|
DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES,
|
|
@@ -99,39 +101,46 @@ import {
|
|
|
99
101
|
COMMIT_RECLAIM_FLOOR_SHARE,
|
|
100
102
|
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
101
103
|
DEFAULT_CONTEXT_WINDOW,
|
|
102
|
-
resolveGuidance,
|
|
103
104
|
assertThresholdsServable,
|
|
104
105
|
resolveThresholds,
|
|
105
106
|
servingBudgetTokens,
|
|
106
107
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
107
108
|
entryTypeNamespace,
|
|
108
109
|
MAX_FOLD_SPAN_CHARS,
|
|
110
|
+
DEFAULT_TOOL_FOLD_THRESHOLD,
|
|
109
111
|
MAX_PINNED_SHARE,
|
|
110
|
-
MAX_WEDGE_ABSORB_TOKENS,
|
|
111
112
|
PEEK_DEFAULT_MAX_BYTES,
|
|
113
|
+
peekIsEphemeral,
|
|
112
114
|
PEEK_MIN_SLICE_BYTES,
|
|
113
115
|
PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
116
|
+
RETIRED_TOOL_ACTIONS,
|
|
114
117
|
AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
115
118
|
USER_RESCUE_MAX_SOURCE_CHARS,
|
|
119
|
+
MEMORY_BODY_CHARS,
|
|
120
|
+
MEMORY_KEY_CHARS,
|
|
121
|
+
MEMORY_KEYS_MAX,
|
|
122
|
+
WORKING_MEMORY_TOOL_ACTIONS,
|
|
116
123
|
} from "./lib/policy.ts";
|
|
117
124
|
import type {
|
|
118
|
-
ActiveContextGuidance,
|
|
119
125
|
ActiveContextSnapshot,
|
|
120
126
|
ActiveContextState,
|
|
121
127
|
ActiveContextThresholds,
|
|
122
128
|
ActiveContextToolAction,
|
|
129
|
+
BriefProvenance,
|
|
123
130
|
FoldCandidate,
|
|
124
131
|
FoldKind,
|
|
125
132
|
FoldRecordEntry,
|
|
126
133
|
PreparedFold,
|
|
134
|
+
WorkingMemoryEntry,
|
|
127
135
|
} from "./lib/policy.ts";
|
|
128
136
|
import {
|
|
129
137
|
absorbWedgeMarks,
|
|
138
|
+
markSpanRefs,
|
|
130
139
|
} from "./lib/scheduling.ts";
|
|
131
140
|
import {
|
|
132
141
|
contextReceipt,
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
foldNoticeText,
|
|
143
|
+
memoryTocText,
|
|
135
144
|
receiptBlockText,
|
|
136
145
|
withReceipt,
|
|
137
146
|
} from "./lib/curation.ts";
|
|
@@ -140,35 +149,35 @@ import type {
|
|
|
140
149
|
} from "./lib/curation.ts";
|
|
141
150
|
import {
|
|
142
151
|
addPendingMark,
|
|
143
|
-
|
|
152
|
+
commitCoverage,
|
|
153
|
+
windowClaims,
|
|
144
154
|
commitPendingMarks,
|
|
155
|
+
toolClipAdditions,
|
|
145
156
|
consolidationMarks,
|
|
146
157
|
ephemeralPeekMarks,
|
|
158
|
+
epochCommitDue,
|
|
147
159
|
estimatedTokens,
|
|
148
160
|
foldMarkFor,
|
|
149
|
-
ladderSelectionMark,
|
|
150
161
|
ladderBrief,
|
|
151
162
|
markAccounting,
|
|
152
|
-
|
|
163
|
+
markRewriteTokens,
|
|
153
164
|
markFreedBytes,
|
|
154
165
|
unmarkedRemainder,
|
|
155
|
-
|
|
156
|
-
markedFoldIds,
|
|
166
|
+
markClaimingRef,
|
|
157
167
|
markOrdinal,
|
|
158
168
|
pendingMarks,
|
|
159
|
-
|
|
169
|
+
refoldMarks,
|
|
160
170
|
schedulingStatus,
|
|
161
171
|
topUpMarks,
|
|
172
|
+
frontierMarks,
|
|
162
173
|
withPendingMarks,
|
|
163
174
|
} from "./lib/scheduling.ts";
|
|
164
175
|
import {
|
|
165
|
-
candidateSpanChars,
|
|
166
176
|
manualFoldCandidate,
|
|
167
177
|
peekedSourceFoldIds,
|
|
168
|
-
selectAutomaticToolBatch,
|
|
169
178
|
snapFoldCandidate,
|
|
170
179
|
snapToFoldBoundaries,
|
|
171
|
-
|
|
180
|
+
spanBytes,
|
|
172
181
|
} from "./lib/selection.ts";
|
|
173
182
|
import type { SpanCorrection } from "./lib/selection.ts";
|
|
174
183
|
import {
|
|
@@ -187,8 +196,8 @@ import type {
|
|
|
187
196
|
ProjectionChange,
|
|
188
197
|
} from "./lib/instrumentation.ts";
|
|
189
198
|
import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
|
|
199
|
+
import { buildFoldEditorData, FoldEditorView } from "./lib/editor-ui.ts";
|
|
190
200
|
import {
|
|
191
|
-
currentTurnRefKeys,
|
|
192
201
|
mapActiveContext,
|
|
193
202
|
} from "./lib/transcript.ts";
|
|
194
203
|
import {
|
|
@@ -214,38 +223,6 @@ export * from "./lib/scheduling.ts";
|
|
|
214
223
|
export * from "./lib/selection.ts";
|
|
215
224
|
export * from "./lib/transcript.ts";
|
|
216
225
|
|
|
217
|
-
export interface BatchedMarkRequest {
|
|
218
|
-
ids: string[];
|
|
219
|
-
brief?: string;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function batchedMarkRequests(params: Record<string, unknown>): BatchedMarkRequest[] {
|
|
223
|
-
const batched = params.marks;
|
|
224
|
-
if (batched === undefined) {
|
|
225
|
-
return [{
|
|
226
|
-
ids: stringIds(params.ids),
|
|
227
|
-
...(typeof params.brief === "string" && params.brief.trim() ? { brief: params.brief } : {}),
|
|
228
|
-
}];
|
|
229
|
-
}
|
|
230
|
-
const values = denseOwnArrayValues(batched);
|
|
231
|
-
if (!values || !values.length || values.length > 64) {
|
|
232
|
-
throw new Error("marks must be a dense array of 1-64 {ids, brief} objects");
|
|
233
|
-
}
|
|
234
|
-
return values.map((value, index) => {
|
|
235
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
236
|
-
throw new Error(`marks[${index}] must be an object with ids and an optional brief`);
|
|
237
|
-
}
|
|
238
|
-
const brief = ownValue(value, "brief");
|
|
239
|
-
if (brief !== undefined && (typeof brief !== "string" || !brief.trim())) {
|
|
240
|
-
throw new Error(`marks[${index}].brief must be a nonempty string`);
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
ids: stringIds(ownValue(value, "ids")),
|
|
244
|
-
...(typeof brief === "string" && brief.trim() ? { brief } : {}),
|
|
245
|
-
};
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
226
|
export function registerActiveContext(pi: any, options: {
|
|
250
227
|
toolName?: string;
|
|
251
228
|
toolLabel?: string;
|
|
@@ -255,7 +232,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
255
232
|
blacklistAutoFoldTools?: ReadonlySet<string>;
|
|
256
233
|
providerInputBudget?: number;
|
|
257
234
|
thresholds?: ActiveContextThresholds;
|
|
258
|
-
|
|
235
|
+
/** THE TOOL-CALL DIET's share point (2026-08-24; default 0.50 since 2026-08-28, Shane,
|
|
236
|
+
* on the sol-20260826-full2 verdict): tool results in the oldest toolFoldThreshold
|
|
237
|
+
* share of the projected window are clipped IN VIEW at each commit, identified head
|
|
238
|
+
* kept, full bytes peek-recoverable behind the entry id the marker names. A share in
|
|
239
|
+
* [0, 1), where 0 turns the diet off; 0.50 is what every fold repetition from 3 onward
|
|
240
|
+
* ran, so the default is the value that was measured rather than the absence of one. */
|
|
241
|
+
toolFoldThreshold?: number;
|
|
242
|
+
/** THE INVITATION SWITCH (public since 2026-08-27; DEFAULT FALSE since 2026-08-28,
|
|
243
|
+
* Shane, on the sol-20260826-full2 verdict). false is the deterministic shape the
|
|
244
|
+
* campaign measured and the one it recommends: no carrier ever invites a brief and
|
|
245
|
+
* every fold goes out with the runtime's own words, which drew 14, 14, 9 and 8 correct
|
|
246
|
+
* against the invited condition's 3. true restores the standing invitation for the
|
|
247
|
+
* agent to improve a brief. Either way the agent verbs stay on the tool, so an agent
|
|
248
|
+
* that finds a fold can still annotate what it reads. */
|
|
249
|
+
postFoldNotice?: boolean;
|
|
250
|
+
/** THE WORKING MEMORY (Shane, 2026-08-26): a session-scoped ordered dictionary the
|
|
251
|
+
* agent maintains beside the fold index. `remember` writes or removes an entry,
|
|
252
|
+
* `recall` reads bodies on demand, and the projection carries one table of contents
|
|
253
|
+
* that refreshes at each commit. Off when absent: the digest channel earned its
|
|
254
|
+
* place in a measured gap (running tallies lost 0/6 against native's 6/6 while the
|
|
255
|
+
* index won everything else), and it must stay separable from the index it is not. */
|
|
256
|
+
workingMemory?: boolean;
|
|
259
257
|
}): {
|
|
260
258
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
261
259
|
} {
|
|
@@ -264,6 +262,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
264
262
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
265
263
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
266
264
|
const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
|
|
265
|
+
if (options.postFoldNotice !== undefined && typeof options.postFoldNotice !== "boolean") {
|
|
266
|
+
throw new Error("postFoldNotice must be a boolean: true keeps the post-fold invitation " +
|
|
267
|
+
"notice, false ships every fold with the runtime's deterministic brief and no " +
|
|
268
|
+
"carrier inviting the agent to improve it");
|
|
269
|
+
}
|
|
270
|
+
const postFoldNotice = options.postFoldNotice ?? false;
|
|
271
|
+
if (options.workingMemory !== undefined && typeof options.workingMemory !== "boolean") {
|
|
272
|
+
throw new Error("workingMemory must be a boolean: it enables the session-scoped " +
|
|
273
|
+
"working-memory dictionary and its two actions, remember and recall");
|
|
274
|
+
}
|
|
275
|
+
const workingMemory = options.workingMemory === true;
|
|
276
|
+
const toolFoldThreshold = options.toolFoldThreshold ?? DEFAULT_TOOL_FOLD_THRESHOLD;
|
|
277
|
+
if (typeof toolFoldThreshold !== "number" || !Number.isFinite(toolFoldThreshold) ||
|
|
278
|
+
toolFoldThreshold < 0 || toolFoldThreshold >= 1) {
|
|
279
|
+
throw new Error("toolFoldThreshold must be a share in [0, 1): it names the oldest " +
|
|
280
|
+
"fraction of the projected window whose tool results are clipped in view at each " +
|
|
281
|
+
"commit, and 0 turns the diet off");
|
|
282
|
+
}
|
|
267
283
|
for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
|
|
268
284
|
if (Object.hasOwn(options, removed)) {
|
|
269
285
|
throw new Error(`${removed} is no longer an option: epoch scheduling, peek foldability ` +
|
|
@@ -288,6 +304,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
288
304
|
"tools it meant to permit");
|
|
289
305
|
}
|
|
290
306
|
}
|
|
307
|
+
if (Object.hasOwn(options, "guidance")) {
|
|
308
|
+
throw new Error("guidance is no longer an option: it switched the copy that taught the " +
|
|
309
|
+
"agent to CHOOSE SPANS, and the agent does not choose spans. The runtime cuts at the " +
|
|
310
|
+
"frontier and the agent annotates what it cut, so the guidance it switched has been " +
|
|
311
|
+
"replaced wholesale by the post-fold notice, which is not optional because a fold the " +
|
|
312
|
+
"agent is never told about is one it can never brief");
|
|
313
|
+
}
|
|
291
314
|
if (Object.hasOwn(options, "commandPrefix")) {
|
|
292
315
|
throw new Error("commandPrefix is no longer an option: give whole names with commandNames");
|
|
293
316
|
}
|
|
@@ -317,20 +340,22 @@ export function registerActiveContext(pi: any, options: {
|
|
|
317
340
|
}
|
|
318
341
|
const thresholds = resolveThresholds(options.thresholds);
|
|
319
342
|
assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
320
|
-
const guidance = resolveGuidance(options.guidance);
|
|
321
343
|
const stateEntryType = `${entryTypePrefix}-state`;
|
|
322
344
|
const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
|
|
323
345
|
const milestoneProjectionType = `${entryTypePrefix}-milestone`;
|
|
324
346
|
const advisoryProjectionType = `${entryTypePrefix}-advisory`;
|
|
347
|
+
const foldNoticeProjectionType = `${entryTypePrefix}-fold-notice`;
|
|
348
|
+
const memoryTocProjectionType = `${entryTypePrefix}-memory-toc`;
|
|
325
349
|
const receiptProjectionType = `${entryTypePrefix}-receipts`;
|
|
326
|
-
const riderProjectionType = `${entryTypePrefix}-rider`;
|
|
327
350
|
const curationProjectionType = `${entryTypePrefix}-curation`;
|
|
328
351
|
const entryNamespace = entryTypeNamespace(entryTypePrefix);
|
|
329
352
|
const providerMeasurementEntryType = `${entryNamespace}-provider-context-measurement`;
|
|
330
353
|
const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
|
|
331
354
|
const contextEventEntryType = `${entryNamespace}-context-event`;
|
|
332
355
|
const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
|
|
333
|
-
const allowedToolActions: readonly ActiveContextToolAction[] =
|
|
356
|
+
const allowedToolActions: readonly ActiveContextToolAction[] = workingMemory
|
|
357
|
+
? [...ACTIVE_CONTEXT_TOOL_ACTIONS, ...WORKING_MEMORY_TOOL_ACTIONS]
|
|
358
|
+
: ACTIVE_CONTEXT_TOOL_ACTIONS;
|
|
334
359
|
const allowedToolActionSet = new Set<string>(allowedToolActions);
|
|
335
360
|
|
|
336
361
|
type AutomaticFailureState = {
|
|
@@ -353,6 +378,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
353
378
|
};
|
|
354
379
|
|
|
355
380
|
const ladder = {
|
|
381
|
+
/** The provider measurement the last band-top commit ran against; one commit per one. */
|
|
382
|
+
bandTopMeasurement: null as unknown,
|
|
383
|
+
/** The snapshot-and-state key whose frontier scan came back empty; see advanceFoldFrontier. */
|
|
384
|
+
frontierEmptyKey: null as string | null,
|
|
356
385
|
pendingManual: false,
|
|
357
386
|
preparing: null as { id: string; controller: AbortController; promise: Promise<void> } | null,
|
|
358
387
|
lastPreparationError: null as string | null,
|
|
@@ -401,7 +430,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
401
430
|
previousText: null as string | null,
|
|
402
431
|
lastChange: "append" as ProjectionChange,
|
|
403
432
|
lastPreservedShare: null as number | null,
|
|
404
|
-
sinceHandoff: [] as Array<{
|
|
433
|
+
sinceHandoff: [] as Array<{
|
|
434
|
+
seq: number;
|
|
435
|
+
kind: string;
|
|
436
|
+
action?: string;
|
|
437
|
+
ok?: boolean;
|
|
438
|
+
}>,
|
|
405
439
|
mutationsSinceHandoff: 0,
|
|
406
440
|
requests: 0,
|
|
407
441
|
lastMutationRequest: 0,
|
|
@@ -462,6 +496,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
462
496
|
hardFenceReleasedProjectionKeys: new Set<string>(),
|
|
463
497
|
};
|
|
464
498
|
|
|
499
|
+
// EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
|
|
500
|
+
// visibility. In-memory ON PURPOSE: a restart forgets the registry and the
|
|
501
|
+
// result stays durable, which is the safe default, and the durable entry
|
|
502
|
+
// always keeps the exact bytes, so folding, peek and rollback read the
|
|
503
|
+
// source unchanged whatever the projection shows.
|
|
504
|
+
const ephemeralPeeks = new Set<string>();
|
|
505
|
+
|
|
465
506
|
const nativeCompaction = {
|
|
466
507
|
lastThresholdDecision: null as Record<string, unknown> | null,
|
|
467
508
|
pendingNativeReceipt: null as NativeCompactionCompletionReceipt | null,
|
|
@@ -540,7 +581,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
540
581
|
revision: persistence.state?.revision ?? 0,
|
|
541
582
|
at: Date.now(),
|
|
542
583
|
}, payload);
|
|
543
|
-
instrumentation.sinceHandoff.push({
|
|
584
|
+
instrumentation.sinceHandoff.push({
|
|
585
|
+
seq: record.seq,
|
|
586
|
+
kind,
|
|
587
|
+
...(typeof record.action === "string" ? { action: record.action } : {}),
|
|
588
|
+
...(typeof record.ok === "boolean" ? { ok: record.ok } : {}),
|
|
589
|
+
});
|
|
544
590
|
const operation = curation.instrumentationQueue.then(async () => {
|
|
545
591
|
try { await pi.appendEntry(contextEventEntryType, record); }
|
|
546
592
|
catch { }
|
|
@@ -550,8 +596,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
550
596
|
};
|
|
551
597
|
|
|
552
598
|
const PREFIX_MUTATING_KINDS: ReadonlySet<string> = new Set([
|
|
553
|
-
"context.commit", "context.fold", "context.absorb", "context.
|
|
599
|
+
"context.commit", "context.fold", "context.absorb", "context.recovery",
|
|
554
600
|
]);
|
|
601
|
+
const CACHE_ACTION_REQUEST_CLASS: Readonly<Record<string, string>> = Object.freeze({
|
|
602
|
+
peek: "after-peek",
|
|
603
|
+
expand: "after-expand",
|
|
604
|
+
refold: "after-refold",
|
|
605
|
+
});
|
|
555
606
|
|
|
556
607
|
const deliverReceipt = (receipt: ContextReceipt): void => {
|
|
557
608
|
curation.receipts = withReceipt(curation.receipts, receipt);
|
|
@@ -561,8 +612,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
561
612
|
folds_committed: receipt.foldsCommitted,
|
|
562
613
|
folds_created: receipt.foldsCreated,
|
|
563
614
|
freed_tokens: receipt.freedTokens,
|
|
564
|
-
split_folds: receipt.splitFolds,
|
|
565
|
-
split_from_chars: receipt.splitFromChars,
|
|
566
615
|
absorbed_wedges: receipt.absorbedWedges,
|
|
567
616
|
recovered: receipt.recovered,
|
|
568
617
|
protected_bytes: receipt.protectedBytes,
|
|
@@ -868,15 +917,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
868
917
|
if (next.folds.length > MAX_ACTIVE_FOLD_RECORDS) {
|
|
869
918
|
throw new Error("Active-context fold closure exceeds persistence limit");
|
|
870
919
|
}
|
|
920
|
+
// AN ALREADY-DURABLE FOLD NEEDS A DIGEST, NOT A RECORD (2026-08-28). This built the
|
|
921
|
+
// whole entry for every fold on every persist just to compare one hash and continue:
|
|
922
|
+
// `makeFoldRecordEntry` canonicalizes, hashes, then `parseFoldRecordEntry` re-validates,
|
|
923
|
+
// clones the fold again, recomputes `foldIdFor`, hashes a second time and stringifies
|
|
924
|
+
// for the wire cap. Replaying a real sealed session, 98.2 percent of 4,947 calls hit
|
|
925
|
+
// the `continue`, and 126 of 135 persists appended nothing while rebuilding all of it.
|
|
926
|
+
// `foldRecordRef` is the same digest by construction (`makeFoldRecordEntry` stores
|
|
927
|
+
// `canonicalFoldRecord`, and `stateFromFoldRefs` already matches records to refs on
|
|
928
|
+
// this equality at persistence.ts:809); gate 152 pins it. Equal digests mean
|
|
929
|
+
// byte-identical canonical bytes to a record that already passed
|
|
930
|
+
// `parseFoldRecordEntry`, so skipping the rebuild cannot reach a different verdict.
|
|
931
|
+
// Measured 85.0ms to 19.6ms at 400 folds.
|
|
871
932
|
for (const fold of next.folds) {
|
|
872
|
-
const
|
|
873
|
-
const existing = persistence.persistedFoldRecords.get(
|
|
933
|
+
const ref = foldRecordRef(fold);
|
|
934
|
+
const existing = persistence.persistedFoldRecords.get(ref.id);
|
|
874
935
|
if (existing) {
|
|
875
|
-
if (existing.recordSha256 !==
|
|
876
|
-
throw new Error(`Conflicting durable active-context fold ${
|
|
936
|
+
if (existing.recordSha256 !== ref.sha256) {
|
|
937
|
+
throw new Error(`Conflicting durable active-context fold ${ref.id}`);
|
|
877
938
|
}
|
|
878
939
|
continue;
|
|
879
940
|
}
|
|
941
|
+
const record = makeFoldRecordEntry(fold, sessionId);
|
|
880
942
|
await pi.appendEntry(foldRecordEntryType, record);
|
|
881
943
|
persistence.persistedFoldRecords.set(record.foldId, record);
|
|
882
944
|
if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
|
|
@@ -885,13 +947,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
885
947
|
}
|
|
886
948
|
}
|
|
887
949
|
const wire = persistence.persistedWireVersion === 2 ? makeStateDelta(persistence.persisted, next) : makeStateCheckpoint(next);
|
|
888
|
-
|
|
950
|
+
// THE WIRE CARRIES BOTH DIGESTS IT JUST DERIVED (2026-08-28). `makeStateDelta` writes
|
|
951
|
+
// `baseStateSha256` from `persistence.persisted` and `stateSha256` from `next`, and
|
|
952
|
+
// nothing mutates either state between that call and here, so re-hashing two whole
|
|
953
|
+
// states was pure repetition at about 7ms per durable write. The drift check keeps its
|
|
954
|
+
// teeth: `baseStateSha256` is still derived from `persistence.persisted`, so a
|
|
955
|
+
// persisted digest that has drifted from the state it claims to describe still raises.
|
|
956
|
+
// Gate 152 pins both equalities.
|
|
957
|
+
if (wire.kind === "delta" && persistence.persistedStateSha256 !== wire.baseStateSha256) {
|
|
889
958
|
throw new Error("Active-context durable base digest drift");
|
|
890
959
|
}
|
|
891
960
|
await pi.appendEntry(stateEntryType, wire);
|
|
892
961
|
persistence.persisted = clone(next);
|
|
893
962
|
persistence.persistedWireVersion = 2;
|
|
894
|
-
persistence.persistedStateSha256 =
|
|
963
|
+
persistence.persistedStateSha256 = wire.stateSha256;
|
|
895
964
|
persistence.state = lifecycle.shuttingDown ? null : clone(next);
|
|
896
965
|
if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
|
|
897
966
|
if (ctx && !contextSessionMatches(ctx, sessionId)) load(ctx);
|
|
@@ -1119,7 +1188,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1119
1188
|
|
|
1120
1189
|
const persistManual = async (
|
|
1121
1190
|
next: ActiveContextState,
|
|
1122
|
-
action: Exclude<ActiveContextToolAction, "status">,
|
|
1123
1191
|
ctx: any,
|
|
1124
1192
|
): Promise<void> => {
|
|
1125
1193
|
const stateAtEntry = persistence.state ? clone(persistence.state) : null;
|
|
@@ -1191,7 +1259,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1191
1259
|
policy: snapshot ? {
|
|
1192
1260
|
maxTarget: snapshot.thresholds.maxTarget,
|
|
1193
1261
|
minTarget: snapshot.thresholds.minTarget,
|
|
1194
|
-
freshTail: snapshot.thresholds.freshTail,
|
|
1195
1262
|
hardFenceRatio: hardFenceRatio(snapshot),
|
|
1196
1263
|
} : null,
|
|
1197
1264
|
});
|
|
@@ -1365,25 +1432,36 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1365
1432
|
};
|
|
1366
1433
|
};
|
|
1367
1434
|
|
|
1368
|
-
|
|
1435
|
+
// THE SERIALIZATION TRAVELS, THE READING DOES NOT (2026-08-28). Six whole-window
|
|
1436
|
+
// serializations rode every provider request and four were the same string from the same
|
|
1437
|
+
// unmutated array. Only the TEXT is threaded, never a finished reading: each site still
|
|
1438
|
+
// re-reads `measurements.projectionAnchor` and `projectionCharsPerToken()` at its own
|
|
1439
|
+
// time, so every emitted number stays what it was. That matters on the commit path, where
|
|
1440
|
+
// `enforceProjectionBudget` awaits `attemptAutomaticCommit` and an `attributionChanged`
|
|
1441
|
+
// can null the anchor between the last measurement and the read: threading a reading would
|
|
1442
|
+
// freeze `basis` at "measured" where today it flips to "unmeasured". Roughly 50ms per
|
|
1443
|
+
// request at a 467KB window, 100ms at 1MB, all of it blocking main-loop CPU. Honest
|
|
1444
|
+
// framing: six become three, not one.
|
|
1445
|
+
const projectedTokenReading = (projected: unknown[], serialized?: string): {
|
|
1369
1446
|
tokens: number;
|
|
1370
1447
|
basis: ProjectionReadingBasis;
|
|
1371
1448
|
chars: number;
|
|
1372
1449
|
anchorTokens: number | null;
|
|
1373
1450
|
deltaChars: number | null;
|
|
1451
|
+
text: string;
|
|
1374
1452
|
} => {
|
|
1375
|
-
const text = stableStringify(projected);
|
|
1453
|
+
const text = serialized ?? stableStringify(projected);
|
|
1376
1454
|
const chars = Buffer.byteLength(text, "utf8");
|
|
1377
1455
|
const charsPerToken = projectionCharsPerToken();
|
|
1378
1456
|
const estimate = Math.ceil(chars / charsPerToken);
|
|
1379
1457
|
const anchor = measurements.projectionAnchor;
|
|
1380
1458
|
if (!anchor || anchor.generation !== lifecycle.generation ||
|
|
1381
1459
|
anchor.sessionId !== persistence.state?.sessionId) {
|
|
1382
|
-
return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null };
|
|
1460
|
+
return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null, text };
|
|
1383
1461
|
}
|
|
1384
1462
|
const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
|
|
1385
1463
|
if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
|
|
1386
|
-
return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null };
|
|
1464
|
+
return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null, text };
|
|
1387
1465
|
}
|
|
1388
1466
|
const deltaChars = chars - anchor.chars;
|
|
1389
1467
|
return {
|
|
@@ -1392,6 +1470,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1392
1470
|
chars,
|
|
1393
1471
|
anchorTokens: anchor.tokens,
|
|
1394
1472
|
deltaChars,
|
|
1473
|
+
text,
|
|
1395
1474
|
};
|
|
1396
1475
|
};
|
|
1397
1476
|
|
|
@@ -1422,7 +1501,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1422
1501
|
estimatorErrorShare() * estimate + expectedInflowTokens(),
|
|
1423
1502
|
));
|
|
1424
1503
|
|
|
1425
|
-
const projectionExceedsBudget = (projected: unknown[], ctx: any): {
|
|
1504
|
+
const projectionExceedsBudget = (projected: unknown[], ctx: any, serialized?: string): {
|
|
1426
1505
|
tokens: number;
|
|
1427
1506
|
sizeTokens: number;
|
|
1428
1507
|
basis: ProjectionReadingBasis;
|
|
@@ -1430,12 +1509,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1430
1509
|
marginTokens: number;
|
|
1431
1510
|
over: boolean;
|
|
1432
1511
|
crowded: boolean;
|
|
1512
|
+
text: string;
|
|
1433
1513
|
} => {
|
|
1434
1514
|
const capacity = currentCapacity(ctx);
|
|
1435
1515
|
const budgetTokens = Number.isFinite(capacity.budgetTokens) && capacity.budgetTokens > 0
|
|
1436
1516
|
? capacity.budgetTokens
|
|
1437
1517
|
: Number.POSITIVE_INFINITY;
|
|
1438
|
-
const reading = projectedTokenReading(projected);
|
|
1518
|
+
const reading = projectedTokenReading(projected, serialized);
|
|
1439
1519
|
const tokens = reading.tokens;
|
|
1440
1520
|
const sizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
|
|
1441
1521
|
const marginTokens = Number.isFinite(budgetTokens)
|
|
@@ -1449,6 +1529,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1449
1529
|
marginTokens,
|
|
1450
1530
|
over: tokens > budgetTokens,
|
|
1451
1531
|
crowded: tokens + marginTokens > budgetTokens,
|
|
1532
|
+
text: reading.text,
|
|
1452
1533
|
};
|
|
1453
1534
|
};
|
|
1454
1535
|
|
|
@@ -1487,14 +1568,247 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1487
1568
|
ctx.abort();
|
|
1488
1569
|
};
|
|
1489
1570
|
|
|
1571
|
+
// THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
|
|
1572
|
+
//
|
|
1573
|
+
// `maxTarget` is what the settings panel calls the commit trigger, and until now it
|
|
1574
|
+
// fired nothing. There were two commit sites: the fence below, which measures against
|
|
1575
|
+
// the WHOLE serving budget plus a margin, and the compaction boundary, which is Pi's
|
|
1576
|
+
// own threshold near the top of the window. A live 1M-window session sat at 571,367
|
|
1577
|
+
// tokens against a stated trigger of 516,096 with `folds: 0`, because neither site was
|
|
1578
|
+
// anywhere near. On a 251,520-token budget the fence sits close enough behind the
|
|
1579
|
+
// boundary to hide it; at 1M the two are half a million tokens apart.
|
|
1580
|
+
//
|
|
1581
|
+
// Shane, on the shape of the fix: "if the context is over our max, we commit. I don't
|
|
1582
|
+
// think that should be anything like once it crosses this threshold or anything like
|
|
1583
|
+
// that, it should just be an if statement. That way there isn't much room for error, if
|
|
1584
|
+
// it's over we fold." So there is no latch and no crossing to detect. Every projection
|
|
1585
|
+
// pass asks one question and acts on the answer. Occupancy only moves when the provider
|
|
1586
|
+
// measures, and a second attempt inside one handoff already defers on the mutation
|
|
1587
|
+
// budget, so "one commit per pass while over" is what this produces. A pass that finds
|
|
1588
|
+
// nothing to fold says so in the stream, which is a state worth seeing rather than one
|
|
1589
|
+
// worth suppressing.
|
|
1590
|
+
/**
|
|
1591
|
+
* Cut the raw material that has piled up past the last fold, and say so.
|
|
1592
|
+
*
|
|
1593
|
+
* This is the whole of "the runtime cuts": no commit, no bytes moved, no persistence
|
|
1594
|
+
* boundary. It stages pending marks the same way a commit's own top-up does, through
|
|
1595
|
+
* the same selector and the same brief builder, so a frontier cut and a commit fill are
|
|
1596
|
+
* the same object and the agent cannot tell them apart except by when it heard.
|
|
1597
|
+
*/
|
|
1598
|
+
const advanceFoldFrontier = (snapshot: ActiveContextSnapshot): number => {
|
|
1599
|
+
if (!persistence.state) return 0;
|
|
1600
|
+
if (ladder.automaticFailure !== null) return 0;
|
|
1601
|
+
// A SCAN THAT FOUND NOTHING IS NOT RE-RUN UNTIL SOMETHING CHANGES. Exhausting the
|
|
1602
|
+
// selector costs one window scan per staged cut and one more to come back empty, and
|
|
1603
|
+
// once everything foldable is staged EVERY later pass pays that last scan to be told
|
|
1604
|
+
// the same thing. Measured on the 90-projection loop inside gate 50's instrumentation
|
|
1605
|
+
// claim: 137ms of a 460ms pass, 30 percent of the runtime's own per-request cost, for
|
|
1606
|
+
// an answer that cannot have changed.
|
|
1607
|
+
//
|
|
1608
|
+
// The key is the one `authoritativeSnapshotFor` already trusts for the snapshot
|
|
1609
|
+
// itself, plus the two pieces of state the selector reads: the revision, which moves
|
|
1610
|
+
// on every durable write, and the pending-mark count, which moves when the agent
|
|
1611
|
+
// withdraws one. It is only consulted when the last scan staged NOTHING; a scan that
|
|
1612
|
+
// staged something changed the state, so its key is stale by construction.
|
|
1613
|
+
const frontierKey = `${snapshot.messages.length}:${snapshot.mapped.length}:` +
|
|
1614
|
+
`${persistence.state.revision}:${pendingMarks(persistence.state).length}`;
|
|
1615
|
+
if (ladder.frontierEmptyKey === frontierKey) return 0;
|
|
1616
|
+
// CONSOLIDATION IS ANNOUNCED THE SAME WAY (Shane, 2026-08-23). When standing folds
|
|
1617
|
+
// reach `consolidateAfter` the parent that will absorb them is staged HERE rather than
|
|
1618
|
+
// being computed inside the commit, for exactly the reason the frontier exists: a
|
|
1619
|
+
// parent's brief indexes ten children, it is the entry the agent will navigate by, and
|
|
1620
|
+
// the agent knows what mattered in those children while the commit only knows their
|
|
1621
|
+
// order. Staged, it is byte-inert like any other pending mark and rides the same
|
|
1622
|
+
// notice, so there is one mechanism and not two.
|
|
1623
|
+
const marks = [
|
|
1624
|
+
...frontierMarks({
|
|
1625
|
+
snapshot,
|
|
1626
|
+
state: persistence.state,
|
|
1627
|
+
ordinal: instrumentation.requests,
|
|
1628
|
+
}),
|
|
1629
|
+
...consolidationMarks({
|
|
1630
|
+
snapshot,
|
|
1631
|
+
state: persistence.state,
|
|
1632
|
+
ordinal: instrumentation.requests,
|
|
1633
|
+
}),
|
|
1634
|
+
];
|
|
1635
|
+
let state = persistence.state;
|
|
1636
|
+
const staged: ReturnType<typeof frontierMarks> = [];
|
|
1637
|
+
for (const mark of marks) {
|
|
1638
|
+
const addition = addPendingMark(state, mark);
|
|
1639
|
+
if (!addition.added) continue;
|
|
1640
|
+
state = addition.state;
|
|
1641
|
+
staged.push(mark);
|
|
1642
|
+
}
|
|
1643
|
+
// NOTHING STAGED, so this key is answered and the next pass under it can skip the
|
|
1644
|
+
// scan. Read from `staged` rather than from `marks`, because a candidate the pending
|
|
1645
|
+
// set already holds comes back from the selector every pass and is refused every
|
|
1646
|
+
// pass: keying on `marks` left the skip unreachable in exactly the steady state it
|
|
1647
|
+
// was built for, and half its measured saving on the floor.
|
|
1648
|
+
if (!staged.length) {
|
|
1649
|
+
ladder.frontierEmptyKey = frontierKey;
|
|
1650
|
+
return 0;
|
|
1651
|
+
}
|
|
1652
|
+
persistence.state = state;
|
|
1653
|
+
emit("context.frontier", {
|
|
1654
|
+
cut: staged.length,
|
|
1655
|
+
pending_marks: (state.pendingMarks ?? []).length,
|
|
1656
|
+
min_fold_chars: thresholds.minFoldChars,
|
|
1657
|
+
});
|
|
1658
|
+
// NO PERSISTENCE BOUNDARY OF ITS OWN, deliberately. A cut is DERIVED: the mark id
|
|
1659
|
+
// comes from the span, so a restart that lost these re-cuts exactly the same spans
|
|
1660
|
+
// with exactly the same ids, and the only thing that has to survive is a brief the
|
|
1661
|
+
// agent WROTE, which its own action persists along with the marks it rides with.
|
|
1662
|
+
// Persisting here instead cost a durable write on every projection pass, and the
|
|
1663
|
+
// branch entry it appended rebuilt the projection: gates 110 and 111 caught it as an
|
|
1664
|
+
// occupancy anchor that stopped reading the provider's own count.
|
|
1665
|
+
return staged.length;
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* Tell the agent what was cut, once there is enough of it to be worth a turn.
|
|
1670
|
+
*
|
|
1671
|
+
* It is built from LIVE state, so it names what is unbriefed at the pass it speaks on,
|
|
1672
|
+
* and it is admitted ONCE per frozen projection, so a pass that adds material leaves it
|
|
1673
|
+
* buried at its own index rather than moving it to the tail. Those two together mean it
|
|
1674
|
+
* is a snapshot rather than a running tally: cuts staged after it spoke wait for the
|
|
1675
|
+
* next pass that rebuilds the projection anyway, and that is the trade being made. A
|
|
1676
|
+
* carrier that re-renders to stay current has to move, moving costs the occupancy
|
|
1677
|
+
* anchor its provider count, and an anchor is worth more than a fold id arriving one
|
|
1678
|
+
* epoch early. What it must never do is ask twice for a brief the agent has written,
|
|
1679
|
+
* and it does not: the rebuild reads the marks, not its own last text.
|
|
1680
|
+
*/
|
|
1681
|
+
const appendFoldNotice = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1682
|
+
// The deterministic experiment condition: cuts stage and commits land exactly as
|
|
1683
|
+
// ever, but nothing invites a brief, so briefs stay the runtime's own.
|
|
1684
|
+
if (!postFoldNotice) return projected;
|
|
1685
|
+
if (!persistence.state) return projected;
|
|
1686
|
+
const held = pendingMarks(persistence.state);
|
|
1687
|
+
const unbriefed = held.filter((mark) =>
|
|
1688
|
+
mark.briefProvenance?.kind !== "supplied" && mark.briefProvenance?.kind !== "augmented");
|
|
1689
|
+
// BATCHED, AND APPENDED ONCE. A notice per cut is noise, and noise is how guidance
|
|
1690
|
+
// gets ignored; the frontier cuts often and this speaks only when a batch has built
|
|
1691
|
+
// up. `carrierAdmitted` is what makes it an APPEND rather than a rewrite: once the
|
|
1692
|
+
// carrier is in the frozen projection it is not pushed again, so raw material
|
|
1693
|
+
// arriving afterwards lands after it and the prefix in front of it never moves.
|
|
1694
|
+
//
|
|
1695
|
+
// The first cut of this withdrew the carrier and re-pushed it at the tail every pass,
|
|
1696
|
+
// the way the steward band had to because a band must be able to stand down. That is
|
|
1697
|
+
// exactly wrong for this one: re-pushing moved the notice past each newly arrived
|
|
1698
|
+
// message, so the byte at its old index changed on a pass where nothing else had, and
|
|
1699
|
+
// gates 110 and 111 read the whole projection as rewritten and dropped the occupancy
|
|
1700
|
+
// anchor back to the estimate. A carrier that moves on a quiet pass costs the cache,
|
|
1701
|
+
// which is the one thing this design exists to avoid.
|
|
1702
|
+
if (unbriefed.length < UNBRIEFED_FOLDS_BEFORE_NOTICE) return projected;
|
|
1703
|
+
if (!carrierAdmitted("fold-notice")) return projected;
|
|
1704
|
+
const text = foldNoticeText({
|
|
1705
|
+
unbriefed: unbriefed.map((mark) => ({
|
|
1706
|
+
id: mark.id,
|
|
1707
|
+
kind: mark.kind,
|
|
1708
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
1709
|
+
brief: typeof mark.brief === "string" ? mark.brief : undefined,
|
|
1710
|
+
})),
|
|
1711
|
+
pending: held.length,
|
|
1712
|
+
toolName,
|
|
1713
|
+
brandNoun,
|
|
1714
|
+
});
|
|
1715
|
+
emit("context.frontier", {
|
|
1716
|
+
cut: 0,
|
|
1717
|
+
pending_marks: held.length,
|
|
1718
|
+
unbriefed: unbriefed.length,
|
|
1719
|
+
notice_chars: text.length,
|
|
1720
|
+
});
|
|
1721
|
+
projected.push({
|
|
1722
|
+
role: "custom",
|
|
1723
|
+
customType: foldNoticeProjectionType,
|
|
1724
|
+
content: text,
|
|
1725
|
+
display: false,
|
|
1726
|
+
details: { source: activeContextSource(entryTypePrefix), ephemeral: true },
|
|
1727
|
+
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
1728
|
+
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
1729
|
+
: 0,
|
|
1730
|
+
});
|
|
1731
|
+
return projected;
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* THE TABLE OF CONTENTS IS ONE COPY PER COMMIT (Shane, 2026-08-26). It is admitted
|
|
1736
|
+
* once per frozen projection, exactly as the fold notice above, and that admission
|
|
1737
|
+
* rule IS the refresh law: between commits the copy stays buried at its index, so a
|
|
1738
|
+
* quiet pass moves no byte and costs no occupancy anchor, and the tool result of every
|
|
1739
|
+
* `remember` carries the live key list in the meantime. A commit rewrites the
|
|
1740
|
+
* projection, the freeze drops, the body filter strips the old copy, and the next pass
|
|
1741
|
+
* seats a fresh one at the tail with the latest keys. Nothing here re-renders in
|
|
1742
|
+
* place, because a carrier that moves on a quiet pass is the exact defect the fold
|
|
1743
|
+
* notice's comment records.
|
|
1744
|
+
*/
|
|
1745
|
+
const appendMemoryToc = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1746
|
+
if (!workingMemory) return projected;
|
|
1747
|
+
if (!persistence.state) return projected;
|
|
1748
|
+
const held = persistence.state.memory ?? [];
|
|
1749
|
+
if (!held.length) return projected;
|
|
1750
|
+
if (!carrierAdmitted("memory-toc")) return projected;
|
|
1751
|
+
const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
|
|
1752
|
+
const text = memoryTocText({
|
|
1753
|
+
entries: entries.map((entry) => ({ key: entry.key, chars: entry.body.length })),
|
|
1754
|
+
toolName,
|
|
1755
|
+
brandNoun,
|
|
1756
|
+
});
|
|
1757
|
+
projected.push({
|
|
1758
|
+
role: "custom",
|
|
1759
|
+
customType: memoryTocProjectionType,
|
|
1760
|
+
content: text,
|
|
1761
|
+
display: false,
|
|
1762
|
+
details: { source: activeContextSource(entryTypePrefix), ephemeral: true },
|
|
1763
|
+
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
1764
|
+
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
1765
|
+
: 0,
|
|
1766
|
+
});
|
|
1767
|
+
return projected;
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
const enforceBandTop = async (
|
|
1771
|
+
snapshot: ActiveContextSnapshot,
|
|
1772
|
+
ctx: any,
|
|
1773
|
+
): Promise<boolean> => {
|
|
1774
|
+
if (!epochCommitDue(snapshot, measurements.latestRatio)) return false;
|
|
1775
|
+
// ONE COMMIT PER MEASUREMENT, which is what "one commit per crossing" means once the
|
|
1776
|
+
// commit stops taking everything. There was no latch here and none was needed: the
|
|
1777
|
+
// band-top commit folded every span the class law offered, so the next pass found
|
|
1778
|
+
// nothing proposable and deferred. The 2026-08-23 depth bound ends that, on purpose,
|
|
1779
|
+
// and without this the parked window commits a SLICE on every pass at the same
|
|
1780
|
+
// declared occupancy, which is a prefix rewrite per pass and the exact cache cost the
|
|
1781
|
+
// one-commit rule exists to prevent. Occupancy is anchored to the provider's own
|
|
1782
|
+
// count, so a new count is the only evidence that the window actually moved; the
|
|
1783
|
+
// latch is keyed to that measurement and nothing else.
|
|
1784
|
+
if (ladder.bandTopMeasurement !== null &&
|
|
1785
|
+
ladder.bandTopMeasurement === measurements.lastProviderMeasurement) {
|
|
1786
|
+
return false;
|
|
1787
|
+
}
|
|
1788
|
+
ladder.bandTopMeasurement = measurements.lastProviderMeasurement;
|
|
1789
|
+
let action: Record<string, unknown> | null = null;
|
|
1790
|
+
try {
|
|
1791
|
+
// NULL RATIO, deliberately. The band top is routine housekeeping at a stated
|
|
1792
|
+
// threshold, not an emergency, and a null ratio is what arms the depth bound: the
|
|
1793
|
+
// commit takes stalest-first and stops at the aim, so the newest events stay raw
|
|
1794
|
+
// for as long as there is room for them. The fence and the compaction boundary
|
|
1795
|
+
// carry a ratio and no bound, because something has already gone wrong there.
|
|
1796
|
+
action = await attemptAutomaticCommit(snapshot, ctx, "band-top", null);
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
suspendAutomatic(error, "band-top", ctx);
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
return action !== null;
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1490
1804
|
const enforceProjectionBudget = async (
|
|
1491
1805
|
snapshot: ActiveContextSnapshot,
|
|
1492
1806
|
projected: unknown[],
|
|
1493
1807
|
ctx: any,
|
|
1494
|
-
): Promise<{ projected: unknown[]; aborted: boolean }> => {
|
|
1808
|
+
): Promise<{ projected: unknown[]; aborted: boolean; text: string }> => {
|
|
1495
1809
|
let measured = projectionExceedsBudget(projected, ctx);
|
|
1496
1810
|
const rejected = curation.pendingRejection !== null;
|
|
1497
|
-
if (!measured.crowded && !rejected) return { projected, aborted: false };
|
|
1811
|
+
if (!measured.crowded && !rejected) return { projected, aborted: false, text: measured.text };
|
|
1498
1812
|
const trigger = measured;
|
|
1499
1813
|
let reduced = projected;
|
|
1500
1814
|
let attempts = 0;
|
|
@@ -1503,9 +1817,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1503
1817
|
attempts += 1;
|
|
1504
1818
|
// THE EMERGENCY IS THE REQUEST THAT WILL NOT FIT, and only that. A projection at
|
|
1505
1819
|
// the margin is ordinary work: the fence commits it because it is the pass holding
|
|
1506
|
-
// the request, but it is not a recovery
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1820
|
+
// the request, but it is not a recovery and it does not buy an exemption from the
|
|
1821
|
+
// reclaim floor. What separates it from the band top is depth alone: the fence
|
|
1822
|
+
// carries a ratio, so its commit takes everything eligible rather than stopping at
|
|
1823
|
+
// the aim.
|
|
1509
1824
|
const emergency = measured.over || rejected;
|
|
1510
1825
|
if (emergency) curation.recoveryAttempts += 1;
|
|
1511
1826
|
let action: Record<string, unknown> | null = null;
|
|
@@ -1597,9 +1912,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1597
1912
|
}));
|
|
1598
1913
|
curation.pendingRejection = null;
|
|
1599
1914
|
}
|
|
1600
|
-
|
|
1915
|
+
// `measured` is recomputed on `reduced` at every turn of the loop above, so its text is
|
|
1916
|
+
// the serialization of the array both of these exits return.
|
|
1917
|
+
if (!measured.over) return { projected: reduced, aborted: false, text: measured.text };
|
|
1601
1918
|
abortOverBudgetProjection(measured.tokens, measured.budgetTokens, ctx, attempts);
|
|
1602
|
-
return { projected: reduced, aborted: true };
|
|
1919
|
+
return { projected: reduced, aborted: true, text: measured.text };
|
|
1603
1920
|
};
|
|
1604
1921
|
|
|
1605
1922
|
const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
|
|
@@ -1678,69 +1995,148 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1678
1995
|
return projected;
|
|
1679
1996
|
};
|
|
1680
1997
|
|
|
1681
|
-
const appendRider = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1682
|
-
const rider = persistence.state?.rider;
|
|
1683
|
-
if (!rider) return projected;
|
|
1684
|
-
if (!carrierAdmitted("rider")) return projected;
|
|
1685
|
-
projected.push({
|
|
1686
|
-
role: "custom",
|
|
1687
|
-
customType: riderProjectionType,
|
|
1688
|
-
content: rider.text,
|
|
1689
|
-
display: false,
|
|
1690
|
-
details: {
|
|
1691
|
-
source: activeContextSource(entryTypePrefix),
|
|
1692
|
-
ephemeral: true,
|
|
1693
|
-
riderEpoch: rider.epoch,
|
|
1694
|
-
},
|
|
1695
|
-
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
1696
|
-
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
1697
|
-
: 0,
|
|
1698
|
-
});
|
|
1699
|
-
return projected;
|
|
1700
|
-
};
|
|
1701
|
-
|
|
1702
1998
|
const holdFrozen = (projected: unknown[]): unknown[] => {
|
|
1703
1999
|
freeze.projection = [...projected];
|
|
1704
2000
|
freeze.active = false;
|
|
1705
2001
|
return projected;
|
|
1706
2002
|
};
|
|
1707
2003
|
|
|
2004
|
+
// An ephemeral peek result rides the projection exactly until the model's
|
|
2005
|
+
// next message, the same answered reading as the directed ask, then its
|
|
2006
|
+
// index holds a one-line placeholder and the reply is the surviving trace.
|
|
2007
|
+
// A message already projection-shaped (a fold's own deterministic brief
|
|
2008
|
+
// placeholder carries the same toolCallId) is never touched: only the raw
|
|
2009
|
+
// result is the ephemeral one.
|
|
2010
|
+
//
|
|
2011
|
+
// Returns what it replaced, keyed by call id, because the freeze has to be
|
|
2012
|
+
// told. Running against the BODY alone was not enough: the freeze compares
|
|
2013
|
+
// this pass's body prefix against the previous one and reuses the previous
|
|
2014
|
+
// PROJECTION when they match, and a withdrawal edits a body index inside
|
|
2015
|
+
// that prefix, so the comparison failed and the whole projection was rebuilt
|
|
2016
|
+
// from the body. The rebuild drops every receipt the freeze had
|
|
2017
|
+
// buried mid-array over the session, which moves divergence back to the
|
|
2018
|
+
// FIRST buried carrier rather than leaving it at the withdrawn index. That
|
|
2019
|
+
// is the opposite of what this mechanism is for: a tail edit was rewriting
|
|
2020
|
+
// the session's whole cached prefix.
|
|
2021
|
+
const withdrawConsumedEphemeralPeeks = (body: unknown[]): Map<string, unknown> => {
|
|
2022
|
+
const withdrawn = new Map<string, unknown>();
|
|
2023
|
+
if (!ephemeralPeeks.size) return withdrawn;
|
|
2024
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
2025
|
+
const message = body[index];
|
|
2026
|
+
const callId = ownValue(message, "toolCallId");
|
|
2027
|
+
if (ownValue(message, "role") !== "toolResult" || typeof callId !== "string" ||
|
|
2028
|
+
!ephemeralPeeks.has(callId)) continue;
|
|
2029
|
+
const details = ownValue(message, "details");
|
|
2030
|
+
if (details && typeof ownValue(details, "projection") === "string") continue;
|
|
2031
|
+
const answered = body.slice(index + 1).some((later) =>
|
|
2032
|
+
ownValue(later, "role") === "assistant");
|
|
2033
|
+
if (!answered) continue;
|
|
2034
|
+
const placeholder = {
|
|
2035
|
+
role: "toolResult",
|
|
2036
|
+
toolCallId: callId,
|
|
2037
|
+
toolName: ownValue(message, "toolName"),
|
|
2038
|
+
content: [{ type: "text", text: "Ephemeral peek consumed; the reply that followed it is the surviving trace." }],
|
|
2039
|
+
isError: false,
|
|
2040
|
+
details: { projection: "ephemeral-peek-consumed" },
|
|
2041
|
+
timestamp: ownValue(message, "timestamp"),
|
|
2042
|
+
};
|
|
2043
|
+
body[index] = placeholder;
|
|
2044
|
+
withdrawn.set(callId, placeholder);
|
|
2045
|
+
}
|
|
2046
|
+
return withdrawn;
|
|
2047
|
+
};
|
|
2048
|
+
|
|
2049
|
+
// Carry a withdrawal into the freeze's own kept arrays so the frozen prefix
|
|
2050
|
+
// stays usable. Matched by CALL ID rather than by position: the projection
|
|
2051
|
+
// interleaves buried carriers, so a body index is not a projection index.
|
|
2052
|
+
// The substitution is identity-preserving on role, call id, tool name and
|
|
2053
|
+
// timestamp, so what changes is the one result's content and nothing else,
|
|
2054
|
+
// and divergence lands at the withdrawn index where the mechanism puts it.
|
|
2055
|
+
const carryWithdrawalIntoFreeze = (messages: unknown[], withdrawn: Map<string, unknown>): void => {
|
|
2056
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
2057
|
+
const message = messages[index];
|
|
2058
|
+
if (ownValue(message, "role") !== "toolResult") continue;
|
|
2059
|
+
const callId = ownValue(message, "toolCallId");
|
|
2060
|
+
if (typeof callId !== "string") continue;
|
|
2061
|
+
const placeholder = withdrawn.get(callId);
|
|
2062
|
+
if (!placeholder) continue;
|
|
2063
|
+
const details = ownValue(message, "details");
|
|
2064
|
+
if (details && typeof ownValue(details, "projection") === "string") continue;
|
|
2065
|
+
messages[index] = placeholder;
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
|
|
1708
2069
|
const projectWithAdvisory = (snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1709
2070
|
const body = projectActiveContext(snapshot, persistence.state!).filter((message) => {
|
|
1710
2071
|
const customType = ownValue(message, "customType");
|
|
1711
2072
|
return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
|
|
1712
|
-
customType !== receiptProjectionType &&
|
|
1713
|
-
customType !==
|
|
2073
|
+
customType !== receiptProjectionType && customType !== curationProjectionType &&
|
|
2074
|
+
customType !== foldNoticeProjectionType && customType !== memoryTocProjectionType;
|
|
1714
2075
|
});
|
|
2076
|
+
const withdrawn = withdrawConsumedEphemeralPeeks(body);
|
|
2077
|
+
if (withdrawn.size && freeze.body) {
|
|
2078
|
+
carryWithdrawalIntoFreeze(freeze.body, withdrawn);
|
|
2079
|
+
freeze.bodyText = stableStringify(freeze.body);
|
|
2080
|
+
if (freeze.projection) carryWithdrawalIntoFreeze(freeze.projection, withdrawn);
|
|
2081
|
+
}
|
|
1715
2082
|
const held = freeze.body?.length ?? 0;
|
|
1716
|
-
|
|
1717
|
-
|
|
2083
|
+
// THE PREFIX IS READ OUT OF THE WHOLE SERIALIZATION (2026-08-28). This asked
|
|
2084
|
+
// `stableStringify(body.slice(0, held)) === freeze.bodyText`, and in steady state `held`
|
|
2085
|
+
// is `body.length` minus the couple of messages that just arrived, so it was a second
|
|
2086
|
+
// full serialization of the window plus a slice allocation, beside the one below that
|
|
2087
|
+
// every pass does anyway. `serializedPrefixHolds` carries why that is sound; a false
|
|
2088
|
+
// POSITIVE here splices the previous pass's `freeze.projection` in front of the new tail
|
|
2089
|
+
// and emits stale messages, which is a content bug rather than a slowdown, and gate 154
|
|
2090
|
+
// drives the predicate itself rather than restating it.
|
|
2091
|
+
const bodyText = stableStringify(body);
|
|
2092
|
+
freeze.active = freeze.projection !== null &&
|
|
2093
|
+
serializedPrefixHolds(bodyText, freeze.bodyText, held, body.length);
|
|
1718
2094
|
const projected = freeze.active
|
|
1719
2095
|
? [...freeze.projection!, ...body.slice(held)]
|
|
1720
2096
|
: [...body];
|
|
1721
2097
|
if (!freeze.active) freeze.keys.clear();
|
|
1722
2098
|
freeze.body = body;
|
|
1723
|
-
freeze.bodyText =
|
|
2099
|
+
freeze.bodyText = bodyText;
|
|
1724
2100
|
appendReceipts(projected, snapshot);
|
|
1725
|
-
|
|
2101
|
+
appendFoldNotice(projected, snapshot);
|
|
2102
|
+
appendMemoryToc(projected, snapshot);
|
|
1726
2103
|
return holdFrozen(projected);
|
|
1727
2104
|
};
|
|
1728
|
-
const noteProjection = (projected: unknown[]): void => {
|
|
2105
|
+
const noteProjection = (projected: unknown[], serialized?: string): void => {
|
|
1729
2106
|
const digests = messageDigests(projected);
|
|
1730
2107
|
const comparison = compareProjections(instrumentation.previousDigests, digests);
|
|
1731
2108
|
recordProjection(instrumentation.ledger, comparison, digests);
|
|
1732
2109
|
instrumentation.previousDigests = digests;
|
|
1733
2110
|
instrumentation.lastChange = comparison.change;
|
|
1734
|
-
const text = stableStringify(projected);
|
|
2111
|
+
const text = serialized ?? stableStringify(projected);
|
|
1735
2112
|
const divergence = prefixDivergence(instrumentation.previousText, text);
|
|
1736
2113
|
const previousChars = instrumentation.previousText?.length ?? 0;
|
|
1737
2114
|
instrumentation.lastPreservedShare = previousChars > 0
|
|
1738
2115
|
? divergence.identicalChars / previousChars
|
|
1739
2116
|
: null;
|
|
1740
2117
|
const charsPerToken = projectionCharsPerToken();
|
|
2118
|
+
const successfulCacheActions = instrumentation.sinceHandoff.filter((event) =>
|
|
2119
|
+
event.kind === "context.attempt" && event.ok === true &&
|
|
2120
|
+
typeof event.action === "string" && CACHE_ACTION_REQUEST_CLASS[event.action]);
|
|
2121
|
+
const latestCacheAction = successfulCacheActions.at(-1) ?? null;
|
|
2122
|
+
const latestCacheActionClass = latestCacheAction?.action
|
|
2123
|
+
? CACHE_ACTION_REQUEST_CLASS[latestCacheAction.action]
|
|
2124
|
+
: null;
|
|
2125
|
+
const projectionActions = successfulCacheActions.filter((event) =>
|
|
2126
|
+
event.action === "expand" || event.action === "refold");
|
|
1741
2127
|
const causes = instrumentation.sinceHandoff.filter((event) =>
|
|
1742
|
-
PREFIX_MUTATING_KINDS.has(event.kind));
|
|
1743
|
-
const
|
|
2128
|
+
PREFIX_MUTATING_KINDS.has(event.kind) || projectionActions.includes(event));
|
|
2129
|
+
const structuralCause = causes.some((event) => event.kind !== "context.attempt");
|
|
2130
|
+
const requestClass = causes.some((event) => event.kind === "context.recovery")
|
|
2131
|
+
? "after-rollback"
|
|
2132
|
+
: (latestCacheAction?.action === "expand" || latestCacheAction?.action === "refold")
|
|
2133
|
+
? latestCacheActionClass!
|
|
2134
|
+
: structuralCause
|
|
2135
|
+
? "after-fold"
|
|
2136
|
+
: latestCacheActionClass
|
|
2137
|
+
? latestCacheActionClass
|
|
2138
|
+
: divergence.index === null ? "steady-state" : "after-message";
|
|
2139
|
+
const reading = projectedTokenReading(projected, text);
|
|
1744
2140
|
emit("context.projection", {
|
|
1745
2141
|
change: comparison.change,
|
|
1746
2142
|
previous_count: comparison.previousCount,
|
|
@@ -1768,14 +2164,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1768
2164
|
estimated_tokens: Math.ceil(text.length / charsPerToken),
|
|
1769
2165
|
cause: divergence.index === null
|
|
1770
2166
|
? "pure-append"
|
|
1771
|
-
: (causes.length ? causes.map((event) => event.kind
|
|
2167
|
+
: (causes.length ? causes.map((event) => event.kind === "context.attempt"
|
|
2168
|
+
? `context.attempt:${String(event.action)}`
|
|
2169
|
+
: event.kind).join(",") : "unattributed"),
|
|
1772
2170
|
cause_event_seqs: causes.map((event) => event.seq).join(","),
|
|
1773
2171
|
events_since_handoff: instrumentation.sinceHandoff.length,
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
? "after-rollback"
|
|
1778
|
-
: (causes.length ? "after-fold" : "after-message")),
|
|
2172
|
+
cache_action: latestCacheAction?.action ?? null,
|
|
2173
|
+
cache_action_seq: latestCacheAction?.seq ?? null,
|
|
2174
|
+
request_class: requestClass,
|
|
1779
2175
|
});
|
|
1780
2176
|
instrumentation.previousText = text;
|
|
1781
2177
|
instrumentation.sinceHandoff = [];
|
|
@@ -1786,6 +2182,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1786
2182
|
snapshot: ActiveContextSnapshot;
|
|
1787
2183
|
candidate: FoldCandidate;
|
|
1788
2184
|
brief?: string;
|
|
2185
|
+
briefProvenance?: "supplied" | "deterministic" | BriefProvenance;
|
|
1789
2186
|
ctx: any;
|
|
1790
2187
|
signal?: AbortSignal;
|
|
1791
2188
|
maximumSourceChars?: number;
|
|
@@ -1803,6 +2200,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1803
2200
|
state: baseState,
|
|
1804
2201
|
generation: generationAtStart,
|
|
1805
2202
|
brief: input.brief,
|
|
2203
|
+
briefProvenance: input.briefProvenance,
|
|
1806
2204
|
ctx: input.ctx,
|
|
1807
2205
|
signal: input.signal,
|
|
1808
2206
|
});
|
|
@@ -1852,6 +2250,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1852
2250
|
let state = persistence.state!;
|
|
1853
2251
|
let peekAdded = 0;
|
|
1854
2252
|
let topUpAdded = 0;
|
|
2253
|
+
let refoldAdded = 0;
|
|
1855
2254
|
let consolidationAdded = 0;
|
|
1856
2255
|
let closingAdded = 0;
|
|
1857
2256
|
for (const mark of ephemeralPeekMarks({ snapshot, state, ordinal })) {
|
|
@@ -1862,7 +2261,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1862
2261
|
const addition = addPendingMark(state, mark);
|
|
1863
2262
|
if (addition.added) { state = addition.state; consolidationAdded += 1; }
|
|
1864
2263
|
}
|
|
1865
|
-
const guarded = currentTurnRefKeys(snapshot);
|
|
1866
2264
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
1867
2265
|
const usedTokens = capacity.usedTokens;
|
|
1868
2266
|
const budgetTokens = capacity.budgetTokens;
|
|
@@ -1879,43 +2277,67 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1879
2277
|
});
|
|
1880
2278
|
return null;
|
|
1881
2279
|
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2280
|
+
// ONE DEFINITION OF THE DROP. The fill sizes itself to it, the advisory warns
|
|
2281
|
+
// against it, and the mark response answers "how much more" with it.
|
|
2282
|
+
const freeingTarget = commitCoverage({ snapshot, state, usedTokens, budgetTokens }).targetShare;
|
|
2283
|
+
// ONE COMMIT, SIZED BEFORE IT RUNS (Shane, 2026-08-21). At the threshold we know how
|
|
2284
|
+
// far the window has to come down: `freeingTarget` is the distance from what is used
|
|
2285
|
+
// now to the minTarget floor. The agent's marks count toward that distance first and
|
|
2286
|
+
// the ladder fills the remainder from the stale end, so an agent that marked 30k of a
|
|
2287
|
+
// 100k drop leaves 70k for the fill and both land in the SAME commit.
|
|
2288
|
+
//
|
|
2289
|
+
// Three rules decide the fill, and each replaced something that was tried first:
|
|
2290
|
+
//
|
|
2291
|
+
// The ladder does not STAND DOWN when the agent has marked. Standing down commits at
|
|
2292
|
+
// whatever depth the agent happened to reach, so one small mark commits a barely-moved
|
|
2293
|
+
// window and the next fence pass fires a second commit in the same cycle, which is the
|
|
2294
|
+
// cache cost the one-commit rule exists to prevent. `topUpMarks` measures progress
|
|
2295
|
+
// against the state it is handed, so agent marks are counted rather than displaced,
|
|
2296
|
+
// and `claimedRefKeys` keeps the fill off spans the agent already took.
|
|
2297
|
+
//
|
|
2298
|
+
// Raw stale material is spent BEFORE expanded folds, so an agent reading an expanded
|
|
2299
|
+
// fold keeps it while anything else can cover the drop. This is also the only route
|
|
2300
|
+
// to the refold rung now that nothing stages between commits.
|
|
2301
|
+
//
|
|
2302
|
+
// The fill measures progress over EVERY mark it makes, not just the ones this commit
|
|
2303
|
+
// will apply. Measuring the eligible share alone is what a starved commit needs and it
|
|
2304
|
+
// is wrong here: a mark over protected evidence moves the eligible share by nothing,
|
|
2305
|
+
// so the loop kept marking and walked a whole 24-batch excursion chasing a target it
|
|
2306
|
+
// could not reach that way (the retainer was the current-turn guard then; pins and
|
|
2307
|
+
// protected evidence retain the same way now). Counting everything bounds the fill at
|
|
2308
|
+
// the drop it was asked for. The backstop still reads the eligible share, because
|
|
2309
|
+
// starvation is the one state where the difference is the point.
|
|
2310
|
+
const fill = (targetShare: number, eligibleOnly: boolean): void => {
|
|
2311
|
+
for (const mark of topUpMarks({ snapshot, state, ordinal, eligibleOnly, targetShare })) {
|
|
1893
2312
|
const addition = addPendingMark(state, mark);
|
|
1894
2313
|
if (addition.added) { state = addition.state; topUpAdded += 1; }
|
|
1895
2314
|
}
|
|
2315
|
+
for (const mark of refoldMarks({ snapshot, state, ordinal, eligibleOnly, targetShare })) {
|
|
2316
|
+
const addition = addPendingMark(state, mark);
|
|
2317
|
+
if (addition.added) { state = addition.state; refoldAdded += 1; }
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
let backstopFired = false;
|
|
2321
|
+
if (topUp) {
|
|
2322
|
+
fill(freeingTarget, false);
|
|
1896
2323
|
const reachedShare = markAccounting(snapshot, state).eligibleFreedBudgetShare;
|
|
1897
2324
|
const shallow = reachedShare < Math.max(commitDepthFloorShare(snapshot), freeingTarget);
|
|
1898
2325
|
if (shallow && atOrAboveBackstop(snapshot, waiverRatio)) {
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
state,
|
|
1902
|
-
ordinal,
|
|
1903
|
-
eligibleOnly: true,
|
|
1904
|
-
targetShare: 1,
|
|
1905
|
-
})) {
|
|
1906
|
-
const addition = addPendingMark(state, mark);
|
|
1907
|
-
if (addition.added) { state = addition.state; topUpAdded += 1; }
|
|
1908
|
-
}
|
|
2326
|
+
backstopFired = true;
|
|
2327
|
+
fill(1, true);
|
|
1909
2328
|
}
|
|
1910
2329
|
}
|
|
1911
2330
|
const wedges = absorbWedgeMarks({
|
|
1912
2331
|
snapshot,
|
|
1913
2332
|
state,
|
|
1914
2333
|
charsPerToken: projectionCharsPerToken(),
|
|
1915
|
-
excludeRefKeys: guarded,
|
|
1916
2334
|
});
|
|
1917
2335
|
state = wedges.state;
|
|
1918
2336
|
const accounting = markAccounting(snapshot, state);
|
|
2337
|
+
// Derived ONCE, here, against the pre-commit state both readers below report against.
|
|
2338
|
+
// It left `MarkAccounting` because it serializes the whole marked tail and every other
|
|
2339
|
+
// caller of `markAccounting` was paying for a number only this path reads.
|
|
2340
|
+
const rewriteTokens = markRewriteTokens(snapshot, state);
|
|
1919
2341
|
if (!accounting.pending) {
|
|
1920
2342
|
const remainder = unmarkedRemainder(snapshot, state, projectionCharsPerToken());
|
|
1921
2343
|
emit("context.commit", {
|
|
@@ -1942,51 +2364,100 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1942
2364
|
eligible_marks: accounting.eligibleMarks,
|
|
1943
2365
|
eligible_freed_share: accounting.eligibleFreedBudgetShare,
|
|
1944
2366
|
reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2367
|
+
// THE REMAINDER RIDES EVERY DEFERRAL, not just the empty one. It was on
|
|
2368
|
+
// `nothing-proposable` alone, which was the only starved shape while staging
|
|
2369
|
+
// happened at commit time. The frontier cuts ahead of the commit, so a starved
|
|
2370
|
+
// pass now usually arrives holding a mark it cannot apply and lands HERE instead,
|
|
2371
|
+
// and "below the reclaim floor" without the remainder does not tell a reader
|
|
2372
|
+
// whether the mass is held or simply absent. That is the distinction the whole
|
|
2373
|
+
// record exists to draw.
|
|
2374
|
+
unmarked_stale_spans: unmarkedRemainder(snapshot, state, projectionCharsPerToken()).spans,
|
|
1945
2375
|
window_tokens: snapshot.contextWindow,
|
|
1946
2376
|
});
|
|
1947
2377
|
return null;
|
|
1948
2378
|
}
|
|
1949
2379
|
const bytesBefore = bytes(projectActiveContext(snapshot, state));
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2380
|
+
// THE COMMIT CUTS ONLY AS DEEP AS THE BUDGET ASKS (Shane, 2026-08-23). The frontier
|
|
2381
|
+
// stages as material arrives, so by now the pending set is most of the window; without
|
|
2382
|
+
// this bound the first commit would fold all of it. `freeingTarget` is a share of the
|
|
2383
|
+
// budget and the bound is bytes, so it is converted once, here, through the same ratio
|
|
2384
|
+
// the projection weighs itself with.
|
|
2385
|
+
//
|
|
2386
|
+
// It binds on the ROUTINE path only: a null ratio is the band top, ordinary
|
|
2387
|
+
// housekeeping at a stated threshold, and that is where "only as deep as it needs"
|
|
2388
|
+
// belongs. The fence and the compaction boundary carry a ratio because something has
|
|
2389
|
+
// already gone wrong there, and a lane holding a request that will not fit is not the
|
|
2390
|
+
// place to leave depth on the table. The backstop is exempt for the same reason: it
|
|
2391
|
+
// fires only when the commit is already starving.
|
|
2392
|
+
//
|
|
2393
|
+
// This bound is also what protects the working set since the current-turn guard was
|
|
2394
|
+
// deleted (Shane, 2026-08-23, from the first live session): the cut takes stalest
|
|
2395
|
+
// first and stops at the aim, so the newest events stay raw exactly as long as there
|
|
2396
|
+
// is room for them, in every session shape including one that never closes a turn.
|
|
1960
2397
|
let result = await commitPendingMarks({
|
|
1961
2398
|
snapshot,
|
|
1962
2399
|
state,
|
|
1963
2400
|
generation: lifecycle.generation,
|
|
1964
2401
|
retainIneligible: true,
|
|
1965
|
-
|
|
1966
|
-
|
|
2402
|
+
...(backstopFired || waiverRatio !== null ? {} : {
|
|
2403
|
+
applyTargetBytes: Math.ceil(freeingTarget * budgetTokens * projectionCharsPerToken()),
|
|
2404
|
+
}),
|
|
1967
2405
|
});
|
|
1968
2406
|
for (;;) {
|
|
2407
|
+
// THE CLOSING PASS COMMITS ONLY WHAT IT SEATS (2026-08-23). It rides the same paid
|
|
2408
|
+
// rewrite to give a group its parent, and it runs UNBOUNDED because a parent's whole
|
|
2409
|
+
// span must land together. Handing it the full pending set therefore leaked the depth
|
|
2410
|
+
// cut: the marks the bound had just retained were still pending, the closing commit
|
|
2411
|
+
// carried no bound, and the first band-top commit after the guard deletion folded all
|
|
2412
|
+
// 24 of them to the floor (gate 09's fixture, applied 26 of 24 with deferred 0). The
|
|
2413
|
+
// marks the cut held back are set aside before the closing commit and restored after,
|
|
2414
|
+
// so the only thing an unbounded closing pass can spend is the parents it just made.
|
|
1969
2415
|
let closing = result.state;
|
|
2416
|
+
const heldBack = pendingMarks(closing);
|
|
2417
|
+
let seeded = withPendingMarks(closing, []);
|
|
2418
|
+
let seededAdded = 0;
|
|
1970
2419
|
for (const mark of consolidationMarks({ snapshot, state: closing, ordinal })) {
|
|
1971
2420
|
const addition = addPendingMark(closing, mark);
|
|
1972
|
-
if (addition.added)
|
|
2421
|
+
if (!addition.added) continue;
|
|
2422
|
+
closing = addition.state;
|
|
2423
|
+
const carried = addPendingMark(seeded, mark);
|
|
2424
|
+
if (carried.added) { seeded = carried.state; closingAdded += 1; seededAdded += 1; }
|
|
1973
2425
|
}
|
|
1974
|
-
if (
|
|
2426
|
+
if (!seededAdded) break;
|
|
1975
2427
|
const closed = await commitPendingMarks({
|
|
1976
2428
|
snapshot,
|
|
1977
|
-
state:
|
|
2429
|
+
state: seeded,
|
|
1978
2430
|
generation: lifecycle.generation,
|
|
1979
2431
|
retainIneligible: true,
|
|
1980
|
-
guardCurrentTurn: true,
|
|
1981
2432
|
});
|
|
1982
2433
|
if (!closed.applied.length) break;
|
|
1983
2434
|
result = {
|
|
1984
2435
|
...closed,
|
|
2436
|
+
state: withPendingMarks(closed.state, [...pendingMarks(closed.state), ...heldBack]),
|
|
1985
2437
|
applied: [...result.applied, ...closed.applied],
|
|
1986
2438
|
refused: [...result.refused.filter((mark) => !mark.retained), ...closed.refused],
|
|
1987
|
-
|
|
2439
|
+
retained: [...result.retained.filter((mark) =>
|
|
2440
|
+
!closed.applied.some((appliedMark) => appliedMark.id === mark.id)), ...closed.retained],
|
|
1988
2441
|
};
|
|
1989
2442
|
}
|
|
2443
|
+
// THE TOOL-CALL DIET RIDES THE SAME TRANSACTION (2026-08-24): clips are selected
|
|
2444
|
+
// against the post-fold state and land in the same revision the commit minted, so
|
|
2445
|
+
// the one paid rewrite carries both and no non-commit pass ever moves a byte. The
|
|
2446
|
+
// full bytes stay in the transcript behind each clip's entry id.
|
|
2447
|
+
let clippedResults = 0;
|
|
2448
|
+
if (toolFoldThreshold > 0) {
|
|
2449
|
+
const clipAdditions = toolClipAdditions({
|
|
2450
|
+
snapshot,
|
|
2451
|
+
state: result.state,
|
|
2452
|
+
threshold: toolFoldThreshold,
|
|
2453
|
+
blacklist: blacklistAutoFoldTools,
|
|
2454
|
+
});
|
|
2455
|
+
if (clipAdditions.length) {
|
|
2456
|
+
clippedResults = clipAdditions.length;
|
|
2457
|
+
result = { ...result, state: { ...result.state,
|
|
2458
|
+
clips: [...(result.state.clips ?? []), ...clipAdditions] } };
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
1990
2461
|
persistence.state = result.state;
|
|
1991
2462
|
const bytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
|
|
1992
2463
|
const freedBytes = Math.max(0, bytesBefore - bytesAfter);
|
|
@@ -2012,18 +2483,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2012
2483
|
applied_marks: result.applied.length,
|
|
2013
2484
|
refused_marks: result.refused.filter((mark) => !mark.retained).length,
|
|
2014
2485
|
deferred_marks: result.retained.length,
|
|
2015
|
-
waived_marks: result.waived.length,
|
|
2016
2486
|
pending_marks: accounting.pending,
|
|
2017
2487
|
agent_marks: accounting.agentMarks,
|
|
2018
2488
|
ladder_marks: accounting.ladderMarks,
|
|
2019
2489
|
peek_marks: peekAdded,
|
|
2020
2490
|
topup_marks: topUpAdded,
|
|
2491
|
+
refold_marks: refoldAdded,
|
|
2021
2492
|
consolidation_marks: consolidationAdded,
|
|
2022
2493
|
closing_consolidation_marks: closingAdded,
|
|
2023
2494
|
absorbed_wedges: wedges.absorbed.length,
|
|
2495
|
+
clipped_results: clippedResults,
|
|
2024
2496
|
freed_bytes: freedBytes,
|
|
2025
2497
|
freed_tokens: estimatedTokens(freedBytes),
|
|
2026
|
-
rewrite_tokens:
|
|
2498
|
+
rewrite_tokens: rewriteTokens,
|
|
2027
2499
|
pinned_bytes: accounting.pinnedBytes,
|
|
2028
2500
|
pinned_results: accounting.pinnedResults,
|
|
2029
2501
|
protected_stale_bytes: pinHeld.bytes,
|
|
@@ -2033,36 +2505,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2033
2505
|
instrumentation.mutationsSinceHandoff += 1;
|
|
2034
2506
|
instrumentation.lastMutationRequest = instrumentation.requests;
|
|
2035
2507
|
instrumentation.lastMutationTokens = usedTokens;
|
|
2036
|
-
const riderEpoch = commitEvent.seq;
|
|
2037
|
-
if (persistence.state.rider?.epoch !== riderEpoch) {
|
|
2038
|
-
const postAccounting = markAccounting(snapshot, persistence.state);
|
|
2039
|
-
const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
|
|
2040
|
-
const pinnedMass = explicitProtectedMass(snapshot, persistence.state);
|
|
2041
|
-
const pinnedShare = budgetTokens > 0
|
|
2042
|
-
? estimatedTokens(pinnedMass.bytes) / budgetTokens
|
|
2043
|
-
: 0;
|
|
2044
|
-
const riderText = contextRiderText({
|
|
2045
|
-
toolName,
|
|
2046
|
-
brandNoun,
|
|
2047
|
-
pendingAgentMarks: postAccounting.agentMarks,
|
|
2048
|
-
eligibleMarks: postAccounting.eligibleMarks,
|
|
2049
|
-
freedTokens: postAccounting.freedTokens,
|
|
2050
|
-
eligibleFreedTokens: postAccounting.eligibleFreedTokens,
|
|
2051
|
-
anchors: remainder.candidates.slice(0, 3).map((candidate) => candidate.id),
|
|
2052
|
-
pinnedShare,
|
|
2053
|
-
maxPinnedShare: MAX_PINNED_SHARE,
|
|
2054
|
-
});
|
|
2055
|
-
persistence.state = { ...persistence.state, rider: { epoch: riderEpoch, text: riderText } };
|
|
2056
|
-
emit("context.rider", {
|
|
2057
|
-
epoch: riderEpoch,
|
|
2058
|
-
chars: riderText.length,
|
|
2059
|
-
anchors: Math.min(3, remainder.candidates.length),
|
|
2060
|
-
pending_agent_marks: postAccounting.agentMarks,
|
|
2061
|
-
eligible_marks: postAccounting.eligibleMarks,
|
|
2062
|
-
pinned_share: pinnedShare,
|
|
2063
|
-
max_pinned_share: MAX_PINNED_SHARE,
|
|
2064
|
-
});
|
|
2065
|
-
}
|
|
2066
2508
|
let foldedToolResults = 0;
|
|
2067
2509
|
let foldedSpans = 0;
|
|
2068
2510
|
for (const applied of result.applied) {
|
|
@@ -2093,7 +2535,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2093
2535
|
end_id: wedge.endId,
|
|
2094
2536
|
entries: wedge.entries,
|
|
2095
2537
|
tokens: wedge.tokens,
|
|
2096
|
-
|
|
2538
|
+
chars: wedge.chars,
|
|
2539
|
+
threshold_chars: thresholds.minFoldChars,
|
|
2097
2540
|
});
|
|
2098
2541
|
}
|
|
2099
2542
|
const epoch = {
|
|
@@ -2105,6 +2548,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2105
2548
|
ladderMarks: accounting.ladderMarks,
|
|
2106
2549
|
peekMarks: peekAdded,
|
|
2107
2550
|
topUpMarks: topUpAdded,
|
|
2551
|
+
refoldMarks: refoldAdded,
|
|
2108
2552
|
consolidationMarks: consolidationAdded,
|
|
2109
2553
|
closingMarks: closingAdded,
|
|
2110
2554
|
absorbedWedges: wedges.absorbed.length,
|
|
@@ -2114,8 +2558,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2114
2558
|
occupancyTokensBefore: usedTokens,
|
|
2115
2559
|
depthFloorShare: commitDepthFloorShare(snapshot),
|
|
2116
2560
|
reclaimFloorShare: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2117
|
-
guardWaived: result.waived.length > 0,
|
|
2118
|
-
waivedMarks: result.waived.length,
|
|
2119
2561
|
appliedMarks: result.applied.length,
|
|
2120
2562
|
refusedMarks: result.refused.filter((mark) => !mark.retained).length,
|
|
2121
2563
|
deferredMarks: result.retained.length,
|
|
@@ -2124,10 +2566,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2124
2566
|
protectedStaleBytes: pinHeld.bytes,
|
|
2125
2567
|
protectedStaleRefs: pinHeld.refs,
|
|
2126
2568
|
retainedMarks: result.retained.length,
|
|
2127
|
-
currentTurnRetained: result.retained.filter((mark) =>
|
|
2128
|
-
markTouchesCurrentTurn(state, mark, guarded)).length,
|
|
2129
2569
|
eligibleMarks: accounting.eligibleMarks,
|
|
2130
|
-
estimatedRewriteTokens:
|
|
2570
|
+
estimatedRewriteTokens: rewriteTokens,
|
|
2131
2571
|
estimatedFreedTokens: accounting.freedTokens,
|
|
2132
2572
|
freedBudgetShare: accounting.freedBudgetShare,
|
|
2133
2573
|
sourceBytesSaved: freedBytes,
|
|
@@ -2177,8 +2617,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2177
2617
|
occupancyAfter: occupancyBefore === null ? null : Math.max(0, occupancyBefore - freedTokens),
|
|
2178
2618
|
spansFolded: epoch ? Number(epoch.foldedSpans ?? 0) : foldIds,
|
|
2179
2619
|
toolResultsFolded: epoch ? Number(epoch.foldedToolResults ?? 0) : 0,
|
|
2180
|
-
splitFolds: Number(action.splitFolds ?? 0),
|
|
2181
|
-
splitFromChars: Number(action.splitFromChars ?? 0),
|
|
2182
2620
|
absorbedWedges: Number(epoch?.absorbedWedges ?? 0),
|
|
2183
2621
|
recovered: curation.recoveryAttempts > 0,
|
|
2184
2622
|
protectedBytes: Number(epoch?.protectedStaleBytes ?? 0),
|
|
@@ -2186,59 +2624,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2186
2624
|
}));
|
|
2187
2625
|
};
|
|
2188
2626
|
|
|
2189
|
-
const applyAutomaticRung = async (
|
|
2190
|
-
snapshot: ActiveContextSnapshot,
|
|
2191
|
-
ratio: number,
|
|
2192
|
-
rungOptions: {
|
|
2193
|
-
toolOnly?: boolean;
|
|
2194
|
-
waiverRatio?: number;
|
|
2195
|
-
} = {},
|
|
2196
|
-
): Promise<Record<string, unknown> | null> => {
|
|
2197
|
-
if (!persistence.state || ladder.automaticFailure || ladder.preparing) return null;
|
|
2198
|
-
const rungSelectionOptions = { toolOnly: rungOptions.toolOnly };
|
|
2199
|
-
const markLadderSelection = (): Record<string, unknown> | null => {
|
|
2200
|
-
if (!persistence.state) return null;
|
|
2201
|
-
const decision = selectAutomaticRung(snapshot, persistence.state, ratio, {
|
|
2202
|
-
...rungSelectionOptions,
|
|
2203
|
-
claimed: claimedRefKeys(persistence.state),
|
|
2204
|
-
claimedFoldIds: markedFoldIds(persistence.state),
|
|
2205
|
-
});
|
|
2206
|
-
const mark = decision
|
|
2207
|
-
? ladderSelectionMark({
|
|
2208
|
-
snapshot, state: persistence.state, selection: decision, ordinal: markOrdinal(snapshot),
|
|
2209
|
-
})
|
|
2210
|
-
: null;
|
|
2211
|
-
if (!mark) return null;
|
|
2212
|
-
const addition = addPendingMark(persistence.state, mark);
|
|
2213
|
-
if (!addition.added) return null;
|
|
2214
|
-
persistence.state = addition.state;
|
|
2215
|
-
ladder.pendingContextNote =
|
|
2216
|
-
`Pending ${mark.mark} mark ${mark.id} recorded; no context bytes moved. ` +
|
|
2217
|
-
"It applies at the next commit epoch.";
|
|
2218
|
-
ladder.lastAutomaticAction = {
|
|
2219
|
-
kind: "mark",
|
|
2220
|
-
foldIds: [mark.id],
|
|
2221
|
-
sourceIds: [],
|
|
2222
|
-
markKind: mark.mark,
|
|
2223
|
-
markOrigin: mark.origin,
|
|
2224
|
-
pendingMarks: pendingMarks(persistence.state).length,
|
|
2225
|
-
sourceBytesSaved: 0,
|
|
2226
|
-
};
|
|
2227
|
-
return ladder.lastAutomaticAction;
|
|
2228
|
-
};
|
|
2229
|
-
// MARKING ONLY. Every mutation the rung used to perform inline -- a prepared chapter,
|
|
2230
|
-
// a tool fold, a refold, a consolidation, a chapter split -- now travels as a pending
|
|
2231
|
-
// mark and lands at the boundary, which is the one point the projection changes.
|
|
2232
|
-
// WHAT the rung decides is unchanged; WHEN it takes effect is.
|
|
2233
|
-
return markLadderSelection();
|
|
2234
|
-
};
|
|
2235
2627
|
/**
|
|
2236
|
-
*
|
|
2237
|
-
*
|
|
2238
|
-
*
|
|
2239
|
-
*
|
|
2240
|
-
*
|
|
2628
|
+
* A COMMIT, THROUGH THE SAME TRANSACTION AND THE SAME QUEUE. Two callers reach it: the
|
|
2629
|
+
* compaction boundary, which is the ordinary one, and the projection fence, which is
|
|
2630
|
+
* the emergency one. The fence cannot wait for a boundary because the request it is
|
|
2631
|
+
* holding does not fit, so it commits in place; that is the one exception, and it is
|
|
2632
|
+
* the exception that already existed.
|
|
2241
2633
|
*/
|
|
2634
|
+
|
|
2242
2635
|
const runAutomaticTransaction = async (
|
|
2243
2636
|
snapshot: ActiveContextSnapshot,
|
|
2244
2637
|
ratio: number,
|
|
@@ -2291,25 +2684,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2291
2684
|
ladder.actionQueue = queued.catch(() => undefined);
|
|
2292
2685
|
return queued;
|
|
2293
2686
|
};
|
|
2294
|
-
const attemptAutomaticRung = (
|
|
2295
|
-
snapshot: ActiveContextSnapshot,
|
|
2296
|
-
ratio: number,
|
|
2297
|
-
ctx: any,
|
|
2298
|
-
phase: string,
|
|
2299
|
-
rungOptions: {
|
|
2300
|
-
toolOnly?: boolean;
|
|
2301
|
-
waiverRatio?: number;
|
|
2302
|
-
} = {},
|
|
2303
|
-
): Promise<Record<string, unknown> | null> => queueAutomatic(
|
|
2304
|
-
snapshot, ratio, ctx, phase, () => applyAutomaticRung(snapshot, ratio, rungOptions),
|
|
2305
|
-
);
|
|
2306
|
-
/**
|
|
2307
|
-
* A COMMIT, THROUGH THE SAME TRANSACTION AND THE SAME QUEUE. Two callers reach it: the
|
|
2308
|
-
* compaction boundary, which is the ordinary one, and the projection fence, which is
|
|
2309
|
-
* the emergency one. The fence cannot wait for a boundary because the request it is
|
|
2310
|
-
* holding does not fit, so it commits in place; that is the one exception, and it is
|
|
2311
|
-
* the exception that already existed.
|
|
2312
|
-
*/
|
|
2313
2687
|
const attemptAutomaticCommit = (
|
|
2314
2688
|
snapshot: ActiveContextSnapshot,
|
|
2315
2689
|
ctx: any,
|
|
@@ -2495,14 +2869,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2495
2869
|
return { messages: event.messages };
|
|
2496
2870
|
}
|
|
2497
2871
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
)
|
|
2502
|
-
|
|
2503
|
-
mutationAttempted = true;
|
|
2504
|
-
persistedSucceeded = true;
|
|
2505
|
-
}
|
|
2872
|
+
// NO EAGER LADDER MARKING (Shane, 2026-08-21): the rung used to stage a mark
|
|
2873
|
+
// here on every measured response, which crowded the agent out of curation
|
|
2874
|
+
// entirely -- by the time an agent looked, every eligible batch was already
|
|
2875
|
+
// marked. The commit epoch fills fresh at commit time (topUpMarks), which is
|
|
2876
|
+
// the only automatic marking there is.
|
|
2506
2877
|
} else measurements.lastProviderMeasurement = observedMeasurement;
|
|
2507
2878
|
} else {
|
|
2508
2879
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
@@ -2524,9 +2895,26 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2524
2895
|
updateStatus(ctx);
|
|
2525
2896
|
return { messages: event.messages };
|
|
2526
2897
|
}
|
|
2898
|
+
// THE FRONTIER CUTS FIRST, and it costs the projection nothing to do so: a pending
|
|
2899
|
+
// mark moves no bytes, so this runs BEFORE the band top and the fence and changes
|
|
2900
|
+
// neither of their inputs. What it changes is WHEN the agent learns a fold exists,
|
|
2901
|
+
// which is now while the material it covers is still in front of it.
|
|
2902
|
+
advanceFoldFrontier(snapshot);
|
|
2903
|
+
// BEFORE the fence, so an ordinary crossing is an ordinary commit and the fence is
|
|
2904
|
+
// left holding only the requests that genuinely will not fit.
|
|
2905
|
+
if (await enforceBandTop(snapshot, ctx)) {
|
|
2906
|
+
mutationAttempted = true;
|
|
2907
|
+
persistedSucceeded = true;
|
|
2908
|
+
try { projected = projectWithAdvisory(snapshot); }
|
|
2909
|
+
catch (error) {
|
|
2910
|
+
suspendAutomatic(error, "projection", ctx);
|
|
2911
|
+
abortUnsafeHardContext(snapshot, ctx);
|
|
2912
|
+
return { messages: event.messages };
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2527
2915
|
const budgeted = await enforceProjectionBudget(snapshot, projected, ctx);
|
|
2528
2916
|
projected = budgeted.projected;
|
|
2529
|
-
const reading = projectedTokenReading(projected);
|
|
2917
|
+
const reading = projectedTokenReading(projected, budgeted.text);
|
|
2530
2918
|
measurements.lastProjectedChars = reading.chars;
|
|
2531
2919
|
measurements.lastProjectedEstimate = reading.tokens;
|
|
2532
2920
|
measurements.lastProjectedEstimateBasis = reading.basis;
|
|
@@ -2536,7 +2924,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2536
2924
|
updateStatus(ctx);
|
|
2537
2925
|
return { messages: projected };
|
|
2538
2926
|
}
|
|
2539
|
-
noteProjection(projected);
|
|
2927
|
+
noteProjection(projected, budgeted.text);
|
|
2540
2928
|
updateStatus(ctx);
|
|
2541
2929
|
return { messages: projected };
|
|
2542
2930
|
} catch (error) {
|
|
@@ -2598,13 +2986,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2598
2986
|
if (measurements.latestRatio >= hardFenceRatio(measurement, ctx) && ladder.preparing) await ladder.preparing.promise;
|
|
2599
2987
|
if (!sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration) ||
|
|
2600
2988
|
!durableProviderMeasurementMatches(measurement)) return;
|
|
2601
|
-
|
|
2602
|
-
authoritativeSnapshotFor(ctx),
|
|
2603
|
-
measurements.latestRatio,
|
|
2604
|
-
ctx,
|
|
2605
|
-
"message-end",
|
|
2606
|
-
);
|
|
2607
|
-
if (!action && measurementStateChanged && persistence.state && persistence.persisted &&
|
|
2989
|
+
if (measurementStateChanged && persistence.state && persistence.persisted &&
|
|
2608
2990
|
!sameStateProjection(persistence.state, persistence.persisted)) {
|
|
2609
2991
|
await persistThroughActionQueue(ctx);
|
|
2610
2992
|
}
|
|
@@ -2629,6 +3011,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2629
3011
|
model: ownValue(message, "model") ?? null,
|
|
2630
3012
|
input_tokens: observation.inputTokens,
|
|
2631
3013
|
cache_read_tokens: observation.cacheReadTokens,
|
|
3014
|
+
cache_write_tokens: observation.cacheWriteTokens,
|
|
2632
3015
|
total_tokens: providerTokens(message),
|
|
2633
3016
|
projection_change: observation.change,
|
|
2634
3017
|
provider_side_miss: observation.providerSideMiss,
|
|
@@ -2672,7 +3055,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2672
3055
|
durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
|
|
2673
3056
|
startPreparation(snapshot, measurements.latestRatio, ctx);
|
|
2674
3057
|
if (measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) await ladder.preparing.promise;
|
|
2675
|
-
|
|
3058
|
+
// No rung here either: turn end stages nothing (see the context-path note).
|
|
2676
3059
|
}
|
|
2677
3060
|
ladder.pendingManual = false;
|
|
2678
3061
|
if (!ladder.automaticFailure) ladder.boundaryFailure = null;
|
|
@@ -3001,6 +3384,194 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3001
3384
|
);
|
|
3002
3385
|
};
|
|
3003
3386
|
|
|
3387
|
+
/**
|
|
3388
|
+
* THE WITHDRAWAL PATH, shared by the tool's unmark action and the editor's
|
|
3389
|
+
* withdraw intent: same refusal for an unknown id, same persist, same status.
|
|
3390
|
+
*/
|
|
3391
|
+
const withdrawMarks = async (ids: string[], ctx: any): Promise<Array<Record<string, unknown>>> => {
|
|
3392
|
+
const requested = new Set(ids);
|
|
3393
|
+
const before = pendingMarks(persistence.state!);
|
|
3394
|
+
const kept = before.filter((mark) => !requested.has(mark.id));
|
|
3395
|
+
const removed = before.filter((mark) => requested.has(mark.id));
|
|
3396
|
+
const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
|
|
3397
|
+
if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
|
|
3398
|
+
await persistManual(withPendingMarks(persistence.state!, kept), ctx);
|
|
3399
|
+
updateStatus(ctx);
|
|
3400
|
+
return removed as Array<Record<string, unknown>>;
|
|
3401
|
+
};
|
|
3402
|
+
|
|
3403
|
+
/**
|
|
3404
|
+
* THE PIN CHANGE PATH, shared by the tool's pin/unpin actions and the editor's
|
|
3405
|
+
* pin toggle: the pinned-share cap, protectEvidence, the canonical event, persist.
|
|
3406
|
+
*/
|
|
3407
|
+
const applyProtectionChange = async (
|
|
3408
|
+
snapshot: ActiveContextSnapshot,
|
|
3409
|
+
ids: string[],
|
|
3410
|
+
pin: boolean,
|
|
3411
|
+
ctx: any,
|
|
3412
|
+
): Promise<void> => {
|
|
3413
|
+
const refsBefore = persistence.state!.protected.length;
|
|
3414
|
+
const nextProtection = protectEvidence(snapshot, persistence.state!, ids, pin);
|
|
3415
|
+
if (pin) {
|
|
3416
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
3417
|
+
const prospectiveShare = capacity.budgetTokens > 0
|
|
3418
|
+
? estimatedTokens(explicitProtectedMass(snapshot, nextProtection).bytes) / capacity.budgetTokens
|
|
3419
|
+
: 0;
|
|
3420
|
+
if (prospectiveShare > MAX_PINNED_SHARE) {
|
|
3421
|
+
throw new Error(
|
|
3422
|
+
`protect refused: these pins would hold ${Math.round(prospectiveShare * 100)}% of the ` +
|
|
3423
|
+
`working window raw, past the ${Math.round(MAX_PINNED_SHARE * 100)}% pinned-share cap. ` +
|
|
3424
|
+
'Release earlier pins with {"action":"unpin","ids":["<entry-id>"]} first; ' +
|
|
3425
|
+
"folding keeps entries exactly recoverable without pinning them.");
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
await persistManual(nextProtection, ctx);
|
|
3429
|
+
emit("context.pin", {
|
|
3430
|
+
pin,
|
|
3431
|
+
ids: ids.join(","),
|
|
3432
|
+
id_count: ids.length,
|
|
3433
|
+
protected_refs_before: refsBefore,
|
|
3434
|
+
protected_refs_after: persistence.state!.protected.length,
|
|
3435
|
+
});
|
|
3436
|
+
updateStatus(ctx);
|
|
3437
|
+
};
|
|
3438
|
+
|
|
3439
|
+
/**
|
|
3440
|
+
* THE ONE STAGING PATH FOR FOLD MARKS (2026-08-23). The /fold-editor's user marks
|
|
3441
|
+
* run THIS code and nothing else: span snapping, claim refusal, brief contract,
|
|
3442
|
+
* prepareFold, replacement of a standing mark over the same span. A user mark is
|
|
3443
|
+
* mechanically an ordinary mark: it counts toward the commit's coverage, answers
|
|
3444
|
+
* with its commit arithmetic, and folds at the epoch exactly as a runtime mark does.
|
|
3445
|
+
*/
|
|
3446
|
+
const stageFoldMark = async (
|
|
3447
|
+
ids: string[],
|
|
3448
|
+
supplied: string | undefined,
|
|
3449
|
+
signal: AbortSignal,
|
|
3450
|
+
ctx: any,
|
|
3451
|
+
snapshot: ActiveContextSnapshot,
|
|
3452
|
+
): Promise<{ mark: Record<string, unknown>; corrections: SpanCorrection[]; staged: ActiveContextState }> => {
|
|
3453
|
+
const snapped = snapFoldCandidate(snapshot, persistence.state, ids, { allowProtected: true });
|
|
3454
|
+
// AN AGENT'S SPAN IS TAKEN WHOLE (Shane, 2026-08-22). The bite-size cap was
|
|
3455
|
+
// applied to manual marks too, and it was the only caller: the ladder never
|
|
3456
|
+
// splits, because `selectAutomaticChapter` stops accumulating at the cap on its
|
|
3457
|
+
// own. So the split existed solely to second-guess the one judgement the design
|
|
3458
|
+
// wants to encourage, and it charged for it twice. One intended fold became five
|
|
3459
|
+
// or ten placeholders, and every piece was handed the SAME brief, so each one
|
|
3460
|
+
// claimed to describe a span it held a fraction of, while the correction text
|
|
3461
|
+
// said "each with its own brief", which was never true.
|
|
3462
|
+
//
|
|
3463
|
+
// The cap stays for the ladder, where it was earned (2026-08-06 rep 6: one
|
|
3464
|
+
// 60,432-byte chapter hid the fact the run needed). The difference is who chose
|
|
3465
|
+
// the boundary. A ladder fold is a span nobody vouched for, so it is kept small
|
|
3466
|
+
// enough to read back cheaply. An agent fold is a span the agent cut and wrote a
|
|
3467
|
+
// brief for, and it stays navigable by a route that did not exist in 2026-08:
|
|
3468
|
+
// peek takes offset and bytes, states the omitted middle, and reads any child id,
|
|
3469
|
+
// so a large fold has a narrow read. An expand that will not fit is refused with
|
|
3470
|
+
// a peek offered instead, so size costs a refusal rather than a window.
|
|
3471
|
+
const candidate = snapped.candidate;
|
|
3472
|
+
const corrections = snapped.corrections;
|
|
3473
|
+
let staged = persistence.state!;
|
|
3474
|
+
const alreadyPrepared = staged.folds.some((fold) =>
|
|
3475
|
+
fold.id === foldIdFor(candidate.kind, candidate.parts));
|
|
3476
|
+
const deferred = refsProtected(candidate.sourceRefs, staged, snapshot);
|
|
3477
|
+
// A MARK IS EDITABLE UNTIL IT COMMITS (Shane, 2026-08-21). Marking a span you
|
|
3478
|
+
// already marked REPLACES the mark, so a brief written in haste is corrected by
|
|
3479
|
+
// writing it again: the span is identical, the id derives from the span, and no
|
|
3480
|
+
// byte has moved yet, so the correction costs nothing at all. After the commit
|
|
3481
|
+
// the fold is standing and there is no rewrite verb, because rewriting a
|
|
3482
|
+
// standing brief rewrites the projection at its placeholder; `reboundary`
|
|
3483
|
+
// re-cuts the span instead, which was going to rewrite anyway.
|
|
3484
|
+
const replacing = foldIdFor(candidate.kind, candidate.parts);
|
|
3485
|
+
const claimed = markClaimingRef(staged, candidate.sourceRefs);
|
|
3486
|
+
if (claimed && claimed.markId !== replacing) {
|
|
3487
|
+
return {
|
|
3488
|
+
mark: {
|
|
3489
|
+
id: replacing,
|
|
3490
|
+
kind: candidate.kind,
|
|
3491
|
+
ok: false,
|
|
3492
|
+
deferred: false,
|
|
3493
|
+
reason: `Evidence ${claimed.entryId} is already held by pending mark ${claimed.markId}, ` +
|
|
3494
|
+
`which folds at the next commit. Unmark ${claimed.markId} first if you want to ` +
|
|
3495
|
+
"mark this span differently.",
|
|
3496
|
+
},
|
|
3497
|
+
corrections,
|
|
3498
|
+
staged,
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
const suppliedComplaint = supplied === undefined
|
|
3502
|
+
? null
|
|
3503
|
+
: briefContractComplaint(supplied.trim(), snapshot.policy.agentBriefReserve, snapshot.toolName);
|
|
3504
|
+
if (suppliedComplaint !== null) {
|
|
3505
|
+
return {
|
|
3506
|
+
mark: {
|
|
3507
|
+
id: replacing,
|
|
3508
|
+
kind: candidate.kind,
|
|
3509
|
+
ok: false,
|
|
3510
|
+
deferred: false,
|
|
3511
|
+
reason: `Supplied brief rejected. ${suppliedComplaint}`,
|
|
3512
|
+
},
|
|
3513
|
+
corrections,
|
|
3514
|
+
staged,
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
const briefed = !deferred && !alreadyPrepared
|
|
3518
|
+
? await prepareFold({
|
|
3519
|
+
candidate,
|
|
3520
|
+
snapshot,
|
|
3521
|
+
state: staged,
|
|
3522
|
+
generation: lifecycle.generation,
|
|
3523
|
+
brief: supplied,
|
|
3524
|
+
ctx,
|
|
3525
|
+
signal,
|
|
3526
|
+
})
|
|
3527
|
+
: null;
|
|
3528
|
+
const mark = foldMarkFor({
|
|
3529
|
+
candidate,
|
|
3530
|
+
brief: briefed?.fold.brief ?? supplied ?? ladderBrief(snapshot, staged, candidate),
|
|
3531
|
+
briefProvenance: briefed?.fold.provenance ??
|
|
3532
|
+
(supplied ? { kind: "supplied" } : { kind: "deterministic" }),
|
|
3533
|
+
origin: "user",
|
|
3534
|
+
ordinal: markOrdinal(snapshot),
|
|
3535
|
+
});
|
|
3536
|
+
// Drop the standing mark over this exact span before adding, so a re-mark is a
|
|
3537
|
+
// replacement rather than a duplicate refusal. `claimed` is this same mark by the
|
|
3538
|
+
// check above, and the id is derived from the span, so the state after is the
|
|
3539
|
+
// state a first mark with the new brief would have produced.
|
|
3540
|
+
const replaced = claimed !== null;
|
|
3541
|
+
if (replaced) {
|
|
3542
|
+
staged = withPendingMarks(staged,
|
|
3543
|
+
pendingMarks(staged).filter((standing) => standing.id !== replacing));
|
|
3544
|
+
}
|
|
3545
|
+
const addition = addPendingMark(staged, mark);
|
|
3546
|
+
if (!addition.added) {
|
|
3547
|
+
return {
|
|
3548
|
+
mark: { id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason },
|
|
3549
|
+
corrections,
|
|
3550
|
+
staged,
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
staged = addition.state;
|
|
3554
|
+
return {
|
|
3555
|
+
mark: {
|
|
3556
|
+
id: mark.id,
|
|
3557
|
+
kind: mark.kind,
|
|
3558
|
+
ok: true,
|
|
3559
|
+
deferred,
|
|
3560
|
+
...(replaced ? { replaced: true } : {}),
|
|
3561
|
+
...(deferred
|
|
3562
|
+
? {
|
|
3563
|
+
scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
|
|
3564
|
+
"first commit after it ages out",
|
|
3565
|
+
}
|
|
3566
|
+
: {}),
|
|
3567
|
+
brief: mark.brief,
|
|
3568
|
+
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3569
|
+
},
|
|
3570
|
+
corrections,
|
|
3571
|
+
staged,
|
|
3572
|
+
};
|
|
3573
|
+
};
|
|
3574
|
+
|
|
3004
3575
|
const executeAction = async (
|
|
3005
3576
|
params: Record<string, unknown>,
|
|
3006
3577
|
signal: AbortSignal | undefined,
|
|
@@ -3010,6 +3581,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3010
3581
|
if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
|
|
3011
3582
|
const executionArgumentsSha256 = sha256Value(params);
|
|
3012
3583
|
const action = String(params.action ?? "");
|
|
3584
|
+
// A deleted action is refused BY NAME rather than by "not enabled": the two are
|
|
3585
|
+
// different facts, and an agent told a verb is merely off will keep offering it.
|
|
3586
|
+
if (Object.prototype.hasOwnProperty.call(RETIRED_TOOL_ACTIONS, action)) {
|
|
3587
|
+
throw new Error(RETIRED_TOOL_ACTIONS[action]);
|
|
3588
|
+
}
|
|
3013
3589
|
if (!allowedToolActionSet.has(action)) {
|
|
3014
3590
|
throw new Error(`${toolName} action '${action}' is not enabled in this runtime`);
|
|
3015
3591
|
}
|
|
@@ -3038,7 +3614,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3038
3614
|
persistence.state,
|
|
3039
3615
|
statusOffset,
|
|
3040
3616
|
statusLimit,
|
|
3041
|
-
snapshot.policy.maxFoldSourceRefs,
|
|
3042
3617
|
{ diet: !paged },
|
|
3043
3618
|
),
|
|
3044
3619
|
headroomTokens: currentCapacity(ctx).headroomTokens,
|
|
@@ -3056,7 +3631,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3056
3631
|
hardFenceRatio: hardFenceRatio(snapshot),
|
|
3057
3632
|
thresholds: { ...snapshot.thresholds },
|
|
3058
3633
|
zones: {
|
|
3059
|
-
freshBoundary: snapshot.freshBoundary,
|
|
3060
3634
|
budgetTokens: snapshot.budgetTokens,
|
|
3061
3635
|
},
|
|
3062
3636
|
responseReserve: currentCapacity(ctx).outputReservation,
|
|
@@ -3121,6 +3695,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3121
3695
|
state: persistence.state,
|
|
3122
3696
|
ratio: measurements.latestRatio,
|
|
3123
3697
|
}),
|
|
3698
|
+
// WHAT THE RUNTIME HAS CUT AND NOT YET FOLDED, by name, because the agent needs
|
|
3699
|
+
// an id to brief one and a notice it saw two turns ago is not a reference. Read
|
|
3700
|
+
// from the LIVE state rather than the durable record: a frontier cut is derived
|
|
3701
|
+
// from its span and is not written on its own, so the durable record is behind
|
|
3702
|
+
// by design and only a brief or a commit brings it forward.
|
|
3703
|
+
pendingFolds: pendingMarks(persistence.state).map((mark) => ({
|
|
3704
|
+
id: mark.id,
|
|
3705
|
+
kind: mark.kind,
|
|
3706
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
3707
|
+
briefed: mark.briefProvenance?.kind === "supplied" ||
|
|
3708
|
+
mark.briefProvenance?.kind === "augmented",
|
|
3709
|
+
brief: mark.brief ?? null,
|
|
3710
|
+
briefProvenance: mark.briefProvenance ? clone(mark.briefProvenance) : null,
|
|
3711
|
+
})),
|
|
3124
3712
|
nativeSummaries: "disabled",
|
|
3125
3713
|
instrumentation: {
|
|
3126
3714
|
enabled: true,
|
|
@@ -3152,16 +3740,53 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3152
3740
|
if (action === "peek") {
|
|
3153
3741
|
const id = String(params.id ?? "").trim();
|
|
3154
3742
|
if (!id) throw new Error("peek requires id");
|
|
3743
|
+
if (params.ephemeral !== undefined && typeof params.ephemeral !== "boolean") {
|
|
3744
|
+
throw new Error("peek ephemeral must be a boolean");
|
|
3745
|
+
}
|
|
3155
3746
|
const offset = boundedInteger(params.offset, 0, 0, 1_000_000_000, "offset");
|
|
3156
3747
|
const sliceBytes = boundedInteger(
|
|
3157
3748
|
params.bytes,
|
|
3158
3749
|
PEEK_DEFAULT_MAX_BYTES,
|
|
3159
3750
|
PEEK_MIN_SLICE_BYTES,
|
|
3160
|
-
snapshot.policy.
|
|
3751
|
+
snapshot.policy.maxSourceChars,
|
|
3161
3752
|
"bytes",
|
|
3162
3753
|
);
|
|
3163
3754
|
const target = persistence.state.folds.find((item) => item.id === id);
|
|
3164
|
-
|
|
3755
|
+
// A CLIPPED RESULT ANSWERS TO ITS ENTRY ID (2026-08-24, the tool-call diet): the
|
|
3756
|
+
// clip marker names the entry, and the full bytes are in the transcript rather
|
|
3757
|
+
// than behind a fold, so the read serves them from the mapped message directly,
|
|
3758
|
+
// through the same admission fence and the same slice bounds as a fold peek.
|
|
3759
|
+
if (!target) {
|
|
3760
|
+
const clip = (persistence.state.clips ?? []).find((item) =>
|
|
3761
|
+
item.entryId === id || item.callId === id);
|
|
3762
|
+
if (clip) {
|
|
3763
|
+
const mapped = snapshot.mapped.find((item) => item.ref?.entryId === clip.entryId);
|
|
3764
|
+
if (!mapped) throw new Error(`Clipped result ${id} is no longer on this branch`);
|
|
3765
|
+
const full = contentText(snapshot.messages[mapped.index]);
|
|
3766
|
+
admit({
|
|
3767
|
+
action: "peek",
|
|
3768
|
+
ctx,
|
|
3769
|
+
foldId: clip.entryId,
|
|
3770
|
+
requestedBytes: Math.max(0, Math.min(sliceBytes, full.length - offset)),
|
|
3771
|
+
children: [],
|
|
3772
|
+
});
|
|
3773
|
+
const slice = full.slice(offset, offset + sliceBytes);
|
|
3774
|
+
return toolPayload({
|
|
3775
|
+
id: clip.entryId,
|
|
3776
|
+
source: slice,
|
|
3777
|
+
truncated: offset + slice.length < full.length,
|
|
3778
|
+
sourceChars: full.length,
|
|
3779
|
+
offset,
|
|
3780
|
+
...(peekIsEphemeral(params) ? {
|
|
3781
|
+
ephemeral: "this result rides your context exactly until your next message; " +
|
|
3782
|
+
"extract what your current task needs into that reply, which takes over its " +
|
|
3783
|
+
"place. Peeking again is lossless and cheap; pass ephemeral false when you " +
|
|
3784
|
+
"need these bytes standing for several turns",
|
|
3785
|
+
} : { durable: "this result stays in your context like any other tool result" }),
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
throw new Error(`Unknown active-context fold ${id}`);
|
|
3789
|
+
}
|
|
3165
3790
|
admit({
|
|
3166
3791
|
action: "peek",
|
|
3167
3792
|
ctx,
|
|
@@ -3169,40 +3794,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3169
3794
|
requestedBytes: Math.max(0, Math.min(sliceBytes, target.sourceChars - offset)),
|
|
3170
3795
|
children: childFoldIds(target),
|
|
3171
3796
|
});
|
|
3172
|
-
const payload = toolPayload(
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
if (brief.length > snapshot.policy.maxBriefChars) {
|
|
3189
|
-
throw new Error(`rebrief brief must be at most ${snapshot.policy.maxBriefChars} characters`);
|
|
3190
|
-
}
|
|
3191
|
-
const fold = requireActiveFold(snapshot, persistence.state, id);
|
|
3192
|
-
const previous = foldBrief(fold, persistence.state);
|
|
3193
|
-
const briefs = { ...(persistence.state.briefs ?? {}), [id]: brief };
|
|
3194
|
-
await persistManual({ ...persistence.state, revision: persistence.state.revision + 1, briefs }, action, ctx);
|
|
3195
|
-
updateStatus(ctx);
|
|
3196
|
-
return toolPayload({
|
|
3197
|
-
version: 1,
|
|
3198
|
-
action,
|
|
3199
|
-
id,
|
|
3200
|
-
brief,
|
|
3201
|
-
previousBrief: previous,
|
|
3202
|
-
durableRevision: persistence.state.revision,
|
|
3203
|
-
activation: "the fold's placeholder and index rows now carry your brief; " +
|
|
3204
|
-
"the exact source and its fold record are untouched.",
|
|
3797
|
+
const payload = toolPayload({
|
|
3798
|
+
...peekFoldSource({
|
|
3799
|
+
foldId: id,
|
|
3800
|
+
state: persistence.state,
|
|
3801
|
+
entries: ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch(),
|
|
3802
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
3803
|
+
maximumBytes: sliceBytes,
|
|
3804
|
+
offset,
|
|
3805
|
+
toolName,
|
|
3806
|
+
}),
|
|
3807
|
+
...(peekIsEphemeral(params) ? {
|
|
3808
|
+
ephemeral: "this result rides your context exactly until your next message; " +
|
|
3809
|
+
"extract what your current task needs into that reply, which takes over its " +
|
|
3810
|
+
"place. Peeking again is lossless and cheap; pass ephemeral false when you " +
|
|
3811
|
+
"need these bytes standing for several turns",
|
|
3812
|
+
} : { durable: "this result stays in your context like any other tool result" }),
|
|
3205
3813
|
});
|
|
3814
|
+
return payload;
|
|
3206
3815
|
}
|
|
3207
3816
|
if (action === "reboundary") {
|
|
3208
3817
|
if (params.ids !== undefined) {
|
|
@@ -3230,10 +3839,18 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3230
3839
|
const recut = manualFoldCandidate(snapshot, persistence.state, snapped.ids, { allowProtected: true });
|
|
3231
3840
|
const created: Array<Record<string, unknown>> = [];
|
|
3232
3841
|
const correctedBriefs: Record<string, string> = {};
|
|
3233
|
-
for
|
|
3842
|
+
// Re-cut whole, for the same reason a mark is taken whole: the agent named this
|
|
3843
|
+
// boundary, and splitting it hands one brief to several folds that each hold a
|
|
3844
|
+
// fraction of what it describes.
|
|
3845
|
+
for (const part of [recut]) {
|
|
3234
3846
|
const durable = persistence.persistedFoldRecords.get(foldIdFor(part.kind, part.parts));
|
|
3235
3847
|
const { preparedFold, nextState } = await prepareAndCommitExplicit({
|
|
3236
|
-
|
|
3848
|
+
// A re-cut carries the durable fold's OWN brief and provenance: those words
|
|
3849
|
+
// were validated when they were first accepted, and re-judging them as a
|
|
3850
|
+
// fresh agent submission would refuse a deterministic or composed brief
|
|
3851
|
+
// against the agent's reserve.
|
|
3852
|
+
snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied,
|
|
3853
|
+
briefProvenance: durable ? durable.fold.provenance : undefined, ctx, signal,
|
|
3237
3854
|
});
|
|
3238
3855
|
persistence.state = nextState;
|
|
3239
3856
|
if (durable && supplied) correctedBriefs[preparedFold.id] = supplied;
|
|
@@ -3249,7 +3866,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3249
3866
|
briefs: { ...(persistence.state.briefs ?? {}), ...correctedBriefs },
|
|
3250
3867
|
};
|
|
3251
3868
|
}
|
|
3252
|
-
await persistManual(persistence.state,
|
|
3869
|
+
await persistManual(persistence.state, ctx);
|
|
3253
3870
|
updateStatus(ctx);
|
|
3254
3871
|
return toolPayload({
|
|
3255
3872
|
version: 1,
|
|
@@ -3287,7 +3904,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3287
3904
|
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3288
3905
|
const leases = { ...next.leases };
|
|
3289
3906
|
delete leases[id];
|
|
3290
|
-
await persistManual({ ...next, leases },
|
|
3907
|
+
await persistManual({ ...next, leases }, ctx);
|
|
3291
3908
|
updateStatus(ctx);
|
|
3292
3909
|
return toolPayload({
|
|
3293
3910
|
version: 1,
|
|
@@ -3333,11 +3950,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3333
3950
|
delete leases[id];
|
|
3334
3951
|
next = { ...next, leases };
|
|
3335
3952
|
}
|
|
3336
|
-
await persistManual(
|
|
3337
|
-
next,
|
|
3338
|
-
action,
|
|
3339
|
-
ctx,
|
|
3340
|
-
);
|
|
3953
|
+
await persistManual(next, ctx);
|
|
3341
3954
|
updateStatus(ctx);
|
|
3342
3955
|
return toolPayload({
|
|
3343
3956
|
version: 1,
|
|
@@ -3348,52 +3961,51 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3348
3961
|
activation: "durable immediately; projected on the next model call in this same turn",
|
|
3349
3962
|
});
|
|
3350
3963
|
}
|
|
3351
|
-
if (action === "
|
|
3964
|
+
if (action === "pin" || action === "unpin") {
|
|
3352
3965
|
const ids = stringIds(params.ids);
|
|
3353
|
-
|
|
3354
|
-
const
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
emit("context.protect", {
|
|
3371
|
-
protect: action === "protect",
|
|
3372
|
-
ids: ids.join(","),
|
|
3373
|
-
id_count: ids.length,
|
|
3374
|
-
protected_refs_before: refsBefore,
|
|
3375
|
-
protected_refs_after: refsAfter,
|
|
3966
|
+
await applyProtectionChange(snapshot, ids, action === "pin", ctx);
|
|
3967
|
+
const refsAfter = persistence.state!.protected.length;
|
|
3968
|
+
// A PIN IS A CLAIM ON THE WINDOW, so it answers with the same picture a mark does.
|
|
3969
|
+
// It never pays the drop, and saying so here is what stops an agent pinning its way
|
|
3970
|
+
// to a coverage it does not have.
|
|
3971
|
+
const pinCapacity = servingCapacity(snapshot.contextWindow);
|
|
3972
|
+
const pinClaims = windowClaims({
|
|
3973
|
+
snapshot,
|
|
3974
|
+
state: persistence.state,
|
|
3975
|
+
budgetTokens: pinCapacity.budgetTokens,
|
|
3976
|
+
charsPerToken: projectionCharsPerToken(),
|
|
3977
|
+
});
|
|
3978
|
+
const pinCoverage = commitCoverage({
|
|
3979
|
+
snapshot,
|
|
3980
|
+
state: persistence.state,
|
|
3981
|
+
usedTokens: pinCapacity.usedTokens,
|
|
3982
|
+
budgetTokens: pinCapacity.budgetTokens,
|
|
3376
3983
|
});
|
|
3377
|
-
updateStatus(ctx);
|
|
3378
3984
|
return toolPayload({
|
|
3379
3985
|
version: 1,
|
|
3380
3986
|
action,
|
|
3381
3987
|
ids,
|
|
3382
3988
|
protectedRefs: refsAfter,
|
|
3383
|
-
|
|
3989
|
+
pinnedTokens: pinClaims.pinnedTokens,
|
|
3990
|
+
pinnedShare: pinClaims.pinnedShare,
|
|
3991
|
+
maxPinnedShare: MAX_PINNED_SHARE,
|
|
3992
|
+
markedTowardCommitTokens: pinClaims.markedTokens,
|
|
3993
|
+
unclaimedTokens: pinClaims.unclaimedTokens,
|
|
3994
|
+
commitTargetTokens: pinCoverage.targetTokens,
|
|
3995
|
+
unmarkedAgainstCommitTokens: pinCoverage.remainingTokens,
|
|
3996
|
+
activation: action === "pin"
|
|
3997
|
+
? "durable immediately; projected on the next model call in this same turn. " +
|
|
3998
|
+
"Pinned entries stay RAW and are never folded, automatically or by a mark, so " +
|
|
3999
|
+
"this is how you keep a span expanded. It frees nothing: the next commit still " +
|
|
4000
|
+
"has to reach its drop from what is left, so pinned mass makes the rest of the " +
|
|
4001
|
+
"window fold sooner rather than later."
|
|
4002
|
+
: "durable immediately; projected on the next model call in this same turn. The " +
|
|
4003
|
+
"released entries are foldable again and rejoin the next commit's candidates.",
|
|
3384
4004
|
});
|
|
3385
4005
|
}
|
|
3386
4006
|
if (action === "unmark") {
|
|
3387
|
-
const
|
|
3388
|
-
const
|
|
3389
|
-
const before = pendingMarks(persistence.state);
|
|
3390
|
-
const kept = before.filter((mark) => !requested.has(mark.id));
|
|
3391
|
-
const removed = before.filter((mark) => requested.has(mark.id));
|
|
3392
|
-
const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
|
|
3393
|
-
if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
|
|
3394
|
-
await persistManual(withPendingMarks(persistence.state, kept), action, ctx);
|
|
3395
|
-
updateStatus(ctx);
|
|
3396
|
-
const accounting = markAccounting(snapshot, persistence.state);
|
|
4007
|
+
const removed = await withdrawMarks(stringIds(params.ids), ctx);
|
|
4008
|
+
const accounting = markAccounting(snapshot, persistence.state!);
|
|
3397
4009
|
return toolPayload({
|
|
3398
4010
|
version: 1,
|
|
3399
4011
|
action,
|
|
@@ -3405,172 +4017,168 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3405
4017
|
activation: "the withdrawn marks will never be applied; no context bytes moved",
|
|
3406
4018
|
});
|
|
3407
4019
|
}
|
|
3408
|
-
if (action === "
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
? [
|
|
3426
|
-
...snapped.corrections,
|
|
3427
|
-
...(parts.length > 1
|
|
3428
|
-
? [{
|
|
3429
|
-
from: request.ids,
|
|
3430
|
-
to: [],
|
|
3431
|
-
reason: `span was ${spanChars} chars, over the ${MAX_FOLD_SPAN_CHARS}-char bite-size cap; ` +
|
|
3432
|
-
`split into ${parts.length} sequential folds, each with its own brief`,
|
|
3433
|
-
}]
|
|
3434
|
-
: []),
|
|
3435
|
-
]
|
|
3436
|
-
: [],
|
|
3437
|
-
splitFrom: parts.length > 1 && index === 0 ? spanChars : 0,
|
|
3438
|
-
});
|
|
3439
|
-
}
|
|
3440
|
-
if (parts.length > 1) {
|
|
3441
|
-
emit("context.split", {
|
|
3442
|
-
source: "agent",
|
|
3443
|
-
span_chars: spanChars,
|
|
3444
|
-
parts: parts.length,
|
|
3445
|
-
cap_chars: MAX_FOLD_SPAN_CHARS,
|
|
3446
|
-
fold_ids: "",
|
|
3447
|
-
});
|
|
3448
|
-
}
|
|
4020
|
+
if (action === "brief") {
|
|
4021
|
+
// THE ONE EDIT THAT IS FREE. A pending mark is not in the window, so writing its
|
|
4022
|
+
// brief moves no bytes and costs no prefix cache; the brief reaches the projection
|
|
4023
|
+
// only when the commit writes that placeholder for the first time. A STANDING fold
|
|
4024
|
+
// is the opposite case and is refused by name: its placeholder is already in the
|
|
4025
|
+
// window, and rewriting it breaks the cache from that point on, which is the
|
|
4026
|
+
// rebrief deletion of 2026-08-21 read from the other side.
|
|
4027
|
+
const id = String(params.id ?? "").trim();
|
|
4028
|
+
if (!id) throw new Error("brief needs the id of a pending fold");
|
|
4029
|
+
const standing = persistence.state!.folds.find((fold) => fold.id === id);
|
|
4030
|
+
if (standing) {
|
|
4031
|
+
throw new Error(
|
|
4032
|
+
`${id} is a standing fold, not a pending one, so its brief is already in your ` +
|
|
4033
|
+
"window and rewriting it would rewrite the projection from that placeholder on. " +
|
|
4034
|
+
`Read it with ${toolName} {"action":"peek","id":"${id}"}, or re-cut the span with ` +
|
|
4035
|
+
`${toolName} {"action":"reboundary","ids":[...]} if the boundary is wrong.`,
|
|
4036
|
+
);
|
|
3449
4037
|
}
|
|
3450
|
-
const
|
|
3451
|
-
const
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
toolRefsProtected(candidate.sourceRefs, staged, snapshot));
|
|
3460
|
-
const suppliedComplaint = item.brief === undefined
|
|
3461
|
-
? null
|
|
3462
|
-
: briefContractComplaint(item.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
|
|
3463
|
-
if (suppliedComplaint !== null) {
|
|
3464
|
-
marks.push({
|
|
3465
|
-
id: foldIdFor(candidate.kind, candidate.parts),
|
|
3466
|
-
kind: candidate.kind,
|
|
3467
|
-
ok: false,
|
|
3468
|
-
deferred: false,
|
|
3469
|
-
reason: `Supplied brief rejected. ${suppliedComplaint}`,
|
|
3470
|
-
});
|
|
3471
|
-
continue;
|
|
3472
|
-
}
|
|
3473
|
-
const briefed = !deferred && !alreadyPrepared
|
|
3474
|
-
? await prepareFold({
|
|
3475
|
-
candidate,
|
|
3476
|
-
snapshot,
|
|
3477
|
-
state: staged,
|
|
3478
|
-
generation: lifecycle.generation,
|
|
3479
|
-
brief: item.brief,
|
|
3480
|
-
ctx,
|
|
3481
|
-
signal,
|
|
3482
|
-
})
|
|
3483
|
-
: null;
|
|
3484
|
-
const mark = foldMarkFor({
|
|
3485
|
-
candidate,
|
|
3486
|
-
brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
|
|
3487
|
-
briefProvenance: briefed?.fold.provenance ??
|
|
3488
|
-
(item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
|
|
3489
|
-
origin: "agent",
|
|
3490
|
-
ordinal: markOrdinal(snapshot),
|
|
3491
|
-
});
|
|
3492
|
-
const addition = addPendingMark(staged, mark);
|
|
3493
|
-
if (!addition.added) {
|
|
3494
|
-
marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
|
|
3495
|
-
continue;
|
|
3496
|
-
}
|
|
3497
|
-
staged = addition.state;
|
|
3498
|
-
marks.push({
|
|
3499
|
-
id: mark.id,
|
|
3500
|
-
kind: mark.kind,
|
|
3501
|
-
ok: true,
|
|
3502
|
-
deferred,
|
|
3503
|
-
...(deferred
|
|
3504
|
-
? {
|
|
3505
|
-
scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
|
|
3506
|
-
"first commit after it ages out",
|
|
3507
|
-
}
|
|
3508
|
-
: {}),
|
|
3509
|
-
brief: mark.brief,
|
|
3510
|
-
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3511
|
-
});
|
|
4038
|
+
const held = pendingMarks(persistence.state!);
|
|
4039
|
+
const target = held.find((mark) => mark.id === id);
|
|
4040
|
+
if (!target) {
|
|
4041
|
+
const names = held.slice(0, 5).map((mark) => mark.id).join(", ");
|
|
4042
|
+
throw new Error(
|
|
4043
|
+
`No pending fold ${id}. ${held.length
|
|
4044
|
+
? `The folds waiting for a brief are: ${names}${held.length > 5 ? ", ..." : ""}.`
|
|
4045
|
+
: "Nothing is pending right now, so there is nothing to brief."}`,
|
|
4046
|
+
);
|
|
3512
4047
|
}
|
|
3513
|
-
|
|
4048
|
+
const text = String(params.brief ?? "").trim();
|
|
4049
|
+
const complaint = briefContractComplaint(text, snapshot.policy.agentBriefReserve, snapshot.toolName);
|
|
4050
|
+
if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
|
|
4051
|
+
const nextState = {
|
|
4052
|
+
...persistence.state!,
|
|
4053
|
+
revision: persistence.state!.revision + 1,
|
|
4054
|
+
pendingMarks: held.map((mark) => mark.id === id
|
|
4055
|
+
? { ...mark, brief: text, briefProvenance: { kind: "supplied" as const } }
|
|
4056
|
+
: mark),
|
|
4057
|
+
};
|
|
4058
|
+
persistence.state = nextState;
|
|
4059
|
+
await persistManual(nextState, ctx);
|
|
3514
4060
|
updateStatus(ctx);
|
|
3515
|
-
|
|
3516
|
-
const
|
|
3517
|
-
|
|
3518
|
-
kind: mark.kind,
|
|
3519
|
-
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
3520
|
-
}));
|
|
3521
|
-
const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
|
|
3522
|
-
const only = marks.length === 1 && marks[0].ok === true ? marks[0] : null;
|
|
4061
|
+
emit("context.brief", { id, chars: text.length, origin: "agent" });
|
|
4062
|
+
const accounting = markAccounting(snapshot, persistence.state!);
|
|
4063
|
+
const remainder = unmarkedRemainder(snapshot, persistence.state!, projectionCharsPerToken());
|
|
3523
4064
|
return toolPayload({
|
|
3524
4065
|
version: 1,
|
|
3525
4066
|
action,
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
? {
|
|
3531
|
-
id: only.id,
|
|
3532
|
-
kind: only.kind,
|
|
3533
|
-
brief: only.brief,
|
|
3534
|
-
provenance: only.provenance,
|
|
3535
|
-
deferred: only.deferred,
|
|
3536
|
-
...(only.scheduled ? { scheduled: only.scheduled } : {}),
|
|
3537
|
-
}
|
|
3538
|
-
: {}),
|
|
3539
|
-
marks,
|
|
3540
|
-
corrections,
|
|
4067
|
+
id,
|
|
4068
|
+
kind: target.kind,
|
|
4069
|
+
brief: text,
|
|
4070
|
+
provenance: "agent",
|
|
3541
4071
|
argumentsSha256: executionArgumentsSha256,
|
|
3542
|
-
durableRevision: persistence.state
|
|
4072
|
+
durableRevision: persistence.state!.revision,
|
|
3543
4073
|
pendingMarks: accounting.pending,
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
estimatedEligibleWindowShare: accounting.eligibleFreedBudgetShare,
|
|
3548
|
-
estimatedRewriteTokens: accounting.rewriteTokens,
|
|
3549
|
-
held,
|
|
3550
|
-
heldNote: "held until it ages out or the next fold event",
|
|
4074
|
+
unbriefed: pendingMarks(persistence.state!)
|
|
4075
|
+
.filter((mark) => mark.briefProvenance?.kind !== "supplied" &&
|
|
4076
|
+
mark.briefProvenance?.kind !== "augmented").map((mark) => mark.id),
|
|
3551
4077
|
unmarkedSpans: remainder.spans,
|
|
3552
4078
|
unmarkedTokens: remainder.tokens,
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
4079
|
+
activation: "durable immediately, and it costs your window nothing: this fold is " +
|
|
4080
|
+
"pending, so the brief you just wrote is stored outside the projection and " +
|
|
4081
|
+
"reaches it only when the commit folds this span. Nothing in your context moved.",
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
4084
|
+
if (action === "remember") {
|
|
4085
|
+
// THE DIGEST'S OWN CHANNEL. A working-memory write is durable state outside the
|
|
4086
|
+
// projection, so like `brief` it moves no bytes and costs no prefix cache: the
|
|
4087
|
+
// table of contents the projection carries refreshes at the next commit, and this
|
|
4088
|
+
// result carries the live key list in the meantime.
|
|
4089
|
+
const key = String(params.key ?? "").trim();
|
|
4090
|
+
if (!key) throw new Error("remember needs a key");
|
|
4091
|
+
if (key.length > MEMORY_KEY_CHARS) {
|
|
4092
|
+
throw new Error(`Working-memory keys have a hard ${MEMORY_KEY_CHARS}-character ` +
|
|
4093
|
+
"maximum: the key is a table-of-contents line, not the note. Put the detail " +
|
|
4094
|
+
"in the body.");
|
|
4095
|
+
}
|
|
4096
|
+
const body = String(params.body ?? "").trim();
|
|
4097
|
+
if (body.length > MEMORY_BODY_CHARS) {
|
|
4098
|
+
throw new Error(`Working-memory bodies have a hard ${MEMORY_BODY_CHARS}-character ` +
|
|
4099
|
+
`maximum and this one is ${body.length}. Split the entry, or keep the part ` +
|
|
4100
|
+
"that changes and let the transcript carry the rest: folded material stays " +
|
|
4101
|
+
"losslessly recoverable by peek.");
|
|
4102
|
+
}
|
|
4103
|
+
const held = persistence.state!.memory ?? [];
|
|
4104
|
+
const existing = held.find((entry) => entry.key === key);
|
|
4105
|
+
let nextMemory: WorkingMemoryEntry[];
|
|
4106
|
+
if (!body) {
|
|
4107
|
+
if (!existing) {
|
|
4108
|
+
throw new Error(`No working-memory entry '${key}' to remove. ${held.length
|
|
4109
|
+
? `The keys are: ${held.map((entry) => entry.key).join(", ")}.`
|
|
4110
|
+
: "Working memory is empty."}`);
|
|
4111
|
+
}
|
|
4112
|
+
nextMemory = held.filter((entry) => entry.key !== key);
|
|
4113
|
+
} else {
|
|
4114
|
+
if (!existing && held.length >= MEMORY_KEYS_MAX) {
|
|
4115
|
+
throw new Error(`Working memory holds its maximum of ${MEMORY_KEYS_MAX} ` +
|
|
4116
|
+
"entries. Merge two entries, or remove one whose facts no longer matter " +
|
|
4117
|
+
`(${toolName} {"action":"remember","key":"<key>","body":""}), then write ` +
|
|
4118
|
+
"this one.");
|
|
4119
|
+
}
|
|
4120
|
+
const ordinal = held.reduce((max, entry) => Math.max(max, entry.ordinal), 0) + 1;
|
|
4121
|
+
nextMemory = existing
|
|
4122
|
+
? held.map((entry) => entry.key === key ? { key, body, ordinal } : entry)
|
|
4123
|
+
: [...held, { key, body, ordinal }];
|
|
4124
|
+
}
|
|
4125
|
+
const nextState = {
|
|
4126
|
+
...persistence.state!,
|
|
4127
|
+
revision: persistence.state!.revision + 1,
|
|
4128
|
+
...(nextMemory.length ? { memory: nextMemory } : {}),
|
|
4129
|
+
};
|
|
4130
|
+
if (!nextMemory.length) delete (nextState as Partial<ActiveContextState>).memory;
|
|
4131
|
+
persistence.state = nextState;
|
|
4132
|
+
await persistManual(nextState, ctx);
|
|
4133
|
+
updateStatus(ctx);
|
|
4134
|
+
const toc = [...nextMemory].sort((left, right) => right.ordinal - left.ordinal)
|
|
4135
|
+
.map((entry) => ({ key: entry.key, chars: entry.body.length }));
|
|
4136
|
+
return toolPayload({
|
|
4137
|
+
version: 1,
|
|
4138
|
+
action,
|
|
4139
|
+
key,
|
|
4140
|
+
...(body ? { chars: body.length } : { removed: true }),
|
|
4141
|
+
argumentsSha256: executionArgumentsSha256,
|
|
4142
|
+
durableRevision: persistence.state!.revision,
|
|
4143
|
+
entries: toc,
|
|
4144
|
+
activation: body
|
|
4145
|
+
? "durable immediately, and it costs your window nothing: the body lives in " +
|
|
4146
|
+
"state, this list is the live view, and the table of contents in your " +
|
|
4147
|
+
"context refreshes at the next commit."
|
|
4148
|
+
: "removed. The table of contents in your context refreshes at the next commit.",
|
|
4149
|
+
});
|
|
4150
|
+
}
|
|
4151
|
+
if (action === "recall") {
|
|
4152
|
+
const held = persistence.state!.memory ?? [];
|
|
4153
|
+
if (!held.length) {
|
|
4154
|
+
throw new Error(`Working memory is empty. Write an entry with ${toolName} ` +
|
|
4155
|
+
'{"action":"remember","key":"<key>","body":"..."}.');
|
|
4156
|
+
}
|
|
4157
|
+
const requested = params.keys === undefined && params.key === undefined
|
|
4158
|
+
? null
|
|
4159
|
+
: (denseOwnArrayValues(params.keys) ?? [params.key ?? params.keys])
|
|
4160
|
+
.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
4161
|
+
const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
|
|
4162
|
+
const chosen = requested === null
|
|
4163
|
+
? entries
|
|
4164
|
+
: entries.filter((entry) => requested.includes(entry.key));
|
|
4165
|
+
if (requested !== null) {
|
|
4166
|
+
const missing = requested.filter((key) => !held.some((entry) => entry.key === key));
|
|
4167
|
+
if (missing.length) {
|
|
4168
|
+
throw new Error(`No working-memory entr${missing.length === 1 ? "y" : "ies"} ` +
|
|
4169
|
+
`${missing.join(", ")}. The keys are: ` +
|
|
4170
|
+
`${entries.map((entry) => entry.key).join(", ")}.`);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return toolPayload({
|
|
4174
|
+
version: 1,
|
|
4175
|
+
action,
|
|
4176
|
+
argumentsSha256: executionArgumentsSha256,
|
|
4177
|
+
entries: chosen.map((entry) => ({ key: entry.key, chars: entry.body.length, body: entry.body })),
|
|
3564
4178
|
});
|
|
3565
4179
|
}
|
|
3566
4180
|
throw new Error(`Unknown ${toolName} action '${action}'`);
|
|
3567
4181
|
};
|
|
3568
|
-
const requestedMarkCount = (params: Record<string, unknown>): number => {
|
|
3569
|
-
const batched = denseOwnArrayValues(params?.marks);
|
|
3570
|
-
if (batched) return batched.length;
|
|
3571
|
-
return params?.ids === undefined && params?.id === undefined ? 0 : 1;
|
|
3572
|
-
};
|
|
3573
|
-
|
|
3574
4182
|
const toolHandler = async (
|
|
3575
4183
|
toolCallId: string,
|
|
3576
4184
|
params: Record<string, unknown>,
|
|
@@ -3587,9 +4195,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3587
4195
|
ok,
|
|
3588
4196
|
error,
|
|
3589
4197
|
tool_call_id: toolCallId,
|
|
3590
|
-
marks_requested: requestedMarkCount(params),
|
|
3591
4198
|
corrections_applied: corrections.length,
|
|
3592
4199
|
arguments_sha256: sha256Value(params ?? {}),
|
|
4200
|
+
...(action === "peek" ? { ephemeral: peekIsEphemeral(params) } : {}),
|
|
3593
4201
|
});
|
|
3594
4202
|
for (const correction of corrections) {
|
|
3595
4203
|
const from = denseOwnArrayValues(ownValue(correction, "from")) ?? [];
|
|
@@ -3609,6 +4217,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3609
4217
|
const result = await executeAction(params, signal, ctx);
|
|
3610
4218
|
const corrections = denseOwnArrayValues(ownValue(ownValue(result, "details"), "corrections"));
|
|
3611
4219
|
attempt(true, null, (corrections ?? []) as Array<Record<string, unknown>>);
|
|
4220
|
+
if (action === "peek" && peekIsEphemeral(params)) ephemeralPeeks.add(toolCallId);
|
|
3612
4221
|
return result;
|
|
3613
4222
|
} catch (error) {
|
|
3614
4223
|
attempt(false, error instanceof Error ? error.message : String(error), []);
|
|
@@ -3642,7 +4251,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3642
4251
|
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
3643
4252
|
const divider = args.indexOf(" -- ");
|
|
3644
4253
|
const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
|
|
3645
|
-
|
|
4254
|
+
// BARE /fold IS THE COMMIT (Shane, dogfooding 2026-08-22): staging is the
|
|
4255
|
+
// normal accumulation and applying it is the default verb. The old bare behavior
|
|
4256
|
+
// rescue-folded an auto-offered chapter, which read as the command ignoring the
|
|
4257
|
+
// staged pile; a rescue now needs explicit ids.
|
|
4258
|
+
if (!selector || selector === "commit") {
|
|
3646
4259
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
3647
4260
|
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
3648
4261
|
? capacity.usedTokens / capacity.budgetTokens
|
|
@@ -3665,16 +4278,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3665
4278
|
committed
|
|
3666
4279
|
? `Committed ${ownValue(committed, "applied_marks") ?? 0} mark(s)${topUp ? " with automatic top-up" : ""}. ` +
|
|
3667
4280
|
"Exact source remains expandable."
|
|
3668
|
-
: "Nothing
|
|
4281
|
+
: "Nothing to commit: no staged mark could apply (pinned or already-folded spans hold).",
|
|
3669
4282
|
"info",
|
|
3670
4283
|
);
|
|
3671
4284
|
return;
|
|
3672
4285
|
}
|
|
3673
4286
|
const supplied = (divider >= 0 ? args.slice(divider + 4) : "").trim() || undefined;
|
|
3674
4287
|
const ids = selector ? selector.replace(/\.\./g, " ").split(/[\s,]+/).filter(Boolean) : [];
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
4288
|
+
if (!ids.length) {
|
|
4289
|
+
throw new Error(
|
|
4290
|
+
"Nothing to rescue without ids: /fold with no arguments commits every staged mark; " +
|
|
4291
|
+
"name the span to fold now (/fold <start-id> [end-id] -- brief)",
|
|
4292
|
+
);
|
|
4293
|
+
}
|
|
4294
|
+
const candidate = manualFoldCandidate(snapshot, persistence.state!, ids);
|
|
3678
4295
|
if (!candidate) throw new Error("No exact stale rescue span is currently eligible");
|
|
3679
4296
|
const stateBefore = persistence.state!;
|
|
3680
4297
|
const persistedBefore = persistence.persisted ? clone(persistence.persisted) : null;
|
|
@@ -3721,12 +4338,199 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3721
4338
|
}
|
|
3722
4339
|
};
|
|
3723
4340
|
|
|
4341
|
+
/**
|
|
4342
|
+
* /fold-editor, V2.2. The handler assembles a plain data snapshot of the working
|
|
4343
|
+
* window and hands it to the view; the view holds no runtime reference and no
|
|
4344
|
+
* mutation path. What changed in V2.2: raw entries are MARK POINTS, and a span
|
|
4345
|
+
* laid down between two of them stages through `stageFoldMarks` -- the SAME
|
|
4346
|
+
* validated path the tool's fold action runs -- with origin "user". The view
|
|
4347
|
+
* computes nothing but the arithmetic it is handed (spanCost) and reports one
|
|
4348
|
+
* intent (onStageMark); this handler owns every state transition.
|
|
4349
|
+
*/
|
|
4350
|
+
const foldEditorCommandHandler = async (_args: string, ctx: any): Promise<void> => {
|
|
4351
|
+
if (typeof ctx.ui?.custom !== "function") {
|
|
4352
|
+
throw new Error("/fold-editor needs an interactive UI; use /fold-status for the text form");
|
|
4353
|
+
}
|
|
4354
|
+
const snapshot = lifecycle.latestSnapshot
|
|
4355
|
+
? authoritativeSnapshotFor(ctx)
|
|
4356
|
+
: snapshotForEvent(ctx, ctx.sessionManager.buildSessionContext().messages);
|
|
4357
|
+
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
4358
|
+
if (!persistence.state) throw new Error("The context window is empty; there is nothing to show yet");
|
|
4359
|
+
|
|
4360
|
+
// THE WOULD-BE FOLD SIZE (Shane, 2026-08-23). The footer's number while a span is
|
|
4361
|
+
// being laid down is the runtime's own estimate -- span bytes over the mapped
|
|
4362
|
+
// window at ESTIMATED_BYTES_PER_TOKEN -- so what the user sees before staging is
|
|
4363
|
+
// what the staged mark answers with after it.
|
|
4364
|
+
const spanCost = (from: number, to: number) => ({
|
|
4365
|
+
entries: Math.abs(to - from) + 1,
|
|
4366
|
+
tokens: estimatedTokens(spanBytes(snapshot, Math.min(from, to), Math.max(from, to) + 1)),
|
|
4367
|
+
});
|
|
4368
|
+
|
|
4369
|
+
// Marks move no bytes until the commit, so window geometry cannot change while
|
|
4370
|
+
// the editor holds it open; only pending state grows. Rebuilding against the SAME
|
|
4371
|
+
// snapshot with fresh state keeps PROPOSED rows honest without re-deriving indices.
|
|
4372
|
+
const buildData = () => {
|
|
4373
|
+
const state = persistence.state!;
|
|
4374
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
4375
|
+
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
4376
|
+
? capacity.usedTokens / capacity.budgetTokens
|
|
4377
|
+
: null;
|
|
4378
|
+
const accounting = markAccounting(snapshot, state);
|
|
4379
|
+
return {
|
|
4380
|
+
title: `pi-fold context editor · ${snapshot.sessionId.slice(0, 8)}`,
|
|
4381
|
+
occupancy: {
|
|
4382
|
+
usedTokens: capacity.usedTokens,
|
|
4383
|
+
budgetTokens: capacity.budgetTokens,
|
|
4384
|
+
commitOccupancy: thresholds.maxTarget,
|
|
4385
|
+
commitDue: occupancy !== null && occupancy >= thresholds.maxTarget,
|
|
4386
|
+
},
|
|
4387
|
+
blocks: buildFoldEditorData(snapshot, state, {
|
|
4388
|
+
foldRows: () => (state.folds ?? []).map((fold: any) => {
|
|
4389
|
+
const row = foldIndexRow(fold, state, snapshot);
|
|
4390
|
+
const entries = flattenFoldRefs(fold, state)
|
|
4391
|
+
.map((ref) => {
|
|
4392
|
+
const item = exactMapped(snapshot, ref);
|
|
4393
|
+
const message = item?.message as Record<string, unknown> | undefined;
|
|
4394
|
+
const role = String(item?.ref?.role ?? message?.role ?? "entry");
|
|
4395
|
+
const content = message?.content;
|
|
4396
|
+
let preview = "";
|
|
4397
|
+
if (typeof content === "string") preview = content.slice(0, 300);
|
|
4398
|
+
else if (Array.isArray(content)) {
|
|
4399
|
+
const firstText = content.find((part) => part?.type === "text");
|
|
4400
|
+
preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
|
|
4401
|
+
}
|
|
4402
|
+
return { id: ref.entryId, role, preview: preview.replace(/\s+/g, " ") };
|
|
4403
|
+
});
|
|
4404
|
+
return {
|
|
4405
|
+
id: String(row.id),
|
|
4406
|
+
kind: String(row.kind ?? ""),
|
|
4407
|
+
brief: String(row.brief ?? ""),
|
|
4408
|
+
sourceCount: Number(row.sourceCount ?? 0),
|
|
4409
|
+
startPosition: Number(row.startPosition),
|
|
4410
|
+
endPosition: Number(row.endPosition),
|
|
4411
|
+
entries,
|
|
4412
|
+
};
|
|
4413
|
+
}),
|
|
4414
|
+
pendingMarkRefs: () => pendingMarks(state)
|
|
4415
|
+
.filter((mark) => mark.mark === "fold")
|
|
4416
|
+
.map((mark) => ({
|
|
4417
|
+
id: mark.id,
|
|
4418
|
+
origin: String(mark.origin),
|
|
4419
|
+
brief: String(mark.brief ?? ""),
|
|
4420
|
+
// WHAT THE STAGED MARK FREES, so a PROPOSED row states its size the
|
|
4421
|
+
// way the footer prices an unstaged span.
|
|
4422
|
+
...(typeof markFreedBytes(snapshot, state, mark) === "number"
|
|
4423
|
+
? { tokens: estimatedTokens(markFreedBytes(snapshot, state, mark)) }
|
|
4424
|
+
: {}),
|
|
4425
|
+
entryIds: markSpanRefs(state, mark).refs
|
|
4426
|
+
.map((ref) => ref.entryId)
|
|
4427
|
+
.filter((entryId): entryId is string => Boolean(entryId)),
|
|
4428
|
+
})),
|
|
4429
|
+
mappedRange: (from: number, to: number) => {
|
|
4430
|
+
const out: Array<{ id: string; role: string; preview: string; index?: number }> = [];
|
|
4431
|
+
for (let index = from; index <= to; index += 1) {
|
|
4432
|
+
const item = snapshot.mapped[index];
|
|
4433
|
+
if (!item) continue;
|
|
4434
|
+
const message = item.message as Record<string, unknown> | undefined;
|
|
4435
|
+
const role = String(item.ref?.role ?? message?.role ?? "entry");
|
|
4436
|
+
const content = message?.content;
|
|
4437
|
+
// 300 chars, same as fold entries: the view shows 48 on the row and
|
|
4438
|
+
// deepens to 240 on Enter, so a 48-char preview made detail a no-op.
|
|
4439
|
+
let preview = "";
|
|
4440
|
+
if (typeof content === "string") preview = content.slice(0, 300);
|
|
4441
|
+
else if (Array.isArray(content)) {
|
|
4442
|
+
const firstText = content.find((part) => part?.type === "text");
|
|
4443
|
+
preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
|
|
4444
|
+
}
|
|
4445
|
+
// THE INDEX IS THE MARK POINT: raw entries carry their mapped position so
|
|
4446
|
+
// the view can price a span and name its two boundary ids.
|
|
4447
|
+
out.push({
|
|
4448
|
+
id: item.ref?.entryId ?? String(index),
|
|
4449
|
+
role,
|
|
4450
|
+
preview: preview.replace(/\s+/g, " "),
|
|
4451
|
+
...(item.ref?.entryId ? { index } : {}),
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4454
|
+
return out;
|
|
4455
|
+
},
|
|
4456
|
+
entryCount: snapshot.mapped.length,
|
|
4457
|
+
}),
|
|
4458
|
+
pending: {
|
|
4459
|
+
count: pendingMarks(state).length,
|
|
4460
|
+
agentMarks: accounting.agentMarks,
|
|
4461
|
+
ladderMarks: accounting.ladderMarks,
|
|
4462
|
+
userMarks: accounting.userMarks,
|
|
4463
|
+
freedTokens: accounting.freedTokens,
|
|
4464
|
+
},
|
|
4465
|
+
pinned: state.protected.map((ref) => ref.entryId),
|
|
4466
|
+
};
|
|
4467
|
+
};
|
|
4468
|
+
|
|
4469
|
+
let view: FoldEditorView | null = null;
|
|
4470
|
+
const runQueued = async (work: () => Promise<void>): Promise<void> => {
|
|
4471
|
+
const operation = ladder.actionQueue.then(work);
|
|
4472
|
+
ladder.actionQueue = operation.catch(() => undefined);
|
|
4473
|
+
await operation;
|
|
4474
|
+
};
|
|
4475
|
+
const refreshAfter = (): void => {
|
|
4476
|
+
if (view) view.refresh(buildData());
|
|
4477
|
+
};
|
|
4478
|
+
const editorActions = {
|
|
4479
|
+
spanCost,
|
|
4480
|
+
onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
|
|
4481
|
+
await runQueued(async () => {
|
|
4482
|
+
const fresh = authoritativeSnapshotFor(ctx);
|
|
4483
|
+
const { mark, staged } = await stageFoldMark(
|
|
4484
|
+
[fromId, toId], brief && brief.trim() ? brief.trim() : undefined,
|
|
4485
|
+
new AbortController().signal, ctx, fresh);
|
|
4486
|
+
// Refuse BEFORE persisting: a refused mark leaves state untouched and
|
|
4487
|
+
// persisting it would be a no-op write against the same-state projection.
|
|
4488
|
+
if (mark.ok !== true) throw new Error(String(mark.reason ?? "the span was refused"));
|
|
4489
|
+
await persistManual(staged, ctx);
|
|
4490
|
+
updateStatus(ctx);
|
|
4491
|
+
});
|
|
4492
|
+
safeNotify(ctx, "Staged user mark(s); they apply at the next commit epoch.", "info");
|
|
4493
|
+
refreshAfter();
|
|
4494
|
+
},
|
|
4495
|
+
onWithdrawMark: async (markId: string): Promise<void> => {
|
|
4496
|
+
let removed: Array<Record<string, unknown>> = [];
|
|
4497
|
+
await runQueued(async () => {
|
|
4498
|
+
removed = await withdrawMarks([markId], ctx) as Array<Record<string, unknown>>;
|
|
4499
|
+
});
|
|
4500
|
+
safeNotify(ctx,
|
|
4501
|
+
`Withdrew ${removed.length} staged mark(s); no context bytes moved.`, "info");
|
|
4502
|
+
refreshAfter();
|
|
4503
|
+
},
|
|
4504
|
+
onTogglePin: async (entryId: string): Promise<void> => {
|
|
4505
|
+
const pinnedAlready = persistence.state!.protected
|
|
4506
|
+
.some((ref: any) => ref.entryId === entryId);
|
|
4507
|
+
await runQueued(async () => {
|
|
4508
|
+
await applyProtectionChange(authoritativeSnapshotFor(ctx), [entryId], !pinnedAlready, ctx);
|
|
4509
|
+
});
|
|
4510
|
+
safeNotify(ctx, pinnedAlready
|
|
4511
|
+
? "Unpinned: the released entries rejoin the next commit's candidates."
|
|
4512
|
+
: "Pinned raw: held through every fold; frees nothing toward the drop.", "info");
|
|
4513
|
+
refreshAfter();
|
|
4514
|
+
},
|
|
4515
|
+
};
|
|
4516
|
+
await ctx.ui.custom((
|
|
4517
|
+
_tui: unknown,
|
|
4518
|
+
theme: any,
|
|
4519
|
+
keybindings: { matches(data: string, action: string): boolean } | null,
|
|
4520
|
+
done: () => void,
|
|
4521
|
+
) => {
|
|
4522
|
+
view = new FoldEditorView(buildData(), done, keybindings, theme, editorActions);
|
|
4523
|
+
return view;
|
|
4524
|
+
});
|
|
4525
|
+
updateStatus(ctx);
|
|
4526
|
+
};
|
|
4527
|
+
|
|
3724
4528
|
pi.registerTool(buildActiveContextTool({
|
|
3725
4529
|
name: toolName,
|
|
3726
4530
|
label: toolLabel,
|
|
3727
4531
|
allowedActions: allowedToolActions,
|
|
3728
4532
|
fullSurface: true,
|
|
3729
|
-
maxBriefChars: ACTIVE_CONTEXT_POLICY.
|
|
4533
|
+
maxBriefChars: ACTIVE_CONTEXT_POLICY.agentBriefReserve,
|
|
3730
4534
|
statusDetails: ["fold_candidates", "tree", "folds", "objects"],
|
|
3731
4535
|
minPeekSliceBytes: PEEK_MIN_SLICE_BYTES,
|
|
3732
4536
|
defaultPeekBytes: PEEK_DEFAULT_MAX_BYTES,
|
|
@@ -3735,8 +4539,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3735
4539
|
for (const command of buildActiveContextCommands({
|
|
3736
4540
|
statusName: commandNames.status,
|
|
3737
4541
|
foldName: commandNames.fold,
|
|
4542
|
+
editorName: "fold-editor",
|
|
3738
4543
|
statusHandler: statusCommandHandler,
|
|
3739
4544
|
foldHandler: foldCommandHandler,
|
|
4545
|
+
editorHandler: foldEditorCommandHandler,
|
|
3740
4546
|
})) {
|
|
3741
4547
|
pi.registerCommand(command.name, {
|
|
3742
4548
|
description: command.description,
|