pi-harness-runtime 0.9.6 → 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 +41 -10
- package/README.md +42 -5
- package/footer-status.ts +108 -11
- package/harness/e2e/minimax-quota-scraper.ts +46 -4
- package/index.ts +272 -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 });
|
|
@@ -81,6 +128,33 @@ async function getCheckpointManager(): Promise<CheckpointManager> {
|
|
|
81
128
|
) as unknown as CheckpointManager;
|
|
82
129
|
}
|
|
83
130
|
|
|
131
|
+
function isOutputLimitResumePromptMessage(message: {
|
|
132
|
+
role?: string;
|
|
133
|
+
content?: unknown;
|
|
134
|
+
}): boolean {
|
|
135
|
+
if (message.role !== "user") {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (typeof message.content === "string") {
|
|
139
|
+
return message.content === OUTPUT_LIMIT_RESUME_PROMPT;
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(message.content)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return (
|
|
145
|
+
message.content
|
|
146
|
+
.filter(
|
|
147
|
+
(part): part is { type: "text"; text: string } =>
|
|
148
|
+
part &&
|
|
149
|
+
typeof part === "object" &&
|
|
150
|
+
(part as { type?: unknown }).type === "text" &&
|
|
151
|
+
typeof (part as { text?: unknown }).text === "string",
|
|
152
|
+
)
|
|
153
|
+
.map((part) => part.text)
|
|
154
|
+
.join("\n") === OUTPUT_LIMIT_RESUME_PROMPT
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
84
158
|
export default function (pi: ExtensionAPI) {
|
|
85
159
|
const tracker = new UsageTracker();
|
|
86
160
|
const mirrorStore = new MirrorStore();
|
|
@@ -88,6 +162,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
162
|
|
|
89
163
|
// ─── Auto-track every assistant message ──────────────────────────────
|
|
90
164
|
pi.on("message_end", async (event, ctx) => {
|
|
165
|
+
if (isOutputLimitResumePromptMessage(event.message)) {
|
|
166
|
+
pendingOutputLimitResumeAfterSettled = false;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
91
169
|
if (event.message.role !== "assistant") return;
|
|
92
170
|
const m = event.message as {
|
|
93
171
|
role?: string;
|
|
@@ -111,9 +189,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
111
189
|
) {
|
|
112
190
|
outputLimitResumeAttempts += 1;
|
|
113
191
|
pendingOutputLimitResumeAfterCompact = true;
|
|
192
|
+
pendingOutputLimitResumeAfterSettled = true;
|
|
114
193
|
queueAutoResume("output-limit", OUTPUT_LIMIT_RESUME_PROMPT, "steer");
|
|
115
194
|
} else if (m.stopReason === "stop") {
|
|
116
195
|
outputLimitResumeAttempts = 0;
|
|
196
|
+
pendingOutputLimitResumeAfterSettled = false;
|
|
117
197
|
}
|
|
118
198
|
|
|
119
199
|
if (!m.usage) return;
|
|
@@ -126,6 +206,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
126
206
|
cache_write: m.usage.cacheWrite ?? 0,
|
|
127
207
|
cost: m.usage.cost?.total ?? 0,
|
|
128
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
|
+
}
|
|
129
222
|
});
|
|
130
223
|
|
|
131
224
|
// ─── Smart quota fetch for MiniMax status ────────────────────────
|
|
@@ -135,7 +228,84 @@ export default function (pi: ExtensionAPI) {
|
|
|
135
228
|
const quotaScraper = process.env.QUOTA_COOKIE_FILE
|
|
136
229
|
? new MiniMaxQuotaScraper({ cookieFile: process.env.QUOTA_COOKIE_FILE })
|
|
137
230
|
: new MiniMaxQuotaScraper();
|
|
138
|
-
|
|
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
|
+
|
|
139
309
|
let footerStatusCtx: {
|
|
140
310
|
ui: { setStatus: (key: string, value: string) => void };
|
|
141
311
|
} | null = null;
|
|
@@ -147,11 +317,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
147
317
|
let proactiveCompactCircuitReported = false;
|
|
148
318
|
let outputLimitResumeAttempts = 0;
|
|
149
319
|
let pendingOutputLimitResumeAfterCompact = false;
|
|
320
|
+
let pendingOutputLimitResumeAfterSettled = false;
|
|
150
321
|
|
|
151
|
-
function writeMirrorRecord(
|
|
152
|
-
|
|
322
|
+
function writeMirrorRecord(
|
|
323
|
+
provider: ProviderId,
|
|
324
|
+
record: Omit<import("./mirror.js").ProviderMirrorRecord, "provider">,
|
|
325
|
+
): void {
|
|
326
|
+
mirrorStore.writeProvider(provider, { ...record, provider });
|
|
153
327
|
if (footerStatusCtx) {
|
|
154
|
-
refreshFooterStatus(
|
|
328
|
+
refreshFooterStatus(
|
|
329
|
+
footerStatusCtx,
|
|
330
|
+
tracker,
|
|
331
|
+
mirrorStore,
|
|
332
|
+
hasCookieSource,
|
|
333
|
+
() => lastActiveProvider,
|
|
334
|
+
);
|
|
155
335
|
}
|
|
156
336
|
}
|
|
157
337
|
|
|
@@ -167,7 +347,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
167
347
|
}
|
|
168
348
|
|
|
169
349
|
function isMiniMaxModel(modelId: string | null | undefined): boolean {
|
|
170
|
-
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
|
+
}
|
|
171
371
|
}
|
|
172
372
|
|
|
173
373
|
function getMiniMaxUsageSince(sinceMs: number): {
|
|
@@ -246,7 +446,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
446
|
const profileRecord =
|
|
247
447
|
await autoFetchQuotaFromBrowserProfile(suppressErrors);
|
|
248
448
|
if (profileRecord) {
|
|
249
|
-
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
|
+
});
|
|
250
458
|
return true;
|
|
251
459
|
}
|
|
252
460
|
|
|
@@ -256,9 +464,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
256
464
|
|
|
257
465
|
try {
|
|
258
466
|
const data = await quotaScraper.scrape();
|
|
259
|
-
writeMirrorRecord({
|
|
467
|
+
writeMirrorRecord("minimax", {
|
|
260
468
|
synced_at: data.scrapedAt,
|
|
261
|
-
|
|
469
|
+
source: "scrape",
|
|
262
470
|
h5_used_pct: data.h5UsedPct,
|
|
263
471
|
h5_resets_at: data.h5ResetsAt,
|
|
264
472
|
weekly_used_pct: data.weeklyUsedPct,
|
|
@@ -339,8 +547,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
339
547
|
(await hasBrowserProfileAutoFetchSource());
|
|
340
548
|
if (!autoFetchAvailable) {
|
|
341
549
|
ctx.ui.notify(
|
|
342
|
-
"
|
|
343
|
-
"
|
|
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",
|
|
344
552
|
);
|
|
345
553
|
return;
|
|
346
554
|
}
|
|
@@ -418,7 +626,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
418
626
|
"info",
|
|
419
627
|
);
|
|
420
628
|
footerStatusCtx = ctx;
|
|
421
|
-
refreshFooterStatus(
|
|
629
|
+
refreshFooterStatus(
|
|
630
|
+
ctx,
|
|
631
|
+
tracker,
|
|
632
|
+
mirrorStore,
|
|
633
|
+
hasCookieSource,
|
|
634
|
+
() => lastActiveProvider,
|
|
635
|
+
);
|
|
422
636
|
} catch (e) {
|
|
423
637
|
ctx.ui.notify(`Failed to clear mirror: ${e}`, "error");
|
|
424
638
|
}
|
|
@@ -673,23 +887,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
673
887
|
// ─── Footer status (persistent badge) ────────────────────────────────
|
|
674
888
|
pi.on("session_start", (_event, ctx) => {
|
|
675
889
|
footerStatusCtx = ctx;
|
|
676
|
-
refreshFooterStatus(
|
|
890
|
+
refreshFooterStatus(
|
|
891
|
+
ctx,
|
|
892
|
+
tracker,
|
|
893
|
+
mirrorStore,
|
|
894
|
+
hasCookieSource,
|
|
895
|
+
() => lastActiveProvider,
|
|
896
|
+
);
|
|
677
897
|
});
|
|
678
898
|
|
|
679
899
|
pi.on("turn_end", (_event, ctx) => {
|
|
900
|
+
noteActiveProvider(ctx.model?.id ?? null);
|
|
680
901
|
footerStatusCtx = ctx;
|
|
681
|
-
refreshFooterStatus(
|
|
902
|
+
refreshFooterStatus(
|
|
903
|
+
ctx,
|
|
904
|
+
tracker,
|
|
905
|
+
mirrorStore,
|
|
906
|
+
hasCookieSource,
|
|
907
|
+
() => lastActiveProvider,
|
|
908
|
+
);
|
|
682
909
|
void maybeAutoFetchQuota(ctx.model?.id ?? null);
|
|
683
910
|
maybeTriggerProactiveCompact(ctx);
|
|
684
911
|
});
|
|
685
912
|
|
|
913
|
+
pi.on("agent_end", () => {
|
|
914
|
+
setTimeout(() => {
|
|
915
|
+
if (!pendingOutputLimitResumeAfterSettled) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
pendingOutputLimitResumeAfterSettled = false;
|
|
919
|
+
queueAutoResume(
|
|
920
|
+
"output-limit-settled",
|
|
921
|
+
OUTPUT_LIMIT_RESUME_PROMPT,
|
|
922
|
+
"followUp",
|
|
923
|
+
);
|
|
924
|
+
}, 0);
|
|
925
|
+
});
|
|
926
|
+
|
|
686
927
|
pi.on("session_compact", (event, ctx) => {
|
|
687
928
|
footerStatusCtx = ctx;
|
|
688
929
|
proactiveCompactInFlight = false;
|
|
689
930
|
lastProactiveCompactAt = Date.now();
|
|
690
931
|
consecutiveCompactFailures = 0;
|
|
691
932
|
proactiveCompactCircuitReported = false;
|
|
692
|
-
refreshFooterStatus(
|
|
933
|
+
refreshFooterStatus(
|
|
934
|
+
ctx,
|
|
935
|
+
tracker,
|
|
936
|
+
mirrorStore,
|
|
937
|
+
hasCookieSource,
|
|
938
|
+
() => lastActiveProvider,
|
|
939
|
+
);
|
|
693
940
|
|
|
694
941
|
const forceOutputLimitResume = pendingOutputLimitResumeAfterCompact;
|
|
695
942
|
pendingOutputLimitResumeAfterCompact = false;
|
|
@@ -784,13 +1031,22 @@ function refreshFooterStatus(
|
|
|
784
1031
|
ctx: { ui: { setStatus: (key: string, value: string) => void } },
|
|
785
1032
|
tracker: UsageTracker,
|
|
786
1033
|
mirrorStore: MirrorStore,
|
|
1034
|
+
hasCookieSource: () => boolean,
|
|
1035
|
+
getActiveProvider: () => ProviderId | null = () => null,
|
|
787
1036
|
) {
|
|
788
1037
|
const nowMs = Date.now();
|
|
789
1038
|
const local = aggregateWindows(tracker.all());
|
|
790
|
-
const
|
|
1039
|
+
const provider = getActiveProvider();
|
|
1040
|
+
const mirror = provider ? mirrorStore.readProvider(provider) : null;
|
|
791
1041
|
const freshness = mirrorStore.freshness(mirror, nowMs);
|
|
792
1042
|
ctx.ui.setStatus(
|
|
793
1043
|
"harness-runtime",
|
|
794
|
-
buildFooterStatusValue(
|
|
1044
|
+
buildFooterStatusValue(
|
|
1045
|
+
local,
|
|
1046
|
+
mirror,
|
|
1047
|
+
freshness,
|
|
1048
|
+
hasCookieSource(),
|
|
1049
|
+
provider,
|
|
1050
|
+
),
|
|
795
1051
|
);
|
|
796
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