pi-fold 4.0.0 → 4.1.1
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 +22 -10
- package/extensions/active-context.ts +585 -134
- package/extensions/index.js +7 -2
- package/extensions/lib/brief-text.ts +116 -0
- package/extensions/lib/canonical.ts +3 -18
- package/extensions/lib/curation.ts +78 -39
- package/extensions/lib/editor-ui.ts +468 -47
- 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/persistence.ts +37 -3
- package/extensions/lib/policy.ts +55 -12
- package/extensions/lib/selection.ts +9 -44
- package/extensions/lib/status-widget.ts +175 -0
- package/extensions/lib/tool-surface.ts +0 -1
- package/extensions/lib/transcript.ts +43 -0
- package/extensions/settings.ts +104 -30
- package/package.json +5 -5
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
stringIds,
|
|
62
62
|
toolPayload,
|
|
63
63
|
} from "./lib/measurement.ts";
|
|
64
|
+
import { renderFoldBar, type FoldBarModel } from "./lib/status-widget.ts";
|
|
64
65
|
import type {
|
|
65
66
|
NativeCompactionCompletionReceipt,
|
|
66
67
|
NativeCompactionDecisionReceipt,
|
|
@@ -92,7 +93,9 @@ import {
|
|
|
92
93
|
activeContextSource,
|
|
93
94
|
ACTIVE_CONTEXT_POLICY,
|
|
94
95
|
ACTIVE_CONTEXT_TOOL_ACTIONS,
|
|
95
|
-
|
|
96
|
+
resolveNoticeLeadShare,
|
|
97
|
+
EDITOR_DETAIL_PAGE_CHARS,
|
|
98
|
+
EDITOR_ROW_PREVIEW_CHARS,
|
|
96
99
|
contextBrand,
|
|
97
100
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
98
101
|
DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES,
|
|
@@ -129,7 +132,6 @@ import type {
|
|
|
129
132
|
FoldKind,
|
|
130
133
|
FoldRecordEntry,
|
|
131
134
|
PreparedFold,
|
|
132
|
-
WorkingMemoryEntry,
|
|
133
135
|
} from "./lib/policy.ts";
|
|
134
136
|
import {
|
|
135
137
|
absorbWedgeMarks,
|
|
@@ -172,6 +174,7 @@ import {
|
|
|
172
174
|
} from "./lib/scheduling.ts";
|
|
173
175
|
import {
|
|
174
176
|
manualFoldCandidate,
|
|
177
|
+
oneLine,
|
|
175
178
|
peekedSourceFoldIds,
|
|
176
179
|
snapFoldCandidate,
|
|
177
180
|
snapToFoldBoundaries,
|
|
@@ -195,7 +198,9 @@ import type {
|
|
|
195
198
|
} from "./lib/instrumentation.ts";
|
|
196
199
|
import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
|
|
197
200
|
import { buildFoldEditorData, FoldEditorView } from "./lib/editor-ui.ts";
|
|
201
|
+
import { publishLiveSettings } from "./lib/live-settings.ts";
|
|
198
202
|
import {
|
|
203
|
+
entryText,
|
|
199
204
|
mapActiveContext,
|
|
200
205
|
} from "./lib/transcript.ts";
|
|
201
206
|
import {
|
|
@@ -207,12 +212,13 @@ import {
|
|
|
207
212
|
unansweredToolCalls,
|
|
208
213
|
} from "./lib/rollback.ts";
|
|
209
214
|
|
|
210
|
-
export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured";
|
|
215
|
+
export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured" | "refused";
|
|
211
216
|
|
|
212
217
|
export * from "./lib/canonical.ts";
|
|
213
218
|
export * from "./lib/curation.ts";
|
|
214
219
|
export * from "./lib/folding.ts";
|
|
215
220
|
export * from "./lib/instrumentation.ts";
|
|
221
|
+
export * from "./lib/live-settings.ts";
|
|
216
222
|
export * from "./lib/measurement.ts";
|
|
217
223
|
export * from "./lib/persistence.ts";
|
|
218
224
|
export * from "./lib/policy.ts";
|
|
@@ -221,6 +227,47 @@ export * from "./lib/scheduling.ts";
|
|
|
221
227
|
export * from "./lib/selection.ts";
|
|
222
228
|
export * from "./lib/transcript.ts";
|
|
223
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The two independent scalars, resolved in ONE place because they are resolved TWICE:
|
|
232
|
+
* once from the registration options and once from every /fold-settings edit that
|
|
233
|
+
* reaches the running session. Absent means the package default in both directions,
|
|
234
|
+
* which is the settings file's own law, and the refusal text is the same sentence
|
|
235
|
+
* wherever the bad value came from.
|
|
236
|
+
*/
|
|
237
|
+
/**
|
|
238
|
+
* ONE ENTRY AS ONE ROW: whitespace collapsed, bounded, and the cut STATED.
|
|
239
|
+
*
|
|
240
|
+
* Shared by the three places /fold-editor builds an entry row, which each carried their
|
|
241
|
+
* own inline reading of `content` until 2026-08-30 and each got it wrong the same way:
|
|
242
|
+
* `content.find((part) => part.type === "text")` sliced to 300 characters, so a message
|
|
243
|
+
* whose first block was not text rendered as an empty row and everything past the first
|
|
244
|
+
* block was unreachable at any depth. `entryText` accounts for every block and
|
|
245
|
+
* `entryDetail` serves the rest; this is the summary that sits on the row.
|
|
246
|
+
*/
|
|
247
|
+
function rowPreview(message: unknown): string {
|
|
248
|
+
return oneLine(entryText(message), EDITOR_ROW_PREVIEW_CHARS);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolvedPreCommitNotice(value: unknown): boolean {
|
|
252
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
253
|
+
throw new Error("preCommitNotice must be a boolean: true states the window's status to " +
|
|
254
|
+
"the agent once per approach to a commit, false ships every fold with the runtime's " +
|
|
255
|
+
"deterministic brief and puts no carrier in the projection at all");
|
|
256
|
+
}
|
|
257
|
+
return value ?? false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function resolvedToolFoldThreshold(value: unknown): number {
|
|
261
|
+
const resolved = value ?? DEFAULT_TOOL_FOLD_THRESHOLD;
|
|
262
|
+
if (typeof resolved !== "number" || !Number.isFinite(resolved) ||
|
|
263
|
+
resolved < 0 || resolved >= 1) {
|
|
264
|
+
throw new Error("toolFoldThreshold must be a share in [0, 1): it names the oldest " +
|
|
265
|
+
"fraction of the projected window whose tool results are clipped in view at each " +
|
|
266
|
+
"commit, and 0 turns the diet off");
|
|
267
|
+
}
|
|
268
|
+
return resolved;
|
|
269
|
+
}
|
|
270
|
+
|
|
224
271
|
export function registerActiveContext(pi: any, options: {
|
|
225
272
|
toolName?: string;
|
|
226
273
|
toolLabel?: string;
|
|
@@ -237,14 +284,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
237
284
|
* [0, 1), where 0 turns the diet off; 0.50 is what every fold repetition from 3 onward
|
|
238
285
|
* ran, so the default is the value that was measured rather than the absence of one. */
|
|
239
286
|
toolFoldThreshold?: number;
|
|
240
|
-
/** THE
|
|
241
|
-
* Shane, on the sol-20260826-full2 verdict
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
287
|
+
/** THE NOTICE SWITCH (public since 2026-08-27 as postFoldNotice; DEFAULT FALSE since
|
|
288
|
+
* 2026-08-28, Shane, on the sol-20260826-full2 verdict; RESHAPED from a brief
|
|
289
|
+
* solicitation into a status statement on 2026-08-30). false is the deterministic
|
|
290
|
+
* shape the campaign measured: no carrier reaches the projection at all and every fold
|
|
291
|
+
* goes out with the runtime's own words, which drew 14, 14, 9 and 8 correct against
|
|
292
|
+
* the soliciting condition's 3. true states the window's status once per approach to a
|
|
293
|
+
* commit: where the commit fires, how far away it is, what is staged, what that frees,
|
|
294
|
+
* what is pinned and what the pin costs, with the verbs listed and no argument made
|
|
295
|
+
* for any of them. Either way the agent verbs stay on the tool. */
|
|
296
|
+
preCommitNotice?: boolean;
|
|
297
|
+
/** HOW FAR BEFORE THE COMMIT THE NOTICE SPEAKS (2026-08-30, Shane), as a share of the
|
|
298
|
+
* serving budget below maxTarget; default DEFAULT_NOTICE_LEAD_SHARE 0.10. A DISTANCE
|
|
299
|
+
* rather than a point, so it tracks a band that moves. Read only when preCommitNotice
|
|
300
|
+
* is true. */
|
|
301
|
+
noticeLeadShare?: number;
|
|
248
302
|
}): {
|
|
249
303
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
250
304
|
} {
|
|
@@ -253,18 +307,27 @@ export function registerActiveContext(pi: any, options: {
|
|
|
253
307
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
254
308
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
255
309
|
const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
310
|
+
// THESE THREE ARE THE SETTINGS SURFACE, AND THEY ARE NOT CONSTANTS (2026-08-29). Each
|
|
311
|
+
// was resolved once here and read from the closure ever after, so a person editing
|
|
312
|
+
// /fold-settings changed the file and nothing else while the session kept the values
|
|
313
|
+
// pi booted with. They are `let` so the live applier published below can replace them,
|
|
314
|
+
// and every reader takes the variable rather than a copy, so a swap reaches the next
|
|
315
|
+
// snapshot, the next commit decision and the status line without anything else moving.
|
|
316
|
+
let preCommitNotice = resolvedPreCommitNotice(options.preCommitNotice);
|
|
317
|
+
let noticeLeadShare = resolveNoticeLeadShare(options.noticeLeadShare);
|
|
318
|
+
let toolFoldThreshold = resolvedToolFoldThreshold(options.toolFoldThreshold);
|
|
319
|
+
// THE RENAMED OPTION IS REFUSED BY NAME, not ignored (2026-08-30). postFoldNotice was
|
|
320
|
+
// public for three days and named a carrier that no longer exists in that shape: it
|
|
321
|
+
// fired on a fold count, after the cut, and asked for briefs. Silently accepting the
|
|
322
|
+
// old name would run the new mechanism under a description of the old one, and
|
|
323
|
+
// silently DROPPING it would turn a deployment that deliberately chose the invitation
|
|
324
|
+
// into a silent one without saying so.
|
|
325
|
+
if (Object.hasOwn(options, "postFoldNotice")) {
|
|
326
|
+
throw new Error("postFoldNotice is now preCommitNotice, and the carrier it switches has " +
|
|
327
|
+
"changed: it fires on occupancy approaching the commit rather than on a count of " +
|
|
328
|
+
"unbriefed folds, and it STATES the window's status rather than asking for briefs. " +
|
|
329
|
+
"The default is still false. noticeLeadShare sets how far ahead of the commit it " +
|
|
330
|
+
"speaks");
|
|
268
331
|
}
|
|
269
332
|
for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
|
|
270
333
|
if (Object.hasOwn(options, removed)) {
|
|
@@ -324,7 +387,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
324
387
|
commandNames.status === commandNames.fold) {
|
|
325
388
|
throw new Error("Active-context command names must be distinct kebab-case strings");
|
|
326
389
|
}
|
|
327
|
-
|
|
390
|
+
let thresholds = resolveThresholds(options.thresholds);
|
|
328
391
|
assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
329
392
|
const stateEntryType = `${entryTypePrefix}-state`;
|
|
330
393
|
const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
|
|
@@ -365,6 +428,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
365
428
|
bandTopMeasurement: null as unknown,
|
|
366
429
|
/** The snapshot-and-state key whose frontier scan came back empty; see advanceFoldFrontier. */
|
|
367
430
|
frontierEmptyKey: null as string | null,
|
|
431
|
+
/** The commit count the pre-commit notice last spoke at; null while it is re-armed.
|
|
432
|
+
* One notice per approach, and a landed commit begins the next approach. */
|
|
433
|
+
noticeLatch: null as number | null,
|
|
368
434
|
pendingManual: false,
|
|
369
435
|
preparing: null as { id: string; controller: AbortController; promise: Promise<void> } | null,
|
|
370
436
|
lastPreparationError: null as string | null,
|
|
@@ -402,6 +468,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
402
468
|
sessionId: string;
|
|
403
469
|
generation: number;
|
|
404
470
|
} | null,
|
|
471
|
+
// A REFUSED ANCHOR IS NOT AN ABSENT ONE (2026-08-30). The plausibility floor below
|
|
472
|
+
// rejects a provider count the projected TEXT cannot explain, which is the earliest
|
|
473
|
+
// and cheapest evidence that what we project is not what reaches the wire. It set
|
|
474
|
+
// `projectionAnchor = null` and said nothing, and the reading then called itself
|
|
475
|
+
// "unmeasured", the same word a session uses before its first provider response.
|
|
476
|
+
// Those are opposite states: one has no number yet, the other has one it disbelieves.
|
|
477
|
+
anchorRefusal: null as { consecutive: number } | null,
|
|
405
478
|
estimatorErrors: [] as number[],
|
|
406
479
|
inflowSteps: [] as number[],
|
|
407
480
|
wallInflowSteps: [] as number[],
|
|
@@ -465,6 +538,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
465
538
|
const curation = {
|
|
466
539
|
receipts: [] as ContextReceipt[],
|
|
467
540
|
contextCalls: 0,
|
|
541
|
+
/** Receipts delivered this session, monotonic; the pre-commit notice's re-arm signal. */
|
|
542
|
+
commitsDelivered: 0,
|
|
468
543
|
wallEpisodeOpen: false,
|
|
469
544
|
recoveryAttempts: 0,
|
|
470
545
|
pendingRejection: null as { status: number; ordinal: number } | null,
|
|
@@ -482,9 +557,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
482
557
|
pendingOverflow: null as { at: number; entryId: string | null } | null,
|
|
483
558
|
};
|
|
484
559
|
|
|
485
|
-
const advisory = {
|
|
486
|
-
};
|
|
487
|
-
|
|
488
560
|
// EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
|
|
489
561
|
// visibility. In-memory ON PURPOSE: a restart forgets the registry and the
|
|
490
562
|
// result stays durable, which is the safe default, and the durable entry
|
|
@@ -595,6 +667,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
595
667
|
|
|
596
668
|
const deliverReceipt = (receipt: ContextReceipt): void => {
|
|
597
669
|
curation.receipts = withReceipt(curation.receipts, receipt);
|
|
670
|
+
// MONOTONIC, unlike `curation.receipts.length`, which saturates at MAX_CONTEXT_RECEIPTS
|
|
671
|
+
// and would stop distinguishing one approach from the next once the session is long
|
|
672
|
+
// enough to matter. This is the only counter the notice latch reads.
|
|
673
|
+
curation.commitsDelivered += 1;
|
|
598
674
|
emit("context.receipt", {
|
|
599
675
|
receipt_kind: receipt.kind,
|
|
600
676
|
trigger: receipt.trigger,
|
|
@@ -631,6 +707,63 @@ export function registerActiveContext(pi: any, options: {
|
|
|
631
707
|
* leads now, occupancy is a percentage of the serving budget, and the commit point is
|
|
632
708
|
* named only when it is close enough to matter.
|
|
633
709
|
*/
|
|
710
|
+
/**
|
|
711
|
+
* THE FOLD BAR (2026-09-02, dogfooding). One widget row directly above the footer,
|
|
712
|
+
* drawn by `renderFoldBar` from a model this function rebuilds on every status update.
|
|
713
|
+
* The component reads the model and the LIVE theme at render time, so it repaints on
|
|
714
|
+
* a theme switch, which the status string cannot; the host is asked to render after
|
|
715
|
+
* each rebuild. Installed once per host UI, on the first update that finds one.
|
|
716
|
+
*/
|
|
717
|
+
const foldBar = {
|
|
718
|
+
model: null as FoldBarModel | null,
|
|
719
|
+
installed: false,
|
|
720
|
+
requestRender: null as (() => void) | null,
|
|
721
|
+
};
|
|
722
|
+
const installFoldBar = (ctx: any): void => {
|
|
723
|
+
if (foldBar.installed || typeof ctx.ui?.setWidget !== "function") return;
|
|
724
|
+
foldBar.installed = true;
|
|
725
|
+
ctx.ui.setWidget(entryTypePrefix, (tui: any) => {
|
|
726
|
+
foldBar.requestRender = () => { try { tui.requestRender(); } catch { } };
|
|
727
|
+
return {
|
|
728
|
+
render(width: number): string[] {
|
|
729
|
+
if (!foldBar.model) return [];
|
|
730
|
+
try { return [renderFoldBar(foldBar.model, width, ctx.ui.theme)]; }
|
|
731
|
+
catch { return []; }
|
|
732
|
+
},
|
|
733
|
+
invalidate() { },
|
|
734
|
+
};
|
|
735
|
+
}, { placement: "belowEditor" });
|
|
736
|
+
};
|
|
737
|
+
const foldBarModel = (input: {
|
|
738
|
+
share: number | null; budgetTokens: number; staged: number; roots: number; weighed: boolean;
|
|
739
|
+
}): FoldBarModel => {
|
|
740
|
+
const state = persistence.state;
|
|
741
|
+
const snapshot = lifecycle.latestSnapshot;
|
|
742
|
+
let stagedTokens = 0;
|
|
743
|
+
let placeholderChars = 0;
|
|
744
|
+
let hiddenChars = 0;
|
|
745
|
+
if (state && snapshot) {
|
|
746
|
+
for (const mark of pendingMarks(state)) stagedTokens += estimatedTokens(markFreedBytes(snapshot, state, mark));
|
|
747
|
+
for (const root of orderedRoots(state, snapshot)) placeholderChars += root.fold.placeholderChars;
|
|
748
|
+
for (const fold of state.folds) hiddenChars += Math.max(0, fold.sourceChars - fold.placeholderChars);
|
|
749
|
+
}
|
|
750
|
+
const budget = input.budgetTokens > 0 ? input.budgetTokens : 1;
|
|
751
|
+
return {
|
|
752
|
+
brand: brandNoun,
|
|
753
|
+
share: input.share,
|
|
754
|
+
commitShare: thresholds.maxTarget,
|
|
755
|
+
aimShare: thresholds.minTarget,
|
|
756
|
+
stagedShare: stagedTokens / budget,
|
|
757
|
+
stagedMarks: input.staged,
|
|
758
|
+
stagedTokens,
|
|
759
|
+
foldedShare: estimatedTokens(placeholderChars) / budget,
|
|
760
|
+
folds: input.roots,
|
|
761
|
+
hiddenTokens: estimatedTokens(hiddenChars),
|
|
762
|
+
weighed: input.weighed,
|
|
763
|
+
stopped: ladder.automaticFailure?.message ?? null,
|
|
764
|
+
};
|
|
765
|
+
};
|
|
766
|
+
|
|
634
767
|
const updateStatus = (ctx: any): void => {
|
|
635
768
|
try {
|
|
636
769
|
const roots = persistence.state && lifecycle.latestSnapshot ? orderedRoots(persistence.state, lifecycle.latestSnapshot).length : 0;
|
|
@@ -638,6 +771,17 @@ export function registerActiveContext(pi: any, options: {
|
|
|
638
771
|
const capacity = currentCapacity(ctx);
|
|
639
772
|
const used = measurements.lastProviderMeasurement?.tokens ?? null;
|
|
640
773
|
const share = used !== null && capacity.budgetTokens > 0 ? used / capacity.budgetTokens : null;
|
|
774
|
+
try {
|
|
775
|
+
installFoldBar(ctx);
|
|
776
|
+
if (foldBar.installed) {
|
|
777
|
+
foldBar.model = foldBarModel({
|
|
778
|
+
share, budgetTokens: capacity.budgetTokens, staged, roots,
|
|
779
|
+
weighed: ladder.bandTopMeasurement !== null &&
|
|
780
|
+
ladder.bandTopMeasurement === measurements.lastProviderMeasurement,
|
|
781
|
+
});
|
|
782
|
+
foldBar.requestRender?.();
|
|
783
|
+
}
|
|
784
|
+
} catch { }
|
|
641
785
|
const parts: string[] = [];
|
|
642
786
|
// FAILURE LEADS. This line is cut to whatever width the host gives it, so the last
|
|
643
787
|
// segment is the first one a narrow terminal removes, and folding having stopped is
|
|
@@ -646,21 +790,103 @@ export function registerActiveContext(pi: any, options: {
|
|
|
646
790
|
// Unmeasured is stated, never guessed: an estimate rendered as a fact is the habit
|
|
647
791
|
// that cost a live session, and this line is the most-read surface there is.
|
|
648
792
|
parts.push(share === null ? "not measured" : `${Math.round(share * 100)}% full`);
|
|
649
|
-
|
|
793
|
+
// THE LIVE BAND, NOT THE LAST SNAPSHOT'S (2026-08-29). This read the threshold off
|
|
794
|
+
// `lifecycle.latestSnapshot`, which is derived per context event, so a band changed
|
|
795
|
+
// in /fold-settings went on being announced at its old value until the next event
|
|
796
|
+
// rebuilt a snapshot. The number this line owes a person is the one that will fire,
|
|
797
|
+
// and that is the runtime's own variable.
|
|
798
|
+
const trigger = thresholds.maxTarget;
|
|
650
799
|
// WHEN, not just how full. "68% full" only answers the second question a reader has
|
|
651
800
|
// if they already remember where the trigger sits; naming it answers both.
|
|
652
801
|
//
|
|
653
802
|
// NOT WHILE STOPPED, though: the line rendered "COMMIT DUE" and "FOLDING STOPPED"
|
|
654
803
|
// in one breath, promising the thing it was announcing would not happen.
|
|
655
|
-
|
|
656
|
-
|
|
804
|
+
// DUE MEANS ABOUT TO HAPPEN, NOT MERELY OVER THE LINE (2026-08-30). This read
|
|
805
|
+
// `share >= trigger`, which is true of the BAND and says nothing about the
|
|
806
|
+
// DECISION. A band-top commit runs ONCE per provider count, so once it has run
|
|
807
|
+
// against the current one, nothing further will happen until a new count arrives,
|
|
808
|
+
// whatever it applied. Live session 01a052b1 sat on `COMMIT DUE` through 48
|
|
809
|
+
// consecutive evaluations that all declined for a recorded reason, and a person
|
|
810
|
+
// reasonably read a line promising an imminent commit as a runtime that had hung.
|
|
811
|
+
//
|
|
812
|
+
// The latch is the whole test and no new state is kept: it already holds the
|
|
813
|
+
// measurement the last band-top commit ran against, for the cache reason documented
|
|
814
|
+
// at its own site, and "a commit has already been weighed against this count" is
|
|
815
|
+
// exactly what a reader needs. Held is lower case because it is not urgent; DUE
|
|
816
|
+
// keeps its shout because it is the one that is about to cost a rewrite.
|
|
817
|
+
if (share !== null && !ladder.automaticFailure) {
|
|
818
|
+
const weighed = ladder.bandTopMeasurement !== null &&
|
|
819
|
+
ladder.bandTopMeasurement === measurements.lastProviderMeasurement;
|
|
820
|
+
parts.push(share < trigger
|
|
821
|
+
? `commit at ${Math.round(trigger * 100)}%`
|
|
822
|
+
: weighed ? "commit held" : "COMMIT DUE");
|
|
657
823
|
}
|
|
658
824
|
if (staged > 0) parts.push(`${staged} staged`);
|
|
659
825
|
if (roots > 0) parts.push(`${roots} fold${roots === 1 ? "" : "s"}`);
|
|
660
|
-
|
|
826
|
+
// ONE SURFACE, NOT TWO (Shane 2026-09-02, from a screenshot showing both). Where
|
|
827
|
+
// the fold bar widget is installed it carries every fact this string carries, so
|
|
828
|
+
// the string is withdrawn; a host without widgets keeps the string exactly as it was.
|
|
829
|
+
ctx.ui?.setStatus?.(entryTypePrefix, foldBar.installed ? undefined : `${brandNoun} ${parts.join(" · ")}`);
|
|
661
830
|
} catch { }
|
|
662
831
|
};
|
|
663
832
|
|
|
833
|
+
/**
|
|
834
|
+
* WHAT A PERSON SAVES IN /fold-settings REACHES THIS SESSION (2026-08-29).
|
|
835
|
+
*
|
|
836
|
+
* The screen writes the file and then hands the whole file here, so an absent field
|
|
837
|
+
* means the package default exactly as it does on disk and at registration; all four
|
|
838
|
+
* values are resolved through the same functions the options went through, which is
|
|
839
|
+
* what stops a value meaning one thing at boot and another at an edit.
|
|
840
|
+
*
|
|
841
|
+
* ATOMIC: every value is validated before any is assigned, so a refusal leaves the
|
|
842
|
+
* session on the settings it already had rather than half-moved. It throws the way
|
|
843
|
+
* registration throws, and the caller decides what that means; the settings screen
|
|
844
|
+
* catches it, because a screen may not take the session down with it.
|
|
845
|
+
*
|
|
846
|
+
* The memo is dropped because `authoritativeSnapshotFor` keys on session, entries and
|
|
847
|
+
* messages, none of which move when a threshold does, so a cached snapshot would serve
|
|
848
|
+
* the old band to the next reader that asked.
|
|
849
|
+
*/
|
|
850
|
+
publishLiveSettings(pi, (settings, ctx) => {
|
|
851
|
+
const nextThresholds = resolveThresholds(settings?.thresholds);
|
|
852
|
+
assertThresholdsServable(nextThresholds,
|
|
853
|
+
providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
854
|
+
const nextToolFoldThreshold = resolvedToolFoldThreshold(settings?.toolFoldThreshold);
|
|
855
|
+
const nextPreCommitNotice = resolvedPreCommitNotice(settings?.preCommitNotice);
|
|
856
|
+
const nextNoticeLeadShare = resolveNoticeLeadShare(settings?.noticeLeadShare);
|
|
857
|
+
const changed: string[] = [];
|
|
858
|
+
for (const field of ["maxTarget", "minTarget", "consolidateAfter", "minFoldChars"] as const) {
|
|
859
|
+
if (nextThresholds[field] !== thresholds[field]) changed.push(field);
|
|
860
|
+
}
|
|
861
|
+
if (nextToolFoldThreshold !== toolFoldThreshold) changed.push("toolFoldThreshold");
|
|
862
|
+
if (nextPreCommitNotice !== preCommitNotice) changed.push("preCommitNotice");
|
|
863
|
+
if (nextNoticeLeadShare !== noticeLeadShare) changed.push("noticeLeadShare");
|
|
864
|
+
// A SAVE THAT MOVED NOTHING IS NOT AN EVENT. The screen saves every keystroke that
|
|
865
|
+
// lands, including a row stepped back to where it started.
|
|
866
|
+
if (!changed.length) return;
|
|
867
|
+
thresholds = nextThresholds;
|
|
868
|
+
toolFoldThreshold = nextToolFoldThreshold;
|
|
869
|
+
preCommitNotice = nextPreCommitNotice;
|
|
870
|
+
noticeLeadShare = nextNoticeLeadShare;
|
|
871
|
+
dropDerivationMemo();
|
|
872
|
+
// THE STREAM CARRIES IT, because from here on the session's commits fire at a point
|
|
873
|
+
// no earlier record explains, and an archive reading this run later has no other way
|
|
874
|
+
// to know the band moved under it.
|
|
875
|
+
emit("context.settings", {
|
|
876
|
+
changed,
|
|
877
|
+
max_target: thresholds.maxTarget,
|
|
878
|
+
min_target: thresholds.minTarget,
|
|
879
|
+
consolidate_after: thresholds.consolidateAfter,
|
|
880
|
+
min_fold_chars: thresholds.minFoldChars,
|
|
881
|
+
tool_fold_threshold: toolFoldThreshold,
|
|
882
|
+
pre_commit_notice: preCommitNotice,
|
|
883
|
+
notice_lead_share: noticeLeadShare,
|
|
884
|
+
});
|
|
885
|
+
// The person is standing in the settings screen, so the line they will look at next
|
|
886
|
+
// is the status line. Without this it keeps the old number until a context event.
|
|
887
|
+
if (ctx) { try { updateStatus(ctx); } catch { } }
|
|
888
|
+
});
|
|
889
|
+
|
|
664
890
|
const budgetWindowFor = (ctx: any): number | null => {
|
|
665
891
|
measurements.descriptorWindow = contextWindowFor(ctx);
|
|
666
892
|
return providerInputBudget ?? measurements.descriptorWindow;
|
|
@@ -784,6 +1010,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
784
1010
|
measurements.lastProviderMeasurement = null;
|
|
785
1011
|
measurements.wallInflowSteps.length = 0;
|
|
786
1012
|
measurements.projectionAnchor = null;
|
|
1013
|
+
// A RESET IS NOT A RESTORATION. The refusal is dropped without a record because the
|
|
1014
|
+
// session it described is gone; emitting "restored" here would report a recovery that
|
|
1015
|
+
// never happened.
|
|
1016
|
+
measurements.anchorRefusal = null;
|
|
787
1017
|
measurements.lastProjectedEstimateBasis = "unmeasured";
|
|
788
1018
|
ladder.pendingManual = false;
|
|
789
1019
|
if (!preserveThresholdDecision) nativeCompaction.lastThresholdDecision = null;
|
|
@@ -1332,7 +1562,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1332
1562
|
safeNotify(ctx, `Automatic context management suspended: ${message}`, "warning");
|
|
1333
1563
|
};
|
|
1334
1564
|
|
|
1335
|
-
|
|
1336
1565
|
const PROJECTION_CALIBRATION_MIN_CHARS = 20_000;
|
|
1337
1566
|
const PROJECTION_CALIBRATION_MIN_TOKENS = 5_000;
|
|
1338
1567
|
const PROJECTION_CHARS_PER_TOKEN_FLOOR = 2;
|
|
@@ -1383,6 +1612,41 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1383
1612
|
);
|
|
1384
1613
|
};
|
|
1385
1614
|
|
|
1615
|
+
/**
|
|
1616
|
+
* THE REFUSAL SPEAKS ONCE PER EPISODE, NOT ONCE PER PASS (2026-08-30).
|
|
1617
|
+
*
|
|
1618
|
+
* Every request in a divergent stretch refuses the anchor for the same reason, and a
|
|
1619
|
+
* record per pass would be 48 identical lines in the one session that found this. The
|
|
1620
|
+
* edge is what carries information: entering the state, and leaving it with a count of
|
|
1621
|
+
* what it cost. Same shape as `context.suspend`, which repeats only when its message
|
|
1622
|
+
* changes rather than once per pass.
|
|
1623
|
+
*/
|
|
1624
|
+
const noteAnchorRefusal = (
|
|
1625
|
+
measurement: ProviderContextMeasurement,
|
|
1626
|
+
textChars: number,
|
|
1627
|
+
images: number,
|
|
1628
|
+
): void => {
|
|
1629
|
+
const previous = measurements.anchorRefusal;
|
|
1630
|
+
measurements.anchorRefusal = { consecutive: (previous?.consecutive ?? 0) + 1 };
|
|
1631
|
+
if (previous) return;
|
|
1632
|
+
emit("context.anchor", {
|
|
1633
|
+
state: "refused",
|
|
1634
|
+
reason: "text-cannot-explain-count",
|
|
1635
|
+
text_chars: textChars,
|
|
1636
|
+
images,
|
|
1637
|
+
provider_tokens: measurement.tokens,
|
|
1638
|
+
chars_per_token: measurement.tokens > 0 ? textChars / measurement.tokens : null,
|
|
1639
|
+
floor: PROJECTION_CHARS_PER_TOKEN_FLOOR,
|
|
1640
|
+
});
|
|
1641
|
+
};
|
|
1642
|
+
|
|
1643
|
+
const clearAnchorRefusal = (): void => {
|
|
1644
|
+
const refusal = measurements.anchorRefusal;
|
|
1645
|
+
measurements.anchorRefusal = null;
|
|
1646
|
+
if (!refusal) return;
|
|
1647
|
+
emit("context.anchor", { state: "restored", refused_requests: refusal.consecutive });
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1386
1650
|
const noteProviderProjectionAnchor = (measurement: ProviderContextMeasurement): void => {
|
|
1387
1651
|
if (measurements.projectionAnchor?.messageSha256 === measurement.messageSha256) return;
|
|
1388
1652
|
const text = instrumentation.previousText;
|
|
@@ -1404,8 +1668,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1404
1668
|
textChars / (measurement.tokens - images * IMAGE_ESTIMATED_TOKENS) <
|
|
1405
1669
|
PROJECTION_CHARS_PER_TOKEN_FLOOR)) {
|
|
1406
1670
|
measurements.projectionAnchor = null;
|
|
1671
|
+
// ONLY A COUNT WE DISBELIEVE IS A REFUSAL. A zero or absent count is simply nothing
|
|
1672
|
+
// to anchor on and stays silent; the floor failing on a positive count is the state
|
|
1673
|
+
// worth announcing, because it means the provider is billing for mass our own
|
|
1674
|
+
// projection does not contain.
|
|
1675
|
+
if (measurement.tokens > 0) noteAnchorRefusal(measurement, textChars, images);
|
|
1407
1676
|
return;
|
|
1408
1677
|
}
|
|
1678
|
+
clearAnchorRefusal();
|
|
1409
1679
|
measurements.projectionAnchor = {
|
|
1410
1680
|
tokens: measurement.tokens,
|
|
1411
1681
|
chars,
|
|
@@ -1451,7 +1721,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1451
1721
|
const imageTokens = mass.images * IMAGE_ESTIMATED_TOKENS;
|
|
1452
1722
|
const estimate = Math.ceil(textChars / charsPerToken) + imageTokens;
|
|
1453
1723
|
const anchor = measurements.projectionAnchor;
|
|
1454
|
-
|
|
1724
|
+
// REFUSED AND UNMEASURED READ THE SAME NUMBER AND MEAN OPPOSITE THINGS. Both fall back
|
|
1725
|
+
// to the character estimate, so the tokens are identical and only the BASIS separates
|
|
1726
|
+
// them: "unmeasured" is a session that has not been told yet, "refused" is one that was
|
|
1727
|
+
// told something its own projection cannot account for. Reading 48 consecutive
|
|
1728
|
+
// "unmeasured" projections as a quiet session is what hid a 19x divergence.
|
|
1729
|
+
if (!anchor) {
|
|
1730
|
+
const basis: ProjectionReadingBasis = measurements.anchorRefusal ? "refused" : "unmeasured";
|
|
1731
|
+
return { tokens: estimate, basis, chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
|
|
1732
|
+
}
|
|
1733
|
+
if (anchor.generation !== lifecycle.generation ||
|
|
1455
1734
|
anchor.sessionId !== persistence.state?.sessionId) {
|
|
1456
1735
|
return { tokens: estimate, basis: "unmeasured", chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
|
|
1457
1736
|
}
|
|
@@ -1535,7 +1814,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1535
1814
|
};
|
|
1536
1815
|
};
|
|
1537
1816
|
|
|
1538
|
-
|
|
1539
1817
|
// THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
|
|
1540
1818
|
//
|
|
1541
1819
|
// `maxTarget` is what the settings panel calls the commit trigger, and until now it
|
|
@@ -1634,9 +1912,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1634
1912
|
};
|
|
1635
1913
|
|
|
1636
1914
|
/**
|
|
1637
|
-
* Tell the agent
|
|
1915
|
+
* Tell the agent where the window stands, once per approach to a commit.
|
|
1638
1916
|
*
|
|
1639
|
-
* It is built from LIVE state, so it names what is
|
|
1917
|
+
* It is built from LIVE state, so it names what is staged at the pass it speaks on,
|
|
1640
1918
|
* and it is admitted ONCE per frozen projection, so a pass that adds material leaves it
|
|
1641
1919
|
* buried at its own index rather than moving it to the tail. Those two together mean it
|
|
1642
1920
|
* is a snapshot rather than a running tally: cuts staged after it spoke wait for the
|
|
@@ -1646,19 +1924,54 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1646
1924
|
* epoch early. What it must never do is ask twice for a brief the agent has written,
|
|
1647
1925
|
* and it does not: the rebuild reads the marks, not its own last text.
|
|
1648
1926
|
*/
|
|
1649
|
-
const appendFoldNotice = (
|
|
1927
|
+
const appendFoldNotice = (
|
|
1928
|
+
projected: unknown[],
|
|
1929
|
+
snapshot: ActiveContextSnapshot,
|
|
1930
|
+
bodyBytes: number,
|
|
1931
|
+
): unknown[] => {
|
|
1650
1932
|
// The deterministic experiment condition: cuts stage and commits land exactly as
|
|
1651
|
-
// ever, but
|
|
1652
|
-
if (!
|
|
1933
|
+
// ever, but no carrier reaches the projection at all.
|
|
1934
|
+
if (!preCommitNotice) return projected;
|
|
1653
1935
|
if (!persistence.state) return projected;
|
|
1654
1936
|
const held = pendingMarks(persistence.state);
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1937
|
+
const pinnedMass = explicitProtectedMass(snapshot, persistence.state);
|
|
1938
|
+
// NOTHING TO REPORT IS NOT AN OCCASION. With no staged fold and no pin there is no
|
|
1939
|
+
// status: the commit that is coming will apply nothing, and a notice saying so is a
|
|
1940
|
+
// turn's attention spent on the absence of news.
|
|
1941
|
+
if (!held.length && !pinnedMass.refs) return projected;
|
|
1942
|
+
const capacity = servingCapacity(snapshot.contextWindow);
|
|
1943
|
+
if (!(capacity.budgetTokens > 0)) return projected;
|
|
1944
|
+
// THE TRIGGER IS OCCUPANCY, AND IT NAMES ITS OWN BASIS. The provider's count is
|
|
1945
|
+
// ground truth; the estimate is what there is before the first response lands, and
|
|
1946
|
+
// this runtime's standing law is that an estimate MAY SCHEDULE WORK and may not veto
|
|
1947
|
+
// a request. Appending a notice is scheduling. `bodyBytes` is the serialization this
|
|
1948
|
+
// pass already performed for the freeze predicate, so reading a share here adds no
|
|
1949
|
+
// second pass over the window (gate 155's law).
|
|
1950
|
+
const measured = capacity.usedTokens !== null;
|
|
1951
|
+
const usedTokens = measured
|
|
1952
|
+
? capacity.usedTokens!
|
|
1953
|
+
: Math.ceil(bodyBytes / projectionCharsPerToken());
|
|
1954
|
+
const share = usedTokens / capacity.budgetTokens;
|
|
1955
|
+
// A DISTANCE BELOW THE TRIGGER, floored at zero so a lead wider than the trigger
|
|
1956
|
+
// means "from the first measurement" rather than a negative point nothing crosses.
|
|
1957
|
+
const firePoint = Math.max(0, thresholds.maxTarget - noticeLeadShare);
|
|
1958
|
+
// EDGE TRIGGERED, ONE NOTICE PER APPROACH. A window sits in the band for many passes,
|
|
1959
|
+
// and the freeze breaks whenever the prefix changes, so `carrierAdmitted` alone would
|
|
1960
|
+
// let this re-admit all the way up the band. Two things re-arm it and nothing else:
|
|
1961
|
+
// falling back below the fire point, and a commit landing, which is the event that
|
|
1962
|
+
// ends one approach and begins the next. Without the second, a commit that lands
|
|
1963
|
+
// above the fire point would leave the latch shut for the rest of the session.
|
|
1964
|
+
if (share < firePoint) {
|
|
1965
|
+
ladder.noticeLatch = null;
|
|
1966
|
+
return projected;
|
|
1967
|
+
}
|
|
1968
|
+
if (ladder.noticeLatch !== null && ladder.noticeLatch === curation.commitsDelivered) {
|
|
1969
|
+
return projected;
|
|
1970
|
+
}
|
|
1971
|
+
// APPENDED ONCE PER FROZEN PROJECTION. `carrierAdmitted` is what makes it an APPEND
|
|
1972
|
+
// rather than a rewrite: once the carrier is in the frozen projection it is not
|
|
1973
|
+
// pushed again, so raw material arriving afterwards lands after it and the prefix in
|
|
1974
|
+
// front of it never moves.
|
|
1662
1975
|
//
|
|
1663
1976
|
// The first cut of this withdrew the carrier and re-pushed it at the tail every pass,
|
|
1664
1977
|
// the way the steward band had to because a band must be able to stand down. That is
|
|
@@ -1667,23 +1980,42 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1667
1980
|
// gates 110 and 111 read the whole projection as rewritten and dropped the occupancy
|
|
1668
1981
|
// anchor back to the estimate. A carrier that moves on a quiet pass costs the cache,
|
|
1669
1982
|
// which is the one thing this design exists to avoid.
|
|
1670
|
-
if (unbriefed.length < UNBRIEFED_FOLDS_BEFORE_NOTICE) return projected;
|
|
1671
1983
|
if (!carrierAdmitted("fold-notice")) return projected;
|
|
1984
|
+
const briefed = (mark: Record<string, unknown>): boolean =>
|
|
1985
|
+
(mark as any).briefProvenance?.kind === "supplied" ||
|
|
1986
|
+
(mark as any).briefProvenance?.kind === "augmented";
|
|
1987
|
+
const staged = held.map((mark) => ({
|
|
1988
|
+
id: mark.id as string,
|
|
1989
|
+
kind: mark.kind as string,
|
|
1990
|
+
tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
|
|
1991
|
+
brief: typeof mark.brief === "string" ? mark.brief : undefined,
|
|
1992
|
+
briefed: briefed(mark),
|
|
1993
|
+
}));
|
|
1994
|
+
// THE SHARE IS MEASURED THE WAY THE CAP MEASURES IT. The notice states the pinned
|
|
1995
|
+
// share against MAX_PINNED_SHARE, and `applyProtectionChange` refuses past that cap
|
|
1996
|
+
// on `explicitProtectedMass`, so quoting the stale-only figure `windowClaims` reports
|
|
1997
|
+
// would put a number beside a cap it is not compared against.
|
|
1672
1998
|
const text = foldNoticeText({
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1999
|
+
staged,
|
|
2000
|
+
freedTokens: staged.reduce((total, fold) => total + fold.tokens, 0),
|
|
2001
|
+
briefedCount: staged.filter((fold) => fold.briefed).length,
|
|
2002
|
+
pinnedEntries: pinnedMass.refs,
|
|
2003
|
+
pinnedShare: estimatedTokens(pinnedMass.bytes) / capacity.budgetTokens,
|
|
2004
|
+
maxPinnedShare: MAX_PINNED_SHARE,
|
|
2005
|
+
headroomTokens: Math.max(0, thresholds.maxTarget * capacity.budgetTokens - usedTokens),
|
|
2006
|
+
commitAtShare: thresholds.maxTarget,
|
|
2007
|
+
measured,
|
|
1680
2008
|
toolName,
|
|
1681
2009
|
brandNoun,
|
|
1682
2010
|
});
|
|
2011
|
+
ladder.noticeLatch = curation.commitsDelivered;
|
|
1683
2012
|
emit("context.frontier", {
|
|
1684
2013
|
cut: 0,
|
|
1685
2014
|
pending_marks: held.length,
|
|
1686
|
-
|
|
2015
|
+
briefed: staged.filter((fold) => fold.briefed).length,
|
|
2016
|
+
occupancy_share: Number(share.toFixed(4)),
|
|
2017
|
+
fire_point_share: firePoint,
|
|
2018
|
+
measured,
|
|
1687
2019
|
notice_chars: text.length,
|
|
1688
2020
|
});
|
|
1689
2021
|
projected.push({
|
|
@@ -1699,7 +2031,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1699
2031
|
return projected;
|
|
1700
2032
|
};
|
|
1701
2033
|
|
|
1702
|
-
|
|
1703
2034
|
const enforceBandTop = async (
|
|
1704
2035
|
snapshot: ActiveContextSnapshot,
|
|
1705
2036
|
ctx: any,
|
|
@@ -2044,7 +2375,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2044
2375
|
freeze.body = body;
|
|
2045
2376
|
freeze.bodyText = bodyText;
|
|
2046
2377
|
appendReceipts(projected, snapshot);
|
|
2047
|
-
appendFoldNotice(projected, snapshot);
|
|
2378
|
+
appendFoldNotice(projected, snapshot, bodyText.length);
|
|
2048
2379
|
return holdFrozen(projected);
|
|
2049
2380
|
};
|
|
2050
2381
|
const noteProjection = (projected: unknown[], serialized?: string): void => {
|
|
@@ -2737,6 +3068,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2737
3068
|
measurements.lastProviderMeasurement = null;
|
|
2738
3069
|
measurements.wallInflowSteps.length = 0;
|
|
2739
3070
|
measurements.projectionAnchor = null;
|
|
3071
|
+
measurements.anchorRefusal = null;
|
|
2740
3072
|
nativeCompaction.lastThresholdDecision = null;
|
|
2741
3073
|
updateStatus(ctx);
|
|
2742
3074
|
};
|
|
@@ -3337,6 +3669,94 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3337
3669
|
return removed as Array<Record<string, unknown>>;
|
|
3338
3670
|
};
|
|
3339
3671
|
|
|
3672
|
+
/**
|
|
3673
|
+
* THE DISSOLVE PATH, shared by the tool's single-id reboundary and the editor's re-cut
|
|
3674
|
+
* (2026-08-30). Returning a fold to raw is the first half of re-cutting it, and the
|
|
3675
|
+
* editor already has the second half: `m` on two raw entries stages the span the person
|
|
3676
|
+
* meant. Extracted rather than reimplemented, because the editor's own law is that
|
|
3677
|
+
* every edit is a skin over an existing validated path, and this one carries the nested
|
|
3678
|
+
* refusal, the brief and lease cleanup and the parent re-derivation with it.
|
|
3679
|
+
*/
|
|
3680
|
+
const dissolveFold = async (
|
|
3681
|
+
snapshot: ActiveContextSnapshot,
|
|
3682
|
+
id: string,
|
|
3683
|
+
ctx: any,
|
|
3684
|
+
): Promise<{ children: string[]; startId: string | null; endId: string | null }> => {
|
|
3685
|
+
const fold = requireActiveFold(snapshot, persistence.state!, id);
|
|
3686
|
+
if (fold.parentId) {
|
|
3687
|
+
throw new Error(
|
|
3688
|
+
`Fold ${id} is nested under ${fold.parentId}; dissolve ${fold.parentId} first so ${id} is a root`,
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
const refs = flattenFoldRefs(fold, persistence.state!);
|
|
3692
|
+
const children = childFoldIds(fold);
|
|
3693
|
+
const briefs = { ...(persistence.state!.briefs ?? {}) };
|
|
3694
|
+
delete briefs[id];
|
|
3695
|
+
const next: ActiveContextState = {
|
|
3696
|
+
...persistence.state!,
|
|
3697
|
+
revision: persistence.state!.revision + 1,
|
|
3698
|
+
folds: deriveFoldParents(persistence.state!.folds.filter((item) => item.id !== id)),
|
|
3699
|
+
expanded: persistence.state!.expanded.filter((expandedId) => expandedId !== id),
|
|
3700
|
+
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
3701
|
+
};
|
|
3702
|
+
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3703
|
+
const leases = { ...next.leases };
|
|
3704
|
+
delete leases[id];
|
|
3705
|
+
await persistManual({ ...next, leases }, ctx);
|
|
3706
|
+
updateStatus(ctx);
|
|
3707
|
+
return { children, startId: refs[0]?.entryId ?? null, endId: refs.at(-1)?.entryId ?? null };
|
|
3708
|
+
};
|
|
3709
|
+
|
|
3710
|
+
/**
|
|
3711
|
+
* THE EXPANSION PATH, shared by the tool's expand/refold actions and the editor's
|
|
3712
|
+
* promote-to-context intent (2026-08-30).
|
|
3713
|
+
*
|
|
3714
|
+
* THIS IS THE ACT THE EDITOR'S OWN EXPANSION IS NOT. Opening a fold row in /fold-editor
|
|
3715
|
+
* moves a key in a Set the view owns: it reveals the fold to the READER and the model
|
|
3716
|
+
* never learns it happened. This restores the fold's raw bytes to the PROJECTION, which
|
|
3717
|
+
* costs the window and is what the lease exists to reclaim. Shane, 2026-08-30: the user
|
|
3718
|
+
* should "page through/expand for their own viewing purposes" while "setting an expanded
|
|
3719
|
+
* fold is a different choice". Two acts, two paths, and this is the one that spends.
|
|
3720
|
+
*/
|
|
3721
|
+
const setFoldExpansion = async (
|
|
3722
|
+
snapshot: ActiveContextSnapshot,
|
|
3723
|
+
id: string,
|
|
3724
|
+
expand: boolean,
|
|
3725
|
+
ctx: any,
|
|
3726
|
+
): Promise<unknown[]> => {
|
|
3727
|
+
let epochApplied: unknown[] = [];
|
|
3728
|
+
if (expand && pendingMarks(persistence.state!).length) {
|
|
3729
|
+
const result = await commitPendingMarks({
|
|
3730
|
+
snapshot,
|
|
3731
|
+
state: persistence.state!,
|
|
3732
|
+
generation: lifecycle.generation,
|
|
3733
|
+
retainIneligible: true,
|
|
3734
|
+
});
|
|
3735
|
+
persistence.state = result.state;
|
|
3736
|
+
epochApplied = result.applied;
|
|
3737
|
+
}
|
|
3738
|
+
const expanding = requireActiveFold(snapshot, persistence.state!, id);
|
|
3739
|
+
if (expand) {
|
|
3740
|
+
admit({
|
|
3741
|
+
action: "expand",
|
|
3742
|
+
ctx,
|
|
3743
|
+
foldId: id,
|
|
3744
|
+
requestedBytes: Math.max(0, expanding.sourceChars - expanding.placeholderChars),
|
|
3745
|
+
children: childFoldIds(expanding),
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
let next = setFoldProjectionState(persistence.state!, id, expand ? "expanded" : "folded");
|
|
3749
|
+
if (expand) next = withExpandLease(next, id);
|
|
3750
|
+
else {
|
|
3751
|
+
const leases = { ...next.leases };
|
|
3752
|
+
delete leases[id];
|
|
3753
|
+
next = { ...next, leases };
|
|
3754
|
+
}
|
|
3755
|
+
await persistManual(next, ctx);
|
|
3756
|
+
updateStatus(ctx);
|
|
3757
|
+
return epochApplied;
|
|
3758
|
+
};
|
|
3759
|
+
|
|
3340
3760
|
/**
|
|
3341
3761
|
* THE PIN CHANGE PATH, shared by the tool's pin/unpin actions and the editor's
|
|
3342
3762
|
* pin toggle: the pinned-share cap, protectEvidence, the canonical event, persist.
|
|
@@ -3821,36 +4241,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3821
4241
|
}
|
|
3822
4242
|
const id = String(params.id ?? "").trim();
|
|
3823
4243
|
if (!id) throw new Error("reboundary requires id, or ids naming the span to re-cut");
|
|
3824
|
-
const
|
|
3825
|
-
if (fold.parentId) {
|
|
3826
|
-
throw new Error(
|
|
3827
|
-
`Fold ${id} is nested under ${fold.parentId}; dissolve ${fold.parentId} first so ${id} is a root`,
|
|
3828
|
-
);
|
|
3829
|
-
}
|
|
3830
|
-
const refs = flattenFoldRefs(fold, persistence.state);
|
|
3831
|
-
const children = childFoldIds(fold);
|
|
3832
|
-
const briefs = { ...(persistence.state.briefs ?? {}) };
|
|
3833
|
-
delete briefs[id];
|
|
3834
|
-
const next: ActiveContextState = {
|
|
3835
|
-
...persistence.state,
|
|
3836
|
-
revision: persistence.state.revision + 1,
|
|
3837
|
-
folds: deriveFoldParents(persistence.state.folds.filter((item) => item.id !== id)),
|
|
3838
|
-
expanded: persistence.state.expanded.filter((expandedId) => expandedId !== id),
|
|
3839
|
-
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
3840
|
-
};
|
|
3841
|
-
if (!Object.keys(briefs).length) delete next.briefs;
|
|
3842
|
-
const leases = { ...next.leases };
|
|
3843
|
-
delete leases[id];
|
|
3844
|
-
await persistManual({ ...next, leases }, ctx);
|
|
3845
|
-
updateStatus(ctx);
|
|
4244
|
+
const dissolved = await dissolveFold(snapshot, id, ctx);
|
|
3846
4245
|
return toolPayload({
|
|
3847
4246
|
version: 1,
|
|
3848
4247
|
action,
|
|
3849
4248
|
id,
|
|
3850
4249
|
dissolved: true,
|
|
3851
|
-
releasedChildren: children,
|
|
3852
|
-
startId:
|
|
3853
|
-
endId:
|
|
4250
|
+
releasedChildren: dissolved.children,
|
|
4251
|
+
startId: dissolved.startId,
|
|
4252
|
+
endId: dissolved.endId,
|
|
3854
4253
|
durableRevision: persistence.state.revision,
|
|
3855
4254
|
activation: `the span ${id} held is raw again and its boundary is yours to re-cut; ` +
|
|
3856
4255
|
"fold the endpoints you meant, or two sub-spans to split it. No evidence moved.",
|
|
@@ -3859,36 +4258,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3859
4258
|
if (action === "expand" || action === "refold") {
|
|
3860
4259
|
const id = String(params.id ?? "").trim();
|
|
3861
4260
|
if (!id) throw new Error(`${action} requires id`);
|
|
3862
|
-
|
|
3863
|
-
if (action === "expand" && pendingMarks(persistence.state).length) {
|
|
3864
|
-
const result = await commitPendingMarks({
|
|
3865
|
-
snapshot,
|
|
3866
|
-
state: persistence.state,
|
|
3867
|
-
generation: lifecycle.generation,
|
|
3868
|
-
retainIneligible: true,
|
|
3869
|
-
});
|
|
3870
|
-
persistence.state = result.state;
|
|
3871
|
-
epochApplied = result.applied;
|
|
3872
|
-
}
|
|
3873
|
-
const expanding = requireActiveFold(snapshot, persistence.state, id);
|
|
3874
|
-
if (action === "expand") {
|
|
3875
|
-
admit({
|
|
3876
|
-
action: "expand",
|
|
3877
|
-
ctx,
|
|
3878
|
-
foldId: id,
|
|
3879
|
-
requestedBytes: Math.max(0, expanding.sourceChars - expanding.placeholderChars),
|
|
3880
|
-
children: childFoldIds(expanding),
|
|
3881
|
-
});
|
|
3882
|
-
}
|
|
3883
|
-
let next = setFoldProjectionState(persistence.state, id, action === "expand" ? "expanded" : "folded");
|
|
3884
|
-
if (action === "expand") next = withExpandLease(next, id);
|
|
3885
|
-
else {
|
|
3886
|
-
const leases = { ...next.leases };
|
|
3887
|
-
delete leases[id];
|
|
3888
|
-
next = { ...next, leases };
|
|
3889
|
-
}
|
|
3890
|
-
await persistManual(next, ctx);
|
|
3891
|
-
updateStatus(ctx);
|
|
4261
|
+
const epochApplied = await setFoldExpansion(snapshot, id, action === "expand", ctx);
|
|
3892
4262
|
return toolPayload({
|
|
3893
4263
|
version: 1,
|
|
3894
4264
|
action,
|
|
@@ -4219,6 +4589,37 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4219
4589
|
tokens: estimatedTokens(spanBytes(snapshot, Math.min(from, to), Math.max(from, to) + 1)),
|
|
4220
4590
|
});
|
|
4221
4591
|
|
|
4592
|
+
/**
|
|
4593
|
+
* THE READER'S OWN DEPTH, AND IT COSTS THE AGENT NOTHING (2026-08-30).
|
|
4594
|
+
*
|
|
4595
|
+
* A row preview is bounded at EDITOR_ROW_PREVIEW_CHARS because it is a SUMMARY, and
|
|
4596
|
+
* the editor used to have nothing behind it: the whole surface stopped at 300
|
|
4597
|
+
* characters of the first text block. This is the way past that. It is an INJECTED
|
|
4598
|
+
* accessor, like `spanCost`, so the view still holds no runtime reference and still
|
|
4599
|
+
* computes nothing it was not handed, and it is a pure READ: it moves no bytes,
|
|
4600
|
+
* writes no state and never touches the projection, so paging an entry to its end
|
|
4601
|
+
* changes nothing the agent will ever see. That separation is the whole point. A
|
|
4602
|
+
* person auditing the window needs to read further than the model is being shown,
|
|
4603
|
+
* and expanding a fold INTO the model's window is a different act with a different
|
|
4604
|
+
* verb (`expand`) and a different cost.
|
|
4605
|
+
*
|
|
4606
|
+
* The cut is STATED rather than silent, which is gate 136's law carried to a surface
|
|
4607
|
+
* a person reads: `total` and `hasMore` travel with the slice.
|
|
4608
|
+
*/
|
|
4609
|
+
const entryDetail = (entryId: string, offset = 0, chars = EDITOR_DETAIL_PAGE_CHARS) => {
|
|
4610
|
+
const item = snapshot.mapped.find((candidate) => candidate.ref?.entryId === entryId);
|
|
4611
|
+
if (!item) return null;
|
|
4612
|
+
const whole = entryText(item.message);
|
|
4613
|
+
const from = Math.max(0, Math.min(offset, whole.length));
|
|
4614
|
+
const width = Math.max(1, Math.min(chars, EDITOR_DETAIL_PAGE_CHARS));
|
|
4615
|
+
return {
|
|
4616
|
+
text: whole.slice(from, from + width),
|
|
4617
|
+
offset: from,
|
|
4618
|
+
total: whole.length,
|
|
4619
|
+
hasMore: from + width < whole.length,
|
|
4620
|
+
};
|
|
4621
|
+
};
|
|
4622
|
+
|
|
4222
4623
|
// Marks move no bytes until the commit, so window geometry cannot change while
|
|
4223
4624
|
// the editor holds it open; only pending state grows. Rebuilding against the SAME
|
|
4224
4625
|
// snapshot with fresh state keeps PROPOSED rows honest without re-deriving indices.
|
|
@@ -4248,14 +4649,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4248
4649
|
const item = exactMapped(snapshot, ref);
|
|
4249
4650
|
const message = item?.message as Record<string, unknown> | undefined;
|
|
4250
4651
|
const role = String(item?.ref?.role ?? message?.role ?? "entry");
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4652
|
+
// A FOLD'S ENTRY IS A MARK POINT AND A PIN POINT TOO (2026-08-30). It
|
|
4653
|
+
// carried no index and no entry id the view could act on, so `p` was
|
|
4654
|
+
// inert on every row inside an expanded fold while working one row over
|
|
4655
|
+
// in a raw gap, for no reason a reader could see.
|
|
4656
|
+
return {
|
|
4657
|
+
id: ref.entryId,
|
|
4658
|
+
role,
|
|
4659
|
+
preview: rowPreview(item?.message),
|
|
4660
|
+
...(item ? { index: item.index } : {}),
|
|
4661
|
+
};
|
|
4259
4662
|
});
|
|
4260
4663
|
return {
|
|
4261
4664
|
id: String(row.id),
|
|
@@ -4264,6 +4667,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4264
4667
|
sourceCount: Number(row.sourceCount ?? 0),
|
|
4265
4668
|
startPosition: Number(row.startPosition),
|
|
4266
4669
|
endPosition: Number(row.endPosition),
|
|
4670
|
+
// WHAT THE AGENT IS ACTUALLY LOOKING AT (2026-08-30). A fold the agent has
|
|
4671
|
+
// expanded is standing RAW in its window, and the editor rendered it
|
|
4672
|
+
// identically to one that is still a placeholder, so the one surface built
|
|
4673
|
+
// to show a person the model's window could not show them this about it.
|
|
4674
|
+
expandedInContext: (state.expanded ?? []).includes(String(row.id)),
|
|
4675
|
+
// WHERE THE BRIEF CAME FROM. Given the campaign verdict on agent-written
|
|
4676
|
+
// briefs, which sentence is the runtime's and which is the model's is the
|
|
4677
|
+
// fact a person auditing this window most needs, and it was not on the
|
|
4678
|
+
// surface at all.
|
|
4679
|
+
provenance: String((fold as any).briefProvenance?.kind ?? "deterministic"),
|
|
4267
4680
|
entries,
|
|
4268
4681
|
};
|
|
4269
4682
|
}),
|
|
@@ -4289,21 +4702,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4289
4702
|
if (!item) continue;
|
|
4290
4703
|
const message = item.message as Record<string, unknown> | undefined;
|
|
4291
4704
|
const role = String(item.ref?.role ?? message?.role ?? "entry");
|
|
4292
|
-
const content = message?.content;
|
|
4293
|
-
// 300 chars, same as fold entries: the view shows 48 on the row and
|
|
4294
|
-
// deepens to 240 on Enter, so a 48-char preview made detail a no-op.
|
|
4295
|
-
let preview = "";
|
|
4296
|
-
if (typeof content === "string") preview = content.slice(0, 300);
|
|
4297
|
-
else if (Array.isArray(content)) {
|
|
4298
|
-
const firstText = content.find((part) => part?.type === "text");
|
|
4299
|
-
preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
|
|
4300
|
-
}
|
|
4301
4705
|
// THE INDEX IS THE MARK POINT: raw entries carry their mapped position so
|
|
4302
4706
|
// the view can price a span and name its two boundary ids.
|
|
4303
4707
|
out.push({
|
|
4304
4708
|
id: item.ref?.entryId ?? String(index),
|
|
4305
4709
|
role,
|
|
4306
|
-
preview:
|
|
4710
|
+
preview: rowPreview(message),
|
|
4307
4711
|
...(item.ref?.entryId ? { index } : {}),
|
|
4308
4712
|
});
|
|
4309
4713
|
}
|
|
@@ -4319,6 +4723,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4319
4723
|
freedTokens: accounting.freedTokens,
|
|
4320
4724
|
},
|
|
4321
4725
|
pinned: state.protected.map((ref) => ref.entryId),
|
|
4726
|
+
// THE FOLDS THE AGENT HAS STANDING RAW IN ITS WINDOW, so the editor can say which
|
|
4727
|
+
// rows are placeholders and which are already costing the projection.
|
|
4728
|
+
expandedInContext: [...(state.expanded ?? [])],
|
|
4322
4729
|
};
|
|
4323
4730
|
};
|
|
4324
4731
|
|
|
@@ -4333,6 +4740,35 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4333
4740
|
};
|
|
4334
4741
|
const editorActions = {
|
|
4335
4742
|
spanCost,
|
|
4743
|
+
entryDetail,
|
|
4744
|
+
// THE READER'S EXPANSION AND THE WINDOW'S ARE DIFFERENT ACTS, and this is the second
|
|
4745
|
+
// one: it restores the fold's raw bytes to the model's projection and takes the
|
|
4746
|
+
// lease that reclaims them at the next commit. The view's own open/close key never
|
|
4747
|
+
// reaches here.
|
|
4748
|
+
onSetExpanded: async (foldId: string, expand: boolean): Promise<void> => {
|
|
4749
|
+
await runQueued(async () => {
|
|
4750
|
+
await setFoldExpansion(authoritativeSnapshotFor(ctx), foldId, expand, ctx);
|
|
4751
|
+
});
|
|
4752
|
+
safeNotify(ctx, expand
|
|
4753
|
+
? "Expanded in the model's window: its exact bytes are back in the projection, " +
|
|
4754
|
+
"reclaimed at the next commit unless pinned."
|
|
4755
|
+
: "Refolded: the placeholder stands where the bytes were, still peek-recoverable.",
|
|
4756
|
+
"info");
|
|
4757
|
+
refreshAfter();
|
|
4758
|
+
},
|
|
4759
|
+
// RE-CUT IS DISSOLVE PLUS MARK, and the editor already has the second half: `m` on
|
|
4760
|
+
// two raw entries stages the span the person meant. This is the first half, through
|
|
4761
|
+
// the tool's own validated path.
|
|
4762
|
+
onDissolveFold: async (foldId: string): Promise<void> => {
|
|
4763
|
+
let released: string[] = [];
|
|
4764
|
+
await runQueued(async () => {
|
|
4765
|
+
released = (await dissolveFold(authoritativeSnapshotFor(ctx), foldId, ctx)).children;
|
|
4766
|
+
});
|
|
4767
|
+
safeNotify(ctx,
|
|
4768
|
+
`The span is raw again${released.length ? `, releasing ${released.length} nested fold(s)` : ""}; ` +
|
|
4769
|
+
"no evidence moved. Mark the boundary you meant with m.", "info");
|
|
4770
|
+
refreshAfter();
|
|
4771
|
+
},
|
|
4336
4772
|
onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
|
|
4337
4773
|
await runQueued(async () => {
|
|
4338
4774
|
const fresh = authoritativeSnapshotFor(ctx);
|
|
@@ -4357,11 +4793,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4357
4793
|
`Withdrew ${removed.length} staged mark(s); no context bytes moved.`, "info");
|
|
4358
4794
|
refreshAfter();
|
|
4359
4795
|
},
|
|
4360
|
-
onTogglePin: async (
|
|
4361
|
-
|
|
4362
|
-
|
|
4796
|
+
onTogglePin: async (id: string): Promise<void> => {
|
|
4797
|
+
// A TOGGLE HAS TO ASK THE SAME QUESTION THE PIN PATH ANSWERS (2026-08-30). This
|
|
4798
|
+
// read `protected.some((ref) => ref.entryId === id)`, which is only ever true for
|
|
4799
|
+
// an ENTRY id, and `protectEvidence` has always taken a FOLD id too, holding
|
|
4800
|
+
// every ref underneath it. So the moment the editor started offering p on a fold
|
|
4801
|
+
// row, the second press pinned the same refs again instead of releasing them: the
|
|
4802
|
+
// question was asked about an id that is never in the set being searched.
|
|
4803
|
+
const state = persistence.state!;
|
|
4804
|
+
const fold = state.folds.find((item: any) => item.id === id);
|
|
4805
|
+
const held = new Set(state.protected.map((ref: any) => objectRefKey(ref)));
|
|
4806
|
+
const refs = fold
|
|
4807
|
+
? flattenFoldRefs(fold, state)
|
|
4808
|
+
: state.protected.filter((ref: any) => ref.entryId === id);
|
|
4809
|
+
const pinnedAlready = fold
|
|
4810
|
+
? refs.length > 0 && refs.every((ref: any) => held.has(objectRefKey(ref)))
|
|
4811
|
+
: refs.length > 0;
|
|
4363
4812
|
await runQueued(async () => {
|
|
4364
|
-
await applyProtectionChange(authoritativeSnapshotFor(ctx), [
|
|
4813
|
+
await applyProtectionChange(authoritativeSnapshotFor(ctx), [id], !pinnedAlready, ctx);
|
|
4365
4814
|
});
|
|
4366
4815
|
safeNotify(ctx, pinnedAlready
|
|
4367
4816
|
? "Unpinned: the released entries rejoin the next commit's candidates."
|
|
@@ -4414,6 +4863,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4414
4863
|
lifecycle.latestSnapshot = null;
|
|
4415
4864
|
curation.receipts = [];
|
|
4416
4865
|
try { ctx.ui?.setStatus?.(entryTypePrefix, undefined); } catch { }
|
|
4866
|
+
foldBar.model = null;
|
|
4867
|
+
foldBar.requestRender?.();
|
|
4417
4868
|
});
|
|
4418
4869
|
|
|
4419
4870
|
return { projectionCandidates };
|