switchroom 0.20.7 → 0.20.9
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 +111 -14
- package/dist/auth-broker/index.js +113 -30
- package/dist/cli/autoaccept-poll.js +5 -3
- package/dist/cli/drive-write-pretool.mjs +5 -3
- package/dist/cli/ms-365-write-pretool.mjs +5 -3
- package/dist/cli/notion-write-pretool.mjs +67 -6
- package/dist/cli/switchroom.js +389 -31
- package/dist/host-control/main.js +69 -8
- package/dist/vault/approvals/kernel-server.js +68 -7
- package/dist/vault/broker/server.js +68 -7
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +12 -13
- package/telegram-plugin/ask-user.ts +6 -7
- package/telegram-plugin/bridge/ipc-client.ts +17 -1
- package/telegram-plugin/dist/bridge/bridge.js +5 -2
- package/telegram-plugin/dist/gateway/gateway.js +410 -178
- package/telegram-plugin/dist/server.js +5 -2
- package/telegram-plugin/gateway/auth-broker-client.ts +1 -1
- package/telegram-plugin/gateway/auth-command.ts +4 -2
- package/telegram-plugin/gateway/boot-reason.ts +61 -0
- package/telegram-plugin/gateway/checklist-fallback.ts +8 -1
- package/telegram-plugin/gateway/cron-session.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +36 -34
- package/telegram-plugin/gateway/narrative-lane.ts +21 -1
- package/telegram-plugin/gateway/outbound-send-path.ts +9 -1
- package/telegram-plugin/gateway/represent-delivery-guard.ts +33 -2
- package/telegram-plugin/gateway/stream-render.ts +11 -2
- package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +21 -1
- package/telegram-plugin/gateway/subagent-handback-marker.ts +94 -0
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +93 -18
- package/telegram-plugin/render/emphasis-guard.ts +92 -12
- package/telegram-plugin/render/line-start-guard.ts +27 -2
- package/telegram-plugin/sticker-aliases.ts +12 -14
- package/telegram-plugin/tests/ask-user.test.ts +15 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +88 -0
- package/telegram-plugin/tests/checklist-fallback.test.ts +21 -0
- package/telegram-plugin/tests/cron-bridge-drain-spool-ack.test.ts +150 -0
- package/telegram-plugin/tests/handback-tasknotif-dedup.test.ts +248 -0
- package/telegram-plugin/tests/ipc-client-reconnect-rejection.test.ts +70 -0
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +86 -0
- package/telegram-plugin/tests/queued-card-surface.test.ts +66 -0
- package/telegram-plugin/tests/render/emphasis-guard.test.ts +105 -6
- package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +123 -36
- package/telegram-plugin/tests/reply-quote-wire.test.ts +47 -0
- package/telegram-plugin/tests/represent-guard.test.ts +45 -0
- package/telegram-plugin/tests/sticker-aliases.test.ts +43 -0
- package/telegram-plugin/tests/throttle-tier-probe-only.test.ts +216 -0
- package/telegram-plugin/tests/throttle-tier-route-429-wiring.test.ts +92 -0
- package/telegram-plugin/tests/throttle-tier-route-429.test.ts +71 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +83 -0
- package/telegram-plugin/throttle-tier.ts +59 -0
- package/telegram-plugin/turn-flush-safety.ts +79 -0
|
@@ -12004,6 +12004,29 @@ var OverlayDocSchema = exports_external.object({
|
|
|
12004
12004
|
// src/config/overlay-loader.ts
|
|
12005
12005
|
var OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
|
|
12006
12006
|
var OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
|
|
12007
|
+
var OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
|
|
12008
|
+
function recordReadFailure(agentCfg, failure) {
|
|
12009
|
+
const node = agentCfg;
|
|
12010
|
+
const existing = node[OVERLAY_READ_FAILURES];
|
|
12011
|
+
if (Array.isArray(existing)) {
|
|
12012
|
+
existing.push(failure);
|
|
12013
|
+
return;
|
|
12014
|
+
}
|
|
12015
|
+
Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
|
|
12016
|
+
value: [failure],
|
|
12017
|
+
enumerable: false,
|
|
12018
|
+
configurable: true,
|
|
12019
|
+
writable: false
|
|
12020
|
+
});
|
|
12021
|
+
}
|
|
12022
|
+
function overlayReadFailures(config, agent, source) {
|
|
12023
|
+
const agentCfg = config.agents?.[agent];
|
|
12024
|
+
if (!agentCfg)
|
|
12025
|
+
return [];
|
|
12026
|
+
const list = agentCfg[OVERLAY_READ_FAILURES];
|
|
12027
|
+
const all = Array.isArray(list) ? list : [];
|
|
12028
|
+
return source ? all.filter((f) => f.source === source) : all;
|
|
12029
|
+
}
|
|
12007
12030
|
function deriveOverlayTitle(raw, fileName) {
|
|
12008
12031
|
const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
|
|
12009
12032
|
if (titleFromComment)
|
|
@@ -12013,17 +12036,39 @@ function deriveOverlayTitle(raw, fileName) {
|
|
|
12013
12036
|
return;
|
|
12014
12037
|
return base.length > 0 ? base : undefined;
|
|
12015
12038
|
}
|
|
12039
|
+
function readOverlayFile(agentName, file, agentCfg, warnings, source) {
|
|
12040
|
+
try {
|
|
12041
|
+
return readFileSync(file, "utf-8");
|
|
12042
|
+
} catch (err) {
|
|
12043
|
+
const code = err.code;
|
|
12044
|
+
if (code === "ENOENT")
|
|
12045
|
+
return;
|
|
12046
|
+
const w = {
|
|
12047
|
+
agent: agentName,
|
|
12048
|
+
file,
|
|
12049
|
+
reason: `read error: ${err.message}`,
|
|
12050
|
+
code: code ?? "EUNKNOWN"
|
|
12051
|
+
};
|
|
12052
|
+
recordReadFailure(agentCfg, { file, code: w.code, source });
|
|
12053
|
+
warnings.push(w);
|
|
12054
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
|
|
12055
|
+
return;
|
|
12056
|
+
}
|
|
12057
|
+
}
|
|
12016
12058
|
function overlayDirFor(agentName, subdir) {
|
|
12017
12059
|
const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
|
|
12018
12060
|
return resolve2(base);
|
|
12019
12061
|
}
|
|
12020
|
-
function listYamlFiles(dir) {
|
|
12062
|
+
function listYamlFiles(dir, onUnreadableDir) {
|
|
12021
12063
|
if (!existsSync2(dir))
|
|
12022
12064
|
return [];
|
|
12023
12065
|
let entries;
|
|
12024
12066
|
try {
|
|
12025
12067
|
entries = readdirSync(dir);
|
|
12026
|
-
} catch {
|
|
12068
|
+
} catch (err) {
|
|
12069
|
+
const code = err.code;
|
|
12070
|
+
if (code !== "ENOENT")
|
|
12071
|
+
onUnreadableDir?.(code ?? "EUNKNOWN");
|
|
12027
12072
|
return [];
|
|
12028
12073
|
}
|
|
12029
12074
|
const out = [];
|
|
@@ -12061,12 +12106,24 @@ function applyAgentOverlays(config) {
|
|
|
12061
12106
|
for (const [agentName, agentCfg] of Object.entries(agents)) {
|
|
12062
12107
|
try {
|
|
12063
12108
|
const scheduleDir = overlayDirFor(agentName, "schedule.d");
|
|
12064
|
-
const files = listYamlFiles(scheduleDir)
|
|
12109
|
+
const files = listYamlFiles(scheduleDir, (code) => {
|
|
12110
|
+
const w = {
|
|
12111
|
+
agent: agentName,
|
|
12112
|
+
file: scheduleDir,
|
|
12113
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
12114
|
+
code
|
|
12115
|
+
};
|
|
12116
|
+
recordReadFailure(agentCfg, { file: scheduleDir, code, source: "schedule" });
|
|
12117
|
+
warnings.push(w);
|
|
12118
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
|
|
12119
|
+
});
|
|
12065
12120
|
if (files.length > 0) {
|
|
12066
12121
|
const merged = [...agentCfg.schedule ?? []];
|
|
12067
12122
|
for (const file of files) {
|
|
12123
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "schedule");
|
|
12124
|
+
if (raw === undefined)
|
|
12125
|
+
continue;
|
|
12068
12126
|
try {
|
|
12069
|
-
const raw = readFileSync(file, "utf-8");
|
|
12070
12127
|
const parsed = $parse(raw);
|
|
12071
12128
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12072
12129
|
const title = deriveOverlayTitle(raw, basename(file));
|
|
@@ -12101,13 +12158,25 @@ function applyAgentOverlays(config) {
|
|
|
12101
12158
|
}
|
|
12102
12159
|
try {
|
|
12103
12160
|
const skillsDir = overlayDirFor(agentName, "skills.d");
|
|
12104
|
-
const skillFiles = listYamlFiles(skillsDir)
|
|
12161
|
+
const skillFiles = listYamlFiles(skillsDir, (code) => {
|
|
12162
|
+
const w = {
|
|
12163
|
+
agent: agentName,
|
|
12164
|
+
file: skillsDir,
|
|
12165
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
12166
|
+
code
|
|
12167
|
+
};
|
|
12168
|
+
recordReadFailure(agentCfg, { file: skillsDir, code, source: "skills" });
|
|
12169
|
+
warnings.push(w);
|
|
12170
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
|
|
12171
|
+
});
|
|
12105
12172
|
if (skillFiles.length === 0) {} else {
|
|
12106
12173
|
const merged = [...agentCfg.skills ?? []];
|
|
12107
12174
|
const seen = new Set(merged);
|
|
12108
12175
|
for (const file of skillFiles) {
|
|
12176
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "skills");
|
|
12177
|
+
if (raw === undefined)
|
|
12178
|
+
continue;
|
|
12109
12179
|
try {
|
|
12110
|
-
const raw = readFileSync(file, "utf-8");
|
|
12111
12180
|
const parsed = $parse(raw);
|
|
12112
12181
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12113
12182
|
for (const skillName of doc.skills ?? []) {
|
|
@@ -13975,7 +14044,8 @@ var MarkThrottledRequestSchema = exports_external.object({
|
|
|
13975
14044
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
13976
14045
|
op: exports_external.literal("mark-throttled"),
|
|
13977
14046
|
id: exports_external.string().min(1),
|
|
13978
|
-
until: exports_external.number().int().positive()
|
|
14047
|
+
until: exports_external.number().int().positive(),
|
|
14048
|
+
probeOnly: exports_external.boolean().optional()
|
|
13979
14049
|
});
|
|
13980
14050
|
var RefreshAccountRequestSchema = exports_external.object({
|
|
13981
14051
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -14392,12 +14462,13 @@ class AuthBrokerClient {
|
|
|
14392
14462
|
const data = await this.send(req);
|
|
14393
14463
|
return data;
|
|
14394
14464
|
}
|
|
14395
|
-
async markThrottled(until) {
|
|
14465
|
+
async markThrottled(until, probeOnly = false) {
|
|
14396
14466
|
const data = await this.send({
|
|
14397
14467
|
v: PROTOCOL_VERSION,
|
|
14398
14468
|
id: randomUUID(),
|
|
14399
14469
|
op: "mark-throttled",
|
|
14400
|
-
until
|
|
14470
|
+
until,
|
|
14471
|
+
...probeOnly ? { probeOnly: true } : {}
|
|
14401
14472
|
});
|
|
14402
14473
|
return data;
|
|
14403
14474
|
}
|
|
@@ -15264,6 +15335,17 @@ function createScheduleReloader(opts) {
|
|
|
15264
15335
|
}
|
|
15265
15336
|
};
|
|
15266
15337
|
}
|
|
15338
|
+
function formatReadFailures(failures) {
|
|
15339
|
+
return failures.map((f) => `${f.file} (${f.code})`).join(", ") + " — fix ownership/mode so the agent uid can read the file " + "(a root-euid writer leaves cron overlays root-owned 0600; " + "`switchroom apply` / a reconcile ownership sweep heals this)";
|
|
15340
|
+
}
|
|
15341
|
+
function loadAgentEntriesStrict(configPath, agentName) {
|
|
15342
|
+
const config = loadConfig(configPath);
|
|
15343
|
+
const failures = overlayReadFailures(config, agentName, "schedule");
|
|
15344
|
+
if (failures.length > 0) {
|
|
15345
|
+
throw new Error(`schedule.d overlay unreadable — refusing a reload that may be ` + `missing entries: ${formatReadFailures(failures)}`);
|
|
15346
|
+
}
|
|
15347
|
+
return collectScheduleEntries(config).filter((e) => e.agent === agentName);
|
|
15348
|
+
}
|
|
15267
15349
|
function resolveReloadPollMs(env) {
|
|
15268
15350
|
const raw = Number.parseInt(env.SWITCHROOM_SCHEDULER_RELOAD_POLL_MS ?? "", 10);
|
|
15269
15351
|
return Number.isFinite(raw) && raw >= 1000 ? raw : 30000;
|
|
@@ -15303,6 +15385,11 @@ async function main() {
|
|
|
15303
15385
|
const config = loadConfig(configPath);
|
|
15304
15386
|
const allEntries = collectScheduleEntries(config);
|
|
15305
15387
|
const entries = allEntries.filter((e) => e.agent === agentName);
|
|
15388
|
+
const bootReadFailures = overlayReadFailures(config, agentName, "schedule");
|
|
15389
|
+
if (bootReadFailures.length > 0) {
|
|
15390
|
+
process.stderr.write(`agent-scheduler: ${agentName} WARNING: ${bootReadFailures.length} ` + `schedule.d overlay file(s) unreadable at boot — their cron entries ` + `are NOT registered: ${formatReadFailures(bootReadFailures)}
|
|
15391
|
+
`);
|
|
15392
|
+
}
|
|
15306
15393
|
const channel = resolveChannelTarget(config, agentName);
|
|
15307
15394
|
if (channel === null) {
|
|
15308
15395
|
process.stderr.write(`agent-scheduler: ${agentName} has no resolvable chat target ` + `(missing telegram.forum_chat_id) — exiting
|
|
@@ -15519,15 +15606,24 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
15519
15606
|
let reloader;
|
|
15520
15607
|
let reloadTimer;
|
|
15521
15608
|
if (isHotReloadEnabled(process.env)) {
|
|
15609
|
+
let lastReloadError;
|
|
15522
15610
|
reloader = createScheduleReloader({
|
|
15523
|
-
loadEntries: () =>
|
|
15611
|
+
loadEntries: () => loadAgentEntriesStrict(configPath, agentName),
|
|
15524
15612
|
register: registerForEntries,
|
|
15525
15613
|
initialTasks: tasks,
|
|
15526
15614
|
initialEntries: entries,
|
|
15527
|
-
log: (m) =>
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
`)
|
|
15615
|
+
log: (m) => {
|
|
15616
|
+
lastReloadError = undefined;
|
|
15617
|
+
process.stdout.write(`agent-scheduler: ${agentName} ${m}
|
|
15618
|
+
`);
|
|
15619
|
+
},
|
|
15620
|
+
onError: (e) => {
|
|
15621
|
+
if (e.message === lastReloadError)
|
|
15622
|
+
return;
|
|
15623
|
+
lastReloadError = e.message;
|
|
15624
|
+
process.stderr.write(`agent-scheduler: ${agentName} reload skipped (config error, keeping current schedule): ${e.message}
|
|
15625
|
+
`);
|
|
15626
|
+
}
|
|
15531
15627
|
});
|
|
15532
15628
|
reloadTimer = setInterval(() => reloader.tick(), resolveReloadPollMs(process.env));
|
|
15533
15629
|
}
|
|
@@ -15560,6 +15656,7 @@ export {
|
|
|
15560
15656
|
registerAgentSchedule,
|
|
15561
15657
|
recoverPendingEscalations,
|
|
15562
15658
|
main,
|
|
15659
|
+
loadAgentEntriesStrict,
|
|
15563
15660
|
isHotReloadEnabled,
|
|
15564
15661
|
ipcDispatcher,
|
|
15565
15662
|
createScheduleReloader
|
|
@@ -12030,6 +12030,20 @@ var init_overlay_schema = __esm(() => {
|
|
|
12030
12030
|
// src/config/overlay-loader.ts
|
|
12031
12031
|
import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
|
|
12032
12032
|
import { basename, resolve as resolve2 } from "node:path";
|
|
12033
|
+
function recordReadFailure(agentCfg, failure) {
|
|
12034
|
+
const node = agentCfg;
|
|
12035
|
+
const existing = node[OVERLAY_READ_FAILURES];
|
|
12036
|
+
if (Array.isArray(existing)) {
|
|
12037
|
+
existing.push(failure);
|
|
12038
|
+
return;
|
|
12039
|
+
}
|
|
12040
|
+
Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
|
|
12041
|
+
value: [failure],
|
|
12042
|
+
enumerable: false,
|
|
12043
|
+
configurable: true,
|
|
12044
|
+
writable: false
|
|
12045
|
+
});
|
|
12046
|
+
}
|
|
12033
12047
|
function deriveOverlayTitle(raw, fileName) {
|
|
12034
12048
|
const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
|
|
12035
12049
|
if (titleFromComment)
|
|
@@ -12039,17 +12053,39 @@ function deriveOverlayTitle(raw, fileName) {
|
|
|
12039
12053
|
return;
|
|
12040
12054
|
return base.length > 0 ? base : undefined;
|
|
12041
12055
|
}
|
|
12056
|
+
function readOverlayFile(agentName, file, agentCfg, warnings, source) {
|
|
12057
|
+
try {
|
|
12058
|
+
return readFileSync(file, "utf-8");
|
|
12059
|
+
} catch (err) {
|
|
12060
|
+
const code = err.code;
|
|
12061
|
+
if (code === "ENOENT")
|
|
12062
|
+
return;
|
|
12063
|
+
const w = {
|
|
12064
|
+
agent: agentName,
|
|
12065
|
+
file,
|
|
12066
|
+
reason: `read error: ${err.message}`,
|
|
12067
|
+
code: code ?? "EUNKNOWN"
|
|
12068
|
+
};
|
|
12069
|
+
recordReadFailure(agentCfg, { file, code: w.code, source });
|
|
12070
|
+
warnings.push(w);
|
|
12071
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
|
|
12072
|
+
return;
|
|
12073
|
+
}
|
|
12074
|
+
}
|
|
12042
12075
|
function overlayDirFor(agentName, subdir) {
|
|
12043
12076
|
const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
|
|
12044
12077
|
return resolve2(base);
|
|
12045
12078
|
}
|
|
12046
|
-
function listYamlFiles(dir) {
|
|
12079
|
+
function listYamlFiles(dir, onUnreadableDir) {
|
|
12047
12080
|
if (!existsSync2(dir))
|
|
12048
12081
|
return [];
|
|
12049
12082
|
let entries;
|
|
12050
12083
|
try {
|
|
12051
12084
|
entries = readdirSync(dir);
|
|
12052
|
-
} catch {
|
|
12085
|
+
} catch (err) {
|
|
12086
|
+
const code = err.code;
|
|
12087
|
+
if (code !== "ENOENT")
|
|
12088
|
+
onUnreadableDir?.(code ?? "EUNKNOWN");
|
|
12053
12089
|
return [];
|
|
12054
12090
|
}
|
|
12055
12091
|
const out = [];
|
|
@@ -12087,12 +12123,24 @@ function applyAgentOverlays(config) {
|
|
|
12087
12123
|
for (const [agentName, agentCfg] of Object.entries(agents)) {
|
|
12088
12124
|
try {
|
|
12089
12125
|
const scheduleDir = overlayDirFor(agentName, "schedule.d");
|
|
12090
|
-
const files = listYamlFiles(scheduleDir)
|
|
12126
|
+
const files = listYamlFiles(scheduleDir, (code) => {
|
|
12127
|
+
const w = {
|
|
12128
|
+
agent: agentName,
|
|
12129
|
+
file: scheduleDir,
|
|
12130
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
12131
|
+
code
|
|
12132
|
+
};
|
|
12133
|
+
recordReadFailure(agentCfg, { file: scheduleDir, code, source: "schedule" });
|
|
12134
|
+
warnings.push(w);
|
|
12135
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
|
|
12136
|
+
});
|
|
12091
12137
|
if (files.length > 0) {
|
|
12092
12138
|
const merged = [...agentCfg.schedule ?? []];
|
|
12093
12139
|
for (const file of files) {
|
|
12140
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "schedule");
|
|
12141
|
+
if (raw === undefined)
|
|
12142
|
+
continue;
|
|
12094
12143
|
try {
|
|
12095
|
-
const raw = readFileSync(file, "utf-8");
|
|
12096
12144
|
const parsed = import_yaml.parse(raw);
|
|
12097
12145
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12098
12146
|
const title = deriveOverlayTitle(raw, basename(file));
|
|
@@ -12127,13 +12175,25 @@ function applyAgentOverlays(config) {
|
|
|
12127
12175
|
}
|
|
12128
12176
|
try {
|
|
12129
12177
|
const skillsDir = overlayDirFor(agentName, "skills.d");
|
|
12130
|
-
const skillFiles = listYamlFiles(skillsDir)
|
|
12178
|
+
const skillFiles = listYamlFiles(skillsDir, (code) => {
|
|
12179
|
+
const w = {
|
|
12180
|
+
agent: agentName,
|
|
12181
|
+
file: skillsDir,
|
|
12182
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
12183
|
+
code
|
|
12184
|
+
};
|
|
12185
|
+
recordReadFailure(agentCfg, { file: skillsDir, code, source: "skills" });
|
|
12186
|
+
warnings.push(w);
|
|
12187
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
|
|
12188
|
+
});
|
|
12131
12189
|
if (skillFiles.length === 0) {} else {
|
|
12132
12190
|
const merged = [...agentCfg.skills ?? []];
|
|
12133
12191
|
const seen = new Set(merged);
|
|
12134
12192
|
for (const file of skillFiles) {
|
|
12193
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "skills");
|
|
12194
|
+
if (raw === undefined)
|
|
12195
|
+
continue;
|
|
12135
12196
|
try {
|
|
12136
|
-
const raw = readFileSync(file, "utf-8");
|
|
12137
12197
|
const parsed = import_yaml.parse(raw);
|
|
12138
12198
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12139
12199
|
for (const skillName of doc.skills ?? []) {
|
|
@@ -12161,7 +12221,7 @@ function applyAgentOverlays(config) {
|
|
|
12161
12221
|
}
|
|
12162
12222
|
return { config, warnings };
|
|
12163
12223
|
}
|
|
12164
|
-
var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE;
|
|
12224
|
+
var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE, OVERLAY_READ_FAILURES;
|
|
12165
12225
|
var init_overlay_loader = __esm(() => {
|
|
12166
12226
|
init_zod();
|
|
12167
12227
|
init_overlay_schema();
|
|
@@ -12169,6 +12229,7 @@ var init_overlay_loader = __esm(() => {
|
|
|
12169
12229
|
import_yaml = __toESM(require_dist(), 1);
|
|
12170
12230
|
OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
|
|
12171
12231
|
OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
|
|
12232
|
+
OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
|
|
12172
12233
|
});
|
|
12173
12234
|
|
|
12174
12235
|
// src/config/merge.ts
|
|
@@ -21225,7 +21286,8 @@ var MarkThrottledRequestSchema = exports_external.object({
|
|
|
21225
21286
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
21226
21287
|
op: exports_external.literal("mark-throttled"),
|
|
21227
21288
|
id: exports_external.string().min(1),
|
|
21228
|
-
until: exports_external.number().int().positive()
|
|
21289
|
+
until: exports_external.number().int().positive(),
|
|
21290
|
+
probeOnly: exports_external.boolean().optional()
|
|
21229
21291
|
});
|
|
21230
21292
|
var RefreshAccountRequestSchema = exports_external.object({
|
|
21231
21293
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -21507,8 +21569,8 @@ var AUTH_BROKER_ROOT2 = "/run/switchroom/auth-broker";
|
|
|
21507
21569
|
var REFRESH_TICK_INTERVAL_MS = 60 * 1000;
|
|
21508
21570
|
var MARK_EXHAUSTED_DEFAULT_MS = 5 * 60 * 60 * 1000;
|
|
21509
21571
|
var MARK_THROTTLED_MAX_MS = 30 * 60 * 1000;
|
|
21510
|
-
var THROTTLE_ESCALATION_HITS = 3;
|
|
21511
21572
|
var THROTTLE_ESCALATION_WINDOW_MS = 10 * 60 * 1000;
|
|
21573
|
+
var THROTTLE_ESCALATION_PROBE_MIN_INTERVAL_MS = 60 * 1000;
|
|
21512
21574
|
var MARK_THROTTLED_MIN_INTERVAL_MS = 5 * 1000;
|
|
21513
21575
|
var MARK_CORROBORATION_PROBE_DELAY_MS = 90 * 1000;
|
|
21514
21576
|
var AUDIT_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
@@ -21602,6 +21664,7 @@ class AuthBroker {
|
|
|
21602
21664
|
externalSpendCache = null;
|
|
21603
21665
|
externalSpendInFlight = null;
|
|
21604
21666
|
probeInFlight = new Map;
|
|
21667
|
+
lastEscalationProbeAt = {};
|
|
21605
21668
|
shaIndex = {};
|
|
21606
21669
|
thresholdViolations = {};
|
|
21607
21670
|
notificationClaims = {};
|
|
@@ -21986,7 +22049,7 @@ class AuthBroker {
|
|
|
21986
22049
|
await this.opMarkExhausted(socket, reqId, identity, req.until);
|
|
21987
22050
|
break;
|
|
21988
22051
|
case "mark-throttled":
|
|
21989
|
-
await this.opMarkThrottled(socket, reqId, identity, req.until);
|
|
22052
|
+
await this.opMarkThrottled(socket, reqId, identity, req.until, req.probeOnly);
|
|
21990
22053
|
break;
|
|
21991
22054
|
case "refresh-account": {
|
|
21992
22055
|
const provider = req.provider ?? "anthropic";
|
|
@@ -22818,7 +22881,7 @@ class AuthBroker {
|
|
|
22818
22881
|
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
|
|
22819
22882
|
socket.write(encodeSuccess(id, { account, rolled, rolledTo }));
|
|
22820
22883
|
}
|
|
22821
|
-
async opMarkThrottled(socket, id, identity, until) {
|
|
22884
|
+
async opMarkThrottled(socket, id, identity, until, probeOnly = false) {
|
|
22822
22885
|
const account = this.callerAccount(identity);
|
|
22823
22886
|
if (!account) {
|
|
22824
22887
|
this.audit({ op: "mark-throttled", identity, accountKind: "claude", ok: false, error: "no-active-account" });
|
|
@@ -22826,6 +22889,17 @@ class AuthBroker {
|
|
|
22826
22889
|
return;
|
|
22827
22890
|
}
|
|
22828
22891
|
const now = this.now();
|
|
22892
|
+
if (probeOnly) {
|
|
22893
|
+
const { escalated: escalated2, rolledTo: rolledTo2 } = await this.maybeEscalateThrottle(account, identity, now);
|
|
22894
|
+
this.audit({ op: "mark-throttled", identity, account, accountKind: "claude", ok: true });
|
|
22895
|
+
socket.write(encodeSuccess(id, {
|
|
22896
|
+
account,
|
|
22897
|
+
throttled_until: this.quota[account]?.throttled_until ?? 0,
|
|
22898
|
+
escalated: escalated2,
|
|
22899
|
+
rolledTo: escalated2 ? rolledTo2 : null
|
|
22900
|
+
}));
|
|
22901
|
+
return;
|
|
22902
|
+
}
|
|
22829
22903
|
const throttledUntil = Math.min(Math.max(until, now + 1000), now + MARK_THROTTLED_MAX_MS);
|
|
22830
22904
|
const entry = this.quota[account] ?? {};
|
|
22831
22905
|
const priorHits = entry.throttle_hits ?? [];
|
|
@@ -22847,31 +22921,19 @@ class AuthBroker {
|
|
|
22847
22921
|
}
|
|
22848
22922
|
const hits = priorHits.filter((t) => now - t < THROTTLE_ESCALATION_WINDOW_MS);
|
|
22849
22923
|
hits.push(now);
|
|
22850
|
-
const escalate = hits.length >= THROTTLE_ESCALATION_HITS;
|
|
22851
22924
|
this.quota[account] = {
|
|
22852
22925
|
...entry,
|
|
22853
22926
|
throttled_until: throttledUntil,
|
|
22854
|
-
throttle_hits:
|
|
22927
|
+
throttle_hits: hits
|
|
22855
22928
|
};
|
|
22856
22929
|
this.persistQuota();
|
|
22857
22930
|
this.audit({ op: "mark-throttled", identity, account, accountKind: "claude", ok: true });
|
|
22858
|
-
process.stdout.write(`auth-broker: mark-throttled ${account} until ${new Date(throttledUntil).toISOString()} ` + `(hit ${hits.length}
|
|
22859
|
-
`);
|
|
22860
|
-
let escalated = false;
|
|
22861
|
-
let rolledTo = null;
|
|
22862
|
-
if (escalate) {
|
|
22863
|
-
const probe = await this.probeThrottleEscalation(account);
|
|
22864
|
-
if (probe.exhausted) {
|
|
22865
|
-
escalated = true;
|
|
22866
|
-
process.stdout.write(`auth-broker: throttle-escalation probe corroborates wall on ${account} — mark-exhausted + roll
|
|
22867
|
-
`);
|
|
22868
|
-
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true, reason: "throttle-escalation" });
|
|
22869
|
-
const roll = await this.markExhaustedAndRoll(account, probe.until ?? undefined, identity);
|
|
22870
|
-
rolledTo = roll.rolledTo;
|
|
22871
|
-
} else {
|
|
22872
|
-
process.stdout.write(`auth-broker: throttle-escalation probe on ${account} did NOT corroborate a wall — staying throttled
|
|
22931
|
+
process.stdout.write(`auth-broker: mark-throttled ${account} until ${new Date(throttledUntil).toISOString()} ` + `(hit ${hits.length} in window)
|
|
22873
22932
|
`);
|
|
22874
|
-
|
|
22933
|
+
const { escalated, rolledTo } = await this.maybeEscalateThrottle(account, identity, now);
|
|
22934
|
+
if (escalated) {
|
|
22935
|
+
this.quota[account] = { ...this.quota[account], throttle_hits: [] };
|
|
22936
|
+
this.persistQuota();
|
|
22875
22937
|
}
|
|
22876
22938
|
socket.write(encodeSuccess(id, {
|
|
22877
22939
|
account,
|
|
@@ -22880,6 +22942,27 @@ class AuthBroker {
|
|
|
22880
22942
|
rolledTo: escalated ? rolledTo : null
|
|
22881
22943
|
}));
|
|
22882
22944
|
}
|
|
22945
|
+
async maybeEscalateThrottle(account, identity, now) {
|
|
22946
|
+
const lastProbeAt = this.lastEscalationProbeAt[account];
|
|
22947
|
+
const probeAllowed = lastProbeAt === undefined || now - lastProbeAt >= THROTTLE_ESCALATION_PROBE_MIN_INTERVAL_MS;
|
|
22948
|
+
if (!probeAllowed) {
|
|
22949
|
+
process.stdout.write(`auth-broker: throttle-escalation probe on ${account} rate-bounded — skipped
|
|
22950
|
+
`);
|
|
22951
|
+
return { escalated: false, rolledTo: null };
|
|
22952
|
+
}
|
|
22953
|
+
this.lastEscalationProbeAt[account] = now;
|
|
22954
|
+
const probe = await this.probeThrottleEscalation(account);
|
|
22955
|
+
if (!probe.exhausted) {
|
|
22956
|
+
process.stdout.write(`auth-broker: throttle-escalation probe on ${account} did NOT corroborate a wall — staying put
|
|
22957
|
+
`);
|
|
22958
|
+
return { escalated: false, rolledTo: null };
|
|
22959
|
+
}
|
|
22960
|
+
process.stdout.write(`auth-broker: throttle-escalation probe corroborates wall on ${account} — mark-exhausted + roll
|
|
22961
|
+
`);
|
|
22962
|
+
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true, reason: "throttle-escalation" });
|
|
22963
|
+
const roll = await this.markExhaustedAndRoll(account, probe.until ?? undefined, identity);
|
|
22964
|
+
return { escalated: true, rolledTo: roll.rolledTo };
|
|
22965
|
+
}
|
|
22883
22966
|
async probeThrottleEscalation(account) {
|
|
22884
22967
|
const creds = readAccountCredentials(account, this.home);
|
|
22885
22968
|
const token = creds?.claudeAiOauth?.accessToken;
|
|
@@ -22887,7 +22970,7 @@ class AuthBroker {
|
|
|
22887
22970
|
return { exhausted: false, until: null };
|
|
22888
22971
|
let result;
|
|
22889
22972
|
try {
|
|
22890
|
-
result = await this.
|
|
22973
|
+
result = await this.probeQuotaSingleFlight(account, token);
|
|
22891
22974
|
} catch (err) {
|
|
22892
22975
|
this.logErr(`throttle-escalation probe ${account}: ${err.message}`);
|
|
22893
22976
|
return { exhausted: false, until: null };
|
|
@@ -4958,7 +4958,8 @@ var MarkThrottledRequestSchema = exports_external.object({
|
|
|
4958
4958
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4959
4959
|
op: exports_external.literal("mark-throttled"),
|
|
4960
4960
|
id: exports_external.string().min(1),
|
|
4961
|
-
until: exports_external.number().int().positive()
|
|
4961
|
+
until: exports_external.number().int().positive(),
|
|
4962
|
+
probeOnly: exports_external.boolean().optional()
|
|
4962
4963
|
});
|
|
4963
4964
|
var RefreshAccountRequestSchema = exports_external.object({
|
|
4964
4965
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -5375,12 +5376,13 @@ class AuthBrokerClient {
|
|
|
5375
5376
|
const data = await this.send(req);
|
|
5376
5377
|
return data;
|
|
5377
5378
|
}
|
|
5378
|
-
async markThrottled(until) {
|
|
5379
|
+
async markThrottled(until, probeOnly = false) {
|
|
5379
5380
|
const data = await this.send({
|
|
5380
5381
|
v: PROTOCOL_VERSION,
|
|
5381
5382
|
id: randomUUID2(),
|
|
5382
5383
|
op: "mark-throttled",
|
|
5383
|
-
until
|
|
5384
|
+
until,
|
|
5385
|
+
...probeOnly ? { probeOnly: true } : {}
|
|
5384
5386
|
});
|
|
5385
5387
|
return data;
|
|
5386
5388
|
}
|
|
@@ -4034,7 +4034,8 @@ var init_protocol = __esm(() => {
|
|
|
4034
4034
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4035
4035
|
op: exports_external.literal("mark-throttled"),
|
|
4036
4036
|
id: exports_external.string().min(1),
|
|
4037
|
-
until: exports_external.number().int().positive()
|
|
4037
|
+
until: exports_external.number().int().positive(),
|
|
4038
|
+
probeOnly: exports_external.boolean().optional()
|
|
4038
4039
|
});
|
|
4039
4040
|
RefreshAccountRequestSchema = exports_external.object({
|
|
4040
4041
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -4430,12 +4431,13 @@ class AuthBrokerClient {
|
|
|
4430
4431
|
const data = await this.send(req);
|
|
4431
4432
|
return data;
|
|
4432
4433
|
}
|
|
4433
|
-
async markThrottled(until) {
|
|
4434
|
+
async markThrottled(until, probeOnly = false) {
|
|
4434
4435
|
const data = await this.send({
|
|
4435
4436
|
v: PROTOCOL_VERSION,
|
|
4436
4437
|
id: randomUUID(),
|
|
4437
4438
|
op: "mark-throttled",
|
|
4438
|
-
until
|
|
4439
|
+
until,
|
|
4440
|
+
...probeOnly ? { probeOnly: true } : {}
|
|
4439
4441
|
});
|
|
4440
4442
|
return data;
|
|
4441
4443
|
}
|
|
@@ -4036,7 +4036,8 @@ var init_protocol = __esm(() => {
|
|
|
4036
4036
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4037
4037
|
op: exports_external.literal("mark-throttled"),
|
|
4038
4038
|
id: exports_external.string().min(1),
|
|
4039
|
-
until: exports_external.number().int().positive()
|
|
4039
|
+
until: exports_external.number().int().positive(),
|
|
4040
|
+
probeOnly: exports_external.boolean().optional()
|
|
4040
4041
|
});
|
|
4041
4042
|
RefreshAccountRequestSchema = exports_external.object({
|
|
4042
4043
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -4432,12 +4433,13 @@ class AuthBrokerClient {
|
|
|
4432
4433
|
const data = await this.send(req);
|
|
4433
4434
|
return data;
|
|
4434
4435
|
}
|
|
4435
|
-
async markThrottled(until) {
|
|
4436
|
+
async markThrottled(until, probeOnly = false) {
|
|
4436
4437
|
const data = await this.send({
|
|
4437
4438
|
v: PROTOCOL_VERSION,
|
|
4438
4439
|
id: randomUUID(),
|
|
4439
4440
|
op: "mark-throttled",
|
|
4440
|
-
until
|
|
4441
|
+
until,
|
|
4442
|
+
...probeOnly ? { probeOnly: true } : {}
|
|
4441
4443
|
});
|
|
4442
4444
|
return data;
|
|
4443
4445
|
}
|
|
@@ -12767,6 +12767,21 @@ var OverlayDocSchema = exports_external.object({
|
|
|
12767
12767
|
// src/config/overlay-loader.ts
|
|
12768
12768
|
var OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
|
|
12769
12769
|
var OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
|
|
12770
|
+
var OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
|
|
12771
|
+
function recordReadFailure(agentCfg, failure) {
|
|
12772
|
+
const node = agentCfg;
|
|
12773
|
+
const existing = node[OVERLAY_READ_FAILURES];
|
|
12774
|
+
if (Array.isArray(existing)) {
|
|
12775
|
+
existing.push(failure);
|
|
12776
|
+
return;
|
|
12777
|
+
}
|
|
12778
|
+
Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
|
|
12779
|
+
value: [failure],
|
|
12780
|
+
enumerable: false,
|
|
12781
|
+
configurable: true,
|
|
12782
|
+
writable: false
|
|
12783
|
+
});
|
|
12784
|
+
}
|
|
12770
12785
|
function deriveOverlayTitle(raw, fileName) {
|
|
12771
12786
|
const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
|
|
12772
12787
|
if (titleFromComment)
|
|
@@ -12776,17 +12791,39 @@ function deriveOverlayTitle(raw, fileName) {
|
|
|
12776
12791
|
return;
|
|
12777
12792
|
return base.length > 0 ? base : undefined;
|
|
12778
12793
|
}
|
|
12794
|
+
function readOverlayFile(agentName, file, agentCfg, warnings, source) {
|
|
12795
|
+
try {
|
|
12796
|
+
return readFileSync(file, "utf-8");
|
|
12797
|
+
} catch (err) {
|
|
12798
|
+
const code = err.code;
|
|
12799
|
+
if (code === "ENOENT")
|
|
12800
|
+
return;
|
|
12801
|
+
const w = {
|
|
12802
|
+
agent: agentName,
|
|
12803
|
+
file,
|
|
12804
|
+
reason: `read error: ${err.message}`,
|
|
12805
|
+
code: code ?? "EUNKNOWN"
|
|
12806
|
+
};
|
|
12807
|
+
recordReadFailure(agentCfg, { file, code: w.code, source });
|
|
12808
|
+
warnings.push(w);
|
|
12809
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
|
|
12810
|
+
return;
|
|
12811
|
+
}
|
|
12812
|
+
}
|
|
12779
12813
|
function overlayDirFor(agentName, subdir) {
|
|
12780
12814
|
const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
|
|
12781
12815
|
return resolve2(base);
|
|
12782
12816
|
}
|
|
12783
|
-
function listYamlFiles(dir) {
|
|
12817
|
+
function listYamlFiles(dir, onUnreadableDir) {
|
|
12784
12818
|
if (!existsSync2(dir))
|
|
12785
12819
|
return [];
|
|
12786
12820
|
let entries;
|
|
12787
12821
|
try {
|
|
12788
12822
|
entries = readdirSync(dir);
|
|
12789
|
-
} catch {
|
|
12823
|
+
} catch (err) {
|
|
12824
|
+
const code = err.code;
|
|
12825
|
+
if (code !== "ENOENT")
|
|
12826
|
+
onUnreadableDir?.(code ?? "EUNKNOWN");
|
|
12790
12827
|
return [];
|
|
12791
12828
|
}
|
|
12792
12829
|
const out = [];
|
|
@@ -12824,12 +12861,24 @@ function applyAgentOverlays(config) {
|
|
|
12824
12861
|
for (const [agentName, agentCfg] of Object.entries(agents)) {
|
|
12825
12862
|
try {
|
|
12826
12863
|
const scheduleDir = overlayDirFor(agentName, "schedule.d");
|
|
12827
|
-
const files = listYamlFiles(scheduleDir)
|
|
12864
|
+
const files = listYamlFiles(scheduleDir, (code) => {
|
|
12865
|
+
const w = {
|
|
12866
|
+
agent: agentName,
|
|
12867
|
+
file: scheduleDir,
|
|
12868
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
12869
|
+
code
|
|
12870
|
+
};
|
|
12871
|
+
recordReadFailure(agentCfg, { file: scheduleDir, code, source: "schedule" });
|
|
12872
|
+
warnings.push(w);
|
|
12873
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
|
|
12874
|
+
});
|
|
12828
12875
|
if (files.length > 0) {
|
|
12829
12876
|
const merged = [...agentCfg.schedule ?? []];
|
|
12830
12877
|
for (const file of files) {
|
|
12878
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "schedule");
|
|
12879
|
+
if (raw === undefined)
|
|
12880
|
+
continue;
|
|
12831
12881
|
try {
|
|
12832
|
-
const raw = readFileSync(file, "utf-8");
|
|
12833
12882
|
const parsed = $parse(raw);
|
|
12834
12883
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12835
12884
|
const title = deriveOverlayTitle(raw, basename(file));
|
|
@@ -12864,13 +12913,25 @@ function applyAgentOverlays(config) {
|
|
|
12864
12913
|
}
|
|
12865
12914
|
try {
|
|
12866
12915
|
const skillsDir = overlayDirFor(agentName, "skills.d");
|
|
12867
|
-
const skillFiles = listYamlFiles(skillsDir)
|
|
12916
|
+
const skillFiles = listYamlFiles(skillsDir, (code) => {
|
|
12917
|
+
const w = {
|
|
12918
|
+
agent: agentName,
|
|
12919
|
+
file: skillsDir,
|
|
12920
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
12921
|
+
code
|
|
12922
|
+
};
|
|
12923
|
+
recordReadFailure(agentCfg, { file: skillsDir, code, source: "skills" });
|
|
12924
|
+
warnings.push(w);
|
|
12925
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
|
|
12926
|
+
});
|
|
12868
12927
|
if (skillFiles.length === 0) {} else {
|
|
12869
12928
|
const merged = [...agentCfg.skills ?? []];
|
|
12870
12929
|
const seen = new Set(merged);
|
|
12871
12930
|
for (const file of skillFiles) {
|
|
12931
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "skills");
|
|
12932
|
+
if (raw === undefined)
|
|
12933
|
+
continue;
|
|
12872
12934
|
try {
|
|
12873
|
-
const raw = readFileSync(file, "utf-8");
|
|
12874
12935
|
const parsed = $parse(raw);
|
|
12875
12936
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12876
12937
|
for (const skillName of doc.skills ?? []) {
|