switchroom 0.15.0 → 0.15.2
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/agent-scheduler/index.js +23 -1
- package/dist/auth-broker/index.js +43 -3
- package/dist/cli/drive-write-pretool.mjs +23 -2
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +375 -18
- package/dist/cli/ui/index.html +67 -1
- package/dist/host-control/main.js +5 -1
- package/dist/vault/approvals/kernel-server.js +1 -0
- package/dist/vault/broker/server.js +2 -1
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +18 -0
- package/telegram-plugin/auth-snapshot-format.ts +9 -0
- package/telegram-plugin/auto-fallback-fleet.ts +59 -0
- package/telegram-plugin/dist/gateway/gateway.js +347 -21
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -0
- package/telegram-plugin/gateway/auth-command.ts +35 -2
- package/telegram-plugin/gateway/gateway.ts +236 -22
- package/telegram-plugin/gateway/model-command.ts +182 -0
- package/telegram-plugin/quota-watch.ts +141 -3
- package/telegram-plugin/tests/auth-quota-util-cell.test.ts +23 -0
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +71 -0
- package/telegram-plugin/tests/model-command.test.ts +205 -0
- package/telegram-plugin/tests/quota-watch.test.ts +266 -0
- package/telegram-plugin/welcome-text.ts +7 -1
|
@@ -16160,7 +16160,7 @@ function decodeResponse(line) {
|
|
|
16160
16160
|
}
|
|
16161
16161
|
return ResponseSchema.parse(parsed);
|
|
16162
16162
|
}
|
|
16163
|
-
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
16163
|
+
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
16164
16164
|
var init_protocol = __esm(() => {
|
|
16165
16165
|
init_zod();
|
|
16166
16166
|
MAX_FRAME_BYTES = 64 * 1024;
|
|
@@ -16276,6 +16276,13 @@ var init_protocol = __esm(() => {
|
|
|
16276
16276
|
accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
|
|
16277
16277
|
timeoutMs: exports_external.number().int().positive().max(60000).optional()
|
|
16278
16278
|
});
|
|
16279
|
+
ClaimNotificationRequestSchema = exports_external.object({
|
|
16280
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
16281
|
+
op: exports_external.literal("claim-notification"),
|
|
16282
|
+
id: exports_external.string().min(1),
|
|
16283
|
+
key: exports_external.string().min(1).max(512),
|
|
16284
|
+
windowMs: exports_external.number().int().positive().max(86400000)
|
|
16285
|
+
});
|
|
16279
16286
|
RequestSchema = exports_external.discriminatedUnion("op", [
|
|
16280
16287
|
GetCredentialsRequestSchema,
|
|
16281
16288
|
ListStateRequestSchema,
|
|
@@ -16287,7 +16294,8 @@ var init_protocol = __esm(() => {
|
|
|
16287
16294
|
SetOverrideRequestSchema,
|
|
16288
16295
|
ListGoogleAccountsRequestSchema,
|
|
16289
16296
|
ListMicrosoftAccountsRequestSchema,
|
|
16290
|
-
ProbeQuotaRequestSchema
|
|
16297
|
+
ProbeQuotaRequestSchema,
|
|
16298
|
+
ClaimNotificationRequestSchema
|
|
16291
16299
|
]);
|
|
16292
16300
|
GetCredentialsDataSchema = exports_external.object({
|
|
16293
16301
|
account: exports_external.string(),
|
|
@@ -16343,6 +16351,9 @@ var init_protocol = __esm(() => {
|
|
|
16343
16351
|
agent: exports_external.string(),
|
|
16344
16352
|
account: exports_external.string().nullable()
|
|
16345
16353
|
});
|
|
16354
|
+
ClaimNotificationDataSchema = exports_external.object({
|
|
16355
|
+
granted: exports_external.boolean()
|
|
16356
|
+
});
|
|
16346
16357
|
GoogleAccountStateSchema = exports_external.object({
|
|
16347
16358
|
account: exports_external.string(),
|
|
16348
16359
|
expiresAt: exports_external.number(),
|
|
@@ -16524,6 +16535,16 @@ class AuthBrokerClient {
|
|
|
16524
16535
|
const data = await this.send(req);
|
|
16525
16536
|
return data;
|
|
16526
16537
|
}
|
|
16538
|
+
async claimNotification(key, windowMs) {
|
|
16539
|
+
const data = await this.send({
|
|
16540
|
+
v: PROTOCOL_VERSION,
|
|
16541
|
+
id: randomUUID3(),
|
|
16542
|
+
op: "claim-notification",
|
|
16543
|
+
key,
|
|
16544
|
+
windowMs
|
|
16545
|
+
});
|
|
16546
|
+
return data;
|
|
16547
|
+
}
|
|
16527
16548
|
async refreshAccount(account) {
|
|
16528
16549
|
const data = await this.send({
|
|
16529
16550
|
v: PROTOCOL_VERSION,
|
|
@@ -24173,6 +24194,7 @@ var init_schema = __esm(() => {
|
|
|
24173
24194
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
24174
24195
|
network_isolation: NetworkIsolationSchema,
|
|
24175
24196
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently \u2014 " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
24197
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet \u2014 read any agent's logs, " + "docker exec into peers, edit host files \u2014 instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent \u2014 it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
24176
24198
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only \u2014 prefer the typed fields when they exist."),
|
|
24177
24199
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
24178
24200
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
|
@@ -30611,7 +30633,8 @@ function buildSnapshotsFromCachedState(state4) {
|
|
|
30611
30633
|
isActive: acc.label === state4.active,
|
|
30612
30634
|
quota: reviveLastQuota(lq),
|
|
30613
30635
|
quotaError: lq ? undefined : "no cached quota (no probe since broker start)",
|
|
30614
|
-
expiresAtMs: acc.expiresAt
|
|
30636
|
+
expiresAtMs: acc.expiresAt,
|
|
30637
|
+
capturedAtMs: lq?.capturedAt
|
|
30615
30638
|
};
|
|
30616
30639
|
});
|
|
30617
30640
|
}
|
|
@@ -40687,17 +40710,31 @@ function renderShowText(state3, now = Date.now(), opts = {}) {
|
|
|
40687
40710
|
`);
|
|
40688
40711
|
}
|
|
40689
40712
|
function formatAccountsTable(state3, now) {
|
|
40690
|
-
const rows = [["ACCOUNT", "STATUS", "EXPIRES", "QUOTA-RESET"]];
|
|
40713
|
+
const rows = [["ACCOUNT", "STATUS", "EXPIRES", "QUOTA 5h\u00b77d", "QUOTA-RESET"]];
|
|
40691
40714
|
for (const acc of state3.accounts) {
|
|
40692
40715
|
const isActive = acc.label === state3.active;
|
|
40693
40716
|
const marker = isActive ? "\u25cf" : acc.exhausted ? "!" : "\u2713";
|
|
40694
40717
|
const status = isActive ? "active" : acc.exhausted ? "exhausted" : "available";
|
|
40695
40718
|
const expires = acc.expiresAt != null ? formatRelativeMs(acc.expiresAt - now) : "\u2014";
|
|
40696
40719
|
const quotaReset = acc.exhausted && acc.exhausted_until != null && acc.exhausted_until > now ? formatRelativeMs(acc.exhausted_until - now) : "\u2014";
|
|
40697
|
-
rows.push([
|
|
40720
|
+
rows.push([
|
|
40721
|
+
`${marker} ${escapeHtml3(acc.label)}`,
|
|
40722
|
+
status,
|
|
40723
|
+
expires,
|
|
40724
|
+
formatQuotaUtilCell(acc, now),
|
|
40725
|
+
quotaReset
|
|
40726
|
+
]);
|
|
40698
40727
|
}
|
|
40699
40728
|
return alignTable(rows);
|
|
40700
40729
|
}
|
|
40730
|
+
function formatQuotaUtilCell(acc, now) {
|
|
40731
|
+
const lq = acc.last_quota;
|
|
40732
|
+
if (!lq)
|
|
40733
|
+
return "no data";
|
|
40734
|
+
const age = now - lq.capturedAt;
|
|
40735
|
+
const ageStr = age > 0 ? formatRelativeMs(age) : "0s";
|
|
40736
|
+
return `${Math.round(lq.fiveHourUtilizationPct)}%\u00b7${Math.round(lq.sevenDayUtilizationPct)}% (${ageStr} ago)`;
|
|
40737
|
+
}
|
|
40701
40738
|
function formatAgentsTable(state3) {
|
|
40702
40739
|
const rows = [["AGENT", "ACTIVE", "SOURCE"]];
|
|
40703
40740
|
for (const a of state3.agents) {
|
|
@@ -40801,7 +40838,8 @@ function createAuthBrokerClient() {
|
|
|
40801
40838
|
rmAccount: (label) => broker.rmAccount(label),
|
|
40802
40839
|
refreshAccount: (label) => broker.refreshAccount(label),
|
|
40803
40840
|
setOverride: (agent, account) => broker.setOverride(agent, account),
|
|
40804
|
-
probeQuota: (accounts, timeoutMs) => broker.probeQuota(accounts, timeoutMs)
|
|
40841
|
+
probeQuota: (accounts, timeoutMs) => broker.probeQuota(accounts, timeoutMs),
|
|
40842
|
+
claimNotification: (key, windowMs) => broker.claimNotification(key, windowMs)
|
|
40805
40843
|
};
|
|
40806
40844
|
return { client: client3, close: () => broker.close() };
|
|
40807
40845
|
}
|
|
@@ -41215,6 +41253,16 @@ class AuthBrokerClient2 {
|
|
|
41215
41253
|
const data = await this.send(req);
|
|
41216
41254
|
return data;
|
|
41217
41255
|
}
|
|
41256
|
+
async claimNotification(key, windowMs) {
|
|
41257
|
+
const data = await this.send({
|
|
41258
|
+
v: PROTOCOL_VERSION,
|
|
41259
|
+
id: randomUUID4(),
|
|
41260
|
+
op: "claim-notification",
|
|
41261
|
+
key,
|
|
41262
|
+
windowMs
|
|
41263
|
+
});
|
|
41264
|
+
return data;
|
|
41265
|
+
}
|
|
41218
41266
|
async refreshAccount(account) {
|
|
41219
41267
|
const data = await this.send({
|
|
41220
41268
|
v: PROTOCOL_VERSION,
|
|
@@ -42085,6 +42133,22 @@ function escHtml2(text) {
|
|
|
42085
42133
|
}
|
|
42086
42134
|
|
|
42087
42135
|
// auto-fallback-fleet.ts
|
|
42136
|
+
function renderFallbackFailureNotice(triggerAgent, reason) {
|
|
42137
|
+
return `\u26a0\ufe0f <b>Auto-failover could not run</b> (trigger: <b>${escFailureHtml(triggerAgent)}</b>)
|
|
42138
|
+
` + `${escFailureHtml(reason)}
|
|
42139
|
+
|
|
42140
|
+
` + `<i>Switch manually with <code>/auth use <label></code>, or <code>/auth</code> for fleet status.</i>`;
|
|
42141
|
+
}
|
|
42142
|
+
function escFailureHtml(s) {
|
|
42143
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
42144
|
+
}
|
|
42145
|
+
var FALLBACK_FAILURE_NOTICE_COOLDOWN_MS = 30 * 60000;
|
|
42146
|
+
function evaluateFallbackFailureNotice(prev, now, cooldownMs = FALLBACK_FAILURE_NOTICE_COOLDOWN_MS) {
|
|
42147
|
+
if (now - prev.lastSentAtMs >= cooldownMs) {
|
|
42148
|
+
return { send: true, next: { lastSentAtMs: now } };
|
|
42149
|
+
}
|
|
42150
|
+
return { send: false, next: prev };
|
|
42151
|
+
}
|
|
42088
42152
|
async function runFleetAutoFallback(deps) {
|
|
42089
42153
|
const now = deps.now ?? new Date;
|
|
42090
42154
|
const tz = deps.tz ?? "UTC";
|
|
@@ -42979,6 +43043,7 @@ var TELEGRAM_MENU_COMMANDS = [
|
|
|
42979
43043
|
{ command: "version", description: "Show versions + running agent health" },
|
|
42980
43044
|
{ command: "logs", description: "Show recent agent logs" },
|
|
42981
43045
|
{ command: "inject", description: "Inject a Claude Code slash command (e.g. /cost)" },
|
|
43046
|
+
{ command: "model", description: "Show or switch the Claude model" },
|
|
42982
43047
|
{ command: "doctor", description: "Health check (deps, services, MCP)" },
|
|
42983
43048
|
{ command: "usage", description: "Pro/Max plan quota (5h + 7d windows)" },
|
|
42984
43049
|
{ command: "vault", description: "Manage vault secrets + capability grants" },
|
|
@@ -43018,6 +43083,8 @@ function switchroomHelpText(agentName3) {
|
|
|
43018
43083
|
`<code>/auth list [agent]</code> \u2014 list account slots and health`,
|
|
43019
43084
|
`<code>/auth use [agent] <slot></code> \u2014 switch active slot and restart`,
|
|
43020
43085
|
`<code>/auth rm [agent] <slot> [--force]</code> \u2014 remove a slot`,
|
|
43086
|
+
`<code>/model</code> \u2014 show the configured Claude model`,
|
|
43087
|
+
`<code>/model <name></code> \u2014 switch the live session's model (opus \u00b7 sonnet \u00b7 haiku or a full id; until restart)`,
|
|
43021
43088
|
`<code>/topics</code> \u2014 topic-to-agent mappings`,
|
|
43022
43089
|
`<code>/permissions [agent]</code> \u2014 show agent permissions`,
|
|
43023
43090
|
`<code>/grant <tool></code> \u2014 grant a tool permission`,
|
|
@@ -44748,6 +44815,106 @@ Allowed: <code>${deps.escapeHtml(allow)}</code>`, { html: true });
|
|
|
44748
44815
|
await deps.reply(ctx, finalText, { html: true, accent });
|
|
44749
44816
|
}
|
|
44750
44817
|
|
|
44818
|
+
// gateway/model-command.ts
|
|
44819
|
+
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "default"];
|
|
44820
|
+
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,99}$/;
|
|
44821
|
+
function isValidModelArg(arg) {
|
|
44822
|
+
return MODEL_ARG_RE.test(arg);
|
|
44823
|
+
}
|
|
44824
|
+
function parseModelCommand(text) {
|
|
44825
|
+
const m = text.match(/^\/model(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
|
|
44826
|
+
if (!m)
|
|
44827
|
+
return null;
|
|
44828
|
+
const rest = (m[1] ?? "").trim();
|
|
44829
|
+
if (rest.length === 0)
|
|
44830
|
+
return { kind: "show" };
|
|
44831
|
+
const parts = rest.split(/\s+/);
|
|
44832
|
+
if (parts.length > 1) {
|
|
44833
|
+
return { kind: "help", reason: "model takes a single argument" };
|
|
44834
|
+
}
|
|
44835
|
+
const arg = parts[0];
|
|
44836
|
+
if (arg.toLowerCase() === "help")
|
|
44837
|
+
return { kind: "help" };
|
|
44838
|
+
if (!isValidModelArg(arg)) {
|
|
44839
|
+
return { kind: "help", reason: `not a valid model name: ${arg}` };
|
|
44840
|
+
}
|
|
44841
|
+
return { kind: "set", model: arg };
|
|
44842
|
+
}
|
|
44843
|
+
var PERSIST_NOTE = "<i>Session-only \u2014 lasts until restart. To persist, set <code>model:</code> in switchroom.yaml and restart.</i>";
|
|
44844
|
+
function helpText2(deps, reason) {
|
|
44845
|
+
const lines = [];
|
|
44846
|
+
if (reason)
|
|
44847
|
+
lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
|
|
44848
|
+
lines.push("<b>/model</b> \u2014 show or switch the Claude model", "<code>/model</code> \u2014 show the configured model", `<code>/model <name></code> \u2014 switch the live session (${MODEL_ALIASES.map((a) => `<code>${a}</code>`).join(" \u00b7 ")} or a full model id)`, PERSIST_NOTE);
|
|
44849
|
+
return { text: lines.join(`
|
|
44850
|
+
`), html: true };
|
|
44851
|
+
}
|
|
44852
|
+
async function handleModelCommand(parsed, deps) {
|
|
44853
|
+
if (parsed.kind === "help")
|
|
44854
|
+
return helpText2(deps, parsed.reason);
|
|
44855
|
+
if (parsed.kind === "show") {
|
|
44856
|
+
const configured = deps.getConfiguredModel();
|
|
44857
|
+
const shown = configured && configured.length > 0 ? configured : "default";
|
|
44858
|
+
return {
|
|
44859
|
+
text: [
|
|
44860
|
+
`<b>Model \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
|
|
44861
|
+
`Configured: <code>${deps.escapeHtml(shown)}</code>`,
|
|
44862
|
+
`Switch the live session: ${MODEL_ALIASES.map((a) => `<code>/model ${a}</code>`).join(" \u00b7 ")}`,
|
|
44863
|
+
"or <code>/model <full-model-id></code>",
|
|
44864
|
+
PERSIST_NOTE
|
|
44865
|
+
].join(`
|
|
44866
|
+
`),
|
|
44867
|
+
html: true
|
|
44868
|
+
};
|
|
44869
|
+
}
|
|
44870
|
+
if (!isValidModelArg(parsed.model)) {
|
|
44871
|
+
return helpText2(deps, `not a valid model name: ${parsed.model}`);
|
|
44872
|
+
}
|
|
44873
|
+
const verbHtml = `<code>/model ${deps.escapeHtml(parsed.model)}</code>`;
|
|
44874
|
+
let result;
|
|
44875
|
+
try {
|
|
44876
|
+
result = await deps.inject(deps.getAgentName(), `/model ${parsed.model}`);
|
|
44877
|
+
} catch (err) {
|
|
44878
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44879
|
+
return {
|
|
44880
|
+
text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`,
|
|
44881
|
+
html: true
|
|
44882
|
+
};
|
|
44883
|
+
}
|
|
44884
|
+
if (result.outcome === "ok") {
|
|
44885
|
+
return {
|
|
44886
|
+
text: [
|
|
44887
|
+
`${verbHtml}`,
|
|
44888
|
+
deps.preBlock(result.output),
|
|
44889
|
+
...result.truncated ? ["<i>truncated</i>"] : [],
|
|
44890
|
+
PERSIST_NOTE
|
|
44891
|
+
].join(`
|
|
44892
|
+
`),
|
|
44893
|
+
html: true
|
|
44894
|
+
};
|
|
44895
|
+
}
|
|
44896
|
+
if (result.outcome === "ok_no_output") {
|
|
44897
|
+
return {
|
|
44898
|
+
text: [
|
|
44899
|
+
`${verbHtml} \u2014 sent, but no response captured. The agent may be mid-turn; check <code>/inject /status</code> to confirm the active model.`,
|
|
44900
|
+
PERSIST_NOTE
|
|
44901
|
+
].join(`
|
|
44902
|
+
`),
|
|
44903
|
+
html: true
|
|
44904
|
+
};
|
|
44905
|
+
}
|
|
44906
|
+
if (result.errorCode === "session_missing") {
|
|
44907
|
+
return {
|
|
44908
|
+
text: "\u274c tmux session not found \u2014 the agent must be running under the tmux supervisor (the default). Remove <code>experimental.legacy_pty: true</code> if set.",
|
|
44909
|
+
html: true
|
|
44910
|
+
};
|
|
44911
|
+
}
|
|
44912
|
+
return {
|
|
44913
|
+
text: `\u274c ${verbHtml} \u2014 ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`,
|
|
44914
|
+
html: true
|
|
44915
|
+
};
|
|
44916
|
+
}
|
|
44917
|
+
|
|
44751
44918
|
// ../src/config/loader.ts
|
|
44752
44919
|
init_dist();
|
|
44753
44920
|
init_zod();
|
|
@@ -52727,9 +52894,35 @@ function emptyQuotaWatchState() {
|
|
|
52727
52894
|
function emptyAccountState() {
|
|
52728
52895
|
return { lastNotifiedHealth: null, lastNotifiedAt: 0 };
|
|
52729
52896
|
}
|
|
52897
|
+
var DEFAULT_QUOTA_WATCH_MAX_STALE_MS = 60 * 60000;
|
|
52898
|
+
var DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS = 6 * 60 * 60000;
|
|
52899
|
+
var QUOTA_WATCH_CLAIM_WINDOW_MS = 30 * 60000;
|
|
52900
|
+
function resolveQuotaWatchTuning(env) {
|
|
52901
|
+
const num = (raw, fallback) => {
|
|
52902
|
+
if (raw === undefined || raw === "")
|
|
52903
|
+
return fallback;
|
|
52904
|
+
const n = Number(raw);
|
|
52905
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
52906
|
+
};
|
|
52907
|
+
return {
|
|
52908
|
+
maxStaleMs: num(env.SWITCHROOM_QUOTA_WATCH_MAX_STALE_MS, DEFAULT_QUOTA_WATCH_MAX_STALE_MS),
|
|
52909
|
+
lateRecoveryMs: num(env.SWITCHROOM_QUOTA_WATCH_LATE_RECOVERY_MS, DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS),
|
|
52910
|
+
fleetDedup: env.SWITCHROOM_QUOTA_WATCH_FLEET_DEDUP !== "0",
|
|
52911
|
+
sendOnProbeFail: env.SWITCHROOM_QUOTA_WATCH_SEND_ON_PROBE_FAIL === "1"
|
|
52912
|
+
};
|
|
52913
|
+
}
|
|
52914
|
+
function buildQuotaClaimKey(accountLabel, transition2, chatId) {
|
|
52915
|
+
return `quota-watch:${accountLabel}:${transition2}:${chatId}`;
|
|
52916
|
+
}
|
|
52730
52917
|
function evaluateQuotaWatchAccount(args) {
|
|
52731
52918
|
const { agentName: agentName3, snap, prev, now } = args;
|
|
52919
|
+
const bootTick = args.bootTick ?? false;
|
|
52920
|
+
const maxStaleMs = args.tuning?.maxStaleMs ?? 0;
|
|
52921
|
+
const lateRecoveryMs = args.tuning?.lateRecoveryMs ?? 0;
|
|
52732
52922
|
const label = snap.label;
|
|
52923
|
+
if (maxStaleMs > 0 && snap.capturedAtMs !== undefined && now - snap.capturedAtMs > maxStaleMs) {
|
|
52924
|
+
return { kind: "skip", accountLabel: label, reason: "stale-snapshot" };
|
|
52925
|
+
}
|
|
52733
52926
|
const currentHealth = classifyHealth(snap);
|
|
52734
52927
|
if (currentHealth === "unknown" || currentHealth === "blocked") {
|
|
52735
52928
|
return { kind: "skip", accountLabel: label, reason: `${currentHealth}-not-our-domain` };
|
|
@@ -52756,6 +52949,24 @@ function evaluateQuotaWatchAccount(args) {
|
|
|
52756
52949
|
lastNotifiedHealth: "healthy",
|
|
52757
52950
|
lastNotifiedAt: now
|
|
52758
52951
|
};
|
|
52952
|
+
if (bootTick) {
|
|
52953
|
+
return {
|
|
52954
|
+
kind: "reconcile",
|
|
52955
|
+
accountLabel: label,
|
|
52956
|
+
newAccountState: newState,
|
|
52957
|
+
transition: "recovered-to-healthy",
|
|
52958
|
+
reason: "boot-tick-recovery"
|
|
52959
|
+
};
|
|
52960
|
+
}
|
|
52961
|
+
if (lateRecoveryMs > 0 && now - prev.lastNotifiedAt > lateRecoveryMs) {
|
|
52962
|
+
return {
|
|
52963
|
+
kind: "reconcile",
|
|
52964
|
+
accountLabel: label,
|
|
52965
|
+
newAccountState: newState,
|
|
52966
|
+
transition: "recovered-to-healthy",
|
|
52967
|
+
reason: "late-recovery"
|
|
52968
|
+
};
|
|
52969
|
+
}
|
|
52759
52970
|
return {
|
|
52760
52971
|
kind: "notify",
|
|
52761
52972
|
accountLabel: label,
|
|
@@ -52971,10 +53182,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
52971
53182
|
}
|
|
52972
53183
|
|
|
52973
53184
|
// ../src/build-info.ts
|
|
52974
|
-
var VERSION = "0.15.
|
|
52975
|
-
var COMMIT_SHA = "
|
|
52976
|
-
var COMMIT_DATE = "2026-06-
|
|
52977
|
-
var LATEST_PR =
|
|
53185
|
+
var VERSION = "0.15.2";
|
|
53186
|
+
var COMMIT_SHA = "95461524";
|
|
53187
|
+
var COMMIT_DATE = "2026-06-10T02:48:01Z";
|
|
53188
|
+
var LATEST_PR = 2260;
|
|
52978
53189
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
52979
53190
|
|
|
52980
53191
|
// gateway/boot-version.ts
|
|
@@ -60618,6 +60829,23 @@ bot.command("inject", async (ctx) => {
|
|
|
60618
60829
|
formatOutput: formatSwitchroomOutput
|
|
60619
60830
|
});
|
|
60620
60831
|
});
|
|
60832
|
+
bot.command("model", async (ctx) => {
|
|
60833
|
+
if (!isAuthorizedSender(ctx))
|
|
60834
|
+
return;
|
|
60835
|
+
const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
|
|
60836
|
+
const parsed = parseModelCommand(text) ?? { kind: "show" };
|
|
60837
|
+
const reply = await handleModelCommand(parsed, {
|
|
60838
|
+
inject: injectSlashCommand,
|
|
60839
|
+
getAgentName: getMyAgentName,
|
|
60840
|
+
getConfiguredModel: () => {
|
|
60841
|
+
const data = switchroomExecJson(["agent", "list"]);
|
|
60842
|
+
return data?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
60843
|
+
},
|
|
60844
|
+
escapeHtml: escapeHtmlForTg,
|
|
60845
|
+
preBlock
|
|
60846
|
+
});
|
|
60847
|
+
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
60848
|
+
});
|
|
60621
60849
|
bot.command("agentstart", async (ctx) => {
|
|
60622
60850
|
if (!isAuthorizedSender(ctx))
|
|
60623
60851
|
return;
|
|
@@ -61156,12 +61384,32 @@ async function fireFleetAutoFallback(triggerAgent, untilMs) {
|
|
|
61156
61384
|
`);
|
|
61157
61385
|
});
|
|
61158
61386
|
}
|
|
61387
|
+
var fallbackFailureNoticeState = { lastSentAtMs: 0 };
|
|
61388
|
+
function broadcastFleetFallbackFailure(triggerAgent, reason) {
|
|
61389
|
+
if (process.env.SWITCHROOM_FLEET_FALLBACK_FAILURE_NOTICE === "0")
|
|
61390
|
+
return;
|
|
61391
|
+
const verdict = evaluateFallbackFailureNotice(fallbackFailureNoticeState, Date.now());
|
|
61392
|
+
if (!verdict.send) {
|
|
61393
|
+
process.stderr.write(`telegram gateway: [fleet-fallback] failure notice suppressed (cooldown) agent=${triggerAgent}: ${reason}
|
|
61394
|
+
`);
|
|
61395
|
+
return;
|
|
61396
|
+
}
|
|
61397
|
+
fallbackFailureNoticeState = verdict.next;
|
|
61398
|
+
const access = loadAccess();
|
|
61399
|
+
if (access.allowFrom.length === 0)
|
|
61400
|
+
return;
|
|
61401
|
+
const html = renderFallbackFailureNotice(triggerAgent, reason);
|
|
61402
|
+
for (const chat_id of access.allowFrom) {
|
|
61403
|
+
swallowingApiCall(() => bot.api.sendMessage(chat_id, html, { parse_mode: "HTML" }), { chat_id, verb: "fleet-fallback:failure-notify" });
|
|
61404
|
+
}
|
|
61405
|
+
}
|
|
61159
61406
|
async function doFireFleetAutoFallback(triggerAgent, untilMs) {
|
|
61160
61407
|
try {
|
|
61161
61408
|
const client3 = await getAuthBrokerClient(triggerAgent);
|
|
61162
61409
|
if (!client3) {
|
|
61163
61410
|
process.stderr.write(`telegram gateway: [fleet-fallback] skipped agent=${triggerAgent} reason=no-broker-client
|
|
61164
61411
|
`);
|
|
61412
|
+
broadcastFleetFallbackFailure(triggerAgent, "auth-broker unreachable (no client).");
|
|
61165
61413
|
return false;
|
|
61166
61414
|
}
|
|
61167
61415
|
const state4 = await client3.listState();
|
|
@@ -61194,6 +61442,7 @@ async function doFireFleetAutoFallback(triggerAgent, untilMs) {
|
|
|
61194
61442
|
} catch (err) {
|
|
61195
61443
|
process.stderr.write(`telegram gateway: [fleet-fallback] error agent=${triggerAgent}: ${err?.message ?? err}
|
|
61196
61444
|
`);
|
|
61445
|
+
broadcastFleetFallbackFailure(triggerAgent, err?.message ?? String(err));
|
|
61197
61446
|
return false;
|
|
61198
61447
|
}
|
|
61199
61448
|
}
|
|
@@ -61230,9 +61479,21 @@ async function runCreditWatch() {
|
|
|
61230
61479
|
`);
|
|
61231
61480
|
}
|
|
61232
61481
|
}
|
|
61233
|
-
async function
|
|
61482
|
+
async function claimQuotaNotification(brokerClient, key) {
|
|
61483
|
+
try {
|
|
61484
|
+
const res = await brokerClient.claimNotification(key, QUOTA_WATCH_CLAIM_WINDOW_MS);
|
|
61485
|
+
return res.granted;
|
|
61486
|
+
} catch (err) {
|
|
61487
|
+
process.stderr.write(`telegram gateway: quota-watch: claim failed (fail-open): ${err}
|
|
61488
|
+
`);
|
|
61489
|
+
return true;
|
|
61490
|
+
}
|
|
61491
|
+
}
|
|
61492
|
+
async function runQuotaWatch(opts = {}) {
|
|
61234
61493
|
const agentName3 = getMyAgentName();
|
|
61235
61494
|
const stateDir = STATE_DIR;
|
|
61495
|
+
const bootTick = opts.bootTick ?? false;
|
|
61496
|
+
const tuning = resolveQuotaWatchTuning(process.env);
|
|
61236
61497
|
const brokerClient = await getAuthBrokerClient(agentName3);
|
|
61237
61498
|
if (!brokerClient) {
|
|
61238
61499
|
process.stderr.write(`telegram gateway: quota-watch: broker client unavailable \u2014 skipping
|
|
@@ -61263,6 +61524,14 @@ async function runQuotaWatch() {
|
|
|
61263
61524
|
});
|
|
61264
61525
|
if (fleetDecision.kind === "notify") {
|
|
61265
61526
|
for (const chat_id of access.allowFrom) {
|
|
61527
|
+
if (tuning.fleetDedup) {
|
|
61528
|
+
const granted = await claimQuotaNotification(brokerClient, buildQuotaClaimKey(FLEET_ALL_EXHAUSTED_KEY, fleetDecision.transition, chat_id));
|
|
61529
|
+
if (!granted) {
|
|
61530
|
+
process.stderr.write(`telegram gateway: quota-watch: fleet-all-exhausted claim denied chat=${chat_id} \u2014 another agent notified
|
|
61531
|
+
`);
|
|
61532
|
+
continue;
|
|
61533
|
+
}
|
|
61534
|
+
}
|
|
61266
61535
|
await swallowingApiCall(() => bot.api.sendMessage(chat_id, fleetDecision.message, {
|
|
61267
61536
|
parse_mode: "HTML",
|
|
61268
61537
|
link_preview_options: { is_disabled: true }
|
|
@@ -61281,10 +61550,17 @@ async function runQuotaWatch() {
|
|
|
61281
61550
|
}
|
|
61282
61551
|
const pendingTransitions = [];
|
|
61283
61552
|
const labelToSnapIndex = new Map(snapshots.map((s, i) => [s.label, i]));
|
|
61553
|
+
let reconciledCount = 0;
|
|
61554
|
+
let mutatedState = watchState;
|
|
61284
61555
|
for (const snap of snapshots) {
|
|
61285
61556
|
const prev = watchState[snap.label] ?? emptyAccountState();
|
|
61286
|
-
const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now });
|
|
61287
|
-
if (decision.kind
|
|
61557
|
+
const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now, bootTick, tuning });
|
|
61558
|
+
if (decision.kind === "reconcile") {
|
|
61559
|
+
mutatedState = patchQuotaWatchState(mutatedState, decision.accountLabel, decision.newAccountState);
|
|
61560
|
+
reconciledCount++;
|
|
61561
|
+
process.stderr.write(`telegram gateway: quota-watch: reconciled ${decision.transition} for account=${decision.accountLabel} (${decision.reason}) \u2014 no notification
|
|
61562
|
+
`);
|
|
61563
|
+
} else if (decision.kind !== "skip") {
|
|
61288
61564
|
pendingTransitions.push({
|
|
61289
61565
|
accountLabel: snap.label,
|
|
61290
61566
|
snapIndex: labelToSnapIndex.get(snap.label) ?? -1,
|
|
@@ -61293,6 +61569,14 @@ async function runQuotaWatch() {
|
|
|
61293
61569
|
}
|
|
61294
61570
|
}
|
|
61295
61571
|
if (pendingTransitions.length === 0) {
|
|
61572
|
+
if (reconciledCount > 0) {
|
|
61573
|
+
try {
|
|
61574
|
+
saveQuotaWatchState(stateDir, mutatedState);
|
|
61575
|
+
} catch (err) {
|
|
61576
|
+
process.stderr.write(`telegram gateway: quota-watch state persist failed: ${err}
|
|
61577
|
+
`);
|
|
61578
|
+
}
|
|
61579
|
+
}
|
|
61296
61580
|
return;
|
|
61297
61581
|
}
|
|
61298
61582
|
const crossingLabels = pendingTransitions.map((t) => t.accountLabel);
|
|
@@ -61305,8 +61589,20 @@ async function runQuotaWatch() {
|
|
|
61305
61589
|
} catch (err) {
|
|
61306
61590
|
process.stderr.write(`telegram gateway: quota-watch: probe for crossing accounts failed: ${err}
|
|
61307
61591
|
`);
|
|
61592
|
+
if (!tuning.sendOnProbeFail) {
|
|
61593
|
+
if (reconciledCount > 0) {
|
|
61594
|
+
try {
|
|
61595
|
+
saveQuotaWatchState(stateDir, mutatedState);
|
|
61596
|
+
} catch (saveErr) {
|
|
61597
|
+
process.stderr.write(`telegram gateway: quota-watch state persist failed: ${saveErr}
|
|
61598
|
+
`);
|
|
61599
|
+
}
|
|
61600
|
+
}
|
|
61601
|
+
process.stderr.write(`telegram gateway: quota-watch: deferring ${pendingTransitions.length} notification(s) until probe succeeds
|
|
61602
|
+
`);
|
|
61603
|
+
return;
|
|
61604
|
+
}
|
|
61308
61605
|
}
|
|
61309
|
-
let mutatedState = watchState;
|
|
61310
61606
|
const notifications = [];
|
|
61311
61607
|
for (const { accountLabel, snapIndex, decision } of pendingTransitions) {
|
|
61312
61608
|
const freshResult = freshProbeMap.get(accountLabel);
|
|
@@ -61314,25 +61610,55 @@ async function runQuotaWatch() {
|
|
|
61314
61610
|
if (decision.kind !== "notify")
|
|
61315
61611
|
continue;
|
|
61316
61612
|
if (freshResult && freshResult.ok && snapIndex >= 0) {
|
|
61317
|
-
const enrichedSnap = { ...snapshots[snapIndex], quota: freshResult.data };
|
|
61613
|
+
const enrichedSnap = { ...snapshots[snapIndex], quota: freshResult.data, capturedAtMs: undefined };
|
|
61318
61614
|
const prev = watchState[accountLabel] ?? emptyAccountState();
|
|
61319
|
-
const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now });
|
|
61615
|
+
const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now, bootTick, tuning });
|
|
61320
61616
|
if (re.kind === "notify" && re.transition === decision.transition) {
|
|
61321
61617
|
enrichedDecision = re;
|
|
61618
|
+
} else if (re.kind === "reconcile") {
|
|
61619
|
+
mutatedState = patchQuotaWatchState(mutatedState, accountLabel, re.newAccountState);
|
|
61620
|
+
reconciledCount++;
|
|
61621
|
+
process.stderr.write(`telegram gateway: quota-watch: reconciled ${re.transition} for account=${accountLabel} (${re.reason}) \u2014 no notification
|
|
61622
|
+
`);
|
|
61623
|
+
continue;
|
|
61322
61624
|
} else if (re.kind === "skip") {
|
|
61323
61625
|
continue;
|
|
61324
61626
|
}
|
|
61627
|
+
} else if (!tuning.sendOnProbeFail) {
|
|
61628
|
+
process.stderr.write(`telegram gateway: quota-watch: probe unavailable for account=${accountLabel} \u2014 deferring notification
|
|
61629
|
+
`);
|
|
61630
|
+
continue;
|
|
61325
61631
|
}
|
|
61326
61632
|
if (enrichedDecision.kind !== "notify")
|
|
61327
61633
|
continue;
|
|
61328
|
-
notifications.push({
|
|
61634
|
+
notifications.push({
|
|
61635
|
+
message: enrichedDecision.message,
|
|
61636
|
+
accountLabel,
|
|
61637
|
+
transition: enrichedDecision.transition
|
|
61638
|
+
});
|
|
61329
61639
|
mutatedState = patchQuotaWatchState(mutatedState, accountLabel, enrichedDecision.newAccountState);
|
|
61330
61640
|
}
|
|
61331
61641
|
if (notifications.length === 0) {
|
|
61642
|
+
if (reconciledCount > 0) {
|
|
61643
|
+
try {
|
|
61644
|
+
saveQuotaWatchState(stateDir, mutatedState);
|
|
61645
|
+
} catch (err) {
|
|
61646
|
+
process.stderr.write(`telegram gateway: quota-watch state persist failed: ${err}
|
|
61647
|
+
`);
|
|
61648
|
+
}
|
|
61649
|
+
}
|
|
61332
61650
|
return;
|
|
61333
61651
|
}
|
|
61334
|
-
for (const { message, accountLabel } of notifications) {
|
|
61652
|
+
for (const { message, accountLabel, transition: transition2 } of notifications) {
|
|
61335
61653
|
for (const chat_id of access.allowFrom) {
|
|
61654
|
+
if (tuning.fleetDedup) {
|
|
61655
|
+
const granted = await claimQuotaNotification(brokerClient, buildQuotaClaimKey(accountLabel, transition2, chat_id));
|
|
61656
|
+
if (!granted) {
|
|
61657
|
+
process.stderr.write(`telegram gateway: quota-watch: claim denied account=${accountLabel} chat=${chat_id} \u2014 another agent notified
|
|
61658
|
+
`);
|
|
61659
|
+
continue;
|
|
61660
|
+
}
|
|
61661
|
+
}
|
|
61336
61662
|
await swallowingApiCall(() => bot.api.sendMessage(chat_id, message, {
|
|
61337
61663
|
parse_mode: "HTML",
|
|
61338
61664
|
link_preview_options: { is_disabled: true }
|
|
@@ -61399,7 +61725,7 @@ bot.command("connect", async (ctx) => {
|
|
|
61399
61725
|
try {
|
|
61400
61726
|
const cfg = loadConfig2();
|
|
61401
61727
|
const me = cfg?.agents?.[getMyAgentName()];
|
|
61402
|
-
isAdmin2 = me?.admin === true;
|
|
61728
|
+
isAdmin2 = me?.admin === true || me?.root === true;
|
|
61403
61729
|
} catch {}
|
|
61404
61730
|
if (!isAuthAdmin({ isAdmin: isAdmin2 })) {
|
|
61405
61731
|
await switchroomReply(ctx, `<b>Not authorized.</b> <code>/connect</code> requires this agent to have <code>admin: true</code> in switchroom.yaml.`, { html: true });
|
|
@@ -61488,7 +61814,7 @@ bot.command("auth", async (ctx) => {
|
|
|
61488
61814
|
try {
|
|
61489
61815
|
const cfg = loadConfig2();
|
|
61490
61816
|
const me = cfg?.agents?.[currentAgent];
|
|
61491
|
-
isAdmin2 = me?.admin === true;
|
|
61817
|
+
isAdmin2 = me?.admin === true || me?.root === true;
|
|
61492
61818
|
} catch {}
|
|
61493
61819
|
const chatId = String(ctx.chat?.id ?? "");
|
|
61494
61820
|
if (parsed.kind === "add" || parsed.kind === "cancel") {
|
|
@@ -64661,7 +64987,7 @@ var didOneTimeSetup = false;
|
|
|
64661
64987
|
const QUOTA_WATCH_POLL_MS = Number(process.env.SWITCHROOM_QUOTA_WATCH_POLL_MS ?? 900000);
|
|
64662
64988
|
if (QUOTA_WATCH_POLL_MS > 0) {
|
|
64663
64989
|
setTimeout(() => {
|
|
64664
|
-
runQuotaWatch().catch((err) => {
|
|
64990
|
+
runQuotaWatch({ bootTick: true }).catch((err) => {
|
|
64665
64991
|
process.stderr.write(`telegram gateway: quota-watch initial run failed: ${err}
|
|
64666
64992
|
`);
|
|
64667
64993
|
});
|
|
@@ -34,6 +34,8 @@ export function createAuthBrokerClient(): {
|
|
|
34
34
|
broker.setOverride(agent, account),
|
|
35
35
|
probeQuota: (accounts: readonly string[], timeoutMs?: number) =>
|
|
36
36
|
broker.probeQuota(accounts, timeoutMs),
|
|
37
|
+
claimNotification: (key: string, windowMs: number) =>
|
|
38
|
+
broker.claimNotification(key, windowMs),
|
|
37
39
|
}
|
|
38
40
|
return { client, close: () => broker.close() }
|
|
39
41
|
}
|
|
@@ -238,6 +238,13 @@ export interface AuthBrokerClient {
|
|
|
238
238
|
accounts: readonly string[],
|
|
239
239
|
timeoutMs?: number,
|
|
240
240
|
): Promise<{ results: Array<{ label: string; result: import('../quota-check.js').QuotaResult }> }>
|
|
241
|
+
/**
|
|
242
|
+
* Fleet notification-dedup claim (quota-watch). First caller of `key`
|
|
243
|
+
* inside `windowMs` gets `granted: true` and sends; everyone else
|
|
244
|
+
* stays silent. Callers fail OPEN on error (skewed-rollout brokers
|
|
245
|
+
* reject the op at the protocol layer).
|
|
246
|
+
*/
|
|
247
|
+
claimNotification(key: string, windowMs: number): Promise<{ granted: boolean }>
|
|
241
248
|
}
|
|
242
249
|
|
|
243
250
|
export interface AuthCommandContext {
|
|
@@ -778,7 +785,7 @@ export function renderShowText(
|
|
|
778
785
|
}
|
|
779
786
|
|
|
780
787
|
function formatAccountsTable(state: ListStateData, now: number): string {
|
|
781
|
-
const rows: string[][] = [['ACCOUNT', 'STATUS', 'EXPIRES', 'QUOTA-RESET']]
|
|
788
|
+
const rows: string[][] = [['ACCOUNT', 'STATUS', 'EXPIRES', 'QUOTA 5h·7d', 'QUOTA-RESET']]
|
|
782
789
|
for (const acc of state.accounts) {
|
|
783
790
|
const isActive = acc.label === state.active
|
|
784
791
|
const marker = isActive
|
|
@@ -792,11 +799,37 @@ function formatAccountsTable(state: ListStateData, now: number): string {
|
|
|
792
799
|
acc.exhausted && acc.exhausted_until != null && acc.exhausted_until > now
|
|
793
800
|
? formatRelativeMs(acc.exhausted_until - now)
|
|
794
801
|
: '—'
|
|
795
|
-
rows.push([
|
|
802
|
+
rows.push([
|
|
803
|
+
`${marker} ${escapeHtml(acc.label)}`,
|
|
804
|
+
status,
|
|
805
|
+
expires,
|
|
806
|
+
formatQuotaUtilCell(acc, now),
|
|
807
|
+
quotaReset,
|
|
808
|
+
])
|
|
796
809
|
}
|
|
797
810
|
return alignTable(rows)
|
|
798
811
|
}
|
|
799
812
|
|
|
813
|
+
/**
|
|
814
|
+
* Cached-utilization cell for the legacy accounts table. Honesty fix
|
|
815
|
+
* (2026-06-09 incident follow-up): the table previously rendered ONLY
|
|
816
|
+
* the broker exhausted flag, so a 99%-utilized account read
|
|
817
|
+
* "available —" — and an agent parroting the table under-reported. Now
|
|
818
|
+
* shows the broker's cached 5h·7d utilization with its age, and
|
|
819
|
+
* distinguishes "no data" (never probed) from "not exhausted".
|
|
820
|
+
* Cached, not live: the (age) suffix is the staleness disclosure.
|
|
821
|
+
*/
|
|
822
|
+
export function formatQuotaUtilCell(
|
|
823
|
+
acc: { last_quota?: { fiveHourUtilizationPct: number; sevenDayUtilizationPct: number; capturedAt: number } | null },
|
|
824
|
+
now: number,
|
|
825
|
+
): string {
|
|
826
|
+
const lq = acc.last_quota
|
|
827
|
+
if (!lq) return 'no data'
|
|
828
|
+
const age = now - lq.capturedAt
|
|
829
|
+
const ageStr = age > 0 ? formatRelativeMs(age) : '0s'
|
|
830
|
+
return `${Math.round(lq.fiveHourUtilizationPct)}%·${Math.round(lq.sevenDayUtilizationPct)}% (${ageStr} ago)`
|
|
831
|
+
}
|
|
832
|
+
|
|
800
833
|
function formatAgentsTable(state: ListStateData): string {
|
|
801
834
|
const rows: string[][] = [['AGENT', 'ACTIVE', 'SOURCE']]
|
|
802
835
|
for (const a of state.agents) {
|