pitroom 0.24.1 → 0.26.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +19 -17
- package/dist/lib.mjs +84 -18
- package/dist/pitroom.mjs +236 -63
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +31 -31
- package/docs/backends.md +4 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/pitroom.mjs
CHANGED
|
@@ -775,7 +775,7 @@ function invocation(req) {
|
|
|
775
775
|
return { command, args: [...prefix, ...args], env };
|
|
776
776
|
}
|
|
777
777
|
function classify(message) {
|
|
778
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
|
|
778
|
+
if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
|
|
779
779
|
if (/not (signed|logged) in|\bsign.?in\b|\blog ?in\b|authenticat|PERMISSION_DENIED|oauth|credential|keyring|keychain|\b401\b|unauthori[sz]ed/i.test(message)) {
|
|
780
780
|
return "auth";
|
|
781
781
|
}
|
|
@@ -1013,7 +1013,7 @@ function classify2(message) {
|
|
|
1013
1013
|
if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
1014
1014
|
return "auth";
|
|
1015
1015
|
}
|
|
1016
|
-
if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|
|
|
1016
|
+
if (/usage limit|session limit|limit reached|rate.?limit|too many requests|\b429\b|\b529\b|overloaded|quota|capacity/i.test(message)) return "rate-limited";
|
|
1017
1017
|
if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
|
|
1018
1018
|
return "other";
|
|
1019
1019
|
}
|
|
@@ -1214,7 +1214,7 @@ function classify3(message) {
|
|
|
1214
1214
|
if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
|
|
1215
1215
|
return "model-unavailable";
|
|
1216
1216
|
}
|
|
1217
|
-
if (/rate.?limit|too many requests|\b429\b|
|
|
1217
|
+
if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
|
|
1218
1218
|
if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
|
|
1219
1219
|
return "other";
|
|
1220
1220
|
}
|
|
@@ -1452,7 +1452,7 @@ function classify4(message) {
|
|
|
1452
1452
|
if (/IneligibleTier|no longer supported for Gemini|error authenticating|not logged in|please (log ?in|sign in)|credentials|api key|UNAUTHENTICATED|PERMISSION_DENIED|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
1453
1453
|
return "auth";
|
|
1454
1454
|
}
|
|
1455
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
1455
|
+
if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
1456
1456
|
if (/model.{0,80}(not found|not available|no longer available|does not exist|unsupported|invalid)|\b404\b|NOT_FOUND|invalid model|is not supported/i.test(message)) return "model-unavailable";
|
|
1457
1457
|
return "other";
|
|
1458
1458
|
}
|
|
@@ -1896,7 +1896,7 @@ function majorVersion(output) {
|
|
|
1896
1896
|
}
|
|
1897
1897
|
function classify5(message) {
|
|
1898
1898
|
if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
|
|
1899
|
-
if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1899
|
+
if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1900
1900
|
if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
|
|
1901
1901
|
return "auth";
|
|
1902
1902
|
}
|
|
@@ -2241,7 +2241,7 @@ function invocation7(req) {
|
|
|
2241
2241
|
}
|
|
2242
2242
|
function classify6(message) {
|
|
2243
2243
|
if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
|
|
2244
|
-
if (
|
|
2244
|
+
if (/usage limit|session limit|\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
|
|
2245
2245
|
if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
|
|
2246
2246
|
return "other";
|
|
2247
2247
|
}
|
|
@@ -2946,6 +2946,15 @@ exec "$real" "$@"
|
|
|
2946
2946
|
function shimDir() {
|
|
2947
2947
|
return path14.join(home(), "shim", `v${VERSION}`);
|
|
2948
2948
|
}
|
|
2949
|
+
function scoopTarget(exe) {
|
|
2950
|
+
try {
|
|
2951
|
+
const text = fs15.readFileSync(exe.replace(/\.exe$/i, ".shim"), "utf8");
|
|
2952
|
+
const target = /^\s*path\s*=\s*"?([^"\r\n]+?)"?\s*$/m.exec(text)?.[1];
|
|
2953
|
+
return target && fs15.statSync(target).isFile() ? target : void 0;
|
|
2954
|
+
} catch {
|
|
2955
|
+
return void 0;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2949
2958
|
function findRealGit(skip) {
|
|
2950
2959
|
const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2951
2960
|
for (const dir of (process.env.PATH ?? "").split(path14.delimiter).filter(Boolean)) {
|
|
@@ -2953,7 +2962,7 @@ function findRealGit(skip) {
|
|
|
2953
2962
|
const p = path14.join(dir, WINDOWS ? "git.exe" : "git");
|
|
2954
2963
|
try {
|
|
2955
2964
|
fs15.accessSync(p, fs15.constants.X_OK);
|
|
2956
|
-
if (fs15.statSync(p).isFile()) return p;
|
|
2965
|
+
if (fs15.statSync(p).isFile()) return WINDOWS && scoopTarget(p) || p;
|
|
2957
2966
|
} catch {
|
|
2958
2967
|
}
|
|
2959
2968
|
}
|
|
@@ -2972,11 +2981,28 @@ function findGitSh(realGit) {
|
|
|
2972
2981
|
}
|
|
2973
2982
|
return void 0;
|
|
2974
2983
|
}
|
|
2975
|
-
var cmdShim = (sh) =>
|
|
2976
|
-
|
|
2977
|
-
"
|
|
2978
|
-
|
|
2979
|
-
|
|
2984
|
+
var cmdShim = (sh) => [
|
|
2985
|
+
"@echo off",
|
|
2986
|
+
"setlocal EnableExtensions DisableDelayedExpansion",
|
|
2987
|
+
'set "MSYS2_ARG_CONV_EXCL=*"',
|
|
2988
|
+
'set "PITROOM_SELF=%~f0"',
|
|
2989
|
+
'set q=^"',
|
|
2990
|
+
"setlocal EnableDelayedExpansion",
|
|
2991
|
+
'set "line=!cmdcmdline!"',
|
|
2992
|
+
'if not "!line: /d =!"=="!line!" goto plain',
|
|
2993
|
+
'if not "!line: /s =!"=="!line!" goto plain',
|
|
2994
|
+
'set "tail=!line:*/c ""%PITROOM_SELF%"=!"',
|
|
2995
|
+
'if "!tail!"=="!line!" set "tail=!line:*/c "%PITROOM_SELF%=!"',
|
|
2996
|
+
'if "!tail!"=="!line!" goto plain',
|
|
2997
|
+
'if "!tail:~-1!"=="!q!" set "tail=!tail:~0,-1!"',
|
|
2998
|
+
`"${sh}" "%~dp0git" !tail!`,
|
|
2999
|
+
"exit /b !errorlevel!",
|
|
3000
|
+
":plain",
|
|
3001
|
+
"endlocal",
|
|
3002
|
+
`"${sh}" "%~dp0git" %*`,
|
|
3003
|
+
"exit /b %errorlevel%",
|
|
3004
|
+
""
|
|
3005
|
+
].join("\r\n");
|
|
2980
3006
|
var HOOK_VERSION = 1;
|
|
2981
3007
|
var REF_HOOK = `#!/bin/sh
|
|
2982
3008
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -3116,6 +3142,7 @@ import fs17 from "node:fs";
|
|
|
3116
3142
|
import path15 from "node:path";
|
|
3117
3143
|
var MAX_MS = 24 * 36e5;
|
|
3118
3144
|
var OVERLOAD_MS = 20 * 6e4;
|
|
3145
|
+
var USAGE_MS = 36e5;
|
|
3119
3146
|
var DAILY_MS = 4 * 36e5;
|
|
3120
3147
|
var file = () => path15.join(home(), "cooldowns.json");
|
|
3121
3148
|
function cooldownKey(target, backend) {
|
|
@@ -3133,10 +3160,41 @@ function retryAfterMs(message) {
|
|
|
3133
3160
|
}
|
|
3134
3161
|
return ms > 0 ? ms : void 0;
|
|
3135
3162
|
}
|
|
3136
|
-
function
|
|
3137
|
-
const
|
|
3163
|
+
function wallClock(at, timeZone) {
|
|
3164
|
+
const p = Object.fromEntries(
|
|
3165
|
+
new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" }).formatToParts(at).map((x) => [x.type, Number(x.value)])
|
|
3166
|
+
);
|
|
3167
|
+
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
3168
|
+
}
|
|
3169
|
+
function retryAtMs(message, now = Date.now()) {
|
|
3170
|
+
const m = /(?:try again at|resets?(?: at)?)\s+(\d{1,2})(?::(\d{2}))?\s*([ap])\.?m\b\.?(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message) ?? /(?:try again at|resets?(?: at)?)\s+(\d{1,2}):(\d{2})()(?!\d)(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message);
|
|
3171
|
+
if (!m) return void 0;
|
|
3172
|
+
let hour = Number(m[1]);
|
|
3173
|
+
const minute = Number(m[2] ?? 0);
|
|
3174
|
+
const half = m[3]?.toLowerCase();
|
|
3175
|
+
if (minute > 59 || (half ? hour < 1 || hour > 12 : hour > 23)) return void 0;
|
|
3176
|
+
if (half === "p" && hour < 12) hour += 12;
|
|
3177
|
+
if (half === "a" && hour === 12) hour = 0;
|
|
3178
|
+
let zone = m[4];
|
|
3179
|
+
try {
|
|
3180
|
+
if (zone) new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
3181
|
+
} catch {
|
|
3182
|
+
zone = void 0;
|
|
3183
|
+
}
|
|
3184
|
+
const wall = wallClock(now, zone);
|
|
3185
|
+
for (const day of [0, 1]) {
|
|
3186
|
+
const d = new Date(wall);
|
|
3187
|
+
const target = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + day, hour, minute);
|
|
3188
|
+
const at = target - (wallClock(target, zone) - target);
|
|
3189
|
+
if (at > now) return at - now;
|
|
3190
|
+
}
|
|
3191
|
+
return void 0;
|
|
3192
|
+
}
|
|
3193
|
+
function cooldownMs(message, now = Date.now()) {
|
|
3194
|
+
const told = retryAfterMs(message) ?? retryAtMs(message, now);
|
|
3138
3195
|
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
3139
|
-
|
|
3196
|
+
if (/daily|per.?day|per day|exhausted your/i.test(message)) return DAILY_MS;
|
|
3197
|
+
return /usage limit|session limit|quota|FreeUsageLimit/i.test(message) ? USAGE_MS : OVERLOAD_MS;
|
|
3140
3198
|
}
|
|
3141
3199
|
function read() {
|
|
3142
3200
|
try {
|
|
@@ -3160,7 +3218,7 @@ var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
|
|
|
3160
3218
|
function recordCooldown(key, targetName, message, now = Date.now()) {
|
|
3161
3219
|
try {
|
|
3162
3220
|
const all = activeCooldowns(now);
|
|
3163
|
-
all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3221
|
+
all[key] = { until: new Date(now + cooldownMs(message, now)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3164
3222
|
write(all);
|
|
3165
3223
|
} catch {
|
|
3166
3224
|
}
|
|
@@ -3349,12 +3407,22 @@ function focusedRate(base2, task, record2) {
|
|
|
3349
3407
|
return { rate: Math.min(rate2, 1), why };
|
|
3350
3408
|
}
|
|
3351
3409
|
function pickAuditor(meta) {
|
|
3410
|
+
return auditors(meta)[0];
|
|
3411
|
+
}
|
|
3412
|
+
function auditors(meta) {
|
|
3352
3413
|
const eff = effective();
|
|
3353
3414
|
const tiers = eff.tiers.value;
|
|
3354
3415
|
const def = parseTarget(eff.worker.value, DEFAULT_BACKEND).backend;
|
|
3355
3416
|
const ran = meta.ran ?? meta.worker;
|
|
3417
|
+
const bare = (t) => ({ backend: t.backend, model: t.model?.split("#")[0] || void 0 });
|
|
3356
3418
|
const candidates = [tiers.audit, tiers.cheap, ...eff.fallback.value, eff.worker.value].filter((s) => !!s);
|
|
3357
|
-
|
|
3419
|
+
const out = [];
|
|
3420
|
+
for (const c of candidates) {
|
|
3421
|
+
const t = bare(parseTarget(c, def));
|
|
3422
|
+
if (sameTarget(t, bare(ran)) || out.some((o) => sameTarget(bare(parseTarget(o, def)), t))) continue;
|
|
3423
|
+
out.push(c);
|
|
3424
|
+
}
|
|
3425
|
+
return out;
|
|
3358
3426
|
}
|
|
3359
3427
|
function relativeToAuditor(answer, meta) {
|
|
3360
3428
|
let out = answer;
|
|
@@ -3611,12 +3679,21 @@ function workerPrice(backend, ...models) {
|
|
|
3611
3679
|
}
|
|
3612
3680
|
var costAt = (usage2, price) => (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
3613
3681
|
var workerCostOf = (usage2) => usage2?.cost ?? usage2?.costEstimate;
|
|
3682
|
+
function addUsage(a, b) {
|
|
3683
|
+
const sum = (k) => (a[k] ?? 0) + (b[k] ?? 0);
|
|
3684
|
+
const out = { input: sum("input"), output: sum("output"), reasoning: sum("reasoning"), cacheRead: sum("cacheRead"), cacheWrite: sum("cacheWrite"), total: sum("total"), steps: sum("steps"), toolCalls: sum("toolCalls"), denied: sum("denied") };
|
|
3685
|
+
const ca = workerCostOf(a);
|
|
3686
|
+
const cb = workerCostOf(b);
|
|
3687
|
+
if (ca === void 0 && cb === void 0) return out;
|
|
3688
|
+
if (a.cost !== void 0 && b.cost !== void 0) return { ...out, cost: a.cost + b.cost };
|
|
3689
|
+
return { ...out, costEstimate: (ca ?? 0) + (cb ?? 0), costSource: a.costSource ?? b.costSource };
|
|
3690
|
+
}
|
|
3614
3691
|
function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
|
|
3615
3692
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
3616
3693
|
return Math.max(0, costAt(usage2, price) - (workerCostOf(usage2) ?? 0) - readingTheReport);
|
|
3617
3694
|
}
|
|
3618
3695
|
function record(meta) {
|
|
3619
|
-
if (!meta.usage?.steps
|
|
3696
|
+
if (meta.auditOf || !meta.usage?.steps && meta.state !== "done") return false;
|
|
3620
3697
|
const entry = {
|
|
3621
3698
|
id: meta.id,
|
|
3622
3699
|
at: meta.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3624,22 +3701,49 @@ function record(meta) {
|
|
|
3624
3701
|
state: meta.state,
|
|
3625
3702
|
backend: (meta.ran ?? meta.worker).backend,
|
|
3626
3703
|
model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
|
|
3627
|
-
tokens: meta.usage
|
|
3704
|
+
tokens: meta.usage?.total ?? 0,
|
|
3628
3705
|
returned: meta.returnedTokens ?? 0,
|
|
3629
3706
|
workerCost: workerCostOf(meta.usage) ?? 0,
|
|
3630
|
-
...meta.usage
|
|
3707
|
+
...meta.usage?.cost === void 0 && meta.usage?.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
|
|
3631
3708
|
saved: meta.savedUsd ?? 0,
|
|
3632
3709
|
price: primaryPrice().name
|
|
3633
3710
|
};
|
|
3634
3711
|
fs20.mkdirSync(path19.dirname(ledgerFile()), { recursive: true });
|
|
3635
3712
|
fs20.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
3636
3713
|
`);
|
|
3714
|
+
return true;
|
|
3715
|
+
}
|
|
3716
|
+
var SETTLING_MS = 10 * 6e4;
|
|
3717
|
+
function settleLedger(metas) {
|
|
3718
|
+
const known = new Set(readLedger().map((e) => e.id));
|
|
3719
|
+
let n = 0;
|
|
3720
|
+
for (const m of metas ?? finishedRuns()) {
|
|
3721
|
+
if (known.has(m.id) || !TERMINAL.includes(m.state)) continue;
|
|
3722
|
+
if (m.pid !== process.pid && isAlive(m.pid) && (!m.endedAt || Date.now() - Date.parse(m.endedAt) < SETTLING_MS)) continue;
|
|
3723
|
+
if (record(m)) {
|
|
3724
|
+
known.add(m.id);
|
|
3725
|
+
n++;
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return n;
|
|
3729
|
+
}
|
|
3730
|
+
function* finishedRuns() {
|
|
3731
|
+
for (const id2 of listRunIds()) {
|
|
3732
|
+
try {
|
|
3733
|
+
yield readMeta(id2);
|
|
3734
|
+
} catch {
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3637
3737
|
}
|
|
3638
3738
|
function readLedger(sinceMs2) {
|
|
3639
3739
|
if (!fs20.existsSync(ledgerFile())) return [];
|
|
3740
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3640
3741
|
return fs20.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
3641
3742
|
try {
|
|
3642
|
-
|
|
3743
|
+
const e = JSON.parse(l);
|
|
3744
|
+
if (seen.has(e.id)) return [];
|
|
3745
|
+
seen.add(e.id);
|
|
3746
|
+
return [e];
|
|
3643
3747
|
} catch {
|
|
3644
3748
|
return [];
|
|
3645
3749
|
}
|
|
@@ -4036,6 +4140,7 @@ function archivedId(ref) {
|
|
|
4036
4140
|
}
|
|
4037
4141
|
}
|
|
4038
4142
|
var VERIFY_FAILED = "(r.state = 'done' AND json_extract(r.meta_json, '$.verifyResult.ok') = 0)";
|
|
4143
|
+
var COST_ESTIMATED = "(json_extract(r.meta_json, '$.usage.cost') IS NULL AND json_extract(r.meta_json, '$.usage.costEstimate') IS NOT NULL)";
|
|
4039
4144
|
var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
|
|
4040
4145
|
function listHistory(q = {}) {
|
|
4041
4146
|
const db = openDb();
|
|
@@ -4070,7 +4175,7 @@ function listHistory(q = {}) {
|
|
|
4070
4175
|
try {
|
|
4071
4176
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
|
|
4072
4177
|
const page2 = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
|
|
4073
|
-
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${page2} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
|
|
4178
|
+
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed, ${COST_ESTIMATED} AS cost_estimated FROM runs r ${page2} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
|
|
4074
4179
|
return {
|
|
4075
4180
|
total,
|
|
4076
4181
|
rows: rows.map((r) => ({
|
|
@@ -4088,6 +4193,8 @@ function listHistory(q = {}) {
|
|
|
4088
4193
|
seconds: r.seconds ?? void 0,
|
|
4089
4194
|
steps: r.steps ?? void 0,
|
|
4090
4195
|
tokens: r.tokens ?? void 0,
|
|
4196
|
+
cost: r.cost ?? void 0,
|
|
4197
|
+
...r.cost_estimated ? { costEstimated: true } : {},
|
|
4091
4198
|
saved: r.saved ?? void 0,
|
|
4092
4199
|
files: r.files_changed ?? 0,
|
|
4093
4200
|
applied: !!r.applied
|
|
@@ -4122,18 +4229,32 @@ function auditStats(db, since2) {
|
|
|
4122
4229
|
}
|
|
4123
4230
|
return { total, byWorker };
|
|
4124
4231
|
}
|
|
4125
|
-
|
|
4232
|
+
var WORK_SQL = { research: "kind = 'research'", change: "kind LIKE 'change%'", review: "kind LIKE '%review'" };
|
|
4233
|
+
function workerRecord(backend, model, sinceMs2, work) {
|
|
4126
4234
|
const db = openDb();
|
|
4127
4235
|
if (!db) return void 0;
|
|
4128
4236
|
const since2 = new Date(sinceMs2).toISOString();
|
|
4129
4237
|
const fits = (m) => !model || m === model || !!m?.endsWith(`/${model}`) || model.endsWith(`/${m}`);
|
|
4130
4238
|
const rec = { runs: 0, ok: 0, audited: 0, agreed: 0 };
|
|
4131
4239
|
try {
|
|
4132
|
-
for (const r of db.prepare(`SELECT model, SUM(NOT ${LIMITED}) runs, SUM(state='done') ok FROM runs WHERE backend = ? AND started_at >= ? AND ${NOT_AUDIT} AND ${ATTEMPTED} GROUP BY model`).all(backend, since2)) {
|
|
4240
|
+
for (const r of db.prepare(`SELECT model, SUM(NOT ${LIMITED}) runs, SUM(state='done') ok FROM runs WHERE backend = ? AND started_at >= ? AND ${NOT_AUDIT} AND ${ATTEMPTED} AND ${work ? WORK_SQL[work] : "1"} GROUP BY model`).all(backend, since2)) {
|
|
4133
4241
|
if (!fits(r.model)) continue;
|
|
4134
4242
|
rec.runs += r.runs ?? 0;
|
|
4135
4243
|
rec.ok += r.ok ?? 0;
|
|
4136
4244
|
}
|
|
4245
|
+
if (work === "review") return rec;
|
|
4246
|
+
if (work === "change") {
|
|
4247
|
+
const reviews = db.prepare(`SELECT r.model, json_extract(v.meta_json, '$.verdict.spec') spec, json_extract(v.meta_json, '$.verdict.quality') quality, COUNT(*) n
|
|
4248
|
+
FROM runs v JOIN runs r ON r.id = v.review_of
|
|
4249
|
+
WHERE r.backend = ? AND v.started_at >= ? AND v.state = 'done' AND r.kind LIKE 'change%' AND json_extract(v.meta_json, '$.reviewOf') IS NOT NULL
|
|
4250
|
+
GROUP BY r.model, spec, quality`).all(backend, since2);
|
|
4251
|
+
for (const r of reviews) {
|
|
4252
|
+
if (!fits(r.model) || !["approved", "needs-fixes"].includes(r.quality)) continue;
|
|
4253
|
+
rec.audited += r.n;
|
|
4254
|
+
if (r.spec === "pass" && r.quality === "approved") rec.agreed += r.n;
|
|
4255
|
+
}
|
|
4256
|
+
return rec;
|
|
4257
|
+
}
|
|
4137
4258
|
const audits = db.prepare(`SELECT r.model, json_extract(a.meta_json, '$.auditVerdict') v, COUNT(*) n
|
|
4138
4259
|
FROM runs a JOIN runs r ON r.id = json_extract(a.meta_json, '$.auditOf')
|
|
4139
4260
|
WHERE r.backend = ? AND a.started_at >= ? AND a.state = 'done' AND json_extract(a.meta_json, '$.auditOf') IS NOT NULL
|
|
@@ -4279,7 +4400,7 @@ function scoreOf(r) {
|
|
|
4279
4400
|
return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
|
|
4280
4401
|
}
|
|
4281
4402
|
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
4282
|
-
function rankChain(chain, recordOf) {
|
|
4403
|
+
function rankChain(chain, recordOf, work) {
|
|
4283
4404
|
const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
|
|
4284
4405
|
const slots = scored.filter((s) => s.score !== void 0);
|
|
4285
4406
|
if (slots.length < 2) return { chain };
|
|
@@ -4290,7 +4411,7 @@ function rankChain(chain, recordOf) {
|
|
|
4290
4411
|
const by = (t) => scored.find((s) => s.t === t).score;
|
|
4291
4412
|
return {
|
|
4292
4413
|
chain: out,
|
|
4293
|
-
note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
4414
|
+
note: `ranked by record${work ? ` on ${work}` : ""} ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
4294
4415
|
};
|
|
4295
4416
|
}
|
|
4296
4417
|
|
|
@@ -4451,7 +4572,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
4451
4572
|
const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
|
|
4452
4573
|
out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
|
|
4453
4574
|
}
|
|
4454
|
-
for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
4575
|
+
for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : a.partial ? `fallback: ${a.target} hit a limit part way and handed the task over (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
4455
4576
|
if (meta.error) out.push(`error: ${meta.error}`);
|
|
4456
4577
|
for (const w of meta.warnings) out.push(`warning: ${w}`);
|
|
4457
4578
|
if (meta.state === "timeout") {
|
|
@@ -5161,7 +5282,7 @@ function inside2(file2, roots) {
|
|
|
5161
5282
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5162
5283
|
|
|
5163
5284
|
// src/core/run.ts
|
|
5164
|
-
var VERSION2 = true ? "0.
|
|
5285
|
+
var VERSION2 = true ? "0.26.0" : "0.0.0-dev";
|
|
5165
5286
|
function planWork(o) {
|
|
5166
5287
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
5167
5288
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -5200,12 +5321,14 @@ function prepareRun(o) {
|
|
|
5200
5321
|
worker = o.model ? { ...ran, model: o.model } : ran;
|
|
5201
5322
|
fallback = o.noFallback ? [] : parent.fallback.filter((t) => t.backend === ran.backend);
|
|
5202
5323
|
} else {
|
|
5203
|
-
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback });
|
|
5324
|
+
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback || !!o.fallbackTo });
|
|
5204
5325
|
({ worker, fallback } = chain);
|
|
5326
|
+
if (o.fallbackTo && !o.noFallback) fallback = o.fallbackTo.map((w) => resolveChain({ worker: w, noFallback: true }).worker);
|
|
5205
5327
|
warnings2.push(...chain.warnings);
|
|
5206
5328
|
if (effective().rankWorkers.value && !o.worker && !o.model && !work.tier && fallback.length) {
|
|
5207
5329
|
const since2 = Date.now() - 30 * 864e5;
|
|
5208
|
-
const
|
|
5330
|
+
const work2 = o.review ? "review" : o.mode === "read" ? "research" : "change";
|
|
5331
|
+
const ranked = rankChain([worker, ...fallback], (t) => workerRecord(t.backend, t.model, since2, work2), work2 === "change" ? "changes" : work2);
|
|
5209
5332
|
[worker, ...fallback] = ranked.chain;
|
|
5210
5333
|
if (ranked.note) warnings2.push(ranked.note);
|
|
5211
5334
|
}
|
|
@@ -5396,7 +5519,8 @@ async function execute(meta) {
|
|
|
5396
5519
|
}
|
|
5397
5520
|
}
|
|
5398
5521
|
if (i === chain.length - 1) break;
|
|
5399
|
-
(
|
|
5522
|
+
if (why.partial) handOver(meta, backend, target);
|
|
5523
|
+
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message, ...why.partial ? { partial: true } : {} });
|
|
5400
5524
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
5401
5525
|
const from = runFile(meta.id, f);
|
|
5402
5526
|
if (fs31.existsSync(from)) fs31.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
@@ -5422,7 +5546,7 @@ function autoAudit(meta) {
|
|
|
5422
5546
|
let rate2 = auditRate(meta);
|
|
5423
5547
|
if (meta.auditRate === void 0 && effective().auditFocus.value) {
|
|
5424
5548
|
const ran = meta.ran ?? meta.worker;
|
|
5425
|
-
const focus = focusedRate(rate2, meta.task, workerRecord(ran.backend, meta.resolvedModel ?? ran.model, Date.now() - 30 * 864e5));
|
|
5549
|
+
const focus = focusedRate(rate2, meta.task, workerRecord(ran.backend, meta.resolvedModel ?? ran.model, Date.now() - 30 * 864e5, "research"));
|
|
5426
5550
|
rate2 = focus.rate;
|
|
5427
5551
|
if (focus.why.length) {
|
|
5428
5552
|
meta.auditFocus = { rate: rate2, why: focus.why };
|
|
@@ -5436,7 +5560,7 @@ function autoAudit(meta) {
|
|
|
5436
5560
|
}
|
|
5437
5561
|
function startAudit(meta, worker) {
|
|
5438
5562
|
const answer = read2(runFile(meta.id, "summary.md"));
|
|
5439
|
-
const auditor = worker
|
|
5563
|
+
const [auditor, ...others] = worker ? [worker] : auditors(meta);
|
|
5440
5564
|
if (!auditor) return void 0;
|
|
5441
5565
|
const a = prepareRun({
|
|
5442
5566
|
mode: "read",
|
|
@@ -5448,8 +5572,9 @@ function startAudit(meta, worker) {
|
|
|
5448
5572
|
timeoutSec: Math.min(meta.timeoutSec, 15 * 60),
|
|
5449
5573
|
allowNonGit: true,
|
|
5450
5574
|
web: false,
|
|
5451
|
-
//
|
|
5452
|
-
noFallback:
|
|
5575
|
+
// the other auditors, never the worker being audited; one you named has none
|
|
5576
|
+
noFallback: !!worker,
|
|
5577
|
+
fallbackTo: others,
|
|
5453
5578
|
group: meta.group,
|
|
5454
5579
|
audit: { of: meta.id }
|
|
5455
5580
|
});
|
|
@@ -5572,7 +5697,9 @@ function sharedSnapshot(meta, root) {
|
|
|
5572
5697
|
function attempt(meta, backend, target) {
|
|
5573
5698
|
const inv = backend.invocation({
|
|
5574
5699
|
mode: meta.mode,
|
|
5575
|
-
prompt: buildPrompt(meta.mode, meta.
|
|
5700
|
+
prompt: buildPrompt(meta.mode, meta.handoff ? `${meta.task}
|
|
5701
|
+
|
|
5702
|
+
${meta.handoff}` : meta.task, !!meta.parent, !!meta.auditOf),
|
|
5576
5703
|
cwd: meta.cwd,
|
|
5577
5704
|
model: target.model,
|
|
5578
5705
|
sessionId: meta.sessionId,
|
|
@@ -5591,9 +5718,28 @@ function attempt(meta, backend, target) {
|
|
|
5591
5718
|
var read2 = (f) => fs31.existsSync(f) ? fs31.readFileSync(f, "utf8") : "";
|
|
5592
5719
|
function retryableFailure(meta, backend, res) {
|
|
5593
5720
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
5594
|
-
if (run2.usage.steps > 0) return void 0;
|
|
5595
5721
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
5596
|
-
|
|
5722
|
+
if (!f || f.kind === "other") return void 0;
|
|
5723
|
+
if (run2.usage.steps === 0) return f;
|
|
5724
|
+
return f.kind === "rate-limited" ? { ...f, partial: true } : void 0;
|
|
5725
|
+
}
|
|
5726
|
+
function handOver(meta, backend, target) {
|
|
5727
|
+
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
5728
|
+
const used = { ...run2.usage };
|
|
5729
|
+
if (used.cost === void 0) {
|
|
5730
|
+
const price = workerPrice(target.backend, run2.model, target.model);
|
|
5731
|
+
if (price) Object.assign(used, { costEstimate: costAt(used, price), costSource: price.source ?? "config" });
|
|
5732
|
+
}
|
|
5733
|
+
meta.priorUsage = meta.priorUsage ? addUsage(meta.priorUsage, used) : used;
|
|
5734
|
+
if (meta.mode === "read") return;
|
|
5735
|
+
let changed = [];
|
|
5736
|
+
try {
|
|
5737
|
+
const root = meta.mode === "isolate" ? meta.worktree : meta.repoRoot;
|
|
5738
|
+
if (meta.baseTree) changed = diffTrees(root, meta.baseTree, snapshotTree(root, meta.mode === "isolate" ? meta.link : [])).changes.map((c) => c.path);
|
|
5739
|
+
} catch {
|
|
5740
|
+
}
|
|
5741
|
+
const list2 = changed.length ? `: ${changed.slice(0, 30).join(", ")}${changed.length > 30 ? `, and ${changed.length - 30} more` : ""}` : "";
|
|
5742
|
+
meta.handoff = `Another worker started this task and stopped part way, at its usage limit. Its edits are already in the files${list2}. Do not start over: check what it changed against the task, keep what is right, fix what is not, and finish the task.`;
|
|
5597
5743
|
}
|
|
5598
5744
|
var HINTS = {
|
|
5599
5745
|
"model-unavailable": "the model is not available; fix the worker config, pass --worker/--model, or configure fallback workers (see `pitroom doctor`)",
|
|
@@ -5659,6 +5805,7 @@ function finalize(meta, res) {
|
|
|
5659
5805
|
meta.usage.costSource = price.source ?? "config";
|
|
5660
5806
|
}
|
|
5661
5807
|
}
|
|
5808
|
+
if (meta.priorUsage && meta.usage) meta.usage = addUsage(meta.priorUsage, meta.usage);
|
|
5662
5809
|
meta.returnedTokens = estimateTokens(formatReport(meta, run2.finalText));
|
|
5663
5810
|
meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
|
|
5664
5811
|
writeMeta(meta);
|
|
@@ -6103,6 +6250,7 @@ var TOPICS = {
|
|
|
6103
6250
|
stats: ["since"],
|
|
6104
6251
|
savings: ["since", "perModel"],
|
|
6105
6252
|
models: ["worker", "all"],
|
|
6253
|
+
prices: ["worker"],
|
|
6106
6254
|
cooldown: [],
|
|
6107
6255
|
config: [],
|
|
6108
6256
|
doctor: []
|
|
@@ -6128,6 +6276,8 @@ function infoArgs(topic, a) {
|
|
|
6128
6276
|
return { args: ["history", "stats", ...since(a)] };
|
|
6129
6277
|
case "savings":
|
|
6130
6278
|
return { args: ["savings", ...since(a), ...bool(a, "perModel") ? ["--models"] : []] };
|
|
6279
|
+
case "prices":
|
|
6280
|
+
return { args: ["prices", ...worker ? ["--", worker] : []] };
|
|
6131
6281
|
case "models":
|
|
6132
6282
|
return { args: ["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []] };
|
|
6133
6283
|
case "doctor":
|
|
@@ -6210,7 +6360,7 @@ var TOOLS2 = [
|
|
|
6210
6360
|
{
|
|
6211
6361
|
name: "pitroom_info",
|
|
6212
6362
|
title: "Pitroom information",
|
|
6213
|
-
description: "Reports, changing nothing. topic: runs (latest runs; running, group), history (search earlier answers before asking again; text, state, model, since, limit), stats (success rate, time, tokens, audits per worker; since), savings (since, perModel), models (a backend's models and costs; worker, all), cooldown (rate-limited models being skipped), config, doctor (setup check).",
|
|
6363
|
+
description: "Reports, changing nothing. topic: runs (latest runs; running, group), history (search earlier answers before asking again; text, state, model, since, limit), stats (success rate, time, tokens, audits per worker; since), savings (since, perModel), models (a backend's models and costs; worker, all), prices (USD per 1M tokens: the primary the savings are measured against, and each worker in use or the one named; worker), cooldown (rate-limited models being skipped), config, doctor (setup check).",
|
|
6214
6364
|
inputSchema: {
|
|
6215
6365
|
type: "object",
|
|
6216
6366
|
properties: {
|
|
@@ -6695,7 +6845,8 @@ var MISSING = `<!doctype html><meta charset="utf-8"><title>Pitroom</title><body
|
|
|
6695
6845
|
|
|
6696
6846
|
// src/core/dash.ts
|
|
6697
6847
|
var DEFAULT_PORT = 7878;
|
|
6698
|
-
var
|
|
6848
|
+
var DAY_MS = 24 * 3600 * 1e3;
|
|
6849
|
+
var PERIODS = { day: 1, week: 7, month: 30, half: 182, all: Infinity };
|
|
6699
6850
|
var SCAN = 400;
|
|
6700
6851
|
var RUN_ID3 = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
|
|
6701
6852
|
var LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]):\d+$/i;
|
|
@@ -6752,6 +6903,7 @@ function toRun(m) {
|
|
|
6752
6903
|
}
|
|
6753
6904
|
function dashState(opts = {}) {
|
|
6754
6905
|
const limit = Math.min(Math.max(opts.limit ?? 40, 1), 200);
|
|
6906
|
+
const scanned = [];
|
|
6755
6907
|
const metas = [];
|
|
6756
6908
|
const groups = /* @__PURE__ */ new Set();
|
|
6757
6909
|
for (const id2 of listRunIds().slice(-SCAN).reverse()) {
|
|
@@ -6761,16 +6913,25 @@ function dashState(opts = {}) {
|
|
|
6761
6913
|
} catch {
|
|
6762
6914
|
continue;
|
|
6763
6915
|
}
|
|
6916
|
+
scanned.push(m);
|
|
6764
6917
|
if (m.group) groups.add(m.group);
|
|
6765
6918
|
if (opts.group && m.group !== opts.group) continue;
|
|
6766
6919
|
metas.push(m);
|
|
6767
6920
|
}
|
|
6768
6921
|
const whole = new Set(metas.slice(0, limit).flatMap((m) => m.group ? [m.group] : []));
|
|
6769
6922
|
const runs = metas.filter((m, i) => i < limit || m.group && whole.has(m.group)).map(toRun);
|
|
6923
|
+
const now = Date.now();
|
|
6924
|
+
settleLedger(scanned);
|
|
6925
|
+
const ledger = readLedger();
|
|
6926
|
+
const within = (at, days2) => !!at && Date.parse(at) >= now - days2 * DAY_MS;
|
|
6927
|
+
const per = (f) => Object.fromEntries(Object.entries(PERIODS).map(([p, days2]) => [p, f(days2)]));
|
|
6770
6928
|
return {
|
|
6771
6929
|
price: primaryPrice().name,
|
|
6772
6930
|
running: runs.filter((r) => r.state === "running" || r.state === "queued").length,
|
|
6773
|
-
|
|
6931
|
+
finished: per(
|
|
6932
|
+
(days2) => opts.group ? metas.filter((m) => m.state === "done" && !m.auditOf && within(m.endedAt, days2)).length : ledger.filter((e) => e.state === "done" && within(e.at, days2)).length
|
|
6933
|
+
),
|
|
6934
|
+
saved: per((days2) => totals(ledger.filter((e) => within(e.at, days2))).saved),
|
|
6774
6935
|
groups: [...groups].sort(),
|
|
6775
6936
|
runs
|
|
6776
6937
|
};
|
|
@@ -7050,6 +7211,7 @@ async function dashCommand(o) {
|
|
|
7050
7211
|
throw new UserError("pitroom dash did not start (is the port taken? try --port 0)");
|
|
7051
7212
|
}
|
|
7052
7213
|
importRuns();
|
|
7214
|
+
settleLedger();
|
|
7053
7215
|
const explicit = o.port !== void 0;
|
|
7054
7216
|
let dash;
|
|
7055
7217
|
try {
|
|
@@ -8171,7 +8333,8 @@ async function cmdAudit(p) {
|
|
|
8171
8333
|
if (m.mode !== "read") throw new UserError(`run ${m.id} changed files; an audit re-checks a read run's answer: pitroom review ${m.id} judges a change`);
|
|
8172
8334
|
const answer = readSummary(m);
|
|
8173
8335
|
if (!answer.trim()) throw new UserError(`run ${m.id} gave no answer to audit`);
|
|
8174
|
-
const
|
|
8336
|
+
const named = flag(p, "worker") ?? flag(p, "tier");
|
|
8337
|
+
const [worker, ...others] = named ? [flag(p, "worker")] : auditors(m);
|
|
8175
8338
|
if (!worker && !flag(p, "tier")) {
|
|
8176
8339
|
throw new UserError('no other worker to audit with: name one with -W, or configure tiers "audit" or "cheap", or fallback workers, that differ from the one that answered', 3);
|
|
8177
8340
|
}
|
|
@@ -8181,7 +8344,8 @@ async function cmdAudit(p) {
|
|
|
8181
8344
|
dir: m.dir,
|
|
8182
8345
|
worker,
|
|
8183
8346
|
group: flag(p, "group") ?? m.group,
|
|
8184
|
-
noFallback:
|
|
8347
|
+
noFallback: !!named,
|
|
8348
|
+
fallbackTo: others,
|
|
8185
8349
|
audit: { of: m.id },
|
|
8186
8350
|
auditRate: void 0
|
|
8187
8351
|
});
|
|
@@ -8199,33 +8363,37 @@ async function cmdPrices(p) {
|
|
|
8199
8363
|
if (!has(p, "quiet")) console.log(`price catalog updated: ${r.models} priced models`);
|
|
8200
8364
|
}
|
|
8201
8365
|
const st = catalogStatus();
|
|
8202
|
-
const
|
|
8366
|
+
const priced = (t) => {
|
|
8367
|
+
const backend = getBackend(t.backend);
|
|
8368
|
+
const model = (t.model ?? "").split("#")[0];
|
|
8369
|
+
const price = workerPrice(t.backend, model);
|
|
8370
|
+
return {
|
|
8371
|
+
worker: model ? `${t.backend}:${model}` : t.backend,
|
|
8372
|
+
reportsCost: backend.capabilities.reportsCost,
|
|
8373
|
+
price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
|
|
8374
|
+
};
|
|
8375
|
+
};
|
|
8376
|
+
const asked = p.positional.length > 0;
|
|
8377
|
+
const used = asked ? p.positional.map((spec) => priced(parseTarget(spec, resolveChain().worker.backend))) : (() => {
|
|
8203
8378
|
try {
|
|
8204
8379
|
const chain = resolveChain();
|
|
8205
|
-
return [chain.worker, ...chain.fallback].map(
|
|
8206
|
-
const backend = getBackend(t.backend);
|
|
8207
|
-
const model = (t.model ?? "").split("#")[0];
|
|
8208
|
-
const price = workerPrice(t.backend, model);
|
|
8209
|
-
return {
|
|
8210
|
-
worker: model ? `${t.backend}:${model}` : t.backend,
|
|
8211
|
-
reportsCost: backend.capabilities.reportsCost,
|
|
8212
|
-
price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
|
|
8213
|
-
};
|
|
8214
|
-
});
|
|
8380
|
+
return [chain.worker, ...chain.fallback].map(priced);
|
|
8215
8381
|
} catch {
|
|
8216
8382
|
return [];
|
|
8217
8383
|
}
|
|
8218
8384
|
})();
|
|
8219
|
-
|
|
8385
|
+
const primary = primaryPrice();
|
|
8386
|
+
if (has(p, "json")) return console.log(JSON.stringify({ ...st, primary, workers: used }, null, 2)), 0;
|
|
8220
8387
|
console.log(`price feed: ${st.enabled ? "on" : 'off ("priceFeed": true in the config turns it on: Pitroom then fetches one public file, see PRIVACY.md)'}`);
|
|
8221
8388
|
console.log(`source: ${st.url}${st.enabled ? `, refreshed after a run when older than ${st.hours} h` : ""}`);
|
|
8222
8389
|
console.log(st.models ? `catalog: ${st.models} priced models, updated ${st.ageHours < 1 ? "within the hour" : `${st.ageHours.toFixed(1)} h ago`}` : "catalog: none yet (pitroom prices --refresh fetches it)");
|
|
8223
8390
|
if (st.lastFailure) console.log(`last try: failed: ${st.lastFailure.replace(/^\S+ /, "")}`);
|
|
8391
|
+
console.log(`primary: ${primary.name}: in ${primary.input} \xB7 out ${primary.output} \xB7 cached ${primary.cachedInput} USD/1M (what the savings are measured against)`);
|
|
8224
8392
|
if (used.length) {
|
|
8225
|
-
console.log("\nthe workers in use:");
|
|
8393
|
+
console.log(asked ? "\nthe workers asked about:" : "\nthe workers in use:");
|
|
8226
8394
|
for (const u of used) {
|
|
8227
8395
|
const what2 = u.reportsCost ? "reports its own cost" : u.price ? `in ${u.price.input} \xB7 out ${u.price.output} \xB7 cached ${u.price.cachedInput} USD/1M (${u.price.source === "catalog" ? `price catalog ${catalogLabel()}` : "your workerPrices"})` : "no price: its runs count as free in the savings";
|
|
8228
|
-
console.log(` ${u.worker.padEnd(36)} ${what2}`);
|
|
8396
|
+
console.log(` ${u.worker.padEnd(Math.max(36, ...used.map((x) => x.worker.length)))} ${what2}`);
|
|
8229
8397
|
}
|
|
8230
8398
|
}
|
|
8231
8399
|
return 0;
|
|
@@ -8397,6 +8565,7 @@ function cmdHistory(p) {
|
|
|
8397
8565
|
return 0;
|
|
8398
8566
|
}
|
|
8399
8567
|
importRuns();
|
|
8568
|
+
settleLedger();
|
|
8400
8569
|
const since2 = sinceMs(flag(p, "since"));
|
|
8401
8570
|
if (sub === "stats") {
|
|
8402
8571
|
const s = historyStats(since2);
|
|
@@ -8416,14 +8585,16 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
|
|
|
8416
8585
|
const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since2, limit });
|
|
8417
8586
|
if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
|
|
8418
8587
|
if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
|
|
8419
|
-
const
|
|
8420
|
-
const
|
|
8588
|
+
const cost = (r) => r.cost === void 0 ? "-" : r.cost === 0 ? "free" : `${r.costEstimated ? "~" : ""}${usd(r.cost)}`;
|
|
8589
|
+
const body = rows.map((r) => [r.id, when(r.startedAt), r.verifyFailed ? "verify failed" : r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), cost(r), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
|
|
8590
|
+
const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "COST", "TASK"];
|
|
8421
8591
|
const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
|
|
8422
8592
|
const fmt = (r, header = false) => r.map((c, i) => {
|
|
8423
8593
|
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
8424
8594
|
return header ? dim(cell2) : i === 2 ? stateColour(r[2], cell2) : cell2;
|
|
8425
8595
|
}).join(" ");
|
|
8426
8596
|
console.log([fmt(head, true), ...body.map((r) => fmt(r))].join("\n"));
|
|
8597
|
+
if (rows.some((r) => r.costEstimated)) console.log(dim("\n~ estimated from a price: the worker's CLI reported no cost"));
|
|
8427
8598
|
if (total > rows.length) console.log(`
|
|
8428
8599
|
${rows.length} of ${total} \xB7 --limit N for more \xB7 pitroom show <run> for one`);
|
|
8429
8600
|
return 0;
|
|
@@ -8548,19 +8719,21 @@ function cmdInit(p) {
|
|
|
8548
8719
|
}
|
|
8549
8720
|
function cmdSavings(p) {
|
|
8550
8721
|
const since2 = flag(p, "since") ?? "all";
|
|
8722
|
+
settleLedger();
|
|
8551
8723
|
const t = totals(readLedger(sinceMs(since2)));
|
|
8552
8724
|
if (has(p, "models")) {
|
|
8553
8725
|
const by = /* @__PURE__ */ new Map();
|
|
8554
8726
|
for (const e of readLedger(sinceMs(since2))) {
|
|
8555
8727
|
const key = `${e.backend ?? "?"} ${e.model ?? "(default model)"}`;
|
|
8556
|
-
const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, saved: 0 };
|
|
8557
|
-
by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, saved: a.saved + e.saved });
|
|
8728
|
+
const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, estimated: false, saved: 0 };
|
|
8729
|
+
by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, estimated: a.estimated || !!e.costEstimated, saved: a.saved + e.saved });
|
|
8558
8730
|
}
|
|
8559
8731
|
const width = Math.max(12, ...[...by.keys()].map((k) => k.length));
|
|
8560
8732
|
console.log(`${"worker model".padEnd(width)} ${"runs".padStart(5)} ${"processed".padStart(10)} ${"returned".padStart(9)} ${"cost".padStart(8)} ${"saved".padStart(8)}`);
|
|
8561
8733
|
for (const [key, a] of [...by].sort((x, y) => y[1].runs - x[1].runs)) {
|
|
8562
|
-
console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${usd(a.cost)
|
|
8734
|
+
console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${`${a.estimated ? "~" : ""}${usd(a.cost)}`.padStart(8)} ${usd(a.saved).padStart(8)}`);
|
|
8563
8735
|
}
|
|
8736
|
+
if ([...by.values()].some((a) => a.estimated)) console.log(dim("\n~ the cost is estimated from a price, in part or in all: the worker's CLI reported none"));
|
|
8564
8737
|
return 0;
|
|
8565
8738
|
}
|
|
8566
8739
|
const period = since2 === "all" ? "all time" : `last ${since2.replace("d", " days")}`;
|
|
@@ -9053,7 +9226,7 @@ Usage
|
|
|
9053
9226
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
9054
9227
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
9055
9228
|
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
9056
|
-
pitroom prices [--refresh] [--json] the price catalog (config "priceFeed") and the price each worker in use gets
|
|
9229
|
+
pitroom prices [WORKER\u2026] [--refresh] [--json] the price catalog (config "priceFeed"), the primary's price and the price each worker in use (or named) gets
|
|
9057
9230
|
pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
|
|
9058
9231
|
--clear tries them again
|
|
9059
9232
|
pitroom mcp [-d DIR] [--http [--port N]]
|