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/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|usage limit|limit reached|quota|capacity/i.test(message)) return "rate-limited";
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|usage limit|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
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 (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
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) => `@echo off\r
2976
- set "MSYS2_ARG_CONV_EXCL=*"\r
2977
- "${sh}" "%~dp0git" %*\r
2978
- exit /b %errorlevel%\r
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 cooldownMs(message) {
3137
- const told = retryAfterMs(message);
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
- return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
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
- return candidates.find((c) => !sameTarget(parseTarget(c, def), ran));
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 || meta.auditOf) return;
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.total,
3704
+ tokens: meta.usage?.total ?? 0,
3628
3705
  returned: meta.returnedTokens ?? 0,
3629
3706
  workerCost: workerCostOf(meta.usage) ?? 0,
3630
- ...meta.usage.cost === void 0 && meta.usage.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
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
- return [JSON.parse(l)];
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
- function workerRecord(backend, model, sinceMs2) {
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.24.1" : "0.0.0-dev";
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 ranked = rankChain([worker, ...fallback], (t) => workerRecord(t.backend, t.model, since2));
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
- (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
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 ?? pickAuditor(meta);
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
- // never fall back to the worker being audited
5452
- noFallback: true,
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.task, !!meta.parent, !!meta.auditOf),
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
- return f && f.kind !== "other" ? f : void 0;
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 WEEK_MS2 = 7 * 24 * 3600 * 1e3;
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
- saved: totals(readLedger(Date.now() - WEEK_MS2)).saved,
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 worker = flag(p, "worker") ?? (flag(p, "tier") ? void 0 : pickAuditor(m));
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: true,
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 used = (() => {
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((t) => {
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
- if (has(p, "json")) return console.log(JSON.stringify({ ...st, workers: used }, null, 2)), 0;
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 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), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
8420
- const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
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).padStart(8)} ${usd(a.saved).padStart(8)}`);
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]]