pi-fold 4.0.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 +11 -10
- package/extensions/active-context.ts +510 -133
- 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/tool-surface.ts +0 -1
- package/extensions/lib/transcript.ts +43 -0
- package/extensions/settings.ts +104 -30
- package/package.json +5 -5
|
@@ -92,7 +92,9 @@ import {
|
|
|
92
92
|
activeContextSource,
|
|
93
93
|
ACTIVE_CONTEXT_POLICY,
|
|
94
94
|
ACTIVE_CONTEXT_TOOL_ACTIONS,
|
|
95
|
-
|
|
95
|
+
resolveNoticeLeadShare,
|
|
96
|
+
EDITOR_DETAIL_PAGE_CHARS,
|
|
97
|
+
EDITOR_ROW_PREVIEW_CHARS,
|
|
96
98
|
contextBrand,
|
|
97
99
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
98
100
|
DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES,
|
|
@@ -129,7 +131,6 @@ import type {
|
|
|
129
131
|
FoldKind,
|
|
130
132
|
FoldRecordEntry,
|
|
131
133
|
PreparedFold,
|
|
132
|
-
WorkingMemoryEntry,
|
|
133
134
|
} from "./lib/policy.ts";
|
|
134
135
|
import {
|
|
135
136
|
absorbWedgeMarks,
|
|
@@ -172,6 +173,7 @@ import {
|
|
|
172
173
|
} from "./lib/scheduling.ts";
|
|
173
174
|
import {
|
|
174
175
|
manualFoldCandidate,
|
|
176
|
+
oneLine,
|
|
175
177
|
peekedSourceFoldIds,
|
|
176
178
|
snapFoldCandidate,
|
|
177
179
|
snapToFoldBoundaries,
|
|
@@ -195,7 +197,9 @@ import type {
|
|
|
195
197
|
} from "./lib/instrumentation.ts";
|
|
196
198
|
import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
|
|
197
199
|
import { buildFoldEditorData, FoldEditorView } from "./lib/editor-ui.ts";
|
|
200
|
+
import { publishLiveSettings } from "./lib/live-settings.ts";
|
|
198
201
|
import {
|
|
202
|
+
entryText,
|
|
199
203
|
mapActiveContext,
|
|
200
204
|
} from "./lib/transcript.ts";
|
|
201
205
|
import {
|
|
@@ -207,12 +211,13 @@ import {
|
|
|
207
211
|
unansweredToolCalls,
|
|
208
212
|
} from "./lib/rollback.ts";
|
|
209
213
|
|
|
210
|
-
export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured";
|
|
214
|
+
export type ProjectionReadingBasis = "anchored" | "rewritten" | "unmeasured" | "refused";
|
|
211
215
|
|
|
212
216
|
export * from "./lib/canonical.ts";
|
|
213
217
|
export * from "./lib/curation.ts";
|
|
214
218
|
export * from "./lib/folding.ts";
|
|
215
219
|
export * from "./lib/instrumentation.ts";
|
|
220
|
+
export * from "./lib/live-settings.ts";
|
|
216
221
|
export * from "./lib/measurement.ts";
|
|
217
222
|
export * from "./lib/persistence.ts";
|
|
218
223
|
export * from "./lib/policy.ts";
|
|
@@ -221,6 +226,47 @@ export * from "./lib/scheduling.ts";
|
|
|
221
226
|
export * from "./lib/selection.ts";
|
|
222
227
|
export * from "./lib/transcript.ts";
|
|
223
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
|
+
|
|
224
270
|
export function registerActiveContext(pi: any, options: {
|
|
225
271
|
toolName?: string;
|
|
226
272
|
toolLabel?: string;
|
|
@@ -237,14 +283,21 @@ export function registerActiveContext(pi: any, options: {
|
|
|
237
283
|
* [0, 1), where 0 turns the diet off; 0.50 is what every fold repetition from 3 onward
|
|
238
284
|
* ran, so the default is the value that was measured rather than the absence of one. */
|
|
239
285
|
toolFoldThreshold?: number;
|
|
240
|
-
/** THE
|
|
241
|
-
* Shane, on the sol-20260826-full2 verdict
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
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;
|
|
248
301
|
}): {
|
|
249
302
|
projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
|
|
250
303
|
} {
|
|
@@ -253,18 +306,27 @@ export function registerActiveContext(pi: any, options: {
|
|
|
253
306
|
const brandNoun = options.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN;
|
|
254
307
|
const entryTypePrefix = options.entryTypePrefix ?? DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX;
|
|
255
308
|
const blacklistAutoFoldTools = options.blacklistAutoFoldTools ?? AUTO_FOLD_BLACKLIST_DEFAULT;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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");
|
|
268
330
|
}
|
|
269
331
|
for (const removed of ["foldScheduling", "foldPeekResults", "toolActions"]) {
|
|
270
332
|
if (Object.hasOwn(options, removed)) {
|
|
@@ -324,7 +386,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
324
386
|
commandNames.status === commandNames.fold) {
|
|
325
387
|
throw new Error("Active-context command names must be distinct kebab-case strings");
|
|
326
388
|
}
|
|
327
|
-
|
|
389
|
+
let thresholds = resolveThresholds(options.thresholds);
|
|
328
390
|
assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
|
|
329
391
|
const stateEntryType = `${entryTypePrefix}-state`;
|
|
330
392
|
const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
|
|
@@ -365,6 +427,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
365
427
|
bandTopMeasurement: null as unknown,
|
|
366
428
|
/** The snapshot-and-state key whose frontier scan came back empty; see advanceFoldFrontier. */
|
|
367
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,
|
|
368
433
|
pendingManual: false,
|
|
369
434
|
preparing: null as { id: string; controller: AbortController; promise: Promise<void> } | null,
|
|
370
435
|
lastPreparationError: null as string | null,
|
|
@@ -402,6 +467,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
402
467
|
sessionId: string;
|
|
403
468
|
generation: number;
|
|
404
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,
|
|
405
477
|
estimatorErrors: [] as number[],
|
|
406
478
|
inflowSteps: [] as number[],
|
|
407
479
|
wallInflowSteps: [] as number[],
|
|
@@ -465,6 +537,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
465
537
|
const curation = {
|
|
466
538
|
receipts: [] as ContextReceipt[],
|
|
467
539
|
contextCalls: 0,
|
|
540
|
+
/** Receipts delivered this session, monotonic; the pre-commit notice's re-arm signal. */
|
|
541
|
+
commitsDelivered: 0,
|
|
468
542
|
wallEpisodeOpen: false,
|
|
469
543
|
recoveryAttempts: 0,
|
|
470
544
|
pendingRejection: null as { status: number; ordinal: number } | null,
|
|
@@ -482,9 +556,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
482
556
|
pendingOverflow: null as { at: number; entryId: string | null } | null,
|
|
483
557
|
};
|
|
484
558
|
|
|
485
|
-
const advisory = {
|
|
486
|
-
};
|
|
487
|
-
|
|
488
559
|
// EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
|
|
489
560
|
// visibility. In-memory ON PURPOSE: a restart forgets the registry and the
|
|
490
561
|
// result stays durable, which is the safe default, and the durable entry
|
|
@@ -595,6 +666,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
595
666
|
|
|
596
667
|
const deliverReceipt = (receipt: ContextReceipt): void => {
|
|
597
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;
|
|
598
673
|
emit("context.receipt", {
|
|
599
674
|
receipt_kind: receipt.kind,
|
|
600
675
|
trigger: receipt.trigger,
|
|
@@ -646,14 +721,36 @@ export function registerActiveContext(pi: any, options: {
|
|
|
646
721
|
// Unmeasured is stated, never guessed: an estimate rendered as a fact is the habit
|
|
647
722
|
// that cost a live session, and this line is the most-read surface there is.
|
|
648
723
|
parts.push(share === null ? "not measured" : `${Math.round(share * 100)}% full`);
|
|
649
|
-
|
|
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;
|
|
650
730
|
// WHEN, not just how full. "68% full" only answers the second question a reader has
|
|
651
731
|
// if they already remember where the trigger sits; naming it answers both.
|
|
652
732
|
//
|
|
653
733
|
// NOT WHILE STOPPED, though: the line rendered "COMMIT DUE" and "FOLDING STOPPED"
|
|
654
734
|
// in one breath, promising the thing it was announcing would not happen.
|
|
655
|
-
|
|
656
|
-
|
|
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");
|
|
657
754
|
}
|
|
658
755
|
if (staged > 0) parts.push(`${staged} staged`);
|
|
659
756
|
if (roots > 0) parts.push(`${roots} fold${roots === 1 ? "" : "s"}`);
|
|
@@ -661,6 +758,63 @@ export function registerActiveContext(pi: any, options: {
|
|
|
661
758
|
} catch { }
|
|
662
759
|
};
|
|
663
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
|
+
|
|
664
818
|
const budgetWindowFor = (ctx: any): number | null => {
|
|
665
819
|
measurements.descriptorWindow = contextWindowFor(ctx);
|
|
666
820
|
return providerInputBudget ?? measurements.descriptorWindow;
|
|
@@ -784,6 +938,10 @@ export function registerActiveContext(pi: any, options: {
|
|
|
784
938
|
measurements.lastProviderMeasurement = null;
|
|
785
939
|
measurements.wallInflowSteps.length = 0;
|
|
786
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;
|
|
787
945
|
measurements.lastProjectedEstimateBasis = "unmeasured";
|
|
788
946
|
ladder.pendingManual = false;
|
|
789
947
|
if (!preserveThresholdDecision) nativeCompaction.lastThresholdDecision = null;
|
|
@@ -1332,7 +1490,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1332
1490
|
safeNotify(ctx, `Automatic context management suspended: ${message}`, "warning");
|
|
1333
1491
|
};
|
|
1334
1492
|
|
|
1335
|
-
|
|
1336
1493
|
const PROJECTION_CALIBRATION_MIN_CHARS = 20_000;
|
|
1337
1494
|
const PROJECTION_CALIBRATION_MIN_TOKENS = 5_000;
|
|
1338
1495
|
const PROJECTION_CHARS_PER_TOKEN_FLOOR = 2;
|
|
@@ -1383,6 +1540,41 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1383
1540
|
);
|
|
1384
1541
|
};
|
|
1385
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
|
+
|
|
1386
1578
|
const noteProviderProjectionAnchor = (measurement: ProviderContextMeasurement): void => {
|
|
1387
1579
|
if (measurements.projectionAnchor?.messageSha256 === measurement.messageSha256) return;
|
|
1388
1580
|
const text = instrumentation.previousText;
|
|
@@ -1404,8 +1596,14 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1404
1596
|
textChars / (measurement.tokens - images * IMAGE_ESTIMATED_TOKENS) <
|
|
1405
1597
|
PROJECTION_CHARS_PER_TOKEN_FLOOR)) {
|
|
1406
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);
|
|
1407
1604
|
return;
|
|
1408
1605
|
}
|
|
1606
|
+
clearAnchorRefusal();
|
|
1409
1607
|
measurements.projectionAnchor = {
|
|
1410
1608
|
tokens: measurement.tokens,
|
|
1411
1609
|
chars,
|
|
@@ -1451,7 +1649,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1451
1649
|
const imageTokens = mass.images * IMAGE_ESTIMATED_TOKENS;
|
|
1452
1650
|
const estimate = Math.ceil(textChars / charsPerToken) + imageTokens;
|
|
1453
1651
|
const anchor = measurements.projectionAnchor;
|
|
1454
|
-
|
|
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 ||
|
|
1455
1662
|
anchor.sessionId !== persistence.state?.sessionId) {
|
|
1456
1663
|
return { tokens: estimate, basis: "unmeasured", chars, sizeTokens: estimate, images: mass.images, anchorTokens: null, deltaChars: null, text };
|
|
1457
1664
|
}
|
|
@@ -1535,7 +1742,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1535
1742
|
};
|
|
1536
1743
|
};
|
|
1537
1744
|
|
|
1538
|
-
|
|
1539
1745
|
// THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
|
|
1540
1746
|
//
|
|
1541
1747
|
// `maxTarget` is what the settings panel calls the commit trigger, and until now it
|
|
@@ -1634,9 +1840,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1634
1840
|
};
|
|
1635
1841
|
|
|
1636
1842
|
/**
|
|
1637
|
-
* Tell the agent
|
|
1843
|
+
* Tell the agent where the window stands, once per approach to a commit.
|
|
1638
1844
|
*
|
|
1639
|
-
* 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,
|
|
1640
1846
|
* and it is admitted ONCE per frozen projection, so a pass that adds material leaves it
|
|
1641
1847
|
* buried at its own index rather than moving it to the tail. Those two together mean it
|
|
1642
1848
|
* is a snapshot rather than a running tally: cuts staged after it spoke wait for the
|
|
@@ -1646,19 +1852,54 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1646
1852
|
* epoch early. What it must never do is ask twice for a brief the agent has written,
|
|
1647
1853
|
* and it does not: the rebuild reads the marks, not its own last text.
|
|
1648
1854
|
*/
|
|
1649
|
-
const appendFoldNotice = (
|
|
1855
|
+
const appendFoldNotice = (
|
|
1856
|
+
projected: unknown[],
|
|
1857
|
+
snapshot: ActiveContextSnapshot,
|
|
1858
|
+
bodyBytes: number,
|
|
1859
|
+
): unknown[] => {
|
|
1650
1860
|
// The deterministic experiment condition: cuts stage and commits land exactly as
|
|
1651
|
-
// ever, but
|
|
1652
|
-
if (!
|
|
1861
|
+
// ever, but no carrier reaches the projection at all.
|
|
1862
|
+
if (!preCommitNotice) return projected;
|
|
1653
1863
|
if (!persistence.state) return projected;
|
|
1654
1864
|
const held = pendingMarks(persistence.state);
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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.
|
|
1662
1903
|
//
|
|
1663
1904
|
// The first cut of this withdrew the carrier and re-pushed it at the tail every pass,
|
|
1664
1905
|
// the way the steward band had to because a band must be able to stand down. That is
|
|
@@ -1667,23 +1908,42 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1667
1908
|
// gates 110 and 111 read the whole projection as rewritten and dropped the occupancy
|
|
1668
1909
|
// anchor back to the estimate. A carrier that moves on a quiet pass costs the cache,
|
|
1669
1910
|
// which is the one thing this design exists to avoid.
|
|
1670
|
-
if (unbriefed.length < UNBRIEFED_FOLDS_BEFORE_NOTICE) return projected;
|
|
1671
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.
|
|
1672
1926
|
const text = foldNoticeText({
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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,
|
|
1680
1936
|
toolName,
|
|
1681
1937
|
brandNoun,
|
|
1682
1938
|
});
|
|
1939
|
+
ladder.noticeLatch = curation.commitsDelivered;
|
|
1683
1940
|
emit("context.frontier", {
|
|
1684
1941
|
cut: 0,
|
|
1685
1942
|
pending_marks: held.length,
|
|
1686
|
-
|
|
1943
|
+
briefed: staged.filter((fold) => fold.briefed).length,
|
|
1944
|
+
occupancy_share: Number(share.toFixed(4)),
|
|
1945
|
+
fire_point_share: firePoint,
|
|
1946
|
+
measured,
|
|
1687
1947
|
notice_chars: text.length,
|
|
1688
1948
|
});
|
|
1689
1949
|
projected.push({
|
|
@@ -1699,7 +1959,6 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1699
1959
|
return projected;
|
|
1700
1960
|
};
|
|
1701
1961
|
|
|
1702
|
-
|
|
1703
1962
|
const enforceBandTop = async (
|
|
1704
1963
|
snapshot: ActiveContextSnapshot,
|
|
1705
1964
|
ctx: any,
|
|
@@ -2044,7 +2303,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2044
2303
|
freeze.body = body;
|
|
2045
2304
|
freeze.bodyText = bodyText;
|
|
2046
2305
|
appendReceipts(projected, snapshot);
|
|
2047
|
-
appendFoldNotice(projected, snapshot);
|
|
2306
|
+
appendFoldNotice(projected, snapshot, bodyText.length);
|
|
2048
2307
|
return holdFrozen(projected);
|
|
2049
2308
|
};
|
|
2050
2309
|
const noteProjection = (projected: unknown[], serialized?: string): void => {
|
|
@@ -2737,6 +2996,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2737
2996
|
measurements.lastProviderMeasurement = null;
|
|
2738
2997
|
measurements.wallInflowSteps.length = 0;
|
|
2739
2998
|
measurements.projectionAnchor = null;
|
|
2999
|
+
measurements.anchorRefusal = null;
|
|
2740
3000
|
nativeCompaction.lastThresholdDecision = null;
|
|
2741
3001
|
updateStatus(ctx);
|
|
2742
3002
|
};
|
|
@@ -3337,6 +3597,94 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3337
3597
|
return removed as Array<Record<string, unknown>>;
|
|
3338
3598
|
};
|
|
3339
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
|
+
|
|
3340
3688
|
/**
|
|
3341
3689
|
* THE PIN CHANGE PATH, shared by the tool's pin/unpin actions and the editor's
|
|
3342
3690
|
* pin toggle: the pinned-share cap, protectEvidence, the canonical event, persist.
|
|
@@ -3821,36 +4169,15 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3821
4169
|
}
|
|
3822
4170
|
const id = String(params.id ?? "").trim();
|
|
3823
4171
|
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);
|
|
4172
|
+
const dissolved = await dissolveFold(snapshot, id, ctx);
|
|
3846
4173
|
return toolPayload({
|
|
3847
4174
|
version: 1,
|
|
3848
4175
|
action,
|
|
3849
4176
|
id,
|
|
3850
4177
|
dissolved: true,
|
|
3851
|
-
releasedChildren: children,
|
|
3852
|
-
startId:
|
|
3853
|
-
endId:
|
|
4178
|
+
releasedChildren: dissolved.children,
|
|
4179
|
+
startId: dissolved.startId,
|
|
4180
|
+
endId: dissolved.endId,
|
|
3854
4181
|
durableRevision: persistence.state.revision,
|
|
3855
4182
|
activation: `the span ${id} held is raw again and its boundary is yours to re-cut; ` +
|
|
3856
4183
|
"fold the endpoints you meant, or two sub-spans to split it. No evidence moved.",
|
|
@@ -3859,36 +4186,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3859
4186
|
if (action === "expand" || action === "refold") {
|
|
3860
4187
|
const id = String(params.id ?? "").trim();
|
|
3861
4188
|
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);
|
|
4189
|
+
const epochApplied = await setFoldExpansion(snapshot, id, action === "expand", ctx);
|
|
3892
4190
|
return toolPayload({
|
|
3893
4191
|
version: 1,
|
|
3894
4192
|
action,
|
|
@@ -4219,6 +4517,37 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4219
4517
|
tokens: estimatedTokens(spanBytes(snapshot, Math.min(from, to), Math.max(from, to) + 1)),
|
|
4220
4518
|
});
|
|
4221
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
|
+
|
|
4222
4551
|
// Marks move no bytes until the commit, so window geometry cannot change while
|
|
4223
4552
|
// the editor holds it open; only pending state grows. Rebuilding against the SAME
|
|
4224
4553
|
// snapshot with fresh state keeps PROPOSED rows honest without re-deriving indices.
|
|
@@ -4248,14 +4577,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4248
4577
|
const item = exactMapped(snapshot, ref);
|
|
4249
4578
|
const message = item?.message as Record<string, unknown> | undefined;
|
|
4250
4579
|
const role = String(item?.ref?.role ?? message?.role ?? "entry");
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
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
|
+
};
|
|
4259
4590
|
});
|
|
4260
4591
|
return {
|
|
4261
4592
|
id: String(row.id),
|
|
@@ -4264,6 +4595,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4264
4595
|
sourceCount: Number(row.sourceCount ?? 0),
|
|
4265
4596
|
startPosition: Number(row.startPosition),
|
|
4266
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"),
|
|
4267
4608
|
entries,
|
|
4268
4609
|
};
|
|
4269
4610
|
}),
|
|
@@ -4289,21 +4630,12 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4289
4630
|
if (!item) continue;
|
|
4290
4631
|
const message = item.message as Record<string, unknown> | undefined;
|
|
4291
4632
|
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
4633
|
// THE INDEX IS THE MARK POINT: raw entries carry their mapped position so
|
|
4302
4634
|
// the view can price a span and name its two boundary ids.
|
|
4303
4635
|
out.push({
|
|
4304
4636
|
id: item.ref?.entryId ?? String(index),
|
|
4305
4637
|
role,
|
|
4306
|
-
preview:
|
|
4638
|
+
preview: rowPreview(message),
|
|
4307
4639
|
...(item.ref?.entryId ? { index } : {}),
|
|
4308
4640
|
});
|
|
4309
4641
|
}
|
|
@@ -4319,6 +4651,9 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4319
4651
|
freedTokens: accounting.freedTokens,
|
|
4320
4652
|
},
|
|
4321
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 ?? [])],
|
|
4322
4657
|
};
|
|
4323
4658
|
};
|
|
4324
4659
|
|
|
@@ -4333,6 +4668,35 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4333
4668
|
};
|
|
4334
4669
|
const editorActions = {
|
|
4335
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
|
+
},
|
|
4336
4700
|
onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
|
|
4337
4701
|
await runQueued(async () => {
|
|
4338
4702
|
const fresh = authoritativeSnapshotFor(ctx);
|
|
@@ -4357,11 +4721,24 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4357
4721
|
`Withdrew ${removed.length} staged mark(s); no context bytes moved.`, "info");
|
|
4358
4722
|
refreshAfter();
|
|
4359
4723
|
},
|
|
4360
|
-
onTogglePin: async (
|
|
4361
|
-
|
|
4362
|
-
|
|
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;
|
|
4363
4740
|
await runQueued(async () => {
|
|
4364
|
-
await applyProtectionChange(authoritativeSnapshotFor(ctx), [
|
|
4741
|
+
await applyProtectionChange(authoritativeSnapshotFor(ctx), [id], !pinnedAlready, ctx);
|
|
4365
4742
|
});
|
|
4366
4743
|
safeNotify(ctx, pinnedAlready
|
|
4367
4744
|
? "Unpinned: the released entries rejoin the next commit's candidates."
|