pi-fold 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -211
- package/extensions/active-context.ts +483 -244
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +4 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +46 -9
- package/extensions/lib/folding.ts +181 -17
- package/extensions/lib/instrumentation.ts +0 -6
- package/extensions/lib/measurement.ts +37 -4
- package/extensions/lib/persistence.ts +212 -9
- package/extensions/lib/policy.ts +103 -29
- package/extensions/lib/scheduling.ts +105 -18
- package/extensions/lib/selection.ts +112 -39
- package/extensions/lib/tool-surface.ts +37 -6
- package/extensions/lib/transcript.ts +65 -14
- package/extensions/settings.ts +11 -36
- package/package.json +1 -1
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
stableStringify,
|
|
7
7
|
} from "./json.ts";
|
|
8
8
|
import {
|
|
9
|
+
contentText,
|
|
9
10
|
bytes,
|
|
10
11
|
clone,
|
|
11
12
|
emptyActiveContextState,
|
|
12
13
|
ownValue,
|
|
14
|
+
serializedPrefixHolds,
|
|
13
15
|
sessionEntryMessages,
|
|
14
16
|
uniqueMessageDigestAnchor,
|
|
15
17
|
} from "./lib/canonical.ts";
|
|
@@ -70,6 +72,7 @@ import {
|
|
|
70
72
|
deriveFoldParents,
|
|
71
73
|
flattenFoldRefs,
|
|
72
74
|
foldIdFor,
|
|
75
|
+
foldRecordRef,
|
|
73
76
|
makeFoldRecordEntry,
|
|
74
77
|
makeStateCheckpoint,
|
|
75
78
|
makeStateDelta,
|
|
@@ -104,6 +107,7 @@ import {
|
|
|
104
107
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
105
108
|
entryTypeNamespace,
|
|
106
109
|
MAX_FOLD_SPAN_CHARS,
|
|
110
|
+
DEFAULT_TOOL_FOLD_THRESHOLD,
|
|
107
111
|
MAX_PINNED_SHARE,
|
|
108
112
|
PEEK_DEFAULT_MAX_BYTES,
|
|
109
113
|
peekIsEphemeral,
|
|
@@ -112,17 +116,22 @@ import {
|
|
|
112
116
|
RETIRED_TOOL_ACTIONS,
|
|
113
117
|
AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
114
118
|
USER_RESCUE_MAX_SOURCE_CHARS,
|
|
119
|
+
MEMORY_BODY_CHARS,
|
|
120
|
+
MEMORY_KEY_CHARS,
|
|
121
|
+
MEMORY_KEYS_MAX,
|
|
122
|
+
WORKING_MEMORY_TOOL_ACTIONS,
|
|
115
123
|
} from "./lib/policy.ts";
|
|
116
124
|
import type {
|
|
117
125
|
ActiveContextSnapshot,
|
|
118
126
|
ActiveContextState,
|
|
119
127
|
ActiveContextThresholds,
|
|
120
128
|
ActiveContextToolAction,
|
|
129
|
+
BriefProvenance,
|
|
121
130
|
FoldCandidate,
|
|
122
131
|
FoldKind,
|
|
123
132
|
FoldRecordEntry,
|
|
124
|
-
MarkOrigin,
|
|
125
133
|
PreparedFold,
|
|
134
|
+
WorkingMemoryEntry,
|
|
126
135
|
} from "./lib/policy.ts";
|
|
127
136
|
import {
|
|
128
137
|
absorbWedgeMarks,
|
|
@@ -131,6 +140,7 @@ import {
|
|
|
131
140
|
import {
|
|
132
141
|
contextReceipt,
|
|
133
142
|
foldNoticeText,
|
|
143
|
+
memoryTocText,
|
|
134
144
|
receiptBlockText,
|
|
135
145
|
withReceipt,
|
|
136
146
|
} from "./lib/curation.ts";
|
|
@@ -142,6 +152,7 @@ import {
|
|
|
142
152
|
commitCoverage,
|
|
143
153
|
windowClaims,
|
|
144
154
|
commitPendingMarks,
|
|
155
|
+
toolClipAdditions,
|
|
145
156
|
consolidationMarks,
|
|
146
157
|
ephemeralPeekMarks,
|
|
147
158
|
epochCommitDue,
|
|
@@ -149,6 +160,7 @@ import {
|
|
|
149
160
|
foldMarkFor,
|
|
150
161
|
ladderBrief,
|
|
151
162
|
markAccounting,
|
|
163
|
+
markRewriteTokens,
|
|
152
164
|
markFreedBytes,
|
|
153
165
|
unmarkedRemainder,
|
|
154
166
|
markClaimingRef,
|
|
@@ -211,38 +223,6 @@ export * from "./lib/scheduling.ts";
|
|
|
211
223
|
export * from "./lib/selection.ts";
|
|
212
224
|
export * from "./lib/transcript.ts";
|
|
213
225
|
|
|
214
|
-
export interface BatchedMarkRequest {
|
|
215
|
-
ids: string[];
|
|
216
|
-
brief?: string;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function batchedMarkRequests(params: Record<string, unknown>): BatchedMarkRequest[] {
|
|
220
|
-
const batched = params.marks;
|
|
221
|
-
if (batched === undefined) {
|
|
222
|
-
return [{
|
|
223
|
-
ids: stringIds(params.ids),
|
|
224
|
-
...(typeof params.brief === "string" && params.brief.trim() ? { brief: params.brief } : {}),
|
|
225
|
-
}];
|
|
226
|
-
}
|
|
227
|
-
const values = denseOwnArrayValues(batched);
|
|
228
|
-
if (!values || !values.length || values.length > 64) {
|
|
229
|
-
throw new Error("marks must be a dense array of 1-64 {ids, brief} objects");
|
|
230
|
-
}
|
|
231
|
-
return values.map((value, index) => {
|
|
232
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
233
|
-
throw new Error(`marks[${index}] must be an object with ids and an optional brief`);
|
|
234
|
-
}
|
|
235
|
-
const brief = ownValue(value, "brief");
|
|
236
|
-
if (brief !== undefined && (typeof brief !== "string" || !brief.trim())) {
|
|
237
|
-
throw new Error(`marks[${index}].brief must be a nonempty string`);
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
ids: stringIds(ownValue(value, "ids")),
|
|
241
|
-
...(typeof brief === "string" && brief.trim() ? { brief } : {}),
|
|
242
|
-
};
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
226
|
export function registerActiveContext(pi: any, options: {
|
|
247
227
|
toolName?: string;
|
|
248
228
|
toolLabel?: string;
|
|
@@ -252,6 +232,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
252
232
|
blacklistAutoFoldTools?: ReadonlySet<string>;
|
|
253
233
|
providerInputBudget?: number;
|
|
254
234
|
thresholds?: ActiveContextThresholds;
|
|
235
|
+
/** THE TOOL-CALL DIET's share point (2026-08-24; default 0.50 since 2026-08-28, Shane,
|
|
236
|
+
* on the sol-20260826-full2 verdict): tool results in the oldest toolFoldThreshold
|
|
237
|
+
* share of the projected window are clipped IN VIEW at each commit, identified head
|
|
238
|
+
* kept, full bytes peek-recoverable behind the entry id the marker names. A share in
|
|
239
|
+
* [0, 1), where 0 turns the diet off; 0.50 is what every fold repetition from 3 onward
|
|
240
|
+
* ran, so the default is the value that was measured rather than the absence of one. */
|
|
241
|
+
toolFoldThreshold?: number;
|
|
242
|
+
/** THE INVITATION SWITCH (public since 2026-08-27; DEFAULT FALSE since 2026-08-28,
|
|
243
|
+
* Shane, on the sol-20260826-full2 verdict). false is the deterministic shape the
|
|
244
|
+
* campaign measured and the one it recommends: no carrier ever invites a brief and
|
|
245
|
+
* every fold goes out with the runtime's own words, which drew 14, 14, 9 and 8 correct
|
|
246
|
+
* against the invited condition's 3. true restores the standing invitation for the
|
|
247
|
+
* agent to improve a brief. Either way the agent verbs stay on the tool, so an agent
|
|
248
|
+
* that finds a fold can still annotate what it reads. */
|
|
249
|
+
postFoldNotice?: boolean;
|
|
250
|
+
/** THE WORKING MEMORY (Shane, 2026-08-26): a session-scoped ordered dictionary the
|
|
251
|
+
* agent maintains beside the fold index. `remember` writes or removes an entry,
|
|
252
|
+
* `recall` reads bodies on demand, and the projection carries one table of contents
|
|
253
|
+
* that refreshes at each commit. Off when absent: the digest channel earned its
|
|
254
|
+
* place in a measured gap (running tallies lost 0/6 against native's 6/6 while the
|
|
255
|
+
* index won everything else), and it must stay separable from the index it is not. */
|
|
256
|
+
workingMemory?: boolean;
|
|
255
257
|
}): {
|
|
256
258
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
257
259
|
} {
|
|
@@ -260,6 +262,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
260
262
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
261
263
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
262
264
|
const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
|
|
265
|
+
if (options.postFoldNotice !== undefined && typeof options.postFoldNotice !== "boolean") {
|
|
266
|
+
throw new Error("postFoldNotice must be a boolean: true keeps the post-fold invitation " +
|
|
267
|
+
"notice, false ships every fold with the runtime's deterministic brief and no " +
|
|
268
|
+
"carrier inviting the agent to improve it");
|
|
269
|
+
}
|
|
270
|
+
const postFoldNotice = options.postFoldNotice ?? false;
|
|
271
|
+
if (options.workingMemory !== undefined && typeof options.workingMemory !== "boolean") {
|
|
272
|
+
throw new Error("workingMemory must be a boolean: it enables the session-scoped " +
|
|
273
|
+
"working-memory dictionary and its two actions, remember and recall");
|
|
274
|
+
}
|
|
275
|
+
const workingMemory = options.workingMemory === true;
|
|
276
|
+
const toolFoldThreshold = options.toolFoldThreshold ?? DEFAULT_TOOL_FOLD_THRESHOLD;
|
|
277
|
+
if (typeof toolFoldThreshold !== "number" || !Number.isFinite(toolFoldThreshold) ||
|
|
278
|
+
toolFoldThreshold < 0 || toolFoldThreshold >= 1) {
|
|
279
|
+
throw new Error("toolFoldThreshold must be a share in [0, 1): it names the oldest " +
|
|
280
|
+
"fraction of the projected window whose tool results are clipped in view at each " +
|
|
281
|
+
"commit, and 0 turns the diet off");
|
|
282
|
+
}
|
|
263
283
|
for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
|
|
264
284
|
if (Object.hasOwn(options, removed)) {
|
|
265
285
|
throw new Error(`${removed} is no longer an option: epoch scheduling, peek foldability ` +
|
|
@@ -325,6 +345,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
325
345
|
const milestoneProjectionType = `${entryTypePrefix}-milestone`;
|
|
326
346
|
const advisoryProjectionType = `${entryTypePrefix}-advisory`;
|
|
327
347
|
const foldNoticeProjectionType = `${entryTypePrefix}-fold-notice`;
|
|
348
|
+
const memoryTocProjectionType = `${entryTypePrefix}-memory-toc`;
|
|
328
349
|
const receiptProjectionType = `${entryTypePrefix}-receipts`;
|
|
329
350
|
const curationProjectionType = `${entryTypePrefix}-curation`;
|
|
330
351
|
const entryNamespace = entryTypeNamespace(entryTypePrefix);
|
|
@@ -332,7 +353,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
332
353
|
const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
|
|
333
354
|
const contextEventEntryType = `${entryNamespace}-context-event`;
|
|
334
355
|
const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
|
|
335
|
-
const allowedToolActions: readonly ActiveContextToolAction[] =
|
|
356
|
+
const allowedToolActions: readonly ActiveContextToolAction[] = workingMemory
|
|
357
|
+
? [...ACTIVE_CONTEXT_TOOL_ACTIONS, ...WORKING_MEMORY_TOOL_ACTIONS]
|
|
358
|
+
: ACTIVE_CONTEXT_TOOL_ACTIONS;
|
|
336
359
|
const allowedToolActionSet = new Set<string>(allowedToolActions);
|
|
337
360
|
|
|
338
361
|
type AutomaticFailureState = {
|
|
@@ -573,10 +596,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
573
596
|
};
|
|
574
597
|
|
|
575
598
|
const PREFIX_MUTATING_KINDS: ReadonlySet<string> = new Set([
|
|
576
|
-
"context.commit", "context.fold", "context.absorb", "context.
|
|
599
|
+
"context.commit", "context.fold", "context.absorb", "context.recovery",
|
|
577
600
|
]);
|
|
578
601
|
const CACHE_ACTION_REQUEST_CLASS: Readonly<Record<string, string>> = Object.freeze({
|
|
579
|
-
fold: "after-mark",
|
|
580
602
|
peek: "after-peek",
|
|
581
603
|
expand: "after-expand",
|
|
582
604
|
refold: "after-refold",
|
|
@@ -590,8 +612,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
590
612
|
folds_committed: receipt.foldsCommitted,
|
|
591
613
|
folds_created: receipt.foldsCreated,
|
|
592
614
|
freed_tokens: receipt.freedTokens,
|
|
593
|
-
split_folds: receipt.splitFolds,
|
|
594
|
-
split_from_chars: receipt.splitFromChars,
|
|
595
615
|
absorbed_wedges: receipt.absorbedWedges,
|
|
596
616
|
recovered: receipt.recovered,
|
|
597
617
|
protected_bytes: receipt.protectedBytes,
|
|
@@ -897,15 +917,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
897
917
|
if (next.folds.length > MAX_ACTIVE_FOLD_RECORDS) {
|
|
898
918
|
throw new Error("Active-context fold closure exceeds persistence limit");
|
|
899
919
|
}
|
|
920
|
+
// AN ALREADY-DURABLE FOLD NEEDS A DIGEST, NOT A RECORD (2026-08-28). This built the
|
|
921
|
+
// whole entry for every fold on every persist just to compare one hash and continue:
|
|
922
|
+
// `makeFoldRecordEntry` canonicalizes, hashes, then `parseFoldRecordEntry` re-validates,
|
|
923
|
+
// clones the fold again, recomputes `foldIdFor`, hashes a second time and stringifies
|
|
924
|
+
// for the wire cap. Replaying a real sealed session, 98.2 percent of 4,947 calls hit
|
|
925
|
+
// the `continue`, and 126 of 135 persists appended nothing while rebuilding all of it.
|
|
926
|
+
// `foldRecordRef` is the same digest by construction (`makeFoldRecordEntry` stores
|
|
927
|
+
// `canonicalFoldRecord`, and `stateFromFoldRefs` already matches records to refs on
|
|
928
|
+
// this equality at persistence.ts:809); gate 152 pins it. Equal digests mean
|
|
929
|
+
// byte-identical canonical bytes to a record that already passed
|
|
930
|
+
// `parseFoldRecordEntry`, so skipping the rebuild cannot reach a different verdict.
|
|
931
|
+
// Measured 85.0ms to 19.6ms at 400 folds.
|
|
900
932
|
for (const fold of next.folds) {
|
|
901
|
-
const
|
|
902
|
-
const existing = persistence.persistedFoldRecords.get(
|
|
933
|
+
const ref = foldRecordRef(fold);
|
|
934
|
+
const existing = persistence.persistedFoldRecords.get(ref.id);
|
|
903
935
|
if (existing) {
|
|
904
|
-
if (existing.recordSha256 !==
|
|
905
|
-
throw new Error(`Conflicting durable active-context fold ${
|
|
936
|
+
if (existing.recordSha256 !== ref.sha256) {
|
|
937
|
+
throw new Error(`Conflicting durable active-context fold ${ref.id}`);
|
|
906
938
|
}
|
|
907
939
|
continue;
|
|
908
940
|
}
|
|
941
|
+
const record = makeFoldRecordEntry(fold, sessionId);
|
|
909
942
|
await pi.appendEntry(foldRecordEntryType, record);
|
|
910
943
|
persistence.persistedFoldRecords.set(record.foldId, record);
|
|
911
944
|
if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
|
|
@@ -914,13 +947,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
914
947
|
}
|
|
915
948
|
}
|
|
916
949
|
const wire = persistence.persistedWireVersion === 2 ? makeStateDelta(persistence.persisted, next) : makeStateCheckpoint(next);
|
|
917
|
-
|
|
950
|
+
// THE WIRE CARRIES BOTH DIGESTS IT JUST DERIVED (2026-08-28). `makeStateDelta` writes
|
|
951
|
+
// `baseStateSha256` from `persistence.persisted` and `stateSha256` from `next`, and
|
|
952
|
+
// nothing mutates either state between that call and here, so re-hashing two whole
|
|
953
|
+
// states was pure repetition at about 7ms per durable write. The drift check keeps its
|
|
954
|
+
// teeth: `baseStateSha256` is still derived from `persistence.persisted`, so a
|
|
955
|
+
// persisted digest that has drifted from the state it claims to describe still raises.
|
|
956
|
+
// Gate 152 pins both equalities.
|
|
957
|
+
if (wire.kind === "delta" && persistence.persistedStateSha256 !== wire.baseStateSha256) {
|
|
918
958
|
throw new Error("Active-context durable base digest drift");
|
|
919
959
|
}
|
|
920
960
|
await pi.appendEntry(stateEntryType, wire);
|
|
921
961
|
persistence.persisted = clone(next);
|
|
922
962
|
persistence.persistedWireVersion = 2;
|
|
923
|
-
persistence.persistedStateSha256 =
|
|
963
|
+
persistence.persistedStateSha256 = wire.stateSha256;
|
|
924
964
|
persistence.state = lifecycle.shuttingDown ? null : clone(next);
|
|
925
965
|
if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
|
|
926
966
|
if (ctx && !contextSessionMatches(ctx, sessionId)) load(ctx);
|
|
@@ -1148,7 +1188,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1148
1188
|
|
|
1149
1189
|
const persistManual = async (
|
|
1150
1190
|
next: ActiveContextState,
|
|
1151
|
-
action: Exclude<ActiveContextToolAction, "status">,
|
|
1152
1191
|
ctx: any,
|
|
1153
1192
|
): Promise<void> => {
|
|
1154
1193
|
const stateAtEntry = persistence.state ? clone(persistence.state) : null;
|
|
@@ -1393,25 +1432,36 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1393
1432
|
};
|
|
1394
1433
|
};
|
|
1395
1434
|
|
|
1396
|
-
|
|
1435
|
+
// THE SERIALIZATION TRAVELS, THE READING DOES NOT (2026-08-28). Six whole-window
|
|
1436
|
+
// serializations rode every provider request and four were the same string from the same
|
|
1437
|
+
// unmutated array. Only the TEXT is threaded, never a finished reading: each site still
|
|
1438
|
+
// re-reads `measurements.projectionAnchor` and `projectionCharsPerToken()` at its own
|
|
1439
|
+
// time, so every emitted number stays what it was. That matters on the commit path, where
|
|
1440
|
+
// `enforceProjectionBudget` awaits `attemptAutomaticCommit` and an `attributionChanged`
|
|
1441
|
+
// can null the anchor between the last measurement and the read: threading a reading would
|
|
1442
|
+
// freeze `basis` at "measured" where today it flips to "unmeasured". Roughly 50ms per
|
|
1443
|
+
// request at a 467KB window, 100ms at 1MB, all of it blocking main-loop CPU. Honest
|
|
1444
|
+
// framing: six become three, not one.
|
|
1445
|
+
const projectedTokenReading = (projected: unknown[], serialized?: string): {
|
|
1397
1446
|
tokens: number;
|
|
1398
1447
|
basis: ProjectionReadingBasis;
|
|
1399
1448
|
chars: number;
|
|
1400
1449
|
anchorTokens: number | null;
|
|
1401
1450
|
deltaChars: number | null;
|
|
1451
|
+
text: string;
|
|
1402
1452
|
} => {
|
|
1403
|
-
const text = stableStringify(projected);
|
|
1453
|
+
const text = serialized ?? stableStringify(projected);
|
|
1404
1454
|
const chars = Buffer.byteLength(text, "utf8");
|
|
1405
1455
|
const charsPerToken = projectionCharsPerToken();
|
|
1406
1456
|
const estimate = Math.ceil(chars / charsPerToken);
|
|
1407
1457
|
const anchor = measurements.projectionAnchor;
|
|
1408
1458
|
if (!anchor || anchor.generation !== lifecycle.generation ||
|
|
1409
1459
|
anchor.sessionId !== persistence.state?.sessionId) {
|
|
1410
|
-
return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null };
|
|
1460
|
+
return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null, text };
|
|
1411
1461
|
}
|
|
1412
1462
|
const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
|
|
1413
1463
|
if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
|
|
1414
|
-
return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null };
|
|
1464
|
+
return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null, text };
|
|
1415
1465
|
}
|
|
1416
1466
|
const deltaChars = chars - anchor.chars;
|
|
1417
1467
|
return {
|
|
@@ -1420,6 +1470,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1420
1470
|
chars,
|
|
1421
1471
|
anchorTokens: anchor.tokens,
|
|
1422
1472
|
deltaChars,
|
|
1473
|
+
text,
|
|
1423
1474
|
};
|
|
1424
1475
|
};
|
|
1425
1476
|
|
|
@@ -1450,7 +1501,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1450
1501
|
estimatorErrorShare() * estimate + expectedInflowTokens(),
|
|
1451
1502
|
));
|
|
1452
1503
|
|
|
1453
|
-
const projectionExceedsBudget = (projected: unknown[], ctx: any): {
|
|
1504
|
+
const projectionExceedsBudget = (projected: unknown[], ctx: any, serialized?: string): {
|
|
1454
1505
|
tokens: number;
|
|
1455
1506
|
sizeTokens: number;
|
|
1456
1507
|
basis: ProjectionReadingBasis;
|
|
@@ -1458,12 +1509,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1458
1509
|
marginTokens: number;
|
|
1459
1510
|
over: boolean;
|
|
1460
1511
|
crowded: boolean;
|
|
1512
|
+
text: string;
|
|
1461
1513
|
} => {
|
|
1462
1514
|
const capacity = currentCapacity(ctx);
|
|
1463
1515
|
const budgetTokens = Number.isFinite(capacity.budgetTokens) && capacity.budgetTokens > 0
|
|
1464
1516
|
? capacity.budgetTokens
|
|
1465
1517
|
: Number.POSITIVE_INFINITY;
|
|
1466
|
-
const reading = projectedTokenReading(projected);
|
|
1518
|
+
const reading = projectedTokenReading(projected, serialized);
|
|
1467
1519
|
const tokens = reading.tokens;
|
|
1468
1520
|
const sizeTokens = Math.ceil(reading.chars / projectionCharsPerToken());
|
|
1469
1521
|
const marginTokens = Number.isFinite(budgetTokens)
|
|
@@ -1477,6 +1529,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1477
1529
|
marginTokens,
|
|
1478
1530
|
over: tokens > budgetTokens,
|
|
1479
1531
|
crowded: tokens + marginTokens > budgetTokens,
|
|
1532
|
+
text: reading.text,
|
|
1480
1533
|
};
|
|
1481
1534
|
};
|
|
1482
1535
|
|
|
@@ -1626,9 +1679,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1626
1679
|
* and it does not: the rebuild reads the marks, not its own last text.
|
|
1627
1680
|
*/
|
|
1628
1681
|
const appendFoldNotice = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1682
|
+
// The deterministic experiment condition: cuts stage and commits land exactly as
|
|
1683
|
+
// ever, but nothing invites a brief, so briefs stay the runtime's own.
|
|
1684
|
+
if (!postFoldNotice) return projected;
|
|
1629
1685
|
if (!persistence.state) return projected;
|
|
1630
1686
|
const held = pendingMarks(persistence.state);
|
|
1631
|
-
const unbriefed = held.filter((mark) =>
|
|
1687
|
+
const unbriefed = held.filter((mark) =>
|
|
1688
|
+
mark.briefProvenance?.kind !== "supplied" && mark.briefProvenance?.kind !== "augmented");
|
|
1632
1689
|
// BATCHED, AND APPENDED ONCE. A notice per cut is noise, and noise is how guidance
|
|
1633
1690
|
// gets ignored; the frontier cuts often and this speaks only when a batch has built
|
|
1634
1691
|
// up. `carrierAdmitted` is what makes it an APPEND rather than a rewrite: once the
|
|
@@ -1649,6 +1706,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1649
1706
|
id: mark.id,
|
|
1650
1707
|
kind: mark.kind,
|
|
1651
1708
|
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
1709
|
+
brief: typeof mark.brief === "string" ? mark.brief : undefined,
|
|
1652
1710
|
})),
|
|
1653
1711
|
pending: held.length,
|
|
1654
1712
|
toolName,
|
|
@@ -1673,6 +1731,42 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1673
1731
|
return projected;
|
|
1674
1732
|
};
|
|
1675
1733
|
|
|
1734
|
+
/**
|
|
1735
|
+
* THE TABLE OF CONTENTS IS ONE COPY PER COMMIT (Shane, 2026-08-26). It is admitted
|
|
1736
|
+
* once per frozen projection, exactly as the fold notice above, and that admission
|
|
1737
|
+
* rule IS the refresh law: between commits the copy stays buried at its index, so a
|
|
1738
|
+
* quiet pass moves no byte and costs no occupancy anchor, and the tool result of every
|
|
1739
|
+
* `remember` carries the live key list in the meantime. A commit rewrites the
|
|
1740
|
+
* projection, the freeze drops, the body filter strips the old copy, and the next pass
|
|
1741
|
+
* seats a fresh one at the tail with the latest keys. Nothing here re-renders in
|
|
1742
|
+
* place, because a carrier that moves on a quiet pass is the exact defect the fold
|
|
1743
|
+
* notice's comment records.
|
|
1744
|
+
*/
|
|
1745
|
+
const appendMemoryToc = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1746
|
+
if (!workingMemory) return projected;
|
|
1747
|
+
if (!persistence.state) return projected;
|
|
1748
|
+
const held = persistence.state.memory ?? [];
|
|
1749
|
+
if (!held.length) return projected;
|
|
1750
|
+
if (!carrierAdmitted("memory-toc")) return projected;
|
|
1751
|
+
const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
|
|
1752
|
+
const text = memoryTocText({
|
|
1753
|
+
entries: entries.map((entry) => ({ key: entry.key, chars: entry.body.length })),
|
|
1754
|
+
toolName,
|
|
1755
|
+
brandNoun,
|
|
1756
|
+
});
|
|
1757
|
+
projected.push({
|
|
1758
|
+
role: "custom",
|
|
1759
|
+
customType: memoryTocProjectionType,
|
|
1760
|
+
content: text,
|
|
1761
|
+
display: false,
|
|
1762
|
+
details: { source: activeContextSource(entryTypePrefix), ephemeral: true },
|
|
1763
|
+
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
1764
|
+
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
1765
|
+
: 0,
|
|
1766
|
+
});
|
|
1767
|
+
return projected;
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1676
1770
|
const enforceBandTop = async (
|
|
1677
1771
|
snapshot: ActiveContextSnapshot,
|
|
1678
1772
|
ctx: any,
|
|
@@ -1711,10 +1805,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1711
1805
|
snapshot: ActiveContextSnapshot,
|
|
1712
1806
|
projected: unknown[],
|
|
1713
1807
|
ctx: any,
|
|
1714
|
-
): Promise<{ projected: unknown[]; aborted: boolean }> => {
|
|
1808
|
+
): Promise<{ projected: unknown[]; aborted: boolean; text: string }> => {
|
|
1715
1809
|
let measured = projectionExceedsBudget(projected, ctx);
|
|
1716
1810
|
const rejected = curation.pendingRejection !== null;
|
|
1717
|
-
if (!measured.crowded && !rejected) return { projected, aborted: false };
|
|
1811
|
+
if (!measured.crowded && !rejected) return { projected, aborted: false, text: measured.text };
|
|
1718
1812
|
const trigger = measured;
|
|
1719
1813
|
let reduced = projected;
|
|
1720
1814
|
let attempts = 0;
|
|
@@ -1818,9 +1912,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1818
1912
|
}));
|
|
1819
1913
|
curation.pendingRejection = null;
|
|
1820
1914
|
}
|
|
1821
|
-
|
|
1915
|
+
// `measured` is recomputed on `reduced` at every turn of the loop above, so its text is
|
|
1916
|
+
// the serialization of the array both of these exits return.
|
|
1917
|
+
if (!measured.over) return { projected: reduced, aborted: false, text: measured.text };
|
|
1822
1918
|
abortOverBudgetProjection(measured.tokens, measured.budgetTokens, ctx, attempts);
|
|
1823
|
-
return { projected: reduced, aborted: true };
|
|
1919
|
+
return { projected: reduced, aborted: true, text: measured.text };
|
|
1824
1920
|
};
|
|
1825
1921
|
|
|
1826
1922
|
const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
|
|
@@ -1975,7 +2071,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1975
2071
|
const customType = ownValue(message, "customType");
|
|
1976
2072
|
return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
|
|
1977
2073
|
customType !== receiptProjectionType && customType !== curationProjectionType &&
|
|
1978
|
-
customType !== foldNoticeProjectionType;
|
|
2074
|
+
customType !== foldNoticeProjectionType && customType !== memoryTocProjectionType;
|
|
1979
2075
|
});
|
|
1980
2076
|
const withdrawn = withdrawConsumedEphemeralPeeks(body);
|
|
1981
2077
|
if (withdrawn.size && freeze.body) {
|
|
@@ -1984,25 +2080,35 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1984
2080
|
if (freeze.projection) carryWithdrawalIntoFreeze(freeze.projection, withdrawn);
|
|
1985
2081
|
}
|
|
1986
2082
|
const held = freeze.body?.length ?? 0;
|
|
1987
|
-
|
|
1988
|
-
|
|
2083
|
+
// THE PREFIX IS READ OUT OF THE WHOLE SERIALIZATION (2026-08-28). This asked
|
|
2084
|
+
// `stableStringify(body.slice(0, held)) === freeze.bodyText`, and in steady state `held`
|
|
2085
|
+
// is `body.length` minus the couple of messages that just arrived, so it was a second
|
|
2086
|
+
// full serialization of the window plus a slice allocation, beside the one below that
|
|
2087
|
+
// every pass does anyway. `serializedPrefixHolds` carries why that is sound; a false
|
|
2088
|
+
// POSITIVE here splices the previous pass's `freeze.projection` in front of the new tail
|
|
2089
|
+
// and emits stale messages, which is a content bug rather than a slowdown, and gate 154
|
|
2090
|
+
// drives the predicate itself rather than restating it.
|
|
2091
|
+
const bodyText = stableStringify(body);
|
|
2092
|
+
freeze.active = freeze.projection !== null &&
|
|
2093
|
+
serializedPrefixHolds(bodyText, freeze.bodyText, held, body.length);
|
|
1989
2094
|
const projected = freeze.active
|
|
1990
2095
|
? [...freeze.projection!, ...body.slice(held)]
|
|
1991
2096
|
: [...body];
|
|
1992
2097
|
if (!freeze.active) freeze.keys.clear();
|
|
1993
2098
|
freeze.body = body;
|
|
1994
|
-
freeze.bodyText =
|
|
2099
|
+
freeze.bodyText = bodyText;
|
|
1995
2100
|
appendReceipts(projected, snapshot);
|
|
1996
2101
|
appendFoldNotice(projected, snapshot);
|
|
2102
|
+
appendMemoryToc(projected, snapshot);
|
|
1997
2103
|
return holdFrozen(projected);
|
|
1998
2104
|
};
|
|
1999
|
-
const noteProjection = (projected: unknown[]): void => {
|
|
2105
|
+
const noteProjection = (projected: unknown[], serialized?: string): void => {
|
|
2000
2106
|
const digests = messageDigests(projected);
|
|
2001
2107
|
const comparison = compareProjections(instrumentation.previousDigests, digests);
|
|
2002
2108
|
recordProjection(instrumentation.ledger, comparison, digests);
|
|
2003
2109
|
instrumentation.previousDigests = digests;
|
|
2004
2110
|
instrumentation.lastChange = comparison.change;
|
|
2005
|
-
const text = stableStringify(projected);
|
|
2111
|
+
const text = serialized ?? stableStringify(projected);
|
|
2006
2112
|
const divergence = prefixDivergence(instrumentation.previousText, text);
|
|
2007
2113
|
const previousChars = instrumentation.previousText?.length ?? 0;
|
|
2008
2114
|
instrumentation.lastPreservedShare = previousChars > 0
|
|
@@ -2030,7 +2136,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2030
2136
|
: latestCacheActionClass
|
|
2031
2137
|
? latestCacheActionClass
|
|
2032
2138
|
: divergence.index === null ? "steady-state" : "after-message";
|
|
2033
|
-
const reading = projectedTokenReading(projected);
|
|
2139
|
+
const reading = projectedTokenReading(projected, text);
|
|
2034
2140
|
emit("context.projection", {
|
|
2035
2141
|
change: comparison.change,
|
|
2036
2142
|
previous_count: comparison.previousCount,
|
|
@@ -2076,6 +2182,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2076
2182
|
snapshot: ActiveContextSnapshot;
|
|
2077
2183
|
candidate: FoldCandidate;
|
|
2078
2184
|
brief?: string;
|
|
2185
|
+
briefProvenance?: "supplied" | "deterministic" | BriefProvenance;
|
|
2079
2186
|
ctx: any;
|
|
2080
2187
|
signal?: AbortSignal;
|
|
2081
2188
|
maximumSourceChars?: number;
|
|
@@ -2093,6 +2200,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2093
2200
|
state: baseState,
|
|
2094
2201
|
generation: generationAtStart,
|
|
2095
2202
|
brief: input.brief,
|
|
2203
|
+
briefProvenance: input.briefProvenance,
|
|
2096
2204
|
ctx: input.ctx,
|
|
2097
2205
|
signal: input.signal,
|
|
2098
2206
|
});
|
|
@@ -2226,6 +2334,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2226
2334
|
});
|
|
2227
2335
|
state = wedges.state;
|
|
2228
2336
|
const accounting = markAccounting(snapshot, state);
|
|
2337
|
+
// Derived ONCE, here, against the pre-commit state both readers below report against.
|
|
2338
|
+
// It left `MarkAccounting` because it serializes the whole marked tail and every other
|
|
2339
|
+
// caller of `markAccounting` was paying for a number only this path reads.
|
|
2340
|
+
const rewriteTokens = markRewriteTokens(snapshot, state);
|
|
2229
2341
|
if (!accounting.pending) {
|
|
2230
2342
|
const remainder = unmarkedRemainder(snapshot, state, projectionCharsPerToken());
|
|
2231
2343
|
emit("context.commit", {
|
|
@@ -2328,6 +2440,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2328
2440
|
!closed.applied.some((appliedMark) => appliedMark.id === mark.id)), ...closed.retained],
|
|
2329
2441
|
};
|
|
2330
2442
|
}
|
|
2443
|
+
// THE TOOL-CALL DIET RIDES THE SAME TRANSACTION (2026-08-24): clips are selected
|
|
2444
|
+
// against the post-fold state and land in the same revision the commit minted, so
|
|
2445
|
+
// the one paid rewrite carries both and no non-commit pass ever moves a byte. The
|
|
2446
|
+
// full bytes stay in the transcript behind each clip's entry id.
|
|
2447
|
+
let clippedResults = 0;
|
|
2448
|
+
if (toolFoldThreshold > 0) {
|
|
2449
|
+
const clipAdditions = toolClipAdditions({
|
|
2450
|
+
snapshot,
|
|
2451
|
+
state: result.state,
|
|
2452
|
+
threshold: toolFoldThreshold,
|
|
2453
|
+
blacklist: blacklistAutoFoldTools,
|
|
2454
|
+
});
|
|
2455
|
+
if (clipAdditions.length) {
|
|
2456
|
+
clippedResults = clipAdditions.length;
|
|
2457
|
+
result = { ...result, state: { ...result.state,
|
|
2458
|
+
clips: [...(result.state.clips ?? []), ...clipAdditions] } };
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2331
2461
|
persistence.state = result.state;
|
|
2332
2462
|
const bytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
|
|
2333
2463
|
const freedBytes = Math.max(0, bytesBefore - bytesAfter);
|
|
@@ -2362,9 +2492,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2362
2492
|
consolidation_marks: consolidationAdded,
|
|
2363
2493
|
closing_consolidation_marks: closingAdded,
|
|
2364
2494
|
absorbed_wedges: wedges.absorbed.length,
|
|
2495
|
+
clipped_results: clippedResults,
|
|
2365
2496
|
freed_bytes: freedBytes,
|
|
2366
2497
|
freed_tokens: estimatedTokens(freedBytes),
|
|
2367
|
-
rewrite_tokens:
|
|
2498
|
+
rewrite_tokens: rewriteTokens,
|
|
2368
2499
|
pinned_bytes: accounting.pinnedBytes,
|
|
2369
2500
|
pinned_results: accounting.pinnedResults,
|
|
2370
2501
|
protected_stale_bytes: pinHeld.bytes,
|
|
@@ -2436,7 +2567,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2436
2567
|
protectedStaleRefs: pinHeld.refs,
|
|
2437
2568
|
retainedMarks: result.retained.length,
|
|
2438
2569
|
eligibleMarks: accounting.eligibleMarks,
|
|
2439
|
-
estimatedRewriteTokens:
|
|
2570
|
+
estimatedRewriteTokens: rewriteTokens,
|
|
2440
2571
|
estimatedFreedTokens: accounting.freedTokens,
|
|
2441
2572
|
freedBudgetShare: accounting.freedBudgetShare,
|
|
2442
2573
|
sourceBytesSaved: freedBytes,
|
|
@@ -2486,8 +2617,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2486
2617
|
occupancyAfter: occupancyBefore === null ? null : Math.max(0, occupancyBefore - freedTokens),
|
|
2487
2618
|
spansFolded: epoch ? Number(epoch.foldedSpans ?? 0) : foldIds,
|
|
2488
2619
|
toolResultsFolded: epoch ? Number(epoch.foldedToolResults ?? 0) : 0,
|
|
2489
|
-
splitFolds: Number(action.splitFolds ?? 0),
|
|
2490
|
-
splitFromChars: Number(action.splitFromChars ?? 0),
|
|
2491
2620
|
absorbedWedges: Number(epoch?.absorbedWedges ?? 0),
|
|
2492
2621
|
recovered: curation.recoveryAttempts > 0,
|
|
2493
2622
|
protectedBytes: Number(epoch?.protectedStaleBytes ?? 0),
|
|
@@ -2785,7 +2914,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2785
2914
|
}
|
|
2786
2915
|
const budgeted = await enforceProjectionBudget(snapshot, projected, ctx);
|
|
2787
2916
|
projected = budgeted.projected;
|
|
2788
|
-
const reading = projectedTokenReading(projected);
|
|
2917
|
+
const reading = projectedTokenReading(projected, budgeted.text);
|
|
2789
2918
|
measurements.lastProjectedChars = reading.chars;
|
|
2790
2919
|
measurements.lastProjectedEstimate = reading.tokens;
|
|
2791
2920
|
measurements.lastProjectedEstimateBasis = reading.basis;
|
|
@@ -2795,7 +2924,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2795
2924
|
updateStatus(ctx);
|
|
2796
2925
|
return { messages: projected };
|
|
2797
2926
|
}
|
|
2798
|
-
noteProjection(projected);
|
|
2927
|
+
noteProjection(projected, budgeted.text);
|
|
2799
2928
|
updateStatus(ctx);
|
|
2800
2929
|
return { messages: projected };
|
|
2801
2930
|
} catch (error) {
|
|
@@ -3255,15 +3384,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3255
3384
|
);
|
|
3256
3385
|
};
|
|
3257
3386
|
|
|
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
3387
|
/**
|
|
3268
3388
|
* THE WITHDRAWAL PATH, shared by the tool's unmark action and the editor's
|
|
3269
3389
|
* withdraw intent: same refusal for an unknown id, same persist, same status.
|
|
@@ -3275,7 +3395,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3275
3395
|
const removed = before.filter((mark) => requested.has(mark.id));
|
|
3276
3396
|
const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
|
|
3277
3397
|
if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
|
|
3278
|
-
await persistManual(withPendingMarks(persistence.state!, kept),
|
|
3398
|
+
await persistManual(withPendingMarks(persistence.state!, kept), ctx);
|
|
3279
3399
|
updateStatus(ctx);
|
|
3280
3400
|
return removed as Array<Record<string, unknown>>;
|
|
3281
3401
|
};
|
|
@@ -3305,7 +3425,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3305
3425
|
"folding keeps entries exactly recoverable without pinning them.");
|
|
3306
3426
|
}
|
|
3307
3427
|
}
|
|
3308
|
-
await persistManual(nextProtection,
|
|
3428
|
+
await persistManual(nextProtection, ctx);
|
|
3309
3429
|
emit("context.pin", {
|
|
3310
3430
|
pin,
|
|
3311
3431
|
ids: ids.join(","),
|
|
@@ -3316,140 +3436,140 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3316
3436
|
updateStatus(ctx);
|
|
3317
3437
|
};
|
|
3318
3438
|
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3439
|
+
/**
|
|
3440
|
+
* THE ONE STAGING PATH FOR FOLD MARKS (2026-08-23). The /fold-editor's user marks
|
|
3441
|
+
* run THIS code and nothing else: span snapping, claim refusal, brief contract,
|
|
3442
|
+
* prepareFold, replacement of a standing mark over the same span. A user mark is
|
|
3443
|
+
* mechanically an ordinary mark: it counts toward the commit's coverage, answers
|
|
3444
|
+
* with its commit arithmetic, and folds at the epoch exactly as a runtime mark does.
|
|
3445
|
+
*/
|
|
3446
|
+
const stageFoldMark = async (
|
|
3447
|
+
ids: string[],
|
|
3448
|
+
supplied: string | undefined,
|
|
3322
3449
|
signal: AbortSignal,
|
|
3323
3450
|
ctx: any,
|
|
3324
3451
|
snapshot: ActiveContextSnapshot,
|
|
3325
|
-
): Promise<{
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
}
|
|
3447
|
-
: {}),
|
|
3448
|
-
brief: mark.brief,
|
|
3449
|
-
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3450
|
-
});
|
|
3451
|
-
}
|
|
3452
|
-
return { marks, corrections, staged };
|
|
3452
|
+
): Promise<{ mark: Record<string, unknown>; corrections: SpanCorrection[]; staged: ActiveContextState }> => {
|
|
3453
|
+
const snapped = snapFoldCandidate(snapshot, persistence.state, ids, { allowProtected: true });
|
|
3454
|
+
// AN AGENT'S SPAN IS TAKEN WHOLE (Shane, 2026-08-22). The bite-size cap was
|
|
3455
|
+
// applied to manual marks too, and it was the only caller: the ladder never
|
|
3456
|
+
// splits, because `selectAutomaticChapter` stops accumulating at the cap on its
|
|
3457
|
+
// own. So the split existed solely to second-guess the one judgement the design
|
|
3458
|
+
// wants to encourage, and it charged for it twice. One intended fold became five
|
|
3459
|
+
// or ten placeholders, and every piece was handed the SAME brief, so each one
|
|
3460
|
+
// claimed to describe a span it held a fraction of, while the correction text
|
|
3461
|
+
// said "each with its own brief", which was never true.
|
|
3462
|
+
//
|
|
3463
|
+
// The cap stays for the ladder, where it was earned (2026-08-06 rep 6: one
|
|
3464
|
+
// 60,432-byte chapter hid the fact the run needed). The difference is who chose
|
|
3465
|
+
// the boundary. A ladder fold is a span nobody vouched for, so it is kept small
|
|
3466
|
+
// enough to read back cheaply. An agent fold is a span the agent cut and wrote a
|
|
3467
|
+
// brief for, and it stays navigable by a route that did not exist in 2026-08:
|
|
3468
|
+
// peek takes offset and bytes, states the omitted middle, and reads any child id,
|
|
3469
|
+
// so a large fold has a narrow read. An expand that will not fit is refused with
|
|
3470
|
+
// a peek offered instead, so size costs a refusal rather than a window.
|
|
3471
|
+
const candidate = snapped.candidate;
|
|
3472
|
+
const corrections = snapped.corrections;
|
|
3473
|
+
let staged = persistence.state!;
|
|
3474
|
+
const alreadyPrepared = staged.folds.some((fold) =>
|
|
3475
|
+
fold.id === foldIdFor(candidate.kind, candidate.parts));
|
|
3476
|
+
const deferred = refsProtected(candidate.sourceRefs, staged, snapshot);
|
|
3477
|
+
// A MARK IS EDITABLE UNTIL IT COMMITS (Shane, 2026-08-21). Marking a span you
|
|
3478
|
+
// already marked REPLACES the mark, so a brief written in haste is corrected by
|
|
3479
|
+
// writing it again: the span is identical, the id derives from the span, and no
|
|
3480
|
+
// byte has moved yet, so the correction costs nothing at all. After the commit
|
|
3481
|
+
// the fold is standing and there is no rewrite verb, because rewriting a
|
|
3482
|
+
// standing brief rewrites the projection at its placeholder; `reboundary`
|
|
3483
|
+
// re-cuts the span instead, which was going to rewrite anyway.
|
|
3484
|
+
const replacing = foldIdFor(candidate.kind, candidate.parts);
|
|
3485
|
+
const claimed = markClaimingRef(staged, candidate.sourceRefs);
|
|
3486
|
+
if (claimed && claimed.markId !== replacing) {
|
|
3487
|
+
return {
|
|
3488
|
+
mark: {
|
|
3489
|
+
id: replacing,
|
|
3490
|
+
kind: candidate.kind,
|
|
3491
|
+
ok: false,
|
|
3492
|
+
deferred: false,
|
|
3493
|
+
reason: `Evidence ${claimed.entryId} is already held by pending mark ${claimed.markId}, ` +
|
|
3494
|
+
`which folds at the next commit. Unmark ${claimed.markId} first if you want to ` +
|
|
3495
|
+
"mark this span differently.",
|
|
3496
|
+
},
|
|
3497
|
+
corrections,
|
|
3498
|
+
staged,
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
const suppliedComplaint = supplied === undefined
|
|
3502
|
+
? null
|
|
3503
|
+
: briefContractComplaint(supplied.trim(), snapshot.policy.agentBriefReserve, snapshot.toolName);
|
|
3504
|
+
if (suppliedComplaint !== null) {
|
|
3505
|
+
return {
|
|
3506
|
+
mark: {
|
|
3507
|
+
id: replacing,
|
|
3508
|
+
kind: candidate.kind,
|
|
3509
|
+
ok: false,
|
|
3510
|
+
deferred: false,
|
|
3511
|
+
reason: `Supplied brief rejected. ${suppliedComplaint}`,
|
|
3512
|
+
},
|
|
3513
|
+
corrections,
|
|
3514
|
+
staged,
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
const briefed = !deferred && !alreadyPrepared
|
|
3518
|
+
? await prepareFold({
|
|
3519
|
+
candidate,
|
|
3520
|
+
snapshot,
|
|
3521
|
+
state: staged,
|
|
3522
|
+
generation: lifecycle.generation,
|
|
3523
|
+
brief: supplied,
|
|
3524
|
+
ctx,
|
|
3525
|
+
signal,
|
|
3526
|
+
})
|
|
3527
|
+
: null;
|
|
3528
|
+
const mark = foldMarkFor({
|
|
3529
|
+
candidate,
|
|
3530
|
+
brief: briefed?.fold.brief ?? supplied ?? ladderBrief(snapshot, staged, candidate),
|
|
3531
|
+
briefProvenance: briefed?.fold.provenance ??
|
|
3532
|
+
(supplied ? { kind: "supplied" } : { kind: "deterministic" }),
|
|
3533
|
+
origin: "user",
|
|
3534
|
+
ordinal: markOrdinal(snapshot),
|
|
3535
|
+
});
|
|
3536
|
+
// Drop the standing mark over this exact span before adding, so a re-mark is a
|
|
3537
|
+
// replacement rather than a duplicate refusal. `claimed` is this same mark by the
|
|
3538
|
+
// check above, and the id is derived from the span, so the state after is the
|
|
3539
|
+
// state a first mark with the new brief would have produced.
|
|
3540
|
+
const replaced = claimed !== null;
|
|
3541
|
+
if (replaced) {
|
|
3542
|
+
staged = withPendingMarks(staged,
|
|
3543
|
+
pendingMarks(staged).filter((standing) => standing.id !== replacing));
|
|
3544
|
+
}
|
|
3545
|
+
const addition = addPendingMark(staged, mark);
|
|
3546
|
+
if (!addition.added) {
|
|
3547
|
+
return {
|
|
3548
|
+
mark: { id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason },
|
|
3549
|
+
corrections,
|
|
3550
|
+
staged,
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
staged = addition.state;
|
|
3554
|
+
return {
|
|
3555
|
+
mark: {
|
|
3556
|
+
id: mark.id,
|
|
3557
|
+
kind: mark.kind,
|
|
3558
|
+
ok: true,
|
|
3559
|
+
deferred,
|
|
3560
|
+
...(replaced ? { replaced: true } : {}),
|
|
3561
|
+
...(deferred
|
|
3562
|
+
? {
|
|
3563
|
+
scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
|
|
3564
|
+
"first commit after it ages out",
|
|
3565
|
+
}
|
|
3566
|
+
: {}),
|
|
3567
|
+
brief: mark.brief,
|
|
3568
|
+
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3569
|
+
},
|
|
3570
|
+
corrections,
|
|
3571
|
+
staged,
|
|
3572
|
+
};
|
|
3453
3573
|
};
|
|
3454
3574
|
|
|
3455
3575
|
const executeAction = async (
|
|
@@ -3584,7 +3704,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3584
3704
|
id: mark.id,
|
|
3585
3705
|
kind: mark.kind,
|
|
3586
3706
|
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
3587
|
-
briefed: mark.briefProvenance?.kind === "supplied"
|
|
3707
|
+
briefed: mark.briefProvenance?.kind === "supplied" ||
|
|
3708
|
+
mark.briefProvenance?.kind === "augmented",
|
|
3588
3709
|
brief: mark.brief ?? null,
|
|
3589
3710
|
briefProvenance: mark.briefProvenance ? clone(mark.briefProvenance) : null,
|
|
3590
3711
|
})),
|
|
@@ -3631,7 +3752,41 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3631
3752
|
"bytes",
|
|
3632
3753
|
);
|
|
3633
3754
|
const target = persistence.state.folds.find((item) => item.id === id);
|
|
3634
|
-
|
|
3755
|
+
// A CLIPPED RESULT ANSWERS TO ITS ENTRY ID (2026-08-24, the tool-call diet): the
|
|
3756
|
+
// clip marker names the entry, and the full bytes are in the transcript rather
|
|
3757
|
+
// than behind a fold, so the read serves them from the mapped message directly,
|
|
3758
|
+
// through the same admission fence and the same slice bounds as a fold peek.
|
|
3759
|
+
if (!target) {
|
|
3760
|
+
const clip = (persistence.state.clips ?? []).find((item) =>
|
|
3761
|
+
item.entryId === id || item.callId === id);
|
|
3762
|
+
if (clip) {
|
|
3763
|
+
const mapped = snapshot.mapped.find((item) => item.ref?.entryId === clip.entryId);
|
|
3764
|
+
if (!mapped) throw new Error(`Clipped result ${id} is no longer on this branch`);
|
|
3765
|
+
const full = contentText(snapshot.messages[mapped.index]);
|
|
3766
|
+
admit({
|
|
3767
|
+
action: "peek",
|
|
3768
|
+
ctx,
|
|
3769
|
+
foldId: clip.entryId,
|
|
3770
|
+
requestedBytes: Math.max(0, Math.min(sliceBytes, full.length - offset)),
|
|
3771
|
+
children: [],
|
|
3772
|
+
});
|
|
3773
|
+
const slice = full.slice(offset, offset + sliceBytes);
|
|
3774
|
+
return toolPayload({
|
|
3775
|
+
id: clip.entryId,
|
|
3776
|
+
source: slice,
|
|
3777
|
+
truncated: offset + slice.length < full.length,
|
|
3778
|
+
sourceChars: full.length,
|
|
3779
|
+
offset,
|
|
3780
|
+
...(peekIsEphemeral(params) ? {
|
|
3781
|
+
ephemeral: "this result rides your context exactly until your next message; " +
|
|
3782
|
+
"extract what your current task needs into that reply, which takes over its " +
|
|
3783
|
+
"place. Peeking again is lossless and cheap; pass ephemeral false when you " +
|
|
3784
|
+
"need these bytes standing for several turns",
|
|
3785
|
+
} : { durable: "this result stays in your context like any other tool result" }),
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
throw new Error(`Unknown active-context fold ${id}`);
|
|
3789
|
+
}
|
|
3635
3790
|
admit({
|
|
3636
3791
|
action: "peek",
|
|
3637
3792
|
ctx,
|
|
@@ -3690,7 +3845,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3690
3845
|
for (const part of [recut]) {
|
|
3691
3846
|
const durable = persistence.persistedFoldRecords.get(foldIdFor(part.kind, part.parts));
|
|
3692
3847
|
const { preparedFold, nextState } = await prepareAndCommitExplicit({
|
|
3693
|
-
|
|
3848
|
+
// A re-cut carries the durable fold's OWN brief and provenance: those words
|
|
3849
|
+
// were validated when they were first accepted, and re-judging them as a
|
|
3850
|
+
// fresh agent submission would refuse a deterministic or composed brief
|
|
3851
|
+
// against the agent's reserve.
|
|
3852
|
+
snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied,
|
|
3853
|
+
briefProvenance: durable ? durable.fold.provenance : undefined, ctx, signal,
|
|
3694
3854
|
});
|
|
3695
3855
|
persistence.state = nextState;
|
|
3696
3856
|
if (durable && supplied) correctedBriefs[preparedFold.id] = supplied;
|
|
@@ -3706,7 +3866,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3706
3866
|
briefs: { ...(persistence.state.briefs ?? {}), ...correctedBriefs },
|
|
3707
3867
|
};
|
|
3708
3868
|
}
|
|
3709
|
-
await persistManual(persistence.state,
|
|
3869
|
+
await persistManual(persistence.state, ctx);
|
|
3710
3870
|
updateStatus(ctx);
|
|
3711
3871
|
return toolPayload({
|
|
3712
3872
|
version: 1,
|
|
@@ -3744,7 +3904,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3744
3904
|
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3745
3905
|
const leases = { ...next.leases };
|
|
3746
3906
|
delete leases[id];
|
|
3747
|
-
await persistManual({ ...next, leases },
|
|
3907
|
+
await persistManual({ ...next, leases }, ctx);
|
|
3748
3908
|
updateStatus(ctx);
|
|
3749
3909
|
return toolPayload({
|
|
3750
3910
|
version: 1,
|
|
@@ -3790,11 +3950,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3790
3950
|
delete leases[id];
|
|
3791
3951
|
next = { ...next, leases };
|
|
3792
3952
|
}
|
|
3793
|
-
await persistManual(
|
|
3794
|
-
next,
|
|
3795
|
-
action,
|
|
3796
|
-
ctx,
|
|
3797
|
-
);
|
|
3953
|
+
await persistManual(next, ctx);
|
|
3798
3954
|
updateStatus(ctx);
|
|
3799
3955
|
return toolPayload({
|
|
3800
3956
|
version: 1,
|
|
@@ -3890,7 +4046,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3890
4046
|
);
|
|
3891
4047
|
}
|
|
3892
4048
|
const text = String(params.brief ?? "").trim();
|
|
3893
|
-
const complaint = briefContractComplaint(text, snapshot.policy.
|
|
4049
|
+
const complaint = briefContractComplaint(text, snapshot.policy.agentBriefReserve, snapshot.toolName);
|
|
3894
4050
|
if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
|
|
3895
4051
|
const nextState = {
|
|
3896
4052
|
...persistence.state!,
|
|
@@ -3900,7 +4056,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3900
4056
|
: mark),
|
|
3901
4057
|
};
|
|
3902
4058
|
persistence.state = nextState;
|
|
3903
|
-
await persistManual(nextState,
|
|
4059
|
+
await persistManual(nextState, ctx);
|
|
3904
4060
|
updateStatus(ctx);
|
|
3905
4061
|
emit("context.brief", { id, chars: text.length, origin: "agent" });
|
|
3906
4062
|
const accounting = markAccounting(snapshot, persistence.state!);
|
|
@@ -3916,7 +4072,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3916
4072
|
durableRevision: persistence.state!.revision,
|
|
3917
4073
|
pendingMarks: accounting.pending,
|
|
3918
4074
|
unbriefed: pendingMarks(persistence.state!)
|
|
3919
|
-
.filter((mark) => mark.briefProvenance?.kind !== "supplied"
|
|
4075
|
+
.filter((mark) => mark.briefProvenance?.kind !== "supplied" &&
|
|
4076
|
+
mark.briefProvenance?.kind !== "augmented").map((mark) => mark.id),
|
|
3920
4077
|
unmarkedSpans: remainder.spans,
|
|
3921
4078
|
unmarkedTokens: remainder.tokens,
|
|
3922
4079
|
activation: "durable immediately, and it costs your window nothing: this fold is " +
|
|
@@ -3924,14 +4081,104 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3924
4081
|
"reaches it only when the commit folds this span. Nothing in your context moved.",
|
|
3925
4082
|
});
|
|
3926
4083
|
}
|
|
4084
|
+
if (action === "remember") {
|
|
4085
|
+
// THE DIGEST'S OWN CHANNEL. A working-memory write is durable state outside the
|
|
4086
|
+
// projection, so like `brief` it moves no bytes and costs no prefix cache: the
|
|
4087
|
+
// table of contents the projection carries refreshes at the next commit, and this
|
|
4088
|
+
// result carries the live key list in the meantime.
|
|
4089
|
+
const key = String(params.key ?? "").trim();
|
|
4090
|
+
if (!key) throw new Error("remember needs a key");
|
|
4091
|
+
if (key.length > MEMORY_KEY_CHARS) {
|
|
4092
|
+
throw new Error(`Working-memory keys have a hard ${MEMORY_KEY_CHARS}-character ` +
|
|
4093
|
+
"maximum: the key is a table-of-contents line, not the note. Put the detail " +
|
|
4094
|
+
"in the body.");
|
|
4095
|
+
}
|
|
4096
|
+
const body = String(params.body ?? "").trim();
|
|
4097
|
+
if (body.length > MEMORY_BODY_CHARS) {
|
|
4098
|
+
throw new Error(`Working-memory bodies have a hard ${MEMORY_BODY_CHARS}-character ` +
|
|
4099
|
+
`maximum and this one is ${body.length}. Split the entry, or keep the part ` +
|
|
4100
|
+
"that changes and let the transcript carry the rest: folded material stays " +
|
|
4101
|
+
"losslessly recoverable by peek.");
|
|
4102
|
+
}
|
|
4103
|
+
const held = persistence.state!.memory ?? [];
|
|
4104
|
+
const existing = held.find((entry) => entry.key === key);
|
|
4105
|
+
let nextMemory: WorkingMemoryEntry[];
|
|
4106
|
+
if (!body) {
|
|
4107
|
+
if (!existing) {
|
|
4108
|
+
throw new Error(`No working-memory entry '${key}' to remove. ${held.length
|
|
4109
|
+
? `The keys are: ${held.map((entry) => entry.key).join(", ")}.`
|
|
4110
|
+
: "Working memory is empty."}`);
|
|
4111
|
+
}
|
|
4112
|
+
nextMemory = held.filter((entry) => entry.key !== key);
|
|
4113
|
+
} else {
|
|
4114
|
+
if (!existing && held.length >= MEMORY_KEYS_MAX) {
|
|
4115
|
+
throw new Error(`Working memory holds its maximum of ${MEMORY_KEYS_MAX} ` +
|
|
4116
|
+
"entries. Merge two entries, or remove one whose facts no longer matter " +
|
|
4117
|
+
`(${toolName} {"action":"remember","key":"<key>","body":""}), then write ` +
|
|
4118
|
+
"this one.");
|
|
4119
|
+
}
|
|
4120
|
+
const ordinal = held.reduce((max, entry) => Math.max(max, entry.ordinal), 0) + 1;
|
|
4121
|
+
nextMemory = existing
|
|
4122
|
+
? held.map((entry) => entry.key === key ? { key, body, ordinal } : entry)
|
|
4123
|
+
: [...held, { key, body, ordinal }];
|
|
4124
|
+
}
|
|
4125
|
+
const nextState = {
|
|
4126
|
+
...persistence.state!,
|
|
4127
|
+
revision: persistence.state!.revision + 1,
|
|
4128
|
+
...(nextMemory.length ? { memory: nextMemory } : {}),
|
|
4129
|
+
};
|
|
4130
|
+
if (!nextMemory.length) delete (nextState as Partial<ActiveContextState>).memory;
|
|
4131
|
+
persistence.state = nextState;
|
|
4132
|
+
await persistManual(nextState, ctx);
|
|
4133
|
+
updateStatus(ctx);
|
|
4134
|
+
const toc = [...nextMemory].sort((left, right) => right.ordinal - left.ordinal)
|
|
4135
|
+
.map((entry) => ({ key: entry.key, chars: entry.body.length }));
|
|
4136
|
+
return toolPayload({
|
|
4137
|
+
version: 1,
|
|
4138
|
+
action,
|
|
4139
|
+
key,
|
|
4140
|
+
...(body ? { chars: body.length } : { removed: true }),
|
|
4141
|
+
argumentsSha256: executionArgumentsSha256,
|
|
4142
|
+
durableRevision: persistence.state!.revision,
|
|
4143
|
+
entries: toc,
|
|
4144
|
+
activation: body
|
|
4145
|
+
? "durable immediately, and it costs your window nothing: the body lives in " +
|
|
4146
|
+
"state, this list is the live view, and the table of contents in your " +
|
|
4147
|
+
"context refreshes at the next commit."
|
|
4148
|
+
: "removed. The table of contents in your context refreshes at the next commit.",
|
|
4149
|
+
});
|
|
4150
|
+
}
|
|
4151
|
+
if (action === "recall") {
|
|
4152
|
+
const held = persistence.state!.memory ?? [];
|
|
4153
|
+
if (!held.length) {
|
|
4154
|
+
throw new Error(`Working memory is empty. Write an entry with ${toolName} ` +
|
|
4155
|
+
'{"action":"remember","key":"<key>","body":"..."}.');
|
|
4156
|
+
}
|
|
4157
|
+
const requested = params.keys === undefined && params.key === undefined
|
|
4158
|
+
? null
|
|
4159
|
+
: (denseOwnArrayValues(params.keys) ?? [params.key ?? params.keys])
|
|
4160
|
+
.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
4161
|
+
const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
|
|
4162
|
+
const chosen = requested === null
|
|
4163
|
+
? entries
|
|
4164
|
+
: entries.filter((entry) => requested.includes(entry.key));
|
|
4165
|
+
if (requested !== null) {
|
|
4166
|
+
const missing = requested.filter((key) => !held.some((entry) => entry.key === key));
|
|
4167
|
+
if (missing.length) {
|
|
4168
|
+
throw new Error(`No working-memory entr${missing.length === 1 ? "y" : "ies"} ` +
|
|
4169
|
+
`${missing.join(", ")}. The keys are: ` +
|
|
4170
|
+
`${entries.map((entry) => entry.key).join(", ")}.`);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return toolPayload({
|
|
4174
|
+
version: 1,
|
|
4175
|
+
action,
|
|
4176
|
+
argumentsSha256: executionArgumentsSha256,
|
|
4177
|
+
entries: chosen.map((entry) => ({ key: entry.key, chars: entry.body.length, body: entry.body })),
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
3927
4180
|
throw new Error(`Unknown ${toolName} action '${action}'`);
|
|
3928
4181
|
};
|
|
3929
|
-
const requestedMarkCount = (params: Record<string, unknown>): number => {
|
|
3930
|
-
const batched = denseOwnArrayValues(params?.marks);
|
|
3931
|
-
if (batched) return batched.length;
|
|
3932
|
-
return params?.ids === undefined && params?.id === undefined ? 0 : 1;
|
|
3933
|
-
};
|
|
3934
|
-
|
|
3935
4182
|
const toolHandler = async (
|
|
3936
4183
|
toolCallId: string,
|
|
3937
4184
|
params: Record<string, unknown>,
|
|
@@ -3948,7 +4195,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3948
4195
|
ok,
|
|
3949
4196
|
error,
|
|
3950
4197
|
tool_call_id: toolCallId,
|
|
3951
|
-
marks_requested: requestedMarkCount(params),
|
|
3952
4198
|
corrections_applied: corrections.length,
|
|
3953
4199
|
arguments_sha256: sha256Value(params ?? {}),
|
|
3954
4200
|
...(action === "peek" ? { ephemeral: peekIsEphemeral(params) } : {}),
|
|
@@ -4234,20 +4480,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4234
4480
|
onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
|
|
4235
4481
|
await runQueued(async () => {
|
|
4236
4482
|
const fresh = authoritativeSnapshotFor(ctx);
|
|
4237
|
-
const
|
|
4238
|
-
|
|
4239
|
-
: { ids: [fromId, toId] };
|
|
4240
|
-
const { marks, staged } = await stageFoldMarks(
|
|
4241
|
-
batchedMarkRequests(request), "user",
|
|
4483
|
+
const { mark, staged } = await stageFoldMark(
|
|
4484
|
+
[fromId, toId], brief && brief.trim() ? brief.trim() : undefined,
|
|
4242
4485
|
new AbortController().signal, ctx, fresh);
|
|
4243
|
-
// Refuse BEFORE persisting:
|
|
4486
|
+
// Refuse BEFORE persisting: a refused mark leaves state untouched and
|
|
4244
4487
|
// persisting it would be a no-op write against the same-state projection.
|
|
4245
|
-
if (
|
|
4246
|
-
|
|
4247
|
-
"the span was refused";
|
|
4248
|
-
throw new Error(reason);
|
|
4249
|
-
}
|
|
4250
|
-
await persistManual(staged, "fold", ctx);
|
|
4488
|
+
if (mark.ok !== true) throw new Error(String(mark.reason ?? "the span was refused"));
|
|
4489
|
+
await persistManual(staged, ctx);
|
|
4251
4490
|
updateStatus(ctx);
|
|
4252
4491
|
});
|
|
4253
4492
|
safeNotify(ctx, "Staged user mark(s); they apply at the next commit epoch.", "info");
|
|
@@ -4291,7 +4530,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4291
4530
|
label: toolLabel,
|
|
4292
4531
|
allowedActions: allowedToolActions,
|
|
4293
4532
|
fullSurface: true,
|
|
4294
|
-
maxBriefChars: ACTIVE_CONTEXT_POLICY.
|
|
4533
|
+
maxBriefChars: ACTIVE_CONTEXT_POLICY.agentBriefReserve,
|
|
4295
4534
|
statusDetails: ["fold_candidates", "tree", "folds", "objects"],
|
|
4296
4535
|
minPeekSliceBytes: PEEK_MIN_SLICE_BYTES,
|
|
4297
4536
|
defaultPeekBytes: PEEK_DEFAULT_MAX_BYTES,
|