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.
@@ -92,7 +92,9 @@ import {
92
92
  activeContextSource,
93
93
  ACTIVE_CONTEXT_POLICY,
94
94
  ACTIVE_CONTEXT_TOOL_ACTIONS,
95
- UNBRIEFED_FOLDS_BEFORE_NOTICE,
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 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;
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
- 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");
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
- const thresholds = resolveThresholds(options.thresholds);
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
- const trigger = lifecycle.latestSnapshot?.thresholds.maxTarget ?? null;
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
- if (share !== null && trigger !== null && !ladder.automaticFailure) {
656
- parts.push(share >= trigger ? "COMMIT DUE" : `commit at ${Math.round(trigger * 100)}%`);
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
- if (!anchor || anchor.generation !== lifecycle.generation ||
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 what was cut, once there is enough of it to be worth a turn.
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 unbriefed at the pass it speaks on,
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 = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
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 nothing invites a brief, so briefs stay the runtime's own.
1652
- if (!postFoldNotice) return projected;
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 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.
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
- 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,
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
- unbriefed: unbriefed.length,
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 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);
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: refs[0]?.entryId ?? null,
3853
- endId: refs.at(-1)?.entryId ?? null,
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
- 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);
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
- 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, " ") };
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: preview.replace(/\s+/g, " "),
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 (entryId: string): Promise<void> => {
4361
- const pinnedAlready = persistence.state!.protected
4362
- .some((ref: any) => ref.entryId === entryId);
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), [entryId], !pinnedAlready, 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."