pi-zip 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +139 -2
- package/package.json +38 -4
- package/src/cache.ts +116 -0
- package/src/classify.ts +77 -0
- package/src/guard.ts +192 -0
- package/src/index.ts +20 -0
- package/src/learn.ts +141 -0
- package/src/notice.ts +108 -0
- package/src/placeholder.ts +135 -0
- package/src/plan.ts +588 -0
- package/src/recall.ts +199 -0
- package/src/run.ts +649 -0
- package/src/summary.ts +500 -0
- package/src/util.ts +50 -0
package/src/run.ts
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
// The per-session state machine behind index.ts: cold detection, the run plan (F8), the warm valve (F10), persistence at turn_end,
|
|
2
|
+
// settle preparation and the away-timer for the summary (F12).
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { detectCold, lastMessageMs, lastPrompt, modelKey, resolveTtl, ttlFor } from "./cache.ts";
|
|
7
|
+
import { describe, lawPrices, loadStats, pWarm, record, sample, GAP_EDGES, type Entry } from "./learn.ts";
|
|
8
|
+
import { validateEdits, repairPayload } from "./guard.ts";
|
|
9
|
+
import { Stats, noticeText, statsText, type NoticeAction, type ZipControl } from "./notice.ts";
|
|
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";
|
|
12
|
+
import { recalledHandlesFromBranch } from "./recall.ts";
|
|
13
|
+
import { buildCut } from "./summary.ts";
|
|
14
|
+
import { type Any, PRODUCT, tok4 } from "./util.ts";
|
|
15
|
+
|
|
16
|
+
export const PLAN_CUSTOM = "pi-zip/plan";
|
|
17
|
+
export const STATE_CUSTOM = "pi-zip/state";
|
|
18
|
+
export const STEER_CUSTOM = "pi-zip/steer";
|
|
19
|
+
export const AWAY_FRACTION = 0.8; // the away-timer fires this far into the cache lifetime
|
|
20
|
+
// Other context managers rewrite the view too (F14); two writers give unpredictable results, so we pause and keep only the Guard.
|
|
21
|
+
const CONFLICT_RE = /billion-context|bc-pi|magic-context|smart-compact|hot-compact|context-prune|pi-vcc|context-mode|prefix-cache/i;
|
|
22
|
+
|
|
23
|
+
/** The guard's view of a candidate edit set; folds whose entry left the projection are ignored. */
|
|
24
|
+
export function checkEdits(blocks: Block[], userTurns: number, folds: FoldTarget[], cut: Cut | null): string | null {
|
|
25
|
+
const byId = new Map(blocks.map((b) => [b.entryId, b]));
|
|
26
|
+
const live = folds.filter((t) => byId.has(t.entryId));
|
|
27
|
+
const cutBlock = cut ? byId.get(cut.firstKeptEntryId) : undefined;
|
|
28
|
+
if (cut && !cutBlock) return `cut target ${cut.firstKeptEntryId} is not in the projection`;
|
|
29
|
+
return validateEdits(blocks, { folds: new Map(live.map((t) => [byId.get(t.entryId)!.idx, t.ph])), recover: new Map(live.map((t) => [byId.get(t.entryId)!.idx, t.recover])), cut: cutBlock ? cutBlock.idx : null }, userTurns);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A summary being prepared (or finished) while the user is away. It has its own AbortController: no run owns it. */
|
|
33
|
+
interface Bg {
|
|
34
|
+
key: string | null; // firstKeptEntryId of the cut it was written for
|
|
35
|
+
done: Cut | null;
|
|
36
|
+
promise: Promise<Cut | null>;
|
|
37
|
+
controller: AbortController;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolves with the promise's value, or null as soon as the signal aborts (Esc cancels the waiting, not the work). */
|
|
41
|
+
function waitFor<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T | null> {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
if (signal?.aborted) return resolve(null);
|
|
44
|
+
const onAbort = () => resolve(null);
|
|
45
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
promise.then(resolve, () => resolve(null)).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const errText = (err: unknown) => (err instanceof Error ? (err.stack ?? err.message) : String(err));
|
|
51
|
+
|
|
52
|
+
export class Zip implements ZipControl {
|
|
53
|
+
off = false;
|
|
54
|
+
quiet = false;
|
|
55
|
+
conflict: string | null = null;
|
|
56
|
+
readonly stats = new Stats();
|
|
57
|
+
readonly recalled = new Set<string>();
|
|
58
|
+
private cold = false;
|
|
59
|
+
private coldReason = "";
|
|
60
|
+
private runChecked = true; // the first request of the run has been looked at
|
|
61
|
+
private runEdits = false; // this run carries an edit plan that turn_end has yet to persist
|
|
62
|
+
private runPlan: RunPlan | null = null;
|
|
63
|
+
private lastReqMs = 0; // the real-time cache clock: newest request start or end
|
|
64
|
+
private lastModelKey = ""; // provider/model of the newest request: a cache entry belongs to one model
|
|
65
|
+
private settleIds: string[] = []; // entries the settle plan folded: only used to label a plan as prepared
|
|
66
|
+
private bg: Bg | null = null;
|
|
67
|
+
private bgWaitMs = 0;
|
|
68
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
69
|
+
private timerGen = 0;
|
|
70
|
+
private wireLedgered = false;
|
|
71
|
+
private model: Any;
|
|
72
|
+
private conflictNoticed = false;
|
|
73
|
+
private ttlNoticed = false;
|
|
74
|
+
private runUserSeen = 0;
|
|
75
|
+
private steerTs = new Set<number>(); // timestamps of steering/follow-up user messages not yet marked in the session
|
|
76
|
+
private steerIds = new Set<string>(); // marked: user entries that are NOT new user turns
|
|
77
|
+
// the law's inputs (round 5): P(warm) of this run's return, growth g, and the free cache signal of every response
|
|
78
|
+
private pWarm = 1;
|
|
79
|
+
private pastTtl = false; // this run is a user return after the declared TTL (the previous user turn is eligible: plan.ts PlanOpts.pastTtl)
|
|
80
|
+
private survSrc = "no prior request";
|
|
81
|
+
private ent: Entry | undefined; // learned class + survival of the current model (learn.ts)
|
|
82
|
+
private g = G0; // EWMA(0.1) of real-token growth between consecutive responses, capped at the p90 of the last 20 deltas
|
|
83
|
+
private deltas: number[] = [];
|
|
84
|
+
private prevTotal = 0; // prompt size (input + cacheRead + cacheWrite) of the newest response, and its model
|
|
85
|
+
private prevKey = "";
|
|
86
|
+
private branchLastMs = 0; // the branch clock at run start (a fresh process knows the gap from the session)
|
|
87
|
+
private nextEdit: number | null = null; // the next request is the first to carry a new edit: its untouched prefix (real tokens)
|
|
88
|
+
private pending: { gapS: number; expect: number; edited: boolean; key: string } | null = null;
|
|
89
|
+
private justEdited = false; // the newest response answered a request that first carried an edit: the next request gets no warm edit
|
|
90
|
+
|
|
91
|
+
constructor(private pi: ExtensionAPI) {}
|
|
92
|
+
|
|
93
|
+
active = () => !this.off && !this.conflict;
|
|
94
|
+
|
|
95
|
+
ledger(rec: Record<string, unknown>) {
|
|
96
|
+
const path = process.env.PI_ZIP_LEDGER; // test only
|
|
97
|
+
if (!path) return;
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
100
|
+
appendFileSync(path, JSON.stringify({ ts: new Date().toISOString(), pid: process.pid, ...rec }) + "\n");
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private notify(ctx: Any, text: string) {
|
|
105
|
+
this.stats.notices++;
|
|
106
|
+
this.ledger({ type: "notice", text });
|
|
107
|
+
if (ctx?.ui?.notify && (ctx.mode === "tui" || ctx.mode === "rpc")) {
|
|
108
|
+
try {
|
|
109
|
+
ctx.ui.notify(text, "info");
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private branch(ctx: Any): Any[] {
|
|
115
|
+
try {
|
|
116
|
+
return ctx.sessionManager.getBranch() as Any[];
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private reserve(ctx: Any): number {
|
|
123
|
+
try {
|
|
124
|
+
return reserveTokensFor(this.pi.getSettings?.(), ctx?.model);
|
|
125
|
+
} catch {
|
|
126
|
+
return reserveTokensFor(undefined, ctx?.model);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private sysTokens(ctx: Any): number {
|
|
131
|
+
try {
|
|
132
|
+
return tok4(ctx.getSystemPrompt?.() ?? "");
|
|
133
|
+
} catch {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Plan options with the token scale read from the branch (stateless: a fresh process calibrates exactly like a long-lived one). */
|
|
139
|
+
private opts(ctx: Any, pending: boolean, mode: "cold" | "warm", entries: Any[], pWarm = mode === "cold" ? 0 : 1): { o: PlanOpts; cal: Calibration } {
|
|
140
|
+
const sys = this.sysTokens(ctx);
|
|
141
|
+
const cal = calibrate(entries, sys);
|
|
142
|
+
return { cal, o: { mode, sys, k: cal.k, cwd: (ctx?.cwd as string) ?? process.cwd(), recalled: this.recalled, model: ctx?.model, promptPending: pending, reserve: this.reserve(ctx), steerIds: this.steerIds, law: this.law(ctx, pWarm), trace: [] } };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private law(ctx: Any, pWarm: number): Law {
|
|
146
|
+
const m = ctx?.model ?? this.model;
|
|
147
|
+
const ent = this.entKey === modelKey(m) ? this.ent : undefined;
|
|
148
|
+
return { pr: lawPrices(ent?.cls, resolveTtl(m, this.branch(ctx)).ms >= 3_600_000), g: this.g, pWarm };
|
|
149
|
+
}
|
|
150
|
+
private entKey = "";
|
|
151
|
+
private loadEnt(key: string) {
|
|
152
|
+
this.entKey = key;
|
|
153
|
+
this.ent = loadStats().models[key];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Ledger form of the law's evaluations: Phi, K, eta, T next to g, w/r, P(warm) and where the inputs came from. */
|
|
157
|
+
private lawLedger(where: string, o: PlanOpts) {
|
|
158
|
+
const l = o.law!, pr = l.pr, rd = (x: number) => (Number.isFinite(x) ? Math.round(x * 1000) / 1000 : x > 0 ? "inf" : null);
|
|
159
|
+
this.ledger({
|
|
160
|
+
type: "law", where, g: Math.round(l.g), pWarm: rd(l.pWarm), survSrc: where === "run" ? this.survSrc : "in-run", cls: pr?.cls ?? null, wr: pr ? rd(pr.w / pr.r) : null, prSrc: pr?.src ?? "legacy",
|
|
161
|
+
steps: (o.trace ?? []).map((t) => ({ at: t.where, ok: t.ok, B: Math.round(t.B), A: Math.round(t.A), T: Math.round(t.T), ...(t.Tsuf !== undefined ? { Tsuf: Math.round(t.Tsuf) } : {}), phi: rd(t.phi), K: rd(t.K), eta: rd(t.eta) })),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private detectConflict(ctx: Any) {
|
|
166
|
+
let found: string | null = null;
|
|
167
|
+
try {
|
|
168
|
+
const items: Any[] = [...(this.pi.getAllTools?.() ?? []), ...(this.pi.getCommands?.() ?? [])];
|
|
169
|
+
for (const it of items) {
|
|
170
|
+
if (it.name === "zip_recall" || it.name === "zip") continue;
|
|
171
|
+
const hay = `${it.name} ${it.sourceInfo?.path ?? ""} ${it.sourceInfo?.source ?? ""}`;
|
|
172
|
+
const m = CONFLICT_RE.exec(hay);
|
|
173
|
+
if (m) { found = m[0]; break; }
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
this.conflict = found;
|
|
177
|
+
if (found && !this.conflictNoticed) {
|
|
178
|
+
this.conflictNoticed = true;
|
|
179
|
+
this.notify(ctx, `${PRODUCT} · "${found}" also manages context, so folding is paused (only the request guard stays on)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---- events ----------------------------------------------------------------------------------------------
|
|
184
|
+
sessionStart(ctx: Any) {
|
|
185
|
+
const branch = this.branch(ctx);
|
|
186
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
187
|
+
const en = branch[i];
|
|
188
|
+
if (en?.type === "custom" && en.customType === STATE_CUSTOM) {
|
|
189
|
+
this.off = !!en.data?.off;
|
|
190
|
+
this.quiet = !!en.data?.quiet;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const en of branch) if (en?.type === "custom" && en.customType === STEER_CUSTOM && Array.isArray(en.data?.ids)) for (const id of en.data.ids) if (typeof id === "string") this.steerIds.add(id);
|
|
195
|
+
for (const h of recalledHandlesFromBranch(branch)) this.recalled.add(h);
|
|
196
|
+
this.model = ctx?.model;
|
|
197
|
+
this.ttlNoticed = false;
|
|
198
|
+
this.detectConflict(ctx);
|
|
199
|
+
this.ledger({ type: "session_start", off: this.off, quiet: this.quiet, conflict: this.conflict });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
shutdown() {
|
|
203
|
+
this.cancelTimer();
|
|
204
|
+
this.bg?.controller.abort();
|
|
205
|
+
this.bg = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
beforeAgentStart(ctx: Any) {
|
|
209
|
+
this.cancelTimer(); // the user is back: nothing is started behind their back any more
|
|
210
|
+
this.model = ctx?.model;
|
|
211
|
+
this.cold = false;
|
|
212
|
+
this.pastTtl = false;
|
|
213
|
+
this.runChecked = true;
|
|
214
|
+
this.runEdits = false;
|
|
215
|
+
this.runPlan = null;
|
|
216
|
+
this.settleIds = [];
|
|
217
|
+
this.bgWaitMs = 0;
|
|
218
|
+
this.wireLedgered = false;
|
|
219
|
+
this.runUserSeen = 0;
|
|
220
|
+
if (!this.active()) return;
|
|
221
|
+
this.detectConflict(ctx);
|
|
222
|
+
if (!this.active()) return;
|
|
223
|
+
const branch = this.branch(ctx);
|
|
224
|
+
const key = modelKey(ctx.model);
|
|
225
|
+
this.loadEnt(key);
|
|
226
|
+
this.branchLastMs = lastMessageMs(branch);
|
|
227
|
+
if (!this.prevKey) ({ key: this.prevKey, total: this.prevTotal } = lastPrompt(branch)); // fresh process: the session's newest response
|
|
228
|
+
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));
|
|
229
|
+
this.cold = cold;
|
|
230
|
+
this.pastTtl = pastTtl;
|
|
231
|
+
this.pWarm = p;
|
|
232
|
+
this.survSrc = src;
|
|
233
|
+
this.runChecked = false; // the first request decides (cold: the cold plan; warm: only above the cap, if the law fires)
|
|
234
|
+
this.coldReason = reason;
|
|
235
|
+
for (const h of recalledHandlesFromBranch(branch)) this.recalled.add(h);
|
|
236
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
237
|
+
const en = branch[i];
|
|
238
|
+
if (en?.type !== "custom" || en.customType !== PLAN_CUSTOM) continue;
|
|
239
|
+
if (en.data?.policy === PRODUCT) {
|
|
240
|
+
if (cold && Array.isArray(en.data.targets)) this.settleIds = en.data.targets.map((t: Any) => (typeof t === "string" ? t : t?.entryId)).filter((x: Any) => typeof x === "string");
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
this.ledger({ type: "prompt", cold, reason, ttlMs: ttl.ms, ttlSource: ttl.source, settleTargets: this.settleIds.length, gapS: gapS === null ? null : Math.round(gapS), pWarm: Math.round(p * 1000) / 1000, survSrc: src, cls: this.ent?.cls ?? null });
|
|
245
|
+
if (ttl.note && !this.ttlNoticed) {
|
|
246
|
+
this.ttlNoticed = true; // once per session
|
|
247
|
+
if (!this.quiet) this.notify(ctx, ttl.note);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private cancelTimer() {
|
|
252
|
+
this.timerGen++;
|
|
253
|
+
if (this.timer) clearTimeout(this.timer);
|
|
254
|
+
this.timer = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A summary nobody adopted still cost money: abort it if it is still running, book it if it finished. */
|
|
258
|
+
private discardBg() {
|
|
259
|
+
const b = this.bg;
|
|
260
|
+
this.bg = null;
|
|
261
|
+
if (!b) return;
|
|
262
|
+
if (!b.done) b.controller.abort();
|
|
263
|
+
b.promise.then((c) => { if (c) this.stats.summaryUsd += c.costUsd; });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async context(e: Any, ctx: Any): Promise<Any> {
|
|
267
|
+
if (!this.active()) return undefined;
|
|
268
|
+
if (!this.runChecked) {
|
|
269
|
+
this.runChecked = true;
|
|
270
|
+
try {
|
|
271
|
+
await this.computeRunPlan(ctx);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
// no plan was sent, so none may be persisted: end the edit run here
|
|
274
|
+
this.runPlan = null;
|
|
275
|
+
this.runEdits = false;
|
|
276
|
+
this.ledger({ type: "error", where: "context/run_plan", error: errText(err) });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (this.runPlan && !this.runPlan.persisted) {
|
|
280
|
+
const out = applyPlanToMessages(e.messages, this.runPlan);
|
|
281
|
+
if (out) {
|
|
282
|
+
if (!this.runPlan.sent) this.nextEdit = this.runPlan.untouched ?? 0;
|
|
283
|
+
this.runPlan.sent = true;
|
|
284
|
+
this.runPlan.applied = out.applied;
|
|
285
|
+
return { messages: out.messages };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** A prepared summary for this cut, or null. A summary still being written is waited for (only the remainder); Esc stops the waiting, not the work. */
|
|
292
|
+
private async takeBg(key: string | null, signal?: AbortSignal): Promise<{ cut: Cut | null; adopted: boolean }> {
|
|
293
|
+
const b = this.bg;
|
|
294
|
+
if (!b || key === null || b.key !== key) {
|
|
295
|
+
this.discardBg();
|
|
296
|
+
return { cut: null, adopted: false };
|
|
297
|
+
}
|
|
298
|
+
const w0 = performance.now();
|
|
299
|
+
const cut = b.done ?? (await waitFor(b.promise, signal));
|
|
300
|
+
this.bgWaitMs = performance.now() - w0;
|
|
301
|
+
if (cut) {
|
|
302
|
+
this.bg = null;
|
|
303
|
+
return { cut, adopted: true };
|
|
304
|
+
}
|
|
305
|
+
if (signal?.aborted) return { cut: null, adopted: false }; // keep it running: a retry of the prompt can still use it
|
|
306
|
+
this.discardBg();
|
|
307
|
+
return { cut: null, adopted: false };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** ONE plan per edit run: computed at the first request (the new prompt is in the session), applied to every request until turn_end persists the same set. */
|
|
311
|
+
private async computeRunPlan(ctx: Any) {
|
|
312
|
+
const t0 = performance.now();
|
|
313
|
+
const entries = ((ctx.sessionManager.buildSessionProjection() as Any)?.entries ?? []) as Any[];
|
|
314
|
+
if (!this.cold && this.justEdited) { // no warm edit on the request right after an edited one (no back-to-back warm rewrites)
|
|
315
|
+
this.ledger({ type: "b2b_skip", where: "run" });
|
|
316
|
+
this.discardBg();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const { o, cal } = this.opts(ctx, false, this.cold ? "cold" : "warm", entries, this.pWarm);
|
|
320
|
+
o.pastTtl = this.pastTtl;
|
|
321
|
+
const p = planContext(entries, o);
|
|
322
|
+
this.lawLedger("run", o);
|
|
323
|
+
if (!p) {
|
|
324
|
+
this.discardBg(); // the law does not fire (warm below the cap or a rewrite that does not pay back; a likely-warm cold return): a prepared summary does not pay back
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.runEdits = true;
|
|
328
|
+
const trigger = this.cold ? "cold" : "valve";
|
|
329
|
+
let folds: FoldTarget[] = p?.folds ?? [];
|
|
330
|
+
let cut: Cut | null = null;
|
|
331
|
+
let adopted = false;
|
|
332
|
+
if (p && p.cutIdx !== null) {
|
|
333
|
+
const got = await this.takeBg(p.firstKeptEntryId, ctx.signal);
|
|
334
|
+
cut = got.cut;
|
|
335
|
+
adopted = got.adopted;
|
|
336
|
+
} else {
|
|
337
|
+
this.discardBg();
|
|
338
|
+
}
|
|
339
|
+
const sameSet = this.settleIds.length === folds.length && this.settleIds.every((id) => folds.some((u) => u.entryId === id));
|
|
340
|
+
const source: RunPlan["source"] = !this.cold ? "valve" : adopted || (this.settleIds.length > 0 && sameSet) ? "settle" : "runstart";
|
|
341
|
+
const foldMs = performance.now() - t0;
|
|
342
|
+
if (!cut && p && p.cutIdx !== null && !ctx.signal?.aborted) cut = await buildCut(p, ctx); // produced now, before the first request: the user waits
|
|
343
|
+
// a plan that cannot be persisted must never be sent: the request view would differ from what turn_end can write (I1)
|
|
344
|
+
if (p) {
|
|
345
|
+
const invalid = checkEdits(p.blocks, p.userTurns, folds, cut);
|
|
346
|
+
if (invalid) {
|
|
347
|
+
this.ledger({ type: "guard_drop", where: "run_plan", reason: invalid, folds: folds.length, summary: !!cut });
|
|
348
|
+
if (cut && checkEdits(p.blocks, p.userTurns, folds, null) === null) cut = null;
|
|
349
|
+
else { folds = []; cut = null; }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const ctxAfter = cut ? (p?.ctxAfterFolds ?? 0) - cal.k * (cut.prefixTokens - cut.summaryTokens) : (p?.ctxAfterFolds ?? 0);
|
|
353
|
+
this.runPlan = { source, folds, cut, ctxBefore: p?.ctxTokens ?? 0, ctxAfter, ms: foldMs, k: cal.k, persisted: false, cutVisibleIdx: p?.cutVisibleIdx ?? -1, cutKeptFirstMsg: p?.cutKeptFirstMsg, untouched: p ? p.k * untouchedEst(p.blocks, folds, !!cut, o.sys) : 0 };
|
|
354
|
+
this.ledger({ type: "cold_plan", trigger, source, folds: folds.length, summary: !!cut, ...this.calOf(cal), ctxBefore: Math.round(this.runPlan.ctxBefore), ctxAfter: Math.round(ctxAfter), ms: Math.round(foldMs), ...(cut ? { summaryMs: cut.ms, llmOk: cut.llmOk, waitedMs: Math.round(adopted ? this.bgWaitMs : cut.ms), ...this.usageOf(cut) } : {}) });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Ledger form of a calibration; ctxBefore/ctxAfter next to it are already scaled (k x estimate). */
|
|
358
|
+
private calOf(cal: Calibration) {
|
|
359
|
+
return { k: Math.round(cal.k * 1000) / 1000, calReal: cal.real, calEst: Math.round(cal.est), calSource: cal.source };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private usageOf(cut: Cut) {
|
|
363
|
+
const u = cut.usage;
|
|
364
|
+
return u ? { summaryUsage: { input: u.input, output: u.output, cacheRead: u.cacheRead, cacheWrite: u.cacheWrite } } : {};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Prepare the next cold return while the user is away (interactive only; print and json mode exit right after settle, so there is
|
|
369
|
+
* nothing to prepare and nothing is ever started in the background). The plan itself is local and free; only a summary needs
|
|
370
|
+
* a model call, and that is started by a timer 0.8 x TTL after the last request, if the user has not come back by then.
|
|
371
|
+
*/
|
|
372
|
+
async settle(e: Any, ctx: Any): Promise<Any> {
|
|
373
|
+
if (!this.active() || !ctx.hasUI) return undefined;
|
|
374
|
+
try {
|
|
375
|
+
const t0 = performance.now();
|
|
376
|
+
const { o, cal } = this.opts(ctx, true, "cold", e.context.contextEntries);
|
|
377
|
+
const p = planContext(e.context.contextEntries, o);
|
|
378
|
+
if (!p) return undefined;
|
|
379
|
+
this.cancelTimer();
|
|
380
|
+
let scheduledMs: number | null = null;
|
|
381
|
+
if (p.cutIdx !== null) {
|
|
382
|
+
if (this.bg && this.bg.key === p.firstKeptEntryId) {
|
|
383
|
+
// the summary for this very cut is already in flight or done: keep it
|
|
384
|
+
} else {
|
|
385
|
+
this.discardBg();
|
|
386
|
+
scheduledMs = this.scheduleSummary(p, ctx);
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
this.discardBg();
|
|
390
|
+
}
|
|
391
|
+
this.ledger({ type: "settle_plan", folds: p.folds.length, summary: p.cutIdx !== null, timerMs: scheduledMs, ...this.calOf(cal), ctxBefore: Math.round(p.ctxTokens), ms: Math.round(performance.now() - t0) });
|
|
392
|
+
if (!p.folds.length && p.cutIdx === null) return undefined;
|
|
393
|
+
// ids only: placeholders and summary text are recomputed (they are pure functions of the session), never stored a second time
|
|
394
|
+
const data = { policy: PRODUCT, ts: Date.now(), targets: p.folds.map((t) => t.entryId), cutKey: p.firstKeptEntryId };
|
|
395
|
+
return { entries: [...e.entries, { type: "custom", customType: PLAN_CUSTOM, data }] };
|
|
396
|
+
} catch (err) {
|
|
397
|
+
this.ledger({ type: "error", where: "settle", error: errText(err) });
|
|
398
|
+
return undefined; // never break the session
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Start the away-timer for a summary; returns the delay in ms. unref'd: it never keeps the process alive. */
|
|
403
|
+
private scheduleSummary(p: PlanResult, ctx: Any): number {
|
|
404
|
+
const ttl = ttlFor(ctx.model, this.branch(ctx));
|
|
405
|
+
const delay = Math.max(0, (this.lastReqMs || Date.now()) + AWAY_FRACTION * ttl - Date.now());
|
|
406
|
+
const gen = ++this.timerGen;
|
|
407
|
+
this.timer = setTimeout(() => void this.runAway(p, ctx, gen), delay);
|
|
408
|
+
this.timer.unref?.();
|
|
409
|
+
return Math.round(delay);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private async runAway(p: PlanResult, ctx: Any, gen: number) {
|
|
413
|
+
this.timer = null;
|
|
414
|
+
if (gen !== this.timerGen || !this.active()) return; // the user came back, or we were switched off
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
const bg: Bg = { key: p.firstKeptEntryId, done: null, promise: Promise.resolve(null), controller };
|
|
417
|
+
bg.promise = buildCut(p, ctx, controller.signal)
|
|
418
|
+
.then((c) => ((bg.done = c), this.ledger({ type: "away_summary", ms: c.ms, llmOk: c.llmOk, costUsd: c.costUsd, ...this.usageOf(c) }), c))
|
|
419
|
+
.catch((err) => {
|
|
420
|
+
this.ledger({ type: "error", where: "away_summary", error: err instanceof Error ? err.message : String(err) });
|
|
421
|
+
return null;
|
|
422
|
+
});
|
|
423
|
+
this.bg = bg;
|
|
424
|
+
this.ledger({ type: "away_summary_start" });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async turnEnd(e: Any, ctx: Any): Promise<Any> {
|
|
428
|
+
if (!this.active()) return undefined;
|
|
429
|
+
try {
|
|
430
|
+
const fresh = this.collectSteer(e); // marked before anything is validated: a steering message is part of its turn
|
|
431
|
+
const out = await this.onTurnEnd(e, ctx);
|
|
432
|
+
if (!fresh.length) return out;
|
|
433
|
+
this.ledger({ type: "steer", ids: fresh });
|
|
434
|
+
return { entries: [...(out?.entries ?? e.entries), { type: "custom", customType: STEER_CUSTOM, data: { ids: fresh } }] };
|
|
435
|
+
} catch (err) {
|
|
436
|
+
this.ledger({ type: "error", where: "turn_end", error: errText(err) });
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Steering and follow-up user messages (typed during a run) are not new user turns; Pi does not mark them, so we persist the ids. */
|
|
442
|
+
private collectSteer(e: Any): string[] {
|
|
443
|
+
if (!this.steerTs.size) return [];
|
|
444
|
+
const ids: string[] = [];
|
|
445
|
+
for (const pe of e.context?.contextEntries ?? []) {
|
|
446
|
+
const src = pe.sourceEntry;
|
|
447
|
+
const m = src?.type === "message" ? src.message : null;
|
|
448
|
+
if (m?.role === "user" && typeof m.timestamp === "number" && this.steerTs.has(m.timestamp) && src.id && !this.steerIds.has(src.id)) ids.push(src.id);
|
|
449
|
+
}
|
|
450
|
+
this.steerTs.clear();
|
|
451
|
+
for (const id of ids) this.steerIds.add(id);
|
|
452
|
+
return ids;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private async onTurnEnd(e: Any, ctx: Any) {
|
|
456
|
+
const msg: Any = e.message;
|
|
457
|
+
const failed = msg?.stopReason === "error" || msg?.stopReason === "aborted";
|
|
458
|
+
const t0 = performance.now();
|
|
459
|
+
const blocks = buildBlocks(e.context.contextEntries, this.steerIds);
|
|
460
|
+
if (!blocks.length) return undefined;
|
|
461
|
+
const userTurns = countUserTurns(blocks);
|
|
462
|
+
const u = msg?.usage;
|
|
463
|
+
const usageTotal = u ? (u.input ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0) : 0;
|
|
464
|
+
const o = { t0, blocks, userTurns, sys: this.sysTokens(ctx), usageTotal };
|
|
465
|
+
if (this.runEdits) {
|
|
466
|
+
this.runEdits = false;
|
|
467
|
+
if (!this.runPlan?.sent && this.runPlan && (this.runPlan.folds.length || this.runPlan.cut)) {
|
|
468
|
+
this.runPlan = null; // never sent (the request failed before the context hook, or nothing matched): persisting it would change a prefix nobody saw
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
if (!this.runPlan) return undefined;
|
|
472
|
+
// the run has carried its plan since the first request: persist EXACTLY it, even when this turn failed (the prefix was sent and cached)
|
|
473
|
+
return this.commit(e, ctx, this.runPlan, o);
|
|
474
|
+
}
|
|
475
|
+
if (failed) return undefined;
|
|
476
|
+
if (this.justEdited) { // the request that just ended carried a fresh edit: no warm edit on the very next one (no back-to-back warm rewrites)
|
|
477
|
+
this.ledger({ type: "b2b_skip", where: "turn_end" });
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
// warm cache (in-run, P = 1): nothing, unless the context is above the cold cap and the law fires (I6, F10): then the warm plan, from the next request on
|
|
481
|
+
const { o: popts } = this.opts(ctx, false, "warm", e.context.contextEntries); // the message that just ended carries the newest usage
|
|
482
|
+
const p = planContext(e.context.contextEntries, popts);
|
|
483
|
+
if (popts.trace?.length) this.lawLedger("turn_end", popts);
|
|
484
|
+
if (!p || (!p.folds.length && p.cutIdx === null)) return undefined;
|
|
485
|
+
let cut: Cut | null = null;
|
|
486
|
+
if (p.cutIdx !== null) {
|
|
487
|
+
const got = await this.takeBg(p.firstKeptEntryId, ctx.signal);
|
|
488
|
+
cut = got.cut ?? (await buildCut(p, ctx)); // the user is here and the cache is warm; the alternative is Pi's lossy compaction
|
|
489
|
+
} else this.discardBg();
|
|
490
|
+
const plan: RunPlan = { source: "valve", folds: p.folds, cut, ctxBefore: p.ctxTokens, ctxAfter: p.ctxAfterFolds, ms: performance.now() - t0, k: p.k, persisted: false, untouched: p.k * untouchedEst(p.blocks, p.folds, !!cut, popts.sys) };
|
|
491
|
+
return this.commit(e, ctx, plan, o);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private commit(e: Any, ctx: Any, plan: RunPlan, o: { t0: number; blocks: Block[]; userTurns: number; sys: number; usageTotal: number }) {
|
|
495
|
+
plan.persisted = true;
|
|
496
|
+
const k = plan.k; // one scale for the whole run: stats, notices and the ledger agree with what the plan compared against the limits
|
|
497
|
+
const ctxTokens = k * (o.sys + o.blocks.reduce((a, b) => a + b.tokens, 0));
|
|
498
|
+
const byId = new Map(o.blocks.map((b) => [b.entryId, b]));
|
|
499
|
+
const wasApplied = (t: FoldTarget) => !plan.applied || plan.applied.has(t.entryId); // persist what the requests actually carried
|
|
500
|
+
const live = plan.folds.filter((t) => byId.has(t.entryId) && wasApplied(t));
|
|
501
|
+
const stale = plan.folds.length - live.length; // e.g. Pi auto-compaction removed them from the projection
|
|
502
|
+
const cut = plan.cut && byId.has(plan.cut.firstKeptEntryId) ? plan.cut : null;
|
|
503
|
+
if (!live.length && !cut) {
|
|
504
|
+
this.ledger({ type: "cold_noop", turnIndex: e.turnIndex, ctx: Math.round(ctxTokens), ...(stale ? { staleTargets: stale } : {}) });
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
const bad = checkEdits(o.blocks, o.userTurns, live, cut);
|
|
508
|
+
if (bad) {
|
|
509
|
+
this.ledger({ type: "guard_drop", turnIndex: e.turnIndex, reason: bad, folds: live.length, source: plan.source });
|
|
510
|
+
plan.persisted = false; // keep the request-local view: never switch the fold set mid-run
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
if (!plan.sent) this.nextEdit = plan.untouched ?? 0; // persisted now, first sent with the next request
|
|
514
|
+
const ours: Any[] = live.map((t) => ({ type: "context_edit", targetId: t.entryId, replacement: { content: [{ type: "text", text: t.ph }] } }));
|
|
515
|
+
if (cut) ours.push({ type: "compaction", summary: cut.text, firstKeptEntryId: cut.firstKeptEntryId, details: { by: PRODUCT, trigger: cut.trigger }, usage: cut.usage });
|
|
516
|
+
const before = Math.round(k * live.reduce((a, t) => a + t.entryTokens, 0));
|
|
517
|
+
const after = Math.round(k * live.reduce((a, t) => a + t.phTokens, 0));
|
|
518
|
+
const cutSaved = cut ? Math.round(k * (cut.prefixTokens - cut.summaryTokens)) : 0;
|
|
519
|
+
const waitMs = cut ? (plan.source === "settle" ? this.bgWaitMs : cut.ms) : 0;
|
|
520
|
+
const s = this.stats;
|
|
521
|
+
s.folds += live.length;
|
|
522
|
+
s.foldedTokens += before - after;
|
|
523
|
+
s.activeSaved += before - after + cutSaved;
|
|
524
|
+
if (plan.source === "valve") {
|
|
525
|
+
s.pressureEdits++;
|
|
526
|
+
s.pressureRewriteTokens += ctxTokens;
|
|
527
|
+
} else {
|
|
528
|
+
s.writeSavedTokens += before - after + cutSaved;
|
|
529
|
+
}
|
|
530
|
+
if (cut) {
|
|
531
|
+
s.summaries++;
|
|
532
|
+
s.summarizedTokens += cutSaved;
|
|
533
|
+
s.summaryMs += waitMs;
|
|
534
|
+
s.summaryUsd += cut.costUsd;
|
|
535
|
+
}
|
|
536
|
+
if (live.length) this.ledger({
|
|
537
|
+
type: "fold", trigger: plan.source, turnIndex: e.turnIndex, count: live.length, entries: live.map((t) => t.entryId), tools: live.map((t) => t.tool),
|
|
538
|
+
recoverability: live.map((t) => t.recover ?? "?"), trigs: live.map((t) => t.trig), handles: live.map((t) => handleFor(t.entryId)),
|
|
539
|
+
entryTokensBefore: before, entryTokensAfter: after, ctxBefore: Math.round(ctxTokens), ctxAfter: Math.round(ctxTokens - (before - after) - cutSaved), k: Math.round(k * 1000) / 1000, usageTotal: o.usageTotal,
|
|
540
|
+
coldReason: this.coldReason, prepared: plan.source === "settle", ...(stale ? { staleTargets: stale } : {}),
|
|
541
|
+
});
|
|
542
|
+
if (cut) this.ledger({ type: "summary", trigger: cut.trigger, source: plan.source, count: cut.count, prefixTokens: cut.prefixTokens, summaryTokens: cut.summaryTokens, ms: cut.ms, waitedMs: Math.round(waitMs), llmOk: cut.llmOk, llmError: cut.llmError, costUsd: cut.costUsd, ...this.usageOf(cut) });
|
|
543
|
+
const valve = plan.source === "valve";
|
|
544
|
+
const notices: NoticeAction[] = [];
|
|
545
|
+
if (live.length) notices.push({ kind: "fold", count: live.length, tokensBefore: ctxTokens, tokensAfter: ctxTokens - (before - after), ms: plan.ms + (performance.now() - o.t0), pressure: valve });
|
|
546
|
+
if (cut) {
|
|
547
|
+
const prepared = plan.source === "settle" && waitMs < 500; // finished while the user was away: report the real production time, not the zero wait
|
|
548
|
+
notices.push({ kind: "summary", count: cut.count, tokensBefore: ctxTokens - (before - after), tokensAfter: ctxTokens - (before - after) - cutSaved, ms: prepared ? cut.ms : waitMs, prepared, pressure: valve });
|
|
549
|
+
}
|
|
550
|
+
if (notices.length && !this.quiet) this.notify(ctx, noticeText(notices));
|
|
551
|
+
return { entries: [...e.entries, ...ours] }; // append, never overwrite other extensions' drafts
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
providerRequest(e: Any): Any {
|
|
555
|
+
const now = Date.now(), key = modelKey(this.model), last = Math.max(this.lastReqMs, this.branchLastMs);
|
|
556
|
+
const edited = this.nextEdit !== null;
|
|
557
|
+
// the free cache signal: this response's cacheRead against what this request re-sent unchanged, after this gap (same model only)
|
|
558
|
+
this.pending = { gapS: last && (this.lastModelKey || this.prevKey) === key ? (now - last) / 1000 : -1, expect: edited ? this.nextEdit! : this.prevTotal, edited, key };
|
|
559
|
+
this.nextEdit = null;
|
|
560
|
+
this.lastReqMs = now;
|
|
561
|
+
this.lastModelKey = key;
|
|
562
|
+
this.stats.readSavedTokens += this.stats.activeSaved;
|
|
563
|
+
const p: Any = e.payload;
|
|
564
|
+
if (process.env.PI_ZIP_LEDGER && this.cold && !this.wireLedgered && p && Array.isArray(p.messages)) {
|
|
565
|
+
this.wireLedgered = true; // proof that the FIRST request after a cold return is already small
|
|
566
|
+
let tok = 0;
|
|
567
|
+
for (const m of p.messages) tok += tok4(JSON.stringify(m?.content ?? "")) + 8;
|
|
568
|
+
const sys = p.system;
|
|
569
|
+
tok += typeof sys === "string" ? tok4(sys) : Array.isArray(sys) ? tok4(sys.map((x: Any) => x?.text ?? "").join("\n")) : 0;
|
|
570
|
+
this.ledger({ type: "cold_wire", tokens: Math.round(tok), messages: p.messages.length, folds: this.runPlan && !this.runPlan.persisted ? this.runPlan.folds.length : 0, summary: !!this.runPlan?.cut, source: this.runPlan?.source ?? null });
|
|
571
|
+
}
|
|
572
|
+
if (this.off) return undefined; // strict no-op
|
|
573
|
+
const r = repairPayload(p);
|
|
574
|
+
if (!r) return undefined;
|
|
575
|
+
this.ledger({ type: "guard_repair", repaired: r.repaired });
|
|
576
|
+
return r.payload;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
messageEnd(m: Any) {
|
|
580
|
+
if (m?.role === "assistant" && m.stopReason !== "error" && m.stopReason !== "aborted") this.lastReqMs = Date.now(); // the response is complete: the entry was refreshed when it finished
|
|
581
|
+
if (m?.role === "user" && this.active()) {
|
|
582
|
+
// the first user message of a run is the prompt; later ones (steering, follow-up) are part of that run's turn. Pi does not mark them, we do.
|
|
583
|
+
if (this.runUserSeen++ > 0 && typeof m.timestamp === "number") this.steerTs.add(m.timestamp);
|
|
584
|
+
}
|
|
585
|
+
const u = m?.usage;
|
|
586
|
+
if (m?.role === "assistant" && u) this.ledger({ type: "usage", stop: m.stopReason, input: u.input, cacheRead: u.cacheRead, cacheWrite: u.cacheWrite, output: u.output });
|
|
587
|
+
if (m?.role === "assistant") this.learn(m);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Regime class, cache survival (persisted) and growth g (this session) from one response's usage. */
|
|
591
|
+
private learn(m: Any) {
|
|
592
|
+
const pend = this.pending, u = m.usage;
|
|
593
|
+
this.pending = null;
|
|
594
|
+
this.justEdited = !!pend?.edited;
|
|
595
|
+
if (!pend || !u || m.stopReason === "error" || m.stopReason === "aborted") return;
|
|
596
|
+
const cr = u.cacheRead ?? 0, cw = u.cacheWrite ?? 0, total = (u.input ?? 0) + cr + cw;
|
|
597
|
+
if (!(total > 0)) return;
|
|
598
|
+
const known = this.entKey === pend.key ? this.ent?.cls : undefined;
|
|
599
|
+
const alive = pend.gapS >= GAP_EDGES[0] ? sample(pend.expect, cr, total, pend.edited, cw > 0 ? "explicit" : known) : null;
|
|
600
|
+
if (pend.gapS >= GAP_EDGES[0]) this.ledger({ type: "cache_sample", gapS: Math.round(pend.gapS), expect: Math.round(pend.expect), cacheRead: cr, total, edited: pend.edited, alive });
|
|
601
|
+
if (alive !== null || !known || (cw > 0 && known !== "explicit")) { // the file is touched only when there is something to learn
|
|
602
|
+
this.ent = record(pend.key, { explicit: cw > 0, total, gapS: pend.gapS, alive });
|
|
603
|
+
this.entKey = pend.key;
|
|
604
|
+
}
|
|
605
|
+
if (!pend.edited && this.prevKey === pend.key && this.prevTotal > 0 && total >= this.prevTotal) {
|
|
606
|
+
const d = total - this.prevTotal;
|
|
607
|
+
this.deltas = [...this.deltas.slice(-19), d];
|
|
608
|
+
const sorted = [...this.deltas].sort((a, b) => a - b);
|
|
609
|
+
this.g = 0.9 * this.g + 0.1 * Math.min(d, sorted[Math.ceil(0.9 * sorted.length) - 1]);
|
|
610
|
+
}
|
|
611
|
+
this.prevTotal = total;
|
|
612
|
+
this.prevKey = pend.key;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
onRecall = (handles: string[], chars: number, entryIds: (string | null)[]) => {
|
|
616
|
+
for (const h of handles) this.recalled.add(h);
|
|
617
|
+
this.stats.recalls += handles.length;
|
|
618
|
+
this.stats.recallChars += chars;
|
|
619
|
+
this.ledger({ type: "recall", handles, chars, entryIds });
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// ---- /zip --------------------------------------------------------------------------------------------------
|
|
623
|
+
private persistState() {
|
|
624
|
+
try {
|
|
625
|
+
this.pi.appendEntry(STATE_CUSTOM, { off: this.off, quiet: this.quiet });
|
|
626
|
+
} catch {}
|
|
627
|
+
}
|
|
628
|
+
status(): string {
|
|
629
|
+
const state = this.off ? "off" : this.conflict ? `paused ("${this.conflict}" also manages context; only the request guard is on)` : "on";
|
|
630
|
+
const key = modelKey(this.model), ttl = ttlFor(this.model), ent = loadStats().models[key];
|
|
631
|
+
const pr = lawPrices(ent?.cls, ttl >= 3_600_000);
|
|
632
|
+
const cache = !key ? "" : `\ncache ${key}: ${pr ? `${pr.cls} (from usage)` : "class unknown until the first response"}` +
|
|
633
|
+
`, ${pr ? `read ${+pr.r.toFixed(4)} / write ${+pr.w.toFixed(4)} / output ${+pr.out.toFixed(4)} x input (class ratios)` : "no prices (legacy rule)"}` +
|
|
634
|
+
`, ${describe(ent, ttl / 1000)}, growth ${(this.g / 1000).toFixed(1)}K/request`;
|
|
635
|
+
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}`;
|
|
636
|
+
}
|
|
637
|
+
statsLine = () => statsText(this.stats, this.model);
|
|
638
|
+
setOff(off: boolean): string {
|
|
639
|
+
this.off = off;
|
|
640
|
+
if (off) { this.runPlan = null; this.runEdits = false; this.cancelTimer(); this.discardBg(); }
|
|
641
|
+
this.persistState();
|
|
642
|
+
return off ? `${PRODUCT}: off. Nothing is folded or summarised and requests are left untouched; earlier folds stay recallable (/zip on to resume)` : `${PRODUCT}: on`;
|
|
643
|
+
}
|
|
644
|
+
toggleQuiet(): string {
|
|
645
|
+
this.quiet = !this.quiet;
|
|
646
|
+
this.persistState();
|
|
647
|
+
return `${PRODUCT}: per-turn notices ${this.quiet ? "off" : "on"} (folding continues)`;
|
|
648
|
+
}
|
|
649
|
+
}
|