switchroom 0.15.7 → 0.15.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 +189 -7
- package/dist/auth-broker/index.js +18 -0
- package/dist/cli/notion-write-pretool.mjs +18 -0
- package/dist/cli/switchroom.js +204 -30
- package/dist/host-control/main.js +18 -0
- package/dist/vault/approvals/kernel-server.js +19 -1
- package/dist/vault/broker/server.js +19 -1
- package/package.json +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +24 -5
- package/telegram-plugin/dist/gateway/gateway.js +170 -8
- package/telegram-plugin/gateway/gateway.ts +136 -2
- package/telegram-plugin/gateway/reaction-dispatch.ts +174 -0
- package/telegram-plugin/tests/reaction-dispatch.test.ts +137 -0
|
@@ -11232,6 +11232,10 @@ var ReactionsSchema = exports_external.object({
|
|
|
11232
11232
|
per_hour_cap: exports_external.number().int().nonnegative().optional().describe("Max reaction-triggered synthetic turns per chat per rolling hour. " + "Refusals are stderr-logged but not surfaced to the agent. " + "Default 10. Set to 0 to disable triggering via the cap path."),
|
|
11233
11233
|
group_admin_only: exports_external.boolean().optional().describe("In groups/supergroups (negative chat_id), only trigger a synthetic " + "turn when the reacter is a chat admin (creator or administrator). " + "Failing the lookup is treated as non-admin (fail-closed). " + "DMs are never affected by this flag — the reacter IS the user. " + "Default true.")
|
|
11234
11234
|
}).optional();
|
|
11235
|
+
var ReactionDispatchSchema = exports_external.object({
|
|
11236
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false — " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
11237
|
+
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) — a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
11238
|
+
}).optional();
|
|
11235
11239
|
var ReleaseBlock = exports_external.object({
|
|
11236
11240
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
11237
11241
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -11267,6 +11271,7 @@ var profileFields = {
|
|
|
11267
11271
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
11268
11272
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
|
|
11269
11273
|
reactions: ReactionsSchema,
|
|
11274
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11270
11275
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
11271
11276
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11272
11277
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Omit to use Claude's default (acceptEdits for switchroom agents). " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -11336,6 +11341,7 @@ var AgentSchema = exports_external.object({
|
|
|
11336
11341
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
11337
11342
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
|
|
11338
11343
|
reactions: ReactionsSchema,
|
|
11344
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11339
11345
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
|
|
11340
11346
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11341
11347
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -12001,6 +12007,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
12001
12007
|
}
|
|
12002
12008
|
merged.reactions = combined;
|
|
12003
12009
|
}
|
|
12010
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
12011
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
12012
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
12013
|
+
const base = dReactionDispatch ?? {};
|
|
12014
|
+
const override = mReactionDispatch ?? {};
|
|
12015
|
+
const combined = { ...base };
|
|
12016
|
+
for (const [k, v] of Object.entries(override)) {
|
|
12017
|
+
if (v !== undefined)
|
|
12018
|
+
combined[k] = v;
|
|
12019
|
+
}
|
|
12020
|
+
merged.reaction_dispatch = combined;
|
|
12021
|
+
}
|
|
12004
12022
|
if (defaults.resources || merged.resources) {
|
|
12005
12023
|
const d = defaults.resources ?? {};
|
|
12006
12024
|
const a = merged.resources ?? {};
|
|
@@ -12258,7 +12276,7 @@ function isKnownCheapModel(model) {
|
|
|
12258
12276
|
}
|
|
12259
12277
|
function isCheapCronEnabled(env = process.env) {
|
|
12260
12278
|
const v = (env.SWITCHROOM_CHEAP_CRON ?? "").toLowerCase();
|
|
12261
|
-
return v === "
|
|
12279
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
12262
12280
|
}
|
|
12263
12281
|
function resolveCronModel(model) {
|
|
12264
12282
|
return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
|
|
@@ -12295,6 +12313,90 @@ function resolveEscalationRouting(input, opts) {
|
|
|
12295
12313
|
return resolveCronRouting({ ...input, kind: "prompt" }, opts);
|
|
12296
12314
|
}
|
|
12297
12315
|
|
|
12316
|
+
// src/scheduler/cron-cadence.ts
|
|
12317
|
+
function csvSmallestGap(field) {
|
|
12318
|
+
if (!field.includes(","))
|
|
12319
|
+
return null;
|
|
12320
|
+
const parts = field.split(",").map((s) => Number(s)).filter((n) => Number.isInteger(n) && n >= 0);
|
|
12321
|
+
if (parts.length < 2)
|
|
12322
|
+
return null;
|
|
12323
|
+
const sorted = [...parts].sort((a, b) => a - b);
|
|
12324
|
+
let smallest = Infinity;
|
|
12325
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
12326
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
12327
|
+
if (gap > 0 && gap < smallest)
|
|
12328
|
+
smallest = gap;
|
|
12329
|
+
}
|
|
12330
|
+
return Number.isFinite(smallest) ? smallest : null;
|
|
12331
|
+
}
|
|
12332
|
+
function estimateCronGapMin(expr) {
|
|
12333
|
+
const fields = expr.trim().split(/\s+/);
|
|
12334
|
+
if (fields.length < 5)
|
|
12335
|
+
return Infinity;
|
|
12336
|
+
const [min, hour] = fields;
|
|
12337
|
+
if (min === "*")
|
|
12338
|
+
return 1;
|
|
12339
|
+
const minStep = min.match(/^\*\/(\d+)$/);
|
|
12340
|
+
if (minStep) {
|
|
12341
|
+
const n = Number(minStep[1]);
|
|
12342
|
+
return n > 0 ? n : Infinity;
|
|
12343
|
+
}
|
|
12344
|
+
const minCsv = csvSmallestGap(min);
|
|
12345
|
+
if (minCsv !== null)
|
|
12346
|
+
return minCsv;
|
|
12347
|
+
if (!/^\d+$/.test(min))
|
|
12348
|
+
return Infinity;
|
|
12349
|
+
if (hour === "*")
|
|
12350
|
+
return 60;
|
|
12351
|
+
const hourStep = hour.match(/^\*\/(\d+)$/);
|
|
12352
|
+
if (hourStep) {
|
|
12353
|
+
const n = Number(hourStep[1]);
|
|
12354
|
+
return n > 0 ? n * 60 : Infinity;
|
|
12355
|
+
}
|
|
12356
|
+
const hourCsv = csvSmallestGap(hour);
|
|
12357
|
+
if (hourCsv !== null)
|
|
12358
|
+
return hourCsv * 60;
|
|
12359
|
+
if (/^\d+$/.test(hour))
|
|
12360
|
+
return 1440;
|
|
12361
|
+
return Infinity;
|
|
12362
|
+
}
|
|
12363
|
+
|
|
12364
|
+
// src/scheduler/tier-selector.ts
|
|
12365
|
+
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
12366
|
+
function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
12367
|
+
if (input.kind === "poll") {
|
|
12368
|
+
return { tier: "poll", source: "explicit", reason: "declared kind: poll (model-free check)" };
|
|
12369
|
+
}
|
|
12370
|
+
if (input.context === "fresh") {
|
|
12371
|
+
return { tier: "cheap", source: "explicit", reason: "declared context: fresh (cheap cron session)" };
|
|
12372
|
+
}
|
|
12373
|
+
if (input.context === "agent") {
|
|
12374
|
+
return { tier: "main", source: "explicit", reason: "declared context: agent (full live session)" };
|
|
12375
|
+
}
|
|
12376
|
+
if (input.model !== undefined) {
|
|
12377
|
+
return isKnownCheapModel(input.model) ? { tier: "cheap", source: "explicit", reason: `cheap model '${input.model}' → cheap cron session` } : { tier: "main", source: "explicit", reason: `model '${input.model}' is not a known-cheap id → full live session` };
|
|
12378
|
+
}
|
|
12379
|
+
if (input.smallestGapMin <= frequentGapMin) {
|
|
12380
|
+
return {
|
|
12381
|
+
tier: "cheap",
|
|
12382
|
+
source: "cadence-default",
|
|
12383
|
+
reason: `fires every ~${input.smallestGapMin}min (≤ ${frequentGapMin}min) — defaulting to a cheap ` + `session; set context: agent (or an Opus/custom model) if this needs the agent's full context`
|
|
12384
|
+
};
|
|
12385
|
+
}
|
|
12386
|
+
return {
|
|
12387
|
+
tier: "main",
|
|
12388
|
+
source: "cadence-default",
|
|
12389
|
+
reason: `fires every ~${input.smallestGapMin}min (> ${frequentGapMin}min) — defaulting to the agent's ` + `full session; set model: sonnet (or context: fresh) to run it cheaply`
|
|
12390
|
+
};
|
|
12391
|
+
}
|
|
12392
|
+
function applyDefaultTier(entry, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
12393
|
+
if (entry.kind === "poll" || entry.context !== undefined || entry.model !== undefined) {
|
|
12394
|
+
return entry;
|
|
12395
|
+
}
|
|
12396
|
+
const rec = recommendCronTier({ smallestGapMin: estimateCronGapMin(entry.cron) }, frequentGapMin);
|
|
12397
|
+
return rec.tier === "cheap" ? { ...entry, context: "fresh" } : entry;
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12298
12400
|
// src/agent-scheduler/cheap-cron-wiring.ts
|
|
12299
12401
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
12300
12402
|
|
|
@@ -14278,7 +14380,8 @@ function registerAgentSchedule(opts) {
|
|
|
14278
14380
|
}
|
|
14279
14381
|
}
|
|
14280
14382
|
const cheapEnabled = opts.cheapCron?.enabled ?? false;
|
|
14281
|
-
const
|
|
14383
|
+
const routed = cheapEnabled ? applyDefaultTier(entry) : entry;
|
|
14384
|
+
const routing = resolveCronRouting(routed, { cheapCronEnabled: cheapEnabled });
|
|
14282
14385
|
const threadId = resolveEntryThreadId(entry, opts.channel);
|
|
14283
14386
|
const record = (fields) => opts.sink.recordFire({
|
|
14284
14387
|
agent: entry.agent,
|
|
@@ -14364,6 +14467,43 @@ function registerAgentSchedule(opts) {
|
|
|
14364
14467
|
}
|
|
14365
14468
|
return tasks;
|
|
14366
14469
|
}
|
|
14470
|
+
function scheduleSignature(entries) {
|
|
14471
|
+
return JSON.stringify(entries);
|
|
14472
|
+
}
|
|
14473
|
+
function createScheduleReloader(opts) {
|
|
14474
|
+
let tasks = opts.initialTasks;
|
|
14475
|
+
let sig = scheduleSignature(opts.initialEntries);
|
|
14476
|
+
return {
|
|
14477
|
+
tick() {
|
|
14478
|
+
let next;
|
|
14479
|
+
try {
|
|
14480
|
+
next = opts.loadEntries();
|
|
14481
|
+
} catch (err) {
|
|
14482
|
+
opts.onError?.(err);
|
|
14483
|
+
return;
|
|
14484
|
+
}
|
|
14485
|
+
const nextSig = scheduleSignature(next);
|
|
14486
|
+
if (nextSig === sig)
|
|
14487
|
+
return;
|
|
14488
|
+
const before = tasks.length;
|
|
14489
|
+
for (const t of tasks)
|
|
14490
|
+
t.task.stop();
|
|
14491
|
+
tasks = opts.register(next);
|
|
14492
|
+
sig = nextSig;
|
|
14493
|
+
opts.log(`schedule reloaded: ${before} → ${tasks.length} task(s)`);
|
|
14494
|
+
},
|
|
14495
|
+
currentTasks() {
|
|
14496
|
+
return tasks;
|
|
14497
|
+
}
|
|
14498
|
+
};
|
|
14499
|
+
}
|
|
14500
|
+
function resolveReloadPollMs(env) {
|
|
14501
|
+
const raw = Number.parseInt(env.SWITCHROOM_SCHEDULER_RELOAD_POLL_MS ?? "", 10);
|
|
14502
|
+
return Number.isFinite(raw) && raw >= 1000 ? raw : 30000;
|
|
14503
|
+
}
|
|
14504
|
+
function isHotReloadEnabled(env) {
|
|
14505
|
+
return env.SWITCHROOM_SCHEDULER_HOT_RELOAD !== "0";
|
|
14506
|
+
}
|
|
14367
14507
|
function ipcDispatcher(client) {
|
|
14368
14508
|
return {
|
|
14369
14509
|
sendToAgent(agentName, inbound) {
|
|
@@ -14405,8 +14545,28 @@ async function main() {
|
|
|
14405
14545
|
if (entries.length === 0) {
|
|
14406
14546
|
process.stdout.write(`agent-scheduler: ${agentName} has no schedule entries — idling ` + `(re-checks on container restart)
|
|
14407
14547
|
`);
|
|
14408
|
-
|
|
14548
|
+
let idleTimer;
|
|
14549
|
+
if (isHotReloadEnabled(process.env)) {
|
|
14550
|
+
idleTimer = setInterval(() => {
|
|
14551
|
+
let appeared;
|
|
14552
|
+
try {
|
|
14553
|
+
appeared = collectScheduleEntries(loadConfig(configPath)).filter((e) => e.agent === agentName);
|
|
14554
|
+
} catch {
|
|
14555
|
+
return;
|
|
14556
|
+
}
|
|
14557
|
+
if (appeared.length > 0) {
|
|
14558
|
+
process.stdout.write(`agent-scheduler: ${agentName} schedule appeared (${appeared.length} ` + `entr${appeared.length === 1 ? "y" : "ies"}) — restarting to activate
|
|
14559
|
+
`);
|
|
14560
|
+
clearInterval(idleTimer);
|
|
14561
|
+
releaseLock(lockPath);
|
|
14562
|
+
process.exit(0);
|
|
14563
|
+
}
|
|
14564
|
+
}, resolveReloadPollMs(process.env));
|
|
14565
|
+
} else {
|
|
14566
|
+
idleTimer = setInterval(() => {}, 1 << 30);
|
|
14567
|
+
}
|
|
14409
14568
|
const cleanup = () => {
|
|
14569
|
+
clearInterval(idleTimer);
|
|
14410
14570
|
releaseLock(lockPath);
|
|
14411
14571
|
process.exit(0);
|
|
14412
14572
|
};
|
|
@@ -14551,8 +14711,8 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
14551
14711
|
process.stdout.write(`agent-scheduler: ${agentName} cheap-cron ENABLED` + (recovered > 0 ? ` (recovered ${recovered} pending escalation(s))` : "") + `
|
|
14552
14712
|
`);
|
|
14553
14713
|
}
|
|
14554
|
-
const
|
|
14555
|
-
entries,
|
|
14714
|
+
const registerForEntries = (es) => registerAgentSchedule({
|
|
14715
|
+
entries: es,
|
|
14556
14716
|
channel,
|
|
14557
14717
|
sink,
|
|
14558
14718
|
cronLib,
|
|
@@ -14560,10 +14720,28 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
14560
14720
|
...quotaGate ? { quotaGate } : {},
|
|
14561
14721
|
...cheapCron ? { cheapCron } : {}
|
|
14562
14722
|
});
|
|
14723
|
+
const tasks = registerForEntries(entries);
|
|
14563
14724
|
process.stdout.write(`agent-scheduler: ${agentName} registered ${tasks.length} task(s); ` + `chat=${channel.chatId} thread=${channel.threadId ?? "(none)"} ` + `socket=${socketPath} jsonl=${jsonlPath}
|
|
14564
14725
|
`);
|
|
14726
|
+
let reloader;
|
|
14727
|
+
let reloadTimer;
|
|
14728
|
+
if (isHotReloadEnabled(process.env)) {
|
|
14729
|
+
reloader = createScheduleReloader({
|
|
14730
|
+
loadEntries: () => collectScheduleEntries(loadConfig(configPath)).filter((e) => e.agent === agentName),
|
|
14731
|
+
register: registerForEntries,
|
|
14732
|
+
initialTasks: tasks,
|
|
14733
|
+
initialEntries: entries,
|
|
14734
|
+
log: (m) => process.stdout.write(`agent-scheduler: ${agentName} ${m}
|
|
14735
|
+
`),
|
|
14736
|
+
onError: (e) => process.stderr.write(`agent-scheduler: ${agentName} reload skipped (config error, keeping current schedule): ${e.message}
|
|
14737
|
+
`)
|
|
14738
|
+
});
|
|
14739
|
+
reloadTimer = setInterval(() => reloader.tick(), resolveReloadPollMs(process.env));
|
|
14740
|
+
}
|
|
14565
14741
|
const shutdown = () => {
|
|
14566
|
-
|
|
14742
|
+
if (reloadTimer)
|
|
14743
|
+
clearInterval(reloadTimer);
|
|
14744
|
+
for (const t of reloader ? reloader.currentTasks() : tasks)
|
|
14567
14745
|
t.task.stop();
|
|
14568
14746
|
sink.close();
|
|
14569
14747
|
ipcClient.close();
|
|
@@ -14581,10 +14759,14 @@ if (import.meta.url === `file://${process.argv[1]}` && /(?:^|[/\\])agent-schedul
|
|
|
14581
14759
|
});
|
|
14582
14760
|
}
|
|
14583
14761
|
export {
|
|
14762
|
+
scheduleSignature,
|
|
14763
|
+
resolveReloadPollMs,
|
|
14584
14764
|
resolveEntryThreadId,
|
|
14585
14765
|
resolveChannelTarget,
|
|
14586
14766
|
registerAgentSchedule,
|
|
14587
14767
|
recoverPendingEscalations,
|
|
14588
14768
|
main,
|
|
14589
|
-
|
|
14769
|
+
isHotReloadEnabled,
|
|
14770
|
+
ipcDispatcher,
|
|
14771
|
+
createScheduleReloader
|
|
14590
14772
|
};
|
|
@@ -11232,6 +11232,10 @@ var ReactionsSchema = exports_external.object({
|
|
|
11232
11232
|
per_hour_cap: exports_external.number().int().nonnegative().optional().describe("Max reaction-triggered synthetic turns per chat per rolling hour. " + "Refusals are stderr-logged but not surfaced to the agent. " + "Default 10. Set to 0 to disable triggering via the cap path."),
|
|
11233
11233
|
group_admin_only: exports_external.boolean().optional().describe("In groups/supergroups (negative chat_id), only trigger a synthetic " + "turn when the reacter is a chat admin (creator or administrator). " + "Failing the lookup is treated as non-admin (fail-closed). " + "DMs are never affected by this flag — the reacter IS the user. " + "Default true.")
|
|
11234
11234
|
}).optional();
|
|
11235
|
+
var ReactionDispatchSchema = exports_external.object({
|
|
11236
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false — " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
11237
|
+
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) — a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
11238
|
+
}).optional();
|
|
11235
11239
|
var ReleaseBlock = exports_external.object({
|
|
11236
11240
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
11237
11241
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -11267,6 +11271,7 @@ var profileFields = {
|
|
|
11267
11271
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
11268
11272
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
|
|
11269
11273
|
reactions: ReactionsSchema,
|
|
11274
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11270
11275
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
11271
11276
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11272
11277
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Omit to use Claude's default (acceptEdits for switchroom agents). " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -11336,6 +11341,7 @@ var AgentSchema = exports_external.object({
|
|
|
11336
11341
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
11337
11342
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
|
|
11338
11343
|
reactions: ReactionsSchema,
|
|
11344
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11339
11345
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
|
|
11340
11346
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11341
11347
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -12001,6 +12007,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
12001
12007
|
}
|
|
12002
12008
|
merged.reactions = combined;
|
|
12003
12009
|
}
|
|
12010
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
12011
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
12012
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
12013
|
+
const base = dReactionDispatch ?? {};
|
|
12014
|
+
const override = mReactionDispatch ?? {};
|
|
12015
|
+
const combined = { ...base };
|
|
12016
|
+
for (const [k, v] of Object.entries(override)) {
|
|
12017
|
+
if (v !== undefined)
|
|
12018
|
+
combined[k] = v;
|
|
12019
|
+
}
|
|
12020
|
+
merged.reaction_dispatch = combined;
|
|
12021
|
+
}
|
|
12004
12022
|
if (defaults.resources || merged.resources) {
|
|
12005
12023
|
const d = defaults.resources ?? {};
|
|
12006
12024
|
const a = merged.resources ?? {};
|
|
@@ -11980,6 +11980,10 @@ var ReactionsSchema = exports_external.object({
|
|
|
11980
11980
|
per_hour_cap: exports_external.number().int().nonnegative().optional().describe("Max reaction-triggered synthetic turns per chat per rolling hour. " + "Refusals are stderr-logged but not surfaced to the agent. " + "Default 10. Set to 0 to disable triggering via the cap path."),
|
|
11981
11981
|
group_admin_only: exports_external.boolean().optional().describe("In groups/supergroups (negative chat_id), only trigger a synthetic " + "turn when the reacter is a chat admin (creator or administrator). " + "Failing the lookup is treated as non-admin (fail-closed). " + "DMs are never affected by this flag \u2014 the reacter IS the user. " + "Default true.")
|
|
11982
11982
|
}).optional();
|
|
11983
|
+
var ReactionDispatchSchema = exports_external.object({
|
|
11984
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false \u2014 " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
11985
|
+
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) \u2014 a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
11986
|
+
}).optional();
|
|
11983
11987
|
var ReleaseBlock = exports_external.object({
|
|
11984
11988
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
11985
11989
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -12015,6 +12019,7 @@ var profileFields = {
|
|
|
12015
12019
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
12016
12020
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
|
|
12017
12021
|
reactions: ReactionsSchema,
|
|
12022
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
12018
12023
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
12019
12024
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
12020
12025
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Omit to use Claude's default (acceptEdits for switchroom agents). " + "Warning: bypassPermissions and dontAsk skip all safety checks \u2014 use only in trusted sandboxes."),
|
|
@@ -12084,6 +12089,7 @@ var AgentSchema = exports_external.object({
|
|
|
12084
12089
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
12085
12090
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
|
|
12086
12091
|
reactions: ReactionsSchema,
|
|
12092
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
12087
12093
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
|
|
12088
12094
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
12089
12095
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks \u2014 use only in trusted sandboxes."),
|
|
@@ -12751,6 +12757,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
12751
12757
|
}
|
|
12752
12758
|
merged.reactions = combined;
|
|
12753
12759
|
}
|
|
12760
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
12761
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
12762
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
12763
|
+
const base = dReactionDispatch ?? {};
|
|
12764
|
+
const override = mReactionDispatch ?? {};
|
|
12765
|
+
const combined = { ...base };
|
|
12766
|
+
for (const [k, v] of Object.entries(override)) {
|
|
12767
|
+
if (v !== undefined)
|
|
12768
|
+
combined[k] = v;
|
|
12769
|
+
}
|
|
12770
|
+
merged.reaction_dispatch = combined;
|
|
12771
|
+
}
|
|
12754
12772
|
if (defaults.resources || merged.resources) {
|
|
12755
12773
|
const d = defaults.resources ?? {};
|
|
12756
12774
|
const a = merged.resources ?? {};
|