switchroom 0.15.7 → 0.15.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +189 -7
- package/dist/auth-broker/index.js +18 -0
- package/dist/cli/notion-write-pretool.mjs +18 -0
- package/dist/cli/switchroom.js +204 -30
- package/dist/host-control/main.js +18 -0
- package/dist/vault/approvals/kernel-server.js +19 -1
- package/dist/vault/broker/server.js +19 -1
- package/package.json +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +24 -5
- package/telegram-plugin/dist/gateway/gateway.js +170 -8
- package/telegram-plugin/gateway/gateway.ts +136 -2
- package/telegram-plugin/gateway/reaction-dispatch.ts +174 -0
- package/telegram-plugin/tests/reaction-dispatch.test.ts +137 -0
|
@@ -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
|
@@ -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?)`** —
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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, "<").replace(/>/g, ">");
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
49920
|
+
}
|
|
49921
|
+
function escapeBody2(s) {
|
|
49922
|
+
return s.replace(/</g, "<").replace(/>/g, ">");
|
|
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,10 +53757,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53683
53757
|
}
|
|
53684
53758
|
|
|
53685
53759
|
// ../src/build-info.ts
|
|
53686
|
-
var VERSION = "0.15.
|
|
53687
|
-
var COMMIT_SHA = "
|
|
53688
|
-
var COMMIT_DATE = "2026-06-
|
|
53689
|
-
var LATEST_PR =
|
|
53760
|
+
var VERSION = "0.15.9";
|
|
53761
|
+
var COMMIT_SHA = "6ed776e2";
|
|
53762
|
+
var COMMIT_DATE = "2026-06-13T00:47:35Z";
|
|
53763
|
+
var LATEST_PR = 2300;
|
|
53690
53764
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53691
53765
|
|
|
53692
53766
|
// gateway/boot-version.ts
|
|
@@ -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:
|
|
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);
|
|
@@ -366,6 +366,7 @@ import type {
|
|
|
366
366
|
PermissionEvent,
|
|
367
367
|
} from './ipc-protocol.js'
|
|
368
368
|
import { DebounceBuffer, HourCap, buildReactionInboundMeta, buildReactionInboundText, evaluateTriggerCandidate, isGroupChat, resolveReactionsConfig, truncatePreview, type PendingReaction, type ReactionBatch, type ReactionsResolvedConfig } from './reaction-trigger.js'
|
|
369
|
+
import { buildReactionDispatchInbound, evaluateReactionDispatch, resolveReactionDispatchConfig, type ReactionDispatchResolvedConfig } from './reaction-dispatch.js'
|
|
369
370
|
import { writePidFile, clearPidFile } from './pid-file.js'
|
|
370
371
|
import { acquireStartupLock, releaseStartupLock } from './startup-mutex.js'
|
|
371
372
|
import { drainShutdown } from './shutdown-drain.js'
|
|
@@ -19825,6 +19826,120 @@ function getReactionDebounce(): DebounceBuffer {
|
|
|
19825
19826
|
return reactionDebounce
|
|
19826
19827
|
}
|
|
19827
19828
|
|
|
19829
|
+
// ─── reaction_dispatch (#2291) ─────────────────────────────────────────────
|
|
19830
|
+
// Event-driven dispatch of ANY qualifying message_reaction as an inbound
|
|
19831
|
+
// `<channel event="reaction">` turn. Default OFF; independent of the
|
|
19832
|
+
// bot-authored `reactions` feedback path above.
|
|
19833
|
+
let reactionDispatchCfg: ReactionDispatchResolvedConfig | null = null
|
|
19834
|
+
|
|
19835
|
+
function getReactionDispatchConfig(): ReactionDispatchResolvedConfig {
|
|
19836
|
+
if (reactionDispatchCfg) return reactionDispatchCfg
|
|
19837
|
+
let raw: unknown = undefined
|
|
19838
|
+
try {
|
|
19839
|
+
const cfg = loadSwitchroomConfig()
|
|
19840
|
+
const agentName = process.env.SWITCHROOM_AGENT_NAME
|
|
19841
|
+
if (agentName) {
|
|
19842
|
+
const rawAgent = cfg.agents?.[agentName]
|
|
19843
|
+
if (rawAgent) {
|
|
19844
|
+
const resolved = resolveAgentConfig(cfg.defaults, cfg.profiles, rawAgent)
|
|
19845
|
+
raw = (resolved as { reaction_dispatch?: unknown }).reaction_dispatch
|
|
19846
|
+
}
|
|
19847
|
+
}
|
|
19848
|
+
} catch (err) {
|
|
19849
|
+
process.stderr.write(
|
|
19850
|
+
`telegram gateway: reaction_dispatch: config load failed, defaulting OFF: ${(err as Error).message}\n`,
|
|
19851
|
+
)
|
|
19852
|
+
}
|
|
19853
|
+
reactionDispatchCfg = resolveReactionDispatchConfig(
|
|
19854
|
+
raw as Parameters<typeof resolveReactionDispatchConfig>[0] ?? null,
|
|
19855
|
+
)
|
|
19856
|
+
return reactionDispatchCfg
|
|
19857
|
+
}
|
|
19858
|
+
|
|
19859
|
+
/**
|
|
19860
|
+
* Event-driven reaction → inbound turn (#2291). Called from the
|
|
19861
|
+
* message_reaction handler for add/change events. Filters by the
|
|
19862
|
+
* `reaction_dispatch` emoji allowlist (default empty ⇒ no-op), looks up
|
|
19863
|
+
* the reacted message's text from the SQLite history buffer (graceful on
|
|
19864
|
+
* miss), and injects an inbound shaped like a button-callback event via
|
|
19865
|
+
* the same ipcServer.sendToAgent path cron uses. Removals never reach
|
|
19866
|
+
* here (the caller filters them).
|
|
19867
|
+
*/
|
|
19868
|
+
function maybeDispatchReaction(args: {
|
|
19869
|
+
chatId: string
|
|
19870
|
+
messageId: number
|
|
19871
|
+
emoji: string | null
|
|
19872
|
+
action: 'add' | 'change'
|
|
19873
|
+
user: string
|
|
19874
|
+
userId: number
|
|
19875
|
+
threadId?: number
|
|
19876
|
+
}): void {
|
|
19877
|
+
const cfg = getReactionDispatchConfig()
|
|
19878
|
+
const decision = evaluateReactionDispatch(cfg, { emoji: args.emoji, action: args.action })
|
|
19879
|
+
if (!decision.ok) {
|
|
19880
|
+
if (decision.reason === 'emoji_not_in_allowlist' && cfg.enabled) {
|
|
19881
|
+
process.stderr.write(
|
|
19882
|
+
`telegram gateway: reaction_dispatch.reject reason=allowlist_miss emoji=${args.emoji} chat=${args.chatId}\n`,
|
|
19883
|
+
)
|
|
19884
|
+
}
|
|
19885
|
+
return
|
|
19886
|
+
}
|
|
19887
|
+
|
|
19888
|
+
const agentName = process.env.SWITCHROOM_AGENT_NAME
|
|
19889
|
+
if (!agentName) {
|
|
19890
|
+
process.stderr.write(
|
|
19891
|
+
`telegram gateway: reaction_dispatch: skipped — SWITCHROOM_AGENT_NAME unset\n`,
|
|
19892
|
+
)
|
|
19893
|
+
return
|
|
19894
|
+
}
|
|
19895
|
+
|
|
19896
|
+
// History lookup is best-effort; a miss yields an empty body. The
|
|
19897
|
+
// envelope still carries emoji / message_id / chat_id / user.
|
|
19898
|
+
let reactedText = ''
|
|
19899
|
+
if (HISTORY_ENABLED) {
|
|
19900
|
+
try {
|
|
19901
|
+
const row = lookupMessageRoleAndText(args.chatId, args.messageId)
|
|
19902
|
+
reactedText = row?.text ?? ''
|
|
19903
|
+
} catch (err) {
|
|
19904
|
+
process.stderr.write(
|
|
19905
|
+
`telegram gateway: reaction_dispatch: history lookup failed: ${err}\n`,
|
|
19906
|
+
)
|
|
19907
|
+
}
|
|
19908
|
+
}
|
|
19909
|
+
|
|
19910
|
+
const { text, meta } = buildReactionDispatchInbound({
|
|
19911
|
+
emoji: args.emoji!,
|
|
19912
|
+
chatId: args.chatId,
|
|
19913
|
+
messageId: args.messageId,
|
|
19914
|
+
user: args.user,
|
|
19915
|
+
userId: args.userId,
|
|
19916
|
+
reactedText,
|
|
19917
|
+
...(typeof args.threadId === 'number' ? { threadId: args.threadId } : {}),
|
|
19918
|
+
})
|
|
19919
|
+
|
|
19920
|
+
const ts = Date.now()
|
|
19921
|
+
const inbound: InboundMessage = {
|
|
19922
|
+
type: 'inbound',
|
|
19923
|
+
chatId: args.chatId,
|
|
19924
|
+
...(typeof args.threadId === 'number' ? { threadId: args.threadId } : {}),
|
|
19925
|
+
messageId: ts,
|
|
19926
|
+
user: args.user,
|
|
19927
|
+
userId: args.userId,
|
|
19928
|
+
ts,
|
|
19929
|
+
text,
|
|
19930
|
+
meta,
|
|
19931
|
+
}
|
|
19932
|
+
const delivered = ipcServer.sendToAgent(agentName, inbound)
|
|
19933
|
+
if (delivered) markClaudeBusyForInbound(inbound)
|
|
19934
|
+
process.stderr.write(
|
|
19935
|
+
`telegram gateway: reaction_dispatch agent=${agentName} chat=${args.chatId} ` +
|
|
19936
|
+
`emoji=${args.emoji} message_id=${args.messageId} delivered=${delivered}\n`,
|
|
19937
|
+
)
|
|
19938
|
+
if (!delivered) {
|
|
19939
|
+
pendingInboundBuffer.push(agentName, inbound)
|
|
19940
|
+
}
|
|
19941
|
+
}
|
|
19942
|
+
|
|
19828
19943
|
/**
|
|
19829
19944
|
* Dispatch a debounce-flushed batch as a synthetic InboundMessage via
|
|
19830
19945
|
* the same `ipcServer.sendToAgent` path the cron-fold-in uses.
|
|
@@ -19949,10 +20064,29 @@ async function handleMessageReaction(ctx: Context): Promise<void> {
|
|
|
19949
20064
|
// From here on we ignore failures rather than reject (the persist
|
|
19950
20065
|
// path above is the v1 contract; trigger is best-effort).
|
|
19951
20066
|
if (action === 'remove' || emoji === null) return
|
|
19952
|
-
if (!HISTORY_ENABLED) return // need history to identify bot-authored target
|
|
19953
20067
|
const reacter = update.user
|
|
19954
20068
|
if (!reacter) return // anonymous group-channel reactions — not user-attributable
|
|
19955
20069
|
|
|
20070
|
+
const reacterName = reacter.first_name ?? reacter.username ?? String(reacter.id)
|
|
20071
|
+
|
|
20072
|
+
// ─── reaction_dispatch (#2291) ───────────────────────────────────────
|
|
20073
|
+
// Event-driven dispatch of ANY qualifying reaction as a button-style
|
|
20074
|
+
// inbound turn. Independent of (and runs before) the bot-authored
|
|
20075
|
+
// `reactions` feedback path below; both may fire for one reaction.
|
|
20076
|
+
maybeDispatchReaction({
|
|
20077
|
+
chatId: chat_id,
|
|
20078
|
+
messageId: message_id,
|
|
20079
|
+
emoji,
|
|
20080
|
+
action,
|
|
20081
|
+
user: reacterName,
|
|
20082
|
+
userId: reacter.id,
|
|
20083
|
+
...(typeof update.message_thread_id === 'number'
|
|
20084
|
+
? { threadId: update.message_thread_id }
|
|
20085
|
+
: {}),
|
|
20086
|
+
})
|
|
20087
|
+
|
|
20088
|
+
if (!HISTORY_ENABLED) return // need history to identify bot-authored target
|
|
20089
|
+
|
|
19956
20090
|
const cfg = getReactionsConfig()
|
|
19957
20091
|
if (!cfg.enabled) return
|
|
19958
20092
|
|
|
@@ -20025,7 +20159,7 @@ async function handleMessageReaction(ctx: Context): Promise<void> {
|
|
|
20025
20159
|
ts: Date.now(),
|
|
20026
20160
|
preview,
|
|
20027
20161
|
userId: reacter.id,
|
|
20028
|
-
user:
|
|
20162
|
+
user: reacterName,
|
|
20029
20163
|
...(typeof update.message_thread_id === 'number'
|
|
20030
20164
|
? { threadId: update.message_thread_id }
|
|
20031
20165
|
: {}),
|