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.
@@ -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
- UNBRIEFED_FOLDS_BEFORE_NOTICE,
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 INVITATION SWITCH (public since 2026-08-27; DEFAULT FALSE since 2026-08-28,
241
- * Shane, on the sol-20260826-full2 verdict). false is the deterministic shape the
242
- * campaign measured and the one it recommends: no carrier ever invites a brief and
243
- * every fold goes out with the runtime's own words, which drew 14, 14, 9 and 8 correct
244
- * against the invited condition's 3. true restores the standing invitation for the
245
- * agent to improve a brief. Either way the agent verbs stay on the tool, so an agent
246
- * that finds a fold can still annotate what it reads. */
247
- postFoldNotice?: boolean;
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
- if (options.postFoldNotice !== undefined && typeof options.postFoldNotice !== "boolean") {
257
- throw new Error("postFoldNotice must be a boolean: true keeps the post-fold invitation " +
258
- "notice, false ships every fold with the runtime's deterministic brief and no " +
259
- "carrier inviting the agent to improve it");
260
- }
261
- const postFoldNotice = options.postFoldNotice ?? false;
262
- const toolFoldThreshold = options.toolFoldThreshold ?? DEFAULT_TOOL_FOLD_THRESHOLD;
263
- if (typeof toolFoldThreshold !== "number" || !Number.isFinite(toolFoldThreshold) ||
264
- toolFoldThreshold < 0 || toolFoldThreshold >= 1) {
265
- throw new Error("toolFoldThreshold must be a share in [0, 1): it names the oldest " +
266
- "fraction of the projected window whose tool results are clipped in view at each " +
267
- "commit, and 0 turns the diet off");
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
- const thresholds = resolveThresholds(options.thresholds);
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
- const trigger = lifecycle.latestSnapshot?.thresholds.maxTarget ?? null;
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
- if (share !== null && trigger !== null && !ladder.automaticFailure) {
656
- parts.push(share >= trigger ? "COMMIT DUE" : `commit at ${Math.round(trigger * 100)}%`);
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
- ctx.ui?.setStatus?.(entryTypePrefix, `${brandNoun} ${parts.join(" · ")}`);
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
- if (!anchor || anchor.generation !== lifecycle.generation ||
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 what was cut, once there is enough of it to be worth a turn.
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 unbriefed at the pass it speaks on,
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 = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
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 nothing invites a brief, so briefs stay the runtime's own.
1652
- if (!postFoldNotice) return projected;
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 unbriefed = held.filter((mark) =>
1656
- mark.briefProvenance?.kind !== "supplied" && mark.briefProvenance?.kind !== "augmented");
1657
- // BATCHED, AND APPENDED ONCE. A notice per cut is noise, and noise is how guidance
1658
- // gets ignored; the frontier cuts often and this speaks only when a batch has built
1659
- // up. `carrierAdmitted` is what makes it an APPEND rather than a rewrite: once the
1660
- // carrier is in the frozen projection it is not pushed again, so raw material
1661
- // arriving afterwards lands after it and the prefix in front of it never moves.
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
- unbriefed: unbriefed.map((mark) => ({
1674
- id: mark.id,
1675
- kind: mark.kind,
1676
- tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
1677
- brief: typeof mark.brief === "string" ? mark.brief : undefined,
1678
- })),
1679
- pending: held.length,
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
- unbriefed: unbriefed.length,
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 fold = requireActiveFold(snapshot, persistence.state, id);
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: refs[0]?.entryId ?? null,
3853
- endId: refs.at(-1)?.entryId ?? null,
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
- let epochApplied: unknown[] = [];
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
- const content = message?.content;
4252
- let preview = "";
4253
- if (typeof content === "string") preview = content.slice(0, 300);
4254
- else if (Array.isArray(content)) {
4255
- const firstText = content.find((part) => part?.type === "text");
4256
- preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
4257
- }
4258
- return { id: ref.entryId, role, preview: preview.replace(/\s+/g, " ") };
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: preview.replace(/\s+/g, " "),
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 (entryId: string): Promise<void> => {
4361
- const pinnedAlready = persistence.state!.protected
4362
- .some((ref: any) => ref.entryId === entryId);
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), [entryId], !pinnedAlready, 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 };