pi-fold 3.1.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -28
- package/extensions/active-context.ts +645 -412
- package/extensions/index.js +11 -2
- package/extensions/lib/brief-text.ts +116 -0
- package/extensions/lib/canonical.ts +3 -18
- package/extensions/lib/curation.ts +208 -74
- package/extensions/lib/editor-ui.ts +595 -66
- package/extensions/lib/folding.ts +20 -2
- package/extensions/lib/instrumentation.ts +3 -1
- package/extensions/lib/live-settings.ts +73 -0
- package/extensions/lib/measurement.ts +47 -0
- package/extensions/lib/persistence.ts +42 -39
- package/extensions/lib/policy.ts +72 -51
- package/extensions/lib/selection.ts +9 -44
- package/extensions/lib/tool-surface.ts +2 -32
- package/extensions/lib/transcript.ts +43 -0
- package/extensions/settings.ts +200 -32
- package/package.json +5 -5
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
explicitProtectedMass,
|
|
47
47
|
budgetOccupancy,
|
|
48
48
|
hardFenceRatio,
|
|
49
|
+
imageMass,
|
|
49
50
|
latestProviderContextMeasurement,
|
|
50
51
|
orderedRoots,
|
|
51
52
|
parseNativeCompactionCompletion,
|
|
@@ -91,7 +92,9 @@ import {
|
|
|
91
92
|
activeContextSource,
|
|
92
93
|
ACTIVE_CONTEXT_POLICY,
|
|
93
94
|
ACTIVE_CONTEXT_TOOL_ACTIONS,
|
|
94
|
-
|
|
95
|
+
resolveNoticeLeadShare,
|
|
96
|
+
EDITOR_DETAIL_PAGE_CHARS,
|
|
97
|
+
EDITOR_ROW_PREVIEW_CHARS,
|
|
95
98
|
contextBrand,
|
|
96
99
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
97
100
|
DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES,
|
|
@@ -105,6 +108,7 @@ import {
|
|
|
105
108
|
resolveThresholds,
|
|
106
109
|
servingBudgetTokens,
|
|
107
110
|
ESTIMATED_BYTES_PER_TOKEN,
|
|
111
|
+
IMAGE_ESTIMATED_TOKENS,
|
|
108
112
|
entryTypeNamespace,
|
|
109
113
|
MAX_FOLD_SPAN_CHARS,
|
|
110
114
|
DEFAULT_TOOL_FOLD_THRESHOLD,
|
|
@@ -116,10 +120,6 @@ import {
|
|
|
116
120
|
RETIRED_TOOL_ACTIONS,
|
|
117
121
|
AUTO_FOLD_BLACKLIST_DEFAULT,
|
|
118
122
|
USER_RESCUE_MAX_SOURCE_CHARS,
|
|
119
|
-
MEMORY_BODY_CHARS,
|
|
120
|
-
MEMORY_KEY_CHARS,
|
|
121
|
-
MEMORY_KEYS_MAX,
|
|
122
|
-
WORKING_MEMORY_TOOL_ACTIONS,
|
|
123
123
|
} from "./lib/policy.ts";
|
|
124
124
|
import type {
|
|
125
125
|
ActiveContextSnapshot,
|
|
@@ -131,7 +131,6 @@ import type {
|
|
|
131
131
|
FoldKind,
|
|
132
132
|
FoldRecordEntry,
|
|
133
133
|
PreparedFold,
|
|
134
|
-
WorkingMemoryEntry,
|
|
135
134
|
} from "./lib/policy.ts";
|
|
136
135
|
import {
|
|
137
136
|
absorbWedgeMarks,
|
|
@@ -140,7 +139,7 @@ import {
|
|
|
140
139
|
import {
|
|
141
140
|
contextReceipt,
|
|
142
141
|
foldNoticeText,
|
|
143
|
-
|
|
142
|
+
foldStatusText,
|
|
144
143
|
receiptBlockText,
|
|
145
144
|
withReceipt,
|
|
146
145
|
} from "./lib/curation.ts";
|
|
@@ -174,6 +173,7 @@ import {
|
|
|
174
173
|
} from "./lib/scheduling.ts";
|
|
175
174
|
import {
|
|
176
175
|
manualFoldCandidate,
|
|
176
|
+
oneLine,
|
|
177
177
|
peekedSourceFoldIds,
|
|
178
178
|
snapFoldCandidate,
|
|
179
179
|
snapToFoldBoundaries,
|
|
@@ -197,7 +197,9 @@ import type {
|
|
|
197
197
|
} from "./lib/instrumentation.ts";
|
|
198
198
|
import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
|
|
199
199
|
import { buildFoldEditorData, FoldEditorView } from "./lib/editor-ui.ts";
|
|
200
|
+
import { publishLiveSettings } from "./lib/live-settings.ts";
|
|
200
201
|
import {
|
|
202
|
+
entryText,
|
|
201
203
|
mapActiveContext,
|
|
202
204
|
} from "./lib/transcript.ts";
|
|
203
205
|
import {
|
|
@@ -209,12 +211,13 @@ import {
|
|
|
209
211
|
unansweredToolCalls,
|
|
210
212
|
} from "./lib/rollback.ts";
|
|
211
213
|
|
|
212
|
-
export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured";
|
|
214
|
+
export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured" | "refused";
|
|
213
215
|
|
|
214
216
|
export * from "./lib/canonical.ts";
|
|
215
217
|
export * from "./lib/curation.ts";
|
|
216
218
|
export * from "./lib/folding.ts";
|
|
217
219
|
export * from "./lib/instrumentation.ts";
|
|
220
|
+
export * from "./lib/live-settings.ts";
|
|
218
221
|
export * from "./lib/measurement.ts";
|
|
219
222
|
export * from "./lib/persistence.ts";
|
|
220
223
|
export * from "./lib/policy.ts";
|
|
@@ -223,6 +226,47 @@ export * from "./lib/scheduling.ts";
|
|
|
223
226
|
export * from "./lib/selection.ts";
|
|
224
227
|
export * from "./lib/transcript.ts";
|
|
225
228
|
|
|
229
|
+
/**
|
|
230
|
+
* The two independent scalars, resolved in ONE place because they are resolved TWICE:
|
|
231
|
+
* once from the registration options and once from every /fold-settings edit that
|
|
232
|
+
* reaches the running session. Absent means the package default in both directions,
|
|
233
|
+
* which is the settings file's own law, and the refusal text is the same sentence
|
|
234
|
+
* wherever the bad value came from.
|
|
235
|
+
*/
|
|
236
|
+
/**
|
|
237
|
+
* ONE ENTRY AS ONE ROW: whitespace collapsed, bounded, and the cut STATED.
|
|
238
|
+
*
|
|
239
|
+
* Shared by the three places /fold-editor builds an entry row, which each carried their
|
|
240
|
+
* own inline reading of `content` until 2026-08-30 and each got it wrong the same way:
|
|
241
|
+
* `content.find((part) => part.type === "text")` sliced to 300 characters, so a message
|
|
242
|
+
* whose first block was not text rendered as an empty row and everything past the first
|
|
243
|
+
* block was unreachable at any depth. `entryText` accounts for every block and
|
|
244
|
+
* `entryDetail` serves the rest; this is the summary that sits on the row.
|
|
245
|
+
*/
|
|
246
|
+
function rowPreview(message: unknown): string {
|
|
247
|
+
return oneLine(entryText(message), EDITOR_ROW_PREVIEW_CHARS);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolvedPreCommitNotice(value: unknown): boolean {
|
|
251
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
252
|
+
throw new Error("preCommitNotice must be a boolean: true states the window's status to " +
|
|
253
|
+
"the agent once per approach to a commit, false ships every fold with the runtime's " +
|
|
254
|
+
"deterministic brief and puts no carrier in the projection at all");
|
|
255
|
+
}
|
|
256
|
+
return value ?? false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolvedToolFoldThreshold(value: unknown): number {
|
|
260
|
+
const resolved = value ?? DEFAULT_TOOL_FOLD_THRESHOLD;
|
|
261
|
+
if (typeof resolved !== "number" || !Number.isFinite(resolved) ||
|
|
262
|
+
resolved < 0 || resolved >= 1) {
|
|
263
|
+
throw new Error("toolFoldThreshold must be a share in [0, 1): it names the oldest " +
|
|
264
|
+
"fraction of the projected window whose tool results are clipped in view at each " +
|
|
265
|
+
"commit, and 0 turns the diet off");
|
|
266
|
+
}
|
|
267
|
+
return resolved;
|
|
268
|
+
}
|
|
269
|
+
|
|
226
270
|
export function registerActiveContext(pi: any, options: {
|
|
227
271
|
toolName?: string;
|
|
228
272
|
toolLabel?: string;
|
|
@@ -239,21 +283,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
239
283
|
* [0, 1), where 0 turns the diet off; 0.50 is what every fold repetition from 3 onward
|
|
240
284
|
* ran, so the default is the value that was measured rather than the absence of one. */
|
|
241
285
|
toolFoldThreshold?: number;
|
|
242
|
-
/** THE
|
|
243
|
-
* Shane, on the sol-20260826-full2 verdict
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
|
|
286
|
+
/** THE NOTICE SWITCH (public since 2026-08-27 as postFoldNotice; DEFAULT FALSE since
|
|
287
|
+
* 2026-08-28, Shane, on the sol-20260826-full2 verdict; RESHAPED from a brief
|
|
288
|
+
* solicitation into a status statement on 2026-08-30). false is the deterministic
|
|
289
|
+
* shape the campaign measured: no carrier reaches the projection at all and every fold
|
|
290
|
+
* goes out with the runtime's own words, which drew 14, 14, 9 and 8 correct against
|
|
291
|
+
* the soliciting condition's 3. true states the window's status once per approach to a
|
|
292
|
+
* commit: where the commit fires, how far away it is, what is staged, what that frees,
|
|
293
|
+
* what is pinned and what the pin costs, with the verbs listed and no argument made
|
|
294
|
+
* for any of them. Either way the agent verbs stay on the tool. */
|
|
295
|
+
preCommitNotice?: boolean;
|
|
296
|
+
/** HOW FAR BEFORE THE COMMIT THE NOTICE SPEAKS (2026-08-30, Shane), as a share of the
|
|
297
|
+
* serving budget below maxTarget; default DEFAULT_NOTICE_LEAD_SHARE 0.10. A DISTANCE
|
|
298
|
+
* rather than a point, so it tracks a band that moves. Read only when preCommitNotice
|
|
299
|
+
* is true. */
|
|
300
|
+
noticeLeadShare?: number;
|
|
257
301
|
}): {
|
|
258
302
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
259
303
|
} {
|
|
@@ -262,23 +306,27 @@ export function registerActiveContext(pi: any, options: {
|
|
|
262
306
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
263
307
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
264
308
|
const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
309
|
+
// THESE THREE ARE THE SETTINGS SURFACE, AND THEY ARE NOT CONSTANTS (2026-08-29). Each
|
|
310
|
+
// was resolved once here and read from the closure ever after, so a person editing
|
|
311
|
+
// /fold-settings changed the file and nothing else while the session kept the values
|
|
312
|
+
// pi booted with. They are `let` so the live applier published below can replace them,
|
|
313
|
+
// and every reader takes the variable rather than a copy, so a swap reaches the next
|
|
314
|
+
// snapshot, the next commit decision and the status line without anything else moving.
|
|
315
|
+
let preCommitNotice = resolvedPreCommitNotice(options.preCommitNotice);
|
|
316
|
+
let noticeLeadShare = resolveNoticeLeadShare(options.noticeLeadShare);
|
|
317
|
+
let toolFoldThreshold = resolvedToolFoldThreshold(options.toolFoldThreshold);
|
|
318
|
+
// THE RENAMED OPTION IS REFUSED BY NAME, not ignored (2026-08-30). postFoldNotice was
|
|
319
|
+
// public for three days and named a carrier that no longer exists in that shape: it
|
|
320
|
+
// fired on a fold count, after the cut, and asked for briefs. Silently accepting the
|
|
321
|
+
// old name would run the new mechanism under a description of the old one, and
|
|
322
|
+
// silently DROPPING it would turn a deployment that deliberately chose the invitation
|
|
323
|
+
// into a silent one without saying so.
|
|
324
|
+
if (Object.hasOwn(options, "postFoldNotice")) {
|
|
325
|
+
throw new Error("postFoldNotice is now preCommitNotice, and the carrier it switches has " +
|
|
326
|
+
"changed: it fires on occupancy approaching the commit rather than on a count of " +
|
|
327
|
+
"unbriefed folds, and it STATES the window's status rather than asking for briefs. " +
|
|
328
|
+
"The default is still false. noticeLeadShare sets how far ahead of the commit it " +
|
|
329
|
+
"speaks");
|
|
282
330
|
}
|
|
283
331
|
for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
|
|
284
332
|
if (Object.hasOwn(options, removed)) {
|
|
@@ -338,14 +386,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
338
386
|
commandNames.status === commandNames.fold) {
|
|
339
387
|
throw new Error("Active-context command names must be distinct kebab-case strings");
|
|
340
388
|
}
|
|
341
|
-
|
|
389
|
+
let thresholds = resolveThresholds(options.thresholds);
|
|
342
390
|
assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
343
391
|
const stateEntryType = `${entryTypePrefix}-state`;
|
|
344
392
|
const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
|
|
345
393
|
const milestoneProjectionType = `${entryTypePrefix}-milestone`;
|
|
346
394
|
const advisoryProjectionType = `${entryTypePrefix}-advisory`;
|
|
347
395
|
const foldNoticeProjectionType = `${entryTypePrefix}-fold-notice`;
|
|
348
|
-
const memoryTocProjectionType = `${entryTypePrefix}-memory-toc`;
|
|
349
396
|
const receiptProjectionType = `${entryTypePrefix}-receipts`;
|
|
350
397
|
const curationProjectionType = `${entryTypePrefix}-curation`;
|
|
351
398
|
const entryNamespace = entryTypeNamespace(entryTypePrefix);
|
|
@@ -353,9 +400,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
353
400
|
const nativeReceiptEntryType = `${entryNamespace}-native-compaction-receipt`;
|
|
354
401
|
const contextEventEntryType = `${entryNamespace}-context-event`;
|
|
355
402
|
const nativeDecisionEntryType = `${entryNamespace}-native-compaction-decision`;
|
|
356
|
-
const allowedToolActions: readonly ActiveContextToolAction[] =
|
|
357
|
-
? [...ACTIVE_CONTEXT_TOOL_ACTIONS, ...WORKING_MEMORY_TOOL_ACTIONS]
|
|
358
|
-
: ACTIVE_CONTEXT_TOOL_ACTIONS;
|
|
403
|
+
const allowedToolActions: readonly ActiveContextToolAction[] = ACTIVE_CONTEXT_TOOL_ACTIONS;
|
|
359
404
|
const allowedToolActionSet = new Set<string>(allowedToolActions);
|
|
360
405
|
|
|
361
406
|
type AutomaticFailureState = {
|
|
@@ -382,6 +427,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
382
427
|
bandTopMeasurement: null as unknown,
|
|
383
428
|
/** The snapshot-and-state key whose frontier scan came back empty; see advanceFoldFrontier. */
|
|
384
429
|
frontierEmptyKey: null as string | null,
|
|
430
|
+
/** The commit count the pre-commit notice last spoke at; null while it is re-armed.
|
|
431
|
+
* One notice per approach, and a landed commit begins the next approach. */
|
|
432
|
+
noticeLatch: null as number | null,
|
|
385
433
|
pendingManual: false,
|
|
386
434
|
preparing: null as { id: string; controller: AbortController; promise: Promise<void> } | null,
|
|
387
435
|
lastPreparationError: null as string | null,
|
|
@@ -401,6 +449,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
401
449
|
lastProviderMeasurement: null as ProviderContextMeasurement | null,
|
|
402
450
|
descriptorWindow: null as number | null,
|
|
403
451
|
lastProjectedChars: null as number | null,
|
|
452
|
+
lastProjectedImages: 0,
|
|
404
453
|
projectionCalibrations: [] as Array<{ chars: number; tokens: number }>,
|
|
405
454
|
lastProjectedEstimate: null as number | null,
|
|
406
455
|
lastProjectedEstimateCalibrated: false,
|
|
@@ -409,11 +458,22 @@ export function registerActiveContext(pi: any, options: {
|
|
|
409
458
|
projectionAnchor: null as {
|
|
410
459
|
tokens: number;
|
|
411
460
|
chars: number;
|
|
461
|
+
/** `chars` less the base64 of any image, which is what the delta is taken on. */
|
|
462
|
+
textChars: number;
|
|
463
|
+
/** Images inside the anchored prefix, each worth IMAGE_ESTIMATED_TOKENS. */
|
|
464
|
+
images: number;
|
|
412
465
|
head: string;
|
|
413
466
|
messageSha256: string;
|
|
414
467
|
sessionId: string;
|
|
415
468
|
generation: number;
|
|
416
469
|
} | null,
|
|
470
|
+
// A REFUSED ANCHOR IS NOT AN ABSENT ONE (2026-08-30). The plausibility floor below
|
|
471
|
+
// rejects a provider count the projected TEXT cannot explain, which is the earliest
|
|
472
|
+
// and cheapest evidence that what we project is not what reaches the wire. It set
|
|
473
|
+
// `projectionAnchor = null` and said nothing, and the reading then called itself
|
|
474
|
+
// "unmeasured", the same word a session uses before its first provider response.
|
|
475
|
+
// Those are opposite states: one has no number yet, the other has one it disbelieves.
|
|
476
|
+
anchorRefusal: null as { consecutive: number } | null,
|
|
417
477
|
estimatorErrors: [] as number[],
|
|
418
478
|
inflowSteps: [] as number[],
|
|
419
479
|
wallInflowSteps: [] as number[],
|
|
@@ -428,6 +488,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
428
488
|
ledger: emptyLedger(),
|
|
429
489
|
previousDigests: null as string[] | null,
|
|
430
490
|
previousText: null as string | null,
|
|
491
|
+
// The image mass of `previousText`, kept beside it because the provider anchor is
|
|
492
|
+
// built from that text and has no access to the array it came from.
|
|
493
|
+
previousImages: 0,
|
|
494
|
+
previousImageChars: 0,
|
|
431
495
|
lastChange: "append" as ProjectionChange,
|
|
432
496
|
lastPreservedShare: null as number | null,
|
|
433
497
|
sinceHandoff: [] as Array<{
|
|
@@ -473,6 +537,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
473
537
|
const curation = {
|
|
474
538
|
receipts: [] as ContextReceipt[],
|
|
475
539
|
contextCalls: 0,
|
|
540
|
+
/** Receipts delivered this session, monotonic; the pre-commit notice's re-arm signal. */
|
|
541
|
+
commitsDelivered: 0,
|
|
476
542
|
wallEpisodeOpen: false,
|
|
477
543
|
recoveryAttempts: 0,
|
|
478
544
|
pendingRejection: null as { status: number; ordinal: number } | null,
|
|
@@ -490,12 +556,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
490
556
|
pendingOverflow: null as { at: number; entryId: string | null } | null,
|
|
491
557
|
};
|
|
492
558
|
|
|
493
|
-
const advisory = {
|
|
494
|
-
hardFenceNoticeKey: null as string | null,
|
|
495
|
-
hardFenceReleaseSessionId: null as string | null,
|
|
496
|
-
hardFenceReleasedProjectionKeys: new Set<string>(),
|
|
497
|
-
};
|
|
498
|
-
|
|
499
559
|
// EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
|
|
500
560
|
// visibility. In-memory ON PURPOSE: a restart forgets the registry and the
|
|
501
561
|
// result stays durable, which is the safe default, and the durable entry
|
|
@@ -606,6 +666,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
606
666
|
|
|
607
667
|
const deliverReceipt = (receipt: ContextReceipt): void => {
|
|
608
668
|
curation.receipts = withReceipt(curation.receipts, receipt);
|
|
669
|
+
// MONOTONIC, unlike `curation.receipts.length`, which saturates at MAX_CONTEXT_RECEIPTS
|
|
670
|
+
// and would stop distinguishing one approach from the next once the session is long
|
|
671
|
+
// enough to matter. This is the only counter the notice latch reads.
|
|
672
|
+
curation.commitsDelivered += 1;
|
|
609
673
|
emit("context.receipt", {
|
|
610
674
|
receipt_kind: receipt.kind,
|
|
611
675
|
trigger: receipt.trigger,
|
|
@@ -632,18 +696,125 @@ export function registerActiveContext(pi: any, options: {
|
|
|
632
696
|
lifecycle.generation === expectedGeneration && persistence.state?.sessionId === sessionId &&
|
|
633
697
|
(!ctx || contextSessionMatches(ctx, sessionId));
|
|
634
698
|
|
|
699
|
+
/**
|
|
700
|
+
* THE ONE LINE THAT IS ALWAYS ON SCREEN, so it answers the question a person actually
|
|
701
|
+
* has: how full am I, and is anything about to happen.
|
|
702
|
+
*
|
|
703
|
+
* It used to read `pi_fold_context folds: 3 · provider 187234/272000`. That led with the
|
|
704
|
+
* AGENT TOOL'S NAME, which a person cannot call and does not care about, and then spent
|
|
705
|
+
* its remaining width on two raw counts they would have to divide themselves. The brand
|
|
706
|
+
* leads now, occupancy is a percentage of the serving budget, and the commit point is
|
|
707
|
+
* named only when it is close enough to matter.
|
|
708
|
+
*/
|
|
635
709
|
const updateStatus = (ctx: any): void => {
|
|
636
710
|
try {
|
|
637
711
|
const roots = persistence.state && lifecycle.latestSnapshot ? orderedRoots(persistence.state, lifecycle.latestSnapshot).length : 0;
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
712
|
+
const staged = persistence.state ? pendingMarks(persistence.state).length : 0;
|
|
713
|
+
const capacity = currentCapacity(ctx);
|
|
714
|
+
const used = measurements.lastProviderMeasurement?.tokens ?? null;
|
|
715
|
+
const share = used !== null && capacity.budgetTokens > 0 ? used / capacity.budgetTokens : null;
|
|
716
|
+
const parts: string[] = [];
|
|
717
|
+
// FAILURE LEADS. This line is cut to whatever width the host gives it, so the last
|
|
718
|
+
// segment is the first one a narrow terminal removes, and folding having stopped is
|
|
719
|
+
// the one state on this line a person has to act on. It was appended last.
|
|
720
|
+
if (ladder.automaticFailure) parts.push("FOLDING STOPPED");
|
|
721
|
+
// Unmeasured is stated, never guessed: an estimate rendered as a fact is the habit
|
|
722
|
+
// that cost a live session, and this line is the most-read surface there is.
|
|
723
|
+
parts.push(share === null ? "not measured" : `${Math.round(share * 100)}% full`);
|
|
724
|
+
// THE LIVE BAND, NOT THE LAST SNAPSHOT'S (2026-08-29). This read the threshold off
|
|
725
|
+
// `lifecycle.latestSnapshot`, which is derived per context event, so a band changed
|
|
726
|
+
// in /fold-settings went on being announced at its old value until the next event
|
|
727
|
+
// rebuilt a snapshot. The number this line owes a person is the one that will fire,
|
|
728
|
+
// and that is the runtime's own variable.
|
|
729
|
+
const trigger = thresholds.maxTarget;
|
|
730
|
+
// WHEN, not just how full. "68% full" only answers the second question a reader has
|
|
731
|
+
// if they already remember where the trigger sits; naming it answers both.
|
|
732
|
+
//
|
|
733
|
+
// NOT WHILE STOPPED, though: the line rendered "COMMIT DUE" and "FOLDING STOPPED"
|
|
734
|
+
// in one breath, promising the thing it was announcing would not happen.
|
|
735
|
+
// DUE MEANS ABOUT TO HAPPEN, NOT MERELY OVER THE LINE (2026-08-30). This read
|
|
736
|
+
// `share >= trigger`, which is true of the BAND and says nothing about the
|
|
737
|
+
// DECISION. A band-top commit runs ONCE per provider count, so once it has run
|
|
738
|
+
// against the current one, nothing further will happen until a new count arrives,
|
|
739
|
+
// whatever it applied. Live session 01a052b1 sat on `COMMIT DUE` through 48
|
|
740
|
+
// consecutive evaluations that all declined for a recorded reason, and a person
|
|
741
|
+
// reasonably read a line promising an imminent commit as a runtime that had hung.
|
|
742
|
+
//
|
|
743
|
+
// The latch is the whole test and no new state is kept: it already holds the
|
|
744
|
+
// measurement the last band-top commit ran against, for the cache reason documented
|
|
745
|
+
// at its own site, and "a commit has already been weighed against this count" is
|
|
746
|
+
// exactly what a reader needs. Held is lower case because it is not urgent; DUE
|
|
747
|
+
// keeps its shout because it is the one that is about to cost a rewrite.
|
|
748
|
+
if (share !== null && !ladder.automaticFailure) {
|
|
749
|
+
const weighed = ladder.bandTopMeasurement !== null &&
|
|
750
|
+
ladder.bandTopMeasurement === measurements.lastProviderMeasurement;
|
|
751
|
+
parts.push(share < trigger
|
|
752
|
+
? `commit at ${Math.round(trigger * 100)}%`
|
|
753
|
+
: weighed ? "commit held" : "COMMIT DUE");
|
|
754
|
+
}
|
|
755
|
+
if (staged > 0) parts.push(`${staged} staged`);
|
|
756
|
+
if (roots > 0) parts.push(`${roots} fold${roots === 1 ? "" : "s"}`);
|
|
757
|
+
ctx.ui?.setStatus?.(entryTypePrefix, `${brandNoun} ${parts.join(" · ")}`);
|
|
644
758
|
} catch { }
|
|
645
759
|
};
|
|
646
760
|
|
|
761
|
+
/**
|
|
762
|
+
* WHAT A PERSON SAVES IN /fold-settings REACHES THIS SESSION (2026-08-29).
|
|
763
|
+
*
|
|
764
|
+
* The screen writes the file and then hands the whole file here, so an absent field
|
|
765
|
+
* means the package default exactly as it does on disk and at registration; all four
|
|
766
|
+
* values are resolved through the same functions the options went through, which is
|
|
767
|
+
* what stops a value meaning one thing at boot and another at an edit.
|
|
768
|
+
*
|
|
769
|
+
* ATOMIC: every value is validated before any is assigned, so a refusal leaves the
|
|
770
|
+
* session on the settings it already had rather than half-moved. It throws the way
|
|
771
|
+
* registration throws, and the caller decides what that means; the settings screen
|
|
772
|
+
* catches it, because a screen may not take the session down with it.
|
|
773
|
+
*
|
|
774
|
+
* The memo is dropped because `authoritativeSnapshotFor` keys on session, entries and
|
|
775
|
+
* messages, none of which move when a threshold does, so a cached snapshot would serve
|
|
776
|
+
* the old band to the next reader that asked.
|
|
777
|
+
*/
|
|
778
|
+
publishLiveSettings(pi, (settings, ctx) => {
|
|
779
|
+
const nextThresholds = resolveThresholds(settings?.thresholds);
|
|
780
|
+
assertThresholdsServable(nextThresholds,
|
|
781
|
+
providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
782
|
+
const nextToolFoldThreshold = resolvedToolFoldThreshold(settings?.toolFoldThreshold);
|
|
783
|
+
const nextPreCommitNotice = resolvedPreCommitNotice(settings?.preCommitNotice);
|
|
784
|
+
const nextNoticeLeadShare = resolveNoticeLeadShare(settings?.noticeLeadShare);
|
|
785
|
+
const changed: string[] = [];
|
|
786
|
+
for (const field of ["maxTarget", "minTarget", "consolidateAfter", "minFoldChars"] as const) {
|
|
787
|
+
if (nextThresholds[field] !== thresholds[field]) changed.push(field);
|
|
788
|
+
}
|
|
789
|
+
if (nextToolFoldThreshold !== toolFoldThreshold) changed.push("toolFoldThreshold");
|
|
790
|
+
if (nextPreCommitNotice !== preCommitNotice) changed.push("preCommitNotice");
|
|
791
|
+
if (nextNoticeLeadShare !== noticeLeadShare) changed.push("noticeLeadShare");
|
|
792
|
+
// A SAVE THAT MOVED NOTHING IS NOT AN EVENT. The screen saves every keystroke that
|
|
793
|
+
// lands, including a row stepped back to where it started.
|
|
794
|
+
if (!changed.length) return;
|
|
795
|
+
thresholds = nextThresholds;
|
|
796
|
+
toolFoldThreshold = nextToolFoldThreshold;
|
|
797
|
+
preCommitNotice = nextPreCommitNotice;
|
|
798
|
+
noticeLeadShare = nextNoticeLeadShare;
|
|
799
|
+
dropDerivationMemo();
|
|
800
|
+
// THE STREAM CARRIES IT, because from here on the session's commits fire at a point
|
|
801
|
+
// no earlier record explains, and an archive reading this run later has no other way
|
|
802
|
+
// to know the band moved under it.
|
|
803
|
+
emit("context.settings", {
|
|
804
|
+
changed,
|
|
805
|
+
max_target: thresholds.maxTarget,
|
|
806
|
+
min_target: thresholds.minTarget,
|
|
807
|
+
consolidate_after: thresholds.consolidateAfter,
|
|
808
|
+
min_fold_chars: thresholds.minFoldChars,
|
|
809
|
+
tool_fold_threshold: toolFoldThreshold,
|
|
810
|
+
pre_commit_notice: preCommitNotice,
|
|
811
|
+
notice_lead_share: noticeLeadShare,
|
|
812
|
+
});
|
|
813
|
+
// The person is standing in the settings screen, so the line they will look at next
|
|
814
|
+
// is the status line. Without this it keeps the old number until a context event.
|
|
815
|
+
if (ctx) { try { updateStatus(ctx); } catch { } }
|
|
816
|
+
});
|
|
817
|
+
|
|
647
818
|
const budgetWindowFor = (ctx: any): number | null => {
|
|
648
819
|
measurements.descriptorWindow = contextWindowFor(ctx);
|
|
649
820
|
return providerInputBudget ?? measurements.descriptorWindow;
|
|
@@ -767,6 +938,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
767
938
|
measurements.lastProviderMeasurement = null;
|
|
768
939
|
measurements.wallInflowSteps.length = 0;
|
|
769
940
|
measurements.projectionAnchor = null;
|
|
941
|
+
// A RESET IS NOT A RESTORATION. The refusal is dropped without a record because the
|
|
942
|
+
// session it described is gone; emitting "restored" here would report a recovery that
|
|
943
|
+
// never happened.
|
|
944
|
+
measurements.anchorRefusal = null;
|
|
770
945
|
measurements.lastProjectedEstimateBasis = "unmeasured";
|
|
771
946
|
ladder.pendingManual = false;
|
|
772
947
|
if (!preserveThresholdDecision) nativeCompaction.lastThresholdDecision = null;
|
|
@@ -779,6 +954,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
779
954
|
instrumentation.ledger = emptyLedger();
|
|
780
955
|
instrumentation.previousDigests = null;
|
|
781
956
|
instrumentation.previousText = null;
|
|
957
|
+
instrumentation.previousImages = 0;
|
|
958
|
+
instrumentation.previousImageChars = 0;
|
|
782
959
|
instrumentation.requests = 0;
|
|
783
960
|
instrumentation.lastMutationRequest = 0;
|
|
784
961
|
instrumentation.lastMutationTokens = null;
|
|
@@ -793,16 +970,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
793
970
|
curation.lastRecovery = null;
|
|
794
971
|
ladder.lastAutomaticAction = null;
|
|
795
972
|
ladder.automaticFailure = null;
|
|
796
|
-
advisory.hardFenceNoticeKey = null;
|
|
797
973
|
measurements.providerMeasurementReceipts.clear();
|
|
798
974
|
measurements.providerMeasurementRevisionByMessageSha.clear();
|
|
799
975
|
measurements.providerMeasurementByMessageSha.clear();
|
|
800
976
|
measurements.providerMeasurementAnchorByMessageSha.clear();
|
|
801
977
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
802
|
-
if (advisory.hardFenceReleaseSessionId !== sessionId) {
|
|
803
|
-
advisory.hardFenceReleaseSessionId = sessionId;
|
|
804
|
-
advisory.hardFenceReleasedProjectionKeys.clear();
|
|
805
|
-
}
|
|
806
978
|
let restored: ActiveContextState | null = null;
|
|
807
979
|
let restoreError: unknown = null;
|
|
808
980
|
let restoredPersistence: MaterializedStatePersistence | null = null;
|
|
@@ -1318,54 +1490,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1318
1490
|
safeNotify(ctx, `Automatic context management suspended: ${message}`, "warning");
|
|
1319
1491
|
};
|
|
1320
1492
|
|
|
1321
|
-
const abortUnsafeHardContext = (
|
|
1322
|
-
snapshot: ActiveContextSnapshot | null,
|
|
1323
|
-
ctx: any,
|
|
1324
|
-
allowUnmeasuredRevisionRelease = false,
|
|
1325
|
-
): boolean => {
|
|
1326
|
-
if (measurements.latestRatio === null || measurements.latestRatio < hardFenceRatio(snapshot ?? undefined, ctx) || !persistence.state) return false;
|
|
1327
|
-
const measuredRevision = measurements.lastProviderMeasurement
|
|
1328
|
-
? measurements.providerMeasurementRevisionByMessageSha.get(measurements.lastProviderMeasurement.messageSha256)
|
|
1329
|
-
: undefined;
|
|
1330
|
-
if (allowUnmeasuredRevisionRelease && !ladder.automaticFailure &&
|
|
1331
|
-
measuredRevision !== undefined && measurements.lastProviderMeasurement &&
|
|
1332
|
-
!durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
|
|
1333
|
-
const releaseKey = sha256Value({
|
|
1334
|
-
sessionId: persistence.state.sessionId,
|
|
1335
|
-
topologySha256: topologySha256(persistence.state),
|
|
1336
|
-
protectionSha256: protectionSha256(persistence.state),
|
|
1337
|
-
measuredRevision,
|
|
1338
|
-
providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
|
|
1339
|
-
});
|
|
1340
|
-
if (!advisory.hardFenceReleasedProjectionKeys.has(releaseKey) &&
|
|
1341
|
-
advisory.hardFenceReleasedProjectionKeys.size < 4_096) {
|
|
1342
|
-
advisory.hardFenceReleasedProjectionKeys.add(releaseKey);
|
|
1343
|
-
return false;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
const key = ladder.automaticFailure?.key ?? sha256Value({
|
|
1347
|
-
sessionId: snapshot?.sessionId ?? persistence.state.sessionId,
|
|
1348
|
-
revision: persistence.state.revision,
|
|
1349
|
-
providerMessageSha256: measurements.lastProviderMeasurement?.messageSha256 ?? null,
|
|
1350
|
-
phase: "hard-provider-fence",
|
|
1351
|
-
});
|
|
1352
|
-
ladder.pendingContextNote = `Provider context reached the hard ${brandNoun} fence without a newly committed lossless fold. ` +
|
|
1353
|
-
"The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
|
|
1354
|
-
if (advisory.hardFenceNoticeKey !== key) {
|
|
1355
|
-
advisory.hardFenceNoticeKey = key;
|
|
1356
|
-
safeNotify(
|
|
1357
|
-
ctx,
|
|
1358
|
-
"Provider request aborted at the hard context fence; run /compact or make an explicit bounded context fold.",
|
|
1359
|
-
"error",
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
if (typeof ctx.abort !== "function") {
|
|
1363
|
-
throw new Error(`Pi hard-fence abort capability is unavailable at ratio ${measurements.latestRatio}`);
|
|
1364
|
-
}
|
|
1365
|
-
ctx.abort();
|
|
1366
|
-
return true;
|
|
1367
|
-
};
|
|
1368
|
-
|
|
1369
1493
|
const PROJECTION_CALIBRATION_MIN_CHARS = 20_000;
|
|
1370
1494
|
const PROJECTION_CALIBRATION_MIN_TOKENS = 5_000;
|
|
1371
1495
|
const PROJECTION_CHARS_PER_TOKEN_FLOOR = 2;
|
|
@@ -1385,6 +1509,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1385
1509
|
}
|
|
1386
1510
|
const chars = measurements.lastProjectedChars;
|
|
1387
1511
|
if (chars === null || chars < PROJECTION_CALIBRATION_MIN_CHARS) return;
|
|
1512
|
+
// NEVER LEARN THE RATIO FROM A WINDOW HOLDING AN IMAGE. chars/token is a fact about
|
|
1513
|
+
// prose; a projection carrying base64 has a far higher true ratio, and because
|
|
1514
|
+
// projectionCharsPerToken takes the MINIMUM over the window such a sample is either
|
|
1515
|
+
// discarded (leaving the over-read in place) or, once images dominate, poisons the
|
|
1516
|
+
// ratio for the text around them. The clean rule is to calibrate on text alone.
|
|
1517
|
+
if (measurements.lastProjectedImages > 0) return;
|
|
1388
1518
|
if (!Number.isFinite(measurement.tokens) || measurement.tokens < PROJECTION_CALIBRATION_MIN_TOKENS) return;
|
|
1389
1519
|
if (measurements.lastProjectedEstimate !== null && measurements.lastProjectedEstimateCalibrated &&
|
|
1390
1520
|
measurement.tokens > 0) {
|
|
@@ -1410,6 +1540,41 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1410
1540
|
);
|
|
1411
1541
|
};
|
|
1412
1542
|
|
|
1543
|
+
/**
|
|
1544
|
+
* THE REFUSAL SPEAKS ONCE PER EPISODE, NOT ONCE PER PASS (2026-08-30).
|
|
1545
|
+
*
|
|
1546
|
+
* Every request in a divergent stretch refuses the anchor for the same reason, and a
|
|
1547
|
+
* record per pass would be 48 identical lines in the one session that found this. The
|
|
1548
|
+
* edge is what carries information: entering the state, and leaving it with a count of
|
|
1549
|
+
* what it cost. Same shape as `context.suspend`, which repeats only when its message
|
|
1550
|
+
* changes rather than once per pass.
|
|
1551
|
+
*/
|
|
1552
|
+
const noteAnchorRefusal = (
|
|
1553
|
+
measurement: ProviderContextMeasurement,
|
|
1554
|
+
textChars: number,
|
|
1555
|
+
images: number,
|
|
1556
|
+
): void => {
|
|
1557
|
+
const previous = measurements.anchorRefusal;
|
|
1558
|
+
measurements.anchorRefusal = { consecutive: (previous?.consecutive ?? 0) + 1 };
|
|
1559
|
+
if (previous) return;
|
|
1560
|
+
emit("context.anchor", {
|
|
1561
|
+
state: "refused",
|
|
1562
|
+
reason: "text-cannot-explain-count",
|
|
1563
|
+
text_chars: textChars,
|
|
1564
|
+
images,
|
|
1565
|
+
provider_tokens: measurement.tokens,
|
|
1566
|
+
chars_per_token: measurement.tokens > 0 ? textChars / measurement.tokens : null,
|
|
1567
|
+
floor: PROJECTION_CHARS_PER_TOKEN_FLOOR,
|
|
1568
|
+
});
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
const clearAnchorRefusal = (): void => {
|
|
1572
|
+
const refusal = measurements.anchorRefusal;
|
|
1573
|
+
measurements.anchorRefusal = null;
|
|
1574
|
+
if (!refusal) return;
|
|
1575
|
+
emit("context.anchor", { state: "restored", refused_requests: refusal.consecutive });
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1413
1578
|
const noteProviderProjectionAnchor = (measurement: ProviderContextMeasurement): void => {
|
|
1414
1579
|
if (measurements.projectionAnchor?.messageSha256 === measurement.messageSha256) return;
|
|
1415
1580
|
const text = instrumentation.previousText;
|
|
@@ -1418,13 +1583,32 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1418
1583
|
return;
|
|
1419
1584
|
}
|
|
1420
1585
|
const chars = bytes(text);
|
|
1421
|
-
|
|
1586
|
+
// The anchor is differenced against TEXT characters, so it must store them. The mass
|
|
1587
|
+
// belongs to the same `previousText` this anchor is built from, recorded by
|
|
1588
|
+
// noteProjection when that text was produced.
|
|
1589
|
+
const textChars = Math.max(0, chars - instrumentation.previousImageChars);
|
|
1590
|
+
const images = instrumentation.previousImages;
|
|
1591
|
+
// The floor test reads TEXT against tokens: an image-heavy projection has a raw ratio
|
|
1592
|
+
// far above any prose floor and would sail through a test that is meant to reject a
|
|
1593
|
+
// measurement the text cannot explain.
|
|
1594
|
+
if (!(measurement.tokens > 0) ||
|
|
1595
|
+
(measurement.tokens > images * IMAGE_ESTIMATED_TOKENS &&
|
|
1596
|
+
textChars / (measurement.tokens - images * IMAGE_ESTIMATED_TOKENS) <
|
|
1597
|
+
PROJECTION_CHARS_PER_TOKEN_FLOOR)) {
|
|
1422
1598
|
measurements.projectionAnchor = null;
|
|
1599
|
+
// ONLY A COUNT WE DISBELIEVE IS A REFUSAL. A zero or absent count is simply nothing
|
|
1600
|
+
// to anchor on and stays silent; the floor failing on a positive count is the state
|
|
1601
|
+
// worth announcing, because it means the provider is billing for mass our own
|
|
1602
|
+
// projection does not contain.
|
|
1603
|
+
if (measurement.tokens > 0) noteAnchorRefusal(measurement, textChars, images);
|
|
1423
1604
|
return;
|
|
1424
1605
|
}
|
|
1606
|
+
clearAnchorRefusal();
|
|
1425
1607
|
measurements.projectionAnchor = {
|
|
1426
1608
|
tokens: measurement.tokens,
|
|
1427
1609
|
chars,
|
|
1610
|
+
textChars,
|
|
1611
|
+
images,
|
|
1428
1612
|
head: text.slice(0, -1),
|
|
1429
1613
|
messageSha256: measurement.messageSha256,
|
|
1430
1614
|
sessionId: persistence.state.sessionId,
|
|
@@ -1446,6 +1630,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1446
1630
|
tokens: number;
|
|
1447
1631
|
basis: ProjectionReadingBasis;
|
|
1448
1632
|
chars: number;
|
|
1633
|
+
/** The image-corrected size of THIS projection, independent of any anchor. */
|
|
1634
|
+
sizeTokens: number;
|
|
1635
|
+
images: number;
|
|
1449
1636
|
anchorTokens: number | null;
|
|
1450
1637
|
deltaChars: number | null;
|
|
1451
1638
|
text: string;
|
|
@@ -1453,21 +1640,43 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1453
1640
|
const text = serialized ?? stableStringify(projected);
|
|
1454
1641
|
const chars = Buffer.byteLength(text, "utf8");
|
|
1455
1642
|
const charsPerToken = projectionCharsPerToken();
|
|
1456
|
-
|
|
1643
|
+
// AN IMAGE IS NOT TEXT (2026-08-28, see imageMass). Its base64 leaves the character
|
|
1644
|
+
// count and comes back as a flat per-image cost, because dividing base64 by a ratio
|
|
1645
|
+
// learned from prose read one screenshot as 110,000 tokens against a true 1,500 and
|
|
1646
|
+
// aborted a session that was less than half full.
|
|
1647
|
+
const mass = imageMass(projected);
|
|
1648
|
+
const textChars = Math.max(0, chars - mass.base64Chars);
|
|
1649
|
+
const imageTokens = mass.images * IMAGE_ESTIMATED_TOKENS;
|
|
1650
|
+
const estimate = Math.ceil(textChars / charsPerToken) + imageTokens;
|
|
1457
1651
|
const anchor = measurements.projectionAnchor;
|
|
1458
|
-
|
|
1652
|
+
// REFUSED AND UNMEASURED READ THE SAME NUMBER AND MEAN OPPOSITE THINGS. Both fall back
|
|
1653
|
+
// to the character estimate, so the tokens are identical and only the BASIS separates
|
|
1654
|
+
// them: "unmeasured" is a session that has not been told yet, "refused" is one that was
|
|
1655
|
+
// told something its own projection cannot account for. Reading 48 consecutive
|
|
1656
|
+
// "unmeasured" projections as a quiet session is what hid a 19x divergence.
|
|
1657
|
+
if (!anchor) {
|
|
1658
|
+
const basis: ProjectionReadingBasis = measurements.anchorRefusal ? "refused" : "unmeasured";
|
|
1659
|
+
return { tokens: estimate, basis, chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
|
|
1660
|
+
}
|
|
1661
|
+
if (anchor.generation !== lifecycle.generation ||
|
|
1459
1662
|
anchor.sessionId !== persistence.state?.sessionId) {
|
|
1460
|
-
return { tokens: estimate, basis: "unmeasured", chars, anchorTokens: null, deltaChars: null, text };
|
|
1663
|
+
return { tokens: estimate, basis: "unmeasured", chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
|
|
1461
1664
|
}
|
|
1462
1665
|
const separator = text.length > anchor.head.length ? text[anchor.head.length] : "";
|
|
1463
1666
|
if (!text.startsWith(anchor.head) || (separator !== "," && separator !== "]")) {
|
|
1464
|
-
return { tokens: estimate, basis: "rewritten", chars, anchorTokens: anchor.tokens, deltaChars: null, text };
|
|
1667
|
+
return { tokens: estimate, basis: "rewritten", chars, sizeTokens: estimate, images: mass.images, anchorTokens: anchor.tokens, deltaChars: null, text };
|
|
1465
1668
|
}
|
|
1466
|
-
|
|
1669
|
+
// The delta is taken on TEXT characters and images separately, so an image arriving
|
|
1670
|
+
// after the anchor costs its own flat price rather than its base64 length.
|
|
1671
|
+
const deltaChars = textChars - anchor.textChars;
|
|
1672
|
+
const deltaImages = mass.images - anchor.images;
|
|
1467
1673
|
return {
|
|
1468
|
-
tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken)
|
|
1674
|
+
tokens: Math.max(0, anchor.tokens + Math.ceil(deltaChars / charsPerToken) +
|
|
1675
|
+
deltaImages * IMAGE_ESTIMATED_TOKENS),
|
|
1469
1676
|
basis: "anchored",
|
|
1470
1677
|
chars,
|
|
1678
|
+
sizeTokens: estimate,
|
|
1679
|
+
images: mass.images,
|
|
1471
1680
|
anchorTokens: anchor.tokens,
|
|
1472
1681
|
deltaChars,
|
|
1473
1682
|
text,
|
|
@@ -1517,7 +1726,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1517
1726
|
: Number.POSITIVE_INFINITY;
|
|
1518
1727
|
const reading = projectedTokenReading(projected, serialized);
|
|
1519
1728
|
const tokens = reading.tokens;
|
|
1520
|
-
const sizeTokens =
|
|
1729
|
+
const sizeTokens = reading.sizeTokens;
|
|
1521
1730
|
const marginTokens = Number.isFinite(budgetTokens)
|
|
1522
1731
|
? projectionMarginTokens(tokens, capacity.window)
|
|
1523
1732
|
: 0;
|
|
@@ -1533,41 +1742,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1533
1742
|
};
|
|
1534
1743
|
};
|
|
1535
1744
|
|
|
1536
|
-
const abortOverBudgetProjection = (
|
|
1537
|
-
tokens: number,
|
|
1538
|
-
budgetTokens: number,
|
|
1539
|
-
ctx: any,
|
|
1540
|
-
recoveryAttempts = 0,
|
|
1541
|
-
): void => {
|
|
1542
|
-
const key = sha256Value({
|
|
1543
|
-
sessionId: persistence.state?.sessionId ?? null,
|
|
1544
|
-
revision: persistence.state?.revision ?? null,
|
|
1545
|
-
tokens,
|
|
1546
|
-
budgetTokens,
|
|
1547
|
-
phase: "projection-budget-fence",
|
|
1548
|
-
});
|
|
1549
|
-
ladder.pendingContextNote =
|
|
1550
|
-
`The ${brandNoun} projection estimates ${tokens} tokens against a ${budgetTokens}-token serving budget` +
|
|
1551
|
-
(recoveryAttempts
|
|
1552
|
-
? `, after ${recoveryAttempts} recovery attempt(s) that folded everything foldable. The remaining ` +
|
|
1553
|
-
"inflow does not fit at any folding depth, which is an impossibility rather than a recoverable state."
|
|
1554
|
-
: ".") +
|
|
1555
|
-
" The provider request was aborted before transmission; run /compact or make an explicit bounded context fold.";
|
|
1556
|
-
if (advisory.hardFenceNoticeKey !== key) {
|
|
1557
|
-
advisory.hardFenceNoticeKey = key;
|
|
1558
|
-
safeNotify(
|
|
1559
|
-
ctx,
|
|
1560
|
-
"Provider request aborted: the projection exceeds the serving budget. " +
|
|
1561
|
-
"Run /compact or make an explicit bounded context fold.",
|
|
1562
|
-
"error",
|
|
1563
|
-
);
|
|
1564
|
-
}
|
|
1565
|
-
if (typeof ctx.abort !== "function") {
|
|
1566
|
-
throw new Error(`Pi projection-budget abort capability is unavailable at ${tokens} estimated tokens`);
|
|
1567
|
-
}
|
|
1568
|
-
ctx.abort();
|
|
1569
|
-
};
|
|
1570
|
-
|
|
1571
1745
|
// THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
|
|
1572
1746
|
//
|
|
1573
1747
|
// `maxTarget` is what the settings panel calls the commit trigger, and until now it
|
|
@@ -1666,9 +1840,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1666
1840
|
};
|
|
1667
1841
|
|
|
1668
1842
|
/**
|
|
1669
|
-
* Tell the agent
|
|
1843
|
+
* Tell the agent where the window stands, once per approach to a commit.
|
|
1670
1844
|
*
|
|
1671
|
-
* It is built from LIVE state, so it names what is
|
|
1845
|
+
* It is built from LIVE state, so it names what is staged at the pass it speaks on,
|
|
1672
1846
|
* and it is admitted ONCE per frozen projection, so a pass that adds material leaves it
|
|
1673
1847
|
* buried at its own index rather than moving it to the tail. Those two together mean it
|
|
1674
1848
|
* is a snapshot rather than a running tally: cuts staged after it spoke wait for the
|
|
@@ -1678,19 +1852,54 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1678
1852
|
* epoch early. What it must never do is ask twice for a brief the agent has written,
|
|
1679
1853
|
* and it does not: the rebuild reads the marks, not its own last text.
|
|
1680
1854
|
*/
|
|
1681
|
-
const appendFoldNotice = (
|
|
1855
|
+
const appendFoldNotice = (
|
|
1856
|
+
projected: unknown[],
|
|
1857
|
+
snapshot: ActiveContextSnapshot,
|
|
1858
|
+
bodyBytes: number,
|
|
1859
|
+
): unknown[] => {
|
|
1682
1860
|
// The deterministic experiment condition: cuts stage and commits land exactly as
|
|
1683
|
-
// ever, but
|
|
1684
|
-
if (!
|
|
1861
|
+
// ever, but no carrier reaches the projection at all.
|
|
1862
|
+
if (!preCommitNotice) return projected;
|
|
1685
1863
|
if (!persistence.state) return projected;
|
|
1686
1864
|
const held = pendingMarks(persistence.state);
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1689
|
-
//
|
|
1690
|
-
//
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1865
|
+
const pinnedMass = explicitProtectedMass(snapshot, persistence.state);
|
|
1866
|
+
// NOTHING TO REPORT IS NOT AN OCCASION. With no staged fold and no pin there is no
|
|
1867
|
+
// status: the commit that is coming will apply nothing, and a notice saying so is a
|
|
1868
|
+
// turn's attention spent on the absence of news.
|
|
1869
|
+
if (!held.length && !pinnedMass.refs) return projected;
|
|
1870
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
1871
|
+
if (!(capacity.budgetTokens > 0)) return projected;
|
|
1872
|
+
// THE TRIGGER IS OCCUPANCY, AND IT NAMES ITS OWN BASIS. The provider's count is
|
|
1873
|
+
// ground truth; the estimate is what there is before the first response lands, and
|
|
1874
|
+
// this runtime's standing law is that an estimate MAY SCHEDULE WORK and may not veto
|
|
1875
|
+
// a request. Appending a notice is scheduling. `bodyBytes` is the serialization this
|
|
1876
|
+
// pass already performed for the freeze predicate, so reading a share here adds no
|
|
1877
|
+
// second pass over the window (gate 155's law).
|
|
1878
|
+
const measured = capacity.usedTokens !== null;
|
|
1879
|
+
const usedTokens = measured
|
|
1880
|
+
? capacity.usedTokens!
|
|
1881
|
+
: Math.ceil(bodyBytes / projectionCharsPerToken());
|
|
1882
|
+
const share = usedTokens / capacity.budgetTokens;
|
|
1883
|
+
// A DISTANCE BELOW THE TRIGGER, floored at zero so a lead wider than the trigger
|
|
1884
|
+
// means "from the first measurement" rather than a negative point nothing crosses.
|
|
1885
|
+
const firePoint = Math.max(0, thresholds.maxTarget - noticeLeadShare);
|
|
1886
|
+
// EDGE TRIGGERED, ONE NOTICE PER APPROACH. A window sits in the band for many passes,
|
|
1887
|
+
// and the freeze breaks whenever the prefix changes, so `carrierAdmitted` alone would
|
|
1888
|
+
// let this re-admit all the way up the band. Two things re-arm it and nothing else:
|
|
1889
|
+
// falling back below the fire point, and a commit landing, which is the event that
|
|
1890
|
+
// ends one approach and begins the next. Without the second, a commit that lands
|
|
1891
|
+
// above the fire point would leave the latch shut for the rest of the session.
|
|
1892
|
+
if (share < firePoint) {
|
|
1893
|
+
ladder.noticeLatch = null;
|
|
1894
|
+
return projected;
|
|
1895
|
+
}
|
|
1896
|
+
if (ladder.noticeLatch !== null && ladder.noticeLatch === curation.commitsDelivered) {
|
|
1897
|
+
return projected;
|
|
1898
|
+
}
|
|
1899
|
+
// APPENDED ONCE PER FROZEN PROJECTION. `carrierAdmitted` is what makes it an APPEND
|
|
1900
|
+
// rather than a rewrite: once the carrier is in the frozen projection it is not
|
|
1901
|
+
// pushed again, so raw material arriving afterwards lands after it and the prefix in
|
|
1902
|
+
// front of it never moves.
|
|
1694
1903
|
//
|
|
1695
1904
|
// The first cut of this withdrew the carrier and re-pushed it at the tail every pass,
|
|
1696
1905
|
// the way the steward band had to because a band must be able to stand down. That is
|
|
@@ -1699,23 +1908,42 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1699
1908
|
// gates 110 and 111 read the whole projection as rewritten and dropped the occupancy
|
|
1700
1909
|
// anchor back to the estimate. A carrier that moves on a quiet pass costs the cache,
|
|
1701
1910
|
// which is the one thing this design exists to avoid.
|
|
1702
|
-
if (unbriefed.length < UNBRIEFED_FOLDS_BEFORE_NOTICE) return projected;
|
|
1703
1911
|
if (!carrierAdmitted("fold-notice")) return projected;
|
|
1912
|
+
const briefed = (mark: Record<string, unknown>): boolean =>
|
|
1913
|
+
(mark as any).briefProvenance?.kind === "supplied" ||
|
|
1914
|
+
(mark as any).briefProvenance?.kind === "augmented";
|
|
1915
|
+
const staged = held.map((mark) => ({
|
|
1916
|
+
id: mark.id as string,
|
|
1917
|
+
kind: mark.kind as string,
|
|
1918
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
1919
|
+
brief: typeof mark.brief === "string" ? mark.brief : undefined,
|
|
1920
|
+
briefed: briefed(mark),
|
|
1921
|
+
}));
|
|
1922
|
+
// THE SHARE IS MEASURED THE WAY THE CAP MEASURES IT. The notice states the pinned
|
|
1923
|
+
// share against MAX_PINNED_SHARE, and `applyProtectionChange` refuses past that cap
|
|
1924
|
+
// on `explicitProtectedMass`, so quoting the stale-only figure `windowClaims` reports
|
|
1925
|
+
// would put a number beside a cap it is not compared against.
|
|
1704
1926
|
const text = foldNoticeText({
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1927
|
+
staged,
|
|
1928
|
+
freedTokens: staged.reduce((total, fold) => total + fold.tokens, 0),
|
|
1929
|
+
briefedCount: staged.filter((fold) => fold.briefed).length,
|
|
1930
|
+
pinnedEntries: pinnedMass.refs,
|
|
1931
|
+
pinnedShare: estimatedTokens(pinnedMass.bytes) / capacity.budgetTokens,
|
|
1932
|
+
maxPinnedShare: MAX_PINNED_SHARE,
|
|
1933
|
+
headroomTokens: Math.max(0, thresholds.maxTarget * capacity.budgetTokens - usedTokens),
|
|
1934
|
+
commitAtShare: thresholds.maxTarget,
|
|
1935
|
+
measured,
|
|
1712
1936
|
toolName,
|
|
1713
1937
|
brandNoun,
|
|
1714
1938
|
});
|
|
1939
|
+
ladder.noticeLatch = curation.commitsDelivered;
|
|
1715
1940
|
emit("context.frontier", {
|
|
1716
1941
|
cut: 0,
|
|
1717
1942
|
pending_marks: held.length,
|
|
1718
|
-
|
|
1943
|
+
briefed: staged.filter((fold) => fold.briefed).length,
|
|
1944
|
+
occupancy_share: Number(share.toFixed(4)),
|
|
1945
|
+
fire_point_share: firePoint,
|
|
1946
|
+
measured,
|
|
1719
1947
|
notice_chars: text.length,
|
|
1720
1948
|
});
|
|
1721
1949
|
projected.push({
|
|
@@ -1731,42 +1959,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1731
1959
|
return projected;
|
|
1732
1960
|
};
|
|
1733
1961
|
|
|
1734
|
-
/**
|
|
1735
|
-
* THE TABLE OF CONTENTS IS ONE COPY PER COMMIT (Shane, 2026-08-26). It is admitted
|
|
1736
|
-
* once per frozen projection, exactly as the fold notice above, and that admission
|
|
1737
|
-
* rule IS the refresh law: between commits the copy stays buried at its index, so a
|
|
1738
|
-
* quiet pass moves no byte and costs no occupancy anchor, and the tool result of every
|
|
1739
|
-
* `remember` carries the live key list in the meantime. A commit rewrites the
|
|
1740
|
-
* projection, the freeze drops, the body filter strips the old copy, and the next pass
|
|
1741
|
-
* seats a fresh one at the tail with the latest keys. Nothing here re-renders in
|
|
1742
|
-
* place, because a carrier that moves on a quiet pass is the exact defect the fold
|
|
1743
|
-
* notice's comment records.
|
|
1744
|
-
*/
|
|
1745
|
-
const appendMemoryToc = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
|
|
1746
|
-
if (!workingMemory) return projected;
|
|
1747
|
-
if (!persistence.state) return projected;
|
|
1748
|
-
const held = persistence.state.memory ?? [];
|
|
1749
|
-
if (!held.length) return projected;
|
|
1750
|
-
if (!carrierAdmitted("memory-toc")) return projected;
|
|
1751
|
-
const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
|
|
1752
|
-
const text = memoryTocText({
|
|
1753
|
-
entries: entries.map((entry) => ({ key: entry.key, chars: entry.body.length })),
|
|
1754
|
-
toolName,
|
|
1755
|
-
brandNoun,
|
|
1756
|
-
});
|
|
1757
|
-
projected.push({
|
|
1758
|
-
role: "custom",
|
|
1759
|
-
customType: memoryTocProjectionType,
|
|
1760
|
-
content: text,
|
|
1761
|
-
display: false,
|
|
1762
|
-
details: { source: activeContextSource(entryTypePrefix), ephemeral: true },
|
|
1763
|
-
timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
|
|
1764
|
-
? ownValue(snapshot.messages.at(-1), "timestamp")
|
|
1765
|
-
: 0,
|
|
1766
|
-
});
|
|
1767
|
-
return projected;
|
|
1768
|
-
};
|
|
1769
|
-
|
|
1770
1962
|
const enforceBandTop = async (
|
|
1771
1963
|
snapshot: ActiveContextSnapshot,
|
|
1772
1964
|
ctx: any,
|
|
@@ -1805,10 +1997,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1805
1997
|
snapshot: ActiveContextSnapshot,
|
|
1806
1998
|
projected: unknown[],
|
|
1807
1999
|
ctx: any,
|
|
1808
|
-
): Promise<{ projected: unknown[];
|
|
2000
|
+
): Promise<{ projected: unknown[]; text: string }> => {
|
|
1809
2001
|
let measured = projectionExceedsBudget(projected, ctx);
|
|
1810
2002
|
const rejected = curation.pendingRejection !== null;
|
|
1811
|
-
if (!measured.crowded && !rejected) return { projected,
|
|
2003
|
+
if (!measured.crowded && !rejected) return { projected, text: measured.text };
|
|
1812
2004
|
const trigger = measured;
|
|
1813
2005
|
let reduced = projected;
|
|
1814
2006
|
let attempts = 0;
|
|
@@ -1912,11 +2104,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1912
2104
|
}));
|
|
1913
2105
|
curation.pendingRejection = null;
|
|
1914
2106
|
}
|
|
1915
|
-
//
|
|
1916
|
-
//
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2107
|
+
// THE RUNTIME COMMITS AND TRANSMITS; ONLY THE PROVIDER REFUSES (Shane, 2026-08-28).
|
|
2108
|
+
//
|
|
2109
|
+
// This used to abort the request when the projection still read over budget after the
|
|
2110
|
+
// loop. It was deleted because it let an ESTIMATE veto a request that ground truth
|
|
2111
|
+
// would have accepted: a live session read 220,883 tokens where the provider counted
|
|
2112
|
+
// 111,837, was refused before transmission, and could not be rescued, because the
|
|
2113
|
+
// commit before the refusal had applied 28 marks and the next applied 0. The advice it
|
|
2114
|
+
// printed, fold manually or run /compact, named the two things the mechanism had just
|
|
2115
|
+
// proven it could not do.
|
|
2116
|
+
//
|
|
2117
|
+
// maxTarget is a TRIGGER meaning "commit now", not a bound meaning "refuse to
|
|
2118
|
+
// proceed", and only the first is something an estimate can be right about. What
|
|
2119
|
+
// remains is the loop above, which commits everything it can reach; the request then
|
|
2120
|
+
// goes to the provider, and a genuine overflow is handled by the rejection path, which
|
|
2121
|
+
// branches back, folds and retries on the provider's own count rather than ours.
|
|
2122
|
+
// `measured` is recomputed on `reduced` at every turn of the loop, so its text is the
|
|
2123
|
+
// serialization of the array this returns.
|
|
2124
|
+
return { projected: reduced, text: measured.text };
|
|
1920
2125
|
};
|
|
1921
2126
|
|
|
1922
2127
|
const startPreparation = (snapshot: ActiveContextSnapshot, ratio: number | null, ctx: any): void => {
|
|
@@ -2071,7 +2276,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2071
2276
|
const customType = ownValue(message, "customType");
|
|
2072
2277
|
return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
|
|
2073
2278
|
customType !== receiptProjectionType && customType !== curationProjectionType &&
|
|
2074
|
-
customType !== foldNoticeProjectionType
|
|
2279
|
+
customType !== foldNoticeProjectionType;
|
|
2075
2280
|
});
|
|
2076
2281
|
const withdrawn = withdrawConsumedEphemeralPeeks(body);
|
|
2077
2282
|
if (withdrawn.size && freeze.body) {
|
|
@@ -2098,8 +2303,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2098
2303
|
freeze.body = body;
|
|
2099
2304
|
freeze.bodyText = bodyText;
|
|
2100
2305
|
appendReceipts(projected, snapshot);
|
|
2101
|
-
appendFoldNotice(projected, snapshot);
|
|
2102
|
-
appendMemoryToc(projected, snapshot);
|
|
2306
|
+
appendFoldNotice(projected, snapshot, bodyText.length);
|
|
2103
2307
|
return holdFrozen(projected);
|
|
2104
2308
|
};
|
|
2105
2309
|
const noteProjection = (projected: unknown[], serialized?: string): void => {
|
|
@@ -2792,6 +2996,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2792
2996
|
measurements.lastProviderMeasurement = null;
|
|
2793
2997
|
measurements.wallInflowSteps.length = 0;
|
|
2794
2998
|
measurements.projectionAnchor = null;
|
|
2999
|
+
measurements.anchorRefusal = null;
|
|
2795
3000
|
nativeCompaction.lastThresholdDecision = null;
|
|
2796
3001
|
updateStatus(ctx);
|
|
2797
3002
|
};
|
|
@@ -2888,11 +3093,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2888
3093
|
try { projected = projectWithAdvisory(snapshot); }
|
|
2889
3094
|
catch (error) {
|
|
2890
3095
|
suspendAutomatic(error, "projection", ctx);
|
|
2891
|
-
abortUnsafeHardContext(snapshot, ctx);
|
|
2892
|
-
return { messages: event.messages };
|
|
2893
|
-
}
|
|
2894
|
-
if (abortUnsafeHardContext(snapshot, ctx, true)) {
|
|
2895
|
-
updateStatus(ctx);
|
|
2896
3096
|
return { messages: event.messages };
|
|
2897
3097
|
}
|
|
2898
3098
|
// THE FRONTIER CUTS FIRST, and it costs the projection nothing to do so: a pending
|
|
@@ -2908,7 +3108,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2908
3108
|
try { projected = projectWithAdvisory(snapshot); }
|
|
2909
3109
|
catch (error) {
|
|
2910
3110
|
suspendAutomatic(error, "projection", ctx);
|
|
2911
|
-
abortUnsafeHardContext(snapshot, ctx);
|
|
2912
3111
|
return { messages: event.messages };
|
|
2913
3112
|
}
|
|
2914
3113
|
}
|
|
@@ -2916,14 +3115,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2916
3115
|
projected = budgeted.projected;
|
|
2917
3116
|
const reading = projectedTokenReading(projected, budgeted.text);
|
|
2918
3117
|
measurements.lastProjectedChars = reading.chars;
|
|
3118
|
+
measurements.lastProjectedImages = reading.images;
|
|
2919
3119
|
measurements.lastProjectedEstimate = reading.tokens;
|
|
2920
3120
|
measurements.lastProjectedEstimateBasis = reading.basis;
|
|
2921
|
-
|
|
3121
|
+
// The size is the reading's own image-corrected figure. Recomputing it from raw
|
|
3122
|
+
// chars here is what fed base64 straight into the fence's own accounting.
|
|
3123
|
+
measurements.lastProjectedSizeTokens = reading.sizeTokens;
|
|
2922
3124
|
measurements.lastProjectedEstimateCalibrated = measurements.projectionCalibrations.length > 0;
|
|
2923
|
-
if (budgeted.aborted) {
|
|
2924
|
-
updateStatus(ctx);
|
|
2925
|
-
return { messages: projected };
|
|
2926
|
-
}
|
|
2927
3125
|
noteProjection(projected, budgeted.text);
|
|
2928
3126
|
updateStatus(ctx);
|
|
2929
3127
|
return { messages: projected };
|
|
@@ -2937,7 +3135,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2937
3135
|
if (!persistedSucceeded) ladder.boundaryFailure = lifecycle.latestSnapshotError;
|
|
2938
3136
|
suspendAutomatic(error, persistedSucceeded ? "post-persist-projection" : "context", ctx);
|
|
2939
3137
|
}
|
|
2940
|
-
abortUnsafeHardContext(lifecycle.latestSnapshot, ctx);
|
|
2941
3138
|
return { messages: event.messages };
|
|
2942
3139
|
}
|
|
2943
3140
|
};
|
|
@@ -3400,6 +3597,94 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3400
3597
|
return removed as Array<Record<string, unknown>>;
|
|
3401
3598
|
};
|
|
3402
3599
|
|
|
3600
|
+
/**
|
|
3601
|
+
* THE DISSOLVE PATH, shared by the tool's single-id reboundary and the editor's re-cut
|
|
3602
|
+
* (2026-08-30). Returning a fold to raw is the first half of re-cutting it, and the
|
|
3603
|
+
* editor already has the second half: `m` on two raw entries stages the span the person
|
|
3604
|
+
* meant. Extracted rather than reimplemented, because the editor's own law is that
|
|
3605
|
+
* every edit is a skin over an existing validated path, and this one carries the nested
|
|
3606
|
+
* refusal, the brief and lease cleanup and the parent re-derivation with it.
|
|
3607
|
+
*/
|
|
3608
|
+
const dissolveFold = async (
|
|
3609
|
+
snapshot: ActiveContextSnapshot,
|
|
3610
|
+
id: string,
|
|
3611
|
+
ctx: any,
|
|
3612
|
+
): Promise<{ children: string[]; startId: string | null; endId: string | null }> => {
|
|
3613
|
+
const fold = requireActiveFold(snapshot, persistence.state!, id);
|
|
3614
|
+
if (fold.parentId) {
|
|
3615
|
+
throw new Error(
|
|
3616
|
+
`Fold ${id} is nested under ${fold.parentId}; dissolve ${fold.parentId} first so ${id} is a root`,
|
|
3617
|
+
);
|
|
3618
|
+
}
|
|
3619
|
+
const refs = flattenFoldRefs(fold, persistence.state!);
|
|
3620
|
+
const children = childFoldIds(fold);
|
|
3621
|
+
const briefs = { ...(persistence.state!.briefs ?? {}) };
|
|
3622
|
+
delete briefs[id];
|
|
3623
|
+
const next: ActiveContextState = {
|
|
3624
|
+
...persistence.state!,
|
|
3625
|
+
revision: persistence.state!.revision + 1,
|
|
3626
|
+
folds: deriveFoldParents(persistence.state!.folds.filter((item) => item.id !== id)),
|
|
3627
|
+
expanded: persistence.state!.expanded.filter((expandedId) => expandedId !== id),
|
|
3628
|
+
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
3629
|
+
};
|
|
3630
|
+
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3631
|
+
const leases = { ...next.leases };
|
|
3632
|
+
delete leases[id];
|
|
3633
|
+
await persistManual({ ...next, leases }, ctx);
|
|
3634
|
+
updateStatus(ctx);
|
|
3635
|
+
return { children, startId: refs[0]?.entryId ?? null, endId: refs.at(-1)?.entryId ?? null };
|
|
3636
|
+
};
|
|
3637
|
+
|
|
3638
|
+
/**
|
|
3639
|
+
* THE EXPANSION PATH, shared by the tool's expand/refold actions and the editor's
|
|
3640
|
+
* promote-to-context intent (2026-08-30).
|
|
3641
|
+
*
|
|
3642
|
+
* THIS IS THE ACT THE EDITOR'S OWN EXPANSION IS NOT. Opening a fold row in /fold-editor
|
|
3643
|
+
* moves a key in a Set the view owns: it reveals the fold to the READER and the model
|
|
3644
|
+
* never learns it happened. This restores the fold's raw bytes to the PROJECTION, which
|
|
3645
|
+
* costs the window and is what the lease exists to reclaim. Shane, 2026-08-30: the user
|
|
3646
|
+
* should "page through/expand for their own viewing purposes" while "setting an expanded
|
|
3647
|
+
* fold is a different choice". Two acts, two paths, and this is the one that spends.
|
|
3648
|
+
*/
|
|
3649
|
+
const setFoldExpansion = async (
|
|
3650
|
+
snapshot: ActiveContextSnapshot,
|
|
3651
|
+
id: string,
|
|
3652
|
+
expand: boolean,
|
|
3653
|
+
ctx: any,
|
|
3654
|
+
): Promise<unknown[]> => {
|
|
3655
|
+
let epochApplied: unknown[] = [];
|
|
3656
|
+
if (expand && pendingMarks(persistence.state!).length) {
|
|
3657
|
+
const result = await commitPendingMarks({
|
|
3658
|
+
snapshot,
|
|
3659
|
+
state: persistence.state!,
|
|
3660
|
+
generation: lifecycle.generation,
|
|
3661
|
+
retainIneligible: true,
|
|
3662
|
+
});
|
|
3663
|
+
persistence.state = result.state;
|
|
3664
|
+
epochApplied = result.applied;
|
|
3665
|
+
}
|
|
3666
|
+
const expanding = requireActiveFold(snapshot, persistence.state!, id);
|
|
3667
|
+
if (expand) {
|
|
3668
|
+
admit({
|
|
3669
|
+
action: "expand",
|
|
3670
|
+
ctx,
|
|
3671
|
+
foldId: id,
|
|
3672
|
+
requestedBytes: Math.max(0, expanding.sourceChars - expanding.placeholderChars),
|
|
3673
|
+
children: childFoldIds(expanding),
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
let next = setFoldProjectionState(persistence.state!, id, expand ? "expanded" : "folded");
|
|
3677
|
+
if (expand) next = withExpandLease(next, id);
|
|
3678
|
+
else {
|
|
3679
|
+
const leases = { ...next.leases };
|
|
3680
|
+
delete leases[id];
|
|
3681
|
+
next = { ...next, leases };
|
|
3682
|
+
}
|
|
3683
|
+
await persistManual(next, ctx);
|
|
3684
|
+
updateStatus(ctx);
|
|
3685
|
+
return epochApplied;
|
|
3686
|
+
};
|
|
3687
|
+
|
|
3403
3688
|
/**
|
|
3404
3689
|
* THE PIN CHANGE PATH, shared by the tool's pin/unpin actions and the editor's
|
|
3405
3690
|
* pin toggle: the pinned-share cap, protectEvidence, the canonical event, persist.
|
|
@@ -3884,36 +4169,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3884
4169
|
}
|
|
3885
4170
|
const id = String(params.id ?? "").trim();
|
|
3886
4171
|
if (!id) throw new Error("reboundary requires id, or ids naming the span to re-cut");
|
|
3887
|
-
const
|
|
3888
|
-
if (fold.parentId) {
|
|
3889
|
-
throw new Error(
|
|
3890
|
-
`Fold ${id} is nested under ${fold.parentId}; dissolve ${fold.parentId} first so ${id} is a root`,
|
|
3891
|
-
);
|
|
3892
|
-
}
|
|
3893
|
-
const refs = flattenFoldRefs(fold, persistence.state);
|
|
3894
|
-
const children = childFoldIds(fold);
|
|
3895
|
-
const briefs = { ...(persistence.state.briefs ?? {}) };
|
|
3896
|
-
delete briefs[id];
|
|
3897
|
-
const next: ActiveContextState = {
|
|
3898
|
-
...persistence.state,
|
|
3899
|
-
revision: persistence.state.revision + 1,
|
|
3900
|
-
folds: deriveFoldParents(persistence.state.folds.filter((item) => item.id !== id)),
|
|
3901
|
-
expanded: persistence.state.expanded.filter((expandedId) => expandedId !== id),
|
|
3902
|
-
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
3903
|
-
};
|
|
3904
|
-
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3905
|
-
const leases = { ...next.leases };
|
|
3906
|
-
delete leases[id];
|
|
3907
|
-
await persistManual({ ...next, leases }, ctx);
|
|
3908
|
-
updateStatus(ctx);
|
|
4172
|
+
const dissolved = await dissolveFold(snapshot, id, ctx);
|
|
3909
4173
|
return toolPayload({
|
|
3910
4174
|
version: 1,
|
|
3911
4175
|
action,
|
|
3912
4176
|
id,
|
|
3913
4177
|
dissolved: true,
|
|
3914
|
-
releasedChildren: children,
|
|
3915
|
-
startId:
|
|
3916
|
-
endId:
|
|
4178
|
+
releasedChildren: dissolved.children,
|
|
4179
|
+
startId: dissolved.startId,
|
|
4180
|
+
endId: dissolved.endId,
|
|
3917
4181
|
durableRevision: persistence.state.revision,
|
|
3918
4182
|
activation: `the span ${id} held is raw again and its boundary is yours to re-cut; ` +
|
|
3919
4183
|
"fold the endpoints you meant, or two sub-spans to split it. No evidence moved.",
|
|
@@ -3922,36 +4186,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3922
4186
|
if (action === "expand" || action === "refold") {
|
|
3923
4187
|
const id = String(params.id ?? "").trim();
|
|
3924
4188
|
if (!id) throw new Error(`${action} requires id`);
|
|
3925
|
-
|
|
3926
|
-
if (action === "expand" && pendingMarks(persistence.state).length) {
|
|
3927
|
-
const result = await commitPendingMarks({
|
|
3928
|
-
snapshot,
|
|
3929
|
-
state: persistence.state,
|
|
3930
|
-
generation: lifecycle.generation,
|
|
3931
|
-
retainIneligible: true,
|
|
3932
|
-
});
|
|
3933
|
-
persistence.state = result.state;
|
|
3934
|
-
epochApplied = result.applied;
|
|
3935
|
-
}
|
|
3936
|
-
const expanding = requireActiveFold(snapshot, persistence.state, id);
|
|
3937
|
-
if (action === "expand") {
|
|
3938
|
-
admit({
|
|
3939
|
-
action: "expand",
|
|
3940
|
-
ctx,
|
|
3941
|
-
foldId: id,
|
|
3942
|
-
requestedBytes: Math.max(0, expanding.sourceChars - expanding.placeholderChars),
|
|
3943
|
-
children: childFoldIds(expanding),
|
|
3944
|
-
});
|
|
3945
|
-
}
|
|
3946
|
-
let next = setFoldProjectionState(persistence.state, id, action === "expand" ? "expanded" : "folded");
|
|
3947
|
-
if (action === "expand") next = withExpandLease(next, id);
|
|
3948
|
-
else {
|
|
3949
|
-
const leases = { ...next.leases };
|
|
3950
|
-
delete leases[id];
|
|
3951
|
-
next = { ...next, leases };
|
|
3952
|
-
}
|
|
3953
|
-
await persistManual(next, ctx);
|
|
3954
|
-
updateStatus(ctx);
|
|
4189
|
+
const epochApplied = await setFoldExpansion(snapshot, id, action === "expand", ctx);
|
|
3955
4190
|
return toolPayload({
|
|
3956
4191
|
version: 1,
|
|
3957
4192
|
action,
|
|
@@ -4081,102 +4316,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4081
4316
|
"reaches it only when the commit folds this span. Nothing in your context moved.",
|
|
4082
4317
|
});
|
|
4083
4318
|
}
|
|
4084
|
-
if (action === "remember") {
|
|
4085
|
-
// THE DIGEST'S OWN CHANNEL. A working-memory write is durable state outside the
|
|
4086
|
-
// projection, so like `brief` it moves no bytes and costs no prefix cache: the
|
|
4087
|
-
// table of contents the projection carries refreshes at the next commit, and this
|
|
4088
|
-
// result carries the live key list in the meantime.
|
|
4089
|
-
const key = String(params.key ?? "").trim();
|
|
4090
|
-
if (!key) throw new Error("remember needs a key");
|
|
4091
|
-
if (key.length > MEMORY_KEY_CHARS) {
|
|
4092
|
-
throw new Error(`Working-memory keys have a hard ${MEMORY_KEY_CHARS}-character ` +
|
|
4093
|
-
"maximum: the key is a table-of-contents line, not the note. Put the detail " +
|
|
4094
|
-
"in the body.");
|
|
4095
|
-
}
|
|
4096
|
-
const body = String(params.body ?? "").trim();
|
|
4097
|
-
if (body.length > MEMORY_BODY_CHARS) {
|
|
4098
|
-
throw new Error(`Working-memory bodies have a hard ${MEMORY_BODY_CHARS}-character ` +
|
|
4099
|
-
`maximum and this one is ${body.length}. Split the entry, or keep the part ` +
|
|
4100
|
-
"that changes and let the transcript carry the rest: folded material stays " +
|
|
4101
|
-
"losslessly recoverable by peek.");
|
|
4102
|
-
}
|
|
4103
|
-
const held = persistence.state!.memory ?? [];
|
|
4104
|
-
const existing = held.find((entry) => entry.key === key);
|
|
4105
|
-
let nextMemory: WorkingMemoryEntry[];
|
|
4106
|
-
if (!body) {
|
|
4107
|
-
if (!existing) {
|
|
4108
|
-
throw new Error(`No working-memory entry '${key}' to remove. ${held.length
|
|
4109
|
-
? `The keys are: ${held.map((entry) => entry.key).join(", ")}.`
|
|
4110
|
-
: "Working memory is empty."}`);
|
|
4111
|
-
}
|
|
4112
|
-
nextMemory = held.filter((entry) => entry.key !== key);
|
|
4113
|
-
} else {
|
|
4114
|
-
if (!existing && held.length >= MEMORY_KEYS_MAX) {
|
|
4115
|
-
throw new Error(`Working memory holds its maximum of ${MEMORY_KEYS_MAX} ` +
|
|
4116
|
-
"entries. Merge two entries, or remove one whose facts no longer matter " +
|
|
4117
|
-
`(${toolName} {"action":"remember","key":"<key>","body":""}), then write ` +
|
|
4118
|
-
"this one.");
|
|
4119
|
-
}
|
|
4120
|
-
const ordinal = held.reduce((max, entry) => Math.max(max, entry.ordinal), 0) + 1;
|
|
4121
|
-
nextMemory = existing
|
|
4122
|
-
? held.map((entry) => entry.key === key ? { key, body, ordinal } : entry)
|
|
4123
|
-
: [...held, { key, body, ordinal }];
|
|
4124
|
-
}
|
|
4125
|
-
const nextState = {
|
|
4126
|
-
...persistence.state!,
|
|
4127
|
-
revision: persistence.state!.revision + 1,
|
|
4128
|
-
...(nextMemory.length ? { memory: nextMemory } : {}),
|
|
4129
|
-
};
|
|
4130
|
-
if (!nextMemory.length) delete (nextState as Partial<ActiveContextState>).memory;
|
|
4131
|
-
persistence.state = nextState;
|
|
4132
|
-
await persistManual(nextState, ctx);
|
|
4133
|
-
updateStatus(ctx);
|
|
4134
|
-
const toc = [...nextMemory].sort((left, right) => right.ordinal - left.ordinal)
|
|
4135
|
-
.map((entry) => ({ key: entry.key, chars: entry.body.length }));
|
|
4136
|
-
return toolPayload({
|
|
4137
|
-
version: 1,
|
|
4138
|
-
action,
|
|
4139
|
-
key,
|
|
4140
|
-
...(body ? { chars: body.length } : { removed: true }),
|
|
4141
|
-
argumentsSha256: executionArgumentsSha256,
|
|
4142
|
-
durableRevision: persistence.state!.revision,
|
|
4143
|
-
entries: toc,
|
|
4144
|
-
activation: body
|
|
4145
|
-
? "durable immediately, and it costs your window nothing: the body lives in " +
|
|
4146
|
-
"state, this list is the live view, and the table of contents in your " +
|
|
4147
|
-
"context refreshes at the next commit."
|
|
4148
|
-
: "removed. The table of contents in your context refreshes at the next commit.",
|
|
4149
|
-
});
|
|
4150
|
-
}
|
|
4151
|
-
if (action === "recall") {
|
|
4152
|
-
const held = persistence.state!.memory ?? [];
|
|
4153
|
-
if (!held.length) {
|
|
4154
|
-
throw new Error(`Working memory is empty. Write an entry with ${toolName} ` +
|
|
4155
|
-
'{"action":"remember","key":"<key>","body":"..."}.');
|
|
4156
|
-
}
|
|
4157
|
-
const requested = params.keys === undefined && params.key === undefined
|
|
4158
|
-
? null
|
|
4159
|
-
: (denseOwnArrayValues(params.keys) ?? [params.key ?? params.keys])
|
|
4160
|
-
.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
4161
|
-
const entries = [...held].sort((left, right) => right.ordinal - left.ordinal);
|
|
4162
|
-
const chosen = requested === null
|
|
4163
|
-
? entries
|
|
4164
|
-
: entries.filter((entry) => requested.includes(entry.key));
|
|
4165
|
-
if (requested !== null) {
|
|
4166
|
-
const missing = requested.filter((key) => !held.some((entry) => entry.key === key));
|
|
4167
|
-
if (missing.length) {
|
|
4168
|
-
throw new Error(`No working-memory entr${missing.length === 1 ? "y" : "ies"} ` +
|
|
4169
|
-
`${missing.join(", ")}. The keys are: ` +
|
|
4170
|
-
`${entries.map((entry) => entry.key).join(", ")}.`);
|
|
4171
|
-
}
|
|
4172
|
-
}
|
|
4173
|
-
return toolPayload({
|
|
4174
|
-
version: 1,
|
|
4175
|
-
action,
|
|
4176
|
-
argumentsSha256: executionArgumentsSha256,
|
|
4177
|
-
entries: chosen.map((entry) => ({ key: entry.key, chars: entry.body.length, body: entry.body })),
|
|
4178
|
-
});
|
|
4179
|
-
}
|
|
4180
4319
|
throw new Error(`Unknown ${toolName} action '${action}'`);
|
|
4181
4320
|
};
|
|
4182
4321
|
const toolHandler = async (
|
|
@@ -4232,12 +4371,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4232
4371
|
try {
|
|
4233
4372
|
const snapshot = authoritativeSnapshotFor(ctx);
|
|
4234
4373
|
const status = activeContextStatus(snapshot, persistence.state, 0, 40);
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4374
|
+
const capacity = currentCapacity(ctx);
|
|
4375
|
+
const staged = pendingMarks(persistence.state);
|
|
4376
|
+
safeNotify(ctx, foldStatusText({
|
|
4377
|
+
brand: brandNoun,
|
|
4378
|
+
usedTokens: measurements.lastProviderMeasurement?.tokens ?? null,
|
|
4379
|
+
budgetTokens: capacity.budgetTokens,
|
|
4380
|
+
commitAtShare: snapshot.thresholds.maxTarget,
|
|
4381
|
+
aimShare: snapshot.thresholds.minTarget,
|
|
4382
|
+
roots: (status.roots as string[]).length,
|
|
4383
|
+
totalFolds: Number(status.totalFolds ?? 0),
|
|
4384
|
+
stagedMarks: staged.length,
|
|
4385
|
+
stagedTokens: staged.reduce((total, mark) =>
|
|
4386
|
+
total + estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)), 0),
|
|
4387
|
+
pinned: persistence.state.protected.length,
|
|
4388
|
+
suspended: ladder.automaticFailure?.message ?? null,
|
|
4389
|
+
foldCommand: `/${commandNames.fold}`,
|
|
4390
|
+
editorCommand: "/fold-editor",
|
|
4391
|
+
}), "info");
|
|
4241
4392
|
} catch (error) {
|
|
4242
4393
|
safeNotify(ctx, `Active-context status unavailable; native Pi context is unchanged: ${String(error)}`, "warning");
|
|
4243
4394
|
}
|
|
@@ -4366,6 +4517,37 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4366
4517
|
tokens: estimatedTokens(spanBytes(snapshot, Math.min(from, to), Math.max(from, to) + 1)),
|
|
4367
4518
|
});
|
|
4368
4519
|
|
|
4520
|
+
/**
|
|
4521
|
+
* THE READER'S OWN DEPTH, AND IT COSTS THE AGENT NOTHING (2026-08-30).
|
|
4522
|
+
*
|
|
4523
|
+
* A row preview is bounded at EDITOR_ROW_PREVIEW_CHARS because it is a SUMMARY, and
|
|
4524
|
+
* the editor used to have nothing behind it: the whole surface stopped at 300
|
|
4525
|
+
* characters of the first text block. This is the way past that. It is an INJECTED
|
|
4526
|
+
* accessor, like `spanCost`, so the view still holds no runtime reference and still
|
|
4527
|
+
* computes nothing it was not handed, and it is a pure READ: it moves no bytes,
|
|
4528
|
+
* writes no state and never touches the projection, so paging an entry to its end
|
|
4529
|
+
* changes nothing the agent will ever see. That separation is the whole point. A
|
|
4530
|
+
* person auditing the window needs to read further than the model is being shown,
|
|
4531
|
+
* and expanding a fold INTO the model's window is a different act with a different
|
|
4532
|
+
* verb (`expand`) and a different cost.
|
|
4533
|
+
*
|
|
4534
|
+
* The cut is STATED rather than silent, which is gate 136's law carried to a surface
|
|
4535
|
+
* a person reads: `total` and `hasMore` travel with the slice.
|
|
4536
|
+
*/
|
|
4537
|
+
const entryDetail = (entryId: string, offset = 0, chars = EDITOR_DETAIL_PAGE_CHARS) => {
|
|
4538
|
+
const item = snapshot.mapped.find((candidate) => candidate.ref?.entryId === entryId);
|
|
4539
|
+
if (!item) return null;
|
|
4540
|
+
const whole = entryText(item.message);
|
|
4541
|
+
const from = Math.max(0, Math.min(offset, whole.length));
|
|
4542
|
+
const width = Math.max(1, Math.min(chars, EDITOR_DETAIL_PAGE_CHARS));
|
|
4543
|
+
return {
|
|
4544
|
+
text: whole.slice(from, from + width),
|
|
4545
|
+
offset: from,
|
|
4546
|
+
total: whole.length,
|
|
4547
|
+
hasMore: from + width < whole.length,
|
|
4548
|
+
};
|
|
4549
|
+
};
|
|
4550
|
+
|
|
4369
4551
|
// Marks move no bytes until the commit, so window geometry cannot change while
|
|
4370
4552
|
// the editor holds it open; only pending state grows. Rebuilding against the SAME
|
|
4371
4553
|
// snapshot with fresh state keeps PROPOSED rows honest without re-deriving indices.
|
|
@@ -4377,12 +4559,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4377
4559
|
: null;
|
|
4378
4560
|
const accounting = markAccounting(snapshot, state);
|
|
4379
4561
|
return {
|
|
4380
|
-
|
|
4562
|
+
// NO SESSION ID. Eight hex characters of a value a person never types, on the
|
|
4563
|
+
// title line of the surface, is the same defect the rows below it just lost.
|
|
4564
|
+
title: `${brandNoun} window`,
|
|
4381
4565
|
occupancy: {
|
|
4382
4566
|
usedTokens: capacity.usedTokens,
|
|
4383
4567
|
budgetTokens: capacity.budgetTokens,
|
|
4384
4568
|
commitOccupancy: thresholds.maxTarget,
|
|
4385
4569
|
commitDue: occupancy !== null && occupancy >= thresholds.maxTarget,
|
|
4570
|
+
suspended: Boolean(ladder.automaticFailure),
|
|
4386
4571
|
},
|
|
4387
4572
|
blocks: buildFoldEditorData(snapshot, state, {
|
|
4388
4573
|
foldRows: () => (state.folds ?? []).map((fold: any) => {
|
|
@@ -4392,14 +4577,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4392
4577
|
const item = exactMapped(snapshot, ref);
|
|
4393
4578
|
const message = item?.message as Record<string, unknown> | undefined;
|
|
4394
4579
|
const role = String(item?.ref?.role ?? message?.role ?? "entry");
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4580
|
+
// A FOLD'S ENTRY IS A MARK POINT AND A PIN POINT TOO (2026-08-30). It
|
|
4581
|
+
// carried no index and no entry id the view could act on, so `p` was
|
|
4582
|
+
// inert on every row inside an expanded fold while working one row over
|
|
4583
|
+
// in a raw gap, for no reason a reader could see.
|
|
4584
|
+
return {
|
|
4585
|
+
id: ref.entryId,
|
|
4586
|
+
role,
|
|
4587
|
+
preview: rowPreview(item?.message),
|
|
4588
|
+
...(item ? { index: item.index } : {}),
|
|
4589
|
+
};
|
|
4403
4590
|
});
|
|
4404
4591
|
return {
|
|
4405
4592
|
id: String(row.id),
|
|
@@ -4408,6 +4595,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4408
4595
|
sourceCount: Number(row.sourceCount ?? 0),
|
|
4409
4596
|
startPosition: Number(row.startPosition),
|
|
4410
4597
|
endPosition: Number(row.endPosition),
|
|
4598
|
+
// WHAT THE AGENT IS ACTUALLY LOOKING AT (2026-08-30). A fold the agent has
|
|
4599
|
+
// expanded is standing RAW in its window, and the editor rendered it
|
|
4600
|
+
// identically to one that is still a placeholder, so the one surface built
|
|
4601
|
+
// to show a person the model's window could not show them this about it.
|
|
4602
|
+
expandedInContext: (state.expanded ?? []).includes(String(row.id)),
|
|
4603
|
+
// WHERE THE BRIEF CAME FROM. Given the campaign verdict on agent-written
|
|
4604
|
+
// briefs, which sentence is the runtime's and which is the model's is the
|
|
4605
|
+
// fact a person auditing this window most needs, and it was not on the
|
|
4606
|
+
// surface at all.
|
|
4607
|
+
provenance: String((fold as any).briefProvenance?.kind ?? "deterministic"),
|
|
4411
4608
|
entries,
|
|
4412
4609
|
};
|
|
4413
4610
|
}),
|
|
@@ -4433,21 +4630,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4433
4630
|
if (!item) continue;
|
|
4434
4631
|
const message = item.message as Record<string, unknown> | undefined;
|
|
4435
4632
|
const role = String(item.ref?.role ?? message?.role ?? "entry");
|
|
4436
|
-
const content = message?.content;
|
|
4437
|
-
// 300 chars, same as fold entries: the view shows 48 on the row and
|
|
4438
|
-
// deepens to 240 on Enter, so a 48-char preview made detail a no-op.
|
|
4439
|
-
let preview = "";
|
|
4440
|
-
if (typeof content === "string") preview = content.slice(0, 300);
|
|
4441
|
-
else if (Array.isArray(content)) {
|
|
4442
|
-
const firstText = content.find((part) => part?.type === "text");
|
|
4443
|
-
preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
|
|
4444
|
-
}
|
|
4445
4633
|
// THE INDEX IS THE MARK POINT: raw entries carry their mapped position so
|
|
4446
4634
|
// the view can price a span and name its two boundary ids.
|
|
4447
4635
|
out.push({
|
|
4448
4636
|
id: item.ref?.entryId ?? String(index),
|
|
4449
4637
|
role,
|
|
4450
|
-
preview:
|
|
4638
|
+
preview: rowPreview(message),
|
|
4451
4639
|
...(item.ref?.entryId ? { index } : {}),
|
|
4452
4640
|
});
|
|
4453
4641
|
}
|
|
@@ -4463,6 +4651,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4463
4651
|
freedTokens: accounting.freedTokens,
|
|
4464
4652
|
},
|
|
4465
4653
|
pinned: state.protected.map((ref) => ref.entryId),
|
|
4654
|
+
// THE FOLDS THE AGENT HAS STANDING RAW IN ITS WINDOW, so the editor can say which
|
|
4655
|
+
// rows are placeholders and which are already costing the projection.
|
|
4656
|
+
expandedInContext: [...(state.expanded ?? [])],
|
|
4466
4657
|
};
|
|
4467
4658
|
};
|
|
4468
4659
|
|
|
@@ -4477,6 +4668,35 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4477
4668
|
};
|
|
4478
4669
|
const editorActions = {
|
|
4479
4670
|
spanCost,
|
|
4671
|
+
entryDetail,
|
|
4672
|
+
// THE READER'S EXPANSION AND THE WINDOW'S ARE DIFFERENT ACTS, and this is the second
|
|
4673
|
+
// one: it restores the fold's raw bytes to the model's projection and takes the
|
|
4674
|
+
// lease that reclaims them at the next commit. The view's own open/close key never
|
|
4675
|
+
// reaches here.
|
|
4676
|
+
onSetExpanded: async (foldId: string, expand: boolean): Promise<void> => {
|
|
4677
|
+
await runQueued(async () => {
|
|
4678
|
+
await setFoldExpansion(authoritativeSnapshotFor(ctx), foldId, expand, ctx);
|
|
4679
|
+
});
|
|
4680
|
+
safeNotify(ctx, expand
|
|
4681
|
+
? "Expanded in the model's window: its exact bytes are back in the projection, " +
|
|
4682
|
+
"reclaimed at the next commit unless pinned."
|
|
4683
|
+
: "Refolded: the placeholder stands where the bytes were, still peek-recoverable.",
|
|
4684
|
+
"info");
|
|
4685
|
+
refreshAfter();
|
|
4686
|
+
},
|
|
4687
|
+
// RE-CUT IS DISSOLVE PLUS MARK, and the editor already has the second half: `m` on
|
|
4688
|
+
// two raw entries stages the span the person meant. This is the first half, through
|
|
4689
|
+
// the tool's own validated path.
|
|
4690
|
+
onDissolveFold: async (foldId: string): Promise<void> => {
|
|
4691
|
+
let released: string[] = [];
|
|
4692
|
+
await runQueued(async () => {
|
|
4693
|
+
released = (await dissolveFold(authoritativeSnapshotFor(ctx), foldId, ctx)).children;
|
|
4694
|
+
});
|
|
4695
|
+
safeNotify(ctx,
|
|
4696
|
+
`The span is raw again${released.length ? `, releasing ${released.length} nested fold(s)` : ""}; ` +
|
|
4697
|
+
"no evidence moved. Mark the boundary you meant with m.", "info");
|
|
4698
|
+
refreshAfter();
|
|
4699
|
+
},
|
|
4480
4700
|
onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
|
|
4481
4701
|
await runQueued(async () => {
|
|
4482
4702
|
const fresh = authoritativeSnapshotFor(ctx);
|
|
@@ -4501,11 +4721,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4501
4721
|
`Withdrew ${removed.length} staged mark(s); no context bytes moved.`, "info");
|
|
4502
4722
|
refreshAfter();
|
|
4503
4723
|
},
|
|
4504
|
-
onTogglePin: async (
|
|
4505
|
-
|
|
4506
|
-
|
|
4724
|
+
onTogglePin: async (id: string): Promise<void> => {
|
|
4725
|
+
// A TOGGLE HAS TO ASK THE SAME QUESTION THE PIN PATH ANSWERS (2026-08-30). This
|
|
4726
|
+
// read `protected.some((ref) => ref.entryId === id)`, which is only ever true for
|
|
4727
|
+
// an ENTRY id, and `protectEvidence` has always taken a FOLD id too, holding
|
|
4728
|
+
// every ref underneath it. So the moment the editor started offering p on a fold
|
|
4729
|
+
// row, the second press pinned the same refs again instead of releasing them: the
|
|
4730
|
+
// question was asked about an id that is never in the set being searched.
|
|
4731
|
+
const state = persistence.state!;
|
|
4732
|
+
const fold = state.folds.find((item: any) => item.id === id);
|
|
4733
|
+
const held = new Set(state.protected.map((ref: any) => objectRefKey(ref)));
|
|
4734
|
+
const refs = fold
|
|
4735
|
+
? flattenFoldRefs(fold, state)
|
|
4736
|
+
: state.protected.filter((ref: any) => ref.entryId === id);
|
|
4737
|
+
const pinnedAlready = fold
|
|
4738
|
+
? refs.length > 0 && refs.every((ref: any) => held.has(objectRefKey(ref)))
|
|
4739
|
+
: refs.length > 0;
|
|
4507
4740
|
await runQueued(async () => {
|
|
4508
|
-
await applyProtectionChange(authoritativeSnapshotFor(ctx), [
|
|
4741
|
+
await applyProtectionChange(authoritativeSnapshotFor(ctx), [id], !pinnedAlready, ctx);
|
|
4509
4742
|
});
|
|
4510
4743
|
safeNotify(ctx, pinnedAlready
|
|
4511
4744
|
? "Unpinned: the released entries rejoin the next commit's candidates."
|