pi-fold 4.2.6 → 4.2.8
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
CHANGED
|
@@ -110,7 +110,7 @@ Four commands ship for the human: `/fold-status`, `/fold` (commit every staged m
|
|
|
110
110
|
|
|
111
111
|
<picture>
|
|
112
112
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/shaneconner/fold/main/media/marks-and-commits-dark.svg">
|
|
113
|
-
<img alt="A completed unit becomes a pending mark that leaves the window and its cached prefix untouched. Marks repeat for free below maxTarget; at maxTarget one commit applies every standing mark in a single rewrite. From the brief, peek appends
|
|
113
|
+
<img alt="A completed unit becomes a pending mark that leaves the window and its cached prefix untouched. Marks repeat for free below maxTarget; at maxTarget one commit applies every standing mark in a single rewrite. From the brief, peek appends verified history without thinking blocks and expand restores the original messages in place." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/marks-and-commits-light.svg">
|
|
114
114
|
</picture>
|
|
115
115
|
|
|
116
116
|
A mark moves nothing. The moment a tool batch or chapter closes, the runtime cuts it into a pending mark, up to eight cuts per pass, stalest first. The mark lives in durable state outside the window, so the projection stays byte-identical and the cached prefix survives. Marks accumulate for free until one commit applies all of them in a single rewrite.
|
|
@@ -136,10 +136,12 @@ Every commit re-reads roughly `minTarget` of the budget uncached, so a shallower
|
|
|
136
136
|
|
|
137
137
|
### Peek and expand
|
|
138
138
|
|
|
139
|
-
`peek`
|
|
139
|
+
`peek` verifies a fold's original source against its SHA-256 references, then returns a history view as a tool result at any depth, with the ancestors still collapsed. Assistant thinking blocks and their opaque signatures are omitted before slicing; visible text, tool calls and tool results stay verbatim. It appends and never edits the prefix, so checking what a fold holds costs nothing a commit has to pay for. It takes `offset` and `bytes` within that view for a bounded slice and reads any child fold by id, so a large fold has a narrow read. The result reports `sourceFormat: "messages-without-thinking"` and `omittedThinkingBlocks`. Pi's stored transcript and exact `expand` restoration remain unchanged.
|
|
140
140
|
|
|
141
141
|
A peek is ephemeral by default: the bytes ride the window until the model's next message, then their place holds a one-line placeholder and the reply that used them is the surviving trace. Peeking again is lossless and costs one append. The default follows what the agent chose: on the run measured for it, six of seven peeks asked for ephemeral at first exposure. `expand` is the commitment, restoring the source in place until the fold is refolded. Looking is not the same as taking.
|
|
142
142
|
|
|
143
|
+
Anthropic documents [reasoning-extraction refusals](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#keep-reasoning-in-thinking-blocks). Omitting native thinking from history reads avoids replaying it as ordinary text, but does not guarantee that a provider will accept a request. Existing tool results can still contain previously printed transcript excerpts. A durable peek stays in later requests, and reloading makes an ephemeral peek durable. Fold does not automatically retry content-filter refusals or remove arbitrary transcript text.
|
|
144
|
+
|
|
143
145
|
### Clipping, pins and the compaction path
|
|
144
146
|
|
|
145
147
|
Tool results get a second, lighter treatment at the same commit. Results in the oldest `toolFoldThreshold` share of the projected window, half of it by default, are clipped in view: the identifying head stays, the rest becomes a marker naming the entry id that peeks it back, and the stored bytes are untouched, so a later fold of that span still encodes the full result.
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
objectRefKey,
|
|
5
5
|
sha256Value,
|
|
6
6
|
stableStringify,
|
|
7
|
+
type EvidenceRef,
|
|
7
8
|
} from "./json.ts";
|
|
8
9
|
import {
|
|
9
10
|
contentText,
|
|
@@ -77,6 +78,7 @@ import type {
|
|
|
77
78
|
ProviderMeasurementAnchor,
|
|
78
79
|
} from "./lib/measurement.ts";
|
|
79
80
|
import {
|
|
81
|
+
assignFoldParents,
|
|
80
82
|
childFoldIds,
|
|
81
83
|
clearPrepared,
|
|
82
84
|
deriveFoldParents,
|
|
@@ -86,6 +88,8 @@ import {
|
|
|
86
88
|
makeFoldRecordEntry,
|
|
87
89
|
makeStateCheckpoint,
|
|
88
90
|
makeStateDelta,
|
|
91
|
+
parseActiveContextState,
|
|
92
|
+
validateFoldForest,
|
|
89
93
|
MAX_ACTIVE_FOLD_RECORDS,
|
|
90
94
|
MAX_DELTAS_BETWEEN_CHECKPOINTS,
|
|
91
95
|
materializeStatePersistence,
|
|
@@ -136,10 +140,12 @@ import type {
|
|
|
136
140
|
ActiveContextSnapshot,
|
|
137
141
|
ActiveContextState,
|
|
138
142
|
ActiveContextThresholds,
|
|
143
|
+
ActiveFold,
|
|
139
144
|
ActiveContextToolAction,
|
|
140
145
|
BriefProvenance,
|
|
141
146
|
FoldCandidate,
|
|
142
147
|
FoldKind,
|
|
148
|
+
FoldPart,
|
|
143
149
|
FoldRecordEntry,
|
|
144
150
|
PreparedFold,
|
|
145
151
|
} from "./lib/policy.ts";
|
|
@@ -445,6 +451,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
445
451
|
* session that never had any, which is what `persistedWireVersion` 0 also means and
|
|
446
452
|
* which legitimately writes the first checkpoint. See `persist`. */
|
|
447
453
|
lineageUnreadable: false,
|
|
454
|
+
forkCheckpointPending: false,
|
|
448
455
|
/** Deltas standing after the newest checkpoint in the branch. See
|
|
449
456
|
* `MAX_DELTAS_BETWEEN_CHECKPOINTS`: at the bound, `persist` writes a checkpoint instead
|
|
450
457
|
* of a delta, which is what keeps a load's replay bounded. */
|
|
@@ -574,6 +581,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
574
581
|
wallEpisodeOpen: false,
|
|
575
582
|
recoveryAttempts: 0,
|
|
576
583
|
pendingRejection: null as { status: number; ordinal: number } | null,
|
|
584
|
+
restoreRevisionGap: false,
|
|
577
585
|
lastRecovery: null as Record<string, unknown> | null,
|
|
578
586
|
instrumentationQueue: Promise.resolve<void>(undefined),
|
|
579
587
|
};
|
|
@@ -1184,6 +1192,146 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1184
1192
|
thresholds,
|
|
1185
1193
|
}));
|
|
1186
1194
|
|
|
1195
|
+
const forkEntriesForSession = (entries: any[], sessionId: string): any[] => {
|
|
1196
|
+
// A child checkpoint supersedes inherited records; keep child records and validate anything newer strictly.
|
|
1197
|
+
const checkpointIndex = entries.findLastIndex((entry) => entry?.type === "custom" &&
|
|
1198
|
+
entry.customType === stateEntryType && ownValue(entry.data, "sessionId") === sessionId &&
|
|
1199
|
+
ownValue(entry.data, "version") === 2 && ownValue(entry.data, "kind") === "checkpoint");
|
|
1200
|
+
if (checkpointIndex < 0) return entries;
|
|
1201
|
+
const inheritedOwners = new Set(entries.slice(0, checkpointIndex).flatMap((entry) => {
|
|
1202
|
+
if (entry?.type !== "custom" || entry.customType !== stateEntryType) return [];
|
|
1203
|
+
const owner = ownValue(entry.data, "sessionId");
|
|
1204
|
+
return typeof owner === "string" && owner !== sessionId ? [owner] : [];
|
|
1205
|
+
}));
|
|
1206
|
+
return entries.filter((entry, index) => {
|
|
1207
|
+
if (entry?.type !== "custom" || entry.customType !== foldRecordEntryType || index >= checkpointIndex) return true;
|
|
1208
|
+
const owner = ownValue(entry.data, "sessionId");
|
|
1209
|
+
return owner === sessionId || typeof owner !== "string" || !inheritedOwners.has(owner);
|
|
1210
|
+
});
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
const forkEntriesForOwner = (entries: any[], sessionId: string): any[] => entries.filter((entry) =>
|
|
1214
|
+
entry?.type !== "custom" || entry.customType !== foldRecordEntryType ||
|
|
1215
|
+
ownValue(entry.data, "sessionId") === sessionId,
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
const rebaseForkState = (
|
|
1219
|
+
state: ActiveContextState,
|
|
1220
|
+
snapshot: ActiveContextSnapshot,
|
|
1221
|
+
sessionId: string,
|
|
1222
|
+
): ActiveContextState => {
|
|
1223
|
+
const refsByEntryId = new Map<string, ActiveContextSnapshot["branchObjects"]>();
|
|
1224
|
+
for (const item of snapshot.branchObjects) {
|
|
1225
|
+
const refs = refsByEntryId.get(item.ref.entryId) ?? [];
|
|
1226
|
+
refs.push(item);
|
|
1227
|
+
refsByEntryId.set(item.ref.entryId, refs);
|
|
1228
|
+
}
|
|
1229
|
+
const rebaseRef = (ref: ActiveContextState["protected"][number]) => {
|
|
1230
|
+
const matches = refsByEntryId.get(ref.entryId)?.filter((item) =>
|
|
1231
|
+
item.ref.role === ref.role,
|
|
1232
|
+
) ?? [];
|
|
1233
|
+
if (matches.length !== 1) {
|
|
1234
|
+
throw new Error(`Inherited active-context evidence ${ref.entryId} is missing or ambiguous in the fork branch`);
|
|
1235
|
+
}
|
|
1236
|
+
// Pi's entry identity locates the source; preserve its recorded digest so the
|
|
1237
|
+
// runtime can report content drift instead of silently erasing its provenance.
|
|
1238
|
+
return { ...clone(ref), sessionId };
|
|
1239
|
+
};
|
|
1240
|
+
const rebaseParts = (parts: FoldPart[], resolveFoldId: (id: string) => string): FoldPart[] =>
|
|
1241
|
+
parts.map((part) => {
|
|
1242
|
+
if (part.kind === "raw") return { kind: "raw", ref: rebaseRef(part.ref) };
|
|
1243
|
+
return { kind: "fold", foldId: resolveFoldId(part.foldId) };
|
|
1244
|
+
});
|
|
1245
|
+
const sourceFolds = new Map(state.folds.map((fold) => [fold.id, fold]));
|
|
1246
|
+
const rebasedFolds = new Map<string, { fold: ActiveFold; refs: EvidenceRef[] }>();
|
|
1247
|
+
const visiting = new Set<string>();
|
|
1248
|
+
const rebaseFold = (id: string): { fold: ActiveFold; refs: EvidenceRef[] } => {
|
|
1249
|
+
const existing = rebasedFolds.get(id);
|
|
1250
|
+
if (existing) return existing;
|
|
1251
|
+
const source = sourceFolds.get(id);
|
|
1252
|
+
if (!source) throw new Error(`Inherited active-context fold ${id} is missing`);
|
|
1253
|
+
if (visiting.has(id)) throw new Error(`Inherited active-context fold cycle at ${id}`);
|
|
1254
|
+
visiting.add(id);
|
|
1255
|
+
const parts = rebaseParts(source.parts, (foldId) => rebaseFold(foldId).fold.id);
|
|
1256
|
+
const refs = source.parts.flatMap((part) => {
|
|
1257
|
+
if (part.kind === "raw") return [rebaseRef(part.ref)];
|
|
1258
|
+
return rebaseFold(part.foldId).refs;
|
|
1259
|
+
});
|
|
1260
|
+
const fold: ActiveFold = {
|
|
1261
|
+
...source,
|
|
1262
|
+
id: foldIdFor(source.kind, parts),
|
|
1263
|
+
parentId: null,
|
|
1264
|
+
parts,
|
|
1265
|
+
sourceSha256: sha256Value(refs),
|
|
1266
|
+
};
|
|
1267
|
+
const rebased = { fold, refs };
|
|
1268
|
+
visiting.delete(id);
|
|
1269
|
+
rebasedFolds.set(id, rebased);
|
|
1270
|
+
return rebased;
|
|
1271
|
+
};
|
|
1272
|
+
const folds = assignFoldParents(state.folds.map((fold) => rebaseFold(fold.id).fold));
|
|
1273
|
+
const rebasedFoldId = (id: string): string => {
|
|
1274
|
+
const rebased = rebasedFolds.get(id);
|
|
1275
|
+
if (!rebased) throw new Error(`Inherited active-context state references missing fold ${id}`);
|
|
1276
|
+
return rebased.fold.id;
|
|
1277
|
+
};
|
|
1278
|
+
let rebased: ActiveContextState;
|
|
1279
|
+
try {
|
|
1280
|
+
const expanded = state.expanded.map(rebasedFoldId);
|
|
1281
|
+
const leases = Object.fromEntries(Object.entries(state.leases).map(([id, value]) => [rebasedFoldId(id), value]));
|
|
1282
|
+
const briefs = Object.fromEntries(Object.entries(state.briefs ?? {}).map(([id, brief]) => [rebasedFoldId(id), brief]));
|
|
1283
|
+
const pendingMarks = (state.pendingMarks ?? []).map((mark) => {
|
|
1284
|
+
if (mark.mark === "refold") return { ...mark, id: rebasedFoldId(mark.id) };
|
|
1285
|
+
const parts = rebaseParts(mark.parts, rebasedFoldId);
|
|
1286
|
+
return { ...mark, id: foldIdFor(mark.kind, parts), parts };
|
|
1287
|
+
});
|
|
1288
|
+
rebased = clearPrepared({
|
|
1289
|
+
...state,
|
|
1290
|
+
sessionId,
|
|
1291
|
+
folds,
|
|
1292
|
+
expanded,
|
|
1293
|
+
protected: state.protected.map(rebaseRef),
|
|
1294
|
+
leases,
|
|
1295
|
+
...(state.pendingMarks ? { pendingMarks } : {}),
|
|
1296
|
+
...(state.briefs ? { briefs } : {}),
|
|
1297
|
+
});
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
// Keep forest errors ahead of curation errors without validating successful rebases twice.
|
|
1300
|
+
validateFoldForest(folds);
|
|
1301
|
+
throw error;
|
|
1302
|
+
}
|
|
1303
|
+
return parseActiveContextState(rebased, sessionId);
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
const prepareForkRecovery = (
|
|
1307
|
+
ctx: any,
|
|
1308
|
+
branchEntries: any[],
|
|
1309
|
+
sessionId: string,
|
|
1310
|
+
restoredMessages: unknown,
|
|
1311
|
+
restoreError: unknown,
|
|
1312
|
+
): { inherited: MaterializedStatePersistence; snapshot: ActiveContextSnapshot } => {
|
|
1313
|
+
const inheritedStateEntry = branchEntries.findLast((entry) =>
|
|
1314
|
+
entry?.type === "custom" && entry.customType === stateEntryType,
|
|
1315
|
+
);
|
|
1316
|
+
const inheritedSessionId = ownValue(inheritedStateEntry?.data, "sessionId");
|
|
1317
|
+
if (typeof inheritedSessionId !== "string" || !inheritedSessionId || inheritedSessionId === sessionId) {
|
|
1318
|
+
throw new Error("No distinct inherited session ID is available for fork recovery", { cause: restoreError });
|
|
1319
|
+
}
|
|
1320
|
+
const inherited = materializeStatePersistence(
|
|
1321
|
+
forkEntriesForOwner(branchEntries, inheritedSessionId),
|
|
1322
|
+
inheritedSessionId,
|
|
1323
|
+
stateEntryType,
|
|
1324
|
+
foldRecordEntryType,
|
|
1325
|
+
);
|
|
1326
|
+
if (!Array.isArray(restoredMessages)) {
|
|
1327
|
+
throw new Error("Pi did not provide the fork branch messages needed to rebase active-context state", {
|
|
1328
|
+
cause: restoreError,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
const snapshot = snapshotForEvent(ctx, restoredMessages);
|
|
1332
|
+
return { inherited, snapshot };
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1187
1335
|
const authoritativeSnapshotFor = (ctx: any): ActiveContextSnapshot => {
|
|
1188
1336
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1189
1337
|
if (!lifecycle.latestSnapshot || lifecycle.latestSnapshot.sessionId !== sessionId) {
|
|
@@ -1251,7 +1399,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1251
1399
|
model: measurement.model,
|
|
1252
1400
|
});
|
|
1253
1401
|
|
|
1254
|
-
const load = (ctx: any, preserveThresholdDecision = false): void => {
|
|
1402
|
+
const load = (ctx: any, preserveThresholdDecision = false, forkedSessionHint = false): void => {
|
|
1255
1403
|
lifecycle.generation += 1;
|
|
1256
1404
|
lifecycle.shuttingDown = false;
|
|
1257
1405
|
cancelPreparation();
|
|
@@ -1291,6 +1439,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1291
1439
|
curation.contextCalls = 0;
|
|
1292
1440
|
curation.recoveryAttempts = 0;
|
|
1293
1441
|
curation.pendingRejection = null;
|
|
1442
|
+
curation.restoreRevisionGap = false;
|
|
1294
1443
|
curation.lastRecovery = null;
|
|
1295
1444
|
ladder.lastAutomaticAction = null;
|
|
1296
1445
|
ladder.automaticFailure = null;
|
|
@@ -1299,14 +1448,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1299
1448
|
measurements.providerMeasurementByMessageSha.clear();
|
|
1300
1449
|
measurements.providerMeasurementAnchorByMessageSha.clear();
|
|
1301
1450
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1451
|
+
const forkedSession = forkedSessionHint || Boolean(ctx.sessionManager.getHeader?.()?.parentSession);
|
|
1302
1452
|
let restored: ActiveContextState | null = null;
|
|
1303
1453
|
let restoreError: unknown = null;
|
|
1304
1454
|
let restoredPersistence: MaterializedStatePersistence | null = null;
|
|
1305
1455
|
let measurementRestoreError: unknown = null;
|
|
1456
|
+
let forkBaseline = false;
|
|
1457
|
+
let startupSnapshot: ActiveContextSnapshot | null = null;
|
|
1458
|
+
let aheadReceiptRevision: number | null = null;
|
|
1306
1459
|
const branchEntries = [...ctx.sessionManager.getBranch()];
|
|
1460
|
+
const restoredMessages = piContextMessages(ctx);
|
|
1307
1461
|
try {
|
|
1308
1462
|
restoredPersistence = materializeStatePersistence(
|
|
1309
|
-
branchEntries,
|
|
1463
|
+
forkedSession ? forkEntriesForSession(branchEntries, sessionId) : branchEntries,
|
|
1310
1464
|
sessionId,
|
|
1311
1465
|
stateEntryType,
|
|
1312
1466
|
foldRecordEntryType,
|
|
@@ -1314,10 +1468,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1314
1468
|
restored = restoredPersistence.state;
|
|
1315
1469
|
} catch (error) {
|
|
1316
1470
|
restoreError = error;
|
|
1471
|
+
if (forkedSession) {
|
|
1472
|
+
try {
|
|
1473
|
+
const recovery = prepareForkRecovery(ctx, branchEntries, sessionId, restoredMessages, error);
|
|
1474
|
+
startupSnapshot = recovery.snapshot;
|
|
1475
|
+
restored = rebaseForkState(recovery.inherited.state, startupSnapshot, sessionId);
|
|
1476
|
+
restoredPersistence = recovery.inherited;
|
|
1477
|
+
forkBaseline = true;
|
|
1478
|
+
restoreError = null;
|
|
1479
|
+
} catch (forkError) {
|
|
1480
|
+
restoreError = forkError;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1317
1483
|
}
|
|
1318
1484
|
for (const entry of branchEntries) {
|
|
1319
1485
|
if (entry?.type !== "custom") continue;
|
|
1320
1486
|
if (entry.customType !== providerMeasurementEntryType) continue;
|
|
1487
|
+
const receiptSessionId = ownValue(entry.data, "sessionId");
|
|
1488
|
+
if (forkedSession && typeof receiptSessionId === "string" && receiptSessionId !== sessionId) {
|
|
1489
|
+
try { parseProviderContextMeasurementReceipt(entry.data, receiptSessionId); }
|
|
1490
|
+
catch (error) { measurementRestoreError = error; }
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1321
1493
|
try {
|
|
1322
1494
|
const receipt = parseProviderContextMeasurementReceipt(entry.data, sessionId);
|
|
1323
1495
|
const boundRevision = measurements.providerMeasurementRevisionByMessageSha.get(receipt.messageSha256);
|
|
@@ -1330,6 +1502,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1330
1502
|
throw new Error("One provider response has conflicting durable measurement receipts");
|
|
1331
1503
|
}
|
|
1332
1504
|
measurements.providerMeasurementByMessageSha.set(receipt.messageSha256, receipt);
|
|
1505
|
+
if (restoreError === null && receipt.projectionRevision > (restored?.revision ?? 0)) {
|
|
1506
|
+
aheadReceiptRevision = Math.max(aheadReceiptRevision ?? 0, receipt.projectionRevision);
|
|
1507
|
+
// This receipt describes a projection whose state is absent from this branch.
|
|
1508
|
+
// Keep the original receipt, but never anchor the restored window to it.
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1333
1511
|
const fingerprint = restoredPersistence?.projectionFingerprints.get(receipt.projectionRevision);
|
|
1334
1512
|
if (fingerprint) {
|
|
1335
1513
|
measurements.providerMeasurementAnchorByMessageSha.set(receipt.messageSha256, {
|
|
@@ -1348,6 +1526,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1348
1526
|
}
|
|
1349
1527
|
}
|
|
1350
1528
|
const durableRestored = restored ?? emptyActiveContextState(sessionId);
|
|
1529
|
+
if (restoreError === null && aheadReceiptRevision !== null) {
|
|
1530
|
+
curation.restoreRevisionGap = true;
|
|
1531
|
+
safeNotify(ctx, `A provider receipt names revision ${aheadReceiptRevision}, ahead of the restored ` +
|
|
1532
|
+
`${brandNoun} ledger at revision ${durableRestored.revision}. Existing folds and raw history are retained; ` +
|
|
1533
|
+
"the next context pass will rebuild eligible folds from that history. Missing ledger changes cannot be replayed.", "warning");
|
|
1534
|
+
}
|
|
1351
1535
|
persistence.state = durableRestored.prepared ? clearPrepared(durableRestored) : clone(durableRestored);
|
|
1352
1536
|
// AN UNREADABLE LINEAGE IS NOT AN EMPTY ONE (2026-09-07). `persistedWireVersion` reads
|
|
1353
1537
|
// 0 for both, and `persist` writes a CHECKPOINT whenever it is not 2, so a restore that
|
|
@@ -1360,23 +1544,25 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1360
1544
|
// the same ids with drifted bytes, 48 of 85 ids left conflicting. The two states are
|
|
1361
1545
|
// told apart here, once, rather than at each of `persist`'s callers.
|
|
1362
1546
|
persistence.lineageUnreadable = restoreError !== null;
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
persistence.
|
|
1366
|
-
persistence.
|
|
1367
|
-
|
|
1547
|
+
const persistedRestore = forkBaseline ? null : restoredPersistence;
|
|
1548
|
+
const persistedState = forkBaseline ? emptyActiveContextState(sessionId) : durableRestored;
|
|
1549
|
+
persistence.persistedWireVersion = persistedRestore?.wireVersion ?? 0;
|
|
1550
|
+
persistence.deltasSinceCheckpoint = persistedRestore?.deltasSinceCheckpoint ?? 0;
|
|
1551
|
+
persistence.persistedFoldRecords = persistedRestore?.records ?? new Map<string, FoldRecordEntry>();
|
|
1552
|
+
persistence.persistedStateSha256 = persistedRestore?.stateSha256 ?? semanticStateSha256(persistedState);
|
|
1553
|
+
persistence.persisted = clone(persistedState);
|
|
1554
|
+
persistence.forkCheckpointPending = forkBaseline;
|
|
1368
1555
|
measurements.lastProviderMeasurement = latestProviderContextMeasurement(
|
|
1369
1556
|
Array.isArray(restoredMessages) ? restoredMessages : [],
|
|
1370
1557
|
budgetWindowFor(ctx),
|
|
1371
1558
|
ctx.model,
|
|
1372
1559
|
);
|
|
1373
1560
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
1374
|
-
persistence.persisted = clone(durableRestored);
|
|
1375
1561
|
// Reuse Pi's own reconstructed messages, including custom messages and branch
|
|
1376
1562
|
// selection. This is ONLY a view for the bar: do not populate latestSnapshot,
|
|
1377
1563
|
// advance the frontier, persist a projection, or pretend a provider event occurred.
|
|
1378
1564
|
if (typeof ctx.ui?.setWidget === "function" && Array.isArray(restoredMessages)) {
|
|
1379
|
-
try { foldBar.startupSnapshot = snapshotForEvent(ctx, restoredMessages); }
|
|
1565
|
+
try { foldBar.startupSnapshot = startupSnapshot ?? snapshotForEvent(ctx, restoredMessages); }
|
|
1380
1566
|
catch { /* Keep the honest mapping fallback if the host cannot supply a view. */ }
|
|
1381
1567
|
}
|
|
1382
1568
|
// The warning names the CONSEQUENCE and not only the cause: "state was ignored" read as
|
|
@@ -1424,7 +1610,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1424
1610
|
next = persistenceProjection(next, projectionSnapshot);
|
|
1425
1611
|
}
|
|
1426
1612
|
next.folds = normalizeFoldsForPersistedRecords(next.folds, persistence.persistedFoldRecords);
|
|
1427
|
-
if (sameStateProjection(next, persistence.persisted)) {
|
|
1613
|
+
if (sameStateProjection(next, persistence.persisted) && !persistence.forkCheckpointPending) {
|
|
1428
1614
|
if (arrivingFoldIds.length) {
|
|
1429
1615
|
const lost = clone(persistence.state).folds.find((fold) => fold.id === arrivingFoldIds[0]);
|
|
1430
1616
|
const lostRefs = lost ? flattenFoldRefs(lost, persistence.state) : [];
|
|
@@ -1503,6 +1689,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1503
1689
|
await pi.appendEntry(stateEntryType, wire);
|
|
1504
1690
|
persistence.persisted = clone(next);
|
|
1505
1691
|
persistence.persistedWireVersion = 2;
|
|
1692
|
+
persistence.forkCheckpointPending = false;
|
|
1506
1693
|
persistence.deltasSinceCheckpoint = wire.kind === "checkpoint" ? 0 : persistence.deltasSinceCheckpoint + 1;
|
|
1507
1694
|
persistence.persistedStateSha256 = wire.stateSha256;
|
|
1508
1695
|
persistence.state = lifecycle.shuttingDown ? null : clone(next);
|
|
@@ -2466,7 +2653,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2466
2653
|
recovered,
|
|
2467
2654
|
note: measured.over
|
|
2468
2655
|
? `The rebuilt request is still ${measured.tokens} tokens against a ${measured.budgetTokens}-token ` +
|
|
2469
|
-
"serving budget
|
|
2656
|
+
"serving budget by our estimate. Available folds have been attempted; the retry is being sent, " +
|
|
2657
|
+
"and the provider will determine whether it fits. Recovery is not yet established."
|
|
2470
2658
|
: recovered
|
|
2471
2659
|
? "A rollback was required: the provider rejected the last request, which overfilled the serving " +
|
|
2472
2660
|
`budget at ${rejectedTokens} estimated tokens against ${measured.budgetTokens}. This pass ` +
|
|
@@ -2843,7 +3031,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2843
3031
|
if (addition.added) { state = addition.state; consolidationAdded += 1; }
|
|
2844
3032
|
}
|
|
2845
3033
|
const capacity = servingCapacity(snapshot.contextWindow);
|
|
2846
|
-
const usedTokens = capacity.usedTokens;
|
|
3034
|
+
const usedTokens = curation.restoreRevisionGap ? null : capacity.usedTokens;
|
|
2847
3035
|
const budgetTokens = capacity.budgetTokens;
|
|
2848
3036
|
const overflowExempt = userRequested || curation.recoveryAttempts > 0 ||
|
|
2849
3037
|
(usedTokens !== null && budgetTokens > 0 && usedTokens > budgetTokens);
|
|
@@ -3231,8 +3419,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3231
3419
|
phase: string,
|
|
3232
3420
|
operation: () => Promise<Record<string, unknown> | null>,
|
|
3233
3421
|
): Promise<Record<string, unknown> | null> => {
|
|
3234
|
-
|
|
3235
|
-
|
|
3422
|
+
const unmeasuredRecovery = phase === "restore-revision-gap" ||
|
|
3423
|
+
(phase === "projection-budget" && curation.pendingRejection !== null && !measurements.lastProviderMeasurement);
|
|
3424
|
+
if (!persistence.state || (!unmeasuredRecovery && (!measurements.lastProviderMeasurement ||
|
|
3425
|
+
!durableProviderMeasurementMatches(measurements.lastProviderMeasurement)))) return null;
|
|
3236
3426
|
if (ladder.automaticFailure) {
|
|
3237
3427
|
ladder.automaticFailure.suppressedCallbacks = Math.min(
|
|
3238
3428
|
Number.MAX_SAFE_INTEGER,
|
|
@@ -3292,10 +3482,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3292
3482
|
return [];
|
|
3293
3483
|
}
|
|
3294
3484
|
};
|
|
3295
|
-
const enqueueLifecycleLoad = async (ctx: any): Promise<void> => {
|
|
3485
|
+
const enqueueLifecycleLoad = async (ctx: any, forkedSessionHint = false): Promise<void> => {
|
|
3296
3486
|
const operation = lifecycle.contextQueue.then(() => {
|
|
3297
3487
|
const loadOperation = ladder.actionQueue.then(async () => {
|
|
3298
|
-
load(ctx);
|
|
3488
|
+
load(ctx, false, forkedSessionHint);
|
|
3489
|
+
if (persistence.forkCheckpointPending) await persist(ctx);
|
|
3299
3490
|
await recoverNativeReceipts(ctx);
|
|
3300
3491
|
});
|
|
3301
3492
|
ladder.actionQueue = loadOperation.catch(() => undefined);
|
|
@@ -3305,8 +3496,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3305
3496
|
await operation;
|
|
3306
3497
|
};
|
|
3307
3498
|
|
|
3308
|
-
const safeLifecycleLoad = async (
|
|
3309
|
-
|
|
3499
|
+
const safeLifecycleLoad = async (
|
|
3500
|
+
ctx: any,
|
|
3501
|
+
phase: "session-start" | "session-tree",
|
|
3502
|
+
forkedSessionHint = false,
|
|
3503
|
+
): Promise<void> => {
|
|
3504
|
+
try { await enqueueLifecycleLoad(ctx, forkedSessionHint); }
|
|
3310
3505
|
catch (error) {
|
|
3311
3506
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
3312
3507
|
if (!persistence.state || persistence.state.sessionId !== sessionId) persistence.state = emptyActiveContextState(sessionId);
|
|
@@ -3328,12 +3523,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3328
3523
|
});
|
|
3329
3524
|
};
|
|
3330
3525
|
|
|
3331
|
-
pi.on("session_start", async (
|
|
3332
|
-
await safeLifecycleLoad(ctx, "session-start");
|
|
3526
|
+
pi.on("session_start", async (event: unknown, ctx: any) => {
|
|
3527
|
+
await safeLifecycleLoad(ctx, "session-start", ownValue(event, "reason") === "fork");
|
|
3333
3528
|
recordResolvedCapacity(ctx);
|
|
3334
3529
|
await armRollbackLane(ctx);
|
|
3335
3530
|
});
|
|
3336
|
-
pi.on("session_tree", async (_event: unknown, ctx: any) => {
|
|
3531
|
+
pi.on("session_tree", async (_event: unknown, ctx: any) => {
|
|
3532
|
+
await safeLifecycleLoad(ctx, "session-tree", Boolean(ctx.sessionManager.getHeader?.()?.parentSession));
|
|
3533
|
+
});
|
|
3337
3534
|
pi.on("session_compact", async (event: Record<string, unknown>, ctx: any) => {
|
|
3338
3535
|
const reason = boundReceiptText(ownValue(event, "reason"), 64, "unknown");
|
|
3339
3536
|
const decision = makeNativeDecision(
|
|
@@ -3508,6 +3705,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3508
3705
|
// neither of their inputs. What it changes is WHEN the agent learns a fold exists,
|
|
3509
3706
|
// which is now while the material it covers is still in front of it.
|
|
3510
3707
|
advanceFoldFrontier(snapshot);
|
|
3708
|
+
if (curation.restoreRevisionGap && !ladder.automaticFailure) {
|
|
3709
|
+
mutationAttempted = true;
|
|
3710
|
+
const action = await attemptAutomaticCommit(snapshot, ctx, "restore-revision-gap", 1);
|
|
3711
|
+
if (action) {
|
|
3712
|
+
persistedSucceeded = true;
|
|
3713
|
+
projected = projectWithAdvisory(snapshot);
|
|
3714
|
+
}
|
|
3715
|
+
curation.restoreRevisionGap = false;
|
|
3716
|
+
}
|
|
3511
3717
|
// BEFORE the fence, so an ordinary crossing is an ordinary commit and the fence is
|
|
3512
3718
|
// left holding only the requests that genuinely will not fit.
|
|
3513
3719
|
if (await enforceBandTop(snapshot, ctx)) {
|
|
@@ -3760,7 +3966,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3760
3966
|
rollback.attempts += 1;
|
|
3761
3967
|
refuseRollback(
|
|
3762
3968
|
"the overflow arrived without a compaction event, so pi never stripped the failed message from agent " +
|
|
3763
|
-
"state and a tree rollback would leave the tree and agent state disagreeing (auto-compaction is off)"
|
|
3969
|
+
"state and a tree rollback would leave the tree and agent state disagreeing (auto-compaction is off); " +
|
|
3970
|
+
`use /${commandNames.fold} to commit eligible stale context, then retry explicitly`,
|
|
3764
3971
|
"message_end",
|
|
3765
3972
|
ctx,
|
|
3766
3973
|
);
|
|
@@ -3828,7 +4035,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3828
4035
|
"turn after an unsatisfied tool call is a malformed transcript for every provider";
|
|
3829
4036
|
}
|
|
3830
4037
|
const occupancyBefore = measurements.lastProviderMeasurement?.tokens ?? null;
|
|
3831
|
-
|
|
4038
|
+
let notice = rollbackNoticeText({
|
|
3832
4039
|
brandNoun,
|
|
3833
4040
|
toolName,
|
|
3834
4041
|
tokensRolledBack: estimatedTokens(rolledBackBytes),
|
|
@@ -3852,7 +4059,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3852
4059
|
}
|
|
3853
4060
|
}
|
|
3854
4061
|
}
|
|
3855
|
-
if (!replayed)
|
|
4062
|
+
if (!replayed) {
|
|
4063
|
+
notice = rollbackNoticeText({
|
|
4064
|
+
brandNoun, toolName, tokensRolledBack: estimatedTokens(rolledBackBytes),
|
|
4065
|
+
entriesAbandoned: abandoned.length, replayed: false, replaySkipReason,
|
|
4066
|
+
});
|
|
4067
|
+
safeNotify(ctx, notice, "warning");
|
|
4068
|
+
}
|
|
3856
4069
|
const record = emit("context.rollback", {
|
|
3857
4070
|
trigger: "session_before_compact",
|
|
3858
4071
|
armed: true,
|
|
@@ -4839,14 +5052,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4839
5052
|
const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
|
|
4840
5053
|
? capacity.usedTokens / capacity.budgetTokens
|
|
4841
5054
|
: null;
|
|
4842
|
-
const
|
|
5055
|
+
const estimatedOccupancy = occupancy ?? (capacity.budgetTokens > 0
|
|
5056
|
+
? estimatedTokens(pricedBytes(projectActiveContext(snapshot, persistence.state!))) / capacity.budgetTokens
|
|
5057
|
+
: 0);
|
|
5058
|
+
const topUp = Boolean(curation.restoreRevisionGap) || estimatedOccupancy >= thresholds.maxTarget;
|
|
4843
5059
|
const committed = await runCommitEpoch(snapshot, "user-command", topUp, measurements.latestRatio, true);
|
|
4844
5060
|
// Until this lands the epoch's folds are in the event stream and nowhere else.
|
|
4845
5061
|
// Swallowing the failure here announced a commit that does not exist and left the
|
|
4846
5062
|
// session computing folds it would keep throwing away, which is the dead session
|
|
4847
5063
|
// gate 122 was built for, reached through the user's own command instead.
|
|
4848
|
-
try {
|
|
4849
|
-
|
|
5064
|
+
try {
|
|
5065
|
+
await persist(ctx);
|
|
5066
|
+
if (committed) curation.restoreRevisionGap = false;
|
|
5067
|
+
} catch (error) {
|
|
4850
5068
|
suspendAutomatic(error, "user-command", ctx);
|
|
4851
5069
|
updateStatus(ctx);
|
|
4852
5070
|
throw error;
|
|
@@ -1427,13 +1427,31 @@ export function peekFoldSource(input: {
|
|
|
1427
1427
|
}): Record<string, unknown> {
|
|
1428
1428
|
const fold = input.state.folds.find((item) => item.id === input.foldId);
|
|
1429
1429
|
if (!fold) throw new Error(`Unknown active-context fold ${input.foldId}`);
|
|
1430
|
-
const
|
|
1430
|
+
const storedMessages = foldStoredSpan({
|
|
1431
1431
|
foldId: input.foldId,
|
|
1432
1432
|
state: input.state,
|
|
1433
1433
|
entries: input.entries,
|
|
1434
1434
|
sessionId: input.sessionId,
|
|
1435
1435
|
projectEntry: input.projectEntry,
|
|
1436
1436
|
});
|
|
1437
|
+
// Verify the ORIGINAL messages first, then build the history read. Native
|
|
1438
|
+
// reasoning and opaque signatures belong to the provider's assistant protocol,
|
|
1439
|
+
// not to a JSON transcript replayed as ordinary tool-result text (issue #10).
|
|
1440
|
+
// Only structural assistant blocks are removed: prose and tool output containing
|
|
1441
|
+
// words such as "thinking" stay verbatim. Stored refs, hashes and expand are intact.
|
|
1442
|
+
let omittedThinkingBlocks = 0;
|
|
1443
|
+
const messages = storedMessages.map((message) => {
|
|
1444
|
+
const content = ownValue(message, "content");
|
|
1445
|
+
if (!isPlainRecord(message) || ownValue(message, "role") !== "assistant" ||
|
|
1446
|
+
!Array.isArray(content)) return message;
|
|
1447
|
+
const visible = content.filter((block) => {
|
|
1448
|
+
const type = ownValue(block, "type");
|
|
1449
|
+
if (type !== "thinking" && type !== "redacted_thinking") return true;
|
|
1450
|
+
omittedThinkingBlocks += 1;
|
|
1451
|
+
return false;
|
|
1452
|
+
});
|
|
1453
|
+
return visible.length === content.length ? message : { ...message, content: visible };
|
|
1454
|
+
});
|
|
1437
1455
|
const source = stableStringify(messages);
|
|
1438
1456
|
const sourceBytes = bytes(source);
|
|
1439
1457
|
const offset = Math.min(Math.max(0, input.offset ?? 0), sourceBytes);
|
|
@@ -1475,6 +1493,8 @@ export function peekFoldSource(input: {
|
|
|
1475
1493
|
brief: foldBrief(fold, input.state),
|
|
1476
1494
|
sourceCount: messages.length,
|
|
1477
1495
|
sourceSha256: fold.sourceSha256,
|
|
1496
|
+
sourceFormat: "messages-without-thinking",
|
|
1497
|
+
omittedThinkingBlocks,
|
|
1478
1498
|
sourceBytes,
|
|
1479
1499
|
offset,
|
|
1480
1500
|
returnedBytes,
|
|
@@ -1493,16 +1513,20 @@ export function peekFoldSource(input: {
|
|
|
1493
1513
|
? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars) } }
|
|
1494
1514
|
: {}),
|
|
1495
1515
|
lifetime: "these bytes stay in the window exactly as returned, like any other tool result, and " +
|
|
1496
|
-
`nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s
|
|
1516
|
+
`nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s source view, so the ` +
|
|
1497
1517
|
`next commit reclaims it behind a placeholder naming ${fold.id}; peek ${fold.id} again for the ` +
|
|
1498
1518
|
"same verbatim bytes, or pin this result to keep the copy raw.",
|
|
1499
1519
|
note: (truncated
|
|
1500
|
-
? `Bounded read: ${returnedBytes} of ${sourceBytes} exact source bytes, ${view.omittedBytes} omitted ` +
|
|
1520
|
+
? `Bounded read: ${returnedBytes} of ${sourceBytes} ${omittedThinkingBlocks ? "source view" : "exact source"} bytes, ${view.omittedBytes} omitted ` +
|
|
1501
1521
|
`from the middle. Widen with bytes, page with offset, or expand ${fold.id} to restore it in place.`
|
|
1502
1522
|
: children.length
|
|
1503
|
-
? `Complete stored span, one level: raw entries exactly, and ${children.length} child fold(s) still ` +
|
|
1523
|
+
? `Complete stored span, one level: raw entries ${omittedThinkingBlocks ? "with thinking blocks omitted" : "exactly"}, and ${children.length} child fold(s) still ` +
|
|
1504
1524
|
"placeheld. Peek a child id to read its own span; the fold stayed collapsed and no projection changed."
|
|
1505
|
-
:
|
|
1525
|
+
: `Complete ${omittedThinkingBlocks ? "source view" : "exact source"}; the fold stayed collapsed and no projection changed.`) +
|
|
1526
|
+
(omittedThinkingBlocks
|
|
1527
|
+
? ` ${omittedThinkingBlocks} thinking blocks omitted from this read, including their opaque ` +
|
|
1528
|
+
"signatures. The original messages remain unchanged for exact in-place restoration."
|
|
1529
|
+
: "") +
|
|
1506
1530
|
(catalog.omitted
|
|
1507
1531
|
? ` The nested index seats the shallowest ${index.length} of ${index.length + catalog.omitted} ` +
|
|
1508
1532
|
"descendant briefs inside the byte budget; the deeper rows are one hop away by peeking a child id, " +
|
|
@@ -152,11 +152,11 @@ export function rollbackNoticeText(input: {
|
|
|
152
152
|
}): string {
|
|
153
153
|
const head = "The provider rejected the last request because the context exceeded its input limit. " +
|
|
154
154
|
`${input.brandNoun} rolled the session back past that request (${input.entriesAbandoned} entry(s), ` +
|
|
155
|
-
`about ${input.tokensRolledBack} tokens, kept in the tree as an abandoned branch)
|
|
156
|
-
"stale window to make room.";
|
|
155
|
+
`about ${input.tokensRolledBack} tokens, kept in the tree as an abandoned branch).`;
|
|
157
156
|
if (input.replayed) {
|
|
158
|
-
return `${head} The request is being reissued now
|
|
159
|
-
|
|
157
|
+
return `${head} The request is being reissued now. Its context pass will attempt to fold eligible stale ` +
|
|
158
|
+
"material; the recovery receipt will report the result. Original entries remain in the session tree, " +
|
|
159
|
+
`and folded material is still readable through ${input.toolName}.`;
|
|
160
160
|
}
|
|
161
161
|
return `${head} The request was NOT reissued: ${input.replaySkipReason ?? "the rolled-back tail is not replayable"}. ` +
|
|
162
162
|
"Reissue the work yourself from the state you can see.";
|
|
@@ -699,9 +699,12 @@ export function commitCoverage(input: {
|
|
|
699
699
|
budgetTokens: number;
|
|
700
700
|
}): CommitCoverage {
|
|
701
701
|
const { snapshot, state, usedTokens, budgetTokens } = input;
|
|
702
|
-
|
|
702
|
+
// Before the first response, or after a restore, an estimate can size a commit.
|
|
703
|
+
// It never decides whether the resulting request is allowed to reach the provider.
|
|
704
|
+
const occupancyTokens = usedTokens ?? estimatedTokens(pricedBytes(projectActiveContext(snapshot, state)));
|
|
705
|
+
const targetShare = !(budgetTokens > 0)
|
|
703
706
|
? 0
|
|
704
|
-
: Math.max(0, (
|
|
707
|
+
: Math.max(0, (occupancyTokens - snapshot.thresholds.minTarget * budgetTokens) / budgetTokens);
|
|
705
708
|
const accounting = markAccounting(snapshot, state);
|
|
706
709
|
const markedShare = accounting.eligibleFreedBudgetShare;
|
|
707
710
|
const targetTokens = Math.round(targetShare * budgetTokens);
|
|
@@ -29,7 +29,7 @@ export function buildActiveContextTool(input: {
|
|
|
29
29
|
handler: ToolHandler;
|
|
30
30
|
}) {
|
|
31
31
|
const peekGuidance = input.allowedActions.includes("peek")
|
|
32
|
-
? ` Peek is the point read: it returns one fold's index view -- nested folds' briefs seated shallowest first inside the byte budget with the omitted rows counted, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back
|
|
32
|
+
? ` Peek is the point read: it returns one fold's index view -- nested folds' briefs seated shallowest first inside the byte budget with the omitted rows counted, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back with thinking blocks and their signatures omitted, all other content unchanged, folds nested inside stay placeheld, and peeking a child id you see there returns that child's own bytes, so anything folded is one hop away at any depth. It returns at most ${input.defaultPeekBytes} bytes unless you widen it with bytes, and it is EPHEMERAL by default: the bytes ride only until your next message, your reply then the surviving trace, so extract what your current task needs from them in that reply. Peeking again is lossless and costs one append, so a read you turn out to need twice is cheap; pass ephemeral false when you need the bytes standing across several turns. Expand is the durable in-place restoration.`
|
|
33
33
|
: "";
|
|
34
34
|
const correctionGuidance = input.allowedActions.includes("reboundary")
|
|
35
35
|
? " Curation is correctable: reboundary with ids re-cuts a span into exactly one fold, which merges adjacent folds when the span covers several and splits one when the span sits inside it. Reboundary with a single id returns that fold's span to raw. A brief is written when you mark the span; a standing fold's brief is not rewritten, because rewriting it rewrites the projection at that fold's placeholder and costs the whole prefix cache from there on."
|
|
@@ -80,7 +80,7 @@ export function buildActiveContextTool(input: {
|
|
|
80
80
|
bytes: {
|
|
81
81
|
type: "integer",
|
|
82
82
|
minimum: input.minPeekSliceBytes,
|
|
83
|
-
description: `Peek only: bytes of
|
|
83
|
+
description: `Peek only: bytes of the source view to return, widening or narrowing the ${input.defaultPeekBytes}-byte default. Thinking blocks and their signatures are omitted before slicing.`,
|
|
84
84
|
},
|
|
85
85
|
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
86
86
|
detail: { type: "string", enum: [...input.statusDetails] },
|