switchroom 0.13.52 → 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.
@@ -16105,11 +16105,11 @@ function decodeResponse(line) {
16105
16105
  }
16106
16106
  return ResponseSchema.parse(parsed);
16107
16107
  }
16108
- var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
16108
+ var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
16109
16109
  var init_protocol = __esm(() => {
16110
16110
  init_zod();
16111
16111
  MAX_FRAME_BYTES = 64 * 1024;
16112
- ProviderNameSchema = exports_external.enum(["anthropic", "google"]);
16112
+ ProviderNameSchema = exports_external.enum(["anthropic", "google", "microsoft"]);
16113
16113
  GetCredentialsRequestSchema = exports_external.object({
16114
16114
  v: exports_external.literal(PROTOCOL_VERSION),
16115
16115
  op: exports_external.literal("get-credentials"),
@@ -16162,9 +16162,24 @@ var init_protocol = __esm(() => {
16162
16162
  tokenType: exports_external.literal("Bearer")
16163
16163
  })
16164
16164
  });
16165
+ MicrosoftCredentialsSchema = exports_external.object({
16166
+ microsoftOauth: exports_external.object({
16167
+ accessToken: exports_external.string(),
16168
+ refreshToken: exports_external.string(),
16169
+ expiresAt: exports_external.number(),
16170
+ scope: exports_external.string(),
16171
+ clientId: exports_external.string(),
16172
+ accountEmail: exports_external.string(),
16173
+ tokenType: exports_external.literal("Bearer"),
16174
+ tenantId: exports_external.string(),
16175
+ accountType: exports_external.enum(["personal", "work"]),
16176
+ homeAccountId: exports_external.string()
16177
+ })
16178
+ });
16165
16179
  ProviderCredentialsSchema = exports_external.union([
16166
16180
  AnthropicCredentialsSchema,
16167
- GoogleCredentialsSchema
16181
+ GoogleCredentialsSchema,
16182
+ MicrosoftCredentialsSchema
16168
16183
  ]);
16169
16184
  AddAccountRequestSchema = exports_external.object({
16170
16185
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -23592,7 +23607,7 @@ var init_dist = __esm(() => {
23592
23607
  });
23593
23608
 
23594
23609
  // ../src/config/schema.ts
23595
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
23610
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
23596
23611
  var init_schema = __esm(() => {
23597
23612
  init_zod();
23598
23613
  CodeRepoEntrySchema = exports_external.object({
@@ -23609,7 +23624,11 @@ var init_schema = __esm(() => {
23609
23624
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
23610
23625
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
23611
23626
  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 " + "\u2014 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."),
23612
- 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 \u2014 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 \u2014 " + "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.")
23627
+ 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 \u2014 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 \u2014 " + "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."),
23628
+ topic: exports_external.union([
23629
+ exports_external.string().min(1, "topic alias must be non-empty"),
23630
+ exports_external.number().int().positive("topic ID must be a positive integer")
23631
+ ]).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 \u2014 typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
23613
23632
  });
23614
23633
  AgentSoulSchema = exports_external.object({
23615
23634
  name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
@@ -23721,8 +23740,35 @@ var init_schema = __esm(() => {
23721
23740
  }).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 \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
23722
23741
  webhook_rate_limit: exports_external.object({
23723
23742
  rpm: exports_external.number().int().positive()
23724
- }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 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.")
23725
- }).optional();
23743
+ }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 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."),
23744
+ 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 \u2014 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."),
23745
+ 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 \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
23746
+ 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 \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
23747
+ }).optional().superRefine((tg, ctx) => {
23748
+ if (!tg)
23749
+ return;
23750
+ if (tg.chat_id != null && tg.default_topic_id == null) {
23751
+ ctx.addIssue({
23752
+ code: exports_external.ZodIssueCode.custom,
23753
+ message: "`channels.telegram.chat_id` requires `default_topic_id` \u2014 supergroup-mode agents need a fallback topic for unclassified outbounds.",
23754
+ path: ["default_topic_id"]
23755
+ });
23756
+ }
23757
+ if (tg.default_topic_id != null && tg.chat_id == null) {
23758
+ ctx.addIssue({
23759
+ code: exports_external.ZodIssueCode.custom,
23760
+ message: "`channels.telegram.default_topic_id` requires `chat_id` \u2014 default_topic_id is only meaningful when the agent owns its own supergroup.",
23761
+ path: ["chat_id"]
23762
+ });
23763
+ }
23764
+ if (tg.topic_aliases != null && tg.chat_id == null) {
23765
+ ctx.addIssue({
23766
+ code: exports_external.ZodIssueCode.custom,
23767
+ message: "`channels.telegram.topic_aliases` requires `chat_id` \u2014 aliases only resolve in supergroup-owned mode.",
23768
+ path: ["topic_aliases"]
23769
+ });
23770
+ }
23771
+ });
23726
23772
  ChannelsSchema = exports_external.object({
23727
23773
  telegram: TelegramChannelSchema
23728
23774
  }).optional();
@@ -23739,6 +23785,12 @@ var init_schema = __esm(() => {
23739
23785
  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."),
23740
23786
  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 \u2014 see RFC G \u00a75).")
23741
23787
  }).optional();
23788
+ MicrosoftWorkspaceConfigSchema = exports_external.object({
23789
+ 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')."),
23790
+ microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional \u2014 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'."),
23791
+ 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."),
23792
+ org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC \u00a76.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false \u2014 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.")
23793
+ }).optional();
23742
23794
  AgentGoogleWorkspaceConfigSchema = exports_external.object({
23743
23795
  account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
23744
23796
  message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
@@ -23746,6 +23798,12 @@ var init_schema = __esm(() => {
23746
23798
  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."),
23747
23799
  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.")
23748
23800
  }).optional();
23801
+ AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
23802
+ account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
23803
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
23804
+ }).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)."),
23805
+ org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 \u00a76.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
23806
+ }).optional();
23749
23807
  ReactionsSchema = exports_external.object({
23750
23808
  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."),
23751
23809
  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', '\u274c', '\uD83D\uDC4D', '\u2705']. Cascade " + "mode: REPLACE (not union) \u2014 setting this at a layer replaces " + "lower layers entirely, so an operator can narrow to [] to " + "disable triggering without flipping `enabled`."),
@@ -23881,6 +23939,7 @@ var init_schema = __esm(() => {
23881
23939
  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."),
23882
23940
  drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 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 \u2014 they live at the top level."),
23883
23941
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides \u2014 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 \u2014 they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
23942
+ microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override \u2014 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."),
23884
23943
  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({
23885
23944
  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."),
23886
23945
  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.")
@@ -23895,6 +23954,33 @@ var init_schema = __esm(() => {
23895
23954
  pids_limit: exports_external.number().int().positive().optional(),
23896
23955
  cpus: exports_external.number().positive().optional()
23897
23956
  }).optional()
23957
+ }).superRefine((agent, ctx) => {
23958
+ if (agent.dm_only !== true)
23959
+ return;
23960
+ const tg = agent.channels?.telegram;
23961
+ if (tg == null)
23962
+ return;
23963
+ if (tg.chat_id != null) {
23964
+ ctx.addIssue({
23965
+ code: exports_external.ZodIssueCode.custom,
23966
+ message: "`dm_only: true` forbids `channels.telegram.chat_id` \u2014 DM-only agents have their own private chat, not a supergroup.",
23967
+ path: ["channels", "telegram", "chat_id"]
23968
+ });
23969
+ }
23970
+ if (tg.default_topic_id != null) {
23971
+ ctx.addIssue({
23972
+ code: exports_external.ZodIssueCode.custom,
23973
+ message: "`dm_only: true` forbids `channels.telegram.default_topic_id` \u2014 DMs don't have forum topics.",
23974
+ path: ["channels", "telegram", "default_topic_id"]
23975
+ });
23976
+ }
23977
+ if (tg.topic_aliases != null) {
23978
+ ctx.addIssue({
23979
+ code: exports_external.ZodIssueCode.custom,
23980
+ message: "`dm_only: true` forbids `channels.telegram.topic_aliases` \u2014 DMs don't have forum topics.",
23981
+ path: ["channels", "telegram", "topic_aliases"]
23982
+ });
23983
+ }
23898
23984
  });
23899
23985
  TelegramConfigSchema = exports_external.object({
23900
23986
  bot_token: exports_external.string().describe("Telegram bot token or vault reference (e.g., 'vault:telegram-bot-token')"),
@@ -23976,6 +24062,7 @@ var init_schema = __esm(() => {
23976
24062
  }).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)."),
23977
24063
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 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."),
23978
24064
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "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)."),
24065
+ microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration \u2014 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."),
23979
24066
  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."),
23980
24067
  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)."),
23981
24068
  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 \u2014 disabled by default)."),
@@ -23986,6 +24073,13 @@ var init_schema = __esm(() => {
23986
24073
  message: "Agent name must match the standard agent-name pattern"
23987
24074
  })).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 \u2014 " + "the agent still authenticates via socket-path-as-identity " + "per RFC D \u00a74.1, broker just gates the cross-agent token share.")
23988
24075
  })).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email \u2192 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')."),
24076
+ microsoft_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
24077
+ message: "Account key must be a Microsoft account email like 'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
24078
+ }).transform((v) => v.trim().toLowerCase()), exports_external.object({
24079
+ enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
24080
+ message: "Agent name must match the standard agent-name pattern"
24081
+ })).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.")
24082
+ })).optional().describe("RFC #1873: per-Microsoft-account ACL. Maps account email \u2192 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."),
23989
24083
  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."),
23990
24084
  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."),
23991
24085
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
@@ -24165,6 +24259,317 @@ var init_overlay_loader = __esm(() => {
24165
24259
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
24166
24260
  });
24167
24261
 
24262
+ // ../src/config/merge.ts
24263
+ function dedupe(items) {
24264
+ const seen = new Set;
24265
+ const out = [];
24266
+ for (const item of items) {
24267
+ if (seen.has(item))
24268
+ continue;
24269
+ seen.add(item);
24270
+ out.push(item);
24271
+ }
24272
+ return out;
24273
+ }
24274
+ function deepMergeJson(base, override) {
24275
+ if (override === undefined)
24276
+ return base;
24277
+ if (base === undefined)
24278
+ return override;
24279
+ if (typeof base !== "object" || base === null || Array.isArray(base) || typeof override !== "object" || override === null || Array.isArray(override)) {
24280
+ return override;
24281
+ }
24282
+ const out = { ...base };
24283
+ for (const [k, v] of Object.entries(override)) {
24284
+ if (k === "__proto__" || k === "constructor" || k === "prototype")
24285
+ continue;
24286
+ out[k] = deepMergeJson(out[k], v);
24287
+ }
24288
+ return out;
24289
+ }
24290
+ function resolveAgentConfig(defaults, profiles, agent) {
24291
+ if (!mergeAgentConfig.suppressDeprecationLogs && !mergeAgentConfig.notifiedWorkerIsolationMove && defaults?.subagents?.worker?.isolation === "worktree") {
24292
+ mergeAgentConfig.notifiedWorkerIsolationMove = true;
24293
+ console.warn("[switchroom] NOTICE: defaults.subagents.worker.isolation moved to the " + "`coding` profile in switchroom 0.6.6 (#682). Agents extending coding " + "still get worktree-isolated workers; other agents would have hard-failed " + "the first time they delegated. See CHANGELOG.");
24294
+ }
24295
+ const name = agent.extends;
24296
+ const profile = name && profiles ? profiles[name] : undefined;
24297
+ if (!profile) {
24298
+ return mergeAgentConfig(defaults, agent);
24299
+ }
24300
+ const { extends: _unused, ...profileWithoutExtends } = profile;
24301
+ const layered = mergeAgentConfig(defaults, profileWithoutExtends);
24302
+ return mergeAgentConfig(layered, agent);
24303
+ }
24304
+ function foldDeprecatedTelegramFields(config) {
24305
+ const c = config;
24306
+ const root = c;
24307
+ const deprecations = [];
24308
+ const hasRoot = root.voice_in !== undefined || root.telegraph !== undefined || root.webhook_sources !== undefined;
24309
+ if (!hasRoot)
24310
+ return { config: c, deprecations };
24311
+ const channels = { ...c.channels ?? {} };
24312
+ const tg = { ...channels.telegram ?? {} };
24313
+ if (root.voice_in !== undefined) {
24314
+ if (tg.voice_in === undefined)
24315
+ tg.voice_in = root.voice_in;
24316
+ deprecations.push("voice_in at the agent root is deprecated; move under channels.telegram.voice_in (#596).");
24317
+ }
24318
+ if (root.telegraph !== undefined) {
24319
+ if (tg.telegraph === undefined)
24320
+ tg.telegraph = root.telegraph;
24321
+ deprecations.push("telegraph at the agent root is deprecated; move under channels.telegram.telegraph (#596).");
24322
+ }
24323
+ if (root.webhook_sources !== undefined) {
24324
+ if (tg.webhook_sources === undefined)
24325
+ tg.webhook_sources = root.webhook_sources;
24326
+ deprecations.push("webhook_sources at the agent root is deprecated; move under channels.telegram.webhook_sources (#596).");
24327
+ }
24328
+ channels.telegram = tg;
24329
+ const { voice_in: _vi, telegraph: _tg, webhook_sources: _ws, ...rest } = root;
24330
+ return {
24331
+ config: { ...rest, channels },
24332
+ deprecations
24333
+ };
24334
+ }
24335
+ function mergeAgentConfig(defaultsIn, agentIn) {
24336
+ const { config: agent, deprecations: agentDeprecations } = foldDeprecatedTelegramFields(agentIn);
24337
+ const defaultsMigration = defaultsIn ? foldDeprecatedTelegramFields(defaultsIn) : null;
24338
+ const defaults = defaultsMigration?.config;
24339
+ const allDeprecations = [
24340
+ ...agentDeprecations,
24341
+ ...defaultsMigration?.deprecations ?? []
24342
+ ];
24343
+ if (allDeprecations.length > 0 && !mergeAgentConfig.suppressDeprecationLogs) {
24344
+ for (const msg of allDeprecations) {
24345
+ console.warn(`[switchroom] DEPRECATION: ${msg}`);
24346
+ }
24347
+ }
24348
+ if (!defaults)
24349
+ return agent;
24350
+ const merged = { ...agent };
24351
+ if (defaults.bot_token !== undefined && merged.bot_token === undefined) {
24352
+ merged.bot_token = defaults.bot_token;
24353
+ }
24354
+ if (defaults.timezone !== undefined && merged.timezone === undefined) {
24355
+ merged.timezone = defaults.timezone;
24356
+ }
24357
+ if (defaults.model !== undefined && merged.model === undefined) {
24358
+ merged.model = defaults.model;
24359
+ }
24360
+ if (defaults.dangerous_mode !== undefined && merged.dangerous_mode === undefined) {
24361
+ merged.dangerous_mode = defaults.dangerous_mode;
24362
+ }
24363
+ if (defaults.network_isolation !== undefined && merged.network_isolation === undefined) {
24364
+ merged.network_isolation = defaults.network_isolation;
24365
+ }
24366
+ if (defaults.thinking_effort !== undefined && merged.thinking_effort === undefined) {
24367
+ merged.thinking_effort = defaults.thinking_effort;
24368
+ }
24369
+ if (defaults.permission_mode !== undefined && merged.permission_mode === undefined) {
24370
+ merged.permission_mode = defaults.permission_mode;
24371
+ }
24372
+ if (defaults.fallback_model !== undefined && merged.fallback_model === undefined) {
24373
+ merged.fallback_model = defaults.fallback_model;
24374
+ }
24375
+ if (defaults.tools || merged.tools) {
24376
+ const dAllow = defaults.tools?.allow ?? [];
24377
+ const aAllow = merged.tools?.allow ?? [];
24378
+ const dDeny = defaults.tools?.deny ?? [];
24379
+ const aDeny = merged.tools?.deny ?? [];
24380
+ merged.tools = {
24381
+ allow: dedupe([...dAllow, ...aAllow]),
24382
+ deny: dedupe([...dDeny, ...aDeny])
24383
+ };
24384
+ }
24385
+ if (defaults.soul || merged.soul) {
24386
+ const base = defaults.soul ?? {};
24387
+ const override = merged.soul ?? {};
24388
+ const combined = { ...base };
24389
+ for (const [k, v] of Object.entries(override)) {
24390
+ if (v !== undefined)
24391
+ combined[k] = v;
24392
+ }
24393
+ merged.soul = combined;
24394
+ }
24395
+ if (defaults.memory || merged.memory) {
24396
+ const base = defaults.memory ?? {};
24397
+ const override = merged.memory ?? {};
24398
+ const combined = { ...base };
24399
+ for (const [k, v] of Object.entries(override)) {
24400
+ if (v === undefined)
24401
+ continue;
24402
+ if (k === "recall" && base.recall && typeof v === "object" && v !== null && !Array.isArray(v)) {
24403
+ combined[k] = { ...base.recall, ...v };
24404
+ } else {
24405
+ combined[k] = v;
24406
+ }
24407
+ }
24408
+ merged.memory = combined;
24409
+ }
24410
+ if (defaults.mcp_servers || merged.mcp_servers) {
24411
+ merged.mcp_servers = {
24412
+ ...defaults.mcp_servers ?? {},
24413
+ ...merged.mcp_servers ?? {}
24414
+ };
24415
+ }
24416
+ const dBundled = defaults.bundled_skills;
24417
+ const mBundled = merged.bundled_skills;
24418
+ if (dBundled || mBundled) {
24419
+ merged.bundled_skills = {
24420
+ ...dBundled ?? {},
24421
+ ...mBundled ?? {}
24422
+ };
24423
+ }
24424
+ if (defaults.hooks || merged.hooks) {
24425
+ const result = {};
24426
+ const dHooks = defaults.hooks ?? {};
24427
+ const aHooks = merged.hooks ?? {};
24428
+ const events = new Set([
24429
+ ...Object.keys(dHooks),
24430
+ ...Object.keys(aHooks)
24431
+ ]);
24432
+ for (const event of events) {
24433
+ const d = dHooks[event] ?? [];
24434
+ const a = aHooks[event] ?? [];
24435
+ result[event] = [...d, ...a];
24436
+ }
24437
+ merged.hooks = result;
24438
+ }
24439
+ if (defaults.env || merged.env) {
24440
+ merged.env = {
24441
+ ...defaults.env ?? {},
24442
+ ...merged.env ?? {}
24443
+ };
24444
+ }
24445
+ if (defaults.subagents || merged.subagents) {
24446
+ const dSub = defaults.subagents ?? {};
24447
+ const mSub = merged.subagents ?? {};
24448
+ const out = { ...dSub };
24449
+ for (const [name, override] of Object.entries(mSub)) {
24450
+ const base = dSub[name];
24451
+ if (base && typeof base === "object" && override && typeof override === "object") {
24452
+ const combined = { ...base };
24453
+ for (const [k, v] of Object.entries(override)) {
24454
+ if (v !== undefined)
24455
+ combined[k] = v;
24456
+ }
24457
+ out[name] = combined;
24458
+ } else {
24459
+ out[name] = override;
24460
+ }
24461
+ }
24462
+ merged.subagents = out;
24463
+ }
24464
+ if (defaults.session || merged.session) {
24465
+ const base = defaults.session ?? {};
24466
+ const override = merged.session ?? {};
24467
+ const combined = { ...base };
24468
+ for (const [k, v] of Object.entries(override)) {
24469
+ if (v !== undefined)
24470
+ combined[k] = v;
24471
+ }
24472
+ merged.session = combined;
24473
+ }
24474
+ if (defaults.session_continuity || merged.session_continuity) {
24475
+ const base = defaults.session_continuity ?? {};
24476
+ const override = merged.session_continuity ?? {};
24477
+ const combined = { ...base };
24478
+ for (const [k, v] of Object.entries(override)) {
24479
+ if (v !== undefined)
24480
+ combined[k] = v;
24481
+ }
24482
+ merged.session_continuity = combined;
24483
+ }
24484
+ if (merged.release === undefined && defaults.release !== undefined) {
24485
+ merged.release = defaults.release;
24486
+ }
24487
+ if (defaults.channels || merged.channels) {
24488
+ const dChan = defaults.channels ?? {};
24489
+ const aChan = merged.channels ?? {};
24490
+ const combined = { ...dChan };
24491
+ for (const [key, value] of Object.entries(aChan)) {
24492
+ if (value === undefined)
24493
+ continue;
24494
+ const base = combined[key] ?? {};
24495
+ const override = value;
24496
+ const field = { ...base };
24497
+ for (const [k, v] of Object.entries(override)) {
24498
+ if (v !== undefined)
24499
+ field[k] = v;
24500
+ }
24501
+ combined[key] = field;
24502
+ }
24503
+ merged.channels = combined;
24504
+ }
24505
+ if (defaults.system_prompt_append || merged.system_prompt_append) {
24506
+ const parts = [
24507
+ defaults.system_prompt_append,
24508
+ merged.system_prompt_append
24509
+ ].filter((p) => typeof p === "string" && p.length > 0);
24510
+ merged.system_prompt_append = parts.join(`
24511
+
24512
+ `);
24513
+ }
24514
+ if (defaults.schedule && defaults.schedule.length > 0) {
24515
+ merged.schedule = [...defaults.schedule, ...merged.schedule ?? []];
24516
+ }
24517
+ if (defaults.skills || merged.skills) {
24518
+ const d = defaults.skills ?? [];
24519
+ const a = merged.skills ?? [];
24520
+ merged.skills = dedupe([...d, ...a]);
24521
+ }
24522
+ if (defaults.settings_raw || merged.settings_raw) {
24523
+ merged.settings_raw = deepMergeJson(defaults.settings_raw ?? {}, merged.settings_raw ?? {});
24524
+ }
24525
+ if (defaults.claude_md_raw || merged.claude_md_raw) {
24526
+ const parts = [defaults.claude_md_raw, merged.claude_md_raw].filter((p) => typeof p === "string" && p.length > 0);
24527
+ merged.claude_md_raw = parts.join(`
24528
+
24529
+ `);
24530
+ }
24531
+ if (defaults.cli_args || merged.cli_args) {
24532
+ merged.cli_args = [
24533
+ ...defaults.cli_args ?? [],
24534
+ ...merged.cli_args ?? []
24535
+ ];
24536
+ }
24537
+ if (defaults.extra_stable_files || merged.extra_stable_files) {
24538
+ const d = defaults.extra_stable_files ?? [];
24539
+ const a = merged.extra_stable_files ?? [];
24540
+ merged.extra_stable_files = dedupe([...d, ...a]);
24541
+ }
24542
+ const dReactions = defaults.reactions;
24543
+ const mReactions = merged.reactions;
24544
+ if (dReactions || mReactions) {
24545
+ const base = dReactions ?? {};
24546
+ const override = mReactions ?? {};
24547
+ const combined = { ...base };
24548
+ for (const [k, v] of Object.entries(override)) {
24549
+ if (v !== undefined)
24550
+ combined[k] = v;
24551
+ }
24552
+ merged.reactions = combined;
24553
+ }
24554
+ if (defaults.resources || merged.resources) {
24555
+ const d = defaults.resources ?? {};
24556
+ const a = merged.resources ?? {};
24557
+ merged.resources = { ...d, ...a };
24558
+ }
24559
+ if (defaults.experimental || merged.experimental) {
24560
+ const d = defaults.experimental ?? {};
24561
+ const a = merged.experimental ?? {};
24562
+ merged.experimental = { ...d, ...a };
24563
+ }
24564
+ return merged;
24565
+ }
24566
+ var init_merge = __esm(() => {
24567
+ ((mergeAgentConfig) => {
24568
+ mergeAgentConfig.suppressDeprecationLogs = false;
24569
+ mergeAgentConfig.notifiedWorkerIsolationMove = false;
24570
+ })(mergeAgentConfig ||= {});
24571
+ });
24572
+
24168
24573
  // ../src/config/loader.ts
24169
24574
  import { readFileSync as readFileSync5, existsSync as existsSync9 } from "node:fs";
24170
24575
  import { homedir as homedir5 } from "node:os";
@@ -24278,8 +24683,34 @@ function loadConfig(configPath) {
24278
24683
  throw err;
24279
24684
  }
24280
24685
  applyAgentOverlays(config);
24686
+ validateAllCronTopicAliases(config, filePath);
24281
24687
  return config;
24282
24688
  }
24689
+ function validateAllCronTopicAliases(config, filePath) {
24690
+ const issues = [];
24691
+ for (const [agentName3, agentRaw] of Object.entries(config.agents)) {
24692
+ if (!agentRaw)
24693
+ continue;
24694
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentRaw);
24695
+ const schedule = resolved.schedule ?? [];
24696
+ if (schedule.length === 0)
24697
+ continue;
24698
+ const tg = resolved.channels?.telegram;
24699
+ const aliases = new Set(Object.keys(tg?.topic_aliases ?? {}));
24700
+ for (const entry of schedule) {
24701
+ if (entry.topic == null)
24702
+ continue;
24703
+ if (typeof entry.topic === "number")
24704
+ continue;
24705
+ if (!aliases.has(entry.topic)) {
24706
+ issues.push(` agents.${agentName3}.schedule cron \`${entry.cron}\`: ` + `topic alias "${entry.topic}" is not defined in ` + `channels.telegram.topic_aliases.`);
24707
+ }
24708
+ }
24709
+ }
24710
+ if (issues.length > 0) {
24711
+ throw new ConfigError(`Cron \`topic:\` alias references unknown topic_aliases in ${filePath}`, issues);
24712
+ }
24713
+ }
24283
24714
  function resolvePath(pathStr) {
24284
24715
  return resolveDualPath(pathStr);
24285
24716
  }
@@ -24290,6 +24721,7 @@ var init_loader = __esm(() => {
24290
24721
  init_schema();
24291
24722
  init_paths();
24292
24723
  init_overlay_loader();
24724
+ init_merge();
24293
24725
  ConfigError = class ConfigError extends Error {
24294
24726
  details;
24295
24727
  constructor(message, details) {
@@ -30801,8 +31233,9 @@ function createInboundCoalescer(opts) {
30801
31233
  }
30802
31234
  };
30803
31235
  }
30804
- function inboundCoalesceKey(chatId, userId) {
30805
- return `${chatId}:${userId}`;
31236
+ function inboundCoalesceKey(chatId, threadId, userId) {
31237
+ const t = threadId == null || threadId === 0 ? "_" : String(threadId);
31238
+ return `${chatId}:${t}:${userId}`;
30806
31239
  }
30807
31240
 
30808
31241
  // status-reactions.ts
@@ -31270,29 +31703,41 @@ function prettifyServer(name) {
31270
31703
  return name.charAt(0).toUpperCase() + name.slice(1);
31271
31704
  }
31272
31705
 
31706
+ // gateway/chat-key.ts
31707
+ function chatKey(chatId, threadId) {
31708
+ const t = threadId == null || threadId === 0 ? "_" : String(threadId);
31709
+ return `${chatId}:${t}`;
31710
+ }
31711
+ function chatKeyWithSuffix(chatId, threadId, suffix) {
31712
+ return `${chatKey(chatId, threadId)}:${suffix}`;
31713
+ }
31714
+
31273
31715
  // typing-wrap.ts
31274
31716
  function createTypingWrapper(deps) {
31275
31717
  const debounceMs = deps.debounceMs ?? 500;
31276
31718
  const pending = new Map;
31277
- const activeChats = new Set;
31719
+ const activeLanes = new Set;
31278
31720
  return {
31279
- onToolUse(toolUseId, chatId, toolName) {
31721
+ onToolUse(toolUseId, chatId, toolName, threadId) {
31280
31722
  if (!toolUseId)
31281
31723
  return;
31282
31724
  if (deps.isSurfaceTool(toolName))
31283
31725
  return;
31726
+ const tid = threadId ?? null;
31727
+ const lane = chatKey(chatId, tid);
31284
31728
  const prior = pending.get(toolUseId);
31285
31729
  if (prior) {
31286
31730
  clearTimeout(prior.timer);
31287
31731
  if (prior.started)
31288
- deps.stopTypingLoop(prior.chatId);
31732
+ deps.stopTypingLoop(prior.chatId, prior.threadId);
31289
31733
  pending.delete(toolUseId);
31290
31734
  }
31291
- if (!activeChats.has(chatId)) {
31292
- deps.startTypingLoop(chatId);
31293
- activeChats.add(chatId);
31735
+ if (!activeLanes.has(lane)) {
31736
+ deps.startTypingLoop(chatId, tid);
31737
+ activeLanes.add(lane);
31294
31738
  const entry2 = {
31295
31739
  chatId,
31740
+ threadId: tid,
31296
31741
  started: true,
31297
31742
  timer: setTimeout(() => {}, 0)
31298
31743
  };
@@ -31301,9 +31746,10 @@ function createTypingWrapper(deps) {
31301
31746
  }
31302
31747
  const entry = {
31303
31748
  chatId,
31749
+ threadId: tid,
31304
31750
  started: false,
31305
31751
  timer: setTimeout(() => {
31306
- deps.startTypingLoop(chatId);
31752
+ deps.startTypingLoop(chatId, tid);
31307
31753
  entry.started = true;
31308
31754
  }, debounceMs)
31309
31755
  };
@@ -31317,8 +31763,8 @@ function createTypingWrapper(deps) {
31317
31763
  return;
31318
31764
  clearTimeout(entry.timer);
31319
31765
  if (entry.started) {
31320
- deps.stopTypingLoop(entry.chatId);
31321
- activeChats.delete(entry.chatId);
31766
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
31767
+ activeLanes.delete(chatKey(entry.chatId, entry.threadId));
31322
31768
  }
31323
31769
  pending.delete(toolUseId);
31324
31770
  },
@@ -31326,10 +31772,10 @@ function createTypingWrapper(deps) {
31326
31772
  for (const entry of pending.values()) {
31327
31773
  clearTimeout(entry.timer);
31328
31774
  if (entry.started)
31329
- deps.stopTypingLoop(entry.chatId);
31775
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
31330
31776
  }
31331
31777
  pending.clear();
31332
- activeChats.clear();
31778
+ activeLanes.clear();
31333
31779
  }
31334
31780
  };
31335
31781
  }
@@ -32004,10 +32450,8 @@ function buildAccentHeader(accent) {
32004
32450
  }
32005
32451
  }
32006
32452
  function streamKey2(chatId, threadId, lane, turnKey) {
32007
- const t = threadId == null || threadId === 0 ? "_" : String(threadId);
32008
- const base = `${chatId}:${t}`;
32009
- const withLane = lane != null && lane.length > 0 ? `${base}:${lane}` : base;
32010
- return turnKey != null && turnKey.length > 0 ? `${withLane}:${turnKey}` : withLane;
32453
+ const base = lane != null && lane.length > 0 ? chatKeyWithSuffix(chatId, threadId ?? null, lane) : chatKey(chatId, threadId ?? null);
32454
+ return turnKey != null && turnKey.length > 0 ? `${base}:${turnKey}` : base;
32011
32455
  }
32012
32456
  async function handleStreamReply(args, state, deps) {
32013
32457
  const chat_id = args.chat_id;
@@ -32202,14 +32646,14 @@ async function handleStreamReply(args, state, deps) {
32202
32646
  // chat-lock.ts
32203
32647
  function createChatLock() {
32204
32648
  const chains = new Map;
32205
- function run(chatId, fn) {
32206
- const prior = chains.get(chatId) ?? Promise.resolve();
32649
+ function run(key, fn) {
32650
+ const prior = chains.get(key) ?? Promise.resolve();
32207
32651
  const next = prior.then(fn, fn);
32208
32652
  const tracked = next.finally(() => {
32209
- if (chains.get(chatId) === tracked)
32210
- chains.delete(chatId);
32653
+ if (chains.get(key) === tracked)
32654
+ chains.delete(key);
32211
32655
  });
32212
- chains.set(chatId, tracked);
32656
+ chains.set(key, tracked);
32213
32657
  return next;
32214
32658
  }
32215
32659
  function wrapBot(bot) {
@@ -32220,8 +32664,21 @@ function createChatLock() {
32220
32664
  return orig;
32221
32665
  return function(...args) {
32222
32666
  const first = args[0];
32223
- const key = typeof first === "string" || typeof first === "number" ? String(first) : "__global__";
32224
- return run(key, () => orig.apply(target, args));
32667
+ if (typeof first !== "string" && typeof first !== "number") {
32668
+ return run("__global__", () => orig.apply(target, args));
32669
+ }
32670
+ const chatId = String(first);
32671
+ const last = args[args.length - 1];
32672
+ const lastIsOpts = last != null && typeof last === "object" && !Array.isArray(last);
32673
+ const threadFromOpts = lastIsOpts ? last.message_thread_id : undefined;
32674
+ let tid = typeof threadFromOpts === "number" ? threadFromOpts : null;
32675
+ if (tid === 1) {
32676
+ tid = null;
32677
+ const cleanedOpts = { ...last };
32678
+ delete cleanedOpts.message_thread_id;
32679
+ args = args.slice(0, -1).concat([cleanedOpts]);
32680
+ }
32681
+ return run(chatKey(chatId, tid), () => orig.apply(target, args));
32225
32682
  };
32226
32683
  }
32227
32684
  });
@@ -41300,6 +41757,7 @@ function flushOnAgentDisconnect(deps) {
41300
41757
  activeStatusReactions,
41301
41758
  activeReactionMsgIds,
41302
41759
  activeTurnStartedAt,
41760
+ claudeBusyKeys,
41303
41761
  activeDraftStreams,
41304
41762
  activeDraftParseModes,
41305
41763
  clearActiveReactions: clearActiveReactions3,
@@ -41316,6 +41774,7 @@ function flushOnAgentDisconnect(deps) {
41316
41774
  activeStatusReactions.delete(key);
41317
41775
  activeReactionMsgIds.delete(key);
41318
41776
  activeTurnStartedAt.delete(key);
41777
+ claudeBusyKeys.delete(key);
41319
41778
  }
41320
41779
  clearActiveReactions3();
41321
41780
  const danglingKeys = [...activeTurnStartedAt.keys()];
@@ -41323,10 +41782,16 @@ function flushOnAgentDisconnect(deps) {
41323
41782
  for (const k of danglingKeys) {
41324
41783
  activeTurnStartedAt.delete(k);
41325
41784
  activeReactionMsgIds.delete(k);
41785
+ claudeBusyKeys.delete(k);
41326
41786
  }
41327
41787
  log(`telegram gateway: disconnect-flush swept ${danglingKeys.length} dangling turn key(s) ` + `post-bridge-death (controller loop missed \u2014 finalize raced disconnect)`);
41328
41788
  onDanglingTurnsSwept?.(danglingKeys);
41329
41789
  }
41790
+ if (claudeBusyKeys.size > 0) {
41791
+ const orphanCount = claudeBusyKeys.size;
41792
+ claudeBusyKeys.clear();
41793
+ log(`telegram gateway: disconnect-flush cleared ${orphanCount} orphan claudeBusyKeys ` + `entr${orphanCount === 1 ? "y" : "ies"} (synthetic-inbound deliveries that never turn_ended)`);
41794
+ }
41330
41795
  disposeProgressDriver();
41331
41796
  for (const [key, stream] of activeDraftStreams.entries()) {
41332
41797
  if (!stream.isFinal())
@@ -42715,6 +43180,7 @@ init_zod();
42715
43180
  init_schema();
42716
43181
  init_paths();
42717
43182
  init_overlay_loader();
43183
+ init_merge();
42718
43184
  import { readFileSync as readFileSync14, existsSync as existsSync18 } from "node:fs";
42719
43185
  import { homedir as homedir8 } from "node:os";
42720
43186
  import { resolve as resolve5 } from "node:path";
@@ -42836,11 +43302,37 @@ function loadConfig2(configPath) {
42836
43302
  throw err;
42837
43303
  }
42838
43304
  applyAgentOverlays(config);
43305
+ validateAllCronTopicAliases2(config, filePath);
42839
43306
  return config;
42840
43307
  }
43308
+ function validateAllCronTopicAliases2(config, filePath) {
43309
+ const issues = [];
43310
+ for (const [agentName3, agentRaw] of Object.entries(config.agents)) {
43311
+ if (!agentRaw)
43312
+ continue;
43313
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentRaw);
43314
+ const schedule = resolved.schedule ?? [];
43315
+ if (schedule.length === 0)
43316
+ continue;
43317
+ const tg = resolved.channels?.telegram;
43318
+ const aliases = new Set(Object.keys(tg?.topic_aliases ?? {}));
43319
+ for (const entry of schedule) {
43320
+ if (entry.topic == null)
43321
+ continue;
43322
+ if (typeof entry.topic === "number")
43323
+ continue;
43324
+ if (!aliases.has(entry.topic)) {
43325
+ issues.push(` agents.${agentName3}.schedule cron \`${entry.cron}\`: ` + `topic alias "${entry.topic}" is not defined in ` + `channels.telegram.topic_aliases.`);
43326
+ }
43327
+ }
43328
+ }
43329
+ if (issues.length > 0) {
43330
+ throw new ConfigError2(`Cron \`topic:\` alias references unknown topic_aliases in ${filePath}`, issues);
43331
+ }
43332
+ }
42841
43333
 
42842
43334
  // ../src/config/merge.ts
42843
- function dedupe(items) {
43335
+ function dedupe2(items) {
42844
43336
  const seen = new Set;
42845
43337
  const out = [];
42846
43338
  for (const item of items) {
@@ -42851,7 +43343,7 @@ function dedupe(items) {
42851
43343
  }
42852
43344
  return out;
42853
43345
  }
42854
- function deepMergeJson(base, override) {
43346
+ function deepMergeJson2(base, override) {
42855
43347
  if (override === undefined)
42856
43348
  return base;
42857
43349
  if (base === undefined)
@@ -42863,25 +43355,25 @@ function deepMergeJson(base, override) {
42863
43355
  for (const [k, v] of Object.entries(override)) {
42864
43356
  if (k === "__proto__" || k === "constructor" || k === "prototype")
42865
43357
  continue;
42866
- out[k] = deepMergeJson(out[k], v);
43358
+ out[k] = deepMergeJson2(out[k], v);
42867
43359
  }
42868
43360
  return out;
42869
43361
  }
42870
- function resolveAgentConfig(defaults, profiles, agent) {
42871
- if (!mergeAgentConfig.suppressDeprecationLogs && !mergeAgentConfig.notifiedWorkerIsolationMove && defaults?.subagents?.worker?.isolation === "worktree") {
42872
- mergeAgentConfig.notifiedWorkerIsolationMove = true;
43362
+ function resolveAgentConfig2(defaults, profiles, agent) {
43363
+ if (!mergeAgentConfig2.suppressDeprecationLogs && !mergeAgentConfig2.notifiedWorkerIsolationMove && defaults?.subagents?.worker?.isolation === "worktree") {
43364
+ mergeAgentConfig2.notifiedWorkerIsolationMove = true;
42873
43365
  console.warn("[switchroom] NOTICE: defaults.subagents.worker.isolation moved to the " + "`coding` profile in switchroom 0.6.6 (#682). Agents extending coding " + "still get worktree-isolated workers; other agents would have hard-failed " + "the first time they delegated. See CHANGELOG.");
42874
43366
  }
42875
43367
  const name = agent.extends;
42876
43368
  const profile = name && profiles ? profiles[name] : undefined;
42877
43369
  if (!profile) {
42878
- return mergeAgentConfig(defaults, agent);
43370
+ return mergeAgentConfig2(defaults, agent);
42879
43371
  }
42880
43372
  const { extends: _unused, ...profileWithoutExtends } = profile;
42881
- const layered = mergeAgentConfig(defaults, profileWithoutExtends);
42882
- return mergeAgentConfig(layered, agent);
43373
+ const layered = mergeAgentConfig2(defaults, profileWithoutExtends);
43374
+ return mergeAgentConfig2(layered, agent);
42883
43375
  }
42884
- function foldDeprecatedTelegramFields(config) {
43376
+ function foldDeprecatedTelegramFields2(config) {
42885
43377
  const c = config;
42886
43378
  const root = c;
42887
43379
  const deprecations = [];
@@ -42912,15 +43404,15 @@ function foldDeprecatedTelegramFields(config) {
42912
43404
  deprecations
42913
43405
  };
42914
43406
  }
42915
- function mergeAgentConfig(defaultsIn, agentIn) {
42916
- const { config: agent, deprecations: agentDeprecations } = foldDeprecatedTelegramFields(agentIn);
42917
- const defaultsMigration = defaultsIn ? foldDeprecatedTelegramFields(defaultsIn) : null;
43407
+ function mergeAgentConfig2(defaultsIn, agentIn) {
43408
+ const { config: agent, deprecations: agentDeprecations } = foldDeprecatedTelegramFields2(agentIn);
43409
+ const defaultsMigration = defaultsIn ? foldDeprecatedTelegramFields2(defaultsIn) : null;
42918
43410
  const defaults = defaultsMigration?.config;
42919
43411
  const allDeprecations = [
42920
43412
  ...agentDeprecations,
42921
43413
  ...defaultsMigration?.deprecations ?? []
42922
43414
  ];
42923
- if (allDeprecations.length > 0 && !mergeAgentConfig.suppressDeprecationLogs) {
43415
+ if (allDeprecations.length > 0 && !mergeAgentConfig2.suppressDeprecationLogs) {
42924
43416
  for (const msg of allDeprecations) {
42925
43417
  console.warn(`[switchroom] DEPRECATION: ${msg}`);
42926
43418
  }
@@ -42958,8 +43450,8 @@ function mergeAgentConfig(defaultsIn, agentIn) {
42958
43450
  const dDeny = defaults.tools?.deny ?? [];
42959
43451
  const aDeny = merged.tools?.deny ?? [];
42960
43452
  merged.tools = {
42961
- allow: dedupe([...dAllow, ...aAllow]),
42962
- deny: dedupe([...dDeny, ...aDeny])
43453
+ allow: dedupe2([...dAllow, ...aAllow]),
43454
+ deny: dedupe2([...dDeny, ...aDeny])
42963
43455
  };
42964
43456
  }
42965
43457
  if (defaults.soul || merged.soul) {
@@ -43097,10 +43589,10 @@ function mergeAgentConfig(defaultsIn, agentIn) {
43097
43589
  if (defaults.skills || merged.skills) {
43098
43590
  const d = defaults.skills ?? [];
43099
43591
  const a = merged.skills ?? [];
43100
- merged.skills = dedupe([...d, ...a]);
43592
+ merged.skills = dedupe2([...d, ...a]);
43101
43593
  }
43102
43594
  if (defaults.settings_raw || merged.settings_raw) {
43103
- merged.settings_raw = deepMergeJson(defaults.settings_raw ?? {}, merged.settings_raw ?? {});
43595
+ merged.settings_raw = deepMergeJson2(defaults.settings_raw ?? {}, merged.settings_raw ?? {});
43104
43596
  }
43105
43597
  if (defaults.claude_md_raw || merged.claude_md_raw) {
43106
43598
  const parts = [defaults.claude_md_raw, merged.claude_md_raw].filter((p) => typeof p === "string" && p.length > 0);
@@ -43117,7 +43609,7 @@ function mergeAgentConfig(defaultsIn, agentIn) {
43117
43609
  if (defaults.extra_stable_files || merged.extra_stable_files) {
43118
43610
  const d = defaults.extra_stable_files ?? [];
43119
43611
  const a = merged.extra_stable_files ?? [];
43120
- merged.extra_stable_files = dedupe([...d, ...a]);
43612
+ merged.extra_stable_files = dedupe2([...d, ...a]);
43121
43613
  }
43122
43614
  const dReactions = defaults.reactions;
43123
43615
  const mReactions = merged.reactions;
@@ -43146,7 +43638,60 @@ function mergeAgentConfig(defaultsIn, agentIn) {
43146
43638
  ((mergeAgentConfig) => {
43147
43639
  mergeAgentConfig.suppressDeprecationLogs = false;
43148
43640
  mergeAgentConfig.notifiedWorkerIsolationMove = false;
43149
- })(mergeAgentConfig ||= {});
43641
+ })(mergeAgentConfig2 ||= {});
43642
+
43643
+ // ../src/telegram/topic-router.ts
43644
+ var ALERTS_ALIAS = "alerts";
43645
+ var ADMIN_ALIAS = "admin";
43646
+ function aliasToId(config, name) {
43647
+ return config.topic_aliases?.[name];
43648
+ }
43649
+ function resolveOutboundTopic(config, event) {
43650
+ const cfg = config ?? {};
43651
+ const inSupergroupMode = cfg.default_topic_id != null;
43652
+ switch (event.kind) {
43653
+ case "reply":
43654
+ return event.originThreadId;
43655
+ case "subagent-progress":
43656
+ return event.parentThreadId;
43657
+ case "command-query":
43658
+ return event.originThreadId;
43659
+ case "vault":
43660
+ case "permission":
43661
+ if (event.turnInitiated) {
43662
+ return event.originThreadId ?? cfg.default_topic_id;
43663
+ }
43664
+ if (!inSupergroupMode)
43665
+ return;
43666
+ return aliasToId(cfg, ADMIN_ALIAS) ?? cfg.default_topic_id;
43667
+ case "hostd-approval":
43668
+ if (event.originThreadId != null)
43669
+ return event.originThreadId;
43670
+ if (!inSupergroupMode)
43671
+ return;
43672
+ return aliasToId(cfg, ADMIN_ALIAS) ?? cfg.default_topic_id;
43673
+ case "cron": {
43674
+ if (typeof event.entryTopic === "number")
43675
+ return event.entryTopic;
43676
+ if (typeof event.entryTopic === "string") {
43677
+ const resolved = aliasToId(cfg, event.entryTopic);
43678
+ if (resolved != null)
43679
+ return resolved;
43680
+ }
43681
+ return cfg.default_topic_id;
43682
+ }
43683
+ case "boot":
43684
+ case "compact-watchdog":
43685
+ if (!inSupergroupMode)
43686
+ return;
43687
+ return aliasToId(cfg, ALERTS_ALIAS) ?? cfg.default_topic_id;
43688
+ case "command-mutation":
43689
+ case "command-heavy":
43690
+ if (!inSupergroupMode)
43691
+ return;
43692
+ return aliasToId(cfg, ADMIN_ALIAS) ?? cfg.default_topic_id;
43693
+ }
43694
+ }
43150
43695
 
43151
43696
  // ../src/agents/perf.ts
43152
43697
  import { existsSync as existsSync19, readFileSync as readFileSync15 } from "node:fs";
@@ -44768,12 +45313,12 @@ function createPendingPermissionBuffer(opts = {}) {
44768
45313
  }
44769
45314
 
44770
45315
  // gateway/chat-key.ts
44771
- function chatKey(chatId, threadId) {
45316
+ function chatKey2(chatId, threadId) {
44772
45317
  const t = threadId == null || threadId === 0 ? "_" : String(threadId);
44773
45318
  return `${chatId}:${t}`;
44774
45319
  }
44775
- function chatKeyWithSuffix(chatId, threadId, suffix) {
44776
- return `${chatKey(chatId, threadId)}:${suffix}`;
45320
+ function chatKeyWithSuffix2(chatId, threadId, suffix) {
45321
+ return `${chatKey2(chatId, threadId)}:${suffix}`;
44777
45322
  }
44778
45323
  function chatIdOfChatKey(key) {
44779
45324
  const idx = key.indexOf(":");
@@ -48732,11 +49277,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
48732
49277
  }
48733
49278
 
48734
49279
  // ../src/build-info.ts
48735
- var VERSION = "0.13.52";
48736
- var COMMIT_SHA = "3d68efa2";
48737
- var COMMIT_DATE = "2026-05-26T00:48:06Z";
48738
- var LATEST_PR = 1864;
48739
- var COMMITS_AHEAD_OF_TAG = 0;
49280
+ var VERSION = "0.13.53";
49281
+ var COMMIT_SHA = "08726c33";
49282
+ var COMMIT_DATE = "2026-05-27T03:52:37Z";
49283
+ var LATEST_PR = 1885;
49284
+ var COMMITS_AHEAD_OF_TAG = 18;
48740
49285
 
48741
49286
  // gateway/boot-version.ts
48742
49287
  function formatRelativeAgo(iso) {
@@ -49650,6 +50195,16 @@ var outboundDedup = new OutboundDedupCache;
49650
50195
  var chatAvailableReactions = new Map;
49651
50196
  var chatProbesInFlight = new Set;
49652
50197
  var activeTurnStartedAt = new Map;
50198
+ var claudeBusyKeys = new Set;
50199
+ function markClaudeBusyForInbound(m) {
50200
+ let tid = m.threadId ?? null;
50201
+ if (tid == null && m.meta?.message_thread_id != null) {
50202
+ const n = Number(m.meta.message_thread_id);
50203
+ if (Number.isFinite(n))
50204
+ tid = n;
50205
+ }
50206
+ claudeBusyKeys.add(chatKey2(m.chatId, tid));
50207
+ }
49653
50208
  var pendingRestarts = new Map;
49654
50209
  var lastSessionActiveFile = null;
49655
50210
  var compactState = initialCompactState();
@@ -49689,10 +50244,10 @@ var CONTEXT_EXHAUSTION_COOLDOWN_MS = 600000;
49689
50244
  var lastContextExhaustionWarningAt = 0;
49690
50245
  var pendingPtyPartial = null;
49691
50246
  function statusKey(chatId, threadId) {
49692
- return chatKey(chatId, threadId);
50247
+ return chatKey2(chatId, threadId);
49693
50248
  }
49694
50249
  function streamKey3(chatId, threadId) {
49695
- return chatKey(chatId, threadId);
50250
+ return chatKey2(chatId, threadId);
49696
50251
  }
49697
50252
  function purgeReactionTracking(key, endingTurn) {
49698
50253
  const outboundEmitted = endingTurn != null ? endingTurn.replyCalled === true : currentTurn?.replyCalled === true;
@@ -49701,16 +50256,29 @@ function purgeReactionTracking(key, endingTurn) {
49701
50256
  activeStatusReactions.delete(key);
49702
50257
  activeReactionMsgIds.delete(key);
49703
50258
  activeTurnStartedAt.delete(key);
49704
- stopTurnTypingLoop(chatIdOfChatKey(key));
50259
+ claudeBusyKeys.delete(key);
50260
+ if (endingTurn != null) {
50261
+ stopTurnTypingLoop(endingTurn.sessionChatId, endingTurn.sessionThreadId ?? null);
50262
+ } else {
50263
+ const chatId = chatIdOfChatKey(key);
50264
+ const threadPart = key.slice(chatId.length + 1);
50265
+ const threadId = threadPart === "_" || threadPart === "" ? null : Number(threadPart);
50266
+ stopTurnTypingLoop(chatId, Number.isFinite(threadId) ? threadId : null);
50267
+ }
49705
50268
  if (msgInfo) {
49706
50269
  const agentDir = resolveAgentDirFromEnv();
49707
50270
  if (agentDir != null)
49708
50271
  removeActiveReaction(agentDir, msgInfo.chatId, msgInfo.messageId);
49709
50272
  }
49710
- if (activeTurnStartedAt.size === 0) {
50273
+ if (claudeBusyKeys.size === 0) {
49711
50274
  const selfAgentForFlush = process.env.SWITCHROOM_AGENT_NAME ?? "";
49712
50275
  if (pendingInboundBuffer.depth(selfAgentForFlush) > 0) {
49713
- const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => ipcServer.sendToAgent(selfAgentForFlush, m), inboundSpool);
50276
+ const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => {
50277
+ const d = ipcServer.sendToAgent(selfAgentForFlush, m);
50278
+ if (d)
50279
+ markClaudeBusyForInbound(m);
50280
+ return d;
50281
+ }, inboundSpool);
49714
50282
  if (fr.redelivered > 0) {
49715
50283
  process.stderr.write(`telegram gateway: turn-complete flushed ${fr.redelivered}/${fr.drained} held inbound for ${selfAgentForFlush}${fr.rebuffered > 0 ? ` (${fr.rebuffered} re-buffered)` : ""}
49716
50284
  `);
@@ -49730,11 +50298,17 @@ function releaseTurnBufferGate(key) {
49730
50298
  if (!activeTurnStartedAt.has(key))
49731
50299
  return;
49732
50300
  activeTurnStartedAt.delete(key);
50301
+ claudeBusyKeys.delete(key);
49733
50302
  shadowEmit({ kind: "turnEnd", key, at: Date.now(), outboundEmitted: true });
49734
- if (activeTurnStartedAt.size === 0) {
50303
+ if (claudeBusyKeys.size === 0) {
49735
50304
  const selfAgentForFlush = process.env.SWITCHROOM_AGENT_NAME ?? "";
49736
50305
  if (pendingInboundBuffer.depth(selfAgentForFlush) > 0) {
49737
- const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => ipcServer.sendToAgent(selfAgentForFlush, m), inboundSpool);
50306
+ const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => {
50307
+ const d = ipcServer.sendToAgent(selfAgentForFlush, m);
50308
+ if (d)
50309
+ markClaudeBusyForInbound(m);
50310
+ return d;
50311
+ }, inboundSpool);
49738
50312
  if (fr.redelivered > 0) {
49739
50313
  process.stderr.write(`telegram gateway: reply-released-gate flushed ${fr.redelivered}/${fr.drained} held inbound for ${selfAgentForFlush}${fr.rebuffered > 0 ? ` (${fr.rebuffered} re-buffered)` : ""}
49740
50314
  `);
@@ -49758,7 +50332,7 @@ function maybeProactiveCompact() {
49758
50332
  try {
49759
50333
  const cfg = loadConfig2();
49760
50334
  const rawAgent = cfg.agents?.[agentName3] ?? {};
49761
- const resolved = resolveAgentConfig(cfg.defaults, cfg.profiles, rawAgent);
50335
+ const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
49762
50336
  cap = resolved.session?.max_context_tokens;
49763
50337
  } catch {
49764
50338
  return;
@@ -49811,7 +50385,7 @@ async function postCompactCard(occ, cap) {
49811
50385
  const chatId = loadAccess().allowFrom[0];
49812
50386
  if (!chatId)
49813
50387
  return;
49814
- const threadId = chatThreadMap.get(chatId);
50388
+ const threadId = resolveAgentOutboundTopic({ kind: "compact-watchdog" }) ?? chatThreadMap.get(chatId);
49815
50389
  const text = `\uD83D\uDDDC\uFE0F <b>Context compaction</b>
49816
50390
  ` + `Working context hit ~${occ.toLocaleString()} tokens (cap ${cap.toLocaleString()}) \u2014 running <code>/compact</code>. ` + `Older detail moves to Hindsight; I'll confirm here once the context has shrunk (may take a turn or two).`;
49817
50391
  const sent = await swallowingApiCall(() => bot.api.sendMessage(chatId, text, {
@@ -49986,53 +50560,59 @@ var CHAT_ACTION_WHITELIST = new Set([
49986
50560
  "record_video_note",
49987
50561
  "upload_video_note"
49988
50562
  ]);
49989
- function startTypingLoop(chat_id, action = "typing") {
49990
- stopTypingLoop(chat_id);
50563
+ function startTypingLoop(chat_id, thread_id = null, action = "typing") {
50564
+ stopTypingLoop(chat_id, thread_id);
50565
+ const key = chatKey2(chat_id, thread_id);
50566
+ const sendOpts = thread_id != null ? { message_thread_id: thread_id } : undefined;
49991
50567
  const send = () => {
49992
- bot.api.sendChatAction(chat_id, action).then(() => {
50568
+ bot.api.sendChatAction(chat_id, action, sendOpts).then(() => {
49993
50569
  typingBackoffMs = 0;
49994
50570
  }, (err) => {
49995
50571
  const msg = err instanceof Error ? err.message : String(err);
49996
50572
  if (msg.includes("401") || msg.includes("Unauthorized")) {
49997
50573
  typingBackoffMs = Math.min(Math.max(typingBackoffMs * 2 || 1000, 1000), TYPING_BACKOFF_MAX);
49998
- stopTypingLoop(chat_id);
50574
+ stopTypingLoop(chat_id, thread_id);
49999
50575
  const retry = setTimeout(() => {
50000
- typingRetryTimers.delete(chat_id);
50001
- startTypingLoop(chat_id, action);
50576
+ typingRetryTimers.delete(key);
50577
+ startTypingLoop(chat_id, thread_id, action);
50002
50578
  }, typingBackoffMs);
50003
- typingRetryTimers.set(chat_id, retry);
50579
+ typingRetryTimers.set(key, retry);
50004
50580
  }
50005
50581
  });
50006
50582
  };
50007
50583
  send();
50008
- typingIntervals.set(chat_id, setInterval(send, 4000));
50584
+ typingIntervals.set(key, setInterval(send, 4000));
50009
50585
  }
50010
- function stopTypingLoop(chat_id) {
50011
- const iv = typingIntervals.get(chat_id);
50586
+ function stopTypingLoop(chat_id, thread_id = null) {
50587
+ const key = chatKey2(chat_id, thread_id);
50588
+ const iv = typingIntervals.get(key);
50012
50589
  if (iv) {
50013
50590
  clearInterval(iv);
50014
- typingIntervals.delete(chat_id);
50591
+ typingIntervals.delete(key);
50015
50592
  }
50016
- const retry = typingRetryTimers.get(chat_id);
50593
+ const retry = typingRetryTimers.get(key);
50017
50594
  if (retry) {
50018
50595
  clearTimeout(retry);
50019
- typingRetryTimers.delete(chat_id);
50596
+ typingRetryTimers.delete(key);
50020
50597
  }
50021
50598
  }
50022
50599
  var turnTypingIntervals = new Map;
50023
- function startTurnTypingLoop(chat_id) {
50024
- stopTurnTypingLoop(chat_id);
50600
+ function startTurnTypingLoop(chat_id, thread_id = null) {
50601
+ stopTurnTypingLoop(chat_id, thread_id);
50602
+ const key = chatKey2(chat_id, thread_id);
50603
+ const sendOpts = thread_id != null ? { message_thread_id: thread_id } : undefined;
50025
50604
  const send = () => {
50026
- bot.api.sendChatAction(chat_id, "typing").catch(() => {});
50605
+ bot.api.sendChatAction(chat_id, "typing", sendOpts).catch(() => {});
50027
50606
  };
50028
50607
  send();
50029
- turnTypingIntervals.set(chat_id, setInterval(send, 4000));
50608
+ turnTypingIntervals.set(key, setInterval(send, 4000));
50030
50609
  }
50031
- function stopTurnTypingLoop(chat_id) {
50032
- const iv = turnTypingIntervals.get(chat_id);
50610
+ function stopTurnTypingLoop(chat_id, thread_id = null) {
50611
+ const key = chatKey2(chat_id, thread_id);
50612
+ const iv = turnTypingIntervals.get(key);
50033
50613
  if (iv) {
50034
50614
  clearInterval(iv);
50035
- turnTypingIntervals.delete(chat_id);
50615
+ turnTypingIntervals.delete(key);
50036
50616
  }
50037
50617
  }
50038
50618
  var typingWrapper = createTypingWrapper({
@@ -50635,7 +51215,12 @@ startTimer({
50635
51215
  clearSilentEndState(fbKey);
50636
51216
  } catch {}
50637
51217
  const fbSelfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
50638
- const fbRedeliver = redeliverBufferedInbound(pendingInboundBuffer, fbSelfAgent, (m) => ipcServer.sendToAgent(fbSelfAgent, m), inboundSpool);
51218
+ const fbRedeliver = redeliverBufferedInbound(pendingInboundBuffer, fbSelfAgent, (m) => {
51219
+ const d = ipcServer.sendToAgent(fbSelfAgent, m);
51220
+ if (d)
51221
+ markClaudeBusyForInbound(m);
51222
+ return d;
51223
+ }, inboundSpool);
50639
51224
  process.stderr.write(`telegram gateway: silence-poke framework-fallback ended wedged turn chat=${fbChatId} thread=${ctx.threadId ?? "-"} silence_ms=${ctx.silenceMs} currentTurn_nulled=${turnMatchesFallback} drained_buffered=${fbRedeliver.redelivered}/${fbRedeliver.drained}${fbRedeliver.rebuffered > 0 ? ` rebuffered=${fbRedeliver.rebuffered}` : ""}${fbExtraPurge.purged.length > 0 ? ` extra_keys_purged=${fbExtraPurge.purged.length}` : ""}
50640
51225
  `);
50641
51226
  }
@@ -50818,6 +51403,7 @@ var ipcServer = createIpcServer({
50818
51403
  activeStatusReactions,
50819
51404
  activeReactionMsgIds,
50820
51405
  activeTurnStartedAt,
51406
+ claudeBusyKeys,
50821
51407
  activeDraftStreams,
50822
51408
  activeDraftParseModes,
50823
51409
  clearActiveReactions: () => {
@@ -50931,7 +51517,7 @@ ${reminder}
50931
51517
  onHeartbeat(_client, _msg) {},
50932
51518
  onScheduleRestart(client3, msg) {
50933
51519
  const { agentName: agentName3 } = msg;
50934
- const turnInFlight = activeTurnStartedAt.size > 0;
51520
+ const turnInFlight = claudeBusyKeys.size > 0;
50935
51521
  if (!turnInFlight) {
50936
51522
  try {
50937
51523
  client3.send({
@@ -51098,6 +51684,8 @@ ${reminder}
51098
51684
  const promptKey = typeof msg.inbound.meta?.prompt_key === "string" ? msg.inbound.meta.prompt_key : "unknown";
51099
51685
  const source = typeof msg.inbound.meta?.source === "string" ? msg.inbound.meta.source : "unknown";
51100
51686
  const delivered = ipcServer.sendToAgent(msg.agentName, msg.inbound);
51687
+ if (delivered)
51688
+ markClaudeBusyForInbound(msg.inbound);
51101
51689
  process.stderr.write(`telegram gateway: inject_inbound agent=${msg.agentName} source=${source} prompt_key=${promptKey} delivered=${delivered}
51102
51690
  `);
51103
51691
  if (!delivered) {
@@ -51112,11 +51700,16 @@ if (!STATIC) {
51112
51700
  setInterval(() => {
51113
51701
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
51114
51702
  const r = idleDrainTick(pendingInboundBuffer, selfAgent, () => {
51115
- if (activeTurnStartedAt.size > 0)
51703
+ if (claudeBusyKeys.size > 0)
51116
51704
  return false;
51117
51705
  const c = ipcServer.getClient(selfAgent);
51118
51706
  return c != null && c.isAlive();
51119
- }, (m) => ipcServer.sendToAgent(selfAgent, m), inboundSpool);
51707
+ }, (m) => {
51708
+ const d = ipcServer.sendToAgent(selfAgent, m);
51709
+ if (d)
51710
+ markClaudeBusyForInbound(m);
51711
+ return d;
51712
+ }, inboundSpool);
51120
51713
  if (r != null && r.redelivered > 0) {
51121
51714
  process.stderr.write(`telegram gateway: idle-drain flushed ${r.redelivered}/${r.drained} buffered inbound for ${selfAgent}${r.rebuffered > 0 ? ` (${r.rebuffered} re-buffered)` : ""}
51122
51715
  `);
@@ -51395,7 +51988,7 @@ ${url}`;
51395
51988
  await deleteStalePreview(previewMessageId);
51396
51989
  previewMessageId = null;
51397
51990
  }
51398
- startTypingLoop(chat_id);
51991
+ startTypingLoop(chat_id, threadId ?? null);
51399
51992
  let silentAnchorEditDone = false;
51400
51993
  {
51401
51994
  const turn2 = currentTurn;
@@ -51464,7 +52057,7 @@ ${url}`;
51464
52057
  }
51465
52058
  }
51466
52059
  if (silentAnchorEditDone) {
51467
- stopTypingLoop(chat_id);
52060
+ stopTypingLoop(chat_id, threadId ?? null);
51468
52061
  return {
51469
52062
  content: [
51470
52063
  {
@@ -51558,7 +52151,7 @@ ${url}`;
51558
52151
  const msg = err instanceof Error ? err.message : String(err);
51559
52152
  throw new Error(`reply failed after ${sentIds.length} of ${chunks.length} chunk(s) sent: ${msg}`);
51560
52153
  } finally {
51561
- stopTypingLoop(chat_id);
52154
+ stopTypingLoop(chat_id, threadId ?? null);
51562
52155
  }
51563
52156
  if (replyButtonMeta != null && replyButtonMeta.size > 0 && sentIds.length >= chunks.length) {
51564
52157
  const keyboardMsgId = sentIds[chunks.length - 1];
@@ -52397,8 +52990,9 @@ async function executeSendTyping(args) {
52397
52990
  }
52398
52991
  action = rawAction;
52399
52992
  }
52400
- startTypingLoop(stChatId, action);
52401
- setTimeout(() => stopTypingLoop(stChatId), 30000);
52993
+ const stThreadId = resolveThreadId(stChatId, args.message_thread_id) ?? null;
52994
+ startTypingLoop(stChatId, stThreadId, action);
52995
+ setTimeout(() => stopTypingLoop(stChatId, stThreadId), 30000);
52402
52996
  for (const [key, ctrl] of activeStatusReactions.entries()) {
52403
52997
  if (key.startsWith(`${stChatId}:`))
52404
52998
  ctrl.setTool();
@@ -52535,7 +53129,7 @@ function resetOrphanedReplyTimeout() {
52535
53129
  }
52536
53130
  }
52537
53131
  function closeActivityLane(chatId, threadId) {
52538
- const key = chatKeyWithSuffix(chatId, threadId, "activity");
53132
+ const key = chatKeyWithSuffix2(chatId, threadId, "activity");
52539
53133
  const stream = activeDraftStreams.get(key);
52540
53134
  if (stream == null)
52541
53135
  return;
@@ -52544,7 +53138,7 @@ function closeActivityLane(chatId, threadId) {
52544
53138
  stream.finalize().catch(() => {});
52545
53139
  }
52546
53140
  function closeProgressLane(chatId, threadId) {
52547
- const prefix = chatKeyWithSuffix(chatId, threadId, "progress");
53141
+ const prefix = chatKeyWithSuffix2(chatId, threadId, "progress");
52548
53142
  for (const [key, stream] of activeDraftStreams) {
52549
53143
  if (key.startsWith(prefix)) {
52550
53144
  activeDraftStreams.delete(key);
@@ -52593,7 +53187,7 @@ function handleSessionEvent(ev) {
52593
53187
  clearSilentEndState(statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null));
52594
53188
  if (turnsDb != null) {
52595
53189
  const evThreadIdNum = ev.threadId != null ? Number(ev.threadId) : null;
52596
- const turnKey = chatKeyWithSuffix(ev.chatId, evThreadIdNum, String(startedAt));
53190
+ const turnKey = chatKeyWithSuffix2(ev.chatId, evThreadIdNum, String(startedAt));
52597
53191
  next.registryKey = turnKey;
52598
53192
  const userPromptPreview = extractUserPromptPreview(ev.rawContent);
52599
53193
  try {
@@ -52656,7 +53250,7 @@ function handleSessionEvent(ev) {
52656
53250
  return;
52657
53251
  ctrl.setTool(name);
52658
53252
  if (ev.toolUseId) {
52659
- typingWrapper.onToolUse(ev.toolUseId, turn.sessionChatId, name);
53253
+ typingWrapper.onToolUse(ev.toolUseId, turn.sessionChatId, name, turn.sessionThreadId ?? null);
52660
53254
  }
52661
53255
  return;
52662
53256
  }
@@ -52774,7 +53368,7 @@ function handleSessionEvent(ev) {
52774
53368
  return;
52775
53369
  if (!ev.toolUseId)
52776
53370
  return;
52777
- typingWrapper.onToolUse(ev.toolUseId, turn.sessionChatId, ev.toolName);
53371
+ typingWrapper.onToolUse(ev.toolUseId, turn.sessionChatId, ev.toolName, turn.sessionThreadId ?? null);
52778
53372
  return;
52779
53373
  }
52780
53374
  case "sub_agent_tool_result": {
@@ -52853,7 +53447,7 @@ function handleSessionEvent(ev) {
52853
53447
  if (flushDecision.kind === "skip" && flushDecision.reason === "silent-marker") {
52854
53448
  process.stderr.write(`telegram gateway: silent-turn-suppression: chat=${chatId} turnKey=${turn.startedAt} reason=silent-marker
52855
53449
  `);
52856
- const suppressPrefix = chatKeyWithSuffix(chatId, threadId, "progress");
53450
+ const suppressPrefix = chatKeyWithSuffix2(chatId, threadId, "progress");
52857
53451
  for (const [key] of activeDraftStreams) {
52858
53452
  if (key.startsWith(suppressPrefix)) {
52859
53453
  activeDraftStreams.delete(key);
@@ -53284,7 +53878,7 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
53284
53878
  if (!from)
53285
53879
  return;
53286
53880
  maybeEarlyAckReaction(ctx, from);
53287
- const key = inboundCoalesceKey(String(ctx.chat.id), String(from.id));
53881
+ const key = inboundCoalesceKey(String(ctx.chat.id), ctx.message?.message_thread_id, String(from.id));
53288
53882
  const result = inboundCoalescer.enqueue(key, { text, ctx, downloadImage, attachment });
53289
53883
  if (result.bypass)
53290
53884
  return handleInbound(ctx, text, undefined, undefined);
@@ -53339,7 +53933,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
53339
53933
  },
53340
53934
  at: Date.now()
53341
53935
  });
53342
- const turnInFlightAtReceipt = activeTurnStartedAt.size > 0;
53936
+ const turnInFlightAtReceipt = claudeBusyKeys.size > 0;
53343
53937
  const access = result.access;
53344
53938
  const from = ctx.from;
53345
53939
  const chat_id = String(ctx.chat.id);
@@ -53435,11 +54029,12 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
53435
54029
  }
53436
54030
  return;
53437
54031
  }
53438
- const pendingAdd = pendingAuthAddFlows.get(chat_id);
54032
+ const interceptKey = chatKey2(chat_id, messageThreadId);
54033
+ const pendingAdd = pendingAuthAddFlows.get(interceptKey);
53439
54034
  if (pendingAdd && looksLikeAuthCode(text)) {
53440
54035
  const elapsed = Date.now() - pendingAdd.startedAt;
53441
54036
  if (elapsed < REAUTH_INTERCEPT_TTL_MS) {
53442
- pendingAuthAddFlows.delete(chat_id);
54037
+ pendingAuthAddFlows.delete(interceptKey);
53443
54038
  try {
53444
54039
  const credentials = await submitAccountAuthCode(pendingAdd, text.trim());
53445
54040
  try {
@@ -53458,13 +54053,13 @@ The fleet's active account hasn't changed. Send <code>/auth use ${escapeHtmlForT
53458
54053
  return;
53459
54054
  }
53460
54055
  cancelAccountAuthSession(pendingAdd);
53461
- pendingAuthAddFlows.delete(chat_id);
54056
+ pendingAuthAddFlows.delete(interceptKey);
53462
54057
  }
53463
- const pendingReauth = pendingReauthFlows.get(chat_id);
54058
+ const pendingReauth = pendingReauthFlows.get(interceptKey);
53464
54059
  if (pendingReauth && looksLikeAuthCode(text)) {
53465
54060
  const elapsed = Date.now() - pendingReauth.startedAt;
53466
54061
  if (elapsed < REAUTH_INTERCEPT_TTL_MS) {
53467
- pendingReauthFlows.delete(chat_id);
54062
+ pendingReauthFlows.delete(interceptKey);
53468
54063
  const { result: result2, errorText } = execAuthCode(pendingReauth.agent, text.trim());
53469
54064
  if (errorText) {
53470
54065
  await switchroomReply(ctx, `<b>auth code failed:</b>
@@ -53483,7 +54078,7 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
53483
54078
  redactAuthCodeMessage(bot.api, chat_id, msgId ?? null, (line) => process.stderr.write(line));
53484
54079
  return;
53485
54080
  }
53486
- pendingReauthFlows.delete(chat_id);
54081
+ pendingReauthFlows.delete(interceptKey);
53487
54082
  }
53488
54083
  const pendingVault = pendingVaultOps.get(chat_id);
53489
54084
  if (pendingVault) {
@@ -53779,7 +54374,7 @@ ${preBlock(write.output)}`;
53779
54374
  reset(statusKey(chat_id, messageThreadId), Date.now());
53780
54375
  startTurn(statusKey(chat_id, messageThreadId), Date.now());
53781
54376
  clearPending(statusKey(chat_id, messageThreadId), "inbound");
53782
- startTurnTypingLoop(chat_id);
54377
+ startTurnTypingLoop(chat_id, messageThreadId ?? null);
53783
54378
  emitRuntimeMetric({
53784
54379
  kind: "turn_started",
53785
54380
  chat_id,
@@ -53911,6 +54506,8 @@ ${preBlock(write.output)}`;
53911
54506
  return;
53912
54507
  }
53913
54508
  const delivered = ipcServer.sendToAgent(selfAgent, inboundMsg);
54509
+ if (delivered)
54510
+ markClaudeBusyForInbound(inboundMsg);
53914
54511
  if (!delivered) {
53915
54512
  pendingInboundBuffer.push(selfAgent, inboundMsg);
53916
54513
  const threadOpts = messageThreadId != null ? { message_thread_id: messageThreadId } : {};
@@ -54061,9 +54658,10 @@ function resolveBootChatId(marker, ageMs) {
54061
54658
  ackMsgId: marker.ack_message_id ?? undefined
54062
54659
  };
54063
54660
  }
54661
+ const supergroupBootTopic = resolveAgentOutboundTopic({ kind: "boot" });
54064
54662
  const envChat = process.env.SUBAGENT_OWNER_CHAT_ID;
54065
54663
  if (envChat)
54066
- return { chatId: envChat, threadId: undefined, ackMsgId: undefined };
54664
+ return { chatId: envChat, threadId: supergroupBootTopic, ackMsgId: undefined };
54067
54665
  if (HISTORY_ENABLED) {
54068
54666
  try {
54069
54667
  const access = loadAccess();
@@ -54071,12 +54669,30 @@ function resolveBootChatId(marker, ageMs) {
54071
54669
  if (ownerChatId) {
54072
54670
  const recent = query({ chat_id: ownerChatId, limit: 1 });
54073
54671
  if (recent.length > 0)
54074
- return { chatId: ownerChatId, threadId: undefined, ackMsgId: undefined };
54672
+ return { chatId: ownerChatId, threadId: supergroupBootTopic, ackMsgId: undefined };
54075
54673
  }
54076
54674
  } catch {}
54077
54675
  }
54078
54676
  return null;
54079
54677
  }
54678
+ function resolveAgentOutboundTopic(event) {
54679
+ const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
54680
+ if (!agentName3)
54681
+ return;
54682
+ try {
54683
+ const cfg = loadConfig2();
54684
+ const rawAgent = cfg.agents?.[agentName3];
54685
+ if (!rawAgent)
54686
+ return;
54687
+ const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
54688
+ const tg = resolved.channels?.telegram;
54689
+ if (!tg)
54690
+ return;
54691
+ return resolveOutboundTopic(tg, event);
54692
+ } catch {
54693
+ return;
54694
+ }
54695
+ }
54080
54696
  function stampUserRestartReason(reason) {
54081
54697
  try {
54082
54698
  writeCleanShutdownMarker(GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH, {
@@ -55308,24 +55924,25 @@ bot.command("auth", async (ctx) => {
55308
55924
  Set <code>admin: true</code> on this agent in switchroom.yaml to unlock (the same flag that gates <code>/agents</code>, <code>/restart</code>, <code>/update</code> etc.).`, { html: true });
55309
55925
  return;
55310
55926
  }
55927
+ const authAddKey = chatKey2(chatId, ctx.message?.message_thread_id ?? null);
55311
55928
  if (parsed.kind === "cancel") {
55312
- const existing = pendingAuthAddFlows.get(chatId);
55929
+ const existing = pendingAuthAddFlows.get(authAddKey);
55313
55930
  if (!existing) {
55314
55931
  await switchroomReply(ctx, "<i>No pending <code>/auth add</code> flow in this chat.</i>", { html: true });
55315
55932
  return;
55316
55933
  }
55317
55934
  cancelAccountAuthSession(existing);
55318
- pendingAuthAddFlows.delete(chatId);
55935
+ pendingAuthAddFlows.delete(authAddKey);
55319
55936
  await switchroomReply(ctx, "Cancelled.", { html: true });
55320
55937
  return;
55321
55938
  }
55322
- if (pendingAuthAddFlows.has(chatId)) {
55939
+ if (pendingAuthAddFlows.has(authAddKey)) {
55323
55940
  await switchroomReply(ctx, "<i>An <code>/auth add</code> flow is already in progress for this chat. Finish the paste, or send <code>/auth cancel</code> to abort.</i>", { html: true });
55324
55941
  return;
55325
55942
  }
55326
55943
  try {
55327
55944
  const { loginUrl, scratchDir, child } = await startAccountAuthSession(parsed.label);
55328
- pendingAuthAddFlows.set(chatId, {
55945
+ pendingAuthAddFlows.set(authAddKey, {
55329
55946
  label: parsed.label,
55330
55947
  scratchDir,
55331
55948
  child,
@@ -55569,6 +56186,8 @@ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, atte
55569
56186
  operatorId: senderId
55570
56187
  });
55571
56188
  const delivered = ipcServer.sendToAgent(pending2.agent, synthetic);
56189
+ if (delivered)
56190
+ markClaudeBusyForInbound(synthetic);
55572
56191
  process.stderr.write(`telegram gateway: vault_grant_approved injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${delivered}
55573
56192
  `);
55574
56193
  if (!delivered) {
@@ -55615,6 +56234,8 @@ async function handleVaultRequestAccessCallback(ctx, data) {
55615
56234
  operatorId: senderId
55616
56235
  });
55617
56236
  const denyDelivered = ipcServer.sendToAgent(pending2.agent, denyInbound);
56237
+ if (denyDelivered)
56238
+ markClaudeBusyForInbound(denyInbound);
55618
56239
  process.stderr.write(`telegram gateway: vault_grant_denied injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${denyDelivered}
55619
56240
  `);
55620
56241
  if (!denyDelivered) {
@@ -55698,6 +56319,8 @@ async function handleVaultRequestSaveCallback(ctx, data) {
55698
56319
  operatorId: senderId
55699
56320
  });
55700
56321
  const dDelivered = ipcServer.sendToAgent(pending2.agent, discardInbound);
56322
+ if (dDelivered)
56323
+ markClaudeBusyForInbound(discardInbound);
55701
56324
  process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${dDelivered}
55702
56325
  `);
55703
56326
  if (!dDelivered)
@@ -55758,6 +56381,8 @@ async function handleVaultRequestSaveCallback(ctx, data) {
55758
56381
  reason: failReason
55759
56382
  });
55760
56383
  const fDelivered = ipcServer.sendToAgent(pending2.agent, failInbound);
56384
+ if (fDelivered)
56385
+ markClaudeBusyForInbound(failInbound);
55761
56386
  process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${fDelivered}
55762
56387
  `);
55763
56388
  if (!fDelivered)
@@ -55775,6 +56400,8 @@ async function handleVaultRequestSaveCallback(ctx, data) {
55775
56400
  operatorId: senderId
55776
56401
  });
55777
56402
  const okDelivered = ipcServer.sendToAgent(pending2.agent, okInbound);
56403
+ if (okDelivered)
56404
+ markClaudeBusyForInbound(okInbound);
55778
56405
  process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${okDelivered}
55779
56406
  `);
55780
56407
  if (!okDelivered)
@@ -56337,7 +56964,8 @@ async function handleOperatorEventCallback(ctx, data) {
56337
56964
  parseMode: "HTML",
56338
56965
  synthInbound: async () => {
56339
56966
  await runSwitchroomAuthCommand(ctx, ["auth", "reauth", agent], `auth reauth ${agent}`);
56340
- pendingReauthFlows.set(String(ctx.chat.id), { agent, startedAt: Date.now() });
56967
+ const reauthThreadId = ctx.callbackQuery?.message?.message_thread_id;
56968
+ pendingReauthFlows.set(chatKey2(String(ctx.chat.id), reauthThreadId ?? null), { agent, startedAt: Date.now() });
56341
56969
  }
56342
56970
  });
56343
56971
  return;
@@ -57216,6 +57844,8 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
57216
57844
  `);
57217
57845
  const selfAgentBtn = process.env.SWITCHROOM_AGENT_NAME ?? "";
57218
57846
  const btnDelivered = ipcServer.sendToAgent(selfAgentBtn, inboundMsg);
57847
+ if (btnDelivered)
57848
+ markClaudeBusyForInbound(inboundMsg);
57219
57849
  if (!btnDelivered) {
57220
57850
  pendingInboundBuffer.push(selfAgentBtn, inboundMsg);
57221
57851
  swallowingApiCall(() => bot.api.sendMessage(cbChatId, "\u23F3 Agent is restarting \u2014 your button tap is queued and will be processed when it comes back.", cbThreadId != null ? { message_thread_id: cbThreadId } : {}), {
@@ -57766,7 +58396,7 @@ function getReactionsConfig() {
57766
58396
  if (agentName3) {
57767
58397
  const rawAgent = cfg.agents?.[agentName3];
57768
58398
  if (rawAgent) {
57769
- const resolved = resolveAgentConfig(cfg.defaults, cfg.profiles, rawAgent);
58399
+ const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
57770
58400
  raw = resolved.reactions;
57771
58401
  }
57772
58402
  }
@@ -57812,6 +58442,8 @@ function flushReactionBatch(batch) {
57812
58442
  meta
57813
58443
  };
57814
58444
  const delivered = ipcServer.sendToAgent(agentName3, inbound);
58445
+ if (delivered)
58446
+ markClaudeBusyForInbound(inbound);
57815
58447
  process.stderr.write(`telegram gateway: reactions.dispatch agent=${agentName3} chat=${batch.chatId} count=${batch.reactions.length} batched=${batch.batched} delivered=${delivered}
57816
58448
  `);
57817
58449
  if (!delivered) {