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 +4 -4
- package/package.json +1 -1
- package/src/notice.ts +4 -36
- package/src/run.ts +33 -7
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,
|
|
65
|
-
| `/zip stats` | tokens
|
|
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
|
|
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.
|
|
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,
|
|
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) => {
|
|
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
|
-
|
|
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, " : ""}
|
|
672
|
+
return `${PRODUCT}: ${state}, cache TTL ${Math.round(ttl / 1000)} s, ${this.quiet ? "notices off, " : ""}${this.sessionTotals(ctx)}${cache}`;
|
|
647
673
|
}
|
|
648
|
-
statsLine = () =>
|
|
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(); }
|