pi-fold 2.0.0 → 3.0.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.
@@ -22,6 +22,7 @@ import {
22
22
  descendantIds,
23
23
  encodedFoldSource,
24
24
  foldCandidatesDetail,
25
+ foldIndexRow,
25
26
  foldTreeDetail,
26
27
  peekFoldSource,
27
28
  prepareFold,
@@ -29,7 +30,6 @@ import {
29
30
  projectionSlateCandidates,
30
31
  protectEvidence,
31
32
  requireActiveFold,
32
- selectAutomaticChapter,
33
33
  selectAutomaticRung,
34
34
  setFoldProjectionState,
35
35
  withExpandLease,
@@ -56,7 +56,6 @@ import {
56
56
  providerTokens,
57
57
  refsProtected,
58
58
  stringIds,
59
- toolRefsProtected,
60
59
  toolPayload,
61
60
  } from "./lib/measurement.ts";
62
61
  import type {
@@ -70,7 +69,6 @@ import {
70
69
  clearPrepared,
71
70
  deriveFoldParents,
72
71
  flattenFoldRefs,
73
- foldBrief,
74
72
  foldIdFor,
75
73
  makeFoldRecordEntry,
76
74
  makeStateCheckpoint,
@@ -90,6 +88,7 @@ import {
90
88
  activeContextSource,
91
89
  ACTIVE_CONTEXT_POLICY,
92
90
  ACTIVE_CONTEXT_TOOL_ACTIONS,
91
+ UNBRIEFED_FOLDS_BEFORE_NOTICE,
93
92
  contextBrand,
94
93
  DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
95
94
  DEFAULT_ACTIVE_CONTEXT_COMMAND_NAMES,
@@ -99,7 +98,6 @@ import {
99
98
  COMMIT_RECLAIM_FLOOR_SHARE,
100
99
  CONTEXT_RECEIPT_BLOCK_BYTES,
101
100
  DEFAULT_CONTEXT_WINDOW,
102
- resolveGuidance,
103
101
  assertThresholdsServable,
104
102
  resolveThresholds,
105
103
  servingBudgetTokens,
@@ -107,15 +105,15 @@ import {
107
105
  entryTypeNamespace,
108
106
  MAX_FOLD_SPAN_CHARS,
109
107
  MAX_PINNED_SHARE,
110
- MAX_WEDGE_ABSORB_TOKENS,
111
108
  PEEK_DEFAULT_MAX_BYTES,
109
+ peekIsEphemeral,
112
110
  PEEK_MIN_SLICE_BYTES,
113
111
  PEEK_READ_ONLY_CONTEXT_ACTIONS,
112
+ RETIRED_TOOL_ACTIONS,
114
113
  AUTO_FOLD_BLACKLIST_DEFAULT,
115
114
  USER_RESCUE_MAX_SOURCE_CHARS,
116
115
  } from "./lib/policy.ts";
117
116
  import type {
118
- ActiveContextGuidance,
119
117
  ActiveContextSnapshot,
120
118
  ActiveContextState,
121
119
  ActiveContextThresholds,
@@ -123,15 +121,16 @@ import type {
123
121
  FoldCandidate,
124
122
  FoldKind,
125
123
  FoldRecordEntry,
124
+ MarkOrigin,
126
125
  PreparedFold,
127
126
  } from "./lib/policy.ts";
128
127
  import {
129
128
  absorbWedgeMarks,
129
+ markSpanRefs,
130
130
  } from "./lib/scheduling.ts";
131
131
  import {
132
132
  contextReceipt,
133
- contextRiderText,
134
- markAwarenessText,
133
+ foldNoticeText,
135
134
  receiptBlockText,
136
135
  withReceipt,
137
136
  } from "./lib/curation.ts";
@@ -140,35 +139,33 @@ import type {
140
139
  } from "./lib/curation.ts";
141
140
  import {
142
141
  addPendingMark,
143
- claimedRefKeys,
142
+ commitCoverage,
143
+ windowClaims,
144
144
  commitPendingMarks,
145
145
  consolidationMarks,
146
146
  ephemeralPeekMarks,
147
+ epochCommitDue,
147
148
  estimatedTokens,
148
149
  foldMarkFor,
149
- ladderSelectionMark,
150
150
  ladderBrief,
151
151
  markAccounting,
152
- markEligibility,
153
152
  markFreedBytes,
154
153
  unmarkedRemainder,
155
- guardWaiverCount,
156
- markedFoldIds,
154
+ markClaimingRef,
157
155
  markOrdinal,
158
156
  pendingMarks,
159
- markTouchesCurrentTurn,
157
+ refoldMarks,
160
158
  schedulingStatus,
161
159
  topUpMarks,
160
+ frontierMarks,
162
161
  withPendingMarks,
163
162
  } from "./lib/scheduling.ts";
164
163
  import {
165
- candidateSpanChars,
166
164
  manualFoldCandidate,
167
165
  peekedSourceFoldIds,
168
- selectAutomaticToolBatch,
169
166
  snapFoldCandidate,
170
167
  snapToFoldBoundaries,
171
- splitCandidateBySize,
168
+ spanBytes,
172
169
  } from "./lib/selection.ts";
173
170
  import type { SpanCorrection } from "./lib/selection.ts";
174
171
  import {
@@ -187,8 +184,8 @@ import type {
187
184
  ProjectionChange,
188
185
  } from "./lib/instrumentation.ts";
189
186
  import { buildActiveContextCommands, buildActiveContextTool } from "./lib/tool-surface.ts";
187
+ import { buildFoldEditorData, FoldEditorView } from "./lib/editor-ui.ts";
190
188
  import {
191
- currentTurnRefKeys,
192
189
  mapActiveContext,
193
190
  } from "./lib/transcript.ts";
194
191
  import {
@@ -255,7 +252,6 @@ export function registerActiveContext(pi: any, options: {
255
252
  blacklistAutoFoldTools?: ReadonlySet<string>;
256
253
  providerInputBudget?: number;
257
254
  thresholds?: ActiveContextThresholds;
258
- guidance?: Partial<ActiveContextGuidance>;
259
255
  }): {
260
256
  projectionCandidates: (ctx: any) => Array<Record<string, unknown>>;
261
257
  } {
@@ -288,6 +284,13 @@ export function registerActiveContext(pi: any, options: {
288
284
  "tools it meant to permit");
289
285
  }
290
286
  }
287
+ if (Object.hasOwn(options, "guidance")) {
288
+ throw new Error("guidance is no longer an option: it switched the copy that taught the " +
289
+ "agent to CHOOSE SPANS, and the agent does not choose spans. The runtime cuts at the " +
290
+ "frontier and the agent annotates what it cut, so the guidance it switched has been " +
291
+ "replaced wholesale by the post-fold notice, which is not optional because a fold the " +
292
+ "agent is never told about is one it can never brief");
293
+ }
291
294
  if (Object.hasOwn(options, "commandPrefix")) {
292
295
  throw new Error("commandPrefix is no longer an option: give whole names with commandNames");
293
296
  }
@@ -317,13 +320,12 @@ export function registerActiveContext(pi: any, options: {
317
320
  }
318
321
  const thresholds = resolveThresholds(options.thresholds);
319
322
  assertThresholdsServable(thresholds, providerInputBudget ?? servingBudgetTokens(DEFAULT_CONTEXT_WINDOW));
320
- const guidance = resolveGuidance(options.guidance);
321
323
  const stateEntryType = `${entryTypePrefix}-state`;
322
324
  const foldRecordEntryType = `${entryTypePrefix}-fold-record`;
323
325
  const milestoneProjectionType = `${entryTypePrefix}-milestone`;
324
326
  const advisoryProjectionType = `${entryTypePrefix}-advisory`;
327
+ const foldNoticeProjectionType = `${entryTypePrefix}-fold-notice`;
325
328
  const receiptProjectionType = `${entryTypePrefix}-receipts`;
326
- const riderProjectionType = `${entryTypePrefix}-rider`;
327
329
  const curationProjectionType = `${entryTypePrefix}-curation`;
328
330
  const entryNamespace = entryTypeNamespace(entryTypePrefix);
329
331
  const providerMeasurementEntryType = `${entryNamespace}-provider-context-measurement`;
@@ -353,6 +355,10 @@ export function registerActiveContext(pi: any, options: {
353
355
  };
354
356
 
355
357
  const ladder = {
358
+ /** The provider measurement the last band-top commit ran against; one commit per one. */
359
+ bandTopMeasurement: null as unknown,
360
+ /** The snapshot-and-state key whose frontier scan came back empty; see advanceFoldFrontier. */
361
+ frontierEmptyKey: null as string | null,
356
362
  pendingManual: false,
357
363
  preparing: null as { id: string; controller: AbortController; promise: Promise<void> } | null,
358
364
  lastPreparationError: null as string | null,
@@ -401,7 +407,12 @@ export function registerActiveContext(pi: any, options: {
401
407
  previousText: null as string | null,
402
408
  lastChange: "append" as ProjectionChange,
403
409
  lastPreservedShare: null as number | null,
404
- sinceHandoff: [] as Array<{ seq: number; kind: string }>,
410
+ sinceHandoff: [] as Array<{
411
+ seq: number;
412
+ kind: string;
413
+ action?: string;
414
+ ok?: boolean;
415
+ }>,
405
416
  mutationsSinceHandoff: 0,
406
417
  requests: 0,
407
418
  lastMutationRequest: 0,
@@ -462,6 +473,13 @@ export function registerActiveContext(pi: any, options: {
462
473
  hardFenceReleasedProjectionKeys: new Set<string>(),
463
474
  };
464
475
 
476
+ // EPHEMERAL PEEK (Build 4b): tool call ids whose peek asked for one-read
477
+ // visibility. In-memory ON PURPOSE: a restart forgets the registry and the
478
+ // result stays durable, which is the safe default, and the durable entry
479
+ // always keeps the exact bytes, so folding, peek and rollback read the
480
+ // source unchanged whatever the projection shows.
481
+ const ephemeralPeeks = new Set<string>();
482
+
465
483
  const nativeCompaction = {
466
484
  lastThresholdDecision: null as Record<string, unknown> | null,
467
485
  pendingNativeReceipt: null as NativeCompactionCompletionReceipt | null,
@@ -540,7 +558,12 @@ export function registerActiveContext(pi: any, options: {
540
558
  revision: persistence.state?.revision ?? 0,
541
559
  at: Date.now(),
542
560
  }, payload);
543
- instrumentation.sinceHandoff.push({ seq: record.seq, kind });
561
+ instrumentation.sinceHandoff.push({
562
+ seq: record.seq,
563
+ kind,
564
+ ...(typeof record.action === "string" ? { action: record.action } : {}),
565
+ ...(typeof record.ok === "boolean" ? { ok: record.ok } : {}),
566
+ });
544
567
  const operation = curation.instrumentationQueue.then(async () => {
545
568
  try { await pi.appendEntry(contextEventEntryType, record); }
546
569
  catch { }
@@ -552,6 +575,12 @@ export function registerActiveContext(pi: any, options: {
552
575
  const PREFIX_MUTATING_KINDS: ReadonlySet<string> = new Set([
553
576
  "context.commit", "context.fold", "context.absorb", "context.split", "context.recovery",
554
577
  ]);
578
+ const CACHE_ACTION_REQUEST_CLASS: Readonly<Record<string, string>> = Object.freeze({
579
+ fold: "after-mark",
580
+ peek: "after-peek",
581
+ expand: "after-expand",
582
+ refold: "after-refold",
583
+ });
555
584
 
556
585
  const deliverReceipt = (receipt: ContextReceipt): void => {
557
586
  curation.receipts = withReceipt(curation.receipts, receipt);
@@ -1191,7 +1220,6 @@ export function registerActiveContext(pi: any, options: {
1191
1220
  policy: snapshot ? {
1192
1221
  maxTarget: snapshot.thresholds.maxTarget,
1193
1222
  minTarget: snapshot.thresholds.minTarget,
1194
- freshTail: snapshot.thresholds.freshTail,
1195
1223
  hardFenceRatio: hardFenceRatio(snapshot),
1196
1224
  } : null,
1197
1225
  });
@@ -1487,6 +1515,198 @@ export function registerActiveContext(pi: any, options: {
1487
1515
  ctx.abort();
1488
1516
  };
1489
1517
 
1518
+ // THE BAND TOP COMMITS ON ITS OWN (Shane, 2026-08-22).
1519
+ //
1520
+ // `maxTarget` is what the settings panel calls the commit trigger, and until now it
1521
+ // fired nothing. There were two commit sites: the fence below, which measures against
1522
+ // the WHOLE serving budget plus a margin, and the compaction boundary, which is Pi's
1523
+ // own threshold near the top of the window. A live 1M-window session sat at 571,367
1524
+ // tokens against a stated trigger of 516,096 with `folds: 0`, because neither site was
1525
+ // anywhere near. On a 251,520-token budget the fence sits close enough behind the
1526
+ // boundary to hide it; at 1M the two are half a million tokens apart.
1527
+ //
1528
+ // Shane, on the shape of the fix: "if the context is over our max, we commit. I don't
1529
+ // think that should be anything like once it crosses this threshold or anything like
1530
+ // that, it should just be an if statement. That way there isn't much room for error, if
1531
+ // it's over we fold." So there is no latch and no crossing to detect. Every projection
1532
+ // pass asks one question and acts on the answer. Occupancy only moves when the provider
1533
+ // measures, and a second attempt inside one handoff already defers on the mutation
1534
+ // budget, so "one commit per pass while over" is what this produces. A pass that finds
1535
+ // nothing to fold says so in the stream, which is a state worth seeing rather than one
1536
+ // worth suppressing.
1537
+ /**
1538
+ * Cut the raw material that has piled up past the last fold, and say so.
1539
+ *
1540
+ * This is the whole of "the runtime cuts": no commit, no bytes moved, no persistence
1541
+ * boundary. It stages pending marks the same way a commit's own top-up does, through
1542
+ * the same selector and the same brief builder, so a frontier cut and a commit fill are
1543
+ * the same object and the agent cannot tell them apart except by when it heard.
1544
+ */
1545
+ const advanceFoldFrontier = (snapshot: ActiveContextSnapshot): number => {
1546
+ if (!persistence.state) return 0;
1547
+ if (ladder.automaticFailure !== null) return 0;
1548
+ // A SCAN THAT FOUND NOTHING IS NOT RE-RUN UNTIL SOMETHING CHANGES. Exhausting the
1549
+ // selector costs one window scan per staged cut and one more to come back empty, and
1550
+ // once everything foldable is staged EVERY later pass pays that last scan to be told
1551
+ // the same thing. Measured on the 90-projection loop inside gate 50's instrumentation
1552
+ // claim: 137ms of a 460ms pass, 30 percent of the runtime's own per-request cost, for
1553
+ // an answer that cannot have changed.
1554
+ //
1555
+ // The key is the one `authoritativeSnapshotFor` already trusts for the snapshot
1556
+ // itself, plus the two pieces of state the selector reads: the revision, which moves
1557
+ // on every durable write, and the pending-mark count, which moves when the agent
1558
+ // withdraws one. It is only consulted when the last scan staged NOTHING; a scan that
1559
+ // staged something changed the state, so its key is stale by construction.
1560
+ const frontierKey = `${snapshot.messages.length}:${snapshot.mapped.length}:` +
1561
+ `${persistence.state.revision}:${pendingMarks(persistence.state).length}`;
1562
+ if (ladder.frontierEmptyKey === frontierKey) return 0;
1563
+ // CONSOLIDATION IS ANNOUNCED THE SAME WAY (Shane, 2026-08-23). When standing folds
1564
+ // reach `consolidateAfter` the parent that will absorb them is staged HERE rather than
1565
+ // being computed inside the commit, for exactly the reason the frontier exists: a
1566
+ // parent's brief indexes ten children, it is the entry the agent will navigate by, and
1567
+ // the agent knows what mattered in those children while the commit only knows their
1568
+ // order. Staged, it is byte-inert like any other pending mark and rides the same
1569
+ // notice, so there is one mechanism and not two.
1570
+ const marks = [
1571
+ ...frontierMarks({
1572
+ snapshot,
1573
+ state: persistence.state,
1574
+ ordinal: instrumentation.requests,
1575
+ }),
1576
+ ...consolidationMarks({
1577
+ snapshot,
1578
+ state: persistence.state,
1579
+ ordinal: instrumentation.requests,
1580
+ }),
1581
+ ];
1582
+ let state = persistence.state;
1583
+ const staged: ReturnType<typeof frontierMarks> = [];
1584
+ for (const mark of marks) {
1585
+ const addition = addPendingMark(state, mark);
1586
+ if (!addition.added) continue;
1587
+ state = addition.state;
1588
+ staged.push(mark);
1589
+ }
1590
+ // NOTHING STAGED, so this key is answered and the next pass under it can skip the
1591
+ // scan. Read from `staged` rather than from `marks`, because a candidate the pending
1592
+ // set already holds comes back from the selector every pass and is refused every
1593
+ // pass: keying on `marks` left the skip unreachable in exactly the steady state it
1594
+ // was built for, and half its measured saving on the floor.
1595
+ if (!staged.length) {
1596
+ ladder.frontierEmptyKey = frontierKey;
1597
+ return 0;
1598
+ }
1599
+ persistence.state = state;
1600
+ emit("context.frontier", {
1601
+ cut: staged.length,
1602
+ pending_marks: (state.pendingMarks ?? []).length,
1603
+ min_fold_chars: thresholds.minFoldChars,
1604
+ });
1605
+ // NO PERSISTENCE BOUNDARY OF ITS OWN, deliberately. A cut is DERIVED: the mark id
1606
+ // comes from the span, so a restart that lost these re-cuts exactly the same spans
1607
+ // with exactly the same ids, and the only thing that has to survive is a brief the
1608
+ // agent WROTE, which its own action persists along with the marks it rides with.
1609
+ // Persisting here instead cost a durable write on every projection pass, and the
1610
+ // branch entry it appended rebuilt the projection: gates 110 and 111 caught it as an
1611
+ // occupancy anchor that stopped reading the provider's own count.
1612
+ return staged.length;
1613
+ };
1614
+
1615
+ /**
1616
+ * Tell the agent what was cut, once there is enough of it to be worth a turn.
1617
+ *
1618
+ * It is built from LIVE state, so it names what is unbriefed at the pass it speaks on,
1619
+ * and it is admitted ONCE per frozen projection, so a pass that adds material leaves it
1620
+ * buried at its own index rather than moving it to the tail. Those two together mean it
1621
+ * is a snapshot rather than a running tally: cuts staged after it spoke wait for the
1622
+ * next pass that rebuilds the projection anyway, and that is the trade being made. A
1623
+ * carrier that re-renders to stay current has to move, moving costs the occupancy
1624
+ * anchor its provider count, and an anchor is worth more than a fold id arriving one
1625
+ * epoch early. What it must never do is ask twice for a brief the agent has written,
1626
+ * and it does not: the rebuild reads the marks, not its own last text.
1627
+ */
1628
+ const appendFoldNotice = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
1629
+ if (!persistence.state) return projected;
1630
+ const held = pendingMarks(persistence.state);
1631
+ const unbriefed = held.filter((mark) => mark.briefProvenance?.kind !== "supplied");
1632
+ // BATCHED, AND APPENDED ONCE. A notice per cut is noise, and noise is how guidance
1633
+ // gets ignored; the frontier cuts often and this speaks only when a batch has built
1634
+ // up. `carrierAdmitted` is what makes it an APPEND rather than a rewrite: once the
1635
+ // carrier is in the frozen projection it is not pushed again, so raw material
1636
+ // arriving afterwards lands after it and the prefix in front of it never moves.
1637
+ //
1638
+ // The first cut of this withdrew the carrier and re-pushed it at the tail every pass,
1639
+ // the way the steward band had to because a band must be able to stand down. That is
1640
+ // exactly wrong for this one: re-pushing moved the notice past each newly arrived
1641
+ // message, so the byte at its old index changed on a pass where nothing else had, and
1642
+ // gates 110 and 111 read the whole projection as rewritten and dropped the occupancy
1643
+ // anchor back to the estimate. A carrier that moves on a quiet pass costs the cache,
1644
+ // which is the one thing this design exists to avoid.
1645
+ if (unbriefed.length < UNBRIEFED_FOLDS_BEFORE_NOTICE) return projected;
1646
+ if (!carrierAdmitted("fold-notice")) return projected;
1647
+ const text = foldNoticeText({
1648
+ unbriefed: unbriefed.map((mark) => ({
1649
+ id: mark.id,
1650
+ kind: mark.kind,
1651
+ tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
1652
+ })),
1653
+ pending: held.length,
1654
+ toolName,
1655
+ brandNoun,
1656
+ });
1657
+ emit("context.frontier", {
1658
+ cut: 0,
1659
+ pending_marks: held.length,
1660
+ unbriefed: unbriefed.length,
1661
+ notice_chars: text.length,
1662
+ });
1663
+ projected.push({
1664
+ role: "custom",
1665
+ customType: foldNoticeProjectionType,
1666
+ content: text,
1667
+ display: false,
1668
+ details: { source: activeContextSource(entryTypePrefix), ephemeral: true },
1669
+ timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
1670
+ ? ownValue(snapshot.messages.at(-1), "timestamp")
1671
+ : 0,
1672
+ });
1673
+ return projected;
1674
+ };
1675
+
1676
+ const enforceBandTop = async (
1677
+ snapshot: ActiveContextSnapshot,
1678
+ ctx: any,
1679
+ ): Promise<boolean> => {
1680
+ if (!epochCommitDue(snapshot, measurements.latestRatio)) return false;
1681
+ // ONE COMMIT PER MEASUREMENT, which is what "one commit per crossing" means once the
1682
+ // commit stops taking everything. There was no latch here and none was needed: the
1683
+ // band-top commit folded every span the class law offered, so the next pass found
1684
+ // nothing proposable and deferred. The 2026-08-23 depth bound ends that, on purpose,
1685
+ // and without this the parked window commits a SLICE on every pass at the same
1686
+ // declared occupancy, which is a prefix rewrite per pass and the exact cache cost the
1687
+ // one-commit rule exists to prevent. Occupancy is anchored to the provider's own
1688
+ // count, so a new count is the only evidence that the window actually moved; the
1689
+ // latch is keyed to that measurement and nothing else.
1690
+ if (ladder.bandTopMeasurement !== null &&
1691
+ ladder.bandTopMeasurement === measurements.lastProviderMeasurement) {
1692
+ return false;
1693
+ }
1694
+ ladder.bandTopMeasurement = measurements.lastProviderMeasurement;
1695
+ let action: Record<string, unknown> | null = null;
1696
+ try {
1697
+ // NULL RATIO, deliberately. The band top is routine housekeeping at a stated
1698
+ // threshold, not an emergency, and a null ratio is what arms the depth bound: the
1699
+ // commit takes stalest-first and stops at the aim, so the newest events stay raw
1700
+ // for as long as there is room for them. The fence and the compaction boundary
1701
+ // carry a ratio and no bound, because something has already gone wrong there.
1702
+ action = await attemptAutomaticCommit(snapshot, ctx, "band-top", null);
1703
+ } catch (error) {
1704
+ suspendAutomatic(error, "band-top", ctx);
1705
+ return false;
1706
+ }
1707
+ return action !== null;
1708
+ };
1709
+
1490
1710
  const enforceProjectionBudget = async (
1491
1711
  snapshot: ActiveContextSnapshot,
1492
1712
  projected: unknown[],
@@ -1503,9 +1723,10 @@ export function registerActiveContext(pi: any, options: {
1503
1723
  attempts += 1;
1504
1724
  // THE EMERGENCY IS THE REQUEST THAT WILL NOT FIT, and only that. A projection at
1505
1725
  // the margin is ordinary work: the fence commits it because it is the pass holding
1506
- // the request, but it is not a recovery, it does not waive the current-turn guard,
1507
- // and it does not buy an exemption from the reclaim floor. Waiving at the margin
1508
- // spends the open turn's own evidence on headroom the boundary was about to buy.
1726
+ // the request, but it is not a recovery and it does not buy an exemption from the
1727
+ // reclaim floor. What separates it from the band top is depth alone: the fence
1728
+ // carries a ratio, so its commit takes everything eligible rather than stopping at
1729
+ // the aim.
1509
1730
  const emergency = measured.over || rejected;
1510
1731
  if (emergency) curation.recoveryAttempts += 1;
1511
1732
  let action: Record<string, unknown> | null = null;
@@ -1678,40 +1899,90 @@ export function registerActiveContext(pi: any, options: {
1678
1899
  return projected;
1679
1900
  };
1680
1901
 
1681
- const appendRider = (projected: unknown[], snapshot: ActiveContextSnapshot): unknown[] => {
1682
- const rider = persistence.state?.rider;
1683
- if (!rider) return projected;
1684
- if (!carrierAdmitted("rider")) return projected;
1685
- projected.push({
1686
- role: "custom",
1687
- customType: riderProjectionType,
1688
- content: rider.text,
1689
- display: false,
1690
- details: {
1691
- source: activeContextSource(entryTypePrefix),
1692
- ephemeral: true,
1693
- riderEpoch: rider.epoch,
1694
- },
1695
- timestamp: typeof ownValue(snapshot.messages.at(-1), "timestamp") === "number"
1696
- ? ownValue(snapshot.messages.at(-1), "timestamp")
1697
- : 0,
1698
- });
1699
- return projected;
1700
- };
1701
-
1702
1902
  const holdFrozen = (projected: unknown[]): unknown[] => {
1703
1903
  freeze.projection = [...projected];
1704
1904
  freeze.active = false;
1705
1905
  return projected;
1706
1906
  };
1707
1907
 
1908
+ // An ephemeral peek result rides the projection exactly until the model's
1909
+ // next message, the same answered reading as the directed ask, then its
1910
+ // index holds a one-line placeholder and the reply is the surviving trace.
1911
+ // A message already projection-shaped (a fold's own deterministic brief
1912
+ // placeholder carries the same toolCallId) is never touched: only the raw
1913
+ // result is the ephemeral one.
1914
+ //
1915
+ // Returns what it replaced, keyed by call id, because the freeze has to be
1916
+ // told. Running against the BODY alone was not enough: the freeze compares
1917
+ // this pass's body prefix against the previous one and reuses the previous
1918
+ // PROJECTION when they match, and a withdrawal edits a body index inside
1919
+ // that prefix, so the comparison failed and the whole projection was rebuilt
1920
+ // from the body. The rebuild drops every receipt the freeze had
1921
+ // buried mid-array over the session, which moves divergence back to the
1922
+ // FIRST buried carrier rather than leaving it at the withdrawn index. That
1923
+ // is the opposite of what this mechanism is for: a tail edit was rewriting
1924
+ // the session's whole cached prefix.
1925
+ const withdrawConsumedEphemeralPeeks = (body: unknown[]): Map<string, unknown> => {
1926
+ const withdrawn = new Map<string, unknown>();
1927
+ if (!ephemeralPeeks.size) return withdrawn;
1928
+ for (let index = 0; index < body.length; index += 1) {
1929
+ const message = body[index];
1930
+ const callId = ownValue(message, "toolCallId");
1931
+ if (ownValue(message, "role") !== "toolResult" || typeof callId !== "string" ||
1932
+ !ephemeralPeeks.has(callId)) continue;
1933
+ const details = ownValue(message, "details");
1934
+ if (details && typeof ownValue(details, "projection") === "string") continue;
1935
+ const answered = body.slice(index + 1).some((later) =>
1936
+ ownValue(later, "role") === "assistant");
1937
+ if (!answered) continue;
1938
+ const placeholder = {
1939
+ role: "toolResult",
1940
+ toolCallId: callId,
1941
+ toolName: ownValue(message, "toolName"),
1942
+ content: [{ type: "text", text: "Ephemeral peek consumed; the reply that followed it is the surviving trace." }],
1943
+ isError: false,
1944
+ details: { projection: "ephemeral-peek-consumed" },
1945
+ timestamp: ownValue(message, "timestamp"),
1946
+ };
1947
+ body[index] = placeholder;
1948
+ withdrawn.set(callId, placeholder);
1949
+ }
1950
+ return withdrawn;
1951
+ };
1952
+
1953
+ // Carry a withdrawal into the freeze's own kept arrays so the frozen prefix
1954
+ // stays usable. Matched by CALL ID rather than by position: the projection
1955
+ // interleaves buried carriers, so a body index is not a projection index.
1956
+ // The substitution is identity-preserving on role, call id, tool name and
1957
+ // timestamp, so what changes is the one result's content and nothing else,
1958
+ // and divergence lands at the withdrawn index where the mechanism puts it.
1959
+ const carryWithdrawalIntoFreeze = (messages: unknown[], withdrawn: Map<string, unknown>): void => {
1960
+ for (let index = 0; index < messages.length; index += 1) {
1961
+ const message = messages[index];
1962
+ if (ownValue(message, "role") !== "toolResult") continue;
1963
+ const callId = ownValue(message, "toolCallId");
1964
+ if (typeof callId !== "string") continue;
1965
+ const placeholder = withdrawn.get(callId);
1966
+ if (!placeholder) continue;
1967
+ const details = ownValue(message, "details");
1968
+ if (details && typeof ownValue(details, "projection") === "string") continue;
1969
+ messages[index] = placeholder;
1970
+ }
1971
+ };
1972
+
1708
1973
  const projectWithAdvisory = (snapshot: ActiveContextSnapshot): unknown[] => {
1709
1974
  const body = projectActiveContext(snapshot, persistence.state!).filter((message) => {
1710
1975
  const customType = ownValue(message, "customType");
1711
1976
  return customType !== milestoneProjectionType && customType !== advisoryProjectionType &&
1712
- customType !== receiptProjectionType &&
1713
- customType !== riderProjectionType && customType !== curationProjectionType;
1977
+ customType !== receiptProjectionType && customType !== curationProjectionType &&
1978
+ customType !== foldNoticeProjectionType;
1714
1979
  });
1980
+ const withdrawn = withdrawConsumedEphemeralPeeks(body);
1981
+ if (withdrawn.size && freeze.body) {
1982
+ carryWithdrawalIntoFreeze(freeze.body, withdrawn);
1983
+ freeze.bodyText = stableStringify(freeze.body);
1984
+ if (freeze.projection) carryWithdrawalIntoFreeze(freeze.projection, withdrawn);
1985
+ }
1715
1986
  const held = freeze.body?.length ?? 0;
1716
1987
  freeze.active = freeze.projection !== null && body.length >= held &&
1717
1988
  stableStringify(body.slice(0, held)) === freeze.bodyText;
@@ -1722,7 +1993,7 @@ export function registerActiveContext(pi: any, options: {
1722
1993
  freeze.body = body;
1723
1994
  freeze.bodyText = stableStringify(body);
1724
1995
  appendReceipts(projected, snapshot);
1725
- appendRider(projected, snapshot);
1996
+ appendFoldNotice(projected, snapshot);
1726
1997
  return holdFrozen(projected);
1727
1998
  };
1728
1999
  const noteProjection = (projected: unknown[]): void => {
@@ -1738,8 +2009,27 @@ export function registerActiveContext(pi: any, options: {
1738
2009
  ? divergence.identicalChars / previousChars
1739
2010
  : null;
1740
2011
  const charsPerToken = projectionCharsPerToken();
2012
+ const successfulCacheActions = instrumentation.sinceHandoff.filter((event) =>
2013
+ event.kind === "context.attempt" && event.ok === true &&
2014
+ typeof event.action === "string" && CACHE_ACTION_REQUEST_CLASS[event.action]);
2015
+ const latestCacheAction = successfulCacheActions.at(-1) ?? null;
2016
+ const latestCacheActionClass = latestCacheAction?.action
2017
+ ? CACHE_ACTION_REQUEST_CLASS[latestCacheAction.action]
2018
+ : null;
2019
+ const projectionActions = successfulCacheActions.filter((event) =>
2020
+ event.action === "expand" || event.action === "refold");
1741
2021
  const causes = instrumentation.sinceHandoff.filter((event) =>
1742
- PREFIX_MUTATING_KINDS.has(event.kind));
2022
+ PREFIX_MUTATING_KINDS.has(event.kind) || projectionActions.includes(event));
2023
+ const structuralCause = causes.some((event) => event.kind !== "context.attempt");
2024
+ const requestClass = causes.some((event) => event.kind === "context.recovery")
2025
+ ? "after-rollback"
2026
+ : (latestCacheAction?.action === "expand" || latestCacheAction?.action === "refold")
2027
+ ? latestCacheActionClass!
2028
+ : structuralCause
2029
+ ? "after-fold"
2030
+ : latestCacheActionClass
2031
+ ? latestCacheActionClass
2032
+ : divergence.index === null ? "steady-state" : "after-message";
1743
2033
  const reading = projectedTokenReading(projected);
1744
2034
  emit("context.projection", {
1745
2035
  change: comparison.change,
@@ -1768,14 +2058,14 @@ export function registerActiveContext(pi: any, options: {
1768
2058
  estimated_tokens: Math.ceil(text.length / charsPerToken),
1769
2059
  cause: divergence.index === null
1770
2060
  ? "pure-append"
1771
- : (causes.length ? causes.map((event) => event.kind).join(",") : "unattributed"),
2061
+ : (causes.length ? causes.map((event) => event.kind === "context.attempt"
2062
+ ? `context.attempt:${String(event.action)}`
2063
+ : event.kind).join(",") : "unattributed"),
1772
2064
  cause_event_seqs: causes.map((event) => event.seq).join(","),
1773
2065
  events_since_handoff: instrumentation.sinceHandoff.length,
1774
- request_class: divergence.index === null
1775
- ? "steady-state"
1776
- : (causes.some((event) => event.kind === "context.recovery")
1777
- ? "after-rollback"
1778
- : (causes.length ? "after-fold" : "after-message")),
2066
+ cache_action: latestCacheAction?.action ?? null,
2067
+ cache_action_seq: latestCacheAction?.seq ?? null,
2068
+ request_class: requestClass,
1779
2069
  });
1780
2070
  instrumentation.previousText = text;
1781
2071
  instrumentation.sinceHandoff = [];
@@ -1852,6 +2142,7 @@ export function registerActiveContext(pi: any, options: {
1852
2142
  let state = persistence.state!;
1853
2143
  let peekAdded = 0;
1854
2144
  let topUpAdded = 0;
2145
+ let refoldAdded = 0;
1855
2146
  let consolidationAdded = 0;
1856
2147
  let closingAdded = 0;
1857
2148
  for (const mark of ephemeralPeekMarks({ snapshot, state, ordinal })) {
@@ -1862,7 +2153,6 @@ export function registerActiveContext(pi: any, options: {
1862
2153
  const addition = addPendingMark(state, mark);
1863
2154
  if (addition.added) { state = addition.state; consolidationAdded += 1; }
1864
2155
  }
1865
- const guarded = currentTurnRefKeys(snapshot);
1866
2156
  const capacity = servingCapacity(snapshot.contextWindow);
1867
2157
  const usedTokens = capacity.usedTokens;
1868
2158
  const budgetTokens = capacity.budgetTokens;
@@ -1879,40 +2169,60 @@ export function registerActiveContext(pi: any, options: {
1879
2169
  });
1880
2170
  return null;
1881
2171
  }
1882
- const freeingTarget = usedTokens === null || budgetTokens <= 0
1883
- ? 0
1884
- : Math.max(0, (usedTokens - thresholds.minTarget * budgetTokens) / budgetTokens);
1885
- if (topUp) {
1886
- for (const mark of topUpMarks({
1887
- snapshot,
1888
- state,
1889
- ordinal,
1890
- eligibleOnly: true,
1891
- targetShare: freeingTarget,
1892
- })) {
2172
+ // ONE DEFINITION OF THE DROP. The fill sizes itself to it, the advisory warns
2173
+ // against it, and the mark response answers "how much more" with it.
2174
+ const freeingTarget = commitCoverage({ snapshot, state, usedTokens, budgetTokens }).targetShare;
2175
+ // ONE COMMIT, SIZED BEFORE IT RUNS (Shane, 2026-08-21). At the threshold we know how
2176
+ // far the window has to come down: `freeingTarget` is the distance from what is used
2177
+ // now to the minTarget floor. The agent's marks count toward that distance first and
2178
+ // the ladder fills the remainder from the stale end, so an agent that marked 30k of a
2179
+ // 100k drop leaves 70k for the fill and both land in the SAME commit.
2180
+ //
2181
+ // Three rules decide the fill, and each replaced something that was tried first:
2182
+ //
2183
+ // The ladder does not STAND DOWN when the agent has marked. Standing down commits at
2184
+ // whatever depth the agent happened to reach, so one small mark commits a barely-moved
2185
+ // window and the next fence pass fires a second commit in the same cycle, which is the
2186
+ // cache cost the one-commit rule exists to prevent. `topUpMarks` measures progress
2187
+ // against the state it is handed, so agent marks are counted rather than displaced,
2188
+ // and `claimedRefKeys` keeps the fill off spans the agent already took.
2189
+ //
2190
+ // Raw stale material is spent BEFORE expanded folds, so an agent reading an expanded
2191
+ // fold keeps it while anything else can cover the drop. This is also the only route
2192
+ // to the refold rung now that nothing stages between commits.
2193
+ //
2194
+ // The fill measures progress over EVERY mark it makes, not just the ones this commit
2195
+ // will apply. Measuring the eligible share alone is what a starved commit needs and it
2196
+ // is wrong here: a mark over protected evidence moves the eligible share by nothing,
2197
+ // so the loop kept marking and walked a whole 24-batch excursion chasing a target it
2198
+ // could not reach that way (the retainer was the current-turn guard then; pins and
2199
+ // protected evidence retain the same way now). Counting everything bounds the fill at
2200
+ // the drop it was asked for. The backstop still reads the eligible share, because
2201
+ // starvation is the one state where the difference is the point.
2202
+ const fill = (targetShare: number, eligibleOnly: boolean): void => {
2203
+ for (const mark of topUpMarks({ snapshot, state, ordinal, eligibleOnly, targetShare })) {
1893
2204
  const addition = addPendingMark(state, mark);
1894
2205
  if (addition.added) { state = addition.state; topUpAdded += 1; }
1895
2206
  }
2207
+ for (const mark of refoldMarks({ snapshot, state, ordinal, eligibleOnly, targetShare })) {
2208
+ const addition = addPendingMark(state, mark);
2209
+ if (addition.added) { state = addition.state; refoldAdded += 1; }
2210
+ }
2211
+ };
2212
+ let backstopFired = false;
2213
+ if (topUp) {
2214
+ fill(freeingTarget, false);
1896
2215
  const reachedShare = markAccounting(snapshot, state).eligibleFreedBudgetShare;
1897
2216
  const shallow = reachedShare < Math.max(commitDepthFloorShare(snapshot), freeingTarget);
1898
2217
  if (shallow && atOrAboveBackstop(snapshot, waiverRatio)) {
1899
- for (const mark of topUpMarks({
1900
- snapshot,
1901
- state,
1902
- ordinal,
1903
- eligibleOnly: true,
1904
- targetShare: 1,
1905
- })) {
1906
- const addition = addPendingMark(state, mark);
1907
- if (addition.added) { state = addition.state; topUpAdded += 1; }
1908
- }
2218
+ backstopFired = true;
2219
+ fill(1, true);
1909
2220
  }
1910
2221
  }
1911
2222
  const wedges = absorbWedgeMarks({
1912
2223
  snapshot,
1913
2224
  state,
1914
2225
  charsPerToken: projectionCharsPerToken(),
1915
- excludeRefKeys: guarded,
1916
2226
  });
1917
2227
  state = wedges.state;
1918
2228
  const accounting = markAccounting(snapshot, state);
@@ -1942,49 +2252,80 @@ export function registerActiveContext(pi: any, options: {
1942
2252
  eligible_marks: accounting.eligibleMarks,
1943
2253
  eligible_freed_share: accounting.eligibleFreedBudgetShare,
1944
2254
  reclaim_floor_share: COMMIT_RECLAIM_FLOOR_SHARE,
2255
+ // THE REMAINDER RIDES EVERY DEFERRAL, not just the empty one. It was on
2256
+ // `nothing-proposable` alone, which was the only starved shape while staging
2257
+ // happened at commit time. The frontier cuts ahead of the commit, so a starved
2258
+ // pass now usually arrives holding a mark it cannot apply and lands HERE instead,
2259
+ // and "below the reclaim floor" without the remainder does not tell a reader
2260
+ // whether the mass is held or simply absent. That is the distinction the whole
2261
+ // record exists to draw.
2262
+ unmarked_stale_spans: unmarkedRemainder(snapshot, state, projectionCharsPerToken()).spans,
1945
2263
  window_tokens: snapshot.contextWindow,
1946
2264
  });
1947
2265
  return null;
1948
2266
  }
1949
2267
  const bytesBefore = bytes(projectActiveContext(snapshot, state));
1950
- const guardWaiver = guardWaiverCount({
1951
- snapshot,
1952
- ratio: waiverRatio,
1953
- boundary: trigger === "compaction-boundary",
1954
- guardedMarks: pendingMarks(state).filter((mark) =>
1955
- markTouchesCurrentTurn(state, mark, guarded)).length,
1956
- otherApplicableMarks: pendingMarks(state).filter((mark) =>
1957
- !markTouchesCurrentTurn(state, mark, guarded) &&
1958
- markEligibility(snapshot, state, mark) === "eligible").length,
1959
- });
2268
+ // THE COMMIT CUTS ONLY AS DEEP AS THE BUDGET ASKS (Shane, 2026-08-23). The frontier
2269
+ // stages as material arrives, so by now the pending set is most of the window; without
2270
+ // this bound the first commit would fold all of it. `freeingTarget` is a share of the
2271
+ // budget and the bound is bytes, so it is converted once, here, through the same ratio
2272
+ // the projection weighs itself with.
2273
+ //
2274
+ // It binds on the ROUTINE path only: a null ratio is the band top, ordinary
2275
+ // housekeeping at a stated threshold, and that is where "only as deep as it needs"
2276
+ // belongs. The fence and the compaction boundary carry a ratio because something has
2277
+ // already gone wrong there, and a lane holding a request that will not fit is not the
2278
+ // place to leave depth on the table. The backstop is exempt for the same reason: it
2279
+ // fires only when the commit is already starving.
2280
+ //
2281
+ // This bound is also what protects the working set since the current-turn guard was
2282
+ // deleted (Shane, 2026-08-23, from the first live session): the cut takes stalest
2283
+ // first and stops at the aim, so the newest events stay raw exactly as long as there
2284
+ // is room for them, in every session shape including one that never closes a turn.
1960
2285
  let result = await commitPendingMarks({
1961
2286
  snapshot,
1962
2287
  state,
1963
2288
  generation: lifecycle.generation,
1964
2289
  retainIneligible: true,
1965
- guardCurrentTurn: true,
1966
- guardWaiver,
2290
+ ...(backstopFired || waiverRatio !== null ? {} : {
2291
+ applyTargetBytes: Math.ceil(freeingTarget * budgetTokens * projectionCharsPerToken()),
2292
+ }),
1967
2293
  });
1968
2294
  for (;;) {
2295
+ // THE CLOSING PASS COMMITS ONLY WHAT IT SEATS (2026-08-23). It rides the same paid
2296
+ // rewrite to give a group its parent, and it runs UNBOUNDED because a parent's whole
2297
+ // span must land together. Handing it the full pending set therefore leaked the depth
2298
+ // cut: the marks the bound had just retained were still pending, the closing commit
2299
+ // carried no bound, and the first band-top commit after the guard deletion folded all
2300
+ // 24 of them to the floor (gate 09's fixture, applied 26 of 24 with deferred 0). The
2301
+ // marks the cut held back are set aside before the closing commit and restored after,
2302
+ // so the only thing an unbounded closing pass can spend is the parents it just made.
1969
2303
  let closing = result.state;
2304
+ const heldBack = pendingMarks(closing);
2305
+ let seeded = withPendingMarks(closing, []);
2306
+ let seededAdded = 0;
1970
2307
  for (const mark of consolidationMarks({ snapshot, state: closing, ordinal })) {
1971
2308
  const addition = addPendingMark(closing, mark);
1972
- if (addition.added) { closing = addition.state; closingAdded += 1; }
2309
+ if (!addition.added) continue;
2310
+ closing = addition.state;
2311
+ const carried = addPendingMark(seeded, mark);
2312
+ if (carried.added) { seeded = carried.state; closingAdded += 1; seededAdded += 1; }
1973
2313
  }
1974
- if (closing === result.state) break;
2314
+ if (!seededAdded) break;
1975
2315
  const closed = await commitPendingMarks({
1976
2316
  snapshot,
1977
- state: closing,
2317
+ state: seeded,
1978
2318
  generation: lifecycle.generation,
1979
2319
  retainIneligible: true,
1980
- guardCurrentTurn: true,
1981
2320
  });
1982
2321
  if (!closed.applied.length) break;
1983
2322
  result = {
1984
2323
  ...closed,
2324
+ state: withPendingMarks(closed.state, [...pendingMarks(closed.state), ...heldBack]),
1985
2325
  applied: [...result.applied, ...closed.applied],
1986
2326
  refused: [...result.refused.filter((mark) => !mark.retained), ...closed.refused],
1987
- waived: [...result.waived, ...closed.waived],
2327
+ retained: [...result.retained.filter((mark) =>
2328
+ !closed.applied.some((appliedMark) => appliedMark.id === mark.id)), ...closed.retained],
1988
2329
  };
1989
2330
  }
1990
2331
  persistence.state = result.state;
@@ -2012,12 +2353,12 @@ export function registerActiveContext(pi: any, options: {
2012
2353
  applied_marks: result.applied.length,
2013
2354
  refused_marks: result.refused.filter((mark) => !mark.retained).length,
2014
2355
  deferred_marks: result.retained.length,
2015
- waived_marks: result.waived.length,
2016
2356
  pending_marks: accounting.pending,
2017
2357
  agent_marks: accounting.agentMarks,
2018
2358
  ladder_marks: accounting.ladderMarks,
2019
2359
  peek_marks: peekAdded,
2020
2360
  topup_marks: topUpAdded,
2361
+ refold_marks: refoldAdded,
2021
2362
  consolidation_marks: consolidationAdded,
2022
2363
  closing_consolidation_marks: closingAdded,
2023
2364
  absorbed_wedges: wedges.absorbed.length,
@@ -2033,36 +2374,6 @@ export function registerActiveContext(pi: any, options: {
2033
2374
  instrumentation.mutationsSinceHandoff += 1;
2034
2375
  instrumentation.lastMutationRequest = instrumentation.requests;
2035
2376
  instrumentation.lastMutationTokens = usedTokens;
2036
- const riderEpoch = commitEvent.seq;
2037
- if (persistence.state.rider?.epoch !== riderEpoch) {
2038
- const postAccounting = markAccounting(snapshot, persistence.state);
2039
- const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
2040
- const pinnedMass = explicitProtectedMass(snapshot, persistence.state);
2041
- const pinnedShare = budgetTokens > 0
2042
- ? estimatedTokens(pinnedMass.bytes) / budgetTokens
2043
- : 0;
2044
- const riderText = contextRiderText({
2045
- toolName,
2046
- brandNoun,
2047
- pendingAgentMarks: postAccounting.agentMarks,
2048
- eligibleMarks: postAccounting.eligibleMarks,
2049
- freedTokens: postAccounting.freedTokens,
2050
- eligibleFreedTokens: postAccounting.eligibleFreedTokens,
2051
- anchors: remainder.candidates.slice(0, 3).map((candidate) => candidate.id),
2052
- pinnedShare,
2053
- maxPinnedShare: MAX_PINNED_SHARE,
2054
- });
2055
- persistence.state = { ...persistence.state, rider: { epoch: riderEpoch, text: riderText } };
2056
- emit("context.rider", {
2057
- epoch: riderEpoch,
2058
- chars: riderText.length,
2059
- anchors: Math.min(3, remainder.candidates.length),
2060
- pending_agent_marks: postAccounting.agentMarks,
2061
- eligible_marks: postAccounting.eligibleMarks,
2062
- pinned_share: pinnedShare,
2063
- max_pinned_share: MAX_PINNED_SHARE,
2064
- });
2065
- }
2066
2377
  let foldedToolResults = 0;
2067
2378
  let foldedSpans = 0;
2068
2379
  for (const applied of result.applied) {
@@ -2093,7 +2404,8 @@ export function registerActiveContext(pi: any, options: {
2093
2404
  end_id: wedge.endId,
2094
2405
  entries: wedge.entries,
2095
2406
  tokens: wedge.tokens,
2096
- threshold_tokens: MAX_WEDGE_ABSORB_TOKENS,
2407
+ chars: wedge.chars,
2408
+ threshold_chars: thresholds.minFoldChars,
2097
2409
  });
2098
2410
  }
2099
2411
  const epoch = {
@@ -2105,6 +2417,7 @@ export function registerActiveContext(pi: any, options: {
2105
2417
  ladderMarks: accounting.ladderMarks,
2106
2418
  peekMarks: peekAdded,
2107
2419
  topUpMarks: topUpAdded,
2420
+ refoldMarks: refoldAdded,
2108
2421
  consolidationMarks: consolidationAdded,
2109
2422
  closingMarks: closingAdded,
2110
2423
  absorbedWedges: wedges.absorbed.length,
@@ -2114,8 +2427,6 @@ export function registerActiveContext(pi: any, options: {
2114
2427
  occupancyTokensBefore: usedTokens,
2115
2428
  depthFloorShare: commitDepthFloorShare(snapshot),
2116
2429
  reclaimFloorShare: COMMIT_RECLAIM_FLOOR_SHARE,
2117
- guardWaived: result.waived.length > 0,
2118
- waivedMarks: result.waived.length,
2119
2430
  appliedMarks: result.applied.length,
2120
2431
  refusedMarks: result.refused.filter((mark) => !mark.retained).length,
2121
2432
  deferredMarks: result.retained.length,
@@ -2124,8 +2435,6 @@ export function registerActiveContext(pi: any, options: {
2124
2435
  protectedStaleBytes: pinHeld.bytes,
2125
2436
  protectedStaleRefs: pinHeld.refs,
2126
2437
  retainedMarks: result.retained.length,
2127
- currentTurnRetained: result.retained.filter((mark) =>
2128
- markTouchesCurrentTurn(state, mark, guarded)).length,
2129
2438
  eligibleMarks: accounting.eligibleMarks,
2130
2439
  estimatedRewriteTokens: accounting.rewriteTokens,
2131
2440
  estimatedFreedTokens: accounting.freedTokens,
@@ -2186,59 +2495,14 @@ export function registerActiveContext(pi: any, options: {
2186
2495
  }));
2187
2496
  };
2188
2497
 
2189
- const applyAutomaticRung = async (
2190
- snapshot: ActiveContextSnapshot,
2191
- ratio: number,
2192
- rungOptions: {
2193
- toolOnly?: boolean;
2194
- waiverRatio?: number;
2195
- } = {},
2196
- ): Promise<Record<string, unknown> | null> => {
2197
- if (!persistence.state || ladder.automaticFailure || ladder.preparing) return null;
2198
- const rungSelectionOptions = { toolOnly: rungOptions.toolOnly };
2199
- const markLadderSelection = (): Record<string, unknown> | null => {
2200
- if (!persistence.state) return null;
2201
- const decision = selectAutomaticRung(snapshot, persistence.state, ratio, {
2202
- ...rungSelectionOptions,
2203
- claimed: claimedRefKeys(persistence.state),
2204
- claimedFoldIds: markedFoldIds(persistence.state),
2205
- });
2206
- const mark = decision
2207
- ? ladderSelectionMark({
2208
- snapshot, state: persistence.state, selection: decision, ordinal: markOrdinal(snapshot),
2209
- })
2210
- : null;
2211
- if (!mark) return null;
2212
- const addition = addPendingMark(persistence.state, mark);
2213
- if (!addition.added) return null;
2214
- persistence.state = addition.state;
2215
- ladder.pendingContextNote =
2216
- `Pending ${mark.mark} mark ${mark.id} recorded; no context bytes moved. ` +
2217
- "It applies at the next commit epoch.";
2218
- ladder.lastAutomaticAction = {
2219
- kind: "mark",
2220
- foldIds: [mark.id],
2221
- sourceIds: [],
2222
- markKind: mark.mark,
2223
- markOrigin: mark.origin,
2224
- pendingMarks: pendingMarks(persistence.state).length,
2225
- sourceBytesSaved: 0,
2226
- };
2227
- return ladder.lastAutomaticAction;
2228
- };
2229
- // MARKING ONLY. Every mutation the rung used to perform inline -- a prepared chapter,
2230
- // a tool fold, a refold, a consolidation, a chapter split -- now travels as a pending
2231
- // mark and lands at the boundary, which is the one point the projection changes.
2232
- // WHAT the rung decides is unchanged; WHEN it takes effect is.
2233
- return markLadderSelection();
2234
- };
2235
2498
  /**
2236
- * The one durable transaction. Whatever it wraps either lands with its state persisted
2237
- * or leaves the state it entered with, and a failure suspends folding by name rather
2238
- * than being retried quietly. Marking and committing both go through it, which is why
2239
- * a commit that vanishes at persistence (gate 122) and a suspension that says nothing
2240
- * (gate 123) are single properties rather than one per caller.
2499
+ * A COMMIT, THROUGH THE SAME TRANSACTION AND THE SAME QUEUE. Two callers reach it: the
2500
+ * compaction boundary, which is the ordinary one, and the projection fence, which is
2501
+ * the emergency one. The fence cannot wait for a boundary because the request it is
2502
+ * holding does not fit, so it commits in place; that is the one exception, and it is
2503
+ * the exception that already existed.
2241
2504
  */
2505
+
2242
2506
  const runAutomaticTransaction = async (
2243
2507
  snapshot: ActiveContextSnapshot,
2244
2508
  ratio: number,
@@ -2291,25 +2555,6 @@ export function registerActiveContext(pi: any, options: {
2291
2555
  ladder.actionQueue = queued.catch(() => undefined);
2292
2556
  return queued;
2293
2557
  };
2294
- const attemptAutomaticRung = (
2295
- snapshot: ActiveContextSnapshot,
2296
- ratio: number,
2297
- ctx: any,
2298
- phase: string,
2299
- rungOptions: {
2300
- toolOnly?: boolean;
2301
- waiverRatio?: number;
2302
- } = {},
2303
- ): Promise<Record<string, unknown> | null> => queueAutomatic(
2304
- snapshot, ratio, ctx, phase, () => applyAutomaticRung(snapshot, ratio, rungOptions),
2305
- );
2306
- /**
2307
- * A COMMIT, THROUGH THE SAME TRANSACTION AND THE SAME QUEUE. Two callers reach it: the
2308
- * compaction boundary, which is the ordinary one, and the projection fence, which is
2309
- * the emergency one. The fence cannot wait for a boundary because the request it is
2310
- * holding does not fit, so it commits in place; that is the one exception, and it is
2311
- * the exception that already existed.
2312
- */
2313
2558
  const attemptAutomaticCommit = (
2314
2559
  snapshot: ActiveContextSnapshot,
2315
2560
  ctx: any,
@@ -2495,14 +2740,11 @@ export function registerActiveContext(pi: any, options: {
2495
2740
  return { messages: event.messages };
2496
2741
  }
2497
2742
  }
2498
- if (selectAutomaticToolBatch(snapshot, persistence.state)[0]) mutationAttempted = true;
2499
- const action = await attemptAutomaticRung(
2500
- snapshot, measurements.latestRatio, ctx, "context",
2501
- );
2502
- if (action) {
2503
- mutationAttempted = true;
2504
- persistedSucceeded = true;
2505
- }
2743
+ // NO EAGER LADDER MARKING (Shane, 2026-08-21): the rung used to stage a mark
2744
+ // here on every measured response, which crowded the agent out of curation
2745
+ // entirely -- by the time an agent looked, every eligible batch was already
2746
+ // marked. The commit epoch fills fresh at commit time (topUpMarks), which is
2747
+ // the only automatic marking there is.
2506
2748
  } else measurements.lastProviderMeasurement = observedMeasurement;
2507
2749
  } else {
2508
2750
  measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
@@ -2524,6 +2766,23 @@ export function registerActiveContext(pi: any, options: {
2524
2766
  updateStatus(ctx);
2525
2767
  return { messages: event.messages };
2526
2768
  }
2769
+ // THE FRONTIER CUTS FIRST, and it costs the projection nothing to do so: a pending
2770
+ // mark moves no bytes, so this runs BEFORE the band top and the fence and changes
2771
+ // neither of their inputs. What it changes is WHEN the agent learns a fold exists,
2772
+ // which is now while the material it covers is still in front of it.
2773
+ advanceFoldFrontier(snapshot);
2774
+ // BEFORE the fence, so an ordinary crossing is an ordinary commit and the fence is
2775
+ // left holding only the requests that genuinely will not fit.
2776
+ if (await enforceBandTop(snapshot, ctx)) {
2777
+ mutationAttempted = true;
2778
+ persistedSucceeded = true;
2779
+ try { projected = projectWithAdvisory(snapshot); }
2780
+ catch (error) {
2781
+ suspendAutomatic(error, "projection", ctx);
2782
+ abortUnsafeHardContext(snapshot, ctx);
2783
+ return { messages: event.messages };
2784
+ }
2785
+ }
2527
2786
  const budgeted = await enforceProjectionBudget(snapshot, projected, ctx);
2528
2787
  projected = budgeted.projected;
2529
2788
  const reading = projectedTokenReading(projected);
@@ -2598,13 +2857,7 @@ export function registerActiveContext(pi: any, options: {
2598
2857
  if (measurements.latestRatio >= hardFenceRatio(measurement, ctx) && ladder.preparing) await ladder.preparing.promise;
2599
2858
  if (!sessionIdentityStillValid(ctx, capturedSessionId, capturedGeneration) ||
2600
2859
  !durableProviderMeasurementMatches(measurement)) return;
2601
- const action = await attemptAutomaticRung(
2602
- authoritativeSnapshotFor(ctx),
2603
- measurements.latestRatio,
2604
- ctx,
2605
- "message-end",
2606
- );
2607
- if (!action && measurementStateChanged && persistence.state && persistence.persisted &&
2860
+ if (measurementStateChanged && persistence.state && persistence.persisted &&
2608
2861
  !sameStateProjection(persistence.state, persistence.persisted)) {
2609
2862
  await persistThroughActionQueue(ctx);
2610
2863
  }
@@ -2629,6 +2882,7 @@ export function registerActiveContext(pi: any, options: {
2629
2882
  model: ownValue(message, "model") ?? null,
2630
2883
  input_tokens: observation.inputTokens,
2631
2884
  cache_read_tokens: observation.cacheReadTokens,
2885
+ cache_write_tokens: observation.cacheWriteTokens,
2632
2886
  total_tokens: providerTokens(message),
2633
2887
  projection_change: observation.change,
2634
2888
  provider_side_miss: observation.providerSideMiss,
@@ -2672,7 +2926,7 @@ export function registerActiveContext(pi: any, options: {
2672
2926
  durableProviderMeasurementMatches(measurements.lastProviderMeasurement)) {
2673
2927
  startPreparation(snapshot, measurements.latestRatio, ctx);
2674
2928
  if (measurements.latestRatio >= hardFenceRatio(snapshot) && ladder.preparing) await ladder.preparing.promise;
2675
- await attemptAutomaticRung(snapshot, measurements.latestRatio, ctx, "turn-end");
2929
+ // No rung here either: turn end stages nothing (see the context-path note).
2676
2930
  }
2677
2931
  ladder.pendingManual = false;
2678
2932
  if (!ladder.automaticFailure) ladder.boundaryFailure = null;
@@ -3001,6 +3255,203 @@ export function registerActiveContext(pi: any, options: {
3001
3255
  );
3002
3256
  };
3003
3257
 
3258
+ /**
3259
+ * THE ONE STAGING PATH FOR FOLD MARKS (2026-08-23). The tool's fold action and the
3260
+ * /fold-editor's user marks run THIS code and nothing else: span snapping, claim
3261
+ * refusal, brief contract, prepareFold, replacement of a standing mark over the
3262
+ * same span. Only the ORIGIN differs -- "agent" for the model, "user" for a human
3263
+ * laying marks in the editor -- and origin changes nothing mechanical: a user mark
3264
+ * counts toward the commit's coverage, answers with its commit arithmetic, and
3265
+ * folds at the epoch exactly as an agent mark does.
3266
+ */
3267
+ /**
3268
+ * THE WITHDRAWAL PATH, shared by the tool's unmark action and the editor's
3269
+ * withdraw intent: same refusal for an unknown id, same persist, same status.
3270
+ */
3271
+ const withdrawMarks = async (ids: string[], ctx: any): Promise<Array<Record<string, unknown>>> => {
3272
+ const requested = new Set(ids);
3273
+ const before = pendingMarks(persistence.state!);
3274
+ const kept = before.filter((mark) => !requested.has(mark.id));
3275
+ const removed = before.filter((mark) => requested.has(mark.id));
3276
+ const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
3277
+ if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
3278
+ await persistManual(withPendingMarks(persistence.state!, kept), "unmark", ctx);
3279
+ updateStatus(ctx);
3280
+ return removed as Array<Record<string, unknown>>;
3281
+ };
3282
+
3283
+ /**
3284
+ * THE PIN CHANGE PATH, shared by the tool's pin/unpin actions and the editor's
3285
+ * pin toggle: the pinned-share cap, protectEvidence, the canonical event, persist.
3286
+ */
3287
+ const applyProtectionChange = async (
3288
+ snapshot: ActiveContextSnapshot,
3289
+ ids: string[],
3290
+ pin: boolean,
3291
+ ctx: any,
3292
+ ): Promise<void> => {
3293
+ const refsBefore = persistence.state!.protected.length;
3294
+ const nextProtection = protectEvidence(snapshot, persistence.state!, ids, pin);
3295
+ if (pin) {
3296
+ const capacity = servingCapacity(snapshot.contextWindow);
3297
+ const prospectiveShare = capacity.budgetTokens > 0
3298
+ ? estimatedTokens(explicitProtectedMass(snapshot, nextProtection).bytes) / capacity.budgetTokens
3299
+ : 0;
3300
+ if (prospectiveShare > MAX_PINNED_SHARE) {
3301
+ throw new Error(
3302
+ `protect refused: these pins would hold ${Math.round(prospectiveShare * 100)}% of the ` +
3303
+ `working window raw, past the ${Math.round(MAX_PINNED_SHARE * 100)}% pinned-share cap. ` +
3304
+ 'Release earlier pins with {"action":"unpin","ids":["<entry-id>"]} first; ' +
3305
+ "folding keeps entries exactly recoverable without pinning them.");
3306
+ }
3307
+ }
3308
+ await persistManual(nextProtection, pin ? "pin" : "unpin", ctx);
3309
+ emit("context.pin", {
3310
+ pin,
3311
+ ids: ids.join(","),
3312
+ id_count: ids.length,
3313
+ protected_refs_before: refsBefore,
3314
+ protected_refs_after: persistence.state!.protected.length,
3315
+ });
3316
+ updateStatus(ctx);
3317
+ };
3318
+
3319
+ const stageFoldMarks = async (
3320
+ requests: BatchedMarkRequest[],
3321
+ origin: MarkOrigin,
3322
+ signal: AbortSignal,
3323
+ ctx: any,
3324
+ snapshot: ActiveContextSnapshot,
3325
+ ): Promise<{ marks: Array<Record<string, unknown>>; corrections: SpanCorrection[]; staged: ActiveContextState }> => {
3326
+ const resolved: Array<{
3327
+ candidate: FoldCandidate;
3328
+ brief?: string;
3329
+ corrections: SpanCorrection[];
3330
+ splitFrom: number;
3331
+ }> = [];
3332
+ for (const request of requests) {
3333
+ const snapped = snapFoldCandidate(snapshot, persistence.state, request.ids, { allowProtected: true });
3334
+ // AN AGENT'S SPAN IS TAKEN WHOLE (Shane, 2026-08-22). The bite-size cap was
3335
+ // applied to manual marks too, and it was the only caller: the ladder never
3336
+ // splits, because `selectAutomaticChapter` stops accumulating at the cap on its
3337
+ // own. So the split existed solely to second-guess the one judgement the design
3338
+ // wants to encourage, and it charged for it twice. One intended fold became five
3339
+ // or ten placeholders, and every piece was handed the SAME brief, so each one
3340
+ // claimed to describe a span it held a fraction of, while the correction text
3341
+ // said "each with its own brief", which was never true.
3342
+ //
3343
+ // The cap stays for the ladder, where it was earned (2026-08-06 rep 6: one
3344
+ // 60,432-byte chapter hid the fact the run needed). The difference is who chose
3345
+ // the boundary. A ladder fold is a span nobody vouched for, so it is kept small
3346
+ // enough to read back cheaply. An agent fold is a span the agent cut and wrote a
3347
+ // brief for, and it stays navigable by a route that did not exist in 2026-08:
3348
+ // peek takes offset and bytes, states the omitted middle, and reads any child id,
3349
+ // so a large fold has a narrow read. An expand that will not fit is refused with
3350
+ // a peek offered instead, so size costs a refusal rather than a window.
3351
+ resolved.push({
3352
+ candidate: snapped.candidate,
3353
+ brief: request.brief,
3354
+ corrections: snapped.corrections,
3355
+ splitFrom: 0,
3356
+ });
3357
+ }
3358
+ const corrections = resolved.flatMap((item) => item.corrections);
3359
+ const marks: Array<Record<string, unknown>> = [];
3360
+ let staged = persistence.state!;
3361
+ for (const item of resolved) {
3362
+ const { candidate } = item;
3363
+ const alreadyPrepared = staged.folds.some((fold) =>
3364
+ fold.id === foldIdFor(candidate.kind, candidate.parts));
3365
+ const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
3366
+ (candidate.kind === "tool-result" &&
3367
+ refsProtected(candidate.sourceRefs, staged, snapshot));
3368
+ // A MARK IS EDITABLE UNTIL IT COMMITS (Shane, 2026-08-21). Marking a span you
3369
+ // already marked REPLACES the mark, so a brief written in haste is corrected by
3370
+ // writing it again: the span is identical, the id derives from the span, and no
3371
+ // byte has moved yet, so the correction costs nothing at all. After the commit
3372
+ // the fold is standing and there is no rewrite verb, because rewriting a
3373
+ // standing brief rewrites the projection at its placeholder; `reboundary`
3374
+ // re-cuts the span instead, which was going to rewrite anyway.
3375
+ const replacing = foldIdFor(candidate.kind, candidate.parts);
3376
+ const claimed = markClaimingRef(staged, candidate.sourceRefs);
3377
+ if (claimed && claimed.markId !== replacing) {
3378
+ marks.push({
3379
+ id: replacing,
3380
+ kind: candidate.kind,
3381
+ ok: false,
3382
+ deferred: false,
3383
+ reason: `Evidence ${claimed.entryId} is already held by pending mark ${claimed.markId}, ` +
3384
+ `which folds at the next commit. Unmark ${claimed.markId} first if you want to ` +
3385
+ "mark this span differently.",
3386
+ });
3387
+ continue;
3388
+ }
3389
+ const suppliedComplaint = item.brief === undefined
3390
+ ? null
3391
+ : briefContractComplaint(item.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
3392
+ if (suppliedComplaint !== null) {
3393
+ marks.push({
3394
+ id: foldIdFor(candidate.kind, candidate.parts),
3395
+ kind: candidate.kind,
3396
+ ok: false,
3397
+ deferred: false,
3398
+ reason: `Supplied brief rejected. ${suppliedComplaint}`,
3399
+ });
3400
+ continue;
3401
+ }
3402
+ const briefed = !deferred && !alreadyPrepared
3403
+ ? await prepareFold({
3404
+ candidate,
3405
+ snapshot,
3406
+ state: staged,
3407
+ generation: lifecycle.generation,
3408
+ brief: item.brief,
3409
+ ctx,
3410
+ signal,
3411
+ })
3412
+ : null;
3413
+ const mark = foldMarkFor({
3414
+ candidate,
3415
+ brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
3416
+ briefProvenance: briefed?.fold.provenance ??
3417
+ (item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
3418
+ origin,
3419
+ ordinal: markOrdinal(snapshot),
3420
+ });
3421
+ // Drop the standing mark over this exact span before adding, so a re-mark is a
3422
+ // replacement rather than a duplicate refusal. `claimed` is this same mark by the
3423
+ // check above, and the id is derived from the span, so the state after is the
3424
+ // state a first mark with the new brief would have produced.
3425
+ const replaced = claimed !== null;
3426
+ if (replaced) {
3427
+ staged = withPendingMarks(staged,
3428
+ pendingMarks(staged).filter((standing) => standing.id !== replacing));
3429
+ }
3430
+ const addition = addPendingMark(staged, mark);
3431
+ if (!addition.added) {
3432
+ marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
3433
+ continue;
3434
+ }
3435
+ staged = addition.state;
3436
+ marks.push({
3437
+ id: mark.id,
3438
+ kind: mark.kind,
3439
+ ok: true,
3440
+ deferred,
3441
+ ...(replaced ? { replaced: true } : {}),
3442
+ ...(deferred
3443
+ ? {
3444
+ scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
3445
+ "first commit after it ages out",
3446
+ }
3447
+ : {}),
3448
+ brief: mark.brief,
3449
+ provenance: normalizeLegacyProvenance(mark.briefProvenance),
3450
+ });
3451
+ }
3452
+ return { marks, corrections, staged };
3453
+ };
3454
+
3004
3455
  const executeAction = async (
3005
3456
  params: Record<string, unknown>,
3006
3457
  signal: AbortSignal | undefined,
@@ -3010,6 +3461,11 @@ export function registerActiveContext(pi: any, options: {
3010
3461
  if (!persistence.state) persistence.state = emptyActiveContextState(ctx.sessionManager.getSessionId());
3011
3462
  const executionArgumentsSha256 = sha256Value(params);
3012
3463
  const action = String(params.action ?? "");
3464
+ // A deleted action is refused BY NAME rather than by "not enabled": the two are
3465
+ // different facts, and an agent told a verb is merely off will keep offering it.
3466
+ if (Object.prototype.hasOwnProperty.call(RETIRED_TOOL_ACTIONS, action)) {
3467
+ throw new Error(RETIRED_TOOL_ACTIONS[action]);
3468
+ }
3013
3469
  if (!allowedToolActionSet.has(action)) {
3014
3470
  throw new Error(`${toolName} action '${action}' is not enabled in this runtime`);
3015
3471
  }
@@ -3038,7 +3494,6 @@ export function registerActiveContext(pi: any, options: {
3038
3494
  persistence.state,
3039
3495
  statusOffset,
3040
3496
  statusLimit,
3041
- snapshot.policy.maxFoldSourceRefs,
3042
3497
  { diet: !paged },
3043
3498
  ),
3044
3499
  headroomTokens: currentCapacity(ctx).headroomTokens,
@@ -3056,7 +3511,6 @@ export function registerActiveContext(pi: any, options: {
3056
3511
  hardFenceRatio: hardFenceRatio(snapshot),
3057
3512
  thresholds: { ...snapshot.thresholds },
3058
3513
  zones: {
3059
- freshBoundary: snapshot.freshBoundary,
3060
3514
  budgetTokens: snapshot.budgetTokens,
3061
3515
  },
3062
3516
  responseReserve: currentCapacity(ctx).outputReservation,
@@ -3121,6 +3575,19 @@ export function registerActiveContext(pi: any, options: {
3121
3575
  state: persistence.state,
3122
3576
  ratio: measurements.latestRatio,
3123
3577
  }),
3578
+ // WHAT THE RUNTIME HAS CUT AND NOT YET FOLDED, by name, because the agent needs
3579
+ // an id to brief one and a notice it saw two turns ago is not a reference. Read
3580
+ // from the LIVE state rather than the durable record: a frontier cut is derived
3581
+ // from its span and is not written on its own, so the durable record is behind
3582
+ // by design and only a brief or a commit brings it forward.
3583
+ pendingFolds: pendingMarks(persistence.state).map((mark) => ({
3584
+ id: mark.id,
3585
+ kind: mark.kind,
3586
+ tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
3587
+ briefed: mark.briefProvenance?.kind === "supplied",
3588
+ brief: mark.brief ?? null,
3589
+ briefProvenance: mark.briefProvenance ? clone(mark.briefProvenance) : null,
3590
+ })),
3124
3591
  nativeSummaries: "disabled",
3125
3592
  instrumentation: {
3126
3593
  enabled: true,
@@ -3152,12 +3619,15 @@ export function registerActiveContext(pi: any, options: {
3152
3619
  if (action === "peek") {
3153
3620
  const id = String(params.id ?? "").trim();
3154
3621
  if (!id) throw new Error("peek requires id");
3622
+ if (params.ephemeral !== undefined && typeof params.ephemeral !== "boolean") {
3623
+ throw new Error("peek ephemeral must be a boolean");
3624
+ }
3155
3625
  const offset = boundedInteger(params.offset, 0, 0, 1_000_000_000, "offset");
3156
3626
  const sliceBytes = boundedInteger(
3157
3627
  params.bytes,
3158
3628
  PEEK_DEFAULT_MAX_BYTES,
3159
3629
  PEEK_MIN_SLICE_BYTES,
3160
- snapshot.policy.maxChapterChars,
3630
+ snapshot.policy.maxSourceChars,
3161
3631
  "bytes",
3162
3632
  );
3163
3633
  const target = persistence.state.folds.find((item) => item.id === id);
@@ -3169,40 +3639,24 @@ export function registerActiveContext(pi: any, options: {
3169
3639
  requestedBytes: Math.max(0, Math.min(sliceBytes, target.sourceChars - offset)),
3170
3640
  children: childFoldIds(target),
3171
3641
  });
3172
- const payload = toolPayload(peekFoldSource({
3173
- foldId: id,
3174
- state: persistence.state,
3175
- entries: ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch(),
3176
- sessionId: ctx.sessionManager.getSessionId(),
3177
- maximumBytes: sliceBytes,
3178
- offset,
3179
- toolName,
3180
- }));
3181
- return payload;
3182
- }
3183
- if (action === "rebrief") {
3184
- const id = String(params.id ?? "").trim();
3185
- if (!id) throw new Error("rebrief requires id");
3186
- const brief = typeof params.brief === "string" ? params.brief.trim() : "";
3187
- if (!brief) throw new Error("rebrief requires a nonempty brief");
3188
- if (brief.length > snapshot.policy.maxBriefChars) {
3189
- throw new Error(`rebrief brief must be at most ${snapshot.policy.maxBriefChars} characters`);
3190
- }
3191
- const fold = requireActiveFold(snapshot, persistence.state, id);
3192
- const previous = foldBrief(fold, persistence.state);
3193
- const briefs = { ...(persistence.state.briefs ?? {}), [id]: brief };
3194
- await persistManual({ ...persistence.state, revision: persistence.state.revision + 1, briefs }, action, ctx);
3195
- updateStatus(ctx);
3196
- return toolPayload({
3197
- version: 1,
3198
- action,
3199
- id,
3200
- brief,
3201
- previousBrief: previous,
3202
- durableRevision: persistence.state.revision,
3203
- activation: "the fold's placeholder and index rows now carry your brief; " +
3204
- "the exact source and its fold record are untouched.",
3642
+ const payload = toolPayload({
3643
+ ...peekFoldSource({
3644
+ foldId: id,
3645
+ state: persistence.state,
3646
+ entries: ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch(),
3647
+ sessionId: ctx.sessionManager.getSessionId(),
3648
+ maximumBytes: sliceBytes,
3649
+ offset,
3650
+ toolName,
3651
+ }),
3652
+ ...(peekIsEphemeral(params) ? {
3653
+ ephemeral: "this result rides your context exactly until your next message; " +
3654
+ "extract what your current task needs into that reply, which takes over its " +
3655
+ "place. Peeking again is lossless and cheap; pass ephemeral false when you " +
3656
+ "need these bytes standing for several turns",
3657
+ } : { durable: "this result stays in your context like any other tool result" }),
3205
3658
  });
3659
+ return payload;
3206
3660
  }
3207
3661
  if (action === "reboundary") {
3208
3662
  if (params.ids !== undefined) {
@@ -3230,7 +3684,10 @@ export function registerActiveContext(pi: any, options: {
3230
3684
  const recut = manualFoldCandidate(snapshot, persistence.state, snapped.ids, { allowProtected: true });
3231
3685
  const created: Array<Record<string, unknown>> = [];
3232
3686
  const correctedBriefs: Record<string, string> = {};
3233
- for (const part of splitCandidateBySize(snapshot, persistence.state, recut)) {
3687
+ // Re-cut whole, for the same reason a mark is taken whole: the agent named this
3688
+ // boundary, and splitting it hands one brief to several folds that each hold a
3689
+ // fraction of what it describes.
3690
+ for (const part of [recut]) {
3234
3691
  const durable = persistence.persistedFoldRecords.get(foldIdFor(part.kind, part.parts));
3235
3692
  const { preparedFold, nextState } = await prepareAndCommitExplicit({
3236
3693
  snapshot, candidate: part, brief: durable ? durable.fold.brief : supplied, ctx, signal,
@@ -3348,52 +3805,51 @@ export function registerActiveContext(pi: any, options: {
3348
3805
  activation: "durable immediately; projected on the next model call in this same turn",
3349
3806
  });
3350
3807
  }
3351
- if (action === "protect" || action === "unprotect") {
3808
+ if (action === "pin" || action === "unpin") {
3352
3809
  const ids = stringIds(params.ids);
3353
- const refsBefore = persistence.state.protected.length;
3354
- const nextProtection = protectEvidence(snapshot, persistence.state, ids, action === "protect");
3355
- if (action === "protect") {
3356
- const capacity = servingCapacity(snapshot.contextWindow);
3357
- const prospectiveShare = capacity.budgetTokens > 0
3358
- ? estimatedTokens(explicitProtectedMass(snapshot, nextProtection).bytes) / capacity.budgetTokens
3359
- : 0;
3360
- if (prospectiveShare > MAX_PINNED_SHARE) {
3361
- throw new Error(
3362
- `protect refused: these pins would hold ${Math.round(prospectiveShare * 100)}% of the ` +
3363
- `working window raw, past the ${Math.round(MAX_PINNED_SHARE * 100)}% pinned-share cap. ` +
3364
- 'Release earlier pins with {"action":"unprotect","ids":["<entry-id>"]} first; ' +
3365
- "folding keeps entries exactly recoverable without pinning them.");
3366
- }
3367
- }
3368
- await persistManual(nextProtection, action, ctx);
3369
- const refsAfter = persistence.state.protected.length;
3370
- emit("context.protect", {
3371
- protect: action === "protect",
3372
- ids: ids.join(","),
3373
- id_count: ids.length,
3374
- protected_refs_before: refsBefore,
3375
- protected_refs_after: refsAfter,
3810
+ await applyProtectionChange(snapshot, ids, action === "pin", ctx);
3811
+ const refsAfter = persistence.state!.protected.length;
3812
+ // A PIN IS A CLAIM ON THE WINDOW, so it answers with the same picture a mark does.
3813
+ // It never pays the drop, and saying so here is what stops an agent pinning its way
3814
+ // to a coverage it does not have.
3815
+ const pinCapacity = servingCapacity(snapshot.contextWindow);
3816
+ const pinClaims = windowClaims({
3817
+ snapshot,
3818
+ state: persistence.state,
3819
+ budgetTokens: pinCapacity.budgetTokens,
3820
+ charsPerToken: projectionCharsPerToken(),
3821
+ });
3822
+ const pinCoverage = commitCoverage({
3823
+ snapshot,
3824
+ state: persistence.state,
3825
+ usedTokens: pinCapacity.usedTokens,
3826
+ budgetTokens: pinCapacity.budgetTokens,
3376
3827
  });
3377
- updateStatus(ctx);
3378
3828
  return toolPayload({
3379
3829
  version: 1,
3380
3830
  action,
3381
3831
  ids,
3382
3832
  protectedRefs: refsAfter,
3383
- activation: "durable immediately; projected on the next model call in this same turn",
3833
+ pinnedTokens: pinClaims.pinnedTokens,
3834
+ pinnedShare: pinClaims.pinnedShare,
3835
+ maxPinnedShare: MAX_PINNED_SHARE,
3836
+ markedTowardCommitTokens: pinClaims.markedTokens,
3837
+ unclaimedTokens: pinClaims.unclaimedTokens,
3838
+ commitTargetTokens: pinCoverage.targetTokens,
3839
+ unmarkedAgainstCommitTokens: pinCoverage.remainingTokens,
3840
+ activation: action === "pin"
3841
+ ? "durable immediately; projected on the next model call in this same turn. " +
3842
+ "Pinned entries stay RAW and are never folded, automatically or by a mark, so " +
3843
+ "this is how you keep a span expanded. It frees nothing: the next commit still " +
3844
+ "has to reach its drop from what is left, so pinned mass makes the rest of the " +
3845
+ "window fold sooner rather than later."
3846
+ : "durable immediately; projected on the next model call in this same turn. The " +
3847
+ "released entries are foldable again and rejoin the next commit's candidates.",
3384
3848
  });
3385
3849
  }
3386
3850
  if (action === "unmark") {
3387
- const ids = stringIds(params.ids);
3388
- const requested = new Set(ids);
3389
- const before = pendingMarks(persistence.state);
3390
- const kept = before.filter((mark) => !requested.has(mark.id));
3391
- const removed = before.filter((mark) => requested.has(mark.id));
3392
- const unknown = ids.filter((id) => !before.some((mark) => mark.id === id));
3393
- if (unknown.length) throw new Error(`No pending mark named ${unknown.join(", ")}`);
3394
- await persistManual(withPendingMarks(persistence.state, kept), action, ctx);
3395
- updateStatus(ctx);
3396
- const accounting = markAccounting(snapshot, persistence.state);
3851
+ const removed = await withdrawMarks(stringIds(params.ids), ctx);
3852
+ const accounting = markAccounting(snapshot, persistence.state!);
3397
3853
  return toolPayload({
3398
3854
  version: 1,
3399
3855
  action,
@@ -3405,162 +3861,67 @@ export function registerActiveContext(pi: any, options: {
3405
3861
  activation: "the withdrawn marks will never be applied; no context bytes moved",
3406
3862
  });
3407
3863
  }
3408
- if (action === "fold") {
3409
- const requested = batchedMarkRequests(params);
3410
- const resolved: Array<{
3411
- candidate: FoldCandidate;
3412
- brief?: string;
3413
- corrections: SpanCorrection[];
3414
- splitFrom: number;
3415
- }> = [];
3416
- for (const request of requested) {
3417
- const snapped = snapFoldCandidate(snapshot, persistence.state, request.ids, { allowProtected: true });
3418
- const spanChars = candidateSpanChars(snapshot, persistence.state, snapped.candidate);
3419
- const parts = splitCandidateBySize(snapshot, persistence.state, snapped.candidate);
3420
- for (const [index, part] of parts.entries()) {
3421
- resolved.push({
3422
- candidate: part,
3423
- brief: request.brief,
3424
- corrections: index === 0
3425
- ? [
3426
- ...snapped.corrections,
3427
- ...(parts.length > 1
3428
- ? [{
3429
- from: request.ids,
3430
- to: [],
3431
- reason: `span was ${spanChars} chars, over the ${MAX_FOLD_SPAN_CHARS}-char bite-size cap; ` +
3432
- `split into ${parts.length} sequential folds, each with its own brief`,
3433
- }]
3434
- : []),
3435
- ]
3436
- : [],
3437
- splitFrom: parts.length > 1 && index === 0 ? spanChars : 0,
3438
- });
3439
- }
3440
- if (parts.length > 1) {
3441
- emit("context.split", {
3442
- source: "agent",
3443
- span_chars: spanChars,
3444
- parts: parts.length,
3445
- cap_chars: MAX_FOLD_SPAN_CHARS,
3446
- fold_ids: "",
3447
- });
3448
- }
3864
+ if (action === "brief") {
3865
+ // THE ONE EDIT THAT IS FREE. A pending mark is not in the window, so writing its
3866
+ // brief moves no bytes and costs no prefix cache; the brief reaches the projection
3867
+ // only when the commit writes that placeholder for the first time. A STANDING fold
3868
+ // is the opposite case and is refused by name: its placeholder is already in the
3869
+ // window, and rewriting it breaks the cache from that point on, which is the
3870
+ // rebrief deletion of 2026-08-21 read from the other side.
3871
+ const id = String(params.id ?? "").trim();
3872
+ if (!id) throw new Error("brief needs the id of a pending fold");
3873
+ const standing = persistence.state!.folds.find((fold) => fold.id === id);
3874
+ if (standing) {
3875
+ throw new Error(
3876
+ `${id} is a standing fold, not a pending one, so its brief is already in your ` +
3877
+ "window and rewriting it would rewrite the projection from that placeholder on. " +
3878
+ `Read it with ${toolName} {"action":"peek","id":"${id}"}, or re-cut the span with ` +
3879
+ `${toolName} {"action":"reboundary","ids":[...]} if the boundary is wrong.`,
3880
+ );
3449
3881
  }
3450
- const corrections = resolved.flatMap((item) => item.corrections);
3451
- const marks: Array<Record<string, unknown>> = [];
3452
- let staged = persistence.state;
3453
- for (const item of resolved) {
3454
- const { candidate } = item;
3455
- const alreadyPrepared = staged.folds.some((fold) =>
3456
- fold.id === foldIdFor(candidate.kind, candidate.parts));
3457
- const deferred = refsProtected(candidate.sourceRefs, staged, snapshot) ||
3458
- (candidate.kind === "tool-result" &&
3459
- toolRefsProtected(candidate.sourceRefs, staged, snapshot));
3460
- const suppliedComplaint = item.brief === undefined
3461
- ? null
3462
- : briefContractComplaint(item.brief.trim(), snapshot.policy.maxBriefChars, snapshot.toolName);
3463
- if (suppliedComplaint !== null) {
3464
- marks.push({
3465
- id: foldIdFor(candidate.kind, candidate.parts),
3466
- kind: candidate.kind,
3467
- ok: false,
3468
- deferred: false,
3469
- reason: `Supplied brief rejected. ${suppliedComplaint}`,
3470
- });
3471
- continue;
3472
- }
3473
- const briefed = !deferred && !alreadyPrepared
3474
- ? await prepareFold({
3475
- candidate,
3476
- snapshot,
3477
- state: staged,
3478
- generation: lifecycle.generation,
3479
- brief: item.brief,
3480
- ctx,
3481
- signal,
3482
- })
3483
- : null;
3484
- const mark = foldMarkFor({
3485
- candidate,
3486
- brief: briefed?.fold.brief ?? item.brief ?? ladderBrief(snapshot, staged, candidate),
3487
- briefProvenance: briefed?.fold.provenance ??
3488
- (item.brief ? { kind: "supplied" } : { kind: "deterministic" }),
3489
- origin: "agent",
3490
- ordinal: markOrdinal(snapshot),
3491
- });
3492
- const addition = addPendingMark(staged, mark);
3493
- if (!addition.added) {
3494
- marks.push({ id: mark.id, kind: mark.kind, ok: false, deferred: false, reason: addition.reason });
3495
- continue;
3496
- }
3497
- staged = addition.state;
3498
- marks.push({
3499
- id: mark.id,
3500
- kind: mark.kind,
3501
- ok: true,
3502
- deferred,
3503
- ...(deferred
3504
- ? {
3505
- scheduled: "the span is still in the fresh window; this mark is held and folds at the " +
3506
- "first commit after it ages out",
3507
- }
3508
- : {}),
3509
- brief: mark.brief,
3510
- provenance: normalizeLegacyProvenance(mark.briefProvenance),
3511
- });
3882
+ const held = pendingMarks(persistence.state!);
3883
+ const target = held.find((mark) => mark.id === id);
3884
+ if (!target) {
3885
+ const names = held.slice(0, 5).map((mark) => mark.id).join(", ");
3886
+ throw new Error(
3887
+ `No pending fold ${id}. ${held.length
3888
+ ? `The folds waiting for a brief are: ${names}${held.length > 5 ? ", ..." : ""}.`
3889
+ : "Nothing is pending right now, so there is nothing to brief."}`,
3890
+ );
3512
3891
  }
3513
- await persistManual(staged, action, ctx);
3892
+ const text = String(params.brief ?? "").trim();
3893
+ const complaint = briefContractComplaint(text, snapshot.policy.maxBriefChars, snapshot.toolName);
3894
+ if (complaint !== null) throw new Error(`Supplied brief rejected. ${complaint}`);
3895
+ const nextState = {
3896
+ ...persistence.state!,
3897
+ revision: persistence.state!.revision + 1,
3898
+ pendingMarks: held.map((mark) => mark.id === id
3899
+ ? { ...mark, brief: text, briefProvenance: { kind: "supplied" as const } }
3900
+ : mark),
3901
+ };
3902
+ persistence.state = nextState;
3903
+ await persistManual(nextState, action, ctx);
3514
3904
  updateStatus(ctx);
3515
- const accounting = markAccounting(snapshot, persistence.state);
3516
- const held = pendingMarks(persistence.state).map((mark) => ({
3517
- id: mark.id,
3518
- kind: mark.kind,
3519
- tokens: estimatedTokens(markFreedBytes(snapshot, persistence.state!, mark)),
3520
- }));
3521
- const remainder = unmarkedRemainder(snapshot, persistence.state, projectionCharsPerToken());
3522
- const only = marks.length === 1 && marks[0].ok === true ? marks[0] : null;
3905
+ emit("context.brief", { id, chars: text.length, origin: "agent" });
3906
+ const accounting = markAccounting(snapshot, persistence.state!);
3907
+ const remainder = unmarkedRemainder(snapshot, persistence.state!, projectionCharsPerToken());
3523
3908
  return toolPayload({
3524
3909
  version: 1,
3525
3910
  action,
3526
- scheduling: "epoch",
3527
- ok: marks.some((mark) => mark.ok === true),
3528
- deferredMarks: marks.filter((mark) => mark.deferred === true).length,
3529
- ...(only
3530
- ? {
3531
- id: only.id,
3532
- kind: only.kind,
3533
- brief: only.brief,
3534
- provenance: only.provenance,
3535
- deferred: only.deferred,
3536
- ...(only.scheduled ? { scheduled: only.scheduled } : {}),
3537
- }
3538
- : {}),
3539
- marks,
3540
- corrections,
3911
+ id,
3912
+ kind: target.kind,
3913
+ brief: text,
3914
+ provenance: "agent",
3541
3915
  argumentsSha256: executionArgumentsSha256,
3542
- durableRevision: persistence.state.revision,
3916
+ durableRevision: persistence.state!.revision,
3543
3917
  pendingMarks: accounting.pending,
3544
- eligibleMarks: accounting.eligibleMarks,
3545
- retainedMarks: accounting.retainedMarks,
3546
- estimatedFreedWindowShare: accounting.freedBudgetShare,
3547
- estimatedEligibleWindowShare: accounting.eligibleFreedBudgetShare,
3548
- estimatedRewriteTokens: accounting.rewriteTokens,
3549
- held,
3550
- heldNote: "held until it ages out or the next fold event",
3918
+ unbriefed: pendingMarks(persistence.state!)
3919
+ .filter((mark) => mark.briefProvenance?.kind !== "supplied").map((mark) => mark.id),
3551
3920
  unmarkedSpans: remainder.spans,
3552
3921
  unmarkedTokens: remainder.tokens,
3553
- unmarkedShare: remainder.share,
3554
- unmarkedCandidates: remainder.candidates,
3555
- ...(guidance.actionResponses ? {
3556
- awareness: markAwarenessText({ held, remainder, toolName, brandNoun }),
3557
- activation: "accepted as pending marks; no context bytes moved, and nothing else in your " +
3558
- "context changed either. They apply together at the next commit epoch, which the runtime " +
3559
- "opens at the fold event. " +
3560
- "A mark over a still-fresh span is held, not refused: it is scheduled, and it folds at the " +
3561
- "first commit after that span ages out of the fresh window. " +
3562
- "Mark several spans in one call: this whole picture comes back with each one.",
3563
- } : {}),
3922
+ activation: "durable immediately, and it costs your window nothing: this fold is " +
3923
+ "pending, so the brief you just wrote is stored outside the projection and " +
3924
+ "reaches it only when the commit folds this span. Nothing in your context moved.",
3564
3925
  });
3565
3926
  }
3566
3927
  throw new Error(`Unknown ${toolName} action '${action}'`);
@@ -3590,6 +3951,7 @@ export function registerActiveContext(pi: any, options: {
3590
3951
  marks_requested: requestedMarkCount(params),
3591
3952
  corrections_applied: corrections.length,
3592
3953
  arguments_sha256: sha256Value(params ?? {}),
3954
+ ...(action === "peek" ? { ephemeral: peekIsEphemeral(params) } : {}),
3593
3955
  });
3594
3956
  for (const correction of corrections) {
3595
3957
  const from = denseOwnArrayValues(ownValue(correction, "from")) ?? [];
@@ -3609,6 +3971,7 @@ export function registerActiveContext(pi: any, options: {
3609
3971
  const result = await executeAction(params, signal, ctx);
3610
3972
  const corrections = denseOwnArrayValues(ownValue(ownValue(result, "details"), "corrections"));
3611
3973
  attempt(true, null, (corrections ?? []) as Array<Record<string, unknown>>);
3974
+ if (action === "peek" && peekIsEphemeral(params)) ephemeralPeeks.add(toolCallId);
3612
3975
  return result;
3613
3976
  } catch (error) {
3614
3977
  attempt(false, error instanceof Error ? error.message : String(error), []);
@@ -3642,7 +4005,11 @@ export function registerActiveContext(pi: any, options: {
3642
4005
  if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
3643
4006
  const divider = args.indexOf(" -- ");
3644
4007
  const selector = (divider >= 0 ? args.slice(0, divider) : args).trim();
3645
- if (selector === "commit") {
4008
+ // BARE /fold IS THE COMMIT (Shane, dogfooding 2026-08-22): staging is the
4009
+ // normal accumulation and applying it is the default verb. The old bare behavior
4010
+ // rescue-folded an auto-offered chapter, which read as the command ignoring the
4011
+ // staged pile; a rescue now needs explicit ids.
4012
+ if (!selector || selector === "commit") {
3646
4013
  const capacity = servingCapacity(snapshot.contextWindow);
3647
4014
  const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
3648
4015
  ? capacity.usedTokens / capacity.budgetTokens
@@ -3665,16 +4032,20 @@ export function registerActiveContext(pi: any, options: {
3665
4032
  committed
3666
4033
  ? `Committed ${ownValue(committed, "applied_marks") ?? 0} mark(s)${topUp ? " with automatic top-up" : ""}. ` +
3667
4034
  "Exact source remains expandable."
3668
- : "Nothing eligible to commit: no pending mark is outside the fresh tail.",
4035
+ : "Nothing to commit: no staged mark could apply (pinned or already-folded spans hold).",
3669
4036
  "info",
3670
4037
  );
3671
4038
  return;
3672
4039
  }
3673
4040
  const supplied = (divider >= 0 ? args.slice(divider + 4) : "").trim() || undefined;
3674
4041
  const ids = selector ? selector.replace(/\.\./g, " ").split(/[\s,]+/).filter(Boolean) : [];
3675
- const candidate = ids.length
3676
- ? manualFoldCandidate(snapshot, persistence.state!, ids)
3677
- : selectAutomaticChapter(snapshot, persistence.state!) ?? selectAutomaticToolBatch(snapshot, persistence.state!)[0] ?? null;
4042
+ if (!ids.length) {
4043
+ throw new Error(
4044
+ "Nothing to rescue without ids: /fold with no arguments commits every staged mark; " +
4045
+ "name the span to fold now (/fold <start-id> [end-id] -- brief)",
4046
+ );
4047
+ }
4048
+ const candidate = manualFoldCandidate(snapshot, persistence.state!, ids);
3678
4049
  if (!candidate) throw new Error("No exact stale rescue span is currently eligible");
3679
4050
  const stateBefore = persistence.state!;
3680
4051
  const persistedBefore = persistence.persisted ? clone(persistence.persisted) : null;
@@ -3721,6 +4092,200 @@ export function registerActiveContext(pi: any, options: {
3721
4092
  }
3722
4093
  };
3723
4094
 
4095
+ /**
4096
+ * /fold-editor, V2.2. The handler assembles a plain data snapshot of the working
4097
+ * window and hands it to the view; the view holds no runtime reference and no
4098
+ * mutation path. What changed in V2.2: raw entries are MARK POINTS, and a span
4099
+ * laid down between two of them stages through `stageFoldMarks` -- the SAME
4100
+ * validated path the tool's fold action runs -- with origin "user". The view
4101
+ * computes nothing but the arithmetic it is handed (spanCost) and reports one
4102
+ * intent (onStageMark); this handler owns every state transition.
4103
+ */
4104
+ const foldEditorCommandHandler = async (_args: string, ctx: any): Promise<void> => {
4105
+ if (typeof ctx.ui?.custom !== "function") {
4106
+ throw new Error("/fold-editor needs an interactive UI; use /fold-status for the text form");
4107
+ }
4108
+ const snapshot = lifecycle.latestSnapshot
4109
+ ? authoritativeSnapshotFor(ctx)
4110
+ : snapshotForEvent(ctx, ctx.sessionManager.buildSessionContext().messages);
4111
+ if (!lifecycle.latestSnapshot) lifecycle.latestSnapshot = snapshot;
4112
+ if (!persistence.state) throw new Error("The context window is empty; there is nothing to show yet");
4113
+
4114
+ // THE WOULD-BE FOLD SIZE (Shane, 2026-08-23). The footer's number while a span is
4115
+ // being laid down is the runtime's own estimate -- span bytes over the mapped
4116
+ // window at ESTIMATED_BYTES_PER_TOKEN -- so what the user sees before staging is
4117
+ // what the staged mark answers with after it.
4118
+ const spanCost = (from: number, to: number) => ({
4119
+ entries: Math.abs(to - from) + 1,
4120
+ tokens: estimatedTokens(spanBytes(snapshot, Math.min(from, to), Math.max(from, to) + 1)),
4121
+ });
4122
+
4123
+ // Marks move no bytes until the commit, so window geometry cannot change while
4124
+ // the editor holds it open; only pending state grows. Rebuilding against the SAME
4125
+ // snapshot with fresh state keeps PROPOSED rows honest without re-deriving indices.
4126
+ const buildData = () => {
4127
+ const state = persistence.state!;
4128
+ const capacity = servingCapacity(snapshot.contextWindow);
4129
+ const occupancy = capacity.usedTokens !== null && capacity.budgetTokens > 0
4130
+ ? capacity.usedTokens / capacity.budgetTokens
4131
+ : null;
4132
+ const accounting = markAccounting(snapshot, state);
4133
+ return {
4134
+ title: `pi-fold context editor · ${snapshot.sessionId.slice(0, 8)}`,
4135
+ occupancy: {
4136
+ usedTokens: capacity.usedTokens,
4137
+ budgetTokens: capacity.budgetTokens,
4138
+ commitOccupancy: thresholds.maxTarget,
4139
+ commitDue: occupancy !== null && occupancy >= thresholds.maxTarget,
4140
+ },
4141
+ blocks: buildFoldEditorData(snapshot, state, {
4142
+ foldRows: () => (state.folds ?? []).map((fold: any) => {
4143
+ const row = foldIndexRow(fold, state, snapshot);
4144
+ const entries = flattenFoldRefs(fold, state)
4145
+ .map((ref) => {
4146
+ const item = exactMapped(snapshot, ref);
4147
+ const message = item?.message as Record<string, unknown> | undefined;
4148
+ const role = String(item?.ref?.role ?? message?.role ?? "entry");
4149
+ const content = message?.content;
4150
+ let preview = "";
4151
+ if (typeof content === "string") preview = content.slice(0, 300);
4152
+ else if (Array.isArray(content)) {
4153
+ const firstText = content.find((part) => part?.type === "text");
4154
+ preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
4155
+ }
4156
+ return { id: ref.entryId, role, preview: preview.replace(/\s+/g, " ") };
4157
+ });
4158
+ return {
4159
+ id: String(row.id),
4160
+ kind: String(row.kind ?? ""),
4161
+ brief: String(row.brief ?? ""),
4162
+ sourceCount: Number(row.sourceCount ?? 0),
4163
+ startPosition: Number(row.startPosition),
4164
+ endPosition: Number(row.endPosition),
4165
+ entries,
4166
+ };
4167
+ }),
4168
+ pendingMarkRefs: () => pendingMarks(state)
4169
+ .filter((mark) => mark.mark === "fold")
4170
+ .map((mark) => ({
4171
+ id: mark.id,
4172
+ origin: String(mark.origin),
4173
+ brief: String(mark.brief ?? ""),
4174
+ // WHAT THE STAGED MARK FREES, so a PROPOSED row states its size the
4175
+ // way the footer prices an unstaged span.
4176
+ ...(typeof markFreedBytes(snapshot, state, mark) === "number"
4177
+ ? { tokens: estimatedTokens(markFreedBytes(snapshot, state, mark)) }
4178
+ : {}),
4179
+ entryIds: markSpanRefs(state, mark).refs
4180
+ .map((ref) => ref.entryId)
4181
+ .filter((entryId): entryId is string => Boolean(entryId)),
4182
+ })),
4183
+ mappedRange: (from: number, to: number) => {
4184
+ const out: Array<{ id: string; role: string; preview: string; index?: number }> = [];
4185
+ for (let index = from; index <= to; index += 1) {
4186
+ const item = snapshot.mapped[index];
4187
+ if (!item) continue;
4188
+ const message = item.message as Record<string, unknown> | undefined;
4189
+ const role = String(item.ref?.role ?? message?.role ?? "entry");
4190
+ const content = message?.content;
4191
+ // 300 chars, same as fold entries: the view shows 48 on the row and
4192
+ // deepens to 240 on Enter, so a 48-char preview made detail a no-op.
4193
+ let preview = "";
4194
+ if (typeof content === "string") preview = content.slice(0, 300);
4195
+ else if (Array.isArray(content)) {
4196
+ const firstText = content.find((part) => part?.type === "text");
4197
+ preview = typeof firstText?.text === "string" ? firstText.text.slice(0, 300) : "";
4198
+ }
4199
+ // THE INDEX IS THE MARK POINT: raw entries carry their mapped position so
4200
+ // the view can price a span and name its two boundary ids.
4201
+ out.push({
4202
+ id: item.ref?.entryId ?? String(index),
4203
+ role,
4204
+ preview: preview.replace(/\s+/g, " "),
4205
+ ...(item.ref?.entryId ? { index } : {}),
4206
+ });
4207
+ }
4208
+ return out;
4209
+ },
4210
+ entryCount: snapshot.mapped.length,
4211
+ }),
4212
+ pending: {
4213
+ count: pendingMarks(state).length,
4214
+ agentMarks: accounting.agentMarks,
4215
+ ladderMarks: accounting.ladderMarks,
4216
+ userMarks: accounting.userMarks,
4217
+ freedTokens: accounting.freedTokens,
4218
+ },
4219
+ pinned: state.protected.map((ref) => ref.entryId),
4220
+ };
4221
+ };
4222
+
4223
+ let view: FoldEditorView | null = null;
4224
+ const runQueued = async (work: () => Promise<void>): Promise<void> => {
4225
+ const operation = ladder.actionQueue.then(work);
4226
+ ladder.actionQueue = operation.catch(() => undefined);
4227
+ await operation;
4228
+ };
4229
+ const refreshAfter = (): void => {
4230
+ if (view) view.refresh(buildData());
4231
+ };
4232
+ const editorActions = {
4233
+ spanCost,
4234
+ onStageMark: async (fromId: string, toId: string, brief?: string): Promise<void> => {
4235
+ await runQueued(async () => {
4236
+ const fresh = authoritativeSnapshotFor(ctx);
4237
+ const request = brief && brief.trim()
4238
+ ? { ids: [fromId, toId], brief: brief.trim() }
4239
+ : { ids: [fromId, toId] };
4240
+ const { marks, staged } = await stageFoldMarks(
4241
+ batchedMarkRequests(request), "user",
4242
+ new AbortController().signal, ctx, fresh);
4243
+ // Refuse BEFORE persisting: an all-refused batch leaves state untouched and
4244
+ // persisting it would be a no-op write against the same-state projection.
4245
+ if (!marks.some((mark) => mark.ok === true)) {
4246
+ const reason = marks.map((mark) => mark.reason).filter(Boolean).join("; ") ||
4247
+ "the span was refused";
4248
+ throw new Error(reason);
4249
+ }
4250
+ await persistManual(staged, "fold", ctx);
4251
+ updateStatus(ctx);
4252
+ });
4253
+ safeNotify(ctx, "Staged user mark(s); they apply at the next commit epoch.", "info");
4254
+ refreshAfter();
4255
+ },
4256
+ onWithdrawMark: async (markId: string): Promise<void> => {
4257
+ let removed: Array<Record<string, unknown>> = [];
4258
+ await runQueued(async () => {
4259
+ removed = await withdrawMarks([markId], ctx) as Array<Record<string, unknown>>;
4260
+ });
4261
+ safeNotify(ctx,
4262
+ `Withdrew ${removed.length} staged mark(s); no context bytes moved.`, "info");
4263
+ refreshAfter();
4264
+ },
4265
+ onTogglePin: async (entryId: string): Promise<void> => {
4266
+ const pinnedAlready = persistence.state!.protected
4267
+ .some((ref: any) => ref.entryId === entryId);
4268
+ await runQueued(async () => {
4269
+ await applyProtectionChange(authoritativeSnapshotFor(ctx), [entryId], !pinnedAlready, ctx);
4270
+ });
4271
+ safeNotify(ctx, pinnedAlready
4272
+ ? "Unpinned: the released entries rejoin the next commit's candidates."
4273
+ : "Pinned raw: held through every fold; frees nothing toward the drop.", "info");
4274
+ refreshAfter();
4275
+ },
4276
+ };
4277
+ await ctx.ui.custom((
4278
+ _tui: unknown,
4279
+ theme: any,
4280
+ keybindings: { matches(data: string, action: string): boolean } | null,
4281
+ done: () => void,
4282
+ ) => {
4283
+ view = new FoldEditorView(buildData(), done, keybindings, theme, editorActions);
4284
+ return view;
4285
+ });
4286
+ updateStatus(ctx);
4287
+ };
4288
+
3724
4289
  pi.registerTool(buildActiveContextTool({
3725
4290
  name: toolName,
3726
4291
  label: toolLabel,
@@ -3735,8 +4300,10 @@ export function registerActiveContext(pi: any, options: {
3735
4300
  for (const command of buildActiveContextCommands({
3736
4301
  statusName: commandNames.status,
3737
4302
  foldName: commandNames.fold,
4303
+ editorName: "fold-editor",
3738
4304
  statusHandler: statusCommandHandler,
3739
4305
  foldHandler: foldCommandHandler,
4306
+ editorHandler: foldEditorCommandHandler,
3740
4307
  })) {
3741
4308
  pi.registerCommand(command.name, {
3742
4309
  description: command.description,