pi-fold 4.2.0 → 4.2.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
@@ -14,7 +14,7 @@
14
14
  pi install npm:pi-fold
15
15
  ```
16
16
 
17
- Node 22 or later, Pi 0.83 or later. MIT licensed.
17
+ Node 22 or later and Pi 0.83 or later. Pi 1.0 is supported and requires Node 22.19 or later. MIT licensed. Pi supplies the extension's runtime packages; they are declared as host-provided peers, not private dependencies.
18
18
 
19
19
  Folding does not require a memory store, but every best result measured came from pairing it with one. The campaigns used [pi-canon](https://github.com/shaneconner/canon), installed separately. The two began as a single working setup and were split into separate extensions for modularity, and the experiments are what established that they work better as complements than either does alone. Only that pairing has been measured. Any durable long-term memory store is expected to compose the same way, though that is a suspicion rather than a result.
20
20
 
@@ -40,11 +40,11 @@ In an interactive session one compact, full-height bar sits directly above Pi's
40
40
  <img alt="A single full-height usage bar. Batlow colours group consolidated folds, span folds, tool folds, marked content, raw context and pinned content in that order. The suffix shows percentage, commit point, and fold, mark, and pin counts." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/fold-bar-light.svg">
41
41
  </picture>
42
42
 
43
- The forty-column bar uses full-height half-blocks for eighty steps of fill, following the provider's measured count against the serving budget. Within that fill, colours show the estimated visible composition, grouped by distance from compression: **Consolidated Folds, Span Folds, Tool Folds, Marked, Raw, Pinned**. The first three are compressed now, deepest first; marked content folds at the next commit; raw content is eligible later; pinned content is held against every commit until released. The six shades, from consolidated to pinned, are read off a colour map, [Crameri's Batlow](https://www.fabiocrameri.ch/colourmaps/) end to end by default, so the bar reads as a gradient from most compressed to most settled. `/fold-settings` offers Batlow, viridis, plasma, magma and cividis, and where on the chosen map the two ends sit, so a slice of a map or a reversed one is a keypress away; the bar repaints on each step. Whatever the choice, each shade's lightness is moved away from the background, hue held, until it clears 4.5:1 on dark and light terminals, and label colours match the bar. Each nonempty category keeps at least one half-cell when the measured fill has room; remaining width follows relative mass. This visibility floor makes small compressed categories visible, so individual category widths are not token-exact. At very low occupancy, the largest shares take the available space. There are no individual fold edges, scores, or height differences.
43
+ The forty-column bar uses full-height half-blocks for eighty steps of fill, reading Pi's `getContextUsage()` directly on every render. Its percentage uses Pi's full context window, the same scale as Pi's footer, including Pi's estimate of messages since the last provider response. Within that fill, colours show the estimated visible composition, grouped by distance from compression: **Consolidated Folds, Span Folds, Tool Folds, Marked, Raw, Pinned**. The first three are compressed now, deepest first; marked content folds at the next commit; raw content is eligible later; pinned content is held against every commit until released. The six shades, from consolidated to pinned, are read off a colour map, [Crameri's Batlow](https://www.fabiocrameri.ch/colourmaps/) end to end by default, so the bar reads as a gradient from most compressed to most settled. `/fold-settings` offers Batlow, viridis, plasma, magma and cividis, and where on the chosen map the two ends sit, so a slice of a map or a reversed one is a keypress away; the bar repaints on each step. Whatever the choice, each shade's lightness is moved away from the background, hue held, until it clears 4.5:1 on dark and light terminals, and label colours match the bar. Each nonempty category keeps at least one half-cell when the measured fill has room; remaining width follows relative mass. This visibility floor makes small compressed categories visible, so individual category widths are not token-exact. At very low occupancy, the largest shares take the available space. There are no individual fold edges, scores, or height differences.
44
44
 
45
- The suffix reads `34% · commit at 55% · N Folds (X Cons., Y Span, Z Tool, X Mark, Y Pin)`, naming only the kinds with something to count, in the bar's order; a fresh session reads `0 Folds`. `N Folds` counts visible compressed folds; `Mark` counts pending marks and `Pin` counts protected entries, not additional compressed folds. A tool fold is lossless compression of tool-result context, not a count of tool calls. `/fold-status` retains the detailed token and nesting figures.
45
+ The suffix reads `34% · commit at 55% · N Folds (X Cons., Y Span, Z Tool, X Mark, Y Pin)`, naming only the kinds with something to count, in the bar's order; a fresh session reads `0 Folds`. `N Folds` counts visible compressed folds; `Mark` counts pending marks and `Pin` counts protected entries, not additional compressed folds. A tool fold is lossless compression of tool-result context, not a count of tool calls. `/fold-status` retains the detailed token and nesting figures. Its `Budget` row and the editor's `Budget` header report the separate serving budget and the tokens remaining before a commit.
46
46
 
47
- Two dotted marks stand in the band's edge cells: one at `minTarget`, where a commit aims to land, and one at `maxTarget`, where automatic folding is triggered. Each is drawn over the fill's own colour in a deeper tone of that same hue, so the fill continues behind the mark; on the empty track they stand in muted ink. Neither takes a colour of its own or a warning colour. Before a measurement the row says `not measured yet`; after a commit it identifies the old reading until the provider measures again. Crossing the target is routine runtime work, not a user action item. On reload, saved session context supplies the fold counts and composition without a new model message; the next real context event replaces that read-only view. Unavailable composition is stated as `mapping`; if folding stops, `FOLDING STOPPED` replaces the bar. Non-truecolor terminals use the theme's own colours.
47
+ Two dotted marks stand in the band's edge cells: one where a commit aims to land and one where automatic folding is triggered. `minTarget` and `maxTarget` remain shares of the serving budget; the marks and displayed commit point are converted to Pi's full-window scale, so the trigger does not move. With a 272,000-token window and the default 255,616-token serving budget, the 80% trigger appears at about 75% of Pi's window. Percentages are rounded to whole numbers; a custom footer may round differently. Each is drawn over the fill's own colour in a deeper tone of that same hue, so the fill continues behind the mark; on the empty track they stand in muted ink. Neither takes a colour of its own or a warning colour. When Pi reports unknown usage, including immediately after native compaction, the row says `not measured yet`; no saved count or local estimate fills the gap. After a fold commit it identifies the old reading until the provider measures again. Crossing the target is routine runtime work, not a user action item. On reload, saved session context supplies the fold counts and composition without a new model message; the next real context event replaces that read-only view. Unavailable composition is stated as `mapping`; if folding stops, `FOLDING STOPPED` replaces the bar. Non-truecolor terminals use the theme's own colours.
48
48
 
49
49
  ## Results
50
50
 
@@ -85,7 +85,7 @@ Seven options, and every other name is refused.
85
85
  | Option | Default | Effect |
86
86
  | --- | --- | --- |
87
87
  | `thresholds` | `{ maxTarget: 0.80, minTarget: 0.20, consolidateAfter: 10, minFoldChars: 8000 }` | The band, set whole or not at all. The first two are proportions of the serving budget. `consolidateAfter` is how many visible roots a parent is owed. `minFoldChars` is the size a fold has to reach to be worth making, and the floor a gap between folds has to clear to stay raw. Validated atomically; an impossible policy is refused by name rather than clamped. |
88
- | `providerInputBudget` | the transport's descriptor | The tokens this deployment may actually put in a request, already net of any output reservation. Every ratio, fence and budget on this page is a share of this one number. |
88
+ | `providerInputBudget` | derived from Pi's context window | The tokens this deployment may actually put in a request, already net of any output reservation when supplied. Without it, folding retains its default headroom of 16,384 tokens, capped at 10% of the window. This is a folding allowance, not a measured provider output reservation or a read of Pi's per-model compaction settings. Thresholds use this serving budget; the fold bar uses Pi's full-window scale. |
89
89
  | `blacklistAutoFoldTools` | `new Set()` | The exception list. Every completed tool batch is foldable without an agent mark; name a tool here and its results stay raw. |
90
90
  | `toolFoldThreshold` | `0.50` | The tool-call diet: a share in `[0, 1)` naming the oldest fraction of the window whose tool results are clipped in view. `0` turns it off. |
91
91
  | `preCommitNotice` | `false` | The notice switch. False is the shape that won the campaign above: no carrier reaches the projection and every fold ships the runtime's own words. `true` states the window's status to the agent once per approach to a commit: where the commit fires, how far away it is, what is staged, what that frees, what is pinned and what the pin costs, with the verbs listed and no argument made for using any of them. |
@@ -118,12 +118,13 @@ What the agent owns is the words. `brief` writes its own sentence onto a pending
118
118
  A commit fires on one condition: occupancy reaching `maxTarget` of the serving budget. It applies marks stalest first and stops at `minTarget`, so the newest events stay raw and a mark past the bound waits for the next epoch. Below `maxTarget` the window is quiet, because a commit is an epoch transition that risks the whole prefix cache and the right cadence is the fewest commits that keep the window inside its budget.
119
119
 
120
120
  ```
121
- 272,000 the transport's descriptor
121
+ 272,000 Pi's full context window
122
+ 255,616 default serving budget, after folding headroom
122
123
  ─────────────────────────────────────────────────────
123
124
  ~250k peak: inflow continues while the epoch computes
124
- 217,600 maxTarget 0.80 ← the commit fires here
125
+ 204,493 maxTarget 0.80 ← about 75% on Pi's scale
125
126
  ⋮ marks accumulate, free, quiet
126
- 54,400 minTarget 0.20 ← the commit stops here
127
+ 51,123 minTarget 0.20 ← about 19% on Pi's scale
127
128
  ─────────────────────────────────────────────────────
128
129
  ```
129
130
 
@@ -739,8 +739,8 @@ export function registerActiveContext(pi: any, options: {
739
739
  /**
740
740
  * THE FOLD BAR. One coloured composition row directly above the footer,
741
741
  * drawn by `renderFoldBar` from a model this function rebuilds on every status update.
742
- * The component reads the model and the LIVE theme at render time, so it repaints on
743
- * a theme switch, which the status string cannot; the host is asked to render after
742
+ * The component reads Pi's context usage and the LIVE theme at render time. Neither
743
+ * reading is cached between status events; the host is asked to render after
744
744
  * each rebuild. Installed once per host UI, on the first update that finds one.
745
745
  */
746
746
  const foldBar = {
@@ -753,6 +753,34 @@ export function registerActiveContext(pi: any, options: {
753
753
  /** Display-only reconstruction while waiting for the first real context event. */
754
754
  startupSnapshot: null as ActiveContextSnapshot | null,
755
755
  };
756
+ // DISPLAY ONLY. Pi owns the live reading, including its unmeasured trailing messages
757
+ // and post-compaction unknown state. Never substitute our saved provider measurement
758
+ // or projection estimate. The band remains a share of the SERVING budget: convert its
759
+ // points onto Pi's full-window axis, without changing any scheduling arithmetic.
760
+ const foldBarUsage = (ctx: any): Pick<FoldBarModel, "share" | "commitShare" | "aimShare"> => {
761
+ let usage: unknown;
762
+ try { usage = ctx.getContextUsage?.(); } catch { }
763
+ const reportedWindow = ownValue(usage, "contextWindow");
764
+ const modelWindow = ownValue(ctx.model, "contextWindow");
765
+ const validWindow = (value: unknown): value is number =>
766
+ typeof value === "number" && Number.isFinite(value) && value > 0;
767
+ const window = validWindow(reportedWindow) ? reportedWindow
768
+ : validWindow(modelWindow) ? modelWindow : DEFAULT_CONTEXT_WINDOW;
769
+ const tokens = ownValue(usage, "tokens");
770
+ const percent = ownValue(usage, "percent");
771
+ const share = validWindow(reportedWindow) && typeof tokens === "number" &&
772
+ Number.isFinite(tokens) && tokens >= 0 && typeof percent === "number" &&
773
+ Number.isFinite(percent) && percent >= 0 ? percent / 100 : null;
774
+ const capacity = capacityAccounting({
775
+ window: providerInputBudget ?? window,
776
+ truthful: providerInputBudget !== null,
777
+ descriptorWindow: window,
778
+ usedTokens: null,
779
+ });
780
+ const budgetShare = capacity.budgetTokens / window;
781
+ return { share, commitShare: thresholds.maxTarget * budgetShare,
782
+ aimShare: thresholds.minTarget * budgetShare };
783
+ };
756
784
  const installFoldBar = (ctx: any): void => {
757
785
  if (foldBar.installed || typeof ctx.ui?.setWidget !== "function") return;
758
786
  foldBar.installed = true;
@@ -760,10 +788,13 @@ export function registerActiveContext(pi: any, options: {
760
788
  foldBar.requestRender = () => { try { tui.requestRender(); } catch { } };
761
789
  return {
762
790
  /** The model behind the row, for the lab and the gates. */
763
- get model(): FoldBarModel | null { return foldBar.model; },
791
+ get model(): FoldBarModel | null {
792
+ return foldBar.model ? { ...foldBar.model, ...foldBarUsage(ctx) } : null;
793
+ },
764
794
  render(width: number): string[] {
765
- if (!foldBar.model) return [];
766
- try { return [renderFoldBar(foldBar.model, width, ctx.ui.theme)]; }
795
+ const model = this.model;
796
+ if (!model) return [];
797
+ try { return [renderFoldBar(model, width, ctx.ui.theme)]; }
767
798
  catch { return []; }
768
799
  },
769
800
  invalidate() { },
@@ -771,7 +802,7 @@ export function registerActiveContext(pi: any, options: {
771
802
  }, { placement: "belowEditor" });
772
803
  };
773
804
  const foldBarModel = (ctx: any, input: {
774
- share: number | null; staged: number; weighed: boolean;
805
+ share: number | null; commitShare: number; aimShare: number; staged: number; weighed: boolean;
775
806
  }): FoldBarModel => {
776
807
  const state = persistence.state;
777
808
  // THE SAME SNAPSHOT /fold-status PRICES AGAINST. `markFreedBytes` answers ZERO, silently,
@@ -785,8 +816,8 @@ export function registerActiveContext(pi: any, options: {
785
816
  const model: FoldBarModel = {
786
817
  brand: brandNoun,
787
818
  share: input.share,
788
- commitShare: thresholds.maxTarget,
789
- aimShare: thresholds.minTarget,
819
+ commitShare: input.commitShare,
820
+ aimShare: input.aimShare,
790
821
  mapped: false, mass: emptyFoldBarMass(),
791
822
  stagedMarks: input.staged,
792
823
  folds: 0, foldSpans: 0, foldTruncations: 0, foldConsolidations: 0,
@@ -863,13 +894,13 @@ export function registerActiveContext(pi: any, options: {
863
894
  const roots = persistence.state && lifecycle.latestSnapshot ? orderedRoots(persistence.state, lifecycle.latestSnapshot).length : 0;
864
895
  const staged = persistence.state ? pendingMarks(persistence.state).length : 0;
865
896
  const capacity = currentCapacity(ctx);
866
- const used = measurements.lastProviderMeasurement?.tokens ?? null;
867
- const share = used !== null && capacity.budgetTokens > 0 ? used / capacity.budgetTokens : null;
897
+ const usage = foldBarUsage(ctx);
898
+ const { share } = usage;
868
899
  try {
869
900
  installFoldBar(ctx);
870
901
  if (foldBar.installed) {
871
902
  foldBar.model = foldBarModel(ctx, {
872
- share, staged,
903
+ ...usage, staged,
873
904
  weighed: ladder.bandTopMeasurement !== null &&
874
905
  ladder.bandTopMeasurement === measurements.lastProviderMeasurement,
875
906
  });
@@ -889,7 +920,7 @@ export function registerActiveContext(pi: any, options: {
889
920
  // in /fold-settings went on being announced at its old value until the next event
890
921
  // rebuilt a snapshot. The number this line owes a person is the one that will fire,
891
922
  // and that is the runtime's own variable.
892
- const trigger = thresholds.maxTarget;
923
+ const trigger = usage.commitShare;
893
924
  // WHEN, not just how full. "68% full" only answers the second question a reader has
894
925
  // if they already remember where the trigger sits; naming it answers both.
895
926
  //
@@ -911,9 +942,13 @@ export function registerActiveContext(pi: any, options: {
911
942
  if (share !== null && !ladder.automaticFailure) {
912
943
  const weighed = ladder.bandTopMeasurement !== null &&
913
944
  ladder.bandTopMeasurement === measurements.lastProviderMeasurement;
945
+ // Pi may include a trailing estimate ahead of the last provider count. Being
946
+ // over the displayed point alone must not promise an imminent runtime commit.
947
+ const due = capacity.usedTokens !== null &&
948
+ capacity.usedTokens >= thresholds.maxTarget * capacity.budgetTokens;
914
949
  parts.push(share < trigger
915
950
  ? `commit at ${Math.round(trigger * 100)}%`
916
- : weighed ? "commit held" : "COMMIT DUE");
951
+ : weighed ? "commit held" : due ? "COMMIT DUE" : "at commit point");
917
952
  }
918
953
  if (staged > 0) parts.push(`${staged} staged`);
919
954
  if (roots > 0) parts.push(`${roots} fold${roots === 1 ? "" : "s"}`);
@@ -261,12 +261,13 @@ export function foldStatusText(input: {
261
261
 
262
262
  if (input.usedTokens === null || !(input.budgetTokens > 0)) {
263
263
  // "provider" is infrastructure vocabulary and "sets it" names no observable event.
264
- lines.push(" Window not measured yet; the first model response will measure it.");
264
+ lines.push(" Budget not measured yet; the first model response will measure it.");
265
265
  } else {
266
266
  const share = input.usedTokens / input.budgetTokens;
267
267
  const filled = Math.max(0, Math.min(STATUS_BAR_WIDTH, Math.round(share * STATUS_BAR_WIDTH)));
268
268
  const bar = `${"▇".repeat(filled)}${"░".repeat(STATUS_BAR_WIDTH - filled)}`;
269
- lines.push(` Window ${bar} ${Math.round(share * 100)}% ` +
269
+ // This detailed view reports serving headroom, not Pi's full-window percentage.
270
+ lines.push(` Budget ${bar} ${Math.round(share * 100)}% ` +
270
271
  `${n(input.usedTokens)} / ${n(input.budgetTokens)} tokens`);
271
272
  const commitAt = input.commitAtShare * input.budgetTokens;
272
273
  // The headroom is what a person actually wants: not the trigger's share, but how much
@@ -1125,7 +1125,7 @@ export class FoldEditorView {
1125
1125
  // one surface the rule was never checked against. Nothing here is estimated.
1126
1126
  if (occupancy.usedTokens === null || !(occupancy.budgetTokens > 0)) {
1127
1127
  lines.push(truncateToWidth(
1128
- "window not measured yet; the first model response will measure it", width));
1128
+ "budget not measured yet; the first model response will measure it", width));
1129
1129
  for (const line of this.stagedLines()) lines.push(truncateToWidth(line, width));
1130
1130
  return lines;
1131
1131
  }
@@ -1153,7 +1153,7 @@ export class FoldEditorView {
1153
1153
  : ` · commit at ${(occupancy.commitOccupancy * 100).toFixed(0)}%${headroom}` +
1154
1154
  `${occupancy.commitDue ? " · COMMIT DUE" : ""}`;
1155
1155
  lines.push(truncateToWidth(
1156
- `${occupancyBar(occupancy.usedTokens, occupancy.budgetTokens)} ` +
1156
+ `Budget ${occupancyBar(occupancy.usedTokens, occupancy.budgetTokens)} ` +
1157
1157
  `${percent}%${timing}` +
1158
1158
  ` · ${usedTokensText}/${occupancy.budgetTokens.toLocaleString("en-US")}`,
1159
1159
  width,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fold",
3
- "version": "4.2.0",
3
+ "version": "4.2.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",
@@ -37,7 +37,8 @@
37
37
  "test": "node tests/verify.mjs"
38
38
  },
39
39
  "peerDependencies": {
40
- "@earendil-works/pi-coding-agent": ">=0.83.0 <1"
40
+ "@earendil-works/pi-coding-agent": "*",
41
+ "@earendil-works/pi-tui": "*"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@eslint/js": "^10.0.1",
@@ -52,8 +53,5 @@
52
53
  "extensions": [
53
54
  "./extensions"
54
55
  ]
55
- },
56
- "dependencies": {
57
- "@earendil-works/pi-tui": "^0.84.2"
58
56
  }
59
57
  }