switchroom 0.15.7 → 0.15.8

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.
@@ -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."),
@@ -327,6 +327,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
327
327
  }
328
328
  merged.reactions = combined;
329
329
  }
330
+ const dReactionDispatch = defaults.reaction_dispatch;
331
+ const mReactionDispatch = merged.reaction_dispatch;
332
+ if (dReactionDispatch || mReactionDispatch) {
333
+ const base = dReactionDispatch ?? {};
334
+ const override = mReactionDispatch ?? {};
335
+ const combined = { ...base };
336
+ for (const [k, v] of Object.entries(override)) {
337
+ if (v !== undefined)
338
+ combined[k] = v;
339
+ }
340
+ merged.reaction_dispatch = combined;
341
+ }
330
342
  if (defaults.resources || merged.resources) {
331
343
  const d = defaults.resources ?? {};
332
344
  const a = merged.resources ?? {};
@@ -11277,7 +11289,7 @@ var init_zod = __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."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.7",
3
+ "version": "0.15.8",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,14 +28,33 @@ tools is to let you do the edit yourself.
28
28
 
29
29
  ### Tools
30
30
 
31
- - **`schedule_add(cron_expr, prompt, name?, secrets?)`** — append a new schedule
32
- entry. The `prompt` is what *you* (the agent) will receive when the cron
33
- fires; phrase it from your future-self's perspective (e.g.
34
- `"Time for the daily digest pull yesterday's GitHub activity and DM the
35
- summary to chat 12345"`, not `"please send the digest"`). Optional
31
+ - **`schedule_add(cron_expr, prompt, name?, secrets?, model?, context?)`** —
32
+ append a new schedule entry. Takes effect within **~30s** (the scheduler
33
+ hot-reloads no restart needed). The `prompt` is what *you* (the agent) will
34
+ receive when the cron fires; phrase it from your future-self's perspective
35
+ (e.g. `"Time for the daily digest — pull yesterday's GitHub activity and DM
36
+ the summary to chat 12345"`, not `"please send the digest"`). Optional
36
37
  `name` is a stable slug for `schedule_remove`; if omitted, a 12-hex hash
37
38
  derived from the entry content is assigned.
38
39
 
40
+ **Mind the cost — pick the cheapest tier that does the job:**
41
+ - *Default (no `model`)* — the fire runs as a **full turn in your live
42
+ session**: your model, your whole context + memory. Right for work that
43
+ genuinely needs *you* (your persona, your conversation history). Costly for
44
+ routine checks — every fire pays your full context.
45
+ - *`model: "sonnet"`* (or `context: "fresh"`) — routes the fire to a **cheap,
46
+ minimal-context cron session** (Tier 1): a fresh Sonnet with just the
47
+ prompt, no heavy context. Use for light, self-contained recurring work
48
+ (summarise a feed, format a digest) where you don't need your memory. Much
49
+ cheaper per fire. *(Honoured only when the operator has enabled cheap-cron;
50
+ otherwise it still runs as a normal turn — never silently dropped.)*
51
+ - *"Only act when X changes"* — don't poll with a frequent prompt cron (every
52
+ fire is a wasted turn when nothing changed). Ask the **operator** to set up
53
+ a **poll** (model-free check, e.g. a webpage/API via `kind: poll`) or, for
54
+ reaction-triggered work, **`reaction_dispatch`** (an emoji reaction wakes
55
+ you instantly — zero polling). These need an operator config commit
56
+ (egress/identity gates), so request them rather than authoring them yourself.
57
+
39
58
  - **`schedule_remove(name | cron_hash)`** — delete by `name` (the slug from
40
59
  add) or by 12-hex `cron_hash` (shown in `cron_list` output). Both
41
60
  arguments are accepted; pass one.
@@ -23802,7 +23802,7 @@ var init_dist = __esm(() => {
23802
23802
  });
23803
23803
 
23804
23804
  // ../src/config/schema.ts
23805
- 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;
23805
+ 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;
23806
23806
  var init_schema = __esm(() => {
23807
23807
  init_zod();
23808
23808
  CodeRepoEntrySchema = exports_external.object({
@@ -24078,6 +24078,10 @@ var init_schema = __esm(() => {
24078
24078
  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."),
24079
24079
  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.")
24080
24080
  }).optional();
24081
+ ReactionDispatchSchema = exports_external.object({
24082
+ 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."),
24083
+ 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.")
24084
+ }).optional();
24081
24085
  ReleaseBlock = exports_external.object({
24082
24086
  channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
24083
24087
  pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
@@ -24113,6 +24117,7 @@ var init_schema = __esm(() => {
24113
24117
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
24114
24118
  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)."),
24115
24119
  reactions: ReactionsSchema,
24120
+ reaction_dispatch: ReactionDispatchSchema,
24116
24121
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
24117
24122
  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."),
24118
24123
  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."),
@@ -24182,6 +24187,7 @@ var init_schema = __esm(() => {
24182
24187
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
24183
24188
  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(),
24184
24189
  reactions: ReactionsSchema,
24190
+ reaction_dispatch: ReactionDispatchSchema,
24185
24191
  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')"),
24186
24192
  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."),
24187
24193
  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."),
@@ -24857,6 +24863,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
24857
24863
  }
24858
24864
  merged.reactions = combined;
24859
24865
  }
24866
+ const dReactionDispatch = defaults.reaction_dispatch;
24867
+ const mReactionDispatch = merged.reaction_dispatch;
24868
+ if (dReactionDispatch || mReactionDispatch) {
24869
+ const base = dReactionDispatch ?? {};
24870
+ const override = mReactionDispatch ?? {};
24871
+ const combined = { ...base };
24872
+ for (const [k, v] of Object.entries(override)) {
24873
+ if (v !== undefined)
24874
+ combined[k] = v;
24875
+ }
24876
+ merged.reaction_dispatch = combined;
24877
+ }
24860
24878
  if (defaults.resources || merged.resources) {
24861
24879
  const d = defaults.resources ?? {};
24862
24880
  const a = merged.resources ?? {};
@@ -45770,6 +45788,18 @@ function mergeAgentConfig2(defaultsIn, agentIn) {
45770
45788
  }
45771
45789
  merged.reactions = combined;
45772
45790
  }
45791
+ const dReactionDispatch = defaults.reaction_dispatch;
45792
+ const mReactionDispatch = merged.reaction_dispatch;
45793
+ if (dReactionDispatch || mReactionDispatch) {
45794
+ const base = dReactionDispatch ?? {};
45795
+ const override = mReactionDispatch ?? {};
45796
+ const combined = { ...base };
45797
+ for (const [k, v] of Object.entries(override)) {
45798
+ if (v !== undefined)
45799
+ combined[k] = v;
45800
+ }
45801
+ merged.reaction_dispatch = combined;
45802
+ }
45773
45803
  if (defaults.resources || merged.resources) {
45774
45804
  const d = defaults.resources ?? {};
45775
45805
  const a = merged.resources ?? {};
@@ -49848,6 +49878,50 @@ function escapeBody(s) {
49848
49878
  return s.replace(/</g, "&lt;").replace(/>/g, "&gt;");
49849
49879
  }
49850
49880
 
49881
+ // gateway/reaction-dispatch.ts
49882
+ var REACTION_DISPATCH_DEFAULTS = Object.freeze({
49883
+ enabled: false,
49884
+ emojis: Object.freeze(new Set)
49885
+ });
49886
+ function resolveReactionDispatchConfig(raw) {
49887
+ if (!raw)
49888
+ return REACTION_DISPATCH_DEFAULTS;
49889
+ return {
49890
+ enabled: raw.enabled ?? REACTION_DISPATCH_DEFAULTS.enabled,
49891
+ emojis: raw.emojis !== undefined ? new Set(raw.emojis) : REACTION_DISPATCH_DEFAULTS.emojis
49892
+ };
49893
+ }
49894
+ function evaluateReactionDispatch(cfg, c) {
49895
+ if (!cfg.enabled)
49896
+ return { ok: false, reason: "disabled" };
49897
+ if (c.emoji === null)
49898
+ return { ok: false, reason: "no_emoji" };
49899
+ if (!cfg.emojis.has(c.emoji))
49900
+ return { ok: false, reason: "emoji_not_in_allowlist" };
49901
+ return { ok: true };
49902
+ }
49903
+ function buildReactionDispatchInbound(input) {
49904
+ const safeEmoji = escapeAttr2(input.emoji);
49905
+ const safeUser = escapeAttr2(input.user);
49906
+ const safeChat = escapeAttr2(input.chatId);
49907
+ const body = escapeBody2(input.reactedText);
49908
+ const text = `<channel source="switchroom-telegram" event="reaction" ` + `emoji="${safeEmoji}" message_id="${input.messageId}" ` + `chat_id="${safeChat}" user="${safeUser}">` + body + `</channel>`;
49909
+ const meta = {
49910
+ source: "switchroom-telegram",
49911
+ event: "reaction",
49912
+ reaction_emoji: input.emoji,
49913
+ target_message_id: String(input.messageId),
49914
+ reacted_text: input.reactedText
49915
+ };
49916
+ return { text, meta };
49917
+ }
49918
+ function escapeAttr2(s) {
49919
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
49920
+ }
49921
+ function escapeBody2(s) {
49922
+ return s.replace(/</g, "&lt;").replace(/>/g, "&gt;");
49923
+ }
49924
+
49851
49925
  // gateway/pid-file.ts
49852
49926
  import { writeFileSync as writeFileSync11, readFileSync as readFileSync18, unlinkSync as unlinkSync7, renameSync as renameSync6 } from "node:fs";
49853
49927
  function writePidFile(path, record) {
@@ -53683,11 +53757,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
53683
53757
  }
53684
53758
 
53685
53759
  // ../src/build-info.ts
53686
- var VERSION = "0.15.7";
53687
- var COMMIT_SHA = "c0a8b988";
53688
- var COMMIT_DATE = "2026-06-12T04:28:04Z";
53689
- var LATEST_PR = 2286;
53690
- var COMMITS_AHEAD_OF_TAG = 0;
53760
+ var VERSION = "0.15.8";
53761
+ var COMMIT_SHA = "318cb85f";
53762
+ var COMMIT_DATE = "2026-06-12T23:50:50Z";
53763
+ var LATEST_PR = 2296;
53764
+ var COMMITS_AHEAD_OF_TAG = 1;
53691
53765
 
53692
53766
  // gateway/boot-version.ts
53693
53767
  function formatRelativeAgo(iso) {
@@ -65034,6 +65108,84 @@ function getReactionDebounce() {
65034
65108
  }
65035
65109
  return reactionDebounce;
65036
65110
  }
65111
+ var reactionDispatchCfg = null;
65112
+ function getReactionDispatchConfig() {
65113
+ if (reactionDispatchCfg)
65114
+ return reactionDispatchCfg;
65115
+ let raw = undefined;
65116
+ try {
65117
+ const cfg = loadConfig2();
65118
+ const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
65119
+ if (agentName3) {
65120
+ const rawAgent = cfg.agents?.[agentName3];
65121
+ if (rawAgent) {
65122
+ const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
65123
+ raw = resolved.reaction_dispatch;
65124
+ }
65125
+ }
65126
+ } catch (err) {
65127
+ process.stderr.write(`telegram gateway: reaction_dispatch: config load failed, defaulting OFF: ${err.message}
65128
+ `);
65129
+ }
65130
+ reactionDispatchCfg = resolveReactionDispatchConfig(raw ?? null);
65131
+ return reactionDispatchCfg;
65132
+ }
65133
+ function maybeDispatchReaction(args) {
65134
+ const cfg = getReactionDispatchConfig();
65135
+ const decision = evaluateReactionDispatch(cfg, { emoji: args.emoji, action: args.action });
65136
+ if (!decision.ok) {
65137
+ if (decision.reason === "emoji_not_in_allowlist" && cfg.enabled) {
65138
+ process.stderr.write(`telegram gateway: reaction_dispatch.reject reason=allowlist_miss emoji=${args.emoji} chat=${args.chatId}
65139
+ `);
65140
+ }
65141
+ return;
65142
+ }
65143
+ const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
65144
+ if (!agentName3) {
65145
+ process.stderr.write(`telegram gateway: reaction_dispatch: skipped \u2014 SWITCHROOM_AGENT_NAME unset
65146
+ `);
65147
+ return;
65148
+ }
65149
+ let reactedText = "";
65150
+ if (HISTORY_ENABLED) {
65151
+ try {
65152
+ const row = lookupMessageRoleAndText(args.chatId, args.messageId);
65153
+ reactedText = row?.text ?? "";
65154
+ } catch (err) {
65155
+ process.stderr.write(`telegram gateway: reaction_dispatch: history lookup failed: ${err}
65156
+ `);
65157
+ }
65158
+ }
65159
+ const { text, meta } = buildReactionDispatchInbound({
65160
+ emoji: args.emoji,
65161
+ chatId: args.chatId,
65162
+ messageId: args.messageId,
65163
+ user: args.user,
65164
+ userId: args.userId,
65165
+ reactedText,
65166
+ ...typeof args.threadId === "number" ? { threadId: args.threadId } : {}
65167
+ });
65168
+ const ts = Date.now();
65169
+ const inbound = {
65170
+ type: "inbound",
65171
+ chatId: args.chatId,
65172
+ ...typeof args.threadId === "number" ? { threadId: args.threadId } : {},
65173
+ messageId: ts,
65174
+ user: args.user,
65175
+ userId: args.userId,
65176
+ ts,
65177
+ text,
65178
+ meta
65179
+ };
65180
+ const delivered = ipcServer.sendToAgent(agentName3, inbound);
65181
+ if (delivered)
65182
+ markClaudeBusyForInbound(inbound);
65183
+ process.stderr.write(`telegram gateway: reaction_dispatch agent=${agentName3} chat=${args.chatId} emoji=${args.emoji} message_id=${args.messageId} delivered=${delivered}
65184
+ `);
65185
+ if (!delivered) {
65186
+ pendingInboundBuffer.push(agentName3, inbound);
65187
+ }
65188
+ }
65037
65189
  function flushReactionBatch(batch) {
65038
65190
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
65039
65191
  if (!agentName3) {
@@ -65109,11 +65261,21 @@ async function handleMessageReaction(ctx) {
65109
65261
  `);
65110
65262
  if (action === "remove" || emoji === null)
65111
65263
  return;
65112
- if (!HISTORY_ENABLED)
65113
- return;
65114
65264
  const reacter = update.user;
65115
65265
  if (!reacter)
65116
65266
  return;
65267
+ const reacterName = reacter.first_name ?? reacter.username ?? String(reacter.id);
65268
+ maybeDispatchReaction({
65269
+ chatId: chat_id,
65270
+ messageId: message_id,
65271
+ emoji,
65272
+ action,
65273
+ user: reacterName,
65274
+ userId: reacter.id,
65275
+ ...typeof update.message_thread_id === "number" ? { threadId: update.message_thread_id } : {}
65276
+ });
65277
+ if (!HISTORY_ENABLED)
65278
+ return;
65117
65279
  const cfg = getReactionsConfig();
65118
65280
  if (!cfg.enabled)
65119
65281
  return;
@@ -65163,7 +65325,7 @@ async function handleMessageReaction(ctx) {
65163
65325
  ts: Date.now(),
65164
65326
  preview,
65165
65327
  userId: reacter.id,
65166
- user: reacter.first_name ?? reacter.username ?? String(reacter.id),
65328
+ user: reacterName,
65167
65329
  ...typeof update.message_thread_id === "number" ? { threadId: update.message_thread_id } : {}
65168
65330
  };
65169
65331
  getReactionDebounce().enqueue(update.chat.id, pending2);