switchroom 0.13.51 → 0.13.53
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 +317 -132
- package/dist/auth-broker/index.js +494 -156
- package/dist/cli/drive-write-pretool.mjs +18 -3
- package/dist/cli/switchroom.js +2452 -1114
- package/dist/host-control/main.js +246 -127
- package/dist/vault/approvals/kernel-server.js +8269 -8146
- package/dist/vault/broker/server.js +2811 -2688
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +17 -4
- package/profiles/_shared/agent-self-service.md.hbs +12 -22
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md.hbs +8 -1
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/skills/switchroom-status/SKILL.md +8 -6
- package/telegram-plugin/chat-lock.ts +87 -19
- package/telegram-plugin/dist/gateway/gateway.js +752 -120
- package/telegram-plugin/gateway/disconnect-flush.ts +32 -0
- package/telegram-plugin/gateway/gateway.ts +258 -55
- package/telegram-plugin/gateway/inbound-coalesce.ts +19 -6
- package/telegram-plugin/stream-reply-handler.ts +10 -8
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +116 -0
- package/telegram-plugin/tests/inbound-coalesce.test.ts +20 -4
- package/telegram-plugin/tests/outbound-ordering.test.ts +228 -0
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +217 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +65 -8
- package/telegram-plugin/typing-wrap.ts +43 -21
|
@@ -10973,7 +10973,11 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
10973
10973
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
10974
10974
|
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
10975
10975
|
model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
|
|
10976
|
-
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")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
|
|
10976
|
+
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")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
10977
|
+
topic: exports_external.union([
|
|
10978
|
+
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
10979
|
+
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
10980
|
+
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
|
|
10977
10981
|
});
|
|
10978
10982
|
var AgentSoulSchema = exports_external.object({
|
|
10979
10983
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11085,8 +11089,35 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11085
11089
|
}).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
|
|
11086
11090
|
webhook_rate_limit: exports_external.object({
|
|
11087
11091
|
rpm: exports_external.number().int().positive()
|
|
11088
|
-
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit.")
|
|
11089
|
-
|
|
11092
|
+
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
11093
|
+
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11094
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11095
|
+
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
11096
|
+
}).optional().superRefine((tg, ctx) => {
|
|
11097
|
+
if (!tg)
|
|
11098
|
+
return;
|
|
11099
|
+
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
11100
|
+
ctx.addIssue({
|
|
11101
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11102
|
+
message: "`channels.telegram.chat_id` requires `default_topic_id` — supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
11103
|
+
path: ["default_topic_id"]
|
|
11104
|
+
});
|
|
11105
|
+
}
|
|
11106
|
+
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
11107
|
+
ctx.addIssue({
|
|
11108
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11109
|
+
message: "`channels.telegram.default_topic_id` requires `chat_id` — default_topic_id is only meaningful when the agent owns its own supergroup.",
|
|
11110
|
+
path: ["chat_id"]
|
|
11111
|
+
});
|
|
11112
|
+
}
|
|
11113
|
+
if (tg.topic_aliases != null && tg.chat_id == null) {
|
|
11114
|
+
ctx.addIssue({
|
|
11115
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11116
|
+
message: "`channels.telegram.topic_aliases` requires `chat_id` — aliases only resolve in supergroup-owned mode.",
|
|
11117
|
+
path: ["topic_aliases"]
|
|
11118
|
+
});
|
|
11119
|
+
}
|
|
11120
|
+
});
|
|
11090
11121
|
var ChannelsSchema = exports_external.object({
|
|
11091
11122
|
telegram: TelegramChannelSchema
|
|
11092
11123
|
}).optional();
|
|
@@ -11103,6 +11134,12 @@ var GoogleWorkspaceConfigSchema = exports_external.object({
|
|
|
11103
11134
|
approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified."),
|
|
11104
11135
|
tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
|
|
11105
11136
|
}).optional();
|
|
11137
|
+
var MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
11138
|
+
microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
|
|
11139
|
+
microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional — public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
|
|
11140
|
+
authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
|
|
11141
|
+
org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
|
|
11142
|
+
}).optional();
|
|
11106
11143
|
var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
11107
11144
|
account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11108
11145
|
message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
|
|
@@ -11110,6 +11147,12 @@ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
|
11110
11147
|
approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
|
|
11111
11148
|
tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
|
|
11112
11149
|
}).optional();
|
|
11150
|
+
var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
11151
|
+
account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11152
|
+
message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
|
|
11153
|
+
}).transform((v) => v.trim().toLowerCase()).optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized)."),
|
|
11154
|
+
org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
|
|
11155
|
+
}).optional();
|
|
11113
11156
|
var ReactionsSchema = exports_external.object({
|
|
11114
11157
|
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
|
|
11115
11158
|
trigger_emojis: exports_external.array(exports_external.string()).optional().describe("Emoji allowlist that triggers a synthetic inbound when reacted " + "to a bot message. Default ['\uD83D\uDC4E', '❌', '\uD83D\uDC4D', '✅']. Cascade " + "mode: REPLACE (not union) — setting this at a layer replaces " + "lower layers entirely, so an operator can narrow to [] to " + "disable triggering without flipping `enabled`."),
|
|
@@ -11245,6 +11288,7 @@ var AgentSchema = exports_external.object({
|
|
|
11245
11288
|
code_repos: exports_external.array(CodeRepoEntrySchema).optional().describe("Git repositories this agent is allowed to claim worktrees from. " + "Each entry provides a short name alias, a source path, and an " + "optional concurrency cap (default 5). When code_repos is set, " + "claim_worktree accepts the alias as the repo argument. " + "Absolute paths may always be passed regardless of this list."),
|
|
11246
11289
|
drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
|
|
11247
11290
|
google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
|
|
11291
|
+
microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
|
|
11248
11292
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
11249
11293
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
11250
11294
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
@@ -11259,6 +11303,33 @@ var AgentSchema = exports_external.object({
|
|
|
11259
11303
|
pids_limit: exports_external.number().int().positive().optional(),
|
|
11260
11304
|
cpus: exports_external.number().positive().optional()
|
|
11261
11305
|
}).optional()
|
|
11306
|
+
}).superRefine((agent, ctx) => {
|
|
11307
|
+
if (agent.dm_only !== true)
|
|
11308
|
+
return;
|
|
11309
|
+
const tg = agent.channels?.telegram;
|
|
11310
|
+
if (tg == null)
|
|
11311
|
+
return;
|
|
11312
|
+
if (tg.chat_id != null) {
|
|
11313
|
+
ctx.addIssue({
|
|
11314
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11315
|
+
message: "`dm_only: true` forbids `channels.telegram.chat_id` — DM-only agents have their own private chat, not a supergroup.",
|
|
11316
|
+
path: ["channels", "telegram", "chat_id"]
|
|
11317
|
+
});
|
|
11318
|
+
}
|
|
11319
|
+
if (tg.default_topic_id != null) {
|
|
11320
|
+
ctx.addIssue({
|
|
11321
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11322
|
+
message: "`dm_only: true` forbids `channels.telegram.default_topic_id` — DMs don't have forum topics.",
|
|
11323
|
+
path: ["channels", "telegram", "default_topic_id"]
|
|
11324
|
+
});
|
|
11325
|
+
}
|
|
11326
|
+
if (tg.topic_aliases != null) {
|
|
11327
|
+
ctx.addIssue({
|
|
11328
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11329
|
+
message: "`dm_only: true` forbids `channels.telegram.topic_aliases` — DMs don't have forum topics.",
|
|
11330
|
+
path: ["channels", "telegram", "topic_aliases"]
|
|
11331
|
+
});
|
|
11332
|
+
}
|
|
11262
11333
|
});
|
|
11263
11334
|
var TelegramConfigSchema = exports_external.object({
|
|
11264
11335
|
bot_token: exports_external.string().describe("Telegram bot token or vault reference (e.g., 'vault:telegram-bot-token')"),
|
|
@@ -11340,6 +11411,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11340
11411
|
}).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
|
|
11341
11412
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11342
11413
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11414
|
+
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
11343
11415
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11344
11416
|
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11345
11417
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
|
|
@@ -11350,6 +11422,13 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11350
11422
|
message: "Agent name must match the standard agent-name pattern"
|
|
11351
11423
|
})).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share.")
|
|
11352
11424
|
})).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email → list of agents " + "permitted to read that account's slots. Written by `switchroom " + "auth google enable|disable` (Phase 3); read by the broker on " + "every Google slot access. Replaces RFC D's per-agent vault slot " + "scope (which can't express 'two agents share one Google account')."),
|
|
11425
|
+
microsoft_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11426
|
+
message: "Account key must be a Microsoft account email like 'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
|
|
11427
|
+
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
11428
|
+
enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11429
|
+
message: "Agent name must match the standard agent-name pattern"
|
|
11430
|
+
})).describe("Agent slugs that may read this Microsoft account's broker " + "credentials. Per-agent ACL enforced at the broker; agents " + "still authenticate via socket-path-as-identity, broker just " + "gates the cross-agent token share. Mirrors google_accounts.")
|
|
11431
|
+
})).optional().describe("RFC #1873: per-Microsoft-account ACL. Maps account email → list of " + "agents permitted to use that account's broker credentials. Written " + "by `switchroom auth microsoft enable|disable`; read by the broker " + "on get-credentials with provider=microsoft."),
|
|
11353
11432
|
defaults: AgentDefaultsSchema.describe("Implicit bottom-of-cascade profile applied to every agent before " + "per-agent config and `extends:` resolution. Tools, mcp_servers, and " + "schedule are unioned/concatenated; scalars and nested objects are " + "shallow-merged with per-agent values winning."),
|
|
11354
11433
|
profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
|
|
11355
11434
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
@@ -11519,130 +11598,6 @@ function applyAgentOverlays(config) {
|
|
|
11519
11598
|
return { config, warnings };
|
|
11520
11599
|
}
|
|
11521
11600
|
|
|
11522
|
-
// src/config/loader.ts
|
|
11523
|
-
class ConfigError extends Error {
|
|
11524
|
-
details;
|
|
11525
|
-
constructor(message, details) {
|
|
11526
|
-
super(message);
|
|
11527
|
-
this.details = details;
|
|
11528
|
-
this.name = "ConfigError";
|
|
11529
|
-
}
|
|
11530
|
-
}
|
|
11531
|
-
function formatZodErrors(error) {
|
|
11532
|
-
return error.errors.map((e) => {
|
|
11533
|
-
const path = e.path.join(".");
|
|
11534
|
-
return ` ${path}: ${e.message}`;
|
|
11535
|
-
});
|
|
11536
|
-
}
|
|
11537
|
-
function coerceLegacyGoogleWorkspaceKeys(parsed, filePath) {
|
|
11538
|
-
const stableStringify = (v) => {
|
|
11539
|
-
if (v === null || typeof v !== "object")
|
|
11540
|
-
return JSON.stringify(v);
|
|
11541
|
-
if (Array.isArray(v))
|
|
11542
|
-
return `[${v.map(stableStringify).join(",")}]`;
|
|
11543
|
-
const obj = v;
|
|
11544
|
-
const keys = Object.keys(obj).sort();
|
|
11545
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
11546
|
-
};
|
|
11547
|
-
const aliasInPlace = (obj, where) => {
|
|
11548
|
-
const a = obj.drive;
|
|
11549
|
-
const b = obj.google_workspace;
|
|
11550
|
-
if (a !== undefined && b !== undefined) {
|
|
11551
|
-
if (stableStringify(a) !== stableStringify(b)) {
|
|
11552
|
-
throw new ConfigError(`Both \`drive:\` and \`google_workspace:\` are set on ${where} in ${filePath} with different values.`, [
|
|
11553
|
-
" These are aliases — pick one and remove the other.",
|
|
11554
|
-
" `google_workspace:` is the RFC G canonical key; `drive:` is the legacy alias.",
|
|
11555
|
-
" Allowed during transition: setting both with identical values."
|
|
11556
|
-
]);
|
|
11557
|
-
}
|
|
11558
|
-
return;
|
|
11559
|
-
}
|
|
11560
|
-
if (a !== undefined && b === undefined)
|
|
11561
|
-
obj.google_workspace = a;
|
|
11562
|
-
if (b !== undefined && a === undefined)
|
|
11563
|
-
obj.drive = b;
|
|
11564
|
-
};
|
|
11565
|
-
aliasInPlace(parsed, "the top level");
|
|
11566
|
-
const agents = parsed.agents;
|
|
11567
|
-
if (agents && typeof agents === "object" && !Array.isArray(agents)) {
|
|
11568
|
-
for (const [name, agent] of Object.entries(agents)) {
|
|
11569
|
-
if (agent && typeof agent === "object" && !Array.isArray(agent)) {
|
|
11570
|
-
aliasInPlace(agent, `agent \`${name}\``);
|
|
11571
|
-
}
|
|
11572
|
-
}
|
|
11573
|
-
}
|
|
11574
|
-
}
|
|
11575
|
-
function findConfigFile(startDir) {
|
|
11576
|
-
const envPath = process.env.SWITCHROOM_CONFIG;
|
|
11577
|
-
const home2 = homedir();
|
|
11578
|
-
const userDir = resolve3(home2, ".switchroom");
|
|
11579
|
-
const searchPaths = [
|
|
11580
|
-
envPath ? resolve3(envPath) : null,
|
|
11581
|
-
startDir ? resolve3(startDir, "switchroom.yaml") : null,
|
|
11582
|
-
startDir ? resolve3(startDir, "switchroom.yml") : null,
|
|
11583
|
-
startDir ? resolve3(startDir, "clerk.yaml") : null,
|
|
11584
|
-
startDir ? resolve3(startDir, "clerk.yml") : null,
|
|
11585
|
-
resolve3(process.cwd(), "switchroom.yaml"),
|
|
11586
|
-
resolve3(process.cwd(), "switchroom.yml"),
|
|
11587
|
-
resolve3(process.cwd(), "clerk.yaml"),
|
|
11588
|
-
resolve3(process.cwd(), "clerk.yml"),
|
|
11589
|
-
resolve3(userDir, "switchroom.yaml"),
|
|
11590
|
-
resolve3(userDir, "switchroom.yml"),
|
|
11591
|
-
resolve3(userDir, "clerk.yaml"),
|
|
11592
|
-
resolve3(userDir, "clerk.yml")
|
|
11593
|
-
].filter(Boolean);
|
|
11594
|
-
for (const path of searchPaths) {
|
|
11595
|
-
if (existsSync3(path)) {
|
|
11596
|
-
return path;
|
|
11597
|
-
}
|
|
11598
|
-
}
|
|
11599
|
-
throw new ConfigError("No switchroom.yaml found", searchPaths.map((p) => ` Searched: ${p}`));
|
|
11600
|
-
}
|
|
11601
|
-
function loadConfig(configPath) {
|
|
11602
|
-
const filePath = configPath ?? findConfigFile();
|
|
11603
|
-
if (!existsSync3(filePath)) {
|
|
11604
|
-
throw new ConfigError(`Config file not found: ${filePath}`);
|
|
11605
|
-
}
|
|
11606
|
-
let raw;
|
|
11607
|
-
try {
|
|
11608
|
-
raw = readFileSync2(filePath, "utf-8");
|
|
11609
|
-
} catch (err) {
|
|
11610
|
-
throw new ConfigError(`Failed to read config file: ${filePath}`, [
|
|
11611
|
-
` ${err.message}`
|
|
11612
|
-
]);
|
|
11613
|
-
}
|
|
11614
|
-
let parsed;
|
|
11615
|
-
try {
|
|
11616
|
-
parsed = $parse(raw);
|
|
11617
|
-
} catch (err) {
|
|
11618
|
-
throw new ConfigError(`Invalid YAML in ${filePath}`, [
|
|
11619
|
-
` ${err.message}`
|
|
11620
|
-
]);
|
|
11621
|
-
}
|
|
11622
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.clerk !== undefined && parsed.switchroom === undefined) {
|
|
11623
|
-
const obj = parsed;
|
|
11624
|
-
obj.switchroom = obj.clerk;
|
|
11625
|
-
delete obj.clerk;
|
|
11626
|
-
}
|
|
11627
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
11628
|
-
coerceLegacyGoogleWorkspaceKeys(parsed, filePath);
|
|
11629
|
-
}
|
|
11630
|
-
let config;
|
|
11631
|
-
try {
|
|
11632
|
-
config = SwitchroomConfigSchema.parse(parsed);
|
|
11633
|
-
} catch (err) {
|
|
11634
|
-
if (err instanceof ZodError) {
|
|
11635
|
-
throw new ConfigError("Invalid switchroom.yaml configuration", formatZodErrors(err));
|
|
11636
|
-
}
|
|
11637
|
-
throw err;
|
|
11638
|
-
}
|
|
11639
|
-
applyAgentOverlays(config);
|
|
11640
|
-
return config;
|
|
11641
|
-
}
|
|
11642
|
-
|
|
11643
|
-
// src/scheduler/dispatch.ts
|
|
11644
|
-
import { createHash } from "node:crypto";
|
|
11645
|
-
|
|
11646
11601
|
// src/config/merge.ts
|
|
11647
11602
|
function dedupe(items) {
|
|
11648
11603
|
const seen = new Set;
|
|
@@ -11952,7 +11907,208 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
11952
11907
|
mergeAgentConfig.notifiedWorkerIsolationMove = false;
|
|
11953
11908
|
})(mergeAgentConfig ||= {});
|
|
11954
11909
|
|
|
11910
|
+
// src/config/loader.ts
|
|
11911
|
+
class ConfigError extends Error {
|
|
11912
|
+
details;
|
|
11913
|
+
constructor(message, details) {
|
|
11914
|
+
super(message);
|
|
11915
|
+
this.details = details;
|
|
11916
|
+
this.name = "ConfigError";
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
function formatZodErrors(error) {
|
|
11920
|
+
return error.errors.map((e) => {
|
|
11921
|
+
const path = e.path.join(".");
|
|
11922
|
+
return ` ${path}: ${e.message}`;
|
|
11923
|
+
});
|
|
11924
|
+
}
|
|
11925
|
+
function coerceLegacyGoogleWorkspaceKeys(parsed, filePath) {
|
|
11926
|
+
const stableStringify = (v) => {
|
|
11927
|
+
if (v === null || typeof v !== "object")
|
|
11928
|
+
return JSON.stringify(v);
|
|
11929
|
+
if (Array.isArray(v))
|
|
11930
|
+
return `[${v.map(stableStringify).join(",")}]`;
|
|
11931
|
+
const obj = v;
|
|
11932
|
+
const keys = Object.keys(obj).sort();
|
|
11933
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
11934
|
+
};
|
|
11935
|
+
const aliasInPlace = (obj, where) => {
|
|
11936
|
+
const a = obj.drive;
|
|
11937
|
+
const b = obj.google_workspace;
|
|
11938
|
+
if (a !== undefined && b !== undefined) {
|
|
11939
|
+
if (stableStringify(a) !== stableStringify(b)) {
|
|
11940
|
+
throw new ConfigError(`Both \`drive:\` and \`google_workspace:\` are set on ${where} in ${filePath} with different values.`, [
|
|
11941
|
+
" These are aliases — pick one and remove the other.",
|
|
11942
|
+
" `google_workspace:` is the RFC G canonical key; `drive:` is the legacy alias.",
|
|
11943
|
+
" Allowed during transition: setting both with identical values."
|
|
11944
|
+
]);
|
|
11945
|
+
}
|
|
11946
|
+
return;
|
|
11947
|
+
}
|
|
11948
|
+
if (a !== undefined && b === undefined)
|
|
11949
|
+
obj.google_workspace = a;
|
|
11950
|
+
if (b !== undefined && a === undefined)
|
|
11951
|
+
obj.drive = b;
|
|
11952
|
+
};
|
|
11953
|
+
aliasInPlace(parsed, "the top level");
|
|
11954
|
+
const agents = parsed.agents;
|
|
11955
|
+
if (agents && typeof agents === "object" && !Array.isArray(agents)) {
|
|
11956
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
11957
|
+
if (agent && typeof agent === "object" && !Array.isArray(agent)) {
|
|
11958
|
+
aliasInPlace(agent, `agent \`${name}\``);
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
function findConfigFile(startDir) {
|
|
11964
|
+
const envPath = process.env.SWITCHROOM_CONFIG;
|
|
11965
|
+
const home2 = homedir();
|
|
11966
|
+
const userDir = resolve3(home2, ".switchroom");
|
|
11967
|
+
const searchPaths = [
|
|
11968
|
+
envPath ? resolve3(envPath) : null,
|
|
11969
|
+
startDir ? resolve3(startDir, "switchroom.yaml") : null,
|
|
11970
|
+
startDir ? resolve3(startDir, "switchroom.yml") : null,
|
|
11971
|
+
startDir ? resolve3(startDir, "clerk.yaml") : null,
|
|
11972
|
+
startDir ? resolve3(startDir, "clerk.yml") : null,
|
|
11973
|
+
resolve3(process.cwd(), "switchroom.yaml"),
|
|
11974
|
+
resolve3(process.cwd(), "switchroom.yml"),
|
|
11975
|
+
resolve3(process.cwd(), "clerk.yaml"),
|
|
11976
|
+
resolve3(process.cwd(), "clerk.yml"),
|
|
11977
|
+
resolve3(userDir, "switchroom.yaml"),
|
|
11978
|
+
resolve3(userDir, "switchroom.yml"),
|
|
11979
|
+
resolve3(userDir, "clerk.yaml"),
|
|
11980
|
+
resolve3(userDir, "clerk.yml")
|
|
11981
|
+
].filter(Boolean);
|
|
11982
|
+
for (const path of searchPaths) {
|
|
11983
|
+
if (existsSync3(path)) {
|
|
11984
|
+
return path;
|
|
11985
|
+
}
|
|
11986
|
+
}
|
|
11987
|
+
throw new ConfigError("No switchroom.yaml found", searchPaths.map((p) => ` Searched: ${p}`));
|
|
11988
|
+
}
|
|
11989
|
+
function loadConfig(configPath) {
|
|
11990
|
+
const filePath = configPath ?? findConfigFile();
|
|
11991
|
+
if (!existsSync3(filePath)) {
|
|
11992
|
+
throw new ConfigError(`Config file not found: ${filePath}`);
|
|
11993
|
+
}
|
|
11994
|
+
let raw;
|
|
11995
|
+
try {
|
|
11996
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
11997
|
+
} catch (err) {
|
|
11998
|
+
throw new ConfigError(`Failed to read config file: ${filePath}`, [
|
|
11999
|
+
` ${err.message}`
|
|
12000
|
+
]);
|
|
12001
|
+
}
|
|
12002
|
+
let parsed;
|
|
12003
|
+
try {
|
|
12004
|
+
parsed = $parse(raw);
|
|
12005
|
+
} catch (err) {
|
|
12006
|
+
throw new ConfigError(`Invalid YAML in ${filePath}`, [
|
|
12007
|
+
` ${err.message}`
|
|
12008
|
+
]);
|
|
12009
|
+
}
|
|
12010
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.clerk !== undefined && parsed.switchroom === undefined) {
|
|
12011
|
+
const obj = parsed;
|
|
12012
|
+
obj.switchroom = obj.clerk;
|
|
12013
|
+
delete obj.clerk;
|
|
12014
|
+
}
|
|
12015
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
12016
|
+
coerceLegacyGoogleWorkspaceKeys(parsed, filePath);
|
|
12017
|
+
}
|
|
12018
|
+
let config;
|
|
12019
|
+
try {
|
|
12020
|
+
config = SwitchroomConfigSchema.parse(parsed);
|
|
12021
|
+
} catch (err) {
|
|
12022
|
+
if (err instanceof ZodError) {
|
|
12023
|
+
throw new ConfigError("Invalid switchroom.yaml configuration", formatZodErrors(err));
|
|
12024
|
+
}
|
|
12025
|
+
throw err;
|
|
12026
|
+
}
|
|
12027
|
+
applyAgentOverlays(config);
|
|
12028
|
+
validateAllCronTopicAliases(config, filePath);
|
|
12029
|
+
return config;
|
|
12030
|
+
}
|
|
12031
|
+
function validateAllCronTopicAliases(config, filePath) {
|
|
12032
|
+
const issues = [];
|
|
12033
|
+
for (const [agentName, agentRaw] of Object.entries(config.agents)) {
|
|
12034
|
+
if (!agentRaw)
|
|
12035
|
+
continue;
|
|
12036
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentRaw);
|
|
12037
|
+
const schedule = resolved.schedule ?? [];
|
|
12038
|
+
if (schedule.length === 0)
|
|
12039
|
+
continue;
|
|
12040
|
+
const tg = resolved.channels?.telegram;
|
|
12041
|
+
const aliases = new Set(Object.keys(tg?.topic_aliases ?? {}));
|
|
12042
|
+
for (const entry of schedule) {
|
|
12043
|
+
if (entry.topic == null)
|
|
12044
|
+
continue;
|
|
12045
|
+
if (typeof entry.topic === "number")
|
|
12046
|
+
continue;
|
|
12047
|
+
if (!aliases.has(entry.topic)) {
|
|
12048
|
+
issues.push(` agents.${agentName}.schedule cron \`${entry.cron}\`: ` + `topic alias "${entry.topic}" is not defined in ` + `channels.telegram.topic_aliases.`);
|
|
12049
|
+
}
|
|
12050
|
+
}
|
|
12051
|
+
}
|
|
12052
|
+
if (issues.length > 0) {
|
|
12053
|
+
throw new ConfigError(`Cron \`topic:\` alias references unknown topic_aliases in ${filePath}`, issues);
|
|
12054
|
+
}
|
|
12055
|
+
}
|
|
12056
|
+
|
|
12057
|
+
// src/telegram/topic-router.ts
|
|
12058
|
+
var ALERTS_ALIAS = "alerts";
|
|
12059
|
+
var ADMIN_ALIAS = "admin";
|
|
12060
|
+
function aliasToId(config, name) {
|
|
12061
|
+
return config.topic_aliases?.[name];
|
|
12062
|
+
}
|
|
12063
|
+
function resolveOutboundTopic(config, event) {
|
|
12064
|
+
const cfg = config ?? {};
|
|
12065
|
+
const inSupergroupMode = cfg.default_topic_id != null;
|
|
12066
|
+
switch (event.kind) {
|
|
12067
|
+
case "reply":
|
|
12068
|
+
return event.originThreadId;
|
|
12069
|
+
case "subagent-progress":
|
|
12070
|
+
return event.parentThreadId;
|
|
12071
|
+
case "command-query":
|
|
12072
|
+
return event.originThreadId;
|
|
12073
|
+
case "vault":
|
|
12074
|
+
case "permission":
|
|
12075
|
+
if (event.turnInitiated) {
|
|
12076
|
+
return event.originThreadId ?? cfg.default_topic_id;
|
|
12077
|
+
}
|
|
12078
|
+
if (!inSupergroupMode)
|
|
12079
|
+
return;
|
|
12080
|
+
return aliasToId(cfg, ADMIN_ALIAS) ?? cfg.default_topic_id;
|
|
12081
|
+
case "hostd-approval":
|
|
12082
|
+
if (event.originThreadId != null)
|
|
12083
|
+
return event.originThreadId;
|
|
12084
|
+
if (!inSupergroupMode)
|
|
12085
|
+
return;
|
|
12086
|
+
return aliasToId(cfg, ADMIN_ALIAS) ?? cfg.default_topic_id;
|
|
12087
|
+
case "cron": {
|
|
12088
|
+
if (typeof event.entryTopic === "number")
|
|
12089
|
+
return event.entryTopic;
|
|
12090
|
+
if (typeof event.entryTopic === "string") {
|
|
12091
|
+
const resolved = aliasToId(cfg, event.entryTopic);
|
|
12092
|
+
if (resolved != null)
|
|
12093
|
+
return resolved;
|
|
12094
|
+
}
|
|
12095
|
+
return cfg.default_topic_id;
|
|
12096
|
+
}
|
|
12097
|
+
case "boot":
|
|
12098
|
+
case "compact-watchdog":
|
|
12099
|
+
if (!inSupergroupMode)
|
|
12100
|
+
return;
|
|
12101
|
+
return aliasToId(cfg, ALERTS_ALIAS) ?? cfg.default_topic_id;
|
|
12102
|
+
case "command-mutation":
|
|
12103
|
+
case "command-heavy":
|
|
12104
|
+
if (!inSupergroupMode)
|
|
12105
|
+
return;
|
|
12106
|
+
return aliasToId(cfg, ADMIN_ALIAS) ?? cfg.default_topic_id;
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
|
|
11955
12110
|
// src/scheduler/dispatch.ts
|
|
12111
|
+
import { createHash } from "node:crypto";
|
|
11956
12112
|
function collectScheduleEntries(config) {
|
|
11957
12113
|
const out = [];
|
|
11958
12114
|
const agentNames = Object.keys(config.agents).sort();
|
|
@@ -11969,7 +12125,8 @@ function collectScheduleEntries(config) {
|
|
|
11969
12125
|
scheduleIndex: i,
|
|
11970
12126
|
cron: entry.cron,
|
|
11971
12127
|
prompt: entry.prompt,
|
|
11972
|
-
promptKey: createHash("sha256").update(entry.prompt).digest("hex").slice(0, 12)
|
|
12128
|
+
promptKey: createHash("sha256").update(entry.prompt).digest("hex").slice(0, 12),
|
|
12129
|
+
...entry.topic !== undefined ? { topic: entry.topic } : {}
|
|
11973
12130
|
});
|
|
11974
12131
|
}
|
|
11975
12132
|
}
|
|
@@ -12460,7 +12617,8 @@ function registerAgentSchedule(opts) {
|
|
|
12460
12617
|
let delivered = false;
|
|
12461
12618
|
let summary = "";
|
|
12462
12619
|
try {
|
|
12463
|
-
const
|
|
12620
|
+
const threadId = resolveEntryThreadId(entry, opts.channel);
|
|
12621
|
+
const result = dispatchAsInbound(entry, { chatId: opts.channel.chatId, threadId, now }, opts.dispatcher);
|
|
12464
12622
|
delivered = result.delivered;
|
|
12465
12623
|
summary = delivered ? "delivered to bridge via gateway" : "no agent client connected — fire dropped";
|
|
12466
12624
|
} catch (err) {
|
|
@@ -12493,16 +12651,40 @@ function ipcDispatcher(client) {
|
|
|
12493
12651
|
};
|
|
12494
12652
|
}
|
|
12495
12653
|
function resolveChannelTarget(config, agentName) {
|
|
12654
|
+
const agent = config.agents?.[agentName];
|
|
12655
|
+
const tgChannel = agent ? resolveAgentConfig(config.defaults, config.profiles, agent).channels?.telegram : undefined;
|
|
12656
|
+
const supergroupChatId = tgChannel?.chat_id;
|
|
12657
|
+
const supergroupDefaultTopic = tgChannel?.default_topic_id;
|
|
12658
|
+
if (typeof supergroupChatId === "string" && supergroupChatId.length > 0) {
|
|
12659
|
+
return {
|
|
12660
|
+
chatId: supergroupChatId,
|
|
12661
|
+
...typeof supergroupDefaultTopic === "number" ? { threadId: supergroupDefaultTopic } : {},
|
|
12662
|
+
routerConfig: {
|
|
12663
|
+
...typeof supergroupDefaultTopic === "number" ? { default_topic_id: supergroupDefaultTopic } : {},
|
|
12664
|
+
...tgChannel?.topic_aliases ? { topic_aliases: tgChannel.topic_aliases } : {}
|
|
12665
|
+
}
|
|
12666
|
+
};
|
|
12667
|
+
}
|
|
12496
12668
|
const forumChatId = config.telegram?.forum_chat_id;
|
|
12497
12669
|
if (typeof forumChatId !== "string" || forumChatId.length === 0)
|
|
12498
12670
|
return null;
|
|
12499
|
-
const agent = config.agents?.[agentName];
|
|
12500
12671
|
const threadId = agent?.topic_id;
|
|
12501
12672
|
return {
|
|
12502
12673
|
chatId: forumChatId,
|
|
12503
12674
|
...typeof threadId === "number" ? { threadId } : {}
|
|
12504
12675
|
};
|
|
12505
12676
|
}
|
|
12677
|
+
function resolveEntryThreadId(entry, channel) {
|
|
12678
|
+
if (channel.routerConfig) {
|
|
12679
|
+
const routed = resolveOutboundTopic(channel.routerConfig, {
|
|
12680
|
+
kind: "cron",
|
|
12681
|
+
entryTopic: entry.topic
|
|
12682
|
+
});
|
|
12683
|
+
if (routed !== undefined)
|
|
12684
|
+
return routed;
|
|
12685
|
+
}
|
|
12686
|
+
return channel.threadId;
|
|
12687
|
+
}
|
|
12506
12688
|
async function main() {
|
|
12507
12689
|
const agentName = process.env.SWITCHROOM_AGENT_NAME;
|
|
12508
12690
|
if (!agentName) {
|
|
@@ -12576,7 +12758,8 @@ async function main() {
|
|
|
12576
12758
|
`);
|
|
12577
12759
|
for (const m of missed) {
|
|
12578
12760
|
const startedAt = Date.now();
|
|
12579
|
-
const
|
|
12761
|
+
const threadId = resolveEntryThreadId(m.entry, channel);
|
|
12762
|
+
const result = dispatchAsInbound(m.entry, { chatId: channel.chatId, threadId }, dispatcher);
|
|
12580
12763
|
sink.recordFire({
|
|
12581
12764
|
agent: m.entry.agent,
|
|
12582
12765
|
scheduleIndex: m.entry.scheduleIndex,
|
|
@@ -12608,7 +12791,8 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
12608
12791
|
promptKey: "skip-notice"
|
|
12609
12792
|
};
|
|
12610
12793
|
const startedAt = Date.now();
|
|
12611
|
-
const
|
|
12794
|
+
const threadId = resolveEntryThreadId(noticeEntry, channel);
|
|
12795
|
+
const result = dispatchAsInbound(noticeEntry, { chatId: channel.chatId, threadId }, dispatcher);
|
|
12612
12796
|
sink.recordFire({
|
|
12613
12797
|
agent: agentName,
|
|
12614
12798
|
scheduleIndex: -1,
|
|
@@ -12666,6 +12850,7 @@ if (import.meta.url === `file://${process.argv[1]}` && /(?:^|[/\\])agent-schedul
|
|
|
12666
12850
|
});
|
|
12667
12851
|
}
|
|
12668
12852
|
export {
|
|
12853
|
+
resolveEntryThreadId,
|
|
12669
12854
|
resolveChannelTarget,
|
|
12670
12855
|
registerAgentSchedule,
|
|
12671
12856
|
main,
|