pi-fold 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -28
- package/extensions/active-context.ts +1011 -444
- package/extensions/index.js +16 -3
- package/extensions/lib/curation.ts +56 -60
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +26 -42
- package/extensions/lib/instrumentation.ts +10 -3
- package/extensions/lib/measurement.ts +6 -12
- package/extensions/lib/persistence.ts +62 -38
- package/extensions/lib/policy.ts +171 -89
- package/extensions/lib/scheduling.ts +341 -108
- package/extensions/lib/selection.ts +6 -78
- package/extensions/lib/tool-surface.ts +29 -33
- package/extensions/lib/transcript.ts +17 -60
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
descendantIds,
|
|
23
23
|
encodedFoldSource,
|
|
24
24
|
foldCandidatesDetail,
|
|
25
|
+
foldIndexRow,
|
|
25
26
|
foldTreeDetail,
|
|
26
27
|
peekFoldSource,
|
|
27
28
|
prepareFold,
|
|
@@ -29,7 +30,6 @@ import {
|
|
|
29
30
|
projectionSlateCandidates,
|
|
30
31
|
protectEvidence,
|
|
31
32
|
requireActiveFold,
|
|
32
|
-
selectAutomaticChapter,
|
|
33
33
|
selectAutomaticRung,
|
|
34
34
|
setFoldProjectionState,
|
|
35
35
|
withExpandLease,
|
|
@@ -56,7 +56,6 @@ import {
|
|
|
56
56
|
providerTokens,
|
|
57
57
|
refsProtected,
|
|
58
58
|
stringIds,
|
|
59
|
-
toolRefsProtected,
|
|
60
59
|
toolPayload,
|
|
61
60
|
} from "./lib/measurement.ts";
|
|
62
61
|
import type {
|
|
@@ -70,7 +69,6 @@ import {
|
|
|
70
69
|
clearPrepared,
|
|
71
70
|
deriveFoldParents,
|
|
72
71
|
flattenFoldRefs,
|
|
73
|
-
foldBrief,
|
|
74
72
|
foldIdFor,
|
|
75
73
|
makeFoldRecordEntry,
|
|
76
74
|
makeStateCheckpoint,
|
|
@@ -90,6 +88,7 @@ import {
|
|
|
90
88
|
activeContextSource,
|
|
91
89
|
ACTIVE_CONTEXT_POLICY,
|
|
92
90
|
ACTIVE_CONTEXT_TOOL_ACTIONS,
|
|
91
|
+
UNBRIEFED_FOLDS_BEFORE_NOTICE,
|
|
93
92
|
contextBrand,
|
|
94
93
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
95
94
|
DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES,
|
|
@@ -99,7 +98,6 @@ import {
|
|
|
99
98
|
COMMIT_RECLAIM_FLOOR_SHARE,
|
|
100
99
|
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
101
100
|
DEFAULT_CONTEXT_WINDOW,
|
|
102
|
-
resolveGuidance,
|
|
103
101
|
assertThresholdsServable,
|
|
104
102
|
resolveThresholds,
|
|
105
103
|
servingBudgetTokens,
|
|
@@ -107,15 +105,15 @@ import {
|
|
|
107
105
|
entryTypeNamespace,
|
|
108
106
|
MAX_FOLD_SPAN_CHARS,
|
|
109
107
|
MAX_PINNED_SHARE,
|
|
110
|
-
MAX_WEDGE_ABSORB_TOKENS,
|
|
111
108
|
PEEK_DEFAULT_MAX_BYTES,
|
|
109
|
+
peekIsEphemeral,
|
|
112
110
|
PEEK_MIN_SLICE_BYTES,
|
|
113
111
|
PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
112
|
+
RETIRED_TOOL_ACTIONS,
|
|
114
113
|
AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
115
114
|
USER_RESCUE_MAX_SOURCE_CHARS,
|
|
116
115
|
} from "./lib/policy.ts";
|
|
117
116
|
import type {
|
|
118
|
-
ActiveContextGuidance,
|
|
119
117
|
ActiveContextSnapshot,
|
|
120
118
|
ActiveContextState,
|
|
121
119
|
ActiveContextThresholds,
|
|
@@ -123,15 +121,16 @@ import type {
|
|
|
123
121
|
FoldCandidate,
|
|
124
122
|
FoldKind,
|
|
125
123
|
FoldRecordEntry,
|
|
124
|
+
MarkOrigin,
|
|
126
125
|
PreparedFold,
|
|
127
126
|
} from "./lib/policy.ts";
|
|
128
127
|
import {
|
|
129
128
|
absorbWedgeMarks,
|
|
129
|
+
markSpanRefs,
|
|
130
130
|
} from "./lib/scheduling.ts";
|
|
131
131
|
import {
|
|
132
132
|
contextReceipt,
|
|
133
|
-
|
|
134
|
-
markAwarenessText,
|
|
133
|
+
foldNoticeText,
|
|
135
134
|
receiptBlockText,
|
|
136
135
|
withReceipt,
|
|
137
136
|
} from "./lib/curation.ts";
|
|
@@ -140,35 +139,33 @@ import type {
|
|
|
140
139
|
} from "./lib/curation.ts";
|
|
141
140
|
import {
|
|
142
141
|
addPendingMark,
|
|
143
|
-
|
|
142
|
+
commitCoverage,
|
|
143
|
+
windowClaims,
|
|
144
144
|
commitPendingMarks,
|
|
145
145
|
consolidationMarks,
|
|
146
146
|
ephemeralPeekMarks,
|
|
147
|
+
epochCommitDue,
|
|
147
148
|
estimatedTokens,
|
|
148
149
|
foldMarkFor,
|
|
149
|
-
ladderSelectionMark,
|
|
150
150
|
ladderBrief,
|
|
151
151
|
markAccounting,
|
|
152
|
-
markEligibility,
|
|
153
152
|
markFreedBytes,
|
|
154
153
|
unmarkedRemainder,
|
|
155
|
-
|
|
156
|
-
markedFoldIds,
|
|
154
|
+
markClaimingRef,
|
|
157
155
|
markOrdinal,
|
|
158
156
|
pendingMarks,
|
|
159
|
-
|
|
157
|
+
refoldMarks,
|
|
160
158
|
schedulingStatus,
|
|
161
159
|
topUpMarks,
|
|
160
|
+
frontierMarks,
|
|
162
161
|
withPendingMarks,
|
|
163
162
|
} from "./lib/scheduling.ts";
|
|
164
163
|
import {
|
|
165
|
-
candidateSpanChars,
|
|
166
164
|
manualFoldCandidate,
|
|
167
165
|
peekedSourceFoldIds,
|
|
168
|
-
selectAutomaticToolBatch,
|
|
169
166
|
snapFoldCandidate,
|
|
170
167
|
snapToFoldBoundaries,
|
|
171
|
-
|
|
168
|
+
spanBytes,
|
|
172
169
|
} from "./lib/selection.ts";
|
|
173
170
|
import type { SpanCorrection } from "./lib/selection.ts";
|
|
174
171
|
import {
|
|
@@ -187,8 +184,8 @@ import type {
|
|
|
187
184
|
ProjectionChange,
|
|
188
185
|
} from "./lib/instrumentation.ts";
|
|
189
186
|
import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
|
|
187
|
+
import { buildFoldEditorData, FoldEditorView } from "./lib/editor-ui.ts";
|
|
190
188
|
import {
|
|
191
|
-
currentTurnRefKeys,
|
|
192
189
|
mapActiveContext,
|
|
193
190
|
} from "./lib/transcript.ts";
|
|
194
191
|
import {
|
|
@@ -255,7 +252,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
255
252
|
blacklistAutoFoldTools?: ReadonlySet<string>;
|
|
256
253
|
providerInputBudget?: number;
|
|
257
254
|
thresholds?: ActiveContextThresholds;
|
|
258
|
-
guidance?: Partial<ActiveContextGuidance>;
|
|
259
255
|
}): {
|
|
260
256
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
261
257
|
} {
|
|
@@ -288,6 +284,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
288
284
|
"tools it meant to permit");
|
|
289
285
|
}
|
|
290
286
|
}
|
|
287
|
+
if (Object.hasOwn(options, "guidance")) {
|
|
288
|
+
throw new Error("guidance is no longer an option: it switched the copy that taught the " +
|
|
289
|
+
"agent to CHOOSE SPANS, and the agent does not choose spans. The runtime cuts at the " +
|
|
290
|
+
"frontier and the agent annotates what it cut, so the guidance it switched has been " +
|
|
291
|
+
"replaced wholesale by the post-fold notice, which is not optional because a fold the " +
|
|
292
|
+
"agent is never told about is one it can never brief");
|
|
293
|
+
}
|
|
291
294
|
if (Object.hasOwn(options, "commandPrefix")) {
|
|
292
295
|
throw new Error("commandPrefix is no longer an option: give whole names with commandNames");
|
|
293
296
|
}
|
|
@@ -317,13 +320,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
317
320
|
}
|
|
318
321
|
const thresholds = resolveThresholds(options.thresholds);
|
|
319
322
|
assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
320
|
-
const guidance = resolveGuidance(options.guidance);
|
|
321
323
|
const stateEntryType = `${entryTypePrefix}-state`;
|
|
322
324
|
const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
|
|
323
325
|
const milestoneProjectionType = `${entryTypePrefix}-milestone`;
|
|
324
326
|
const advisoryProjectionType = `${entryTypePrefix}-advisory`;
|
|
327
|
+
const foldNoticeProjectionType = `${entryTypePrefix}-fold-notice`;
|
|
325
328
|
const receiptProjectionType = `${entryTypePrefix}-receipts`;
|
|
326
|
-
const riderProjectionType = `${entryTypePrefix}-rider`;
|
|
327
329
|
const curationProjectionType = `${entryTypePrefix}-curation`;
|
|
328
330
|
const entryNamespace = entryTypeNamespace(entryTypePrefix);
|
|
329
331
|
const providerMeasurementEntryType = `${entryNamespace}-provider-context-measurement`;
|
|
@@ -353,6 +355,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
353
355
|
};
|
|
354
356
|
|
|
355
357
|
const ladder = {
|
|
358
|
+
/** The provider measurement the last band-top commit ran against; one commit per one. */
|
|
359
|
+
bandTopMeasurement: null as unknown,
|
|
360
|
+
/** The snapshot-and-state key whose frontier scan came back empty; see advanceFoldFrontier. */
|
|
361
|
+
frontierEmptyKey: null as string | null,
|
|
356
362
|
pendingManual: false,
|
|
357
363
|
preparing: null as { id: string; controller: AbortController; promise: Promise<void> } | null,
|
|
358
364
|
lastPreparationError: null as string | null,
|
|
@@ -401,7 +407,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
401
407
|
previousText: null as string | null,
|
|
402
408
|
lastChange: "append" as ProjectionChange,
|
|
403
409
|
lastPreservedShare: null as number | null,
|
|
404
|
-
sinceHandoff: [] as Array<{
|
|
410
|
+
sinceHandoff: [] as Array<{
|
|
411
|
+
seq: number;
|
|
412
|
+
kind: string;
|
|
413
|
+
action?: string;
|
|
414
|
+
ok?: boolean;
|
|
415
|
+
}>,
|
|
405
416
|
mutationsSinceHandoff: 0,
|
|
406
417
|
requests: 0,
|
|
407
418
|
lastMutationRequest: 0,
|
|
@@ -462,6 +473,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
462
473
|
hardFenceReleasedProjectionKeys: new Set<string>(),
|
|
463
474
|
};
|
|
464
475
|
|
|
476
|
+
// EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
|
|
477
|
+
// visibility. In-memory ON PURPOSE: a restart forgets the registry and the
|
|
478
|
+
// result stays durable, which is the safe default, and the durable entry
|
|
479
|
+
// always keeps the exact bytes, so folding, peek and rollback read the
|
|
480
|
+
// source unchanged whatever the projection shows.
|
|
481
|
+
const ephemeralPeeks = new Set<string>();
|
|
482
|
+
|
|
465
483
|
const nativeCompaction = {
|
|
466
484
|
lastThresholdDecision: null as Record<string, unknown> | null,
|
|
467
485
|
pendingNativeReceipt: null as NativeCompactionCompletionReceipt | null,
|
|
@@ -540,7 +558,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
540
558
|
revision: persistence.state?.revision ?? 0,
|
|
541
559
|
at: Date.now(),
|
|
542
560
|
}, payload);
|
|
543
|
-
instrumentation.sinceHandoff.push({
|
|
561
|
+
instrumentation.sinceHandoff.push({
|
|
562
|
+
seq: record.seq,
|
|
563
|
+
kind,
|
|
564
|
+
...(typeof record.action === "string" ? { action: record.action } : {}),
|
|
565
|
+
...(typeof record.ok === "boolean" ? { ok: record.ok } : {}),
|
|
566
|
+
});
|
|
544
567
|
const operation = curation.instrumentationQueue.then(async () => {
|
|
545
568
|
try { await pi.appendEntry(contextEventEntryType, record); }
|
|
546
569
|
catch { }
|
|
@@ -552,6 +575,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
552
575
|
const PREFIX_MUTATING_KINDS: ReadonlySet<string> = new Set([
|
|
553
576
|
"context.commit", "context.fold", "context.absorb", "context.split", "context.recovery",
|
|
554
577
|
]);
|
|
578
|
+
const CACHE_ACTION_REQUEST_CLASS: Readonly<Record<string, string>> = Object.freeze({
|
|
579
|
+
fold: "after-mark",
|
|
580
|
+
peek: "after-peek",
|
|
581
|
+
expand: "after-expand",
|
|
582
|
+
refold: "after-refold",
|
|
583
|
+
});
|
|
555
584
|
|
|
556
585
|
const deliverReceipt = (receipt: ContextReceipt): void => {
|
|
557
586
|
curation.receipts = withReceipt(curation.receipts, receipt);
|
|
@@ -1191,7 +1220,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1191
1220
|
policy: snapshot ? {
|
|
1192
1221
|
maxTarget: snapshot.thresholds.maxTarget,
|
|
1193
1222
|
minTarget: snapshot.thresholds.minTarget,
|
|
1194
|
-
freshTail: snapshot.thresholds.freshTail,
|
|
1195
1223
|
hardFenceRatio: hardFenceRatio(snapshot),
|
|
1196
1224
|
} : null,
|
|
1197
1225
|
});
|
|
@@ -1487,6 +1515,198 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1487
1515
|
ctx.abort();
|
|
1488
1516
|
};
|
|
1489
1517
|
|
|
1518
|
+
// THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
|
|
1519
|
+
//
|
|
1520
|
+
// `maxTarget` is what the settings panel calls the commit trigger, and until now it
|
|
1521
|
+
// fired nothing. There were two commit sites: the fence below, which measures against
|
|
1522
|
+
// the WHOLE serving budget plus a margin, and the compaction boundary, which is Pi's
|
|
1523
|
+
// own threshold near the top of the window. A live 1M-window session sat at 571,367
|
|
1524
|
+
// tokens against a stated trigger of 516,096 with `folds: 0`, because neither site was
|
|
1525
|
+
// anywhere near. On a 251,520-token budget the fence sits close enough behind the
|
|
1526
|
+
// boundary to hide it; at 1M the two are half a million tokens apart.
|
|
1527
|
+
//
|
|
1528
|
+
// Shane, on the shape of the fix: "if the context is over our max, we commit. I don't
|
|
1529
|
+
// think that should be anything like once it crosses this threshold or anything like
|
|
1530
|
+
// that, it should just be an if statement. That way there isn't much room for error, if
|
|
1531
|
+
// it's over we fold." So there is no latch and no crossing to detect. Every projection
|
|
1532
|
+
// pass asks one question and acts on the answer. Occupancy only moves when the provider
|
|
1533
|
+
// measures, and a second attempt inside one handoff already defers on the mutation
|
|
1534
|
+
// budget, so "one commit per pass while over" is what this produces. A pass that finds
|
|
1535
|
+
// nothing to fold says so in the stream, which is a state worth seeing rather than one
|
|
1536
|
+
// worth suppressing.
|
|
1537
|
+
/**
|
|
1538
|
+
* Cut the raw material that has piled up past the last fold, and say so.
|
|
1539
|
+
*
|
|
1540
|
+
* This is the whole of "the runtime cuts": no commit, no bytes moved, no persistence
|
|
1541
|
+
* boundary. It stages pending marks the same way a commit's own top-up does, through
|
|
1542
|
+
* the same selector and the same brief builder, so a frontier cut and a commit fill are
|
|
1543
|
+
* the same object and the agent cannot tell them apart except by when it heard.
|
|
1544
|
+
*/
|
|
1545
|
+
const advanceFoldFrontier = (snapshot: ActiveContextSnapshot): number => {
|
|
1546
|
+
if (!persistence.state) return 0;
|
|
1547
|
+
if (ladder.automaticFailure !== null) return 0;
|
|
1548
|
+
// A SCAN THAT FOUND NOTHING IS NOT RE-RUN UNTIL SOMETHING CHANGES. Exhausting the
|
|
1549
|
+
// selector costs one window scan per staged cut and one more to come back empty, and
|
|
1550
|
+
// once everything foldable is staged EVERY later pass pays that last scan to be told
|
|
1551
|
+
// the same thing. Measured on the 90-projection loop inside gate 50's instrumentation
|
|
1552
|
+
// claim: 137ms of a 460ms pass, 30 percent of the runtime's own per-request cost, for
|
|
1553
|
+
// an answer that cannot have changed.
|
|
1554
|
+
//
|
|
1555
|
+
// The key is the one `authoritativeSnapshotFor` already trusts for the snapshot
|
|
1556
|
+
// itself, plus the two pieces of state the selector reads: the revision, which moves
|
|
1557
|
+
// on every durable write, and the pending-mark count, which moves when the agent
|
|
1558
|
+
// withdraws one. It is only consulted when the last scan staged NOTHING; a scan that
|
|
1559
|
+
// staged something changed the state, so its key is stale by construction.
|
|
1560
|
+
const frontierKey = `${snapshot.messages.length}:${snapshot.mapped.length}:` +
|
|
1561
|
+
`${persistence.state.revision}:${pendingMarks(persistence.state).length}`;
|
|
1562
|
+
if (ladder.frontierEmptyKey === frontierKey) return 0;
|
|
1563
|
+
// CONSOLIDATION IS ANNOUNCED THE SAME WAY (Shane, 2026-08-23). When standing folds
|
|
1564
|
+
// reach `consolidateAfter` the parent that will absorb them is staged HERE rather than
|
|
1565
|
+
// being computed inside the commit, for exactly the reason the frontier exists: a
|
|
1566
|
+
// parent's brief indexes ten children, it is the entry the agent will navigate by, and
|
|
1567
|
+
// the agent knows what mattered in those children while the commit only knows their
|
|
1568
|
+
// order. Staged, it is byte-inert like any other pending mark and rides the same
|
|
1569
|
+
// notice, so there is one mechanism and not two.
|
|
1570
|
+
const marks = [
|
|
1571
|
+
...frontierMarks({
|
|
1572
|
+
snapshot,
|
|
1573
|
+
state: persistence.state,
|
|
1574
|
+
ordinal: instrumentation.requests,
|
|
1575
|
+
}),
|
|
1576
|
+
...consolidationMarks({
|
|
1577
|
+
snapshot,
|
|
1578
|
+
state: persistence.state,
|
|
1579
|
+
ordinal: instrumentation.requests,
|
|
1580
|
+
}),
|
|
1581
|
+
];
|
|
1582
|
+
let state = persistence.state;
|
|
1583
|
+
const staged: ReturnType<typeof frontierMarks> = [];
|
|
1584
|
+
for (const mark of marks) {
|
|
1585
|
+
const addition = addPendingMark(state, mark);
|
|
1586
|
+
if (!addition.added) continue;
|
|
1587
|
+
state = addition.state;
|
|
1588
|
+
staged.push(mark);
|
|
1589
|
+
}
|
|
1590
|
+
// NOTHING STAGED, so this key is answered and the next pass under it can skip the
|
|
1591
|
+
// scan. Read from `staged` rather than from `marks`, because a candidate the pending
|
|
1592
|
+
// set already holds comes back from the selector every pass and is refused every
|
|
1593
|
+
// pass: keying on `marks` left the skip unreachable in exactly the steady state it
|
|
1594
|
+
// was built for, and half its measured saving on the floor.
|
|
1595
|
+
if (!staged.length) {
|
|
1596
|
+
ladder.frontierEmptyKey = frontierKey;
|
|
1597
|
+
return 0;
|
|
1598
|
+
}
|
|
1599
|
+
persistence.state = state;
|
|
1600
|
+
emit("context.frontier", {
|
|
1601
|
+
cut: staged.length,
|
|
1602
|
+
pending_marks: (state.pendingMarks ?? []).length,
|
|
1603
|
+
min_fold_chars: thresholds.minFoldChars,
|
|
1604
|
+
});
|
|
1605
|
+
// NO PERSISTENCE BOUNDARY OF ITS OWN, deliberately. A cut is DERIVED: the mark id
|
|
1606
|
+
// comes from the span, so a restart that lost these re-cuts exactly the same spans
|
|
1607
|
+
// with exactly the same ids, and the only thing that has to survive is a brief the
|
|
1608
|
+
// agent WROTE, which its own action persists along with the marks it rides with.
|
|
1609
|
+
// Persisting here instead cost a durable write on every projection pass, and the
|
|
1610
|
+
// branch entry it appended rebuilt the projection: gates 110 and 111 caught it as an
|
|
1611
|
+
// occupancy anchor that stopped reading the provider's own count.
|
|
1612
|
+
return staged.length;
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Tell the agent what was cut, once there is enough of it to be worth a turn.
|
|
1617
|
+
*
|
|
1618
|
+
* It is built from LIVE state, so it names what is unbriefed at the pass it speaks on,
|
|
1619
|
+
* and it is admitted ONCE per frozen projection, so a pass that adds material leaves it
|
|
1620
|
+
* buried at its own index rather than moving it to the tail. Those two together mean it
|
|
1621
|
+
* is a snapshot rather than a running tally: cuts staged after it spoke wait for the
|
|
1622
|
+
* next pass that rebuilds the projection anyway, and that is the trade being made. A
|
|
1623
|
+
* carrier that re-renders to stay current has to move, moving costs the occupancy
|
|
1624
|
+
* anchor its provider count, and an anchor is worth more than a fold id arriving one
|
|
1625
|
+
* epoch early. What it must never do is ask twice for a brief the agent has written,
|
|
1626
|
+
* and it does not: the rebuild reads the marks, not its own last text.
|
|
1627
|
+
*/
|
|
1628
|
+
const appendFoldNotice = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1629
|
+
if (!persistence.state) return projected;
|
|
1630
|
+
const held = pendingMarks(persistence.state);
|
|
1631
|
+
const unbriefed = held.filter((mark) => mark.briefProvenance?.kind !== "supplied");
|
|
1632
|
+
// BATCHED, AND APPENDED ONCE. A notice per cut is noise, and noise is how guidance
|
|
1633
|
+
// gets ignored; the frontier cuts often and this speaks only when a batch has built
|
|
1634
|
+
// up. `carrierAdmitted` is what makes it an APPEND rather than a rewrite: once the
|
|
1635
|
+
// carrier is in the frozen projection it is not pushed again, so raw material
|
|
1636
|
+
// arriving afterwards lands after it and the prefix in front of it never moves.
|
|
1637
|
+
//
|
|
1638
|
+
// The first cut of this withdrew the carrier and re-pushed it at the tail every pass,
|
|
1639
|
+
// the way the steward band had to because a band must be able to stand down. That is
|
|
1640
|
+
// exactly wrong for this one: re-pushing moved the notice past each newly arrived
|
|
1641
|
+
// message, so the byte at its old index changed on a pass where nothing else had, and
|
|
1642
|
+
// gates 110 and 111 read the whole projection as rewritten and dropped the occupancy
|
|
1643
|
+
// anchor back to the estimate. A carrier that moves on a quiet pass costs the cache,
|
|
1644
|
+
// which is the one thing this design exists to avoid.
|
|
1645
|
+
if (unbriefed.length < UNBRIEFED_FOLDS_BEFORE_NOTICE) return projected;
|
|
1646
|
+
if (!carrierAdmitted("fold-notice")) return projected;
|
|
1647
|
+
const text = foldNoticeText({
|
|
1648
|
+
unbriefed: unbriefed.map((mark) => ({
|
|
1649
|
+
id: mark.id,
|
|
1650
|
+
kind: mark.kind,
|
|
1651
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
1652
|
+
})),
|
|
1653
|
+
pending: held.length,
|
|
1654
|
+
toolName,
|
|
1655
|
+
brandNoun,
|
|
1656
|
+
});
|
|
1657
|
+
emit("context.frontier", {
|
|
1658
|
+
cut: 0,
|
|
1659
|
+
pending_marks: held.length,
|
|
1660
|
+
unbriefed: unbriefed.length,
|
|
1661
|
+
notice_chars: text.length,
|
|
1662
|
+
});
|
|
1663
|
+
projected.push({
|
|
1664
|
+
role: "custom",
|
|
1665
|
+
customType: foldNoticeProjectionType,
|
|
1666
|
+
content: text,
|
|
1667
|
+
display: false,
|
|
1668
|
+
details: { source: activeContextSource(entryTypePrefix), ephemeral: true },
|
|
1669
|
+
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
1670
|
+
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
1671
|
+
: 0,
|
|
1672
|
+
});
|
|
1673
|
+
return projected;
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
const enforceBandTop = async (
|
|
1677
|
+
snapshot: ActiveContextSnapshot,
|
|
1678
|
+
ctx: any,
|
|
1679
|
+
): Promise<boolean> => {
|
|
1680
|
+
if (!epochCommitDue(snapshot, measurements.latestRatio)) return false;
|
|
1681
|
+
// ONE COMMIT PER MEASUREMENT, which is what "one commit per crossing" means once the
|
|
1682
|
+
// commit stops taking everything. There was no latch here and none was needed: the
|
|
1683
|
+
// band-top commit folded every span the class law offered, so the next pass found
|
|
1684
|
+
// nothing proposable and deferred. The 2026-08-23 depth bound ends that, on purpose,
|
|
1685
|
+
// and without this the parked window commits a SLICE on every pass at the same
|
|
1686
|
+
// declared occupancy, which is a prefix rewrite per pass and the exact cache cost the
|
|
1687
|
+
// one-commit rule exists to prevent. Occupancy is anchored to the provider's own
|
|
1688
|
+
// count, so a new count is the only evidence that the window actually moved; the
|
|
1689
|
+
// latch is keyed to that measurement and nothing else.
|
|
1690
|
+
if (ladder.bandTopMeasurement !== null &&
|
|
1691
|
+
ladder.bandTopMeasurement === measurements.lastProviderMeasurement) {
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
ladder.bandTopMeasurement = measurements.lastProviderMeasurement;
|
|
1695
|
+
let action: Record<string, unknown> | null = null;
|
|
1696
|
+
try {
|
|
1697
|
+
// NULL RATIO, deliberately. The band top is routine housekeeping at a stated
|
|
1698
|
+
// threshold, not an emergency, and a null ratio is what arms the depth bound: the
|
|
1699
|
+
// commit takes stalest-first and stops at the aim, so the newest events stay raw
|
|
1700
|
+
// for as long as there is room for them. The fence and the compaction boundary
|
|
1701
|
+
// carry a ratio and no bound, because something has already gone wrong there.
|
|
1702
|
+
action = await attemptAutomaticCommit(snapshot, ctx, "band-top", null);
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
suspendAutomatic(error, "band-top", ctx);
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
return action !== null;
|
|
1708
|
+
};
|
|
1709
|
+
|
|
1490
1710
|
const enforceProjectionBudget = async (
|
|
1491
1711
|
snapshot: ActiveContextSnapshot,
|
|
1492
1712
|
projected: unknown[],
|
|
@@ -1503,9 +1723,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1503
1723
|
attempts += 1;
|
|
1504
1724
|
// THE EMERGENCY IS THE REQUEST THAT WILL NOT FIT, and only that. A projection at
|
|
1505
1725
|
// 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
|
-
//
|
|
1726
|
+
// the request, but it is not a recovery and it does not buy an exemption from the
|
|
1727
|
+
// reclaim floor. What separates it from the band top is depth alone: the fence
|
|
1728
|
+
// carries a ratio, so its commit takes everything eligible rather than stopping at
|
|
1729
|
+
// the aim.
|
|
1509
1730
|
const emergency = measured.over || rejected;
|
|
1510
1731
|
if (emergency) curation.recoveryAttempts += 1;
|
|
1511
1732
|
let action: Record<string, unknown> | null = null;
|
|
@@ -1678,40 +1899,90 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1678
1899
|
return projected;
|
|
1679
1900
|
};
|
|
1680
1901
|
|
|
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
1902
|
const holdFrozen = (projected: unknown[]): unknown[] => {
|
|
1703
1903
|
freeze.projection = [...projected];
|
|
1704
1904
|
freeze.active = false;
|
|
1705
1905
|
return projected;
|
|
1706
1906
|
};
|
|
1707
1907
|
|
|
1908
|
+
// An ephemeral peek result rides the projection exactly until the model's
|
|
1909
|
+
// next message, the same answered reading as the directed ask, then its
|
|
1910
|
+
// index holds a one-line placeholder and the reply is the surviving trace.
|
|
1911
|
+
// A message already projection-shaped (a fold's own deterministic brief
|
|
1912
|
+
// placeholder carries the same toolCallId) is never touched: only the raw
|
|
1913
|
+
// result is the ephemeral one.
|
|
1914
|
+
//
|
|
1915
|
+
// Returns what it replaced, keyed by call id, because the freeze has to be
|
|
1916
|
+
// told. Running against the BODY alone was not enough: the freeze compares
|
|
1917
|
+
// this pass's body prefix against the previous one and reuses the previous
|
|
1918
|
+
// PROJECTION when they match, and a withdrawal edits a body index inside
|
|
1919
|
+
// that prefix, so the comparison failed and the whole projection was rebuilt
|
|
1920
|
+
// from the body. The rebuild drops every receipt the freeze had
|
|
1921
|
+
// buried mid-array over the session, which moves divergence back to the
|
|
1922
|
+
// FIRST buried carrier rather than leaving it at the withdrawn index. That
|
|
1923
|
+
// is the opposite of what this mechanism is for: a tail edit was rewriting
|
|
1924
|
+
// the session's whole cached prefix.
|
|
1925
|
+
const withdrawConsumedEphemeralPeeks = (body: unknown[]): Map<string, unknown> => {
|
|
1926
|
+
const withdrawn = new Map<string, unknown>();
|
|
1927
|
+
if (!ephemeralPeeks.size) return withdrawn;
|
|
1928
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
1929
|
+
const message = body[index];
|
|
1930
|
+
const callId = ownValue(message, "toolCallId");
|
|
1931
|
+
if (ownValue(message, "role") !== "toolResult" || typeof callId !== "string" ||
|
|
1932
|
+
!ephemeralPeeks.has(callId)) continue;
|
|
1933
|
+
const details = ownValue(message, "details");
|
|
1934
|
+
if (details && typeof ownValue(details, "projection") === "string") continue;
|
|
1935
|
+
const answered = body.slice(index + 1).some((later) =>
|
|
1936
|
+
ownValue(later, "role") === "assistant");
|
|
1937
|
+
if (!answered) continue;
|
|
1938
|
+
const placeholder = {
|
|
1939
|
+
role: "toolResult",
|
|
1940
|
+
toolCallId: callId,
|
|
1941
|
+
toolName: ownValue(message, "toolName"),
|
|
1942
|
+
content: [{ type: "text", text: "Ephemeral peek consumed; the reply that followed it is the surviving trace." }],
|
|
1943
|
+
isError: false,
|
|
1944
|
+
details: { projection: "ephemeral-peek-consumed" },
|
|
1945
|
+
timestamp: ownValue(message, "timestamp"),
|
|
1946
|
+
};
|
|
1947
|
+
body[index] = placeholder;
|
|
1948
|
+
withdrawn.set(callId, placeholder);
|
|
1949
|
+
}
|
|
1950
|
+
return withdrawn;
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
// Carry a withdrawal into the freeze's own kept arrays so the frozen prefix
|
|
1954
|
+
// stays usable. Matched by CALL ID rather than by position: the projection
|
|
1955
|
+
// interleaves buried carriers, so a body index is not a projection index.
|
|
1956
|
+
// The substitution is identity-preserving on role, call id, tool name and
|
|
1957
|
+
// timestamp, so what changes is the one result's content and nothing else,
|
|
1958
|
+
// and divergence lands at the withdrawn index where the mechanism puts it.
|
|
1959
|
+
const carryWithdrawalIntoFreeze = (messages: unknown[], withdrawn: Map<string, unknown>): void => {
|
|
1960
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
1961
|
+
const message = messages[index];
|
|
1962
|
+
if (ownValue(message, "role") !== "toolResult") continue;
|
|
1963
|
+
const callId = ownValue(message, "toolCallId");
|
|
1964
|
+
if (typeof callId !== "string") continue;
|
|
1965
|
+
const placeholder = withdrawn.get(callId);
|
|
1966
|
+
if (!placeholder) continue;
|
|
1967
|
+
const details = ownValue(message, "details");
|
|
1968
|
+
if (details && typeof ownValue(details, "projection") === "string") continue;
|
|
1969
|
+
messages[index] = placeholder;
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1708
1973
|
const projectWithAdvisory = (snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1709
1974
|
const body = projectActiveContext(snapshot, persistence.state!).filter((message) => {
|
|
1710
1975
|
const customType = ownValue(message, "customType");
|
|
1711
1976
|
return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
|
|
1712
|
-
customType !== receiptProjectionType &&
|
|
1713
|
-
customType !==
|
|
1977
|
+
customType !== receiptProjectionType && customType !== curationProjectionType &&
|
|
1978
|
+
customType !== foldNoticeProjectionType;
|
|
1714
1979
|
});
|
|
1980
|
+
const withdrawn = withdrawConsumedEphemeralPeeks(body);
|
|
1981
|
+
if (withdrawn.size && freeze.body) {
|
|
1982
|
+
carryWithdrawalIntoFreeze(freeze.body, withdrawn);
|
|
1983
|
+
freeze.bodyText = stableStringify(freeze.body);
|
|
1984
|
+
if (freeze.projection) carryWithdrawalIntoFreeze(freeze.projection, withdrawn);
|
|
1985
|
+
}
|
|
1715
1986
|
const held = freeze.body?.length ?? 0;
|
|
1716
1987
|
freeze.active = freeze.projection !== null && body.length >= held &&
|
|
1717
1988
|
stableStringify(body.slice(0, held)) === freeze.bodyText;
|
|
@@ -1722,7 +1993,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1722
1993
|
freeze.body = body;
|
|
1723
1994
|
freeze.bodyText = stableStringify(body);
|
|
1724
1995
|
appendReceipts(projected, snapshot);
|
|
1725
|
-
|
|
1996
|
+
appendFoldNotice(projected, snapshot);
|
|
1726
1997
|
return holdFrozen(projected);
|
|
1727
1998
|
};
|
|
1728
1999
|
const noteProjection = (projected: unknown[]): void => {
|
|
@@ -1738,8 +2009,27 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1738
2009
|
? divergence.identicalChars / previousChars
|
|
1739
2010
|
: null;
|
|
1740
2011
|
const charsPerToken = projectionCharsPerToken();
|
|
2012
|
+
const successfulCacheActions = instrumentation.sinceHandoff.filter((event) =>
|
|
2013
|
+
event.kind === "context.attempt" && event.ok === true &&
|
|
2014
|
+
typeof event.action === "string" && CACHE_ACTION_REQUEST_CLASS[event.action]);
|
|
2015
|
+
const latestCacheAction = successfulCacheActions.at(-1) ?? null;
|
|
2016
|
+
const latestCacheActionClass = latestCacheAction?.action
|
|
2017
|
+
? CACHE_ACTION_REQUEST_CLASS[latestCacheAction.action]
|
|
2018
|
+
: null;
|
|
2019
|
+
const projectionActions = successfulCacheActions.filter((event) =>
|
|
2020
|
+
event.action === "expand" || event.action === "refold");
|
|
1741
2021
|
const causes = instrumentation.sinceHandoff.filter((event) =>
|
|
1742
|
-
PREFIX_MUTATING_KINDS.has(event.kind));
|
|
2022
|
+
PREFIX_MUTATING_KINDS.has(event.kind) || projectionActions.includes(event));
|
|
2023
|
+
const structuralCause = causes.some((event) => event.kind !== "context.attempt");
|
|
2024
|
+
const requestClass = causes.some((event) => event.kind === "context.recovery")
|
|
2025
|
+
? "after-rollback"
|
|
2026
|
+
: (latestCacheAction?.action === "expand" || latestCacheAction?.action === "refold")
|
|
2027
|
+
? latestCacheActionClass!
|
|
2028
|
+
: structuralCause
|
|
2029
|
+
? "after-fold"
|
|
2030
|
+
: latestCacheActionClass
|
|
2031
|
+
? latestCacheActionClass
|
|
2032
|
+
: divergence.index === null ? "steady-state" : "after-message";
|
|
1743
2033
|
const reading = projectedTokenReading(projected);
|
|
1744
2034
|
emit("context.projection", {
|
|
1745
2035
|
change: comparison.change,
|
|
@@ -1768,14 +2058,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1768
2058
|
estimated_tokens: Math.ceil(text.length / charsPerToken),
|
|
1769
2059
|
cause: divergence.index === null
|
|
1770
2060
|
? "pure-append"
|
|
1771
|
-
: (causes.length ? causes.map((event) => event.kind
|
|
2061
|
+
: (causes.length ? causes.map((event) => event.kind === "context.attempt"
|
|
2062
|
+
? `context.attempt:${String(event.action)}`
|
|
2063
|
+
: event.kind).join(",") : "unattributed"),
|
|
1772
2064
|
cause_event_seqs: causes.map((event) => event.seq).join(","),
|
|
1773
2065
|
events_since_handoff: instrumentation.sinceHandoff.length,
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
? "after-rollback"
|
|
1778
|
-
: (causes.length ? "after-fold" : "after-message")),
|
|
2066
|
+
cache_action: latestCacheAction?.action ?? null,
|
|
2067
|
+
cache_action_seq: latestCacheAction?.seq ?? null,
|
|
2068
|
+
request_class: requestClass,
|
|
1779
2069
|
});
|
|
1780
2070
|
instrumentation.previousText = text;
|
|
1781
2071
|
instrumentation.sinceHandoff = [];
|
|
@@ -1852,6 +2142,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1852
2142
|
let state = persistence.state!;
|
|
1853
2143
|
let peekAdded = 0;
|
|
1854
2144
|
let topUpAdded = 0;
|
|
2145
|
+
let refoldAdded = 0;
|
|
1855
2146
|
let consolidationAdded = 0;
|
|
1856
2147
|
let closingAdded = 0;
|
|
1857
2148
|
for (const mark of ephemeralPeekMarks({ snapshot, state, ordinal })) {
|
|
@@ -1862,7 +2153,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1862
2153
|
const addition = addPendingMark(state, mark);
|
|
1863
2154
|
if (addition.added) { state = addition.state; consolidationAdded += 1; }
|
|
1864
2155
|
}
|
|
1865
|
-
const guarded = currentTurnRefKeys(snapshot);
|
|
1866
2156
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
1867
2157
|
const usedTokens = capacity.usedTokens;
|
|
1868
2158
|
const budgetTokens = capacity.budgetTokens;
|
|
@@ -1879,40 +2169,60 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1879
2169
|
});
|
|
1880
2170
|
return null;
|
|
1881
2171
|
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2172
|
+
// ONE DEFINITION OF THE DROP. The fill sizes itself to it, the advisory warns
|
|
2173
|
+
// against it, and the mark response answers "how much more" with it.
|
|
2174
|
+
const freeingTarget = commitCoverage({ snapshot, state, usedTokens, budgetTokens }).targetShare;
|
|
2175
|
+
// ONE COMMIT, SIZED BEFORE IT RUNS (Shane, 2026-08-21). At the threshold we know how
|
|
2176
|
+
// far the window has to come down: `freeingTarget` is the distance from what is used
|
|
2177
|
+
// now to the minTarget floor. The agent's marks count toward that distance first and
|
|
2178
|
+
// the ladder fills the remainder from the stale end, so an agent that marked 30k of a
|
|
2179
|
+
// 100k drop leaves 70k for the fill and both land in the SAME commit.
|
|
2180
|
+
//
|
|
2181
|
+
// Three rules decide the fill, and each replaced something that was tried first:
|
|
2182
|
+
//
|
|
2183
|
+
// The ladder does not STAND DOWN when the agent has marked. Standing down commits at
|
|
2184
|
+
// whatever depth the agent happened to reach, so one small mark commits a barely-moved
|
|
2185
|
+
// window and the next fence pass fires a second commit in the same cycle, which is the
|
|
2186
|
+
// cache cost the one-commit rule exists to prevent. `topUpMarks` measures progress
|
|
2187
|
+
// against the state it is handed, so agent marks are counted rather than displaced,
|
|
2188
|
+
// and `claimedRefKeys` keeps the fill off spans the agent already took.
|
|
2189
|
+
//
|
|
2190
|
+
// Raw stale material is spent BEFORE expanded folds, so an agent reading an expanded
|
|
2191
|
+
// fold keeps it while anything else can cover the drop. This is also the only route
|
|
2192
|
+
// to the refold rung now that nothing stages between commits.
|
|
2193
|
+
//
|
|
2194
|
+
// The fill measures progress over EVERY mark it makes, not just the ones this commit
|
|
2195
|
+
// will apply. Measuring the eligible share alone is what a starved commit needs and it
|
|
2196
|
+
// is wrong here: a mark over protected evidence moves the eligible share by nothing,
|
|
2197
|
+
// so the loop kept marking and walked a whole 24-batch excursion chasing a target it
|
|
2198
|
+
// could not reach that way (the retainer was the current-turn guard then; pins and
|
|
2199
|
+
// protected evidence retain the same way now). Counting everything bounds the fill at
|
|
2200
|
+
// the drop it was asked for. The backstop still reads the eligible share, because
|
|
2201
|
+
// starvation is the one state where the difference is the point.
|
|
2202
|
+
const fill = (targetShare: number, eligibleOnly: boolean): void => {
|
|
2203
|
+
for (const mark of topUpMarks({ snapshot, state, ordinal, eligibleOnly, targetShare })) {
|
|
1893
2204
|
const addition = addPendingMark(state, mark);
|
|
1894
2205
|
if (addition.added) { state = addition.state; topUpAdded += 1; }
|
|
1895
2206
|
}
|
|
2207
|
+
for (const mark of refoldMarks({ snapshot, state, ordinal, eligibleOnly, targetShare })) {
|
|
2208
|
+
const addition = addPendingMark(state, mark);
|
|
2209
|
+
if (addition.added) { state = addition.state; refoldAdded += 1; }
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
let backstopFired = false;
|
|
2213
|
+
if (topUp) {
|
|
2214
|
+
fill(freeingTarget, false);
|
|
1896
2215
|
const reachedShare = markAccounting(snapshot, state).eligibleFreedBudgetShare;
|
|
1897
2216
|
const shallow = reachedShare < Math.max(commitDepthFloorShare(snapshot), freeingTarget);
|
|
1898
2217
|
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
|
-
}
|
|
2218
|
+
backstopFired = true;
|
|
2219
|
+
fill(1, true);
|
|
1909
2220
|
}
|
|
1910
2221
|
}
|
|
1911
2222
|
const wedges = absorbWedgeMarks({
|
|
1912
2223
|
snapshot,
|
|
1913
2224
|
state,
|
|
1914
2225
|
charsPerToken: projectionCharsPerToken(),
|
|
1915
|
-
excludeRefKeys: guarded,
|
|
1916
2226
|
});
|
|
1917
2227
|
state = wedges.state;
|
|
1918
2228
|
const accounting = markAccounting(snapshot, state);
|
|
@@ -1942,49 +2252,80 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1942
2252
|
eligible_marks: accounting.eligibleMarks,
|
|
1943
2253
|
eligible_freed_share: accounting.eligibleFreedBudgetShare,
|
|
1944
2254
|
reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2255
|
+
// THE REMAINDER RIDES EVERY DEFERRAL, not just the empty one. It was on
|
|
2256
|
+
// `nothing-proposable` alone, which was the only starved shape while staging
|
|
2257
|
+
// happened at commit time. The frontier cuts ahead of the commit, so a starved
|
|
2258
|
+
// pass now usually arrives holding a mark it cannot apply and lands HERE instead,
|
|
2259
|
+
// and "below the reclaim floor" without the remainder does not tell a reader
|
|
2260
|
+
// whether the mass is held or simply absent. That is the distinction the whole
|
|
2261
|
+
// record exists to draw.
|
|
2262
|
+
unmarked_stale_spans: unmarkedRemainder(snapshot, state, projectionCharsPerToken()).spans,
|
|
1945
2263
|
window_tokens: snapshot.contextWindow,
|
|
1946
2264
|
});
|
|
1947
2265
|
return null;
|
|
1948
2266
|
}
|
|
1949
2267
|
const bytesBefore = bytes(projectActiveContext(snapshot, state));
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2268
|
+
// THE COMMIT CUTS ONLY AS DEEP AS THE BUDGET ASKS (Shane, 2026-08-23). The frontier
|
|
2269
|
+
// stages as material arrives, so by now the pending set is most of the window; without
|
|
2270
|
+
// this bound the first commit would fold all of it. `freeingTarget` is a share of the
|
|
2271
|
+
// budget and the bound is bytes, so it is converted once, here, through the same ratio
|
|
2272
|
+
// the projection weighs itself with.
|
|
2273
|
+
//
|
|
2274
|
+
// It binds on the ROUTINE path only: a null ratio is the band top, ordinary
|
|
2275
|
+
// housekeeping at a stated threshold, and that is where "only as deep as it needs"
|
|
2276
|
+
// belongs. The fence and the compaction boundary carry a ratio because something has
|
|
2277
|
+
// already gone wrong there, and a lane holding a request that will not fit is not the
|
|
2278
|
+
// place to leave depth on the table. The backstop is exempt for the same reason: it
|
|
2279
|
+
// fires only when the commit is already starving.
|
|
2280
|
+
//
|
|
2281
|
+
// This bound is also what protects the working set since the current-turn guard was
|
|
2282
|
+
// deleted (Shane, 2026-08-23, from the first live session): the cut takes stalest
|
|
2283
|
+
// first and stops at the aim, so the newest events stay raw exactly as long as there
|
|
2284
|
+
// is room for them, in every session shape including one that never closes a turn.
|
|
1960
2285
|
let result = await commitPendingMarks({
|
|
1961
2286
|
snapshot,
|
|
1962
2287
|
state,
|
|
1963
2288
|
generation: lifecycle.generation,
|
|
1964
2289
|
retainIneligible: true,
|
|
1965
|
-
|
|
1966
|
-
|
|
2290
|
+
...(backstopFired || waiverRatio !== null ? {} : {
|
|
2291
|
+
applyTargetBytes: Math.ceil(freeingTarget * budgetTokens * projectionCharsPerToken()),
|
|
2292
|
+
}),
|
|
1967
2293
|
});
|
|
1968
2294
|
for (;;) {
|
|
2295
|
+
// THE CLOSING PASS COMMITS ONLY WHAT IT SEATS (2026-08-23). It rides the same paid
|
|
2296
|
+
// rewrite to give a group its parent, and it runs UNBOUNDED because a parent's whole
|
|
2297
|
+
// span must land together. Handing it the full pending set therefore leaked the depth
|
|
2298
|
+
// cut: the marks the bound had just retained were still pending, the closing commit
|
|
2299
|
+
// carried no bound, and the first band-top commit after the guard deletion folded all
|
|
2300
|
+
// 24 of them to the floor (gate 09's fixture, applied 26 of 24 with deferred 0). The
|
|
2301
|
+
// marks the cut held back are set aside before the closing commit and restored after,
|
|
2302
|
+
// so the only thing an unbounded closing pass can spend is the parents it just made.
|
|
1969
2303
|
let closing = result.state;
|
|
2304
|
+
const heldBack = pendingMarks(closing);
|
|
2305
|
+
let seeded = withPendingMarks(closing, []);
|
|
2306
|
+
let seededAdded = 0;
|
|
1970
2307
|
for (const mark of consolidationMarks({ snapshot, state: closing, ordinal })) {
|
|
1971
2308
|
const addition = addPendingMark(closing, mark);
|
|
1972
|
-
if (addition.added)
|
|
2309
|
+
if (!addition.added) continue;
|
|
2310
|
+
closing = addition.state;
|
|
2311
|
+
const carried = addPendingMark(seeded, mark);
|
|
2312
|
+
if (carried.added) { seeded = carried.state; closingAdded += 1; seededAdded += 1; }
|
|
1973
2313
|
}
|
|
1974
|
-
if (
|
|
2314
|
+
if (!seededAdded) break;
|
|
1975
2315
|
const closed = await commitPendingMarks({
|
|
1976
2316
|
snapshot,
|
|
1977
|
-
state:
|
|
2317
|
+
state: seeded,
|
|
1978
2318
|
generation: lifecycle.generation,
|
|
1979
2319
|
retainIneligible: true,
|
|
1980
|
-
guardCurrentTurn: true,
|
|
1981
2320
|
});
|
|
1982
2321
|
if (!closed.applied.length) break;
|
|
1983
2322
|
result = {
|
|
1984
2323
|
...closed,
|
|
2324
|
+
state: withPendingMarks(closed.state, [...pendingMarks(closed.state), ...heldBack]),
|
|
1985
2325
|
applied: [...result.applied, ...closed.applied],
|
|
1986
2326
|
refused: [...result.refused.filter((mark) => !mark.retained), ...closed.refused],
|
|
1987
|
-
|
|
2327
|
+
retained: [...result.retained.filter((mark) =>
|
|
2328
|
+
!closed.applied.some((appliedMark) => appliedMark.id === mark.id)), ...closed.retained],
|
|
1988
2329
|
};
|
|
1989
2330
|
}
|
|
1990
2331
|
persistence.state = result.state;
|
|
@@ -2012,12 +2353,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2012
2353
|
applied_marks: result.applied.length,
|
|
2013
2354
|
refused_marks: result.refused.filter((mark) => !mark.retained).length,
|
|
2014
2355
|
deferred_marks: result.retained.length,
|
|
2015
|
-
waived_marks: result.waived.length,
|
|
2016
2356
|
pending_marks: accounting.pending,
|
|
2017
2357
|
agent_marks: accounting.agentMarks,
|
|
2018
2358
|
ladder_marks: accounting.ladderMarks,
|
|
2019
2359
|
peek_marks: peekAdded,
|
|
2020
2360
|
topup_marks: topUpAdded,
|
|
2361
|
+
refold_marks: refoldAdded,
|
|
2021
2362
|
consolidation_marks: consolidationAdded,
|
|
2022
2363
|
closing_consolidation_marks: closingAdded,
|
|
2023
2364
|
absorbed_wedges: wedges.absorbed.length,
|
|
@@ -2033,36 +2374,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2033
2374
|
instrumentation.mutationsSinceHandoff += 1;
|
|
2034
2375
|
instrumentation.lastMutationRequest = instrumentation.requests;
|
|
2035
2376
|
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
2377
|
let foldedToolResults = 0;
|
|
2067
2378
|
let foldedSpans = 0;
|
|
2068
2379
|
for (const applied of result.applied) {
|
|
@@ -2093,7 +2404,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2093
2404
|
end_id: wedge.endId,
|
|
2094
2405
|
entries: wedge.entries,
|
|
2095
2406
|
tokens: wedge.tokens,
|
|
2096
|
-
|
|
2407
|
+
chars: wedge.chars,
|
|
2408
|
+
threshold_chars: thresholds.minFoldChars,
|
|
2097
2409
|
});
|
|
2098
2410
|
}
|
|
2099
2411
|
const epoch = {
|
|
@@ -2105,6 +2417,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2105
2417
|
ladderMarks: accounting.ladderMarks,
|
|
2106
2418
|
peekMarks: peekAdded,
|
|
2107
2419
|
topUpMarks: topUpAdded,
|
|
2420
|
+
refoldMarks: refoldAdded,
|
|
2108
2421
|
consolidationMarks: consolidationAdded,
|
|
2109
2422
|
closingMarks: closingAdded,
|
|
2110
2423
|
absorbedWedges: wedges.absorbed.length,
|
|
@@ -2114,8 +2427,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2114
2427
|
occupancyTokensBefore: usedTokens,
|
|
2115
2428
|
depthFloorShare: commitDepthFloorShare(snapshot),
|
|
2116
2429
|
reclaimFloorShare: COMMIT_RECLAIM_FLOOR_SHARE,
|
|
2117
|
-
guardWaived: result.waived.length > 0,
|
|
2118
|
-
waivedMarks: result.waived.length,
|
|
2119
2430
|
appliedMarks: result.applied.length,
|
|
2120
2431
|
refusedMarks: result.refused.filter((mark) => !mark.retained).length,
|
|
2121
2432
|
deferredMarks: result.retained.length,
|
|
@@ -2124,8 +2435,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2124
2435
|
protectedStaleBytes: pinHeld.bytes,
|
|
2125
2436
|
protectedStaleRefs: pinHeld.refs,
|
|
2126
2437
|
retainedMarks: result.retained.length,
|
|
2127
|
-
currentTurnRetained: result.retained.filter((mark) =>
|
|
2128
|
-
markTouchesCurrentTurn(state, mark, guarded)).length,
|
|
2129
2438
|
eligibleMarks: accounting.eligibleMarks,
|
|
2130
2439
|
estimatedRewriteTokens: accounting.rewriteTokens,
|
|
2131
2440
|
estimatedFreedTokens: accounting.freedTokens,
|
|
@@ -2186,59 +2495,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2186
2495
|
}));
|
|
2187
2496
|
};
|
|
2188
2497
|
|
|
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
2498
|
/**
|
|
2236
|
-
*
|
|
2237
|
-
*
|
|
2238
|
-
*
|
|
2239
|
-
*
|
|
2240
|
-
*
|
|
2499
|
+
* A COMMIT, THROUGH THE SAME TRANSACTION AND THE SAME QUEUE. Two callers reach it: the
|
|
2500
|
+
* compaction boundary, which is the ordinary one, and the projection fence, which is
|
|
2501
|
+
* the emergency one. The fence cannot wait for a boundary because the request it is
|
|
2502
|
+
* holding does not fit, so it commits in place; that is the one exception, and it is
|
|
2503
|
+
* the exception that already existed.
|
|
2241
2504
|
*/
|
|
2505
|
+
|
|
2242
2506
|
const runAutomaticTransaction = async (
|
|
2243
2507
|
snapshot: ActiveContextSnapshot,
|
|
2244
2508
|
ratio: number,
|
|
@@ -2291,25 +2555,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2291
2555
|
ladder.actionQueue = queued.catch(() => undefined);
|
|
2292
2556
|
return queued;
|
|
2293
2557
|
};
|
|
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
2558
|
const attemptAutomaticCommit = (
|
|
2314
2559
|
snapshot: ActiveContextSnapshot,
|
|
2315
2560
|
ctx: any,
|
|
@@ -2495,14 +2740,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2495
2740
|
return { messages: event.messages };
|
|
2496
2741
|
}
|
|
2497
2742
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
)
|
|
2502
|
-
|
|
2503
|
-
mutationAttempted = true;
|
|
2504
|
-
persistedSucceeded = true;
|
|
2505
|
-
}
|
|
2743
|
+
// NO EAGER LADDER MARKING (Shane, 2026-08-21): the rung used to stage a mark
|
|
2744
|
+
// here on every measured response, which crowded the agent out of curation
|
|
2745
|
+
// entirely -- by the time an agent looked, every eligible batch was already
|
|
2746
|
+
// marked. The commit epoch fills fresh at commit time (topUpMarks), which is
|
|
2747
|
+
// the only automatic marking there is.
|
|
2506
2748
|
} else measurements.lastProviderMeasurement = observedMeasurement;
|
|
2507
2749
|
} else {
|
|
2508
2750
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
@@ -2524,6 +2766,23 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2524
2766
|
updateStatus(ctx);
|
|
2525
2767
|
return { messages: event.messages };
|
|
2526
2768
|
}
|
|
2769
|
+
// THE FRONTIER CUTS FIRST, and it costs the projection nothing to do so: a pending
|
|
2770
|
+
// mark moves no bytes, so this runs BEFORE the band top and the fence and changes
|
|
2771
|
+
// neither of their inputs. What it changes is WHEN the agent learns a fold exists,
|
|
2772
|
+
// which is now while the material it covers is still in front of it.
|
|
2773
|
+
advanceFoldFrontier(snapshot);
|
|
2774
|
+
// BEFORE the fence, so an ordinary crossing is an ordinary commit and the fence is
|
|
2775
|
+
// left holding only the requests that genuinely will not fit.
|
|
2776
|
+
if (await enforceBandTop(snapshot, ctx)) {
|
|
2777
|
+
mutationAttempted = true;
|
|
2778
|
+
persistedSucceeded = true;
|
|
2779
|
+
try { projected = projectWithAdvisory(snapshot); }
|
|
2780
|
+
catch (error) {
|
|
2781
|
+
suspendAutomatic(error, "projection", ctx);
|
|
2782
|
+
abortUnsafeHardContext(snapshot, ctx);
|
|
2783
|
+
return { messages: event.messages };
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2527
2786
|
const budgeted = await enforceProjectionBudget(snapshot, projected, ctx);
|
|
2528
2787
|
projected = budgeted.projected;
|
|
2529
2788
|
const reading = projectedTokenReading(projected);
|
|
@@ -2598,13 +2857,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2598
2857
|
if (measurements.latestRatio >= hardFenceRatio(measurement, ctx) && ladder.preparing) await ladder.preparing.promise;
|
|
2599
2858
|
if (!sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration) ||
|
|
2600
2859
|
!durableProviderMeasurementMatches(measurement)) return;
|
|
2601
|
-
|
|
2602
|
-
authoritativeSnapshotFor(ctx),
|
|
2603
|
-
measurements.latestRatio,
|
|
2604
|
-
ctx,
|
|
2605
|
-
"message-end",
|
|
2606
|
-
);
|
|
2607
|
-
if (!action && measurementStateChanged && persistence.state && persistence.persisted &&
|
|
2860
|
+
if (measurementStateChanged && persistence.state && persistence.persisted &&
|
|
2608
2861
|
!sameStateProjection(persistence.state, persistence.persisted)) {
|
|
2609
2862
|
await persistThroughActionQueue(ctx);
|
|
2610
2863
|
}
|
|
@@ -2629,6 +2882,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2629
2882
|
model: ownValue(message, "model") ?? null,
|
|
2630
2883
|
input_tokens: observation.inputTokens,
|
|
2631
2884
|
cache_read_tokens: observation.cacheReadTokens,
|
|
2885
|
+
cache_write_tokens: observation.cacheWriteTokens,
|
|
2632
2886
|
total_tokens: providerTokens(message),
|
|
2633
2887
|
projection_change: observation.change,
|
|
2634
2888
|
provider_side_miss: observation.providerSideMiss,
|
|
@@ -2672,7 +2926,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2672
2926
|
durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
|
|
2673
2927
|
startPreparation(snapshot, measurements.latestRatio, ctx);
|
|
2674
2928
|
if (measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) await ladder.preparing.promise;
|
|
2675
|
-
|
|
2929
|
+
// No rung here either: turn end stages nothing (see the context-path note).
|
|
2676
2930
|
}
|
|
2677
2931
|
ladder.pendingManual = false;
|
|
2678
2932
|
if (!ladder.automaticFailure) ladder.boundaryFailure = null;
|
|
@@ -3001,6 +3255,203 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3001
3255
|
);
|
|
3002
3256
|
};
|
|
3003
3257
|
|
|
3258
|
+
/**
|
|
3259
|
+
* THE ONE STAGING PATH FOR FOLD MARKS (2026-08-23). The tool's fold action and the
|
|
3260
|
+
* /fold-editor's user marks run THIS code and nothing else: span snapping, claim
|
|
3261
|
+
* refusal, brief contract, prepareFold, replacement of a standing mark over the
|
|
3262
|
+
* same span. Only the ORIGIN differs -- "agent" for the model, "user" for a human
|
|
3263
|
+
* laying marks in the editor -- and origin changes nothing mechanical: a user mark
|
|
3264
|
+
* counts toward the commit's coverage, answers with its commit arithmetic, and
|
|
3265
|
+
* folds at the epoch exactly as an agent mark does.
|
|
3266
|
+
*/
|
|
3267
|
+
/**
|
|
3268
|
+
* THE WITHDRAWAL PATH, shared by the tool's unmark action and the editor's
|
|
3269
|
+
* withdraw intent: same refusal for an unknown id, same persist, same status.
|
|
3270
|
+
*/
|
|
3271
|
+
const withdrawMarks = async (ids: string[], ctx: any): Promise<Array<Record<string, unknown>>> => {
|
|
3272
|
+
const requested = new Set(ids);
|
|
3273
|
+
const before = pendingMarks(persistence.state!);
|
|
3274
|
+
const kept = before.filter((mark) => !requested.has(mark.id));
|
|
3275
|
+
const removed = before.filter((mark) => requested.has(mark.id));
|
|
3276
|
+
const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
|
|
3277
|
+
if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
|
|
3278
|
+
await persistManual(withPendingMarks(persistence.state!, kept), "unmark", ctx);
|
|
3279
|
+
updateStatus(ctx);
|
|
3280
|
+
return removed as Array<Record<string, unknown>>;
|
|
3281
|
+
};
|
|
3282
|
+
|
|
3283
|
+
/**
|
|
3284
|
+
* THE PIN CHANGE PATH, shared by the tool's pin/unpin actions and the editor's
|
|
3285
|
+
* pin toggle: the pinned-share cap, protectEvidence, the canonical event, persist.
|
|
3286
|
+
*/
|
|
3287
|
+
const applyProtectionChange = async (
|
|
3288
|
+
snapshot: ActiveContextSnapshot,
|
|
3289
|
+
ids: string[],
|
|
3290
|
+
pin: boolean,
|
|
3291
|
+
ctx: any,
|
|
3292
|
+
): Promise<void> => {
|
|
3293
|
+
const refsBefore = persistence.state!.protected.length;
|
|
3294
|
+
const nextProtection = protectEvidence(snapshot, persistence.state!, ids, pin);
|
|
3295
|
+
if (pin) {
|
|
3296
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
3297
|
+
const prospectiveShare = capacity.budgetTokens > 0
|
|
3298
|
+
? estimatedTokens(explicitProtectedMass(snapshot, nextProtection).bytes) / capacity.budgetTokens
|
|
3299
|
+
: 0;
|
|
3300
|
+
if (prospectiveShare > MAX_PINNED_SHARE) {
|
|
3301
|
+
throw new Error(
|
|
3302
|
+
`protect refused: these pins would hold ${Math.round(prospectiveShare * 100)}% of the ` +
|
|
3303
|
+
`working window raw, past the ${Math.round(MAX_PINNED_SHARE * 100)}% pinned-share cap. ` +
|
|
3304
|
+
'Release earlier pins with {"action":"unpin","ids":["<entry-id>"]} first; ' +
|
|
3305
|
+
"folding keeps entries exactly recoverable without pinning them.");
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
await persistManual(nextProtection, pin ? "pin" : "unpin", ctx);
|
|
3309
|
+
emit("context.pin", {
|
|
3310
|
+
pin,
|
|
3311
|
+
ids: ids.join(","),
|
|
3312
|
+
id_count: ids.length,
|
|
3313
|
+
protected_refs_before: refsBefore,
|
|
3314
|
+
protected_refs_after: persistence.state!.protected.length,
|
|
3315
|
+
});
|
|
3316
|
+
updateStatus(ctx);
|
|
3317
|
+
};
|
|
3318
|
+
|
|
3319
|
+
const stageFoldMarks = async (
|
|
3320
|
+
requests: BatchedMarkRequest[],
|
|
3321
|
+
origin: MarkOrigin,
|
|
3322
|
+
signal: AbortSignal,
|
|
3323
|
+
ctx: any,
|
|
3324
|
+
snapshot: ActiveContextSnapshot,
|
|
3325
|
+
): Promise<{ marks: Array<Record<string, unknown>>; corrections: SpanCorrection[]; staged: ActiveContextState }> => {
|
|
3326
|
+
const resolved: Array<{
|
|
3327
|
+
candidate: FoldCandidate;
|
|
3328
|
+
brief?: string;
|
|
3329
|
+
corrections: SpanCorrection[];
|
|
3330
|
+
splitFrom: number;
|
|
3331
|
+
}> = [];
|
|
3332
|
+
for (const request of requests) {
|
|
3333
|
+
const snapped = snapFoldCandidate(snapshot, persistence.state, request.ids, { allowProtected: true });
|
|
3334
|
+
// AN AGENT'S SPAN IS TAKEN WHOLE (Shane, 2026-08-22). The bite-size cap was
|
|
3335
|
+
// applied to manual marks too, and it was the only caller: the ladder never
|
|
3336
|
+
// splits, because `selectAutomaticChapter` stops accumulating at the cap on its
|
|
3337
|
+
// own. So the split existed solely to second-guess the one judgement the design
|
|
3338
|
+
// wants to encourage, and it charged for it twice. One intended fold became five
|
|
3339
|
+
// or ten placeholders, and every piece was handed the SAME brief, so each one
|
|
3340
|
+
// claimed to describe a span it held a fraction of, while the correction text
|
|
3341
|
+
// said "each with its own brief", which was never true.
|
|
3342
|
+
//
|
|
3343
|
+
// The cap stays for the ladder, where it was earned (2026-08-06 rep 6: one
|
|
3344
|
+
// 60,432-byte chapter hid the fact the run needed). The difference is who chose
|
|
3345
|
+
// the boundary. A ladder fold is a span nobody vouched for, so it is kept small
|
|
3346
|
+
// enough to read back cheaply. An agent fold is a span the agent cut and wrote a
|
|
3347
|
+
// brief for, and it stays navigable by a route that did not exist in 2026-08:
|
|
3348
|
+
// peek takes offset and bytes, states the omitted middle, and reads any child id,
|
|
3349
|
+
// so a large fold has a narrow read. An expand that will not fit is refused with
|
|
3350
|
+
// a peek offered instead, so size costs a refusal rather than a window.
|
|
3351
|
+
resolved.push({
|
|
3352
|
+
candidate: snapped.candidate,
|
|
3353
|
+
brief: request.brief,
|
|
3354
|
+
corrections: snapped.corrections,
|
|
3355
|
+
splitFrom: 0,
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
const corrections = resolved.flatMap((item) => item.corrections);
|
|
3359
|
+
const marks: Array<Record<string, unknown>> = [];
|
|
3360
|
+
let staged = persistence.state!;
|
|
3361
|
+
for (const item of resolved) {
|
|
3362
|
+
const { candidate } = item;
|
|
3363
|
+
const alreadyPrepared = staged.folds.some((fold) =>
|
|
3364
|
+
fold.id === foldIdFor(candidate.kind, candidate.parts));
|
|
3365
|
+
const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
|
|
3366
|
+
(candidate.kind === "tool-result" &&
|
|
3367
|
+
refsProtected(candidate.sourceRefs, staged, snapshot));
|
|
3368
|
+
// A MARK IS EDITABLE UNTIL IT COMMITS (Shane, 2026-08-21). Marking a span you
|
|
3369
|
+
// already marked REPLACES the mark, so a brief written in haste is corrected by
|
|
3370
|
+
// writing it again: the span is identical, the id derives from the span, and no
|
|
3371
|
+
// byte has moved yet, so the correction costs nothing at all. After the commit
|
|
3372
|
+
// the fold is standing and there is no rewrite verb, because rewriting a
|
|
3373
|
+
// standing brief rewrites the projection at its placeholder; `reboundary`
|
|
3374
|
+
// re-cuts the span instead, which was going to rewrite anyway.
|
|
3375
|
+
const replacing = foldIdFor(candidate.kind, candidate.parts);
|
|
3376
|
+
const claimed = markClaimingRef(staged, candidate.sourceRefs);
|
|
3377
|
+
if (claimed && claimed.markId !== replacing) {
|
|
3378
|
+
marks.push({
|
|
3379
|
+
id: replacing,
|
|
3380
|
+
kind: candidate.kind,
|
|
3381
|
+
ok: false,
|
|
3382
|
+
deferred: false,
|
|
3383
|
+
reason: `Evidence ${claimed.entryId} is already held by pending mark ${claimed.markId}, ` +
|
|
3384
|
+
`which folds at the next commit. Unmark ${claimed.markId} first if you want to ` +
|
|
3385
|
+
"mark this span differently.",
|
|
3386
|
+
});
|
|
3387
|
+
continue;
|
|
3388
|
+
}
|
|
3389
|
+
const suppliedComplaint = item.brief === undefined
|
|
3390
|
+
? null
|
|
3391
|
+
: briefContractComplaint(item.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
|
|
3392
|
+
if (suppliedComplaint !== null) {
|
|
3393
|
+
marks.push({
|
|
3394
|
+
id: foldIdFor(candidate.kind, candidate.parts),
|
|
3395
|
+
kind: candidate.kind,
|
|
3396
|
+
ok: false,
|
|
3397
|
+
deferred: false,
|
|
3398
|
+
reason: `Supplied brief rejected. ${suppliedComplaint}`,
|
|
3399
|
+
});
|
|
3400
|
+
continue;
|
|
3401
|
+
}
|
|
3402
|
+
const briefed = !deferred && !alreadyPrepared
|
|
3403
|
+
? await prepareFold({
|
|
3404
|
+
candidate,
|
|
3405
|
+
snapshot,
|
|
3406
|
+
state: staged,
|
|
3407
|
+
generation: lifecycle.generation,
|
|
3408
|
+
brief: item.brief,
|
|
3409
|
+
ctx,
|
|
3410
|
+
signal,
|
|
3411
|
+
})
|
|
3412
|
+
: null;
|
|
3413
|
+
const mark = foldMarkFor({
|
|
3414
|
+
candidate,
|
|
3415
|
+
brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
|
|
3416
|
+
briefProvenance: briefed?.fold.provenance ??
|
|
3417
|
+
(item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
|
|
3418
|
+
origin,
|
|
3419
|
+
ordinal: markOrdinal(snapshot),
|
|
3420
|
+
});
|
|
3421
|
+
// Drop the standing mark over this exact span before adding, so a re-mark is a
|
|
3422
|
+
// replacement rather than a duplicate refusal. `claimed` is this same mark by the
|
|
3423
|
+
// check above, and the id is derived from the span, so the state after is the
|
|
3424
|
+
// state a first mark with the new brief would have produced.
|
|
3425
|
+
const replaced = claimed !== null;
|
|
3426
|
+
if (replaced) {
|
|
3427
|
+
staged = withPendingMarks(staged,
|
|
3428
|
+
pendingMarks(staged).filter((standing) => standing.id !== replacing));
|
|
3429
|
+
}
|
|
3430
|
+
const addition = addPendingMark(staged, mark);
|
|
3431
|
+
if (!addition.added) {
|
|
3432
|
+
marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
|
|
3433
|
+
continue;
|
|
3434
|
+
}
|
|
3435
|
+
staged = addition.state;
|
|
3436
|
+
marks.push({
|
|
3437
|
+
id: mark.id,
|
|
3438
|
+
kind: mark.kind,
|
|
3439
|
+
ok: true,
|
|
3440
|
+
deferred,
|
|
3441
|
+
...(replaced ? { replaced: true } : {}),
|
|
3442
|
+
...(deferred
|
|
3443
|
+
? {
|
|
3444
|
+
scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
|
|
3445
|
+
"first commit after it ages out",
|
|
3446
|
+
}
|
|
3447
|
+
: {}),
|
|
3448
|
+
brief: mark.brief,
|
|
3449
|
+
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
return { marks, corrections, staged };
|
|
3453
|
+
};
|
|
3454
|
+
|
|
3004
3455
|
const executeAction = async (
|
|
3005
3456
|
params: Record<string, unknown>,
|
|
3006
3457
|
signal: AbortSignal | undefined,
|
|
@@ -3010,6 +3461,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3010
3461
|
if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
|
|
3011
3462
|
const executionArgumentsSha256 = sha256Value(params);
|
|
3012
3463
|
const action = String(params.action ?? "");
|
|
3464
|
+
// A deleted action is refused BY NAME rather than by "not enabled": the two are
|
|
3465
|
+
// different facts, and an agent told a verb is merely off will keep offering it.
|
|
3466
|
+
if (Object.prototype.hasOwnProperty.call(RETIRED_TOOL_ACTIONS, action)) {
|
|
3467
|
+
throw new Error(RETIRED_TOOL_ACTIONS[action]);
|
|
3468
|
+
}
|
|
3013
3469
|
if (!allowedToolActionSet.has(action)) {
|
|
3014
3470
|
throw new Error(`${toolName} action '${action}' is not enabled in this runtime`);
|
|
3015
3471
|
}
|
|
@@ -3038,7 +3494,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3038
3494
|
persistence.state,
|
|
3039
3495
|
statusOffset,
|
|
3040
3496
|
statusLimit,
|
|
3041
|
-
snapshot.policy.maxFoldSourceRefs,
|
|
3042
3497
|
{ diet: !paged },
|
|
3043
3498
|
),
|
|
3044
3499
|
headroomTokens: currentCapacity(ctx).headroomTokens,
|
|
@@ -3056,7 +3511,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3056
3511
|
hardFenceRatio: hardFenceRatio(snapshot),
|
|
3057
3512
|
thresholds: { ...snapshot.thresholds },
|
|
3058
3513
|
zones: {
|
|
3059
|
-
freshBoundary: snapshot.freshBoundary,
|
|
3060
3514
|
budgetTokens: snapshot.budgetTokens,
|
|
3061
3515
|
},
|
|
3062
3516
|
responseReserve: currentCapacity(ctx).outputReservation,
|
|
@@ -3121,6 +3575,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3121
3575
|
state: persistence.state,
|
|
3122
3576
|
ratio: measurements.latestRatio,
|
|
3123
3577
|
}),
|
|
3578
|
+
// WHAT THE RUNTIME HAS CUT AND NOT YET FOLDED, by name, because the agent needs
|
|
3579
|
+
// an id to brief one and a notice it saw two turns ago is not a reference. Read
|
|
3580
|
+
// from the LIVE state rather than the durable record: a frontier cut is derived
|
|
3581
|
+
// from its span and is not written on its own, so the durable record is behind
|
|
3582
|
+
// by design and only a brief or a commit brings it forward.
|
|
3583
|
+
pendingFolds: pendingMarks(persistence.state).map((mark) => ({
|
|
3584
|
+
id: mark.id,
|
|
3585
|
+
kind: mark.kind,
|
|
3586
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
3587
|
+
briefed: mark.briefProvenance?.kind === "supplied",
|
|
3588
|
+
brief: mark.brief ?? null,
|
|
3589
|
+
briefProvenance: mark.briefProvenance ? clone(mark.briefProvenance) : null,
|
|
3590
|
+
})),
|
|
3124
3591
|
nativeSummaries: "disabled",
|
|
3125
3592
|
instrumentation: {
|
|
3126
3593
|
enabled: true,
|
|
@@ -3152,12 +3619,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3152
3619
|
if (action === "peek") {
|
|
3153
3620
|
const id = String(params.id ?? "").trim();
|
|
3154
3621
|
if (!id) throw new Error("peek requires id");
|
|
3622
|
+
if (params.ephemeral !== undefined && typeof params.ephemeral !== "boolean") {
|
|
3623
|
+
throw new Error("peek ephemeral must be a boolean");
|
|
3624
|
+
}
|
|
3155
3625
|
const offset = boundedInteger(params.offset, 0, 0, 1_000_000_000, "offset");
|
|
3156
3626
|
const sliceBytes = boundedInteger(
|
|
3157
3627
|
params.bytes,
|
|
3158
3628
|
PEEK_DEFAULT_MAX_BYTES,
|
|
3159
3629
|
PEEK_MIN_SLICE_BYTES,
|
|
3160
|
-
snapshot.policy.
|
|
3630
|
+
snapshot.policy.maxSourceChars,
|
|
3161
3631
|
"bytes",
|
|
3162
3632
|
);
|
|
3163
3633
|
const target = persistence.state.folds.find((item) => item.id === id);
|
|
@@ -3169,40 +3639,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3169
3639
|
requestedBytes: Math.max(0, Math.min(sliceBytes, target.sourceChars - offset)),
|
|
3170
3640
|
children: childFoldIds(target),
|
|
3171
3641
|
});
|
|
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.",
|
|
3642
|
+
const payload = toolPayload({
|
|
3643
|
+
...peekFoldSource({
|
|
3644
|
+
foldId: id,
|
|
3645
|
+
state: persistence.state,
|
|
3646
|
+
entries: ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch(),
|
|
3647
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
3648
|
+
maximumBytes: sliceBytes,
|
|
3649
|
+
offset,
|
|
3650
|
+
toolName,
|
|
3651
|
+
}),
|
|
3652
|
+
...(peekIsEphemeral(params) ? {
|
|
3653
|
+
ephemeral: "this result rides your context exactly until your next message; " +
|
|
3654
|
+
"extract what your current task needs into that reply, which takes over its " +
|
|
3655
|
+
"place. Peeking again is lossless and cheap; pass ephemeral false when you " +
|
|
3656
|
+
"need these bytes standing for several turns",
|
|
3657
|
+
} : { durable: "this result stays in your context like any other tool result" }),
|
|
3205
3658
|
});
|
|
3659
|
+
return payload;
|
|
3206
3660
|
}
|
|
3207
3661
|
if (action === "reboundary") {
|
|
3208
3662
|
if (params.ids !== undefined) {
|
|
@@ -3230,7 +3684,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3230
3684
|
const recut = manualFoldCandidate(snapshot, persistence.state, snapped.ids, { allowProtected: true });
|
|
3231
3685
|
const created: Array<Record<string, unknown>> = [];
|
|
3232
3686
|
const correctedBriefs: Record<string, string> = {};
|
|
3233
|
-
for
|
|
3687
|
+
// Re-cut whole, for the same reason a mark is taken whole: the agent named this
|
|
3688
|
+
// boundary, and splitting it hands one brief to several folds that each hold a
|
|
3689
|
+
// fraction of what it describes.
|
|
3690
|
+
for (const part of [recut]) {
|
|
3234
3691
|
const durable = persistence.persistedFoldRecords.get(foldIdFor(part.kind, part.parts));
|
|
3235
3692
|
const { preparedFold, nextState } = await prepareAndCommitExplicit({
|
|
3236
3693
|
snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied, ctx, signal,
|
|
@@ -3348,52 +3805,51 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3348
3805
|
activation: "durable immediately; projected on the next model call in this same turn",
|
|
3349
3806
|
});
|
|
3350
3807
|
}
|
|
3351
|
-
if (action === "
|
|
3808
|
+
if (action === "pin" || action === "unpin") {
|
|
3352
3809
|
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,
|
|
3810
|
+
await applyProtectionChange(snapshot, ids, action === "pin", ctx);
|
|
3811
|
+
const refsAfter = persistence.state!.protected.length;
|
|
3812
|
+
// A PIN IS A CLAIM ON THE WINDOW, so it answers with the same picture a mark does.
|
|
3813
|
+
// It never pays the drop, and saying so here is what stops an agent pinning its way
|
|
3814
|
+
// to a coverage it does not have.
|
|
3815
|
+
const pinCapacity = servingCapacity(snapshot.contextWindow);
|
|
3816
|
+
const pinClaims = windowClaims({
|
|
3817
|
+
snapshot,
|
|
3818
|
+
state: persistence.state,
|
|
3819
|
+
budgetTokens: pinCapacity.budgetTokens,
|
|
3820
|
+
charsPerToken: projectionCharsPerToken(),
|
|
3821
|
+
});
|
|
3822
|
+
const pinCoverage = commitCoverage({
|
|
3823
|
+
snapshot,
|
|
3824
|
+
state: persistence.state,
|
|
3825
|
+
usedTokens: pinCapacity.usedTokens,
|
|
3826
|
+
budgetTokens: pinCapacity.budgetTokens,
|
|
3376
3827
|
});
|
|
3377
|
-
updateStatus(ctx);
|
|
3378
3828
|
return toolPayload({
|
|
3379
3829
|
version: 1,
|
|
3380
3830
|
action,
|
|
3381
3831
|
ids,
|
|
3382
3832
|
protectedRefs: refsAfter,
|
|
3383
|
-
|
|
3833
|
+
pinnedTokens: pinClaims.pinnedTokens,
|
|
3834
|
+
pinnedShare: pinClaims.pinnedShare,
|
|
3835
|
+
maxPinnedShare: MAX_PINNED_SHARE,
|
|
3836
|
+
markedTowardCommitTokens: pinClaims.markedTokens,
|
|
3837
|
+
unclaimedTokens: pinClaims.unclaimedTokens,
|
|
3838
|
+
commitTargetTokens: pinCoverage.targetTokens,
|
|
3839
|
+
unmarkedAgainstCommitTokens: pinCoverage.remainingTokens,
|
|
3840
|
+
activation: action === "pin"
|
|
3841
|
+
? "durable immediately; projected on the next model call in this same turn. " +
|
|
3842
|
+
"Pinned entries stay RAW and are never folded, automatically or by a mark, so " +
|
|
3843
|
+
"this is how you keep a span expanded. It frees nothing: the next commit still " +
|
|
3844
|
+
"has to reach its drop from what is left, so pinned mass makes the rest of the " +
|
|
3845
|
+
"window fold sooner rather than later."
|
|
3846
|
+
: "durable immediately; projected on the next model call in this same turn. The " +
|
|
3847
|
+
"released entries are foldable again and rejoin the next commit's candidates.",
|
|
3384
3848
|
});
|
|
3385
3849
|
}
|
|
3386
3850
|
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);
|
|
3851
|
+
const removed = await withdrawMarks(stringIds(params.ids), ctx);
|
|
3852
|
+
const accounting = markAccounting(snapshot, persistence.state!);
|
|
3397
3853
|
return toolPayload({
|
|
3398
3854
|
version: 1,
|
|
3399
3855
|
action,
|
|
@@ -3405,162 +3861,67 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3405
3861
|
activation: "the withdrawn marks will never be applied; no context bytes moved",
|
|
3406
3862
|
});
|
|
3407
3863
|
}
|
|
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
|
-
}
|
|
3864
|
+
if (action === "brief") {
|
|
3865
|
+
// THE ONE EDIT THAT IS FREE. A pending mark is not in the window, so writing its
|
|
3866
|
+
// brief moves no bytes and costs no prefix cache; the brief reaches the projection
|
|
3867
|
+
// only when the commit writes that placeholder for the first time. A STANDING fold
|
|
3868
|
+
// is the opposite case and is refused by name: its placeholder is already in the
|
|
3869
|
+
// window, and rewriting it breaks the cache from that point on, which is the
|
|
3870
|
+
// rebrief deletion of 2026-08-21 read from the other side.
|
|
3871
|
+
const id = String(params.id ?? "").trim();
|
|
3872
|
+
if (!id) throw new Error("brief needs the id of a pending fold");
|
|
3873
|
+
const standing = persistence.state!.folds.find((fold) => fold.id === id);
|
|
3874
|
+
if (standing) {
|
|
3875
|
+
throw new Error(
|
|
3876
|
+
`${id} is a standing fold, not a pending one, so its brief is already in your ` +
|
|
3877
|
+
"window and rewriting it would rewrite the projection from that placeholder on. " +
|
|
3878
|
+
`Read it with ${toolName} {"action":"peek","id":"${id}"}, or re-cut the span with ` +
|
|
3879
|
+
`${toolName} {"action":"reboundary","ids":[...]} if the boundary is wrong.`,
|
|
3880
|
+
);
|
|
3449
3881
|
}
|
|
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
|
-
});
|
|
3882
|
+
const held = pendingMarks(persistence.state!);
|
|
3883
|
+
const target = held.find((mark) => mark.id === id);
|
|
3884
|
+
if (!target) {
|
|
3885
|
+
const names = held.slice(0, 5).map((mark) => mark.id).join(", ");
|
|
3886
|
+
throw new Error(
|
|
3887
|
+
`No pending fold ${id}. ${held.length
|
|
3888
|
+
? `The folds waiting for a brief are: ${names}${held.length > 5 ? ", ..." : ""}.`
|
|
3889
|
+
: "Nothing is pending right now, so there is nothing to brief."}`,
|
|
3890
|
+
);
|
|
3512
3891
|
}
|
|
3513
|
-
|
|
3892
|
+
const text = String(params.brief ?? "").trim();
|
|
3893
|
+
const complaint = briefContractComplaint(text, snapshot.policy.maxBriefChars, snapshot.toolName);
|
|
3894
|
+
if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
|
|
3895
|
+
const nextState = {
|
|
3896
|
+
...persistence.state!,
|
|
3897
|
+
revision: persistence.state!.revision + 1,
|
|
3898
|
+
pendingMarks: held.map((mark) => mark.id === id
|
|
3899
|
+
? { ...mark, brief: text, briefProvenance: { kind: "supplied" as const } }
|
|
3900
|
+
: mark),
|
|
3901
|
+
};
|
|
3902
|
+
persistence.state = nextState;
|
|
3903
|
+
await persistManual(nextState, action, ctx);
|
|
3514
3904
|
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;
|
|
3905
|
+
emit("context.brief", { id, chars: text.length, origin: "agent" });
|
|
3906
|
+
const accounting = markAccounting(snapshot, persistence.state!);
|
|
3907
|
+
const remainder = unmarkedRemainder(snapshot, persistence.state!, projectionCharsPerToken());
|
|
3523
3908
|
return toolPayload({
|
|
3524
3909
|
version: 1,
|
|
3525
3910
|
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,
|
|
3911
|
+
id,
|
|
3912
|
+
kind: target.kind,
|
|
3913
|
+
brief: text,
|
|
3914
|
+
provenance: "agent",
|
|
3541
3915
|
argumentsSha256: executionArgumentsSha256,
|
|
3542
|
-
durableRevision: persistence.state
|
|
3916
|
+
durableRevision: persistence.state!.revision,
|
|
3543
3917
|
pendingMarks: accounting.pending,
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
estimatedFreedWindowShare: accounting.freedBudgetShare,
|
|
3547
|
-
estimatedEligibleWindowShare: accounting.eligibleFreedBudgetShare,
|
|
3548
|
-
estimatedRewriteTokens: accounting.rewriteTokens,
|
|
3549
|
-
held,
|
|
3550
|
-
heldNote: "held until it ages out or the next fold event",
|
|
3918
|
+
unbriefed: pendingMarks(persistence.state!)
|
|
3919
|
+
.filter((mark) => mark.briefProvenance?.kind !== "supplied").map((mark) => mark.id),
|
|
3551
3920
|
unmarkedSpans: remainder.spans,
|
|
3552
3921
|
unmarkedTokens: remainder.tokens,
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
awareness: markAwarenessText({ held, remainder, toolName, brandNoun }),
|
|
3557
|
-
activation: "accepted as pending marks; no context bytes moved, and nothing else in your " +
|
|
3558
|
-
"context changed either. They apply together at the next commit epoch, which the runtime " +
|
|
3559
|
-
"opens at the fold event. " +
|
|
3560
|
-
"A mark over a still-fresh span is held, not refused: it is scheduled, and it folds at the " +
|
|
3561
|
-
"first commit after that span ages out of the fresh window. " +
|
|
3562
|
-
"Mark several spans in one call: this whole picture comes back with each one.",
|
|
3563
|
-
} : {}),
|
|
3922
|
+
activation: "durable immediately, and it costs your window nothing: this fold is " +
|
|
3923
|
+
"pending, so the brief you just wrote is stored outside the projection and " +
|
|
3924
|
+
"reaches it only when the commit folds this span. Nothing in your context moved.",
|
|
3564
3925
|
});
|
|
3565
3926
|
}
|
|
3566
3927
|
throw new Error(`Unknown ${toolName} action '${action}'`);
|
|
@@ -3590,6 +3951,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3590
3951
|
marks_requested: requestedMarkCount(params),
|
|
3591
3952
|
corrections_applied: corrections.length,
|
|
3592
3953
|
arguments_sha256: sha256Value(params ?? {}),
|
|
3954
|
+
...(action === "peek" ? { ephemeral: peekIsEphemeral(params) } : {}),
|
|
3593
3955
|
});
|
|
3594
3956
|
for (const correction of corrections) {
|
|
3595
3957
|
const from = denseOwnArrayValues(ownValue(correction, "from")) ?? [];
|
|
@@ -3609,6 +3971,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3609
3971
|
const result = await executeAction(params, signal, ctx);
|
|
3610
3972
|
const corrections = denseOwnArrayValues(ownValue(ownValue(result, "details"), "corrections"));
|
|
3611
3973
|
attempt(true, null, (corrections ?? []) as Array<Record<string, unknown>>);
|
|
3974
|
+
if (action === "peek" && peekIsEphemeral(params)) ephemeralPeeks.add(toolCallId);
|
|
3612
3975
|
return result;
|
|
3613
3976
|
} catch (error) {
|
|
3614
3977
|
attempt(false, error instanceof Error ? error.message : String(error), []);
|
|
@@ -3642,7 +4005,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3642
4005
|
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
3643
4006
|
const divider = args.indexOf(" -- ");
|
|
3644
4007
|
const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
|
|
3645
|
-
|
|
4008
|
+
// BARE /fold IS THE COMMIT (Shane, dogfooding 2026-08-22): staging is the
|
|
4009
|
+
// normal accumulation and applying it is the default verb. The old bare behavior
|
|
4010
|
+
// rescue-folded an auto-offered chapter, which read as the command ignoring the
|
|
4011
|
+
// staged pile; a rescue now needs explicit ids.
|
|
4012
|
+
if (!selector || selector === "commit") {
|
|
3646
4013
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
3647
4014
|
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
3648
4015
|
? capacity.usedTokens / capacity.budgetTokens
|
|
@@ -3665,16 +4032,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3665
4032
|
committed
|
|
3666
4033
|
? `Committed ${ownValue(committed, "applied_marks") ?? 0} mark(s)${topUp ? " with automatic top-up" : ""}. ` +
|
|
3667
4034
|
"Exact source remains expandable."
|
|
3668
|
-
: "Nothing
|
|
4035
|
+
: "Nothing to commit: no staged mark could apply (pinned or already-folded spans hold).",
|
|
3669
4036
|
"info",
|
|
3670
4037
|
);
|
|
3671
4038
|
return;
|
|
3672
4039
|
}
|
|
3673
4040
|
const supplied = (divider >= 0 ? args.slice(divider + 4) : "").trim() || undefined;
|
|
3674
4041
|
const ids = selector ? selector.replace(/\.\./g, " ").split(/[\s,]+/).filter(Boolean) : [];
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
4042
|
+
if (!ids.length) {
|
|
4043
|
+
throw new Error(
|
|
4044
|
+
"Nothing to rescue without ids: /fold with no arguments commits every staged mark; " +
|
|
4045
|
+
"name the span to fold now (/fold <start-id> [end-id] -- brief)",
|
|
4046
|
+
);
|
|
4047
|
+
}
|
|
4048
|
+
const candidate = manualFoldCandidate(snapshot, persistence.state!, ids);
|
|
3678
4049
|
if (!candidate) throw new Error("No exact stale rescue span is currently eligible");
|
|
3679
4050
|
const stateBefore = persistence.state!;
|
|
3680
4051
|
const persistedBefore = persistence.persisted ? clone(persistence.persisted) : null;
|
|
@@ -3721,6 +4092,200 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3721
4092
|
}
|
|
3722
4093
|
};
|
|
3723
4094
|
|
|
4095
|
+
/**
|
|
4096
|
+
* /fold-editor, V2.2. The handler assembles a plain data snapshot of the working
|
|
4097
|
+
* window and hands it to the view; the view holds no runtime reference and no
|
|
4098
|
+
* mutation path. What changed in V2.2: raw entries are MARK POINTS, and a span
|
|
4099
|
+
* laid down between two of them stages through `stageFoldMarks` -- the SAME
|
|
4100
|
+
* validated path the tool's fold action runs -- with origin "user". The view
|
|
4101
|
+
* computes nothing but the arithmetic it is handed (spanCost) and reports one
|
|
4102
|
+
* intent (onStageMark); this handler owns every state transition.
|
|
4103
|
+
*/
|
|
4104
|
+
const foldEditorCommandHandler = async (_args: string, ctx: any): Promise<void> => {
|
|
4105
|
+
if (typeof ctx.ui?.custom !== "function") {
|
|
4106
|
+
throw new Error("/fold-editor needs an interactive UI; use /fold-status for the text form");
|
|
4107
|
+
}
|
|
4108
|
+
const snapshot = lifecycle.latestSnapshot
|
|
4109
|
+
? authoritativeSnapshotFor(ctx)
|
|
4110
|
+
: snapshotForEvent(ctx, ctx.sessionManager.buildSessionContext().messages);
|
|
4111
|
+
if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
|
|
4112
|
+
if (!persistence.state) throw new Error("The context window is empty; there is nothing to show yet");
|
|
4113
|
+
|
|
4114
|
+
// THE WOULD-BE FOLD SIZE (Shane, 2026-08-23). The footer's number while a span is
|
|
4115
|
+
// being laid down is the runtime's own estimate -- span bytes over the mapped
|
|
4116
|
+
// window at ESTIMATED_BYTES_PER_TOKEN -- so what the user sees before staging is
|
|
4117
|
+
// what the staged mark answers with after it.
|
|
4118
|
+
const spanCost = (from: number, to: number) => ({
|
|
4119
|
+
entries: Math.abs(to - from) + 1,
|
|
4120
|
+
tokens: estimatedTokens(spanBytes(snapshot, Math.min(from, to), Math.max(from, to) + 1)),
|
|
4121
|
+
});
|
|
4122
|
+
|
|
4123
|
+
// Marks move no bytes until the commit, so window geometry cannot change while
|
|
4124
|
+
// the editor holds it open; only pending state grows. Rebuilding against the SAME
|
|
4125
|
+
// snapshot with fresh state keeps PROPOSED rows honest without re-deriving indices.
|
|
4126
|
+
const buildData = () => {
|
|
4127
|
+
const state = persistence.state!;
|
|
4128
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
4129
|
+
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
4130
|
+
? capacity.usedTokens / capacity.budgetTokens
|
|
4131
|
+
: null;
|
|
4132
|
+
const accounting = markAccounting(snapshot, state);
|
|
4133
|
+
return {
|
|
4134
|
+
title: `pi-fold context editor · ${snapshot.sessionId.slice(0, 8)}`,
|
|
4135
|
+
occupancy: {
|
|
4136
|
+
usedTokens: capacity.usedTokens,
|
|
4137
|
+
budgetTokens: capacity.budgetTokens,
|
|
4138
|
+
commitOccupancy: thresholds.maxTarget,
|
|
4139
|
+
commitDue: occupancy !== null && occupancy >= thresholds.maxTarget,
|
|
4140
|
+
},
|
|
4141
|
+
blocks: buildFoldEditorData(snapshot, state, {
|
|
4142
|
+
foldRows: () => (state.folds ?? []).map((fold: any) => {
|
|
4143
|
+
const row = foldIndexRow(fold, state, snapshot);
|
|
4144
|
+
const entries = flattenFoldRefs(fold, state)
|
|
4145
|
+
.map((ref) => {
|
|
4146
|
+
const item = exactMapped(snapshot, ref);
|
|
4147
|
+
const message = item?.message as Record<string, unknown> | undefined;
|
|
4148
|
+
const role = String(item?.ref?.role ?? message?.role ?? "entry");
|
|
4149
|
+
const content = message?.content;
|
|
4150
|
+
let preview = "";
|
|
4151
|
+
if (typeof content === "string") preview = content.slice(0, 300);
|
|
4152
|
+
else if (Array.isArray(content)) {
|
|
4153
|
+
const firstText = content.find((part) => part?.type === "text");
|
|
4154
|
+
preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
|
|
4155
|
+
}
|
|
4156
|
+
return { id: ref.entryId, role, preview: preview.replace(/\s+/g, " ") };
|
|
4157
|
+
});
|
|
4158
|
+
return {
|
|
4159
|
+
id: String(row.id),
|
|
4160
|
+
kind: String(row.kind ?? ""),
|
|
4161
|
+
brief: String(row.brief ?? ""),
|
|
4162
|
+
sourceCount: Number(row.sourceCount ?? 0),
|
|
4163
|
+
startPosition: Number(row.startPosition),
|
|
4164
|
+
endPosition: Number(row.endPosition),
|
|
4165
|
+
entries,
|
|
4166
|
+
};
|
|
4167
|
+
}),
|
|
4168
|
+
pendingMarkRefs: () => pendingMarks(state)
|
|
4169
|
+
.filter((mark) => mark.mark === "fold")
|
|
4170
|
+
.map((mark) => ({
|
|
4171
|
+
id: mark.id,
|
|
4172
|
+
origin: String(mark.origin),
|
|
4173
|
+
brief: String(mark.brief ?? ""),
|
|
4174
|
+
// WHAT THE STAGED MARK FREES, so a PROPOSED row states its size the
|
|
4175
|
+
// way the footer prices an unstaged span.
|
|
4176
|
+
...(typeof markFreedBytes(snapshot, state, mark) === "number"
|
|
4177
|
+
? { tokens: estimatedTokens(markFreedBytes(snapshot, state, mark)) }
|
|
4178
|
+
: {}),
|
|
4179
|
+
entryIds: markSpanRefs(state, mark).refs
|
|
4180
|
+
.map((ref) => ref.entryId)
|
|
4181
|
+
.filter((entryId): entryId is string => Boolean(entryId)),
|
|
4182
|
+
})),
|
|
4183
|
+
mappedRange: (from: number, to: number) => {
|
|
4184
|
+
const out: Array<{ id: string; role: string; preview: string; index?: number }> = [];
|
|
4185
|
+
for (let index = from; index <= to; index += 1) {
|
|
4186
|
+
const item = snapshot.mapped[index];
|
|
4187
|
+
if (!item) continue;
|
|
4188
|
+
const message = item.message as Record<string, unknown> | undefined;
|
|
4189
|
+
const role = String(item.ref?.role ?? message?.role ?? "entry");
|
|
4190
|
+
const content = message?.content;
|
|
4191
|
+
// 300 chars, same as fold entries: the view shows 48 on the row and
|
|
4192
|
+
// deepens to 240 on Enter, so a 48-char preview made detail a no-op.
|
|
4193
|
+
let preview = "";
|
|
4194
|
+
if (typeof content === "string") preview = content.slice(0, 300);
|
|
4195
|
+
else if (Array.isArray(content)) {
|
|
4196
|
+
const firstText = content.find((part) => part?.type === "text");
|
|
4197
|
+
preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
|
|
4198
|
+
}
|
|
4199
|
+
// THE INDEX IS THE MARK POINT: raw entries carry their mapped position so
|
|
4200
|
+
// the view can price a span and name its two boundary ids.
|
|
4201
|
+
out.push({
|
|
4202
|
+
id: item.ref?.entryId ?? String(index),
|
|
4203
|
+
role,
|
|
4204
|
+
preview: preview.replace(/\s+/g, " "),
|
|
4205
|
+
...(item.ref?.entryId ? { index } : {}),
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4208
|
+
return out;
|
|
4209
|
+
},
|
|
4210
|
+
entryCount: snapshot.mapped.length,
|
|
4211
|
+
}),
|
|
4212
|
+
pending: {
|
|
4213
|
+
count: pendingMarks(state).length,
|
|
4214
|
+
agentMarks: accounting.agentMarks,
|
|
4215
|
+
ladderMarks: accounting.ladderMarks,
|
|
4216
|
+
userMarks: accounting.userMarks,
|
|
4217
|
+
freedTokens: accounting.freedTokens,
|
|
4218
|
+
},
|
|
4219
|
+
pinned: state.protected.map((ref) => ref.entryId),
|
|
4220
|
+
};
|
|
4221
|
+
};
|
|
4222
|
+
|
|
4223
|
+
let view: FoldEditorView | null = null;
|
|
4224
|
+
const runQueued = async (work: () => Promise<void>): Promise<void> => {
|
|
4225
|
+
const operation = ladder.actionQueue.then(work);
|
|
4226
|
+
ladder.actionQueue = operation.catch(() => undefined);
|
|
4227
|
+
await operation;
|
|
4228
|
+
};
|
|
4229
|
+
const refreshAfter = (): void => {
|
|
4230
|
+
if (view) view.refresh(buildData());
|
|
4231
|
+
};
|
|
4232
|
+
const editorActions = {
|
|
4233
|
+
spanCost,
|
|
4234
|
+
onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
|
|
4235
|
+
await runQueued(async () => {
|
|
4236
|
+
const fresh = authoritativeSnapshotFor(ctx);
|
|
4237
|
+
const request = brief && brief.trim()
|
|
4238
|
+
? { ids: [fromId, toId], brief: brief.trim() }
|
|
4239
|
+
: { ids: [fromId, toId] };
|
|
4240
|
+
const { marks, staged } = await stageFoldMarks(
|
|
4241
|
+
batchedMarkRequests(request), "user",
|
|
4242
|
+
new AbortController().signal, ctx, fresh);
|
|
4243
|
+
// Refuse BEFORE persisting: an all-refused batch leaves state untouched and
|
|
4244
|
+
// persisting it would be a no-op write against the same-state projection.
|
|
4245
|
+
if (!marks.some((mark) => mark.ok === true)) {
|
|
4246
|
+
const reason = marks.map((mark) => mark.reason).filter(Boolean).join("; ") ||
|
|
4247
|
+
"the span was refused";
|
|
4248
|
+
throw new Error(reason);
|
|
4249
|
+
}
|
|
4250
|
+
await persistManual(staged, "fold", ctx);
|
|
4251
|
+
updateStatus(ctx);
|
|
4252
|
+
});
|
|
4253
|
+
safeNotify(ctx, "Staged user mark(s); they apply at the next commit epoch.", "info");
|
|
4254
|
+
refreshAfter();
|
|
4255
|
+
},
|
|
4256
|
+
onWithdrawMark: async (markId: string): Promise<void> => {
|
|
4257
|
+
let removed: Array<Record<string, unknown>> = [];
|
|
4258
|
+
await runQueued(async () => {
|
|
4259
|
+
removed = await withdrawMarks([markId], ctx) as Array<Record<string, unknown>>;
|
|
4260
|
+
});
|
|
4261
|
+
safeNotify(ctx,
|
|
4262
|
+
`Withdrew ${removed.length} staged mark(s); no context bytes moved.`, "info");
|
|
4263
|
+
refreshAfter();
|
|
4264
|
+
},
|
|
4265
|
+
onTogglePin: async (entryId: string): Promise<void> => {
|
|
4266
|
+
const pinnedAlready = persistence.state!.protected
|
|
4267
|
+
.some((ref: any) => ref.entryId === entryId);
|
|
4268
|
+
await runQueued(async () => {
|
|
4269
|
+
await applyProtectionChange(authoritativeSnapshotFor(ctx), [entryId], !pinnedAlready, ctx);
|
|
4270
|
+
});
|
|
4271
|
+
safeNotify(ctx, pinnedAlready
|
|
4272
|
+
? "Unpinned: the released entries rejoin the next commit's candidates."
|
|
4273
|
+
: "Pinned raw: held through every fold; frees nothing toward the drop.", "info");
|
|
4274
|
+
refreshAfter();
|
|
4275
|
+
},
|
|
4276
|
+
};
|
|
4277
|
+
await ctx.ui.custom((
|
|
4278
|
+
_tui: unknown,
|
|
4279
|
+
theme: any,
|
|
4280
|
+
keybindings: { matches(data: string, action: string): boolean } | null,
|
|
4281
|
+
done: () => void,
|
|
4282
|
+
) => {
|
|
4283
|
+
view = new FoldEditorView(buildData(), done, keybindings, theme, editorActions);
|
|
4284
|
+
return view;
|
|
4285
|
+
});
|
|
4286
|
+
updateStatus(ctx);
|
|
4287
|
+
};
|
|
4288
|
+
|
|
3724
4289
|
pi.registerTool(buildActiveContextTool({
|
|
3725
4290
|
name: toolName,
|
|
3726
4291
|
label: toolLabel,
|
|
@@ -3735,8 +4300,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3735
4300
|
for (const command of buildActiveContextCommands({
|
|
3736
4301
|
statusName: commandNames.status,
|
|
3737
4302
|
foldName: commandNames.fold,
|
|
4303
|
+
editorName: "fold-editor",
|
|
3738
4304
|
statusHandler: statusCommandHandler,
|
|
3739
4305
|
foldHandler: foldCommandHandler,
|
|
4306
|
+
editorHandler: foldEditorCommandHandler,
|
|
3740
4307
|
})) {
|
|
3741
4308
|
pi.registerCommand(command.name, {
|
|
3742
4309
|
description: command.description,
|