switchroom 0.19.1 → 0.19.3

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.
Files changed (80) hide show
  1. package/dist/agent-scheduler/index.js +31 -1
  2. package/dist/auth-broker/index.js +565 -48
  3. package/dist/cli/autoaccept-poll.js +31 -1
  4. package/dist/cli/drive-write-pretool.mjs +32 -2
  5. package/dist/cli/ms-365-write-pretool.mjs +32 -2
  6. package/dist/cli/switchroom.js +1148 -274
  7. package/dist/host-control/main.js +3 -3
  8. package/dist/vault/approvals/kernel-server.js +2 -2
  9. package/dist/vault/broker/server.js +2 -2
  10. package/package.json +3 -2
  11. package/profiles/_base/start.sh.hbs +1 -0
  12. package/profiles/default/CLAUDE.md.hbs +8 -0
  13. package/skills/mental-model-curator/SKILL.md +68 -2
  14. package/skills/switchroom-cli/SKILL.md +25 -0
  15. package/telegram-plugin/auth-snapshot-format.ts +143 -12
  16. package/telegram-plugin/dist/bridge/bridge.js +8 -2
  17. package/telegram-plugin/dist/gateway/gateway.js +1427 -689
  18. package/telegram-plugin/dist/server.js +8 -2
  19. package/telegram-plugin/external-spend.ts +135 -0
  20. package/telegram-plugin/flushed-turn-supersede.ts +117 -13
  21. package/telegram-plugin/gateway/auth-add-flow.ts +215 -6
  22. package/telegram-plugin/gateway/auth-command.ts +138 -5
  23. package/telegram-plugin/gateway/gateway.ts +141 -158
  24. package/telegram-plugin/gateway/inbound-interceptors.ts +13 -3
  25. package/telegram-plugin/gateway/model-command.ts +309 -1
  26. package/telegram-plugin/gateway/narrative-lane.ts +23 -9
  27. package/telegram-plugin/gateway/outbound-send-path.ts +68 -15
  28. package/telegram-plugin/gateway/session-model-source.ts +90 -10
  29. package/telegram-plugin/gateway/status-pin-store.ts +64 -4
  30. package/telegram-plugin/gateway/stream-render.ts +22 -5
  31. package/telegram-plugin/gateway/usage-mask.ts +29 -0
  32. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +19 -2
  33. package/telegram-plugin/quota-bar-format.ts +78 -12
  34. package/telegram-plugin/quota-check.ts +17 -2
  35. package/telegram-plugin/reply-owner-resolve.ts +76 -11
  36. package/telegram-plugin/session-tail.ts +27 -3
  37. package/telegram-plugin/tests/activity-card-wiring.test.ts +47 -0
  38. package/telegram-plugin/tests/auth-add-flow.test.ts +367 -5
  39. package/telegram-plugin/tests/auth-snapshot-format.test.ts +41 -0
  40. package/telegram-plugin/tests/external-spend.test.ts +168 -0
  41. package/telegram-plugin/tests/flushed-turn-supersede.test.ts +117 -0
  42. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +219 -29
  43. package/telegram-plugin/tests/model-command.test.ts +220 -0
  44. package/telegram-plugin/tests/quota-bar-format.test.ts +43 -0
  45. package/telegram-plugin/tests/quota-check.test.ts +57 -0
  46. package/telegram-plugin/tests/reply-owner-resolve.test.ts +257 -13
  47. package/telegram-plugin/tests/send-reply-golden.test.ts +154 -0
  48. package/telegram-plugin/tests/session-model-source.test.ts +142 -0
  49. package/telegram-plugin/tests/session-tail-first-attach.test.ts +115 -2
  50. package/telegram-plugin/tests/status-pin-store.test.ts +198 -0
  51. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +50 -0
  52. package/telegram-plugin/tests/usage-footer-freshness.test.ts +141 -0
  53. package/telegram-plugin/tests/usage-mask.test.ts +35 -0
  54. package/telegram-plugin/tests/worker-feed-dispatch.test.ts +27 -0
  55. package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +131 -1
  56. package/vendor/hindsight-memory/CHANGELOG.md +102 -0
  57. package/vendor/hindsight-memory/README.md +2 -1
  58. package/vendor/hindsight-memory/hooks/hooks.json +12 -0
  59. package/vendor/hindsight-memory/scripts/directive_verify.py +100 -3
  60. package/vendor/hindsight-memory/scripts/lib/config.py +150 -1
  61. package/vendor/hindsight-memory/scripts/lib/content.py +55 -5
  62. package/vendor/hindsight-memory/scripts/lib/directives.py +152 -15
  63. package/vendor/hindsight-memory/scripts/lib/parallel_recall.py +142 -0
  64. package/vendor/hindsight-memory/scripts/lib/state.py +31 -0
  65. package/vendor/hindsight-memory/scripts/recall.py +789 -143
  66. package/vendor/hindsight-memory/scripts/reconcile_tail.py +22 -1
  67. package/vendor/hindsight-memory/scripts/retain.py +71 -2
  68. package/vendor/hindsight-memory/scripts/subagent_retain.py +501 -0
  69. package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +169 -0
  70. package/vendor/hindsight-memory/scripts/tests/test_directives.py +177 -0
  71. package/vendor/hindsight-memory/scripts/tests/test_lesson_tagging.py +200 -0
  72. package/vendor/hindsight-memory/scripts/tests/test_recall_context_turns_default.py +200 -0
  73. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +477 -0
  74. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +51 -0
  75. package/vendor/hindsight-memory/scripts/tests/test_recall_parallel_deadline.py +409 -0
  76. package/vendor/hindsight-memory/scripts/tests/test_recall_tag_weights.py +96 -0
  77. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +413 -0
  78. package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +49 -0
  79. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +439 -0
  80. package/vendor/hindsight-memory/settings.json +3 -1
@@ -7012,14 +7012,27 @@ var require_dist = __commonJS((exports) => {
7012
7012
 
7013
7013
  // src/util/atomic.ts
7014
7014
  import { randomBytes } from "node:crypto";
7015
- import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
7016
- function atomicWriteFileSync(destPath, contents, mode = 384) {
7015
+ import { closeSync, constants, fchmodSync, fchownSync, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
7016
+ function atomicWriteFileSync(destPath, contents, modeOrOpts = 384) {
7017
+ const opts = typeof modeOrOpts === "number" ? { mode: modeOrOpts } : modeOrOpts;
7018
+ const mode = opts.mode ?? 384;
7017
7019
  const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
7018
7020
  const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
7019
7021
  let fd = null;
7020
7022
  try {
7021
7023
  fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
7022
7024
  writeSync(fd, buf, 0, buf.length, 0);
7025
+ fchmodSync(fd, mode);
7026
+ if (opts.uid !== undefined) {
7027
+ try {
7028
+ fchownSync(fd, opts.uid, opts.gid ?? opts.uid);
7029
+ } catch (chownErr) {
7030
+ if (opts.onChownError)
7031
+ opts.onChownError(chownErr);
7032
+ else
7033
+ throw chownErr;
7034
+ }
7035
+ }
7023
7036
  fsyncSync(fd);
7024
7037
  closeSync(fd);
7025
7038
  fd = null;
@@ -18221,9 +18234,10 @@ import {
18221
18234
  chmodSync,
18222
18235
  chownSync as chownSync2,
18223
18236
  existsSync as existsSync8,
18224
- lstatSync,
18237
+ lstatSync as lstatSync2,
18225
18238
  mkdirSync as mkdirSync5,
18226
18239
  readFileSync as readFileSync7,
18240
+ realpathSync as realpathSync3,
18227
18241
  renameSync as renameSync3,
18228
18242
  rmSync as rmSync5,
18229
18243
  statSync as statSync6,
@@ -18233,7 +18247,7 @@ import {
18233
18247
  import { closeSync as closeSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
18234
18248
  import * as constants2 from "node:constants";
18235
18249
  import { createHash as createHash2 } from "node:crypto";
18236
- import { dirname as dirname3, join as join8, resolve as resolve9 } from "node:path";
18250
+ import { basename as basename2, dirname as dirname3, join as join8, resolve as resolve9 } from "node:path";
18237
18251
 
18238
18252
  // src/agents/scaffold.ts
18239
18253
  import { join as join4, resolve as resolve5 } from "node:path";
@@ -18343,7 +18357,7 @@ var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.
18343
18357
  var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
18344
18358
 
18345
18359
  // src/memory/hindsight.ts
18346
- var DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
18360
+ var DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise, " + "including in-flight workflow/process narration (a sub-task started, " + "paused, or is still running) — only retain the outcome once a task " + "actually completes or a decision is made.";
18347
18361
  var PROFILE_MEMORY_DEFAULTS = {
18348
18362
  "health-coach": {
18349
18363
  disposition: { skepticism: 2, literalism: 2, empathy: 5 },
@@ -19198,6 +19212,187 @@ function bestPremiumHeadroomAccount(ledger, candidates, now) {
19198
19212
  // src/auth/broker/server.ts
19199
19213
  init_atomic();
19200
19214
 
19215
+ // src/litellm/external-spend.ts
19216
+ var EXTERNAL_SPEND_TOP_N = 3;
19217
+ var DEFAULT_LITELLM_BASE = "http://127.0.0.1:4010";
19218
+ var EXTERNAL_SPEND_CACHE_TTL_MS = 90000;
19219
+ var EXTERNAL_SPEND_FETCH_TIMEOUT_MS = 5000;
19220
+ var LITELLM_MASTER_KEY_STATE_BASENAME = "litellm-master-key";
19221
+ var BARE_EXTERNAL_NEEDLES = [
19222
+ "gpt-oss",
19223
+ "grok",
19224
+ "gemini",
19225
+ "deepseek",
19226
+ "kimi",
19227
+ "glm-",
19228
+ "qwen",
19229
+ "llama"
19230
+ ];
19231
+ function isExternalModel(model) {
19232
+ const m = model.trim().toLowerCase();
19233
+ if (!m)
19234
+ return false;
19235
+ if (m.startsWith("claude"))
19236
+ return false;
19237
+ if (m.includes("anthropic/claude"))
19238
+ return false;
19239
+ if (m.startsWith("openrouter/"))
19240
+ return true;
19241
+ if (m.startsWith("sr-"))
19242
+ return true;
19243
+ for (const needle of BARE_EXTERNAL_NEEDLES) {
19244
+ if (m.includes(needle))
19245
+ return true;
19246
+ }
19247
+ return false;
19248
+ }
19249
+ function shortModelLabel(model) {
19250
+ let m = model.trim();
19251
+ if (m.toLowerCase().startsWith("openrouter/")) {
19252
+ m = m.slice("openrouter/".length);
19253
+ }
19254
+ const parts = m.split("/").filter(Boolean);
19255
+ if (parts.length >= 2) {
19256
+ m = parts[parts.length - 1];
19257
+ }
19258
+ if (m.toLowerCase().startsWith("sr-")) {
19259
+ m = m.slice(3);
19260
+ }
19261
+ return m || model.trim();
19262
+ }
19263
+ function utcDateString(d) {
19264
+ return d.toISOString().slice(0, 10);
19265
+ }
19266
+ function addUtcDays(dateStr, days) {
19267
+ const [y, m, d] = dateStr.split("-").map(Number);
19268
+ const dt = new Date(Date.UTC(y, m - 1, d));
19269
+ dt.setUTCDate(dt.getUTCDate() + days);
19270
+ return utcDateString(dt);
19271
+ }
19272
+ function rowDay(row) {
19273
+ const st = row.startTime;
19274
+ if (!st || typeof st !== "string")
19275
+ return null;
19276
+ return st.length >= 10 ? st.slice(0, 10) : null;
19277
+ }
19278
+ function externalModelsFromRow(row) {
19279
+ const out = {};
19280
+ const models = row.models ?? {};
19281
+ for (const [name, raw] of Object.entries(models)) {
19282
+ if (!isExternalModel(name))
19283
+ continue;
19284
+ const n = typeof raw === "number" ? raw : Number(raw);
19285
+ if (!Number.isFinite(n) || n === 0)
19286
+ continue;
19287
+ out[name] = (out[name] ?? 0) + n;
19288
+ }
19289
+ return out;
19290
+ }
19291
+ function summarizeExternalSpend(days, now = new Date) {
19292
+ const today = utcDateString(now);
19293
+ const start7 = addUtcDays(today, -6);
19294
+ let day24hUsd = 0;
19295
+ let day7dUsd = 0;
19296
+ const byModel = {};
19297
+ for (const row of days) {
19298
+ const day = rowDay(row);
19299
+ if (!day)
19300
+ continue;
19301
+ if (day < start7 || day > today)
19302
+ continue;
19303
+ const ext = externalModelsFromRow(row);
19304
+ let rowSum = 0;
19305
+ for (const [name, usd] of Object.entries(ext)) {
19306
+ rowSum += usd;
19307
+ byModel[name] = (byModel[name] ?? 0) + usd;
19308
+ }
19309
+ day7dUsd += rowSum;
19310
+ if (day === today)
19311
+ day24hUsd += rowSum;
19312
+ }
19313
+ const top = Object.entries(byModel).filter(([, usd]) => usd > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, EXTERNAL_SPEND_TOP_N).map(([name, usd]) => ({ label: shortModelLabel(name), usd }));
19314
+ return { day24hUsd, day7dUsd, top };
19315
+ }
19316
+ function normalizeSpendLogRows(body) {
19317
+ if (Array.isArray(body))
19318
+ return body;
19319
+ if (body && typeof body === "object") {
19320
+ const data = body.data;
19321
+ if (Array.isArray(data))
19322
+ return data;
19323
+ }
19324
+ return [];
19325
+ }
19326
+ async function fetchAndSummarizeExternalSpend(opts) {
19327
+ const now = opts.now ?? new Date;
19328
+ const base = (opts.baseUrl ?? DEFAULT_LITELLM_BASE).replace(/\/+$/, "");
19329
+ const today = utcDateString(now);
19330
+ const start = addUtcDays(today, -6);
19331
+ const end = addUtcDays(today, 1);
19332
+ const url = `${base}/spend/logs?start_date=${encodeURIComponent(start)}` + `&end_date=${encodeURIComponent(end)}`;
19333
+ const fetchImpl = opts.fetchImpl ?? fetch;
19334
+ const timeoutMs = opts.timeoutMs ?? EXTERNAL_SPEND_FETCH_TIMEOUT_MS;
19335
+ const ac = new AbortController;
19336
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
19337
+ try {
19338
+ const res = await fetchImpl(url, {
19339
+ method: "GET",
19340
+ headers: {
19341
+ Authorization: `Bearer ${opts.adminKey}`,
19342
+ Accept: "application/json"
19343
+ },
19344
+ signal: ac.signal
19345
+ });
19346
+ if (!res.ok)
19347
+ return null;
19348
+ let body;
19349
+ try {
19350
+ body = await res.json();
19351
+ } catch {
19352
+ return null;
19353
+ }
19354
+ return summarizeExternalSpend(normalizeSpendLogRows(body), now);
19355
+ } catch {
19356
+ return null;
19357
+ } finally {
19358
+ clearTimeout(timer);
19359
+ }
19360
+ }
19361
+
19362
+ // src/auth/broker/mirror-write.ts
19363
+ init_atomic();
19364
+ import { lstatSync, realpathSync as realpathSync2 } from "node:fs";
19365
+ function safeMirrorWrite(input) {
19366
+ let realParent;
19367
+ try {
19368
+ realParent = realpathSync2(input.parentDir);
19369
+ } catch (err) {
19370
+ input.logErr(`mirror parent realpath failed: ${err.message}`);
19371
+ return false;
19372
+ }
19373
+ if (realParent !== input.expectedRealParent) {
19374
+ input.refuse("parentDir", "realpath-mismatch");
19375
+ return false;
19376
+ }
19377
+ try {
19378
+ atomicWriteFileSync(input.targetPath, input.content, {
19379
+ mode: input.mode ?? 384,
19380
+ uid: input.uid,
19381
+ gid: input.gid ?? input.uid,
19382
+ onChownError: input.onChownError
19383
+ });
19384
+ return true;
19385
+ } catch (err) {
19386
+ const e = err;
19387
+ if (e.code === "ELOOP" || e.code === "EEXIST") {
19388
+ input.refuse("targetPath", `open-${e.code}`);
19389
+ return false;
19390
+ }
19391
+ input.logErr(`mirror write failed: ${e.message}`);
19392
+ return false;
19393
+ }
19394
+ }
19395
+
19201
19396
  // src/auth/account-store.ts
19202
19397
  init_atomic();
19203
19398
  import {
@@ -19998,14 +20193,20 @@ class ProviderRegistry {
19998
20193
  }
19999
20194
 
20000
20195
  // src/auth/broker/peercred.ts
20001
- var AUTH_BROKER_SOCKET_PATH_RE = /^\/run\/switchroom\/auth-broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
20196
+ var AUTH_BROKER_ROOT = "/run/switchroom/auth-broker";
20197
+ function authBrokerSocketRe(socketRoot) {
20198
+ const escaped = socketRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20199
+ return new RegExp(`^${escaped}/([a-zA-Z0-9][a-zA-Z0-9_-]*)/sock$`);
20200
+ }
20201
+ var AUTH_BROKER_SOCKET_PATH_RE = authBrokerSocketRe(AUTH_BROKER_ROOT);
20002
20202
  var AUTH_BROKER_MAX_NAME_LEN = 63;
20003
20203
  var RESERVED_NAMES = new Set(["operator"]);
20004
- function socketPathToName(socketPath) {
20005
- return matchSocketName(socketPath, [AUTH_BROKER_SOCKET_PATH_RE], AUTH_BROKER_MAX_NAME_LEN);
20204
+ function socketPathToName(socketPath, socketRoot) {
20205
+ const pattern = socketRoot === undefined || socketRoot === AUTH_BROKER_ROOT ? AUTH_BROKER_SOCKET_PATH_RE : authBrokerSocketRe(socketRoot);
20206
+ return matchSocketName(socketPath, [pattern], AUTH_BROKER_MAX_NAME_LEN);
20006
20207
  }
20007
- function classify(socketPath, config) {
20008
- const name = socketPathToName(socketPath);
20208
+ function classify(socketPath, config, socketRoot) {
20209
+ const name = socketPathToName(socketPath, socketRoot);
20009
20210
  if (!name)
20010
20211
  return null;
20011
20212
  if (name === "operator")
@@ -20169,6 +20370,12 @@ var ClaimNotificationRequestSchema = exports_external.object({
20169
20370
  key: exports_external.string().min(1).max(512),
20170
20371
  windowMs: exports_external.number().int().positive().max(86400000)
20171
20372
  });
20373
+ var GetExternalSpendRequestSchema = exports_external.object({
20374
+ v: exports_external.literal(PROTOCOL_VERSION),
20375
+ op: exports_external.literal("get-external-spend"),
20376
+ id: exports_external.string().min(1),
20377
+ forceLive: exports_external.boolean().optional()
20378
+ });
20172
20379
  var RequestSchema2 = exports_external.discriminatedUnion("op", [
20173
20380
  GetCredentialsRequestSchema,
20174
20381
  ListStateRequestSchema,
@@ -20182,7 +20389,8 @@ var RequestSchema2 = exports_external.discriminatedUnion("op", [
20182
20389
  ListGoogleAccountsRequestSchema,
20183
20390
  ListMicrosoftAccountsRequestSchema,
20184
20391
  ProbeQuotaRequestSchema,
20185
- ClaimNotificationRequestSchema
20392
+ ClaimNotificationRequestSchema,
20393
+ GetExternalSpendRequestSchema
20186
20394
  ]);
20187
20395
  var GetCredentialsDataSchema = exports_external.object({
20188
20396
  account: exports_external.string(),
@@ -20193,6 +20401,8 @@ var AccountStateSchema = exports_external.object({
20193
20401
  label: exports_external.string(),
20194
20402
  expiresAt: exports_external.number().optional(),
20195
20403
  exhausted: exports_external.boolean(),
20404
+ in_service: exports_external.boolean().optional(),
20405
+ entitlement_blocked: exports_external.boolean().optional(),
20196
20406
  exhausted_until: exports_external.number().optional(),
20197
20407
  throttled_until: exports_external.number().optional(),
20198
20408
  threshold_violations: exports_external.number().int().nonnegative().optional(),
@@ -20249,6 +20459,18 @@ var SetOverrideDataSchema = exports_external.object({
20249
20459
  var ClaimNotificationDataSchema = exports_external.object({
20250
20460
  granted: exports_external.boolean()
20251
20461
  });
20462
+ var GetExternalSpendDataSchema = exports_external.object({
20463
+ available: exports_external.boolean(),
20464
+ day24hUsd: exports_external.number().optional(),
20465
+ day7dUsd: exports_external.number().optional(),
20466
+ top: exports_external.array(exports_external.object({
20467
+ label: exports_external.string(),
20468
+ usd: exports_external.number()
20469
+ })).optional(),
20470
+ capturedAtMs: exports_external.number().int().nonnegative().optional(),
20471
+ served: exports_external.enum(["live", "cache"]).optional(),
20472
+ reason: exports_external.string().optional()
20473
+ });
20252
20474
  var GoogleAccountStateSchema = exports_external.object({
20253
20475
  account: exports_external.string(),
20254
20476
  expiresAt: exports_external.number(),
@@ -20329,13 +20551,14 @@ function encodeError(id, code, message) {
20329
20551
  }
20330
20552
 
20331
20553
  // src/auth/broker/server.ts
20332
- var AUTH_BROKER_ROOT = "/run/switchroom/auth-broker";
20554
+ var AUTH_BROKER_ROOT2 = "/run/switchroom/auth-broker";
20333
20555
  var REFRESH_TICK_INTERVAL_MS = 60 * 1000;
20334
20556
  var MARK_EXHAUSTED_DEFAULT_MS = 5 * 60 * 60 * 1000;
20335
20557
  var MARK_THROTTLED_MAX_MS = 30 * 60 * 1000;
20336
20558
  var THROTTLE_ESCALATION_HITS = 3;
20337
20559
  var THROTTLE_ESCALATION_WINDOW_MS = 10 * 60 * 1000;
20338
20560
  var MARK_THROTTLED_MIN_INTERVAL_MS = 5 * 1000;
20561
+ var MARK_CORROBORATION_PROBE_DELAY_MS = 90 * 1000;
20339
20562
  var AUDIT_ROTATE_BYTES = 10 * 1024 * 1024;
20340
20563
  var AUDIT_KEEP = 5;
20341
20564
  var AUDIT_LINE_MAX = 4000;
@@ -20412,6 +20635,8 @@ class AuthBroker {
20412
20635
  refreshTimer = null;
20413
20636
  consumerProbeTimer = null;
20414
20637
  fleetProbeTimer = null;
20638
+ markCorroborationTimers = new Set;
20639
+ markCorroborationDelayMs = MARK_CORROBORATION_PROBE_DELAY_MS;
20415
20640
  fetchQuotaImpl;
20416
20641
  stateDir;
20417
20642
  socketRoot;
@@ -20422,6 +20647,8 @@ class AuthBroker {
20422
20647
  providers;
20423
20648
  quota = {};
20424
20649
  lastQuotaCache = {};
20650
+ externalSpendCache = null;
20651
+ externalSpendInFlight = null;
20425
20652
  probeInFlight = new Map;
20426
20653
  shaIndex = {};
20427
20654
  thresholdViolations = {};
@@ -20446,8 +20673,9 @@ class AuthBroker {
20446
20673
  this.operatorUid = opts.operatorUid;
20447
20674
  this.fetcher = opts.fetcher;
20448
20675
  this.fetchQuotaImpl = opts._testFetchQuota ?? fetchQuota;
20676
+ this.markCorroborationDelayMs = opts._testCorroborationDelayMs ?? MARK_CORROBORATION_PROBE_DELAY_MS;
20449
20677
  this.stateDir = opts.stateDir ?? resolve9(this.homeRoot(), ".switchroom", "state", "auth-broker");
20450
- this.socketRoot = opts.socketRoot ?? AUTH_BROKER_ROOT;
20678
+ this.socketRoot = opts.socketRoot ?? AUTH_BROKER_ROOT2;
20451
20679
  this.providers = new ProviderRegistry;
20452
20680
  this.providers.register(new AnthropicProvider);
20453
20681
  const googleClientId = config.google_workspace?.google_client_id;
@@ -20547,10 +20775,18 @@ class AuthBroker {
20547
20775
  clearInterval(this.fleetProbeTimer);
20548
20776
  this.fleetProbeTimer = null;
20549
20777
  }
20778
+ for (const timer of this.markCorroborationTimers)
20779
+ clearTimeout(timer);
20780
+ this.markCorroborationTimers.clear();
20550
20781
  for (const [sock, lis] of this.listeners) {
20551
20782
  try {
20552
20783
  lis.server.close();
20553
20784
  } catch {}
20785
+ for (const s of lis.sockets) {
20786
+ try {
20787
+ s.destroy();
20788
+ } catch {}
20789
+ }
20554
20790
  try {
20555
20791
  if (existsSync8(sock))
20556
20792
  unlinkSync(sock);
@@ -20579,7 +20815,12 @@ class AuthBroker {
20579
20815
  if (this.operatorUid !== undefined) {
20580
20816
  wanted.add(this.operatorSocketPath());
20581
20817
  }
20818
+ const prevShape = configToShape(prev);
20819
+ const nextShape = configToShape(config);
20582
20820
  for (const [sock, lis] of [...this.listeners]) {
20821
+ const before = classify(lis.socketPath, prevShape, this.socketRoot);
20822
+ const after = classify(lis.socketPath, nextShape, this.socketRoot);
20823
+ const identityChanged = JSON.stringify(before ?? null) !== JSON.stringify(after ?? null);
20583
20824
  if (!wanted.has(sock)) {
20584
20825
  try {
20585
20826
  lis.server.close();
@@ -20588,7 +20829,18 @@ class AuthBroker {
20588
20829
  if (existsSync8(sock))
20589
20830
  unlinkSync(sock);
20590
20831
  } catch {}
20832
+ for (const s of lis.sockets) {
20833
+ try {
20834
+ s.destroy();
20835
+ } catch {}
20836
+ }
20591
20837
  this.listeners.delete(sock);
20838
+ } else if (identityChanged) {
20839
+ for (const s of lis.sockets) {
20840
+ try {
20841
+ s.destroy();
20842
+ } catch {}
20843
+ }
20592
20844
  }
20593
20845
  }
20594
20846
  for (const name of Object.keys(config.agents ?? {})) {
@@ -20676,12 +20928,24 @@ class AuthBroker {
20676
20928
  try {
20677
20929
  chownSync2(dir, targetUid, targetUid);
20678
20930
  } catch {}
20679
- this.listeners.set(sockPath, { server, identity, socketPath: sockPath });
20931
+ this.listeners.set(sockPath, {
20932
+ server,
20933
+ identity,
20934
+ socketPath: sockPath,
20935
+ sockets: new Set
20936
+ });
20680
20937
  resolveP();
20681
20938
  });
20682
20939
  });
20683
20940
  }
20684
20941
  handleConnection(socket, sockPath, boundIdentity) {
20942
+ const listener = this.listeners.get(sockPath);
20943
+ if (listener) {
20944
+ listener.sockets.add(socket);
20945
+ socket.on("close", () => {
20946
+ listener.sockets.delete(socket);
20947
+ });
20948
+ }
20685
20949
  let buf = "";
20686
20950
  socket.on("data", (chunk) => {
20687
20951
  buf += chunk.toString("utf-8");
@@ -20720,7 +20984,17 @@ class AuthBroker {
20720
20984
  socket.write(encodeError(reqId, "INVALID_ARGS", err.message));
20721
20985
  return;
20722
20986
  }
20723
- const identity = classify(sockPath, configToShape(this.config)) ?? boundIdentity;
20987
+ const identity = classify(sockPath, configToShape(this.config), this.socketRoot);
20988
+ if (!identity) {
20989
+ this.audit({
20990
+ op: "stale-identity-rejected",
20991
+ identity: boundIdentity,
20992
+ ok: false,
20993
+ error: "FORBIDDEN"
20994
+ });
20995
+ socket.end(encodeError(reqId, "FORBIDDEN", "identity no longer configured"));
20996
+ return;
20997
+ }
20724
20998
  try {
20725
20999
  switch (req.op) {
20726
21000
  case "get-credentials": {
@@ -20840,6 +21114,9 @@ class AuthBroker {
20840
21114
  case "claim-notification":
20841
21115
  this.opClaimNotification(socket, reqId, identity, req.key, req.windowMs);
20842
21116
  break;
21117
+ case "get-external-spend":
21118
+ await this.opGetExternalSpend(socket, reqId, identity, req.forceLive);
21119
+ break;
20843
21120
  }
20844
21121
  } catch (err) {
20845
21122
  socket.write(encodeError(reqId, "INTERNAL", err.message));
@@ -21166,6 +21443,7 @@ class AuthBroker {
21166
21443
  }
21167
21444
  async opListState(socket, id, identity) {
21168
21445
  const auth = this.config.auth ?? {};
21446
+ const inService = this.inServiceSet();
21169
21447
  const accounts = listAccounts(this.home).map((label) => {
21170
21448
  const creds = readAccountCredentials(label, this.home);
21171
21449
  const meta = readAccountMeta(label, this.home);
@@ -21176,6 +21454,8 @@ class AuthBroker {
21176
21454
  label,
21177
21455
  expiresAt: creds?.claudeAiOauth?.expiresAt,
21178
21456
  exhausted,
21457
+ in_service: inService.has(label),
21458
+ entitlement_blocked: false,
21179
21459
  exhausted_until: q?.exhausted_until,
21180
21460
  throttled_until: q?.throttled_until,
21181
21461
  threshold_violations: this.thresholdViolations[label] ?? 0,
@@ -21425,6 +21705,14 @@ class AuthBroker {
21425
21705
  set.add(acct);
21426
21706
  return set;
21427
21707
  }
21708
+ inServiceSet() {
21709
+ const set = this.premiumCanarySet();
21710
+ for (const c of this.config.auth?.consumers ?? []) {
21711
+ if (c.account)
21712
+ set.add(c.account);
21713
+ }
21714
+ return set;
21715
+ }
21428
21716
  pinnedAgentAccounts() {
21429
21717
  return new Set(Object.values(this.config.agents ?? {}).map((a) => a.auth?.override).filter((x) => typeof x === "string" && x.length > 0));
21430
21718
  }
@@ -21612,6 +21900,9 @@ class AuthBroker {
21612
21900
  });
21613
21901
  this.quota[account] = { ...this.quota[account], exhausted_until: exhaustedUntil, marked_at: now };
21614
21902
  this.persistQuota();
21903
+ if (exhaustedUntil > now + MARK_EXHAUSTED_DEFAULT_MS) {
21904
+ this.scheduleMarkCorroboration(account, now, identity);
21905
+ }
21615
21906
  const rolledTo = await this.nextHealthyAccountLive(account, this.config.auth?.fallback_order ?? []);
21616
21907
  const rolled = this.fanoutFailoverTo(account, rolledTo);
21617
21908
  this.fanoutToAffectedConsumers(account);
@@ -21628,6 +21919,67 @@ class AuthBroker {
21628
21919
  }
21629
21920
  return { rolled, rolledTo };
21630
21921
  }
21922
+ scheduleMarkCorroboration(account, markedAt, identity) {
21923
+ if (this.markCorroborationDelayMs <= 0)
21924
+ return;
21925
+ const timer = setTimeout(() => {
21926
+ this.markCorroborationTimers.delete(timer);
21927
+ this.corroborateExhaustionMark(account, markedAt, identity).catch((err) => {
21928
+ this.logErr(`mark-corroboration ${account}: ${err.message}`);
21929
+ });
21930
+ }, this.markCorroborationDelayMs);
21931
+ timer.unref?.();
21932
+ this.markCorroborationTimers.add(timer);
21933
+ }
21934
+ async corroborateExhaustionMark(account, markedAt, identity) {
21935
+ const entry = this.quota[account];
21936
+ if (!entry || entry.marked_at !== markedAt) {
21937
+ return { clamped: false, reason: "mark-superseded" };
21938
+ }
21939
+ const until = entry.exhausted_until;
21940
+ const now = this.now();
21941
+ if (until == null || until <= now + MARK_EXHAUSTED_DEFAULT_MS) {
21942
+ return { clamped: false, reason: "not-long" };
21943
+ }
21944
+ const creds = readAccountCredentials(account, this.home);
21945
+ const token = creds?.claudeAiOauth?.accessToken;
21946
+ if (!token)
21947
+ return { clamped: false, reason: "no-token" };
21948
+ let result;
21949
+ try {
21950
+ result = await this.fetchQuotaImpl({ accessToken: token });
21951
+ } catch (err) {
21952
+ this.logErr(`mark-corroboration probe ${account}: ${err.message}`);
21953
+ return { clamped: false, reason: "probe-failed" };
21954
+ }
21955
+ this.cacheQuotaSnapshot(account, result);
21956
+ const snapshot = this.lastQuotaCache[account];
21957
+ const clampedUntil = clampMarkExpiry({
21958
+ proposedUntil: until,
21959
+ now,
21960
+ shortMs: MARK_EXHAUSTED_DEFAULT_MS,
21961
+ snapshot
21962
+ });
21963
+ if (clampedUntil >= until) {
21964
+ return { clamped: false, reason: "not-contradicted" };
21965
+ }
21966
+ const live = this.quota[account];
21967
+ if (!live || live.marked_at !== markedAt || (live.exhausted_until ?? 0) <= clampedUntil) {
21968
+ return { clamped: false, reason: "mark-superseded" };
21969
+ }
21970
+ this.quota[account] = { ...live, exhausted_until: clampedUntil };
21971
+ this.persistQuota();
21972
+ this.audit({
21973
+ op: "mark-corroboration-clamp",
21974
+ identity,
21975
+ account,
21976
+ accountKind: "claude",
21977
+ ok: true
21978
+ });
21979
+ process.stdout.write(`auth-broker: mark-corroboration probe CONTRADICTS ${account} weekly wall — ` + `clamped exhausted_until ${new Date(until).toISOString()} → ` + `${new Date(clampedUntil).toISOString()}
21980
+ `);
21981
+ return { clamped: true, reason: "contradicted", until: clampedUntil };
21982
+ }
21631
21983
  exhaustionLiveCorroborated(account) {
21632
21984
  const snapshot = this.lastQuotaCache[account];
21633
21985
  if (!snapshot || !snapshotFresh(snapshot, this.now(), MARK_EXHAUSTED_DEFAULT_MS))
@@ -21636,6 +21988,105 @@ class AuthBroker {
21636
21988
  return false;
21637
21989
  return !overageLiftsWall(snapshot, this.isOverageAllowed(account));
21638
21990
  }
21991
+ async opGetExternalSpend(socket, id, _identity, forceLive) {
21992
+ const nowMs2 = this.now();
21993
+ const ttl = EXTERNAL_SPEND_CACHE_TTL_MS;
21994
+ const fresh = this.externalSpendCache != null && nowMs2 - this.externalSpendCache.capturedAtMs < ttl;
21995
+ if (forceLive || !fresh) {
21996
+ try {
21997
+ await this.refreshExternalSpend(forceLive === true);
21998
+ } catch (err) {
21999
+ this.logErr(`external-spend refresh failed: ${err?.message ?? err}`);
22000
+ }
22001
+ }
22002
+ if (!this.externalSpendCache) {
22003
+ socket.write(encodeSuccess(id, {
22004
+ available: false,
22005
+ reason: this.resolveLitellmMasterKey() ? "litellm_unreachable" : "master_key_unavailable"
22006
+ }));
22007
+ return;
22008
+ }
22009
+ const recently = nowMs2 - this.externalSpendCache.capturedAtMs < 5000;
22010
+ const outServed = recently && (forceLive || !fresh) ? "live" : "cache";
22011
+ const s = this.externalSpendCache.summary;
22012
+ socket.write(encodeSuccess(id, {
22013
+ available: true,
22014
+ day24hUsd: s.day24hUsd,
22015
+ day7dUsd: s.day7dUsd,
22016
+ top: s.top,
22017
+ capturedAtMs: this.externalSpendCache.capturedAtMs,
22018
+ served: outServed
22019
+ }));
22020
+ }
22021
+ resolveLitellmMasterKey() {
22022
+ if (this.opts._testLitellmMasterKey !== undefined) {
22023
+ const v = this.opts._testLitellmMasterKey;
22024
+ return v && v.trim() ? v.trim() : null;
22025
+ }
22026
+ const envKey = (process.env.SWITCHROOM_LITELLM_ADMIN_KEY ?? process.env.LITELLM_MASTER_KEY ?? "").trim();
22027
+ if (envKey)
22028
+ return envKey;
22029
+ try {
22030
+ const path = join8(this.stateDir, LITELLM_MASTER_KEY_STATE_BASENAME);
22031
+ if (!existsSync8(path))
22032
+ return null;
22033
+ const raw = readFileSync7(path, "utf-8").trim();
22034
+ return raw.length > 0 ? raw : null;
22035
+ } catch {
22036
+ return null;
22037
+ }
22038
+ }
22039
+ resolveLitellmBaseUrl() {
22040
+ const fromCfg = this.config.litellm?.base_url;
22041
+ const raw = (process.env.SWITCHROOM_LITELLM_BASE ?? fromCfg ?? DEFAULT_LITELLM_BASE).trim();
22042
+ return raw.replace(/\/+$/, "") || DEFAULT_LITELLM_BASE;
22043
+ }
22044
+ async refreshExternalSpend(forceLive) {
22045
+ while (this.externalSpendInFlight) {
22046
+ await this.externalSpendInFlight;
22047
+ if (!forceLive)
22048
+ return;
22049
+ const justNow = this.now();
22050
+ if (this.externalSpendCache && justNow - this.externalSpendCache.capturedAtMs < 5000) {
22051
+ return;
22052
+ }
22053
+ }
22054
+ const run = (async () => {
22055
+ const nowMs2 = this.now();
22056
+ if (!forceLive && this.externalSpendCache && nowMs2 - this.externalSpendCache.capturedAtMs < EXTERNAL_SPEND_CACHE_TTL_MS) {
22057
+ return;
22058
+ }
22059
+ const key = this.resolveLitellmMasterKey();
22060
+ if (!key)
22061
+ return;
22062
+ const baseUrl = this.resolveLitellmBaseUrl();
22063
+ const now = new Date(nowMs2);
22064
+ let summary = null;
22065
+ if (this.opts._testFetchExternalSpend) {
22066
+ summary = await this.opts._testFetchExternalSpend({
22067
+ adminKey: key,
22068
+ baseUrl,
22069
+ now,
22070
+ forceLive
22071
+ });
22072
+ } else {
22073
+ summary = await fetchAndSummarizeExternalSpend({
22074
+ adminKey: key,
22075
+ baseUrl,
22076
+ now,
22077
+ timeoutMs: EXTERNAL_SPEND_FETCH_TIMEOUT_MS
22078
+ });
22079
+ }
22080
+ if (!summary)
22081
+ return;
22082
+ this.externalSpendCache = { summary, capturedAtMs: this.now() };
22083
+ this.persistExternalSpendCache();
22084
+ })();
22085
+ this.externalSpendInFlight = run.finally(() => {
22086
+ this.externalSpendInFlight = null;
22087
+ });
22088
+ await this.externalSpendInFlight;
22089
+ }
21639
22090
  opClaimNotification(socket, id, identity, key, windowMs) {
21640
22091
  const now = this.now();
21641
22092
  const prev = this.notificationClaims[key];
@@ -22201,25 +22652,68 @@ class AuthBroker {
22201
22652
  const mirrorDir = consumer.mirror_dir;
22202
22653
  if (!mirrorDir)
22203
22654
  return false;
22204
- const targetPath = join8(mirrorDir, ".credentials.json");
22205
22655
  const credsPath = accountCredentialsPath(label, this.home);
22206
22656
  if (!existsSync8(credsPath))
22207
22657
  return false;
22208
22658
  const mirrorContent = enrichMirrorContent(readFileSync7(credsPath, "utf-8"));
22209
- try {
22210
- mkdirSync5(mirrorDir, { recursive: true, mode: 448 });
22211
- atomicWriteFileSync(targetPath, mirrorContent, 384);
22659
+ const safe = this.resolveConsumerMirrorPathsSafe(consumer.name, mirrorDir);
22660
+ if (!safe)
22661
+ return false;
22662
+ return safeMirrorWrite({
22663
+ parentDir: safe.mirrorDir,
22664
+ expectedRealParent: safe.expectedRealParent,
22665
+ targetPath: safe.targetPath,
22666
+ content: mirrorContent,
22667
+ uid: consumer.uid ?? 0,
22668
+ onChownError: (err) => this.warnCapChownMissing(err),
22669
+ refuse: (component, reason) => this.auditMirrorRefused({ kind: "consumer", name: consumer.name }, component, reason),
22670
+ logErr: (msg) => this.logErr(`consumer-mirror ${consumer.name} <- ${label}: ${msg}`)
22671
+ });
22672
+ }
22673
+ resolveConsumerMirrorPathsSafe(consumerName, mirrorDir) {
22674
+ const identity = { kind: "consumer", name: consumerName };
22675
+ const refuse = (component, reason) => {
22676
+ this.auditMirrorRefused(identity, component, reason);
22677
+ return null;
22678
+ };
22679
+ const parent = dirname3(mirrorDir);
22680
+ const mirrorStat = this.lstatOrNull(mirrorDir);
22681
+ if (mirrorStat) {
22682
+ if (mirrorStat.isSymbolicLink())
22683
+ return refuse("mirror_dir", "is-a-symlink");
22684
+ if (!mirrorStat.isDirectory())
22685
+ return refuse("mirror_dir", "is-not-a-directory");
22686
+ } else {
22687
+ const parentStat = this.lstatOrNull(parent);
22688
+ if (!parentStat)
22689
+ return refuse("mirror_dir-parent", "does-not-exist");
22690
+ if (parentStat.isSymbolicLink())
22691
+ return refuse("mirror_dir-parent", "is-a-symlink");
22692
+ if (!parentStat.isDirectory())
22693
+ return refuse("mirror_dir-parent", "is-not-a-directory");
22212
22694
  try {
22213
- const uid = consumer.uid ?? 0;
22214
- chownSync2(targetPath, uid, uid);
22695
+ mkdirSync5(mirrorDir, { recursive: false, mode: 448 });
22215
22696
  } catch (err) {
22216
- this.warnCapChownMissing(err);
22697
+ this.logErr(`consumer-mirror ${consumerName}: mkdir ${mirrorDir}: ${err.message}`);
22698
+ return null;
22217
22699
  }
22218
- return true;
22700
+ }
22701
+ const targetPath = join8(mirrorDir, ".credentials.json");
22702
+ const targetStat = this.lstatOrNull(targetPath);
22703
+ if (targetStat) {
22704
+ if (targetStat.isSymbolicLink())
22705
+ return refuse(".credentials.json", "is-a-symlink");
22706
+ if (!targetStat.isFile())
22707
+ return refuse(".credentials.json", "is-not-a-regular-file");
22708
+ }
22709
+ let expectedRealParent;
22710
+ try {
22711
+ expectedRealParent = join8(realpathSync3(parent), basename2(mirrorDir));
22219
22712
  } catch (err) {
22220
- this.logErr(`consumer-mirror ${consumer.name} <- ${label}: ${err.message}`);
22221
- return false;
22713
+ this.logErr(`consumer-mirror ${consumerName}: realpath ${parent}: ${err.message}`);
22714
+ return null;
22222
22715
  }
22716
+ return { mirrorDir, targetPath, expectedRealParent };
22223
22717
  }
22224
22718
  fanoutFailoverTo(label, next) {
22225
22719
  const auth = this.config.auth ?? {};
@@ -22274,13 +22768,7 @@ class AuthBroker {
22274
22768
  const claudeDir = join8(agentDir, ".claude");
22275
22769
  const targetPath = join8(claudeDir, ".credentials.json");
22276
22770
  const refuse = (component, reason) => {
22277
- this.logErr(`fanout ${agentName}: REFUSING mirror ${component} ${reason} ` + `(symlink-guard #1393; agent-owned tree may be attacker-poisoned)`);
22278
- this.audit({
22279
- op: "mirror-symlink-refused",
22280
- identity: { kind: "agent", name: agentName, admin: false },
22281
- ok: false,
22282
- error: `${component}:${reason}`
22283
- });
22771
+ this.auditMirrorRefused({ kind: "agent", name: agentName, admin: false }, component, reason);
22284
22772
  return null;
22285
22773
  };
22286
22774
  const agentsStat = this.lstatOrNull(agentsDir);
@@ -22311,15 +22799,26 @@ class AuthBroker {
22311
22799
  if (!targetStat.isFile())
22312
22800
  return refuse(".credentials.json", "is-not-a-regular-file");
22313
22801
  }
22314
- return { agentDir, claudeDir, targetPath };
22802
+ const expectedRealParent = join8(realpathSync3(agentsDir), agentName, ".claude");
22803
+ return { agentDir, claudeDir, targetPath, expectedRealParent };
22315
22804
  }
22316
22805
  lstatOrNull(path) {
22317
22806
  try {
22318
- return lstatSync(path);
22807
+ return lstatSync2(path);
22319
22808
  } catch {
22320
22809
  return null;
22321
22810
  }
22322
22811
  }
22812
+ auditMirrorRefused(identity, component, reason) {
22813
+ const who = identity.kind === "operator" ? "operator" : identity.name;
22814
+ this.logErr(`mirror ${who}: REFUSING mirror — ${component} ${reason} ` + `(symlink-guard #1393; peer-owned tree may be attacker-poisoned)`);
22815
+ this.audit({
22816
+ op: "mirror-symlink-refused",
22817
+ identity,
22818
+ ok: false,
22819
+ error: `${component}:${reason}`
22820
+ });
22821
+ }
22323
22822
  mirrorAccountToAgent(label, agentName) {
22324
22823
  const credsPath = accountCredentialsPath(label, this.home);
22325
22824
  if (!existsSync8(credsPath))
@@ -22328,21 +22827,18 @@ class AuthBroker {
22328
22827
  const safe = this.resolveMirrorPathsSafe(agentName);
22329
22828
  if (!safe)
22330
22829
  return false;
22331
- const { claudeDir, targetPath } = safe;
22830
+ const { claudeDir, targetPath, expectedRealParent } = safe;
22332
22831
  mkdirSync5(claudeDir, { recursive: true, mode: 448 });
22333
- try {
22334
- atomicWriteFileSync(targetPath, mirrorContent, 384);
22335
- try {
22336
- const uid = allocateAgentUid(agentName);
22337
- chownSync2(targetPath, uid, uid);
22338
- } catch (err) {
22339
- this.warnCapChownMissing(err);
22340
- }
22341
- return true;
22342
- } catch (err) {
22343
- this.logErr(`fanout ${agentName} <- ${label}: ${err.message}`);
22344
- return false;
22345
- }
22832
+ return safeMirrorWrite({
22833
+ parentDir: claudeDir,
22834
+ expectedRealParent,
22835
+ targetPath,
22836
+ content: mirrorContent,
22837
+ uid: allocateAgentUid(agentName),
22838
+ onChownError: (err) => this.warnCapChownMissing(err),
22839
+ refuse: (component, reason) => this.auditMirrorRefused({ kind: "agent", name: agentName, admin: false }, component, reason),
22840
+ logErr: (msg) => this.logErr(`fanout ${agentName} <- ${label}: ${msg}`)
22841
+ });
22346
22842
  }
22347
22843
  loadStateFromDisk() {
22348
22844
  this.quota = this.readJson("quota.json") ?? {};
@@ -22354,6 +22850,22 @@ class AuthBroker {
22354
22850
  this.lastTierQuotaCache = this.readJson("last-tier-quota.json") ?? {};
22355
22851
  this.premiumTierAllWalled = this.readJson("premium-tier-all-walled.json") ?? null;
22356
22852
  this.usageLedger = this.readJson("usage-ledger.json") ?? {};
22853
+ {
22854
+ const es = this.readJson("external-spend.json");
22855
+ if (es && es.summary && typeof es.capturedAtMs === "number" && Number.isFinite(es.summary.day24hUsd) && Number.isFinite(es.summary.day7dUsd) && Array.isArray(es.summary.top) && es.summary.top.every((t) => t && typeof t.label === "string" && typeof t.usd === "number" && Number.isFinite(t.usd))) {
22856
+ this.externalSpendCache = {
22857
+ summary: {
22858
+ day24hUsd: es.summary.day24hUsd,
22859
+ day7dUsd: es.summary.day7dUsd,
22860
+ top: es.summary.top.map((t) => ({
22861
+ label: t.label,
22862
+ usd: t.usd
22863
+ }))
22864
+ },
22865
+ capturedAtMs: es.capturedAtMs
22866
+ };
22867
+ }
22868
+ }
22357
22869
  this.applyActiveOverride();
22358
22870
  }
22359
22871
  applyActiveOverride() {
@@ -22407,6 +22919,11 @@ class AuthBroker {
22407
22919
  persistLastQuotaCache() {
22408
22920
  atomicWriteJsonSync(join8(this.stateDir, "last-quota.json"), this.lastQuotaCache, 384);
22409
22921
  }
22922
+ persistExternalSpendCache() {
22923
+ if (!this.externalSpendCache)
22924
+ return;
22925
+ atomicWriteJsonSync(join8(this.stateDir, "external-spend.json"), this.externalSpendCache, 384);
22926
+ }
22410
22927
  persistLastTierQuotaCache() {
22411
22928
  atomicWriteJsonSync(join8(this.stateDir, "last-tier-quota.json"), this.lastTierQuotaCache, 384);
22412
22929
  }