pi-fold 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -212
- package/extensions/active-context.ts +457 -362
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +8 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +141 -9
- package/extensions/lib/editor-ui.ts +133 -25
- package/extensions/lib/folding.ts +181 -17
- package/extensions/lib/instrumentation.ts +0 -6
- package/extensions/lib/measurement.ts +84 -4
- package/extensions/lib/persistence.ts +181 -9
- package/extensions/lib/policy.ts +80 -28
- package/extensions/lib/scheduling.ts +105 -18
- package/extensions/lib/selection.ts +112 -39
- package/extensions/lib/tool-surface.ts +6 -4
- package/extensions/lib/transcript.ts +65 -14
- package/extensions/settings.ts +123 -54
- 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";
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
explicitProtectedMass,
|
|
45
47
|
budgetOccupancy,
|
|
46
48
|
hardFenceRatio,
|
|
49
|
+
imageMass,
|
|
47
50
|
latestProviderContextMeasurement,
|
|
48
51
|
orderedRoots,
|
|
49
52
|
parseNativeCompactionCompletion,
|
|
@@ -70,6 +73,7 @@ import {
|
|
|
70
73
|
deriveFoldParents,
|
|
71
74
|
flattenFoldRefs,
|
|
72
75
|
foldIdFor,
|
|
76
|
+
foldRecordRef,
|
|
73
77
|
makeFoldRecordEntry,
|
|
74
78
|
makeStateCheckpoint,
|
|
75
79
|
makeStateDelta,
|
|
@@ -102,8 +106,10 @@ import {
|
|
|
102
106
|
resolveThresholds,
|
|
103
107
|
servingBudgetTokens,
|
|
104
108
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
109
|
+
IMAGE_ESTIMATED_TOKENS,
|
|
105
110
|
entryTypeNamespace,
|
|
106
111
|
MAX_FOLD_SPAN_CHARS,
|
|
112
|
+
DEFAULT_TOOL_FOLD_THRESHOLD,
|
|
107
113
|
MAX_PINNED_SHARE,
|
|
108
114
|
PEEK_DEFAULT_MAX_BYTES,
|
|
109
115
|
peekIsEphemeral,
|
|
@@ -118,11 +124,12 @@ import type {
|
|
|
118
124
|
ActiveContextState,
|
|
119
125
|
ActiveContextThresholds,
|
|
120
126
|
ActiveContextToolAction,
|
|
127
|
+
BriefProvenance,
|
|
121
128
|
FoldCandidate,
|
|
122
129
|
FoldKind,
|
|
123
130
|
FoldRecordEntry,
|
|
124
|
-
MarkOrigin,
|
|
125
131
|
PreparedFold,
|
|
132
|
+
WorkingMemoryEntry,
|
|
126
133
|
} from "./lib/policy.ts";
|
|
127
134
|
import {
|
|
128
135
|
absorbWedgeMarks,
|
|
@@ -131,6 +138,7 @@ import {
|
|
|
131
138
|
import {
|
|
132
139
|
contextReceipt,
|
|
133
140
|
foldNoticeText,
|
|
141
|
+
foldStatusText,
|
|
134
142
|
receiptBlockText,
|
|
135
143
|
withReceipt,
|
|
136
144
|
} from "./lib/curation.ts";
|
|
@@ -142,6 +150,7 @@ import {
|
|
|
142
150
|
commitCoverage,
|
|
143
151
|
windowClaims,
|
|
144
152
|
commitPendingMarks,
|
|
153
|
+
toolClipAdditions,
|
|
145
154
|
consolidationMarks,
|
|
146
155
|
ephemeralPeekMarks,
|
|
147
156
|
epochCommitDue,
|
|
@@ -149,6 +158,7 @@ import {
|
|
|
149
158
|
foldMarkFor,
|
|
150
159
|
ladderBrief,
|
|
151
160
|
markAccounting,
|
|
161
|
+
markRewriteTokens,
|
|
152
162
|
markFreedBytes,
|
|
153
163
|
unmarkedRemainder,
|
|
154
164
|
markClaimingRef,
|
|
@@ -211,38 +221,6 @@ export * from "./lib/scheduling.ts";
|
|
|
211
221
|
export * from "./lib/selection.ts";
|
|
212
222
|
export * from "./lib/transcript.ts";
|
|
213
223
|
|
|
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
224
|
export function registerActiveContext(pi: any, options: {
|
|
247
225
|
toolName?: string;
|
|
248
226
|
toolLabel?: string;
|
|
@@ -252,6 +230,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
252
230
|
blacklistAutoFoldTools?: ReadonlySet<string>;
|
|
253
231
|
providerInputBudget?: number;
|
|
254
232
|
thresholds?: ActiveContextThresholds;
|
|
233
|
+
/** THE TOOL-CALL DIET's share point (2026-08-24; default 0.50 since 2026-08-28, Shane,
|
|
234
|
+
* on the sol-20260826-full2 verdict): tool results in the oldest toolFoldThreshold
|
|
235
|
+
* share of the projected window are clipped IN VIEW at each commit, identified head
|
|
236
|
+
* kept, full bytes peek-recoverable behind the entry id the marker names. A share in
|
|
237
|
+
* [0, 1), where 0 turns the diet off; 0.50 is what every fold repetition from 3 onward
|
|
238
|
+
* ran, so the default is the value that was measured rather than the absence of one. */
|
|
239
|
+
toolFoldThreshold?: number;
|
|
240
|
+
/** THE INVITATION SWITCH (public since 2026-08-27; DEFAULT FALSE since 2026-08-28,
|
|
241
|
+
* Shane, on the sol-20260826-full2 verdict). false is the deterministic shape the
|
|
242
|
+
* campaign measured and the one it recommends: no carrier ever invites a brief and
|
|
243
|
+
* every fold goes out with the runtime's own words, which drew 14, 14, 9 and 8 correct
|
|
244
|
+
* against the invited condition's 3. true restores the standing invitation for the
|
|
245
|
+
* agent to improve a brief. Either way the agent verbs stay on the tool, so an agent
|
|
246
|
+
* that finds a fold can still annotate what it reads. */
|
|
247
|
+
postFoldNotice?: boolean;
|
|
255
248
|
}): {
|
|
256
249
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
257
250
|
} {
|
|
@@ -260,6 +253,19 @@ export function registerActiveContext(pi: any, options: {
|
|
|
260
253
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
261
254
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
262
255
|
const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
|
|
256
|
+
if (options.postFoldNotice !== undefined && typeof options.postFoldNotice !== "boolean") {
|
|
257
|
+
throw new Error("postFoldNotice must be a boolean: true keeps the post-fold invitation " +
|
|
258
|
+
"notice, false ships every fold with the runtime's deterministic brief and no " +
|
|
259
|
+
"carrier inviting the agent to improve it");
|
|
260
|
+
}
|
|
261
|
+
const postFoldNotice = options.postFoldNotice ?? false;
|
|
262
|
+
const toolFoldThreshold = options.toolFoldThreshold ?? DEFAULT_TOOL_FOLD_THRESHOLD;
|
|
263
|
+
if (typeof toolFoldThreshold !== "number" || !Number.isFinite(toolFoldThreshold) ||
|
|
264
|
+
toolFoldThreshold < 0 || toolFoldThreshold >= 1) {
|
|
265
|
+
throw new Error("toolFoldThreshold must be a share in [0, 1): it names the oldest " +
|
|
266
|
+
"fraction of the projected window whose tool results are clipped in view at each " +
|
|
267
|
+
"commit, and 0 turns the diet off");
|
|
268
|
+
}
|
|
263
269
|
for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
|
|
264
270
|
if (Object.hasOwn(options, removed)) {
|
|
265
271
|
throw new Error(`${removed} is no longer an option: epoch scheduling, peek foldability ` +
|
|
@@ -378,6 +384,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
378
384
|
lastProviderMeasurement: null as ProviderContextMeasurement | null,
|
|
379
385
|
descriptorWindow: null as number | null,
|
|
380
386
|
lastProjectedChars: null as number | null,
|
|
387
|
+
lastProjectedImages: 0,
|
|
381
388
|
projectionCalibrations: [] as Array<{ chars: number; tokens: number }>,
|
|
382
389
|
lastProjectedEstimate: null as number | null,
|
|
383
390
|
lastProjectedEstimateCalibrated: false,
|
|
@@ -386,6 +393,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
386
393
|
projectionAnchor: null as {
|
|
387
394
|
tokens: number;
|
|
388
395
|
chars: number;
|
|
396
|
+
/** `chars` less the base64 of any image, which is what the delta is taken on. */
|
|
397
|
+
textChars: number;
|
|
398
|
+
/** Images inside the anchored prefix, each worth IMAGE_ESTIMATED_TOKENS. */
|
|
399
|
+
images: number;
|
|
389
400
|
head: string;
|
|
390
401
|
messageSha256: string;
|
|
391
402
|
sessionId: string;
|
|
@@ -405,6 +416,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
405
416
|
ledger: emptyLedger(),
|
|
406
417
|
previousDigests: null as string[] | null,
|
|
407
418
|
previousText: null as string | null,
|
|
419
|
+
// The image mass of `previousText`, kept beside it because the provider anchor is
|
|
420
|
+
// built from that text and has no access to the array it came from.
|
|
421
|
+
previousImages: 0,
|
|
422
|
+
previousImageChars: 0,
|
|
408
423
|
lastChange: "append" as ProjectionChange,
|
|
409
424
|
lastPreservedShare: null as number | null,
|
|
410
425
|
sinceHandoff: [] as Array<{
|
|
@@ -468,9 +483,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
468
483
|
};
|
|
469
484
|
|
|
470
485
|
const advisory = {
|
|
471
|
-
hardFenceNoticeKey: null as string | null,
|
|
472
|
-
hardFenceReleaseSessionId: null as string | null,
|
|
473
|
-
hardFenceReleasedProjectionKeys: new Set<string>(),
|
|
474
486
|
};
|
|
475
487
|
|
|
476
488
|
// EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
|
|
@@ -573,10 +585,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
573
585
|
};
|
|
574
586
|
|
|
575
587
|
const PREFIX_MUTATING_KINDS: ReadonlySet<string> = new Set([
|
|
576
|
-
"context.commit", "context.fold", "context.absorb", "context.
|
|
588
|
+
"context.commit", "context.fold", "context.absorb", "context.recovery",
|
|
577
589
|
]);
|
|
578
590
|
const CACHE_ACTION_REQUEST_CLASS: Readonly<Record<string, string>> = Object.freeze({
|
|
579
|
-
fold: "after-mark",
|
|
580
591
|
peek: "after-peek",
|
|
581
592
|
expand: "after-expand",
|
|
582
593
|
refold: "after-refold",
|
|
@@ -590,8 +601,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
590
601
|
folds_committed: receipt.foldsCommitted,
|
|
591
602
|
folds_created: receipt.foldsCreated,
|
|
592
603
|
freed_tokens: receipt.freedTokens,
|
|
593
|
-
split_folds: receipt.splitFolds,
|
|
594
|
-
split_from_chars: receipt.splitFromChars,
|
|
595
604
|
absorbed_wedges: receipt.absorbedWedges,
|
|
596
605
|
recovered: receipt.recovered,
|
|
597
606
|
protected_bytes: receipt.protectedBytes,
|
|
@@ -612,15 +621,43 @@ export function registerActiveContext(pi: any, options: {
|
|
|
612
621
|
lifecycle.generation === expectedGeneration && persistence.state?.sessionId === sessionId &&
|
|
613
622
|
(!ctx || contextSessionMatches(ctx, sessionId));
|
|
614
623
|
|
|
624
|
+
/**
|
|
625
|
+
* THE ONE LINE THAT IS ALWAYS ON SCREEN, so it answers the question a person actually
|
|
626
|
+
* has: how full am I, and is anything about to happen.
|
|
627
|
+
*
|
|
628
|
+
* It used to read `pi_fold_context folds: 3 · provider 187234/272000`. That led with the
|
|
629
|
+
* AGENT TOOL'S NAME, which a person cannot call and does not care about, and then spent
|
|
630
|
+
* its remaining width on two raw counts they would have to divide themselves. The brand
|
|
631
|
+
* leads now, occupancy is a percentage of the serving budget, and the commit point is
|
|
632
|
+
* named only when it is close enough to matter.
|
|
633
|
+
*/
|
|
615
634
|
const updateStatus = (ctx: any): void => {
|
|
616
635
|
try {
|
|
617
636
|
const roots = persistence.state && lifecycle.latestSnapshot ? orderedRoots(persistence.state, lifecycle.latestSnapshot).length : 0;
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
637
|
+
const staged = persistence.state ? pendingMarks(persistence.state).length : 0;
|
|
638
|
+
const capacity = currentCapacity(ctx);
|
|
639
|
+
const used = measurements.lastProviderMeasurement?.tokens ?? null;
|
|
640
|
+
const share = used !== null && capacity.budgetTokens > 0 ? used / capacity.budgetTokens : null;
|
|
641
|
+
const parts: string[] = [];
|
|
642
|
+
// FAILURE LEADS. This line is cut to whatever width the host gives it, so the last
|
|
643
|
+
// segment is the first one a narrow terminal removes, and folding having stopped is
|
|
644
|
+
// the one state on this line a person has to act on. It was appended last.
|
|
645
|
+
if (ladder.automaticFailure) parts.push("FOLDING STOPPED");
|
|
646
|
+
// Unmeasured is stated, never guessed: an estimate rendered as a fact is the habit
|
|
647
|
+
// that cost a live session, and this line is the most-read surface there is.
|
|
648
|
+
parts.push(share === null ? "not measured" : `${Math.round(share * 100)}% full`);
|
|
649
|
+
const trigger = lifecycle.latestSnapshot?.thresholds.maxTarget ?? null;
|
|
650
|
+
// WHEN, not just how full. "68% full" only answers the second question a reader has
|
|
651
|
+
// if they already remember where the trigger sits; naming it answers both.
|
|
652
|
+
//
|
|
653
|
+
// NOT WHILE STOPPED, though: the line rendered "COMMIT DUE" and "FOLDING STOPPED"
|
|
654
|
+
// in one breath, promising the thing it was announcing would not happen.
|
|
655
|
+
if (share !== null && trigger !== null && !ladder.automaticFailure) {
|
|
656
|
+
parts.push(share >= trigger ? "COMMIT DUE" : `commit at ${Math.round(trigger * 100)}%`);
|
|
657
|
+
}
|
|
658
|
+
if (staged > 0) parts.push(`${staged} staged`);
|
|
659
|
+
if (roots > 0) parts.push(`${roots} fold${roots === 1 ? "" : "s"}`);
|
|
660
|
+
ctx.ui?.setStatus?.(entryTypePrefix, `${brandNoun} ${parts.join(" · ")}`);
|
|
624
661
|
} catch { }
|
|
625
662
|
};
|
|
626
663
|
|
|
@@ -759,6 +796,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
759
796
|
instrumentation.ledger = emptyLedger();
|
|
760
797
|
instrumentation.previousDigests = null;
|
|
761
798
|
instrumentation.previousText = null;
|
|
799
|
+
instrumentation.previousImages = 0;
|
|
800
|
+
instrumentation.previousImageChars = 0;
|
|
762
801
|
instrumentation.requests = 0;
|
|
763
802
|
instrumentation.lastMutationRequest = 0;
|
|
764
803
|
instrumentation.lastMutationTokens = null;
|
|
@@ -773,16 +812,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
773
812
|
curation.lastRecovery = null;
|
|
774
813
|
ladder.lastAutomaticAction = null;
|
|
775
814
|
ladder.automaticFailure = null;
|
|
776
|
-
advisory.hardFenceNoticeKey = null;
|
|
777
815
|
measurements.providerMeasurementReceipts.clear();
|
|
778
816
|
measurements.providerMeasurementRevisionByMessageSha.clear();
|
|
779
817
|
measurements.providerMeasurementByMessageSha.clear();
|
|
780
818
|
measurements.providerMeasurementAnchorByMessageSha.clear();
|
|
781
819
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
782
|
-
if (advisory.hardFenceReleaseSessionId !== sessionId) {
|
|
783
|
-
advisory.hardFenceReleaseSessionId = sessionId;
|
|
784
|
-
advisory.hardFenceReleasedProjectionKeys.clear();
|
|
785
|
-
}
|
|
786
820
|
let restored: ActiveContextState | null = null;
|
|
787
821
|
let restoreError: unknown = null;
|
|
788
822
|
let restoredPersistence: MaterializedStatePersistence | null = null;
|
|
@@ -897,15 +931,28 @@ export function registerActiveContext(pi: any, options: {
|
|
|
897
931
|
if (next.folds.length > MAX_ACTIVE_FOLD_RECORDS) {
|
|
898
932
|
throw new Error("Active-context fold closure exceeds persistence limit");
|
|
899
933
|
}
|
|
934
|
+
// AN ALREADY-DURABLE FOLD NEEDS A DIGEST, NOT A RECORD (2026-08-28). This built the
|
|
935
|
+
// whole entry for every fold on every persist just to compare one hash and continue:
|
|
936
|
+
// `makeFoldRecordEntry` canonicalizes, hashes, then `parseFoldRecordEntry` re-validates,
|
|
937
|
+
// clones the fold again, recomputes `foldIdFor`, hashes a second time and stringifies
|
|
938
|
+
// for the wire cap. Replaying a real sealed session, 98.2 percent of 4,947 calls hit
|
|
939
|
+
// the `continue`, and 126 of 135 persists appended nothing while rebuilding all of it.
|
|
940
|
+
// `foldRecordRef` is the same digest by construction (`makeFoldRecordEntry` stores
|
|
941
|
+
// `canonicalFoldRecord`, and `stateFromFoldRefs` already matches records to refs on
|
|
942
|
+
// this equality at persistence.ts:809); gate 152 pins it. Equal digests mean
|
|
943
|
+
// byte-identical canonical bytes to a record that already passed
|
|
944
|
+
// `parseFoldRecordEntry`, so skipping the rebuild cannot reach a different verdict.
|
|
945
|
+
// Measured 85.0ms to 19.6ms at 400 folds.
|
|
900
946
|
for (const fold of next.folds) {
|
|
901
|
-
const
|
|
902
|
-
const existing = persistence.persistedFoldRecords.get(
|
|
947
|
+
const ref = foldRecordRef(fold);
|
|
948
|
+
const existing = persistence.persistedFoldRecords.get(ref.id);
|
|
903
949
|
if (existing) {
|
|
904
|
-
if (existing.recordSha256 !==
|
|
905
|
-
throw new Error(`Conflicting durable active-context fold ${
|
|
950
|
+
if (existing.recordSha256 !== ref.sha256) {
|
|
951
|
+
throw new Error(`Conflicting durable active-context fold ${ref.id}`);
|
|
906
952
|
}
|
|
907
953
|
continue;
|
|
908
954
|
}
|
|
955
|
+
const record = makeFoldRecordEntry(fold, sessionId);
|
|
909
956
|
await pi.appendEntry(foldRecordEntryType, record);
|
|
910
957
|
persistence.persistedFoldRecords.set(record.foldId, record);
|
|
911
958
|
if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
|
|
@@ -914,13 +961,20 @@ export function registerActiveContext(pi: any, options: {
|
|
|
914
961
|
}
|
|
915
962
|
}
|
|
916
963
|
const wire = persistence.persistedWireVersion === 2 ? makeStateDelta(persistence.persisted, next) : makeStateCheckpoint(next);
|
|
917
|
-
|
|
964
|
+
// THE WIRE CARRIES BOTH DIGESTS IT JUST DERIVED (2026-08-28). `makeStateDelta` writes
|
|
965
|
+
// `baseStateSha256` from `persistence.persisted` and `stateSha256` from `next`, and
|
|
966
|
+
// nothing mutates either state between that call and here, so re-hashing two whole
|
|
967
|
+
// states was pure repetition at about 7ms per durable write. The drift check keeps its
|
|
968
|
+
// teeth: `baseStateSha256` is still derived from `persistence.persisted`, so a
|
|
969
|
+
// persisted digest that has drifted from the state it claims to describe still raises.
|
|
970
|
+
// Gate 152 pins both equalities.
|
|
971
|
+
if (wire.kind === "delta" && persistence.persistedStateSha256 !== wire.baseStateSha256) {
|
|
918
972
|
throw new Error("Active-context durable base digest drift");
|
|
919
973
|
}
|
|
920
974
|
await pi.appendEntry(stateEntryType, wire);
|
|
921
975
|
persistence.persisted = clone(next);
|
|
922
976
|
persistence.persistedWireVersion = 2;
|
|
923
|
-
persistence.persistedStateSha256 =
|
|
977
|
+
persistence.persistedStateSha256 = wire.stateSha256;
|
|
924
978
|
persistence.state = lifecycle.shuttingDown ? null : clone(next);
|
|
925
979
|
if (!sessionIdentityStillValid(ctx, sessionId, generationAtStart)) {
|
|
926
980
|
if (ctx && !contextSessionMatches(ctx, sessionId)) load(ctx);
|
|
@@ -1148,7 +1202,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1148
1202
|
|
|
1149
1203
|
const persistManual = async (
|
|
1150
1204
|
next: ActiveContextState,
|
|
1151
|
-
action: Exclude<ActiveContextToolAction, "status">,
|
|
1152
1205
|
ctx: any,
|
|
1153
1206
|
): Promise<void> => {
|
|
1154
1207
|
const stateAtEntry = persistence.state ? clone(persistence.state) : null;
|
|
@@ -1279,53 +1332,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1279
1332
|
safeNotify(ctx, `Automatic context management suspended: ${message}`, "warning");
|
|
1280
1333
|
};
|
|
1281
1334
|
|
|
1282
|
-
const abortUnsafeHardContext = (
|
|
1283
|
-
snapshot: ActiveContextSnapshot | null,
|
|
1284
|
-
ctx: any,
|
|
1285
|
-
allowUnmeasuredRevisionRelease = false,
|
|
1286
|
-
): boolean => {
|
|
1287
|
-
if (measurements.latestRatio === null || measurements.latestRatio < hardFenceRatio(snapshot ?? undefined, ctx) || !persistence.state) return false;
|
|
1288
|
-
const measuredRevision = measurements.lastProviderMeasurement
|
|
1289
|
-
? measurements.providerMeasurementRevisionByMessageSha.get(measurements.lastProviderMeasurement.messageSha256)
|
|
1290
|
-
: undefined;
|
|
1291
|
-
if (allowUnmeasuredRevisionRelease && !ladder.automaticFailure &&
|
|
1292
|
-
measuredRevision !== undefined && measurements.lastProviderMeasurement &&
|
|
1293
|
-
!durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
|
|
1294
|
-
const releaseKey = sha256Value({
|
|
1295
|
-
sessionId: persistence.state.sessionId,
|
|
1296
|
-
topologySha256: topologySha256(persistence.state),
|
|
1297
|
-
protectionSha256: protectionSha256(persistence.state),
|
|
1298
|
-
measuredRevision,
|
|
1299
|
-
providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
|
|
1300
|
-
});
|
|
1301
|
-
if (!advisory.hardFenceReleasedProjectionKeys.has(releaseKey) &&
|
|
1302
|
-
advisory.hardFenceReleasedProjectionKeys.size < 4_096) {
|
|
1303
|
-
advisory.hardFenceReleasedProjectionKeys.add(releaseKey);
|
|
1304
|
-
return false;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
const key = ladder.automaticFailure?.key ?? sha256Value({
|
|
1308
|
-
sessionId: snapshot?.sessionId ?? persistence.state.sessionId,
|
|
1309
|
-
revision: persistence.state.revision,
|
|
1310
|
-
providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
|
|
1311
|
-
phase: "hard-provider-fence",
|
|
1312
|
-
});
|
|
1313
|
-
ladder.pendingContextNote = `Provider context reached the hard ${brandNoun} fence without a newly committed lossless fold. ` +
|
|
1314
|
-
"The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
|
|
1315
|
-
if (advisory.hardFenceNoticeKey !== key) {
|
|
1316
|
-
advisory.hardFenceNoticeKey = key;
|
|
1317
|
-
safeNotify(
|
|
1318
|
-
ctx,
|
|
1319
|
-
"Provider request aborted at the hard context fence; run /compact or make an explicit bounded context fold.",
|
|
1320
|
-
"error",
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1323
|
-
if (typeof ctx.abort !== "function") {
|
|
1324
|
-
throw new Error(`Pi hard-fence abort capability is unavailable at ratio ${measurements.latestRatio}`);
|
|
1325
|
-
}
|
|
1326
|
-
ctx.abort();
|
|
1327
|
-
return true;
|
|
1328
|
-
};
|
|
1329
1335
|
|
|
1330
1336
|
const PROJECTION_CALIBRATION_MIN_CHARS = 20_000;
|
|
1331
1337
|
const PROJECTION_CALIBRATION_MIN_TOKENS = 5_000;
|
|
@@ -1346,6 +1352,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1346
1352
|
}
|
|
1347
1353
|
const chars = measurements.lastProjectedChars;
|
|
1348
1354
|
if (chars === null || chars < PROJECTION_CALIBRATION_MIN_CHARS) return;
|
|
1355
|
+
// NEVER LEARN THE RATIO FROM A WINDOW HOLDING AN IMAGE. chars/token is a fact about
|
|
1356
|
+
// prose; a projection carrying base64 has a far higher true ratio, and because
|
|
1357
|
+
// projectionCharsPerToken takes the MINIMUM over the window such a sample is either
|
|
1358
|
+
// discarded (leaving the over-read in place) or, once images dominate, poisons the
|
|
1359
|
+
// ratio for the text around them. The clean rule is to calibrate on text alone.
|
|
1360
|
+
if (measurements.lastProjectedImages > 0) return;
|
|
1349
1361
|
if (!Number.isFinite(measurement.tokens) || measurement.tokens < PROJECTION_CALIBRATION_MIN_TOKENS) return;
|
|
1350
1362
|
if (measurements.lastProjectedEstimate !== null && measurements.lastProjectedEstimateCalibrated &&
|
|
1351
1363
|
measurement.tokens > 0) {
|
|
@@ -1379,13 +1391,26 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1379
1391
|
return;
|
|
1380
1392
|
}
|
|
1381
1393
|
const chars = bytes(text);
|
|
1382
|
-
|
|
1394
|
+
// The anchor is differenced against TEXT characters, so it must store them. The mass
|
|
1395
|
+
// belongs to the same `previousText` this anchor is built from, recorded by
|
|
1396
|
+
// noteProjection when that text was produced.
|
|
1397
|
+
const textChars = Math.max(0, chars - instrumentation.previousImageChars);
|
|
1398
|
+
const images = instrumentation.previousImages;
|
|
1399
|
+
// The floor test reads TEXT against tokens: an image-heavy projection has a raw ratio
|
|
1400
|
+
// far above any prose floor and would sail through a test that is meant to reject a
|
|
1401
|
+
// measurement the text cannot explain.
|
|
1402
|
+
if (!(measurement.tokens > 0) ||
|
|
1403
|
+
(measurement.tokens > images * IMAGE_ESTIMATED_TOKENS &&
|
|
1404
|
+
textChars / (measurement.tokens - images * IMAGE_ESTIMATED_TOKENS) <
|
|
1405
|
+
PROJECTION_CHARS_PER_TOKEN_FLOOR)) {
|
|
1383
1406
|
measurements.projectionAnchor = null;
|
|
1384
1407
|
return;
|
|
1385
1408
|
}
|
|
1386
1409
|
measurements.projectionAnchor = {
|
|
1387
1410
|
tokens: measurement.tokens,
|
|
1388
1411
|
chars,
|
|
1412
|
+
textChars,
|
|
1413
|
+
images,
|
|
1389
1414
|
head: text.slice(0, -1),
|
|
1390
1415
|
messageSha256: measurement.messageSha256,
|
|
1391
1416
|
sessionId: persistence.state.sessionId,
|
|
@@ -1393,33 +1418,61 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1393
1418
|
};
|
|
1394
1419
|
};
|
|
1395
1420
|
|
|
1396
|
-
|
|
1421
|
+
// THE SERIALIZATION TRAVELS, THE READING DOES NOT (2026-08-28). Six whole-window
|
|
1422
|
+
// serializations rode every provider request and four were the same string from the same
|
|
1423
|
+
// unmutated array. Only the TEXT is threaded, never a finished reading: each site still
|
|
1424
|
+
// re-reads `measurements.projectionAnchor` and `projectionCharsPerToken()` at its own
|
|
1425
|
+
// time, so every emitted number stays what it was. That matters on the commit path, where
|
|
1426
|
+
// `enforceProjectionBudget` awaits `attemptAutomaticCommit` and an `attributionChanged`
|
|
1427
|
+
// can null the anchor between the last measurement and the read: threading a reading would
|
|
1428
|
+
// freeze `basis` at "measured" where today it flips to "unmeasured". Roughly 50ms per
|
|
1429
|
+
// request at a 467KB window, 100ms at 1MB, all of it blocking main-loop CPU. Honest
|
|
1430
|
+
// framing: six become three, not one.
|
|
1431
|
+
const projectedTokenReading = (projected: unknown[], serialized?: string): {
|
|
1397
1432
|
tokens: number;
|
|
1398
1433
|
basis: ProjectionReadingBasis;
|
|
1399
1434
|
chars: number;
|
|
1435
|
+
/** The image-corrected size of THIS projection, independent of any anchor. */
|
|
1436
|
+
sizeTokens: number;
|
|
1437
|
+
images: number;
|
|
1400
1438
|
anchorTokens: number | null;
|
|
1401
1439
|
deltaChars: number | null;
|
|
1440
|
+
text: string;
|
|
1402
1441
|
} => {
|
|
1403
|
-
const text = stableStringify(projected);
|
|
1442
|
+
const text = serialized ?? stableStringify(projected);
|
|
1404
1443
|
const chars = Buffer.byteLength(text, "utf8");
|
|
1405
1444
|
const charsPerToken = projectionCharsPerToken();
|
|
1406
|
-
|
|
1445
|
+
// AN IMAGE IS NOT TEXT (2026-08-28, see imageMass). Its base64 leaves the character
|
|
1446
|
+
// count and comes back as a flat per-image cost, because dividing base64 by a ratio
|
|
1447
|
+
// learned from prose read one screenshot as 110,000 tokens against a true 1,500 and
|
|
1448
|
+
// aborted a session that was less than half full.
|
|
1449
|
+
const mass = imageMass(projected);
|
|
1450
|
+
const textChars = Math.max(0, chars - mass.base64Chars);
|
|
1451
|
+
const imageTokens = mass.images * IMAGE_ESTIMATED_TOKENS;
|
|
1452
|
+
const estimate = Math.ceil(textChars / charsPerToken) + imageTokens;
|
|
1407
1453
|
const anchor = measurements.projectionAnchor;
|
|
1408
1454
|
if (!anchor || anchor.generation !== lifecycle.generation ||
|
|
1409
1455
|
anchor.sessionId !== persistence.state?.sessionId) {
|
|
1410
|
-
return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null };
|
|
1456
|
+
return { tokens: estimate, basis: "unmeasured", chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
|
|
1411
1457
|
}
|
|
1412
1458
|
const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
|
|
1413
1459
|
if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
|
|
1414
|
-
return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null };
|
|
1460
|
+
return { tokens: estimate, basis: "rewritten", chars, sizeTokens: estimate, images: mass.images, anchorTokens: anchor.tokens, deltaChars: null, text };
|
|
1415
1461
|
}
|
|
1416
|
-
|
|
1462
|
+
// The delta is taken on TEXT characters and images separately, so an image arriving
|
|
1463
|
+
// after the anchor costs its own flat price rather than its base64 length.
|
|
1464
|
+
const deltaChars = textChars - anchor.textChars;
|
|
1465
|
+
const deltaImages = mass.images - anchor.images;
|
|
1417
1466
|
return {
|
|
1418
|
-
tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken)
|
|
1467
|
+
tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken) +
|
|
1468
|
+
deltaImages * IMAGE_ESTIMATED_TOKENS),
|
|
1419
1469
|
basis: "anchored",
|
|
1420
1470
|
chars,
|
|
1471
|
+
sizeTokens: estimate,
|
|
1472
|
+
images: mass.images,
|
|
1421
1473
|
anchorTokens: anchor.tokens,
|
|
1422
1474
|
deltaChars,
|
|
1475
|
+
text,
|
|
1423
1476
|
};
|
|
1424
1477
|
};
|
|
1425
1478
|
|
|
@@ -1450,7 +1503,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1450
1503
|
estimatorErrorShare() * estimate + expectedInflowTokens(),
|
|
1451
1504
|
));
|
|
1452
1505
|
|
|
1453
|
-
const projectionExceedsBudget = (projected: unknown[], ctx: any): {
|
|
1506
|
+
const projectionExceedsBudget = (projected: unknown[], ctx: any, serialized?: string): {
|
|
1454
1507
|
tokens: number;
|
|
1455
1508
|
sizeTokens: number;
|
|
1456
1509
|
basis: ProjectionReadingBasis;
|
|
@@ -1458,14 +1511,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1458
1511
|
marginTokens: number;
|
|
1459
1512
|
over: boolean;
|
|
1460
1513
|
crowded: boolean;
|
|
1514
|
+
text: string;
|
|
1461
1515
|
} => {
|
|
1462
1516
|
const capacity = currentCapacity(ctx);
|
|
1463
1517
|
const budgetTokens = Number.isFinite(capacity.budgetTokens) && capacity.budgetTokens > 0
|
|
1464
1518
|
? capacity.budgetTokens
|
|
1465
1519
|
: Number.POSITIVE_INFINITY;
|
|
1466
|
-
const reading = projectedTokenReading(projected);
|
|
1520
|
+
const reading = projectedTokenReading(projected, serialized);
|
|
1467
1521
|
const tokens = reading.tokens;
|
|
1468
|
-
const sizeTokens =
|
|
1522
|
+
const sizeTokens = reading.sizeTokens;
|
|
1469
1523
|
const marginTokens = Number.isFinite(budgetTokens)
|
|
1470
1524
|
? projectionMarginTokens(tokens, capacity.window)
|
|
1471
1525
|
: 0;
|
|
@@ -1477,43 +1531,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1477
1531
|
marginTokens,
|
|
1478
1532
|
over: tokens > budgetTokens,
|
|
1479
1533
|
crowded: tokens + marginTokens > budgetTokens,
|
|
1534
|
+
text: reading.text,
|
|
1480
1535
|
};
|
|
1481
1536
|
};
|
|
1482
1537
|
|
|
1483
|
-
const abortOverBudgetProjection = (
|
|
1484
|
-
tokens: number,
|
|
1485
|
-
budgetTokens: number,
|
|
1486
|
-
ctx: any,
|
|
1487
|
-
recoveryAttempts = 0,
|
|
1488
|
-
): void => {
|
|
1489
|
-
const key = sha256Value({
|
|
1490
|
-
sessionId: persistence.state?.sessionId ?? null,
|
|
1491
|
-
revision: persistence.state?.revision ?? null,
|
|
1492
|
-
tokens,
|
|
1493
|
-
budgetTokens,
|
|
1494
|
-
phase: "projection-budget-fence",
|
|
1495
|
-
});
|
|
1496
|
-
ladder.pendingContextNote =
|
|
1497
|
-
`The ${brandNoun} projection estimates ${tokens} tokens against a ${budgetTokens}-token serving budget` +
|
|
1498
|
-
(recoveryAttempts
|
|
1499
|
-
? `, after ${recoveryAttempts} recovery attempt(s) that folded everything foldable. The remaining ` +
|
|
1500
|
-
"inflow does not fit at any folding depth, which is an impossibility rather than a recoverable state."
|
|
1501
|
-
: ".") +
|
|
1502
|
-
" The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
|
|
1503
|
-
if (advisory.hardFenceNoticeKey !== key) {
|
|
1504
|
-
advisory.hardFenceNoticeKey = key;
|
|
1505
|
-
safeNotify(
|
|
1506
|
-
ctx,
|
|
1507
|
-
"Provider request aborted: the projection exceeds the serving budget. " +
|
|
1508
|
-
"Run /compact or make an explicit bounded context fold.",
|
|
1509
|
-
"error",
|
|
1510
|
-
);
|
|
1511
|
-
}
|
|
1512
|
-
if (typeof ctx.abort !== "function") {
|
|
1513
|
-
throw new Error(`Pi projection-budget abort capability is unavailable at ${tokens} estimated tokens`);
|
|
1514
|
-
}
|
|
1515
|
-
ctx.abort();
|
|
1516
|
-
};
|
|
1517
1538
|
|
|
1518
1539
|
// THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
|
|
1519
1540
|
//
|
|
@@ -1626,9 +1647,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1626
1647
|
* and it does not: the rebuild reads the marks, not its own last text.
|
|
1627
1648
|
*/
|
|
1628
1649
|
const appendFoldNotice = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1650
|
+
// The deterministic experiment condition: cuts stage and commits land exactly as
|
|
1651
|
+
// ever, but nothing invites a brief, so briefs stay the runtime's own.
|
|
1652
|
+
if (!postFoldNotice) return projected;
|
|
1629
1653
|
if (!persistence.state) return projected;
|
|
1630
1654
|
const held = pendingMarks(persistence.state);
|
|
1631
|
-
const unbriefed = held.filter((mark) =>
|
|
1655
|
+
const unbriefed = held.filter((mark) =>
|
|
1656
|
+
mark.briefProvenance?.kind !== "supplied" && mark.briefProvenance?.kind !== "augmented");
|
|
1632
1657
|
// BATCHED, AND APPENDED ONCE. A notice per cut is noise, and noise is how guidance
|
|
1633
1658
|
// gets ignored; the frontier cuts often and this speaks only when a batch has built
|
|
1634
1659
|
// up. `carrierAdmitted` is what makes it an APPEND rather than a rewrite: once the
|
|
@@ -1649,6 +1674,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1649
1674
|
id: mark.id,
|
|
1650
1675
|
kind: mark.kind,
|
|
1651
1676
|
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
1677
|
+
brief: typeof mark.brief === "string" ? mark.brief : undefined,
|
|
1652
1678
|
})),
|
|
1653
1679
|
pending: held.length,
|
|
1654
1680
|
toolName,
|
|
@@ -1673,6 +1699,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1673
1699
|
return projected;
|
|
1674
1700
|
};
|
|
1675
1701
|
|
|
1702
|
+
|
|
1676
1703
|
const enforceBandTop = async (
|
|
1677
1704
|
snapshot: ActiveContextSnapshot,
|
|
1678
1705
|
ctx: any,
|
|
@@ -1711,10 +1738,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1711
1738
|
snapshot: ActiveContextSnapshot,
|
|
1712
1739
|
projected: unknown[],
|
|
1713
1740
|
ctx: any,
|
|
1714
|
-
): Promise<{ projected: unknown[];
|
|
1741
|
+
): Promise<{ projected: unknown[]; text: string }> => {
|
|
1715
1742
|
let measured = projectionExceedsBudget(projected, ctx);
|
|
1716
1743
|
const rejected = curation.pendingRejection !== null;
|
|
1717
|
-
if (!measured.crowded && !rejected) return { projected,
|
|
1744
|
+
if (!measured.crowded && !rejected) return { projected, text: measured.text };
|
|
1718
1745
|
const trigger = measured;
|
|
1719
1746
|
let reduced = projected;
|
|
1720
1747
|
let attempts = 0;
|
|
@@ -1818,9 +1845,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1818
1845
|
}));
|
|
1819
1846
|
curation.pendingRejection = null;
|
|
1820
1847
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1848
|
+
// THE RUNTIME COMMITS AND TRANSMITS; ONLY THE PROVIDER REFUSES (Shane, 2026-08-28).
|
|
1849
|
+
//
|
|
1850
|
+
// This used to abort the request when the projection still read over budget after the
|
|
1851
|
+
// loop. It was deleted because it let an ESTIMATE veto a request that ground truth
|
|
1852
|
+
// would have accepted: a live session read 220,883 tokens where the provider counted
|
|
1853
|
+
// 111,837, was refused before transmission, and could not be rescued, because the
|
|
1854
|
+
// commit before the refusal had applied 28 marks and the next applied 0. The advice it
|
|
1855
|
+
// printed, fold manually or run /compact, named the two things the mechanism had just
|
|
1856
|
+
// proven it could not do.
|
|
1857
|
+
//
|
|
1858
|
+
// maxTarget is a TRIGGER meaning "commit now", not a bound meaning "refuse to
|
|
1859
|
+
// proceed", and only the first is something an estimate can be right about. What
|
|
1860
|
+
// remains is the loop above, which commits everything it can reach; the request then
|
|
1861
|
+
// goes to the provider, and a genuine overflow is handled by the rejection path, which
|
|
1862
|
+
// branches back, folds and retries on the provider's own count rather than ours.
|
|
1863
|
+
// `measured` is recomputed on `reduced` at every turn of the loop, so its text is the
|
|
1864
|
+
// serialization of the array this returns.
|
|
1865
|
+
return { projected: reduced, text: measured.text };
|
|
1824
1866
|
};
|
|
1825
1867
|
|
|
1826
1868
|
const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
|
|
@@ -1984,25 +2026,34 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1984
2026
|
if (freeze.projection) carryWithdrawalIntoFreeze(freeze.projection, withdrawn);
|
|
1985
2027
|
}
|
|
1986
2028
|
const held = freeze.body?.length ?? 0;
|
|
1987
|
-
|
|
1988
|
-
|
|
2029
|
+
// THE PREFIX IS READ OUT OF THE WHOLE SERIALIZATION (2026-08-28). This asked
|
|
2030
|
+
// `stableStringify(body.slice(0, held)) === freeze.bodyText`, and in steady state `held`
|
|
2031
|
+
// is `body.length` minus the couple of messages that just arrived, so it was a second
|
|
2032
|
+
// full serialization of the window plus a slice allocation, beside the one below that
|
|
2033
|
+
// every pass does anyway. `serializedPrefixHolds` carries why that is sound; a false
|
|
2034
|
+
// POSITIVE here splices the previous pass's `freeze.projection` in front of the new tail
|
|
2035
|
+
// and emits stale messages, which is a content bug rather than a slowdown, and gate 154
|
|
2036
|
+
// drives the predicate itself rather than restating it.
|
|
2037
|
+
const bodyText = stableStringify(body);
|
|
2038
|
+
freeze.active = freeze.projection !== null &&
|
|
2039
|
+
serializedPrefixHolds(bodyText, freeze.bodyText, held, body.length);
|
|
1989
2040
|
const projected = freeze.active
|
|
1990
2041
|
? [...freeze.projection!, ...body.slice(held)]
|
|
1991
2042
|
: [...body];
|
|
1992
2043
|
if (!freeze.active) freeze.keys.clear();
|
|
1993
2044
|
freeze.body = body;
|
|
1994
|
-
freeze.bodyText =
|
|
2045
|
+
freeze.bodyText = bodyText;
|
|
1995
2046
|
appendReceipts(projected, snapshot);
|
|
1996
2047
|
appendFoldNotice(projected, snapshot);
|
|
1997
2048
|
return holdFrozen(projected);
|
|
1998
2049
|
};
|
|
1999
|
-
const noteProjection = (projected: unknown[]): void => {
|
|
2050
|
+
const noteProjection = (projected: unknown[], serialized?: string): void => {
|
|
2000
2051
|
const digests = messageDigests(projected);
|
|
2001
2052
|
const comparison = compareProjections(instrumentation.previousDigests, digests);
|
|
2002
2053
|
recordProjection(instrumentation.ledger, comparison, digests);
|
|
2003
2054
|
instrumentation.previousDigests = digests;
|
|
2004
2055
|
instrumentation.lastChange = comparison.change;
|
|
2005
|
-
const text = stableStringify(projected);
|
|
2056
|
+
const text = serialized ?? stableStringify(projected);
|
|
2006
2057
|
const divergence = prefixDivergence(instrumentation.previousText, text);
|
|
2007
2058
|
const previousChars = instrumentation.previousText?.length ?? 0;
|
|
2008
2059
|
instrumentation.lastPreservedShare = previousChars > 0
|
|
@@ -2030,7 +2081,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2030
2081
|
: latestCacheActionClass
|
|
2031
2082
|
? latestCacheActionClass
|
|
2032
2083
|
: divergence.index === null ? "steady-state" : "after-message";
|
|
2033
|
-
const reading = projectedTokenReading(projected);
|
|
2084
|
+
const reading = projectedTokenReading(projected, text);
|
|
2034
2085
|
emit("context.projection", {
|
|
2035
2086
|
change: comparison.change,
|
|
2036
2087
|
previous_count: comparison.previousCount,
|
|
@@ -2076,6 +2127,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2076
2127
|
snapshot: ActiveContextSnapshot;
|
|
2077
2128
|
candidate: FoldCandidate;
|
|
2078
2129
|
brief?: string;
|
|
2130
|
+
briefProvenance?: "supplied" | "deterministic" | BriefProvenance;
|
|
2079
2131
|
ctx: any;
|
|
2080
2132
|
signal?: AbortSignal;
|
|
2081
2133
|
maximumSourceChars?: number;
|
|
@@ -2093,6 +2145,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2093
2145
|
state: baseState,
|
|
2094
2146
|
generation: generationAtStart,
|
|
2095
2147
|
brief: input.brief,
|
|
2148
|
+
briefProvenance: input.briefProvenance,
|
|
2096
2149
|
ctx: input.ctx,
|
|
2097
2150
|
signal: input.signal,
|
|
2098
2151
|
});
|
|
@@ -2226,6 +2279,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2226
2279
|
});
|
|
2227
2280
|
state = wedges.state;
|
|
2228
2281
|
const accounting = markAccounting(snapshot, state);
|
|
2282
|
+
// Derived ONCE, here, against the pre-commit state both readers below report against.
|
|
2283
|
+
// It left `MarkAccounting` because it serializes the whole marked tail and every other
|
|
2284
|
+
// caller of `markAccounting` was paying for a number only this path reads.
|
|
2285
|
+
const rewriteTokens = markRewriteTokens(snapshot, state);
|
|
2229
2286
|
if (!accounting.pending) {
|
|
2230
2287
|
const remainder = unmarkedRemainder(snapshot, state, projectionCharsPerToken());
|
|
2231
2288
|
emit("context.commit", {
|
|
@@ -2328,6 +2385,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2328
2385
|
!closed.applied.some((appliedMark) => appliedMark.id === mark.id)), ...closed.retained],
|
|
2329
2386
|
};
|
|
2330
2387
|
}
|
|
2388
|
+
// THE TOOL-CALL DIET RIDES THE SAME TRANSACTION (2026-08-24): clips are selected
|
|
2389
|
+
// against the post-fold state and land in the same revision the commit minted, so
|
|
2390
|
+
// the one paid rewrite carries both and no non-commit pass ever moves a byte. The
|
|
2391
|
+
// full bytes stay in the transcript behind each clip's entry id.
|
|
2392
|
+
let clippedResults = 0;
|
|
2393
|
+
if (toolFoldThreshold > 0) {
|
|
2394
|
+
const clipAdditions = toolClipAdditions({
|
|
2395
|
+
snapshot,
|
|
2396
|
+
state: result.state,
|
|
2397
|
+
threshold: toolFoldThreshold,
|
|
2398
|
+
blacklist: blacklistAutoFoldTools,
|
|
2399
|
+
});
|
|
2400
|
+
if (clipAdditions.length) {
|
|
2401
|
+
clippedResults = clipAdditions.length;
|
|
2402
|
+
result = { ...result, state: { ...result.state,
|
|
2403
|
+
clips: [...(result.state.clips ?? []), ...clipAdditions] } };
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2331
2406
|
persistence.state = result.state;
|
|
2332
2407
|
const bytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
|
|
2333
2408
|
const freedBytes = Math.max(0, bytesBefore - bytesAfter);
|
|
@@ -2362,9 +2437,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2362
2437
|
consolidation_marks: consolidationAdded,
|
|
2363
2438
|
closing_consolidation_marks: closingAdded,
|
|
2364
2439
|
absorbed_wedges: wedges.absorbed.length,
|
|
2440
|
+
clipped_results: clippedResults,
|
|
2365
2441
|
freed_bytes: freedBytes,
|
|
2366
2442
|
freed_tokens: estimatedTokens(freedBytes),
|
|
2367
|
-
rewrite_tokens:
|
|
2443
|
+
rewrite_tokens: rewriteTokens,
|
|
2368
2444
|
pinned_bytes: accounting.pinnedBytes,
|
|
2369
2445
|
pinned_results: accounting.pinnedResults,
|
|
2370
2446
|
protected_stale_bytes: pinHeld.bytes,
|
|
@@ -2436,7 +2512,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2436
2512
|
protectedStaleRefs: pinHeld.refs,
|
|
2437
2513
|
retainedMarks: result.retained.length,
|
|
2438
2514
|
eligibleMarks: accounting.eligibleMarks,
|
|
2439
|
-
estimatedRewriteTokens:
|
|
2515
|
+
estimatedRewriteTokens: rewriteTokens,
|
|
2440
2516
|
estimatedFreedTokens: accounting.freedTokens,
|
|
2441
2517
|
freedBudgetShare: accounting.freedBudgetShare,
|
|
2442
2518
|
sourceBytesSaved: freedBytes,
|
|
@@ -2486,8 +2562,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2486
2562
|
occupancyAfter: occupancyBefore === null ? null : Math.max(0, occupancyBefore - freedTokens),
|
|
2487
2563
|
spansFolded: epoch ? Number(epoch.foldedSpans ?? 0) : foldIds,
|
|
2488
2564
|
toolResultsFolded: epoch ? Number(epoch.foldedToolResults ?? 0) : 0,
|
|
2489
|
-
splitFolds: Number(action.splitFolds ?? 0),
|
|
2490
|
-
splitFromChars: Number(action.splitFromChars ?? 0),
|
|
2491
2565
|
absorbedWedges: Number(epoch?.absorbedWedges ?? 0),
|
|
2492
2566
|
recovered: curation.recoveryAttempts > 0,
|
|
2493
2567
|
protectedBytes: Number(epoch?.protectedStaleBytes ?? 0),
|
|
@@ -2759,11 +2833,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2759
2833
|
try { projected = projectWithAdvisory(snapshot); }
|
|
2760
2834
|
catch (error) {
|
|
2761
2835
|
suspendAutomatic(error, "projection", ctx);
|
|
2762
|
-
abortUnsafeHardContext(snapshot, ctx);
|
|
2763
|
-
return { messages: event.messages };
|
|
2764
|
-
}
|
|
2765
|
-
if (abortUnsafeHardContext(snapshot, ctx, true)) {
|
|
2766
|
-
updateStatus(ctx);
|
|
2767
2836
|
return { messages: event.messages };
|
|
2768
2837
|
}
|
|
2769
2838
|
// THE FRONTIER CUTS FIRST, and it costs the projection nothing to do so: a pending
|
|
@@ -2779,23 +2848,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2779
2848
|
try { projected = projectWithAdvisory(snapshot); }
|
|
2780
2849
|
catch (error) {
|
|
2781
2850
|
suspendAutomatic(error, "projection", ctx);
|
|
2782
|
-
abortUnsafeHardContext(snapshot, ctx);
|
|
2783
2851
|
return { messages: event.messages };
|
|
2784
2852
|
}
|
|
2785
2853
|
}
|
|
2786
2854
|
const budgeted = await enforceProjectionBudget(snapshot, projected, ctx);
|
|
2787
2855
|
projected = budgeted.projected;
|
|
2788
|
-
const reading = projectedTokenReading(projected);
|
|
2856
|
+
const reading = projectedTokenReading(projected, budgeted.text);
|
|
2789
2857
|
measurements.lastProjectedChars = reading.chars;
|
|
2858
|
+
measurements.lastProjectedImages = reading.images;
|
|
2790
2859
|
measurements.lastProjectedEstimate = reading.tokens;
|
|
2791
2860
|
measurements.lastProjectedEstimateBasis = reading.basis;
|
|
2792
|
-
|
|
2861
|
+
// The size is the reading's own image-corrected figure. Recomputing it from raw
|
|
2862
|
+
// chars here is what fed base64 straight into the fence's own accounting.
|
|
2863
|
+
measurements.lastProjectedSizeTokens = reading.sizeTokens;
|
|
2793
2864
|
measurements.lastProjectedEstimateCalibrated = measurements.projectionCalibrations.length > 0;
|
|
2794
|
-
|
|
2795
|
-
updateStatus(ctx);
|
|
2796
|
-
return { messages: projected };
|
|
2797
|
-
}
|
|
2798
|
-
noteProjection(projected);
|
|
2865
|
+
noteProjection(projected, budgeted.text);
|
|
2799
2866
|
updateStatus(ctx);
|
|
2800
2867
|
return { messages: projected };
|
|
2801
2868
|
} catch (error) {
|
|
@@ -2808,7 +2875,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2808
2875
|
if (!persistedSucceeded) ladder.boundaryFailure = lifecycle.latestSnapshotError;
|
|
2809
2876
|
suspendAutomatic(error, persistedSucceeded ? "post-persist-projection" : "context", ctx);
|
|
2810
2877
|
}
|
|
2811
|
-
abortUnsafeHardContext(lifecycle.latestSnapshot, ctx);
|
|
2812
2878
|
return { messages: event.messages };
|
|
2813
2879
|
}
|
|
2814
2880
|
};
|
|
@@ -3255,15 +3321,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3255
3321
|
);
|
|
3256
3322
|
};
|
|
3257
3323
|
|
|
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
3324
|
/**
|
|
3268
3325
|
* THE WITHDRAWAL PATH, shared by the tool's unmark action and the editor's
|
|
3269
3326
|
* withdraw intent: same refusal for an unknown id, same persist, same status.
|
|
@@ -3275,7 +3332,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3275
3332
|
const removed = before.filter((mark) => requested.has(mark.id));
|
|
3276
3333
|
const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
|
|
3277
3334
|
if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
|
|
3278
|
-
await persistManual(withPendingMarks(persistence.state!, kept),
|
|
3335
|
+
await persistManual(withPendingMarks(persistence.state!, kept), ctx);
|
|
3279
3336
|
updateStatus(ctx);
|
|
3280
3337
|
return removed as Array<Record<string, unknown>>;
|
|
3281
3338
|
};
|
|
@@ -3305,7 +3362,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3305
3362
|
"folding keeps entries exactly recoverable without pinning them.");
|
|
3306
3363
|
}
|
|
3307
3364
|
}
|
|
3308
|
-
await persistManual(nextProtection,
|
|
3365
|
+
await persistManual(nextProtection, ctx);
|
|
3309
3366
|
emit("context.pin", {
|
|
3310
3367
|
pin,
|
|
3311
3368
|
ids: ids.join(","),
|
|
@@ -3316,140 +3373,140 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3316
3373
|
updateStatus(ctx);
|
|
3317
3374
|
};
|
|
3318
3375
|
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3376
|
+
/**
|
|
3377
|
+
* THE ONE STAGING PATH FOR FOLD MARKS (2026-08-23). The /fold-editor's user marks
|
|
3378
|
+
* run THIS code and nothing else: span snapping, claim refusal, brief contract,
|
|
3379
|
+
* prepareFold, replacement of a standing mark over the same span. A user mark is
|
|
3380
|
+
* mechanically an ordinary mark: it counts toward the commit's coverage, answers
|
|
3381
|
+
* with its commit arithmetic, and folds at the epoch exactly as a runtime mark does.
|
|
3382
|
+
*/
|
|
3383
|
+
const stageFoldMark = async (
|
|
3384
|
+
ids: string[],
|
|
3385
|
+
supplied: string | undefined,
|
|
3322
3386
|
signal: AbortSignal,
|
|
3323
3387
|
ctx: any,
|
|
3324
3388
|
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 };
|
|
3389
|
+
): Promise<{ mark: Record<string, unknown>; corrections: SpanCorrection[]; staged: ActiveContextState }> => {
|
|
3390
|
+
const snapped = snapFoldCandidate(snapshot, persistence.state, ids, { allowProtected: true });
|
|
3391
|
+
// AN AGENT'S SPAN IS TAKEN WHOLE (Shane, 2026-08-22). The bite-size cap was
|
|
3392
|
+
// applied to manual marks too, and it was the only caller: the ladder never
|
|
3393
|
+
// splits, because `selectAutomaticChapter` stops accumulating at the cap on its
|
|
3394
|
+
// own. So the split existed solely to second-guess the one judgement the design
|
|
3395
|
+
// wants to encourage, and it charged for it twice. One intended fold became five
|
|
3396
|
+
// or ten placeholders, and every piece was handed the SAME brief, so each one
|
|
3397
|
+
// claimed to describe a span it held a fraction of, while the correction text
|
|
3398
|
+
// said "each with its own brief", which was never true.
|
|
3399
|
+
//
|
|
3400
|
+
// The cap stays for the ladder, where it was earned (2026-08-06 rep 6: one
|
|
3401
|
+
// 60,432-byte chapter hid the fact the run needed). The difference is who chose
|
|
3402
|
+
// the boundary. A ladder fold is a span nobody vouched for, so it is kept small
|
|
3403
|
+
// enough to read back cheaply. An agent fold is a span the agent cut and wrote a
|
|
3404
|
+
// brief for, and it stays navigable by a route that did not exist in 2026-08:
|
|
3405
|
+
// peek takes offset and bytes, states the omitted middle, and reads any child id,
|
|
3406
|
+
// so a large fold has a narrow read. An expand that will not fit is refused with
|
|
3407
|
+
// a peek offered instead, so size costs a refusal rather than a window.
|
|
3408
|
+
const candidate = snapped.candidate;
|
|
3409
|
+
const corrections = snapped.corrections;
|
|
3410
|
+
let staged = persistence.state!;
|
|
3411
|
+
const alreadyPrepared = staged.folds.some((fold) =>
|
|
3412
|
+
fold.id === foldIdFor(candidate.kind, candidate.parts));
|
|
3413
|
+
const deferred = refsProtected(candidate.sourceRefs, staged, snapshot);
|
|
3414
|
+
// A MARK IS EDITABLE UNTIL IT COMMITS (Shane, 2026-08-21). Marking a span you
|
|
3415
|
+
// already marked REPLACES the mark, so a brief written in haste is corrected by
|
|
3416
|
+
// writing it again: the span is identical, the id derives from the span, and no
|
|
3417
|
+
// byte has moved yet, so the correction costs nothing at all. After the commit
|
|
3418
|
+
// the fold is standing and there is no rewrite verb, because rewriting a
|
|
3419
|
+
// standing brief rewrites the projection at its placeholder; `reboundary`
|
|
3420
|
+
// re-cuts the span instead, which was going to rewrite anyway.
|
|
3421
|
+
const replacing = foldIdFor(candidate.kind, candidate.parts);
|
|
3422
|
+
const claimed = markClaimingRef(staged, candidate.sourceRefs);
|
|
3423
|
+
if (claimed && claimed.markId !== replacing) {
|
|
3424
|
+
return {
|
|
3425
|
+
mark: {
|
|
3426
|
+
id: replacing,
|
|
3427
|
+
kind: candidate.kind,
|
|
3428
|
+
ok: false,
|
|
3429
|
+
deferred: false,
|
|
3430
|
+
reason: `Evidence ${claimed.entryId} is already held by pending mark ${claimed.markId}, ` +
|
|
3431
|
+
`which folds at the next commit. Unmark ${claimed.markId} first if you want to ` +
|
|
3432
|
+
"mark this span differently.",
|
|
3433
|
+
},
|
|
3434
|
+
corrections,
|
|
3435
|
+
staged,
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
const suppliedComplaint = supplied === undefined
|
|
3439
|
+
? null
|
|
3440
|
+
: briefContractComplaint(supplied.trim(), snapshot.policy.agentBriefReserve, snapshot.toolName);
|
|
3441
|
+
if (suppliedComplaint !== null) {
|
|
3442
|
+
return {
|
|
3443
|
+
mark: {
|
|
3444
|
+
id: replacing,
|
|
3445
|
+
kind: candidate.kind,
|
|
3446
|
+
ok: false,
|
|
3447
|
+
deferred: false,
|
|
3448
|
+
reason: `Supplied brief rejected. ${suppliedComplaint}`,
|
|
3449
|
+
},
|
|
3450
|
+
corrections,
|
|
3451
|
+
staged,
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
const briefed = !deferred && !alreadyPrepared
|
|
3455
|
+
? await prepareFold({
|
|
3456
|
+
candidate,
|
|
3457
|
+
snapshot,
|
|
3458
|
+
state: staged,
|
|
3459
|
+
generation: lifecycle.generation,
|
|
3460
|
+
brief: supplied,
|
|
3461
|
+
ctx,
|
|
3462
|
+
signal,
|
|
3463
|
+
})
|
|
3464
|
+
: null;
|
|
3465
|
+
const mark = foldMarkFor({
|
|
3466
|
+
candidate,
|
|
3467
|
+
brief: briefed?.fold.brief ?? supplied ?? ladderBrief(snapshot, staged, candidate),
|
|
3468
|
+
briefProvenance: briefed?.fold.provenance ??
|
|
3469
|
+
(supplied ? { kind: "supplied" } : { kind: "deterministic" }),
|
|
3470
|
+
origin: "user",
|
|
3471
|
+
ordinal: markOrdinal(snapshot),
|
|
3472
|
+
});
|
|
3473
|
+
// Drop the standing mark over this exact span before adding, so a re-mark is a
|
|
3474
|
+
// replacement rather than a duplicate refusal. `claimed` is this same mark by the
|
|
3475
|
+
// check above, and the id is derived from the span, so the state after is the
|
|
3476
|
+
// state a first mark with the new brief would have produced.
|
|
3477
|
+
const replaced = claimed !== null;
|
|
3478
|
+
if (replaced) {
|
|
3479
|
+
staged = withPendingMarks(staged,
|
|
3480
|
+
pendingMarks(staged).filter((standing) => standing.id !== replacing));
|
|
3481
|
+
}
|
|
3482
|
+
const addition = addPendingMark(staged, mark);
|
|
3483
|
+
if (!addition.added) {
|
|
3484
|
+
return {
|
|
3485
|
+
mark: { id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason },
|
|
3486
|
+
corrections,
|
|
3487
|
+
staged,
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
staged = addition.state;
|
|
3491
|
+
return {
|
|
3492
|
+
mark: {
|
|
3493
|
+
id: mark.id,
|
|
3494
|
+
kind: mark.kind,
|
|
3495
|
+
ok: true,
|
|
3496
|
+
deferred,
|
|
3497
|
+
...(replaced ? { replaced: true } : {}),
|
|
3498
|
+
...(deferred
|
|
3499
|
+
? {
|
|
3500
|
+
scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
|
|
3501
|
+
"first commit after it ages out",
|
|
3502
|
+
}
|
|
3503
|
+
: {}),
|
|
3504
|
+
brief: mark.brief,
|
|
3505
|
+
provenance: normalizeLegacyProvenance(mark.briefProvenance),
|
|
3506
|
+
},
|
|
3507
|
+
corrections,
|
|
3508
|
+
staged,
|
|
3509
|
+
};
|
|
3453
3510
|
};
|
|
3454
3511
|
|
|
3455
3512
|
const executeAction = async (
|
|
@@ -3584,7 +3641,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3584
3641
|
id: mark.id,
|
|
3585
3642
|
kind: mark.kind,
|
|
3586
3643
|
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
3587
|
-
briefed: mark.briefProvenance?.kind === "supplied"
|
|
3644
|
+
briefed: mark.briefProvenance?.kind === "supplied" ||
|
|
3645
|
+
mark.briefProvenance?.kind === "augmented",
|
|
3588
3646
|
brief: mark.brief ?? null,
|
|
3589
3647
|
briefProvenance: mark.briefProvenance ? clone(mark.briefProvenance) : null,
|
|
3590
3648
|
})),
|
|
@@ -3631,7 +3689,41 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3631
3689
|
"bytes",
|
|
3632
3690
|
);
|
|
3633
3691
|
const target = persistence.state.folds.find((item) => item.id === id);
|
|
3634
|
-
|
|
3692
|
+
// A CLIPPED RESULT ANSWERS TO ITS ENTRY ID (2026-08-24, the tool-call diet): the
|
|
3693
|
+
// clip marker names the entry, and the full bytes are in the transcript rather
|
|
3694
|
+
// than behind a fold, so the read serves them from the mapped message directly,
|
|
3695
|
+
// through the same admission fence and the same slice bounds as a fold peek.
|
|
3696
|
+
if (!target) {
|
|
3697
|
+
const clip = (persistence.state.clips ?? []).find((item) =>
|
|
3698
|
+
item.entryId === id || item.callId === id);
|
|
3699
|
+
if (clip) {
|
|
3700
|
+
const mapped = snapshot.mapped.find((item) => item.ref?.entryId === clip.entryId);
|
|
3701
|
+
if (!mapped) throw new Error(`Clipped result ${id} is no longer on this branch`);
|
|
3702
|
+
const full = contentText(snapshot.messages[mapped.index]);
|
|
3703
|
+
admit({
|
|
3704
|
+
action: "peek",
|
|
3705
|
+
ctx,
|
|
3706
|
+
foldId: clip.entryId,
|
|
3707
|
+
requestedBytes: Math.max(0, Math.min(sliceBytes, full.length - offset)),
|
|
3708
|
+
children: [],
|
|
3709
|
+
});
|
|
3710
|
+
const slice = full.slice(offset, offset + sliceBytes);
|
|
3711
|
+
return toolPayload({
|
|
3712
|
+
id: clip.entryId,
|
|
3713
|
+
source: slice,
|
|
3714
|
+
truncated: offset + slice.length < full.length,
|
|
3715
|
+
sourceChars: full.length,
|
|
3716
|
+
offset,
|
|
3717
|
+
...(peekIsEphemeral(params) ? {
|
|
3718
|
+
ephemeral: "this result rides your context exactly until your next message; " +
|
|
3719
|
+
"extract what your current task needs into that reply, which takes over its " +
|
|
3720
|
+
"place. Peeking again is lossless and cheap; pass ephemeral false when you " +
|
|
3721
|
+
"need these bytes standing for several turns",
|
|
3722
|
+
} : { durable: "this result stays in your context like any other tool result" }),
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
throw new Error(`Unknown active-context fold ${id}`);
|
|
3726
|
+
}
|
|
3635
3727
|
admit({
|
|
3636
3728
|
action: "peek",
|
|
3637
3729
|
ctx,
|
|
@@ -3690,7 +3782,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3690
3782
|
for (const part of [recut]) {
|
|
3691
3783
|
const durable = persistence.persistedFoldRecords.get(foldIdFor(part.kind, part.parts));
|
|
3692
3784
|
const { preparedFold, nextState } = await prepareAndCommitExplicit({
|
|
3693
|
-
|
|
3785
|
+
// A re-cut carries the durable fold's OWN brief and provenance: those words
|
|
3786
|
+
// were validated when they were first accepted, and re-judging them as a
|
|
3787
|
+
// fresh agent submission would refuse a deterministic or composed brief
|
|
3788
|
+
// against the agent's reserve.
|
|
3789
|
+
snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied,
|
|
3790
|
+
briefProvenance: durable ? durable.fold.provenance : undefined, ctx, signal,
|
|
3694
3791
|
});
|
|
3695
3792
|
persistence.state = nextState;
|
|
3696
3793
|
if (durable && supplied) correctedBriefs[preparedFold.id] = supplied;
|
|
@@ -3706,7 +3803,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3706
3803
|
briefs: { ...(persistence.state.briefs ?? {}), ...correctedBriefs },
|
|
3707
3804
|
};
|
|
3708
3805
|
}
|
|
3709
|
-
await persistManual(persistence.state,
|
|
3806
|
+
await persistManual(persistence.state, ctx);
|
|
3710
3807
|
updateStatus(ctx);
|
|
3711
3808
|
return toolPayload({
|
|
3712
3809
|
version: 1,
|
|
@@ -3744,7 +3841,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3744
3841
|
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3745
3842
|
const leases = { ...next.leases };
|
|
3746
3843
|
delete leases[id];
|
|
3747
|
-
await persistManual({ ...next, leases },
|
|
3844
|
+
await persistManual({ ...next, leases }, ctx);
|
|
3748
3845
|
updateStatus(ctx);
|
|
3749
3846
|
return toolPayload({
|
|
3750
3847
|
version: 1,
|
|
@@ -3790,11 +3887,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3790
3887
|
delete leases[id];
|
|
3791
3888
|
next = { ...next, leases };
|
|
3792
3889
|
}
|
|
3793
|
-
await persistManual(
|
|
3794
|
-
next,
|
|
3795
|
-
action,
|
|
3796
|
-
ctx,
|
|
3797
|
-
);
|
|
3890
|
+
await persistManual(next, ctx);
|
|
3798
3891
|
updateStatus(ctx);
|
|
3799
3892
|
return toolPayload({
|
|
3800
3893
|
version: 1,
|
|
@@ -3890,7 +3983,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3890
3983
|
);
|
|
3891
3984
|
}
|
|
3892
3985
|
const text = String(params.brief ?? "").trim();
|
|
3893
|
-
const complaint = briefContractComplaint(text, snapshot.policy.
|
|
3986
|
+
const complaint = briefContractComplaint(text, snapshot.policy.agentBriefReserve, snapshot.toolName);
|
|
3894
3987
|
if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
|
|
3895
3988
|
const nextState = {
|
|
3896
3989
|
...persistence.state!,
|
|
@@ -3900,7 +3993,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3900
3993
|
: mark),
|
|
3901
3994
|
};
|
|
3902
3995
|
persistence.state = nextState;
|
|
3903
|
-
await persistManual(nextState,
|
|
3996
|
+
await persistManual(nextState, ctx);
|
|
3904
3997
|
updateStatus(ctx);
|
|
3905
3998
|
emit("context.brief", { id, chars: text.length, origin: "agent" });
|
|
3906
3999
|
const accounting = markAccounting(snapshot, persistence.state!);
|
|
@@ -3916,7 +4009,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3916
4009
|
durableRevision: persistence.state!.revision,
|
|
3917
4010
|
pendingMarks: accounting.pending,
|
|
3918
4011
|
unbriefed: pendingMarks(persistence.state!)
|
|
3919
|
-
.filter((mark) => mark.briefProvenance?.kind !== "supplied"
|
|
4012
|
+
.filter((mark) => mark.briefProvenance?.kind !== "supplied" &&
|
|
4013
|
+
mark.briefProvenance?.kind !== "augmented").map((mark) => mark.id),
|
|
3920
4014
|
unmarkedSpans: remainder.spans,
|
|
3921
4015
|
unmarkedTokens: remainder.tokens,
|
|
3922
4016
|
activation: "durable immediately, and it costs your window nothing: this fold is " +
|
|
@@ -3926,12 +4020,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3926
4020
|
}
|
|
3927
4021
|
throw new Error(`Unknown ${toolName} action '${action}'`);
|
|
3928
4022
|
};
|
|
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
4023
|
const toolHandler = async (
|
|
3936
4024
|
toolCallId: string,
|
|
3937
4025
|
params: Record<string, unknown>,
|
|
@@ -3948,7 +4036,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3948
4036
|
ok,
|
|
3949
4037
|
error,
|
|
3950
4038
|
tool_call_id: toolCallId,
|
|
3951
|
-
marks_requested: requestedMarkCount(params),
|
|
3952
4039
|
corrections_applied: corrections.length,
|
|
3953
4040
|
arguments_sha256: sha256Value(params ?? {}),
|
|
3954
4041
|
...(action === "peek" ? { ephemeral: peekIsEphemeral(params) } : {}),
|
|
@@ -3986,12 +4073,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3986
4073
|
try {
|
|
3987
4074
|
const snapshot = authoritativeSnapshotFor(ctx);
|
|
3988
4075
|
const status = activeContextStatus(snapshot, persistence.state, 0, 40);
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
4076
|
+
const capacity = currentCapacity(ctx);
|
|
4077
|
+
const staged = pendingMarks(persistence.state);
|
|
4078
|
+
safeNotify(ctx, foldStatusText({
|
|
4079
|
+
brand: brandNoun,
|
|
4080
|
+
usedTokens: measurements.lastProviderMeasurement?.tokens ?? null,
|
|
4081
|
+
budgetTokens: capacity.budgetTokens,
|
|
4082
|
+
commitAtShare: snapshot.thresholds.maxTarget,
|
|
4083
|
+
aimShare: snapshot.thresholds.minTarget,
|
|
4084
|
+
roots: (status.roots as string[]).length,
|
|
4085
|
+
totalFolds: Number(status.totalFolds ?? 0),
|
|
4086
|
+
stagedMarks: staged.length,
|
|
4087
|
+
stagedTokens: staged.reduce((total, mark) =>
|
|
4088
|
+
total + estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)), 0),
|
|
4089
|
+
pinned: persistence.state.protected.length,
|
|
4090
|
+
suspended: ladder.automaticFailure?.message ?? null,
|
|
4091
|
+
foldCommand: `/${commandNames.fold}`,
|
|
4092
|
+
editorCommand: "/fold-editor",
|
|
4093
|
+
}), "info");
|
|
3995
4094
|
} catch (error) {
|
|
3996
4095
|
safeNotify(ctx, `Active-context status unavailable; native Pi context is unchanged: ${String(error)}`, "warning");
|
|
3997
4096
|
}
|
|
@@ -4131,12 +4230,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4131
4230
|
: null;
|
|
4132
4231
|
const accounting = markAccounting(snapshot, state);
|
|
4133
4232
|
return {
|
|
4134
|
-
|
|
4233
|
+
// NO SESSION ID. Eight hex characters of a value a person never types, on the
|
|
4234
|
+
// title line of the surface, is the same defect the rows below it just lost.
|
|
4235
|
+
title: `${brandNoun} window`,
|
|
4135
4236
|
occupancy: {
|
|
4136
4237
|
usedTokens: capacity.usedTokens,
|
|
4137
4238
|
budgetTokens: capacity.budgetTokens,
|
|
4138
4239
|
commitOccupancy: thresholds.maxTarget,
|
|
4139
4240
|
commitDue: occupancy !== null && occupancy >= thresholds.maxTarget,
|
|
4241
|
+
suspended: Boolean(ladder.automaticFailure),
|
|
4140
4242
|
},
|
|
4141
4243
|
blocks: buildFoldEditorData(snapshot, state, {
|
|
4142
4244
|
foldRows: () => (state.folds ?? []).map((fold: any) => {
|
|
@@ -4234,20 +4336,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4234
4336
|
onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
|
|
4235
4337
|
await runQueued(async () => {
|
|
4236
4338
|
const fresh = authoritativeSnapshotFor(ctx);
|
|
4237
|
-
const
|
|
4238
|
-
|
|
4239
|
-
: { ids: [fromId, toId] };
|
|
4240
|
-
const { marks, staged } = await stageFoldMarks(
|
|
4241
|
-
batchedMarkRequests(request), "user",
|
|
4339
|
+
const { mark, staged } = await stageFoldMark(
|
|
4340
|
+
[fromId, toId], brief && brief.trim() ? brief.trim() : undefined,
|
|
4242
4341
|
new AbortController().signal, ctx, fresh);
|
|
4243
|
-
// Refuse BEFORE persisting:
|
|
4342
|
+
// Refuse BEFORE persisting: a refused mark leaves state untouched and
|
|
4244
4343
|
// 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);
|
|
4344
|
+
if (mark.ok !== true) throw new Error(String(mark.reason ?? "the span was refused"));
|
|
4345
|
+
await persistManual(staged, ctx);
|
|
4251
4346
|
updateStatus(ctx);
|
|
4252
4347
|
});
|
|
4253
4348
|
safeNotify(ctx, "Staged user mark(s); they apply at the next commit epoch.", "info");
|
|
@@ -4291,7 +4386,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4291
4386
|
label: toolLabel,
|
|
4292
4387
|
allowedActions: allowedToolActions,
|
|
4293
4388
|
fullSurface: true,
|
|
4294
|
-
maxBriefChars: ACTIVE_CONTEXT_POLICY.
|
|
4389
|
+
maxBriefChars: ACTIVE_CONTEXT_POLICY.agentBriefReserve,
|
|
4295
4390
|
statusDetails: ["fold_candidates", "tree", "folds", "objects"],
|
|
4296
4391
|
minPeekSliceBytes: PEEK_MIN_SLICE_BYTES,
|
|
4297
4392
|
defaultPeekBytes: PEEK_DEFAULT_MAX_BYTES,
|