pi-zip 0.2.2 → 0.2.4

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
@@ -61,8 +61,8 @@ The cold cap is kept below Pi's own compaction trigger (window minus `compaction
61
61
 
62
62
  | Command | Effect |
63
63
  |---|---|
64
- | `/zip status` | on / off / paused, cache TTL, totals |
65
- | `/zip stats` | tokens folded, estimated $ saved versus doing nothing, summaries, recalls |
64
+ | `/zip status` | on / off / paused, cache TTL, what this session has folded, summarised and recalled, learned cache survival |
65
+ | `/zip stats` | this session's folds (with tokens removed), summaries and recalls; persisted in the session, so a restart does not reset them |
66
66
  | `/zip off` | strict no-op: no folds, no summaries, requests left untouched (earlier folds stay recallable) |
67
67
  | `/zip on` | resume |
68
68
  | `/zip quiet` | toggle the per-turn notice (folding continues) |
@@ -109,9 +109,9 @@ The guard has two parts. Before saving a fold or a summary it checks that the ed
109
109
 
110
110
  ## FAQ
111
111
 
112
- **Will it save money?** Mostly on cold returns, which is where a long session pays for a full cache rewrite. While the cache is warm it edits only when the inequality above says the rewrite pays back (large contexts, near Pi's compaction trigger, outputs 60+ requests old). `/zip stats` shows an estimate based on the model's declared prices: avoided cache writes and reads, minus summary calls, minus the cost of content you recalled. It is an estimate (token counts are chars/4, scaled by the calibration above), and it can be negative.
112
+ **Will it save money?** Mostly on cold returns, which is where a long session pays for a full cache rewrite. While the cache is warm it edits only when the inequality above says the rewrite pays back (large contexts, near Pi's compaction trigger, outputs 60+ requests old). `/zip stats` shows what this session has folded (and roughly how many tokens that removed), its summaries with what their model calls cost, and its recalls. The numbers live in the session file, so a restart does not reset them.
113
113
 
114
- **Does it cost extra?** Planning is free. A summary is one extra model call (the current model, no tools, no prompt cache), shown in `/zip stats`. A background summary you never use (you came back while the cache was warm) is counted too. Recalled content re-enters the context at normal prices.
114
+ **Does it cost extra?** Planning is free. A summary is one extra model call (the current model, no tools, no prompt cache), shown in `/zip stats`. A background summary you never use (you came back while the cache was warm) is counted there too. Recalled content re-enters the context at normal prices.
115
115
 
116
116
  **Can the model lose information?** Folded outputs are replaced by a placeholder with key lines and a handle, and the placeholder tells the model not to guess. Summaries quote your requests verbatim and never paraphrase the model's reasoning. If the model ignores the handle and guesses, that is a model failure pi-zip cannot catch; this is why only re-readable outputs of the previous turn are folded at a cold return; other outputs of the protected turns wait until they are 60 requests old.
117
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zip",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Keeps long Pi sessions cheap without losing anything: folds old tool output only when the prompt cache has already gone cold, and every fold can be recalled byte for byte. Zero config.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/notice.ts CHANGED
@@ -27,12 +27,6 @@ export function noticeText(actions: NoticeAction[]): string {
27
27
  return `${PRODUCT} · ${segs.join(" · ")}`;
28
28
  }
29
29
 
30
- /** Prices are $ per million tokens, as declared by the model. */
31
- export interface Prices {
32
- cacheWrite: number;
33
- cacheRead: number;
34
- }
35
-
36
30
  export class Stats {
37
31
  folds = 0; // outputs folded
38
32
  foldedTokens = 0; // tokens removed by folds (estimate: chars/4)
@@ -49,37 +43,11 @@ export class Stats {
49
43
  activeSaved = 0; // tokens currently removed from the context by our edits
50
44
  notices = 0;
51
45
 
52
- /** Estimated $ saved versus doing nothing: avoided cache writes and reads, minus summary calls, recalled content re-entering the context, and warm-valve rewrites. */
53
- savedUsd(p: Prices | null): number | null {
54
- if (!p) return null;
55
- const gross = (this.writeSavedTokens * p.cacheWrite + this.readSavedTokens * p.cacheRead) / 1e6;
56
- const recall = (this.recallChars / 4) * p.cacheWrite / 1e6;
57
- const pressure = this.pressureRewriteTokens * Math.max(p.cacheWrite - p.cacheRead, 0) / 1e6;
58
- return gross - this.summaryUsd - recall - pressure;
59
- }
60
- }
61
-
62
- export const pricesOf = (model: Any): Prices | null => {
63
- const c = model?.cost;
64
- const cw = c?.cacheWrite > 0 ? c.cacheWrite : c?.input > 0 ? c.input : 0;
65
- const cr = c?.cacheRead > 0 ? c.cacheRead : cw * 0.1;
66
- return cw > 0 ? { cacheWrite: cw, cacheRead: cr } : null;
67
- };
68
-
69
- export function statsText(s: Stats, model: Any): string {
70
- const usd = s.savedUsd(pricesOf(model));
71
- const money = usd === null ? "n/a (model declares no prices)" : `${usd < 0 ? "-" : ""}$${Math.abs(usd).toFixed(usd !== 0 && Math.abs(usd) < 0.1 ? 4 : 2)}`;
72
- return (
73
- `${PRODUCT} stats (since pi started): folded ${s.folds} output${s.folds === 1 ? "" : "s"} (${fmtK(s.foldedTokens)} tokens), ` +
74
- `${s.summaries} summar${s.summaries === 1 ? "y" : "ies"} (${fmtK(s.summarizedTokens)} tokens, ${(s.summaryMs / 1000).toFixed(1)} s waited, $${s.summaryUsd.toFixed(4)}), ` +
75
- `${s.recalls} recall${s.recalls === 1 ? "" : "s"} (~${fmtK(s.recallChars / 4)} tokens re-read). ` +
76
- `Estimated saved vs doing nothing: ${money}, counting avoided cache writes and reads minus summary calls, recalled content and warm-valve rewrites; token counts are chars/4 estimates.`
77
- );
78
46
  }
79
47
 
80
48
  export interface ZipControl {
81
- status(): string;
82
- statsLine(): string;
49
+ status(ctx?: Any): string;
50
+ statsLine(ctx?: Any): string;
83
51
  setOff(off: boolean): string;
84
52
  toggleQuiet(): string;
85
53
  }
@@ -90,8 +58,8 @@ export function registerZipCommand(pi: ExtensionAPI, zip: ZipControl) {
90
58
  handler: async (args: string, cctx: Any) => {
91
59
  const sub = (args ?? "").trim().toLowerCase() || "status";
92
60
  const text =
93
- sub === "status" ? zip.status()
94
- : sub === "stats" ? zip.statsLine()
61
+ sub === "status" ? zip.status(cctx)
62
+ : sub === "stats" ? zip.statsLine(cctx)
95
63
  : sub === "off" ? zip.setOff(true)
96
64
  : sub === "on" ? zip.setOff(false)
97
65
  : sub === "quiet" ? zip.toggleQuiet()
package/src/run.ts CHANGED
@@ -6,16 +6,17 @@ import { dirname } from "node:path";
6
6
  import { detectCold, lastMessageMs, lastPrompt, modelKey, resolveTtl, ttlFor } from "./cache.ts";
7
7
  import { describe, lawPrices, loadStats, pWarm, record, sample, GAP_EDGES, type Entry } from "./learn.ts";
8
8
  import { validateEdits, repairPayload } from "./guard.ts";
9
- import { Stats, noticeText, statsText, type NoticeAction, type ZipControl } from "./notice.ts";
9
+ import { fmtK, Stats, noticeText, type NoticeAction, type ZipControl } from "./notice.ts";
10
10
  import { applyPlanToMessages, buildBlocks, calibrate, countUserTurns, G0, planContext, reserveTokensFor, untouchedEst, type Block, type Calibration, type Cut, type FoldTarget, type Law, type PlanOpts, type PlanResult, type RunPlan } from "./plan.ts";
11
- import { handleFor } from "./placeholder.ts";
11
+ import { handleFor, PH_MARK, RECALL_TOOL } from "./placeholder.ts";
12
12
  import { recalledHandlesFromBranch } from "./recall.ts";
13
13
  import { buildCut } from "./summary.ts";
14
- import { type Any, PRODUCT, tok4 } from "./util.ts";
14
+ import { type Any, PRODUCT, textOf, tok4 } from "./util.ts";
15
15
 
16
16
  export const PLAN_CUSTOM = "pi-zip/plan";
17
17
  export const STATE_CUSTOM = "pi-zip/state";
18
18
  export const STEER_CUSTOM = "pi-zip/steer";
19
+ export const UNUSED_SUMMARY_CUSTOM = "pi-zip/unused-summary"; // a background summary nobody adopted: its cost, so the session totals stay honest
19
20
  export const AWAY_FRACTION = 0.8; // the away-timer fires this far into the cache lifetime
20
21
  // Other context managers rewrite the view too (F14); two writers give unpredictable results, so we pause and keep only the Guard.
21
22
  const CONFLICT_RE = /billion-context|bc-pi|magic-context|smart-compact|hot-compact|context-prune|pi-vcc|context-mode|prefix-cache/i;
@@ -270,7 +271,13 @@ export class Zip implements ZipControl {
270
271
  this.bg = null;
271
272
  if (!b) return;
272
273
  if (!b.done) b.controller.abort();
273
- b.promise.then((c) => { if (c) this.stats.summaryUsd += c.costUsd; });
274
+ b.promise.then((c) => {
275
+ if (!c) return;
276
+ this.stats.summaryUsd += c.costUsd;
277
+ try {
278
+ this.pi.appendEntry(UNUSED_SUMMARY_CUSTOM, { usd: c.costUsd });
279
+ } catch {}
280
+ });
274
281
  }
275
282
 
276
283
  async context(e: Any, ctx: Any): Promise<Any> {
@@ -636,16 +643,35 @@ export class Zip implements ZipControl {
636
643
  this.pi.appendEntry(STATE_CUSTOM, { off: this.off, quiet: this.quiet });
637
644
  } catch {}
638
645
  }
639
- status(): string {
646
+ /** What this session carries: folds and summaries persisted in its file (they survive restarts) and the recalls in its branch. */
647
+ private sessionTotals(ctx: Any): string {
648
+ const branch = ctx ? this.branch(ctx) : [];
649
+ const byId = new Map(branch.map((e: Any) => [e?.id, e]));
650
+ let folds = 0, folded = 0, summaries = 0, recalls = 0, usd = 0;
651
+ for (const e of branch) {
652
+ const ph = String(e?.replacement?.content?.[0]?.text ?? "");
653
+ if (e?.type === "context_edit" && ph.startsWith(PH_MARK)) {
654
+ folds++;
655
+ folded += Math.max(0, textOf(byId.get(e.targetId)?.message?.content).length - ph.length) / 4;
656
+ } else if (e?.type === "compaction" && e.details?.by === PRODUCT) {
657
+ summaries++;
658
+ usd += Number(e.usage?.cost?.total) || 0;
659
+ } else if (e?.type === "custom" && e.customType === UNUSED_SUMMARY_CUSTOM) usd += Number(e.data?.usd) || 0;
660
+ else if (e?.type === "message" && e.message?.role === "toolResult" && e.message?.toolName === RECALL_TOOL) recalls++;
661
+ }
662
+ const n = (x: number, one: string, many: string) => `${x} ${x === 1 ? one : many}`;
663
+ return `this session: ${n(folds, "folded output", "folded outputs")} (~${fmtK(folded)} tokens), ${n(summaries, "summary", "summaries")}${usd > 0 ? ` (summary calls $${usd.toFixed(4)})` : ""}, ${n(recalls, "recall", "recalls")}`;
664
+ }
665
+ status(ctx?: Any): string {
640
666
  const state = this.off ? "off" : this.conflict ? `paused ("${this.conflict}" also manages context; only the request guard is on)` : "on";
641
667
  const key = modelKey(this.model), ttl = ttlFor(this.model), ent = loadStats().models[key];
642
668
  const pr = lawPrices(ent?.cls, ttl >= 3_600_000);
643
669
  const cache = !key ? "" : `\ncache ${key}: ${pr ? `${pr.cls} (from usage)` : "class unknown until the first response"}` +
644
670
  `, ${pr ? `read ${+pr.r.toFixed(4)} / write ${+pr.w.toFixed(4)} / output ${+pr.out.toFixed(4)} x input (class ratios)` : "no prices (legacy rule)"}` +
645
671
  `, ${describe(ent, ttl / 1000)}, growth ${(this.g / 1000).toFixed(1)}K/request`;
646
- return `${PRODUCT}: ${state}, cache TTL ${Math.round(ttl / 1000)} s, ${this.quiet ? "notices off, " : ""}folded ${this.stats.folds} output${this.stats.folds === 1 ? "" : "s"}, ${this.stats.summaries} summar${this.stats.summaries === 1 ? "y" : "ies"} (/zip stats for details)${cache}`;
672
+ return `${PRODUCT}: ${state}, cache TTL ${Math.round(ttl / 1000)} s, ${this.quiet ? "notices off, " : ""}${this.sessionTotals(ctx)}${cache}`;
647
673
  }
648
- statsLine = () => statsText(this.stats, this.model);
674
+ statsLine = (ctx?: Any) => `${PRODUCT} ${this.sessionTotals(ctx)} (token counts are chars/4 estimates)`;
649
675
  setOff(off: boolean): string {
650
676
  this.off = off;
651
677
  if (off) { this.runPlan = null; this.runEdits = false; this.cancelTimer(); this.discardBg(); }