pi-fold 4.1.1 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,10 +37,10 @@ In an interactive session one row sits directly above Pi's footer and shows the
37
37
 
38
38
  <picture>
39
39
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/shaneconner/fold/main/media/fold-bar-dark.svg">
40
- <img alt="The fold bar: forty cells of the serving budget, oldest on the left, with ticks at the aim and the commit point. Staged spans, raw material and fold placeholders fill it in three colours, and the label reads 42%, commit at 80%, 12 staged, 118k to free, 3 folds hold 412k." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/fold-bar-light.svg">
40
+ <img alt="The fold bar: forty cells of the serving budget, oldest on the left, with ticks at the aim and the commit point. Staged spans, raw material and fold placeholders fill it in three colours, and the label reads 42%, commit at 80%, 12 staged, 3 folds hide 412k tokens." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/fold-bar-light.svg">
41
41
  </picture>
42
42
 
43
- The bar is the serving budget, forty cells wide, oldest material on the left. The two ticks are the band: the left one is `minTarget`, where a commit aims to land, and the right one is `maxTarget`, where the commit fires. The fill is the provider's own count of what is in the window, never an estimate, so before the first response the row says `not measured yet` and draws nothing. One cell is one fortieth of the budget, and the fill reads left to right in three colours: the placeholders of standing folds first, drawn as low slivers and small by design; then staged spans, the raw material the next commit collapses; then raw material the runtime has not claimed yet. The label repeats the percentage, names the commit point (or reads `COMMIT DUE` once the fill has crossed it and `commit held` once that count has been weighed), states how many spans are staged and what they free, and ends with what folding has already bought: the tokens the standing folds hide. If automatic folding ever stops, the row reads `FOLDING STOPPED` in place of the bar. On a truecolor terminal the three fill classes take three steps of Crameri's batlow colour map; elsewhere they use the theme's own colours.
43
+ The bar is the serving budget, forty cells wide, oldest material on the left. The two ticks are the band: the left one is `minTarget`, where a commit aims to land, and the right one is `maxTarget`, where the commit fires. The fill is the provider's own count of what is in the window, never an estimate, so before the first response the row says `not measured yet` and draws nothing. One cell is one fortieth of the budget, and the fill reads left to right in three colours: the placeholders of standing folds first, drawn as low slivers and small by design; then staged spans, the raw material the next commit collapses; then raw material the runtime has not claimed yet. The label repeats the percentage, names the commit point (or reads `COMMIT DUE` once the fill has crossed it and `commit held` once that count has been weighed), states how many spans are staged, and ends with what folding has already bought: the tokens the standing folds hide from the window. Every token figure prices a pasted image as one image rather than as its base64 text, the same way the occupancy estimate does. Right after a commit the percentage is still the provider's last count, so the label says it is from before the commit until the next response measures the window again. If automatic folding ever stops, the row reads `FOLDING STOPPED` in place of the bar. On a truecolor terminal the three fill classes take three steps of Crameri's batlow colour map; elsewhere they use the theme's own colours.
44
44
 
45
45
  ## Results
46
46
 
@@ -35,6 +35,7 @@ import {
35
35
  selectAutomaticRung,
36
36
  setFoldProjectionState,
37
37
  withExpandLease,
38
+ renderFoldParts,
38
39
  } from "./lib/folding.ts";
39
40
  import {
40
41
  admissionVerdict,
@@ -54,6 +55,7 @@ import {
54
55
  parseNativeCompactionDecision,
55
56
  parseProviderContextMeasurementReceipt,
56
57
  persistenceProjection,
58
+ pricedBytes,
57
59
  protectedStaleMass,
58
60
  providerContextMeasurement,
59
61
  providerTokens,
@@ -178,7 +180,6 @@ import {
178
180
  peekedSourceFoldIds,
179
181
  snapFoldCandidate,
180
182
  snapToFoldBoundaries,
181
- spanBytes,
182
183
  } from "./lib/selection.ts";
183
184
  import type { SpanCorrection } from "./lib/selection.ts";
184
185
  import {
@@ -667,6 +668,12 @@ export function registerActiveContext(pi: any, options: {
667
668
 
668
669
  const deliverReceipt = (receipt: ContextReceipt): void => {
669
670
  curation.receipts = withReceipt(curation.receipts, receipt);
671
+ // THE BAR'S READING GOES STALE AT A COMMIT. Occupancy is the provider's last count,
672
+ // which does not move until the next response, so after a commit the bar went on
673
+ // showing the pre-commit number as if it were current (38% beside "10 folds hold
674
+ // 8.6M", Shane 2026-09-03). Remember which measurement the commit landed against;
675
+ // while it is still the latest, the bar says the number is from before the commit.
676
+ if (receipt.foldsCommitted > 0) foldBar.staleSince = measurements.lastProviderMeasurement;
670
677
  // MONOTONIC, unlike `curation.receipts.length`, which saturates at MAX_CONTEXT_RECEIPTS
671
678
  // and would stop distinguishing one approach from the next once the session is long
672
679
  // enough to matter. This is the only counter the notice latch reads.
@@ -718,6 +725,11 @@ export function registerActiveContext(pi: any, options: {
718
725
  model: null as FoldBarModel | null,
719
726
  installed: false,
720
727
  requestRender: null as (() => void) | null,
728
+ /** The provider measurement the last landed commit ran against; the reading is
729
+ * stale while it is still the latest one. */
730
+ staleSince: null as unknown,
731
+ /** Each standing fold's source priced by the image law, keyed by the fold object. */
732
+ pricedSource: new WeakMap<ActiveFold, number>(),
721
733
  };
722
734
  const installFoldBar = (ctx: any): void => {
723
735
  if (foldBar.installed || typeof ctx.ui?.setWidget !== "function") return;
@@ -745,7 +757,20 @@ export function registerActiveContext(pi: any, options: {
745
757
  if (state && snapshot) {
746
758
  for (const mark of pendingMarks(state)) stagedTokens += estimatedTokens(markFreedBytes(snapshot, state, mark));
747
759
  for (const root of orderedRoots(state, snapshot)) placeholderChars += root.fold.placeholderChars;
748
- for (const fold of state.folds) hiddenChars += Math.max(0, fold.sourceChars - fold.placeholderChars);
760
+ // WHAT FOLDING BOUGHT, IN WINDOW TERMS. `sourceChars` is the transcript count, base64
761
+ // and all, and read as tokens it said "8.6M" over a 1M window. Each standing fold's
762
+ // source is priced by the image law once per fold OBJECT (folds are immutable and
763
+ // replaced, never mutated, so a WeakMap memo is exact) and summed over the whole
764
+ // forest, which is additive because a parent's source is its children's placeholders.
765
+ for (const fold of state.folds) {
766
+ let priced = foldBar.pricedSource.get(fold);
767
+ if (priced === undefined) {
768
+ const source = renderFoldParts(fold.parts, state, snapshot);
769
+ priced = source ? pricedBytes(source) : fold.sourceChars;
770
+ foldBar.pricedSource.set(fold, priced);
771
+ }
772
+ hiddenChars += Math.max(0, priced - fold.placeholderChars);
773
+ }
749
774
  }
750
775
  const budget = input.budgetTokens > 0 ? input.budgetTokens : 1;
751
776
  return {
@@ -760,6 +785,8 @@ export function registerActiveContext(pi: any, options: {
760
785
  folds: input.roots,
761
786
  hiddenTokens: estimatedTokens(hiddenChars),
762
787
  weighed: input.weighed,
788
+ staleAfterCommit: input.share !== null && foldBar.staleSince !== null &&
789
+ foldBar.staleSince === measurements.lastProviderMeasurement,
763
790
  stopped: ladder.automaticFailure?.message ?? null,
764
791
  };
765
792
  };
@@ -2652,7 +2679,7 @@ export function registerActiveContext(pi: any, options: {
2652
2679
  });
2653
2680
  return null;
2654
2681
  }
2655
- const bytesBefore = bytes(projectActiveContext(snapshot, state));
2682
+ const bytesBefore = pricedBytes(projectActiveContext(snapshot, state));
2656
2683
  // THE COMMIT CUTS ONLY AS DEEP AS THE BUDGET ASKS (Shane, 2026-08-23). The frontier
2657
2684
  // stages as material arrives, so by now the pending set is most of the window; without
2658
2685
  // this bound the first commit would fold all of it. `freeingTarget` is a share of the
@@ -2735,7 +2762,7 @@ export function registerActiveContext(pi: any, options: {
2735
2762
  }
2736
2763
  }
2737
2764
  persistence.state = result.state;
2738
- const bytesAfter = bytes(projectActiveContext(snapshot, persistence.state));
2765
+ const bytesAfter = pricedBytes(projectActiveContext(snapshot, persistence.state));
2739
2766
  const freedBytes = Math.max(0, bytesBefore - bytesAfter);
2740
2767
  const pinHeld = protectedStaleMass(snapshot, result.state);
2741
2768
  const commitEvent = emit("context.commit", {
@@ -4499,7 +4526,11 @@ export function registerActiveContext(pi: any, options: {
4499
4526
  safeNotify(
4500
4527
  ctx,
4501
4528
  committed
4502
- ? `Committed ${ownValue(committed, "applied_marks") ?? 0} mark(s)${topUp ? " with automatic top-up" : ""}. ` +
4529
+ // THE EPOCH RECORD IS camelCase; `applied_marks` is the EVENT's spelling. Reading
4530
+ // the event's name off the record printed "Committed 0 mark(s)" after a /fold that
4531
+ // had just applied forty (Shane, 2026-09-03, session 01a06746: 378,216 tokens to
4532
+ // 97,577 with the notice saying nothing happened).
4533
+ ? `Committed ${ownValue(committed, "appliedMarks") ?? 0} mark(s)${topUp ? " with automatic top-up" : ""}. ` +
4503
4534
  "Exact source remains expandable."
4504
4535
  : "Nothing to commit: no staged mark could apply (pinned or already-folded spans hold).",
4505
4536
  "info",
@@ -4586,7 +4617,7 @@ export function registerActiveContext(pi: any, options: {
4586
4617
  // what the staged mark answers with after it.
4587
4618
  const spanCost = (from: number, to: number) => ({
4588
4619
  entries: Math.abs(to - from) + 1,
4589
- tokens: estimatedTokens(spanBytes(snapshot, Math.min(from, to), Math.max(from, to) + 1)),
4620
+ tokens: estimatedTokens(pricedBytes(snapshot.messages.slice(Math.min(from, to), Math.max(from, to) + 1))),
4590
4621
  });
4591
4622
 
4592
4623
  /**
@@ -23,6 +23,8 @@ import {
23
23
  import {
24
24
  ACTIVE_CONTEXT_POLICY,
25
25
  DEFAULT_CONTEXT_WINDOW,
26
+ ESTIMATED_BYTES_PER_TOKEN,
27
+ IMAGE_ESTIMATED_TOKENS,
26
28
  servingBudgetTokens,
27
29
  } from "./policy.ts";
28
30
  import type {
@@ -594,6 +596,25 @@ export function parseNativeCompactionCompletion(value: unknown, expectedSessionI
594
596
  * needs to be the right order of magnitude instead of wrong by seventy times, and the
595
597
  * provider anchor corrects the remainder the moment a real measurement lands.
596
598
  */
599
+ /**
600
+ * WHAT A SPAN COSTS THE WINDOW, in the byte unit every estimator already speaks.
601
+ *
602
+ * `bytes()` counts a message's serialized characters, and a pasted screenshot serializes
603
+ * as hundreds of thousands of base64 characters the provider prices as one image. The
604
+ * occupancy estimate has applied that law since gate 156; the pricing of what a fold
605
+ * FREES had not, so a session holding 83 screenshots (67.3M base64 characters against
606
+ * 1.5M of text) told its owner a commit had "5.8M to free" against a 1M window, and a
607
+ * commit that moved the window 280,000 tokens recorded 8.6M freed (Shane, 2026-09-03,
608
+ * session 01a06746). Text is counted as its bytes; each image is counted as the bytes
609
+ * that estimate to IMAGE_ESTIMATED_TOKENS, so `estimatedTokens(pricedBytes(m))` is the
610
+ * same reading gate 156 gives the projection. Every "frees N" figure derives from this.
611
+ */
612
+ export function pricedBytes(messages: unknown[]): number {
613
+ const images = imageMass(messages);
614
+ return Math.max(0, bytes(messages) - images.base64Chars) +
615
+ images.images * IMAGE_ESTIMATED_TOKENS * ESTIMATED_BYTES_PER_TOKEN;
616
+ }
617
+
597
618
  export function imageMass(projected: unknown[]): { base64Chars: number; images: number } {
598
619
  let base64Chars = 0;
599
620
  let images = 0;
@@ -27,6 +27,7 @@ import {
27
27
  orderedRoots,
28
28
  protectedStaleMass,
29
29
  refsProtected,
30
+ pricedBytes,
30
31
  } from "./measurement.ts";
31
32
  import {
32
33
  childFoldIds,
@@ -179,13 +180,15 @@ export function markFreedBytes(
179
180
  ...state,
180
181
  expanded: state.expanded.filter((id) => id !== mark.id),
181
182
  }, snapshot);
182
- return expanded && folded ? Math.max(0, bytes(expanded) - bytes(folded)) : 0;
183
+ return expanded && folded ? Math.max(0, pricedBytes(expanded) - pricedBytes(folded)) : 0;
183
184
  }
184
185
  const source = renderFoldParts(mark.parts, state, snapshot);
185
186
  if (!source) return 0;
186
187
  const placeholders = mark.kind === "tool-result" ? mark.parts.length : 1;
187
188
  const placeholder = placeholders * (mark.brief.length + ESTIMATED_PLACEHOLDER_OVERHEAD_BYTES);
188
- return Math.max(0, bytes(source) - placeholder);
189
+ // PRICED, NOT COUNTED: a span holding a screenshot is worth one image to the
190
+ // window, not its base64 length (measurement.ts, pricedBytes).
191
+ return Math.max(0, pricedBytes(source) - placeholder);
189
192
  }
190
193
 
191
194
  export function markSpanStart(
@@ -55,6 +55,8 @@ export interface FoldBarModel {
55
55
  hiddenTokens: number;
56
56
  /** Whether a band-top commit has already been weighed against the current count. */
57
57
  weighed: boolean;
58
+ /** A commit landed after the count `share` reads; the number is from before it. */
59
+ staleAfterCommit: boolean;
58
60
  /** The suspension message when automatic folding has stopped, else null. */
59
61
  stopped: string | null;
60
62
  }
@@ -154,19 +156,27 @@ export function renderFoldBar(model: FoldBarModel, width: number, theme: FoldBar
154
156
  else bar += ink[cells[i]](GLYPH[cells[i]]);
155
157
  }
156
158
  const pct = Math.round(model.share * 100);
157
- const parts: string[] = [theme.fg("text", `${pct}%`)];
158
- if (model.share < model.commitShare) parts.push(theme.fg("muted", `commit at ${Math.round(model.commitShare * 100)}%`));
159
- else if (model.weighed) parts.push(theme.fg("muted", "commit held"));
160
- else parts.push(theme.fg("warning", "COMMIT DUE"));
161
- // A staged count with nothing priced behind it reads "0 to free", which is a
162
- // contradiction on the face of the row; state the count alone until the marks price.
163
- if (model.stagedMarks > 0) {
164
- parts.push(ink.staged(model.stagedTokens > 0
165
- ? `${model.stagedMarks} staged, ${formatTokens(model.stagedTokens)} to free`
166
- : `${model.stagedMarks} staged`));
159
+ const parts: string[] = [];
160
+ // A READING FROM BEFORE THE COMMIT SAYS SO. The count only moves when the provider
161
+ // answers, so right after a commit the bar showed the pre-commit percentage in the
162
+ // same type as a live one and read as a commit that did nothing. No estimate is
163
+ // drawn in its place: the freed bytes are transcript mass and the arithmetic would
164
+ // go negative against the window.
165
+ if (model.staleAfterCommit) {
166
+ parts.push(theme.fg("muted", `${pct}% before the commit`));
167
+ } else {
168
+ parts.push(theme.fg("text", `${pct}%`));
169
+ if (model.share < model.commitShare) parts.push(theme.fg("muted", `commit at ${Math.round(model.commitShare * 100)}%`));
170
+ else if (model.weighed) parts.push(theme.fg("muted", "commit held"));
171
+ else parts.push(theme.fg("warning", "COMMIT DUE"));
167
172
  }
173
+ // A COUNT FOR WHAT IS STAGED, TOKENS FOR WHAT FOLDING BOUGHT. Both figures are priced
174
+ // by the image law now (measurement.ts pricedBytes): "5.8M to free" beside a 1M window
175
+ // was 83 screenshots' base64 counted as text (Shane 2026-09-03). The unit is written
176
+ // out because pi's k/M format read as megabytes.
177
+ if (model.stagedMarks > 0) parts.push(ink.staged(`${model.stagedMarks} staged`));
168
178
  if (model.folds > 0) {
169
- parts.push(ink.folded(`${model.folds} fold${model.folds === 1 ? "" : "s"} hold ${formatTokens(model.hiddenTokens)}`));
179
+ parts.push(ink.folded(`${model.folds} fold${model.folds === 1 ? "" : "s"} hide ${formatTokens(model.hiddenTokens)} tokens`));
170
180
  }
171
181
  // NO BRAND IN FRONT OF THE BAR (Shane 2026-09-02): the bar identifies itself, and the
172
182
  // two prose states above keep the brand because nothing else on them says whose they are.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fold",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "description": "Lossless context folding for the Pi coding agent: only the oldest context changes; the fresh window stays untouched.",
5
5
  "keywords": [
6
6
  "pi-package",