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.
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
13520
13520
  });
13521
13521
 
13522
13522
  // src/config/schema.ts
13523
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
13523
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
13524
13524
  var init_schema = __esm(() => {
13525
13525
  init_zod();
13526
13526
  CodeRepoEntrySchema = exports_external.object({
@@ -13796,6 +13796,10 @@ var init_schema = __esm(() => {
13796
13796
  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."),
13797
13797
  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.")
13798
13798
  }).optional();
13799
+ ReactionDispatchSchema = exports_external.object({
13800
+ 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."),
13801
+ 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.")
13802
+ }).optional();
13799
13803
  ReleaseBlock = exports_external.object({
13800
13804
  channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
13801
13805
  pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
@@ -13831,6 +13835,7 @@ var init_schema = __esm(() => {
13831
13835
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
13832
13836
  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)."),
13833
13837
  reactions: ReactionsSchema,
13838
+ reaction_dispatch: ReactionDispatchSchema,
13834
13839
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
13835
13840
  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."),
13836
13841
  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."),
@@ -13900,6 +13905,7 @@ var init_schema = __esm(() => {
13900
13905
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
13901
13906
  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(),
13902
13907
  reactions: ReactionsSchema,
13908
+ reaction_dispatch: ReactionDispatchSchema,
13903
13909
  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')"),
13904
13910
  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."),
13905
13911
  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."),
@@ -14617,6 +14623,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14617
14623
  }
14618
14624
  merged.reactions = combined;
14619
14625
  }
14626
+ const dReactionDispatch = defaults.reaction_dispatch;
14627
+ const mReactionDispatch = merged.reaction_dispatch;
14628
+ if (dReactionDispatch || mReactionDispatch) {
14629
+ const base = dReactionDispatch ?? {};
14630
+ const override = mReactionDispatch ?? {};
14631
+ const combined = { ...base };
14632
+ for (const [k, v] of Object.entries(override)) {
14633
+ if (v !== undefined)
14634
+ combined[k] = v;
14635
+ }
14636
+ merged.reaction_dispatch = combined;
14637
+ }
14620
14638
  if (defaults.resources || merged.resources) {
14621
14639
  const d = defaults.resources ?? {};
14622
14640
  const a = merged.resources ?? {};
@@ -15417,6 +15435,93 @@ var init_cron_routing = __esm(() => {
15417
15435
  OPUS_MODEL_RE = /opus/i;
15418
15436
  });
15419
15437
 
15438
+ // src/scheduler/cron-cadence.ts
15439
+ function csvSmallestGap(field) {
15440
+ if (!field.includes(","))
15441
+ return null;
15442
+ const parts = field.split(",").map((s) => Number(s)).filter((n) => Number.isInteger(n) && n >= 0);
15443
+ if (parts.length < 2)
15444
+ return null;
15445
+ const sorted = [...parts].sort((a, b) => a - b);
15446
+ let smallest = Infinity;
15447
+ for (let i = 1;i < sorted.length; i++) {
15448
+ const gap = sorted[i] - sorted[i - 1];
15449
+ if (gap > 0 && gap < smallest)
15450
+ smallest = gap;
15451
+ }
15452
+ return Number.isFinite(smallest) ? smallest : null;
15453
+ }
15454
+ function estimateCronGapMin(expr) {
15455
+ const fields = expr.trim().split(/\s+/);
15456
+ if (fields.length < 5)
15457
+ return Infinity;
15458
+ const [min, hour] = fields;
15459
+ if (min === "*")
15460
+ return 1;
15461
+ const minStep = min.match(/^\*\/(\d+)$/);
15462
+ if (minStep) {
15463
+ const n = Number(minStep[1]);
15464
+ return n > 0 ? n : Infinity;
15465
+ }
15466
+ const minCsv = csvSmallestGap(min);
15467
+ if (minCsv !== null)
15468
+ return minCsv;
15469
+ if (!/^\d+$/.test(min))
15470
+ return Infinity;
15471
+ if (hour === "*")
15472
+ return 60;
15473
+ const hourStep = hour.match(/^\*\/(\d+)$/);
15474
+ if (hourStep) {
15475
+ const n = Number(hourStep[1]);
15476
+ return n > 0 ? n * 60 : Infinity;
15477
+ }
15478
+ const hourCsv = csvSmallestGap(hour);
15479
+ if (hourCsv !== null)
15480
+ return hourCsv * 60;
15481
+ if (/^\d+$/.test(hour))
15482
+ return 1440;
15483
+ return Infinity;
15484
+ }
15485
+
15486
+ // src/scheduler/tier-selector.ts
15487
+ function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
15488
+ if (input.kind === "poll") {
15489
+ return { tier: "poll", source: "explicit", reason: "declared kind: poll (model-free check)" };
15490
+ }
15491
+ if (input.context === "fresh") {
15492
+ return { tier: "cheap", source: "explicit", reason: "declared context: fresh (cheap cron session)" };
15493
+ }
15494
+ if (input.context === "agent") {
15495
+ return { tier: "main", source: "explicit", reason: "declared context: agent (full live session)" };
15496
+ }
15497
+ if (input.model !== undefined) {
15498
+ return isKnownCheapModel(input.model) ? { tier: "cheap", source: "explicit", reason: `cheap model '${input.model}' \u2192 cheap cron session` } : { tier: "main", source: "explicit", reason: `model '${input.model}' is not a known-cheap id \u2192 full live session` };
15499
+ }
15500
+ if (input.smallestGapMin <= frequentGapMin) {
15501
+ return {
15502
+ tier: "cheap",
15503
+ source: "cadence-default",
15504
+ reason: `fires every ~${input.smallestGapMin}min (\u2264 ${frequentGapMin}min) \u2014 defaulting to a cheap ` + `session; set context: agent (or an Opus/custom model) if this needs the agent's full context`
15505
+ };
15506
+ }
15507
+ return {
15508
+ tier: "main",
15509
+ source: "cadence-default",
15510
+ reason: `fires every ~${input.smallestGapMin}min (> ${frequentGapMin}min) \u2014 defaulting to the agent's ` + `full session; set model: sonnet (or context: fresh) to run it cheaply`
15511
+ };
15512
+ }
15513
+ function applyDefaultTier(entry, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
15514
+ if (entry.kind === "poll" || entry.context !== undefined || entry.model !== undefined) {
15515
+ return entry;
15516
+ }
15517
+ const rec = recommendCronTier({ smallestGapMin: estimateCronGapMin(entry.cron) }, frequentGapMin);
15518
+ return rec.tier === "cheap" ? { ...entry, context: "fresh" } : entry;
15519
+ }
15520
+ var DEFAULT_FREQUENT_GAP_MIN = 60;
15521
+ var init_tier_selector = __esm(() => {
15522
+ init_cron_routing();
15523
+ });
15524
+
15420
15525
  // src/config/timezone.ts
15421
15526
  import { readFileSync as readFileSync3, readlinkSync } from "node:fs";
15422
15527
  function defaultReadEtcTimezone() {
@@ -23244,7 +23349,7 @@ function describeAgents(config) {
23244
23349
  const resolved = resolveAgentConfig(config.defaults, config.profiles, agent);
23245
23350
  const profile = agent.extends ?? "default";
23246
23351
  const uid = allocateAgentUid(name);
23247
- const cronSession = scheduleNeedsCronSession((resolved.schedule ?? []).map((e) => ({ kind: e.kind, model: e.model, context: e.context })), { cheapCronEnabled: true });
23352
+ const cronSession = scheduleNeedsCronSession((resolved.schedule ?? []).map((e) => applyDefaultTier({ cron: e.cron, kind: e.kind, model: e.model, context: e.context })), { cheapCronEnabled: true });
23248
23353
  const resources = resolveResourceDefaults(name, profile, resolved.resources, { cronSession });
23249
23354
  const strippedCaps = readStrippedCaps(agent);
23250
23355
  out.push({
@@ -23742,6 +23847,7 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23742
23847
  var AGENT_UID_MIN = 10001, AGENT_UID_MAX = 10999, RESOURCE_BY_PROFILE, CRON_SESSION_MEM_BUMP_MIB = 512, CRON_SESSION_PIDS_BUMP = 128, BIND_MOUNT_SOURCE_DENYLIST, BIND_MOUNT_TARGET_DENYLIST, BIND_MOUNT_EXACT_SOURCE_DENY, CONTAINER_CONFIG_PATH = "/state/config/switchroom.yaml";
23743
23848
  var init_compose = __esm(() => {
23744
23849
  init_cron_routing();
23850
+ init_tier_selector();
23745
23851
  init_merge();
23746
23852
  init_timezone();
23747
23853
  init_peercred();
@@ -49409,6 +49515,10 @@ function dispatchTool(name, args) {
49409
49515
  base.push("--name", a.name);
49410
49516
  if (a.secrets && a.secrets.length > 0)
49411
49517
  base.push("--secrets", a.secrets.join(","));
49518
+ if (a.model)
49519
+ base.push("--model", a.model);
49520
+ if (a.context)
49521
+ base.push("--context", a.context);
49412
49522
  cliArgs = base;
49413
49523
  parseMode = "json";
49414
49524
  break;
@@ -49593,7 +49703,7 @@ var init_server3 = __esm(() => {
49593
49703
  },
49594
49704
  {
49595
49705
  name: "schedule_add",
49596
- description: "Append a cron schedule entry to the agent's overlay dir. " + "Overlay-sourced entries with non-empty `secrets:` are REJECTED " + "(E_OVERLAY_SECRETS_REQUIRES_APPROVAL); operator-authored entries " + "in switchroom.yaml are unaffected.",
49706
+ description: "Append a cron schedule entry to your overlay. Takes effect within ~30s \u2014 " + "the scheduler hot-reloads, no restart needed. Overlay entries with " + "non-empty `secrets:` are REJECTED (E_OVERLAY_SECRETS_REQUIRES_APPROVAL). " + "COST: by default each fire runs as a full turn in your live session " + "(your model, your whole context) \u2014 fine for work that needs your memory/" + "persona, but costly for routine checks. For a lighter recurring task, set " + '`model: "sonnet"` to run that fire in a cheap, minimal-context cron ' + "session instead (saves tokens; needs cheap-cron enabled by the operator). " + "For 'only do something when X changes' (e.g. a webpage, or a reaction), " + "ask the operator to set up a poll or reaction-dispatch instead of a " + "frequent prompt cron \u2014 far cheaper than polling with a full turn.",
49597
49707
  inputSchema: {
49598
49708
  type: "object",
49599
49709
  required: ["cron_expr", "prompt"],
@@ -49601,7 +49711,16 @@ var init_server3 = __esm(() => {
49601
49711
  cron_expr: { type: "string" },
49602
49712
  prompt: { type: "string", minLength: 1, maxLength: 4000 },
49603
49713
  secrets: { type: "array", items: { type: "string" } },
49604
- name: { type: "string", pattern: "^[a-z0-9-]{1,40}$" }
49714
+ name: { type: "string", pattern: "^[a-z0-9-]{1,40}$" },
49715
+ model: {
49716
+ type: "string",
49717
+ description: "Optional cheap-cron tier hint. A known-cheap model ('sonnet'/'haiku') " + "routes this fire to a fresh, minimal-context cron session (Tier 1) " + "instead of your full live session (Tier 2) \u2014 cheaper per fire. Omit " + "for context-heavy work that needs your memory/persona. Inert unless " + "the operator has enabled cheap-cron."
49718
+ },
49719
+ context: {
49720
+ type: "string",
49721
+ enum: ["fresh", "agent"],
49722
+ description: "Tier hint: 'fresh' = minimal-context cheap session (Tier 1); 'agent' " + "= your full live session (Tier 2). Usually inferred from `model`."
49723
+ }
49605
49724
  }
49606
49725
  }
49607
49726
  },
@@ -50204,8 +50323,8 @@ var {
50204
50323
  } = import__.default;
50205
50324
 
50206
50325
  // src/build-info.ts
50207
- var VERSION = "0.15.7";
50208
- var COMMIT_SHA = "c0a8b988";
50326
+ var VERSION = "0.15.9";
50327
+ var COMMIT_SHA = "6ed776e2";
50209
50328
 
50210
50329
  // src/cli/agent.ts
50211
50330
  init_source();
@@ -50316,6 +50435,7 @@ var LEGACY_CRON_SCRIPT_BASENAME_RE = /^cron-(\d+)\.sh$/;
50316
50435
  // src/agents/scaffold.ts
50317
50436
  init_schema();
50318
50437
  init_cron_routing();
50438
+ init_tier_selector();
50319
50439
  init_merge();
50320
50440
  init_timezone();
50321
50441
 
@@ -50409,6 +50529,13 @@ function stripSecretValues(value) {
50409
50529
  function restartRequiredNote(agent) {
50410
50530
  return `Not live yet \u2014 claude loads skills, MCP servers and scheduled ` + `tasks at process start. Run \`switchroom agent restart ${agent}\` ` + `for this to take effect.`;
50411
50531
  }
50532
+ function scheduleRestartRequired() {
50533
+ return (process.env.SWITCHROOM_SCHEDULER_HOT_RELOAD ?? "") === "0";
50534
+ }
50535
+ function scheduleLiveNote(agent) {
50536
+ const hotReload = (process.env.SWITCHROOM_SCHEDULER_HOT_RELOAD ?? "") !== "0";
50537
+ return hotReload ? `Live within ~30s \u2014 the in-agent scheduler hot-reloads the overlay; no restart needed.` : `Not live yet \u2014 hot-reload is disabled (SWITCHROOM_SCHEDULER_HOT_RELOAD=0). ` + `Run \`switchroom agent restart ${agent}\` for this to take effect.`;
50538
+ }
50412
50539
  function getAgentSlice(config, agent) {
50413
50540
  const slice = config.agents?.[agent];
50414
50541
  if (!slice) {
@@ -51451,11 +51578,7 @@ function renderFleetInvariants() {
51451
51578
  `);
51452
51579
  }
51453
51580
  function buildCronSessionContext(agentConfig) {
51454
- const entries = (agentConfig.schedule ?? []).map((e) => ({
51455
- kind: e.kind,
51456
- model: e.model,
51457
- context: e.context
51458
- }));
51581
+ const entries = (agentConfig.schedule ?? []).map((e) => applyDefaultTier({ cron: e.cron, kind: e.kind, model: e.model, context: e.context }));
51459
51582
  return {
51460
51583
  cronSessionEnabled: scheduleNeedsCronSession(entries, { cheapCronEnabled: true }),
51461
51584
  cronModelQ: shellSingleQuote(DEFAULT_CRON_MODEL)
@@ -68118,6 +68241,7 @@ init_source();
68118
68241
  // src/web/server.ts
68119
68242
  init_merge();
68120
68243
  init_loader();
68244
+ init_client();
68121
68245
  init_lifecycle();
68122
68246
  import {
68123
68247
  readFileSync as readFileSync45,
@@ -74490,12 +74614,37 @@ function loadWebhookSecrets() {
74490
74614
  return {};
74491
74615
  }
74492
74616
  }
74617
+ async function resolveWebhookSecretFromVault(agent, source, config) {
74618
+ const key = `webhook/${agent}/${source}`;
74619
+ try {
74620
+ const socket = resolveBrokerSocketPath({
74621
+ vaultBrokerSocket: config.vault?.broker?.socket ? resolvePath(config.vault.broker.socket) : undefined
74622
+ });
74623
+ const result = await getViaBrokerStructured(key, { socket });
74624
+ if (result.kind === "ok" && result.entry.kind === "string") {
74625
+ return result.entry.value;
74626
+ }
74627
+ if (result.kind === "denied" || result.kind === "unreachable") {
74628
+ process.stderr.write(`webhook-ingest: vault resolve for ${key} \u2192 ${result.kind}` + `${"code" in result && result.code ? ` (${result.code})` : ""}` + `; webhook will 401 until the secret is operator-readable
74629
+ `);
74630
+ }
74631
+ } catch (err) {
74632
+ process.stderr.write(`webhook-ingest: vault resolve for ${key} threw: ${err.message}
74633
+ `);
74634
+ }
74635
+ return null;
74636
+ }
74493
74637
  async function handleWebhookRoute(req, agent, source, config) {
74494
74638
  const agentConfigRaw = config.agents[agent];
74495
74639
  const agentConfig = agentConfigRaw ? resolveAgentConfig(config.defaults, config.profiles, agentConfigRaw) : undefined;
74496
74640
  const allowedSources = agentConfig?.channels?.telegram?.webhook_sources ?? [];
74497
74641
  const allSecrets = loadWebhookSecrets();
74498
- const agentSecrets = allSecrets[agent] ?? {};
74642
+ const agentSecrets = { ...allSecrets[agent] ?? {} };
74643
+ if (!agentSecrets[source]) {
74644
+ const fromVault = await resolveWebhookSecretFromVault(agent, source, config);
74645
+ if (fromVault)
74646
+ agentSecrets[source] = fromVault;
74647
+ }
74499
74648
  const requireEdge = agentConfig?.channels?.telegram?.webhook_require_edge === true;
74500
74649
  const edgeSecret = requireEdge ? loadEdgeSecret() : null;
74501
74650
  let bodyBuf;
@@ -82933,6 +83082,10 @@ function scheduleAdd(opts) {
82933
83082
  entry.secrets = opts.secrets;
82934
83083
  if (opts.name)
82935
83084
  entry.name = opts.name;
83085
+ if (opts.model)
83086
+ entry.model = opts.model;
83087
+ if (opts.context)
83088
+ entry.context = opts.context;
82936
83089
  const doc = {
82937
83090
  schedule: [
82938
83091
  Object.fromEntries(Object.entries(entry).filter(([k]) => k !== "name"))
@@ -83017,8 +83170,8 @@ function scheduleAdd(opts) {
83017
83170
  path: path8,
83018
83171
  cron_hash: hash2,
83019
83172
  would_recreate: false,
83020
- restart_required: true,
83021
- restart_hint: restartRequiredNote(agent)
83173
+ restart_required: scheduleRestartRequired(),
83174
+ restart_hint: scheduleLiveNote(agent)
83022
83175
  };
83023
83176
  }
83024
83177
  function scheduleAddOrStage(opts) {
@@ -83043,6 +83196,10 @@ function scheduleAddOrStage(opts) {
83043
83196
  entry.secrets = opts.secrets;
83044
83197
  if (opts.name)
83045
83198
  entry.name = opts.name;
83199
+ if (opts.model)
83200
+ entry.model = opts.model;
83201
+ if (opts.context)
83202
+ entry.context = opts.context;
83046
83203
  const doc = {
83047
83204
  schedule: [
83048
83205
  Object.fromEntries(Object.entries(entry).filter(([k]) => k !== "name"))
@@ -83142,13 +83299,17 @@ function scheduleRemove(opts) {
83142
83299
  ok: true,
83143
83300
  slug: match.slug,
83144
83301
  path: match.path,
83145
- restart_required: true,
83146
- restart_hint: restartRequiredNote(agent)
83302
+ restart_required: scheduleRestartRequired(),
83303
+ restart_hint: scheduleLiveNote(agent)
83147
83304
  };
83148
83305
  }
83149
83306
  function registerAgentConfigWriteCommands(program3) {
83150
83307
  const schedule = program3.command("schedule").description("Add / remove an agent's scheduled cron entries (overlay-backed)");
83151
- schedule.command("add").description("Append a schedule entry to the agent's overlay dir").requiredOption("--cron <expr>", "Cron expression").requiredOption("--prompt <text>", "Prompt to fire at the scheduled time").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").option("--secrets <list>", "Comma-separated vault keys (REJECTED for agent-authored overlays)").option("--name <slug>", "Optional human-readable name (a-z 0-9 -)").option("--stage-on-reject", "When a security gate trips (secrets/quota/min-interval), stage the entry under .pending/ for operator approval instead of rejecting with exit 9. Used by the MCP path; operator CLI defaults to off.").action(async (opts) => {
83308
+ schedule.command("add").description("Append a schedule entry to the agent's overlay dir").requiredOption("--cron <expr>", "Cron expression").requiredOption("--prompt <text>", "Prompt to fire at the scheduled time").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").option("--secrets <list>", "Comma-separated vault keys (REJECTED for agent-authored overlays)").option("--name <slug>", "Optional human-readable name (a-z 0-9 -)").option("--model <id>", "Cheap-cron tier hint: a known-cheap model (sonnet/haiku) routes this fire to a fresh, minimal-context cron session (Tier 1) instead of the agent's full live session (Tier 2), cutting token cost. Inert unless SWITCHROOM_CHEAP_CRON is on.").option("--context <mode>", "Tier hint: 'fresh' (minimal-context cheap session) or 'agent' (full live session). Unset \u2192 inferred from --model.").option("--stage-on-reject", "When a security gate trips (secrets/quota/min-interval), stage the entry under .pending/ for operator approval instead of rejecting with exit 9. Used by the MCP path; operator CLI defaults to off.").action(async (opts) => {
83309
+ if (opts.context && opts.context !== "fresh" && opts.context !== "agent") {
83310
+ emitError("E_INVALID_PROMPT", "--context must be 'fresh' or 'agent'");
83311
+ process.exit(1);
83312
+ }
83152
83313
  const secrets = opts.secrets ? opts.secrets.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
83153
83314
  if (opts.name && !/^[a-z0-9-]{1,40}$/.test(opts.name)) {
83154
83315
  emitError("E_INVALID_PROMPT", "name must match [a-z0-9-]{1,40}");
@@ -83166,7 +83327,9 @@ function registerAgentConfigWriteCommands(program3) {
83166
83327
  cronExpr: opts.cron,
83167
83328
  prompt: opts.prompt,
83168
83329
  secrets,
83169
- name: opts.name
83330
+ name: opts.name,
83331
+ model: opts.model,
83332
+ context: opts.context
83170
83333
  });
83171
83334
  } catch (err) {
83172
83335
  process.stderr.write(`${err.message}
@@ -85050,7 +85213,11 @@ import { homedir as homedir48 } from "node:os";
85050
85213
  import { join as join82 } from "node:path";
85051
85214
  import { spawnSync as spawnSync14 } from "node:child_process";
85052
85215
  init_audit_reader();
85053
- var DEFAULT_IMAGE_TAG = "latest";
85216
+ function resolveHostdImageTag(explicitTag, release) {
85217
+ if (explicitTag)
85218
+ return explicitTag;
85219
+ return resolveImageTag(resolveRelease({ root: release }));
85220
+ }
85054
85221
  var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
85055
85222
  function renderHostdComposeFile(opts) {
85056
85223
  const { hostHome, imageTag, operatorUid } = opts;
@@ -85202,9 +85369,10 @@ async function doInstall(opts, program3) {
85202
85369
  const dir = hostdDir();
85203
85370
  const composePath = hostdComposePath();
85204
85371
  mkdirSync47(dir, { recursive: true });
85372
+ const imageTag = resolveHostdImageTag(opts.tag, cfg.release);
85205
85373
  const yaml = renderHostdComposeFile({
85206
85374
  hostHome: resolveHostdHostHome(),
85207
- imageTag: opts.tag ?? DEFAULT_IMAGE_TAG,
85375
+ imageTag,
85208
85376
  operatorUid: resolveOperatorUid()
85209
85377
  });
85210
85378
  if (opts.dryRun) {
@@ -85221,12 +85389,12 @@ async function doInstall(opts, program3) {
85221
85389
  const adminAgents = Object.entries(cfg.agents ?? {}).filter(([, a]) => a?.admin === true).map(([name]) => name);
85222
85390
  console.log(source_default.dim(` agents served (one socket each): ${allAgents.length === 0 ? "(none)" : allAgents.join(", ")}`));
85223
85391
  console.log(source_default.dim(` admin agents (full config-edit verbs): ${adminAgents.length === 0 ? "(none)" : adminAgents.join(", ")}`));
85224
- console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${opts.tag ?? DEFAULT_IMAGE_TAG}\u2026`));
85392
+ console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${imageTag}\u2026`));
85225
85393
  const pull = runDocker(["compose", "-p", HOSTD_COMPOSE_PROJECT, "-f", composePath, "pull"]);
85226
85394
  if (!pull.ok) {
85227
85395
  console.error(source_default.red(` pull failed:
85228
85396
  ${pull.stderr}`));
85229
- console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-hostd:${opts.tag ?? DEFAULT_IMAGE_TAG}\` may not be published yet.
85397
+ console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-hostd:${imageTag}\` may not be published yet.
85230
85398
  ` + ` Check the docker-images workflow run and verify the tag at:
85231
85399
  ` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-hostd`));
85232
85400
  process.exit(1);
@@ -85311,7 +85479,7 @@ ${down.stderr}`));
85311
85479
  }
85312
85480
  function registerHostdCommand(program3) {
85313
85481
  const hostd = program3.command("hostd").description("Manage switchroom-hostd, the host-control daemon for admin agents (RFC C)");
85314
- hostd.command("install").description("Install or refresh the hostd container (writes ~/.switchroom/hostd/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag (default: latest)", DEFAULT_IMAGE_TAG).option("--dry-run", "Print the compose file and the docker commands without writing or running anything").action(withConfigError(async (opts) => {
85482
+ hostd.command("install").description("Install or refresh the hostd container (writes ~/.switchroom/hostd/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag override (default: resolved from release.pin in switchroom.yaml, else latest)").option("--dry-run", "Print the compose file and the docker commands without writing or running anything").action(withConfigError(async (opts) => {
85315
85483
  await doInstall(opts, program3);
85316
85484
  }));
85317
85485
  hostd.command("status").description("Show daemon state and bound sockets").action(() => doStatus());
@@ -85369,7 +85537,11 @@ import { existsSync as existsSync85, mkdirSync as mkdirSync48, writeFileSync as
85369
85537
  import { homedir as homedir49 } from "node:os";
85370
85538
  import { join as join83 } from "node:path";
85371
85539
  import { spawnSync as spawnSync15 } from "node:child_process";
85372
- var DEFAULT_IMAGE_TAG2 = "latest";
85540
+ function resolveWebImageTag(explicitTag, release) {
85541
+ if (explicitTag)
85542
+ return explicitTag;
85543
+ return resolveImageTag(resolveRelease({ root: release }));
85544
+ }
85373
85545
  var WEB_COMPOSE_PROJECT = "switchroom-web";
85374
85546
  function renderWebComposeFile(opts) {
85375
85547
  const { hostHome, imageTag, operatorUid } = opts;
@@ -85470,7 +85642,7 @@ function runDocker2(args) {
85470
85642
  stderr: r.stderr ?? ""
85471
85643
  };
85472
85644
  }
85473
- async function doInstall2(opts) {
85645
+ async function doInstall2(opts, program3) {
85474
85646
  const operatorUid = resolveOperatorUid();
85475
85647
  if (operatorUid === undefined) {
85476
85648
  console.error(source_default.red(`Could not resolve the operator uid (no SUDO_UID and getuid() is 0 or unavailable).
@@ -85481,9 +85653,11 @@ async function doInstall2(opts) {
85481
85653
  const dir = webdDir();
85482
85654
  const composePath = webdComposePath();
85483
85655
  mkdirSync48(dir, { recursive: true });
85656
+ const cfg = getConfig(program3);
85657
+ const imageTag = resolveWebImageTag(opts.tag, cfg.release);
85484
85658
  const yaml = renderWebComposeFile({
85485
85659
  hostHome: homedir49(),
85486
- imageTag: opts.tag ?? DEFAULT_IMAGE_TAG2,
85660
+ imageTag,
85487
85661
  operatorUid
85488
85662
  });
85489
85663
  if (opts.dryRun) {
@@ -85498,12 +85672,12 @@ async function doInstall2(opts) {
85498
85672
  writeFileSync41(composePath, yaml, "utf8");
85499
85673
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
85500
85674
  console.log(source_default.dim(` running as uid ${operatorUid} (operator), network_mode: host`));
85501
- console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${opts.tag ?? DEFAULT_IMAGE_TAG2}\u2026`));
85675
+ console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${imageTag}\u2026`));
85502
85676
  const pull = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composePath, "pull"]);
85503
85677
  if (!pull.ok) {
85504
85678
  console.error(source_default.red(` pull failed:
85505
85679
  ${pull.stderr}`));
85506
- console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${opts.tag ?? DEFAULT_IMAGE_TAG2}\` may not be published yet.
85680
+ console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${imageTag}\` may not be published yet.
85507
85681
  ` + ` Check the docker-images workflow run and verify the tag at:
85508
85682
  ` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-web`));
85509
85683
  process.exit(1);
@@ -85571,8 +85745,8 @@ ${down.stderr}`));
85571
85745
  }
85572
85746
  function registerWebdCommand(program3) {
85573
85747
  const webd = program3.command("webd").description("Manage switchroom-web, the dashboard + GitHub-webhook receiver container");
85574
- webd.command("install").description("Install or refresh the web container (writes ~/.switchroom/web/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag (default: latest)", DEFAULT_IMAGE_TAG2).option("--dry-run", "Print the compose file and the docker commands without writing or running anything").action(withConfigError(async (opts) => {
85575
- await doInstall2(opts);
85748
+ webd.command("install").description("Install or refresh the web container (writes ~/.switchroom/web/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag override (default: resolved from release.pin in switchroom.yaml, else latest)").option("--dry-run", "Print the compose file and the docker commands without writing or running anything").action(withConfigError(async (opts) => {
85749
+ await doInstall2(opts, program3);
85576
85750
  }));
85577
85751
  webd.command("status").description("Show web-service container state").action(() => doStatus2());
85578
85752
  webd.command("uninstall").description("Stop the web container. Leaves the compose file in place for re-install.").action(() => doUninstall2());
@@ -13967,6 +13967,10 @@ var ReactionsSchema = exports_external.object({
13967
13967
  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."),
13968
13968
  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.")
13969
13969
  }).optional();
13970
+ var ReactionDispatchSchema = exports_external.object({
13971
+ 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."),
13972
+ 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.")
13973
+ }).optional();
13970
13974
  var ReleaseBlock = exports_external.object({
13971
13975
  channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
13972
13976
  pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
@@ -14002,6 +14006,7 @@ var profileFields = {
14002
14006
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
14003
14007
  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)."),
14004
14008
  reactions: ReactionsSchema,
14009
+ reaction_dispatch: ReactionDispatchSchema,
14005
14010
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
14006
14011
  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."),
14007
14012
  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."),
@@ -14071,6 +14076,7 @@ var AgentSchema = exports_external.object({
14071
14076
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
14072
14077
  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(),
14073
14078
  reactions: ReactionsSchema,
14079
+ reaction_dispatch: ReactionDispatchSchema,
14074
14080
  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')"),
14075
14081
  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."),
14076
14082
  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."),
@@ -14746,6 +14752,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14746
14752
  }
14747
14753
  merged.reactions = combined;
14748
14754
  }
14755
+ const dReactionDispatch = defaults.reaction_dispatch;
14756
+ const mReactionDispatch = merged.reaction_dispatch;
14757
+ if (dReactionDispatch || mReactionDispatch) {
14758
+ const base = dReactionDispatch ?? {};
14759
+ const override = mReactionDispatch ?? {};
14760
+ const combined = { ...base };
14761
+ for (const [k, v] of Object.entries(override)) {
14762
+ if (v !== undefined)
14763
+ combined[k] = v;
14764
+ }
14765
+ merged.reaction_dispatch = combined;
14766
+ }
14749
14767
  if (defaults.resources || merged.resources) {
14750
14768
  const d = defaults.resources ?? {};
14751
14769
  const a = merged.resources ?? {};
@@ -4293,6 +4293,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
4293
4293
  }
4294
4294
  merged.reactions = combined;
4295
4295
  }
4296
+ const dReactionDispatch = defaults.reaction_dispatch;
4297
+ const mReactionDispatch = merged.reaction_dispatch;
4298
+ if (dReactionDispatch || mReactionDispatch) {
4299
+ const base = dReactionDispatch ?? {};
4300
+ const override = mReactionDispatch ?? {};
4301
+ const combined = { ...base };
4302
+ for (const [k, v] of Object.entries(override)) {
4303
+ if (v !== undefined)
4304
+ combined[k] = v;
4305
+ }
4306
+ merged.reaction_dispatch = combined;
4307
+ }
4296
4308
  if (defaults.resources || merged.resources) {
4297
4309
  const d = defaults.resources ?? {};
4298
4310
  const a = merged.resources ?? {};
@@ -11277,7 +11289,7 @@ var init_dist = __esm(() => {
11277
11289
  });
11278
11290
 
11279
11291
  // src/config/schema.ts
11280
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
11292
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
11281
11293
  var init_schema = __esm(() => {
11282
11294
  init_zod();
11283
11295
  CodeRepoEntrySchema = exports_external.object({
@@ -11553,6 +11565,10 @@ var init_schema = __esm(() => {
11553
11565
  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."),
11554
11566
  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.")
11555
11567
  }).optional();
11568
+ ReactionDispatchSchema = exports_external.object({
11569
+ 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."),
11570
+ 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.")
11571
+ }).optional();
11556
11572
  ReleaseBlock = exports_external.object({
11557
11573
  channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
11558
11574
  pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
@@ -11588,6 +11604,7 @@ var init_schema = __esm(() => {
11588
11604
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11589
11605
  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)."),
11590
11606
  reactions: ReactionsSchema,
11607
+ reaction_dispatch: ReactionDispatchSchema,
11591
11608
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11592
11609
  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."),
11593
11610
  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."),
@@ -11657,6 +11674,7 @@ var init_schema = __esm(() => {
11657
11674
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11658
11675
  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(),
11659
11676
  reactions: ReactionsSchema,
11677
+ reaction_dispatch: ReactionDispatchSchema,
11660
11678
  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')"),
11661
11679
  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."),
11662
11680
  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."),