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.
@@ -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 === "1" || v === "true" || v === "on";
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 routing = resolveCronRouting(entry, { cheapCronEnabled: cheapEnabled });
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
- setInterval(() => {}, 1 << 30);
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 tasks = registerAgentSchedule({
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
- for (const t of tasks)
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
- ipcDispatcher
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 ?? {};