pi-harness-runtime 0.9.7 → 0.9.10
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/CHANGELOG.md +28 -12
- package/README.md +42 -5
- package/footer-status.ts +108 -11
- package/harness/e2e/minimax-quota-scraper.ts +46 -4
- package/index.ts +224 -16
- package/mirror.ts +194 -18
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/README.md +141 -0
- package/packages/cookie-sanitizer/dist/atomic-write.d.ts +38 -0
- package/packages/cookie-sanitizer/dist/atomic-write.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/atomic-write.js +67 -0
- package/packages/cookie-sanitizer/dist/atomic-write.js.map +1 -0
- package/packages/cookie-sanitizer/dist/detect-format.d.ts +36 -0
- package/packages/cookie-sanitizer/dist/detect-format.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/detect-format.js +88 -0
- package/packages/cookie-sanitizer/dist/detect-format.js.map +1 -0
- package/packages/cookie-sanitizer/dist/index.d.ts +41 -0
- package/packages/cookie-sanitizer/dist/index.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/index.js +39 -0
- package/packages/cookie-sanitizer/dist/index.js.map +1 -0
- package/packages/cookie-sanitizer/dist/infer-provider.d.ts +29 -0
- package/packages/cookie-sanitizer/dist/infer-provider.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/infer-provider.js +96 -0
- package/packages/cookie-sanitizer/dist/infer-provider.js.map +1 -0
- package/packages/cookie-sanitizer/dist/normalize.d.ts +26 -0
- package/packages/cookie-sanitizer/dist/normalize.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/normalize.js +75 -0
- package/packages/cookie-sanitizer/dist/normalize.js.map +1 -0
- package/packages/cookie-sanitizer/dist/parse-json.d.ts +45 -0
- package/packages/cookie-sanitizer/dist/parse-json.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/parse-json.js +108 -0
- package/packages/cookie-sanitizer/dist/parse-json.js.map +1 -0
- package/packages/cookie-sanitizer/dist/parse-netscape.d.ts +43 -0
- package/packages/cookie-sanitizer/dist/parse-netscape.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/parse-netscape.js +99 -0
- package/packages/cookie-sanitizer/dist/parse-netscape.js.map +1 -0
- package/packages/cookie-sanitizer/dist/sync.d.ts +62 -0
- package/packages/cookie-sanitizer/dist/sync.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/sync.js +266 -0
- package/packages/cookie-sanitizer/dist/sync.js.map +1 -0
- package/packages/cookie-sanitizer/dist/types.d.ts +100 -0
- package/packages/cookie-sanitizer/dist/types.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/types.js +13 -0
- package/packages/cookie-sanitizer/dist/types.js.map +1 -0
- package/packages/cookie-sanitizer/dist/watcher.d.ts +51 -0
- package/packages/cookie-sanitizer/dist/watcher.d.ts.map +1 -0
- package/packages/cookie-sanitizer/dist/watcher.js +148 -0
- package/packages/cookie-sanitizer/dist/watcher.js.map +1 -0
- package/packages/cookie-sanitizer/package.json +27 -0
- package/packages/cookie-sanitizer/src/atomic-write.ts +79 -0
- package/packages/cookie-sanitizer/src/detect-format.ts +102 -0
- package/packages/cookie-sanitizer/src/index.ts +89 -0
- package/packages/cookie-sanitizer/src/infer-provider.ts +103 -0
- package/packages/cookie-sanitizer/src/normalize.ts +92 -0
- package/packages/cookie-sanitizer/src/parse-json.ts +122 -0
- package/packages/cookie-sanitizer/src/parse-netscape.ts +118 -0
- package/packages/cookie-sanitizer/src/sync.ts +304 -0
- package/packages/cookie-sanitizer/src/types.ts +101 -0
- package/packages/cookie-sanitizer/src/watcher.ts +180 -0
- package/packages/cookie-sanitizer/test/infer.test.ts +110 -0
- package/packages/cookie-sanitizer/test/normalize.test.ts +113 -0
- package/packages/cookie-sanitizer/test/parse.test.ts +204 -0
- package/packages/cookie-sanitizer/test/sync.test.ts +231 -0
- package/packages/cookie-sanitizer/test/watcher.test.ts +27 -0
- package/packages/cookie-sanitizer/tsconfig.json +8 -0
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/dist/index.d.ts +1 -0
- package/packages/providers/dist/index.d.ts.map +1 -1
- package/packages/providers/dist/index.js +1 -0
- package/packages/providers/dist/index.js.map +1 -1
- package/packages/providers/dist/provider-id.d.ts +39 -0
- package/packages/providers/dist/provider-id.d.ts.map +1 -0
- package/packages/providers/dist/provider-id.js +96 -0
- package/packages/providers/dist/provider-id.js.map +1 -0
- package/packages/providers/package.json +1 -1
- package/packages/providers/src/index.ts +1 -0
- package/packages/providers/src/provider-id.ts +110 -0
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/worktree/package.json +1 -1
package/index.ts
CHANGED
|
@@ -23,6 +23,20 @@ import { UsageTracker } from "./tracker.ts";
|
|
|
23
23
|
import { MirrorStore, type MirrorRecord } from "./mirror.ts";
|
|
24
24
|
import { MiniMaxQuotaScraper } from "./harness/e2e/minimax-quota-scraper.js";
|
|
25
25
|
import { parseMiniMaxQuotaText } from "./harness/e2e/minimax-quota-parser.js";
|
|
26
|
+
import {
|
|
27
|
+
CookieWatcher,
|
|
28
|
+
DEFAULT_DROP_DIR as COOKIE_DROP_DIR,
|
|
29
|
+
hasAnyCookieSource as sanitizerHasAnyCookieSource,
|
|
30
|
+
} from "./packages/cookie-sanitizer/src/index.ts";
|
|
31
|
+
import {
|
|
32
|
+
providerFromModelId,
|
|
33
|
+
type ProviderId,
|
|
34
|
+
} from "./packages/providers/src/provider-id.ts";
|
|
35
|
+
import {
|
|
36
|
+
TUIUsageMonitor,
|
|
37
|
+
type TUIUsageSignal,
|
|
38
|
+
} from "./packages/quota-manager/src/tui-usage-monitor.ts";
|
|
39
|
+
import { QuotaManager } from "./packages/quota-manager/src/quota-manager.ts";
|
|
26
40
|
import { buildFooterStatusValue } from "./footer-status.ts";
|
|
27
41
|
import {
|
|
28
42
|
MAX_PROACTIVE_COMPACT_FAILURES,
|
|
@@ -66,6 +80,39 @@ interface HarnessSession {
|
|
|
66
80
|
|
|
67
81
|
let currentSession: HarnessSession | null = null;
|
|
68
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Safely extract a text view from an LLM message. Used to feed the TUI
|
|
85
|
+
* quota-signal extractor. Handles string content, array-of-parts content
|
|
86
|
+
* (with `text` fields), and falls back to a stringified JSON for unknown
|
|
87
|
+
* shapes. Never throws.
|
|
88
|
+
*/
|
|
89
|
+
function readMessageText(message: unknown): string {
|
|
90
|
+
try {
|
|
91
|
+
if (!message || typeof message !== "object") return "";
|
|
92
|
+
const m = message as { role?: unknown; content?: unknown };
|
|
93
|
+
if (typeof m.content === "string") return m.content;
|
|
94
|
+
if (Array.isArray(m.content)) {
|
|
95
|
+
const parts: string[] = [];
|
|
96
|
+
for (const p of m.content) {
|
|
97
|
+
if (!p) continue;
|
|
98
|
+
if (typeof p === "string") {
|
|
99
|
+
parts.push(p);
|
|
100
|
+
} else if (typeof p === "object") {
|
|
101
|
+
const obj = p as { text?: unknown; content?: unknown };
|
|
102
|
+
if (typeof obj.text === "string") parts.push(obj.text);
|
|
103
|
+
else if (typeof obj.content === "string") parts.push(obj.content);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return parts.join("\n");
|
|
107
|
+
}
|
|
108
|
+
// Fallback: best-effort stringification. Never include cookie-shaped
|
|
109
|
+
// data; we only stringify LLM message shapes which are JSON-safe.
|
|
110
|
+
return JSON.stringify(message).slice(0, 8000);
|
|
111
|
+
} catch {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
69
116
|
function ensureHarnessDir() {
|
|
70
117
|
if (!existsSync(HARNESS_ROOT_DIR)) {
|
|
71
118
|
mkdirSync(HARNESS_ROOT_DIR, { recursive: true });
|
|
@@ -159,6 +206,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
159
206
|
cache_write: m.usage.cacheWrite ?? 0,
|
|
160
207
|
cost: m.usage.cost?.total ?? 0,
|
|
161
208
|
});
|
|
209
|
+
|
|
210
|
+
// Feed TUI quota-signal extractor with the assistant message text.
|
|
211
|
+
// Best-effort — never throws. Coalesces provider quota notifications
|
|
212
|
+
// (e.g. "OpenAI: context length exceeded, reset in 3 hr 27 min") into
|
|
213
|
+
// the per-provider mirror via `tuiMonitor`.
|
|
214
|
+
try {
|
|
215
|
+
const text = readMessageText(event.message);
|
|
216
|
+
if (text) {
|
|
217
|
+
tuiMonitor.processMessage(text);
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// best-effort
|
|
221
|
+
}
|
|
162
222
|
});
|
|
163
223
|
|
|
164
224
|
// ─── Smart quota fetch for MiniMax status ────────────────────────
|
|
@@ -168,7 +228,84 @@ export default function (pi: ExtensionAPI) {
|
|
|
168
228
|
const quotaScraper = process.env.QUOTA_COOKIE_FILE
|
|
169
229
|
? new MiniMaxQuotaScraper({ cookieFile: process.env.QUOTA_COOKIE_FILE })
|
|
170
230
|
: new MiniMaxQuotaScraper();
|
|
171
|
-
|
|
231
|
+
|
|
232
|
+
// ─── Cookie sanitizer integration ────────────────────────────────────
|
|
233
|
+
// The drop folder is the user-facing, forgiving input. The canonical
|
|
234
|
+
// cache (`~/.config/minimax-cookies.txt`) is the runtime-owned,
|
|
235
|
+
// normalized output that `MiniMaxQuotaScraper` reads. Either being
|
|
236
|
+
// present enables scraping.
|
|
237
|
+
const cookieDropDir = COOKIE_DROP_DIR;
|
|
238
|
+
const cookieCachePath = join(homedir(), ".config", "minimax-cookies.txt");
|
|
239
|
+
|
|
240
|
+
const hasCookieSource = (): boolean => {
|
|
241
|
+
try {
|
|
242
|
+
if (existsSync(cookieCachePath)) return true;
|
|
243
|
+
} catch {
|
|
244
|
+
// ignore
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
return sanitizerHasAnyCookieSource(cookieDropDir);
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const cookieQuotaAutoFetchAvailable = hasCookieSource();
|
|
254
|
+
|
|
255
|
+
// ─── TUI quota signal plumbing (OpenAI / GLM / Anthropic / OpenRouter) ──
|
|
256
|
+
// The TUIUsageMonitor parses provider quota-exhaustion messages from pi's
|
|
257
|
+
// TUI / message stream and emits signals we write to per-provider mirror
|
|
258
|
+
// entries. For providers that don't expose a continuous usage API this is
|
|
259
|
+
// the only path to surface data in the footer.
|
|
260
|
+
const quotaManager = new QuotaManager();
|
|
261
|
+
const tuiMonitor = new TUIUsageMonitor({ quotaManager });
|
|
262
|
+
|
|
263
|
+
tuiMonitor.on("signal", (signal: TUIUsageSignal) => {
|
|
264
|
+
try {
|
|
265
|
+
writeMirrorRecord(signal.provider as ProviderId, {
|
|
266
|
+
synced_at: signal.timestamp,
|
|
267
|
+
source: "tui-signal",
|
|
268
|
+
exhausted: signal.exhausted,
|
|
269
|
+
limitType: signal.limitType,
|
|
270
|
+
remainingPct: signal.remainingPct,
|
|
271
|
+
resets_at: signal.resetsAt,
|
|
272
|
+
});
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.error(
|
|
275
|
+
"[pi-harness] tui-signal write failed:",
|
|
276
|
+
e instanceof Error ? e.message : String(e),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Live watcher — sanitises on every change in the drop folder.
|
|
282
|
+
const cookieWatcher = new CookieWatcher({
|
|
283
|
+
dropDir: cookieDropDir,
|
|
284
|
+
syncOptions: { cachePath: cookieCachePath },
|
|
285
|
+
onEvent: (event) => {
|
|
286
|
+
if (event.kind === "sync-error" || event.kind === "watcher-error") {
|
|
287
|
+
console.error(
|
|
288
|
+
"[pi-harness] cookie-sanitizer:",
|
|
289
|
+
"message" in event ? event.message : "",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
// A successful sync means the canonical cache is fresh; the
|
|
293
|
+
// next autoFetchQuota() should pick it up immediately. Reset
|
|
294
|
+
// the rate-limit so we don't wait 15 min for the first scrape.
|
|
295
|
+
if (event.kind === "sync-ok") {
|
|
296
|
+
lastQuotaAutoFetchAt = 0;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
try {
|
|
301
|
+
cookieWatcher.start();
|
|
302
|
+
} catch (e) {
|
|
303
|
+
console.error(
|
|
304
|
+
"[pi-harness] cookie-sanitizer watcher failed to start:",
|
|
305
|
+
e instanceof Error ? e.message : String(e),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
172
309
|
let footerStatusCtx: {
|
|
173
310
|
ui: { setStatus: (key: string, value: string) => void };
|
|
174
311
|
} | null = null;
|
|
@@ -182,10 +319,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
182
319
|
let pendingOutputLimitResumeAfterCompact = false;
|
|
183
320
|
let pendingOutputLimitResumeAfterSettled = false;
|
|
184
321
|
|
|
185
|
-
function writeMirrorRecord(
|
|
186
|
-
|
|
322
|
+
function writeMirrorRecord(
|
|
323
|
+
provider: ProviderId,
|
|
324
|
+
record: Omit<import("./mirror.js").ProviderMirrorRecord, "provider">,
|
|
325
|
+
): void {
|
|
326
|
+
mirrorStore.writeProvider(provider, { ...record, provider });
|
|
187
327
|
if (footerStatusCtx) {
|
|
188
|
-
refreshFooterStatus(
|
|
328
|
+
refreshFooterStatus(
|
|
329
|
+
footerStatusCtx,
|
|
330
|
+
tracker,
|
|
331
|
+
mirrorStore,
|
|
332
|
+
hasCookieSource,
|
|
333
|
+
() => lastActiveProvider,
|
|
334
|
+
);
|
|
189
335
|
}
|
|
190
336
|
}
|
|
191
337
|
|
|
@@ -201,7 +347,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
201
347
|
}
|
|
202
348
|
|
|
203
349
|
function isMiniMaxModel(modelId: string | null | undefined): boolean {
|
|
204
|
-
return
|
|
350
|
+
return providerFromModelId(modelId) === "minimax";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Active provider for the current/last-seen model. Updated by event handlers. */
|
|
354
|
+
let lastActiveProvider: ProviderId | null = null;
|
|
355
|
+
|
|
356
|
+
/** Set the active provider; triggers a footer refresh. */
|
|
357
|
+
function noteActiveProvider(modelId: string | null | undefined): void {
|
|
358
|
+
const p = providerFromModelId(modelId);
|
|
359
|
+
if (p !== lastActiveProvider) {
|
|
360
|
+
lastActiveProvider = p;
|
|
361
|
+
if (footerStatusCtx) {
|
|
362
|
+
refreshFooterStatus(
|
|
363
|
+
footerStatusCtx,
|
|
364
|
+
tracker,
|
|
365
|
+
mirrorStore,
|
|
366
|
+
hasCookieSource,
|
|
367
|
+
() => lastActiveProvider,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
205
371
|
}
|
|
206
372
|
|
|
207
373
|
function getMiniMaxUsageSince(sinceMs: number): {
|
|
@@ -280,7 +446,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
280
446
|
const profileRecord =
|
|
281
447
|
await autoFetchQuotaFromBrowserProfile(suppressErrors);
|
|
282
448
|
if (profileRecord) {
|
|
283
|
-
writeMirrorRecord(
|
|
449
|
+
writeMirrorRecord("minimax", {
|
|
450
|
+
synced_at: profileRecord.synced_at,
|
|
451
|
+
source: "scrape",
|
|
452
|
+
model: profileRecord.model,
|
|
453
|
+
h5_used_pct: profileRecord.h5_used_pct,
|
|
454
|
+
h5_resets_at: profileRecord.h5_resets_at,
|
|
455
|
+
weekly_used_pct: profileRecord.weekly_used_pct,
|
|
456
|
+
weekly_resets_at: profileRecord.weekly_resets_at,
|
|
457
|
+
});
|
|
284
458
|
return true;
|
|
285
459
|
}
|
|
286
460
|
|
|
@@ -290,9 +464,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
290
464
|
|
|
291
465
|
try {
|
|
292
466
|
const data = await quotaScraper.scrape();
|
|
293
|
-
writeMirrorRecord({
|
|
467
|
+
writeMirrorRecord("minimax", {
|
|
294
468
|
synced_at: data.scrapedAt,
|
|
295
|
-
|
|
469
|
+
source: "scrape",
|
|
296
470
|
h5_used_pct: data.h5UsedPct,
|
|
297
471
|
h5_resets_at: data.h5ResetsAt,
|
|
298
472
|
weekly_used_pct: data.weeklyUsedPct,
|
|
@@ -373,8 +547,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
373
547
|
(await hasBrowserProfileAutoFetchSource());
|
|
374
548
|
if (!autoFetchAvailable) {
|
|
375
549
|
ctx.ui.notify(
|
|
376
|
-
"
|
|
377
|
-
"
|
|
550
|
+
"MiniMax cookies not found. Drop any cookie file (Netscape or EditThisCookie JSON) into ~/.pi-harness-runtime/cookies/ — the runtime normalizes it for you. Or run `bun packages/auth/src/run-minimax-auth.ts auth`.",
|
|
551
|
+
"warning",
|
|
378
552
|
);
|
|
379
553
|
return;
|
|
380
554
|
}
|
|
@@ -452,7 +626,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
452
626
|
"info",
|
|
453
627
|
);
|
|
454
628
|
footerStatusCtx = ctx;
|
|
455
|
-
refreshFooterStatus(
|
|
629
|
+
refreshFooterStatus(
|
|
630
|
+
ctx,
|
|
631
|
+
tracker,
|
|
632
|
+
mirrorStore,
|
|
633
|
+
hasCookieSource,
|
|
634
|
+
() => lastActiveProvider,
|
|
635
|
+
);
|
|
456
636
|
} catch (e) {
|
|
457
637
|
ctx.ui.notify(`Failed to clear mirror: ${e}`, "error");
|
|
458
638
|
}
|
|
@@ -707,12 +887,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
707
887
|
// ─── Footer status (persistent badge) ────────────────────────────────
|
|
708
888
|
pi.on("session_start", (_event, ctx) => {
|
|
709
889
|
footerStatusCtx = ctx;
|
|
710
|
-
refreshFooterStatus(
|
|
890
|
+
refreshFooterStatus(
|
|
891
|
+
ctx,
|
|
892
|
+
tracker,
|
|
893
|
+
mirrorStore,
|
|
894
|
+
hasCookieSource,
|
|
895
|
+
() => lastActiveProvider,
|
|
896
|
+
);
|
|
711
897
|
});
|
|
712
898
|
|
|
713
899
|
pi.on("turn_end", (_event, ctx) => {
|
|
900
|
+
noteActiveProvider(ctx.model?.id ?? null);
|
|
714
901
|
footerStatusCtx = ctx;
|
|
715
|
-
refreshFooterStatus(
|
|
902
|
+
refreshFooterStatus(
|
|
903
|
+
ctx,
|
|
904
|
+
tracker,
|
|
905
|
+
mirrorStore,
|
|
906
|
+
hasCookieSource,
|
|
907
|
+
() => lastActiveProvider,
|
|
908
|
+
);
|
|
716
909
|
void maybeAutoFetchQuota(ctx.model?.id ?? null);
|
|
717
910
|
maybeTriggerProactiveCompact(ctx);
|
|
718
911
|
});
|
|
@@ -737,7 +930,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
737
930
|
lastProactiveCompactAt = Date.now();
|
|
738
931
|
consecutiveCompactFailures = 0;
|
|
739
932
|
proactiveCompactCircuitReported = false;
|
|
740
|
-
refreshFooterStatus(
|
|
933
|
+
refreshFooterStatus(
|
|
934
|
+
ctx,
|
|
935
|
+
tracker,
|
|
936
|
+
mirrorStore,
|
|
937
|
+
hasCookieSource,
|
|
938
|
+
() => lastActiveProvider,
|
|
939
|
+
);
|
|
741
940
|
|
|
742
941
|
const forceOutputLimitResume = pendingOutputLimitResumeAfterCompact;
|
|
743
942
|
pendingOutputLimitResumeAfterCompact = false;
|
|
@@ -832,13 +1031,22 @@ function refreshFooterStatus(
|
|
|
832
1031
|
ctx: { ui: { setStatus: (key: string, value: string) => void } },
|
|
833
1032
|
tracker: UsageTracker,
|
|
834
1033
|
mirrorStore: MirrorStore,
|
|
1034
|
+
hasCookieSource: () => boolean,
|
|
1035
|
+
getActiveProvider: () => ProviderId | null = () => null,
|
|
835
1036
|
) {
|
|
836
1037
|
const nowMs = Date.now();
|
|
837
1038
|
const local = aggregateWindows(tracker.all());
|
|
838
|
-
const
|
|
1039
|
+
const provider = getActiveProvider();
|
|
1040
|
+
const mirror = provider ? mirrorStore.readProvider(provider) : null;
|
|
839
1041
|
const freshness = mirrorStore.freshness(mirror, nowMs);
|
|
840
1042
|
ctx.ui.setStatus(
|
|
841
1043
|
"harness-runtime",
|
|
842
|
-
buildFooterStatusValue(
|
|
1044
|
+
buildFooterStatusValue(
|
|
1045
|
+
local,
|
|
1046
|
+
mirror,
|
|
1047
|
+
freshness,
|
|
1048
|
+
hasCookieSource(),
|
|
1049
|
+
provider,
|
|
1050
|
+
),
|
|
843
1051
|
);
|
|
844
1052
|
}
|
package/mirror.ts
CHANGED
|
@@ -1,29 +1,132 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MirrorStore — cached provider quota snapshot.
|
|
2
|
+
* MirrorStore — cached per-provider quota snapshot.
|
|
3
3
|
*
|
|
4
|
-
* Auto refresh
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Auto refresh writes per-provider data to ~/.pi/usage-status/mirror.json so
|
|
5
|
+
* the footer status and `/usage` can render the latest provider-side
|
|
6
|
+
* usage. Local tracking counts OUR usage; the mirror counts what's
|
|
7
|
+
* currently knowable from the provider (continuous for MiniMax via
|
|
8
|
+
* scrape; one-shot TUI signal for OpenAI / GLM / etc.).
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Shape (per-provider map):
|
|
11
|
+
*
|
|
12
|
+
* ```jsonc
|
|
13
|
+
* {
|
|
14
|
+
* "minimax": {
|
|
15
|
+
* "synced_at": "2026-07-23T06:20:04Z",
|
|
16
|
+
* "source": "scrape" | "tui-signal",
|
|
17
|
+
* "h5_used_pct": 15,
|
|
18
|
+
* "h5_resets_at": "3 hr 39 min",
|
|
19
|
+
* "weekly_used_pct": 21,
|
|
20
|
+
* "weekly_resets_at": "3 days 17 hr 39 min"
|
|
21
|
+
* },
|
|
22
|
+
* "openai": { ... }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Back-compat: legacy single-row files are read and upgraded in place
|
|
27
|
+
* on the first `readAll()` call after upgrade. Old rows that match a
|
|
28
|
+
* known provider (have a `provider` field) are placed under that key.
|
|
29
|
+
*
|
|
30
|
+
* Models live here: `~/.pi/usage-status/mirror.json`.
|
|
10
31
|
*/
|
|
11
32
|
|
|
12
33
|
import { getMirrorPath, readJson, writeJson } from "./cli.ts";
|
|
13
34
|
|
|
35
|
+
/** Where a piece of data came from. */
|
|
36
|
+
export type MirrorSource = "scrape" | "tui-signal" | "manual";
|
|
37
|
+
|
|
38
|
+
/** Per-provider mirror record. */
|
|
39
|
+
export interface ProviderMirrorRecord {
|
|
40
|
+
/** ISO-8601 UTC timestamp when this row was last updated. */
|
|
41
|
+
synced_at: string;
|
|
42
|
+
/** Provider id (e.g. "minimax", "openai", "glm"). Must match the key. */
|
|
43
|
+
provider: string;
|
|
44
|
+
/** Source of the data — used for diagnostics. */
|
|
45
|
+
source: MirrorSource;
|
|
46
|
+
/** Optional model id the data was captured for. */
|
|
47
|
+
model?: string;
|
|
48
|
+
/** 5h window used percent (0-100). Provider-reported when available. */
|
|
49
|
+
h5_used_pct?: number;
|
|
50
|
+
/** When the 5h window resets — human-readable or ISO. */
|
|
51
|
+
h5_resets_at?: string;
|
|
52
|
+
/** Weekly used percent. */
|
|
53
|
+
weekly_used_pct?: number;
|
|
54
|
+
/** When the weekly window resets. */
|
|
55
|
+
weekly_resets_at?: string;
|
|
56
|
+
/** Set when a limit was hit and we have the exhaustion signal. */
|
|
57
|
+
exhausted?: boolean;
|
|
58
|
+
/** Limit type from TUI signal: tokens, context_window, rate_limit, unknown. */
|
|
59
|
+
limitType?: "tokens" | "context_window" | "rate_limit" | "unknown";
|
|
60
|
+
/** Remaining pct (0-100) at exhaustion — 0 means exactly exhausted. */
|
|
61
|
+
remainingPct?: number;
|
|
62
|
+
/** ISO-8601 reset timestamp from TUI signal, if absolute. */
|
|
63
|
+
resets_at?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Map of provider id → its mirror record. */
|
|
67
|
+
export type PerProviderMirror = Record<string, ProviderMirrorRecord>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Legacy single-row shape — accepted on read and migrated forward on
|
|
71
|
+
* the first `readAll()` call after upgrade. Writers should use the
|
|
72
|
+
* per-provider shape via `writeProvider(provider, record)`.
|
|
73
|
+
*/
|
|
14
74
|
export interface MirrorRecord {
|
|
15
|
-
synced_at: string;
|
|
16
|
-
provider
|
|
17
|
-
model?: string;
|
|
18
|
-
h5_used_pct?: number;
|
|
19
|
-
h5_resets_at?: string;
|
|
20
|
-
weekly_used_pct?: number;
|
|
21
|
-
weekly_resets_at?: string;
|
|
75
|
+
synced_at: string;
|
|
76
|
+
provider?: string;
|
|
77
|
+
model?: string;
|
|
78
|
+
h5_used_pct?: number;
|
|
79
|
+
h5_resets_at?: string;
|
|
80
|
+
weekly_used_pct?: number;
|
|
81
|
+
weekly_resets_at?: string;
|
|
22
82
|
}
|
|
23
83
|
|
|
84
|
+
/** Known provider ids used as keys in the per-provider map. */
|
|
85
|
+
export type KnownProviderId =
|
|
86
|
+
| "minimax"
|
|
87
|
+
| "openai"
|
|
88
|
+
| "openai-codex"
|
|
89
|
+
| "glm"
|
|
90
|
+
| "anthropic"
|
|
91
|
+
| "openrouter"
|
|
92
|
+
| (string & {}); // allow extension
|
|
93
|
+
|
|
24
94
|
const STALE_WARN_MS = 30 * 60 * 1000; // 30 min → orange
|
|
25
95
|
const STALE_ERROR_MS = 2 * 60 * 60 * 1000; // 2 h → red
|
|
26
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Detect whether a raw read looks like a legacy single-row shape
|
|
99
|
+
* (i.e. not yet the per-provider map). Used to gate the upgrade path.
|
|
100
|
+
*/
|
|
101
|
+
function isLegacyShape(raw: unknown): raw is MirrorRecord & {
|
|
102
|
+
provider: string;
|
|
103
|
+
[key: string]: unknown;
|
|
104
|
+
} {
|
|
105
|
+
if (!raw || typeof raw !== "object") return false;
|
|
106
|
+
const obj = raw as Record<string, unknown>;
|
|
107
|
+
if ("provider" in obj && typeof obj.provider === "string") {
|
|
108
|
+
// Legacy single-row shape: has flat provider string, no map-of-records structure.
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Convert a legacy single-row record to per-provider shape. */
|
|
115
|
+
function upgradeLegacyRecord(legacy: MirrorRecord): PerProviderMirror {
|
|
116
|
+
const provider = legacy.provider ?? "minimax";
|
|
117
|
+
const rec: ProviderMirrorRecord = {
|
|
118
|
+
synced_at: legacy.synced_at ?? new Date().toISOString(),
|
|
119
|
+
provider,
|
|
120
|
+
source: "scrape", // legacy was always scrape
|
|
121
|
+
model: legacy.model,
|
|
122
|
+
h5_used_pct: legacy.h5_used_pct,
|
|
123
|
+
h5_resets_at: legacy.h5_resets_at,
|
|
124
|
+
weekly_used_pct: legacy.weekly_used_pct,
|
|
125
|
+
weekly_resets_at: legacy.weekly_resets_at,
|
|
126
|
+
};
|
|
127
|
+
return { [provider]: rec };
|
|
128
|
+
}
|
|
129
|
+
|
|
27
130
|
export class MirrorStore {
|
|
28
131
|
private path: string;
|
|
29
132
|
|
|
@@ -31,21 +134,88 @@ export class MirrorStore {
|
|
|
31
134
|
this.path = path;
|
|
32
135
|
}
|
|
33
136
|
|
|
34
|
-
/** Read the mirror record.
|
|
137
|
+
/** Read the mirror record. LEGACY: returns whatever is in the file. */
|
|
35
138
|
read(): MirrorRecord | null {
|
|
36
139
|
const raw = readJson(this.path);
|
|
37
140
|
if (!raw || typeof raw !== "object") return null;
|
|
38
141
|
return raw as MirrorRecord;
|
|
39
142
|
}
|
|
40
143
|
|
|
41
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Read the entire per-provider mirror. Auto-migrates legacy shape
|
|
146
|
+
* on first read after upgrade. Returns null if the file is missing
|
|
147
|
+
* or corrupted.
|
|
148
|
+
*/
|
|
149
|
+
readAll(): PerProviderMirror | null {
|
|
150
|
+
const raw = readJson(this.path);
|
|
151
|
+
if (!raw || typeof raw !== "object") return null;
|
|
152
|
+
|
|
153
|
+
if (isLegacyShape(raw)) {
|
|
154
|
+
const upgraded = upgradeLegacyRecord(raw as MirrorRecord);
|
|
155
|
+
// Persist the upgrade so subsequent reads are fast.
|
|
156
|
+
try {
|
|
157
|
+
writeJson(this.path, upgraded);
|
|
158
|
+
} catch {
|
|
159
|
+
// best-effort
|
|
160
|
+
}
|
|
161
|
+
return upgraded;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Already per-provider shape.
|
|
165
|
+
const out: PerProviderMirror = {};
|
|
166
|
+
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
167
|
+
if (v && typeof v === "object") {
|
|
168
|
+
out[k] = v as ProviderMirrorRecord;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Read a single provider's record. Returns null if missing. */
|
|
175
|
+
readProvider(provider: string): ProviderMirrorRecord | null {
|
|
176
|
+
const all = this.readAll();
|
|
177
|
+
if (!all) return null;
|
|
178
|
+
return all[provider] ?? null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Write a single provider's record (overwrites just that key). */
|
|
182
|
+
writeProvider(provider: string, record: ProviderMirrorRecord): void {
|
|
183
|
+
const all = this.readAll() ?? {};
|
|
184
|
+
all[provider] = record;
|
|
185
|
+
writeJson(this.path, all);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Write a new mirror record (overwrites). LEGACY: deprecated, use writeProvider. */
|
|
42
189
|
write(record: MirrorRecord): void {
|
|
190
|
+
// If the caller passes a record that already has a provider key,
|
|
191
|
+
// route through writeProvider so we keep the per-provider shape.
|
|
192
|
+
if (
|
|
193
|
+
record &&
|
|
194
|
+
typeof record === "object" &&
|
|
195
|
+
"provider" in record &&
|
|
196
|
+
typeof (record as { provider: unknown }).provider === "string"
|
|
197
|
+
) {
|
|
198
|
+
const r = record as MirrorRecord;
|
|
199
|
+
const provider = (r.provider as string) ?? "minimax";
|
|
200
|
+
const prev = this.readProvider(provider);
|
|
201
|
+
this.writeProvider(provider, {
|
|
202
|
+
synced_at: r.synced_at ?? new Date().toISOString(),
|
|
203
|
+
provider,
|
|
204
|
+
source: prev?.source ?? "scrape",
|
|
205
|
+
model: r.model ?? prev?.model,
|
|
206
|
+
h5_used_pct: r.h5_used_pct ?? prev?.h5_used_pct,
|
|
207
|
+
h5_resets_at: r.h5_resets_at ?? prev?.h5_resets_at,
|
|
208
|
+
weekly_used_pct: r.weekly_used_pct ?? prev?.weekly_used_pct,
|
|
209
|
+
weekly_resets_at: r.weekly_resets_at ?? prev?.weekly_resets_at,
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
43
213
|
writeJson(this.path, record);
|
|
44
214
|
}
|
|
45
215
|
|
|
46
216
|
/** Returns "fresh" | "stale" | "expired" based on age. */
|
|
47
217
|
freshness(
|
|
48
|
-
record: MirrorRecord | null,
|
|
218
|
+
record: ProviderMirrorRecord | MirrorRecord | null,
|
|
49
219
|
nowMs: number,
|
|
50
220
|
): "fresh" | "stale" | "expired" | "missing" {
|
|
51
221
|
if (!record || !record.synced_at) return "missing";
|
|
@@ -58,12 +228,18 @@ export class MirrorStore {
|
|
|
58
228
|
}
|
|
59
229
|
|
|
60
230
|
/** True if data is too stale to trust (> 2 hours old). */
|
|
61
|
-
isExpired(
|
|
231
|
+
isExpired(
|
|
232
|
+
record: ProviderMirrorRecord | MirrorRecord | null,
|
|
233
|
+
nowMs: number,
|
|
234
|
+
): boolean {
|
|
62
235
|
return this.freshness(record, nowMs) === "expired";
|
|
63
236
|
}
|
|
64
237
|
|
|
65
238
|
/** Convenience: human-readable age like "5 min ago" or "1 d 2 h ago". */
|
|
66
|
-
ageString(
|
|
239
|
+
ageString(
|
|
240
|
+
record: ProviderMirrorRecord | MirrorRecord | null,
|
|
241
|
+
nowMs: number,
|
|
242
|
+
): string {
|
|
67
243
|
if (!record || !record.synced_at) return "never";
|
|
68
244
|
const syncedMs = Date.parse(record.synced_at);
|
|
69
245
|
if (isNaN(syncedMs)) return "unknown";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|