pi-zip 0.2.1 → 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/plan.ts +9 -2
- package/src/run.ts +38 -8
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/plan.ts
CHANGED
|
@@ -282,6 +282,7 @@ export interface PlanOpts {
|
|
|
282
282
|
relax?: boolean; // default RELAX_PREV_TURN
|
|
283
283
|
minGain?: number;
|
|
284
284
|
inturnAge?: number; // default settings().inturnAge (PI_ZIP_INTURN_AGE, 60); 0 = outputs of the protected turns never fold on age
|
|
285
|
+
noSummary?: boolean; // a summary was already made while this cache stayed warm: no second one unless the context is at/above the compaction room
|
|
285
286
|
rereadOnly?: boolean; // zip_recall is not available to the model: fold only outputs that can be re-read (classify "rereadable")
|
|
286
287
|
pastTtl?: boolean; // a user return after the declared TTL: a warm plan may relax into the previous user turn too (RELAX_PREV_TURN)
|
|
287
288
|
}
|
|
@@ -417,7 +418,13 @@ export function planContext(entries: Any[], o: PlanOpts): PlanResult | null {
|
|
|
417
418
|
}
|
|
418
419
|
}
|
|
419
420
|
let ctxAfterFolds = ctxEst - savings();
|
|
420
|
-
|
|
421
|
+
// what no summary can remove: the system prompt, the protected turns (after their folds) and the previous summary, which the next
|
|
422
|
+
// one carries forward; the cap is never chased below it (a context that is mostly this floor would be re-summarised for nothing)
|
|
423
|
+
const prevSum = blocks[0]?.kind === "summary" ? blocks[0].tokens : 0;
|
|
424
|
+
const floor = o.sys + prevSum + blocks.reduce((a, b) => a + (protectedTurn(b) ? (tokOverride.get(b.idx) ?? b.tokens) : 0), 0);
|
|
425
|
+
const target = Math.max(coldCap, floor + SUMMARY_FLOOR);
|
|
426
|
+
const atRoom = room !== null && k * ctxAfterFolds >= room;
|
|
427
|
+
const sumTrigger: "cold" | "valve" | null = ctxAfterFolds > target && (!o.noSummary || atRoom) ? trig : null;
|
|
421
428
|
let cutIdx: number | null = null;
|
|
422
429
|
let prefixTokens = 0;
|
|
423
430
|
let summaryTokensPlanned = 0;
|
|
@@ -436,7 +443,7 @@ export function planContext(entries: Any[], o: PlanOpts): PlanResult | null {
|
|
|
436
443
|
acc += tokOverride.get(i) ?? blocks[i].tokens;
|
|
437
444
|
}
|
|
438
445
|
const total = o.sys + acc;
|
|
439
|
-
const S = (x: number) => clamp(SUMMARY_RATIO * x, SUMMARY_FLOOR, SUMMARY_CAP);
|
|
446
|
+
const S = (x: number) => Math.max(clamp(SUMMARY_RATIO * x, SUMMARY_FLOOR, SUMMARY_CAP), prevSum); // the next summary carries the previous one forward
|
|
440
447
|
if (cuts.length) {
|
|
441
448
|
let pick = cuts[cuts.length - 1];
|
|
442
449
|
for (const c of cuts) if (total - pre[c] + S(pre[c]) <= coldCap) { pick = c; break; }
|
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;
|
|
@@ -94,6 +95,8 @@ export class Zip implements ZipControl {
|
|
|
94
95
|
|
|
95
96
|
/** zip_recall declared to the model? Checked at session start and before every run (index.ts); a `--tools` allowlist hides it. */
|
|
96
97
|
recallOk = true;
|
|
98
|
+
/** A summary was persisted since the last cold run start: later warm plans make none unless at the compaction room. */
|
|
99
|
+
summarizedWarm = false;
|
|
97
100
|
setRecallOk(ok: boolean) {
|
|
98
101
|
if (ok !== this.recallOk) this.ledger({ type: "recall_available", ok });
|
|
99
102
|
this.recallOk = ok;
|
|
@@ -146,7 +149,7 @@ export class Zip implements ZipControl {
|
|
|
146
149
|
private opts(ctx: Any, pending: boolean, mode: "cold" | "warm", entries: Any[], pWarm = mode === "cold" ? 0 : 1): { o: PlanOpts; cal: Calibration } {
|
|
147
150
|
const sys = this.sysTokens(ctx);
|
|
148
151
|
const cal = calibrate(entries, sys);
|
|
149
|
-
return { cal, o: { mode, sys, k: cal.k, cwd: (ctx?.cwd as string) ?? process.cwd(), recalled: this.recalled, model: ctx?.model, promptPending: pending, rereadOnly: !this.recallOk, reserve: this.reserve(ctx), steerIds: this.steerIds, law: this.law(ctx, pWarm), trace: [] } };
|
|
152
|
+
return { cal, o: { mode, sys, k: cal.k, cwd: (ctx?.cwd as string) ?? process.cwd(), recalled: this.recalled, model: ctx?.model, promptPending: pending, rereadOnly: !this.recallOk, noSummary: mode === "warm" && this.summarizedWarm, reserve: this.reserve(ctx), steerIds: this.steerIds, law: this.law(ctx, pWarm), trace: [] } };
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
private law(ctx: Any, pWarm: number): Law {
|
|
@@ -234,6 +237,7 @@ export class Zip implements ZipControl {
|
|
|
234
237
|
if (!this.prevKey) ({ key: this.prevKey, total: this.prevTotal } = lastPrompt(branch)); // fresh process: the session's newest response
|
|
235
238
|
const { cold, reason, ttl, pWarm: p, src, gapS, pastTtl } = detectCold(ctx.model, this.lastReqMs, branch, Date.now(), this.lastModelKey, (g, prior) => pWarm(this.ent, g, prior));
|
|
236
239
|
this.cold = cold;
|
|
240
|
+
if (cold) this.summarizedWarm = false; // a cold return may summarise again
|
|
237
241
|
this.pastTtl = pastTtl;
|
|
238
242
|
this.pWarm = p;
|
|
239
243
|
this.survSrc = src;
|
|
@@ -267,7 +271,13 @@ export class Zip implements ZipControl {
|
|
|
267
271
|
this.bg = null;
|
|
268
272
|
if (!b) return;
|
|
269
273
|
if (!b.done) b.controller.abort();
|
|
270
|
-
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
|
+
});
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
async context(e: Any, ctx: Any): Promise<Any> {
|
|
@@ -512,6 +522,7 @@ export class Zip implements ZipControl {
|
|
|
512
522
|
return undefined;
|
|
513
523
|
}
|
|
514
524
|
const bad = checkEdits(o.blocks, o.userTurns, live, cut);
|
|
525
|
+
if (!bad && cut) this.summarizedWarm = true; // one summary per warm stretch (two in a row cost two uncached calls; the second gains little)
|
|
515
526
|
if (bad) {
|
|
516
527
|
this.ledger({ type: "guard_drop", turnIndex: e.turnIndex, reason: bad, folds: live.length, source: plan.source });
|
|
517
528
|
plan.persisted = false; // keep the request-local view: never switch the fold set mid-run
|
|
@@ -632,16 +643,35 @@ export class Zip implements ZipControl {
|
|
|
632
643
|
this.pi.appendEntry(STATE_CUSTOM, { off: this.off, quiet: this.quiet });
|
|
633
644
|
} catch {}
|
|
634
645
|
}
|
|
635
|
-
|
|
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 {
|
|
636
666
|
const state = this.off ? "off" : this.conflict ? `paused ("${this.conflict}" also manages context; only the request guard is on)` : "on";
|
|
637
667
|
const key = modelKey(this.model), ttl = ttlFor(this.model), ent = loadStats().models[key];
|
|
638
668
|
const pr = lawPrices(ent?.cls, ttl >= 3_600_000);
|
|
639
669
|
const cache = !key ? "" : `\ncache ${key}: ${pr ? `${pr.cls} (from usage)` : "class unknown until the first response"}` +
|
|
640
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)"}` +
|
|
641
671
|
`, ${describe(ent, ttl / 1000)}, growth ${(this.g / 1000).toFixed(1)}K/request`;
|
|
642
|
-
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}`;
|
|
643
673
|
}
|
|
644
|
-
statsLine = () =>
|
|
674
|
+
statsLine = (ctx?: Any) => `${PRODUCT} ${this.sessionTotals(ctx)} (token counts are chars/4 estimates)`;
|
|
645
675
|
setOff(off: boolean): string {
|
|
646
676
|
this.off = off;
|
|
647
677
|
if (off) { this.runPlan = null; this.runEdits = false; this.cancelTimer(); this.discardBg(); }
|