switchroom 0.18.12 → 0.18.14
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 +57 -9
- package/dist/auth-broker/index.js +174 -72
- package/dist/cli/autoaccept-poll.js +23 -0
- package/dist/cli/drive-write-pretool.mjs +24 -1
- package/dist/cli/foreground-hog-pretool.mjs +264 -0
- package/dist/cli/ms-365-write-pretool.mjs +31 -8
- package/dist/cli/notion-write-pretool.mjs +9 -2
- package/dist/cli/skill-validate-pretool.mjs +144 -2847
- package/dist/cli/switchroom.js +986 -3131
- package/dist/host-control/main.js +216 -2863
- package/dist/vault/approvals/kernel-server.js +67 -1
- package/dist/vault/broker/server.js +98 -45
- package/package.json +1 -1
- package/profiles/coding/CLAUDE.md.hbs +2 -0
- package/profiles/default/CLAUDE.md.hbs +2 -0
- package/skills/switchroom-architecture/telegram.md +0 -1
- package/telegram-plugin/auth-snapshot-format.ts +37 -5
- package/telegram-plugin/auto-fallback-fleet.ts +29 -1
- package/telegram-plugin/bridge/bridge.ts +2 -0
- package/telegram-plugin/dist/bridge/bridge.js +51 -3
- package/telegram-plugin/dist/gateway/gateway.js +1251 -2368
- package/telegram-plugin/dist/server.js +67 -3
- package/telegram-plugin/format.ts +19 -0
- package/telegram-plugin/gateway/approval-hold.ts +21 -2
- package/telegram-plugin/gateway/auth-broker-client.ts +1 -0
- package/telegram-plugin/gateway/auth-command.ts +14 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +12 -0
- package/telegram-plugin/gateway/forward-origin.ts +235 -0
- package/telegram-plugin/gateway/gateway.ts +445 -83
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +268 -0
- package/telegram-plugin/history.ts +106 -6
- package/telegram-plugin/inline-keyboard-callbacks.ts +94 -0
- package/telegram-plugin/model-unavailable.ts +61 -13
- package/telegram-plugin/outbound-field-redact.ts +69 -0
- package/telegram-plugin/render/render.ts +32 -14
- package/telegram-plugin/render/rich-render.ts +40 -32
- package/telegram-plugin/scoped-approval.ts +11 -2
- package/telegram-plugin/secret-detect/chunker.ts +18 -4
- package/telegram-plugin/secret-detect/index.ts +12 -56
- package/telegram-plugin/send-gate-degraded.test.ts +131 -0
- package/telegram-plugin/send-gate.test.ts +25 -6
- package/telegram-plugin/send-gate.ts +82 -8
- package/telegram-plugin/session-tail.ts +82 -7
- package/telegram-plugin/stream-controller.ts +3 -2
- package/telegram-plugin/subagent-watcher.ts +71 -16
- package/telegram-plugin/tests/approval-hold-outcome.test.ts +36 -5
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +72 -0
- package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
- package/telegram-plugin/tests/forward-origin.test.ts +309 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
- package/telegram-plugin/tests/history.test.ts +272 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +164 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
- package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
- package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +6 -4
- package/telegram-plugin/tests/render/render.test.ts +88 -0
- package/telegram-plugin/tests/render/rich-render.test.ts +41 -22
- package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
- package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
- package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +5 -3
- package/telegram-plugin/tests/status-accent.test.ts +5 -3
- package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +20 -20
- package/telegram-plugin/tests/stream-reply-handler.test.ts +5 -2
- package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
- package/telegram-plugin/tests/throttle-tier-wiring.test.ts +290 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +278 -0
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
- package/telegram-plugin/throttle-tier.ts +226 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +8 -7
- package/telegram-plugin/worktree-watch-cwds.ts +194 -5
- package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
- package/telegram-plugin/tests/secret-detect-secretlint.test.ts +0 -105
|
@@ -11187,7 +11187,6 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11187
11187
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
11188
11188
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
11189
11189
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
11190
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
11191
11190
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
11192
11191
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
11193
11192
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
|
@@ -13120,6 +13119,13 @@ var ApprovalLookupRequestSchema = exports_external.object({
|
|
|
13120
13119
|
action: exports_external.string().min(1),
|
|
13121
13120
|
current_approver_set: exports_external.array(exports_external.string())
|
|
13122
13121
|
});
|
|
13122
|
+
var ApprovalLookupByRequestRequestSchema = exports_external.object({
|
|
13123
|
+
v: exports_external.literal(1),
|
|
13124
|
+
op: exports_external.literal("approval_lookup_by_request"),
|
|
13125
|
+
agent_unit: exports_external.string().min(1),
|
|
13126
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
13127
|
+
current_approver_set: exports_external.array(exports_external.string())
|
|
13128
|
+
});
|
|
13123
13129
|
var ApprovalConsumeRequestSchema = exports_external.object({
|
|
13124
13130
|
v: exports_external.literal(1),
|
|
13125
13131
|
op: exports_external.literal("approval_consume"),
|
|
@@ -13174,6 +13180,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
13174
13180
|
RevokeGrantRequestSchema,
|
|
13175
13181
|
ApprovalRequestRequestSchema,
|
|
13176
13182
|
ApprovalLookupRequestSchema,
|
|
13183
|
+
ApprovalLookupByRequestRequestSchema,
|
|
13177
13184
|
ApprovalConsumeRequestSchema,
|
|
13178
13185
|
ApprovalRevokeRequestSchema,
|
|
13179
13186
|
ApprovalListRequestSchema,
|
|
@@ -13628,19 +13635,30 @@ class JsonlAuditSink {
|
|
|
13628
13635
|
}
|
|
13629
13636
|
|
|
13630
13637
|
// src/scheduler/quota-preflight.ts
|
|
13631
|
-
function decideQuotaPreflight(state) {
|
|
13638
|
+
function decideQuotaPreflight(state, opts = {}) {
|
|
13639
|
+
const now = opts.now ?? Date.now();
|
|
13632
13640
|
const accounts = state.accounts ?? [];
|
|
13633
13641
|
if (accounts.length === 0) {
|
|
13634
13642
|
return { defer: false, reason: "no accounts in broker state" };
|
|
13635
13643
|
}
|
|
13636
13644
|
const healthy = accounts.filter((a) => !a.exhausted);
|
|
13637
|
-
if (healthy.length
|
|
13645
|
+
if (healthy.length === 0) {
|
|
13646
|
+
return { defer: true, reason: `all ${accounts.length} account(s) exhausted` };
|
|
13647
|
+
}
|
|
13648
|
+
const effectiveLabel = (opts.agent !== undefined ? (state.agents ?? []).find((a) => a.name === opts.agent)?.account : undefined) ?? state.active;
|
|
13649
|
+
const effective = accounts.find((a) => a.label === effectiveLabel);
|
|
13650
|
+
if (effective !== undefined && !effective.exhausted && effective.throttled_until !== undefined && effective.throttled_until > now) {
|
|
13651
|
+
const inS = Math.ceil((effective.throttled_until - now) / 1000);
|
|
13638
13652
|
return {
|
|
13639
|
-
defer:
|
|
13640
|
-
reason:
|
|
13653
|
+
defer: true,
|
|
13654
|
+
reason: `account '${effective.label}' rate-throttled (clears in ${inS}s)`,
|
|
13655
|
+
retryAtMs: effective.throttled_until
|
|
13641
13656
|
};
|
|
13642
13657
|
}
|
|
13643
|
-
return {
|
|
13658
|
+
return {
|
|
13659
|
+
defer: false,
|
|
13660
|
+
reason: `${healthy.length}/${accounts.length} account(s) healthy`
|
|
13661
|
+
};
|
|
13644
13662
|
}
|
|
13645
13663
|
|
|
13646
13664
|
// src/auth/broker/client.ts
|
|
@@ -13677,6 +13695,12 @@ var MarkExhaustedRequestSchema = exports_external.object({
|
|
|
13677
13695
|
id: exports_external.string().min(1),
|
|
13678
13696
|
until: exports_external.number().int().positive().optional()
|
|
13679
13697
|
});
|
|
13698
|
+
var MarkThrottledRequestSchema = exports_external.object({
|
|
13699
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
13700
|
+
op: exports_external.literal("mark-throttled"),
|
|
13701
|
+
id: exports_external.string().min(1),
|
|
13702
|
+
until: exports_external.number().int().positive()
|
|
13703
|
+
});
|
|
13680
13704
|
var RefreshAccountRequestSchema = exports_external.object({
|
|
13681
13705
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
13682
13706
|
op: exports_external.literal("refresh-account"),
|
|
@@ -13777,6 +13801,7 @@ var RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
|
13777
13801
|
ListStateRequestSchema,
|
|
13778
13802
|
SetActiveRequestSchema,
|
|
13779
13803
|
MarkExhaustedRequestSchema,
|
|
13804
|
+
MarkThrottledRequestSchema,
|
|
13780
13805
|
RefreshAccountRequestSchema,
|
|
13781
13806
|
AddAccountRequestSchema,
|
|
13782
13807
|
RmAccountRequestSchema,
|
|
@@ -13796,6 +13821,7 @@ var AccountStateSchema = exports_external.object({
|
|
|
13796
13821
|
expiresAt: exports_external.number().optional(),
|
|
13797
13822
|
exhausted: exports_external.boolean(),
|
|
13798
13823
|
exhausted_until: exports_external.number().optional(),
|
|
13824
|
+
throttled_until: exports_external.number().optional(),
|
|
13799
13825
|
threshold_violations: exports_external.number().int().nonnegative().optional(),
|
|
13800
13826
|
last_refreshed_at: exports_external.number().optional()
|
|
13801
13827
|
});
|
|
@@ -13826,6 +13852,12 @@ var MarkExhaustedDataSchema = exports_external.object({
|
|
|
13826
13852
|
rolled: exports_external.array(exports_external.string()),
|
|
13827
13853
|
rolledTo: exports_external.string().nullable().optional()
|
|
13828
13854
|
});
|
|
13855
|
+
var MarkThrottledDataSchema = exports_external.object({
|
|
13856
|
+
account: exports_external.string(),
|
|
13857
|
+
throttled_until: exports_external.number(),
|
|
13858
|
+
escalated: exports_external.boolean(),
|
|
13859
|
+
rolledTo: exports_external.string().nullable().optional()
|
|
13860
|
+
});
|
|
13829
13861
|
var RefreshAccountDataSchema = exports_external.object({
|
|
13830
13862
|
account: exports_external.string(),
|
|
13831
13863
|
expiresAt: exports_external.number().optional()
|
|
@@ -14051,6 +14083,15 @@ class AuthBrokerClient {
|
|
|
14051
14083
|
const data = await this.send(req);
|
|
14052
14084
|
return data;
|
|
14053
14085
|
}
|
|
14086
|
+
async markThrottled(until) {
|
|
14087
|
+
const data = await this.send({
|
|
14088
|
+
v: PROTOCOL_VERSION,
|
|
14089
|
+
id: randomUUID(),
|
|
14090
|
+
op: "mark-throttled",
|
|
14091
|
+
until
|
|
14092
|
+
});
|
|
14093
|
+
return data;
|
|
14094
|
+
}
|
|
14054
14095
|
async claimNotification(key, windowMs) {
|
|
14055
14096
|
const data = await this.send({
|
|
14056
14097
|
v: PROTOCOL_VERSION,
|
|
@@ -14721,6 +14762,12 @@ var DEFAULT_MAX_QUOTA_DEFER_ATTEMPTS = 3;
|
|
|
14721
14762
|
function defaultQuotaDeferBackoffMs(attempt) {
|
|
14722
14763
|
return [60000, 180000, 300000][attempt] ?? 300000;
|
|
14723
14764
|
}
|
|
14765
|
+
function resolveQuotaDeferDelayMs(decision, fallbackMs, nowMs) {
|
|
14766
|
+
if (decision.retryAtMs === undefined)
|
|
14767
|
+
return fallbackMs;
|
|
14768
|
+
const target = decision.retryAtMs - nowMs + 2000;
|
|
14769
|
+
return Math.min(Math.max(target, 5000), 10 * 60000);
|
|
14770
|
+
}
|
|
14724
14771
|
function registerAgentSchedule(opts) {
|
|
14725
14772
|
const tasks = [];
|
|
14726
14773
|
const now = opts.now ?? Date.now;
|
|
@@ -14761,7 +14808,7 @@ function registerAgentSchedule(opts) {
|
|
|
14761
14808
|
handle = scheduleRetry(() => {
|
|
14762
14809
|
pendingRetries.delete(handle);
|
|
14763
14810
|
attemptFire(attempt + 1);
|
|
14764
|
-
}, backoff(attempt));
|
|
14811
|
+
}, resolveQuotaDeferDelayMs(decision, backoff(attempt), now()));
|
|
14765
14812
|
pendingRetries.add(handle);
|
|
14766
14813
|
}
|
|
14767
14814
|
return;
|
|
@@ -15116,10 +15163,10 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
15116
15163
|
}
|
|
15117
15164
|
const cronLib = __require("node-cron");
|
|
15118
15165
|
const quotaPreflightEnabled = process.env.SWITCHROOM_DISABLE_CRON_QUOTA_PREFLIGHT !== "1";
|
|
15119
|
-
const quotaGate = quotaPreflightEnabled ? async () => {
|
|
15166
|
+
const quotaGate = quotaPreflightEnabled ? async (agent) => {
|
|
15120
15167
|
const client = new AuthBrokerClient;
|
|
15121
15168
|
try {
|
|
15122
|
-
return decideQuotaPreflight(await client.listState());
|
|
15169
|
+
return decideQuotaPreflight(await client.listState(), { agent });
|
|
15123
15170
|
} finally {
|
|
15124
15171
|
await client.close().catch(() => {});
|
|
15125
15172
|
}
|
|
@@ -15198,6 +15245,7 @@ if (import.meta.url === `file://${process.argv[1]}` && /(?:^|[/\\])agent-schedul
|
|
|
15198
15245
|
export {
|
|
15199
15246
|
scheduleSignature,
|
|
15200
15247
|
resolveReloadPollMs,
|
|
15248
|
+
resolveQuotaDeferDelayMs,
|
|
15201
15249
|
resolveEntryThreadId,
|
|
15202
15250
|
resolveChannelTarget,
|
|
15203
15251
|
registerAgentSchedule,
|
|
@@ -7010,6 +7010,41 @@ var require_dist = __commonJS((exports) => {
|
|
|
7010
7010
|
exports.visitAsync = visit.visitAsync;
|
|
7011
7011
|
});
|
|
7012
7012
|
|
|
7013
|
+
// src/util/atomic.ts
|
|
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) {
|
|
7017
|
+
const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
7018
|
+
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
7019
|
+
let fd = null;
|
|
7020
|
+
try {
|
|
7021
|
+
fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
|
|
7022
|
+
writeSync(fd, buf, 0, buf.length, 0);
|
|
7023
|
+
fsyncSync(fd);
|
|
7024
|
+
closeSync(fd);
|
|
7025
|
+
fd = null;
|
|
7026
|
+
renameSync(tmp, destPath);
|
|
7027
|
+
} catch (err) {
|
|
7028
|
+
if (fd !== null) {
|
|
7029
|
+
try {
|
|
7030
|
+
closeSync(fd);
|
|
7031
|
+
} catch {}
|
|
7032
|
+
}
|
|
7033
|
+
try {
|
|
7034
|
+
rmSync(tmp, { force: true });
|
|
7035
|
+
} catch {}
|
|
7036
|
+
throw err;
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
7039
|
+
function atomicWriteJsonSync(destPath, value, mode = 384) {
|
|
7040
|
+
atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
|
|
7041
|
+
`, mode);
|
|
7042
|
+
}
|
|
7043
|
+
var TMP_OPEN_FLAGS;
|
|
7044
|
+
var init_atomic = __esm(() => {
|
|
7045
|
+
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
7046
|
+
});
|
|
7047
|
+
|
|
7013
7048
|
// node_modules/.bun/handlebars@4.7.9/node_modules/handlebars/dist/cjs/handlebars/utils.js
|
|
7014
7049
|
var require_utils = __commonJS((exports) => {
|
|
7015
7050
|
exports.__esModule = true;
|
|
@@ -12570,41 +12605,6 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
12570
12605
|
}
|
|
12571
12606
|
});
|
|
12572
12607
|
|
|
12573
|
-
// src/util/atomic.ts
|
|
12574
|
-
import { randomBytes } from "node:crypto";
|
|
12575
|
-
import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
12576
|
-
function atomicWriteFileSync(destPath, contents, mode = 384) {
|
|
12577
|
-
const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
12578
|
-
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
12579
|
-
let fd = null;
|
|
12580
|
-
try {
|
|
12581
|
-
fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
|
|
12582
|
-
writeSync(fd, buf, 0, buf.length, 0);
|
|
12583
|
-
fsyncSync(fd);
|
|
12584
|
-
closeSync(fd);
|
|
12585
|
-
fd = null;
|
|
12586
|
-
renameSync(tmp, destPath);
|
|
12587
|
-
} catch (err) {
|
|
12588
|
-
if (fd !== null) {
|
|
12589
|
-
try {
|
|
12590
|
-
closeSync(fd);
|
|
12591
|
-
} catch {}
|
|
12592
|
-
}
|
|
12593
|
-
try {
|
|
12594
|
-
rmSync(tmp, { force: true });
|
|
12595
|
-
} catch {}
|
|
12596
|
-
throw err;
|
|
12597
|
-
}
|
|
12598
|
-
}
|
|
12599
|
-
function atomicWriteJsonSync(destPath, value, mode = 384) {
|
|
12600
|
-
atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
|
|
12601
|
-
`, mode);
|
|
12602
|
-
}
|
|
12603
|
-
var TMP_OPEN_FLAGS;
|
|
12604
|
-
var init_atomic = __esm(() => {
|
|
12605
|
-
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
12606
|
-
});
|
|
12607
|
-
|
|
12608
12608
|
// src/auth/broker/index.ts
|
|
12609
12609
|
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
12610
12610
|
|
|
@@ -16816,7 +16816,6 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
16816
16816
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
16817
16817
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
16818
16818
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
16819
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
16820
16819
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
16821
16820
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
16822
16821
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
|
@@ -18112,7 +18111,7 @@ import {
|
|
|
18112
18111
|
rmSync as rmSync5,
|
|
18113
18112
|
statSync as statSync6,
|
|
18114
18113
|
unlinkSync,
|
|
18115
|
-
writeFileSync as
|
|
18114
|
+
writeFileSync as writeFileSync2
|
|
18116
18115
|
} from "node:fs";
|
|
18117
18116
|
import { closeSync as closeSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
|
|
18118
18117
|
import * as constants2 from "node:constants";
|
|
@@ -18124,6 +18123,7 @@ import { createHash } from "node:crypto";
|
|
|
18124
18123
|
|
|
18125
18124
|
// src/agents/scaffold.ts
|
|
18126
18125
|
import { join as join4, resolve as resolve5 } from "node:path";
|
|
18126
|
+
init_atomic();
|
|
18127
18127
|
|
|
18128
18128
|
// src/config/timezone.ts
|
|
18129
18129
|
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
@@ -18390,6 +18390,13 @@ var ApprovalLookupRequestSchema = exports_external.object({
|
|
|
18390
18390
|
action: exports_external.string().min(1),
|
|
18391
18391
|
current_approver_set: exports_external.array(exports_external.string())
|
|
18392
18392
|
});
|
|
18393
|
+
var ApprovalLookupByRequestRequestSchema = exports_external.object({
|
|
18394
|
+
v: exports_external.literal(1),
|
|
18395
|
+
op: exports_external.literal("approval_lookup_by_request"),
|
|
18396
|
+
agent_unit: exports_external.string().min(1),
|
|
18397
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
18398
|
+
current_approver_set: exports_external.array(exports_external.string())
|
|
18399
|
+
});
|
|
18393
18400
|
var ApprovalConsumeRequestSchema = exports_external.object({
|
|
18394
18401
|
v: exports_external.literal(1),
|
|
18395
18402
|
op: exports_external.literal("approval_consume"),
|
|
@@ -18444,6 +18451,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
18444
18451
|
RevokeGrantRequestSchema,
|
|
18445
18452
|
ApprovalRequestRequestSchema,
|
|
18446
18453
|
ApprovalLookupRequestSchema,
|
|
18454
|
+
ApprovalLookupByRequestRequestSchema,
|
|
18447
18455
|
ApprovalConsumeRequestSchema,
|
|
18448
18456
|
ApprovalRevokeRequestSchema,
|
|
18449
18457
|
ApprovalListRequestSchema,
|
|
@@ -18643,9 +18651,6 @@ var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
|
18643
18651
|
var OAUTH_BETA = "oauth-2025-04-20";
|
|
18644
18652
|
var DEFAULT_USER_AGENT = "claude-cli/1.0.0 (external, cli)";
|
|
18645
18653
|
var DEFAULT_PROBE_MODEL = "claude-haiku-4-5-20251001";
|
|
18646
|
-
function isProbeThin(q) {
|
|
18647
|
-
return q.fiveHourUtilPresent === false && q.sevenDayUtilPresent === false;
|
|
18648
|
-
}
|
|
18649
18654
|
function parseFloatHeader(headers, name) {
|
|
18650
18655
|
const v = headers.get(name);
|
|
18651
18656
|
if (v == null || v.trim().length === 0)
|
|
@@ -18742,6 +18747,9 @@ function snapshotFresh(s, now, maxAgeMs = SNAPSHOT_STALE_AGE_MS) {
|
|
|
18742
18747
|
function snapshotWalled(s) {
|
|
18743
18748
|
return s.fiveHourUtilizationPct >= WALL_PCT || s.sevenDayUtilizationPct >= WALL_PCT;
|
|
18744
18749
|
}
|
|
18750
|
+
function snapshotComplete(s) {
|
|
18751
|
+
return s.fiveHourUtilPresent !== false && s.sevenDayUtilPresent !== false;
|
|
18752
|
+
}
|
|
18745
18753
|
function overageLiftsWall(snapshot, inAllowList) {
|
|
18746
18754
|
if (!inAllowList)
|
|
18747
18755
|
return false;
|
|
@@ -18757,16 +18765,19 @@ function snapshotClearlyHealthy(s) {
|
|
|
18757
18765
|
}
|
|
18758
18766
|
function accountEligibility(opts) {
|
|
18759
18767
|
const { mark, snapshot, now, allowOverage = false } = opts;
|
|
18768
|
+
if (snapshotFresh(snapshot, now) && snapshotWalled(snapshot) && !overageLiftsWall(snapshot, allowOverage)) {
|
|
18769
|
+
return "blocked";
|
|
18770
|
+
}
|
|
18760
18771
|
if (snapshotFresh(snapshot, now)) {
|
|
18761
18772
|
const markedAt = mark?.marked_at ?? 0;
|
|
18762
18773
|
if (snapshot.capturedAt >= markedAt) {
|
|
18763
18774
|
if (snapshotWalled(snapshot)) {
|
|
18764
|
-
if (overageLiftsWall(snapshot, allowOverage))
|
|
18775
|
+
if (overageLiftsWall(snapshot, allowOverage))
|
|
18765
18776
|
return "eligible";
|
|
18766
|
-
}
|
|
18767
18777
|
return "blocked";
|
|
18768
18778
|
}
|
|
18769
|
-
|
|
18779
|
+
if (snapshotComplete(snapshot))
|
|
18780
|
+
return "eligible";
|
|
18770
18781
|
}
|
|
18771
18782
|
}
|
|
18772
18783
|
if (mark !== undefined && mark.exhausted_until > now)
|
|
@@ -18783,7 +18794,7 @@ function snapshotShouldClearMark(snapshot, mark, now) {
|
|
|
18783
18794
|
return false;
|
|
18784
18795
|
if (snapshot.capturedAt < (mark.marked_at ?? 0))
|
|
18785
18796
|
return false;
|
|
18786
|
-
if (
|
|
18797
|
+
if (!snapshotComplete(snapshot))
|
|
18787
18798
|
return false;
|
|
18788
18799
|
return snapshotClearlyHealthy(snapshot);
|
|
18789
18800
|
}
|
|
@@ -18792,7 +18803,7 @@ function clampMarkExpiry(opts) {
|
|
|
18792
18803
|
const shortCeil = now + shortMs;
|
|
18793
18804
|
if (proposedUntil <= shortCeil)
|
|
18794
18805
|
return proposedUntil;
|
|
18795
|
-
const liveContradictsWeeklyWall = snapshotFresh(snapshot, now) && snapshot.sevenDayUtilizationPct < WALL_PCT;
|
|
18806
|
+
const liveContradictsWeeklyWall = snapshotFresh(snapshot, now) && snapshot.sevenDayUtilPresent !== false && snapshot.sevenDayUtilizationPct < WALL_PCT;
|
|
18796
18807
|
return liveContradictsWeeklyWall ? shortCeil : proposedUntil;
|
|
18797
18808
|
}
|
|
18798
18809
|
var SOFT_AVOID_FIVE_HOUR_CAP = 98;
|
|
@@ -18885,6 +18896,7 @@ function resolveConsumerProbeIntervalMs(env) {
|
|
|
18885
18896
|
init_atomic();
|
|
18886
18897
|
|
|
18887
18898
|
// src/auth/account-store.ts
|
|
18899
|
+
init_atomic();
|
|
18888
18900
|
import {
|
|
18889
18901
|
chownSync,
|
|
18890
18902
|
existsSync as existsSync5,
|
|
@@ -18893,10 +18905,8 @@ import {
|
|
|
18893
18905
|
readdirSync as readdirSync3,
|
|
18894
18906
|
renameSync as renameSync2,
|
|
18895
18907
|
rmSync as rmSync2,
|
|
18896
|
-
statSync as statSync3
|
|
18897
|
-
writeFileSync as writeFileSync2
|
|
18908
|
+
statSync as statSync3
|
|
18898
18909
|
} from "node:fs";
|
|
18899
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
18900
18910
|
import { homedir as homedir4 } from "node:os";
|
|
18901
18911
|
import { join as join5, resolve as resolve6 } from "node:path";
|
|
18902
18912
|
var LABEL_MAX = 64;
|
|
@@ -18980,8 +18990,8 @@ function enrichClaudeCreds(value) {
|
|
|
18980
18990
|
}
|
|
18981
18991
|
function writeAccountCredentials(label, value, home2 = homedir4()) {
|
|
18982
18992
|
validateAccountLabel(label);
|
|
18983
|
-
mkdirSync2(accountDir(label, home2), { recursive: true });
|
|
18984
|
-
|
|
18993
|
+
mkdirSync2(accountDir(label, home2), { recursive: true, mode: 448 });
|
|
18994
|
+
atomicWriteJsonSync(accountCredentialsPath(label, home2), enrichClaudeCreds(value));
|
|
18985
18995
|
}
|
|
18986
18996
|
function chownAccountFiles(label, uid, home2 = homedir4()) {
|
|
18987
18997
|
validateAccountLabel(label);
|
|
@@ -19007,8 +19017,8 @@ function readAccountMeta(label, home2 = homedir4()) {
|
|
|
19007
19017
|
}
|
|
19008
19018
|
function writeAccountMeta(label, value, home2 = homedir4()) {
|
|
19009
19019
|
validateAccountLabel(label);
|
|
19010
|
-
mkdirSync2(accountDir(label, home2), { recursive: true });
|
|
19011
|
-
|
|
19020
|
+
mkdirSync2(accountDir(label, home2), { recursive: true, mode: 448 });
|
|
19021
|
+
atomicWriteJsonSync(accountMetaPath(label, home2), value);
|
|
19012
19022
|
}
|
|
19013
19023
|
function patchAccountMeta(label, patch, home2 = homedir4()) {
|
|
19014
19024
|
const existing = readAccountMeta(label, home2) ?? { createdAt: Date.now() };
|
|
@@ -19016,19 +19026,6 @@ function patchAccountMeta(label, patch, home2 = homedir4()) {
|
|
|
19016
19026
|
writeAccountMeta(label, merged, home2);
|
|
19017
19027
|
return merged;
|
|
19018
19028
|
}
|
|
19019
|
-
function atomicWriteJson(destPath, value, mode = 384) {
|
|
19020
|
-
const tmp = `${destPath}.tmp-${process.pid}-${randomBytes2(4).toString("hex")}`;
|
|
19021
|
-
try {
|
|
19022
|
-
writeFileSync2(tmp, JSON.stringify(value, null, 2) + `
|
|
19023
|
-
`, { mode });
|
|
19024
|
-
renameSync2(tmp, destPath);
|
|
19025
|
-
} catch (err) {
|
|
19026
|
-
try {
|
|
19027
|
-
rmSync2(tmp, { force: true });
|
|
19028
|
-
} catch {}
|
|
19029
|
-
throw err;
|
|
19030
|
-
}
|
|
19031
|
-
}
|
|
19032
19029
|
|
|
19033
19030
|
// src/auth/account-refresh.ts
|
|
19034
19031
|
var REFRESH_THRESHOLD_MS = 60 * 60 * 1000;
|
|
@@ -19767,6 +19764,12 @@ var MarkExhaustedRequestSchema = exports_external.object({
|
|
|
19767
19764
|
id: exports_external.string().min(1),
|
|
19768
19765
|
until: exports_external.number().int().positive().optional()
|
|
19769
19766
|
});
|
|
19767
|
+
var MarkThrottledRequestSchema = exports_external.object({
|
|
19768
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
19769
|
+
op: exports_external.literal("mark-throttled"),
|
|
19770
|
+
id: exports_external.string().min(1),
|
|
19771
|
+
until: exports_external.number().int().positive()
|
|
19772
|
+
});
|
|
19770
19773
|
var RefreshAccountRequestSchema = exports_external.object({
|
|
19771
19774
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
19772
19775
|
op: exports_external.literal("refresh-account"),
|
|
@@ -19867,6 +19870,7 @@ var RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
|
19867
19870
|
ListStateRequestSchema,
|
|
19868
19871
|
SetActiveRequestSchema,
|
|
19869
19872
|
MarkExhaustedRequestSchema,
|
|
19873
|
+
MarkThrottledRequestSchema,
|
|
19870
19874
|
RefreshAccountRequestSchema,
|
|
19871
19875
|
AddAccountRequestSchema,
|
|
19872
19876
|
RmAccountRequestSchema,
|
|
@@ -19886,6 +19890,7 @@ var AccountStateSchema = exports_external.object({
|
|
|
19886
19890
|
expiresAt: exports_external.number().optional(),
|
|
19887
19891
|
exhausted: exports_external.boolean(),
|
|
19888
19892
|
exhausted_until: exports_external.number().optional(),
|
|
19893
|
+
throttled_until: exports_external.number().optional(),
|
|
19889
19894
|
threshold_violations: exports_external.number().int().nonnegative().optional(),
|
|
19890
19895
|
last_refreshed_at: exports_external.number().optional()
|
|
19891
19896
|
});
|
|
@@ -19916,6 +19921,12 @@ var MarkExhaustedDataSchema = exports_external.object({
|
|
|
19916
19921
|
rolled: exports_external.array(exports_external.string()),
|
|
19917
19922
|
rolledTo: exports_external.string().nullable().optional()
|
|
19918
19923
|
});
|
|
19924
|
+
var MarkThrottledDataSchema = exports_external.object({
|
|
19925
|
+
account: exports_external.string(),
|
|
19926
|
+
throttled_until: exports_external.number(),
|
|
19927
|
+
escalated: exports_external.boolean(),
|
|
19928
|
+
rolledTo: exports_external.string().nullable().optional()
|
|
19929
|
+
});
|
|
19919
19930
|
var RefreshAccountDataSchema = exports_external.object({
|
|
19920
19931
|
account: exports_external.string(),
|
|
19921
19932
|
expiresAt: exports_external.number().optional()
|
|
@@ -20017,6 +20028,10 @@ function encodeError(id, code, message) {
|
|
|
20017
20028
|
var AUTH_BROKER_ROOT = "/run/switchroom/auth-broker";
|
|
20018
20029
|
var REFRESH_TICK_INTERVAL_MS = 60 * 1000;
|
|
20019
20030
|
var MARK_EXHAUSTED_DEFAULT_MS = 5 * 60 * 60 * 1000;
|
|
20031
|
+
var MARK_THROTTLED_MAX_MS = 30 * 60 * 1000;
|
|
20032
|
+
var THROTTLE_ESCALATION_HITS = 3;
|
|
20033
|
+
var THROTTLE_ESCALATION_WINDOW_MS = 10 * 60 * 1000;
|
|
20034
|
+
var MARK_THROTTLED_MIN_INTERVAL_MS = 5 * 1000;
|
|
20020
20035
|
var AUDIT_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
20021
20036
|
var AUDIT_KEEP = 5;
|
|
20022
20037
|
var AUDIT_LINE_MAX = 4000;
|
|
@@ -20195,7 +20210,7 @@ class AuthBroker {
|
|
|
20195
20210
|
if (!this.opts.skipHealthyMarker) {
|
|
20196
20211
|
try {
|
|
20197
20212
|
const healthyPath = join8(this.stateDir, "healthy");
|
|
20198
|
-
|
|
20213
|
+
writeFileSync2(healthyPath, String(this.now()) + `
|
|
20199
20214
|
`, { mode: 384 });
|
|
20200
20215
|
} catch (err) {
|
|
20201
20216
|
this.logErr(`failed to write healthy marker: ${err.message}`);
|
|
@@ -20432,6 +20447,9 @@ class AuthBroker {
|
|
|
20432
20447
|
case "mark-exhausted":
|
|
20433
20448
|
await this.opMarkExhausted(socket, reqId, identity, req.until);
|
|
20434
20449
|
break;
|
|
20450
|
+
case "mark-throttled":
|
|
20451
|
+
await this.opMarkThrottled(socket, reqId, identity, req.until);
|
|
20452
|
+
break;
|
|
20435
20453
|
case "refresh-account": {
|
|
20436
20454
|
const provider = req.provider ?? "anthropic";
|
|
20437
20455
|
if (!this.providers.has(provider)) {
|
|
@@ -20552,9 +20570,15 @@ class AuthBroker {
|
|
|
20552
20570
|
isOverageAllowed(account) {
|
|
20553
20571
|
return (this.config.auth?.allow_overage_accounts ?? []).includes(account);
|
|
20554
20572
|
}
|
|
20573
|
+
exhaustionMarkOf(account) {
|
|
20574
|
+
const q = this.quota[account];
|
|
20575
|
+
if (!q || q.exhausted_until === undefined)
|
|
20576
|
+
return;
|
|
20577
|
+
return { exhausted_until: q.exhausted_until, marked_at: q.marked_at };
|
|
20578
|
+
}
|
|
20555
20579
|
isAccountExhausted(account) {
|
|
20556
20580
|
return isAccountBlocked({
|
|
20557
|
-
mark: this.
|
|
20581
|
+
mark: this.exhaustionMarkOf(account),
|
|
20558
20582
|
snapshot: this.lastQuotaCache[account],
|
|
20559
20583
|
now: this.now(),
|
|
20560
20584
|
allowOverage: this.isOverageAllowed(account)
|
|
@@ -20607,7 +20631,7 @@ class AuthBroker {
|
|
|
20607
20631
|
if (!overageLiftsWall(snapshot, true))
|
|
20608
20632
|
return false;
|
|
20609
20633
|
return accountEligibility({
|
|
20610
|
-
mark: this.
|
|
20634
|
+
mark: this.exhaustionMarkOf(account),
|
|
20611
20635
|
snapshot,
|
|
20612
20636
|
now,
|
|
20613
20637
|
allowOverage: true
|
|
@@ -20617,7 +20641,7 @@ class AuthBroker {
|
|
|
20617
20641
|
const snapshot = this.lastQuotaCache[account];
|
|
20618
20642
|
const allowOverage = this.isOverageAllowed(account);
|
|
20619
20643
|
const verdict = accountEligibility({
|
|
20620
|
-
mark: this.
|
|
20644
|
+
mark: this.exhaustionMarkOf(account),
|
|
20621
20645
|
snapshot,
|
|
20622
20646
|
now: this.now(),
|
|
20623
20647
|
allowOverage
|
|
@@ -20735,6 +20759,7 @@ class AuthBroker {
|
|
|
20735
20759
|
expiresAt: creds?.claudeAiOauth?.expiresAt,
|
|
20736
20760
|
exhausted,
|
|
20737
20761
|
exhausted_until: q?.exhausted_until,
|
|
20762
|
+
throttled_until: q?.throttled_until,
|
|
20738
20763
|
threshold_violations: this.thresholdViolations[label] ?? 0,
|
|
20739
20764
|
last_refreshed_at: meta?.lastRefreshedAt,
|
|
20740
20765
|
last_quota: lq ?? null
|
|
@@ -20845,7 +20870,7 @@ class AuthBroker {
|
|
|
20845
20870
|
};
|
|
20846
20871
|
this.lastQuotaCache[label] = snapshot;
|
|
20847
20872
|
this.persistLastQuotaCache();
|
|
20848
|
-
if (snapshotShouldClearMark(snapshot, this.
|
|
20873
|
+
if (snapshotShouldClearMark(snapshot, this.exhaustionMarkOf(label), this.now())) {
|
|
20849
20874
|
delete this.quota[label];
|
|
20850
20875
|
this.persistQuota();
|
|
20851
20876
|
process.stdout.write(`auth-broker: live probe shows ${label} healthy (5h=${snapshot.fiveHourUtilizationPct}% 7d=${snapshot.sevenDayUtilizationPct}%) — cleared stale exhaustion mark
|
|
@@ -20963,7 +20988,7 @@ class AuthBroker {
|
|
|
20963
20988
|
const existing = this.quota[label]?.exhausted_until;
|
|
20964
20989
|
if (existing !== undefined && existing >= exhaustedUntil)
|
|
20965
20990
|
continue;
|
|
20966
|
-
this.quota[label] = { exhausted_until: exhaustedUntil, marked_at: now };
|
|
20991
|
+
this.quota[label] = { ...this.quota[label], exhausted_until: exhaustedUntil, marked_at: now };
|
|
20967
20992
|
this.persistQuota();
|
|
20968
20993
|
this.audit({ op: "mark-exhausted", identity: { kind: "operator" }, account: label, accountKind: "claude", ok: true });
|
|
20969
20994
|
process.stdout.write(`auth-broker: consumer-quota-sensor marked ${label} exhausted until ${new Date(exhaustedUntil).toISOString()} — consumer(s) fail over
|
|
@@ -21004,6 +21029,83 @@ class AuthBroker {
|
|
|
21004
21029
|
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
|
|
21005
21030
|
socket.write(encodeSuccess(id, { account, rolled, rolledTo }));
|
|
21006
21031
|
}
|
|
21032
|
+
async opMarkThrottled(socket, id, identity, until) {
|
|
21033
|
+
const account = this.callerAccount(identity);
|
|
21034
|
+
if (!account) {
|
|
21035
|
+
this.audit({ op: "mark-throttled", identity, accountKind: "claude", ok: false, error: "no-active-account" });
|
|
21036
|
+
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", "no active account configured"));
|
|
21037
|
+
return;
|
|
21038
|
+
}
|
|
21039
|
+
const now = this.now();
|
|
21040
|
+
const throttledUntil = Math.min(Math.max(until, now + 1000), now + MARK_THROTTLED_MAX_MS);
|
|
21041
|
+
const entry = this.quota[account] ?? {};
|
|
21042
|
+
const priorHits = entry.throttle_hits ?? [];
|
|
21043
|
+
const lastHit = priorHits.length > 0 ? priorHits[priorHits.length - 1] : undefined;
|
|
21044
|
+
if (lastHit !== undefined && now - lastHit < MARK_THROTTLED_MIN_INTERVAL_MS) {
|
|
21045
|
+
const refreshed = Math.max(entry.throttled_until ?? 0, throttledUntil);
|
|
21046
|
+
this.quota[account] = { ...entry, throttled_until: refreshed };
|
|
21047
|
+
this.persistQuota();
|
|
21048
|
+
this.audit({ op: "mark-throttled", identity, account, accountKind: "claude", ok: true });
|
|
21049
|
+
process.stdout.write(`auth-broker: mark-throttled ${account} deduped (re-mark within ${MARK_THROTTLED_MIN_INTERVAL_MS / 1000}s) — expiry refreshed, no new hit
|
|
21050
|
+
`);
|
|
21051
|
+
socket.write(encodeSuccess(id, {
|
|
21052
|
+
account,
|
|
21053
|
+
throttled_until: refreshed,
|
|
21054
|
+
escalated: false,
|
|
21055
|
+
rolledTo: null
|
|
21056
|
+
}));
|
|
21057
|
+
return;
|
|
21058
|
+
}
|
|
21059
|
+
const hits = priorHits.filter((t) => now - t < THROTTLE_ESCALATION_WINDOW_MS);
|
|
21060
|
+
hits.push(now);
|
|
21061
|
+
const escalate = hits.length >= THROTTLE_ESCALATION_HITS;
|
|
21062
|
+
this.quota[account] = {
|
|
21063
|
+
...entry,
|
|
21064
|
+
throttled_until: throttledUntil,
|
|
21065
|
+
throttle_hits: escalate ? [] : hits
|
|
21066
|
+
};
|
|
21067
|
+
this.persistQuota();
|
|
21068
|
+
this.audit({ op: "mark-throttled", identity, account, accountKind: "claude", ok: true });
|
|
21069
|
+
process.stdout.write(`auth-broker: mark-throttled ${account} until ${new Date(throttledUntil).toISOString()} ` + `(hit ${hits.length}/${THROTTLE_ESCALATION_HITS} in window)
|
|
21070
|
+
`);
|
|
21071
|
+
let escalated = false;
|
|
21072
|
+
let rolledTo = null;
|
|
21073
|
+
if (escalate) {
|
|
21074
|
+
const probe = await this.probeThrottleEscalation(account);
|
|
21075
|
+
if (probe.exhausted) {
|
|
21076
|
+
escalated = true;
|
|
21077
|
+
process.stdout.write(`auth-broker: throttle-escalation probe corroborates wall on ${account} — mark-exhausted + roll
|
|
21078
|
+
`);
|
|
21079
|
+
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true, reason: "throttle-escalation" });
|
|
21080
|
+
const roll = await this.markExhaustedAndRoll(account, probe.until ?? undefined, identity);
|
|
21081
|
+
rolledTo = roll.rolledTo;
|
|
21082
|
+
} else {
|
|
21083
|
+
process.stdout.write(`auth-broker: throttle-escalation probe on ${account} did NOT corroborate a wall — staying throttled
|
|
21084
|
+
`);
|
|
21085
|
+
}
|
|
21086
|
+
}
|
|
21087
|
+
socket.write(encodeSuccess(id, {
|
|
21088
|
+
account,
|
|
21089
|
+
throttled_until: throttledUntil,
|
|
21090
|
+
escalated,
|
|
21091
|
+
rolledTo: escalated ? rolledTo : null
|
|
21092
|
+
}));
|
|
21093
|
+
}
|
|
21094
|
+
async probeThrottleEscalation(account) {
|
|
21095
|
+
const creds = readAccountCredentials(account, this.home);
|
|
21096
|
+
const token = creds?.claudeAiOauth?.accessToken;
|
|
21097
|
+
if (!token)
|
|
21098
|
+
return { exhausted: false, until: null };
|
|
21099
|
+
let result;
|
|
21100
|
+
try {
|
|
21101
|
+
result = await this.fetchQuotaImpl({ accessToken: token });
|
|
21102
|
+
} catch (err) {
|
|
21103
|
+
this.logErr(`throttle-escalation probe ${account}: ${err.message}`);
|
|
21104
|
+
return { exhausted: false, until: null };
|
|
21105
|
+
}
|
|
21106
|
+
this.cacheQuotaSnapshot(account, result);
|
|
21107
|
+
return quotaIndicatesExhaustion(result, this.isOverageAllowed(account));
|
|
21108
|
+
}
|
|
21007
21109
|
async markExhaustedAndRoll(account, until, identity) {
|
|
21008
21110
|
const now = this.now();
|
|
21009
21111
|
const exhaustedUntil = clampMarkExpiry({
|
|
@@ -21012,7 +21114,7 @@ class AuthBroker {
|
|
|
21012
21114
|
shortMs: MARK_EXHAUSTED_DEFAULT_MS,
|
|
21013
21115
|
snapshot: this.lastQuotaCache[account]
|
|
21014
21116
|
});
|
|
21015
|
-
this.quota[account] = { exhausted_until: exhaustedUntil, marked_at: now };
|
|
21117
|
+
this.quota[account] = { ...this.quota[account], exhausted_until: exhaustedUntil, marked_at: now };
|
|
21016
21118
|
this.persistQuota();
|
|
21017
21119
|
const rolledTo = await this.nextHealthyAccountLive(account, this.config.auth?.fallback_order ?? []);
|
|
21018
21120
|
const rolled = this.fanoutFailoverTo(account, rolledTo);
|