switchroom 0.15.36 → 0.15.38

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.
Files changed (78) hide show
  1. package/dist/agent-scheduler/index.js +10 -9
  2. package/dist/auth-broker/index.js +9 -9
  3. package/dist/cli/autoaccept-poll.js +13 -7
  4. package/dist/cli/notion-write-pretool.mjs +9 -9
  5. package/dist/cli/switchroom.js +480 -217
  6. package/dist/cli/ui/index.html +87 -17
  7. package/dist/host-control/main.js +10 -10
  8. package/dist/vault/approvals/kernel-server.js +9 -9
  9. package/dist/vault/broker/server.js +9 -9
  10. package/package.json +1 -1
  11. package/profiles/_base/cron-session.sh.hbs +1 -1
  12. package/profiles/_base/start.sh.hbs +1 -1
  13. package/profiles/_shared/agent-self-service.md.hbs +25 -0
  14. package/skills/switchroom-manage/SKILL.md +1 -1
  15. package/skills/switchroom-runtime/SKILL.md +1 -1
  16. package/telegram-plugin/answer-stream.ts +1 -1
  17. package/telegram-plugin/bridge/bridge.ts +50 -1
  18. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  19. package/telegram-plugin/bridge/tool-filter.ts +77 -0
  20. package/telegram-plugin/chat-lock.ts +1 -1
  21. package/telegram-plugin/credits-watch.ts +1 -1
  22. package/telegram-plugin/dist/bridge/bridge.js +60 -3
  23. package/telegram-plugin/dist/gateway/gateway.js +753 -207
  24. package/telegram-plugin/dist/server.js +64 -4
  25. package/telegram-plugin/gateway/auto-classify-mid-turn.ts +1 -1
  26. package/telegram-plugin/gateway/boot-card.ts +5 -1
  27. package/telegram-plugin/gateway/boot-probes.ts +62 -0
  28. package/telegram-plugin/gateway/cron-session.ts +1 -1
  29. package/telegram-plugin/gateway/gateway.ts +254 -15
  30. package/telegram-plugin/gateway/grant-restart.ts +1 -1
  31. package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +1 -1
  32. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +1 -1
  33. package/telegram-plugin/gateway/inbound-delivery-machine.ts +1 -1
  34. package/telegram-plugin/gateway/interrupt-defer.ts +1 -1
  35. package/telegram-plugin/gateway/ipc-protocol.ts +12 -0
  36. package/telegram-plugin/gateway/linear-activity.ts +56 -0
  37. package/telegram-plugin/gateway/linear-auth-watch.ts +102 -0
  38. package/telegram-plugin/gateway/linear-setup.ts +196 -0
  39. package/telegram-plugin/gateway/permission-card-origin.ts +62 -0
  40. package/telegram-plugin/gateway/permission-timeout.ts +70 -0
  41. package/telegram-plugin/gateway/prefix-warmup.ts +1 -1
  42. package/telegram-plugin/gateway/webhook-ingest-server.test.ts +1 -1
  43. package/telegram-plugin/gateway/webhook-ingest-server.ts +1 -1
  44. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +1 -1
  45. package/telegram-plugin/interrupt-marker.ts +1 -1
  46. package/telegram-plugin/over-ping-safety-net.ts +1 -1
  47. package/telegram-plugin/scoped-approval.ts +1 -1
  48. package/telegram-plugin/secret-detect/vault-error.ts +1 -1
  49. package/telegram-plugin/silence-poke.ts +2 -2
  50. package/telegram-plugin/silent-reply-anchor.ts +1 -1
  51. package/telegram-plugin/slot-banner-driver.ts +1 -1
  52. package/telegram-plugin/startup-reset.ts +1 -1
  53. package/telegram-plugin/tests/boot-probes-connections.test.ts +66 -0
  54. package/telegram-plugin/tests/gateway-startup-reset.test.ts +1 -1
  55. package/telegram-plugin/tests/inbound-delivery-machine.test.ts +1 -1
  56. package/telegram-plugin/tests/linear-agent-activity.test.ts +77 -0
  57. package/telegram-plugin/tests/linear-agent-setup.test.ts +132 -0
  58. package/telegram-plugin/tests/linear-auth-watch.test.ts +79 -0
  59. package/telegram-plugin/tests/linear-create-issue.test.ts +3 -1
  60. package/telegram-plugin/tests/permission-card-origin.test.ts +97 -0
  61. package/telegram-plugin/tests/permission-card-routing.test.ts +23 -0
  62. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +76 -0
  63. package/telegram-plugin/tests/permission-timeout.test.ts +87 -0
  64. package/telegram-plugin/tests/scoped-approval.test.ts +1 -1
  65. package/telegram-plugin/tests/silence-poke.test.ts +1 -1
  66. package/telegram-plugin/tests/tool-filter.test.ts +87 -0
  67. package/telegram-plugin/tests/turn-flush-safety.test.ts +1 -1
  68. package/telegram-plugin/turn-flush-safety.ts +1 -1
  69. package/telegram-plugin/uat/assertions.ts +1 -1
  70. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +1 -1
  71. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +1 -1
  72. package/telegram-plugin/uat/scenarios/jtbd-fast-ack-dm.test.ts +1 -1
  73. package/telegram-plugin/uat/scenarios/jtbd-fast-trivial-dm.test.ts +2 -2
  74. package/telegram-plugin/uat/scenarios/jtbd-forwarded-burst-dm.test.ts +1 -1
  75. package/telegram-plugin/uat/scenarios/jtbd-memory-survives-restart-dm.test.ts +1 -1
  76. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +1 -1
  77. package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +1 -1
  78. package/telegram-plugin/uat/scenarios/jtbd-wake-audit-content-dm.test.ts +1 -1
@@ -23847,7 +23847,7 @@ var init_schema = __esm(() => {
23847
23847
  ScheduleEntrySchema = exports_external.object({
23848
23848
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
23849
23849
  prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
23850
- kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
23850
+ kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
23851
23851
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
23852
23852
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
23853
23853
  model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
@@ -23856,7 +23856,7 @@ var init_schema = __esm(() => {
23856
23856
  topic: exports_external.union([
23857
23857
  exports_external.string().min(1, "topic alias must be non-empty"),
23858
23858
  exports_external.number().int().positive("topic ID must be a positive integer")
23859
- ]).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.")
23859
+ ]).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 reference/rfcs/supergroup-mode.md.")
23860
23860
  }).superRefine((entry, ctx) => {
23861
23861
  const kind = entry.kind ?? "prompt";
23862
23862
  if (kind === "poll" && !entry.poll) {
@@ -24029,15 +24029,15 @@ var init_schema = __esm(() => {
24029
24029
  webhook_rate_limit: exports_external.object({
24030
24030
  rpm: exports_external.number().int().positive()
24031
24031
  }).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."),
24032
- webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
24033
- webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
24032
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See reference/rfcs/webhook-via-gateway-socket.md."),
24033
+ webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See reference/rfcs/webhook-cloudflare-edge-lock.md."),
24034
24034
  linear_agent: exports_external.object({
24035
24035
  enabled: exports_external.boolean(),
24036
24036
  token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
24037
24037
  workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
24038
24038
  default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
24039
24039
  }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
24040
- 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."),
24040
+ 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 reference/rfcs/supergroup-mode.md."),
24041
24041
  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. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "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`."),
24042
24042
  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.")
24043
24043
  }).optional().superRefine((tg, ctx) => {
@@ -24263,7 +24263,7 @@ var init_schema = __esm(() => {
24263
24263
  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."),
24264
24264
  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)."),
24265
24265
  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."),
24266
- notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
24266
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
24267
24267
  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({
24268
24268
  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."),
24269
24269
  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.")
@@ -24398,9 +24398,9 @@ var init_schema = __esm(() => {
24398
24398
  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."),
24399
24399
  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)."),
24400
24400
  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."),
24401
- notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
24401
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
24402
24402
  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."),
24403
- 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)."),
24403
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/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)."),
24404
24404
  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. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
24405
24405
  web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
24406
24406
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -24422,7 +24422,7 @@ var init_schema = __esm(() => {
24422
24422
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
24423
24423
  message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
24424
24424
  }), AgentSchema).describe("Map of agent name to agent configuration"),
24425
- cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
24425
+ cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
24426
24426
  });
24427
24427
  });
24428
24428
 
@@ -29729,6 +29729,33 @@ function renderBucketedSkills(switchroom, agent) {
29729
29729
  parts.push(`Agent: ${agent.join(", ")}`);
29730
29730
  return parts.length === 0 ? "none resolved" : parts.join(" \u00b7 ");
29731
29731
  }
29732
+ async function probeConnections(agentDir, opts = {}) {
29733
+ return withTimeout("Connections", (async () => {
29734
+ const path = join24(agentDir, ".claude", "connection-health.json");
29735
+ const read = opts.readFileImpl ?? ((p) => readFileSync25(p, "utf8"));
29736
+ let issues = [];
29737
+ try {
29738
+ const parsed = JSON.parse(read(path));
29739
+ issues = Array.isArray(parsed.issues) ? parsed.issues : [];
29740
+ } catch {
29741
+ return { status: "ok", label: "Connections", detail: "no issues" };
29742
+ }
29743
+ if (issues.length === 0) {
29744
+ return { status: "ok", label: "Connections", detail: "all authed" };
29745
+ }
29746
+ const servers = [...new Set(issues.map((i) => i.server))];
29747
+ const named = servers.slice(0, 4).join(", ");
29748
+ const more = servers.length > 4 ? ` +${servers.length - 4} more` : "";
29749
+ const first = issues[0];
29750
+ const extra = issues.length > 1 ? ` (+${issues.length - 1} more \u2014 run \`switchroom doctor\`)` : "";
29751
+ return {
29752
+ status: "degraded",
29753
+ label: "Connections",
29754
+ detail: `${servers.length} integration(s) configured but not authed: ${named}${more}`,
29755
+ nextStep: `${first.fix}${extra}`
29756
+ };
29757
+ })());
29758
+ }
29732
29759
  var execFile3, PROBE_TIMEOUT_MS = 2000, QUOTA_BROKER_TIMEOUT_MS = 7000, QUOTA_DIRECT_FALLBACK_TIMEOUT_MS = 5000, QUOTA_PROBE_OUTER_TIMEOUT_MS = 9000, TOKEN_EXPIRING_SOON_DAYS = 7, AGENT_RETRY_INTERVAL_MS = 1500, AGENT_RETRY_MAX_MS = 12000, AGENT_LIVE_WINDOW_MS = 45000, AGENT_LIVE_POLL_INTERVAL_MS = 2000, AGENT_LIVE_FOLLOWUP_REPOLL_MS = 30000, realProcFs, SCHEDULER_LOCK_PATH_DEFAULT = "/state/agent/scheduler.lock", SCHEDULER_JSONL_PATH_DEFAULT = "/state/agent/scheduler.jsonl", SCHEDULER_FRESH_BOOT_MS = 30000, realSchedulerFs, realSkillsFs;
29733
29760
  var init_boot_probes = __esm(() => {
29734
29761
  init_quota_cache();
@@ -30164,6 +30191,9 @@ async function runAllProbes(opts) {
30164
30191
  }),
30165
30192
  probeSkills(opts.agentDir, { agentName: opts.agentSlug ?? opts.agentName }).then((r) => {
30166
30193
  probes.skills = r;
30194
+ }),
30195
+ probeConnections(opts.agentDir).then((r) => {
30196
+ probes.connections = r;
30167
30197
  })
30168
30198
  ]);
30169
30199
  return probes;
@@ -30404,7 +30434,8 @@ var init_boot_card = __esm(() => {
30404
30434
  scheduler: "Scheduler",
30405
30435
  broker: "Broker",
30406
30436
  kernel: "Kernel",
30407
- skills: "Skills"
30437
+ skills: "Skills",
30438
+ connections: "Connections"
30408
30439
  };
30409
30440
  PROBE_KEYS = [
30410
30441
  "account",
@@ -30415,7 +30446,8 @@ var init_boot_card = __esm(() => {
30415
30446
  "scheduler",
30416
30447
  "broker",
30417
30448
  "kernel",
30418
- "skills"
30449
+ "skills",
30450
+ "connections"
30419
30451
  ];
30420
30452
  REASON_EMOJI = {
30421
30453
  planned: "\u2705",
@@ -32953,6 +32985,32 @@ function parseSourceMessageId(raw) {
32953
32985
  return n;
32954
32986
  }
32955
32987
 
32988
+ // gateway/permission-timeout.ts
32989
+ var SIGNATURE_SEP = String.fromCharCode(0);
32990
+ function permissionSignature(toolName, inputPreview) {
32991
+ return toolName + SIGNATURE_SEP + inputPreview;
32992
+ }
32993
+ function timeoutDenyMessage(timeoutMinutes) {
32994
+ return `No operator responded within ${timeoutMinutes} minutes, so this request timed out. ` + `This is a TIMEOUT, not a denial \u2014 the operator is likely away. ` + `Do NOT retry this exact action automatically. Tell the user it is still ` + `awaiting their approval, then continue with other work or stop.`;
32995
+ }
32996
+ var duplicateDenyMessage = `This exact action already timed out awaiting the operator, and they have not ` + `responded since. Do NOT keep re-requesting it \u2014 tell the user it needs their ` + `approval when they are back, and move on to other work or stop.`;
32997
+ function isRecentTimeoutDuplicate(timeouts, sig, now, windowMs) {
32998
+ const at = timeouts.get(sig);
32999
+ return at != null && now - at <= windowMs;
33000
+ }
33001
+
33002
+ // gateway/permission-card-origin.ts
33003
+ function pickRecoveredPermissionOrigin(recentTurns, now, maxAgeMs) {
33004
+ let best = null;
33005
+ for (const t of recentTurns) {
33006
+ if (now - t.startedAt > maxAgeMs)
33007
+ continue;
33008
+ if (best == null || t.startedAt >= best.startedAt)
33009
+ best = t;
33010
+ }
33011
+ return best == null ? null : { chatId: best.sessionChatId, threadId: best.sessionThreadId };
33012
+ }
33013
+
32956
33014
  // tool-names.ts
32957
33015
  var TELEGRAM_TOOL_PREFIX_RE = /^mcp__[^_].*?telegram__/;
32958
33016
  function stripPrefix(toolName) {
@@ -46206,6 +46264,7 @@ function resolveOutboundTopic(config, event) {
46206
46264
  }
46207
46265
  case "boot":
46208
46266
  case "compact-watchdog":
46267
+ case "linear-auth":
46209
46268
  if (!inSupergroupMode)
46210
46269
  return;
46211
46270
  return aliasToId(cfg, ALERTS_ALIAS) ?? cfg.default_topic_id;
@@ -54460,11 +54519,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54460
54519
  }
54461
54520
 
54462
54521
  // ../src/build-info.ts
54463
- var VERSION = "0.15.36";
54464
- var COMMIT_SHA = "18736aec";
54465
- var COMMIT_DATE = "2026-06-16T09:16:32Z";
54466
- var LATEST_PR = 2395;
54467
- var COMMITS_AHEAD_OF_TAG = 0;
54522
+ var VERSION = "0.15.38";
54523
+ var COMMIT_SHA = "d28a331f";
54524
+ var COMMIT_DATE = "2026-06-18T11:43:40+10:00";
54525
+ var LATEST_PR = null;
54526
+ var COMMITS_AHEAD_OF_TAG = 13;
54468
54527
 
54469
54528
  // gateway/boot-version.ts
54470
54529
  function formatRelativeAgo(iso) {
@@ -54759,7 +54818,79 @@ init_client2();
54759
54818
 
54760
54819
  // ../src/linear/oauth-refresh.ts
54761
54820
  var LINEAR_TOKEN_ENDPOINT = "https://api.linear.app/oauth/token";
54821
+ var LINEAR_AUTHORIZE_ENDPOINT = "https://linear.app/oauth/authorize";
54822
+ var LINEAR_AGENT_SCOPES = [
54823
+ "read",
54824
+ "write",
54825
+ "app:assignable",
54826
+ "app:mentionable"
54827
+ ];
54762
54828
  var DEFAULT_REFRESH_SKEW_SEC = 2 * 3600;
54829
+ function buildLinearAuthorizeUrl(args) {
54830
+ const params = new URLSearchParams({
54831
+ client_id: args.clientId,
54832
+ redirect_uri: args.redirectUri,
54833
+ response_type: "code",
54834
+ scope: (args.scopes ?? LINEAR_AGENT_SCOPES).join(","),
54835
+ actor: "app",
54836
+ prompt: "consent"
54837
+ });
54838
+ if (args.state)
54839
+ params.set("state", args.state);
54840
+ return `${LINEAR_AUTHORIZE_ENDPOINT}?${params.toString()}`;
54841
+ }
54842
+ async function exchangeLinearAuthCode(args, opts = {}) {
54843
+ const fetchImpl = opts.fetchImpl ?? fetch;
54844
+ const nowSec = opts.nowSec ?? (() => Math.floor(Date.now() / 1000));
54845
+ const form = new URLSearchParams({
54846
+ grant_type: "authorization_code",
54847
+ code: args.code,
54848
+ client_id: args.clientId,
54849
+ client_secret: args.clientSecret,
54850
+ redirect_uri: args.redirectUri
54851
+ });
54852
+ let resp;
54853
+ try {
54854
+ resp = await fetchImpl(LINEAR_TOKEN_ENDPOINT, {
54855
+ method: "POST",
54856
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
54857
+ body: form.toString()
54858
+ });
54859
+ } catch (err) {
54860
+ return { ok: false, reason: "network", detail: err.message };
54861
+ }
54862
+ if (!resp.ok) {
54863
+ const txt = await resp.text().catch(() => "");
54864
+ const badCode = resp.status === 400 || /invalid_grant|invalid_request/i.test(txt);
54865
+ return {
54866
+ ok: false,
54867
+ reason: badCode ? "bad_code" : "http_error",
54868
+ detail: `HTTP ${resp.status}${txt ? ` ${txt.slice(0, 200)}` : ""}`
54869
+ };
54870
+ }
54871
+ let json;
54872
+ try {
54873
+ json = await resp.json();
54874
+ } catch {
54875
+ return { ok: false, reason: "bad_response", detail: "non-JSON token response" };
54876
+ }
54877
+ const accessToken = json.access_token;
54878
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
54879
+ return { ok: false, reason: "bad_response", detail: "no access_token in response" };
54880
+ }
54881
+ const refreshToken = json.refresh_token;
54882
+ if (typeof refreshToken !== "string" || refreshToken.length === 0) {
54883
+ return { ok: false, reason: "bad_response", detail: "no refresh_token in response (was actor=app + a fresh consent used?)" };
54884
+ }
54885
+ const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 86400;
54886
+ return {
54887
+ ok: true,
54888
+ accessToken,
54889
+ refreshToken,
54890
+ expiresAt: nowSec() + expiresIn,
54891
+ ...typeof json.scope === "string" ? { scope: json.scope } : {}
54892
+ };
54893
+ }
54763
54894
  async function refreshLinearAppToken(bundle, opts = {}) {
54764
54895
  const fetchImpl = opts.fetchImpl ?? fetch;
54765
54896
  const nowSec = opts.nowSec ?? (() => Math.floor(Date.now() / 1000));
@@ -54808,6 +54939,11 @@ async function refreshLinearAppToken(bundle, opts = {}) {
54808
54939
  ...typeof json.scope === "string" ? { scope: json.scope } : {}
54809
54940
  };
54810
54941
  }
54942
+ function needsRefresh(expiresAt, nowSec, skewSec = DEFAULT_REFRESH_SKEW_SEC) {
54943
+ if (expiresAt == null)
54944
+ return false;
54945
+ return nowSec >= expiresAt - skewSec;
54946
+ }
54811
54947
  function parseBundle(raw) {
54812
54948
  if (raw == null || raw === "")
54813
54949
  return null;
@@ -54863,6 +54999,17 @@ async function performLinearRefresh(io) {
54863
54999
 
54864
55000
  // gateway/linear-activity.ts
54865
55001
  var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
55002
+ function escapeHtmlMin(s) {
55003
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
55004
+ }
55005
+ function buildLinearAuthDeadMessage(agent, reason) {
55006
+ const a = escapeHtmlMin(agent);
55007
+ const why = reason === "no_bundle" ? `no refresh credentials are stored (<code>linear/${a}/oauth</code> is missing), so its daily-expiring token can't renew` : `its Linear refresh token was revoked`;
55008
+ return `\uD83D\uDD11 <b>Linear auth needs you</b>
55009
+ ` + `<b>${a}</b> can't reach Linear \u2014 ${why}. ` + `Its access token will keep failing until you re-authorize.
55010
+
55011
+ ` + `Re-auth (actor=app) then run <code>switchroom linear-agent setup --agent ${a} ` + `--token \u2026 --refresh-token \u2026 --client-id \u2026 --client-secret \u2026</code> on the host, ` + `or ask me to walk you through it.`;
55012
+ }
54866
55013
  async function defaultResolveLinearToken(agent) {
54867
55014
  const key = `linear/${agent}/token`;
54868
55015
  const token = readVaultTokenFile(agent) ?? undefined;
@@ -54899,7 +55046,7 @@ function brokerRefreshIO(agent, fetchImpl) {
54899
55046
  ...fetchImpl ? { fetchImpl } : {}
54900
55047
  };
54901
55048
  }
54902
- async function linearPostWithRefresh(body, token, agent, fetchImpl, log, refreshIO) {
55049
+ async function linearPostWithRefresh(body, token, agent, fetchImpl, log, refreshIO, onAuthUnrecoverable) {
54903
55050
  const post = (t) => fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
54904
55051
  method: "POST",
54905
55052
  headers: { "Content-Type": "application/json", Authorization: t },
@@ -54914,6 +55061,11 @@ async function linearPostWithRefresh(body, token, agent, fetchImpl, log, refresh
54914
55061
  if (refreshed.reason === "revoked") {
54915
55062
  log(`telegram gateway: linear token REVOKED agent=${agent} \u2014 refresh token is dead; ` + `operator must re-authorize (linear-agent setup --refresh-token \u2026)
54916
55063
  `);
55064
+ onAuthUnrecoverable?.({ agent, reason: "revoked", detail: refreshed.detail });
55065
+ } else if (refreshed.reason === "no_bundle") {
55066
+ log(`telegram gateway: linear token DEAD agent=${agent} \u2014 no refresh bundle stored ` + `(linear/${agent}/oauth absent); operator must re-authorize
55067
+ `);
55068
+ onAuthUnrecoverable?.({ agent, reason: "no_bundle", detail: refreshed.detail });
54917
55069
  } else {
54918
55070
  log(`telegram gateway: linear token refresh failed agent=${agent} reason=${refreshed.reason}
54919
55071
  `);
@@ -54969,7 +55121,7 @@ async function emitLinearAgentActivity(args, deps = {}) {
54969
55121
  const fetchImpl = deps.fetchImpl ?? fetch;
54970
55122
  let resp;
54971
55123
  try {
54972
- ({ resp } = await linearPostWithRefresh(JSON.stringify({ query: mutation, variables }), tokenResult.token, agent, fetchImpl, log, deps.refreshIO));
55124
+ ({ resp } = await linearPostWithRefresh(JSON.stringify({ query: mutation, variables }), tokenResult.token, agent, fetchImpl, log, deps.refreshIO, deps.onAuthUnrecoverable));
54973
55125
  } catch (err) {
54974
55126
  return {
54975
55127
  content: [{ type: "text", text: `linear_agent_activity failed: request error: ${err.message}` }]
@@ -55033,7 +55185,7 @@ async function createLinearIssue(args, deps = {}) {
55033
55185
  const gql = async (query2, variables) => {
55034
55186
  let resp;
55035
55187
  try {
55036
- const out = await linearPostWithRefresh(JSON.stringify({ query: query2, variables }), activeToken, agent, fetchImpl, log, deps.refreshIO);
55188
+ const out = await linearPostWithRefresh(JSON.stringify({ query: query2, variables }), activeToken, agent, fetchImpl, log, deps.refreshIO, deps.onAuthUnrecoverable);
55037
55189
  resp = out.resp;
55038
55190
  activeToken = out.token;
55039
55191
  } catch (err) {
@@ -55102,6 +55254,261 @@ async function createLinearIssue(args, deps = {}) {
55102
55254
  return { content: [{ type: "text", text: `Filed: ${title} \u2192 ${issue.url}` }] };
55103
55255
  }
55104
55256
 
55257
+ // gateway/linear-setup.ts
55258
+ init_client2();
55259
+ var tokenKey = (agent) => `linear/${agent}/token`;
55260
+ var bundleKey = (agent) => `linear/${agent}/oauth`;
55261
+ function defaultPut(agent, key, value) {
55262
+ const token = readVaultTokenFile(agent) ?? undefined;
55263
+ const opt = token ? { token } : {};
55264
+ return putViaBroker(key, { kind: "string", value }, opt).then((r) => {
55265
+ if (r.kind === "ok")
55266
+ return { kind: "ok" };
55267
+ if (r.kind === "unreachable")
55268
+ return { kind: "unreachable", msg: r.msg };
55269
+ if (r.kind === "not_found")
55270
+ return { kind: "not_found", msg: r.msg };
55271
+ return { kind: "denied", msg: r.msg };
55272
+ });
55273
+ }
55274
+ function text(s) {
55275
+ return { content: [{ type: "text", text: s }] };
55276
+ }
55277
+ function writeGrantGuidance(agent) {
55278
+ return `I need write access to store the Linear credentials. Call:
55279
+ ` + `\u2022 vault_request_access(key: "${tokenKey(agent)}", scope: "write", reason: "store Linear app access token")
55280
+ ` + `\u2022 vault_request_access(key: "${bundleKey(agent)}", scope: "write", reason: "store Linear OAuth refresh bundle")
55281
+ ` + `Once the operator approves both, re-run linear_agent_setup with action "complete" (same code is single-use \u2014 if it expired, re-open the authorize URL first).`;
55282
+ }
55283
+ function durableConfigGuidance(agent) {
55284
+ return `Stored. To make this durable (survive restarts + enable auto-refresh), propose a config edit ` + `(config_propose_edit) that, under agents.${agent}:
55285
+ ` + ` \u2022 adds channels.telegram.linear_agent: { enabled: true, token: "vault:${tokenKey(agent)}" }
55286
+ ` + ` \u2022 adds "${tokenKey(agent)}" and "${bundleKey(agent)}" to secrets[]
55287
+ ` + `Then the operator approves it and you restart to pick up the linear_agent block.`;
55288
+ }
55289
+ async function runLinearAgentSetup(args, deps = {}) {
55290
+ const log = deps.log ?? ((s) => process.stderr.write(s));
55291
+ const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? "-";
55292
+ if (agent === "-" || !/^[a-z][a-z0-9_-]{0,63}$/.test(agent)) {
55293
+ return text(`linear_agent_setup failed: could not resolve a valid agent name (got '${agent}').`);
55294
+ }
55295
+ const action = args.action;
55296
+ if (action !== "authorize_url" && action !== "complete") {
55297
+ return text(`linear_agent_setup failed: action must be "authorize_url" or "complete".`);
55298
+ }
55299
+ const clientId = args.client_id?.trim();
55300
+ const redirectUri = args.redirect_uri?.trim();
55301
+ if (!clientId)
55302
+ return text("linear_agent_setup failed: client_id is required.");
55303
+ if (!redirectUri || !/^https?:\/\//.test(redirectUri)) {
55304
+ return text("linear_agent_setup failed: redirect_uri is required and must be an http(s) URL registered on the Linear OAuth app.");
55305
+ }
55306
+ if (action === "authorize_url") {
55307
+ const url = buildLinearAuthorizeUrl({ clientId, redirectUri });
55308
+ return text(`Open this URL in a browser to authorize <b>${agent}</b> as a Linear app actor (actor=app):
55309
+
55310
+ ${url}
55311
+
55312
+ ` + `After you approve, Linear redirects to ${redirectUri}?code=\u2026 (it may show a blank/error page \u2014 that's fine). ` + `Copy the code value from the URL bar, then run linear_agent_setup with action "complete", the same client_id + redirect_uri, ` + `your client_secret, and that code.`);
55313
+ }
55314
+ const clientSecret = args.client_secret?.trim();
55315
+ const code = args.code?.trim();
55316
+ if (!clientSecret)
55317
+ return text('linear_agent_setup failed: client_secret is required for action "complete".');
55318
+ if (!code)
55319
+ return text('linear_agent_setup failed: code (from the redirect URL) is required for action "complete".');
55320
+ const exchanged = await exchangeLinearAuthCode({ clientId, clientSecret, code, redirectUri }, deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {});
55321
+ if (!exchanged.ok) {
55322
+ log(`telegram gateway: linear_agent_setup exchange failed agent=${agent} reason=${exchanged.reason}
55323
+ `);
55324
+ if (exchanged.reason === "bad_code") {
55325
+ return text(`linear_agent_setup failed: Linear rejected the authorization code (expired, already used, or wrong redirect_uri). ` + `Re-run action "authorize_url", open the fresh URL, and copy a new code.`);
55326
+ }
55327
+ return text(`linear_agent_setup failed: token exchange ${exchanged.reason} \u2014 ${exchanged.detail}. Retry shortly.`);
55328
+ }
55329
+ const bundle = serializeBundle({
55330
+ clientId,
55331
+ clientSecret,
55332
+ refreshToken: exchanged.refreshToken,
55333
+ expiresAt: exchanged.expiresAt
55334
+ });
55335
+ const putBundle = deps.putBundle ?? ((a, j) => defaultPut(a, bundleKey(a), j));
55336
+ const putToken = deps.putToken ?? ((a, t2) => defaultPut(a, tokenKey(a), t2));
55337
+ const b = await putBundle(agent, bundle);
55338
+ if (b.kind !== "ok") {
55339
+ if (b.kind === "not_found" || b.kind === "denied") {
55340
+ return text(writeGrantGuidance(agent));
55341
+ }
55342
+ log(`telegram gateway: linear_agent_setup bundle write ${b.kind} agent=${agent}
55343
+ `);
55344
+ return text(`linear_agent_setup failed: couldn't store the refresh bundle (broker ${b.kind}: ${b.msg}).`);
55345
+ }
55346
+ const t = await putToken(agent, exchanged.accessToken);
55347
+ if (t.kind !== "ok") {
55348
+ if (t.kind === "not_found" || t.kind === "denied") {
55349
+ return text(writeGrantGuidance(agent));
55350
+ }
55351
+ log(`telegram gateway: linear_agent_setup token write ${t.kind} agent=${agent}
55352
+ `);
55353
+ return text(`linear_agent_setup failed: couldn't store the access token (broker ${t.kind}: ${t.msg}).`);
55354
+ }
55355
+ const hours = Math.max(1, Math.round((exchanged.expiresAt - Date.now() / 1000) / 3600));
55356
+ log(`telegram gateway: linear_agent_setup stored token+bundle agent=${agent} (expires ~${hours}h)
55357
+ `);
55358
+ return text(`\u2705 Linear app token + refresh bundle stored for ${agent} (access token expires in ~${hours}h; it now auto-renews).
55359
+
55360
+ ` + durableConfigGuidance(agent));
55361
+ }
55362
+
55363
+ // gateway/linear-auth-watch.ts
55364
+ async function runLinearAuthCheck(deps) {
55365
+ const log = deps.log ?? (() => {});
55366
+ if (!deps.linearEnabled())
55367
+ return "disabled";
55368
+ let raw;
55369
+ try {
55370
+ raw = await deps.readBundle();
55371
+ } catch (err) {
55372
+ log(`telegram gateway: linear-auth-watch agent=${deps.agent} bundle read error: ${err.message}
55373
+ `);
55374
+ return "refresh_failed";
55375
+ }
55376
+ const bundle = parseBundle(raw);
55377
+ if (!bundle) {
55378
+ log(`telegram gateway: linear-auth-watch agent=${deps.agent} \u2014 no refresh bundle (proactive)
55379
+ `);
55380
+ deps.onAuthDead({ agent: deps.agent, reason: "no_bundle", detail: "proactive watch: linear/<agent>/oauth missing or invalid" });
55381
+ return "no_bundle";
55382
+ }
55383
+ const now = deps.nowSec ? deps.nowSec() : Math.floor(Date.now() / 1000);
55384
+ if (!needsRefresh(bundle.expiresAt, now)) {
55385
+ return "fresh";
55386
+ }
55387
+ const res = await deps.refresh();
55388
+ if (res.ok) {
55389
+ log(`telegram gateway: linear-auth-watch agent=${deps.agent} proactively refreshed (was near expiry)
55390
+ `);
55391
+ return "refreshed";
55392
+ }
55393
+ if (res.reason === "revoked") {
55394
+ log(`telegram gateway: linear-auth-watch agent=${deps.agent} refresh REVOKED (proactive)
55395
+ `);
55396
+ deps.onAuthDead({ agent: deps.agent, reason: "revoked", detail: res.detail });
55397
+ return "revoked";
55398
+ }
55399
+ if (res.reason === "no_bundle") {
55400
+ deps.onAuthDead({ agent: deps.agent, reason: "no_bundle", detail: res.detail });
55401
+ return "no_bundle";
55402
+ }
55403
+ log(`telegram gateway: linear-auth-watch agent=${deps.agent} proactive refresh failed reason=${res.reason}
55404
+ `);
55405
+ return "refresh_failed";
55406
+ }
55407
+
55408
+ // ../src/linear/oauth-refresh.ts
55409
+ var LINEAR_TOKEN_ENDPOINT2 = "https://api.linear.app/oauth/token";
55410
+ var DEFAULT_REFRESH_SKEW_SEC2 = 2 * 3600;
55411
+ async function refreshLinearAppToken2(bundle, opts = {}) {
55412
+ const fetchImpl = opts.fetchImpl ?? fetch;
55413
+ const nowSec = opts.nowSec ?? (() => Math.floor(Date.now() / 1000));
55414
+ const form = new URLSearchParams({
55415
+ grant_type: "refresh_token",
55416
+ refresh_token: bundle.refreshToken,
55417
+ client_id: bundle.clientId,
55418
+ client_secret: bundle.clientSecret
55419
+ });
55420
+ let resp;
55421
+ try {
55422
+ resp = await fetchImpl(LINEAR_TOKEN_ENDPOINT2, {
55423
+ method: "POST",
55424
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
55425
+ body: form.toString()
55426
+ });
55427
+ } catch (err) {
55428
+ return { ok: false, reason: "network", detail: err.message };
55429
+ }
55430
+ if (!resp.ok) {
55431
+ const txt = await resp.text().catch(() => "");
55432
+ const revoked = resp.status === 400 || /invalid_grant|invalid_token/i.test(txt);
55433
+ return {
55434
+ ok: false,
55435
+ reason: revoked ? "revoked" : "http_error",
55436
+ detail: `HTTP ${resp.status}${txt ? ` ${txt.slice(0, 200)}` : ""}`
55437
+ };
55438
+ }
55439
+ let json;
55440
+ try {
55441
+ json = await resp.json();
55442
+ } catch {
55443
+ return { ok: false, reason: "bad_response", detail: "non-JSON token response" };
55444
+ }
55445
+ const accessToken = json.access_token;
55446
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
55447
+ return { ok: false, reason: "bad_response", detail: "no access_token in response" };
55448
+ }
55449
+ const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 86400;
55450
+ const rotated = typeof json.refresh_token === "string" && json.refresh_token.length > 0 ? json.refresh_token : bundle.refreshToken;
55451
+ return {
55452
+ ok: true,
55453
+ accessToken,
55454
+ refreshToken: rotated,
55455
+ expiresAt: nowSec() + expiresIn,
55456
+ ...typeof json.scope === "string" ? { scope: json.scope } : {}
55457
+ };
55458
+ }
55459
+ function parseBundle2(raw) {
55460
+ if (raw == null || raw === "")
55461
+ return null;
55462
+ let o;
55463
+ try {
55464
+ o = JSON.parse(raw);
55465
+ } catch {
55466
+ return null;
55467
+ }
55468
+ if (typeof o.client_id === "string" && typeof o.client_secret === "string" && typeof o.refresh_token === "string" && o.client_id.length > 0 && o.client_secret.length > 0 && o.refresh_token.length > 0) {
55469
+ return {
55470
+ clientId: o.client_id,
55471
+ clientSecret: o.client_secret,
55472
+ refreshToken: o.refresh_token,
55473
+ ...typeof o.expires_at === "number" ? { expiresAt: o.expires_at } : {}
55474
+ };
55475
+ }
55476
+ return null;
55477
+ }
55478
+ function serializeBundle2(b) {
55479
+ return JSON.stringify({
55480
+ client_id: b.clientId,
55481
+ client_secret: b.clientSecret,
55482
+ refresh_token: b.refreshToken,
55483
+ ...b.expiresAt != null ? { expires_at: b.expiresAt } : {}
55484
+ });
55485
+ }
55486
+ async function performLinearRefresh2(io) {
55487
+ const raw = await io.readBundle();
55488
+ const bundle = parseBundle2(raw);
55489
+ if (!bundle) {
55490
+ return { ok: false, reason: "no_bundle", detail: "no/invalid refresh bundle" };
55491
+ }
55492
+ const res = await refreshLinearAppToken2(bundle, {
55493
+ ...io.fetchImpl ? { fetchImpl: io.fetchImpl } : {},
55494
+ ...io.nowSec ? { nowSec: io.nowSec } : {}
55495
+ });
55496
+ if (!res.ok)
55497
+ return { ok: false, reason: res.reason, detail: res.detail };
55498
+ try {
55499
+ await io.writeBundle(serializeBundle2({
55500
+ clientId: bundle.clientId,
55501
+ clientSecret: bundle.clientSecret,
55502
+ refreshToken: res.refreshToken,
55503
+ expiresAt: res.expiresAt
55504
+ }));
55505
+ await io.writeToken(res.accessToken);
55506
+ } catch (err) {
55507
+ return { ok: false, reason: "persist_failed", detail: err.message };
55508
+ }
55509
+ return { ok: true, accessToken: res.accessToken, expiresAt: res.expiresAt };
55510
+ }
55511
+
55105
55512
  // vault-approval-posture.ts
55106
55513
  function resolveVaultApprovalPosture(broker) {
55107
55514
  if (broker?.approvalAuth === "telegram-id") {
@@ -55637,10 +56044,10 @@ var GRAMMY_VERSION = (() => {
55637
56044
  return "unknown";
55638
56045
  }
55639
56046
  })();
55640
- var sendMessageDraftFn = !DRAFT_ANSWER_LANE_RETIRED && typeof _rawSendMessageDraft === "function" ? (chatId, draftId, text, params) => _rawSendMessageDraft({
56047
+ var sendMessageDraftFn = !DRAFT_ANSWER_LANE_RETIRED && typeof _rawSendMessageDraft === "function" ? (chatId, draftId, text2, params) => _rawSendMessageDraft({
55641
56048
  chat_id: Number(chatId),
55642
56049
  draft_id: draftId,
55643
- text,
56050
+ text: text2,
55644
56051
  ...params ?? {}
55645
56052
  }) : undefined;
55646
56053
  var _rawSendChecklist = bot.api.raw.sendChecklist;
@@ -56262,9 +56669,9 @@ function postQueuedStatus(chatId, bufferedThread, inFlightThread) {
56262
56669
  if (queuedStatusMsgIds.has(key))
56263
56670
  return;
56264
56671
  const otherTopic = inFlightThread != null ? `another topic` : `another conversation`;
56265
- const text = `\u23F3 Queued \u2014 replying in ${otherTopic} first, then I'll get to this.`;
56672
+ const text2 = `\u23F3 Queued \u2014 replying in ${otherTopic} first, then I'll get to this.`;
56266
56673
  (async () => {
56267
- const sent = await swallowingApiCall(() => bot.api.sendMessage(chatId, text, { message_thread_id: bufferedThread }), { chat_id: chatId, verb: "queued-status.post", threadId: bufferedThread });
56674
+ const sent = await swallowingApiCall(() => bot.api.sendMessage(chatId, text2, { message_thread_id: bufferedThread }), { chat_id: chatId, verb: "queued-status.post", threadId: bufferedThread });
56268
56675
  const messageId = sent?.message_id;
56269
56676
  if (typeof messageId !== "number")
56270
56677
  return;
@@ -56558,9 +56965,9 @@ async function postCompactCard(occ, cap) {
56558
56965
  resolvedTopic: resolveAgentOutboundTopic({ kind: "compact-watchdog" }) ?? chatThreadMap.get(chatId),
56559
56966
  supergroupChatId: resolveAgentSupergroupChatId()
56560
56967
  });
56561
- const text = `\uD83D\uDDDC\uFE0F <b>Context compaction</b>
56968
+ const text2 = `\uD83D\uDDDC\uFE0F <b>Context compaction</b>
56562
56969
  ` + `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).`;
56563
- const sent = await swallowingApiCall(() => bot.api.sendMessage(chatId, text, {
56970
+ const sent = await swallowingApiCall(() => bot.api.sendMessage(chatId, text2, {
56564
56971
  parse_mode: "HTML",
56565
56972
  ...threadId != null ? { message_thread_id: threadId } : {}
56566
56973
  }), { chat_id: chatId, verb: "proactiveCompact.start" });
@@ -56592,19 +56999,19 @@ async function resolveCompactCard(kind, occNow) {
56592
56999
  clearTimeout(card.timer);
56593
57000
  if (kind === "timeout")
56594
57001
  compactNotifyState = idleCompactNotifyState();
56595
- let text;
57002
+ let text2;
56596
57003
  if (kind === "finished") {
56597
- text = `\u2705 <b>Context compacted</b>
57004
+ text2 = `\u2705 <b>Context compacted</b>
56598
57005
  ` + `Working context reduced` + (occNow != null ? ` (~${card.occAtStart.toLocaleString()} \u2192 ` + `~${occNow.toLocaleString()} tokens)` : "") + `. Hindsight retains the detail.`;
56599
57006
  } else if (kind === "superseded") {
56600
- text = `\u21A9\uFE0F <b>Context compaction superseded</b>
57007
+ text2 = `\u21A9\uFE0F <b>Context compaction superseded</b>
56601
57008
  ` + `A newer compaction started before this one confirmed.`;
56602
57009
  } else {
56603
- text = `\u26A0\uFE0F <b>Compaction issued</b>
57010
+ text2 = `\u26A0\uFE0F <b>Compaction issued</b>
56604
57011
  ` + `<code>/compact</code> was requested but the context isn't confirmed reduced yet. Native compaction and Hindsight still apply.`;
56605
57012
  }
56606
57013
  try {
56607
- await swallowingApiCall(() => bot.api.editMessageText(card.chatId, card.messageId, text, {
57014
+ await swallowingApiCall(() => bot.api.editMessageText(card.chatId, card.messageId, text2, {
56608
57015
  parse_mode: "HTML"
56609
57016
  }), { chat_id: card.chatId, verb: `proactiveCompact.${kind}` });
56610
57017
  } catch (err) {
@@ -56648,6 +57055,14 @@ function resolvePermissionCardTargets() {
56648
57055
  if (turn != null) {
56649
57056
  return [{ chatId: turn.sessionChatId, threadId: turn.sessionThreadId }];
56650
57057
  }
57058
+ if (PERMISSION_CARD_ORIGIN_RECOVERY_ENABLED) {
57059
+ const recovered = pickRecoveredPermissionOrigin(recentTurnsById.values(), Date.now(), PERMISSION_CARD_ORIGIN_MAX_AGE_MS);
57060
+ if (recovered != null) {
57061
+ process.stderr.write(`telegram gateway: permission-card origin recovered from recent turn chat=${recovered.chatId} thread=${recovered.threadId ?? "-"} ` + `(currentTurn was null \u2014 force-closed turn)
57062
+ `);
57063
+ return [recovered];
57064
+ }
57065
+ }
56651
57066
  const sg = resolveAgentSupergroupChatId();
56652
57067
  const topic = resolveAgentOutboundTopic({
56653
57068
  kind: "permission",
@@ -56662,7 +57077,7 @@ function resolvePermissionCardTargets() {
56662
57077
  function postPermissionResumeMessage(opts) {
56663
57078
  if (process.env.SWITCHROOM_RESUME_MSG === "0")
56664
57079
  return;
56665
- const text = formatPermissionResumeMessage({
57080
+ const text2 = formatPermissionResumeMessage({
56666
57081
  agentName: process.env.SWITCHROOM_AGENT_NAME ?? null,
56667
57082
  behavior: opts.behavior,
56668
57083
  action: opts.action,
@@ -56670,7 +57085,7 @@ function postPermissionResumeMessage(opts) {
56670
57085
  });
56671
57086
  const targets = resolvePermissionCardTargets();
56672
57087
  for (const { chatId, threadId } of targets) {
56673
- swallowingApiCall(() => bot.api.sendMessage(chatId, text, {
57088
+ swallowingApiCall(() => bot.api.sendMessage(chatId, text2, {
56674
57089
  parse_mode: "HTML",
56675
57090
  ...threadId != null ? { message_thread_id: threadId } : {}
56676
57091
  }), { chat_id: chatId, verb: "permission-resume", ...threadId != null ? { threadId } : {} });
@@ -56712,11 +57127,11 @@ function probeAvailableReactions(chatId) {
56712
57127
  })();
56713
57128
  }
56714
57129
  var PHOTO_EXTS = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp"]);
56715
- function chunk2(text, limit, mode) {
56716
- if (text.length <= limit)
56717
- return [text];
57130
+ function chunk2(text2, limit, mode) {
57131
+ if (text2.length <= limit)
57132
+ return [text2];
56718
57133
  const out = [];
56719
- let rest = text;
57134
+ let rest = text2;
56720
57135
  while (rest.length > limit) {
56721
57136
  let cut = limit;
56722
57137
  if (mode === "newline") {
@@ -56735,20 +57150,20 @@ function chunk2(text, limit, mode) {
56735
57150
  out.push(rest);
56736
57151
  return out;
56737
57152
  }
56738
- function escapeMarkdownV2(text) {
57153
+ function escapeMarkdownV2(text2) {
56739
57154
  const specialChars = /[_*\[\]()~`>#+\-=|{}.!\\]/g;
56740
57155
  const parts = [];
56741
57156
  let last = 0;
56742
57157
  const codeRe = /(```[\s\S]*?```|`[^`\n]+`)/g;
56743
57158
  let m;
56744
- while ((m = codeRe.exec(text)) !== null) {
57159
+ while ((m = codeRe.exec(text2)) !== null) {
56745
57160
  if (m.index > last)
56746
- parts.push(text.slice(last, m.index).replace(specialChars, "\\$&"));
57161
+ parts.push(text2.slice(last, m.index).replace(specialChars, "\\$&"));
56747
57162
  parts.push(m[0]);
56748
57163
  last = m.index + m[0].length;
56749
57164
  }
56750
- if (last < text.length)
56751
- parts.push(text.slice(last).replace(specialChars, "\\$&"));
57165
+ if (last < text2.length)
57166
+ parts.push(text2.slice(last).replace(specialChars, "\\$&"));
56752
57167
  return parts.join("");
56753
57168
  }
56754
57169
  var typingIntervals = new Map;
@@ -56839,14 +57254,14 @@ function wrapBootCardApi(threadId) {
56839
57254
  ...threadId != null ? { threadId } : {}
56840
57255
  });
56841
57256
  return {
56842
- sendMessage: async (cid, text, sendOpts) => {
56843
- const sent = await robustApiCall(() => lockedBot.api.sendMessage(cid, text, sendOpts), opts(cid));
57257
+ sendMessage: async (cid, text2, sendOpts) => {
57258
+ const sent = await robustApiCall(() => lockedBot.api.sendMessage(cid, text2, sendOpts), opts(cid));
56844
57259
  return sent;
56845
57260
  },
56846
- editMessageText: (cid, mid, text, editOpts) => robustApiCall(() => lockedBot.api.editMessageText(cid, mid, text, editOpts), opts(cid)),
56847
- editMessageTextStrict: async (cid, mid, text, editOpts) => {
57261
+ editMessageText: (cid, mid, text2, editOpts) => robustApiCall(() => lockedBot.api.editMessageText(cid, mid, text2, editOpts), opts(cid)),
57262
+ editMessageTextStrict: async (cid, mid, text2, editOpts) => {
56848
57263
  try {
56849
- await lockedBot.api.editMessageText(cid, mid, text, editOpts);
57264
+ await lockedBot.api.editMessageText(cid, mid, text2, editOpts);
56850
57265
  return "edited";
56851
57266
  } catch (err) {
56852
57267
  const desc = err instanceof import_grammy9.GrammyError ? err.description : err instanceof Error ? err.message : String(err);
@@ -56864,11 +57279,11 @@ function wrapIssuesCardApi(threadId) {
56864
57279
  ...threadId != null ? { threadId } : {}
56865
57280
  });
56866
57281
  return {
56867
- sendMessage: async (cid, text, sendOpts) => {
56868
- const sent = await robustApiCall(() => lockedBot.api.sendMessage(cid, text, sendOpts), opts(cid));
57282
+ sendMessage: async (cid, text2, sendOpts) => {
57283
+ const sent = await robustApiCall(() => lockedBot.api.sendMessage(cid, text2, sendOpts), opts(cid));
56869
57284
  return sent;
56870
57285
  },
56871
- editMessageText: (cid, mid, text, editOpts) => robustApiCall(() => lockedBot.api.editMessageText(cid, mid, text, editOpts), opts(cid)),
57286
+ editMessageText: (cid, mid, text2, editOpts) => robustApiCall(() => lockedBot.api.editMessageText(cid, mid, text2, editOpts), opts(cid)),
56872
57287
  deleteMessage: (cid, mid) => robustApiCall(() => lockedBot.api.deleteMessage(cid, mid), opts(cid))
56873
57288
  };
56874
57289
  }
@@ -56884,6 +57299,19 @@ var STATUS_QUERY_RE = /^\s*status\??\s*$/i;
56884
57299
  var PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
56885
57300
  var pendingPermissions = new Map;
56886
57301
  var PERMISSION_TTL_MS = 600000;
57302
+ var PERMISSION_NO_REPEAT_ENABLED = process.env.SWITCHROOM_PERMISSION_NO_REPEAT !== "0";
57303
+ var PERMISSION_DUPLICATE_WINDOW_MS = 3600000;
57304
+ var permissionTimeoutSignatures = new Map;
57305
+ function clearPermissionTimeoutSuppression(reason) {
57306
+ if (permissionTimeoutSignatures.size === 0)
57307
+ return;
57308
+ const n = permissionTimeoutSignatures.size;
57309
+ permissionTimeoutSignatures.clear();
57310
+ process.stderr.write(`telegram gateway: permission no-repeat suppression cleared (${n} sig(s)) \u2014 ${reason}
57311
+ `);
57312
+ }
57313
+ var PERMISSION_CARD_ORIGIN_RECOVERY_ENABLED = process.env.SWITCHROOM_PERMISSION_CARD_ORIGIN_RECOVERY !== "0";
57314
+ var PERMISSION_CARD_ORIGIN_MAX_AGE_MS = 1800000;
56887
57315
  var pendingAlwaysAllowCorrelations = new Map;
56888
57316
  var ALWAYS_ALLOW_CORRELATION_TTL_MS = 30000;
56889
57317
  function sweepStaleAlwaysAllowCorrelations(now = Date.now()) {
@@ -57113,18 +57541,31 @@ var pendingStateReaper = setInterval(() => {
57113
57541
  }
57114
57542
  for (const [k, v] of pendingPermissions) {
57115
57543
  if (now - v.startedAt > PERMISSION_TTL_MS) {
57116
- dispatchPermissionVerdict({ type: "permission", requestId: k, behavior: "deny" });
57544
+ const timeoutMinutes = Math.round(PERMISSION_TTL_MS / 60000);
57545
+ dispatchPermissionVerdict({
57546
+ type: "permission",
57547
+ requestId: k,
57548
+ behavior: "deny",
57549
+ message: timeoutDenyMessage(timeoutMinutes)
57550
+ });
57117
57551
  resumeReactionAfterVerdict();
57118
57552
  postPermissionResumeMessage({
57119
57553
  behavior: "deny",
57120
57554
  action: naturalAction(v.tool_name, v.input_preview),
57121
- timeoutMinutes: Math.round(PERMISSION_TTL_MS / 60000)
57555
+ timeoutMinutes
57122
57556
  });
57123
- process.stderr.write(`telegram gateway: permission TTL expired \u2014 auto-deny request=${k} tool=${v.tool_name} (no operator response in ${Math.round(PERMISSION_TTL_MS / 60000)}m)
57557
+ if (PERMISSION_NO_REPEAT_ENABLED) {
57558
+ permissionTimeoutSignatures.set(permissionSignature(v.tool_name, v.input_preview), now);
57559
+ }
57560
+ process.stderr.write(`telegram gateway: permission TTL expired \u2014 auto-deny request=${k} tool=${v.tool_name} (no operator response in ${timeoutMinutes}m)
57124
57561
  `);
57125
57562
  pendingPermissions.delete(k);
57126
57563
  }
57127
57564
  }
57565
+ for (const [sig, at] of permissionTimeoutSignatures) {
57566
+ if (now - at > PERMISSION_DUPLICATE_WINDOW_MS)
57567
+ permissionTimeoutSignatures.delete(sig);
57568
+ }
57128
57569
  for (const [k, v] of vaultPassphraseCache) {
57129
57570
  if (now > v.expiresAt)
57130
57571
  vaultPassphraseCache.delete(k);
@@ -57167,8 +57608,8 @@ var pendingStateReaper = setInterval(() => {
57167
57608
  }
57168
57609
  }, 60000);
57169
57610
  pendingStateReaper.unref();
57170
- function looksLikeAuthCode(text) {
57171
- const trimmed = text.trim();
57611
+ function looksLikeAuthCode(text2) {
57612
+ const trimmed = text2.trim();
57172
57613
  if (!trimmed || /\s/.test(trimmed))
57173
57614
  return false;
57174
57615
  if (trimmed.startsWith("session_"))
@@ -57266,10 +57707,10 @@ function emitGatewayOperatorEvent(event) {
57266
57707
  }
57267
57708
  }
57268
57709
  function postLegacyBanner(chatId, threadId, ackMessageId, ageSec, site) {
57269
- const text = `\uD83C\uDF9B\uFE0F Switchroom restarted \u2014 ready. (took ~${ageSec}s)`;
57710
+ const text2 = `\uD83C\uDF9B\uFE0F Switchroom restarted \u2014 ready. (took ~${ageSec}s)`;
57270
57711
  process.stderr.write(`telegram gateway: ${site}: posting legacy banner chat_id=${chatId}
57271
57712
  `);
57272
- retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chatId, text, {
57713
+ retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chatId, text2, {
57273
57714
  parse_mode: "HTML",
57274
57715
  link_preview_options: { is_disabled: true },
57275
57716
  ...tid != null ? { message_thread_id: tid } : {},
@@ -57281,7 +57722,7 @@ function postLegacyBanner(chatId, threadId, ackMessageId, ageSec, site) {
57281
57722
  chat_id: chatId,
57282
57723
  thread_id: threadId ?? null,
57283
57724
  message_ids: [sent.message_id],
57284
- texts: [text],
57725
+ texts: [text2],
57285
57726
  attachment_kinds: []
57286
57727
  });
57287
57728
  } catch {}
@@ -57414,22 +57855,22 @@ startTimer({
57414
57855
  endTurn(ctx.key);
57415
57856
  return;
57416
57857
  }
57417
- let text = null;
57858
+ let text2 = null;
57418
57859
  const upd = inFlightUpdate;
57419
57860
  if (upd != null) {
57420
57861
  try {
57421
57862
  const st = await hostdGetStatusOnce(getMyAgentName(), upd.requestId);
57422
57863
  if (st !== "not-configured" && st !== "unavailable") {
57423
- text = formatUpdateStatusLine(st, upd.startedAt, Date.now());
57864
+ text2 = formatUpdateStatusLine(st, upd.startedAt, Date.now());
57424
57865
  }
57425
57866
  } catch {}
57426
57867
  }
57427
- if (text == null) {
57868
+ if (text2 == null) {
57428
57869
  const blockedOnApproval = activeStatusReactions.get(statusKey(ctx.chatId, ctx.threadId))?.isAwaiting() ?? false;
57429
- text = formatFrameworkFallbackText(ctx.fallbackKind, ctx.silenceMs, ctx.inFlightTools, blockedOnApproval);
57870
+ text2 = formatFrameworkFallbackText(ctx.fallbackKind, ctx.silenceMs, ctx.inFlightTools, blockedOnApproval);
57430
57871
  }
57431
57872
  try {
57432
- await robustApiCall(() => bot.api.sendMessage(ctx.chatId, text, {
57873
+ await robustApiCall(() => bot.api.sendMessage(ctx.chatId, text2, {
57433
57874
  ...ctx.threadId != null ? { message_thread_id: ctx.threadId } : {},
57434
57875
  disable_notification: false
57435
57876
  }), { chat_id: ctx.chatId, ...ctx.threadId != null ? { threadId: ctx.threadId } : {} });
@@ -57915,12 +58356,26 @@ var ipcServer = createIpcServer({
57915
58356
  if (hit) {
57916
58357
  dispatchPermissionVerdict({ type: "permission", requestId, behavior: "allow" });
57917
58358
  process.stderr.write(`telegram gateway: scoped-approval auto-allow tool=${toolName} rule="${hit}" request=${requestId} (time-boxed window)
58359
+ `);
58360
+ return;
58361
+ }
58362
+ }
58363
+ if (PERMISSION_NO_REPEAT_ENABLED) {
58364
+ const sig = permissionSignature(toolName, inputPreview);
58365
+ if (isRecentTimeoutDuplicate(permissionTimeoutSignatures, sig, Date.now(), PERMISSION_DUPLICATE_WINDOW_MS)) {
58366
+ dispatchPermissionVerdict({
58367
+ type: "permission",
58368
+ requestId,
58369
+ behavior: "deny",
58370
+ message: duplicateDenyMessage
58371
+ });
58372
+ process.stderr.write(`telegram gateway: permission no-repeat short-circuit \u2014 duplicate of a ` + `timed-out request tool=${toolName} request=${requestId} (no card posted)
57918
58373
  `);
57919
58374
  return;
57920
58375
  }
57921
58376
  }
57922
58377
  pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now() });
57923
- const text = formatPermissionCardBody({
58378
+ const text2 = formatPermissionCardBody({
57924
58379
  toolName,
57925
58380
  inputPreview,
57926
58381
  description,
@@ -57931,7 +58386,7 @@ var ipcServer = createIpcServer({
57931
58386
  const activeTurn = currentTurn;
57932
58387
  const targets = resolvePermissionCardTargets();
57933
58388
  for (const { chatId, threadId } of targets) {
57934
- retryWithThreadFallback(robustApiCall, (tid) => bot.api.sendMessage(chatId, text, {
58389
+ retryWithThreadFallback(robustApiCall, (tid) => bot.api.sendMessage(chatId, text2, {
57935
58390
  parse_mode: "HTML",
57936
58391
  reply_markup: keyboard,
57937
58392
  ...tid != null ? { message_thread_id: tid } : {}
@@ -58365,7 +58820,8 @@ var ALLOWED_TOOLS = new Set([
58365
58820
  "vault_request_access",
58366
58821
  "request_secret",
58367
58822
  "linear_agent_activity",
58368
- "linear_create_issue"
58823
+ "linear_create_issue",
58824
+ "linear_agent_setup"
58369
58825
  ]);
58370
58826
  async function executeToolCall(tool, args) {
58371
58827
  if (!ALLOWED_TOOLS.has(tool)) {
@@ -58414,6 +58870,8 @@ async function executeToolCall(tool, args) {
58414
58870
  return executeLinearAgentActivity(args);
58415
58871
  case "linear_create_issue":
58416
58872
  return executeLinearCreateIssue(args);
58873
+ case "linear_agent_setup":
58874
+ return executeLinearAgentSetup(args);
58417
58875
  default:
58418
58876
  throw new Error(`unknown tool: ${tool}`);
58419
58877
  }
@@ -58444,11 +58902,45 @@ async function executeSendChecklist(args) {
58444
58902
  `);
58445
58903
  return { content: [{ type: "text", text: `checklist sent (id: ${sent.message_id})` }] };
58446
58904
  }
58905
+ var linearAuthAlertLast = new Map;
58906
+ var LINEAR_AUTH_ALERT_COOLDOWN_MS = 21600000;
58907
+ function notifyLinearAuthDead(info) {
58908
+ if (process.env.SWITCHROOM_LINEAR_AUTH_ALERT === "0")
58909
+ return;
58910
+ const key = `${info.agent}:${info.reason}`;
58911
+ const now = Date.now();
58912
+ const last = linearAuthAlertLast.get(key);
58913
+ if (last != null && now - last < LINEAR_AUTH_ALERT_COOLDOWN_MS)
58914
+ return;
58915
+ (async () => {
58916
+ try {
58917
+ const chatId = loadAccess().allowFrom[0];
58918
+ if (!chatId)
58919
+ return;
58920
+ const threadId = topicForRecipient({
58921
+ recipientChatId: chatId,
58922
+ resolvedTopic: resolveAgentOutboundTopic({ kind: "linear-auth" }) ?? chatThreadMap.get(chatId),
58923
+ supergroupChatId: resolveAgentSupergroupChatId()
58924
+ });
58925
+ const text2 = buildLinearAuthDeadMessage(info.agent, info.reason);
58926
+ await swallowingApiCall(() => bot.api.sendMessage(chatId, text2, {
58927
+ parse_mode: "HTML",
58928
+ ...threadId != null ? { message_thread_id: threadId } : {}
58929
+ }), { chat_id: chatId, verb: "linearAuthDead" });
58930
+ linearAuthAlertLast.set(key, now);
58931
+ process.stderr.write(`telegram gateway: linear auth-dead alert sent agent=${info.agent} reason=${info.reason}
58932
+ `);
58933
+ } catch {}
58934
+ })();
58935
+ }
58447
58936
  async function executeLinearAgentActivity(args) {
58448
- return emitLinearAgentActivity(args);
58937
+ return emitLinearAgentActivity(args, { onAuthUnrecoverable: notifyLinearAuthDead });
58449
58938
  }
58450
58939
  async function executeLinearCreateIssue(args) {
58451
- return createLinearIssue(args);
58940
+ return createLinearIssue(args, { onAuthUnrecoverable: notifyLinearAuthDead });
58941
+ }
58942
+ async function executeLinearAgentSetup(args) {
58943
+ return runLinearAgentSetup(args);
58452
58944
  }
58453
58945
  async function executeUpdateChecklist(args) {
58454
58946
  const chat_id = args.chat_id;
@@ -58465,9 +58957,9 @@ async function executeUpdateChecklist(args) {
58465
58957
  `);
58466
58958
  return { content: [{ type: "text", text: `checklist updated (id: ${message_id})` }] };
58467
58959
  }
58468
- function redactOutboundText(text, site) {
58469
- const masked = redact2(text);
58470
- if (masked !== text) {
58960
+ function redactOutboundText(text2, site) {
58961
+ const masked = redact2(text2);
58962
+ if (masked !== text2) {
58471
58963
  process.stderr.write(`telegram gateway: outbound secret masked site=${site}
58472
58964
  `);
58473
58965
  }
@@ -58481,12 +58973,12 @@ async function executeReply(args) {
58481
58973
  const rawText = args.text;
58482
58974
  if (rawText == null || rawText === "")
58483
58975
  throw new Error("reply: text is required and cannot be empty");
58484
- let text = repairEscapedWhitespace(rawText);
58485
- text = redactOutboundText(text, "reply");
58976
+ let text2 = repairEscapedWhitespace(rawText);
58977
+ text2 = redactOutboundText(text2, "reply");
58486
58978
  {
58487
- const scrub = scrubVoice(text);
58979
+ const scrub = scrubVoice(text2);
58488
58980
  if (scrub.replaced > 0) {
58489
- text = scrub.scrubbed;
58981
+ text2 = scrub.scrubbed;
58490
58982
  emitRuntimeMetric({
58491
58983
  kind: "voice_scrub_applied",
58492
58984
  chatKey: statusKey(chat_id, args.message_thread_id != null ? Number(args.message_thread_id) : undefined),
@@ -58495,11 +58987,11 @@ async function executeReply(args) {
58495
58987
  });
58496
58988
  }
58497
58989
  }
58498
- process.stderr.write(`telegram channel: reply: invoked chatId=${chat_id} charCount=${text.length} preview=${JSON.stringify(text.slice(0, 80))}
58990
+ process.stderr.write(`telegram channel: reply: invoked chatId=${chat_id} charCount=${text2.length} preview=${JSON.stringify(text2.slice(0, 80))}
58499
58991
  `);
58500
58992
  {
58501
58993
  const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
58502
- const dup = outboundDedup.check(chat_id, replyThreadId, text, Date.now(), currentTurn?.registryKey ?? null);
58994
+ const dup = outboundDedup.check(chat_id, replyThreadId, text2, Date.now(), currentTurn?.registryKey ?? null);
58503
58995
  if (dup != null) {
58504
58996
  process.stderr.write(`telegram gateway: reply: deduped (#546) chatId=${chat_id} ageMs=${dup.ageMs} preview=${JSON.stringify(dup.preview)}
58505
58997
  `);
@@ -58543,13 +59035,13 @@ async function executeReply(args) {
58543
59035
  }
58544
59036
  const tg = access.telegraph;
58545
59037
  const tgThreshold = tg?.threshold ?? 3000;
58546
- if (tg?.enabled && files.length === 0 && text.length > tgThreshold) {
59038
+ if (tg?.enabled && files.length === 0 && text2.length > tgThreshold) {
58547
59039
  const agentSlug = process.env.SWITCHROOM_AGENT_NAME ?? "switchroom-agent";
58548
59040
  const shortName = tg.short_name ?? agentSlug;
58549
- const url = await publishToTelegraph(text, shortName, tg.author_name);
59041
+ const url = await publishToTelegraph(text2, shortName, tg.author_name);
58550
59042
  if (url != null) {
58551
- const title = deriveTelegraphTitle(text);
58552
- text = `<b>${title.replace(/[<>&]/g, (c) => c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&amp;")}</b>
59043
+ const title = deriveTelegraphTitle(text2);
59044
+ text2 = `<b>${title.replace(/[<>&]/g, (c) => c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&amp;")}</b>
58553
59045
  ${url}`;
58554
59046
  }
58555
59047
  }
@@ -58557,13 +59049,13 @@ ${url}`;
58557
59049
  let effectiveText;
58558
59050
  if (format === "html") {
58559
59051
  parseMode = "HTML";
58560
- effectiveText = markdownToHtml(text);
59052
+ effectiveText = markdownToHtml(text2);
58561
59053
  } else if (format === "markdownv2") {
58562
59054
  parseMode = "MarkdownV2";
58563
- effectiveText = escapeMarkdownV2(text);
59055
+ effectiveText = escapeMarkdownV2(text2);
58564
59056
  } else {
58565
59057
  parseMode = undefined;
58566
- effectiveText = text;
59058
+ effectiveText = text2;
58567
59059
  }
58568
59060
  assertAllowedChat(chat_id);
58569
59061
  let replyRoutedOriginTurn = null;
@@ -58920,7 +59412,7 @@ ${url}`;
58920
59412
  process.stderr.write(`telegram channel: reply: finalized chatId=${chat_id} messageIds=[${sentIds.join(",")}] chunks=${chunks.length}
58921
59413
  `);
58922
59414
  if (sentIds.length > 0) {
58923
- outboundDedup.record(chat_id, threadId, text, Date.now(), currentTurn?.registryKey ?? null);
59415
+ outboundDedup.record(chat_id, threadId, text2, Date.now(), currentTurn?.registryKey ?? null);
58924
59416
  }
58925
59417
  return { content: [{ type: "text", text: result }] };
58926
59418
  }
@@ -59105,12 +59597,12 @@ async function executeProgressUpdate(args) {
59105
59597
  if (!args.text)
59106
59598
  throw new Error("progress_update: text is required");
59107
59599
  const chat_id = args.chat_id;
59108
- let text = args.text;
59600
+ let text2 = args.text;
59109
59601
  const threadId = resolveThreadId(chat_id, args.message_thread_id);
59110
59602
  const key = statusKey(chat_id, threadId);
59111
59603
  assertAllowedChat(chat_id);
59112
- if (text.length > 300) {
59113
- text = text.slice(0, 299) + "\u2026";
59604
+ if (text2.length > 300) {
59605
+ text2 = text2.slice(0, 299) + "\u2026";
59114
59606
  }
59115
59607
  const now = Date.now();
59116
59608
  const lastSent = progressUpdateLastSent.get(key);
@@ -59152,7 +59644,7 @@ async function executeProgressUpdate(args) {
59152
59644
  toolUseIdHint
59153
59645
  });
59154
59646
  if (subAgent != null && progressDriver != null) {
59155
- const cardText = text.length > 200 ? text.slice(0, 199) + "\u2026" : text;
59647
+ const cardText = text2.length > 200 ? text2.slice(0, 199) + "\u2026" : text2;
59156
59648
  const result = progressDriver.recordSubAgentNarrative({
59157
59649
  chatId: chat_id,
59158
59650
  threadId: threadId != null ? String(threadId) : undefined,
@@ -59177,7 +59669,7 @@ async function executeProgressUpdate(args) {
59177
59669
  const access = loadAccess();
59178
59670
  const configParseMode = access.parseMode ?? "html";
59179
59671
  const parseMode = configParseMode === "html" ? "HTML" : undefined;
59180
- const effectiveText = configParseMode === "html" ? markdownToHtml(text) : text;
59672
+ const effectiveText = configParseMode === "html" ? markdownToHtml(text2) : text2;
59181
59673
  const sendOpts = {
59182
59674
  ...parseMode ? { parse_mode: parseMode } : {},
59183
59675
  ...threadId != null ? { message_thread_id: threadId } : {}
@@ -59188,7 +59680,7 @@ async function executeProgressUpdate(args) {
59188
59680
  chat_id,
59189
59681
  thread_id: threadId ?? null,
59190
59682
  message_ids: [sent.message_id],
59191
- texts: [text]
59683
+ texts: [text2]
59192
59684
  });
59193
59685
  }
59194
59686
  progressUpdateLastSent.set(key, now);
@@ -59355,7 +59847,7 @@ async function executeSendGif(rawArgs) {
59355
59847
  }]
59356
59848
  };
59357
59849
  }
59358
- async function publishToTelegraph(text, shortName, authorName) {
59850
+ async function publishToTelegraph(text2, shortName, authorName) {
59359
59851
  const accountPath = join35(STATE_DIR, "telegraph-account.json");
59360
59852
  let account = null;
59361
59853
  try {
@@ -59386,8 +59878,8 @@ async function publishToTelegraph(text, shortName, authorName) {
59386
59878
  `);
59387
59879
  }
59388
59880
  }
59389
- const title = deriveTelegraphTitle(text);
59390
- const content = markdownToTelegraphNodes(text);
59881
+ const title = deriveTelegraphTitle(text2);
59882
+ const content = markdownToTelegraphNodes(text2);
59391
59883
  const page = await createTelegraphPage({
59392
59884
  accessToken: account.accessToken,
59393
59885
  title,
@@ -59399,7 +59891,7 @@ async function publishToTelegraph(text, shortName, authorName) {
59399
59891
  `);
59400
59892
  return null;
59401
59893
  }
59402
- process.stderr.write(`telegram gateway: telegraph published url=${page.value.url} title=${JSON.stringify(title)} chars=${text.length}
59894
+ process.stderr.write(`telegram gateway: telegraph published url=${page.value.url} title=${JSON.stringify(title)} chars=${text2.length}
59403
59895
  `);
59404
59896
  return page.value.url;
59405
59897
  }
@@ -59461,11 +59953,11 @@ async function executeVaultRequestSave(args) {
59461
59953
  };
59462
59954
  pendingVaultRequestSaves.set(stageId, pending2);
59463
59955
  sweepPendingVaultRequestSaves();
59464
- const text = renderVaultRequestSaveCard(pending2, agentSlug);
59956
+ const text2 = renderVaultRequestSaveCard(pending2, agentSlug);
59465
59957
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
59466
59958
  if (threadId != null)
59467
59959
  pending2.threadId = threadId;
59468
- const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
59960
+ const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text2, {
59469
59961
  parse_mode: "HTML",
59470
59962
  reply_markup: buildVaultRequestSaveKeyboard(stageId),
59471
59963
  ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
@@ -59537,11 +60029,11 @@ async function executeRequestSecret(args) {
59537
60029
  const pending2 = { agent: agentSlug, chat_id, key, reason, staged_at: Date.now() };
59538
60030
  pendingSecretRequests.set(stageId, pending2);
59539
60031
  sweepSecretRequests();
59540
- const text = renderSecretRequestCard(pending2);
60032
+ const text2 = renderSecretRequestCard(pending2);
59541
60033
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
59542
60034
  if (threadId != null)
59543
60035
  pending2.threadId = threadId;
59544
- const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
60036
+ const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text2, {
59545
60037
  parse_mode: "HTML",
59546
60038
  reply_markup: buildSecretRequestKeyboard(stageId),
59547
60039
  ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
@@ -59782,11 +60274,11 @@ async function executeVaultRequestAccess(args) {
59782
60274
  };
59783
60275
  pendingVaultRequestAccesses.set(stageId, pending2);
59784
60276
  sweepPendingVaultRequestAccesses();
59785
- const text = renderVaultRequestAccessCard(pending2);
60277
+ const text2 = renderVaultRequestAccessCard(pending2);
59786
60278
  const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
59787
60279
  if (threadId != null)
59788
60280
  pending2.threadId = threadId;
59789
- const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text, {
60281
+ const sent = await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendMessage(chat_id, text2, {
59790
60282
  parse_mode: "HTML",
59791
60283
  reply_markup: buildVaultRequestAccessKeyboard(stageId),
59792
60284
  ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
@@ -60344,10 +60836,10 @@ function handleSessionEvent(ev) {
60344
60836
  isPrivateChat: turn.isDm,
60345
60837
  threadId: turn.sessionThreadId,
60346
60838
  ...ANSWER_LANE.usesDraftTransport ? { sendMessageDraft: sendMessageDraftFn, minInitialChars: ANSWER_LANE.minInitialChars } : { minInitialChars: ANSWER_LANE.minInitialChars },
60347
- sendMessage: async (chatId, text, params) => {
60839
+ sendMessage: async (chatId, text2, params) => {
60348
60840
  const tid = params?.message_thread_id;
60349
60841
  const silent = params?.purpose !== "materialize";
60350
- const msg = await robustApiCall(() => bot.api.sendMessage(chatId, text, {
60842
+ const msg = await robustApiCall(() => bot.api.sendMessage(chatId, text2, {
60351
60843
  parse_mode: params?.parse_mode,
60352
60844
  disable_notification: silent,
60353
60845
  ...tid != null ? { message_thread_id: tid } : {},
@@ -60360,9 +60852,9 @@ function handleSessionEvent(ev) {
60360
60852
  });
60361
60853
  return { message_id: msg.message_id };
60362
60854
  },
60363
- editMessageText: (chatId, messageId, text, params) => {
60855
+ editMessageText: (chatId, messageId, text2, params) => {
60364
60856
  const tid = params?.message_thread_id;
60365
- return robustApiCall(() => bot.api.editMessageText(chatId, messageId, text, {
60857
+ return robustApiCall(() => bot.api.editMessageText(chatId, messageId, text2, {
60366
60858
  parse_mode: params?.parse_mode,
60367
60859
  ...tid != null ? { message_thread_id: tid } : {},
60368
60860
  ...params?.link_preview_options != null ? { link_preview_options: params.link_preview_options } : {}
@@ -60386,13 +60878,13 @@ function handleSessionEvent(ev) {
60386
60878
  }
60387
60879
  }
60388
60880
  },
60389
- checkDedup: (text) => {
60390
- return outboundDedup.check(turn.sessionChatId, turn.sessionThreadId, text, Date.now(), turn.registryKey ?? null) != null;
60881
+ checkDedup: (text2) => {
60882
+ return outboundDedup.check(turn.sessionChatId, turn.sessionThreadId, text2, Date.now(), turn.registryKey ?? null) != null;
60391
60883
  },
60392
- recordDedup: (text) => {
60393
- outboundDedup.record(turn.sessionChatId, turn.sessionThreadId, text, Date.now(), turn.registryKey ?? null);
60884
+ recordDedup: (text2) => {
60885
+ outboundDedup.record(turn.sessionChatId, turn.sessionThreadId, text2, Date.now(), turn.registryKey ?? null);
60394
60886
  },
60395
- recordOutbound: ({ messageId, text }) => {
60887
+ recordOutbound: ({ messageId, text: text2 }) => {
60396
60888
  if (!HISTORY_ENABLED)
60397
60889
  return;
60398
60890
  try {
@@ -60400,7 +60892,7 @@ function handleSessionEvent(ev) {
60400
60892
  chat_id: turn.sessionChatId,
60401
60893
  thread_id: turn.sessionThreadId ?? null,
60402
60894
  message_ids: [messageId],
60403
- texts: [text]
60895
+ texts: [text2]
60404
60896
  });
60405
60897
  } catch {}
60406
60898
  }
@@ -60791,7 +61283,7 @@ function handleSessionEvent(ev) {
60791
61283
  }
60792
61284
  }
60793
61285
  }
60794
- function handlePtyPartial(text) {
61286
+ function handlePtyPartial(text2) {
60795
61287
  const turn = currentTurn;
60796
61288
  const state4 = {
60797
61289
  currentSessionChatId: turn?.sessionChatId ?? null,
@@ -60802,7 +61294,7 @@ function handlePtyPartial(text) {
60802
61294
  suppressPtyPreview,
60803
61295
  lastPtyPreviewByChat
60804
61296
  };
60805
- handlePtyPartialPure(text, state4, {
61297
+ handlePtyPartialPure(text2, state4, {
60806
61298
  bot,
60807
61299
  retry: robustApiCall,
60808
61300
  renderText: markdownToHtml,
@@ -60914,10 +61406,10 @@ function gate(ctx) {
60914
61406
  }
60915
61407
  function isMentioned(ctx, extraPatterns) {
60916
61408
  const entities = ctx.message?.entities ?? ctx.message?.caption_entities ?? [];
60917
- const text = ctx.message?.text ?? ctx.message?.caption ?? "";
61409
+ const text2 = ctx.message?.text ?? ctx.message?.caption ?? "";
60918
61410
  for (const e of entities) {
60919
61411
  if (e.type === "mention") {
60920
- const mentioned = text.slice(e.offset, e.offset + e.length);
61412
+ const mentioned = text2.slice(e.offset, e.offset + e.length);
60921
61413
  if (mentioned.toLowerCase() === `@${botUsername}`.toLowerCase())
60922
61414
  return true;
60923
61415
  }
@@ -60928,7 +61420,7 @@ function isMentioned(ctx, extraPatterns) {
60928
61420
  return true;
60929
61421
  for (const pat of extraPatterns ?? []) {
60930
61422
  try {
60931
- if (new RegExp(pat, "i").test(text))
61423
+ if (new RegExp(pat, "i").test(text2))
60932
61424
  return true;
60933
61425
  } catch {}
60934
61426
  }
@@ -60957,14 +61449,14 @@ function isAuthorizedSender(ctx) {
60957
61449
  function safeName(s) {
60958
61450
  return s?.replace(/[<>\[\]\r\n;]/g, "_");
60959
61451
  }
60960
- async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
60961
- if (parseInterruptMarker(text).isInterrupt) {
60962
- return handleInbound(ctx, text, downloadImage, attachment);
61452
+ async function handleInboundCoalesced(ctx, text2, downloadImage, attachment) {
61453
+ if (parseInterruptMarker(text2).isInterrupt) {
61454
+ return handleInbound(ctx, text2, downloadImage, attachment);
60963
61455
  }
60964
61456
  const hasAttachment = downloadImage != null || attachment != null;
60965
61457
  const maxAttachments = coalesceMaxAttachments();
60966
61458
  if (hasAttachment && ctx.message?.media_group_id != null && maxAttachments <= 1) {
60967
- return handleInbound(ctx, text, downloadImage, attachment);
61459
+ return handleInbound(ctx, text2, downloadImage, attachment);
60968
61460
  }
60969
61461
  const from = ctx.from;
60970
61462
  if (!from)
@@ -60972,14 +61464,14 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
60972
61464
  if (hasAttachment) {
60973
61465
  const probeKey = inboundCoalesceKey(String(ctx.chat.id), ctx.message?.message_thread_id, String(from.id));
60974
61466
  if ((bufferedAttachmentKeys.get(probeKey) ?? 0) >= maxAttachments) {
60975
- return handleInbound(ctx, text, downloadImage, attachment);
61467
+ return handleInbound(ctx, text2, downloadImage, attachment);
60976
61468
  }
60977
61469
  }
60978
61470
  maybeEarlyAckReaction(ctx, from);
60979
61471
  const key = inboundCoalesceKey(String(ctx.chat.id), ctx.message?.message_thread_id, String(from.id));
60980
- const result = inboundCoalescer.enqueue(key, { text, ctx, downloadImage, attachment });
61472
+ const result = inboundCoalescer.enqueue(key, { text: text2, ctx, downloadImage, attachment });
60981
61473
  if (result.bypass)
60982
- return handleInbound(ctx, text, downloadImage, attachment);
61474
+ return handleInbound(ctx, text2, downloadImage, attachment);
60983
61475
  if (hasAttachment)
60984
61476
  bufferedAttachmentKeys.set(key, (bufferedAttachmentKeys.get(key) ?? 0) + 1);
60985
61477
  }
@@ -61002,7 +61494,7 @@ function maybeEarlyAckReaction(ctx, from) {
61002
61494
  ]).catch(() => {});
61003
61495
  bot.api.sendChatAction(chatId, "typing").catch(() => {});
61004
61496
  }
61005
- async function handleInbound(ctx, text, downloadImage, attachment, extraAttachments) {
61497
+ async function handleInbound(ctx, text2, downloadImage, attachment, extraAttachments) {
61006
61498
  const isTopicMessage = ctx.message?.is_topic_message ?? false;
61007
61499
  const messageThreadId = ctx.message?.message_thread_id;
61008
61500
  if (TOPIC_ID != null) {
@@ -61021,6 +61513,7 @@ async function handleInbound(ctx, text, downloadImage, attachment, extraAttachme
61021
61513
  /telegram:access pair ${result.code}`);
61022
61514
  return;
61023
61515
  }
61516
+ clearPermissionTimeoutSuppression("operator inbound");
61024
61517
  const inboundReceivedAt = Date.now();
61025
61518
  const _shadowKey = statusKey(ctx.chat?.id != null ? String(ctx.chat.id) : "0", ctx.message?.message_thread_id);
61026
61519
  const machineInTurnAtReceipt = isDeliveryCutoverEnabled() ? isMachineInTurn() : null;
@@ -61042,7 +61535,7 @@ async function handleInbound(ctx, text, downloadImage, attachment, extraAttachme
61042
61535
  if (messageThreadId != null)
61043
61536
  chatThreadMap.set(chat_id, messageThreadId);
61044
61537
  try {
61045
- const classification = classifyInbound(text);
61538
+ const classification = classifyInbound(text2);
61046
61539
  if (classification.isStatusQuery) {
61047
61540
  const priorKey = statusKey(chat_id, messageThreadId);
61048
61541
  const priorTurnStartedAt2 = activeTurnStartedAt.get(priorKey);
@@ -61052,7 +61545,7 @@ async function handleInbound(ctx, text, downloadImage, attachment, extraAttachme
61052
61545
  chat_id,
61053
61546
  message_id: msgId ?? null,
61054
61547
  thread_id: messageThreadId ?? null,
61055
- text_length: text.length,
61548
+ text_length: text2.length,
61056
61549
  prior_turn_in_flight: priorTurnInFlight,
61057
61550
  seconds_since_turn_start: priorTurnStartedAt2 != null ? Math.round((inboundReceivedAt - priorTurnStartedAt2) / 1000) : null
61058
61551
  });
@@ -61061,7 +61554,7 @@ async function handleInbound(ctx, text, downloadImage, attachment, extraAttachme
61061
61554
  process.stderr.write(`telegram gateway: inbound classifier error: ${err.message}
61062
61555
  `);
61063
61556
  }
61064
- const interrupt = parseInterruptMarker(text);
61557
+ const interrupt = parseInterruptMarker(text2);
61065
61558
  let deferInterrupt = false;
61066
61559
  if (interrupt.isInterrupt) {
61067
61560
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
@@ -61102,9 +61595,9 @@ async function handleInbound(ctx, text, downloadImage, attachment, extraAttachme
61102
61595
  });
61103
61596
  return;
61104
61597
  }
61105
- text = interrupt.body;
61598
+ text2 = interrupt.body;
61106
61599
  }
61107
- if (STATUS_QUERY_RE.test(text)) {
61600
+ if (STATUS_QUERY_RE.test(text2)) {
61108
61601
  try {
61109
61602
  const threadKey = messageThreadId != null ? String(messageThreadId) : undefined;
61110
61603
  const cardState = progressDriver?.peek(chat_id, threadKey);
@@ -61120,7 +61613,7 @@ async function handleInbound(ctx, text, downloadImage, attachment, extraAttachme
61120
61613
  `);
61121
61614
  }
61122
61615
  }
61123
- const permMatch = PERMISSION_REPLY_RE.exec(text);
61616
+ const permMatch = PERMISSION_REPLY_RE.exec(text2);
61124
61617
  if (permMatch) {
61125
61618
  const behavior = permMatch[1].toLowerCase().startsWith("y") ? "allow" : "deny";
61126
61619
  const request_id = permMatch[2].toLowerCase();
@@ -61145,12 +61638,12 @@ async function handleInbound(ctx, text, downloadImage, attachment, extraAttachme
61145
61638
  }
61146
61639
  const interceptKey = chatKey2(chat_id, messageThreadId);
61147
61640
  const pendingAdd = pendingAuthAddFlows.get(interceptKey);
61148
- if (pendingAdd && looksLikeAuthCode(text)) {
61641
+ if (pendingAdd && looksLikeAuthCode(text2)) {
61149
61642
  const elapsed = Date.now() - pendingAdd.startedAt;
61150
61643
  if (elapsed < REAUTH_INTERCEPT_TTL_MS) {
61151
61644
  pendingAuthAddFlows.delete(interceptKey);
61152
61645
  try {
61153
- const credentials = await submitAccountAuthCode(pendingAdd, text.trim());
61646
+ const credentials = await submitAccountAuthCode(pendingAdd, text2.trim());
61154
61647
  try {
61155
61648
  await addAccountViaBroker(pendingAdd.label, credentials, { replace: false });
61156
61649
  cleanScratchDir(pendingAdd.scratchDir);
@@ -61170,11 +61663,11 @@ The fleet's active account hasn't changed. Send <code>/auth use ${escapeHtmlForT
61170
61663
  pendingAuthAddFlows.delete(interceptKey);
61171
61664
  }
61172
61665
  const pendingReauth = pendingReauthFlows.get(interceptKey);
61173
- if (pendingReauth && looksLikeAuthCode(text)) {
61666
+ if (pendingReauth && looksLikeAuthCode(text2)) {
61174
61667
  const elapsed = Date.now() - pendingReauth.startedAt;
61175
61668
  if (elapsed < REAUTH_INTERCEPT_TTL_MS) {
61176
61669
  pendingReauthFlows.delete(interceptKey);
61177
- const { result: result2, errorText } = execAuthCode(pendingReauth.agent, text.trim());
61670
+ const { result: result2, errorText } = execAuthCode(pendingReauth.agent, text2.trim());
61178
61671
  if (errorText) {
61179
61672
  await switchroomReply(ctx, `<b>auth code failed:</b>
61180
61673
  ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
@@ -61202,7 +61695,7 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
61202
61695
  } else {
61203
61696
  pendingVaultOps.delete(chat_id);
61204
61697
  if (pendingVault.kind === "passphrase") {
61205
- const passphrase = text.trim();
61698
+ const passphrase = text2.trim();
61206
61699
  if (!passphrase) {
61207
61700
  await switchroomReply(ctx, "Passphrase cannot be empty. Try /vault again.", { html: true });
61208
61701
  return;
@@ -61212,7 +61705,7 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
61212
61705
  await deleteSensitiveMessage(chat_id, msgId, "vault passphrase");
61213
61706
  await executeVaultOp(ctx, chat_id, pendingVault.op, pendingVault.key, passphrase, undefined);
61214
61707
  } else if (pendingVault.kind === "unlock") {
61215
- const passphrase = text.trim();
61708
+ const passphrase = text2.trim();
61216
61709
  if (!passphrase) {
61217
61710
  await switchroomReply(ctx, "Passphrase cannot be empty. Try /vault unlock again.", { html: true });
61218
61711
  return;
@@ -61226,7 +61719,7 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
61226
61719
  await switchroomReply(ctx, `<b>vault unlock failed:</b> ${escapeHtmlForTg(result2.msg ?? "unknown error")}`, { html: true });
61227
61720
  }
61228
61721
  } else if (pendingVault.kind === "passphrase-for-deferred") {
61229
- const passphrase = text.trim();
61722
+ const passphrase = text2.trim();
61230
61723
  if (!passphrase) {
61231
61724
  await switchroomReply(ctx, "Passphrase cannot be empty. Tap the unlock button again.", { html: true });
61232
61725
  return;
@@ -61236,7 +61729,7 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
61236
61729
  await deleteSensitiveMessage(chat_id, msgId, "vault passphrase");
61237
61730
  await executeDeferredSecretSave(ctx, pendingVault.deferKey, passphrase, pendingVault.cardMessageId);
61238
61731
  } else if (pendingVault.kind === "passphrase-for-access-approve") {
61239
- const passphrase = text.trim();
61732
+ const passphrase = text2.trim();
61240
61733
  if (!passphrase) {
61241
61734
  await switchroomReply(ctx, "Passphrase cannot be empty. Ask the agent to re-issue the request card.", { html: true });
61242
61735
  return;
@@ -61253,7 +61746,7 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
61253
61746
  await performVaultAccessApproval(ctx, stagedAccess, item.stageId, item.senderId, { kind: "passphrase", passphrase });
61254
61747
  }
61255
61748
  } else if (pendingVault.kind === "grant-wizard" && pendingVault.awaitingCustomDuration) {
61256
- const input = text.trim();
61749
+ const input = text2.trim();
61257
61750
  const ttlSeconds = parseGrantDuration(input);
61258
61751
  if (ttlSeconds === null) {
61259
61752
  pendingVaultOps.set(chat_id, { ...pendingVault, startedAt: Date.now() });
@@ -61265,15 +61758,15 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
61265
61758
  } else if (pendingVault.kind === "grant-wizard") {
61266
61759
  pendingVaultOps.set(chat_id, { ...pendingVault, startedAt: Date.now() });
61267
61760
  } else if (pendingVault.kind === "value") {
61268
- let value = text;
61269
- const codeBlockMatch = /^```[\w]*\n?([\s\S]*?)```$/m.exec(text);
61761
+ let value = text2;
61762
+ const codeBlockMatch = /^```[\w]*\n?([\s\S]*?)```$/m.exec(text2);
61270
61763
  if (codeBlockMatch)
61271
61764
  value = codeBlockMatch[1];
61272
61765
  if (msgId != null)
61273
61766
  await deleteSensitiveMessage(chat_id, msgId, "vault secret value");
61274
61767
  await executeVaultOp(ctx, chat_id, "set", pendingVault.key, pendingVault.passphrase, value.trim());
61275
61768
  } else if (pendingVault.kind === "rename-vault-save") {
61276
- const newKey = text.trim();
61769
+ const newKey = text2.trim();
61277
61770
  const staged = pendingVaultRequestSaves.get(pendingVault.stageId);
61278
61771
  if (!staged) {
61279
61772
  await switchroomReply(ctx, "\u231B That save card expired before you renamed. Ask the agent to re-issue.", { html: true });
@@ -61294,7 +61787,7 @@ ${preBlock(formatSwitchroomOutput(errorText))}`, { html: true });
61294
61787
  return;
61295
61788
  }
61296
61789
  }
61297
- const stagedMatch = /^\s*(stash|ignore|rename|forget)\b\s*(\S+)?/i.exec(text);
61790
+ const stagedMatch = /^\s*(stash|ignore|rename|forget)\b\s*(\S+)?/i.exec(text2);
61298
61791
  if (stagedMatch) {
61299
61792
  const staged = secretStaging.latestForChat(chat_id);
61300
61793
  if (staged != null) {
@@ -61340,13 +61833,13 @@ ${preBlock(write.output)}`;
61340
61833
  }
61341
61834
  }
61342
61835
  bot.api.sendChatAction(chat_id, "typing", messageThreadId != null ? { message_thread_id: messageThreadId } : {}).catch(() => {});
61343
- const parsedSteer = parseSteerPrefix(text);
61836
+ const parsedSteer = parseSteerPrefix(text2);
61344
61837
  const isSteerPrefix = parsedSteer.steering;
61345
- const parsedQueue = isSteerPrefix ? { queued: false, body: parsedSteer.body } : parseQueuePrefix(text);
61838
+ const parsedQueue = isSteerPrefix ? { queued: false, body: parsedSteer.body } : parseQueuePrefix(text2);
61346
61839
  const isQueuedPrefix = parsedQueue.queued;
61347
- let effectiveText = isSteerPrefix ? parsedSteer.body : isQueuedPrefix ? parsedQueue.body : text;
61840
+ let effectiveText = isSteerPrefix ? parsedSteer.body : isQueuedPrefix ? parsedQueue.body : text2;
61348
61841
  if (armedSecretCaptures.has(chat_id)) {
61349
- const consumed = await captureProvidedSecret(ctx, chat_id, msgId ?? undefined, text);
61842
+ const consumed = await captureProvidedSecret(ctx, chat_id, msgId ?? undefined, text2);
61350
61843
  if (consumed)
61351
61844
  return;
61352
61845
  }
@@ -61516,7 +62009,7 @@ ${preBlock(write.output)}`;
61516
62009
  chat_id,
61517
62010
  message_id: msgId,
61518
62011
  thread_id: messageThreadId ?? null,
61519
- inbound_classified_as_status_query: classifyInbound(text).isStatusQuery
62012
+ inbound_classified_as_status_query: classifyInbound(text2).isStatusQuery
61520
62013
  });
61521
62014
  const agentDir = resolveAgentDirFromEnv();
61522
62015
  if (agentDir != null) {
@@ -61762,21 +62255,21 @@ function formatSwitchroomOutput(output, maxLen = 4000) {
61762
62255
  return trimmed.slice(0, maxLen - 20) + `
61763
62256
  ... (truncated)`;
61764
62257
  }
61765
- function stripAnsi2(text) {
61766
- return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
62258
+ function stripAnsi2(text2) {
62259
+ return text2.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
61767
62260
  }
61768
- function escapeHtmlForTg(text) {
61769
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
62261
+ function escapeHtmlForTg(text2) {
62262
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
61770
62263
  }
61771
- function preBlock(text) {
61772
- return "<pre>" + escapeHtmlForTg(text) + "</pre>";
62264
+ function preBlock(text2) {
62265
+ return "<pre>" + escapeHtmlForTg(text2) + "</pre>";
61773
62266
  }
61774
- async function switchroomReply(ctx, text, options = {}) {
62267
+ async function switchroomReply(ctx, text2, options = {}) {
61775
62268
  const chatId = String(ctx.chat.id);
61776
62269
  const baseThreadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
61777
62270
  const routedOpts = options.classification ? slashCommandReplyOpts(ctx, options.classification) : {};
61778
62271
  const threadId = routedOpts.message_thread_id ?? baseThreadId;
61779
- await ctx.reply(text, {
62272
+ await ctx.reply(text2, {
61780
62273
  ...threadId != null ? { message_thread_id: threadId } : {},
61781
62274
  ...options.html ? { parse_mode: "HTML", link_preview_options: { is_disabled: true } } : {},
61782
62275
  ...options.reply_markup ? { reply_markup: options.reply_markup } : {}
@@ -61800,8 +62293,8 @@ function getCommandArgs(ctx) {
61800
62293
  const fromMatch = typeof ctx.match === "string" ? ctx.match.trim() : "";
61801
62294
  if (fromMatch)
61802
62295
  return fromMatch;
61803
- const text = ctx.msg?.text ?? ctx.message?.text ?? "";
61804
- const m = text.match(/^\/\S+\s+([\s\S]*)$/);
62296
+ const text2 = ctx.msg?.text ?? ctx.message?.text ?? "";
62297
+ const m = text2.match(/^\/\S+\s+([\s\S]*)$/);
61805
62298
  return m ? m[1].trim() : "";
61806
62299
  }
61807
62300
  function assertSafeAgentName(name) {
@@ -61927,6 +62420,40 @@ function resolveAgentSupergroupChatId() {
61927
62420
  return;
61928
62421
  }
61929
62422
  }
62423
+ function isSelfLinearAgentEnabled() {
62424
+ const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
62425
+ if (!agentName3)
62426
+ return false;
62427
+ try {
62428
+ const cfg = loadConfig2();
62429
+ const rawAgent = cfg.agents?.[agentName3];
62430
+ if (!rawAgent)
62431
+ return false;
62432
+ const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
62433
+ const la = resolved.channels?.telegram?.linear_agent;
62434
+ return la?.enabled === true;
62435
+ } catch {
62436
+ return false;
62437
+ }
62438
+ }
62439
+ async function runLinearAuthWatch() {
62440
+ const agent = process.env.SWITCHROOM_AGENT_NAME;
62441
+ if (!agent)
62442
+ return;
62443
+ const io = brokerRefreshIO(agent);
62444
+ const status = await runLinearAuthCheck({
62445
+ agent,
62446
+ linearEnabled: isSelfLinearAgentEnabled,
62447
+ readBundle: io.readBundle,
62448
+ refresh: () => performLinearRefresh2(io),
62449
+ onAuthDead: notifyLinearAuthDead,
62450
+ log: (s) => process.stderr.write(s)
62451
+ });
62452
+ if (status !== "disabled" && status !== "fresh") {
62453
+ process.stderr.write(`telegram gateway: linear-auth-watch agent=${agent} status=${status}
62454
+ `);
62455
+ }
62456
+ }
61930
62457
  function stampUserRestartReason(reason) {
61931
62458
  try {
61932
62459
  writeCleanShutdownMarker(GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH, {
@@ -62039,9 +62566,9 @@ function notifyDetachedFailure(chatId, threadId, label) {
62039
62566
  return ({ code, tail }) => {
62040
62567
  clearRestartMarker();
62041
62568
  const snippet = tail ? tail.slice(-800) : "(no output captured)";
62042
- const text = `\u274C <b>${escapeHtmlForTg(label)} failed</b> (exit ${code}):
62569
+ const text2 = `\u274C <b>${escapeHtmlForTg(label)} failed</b> (exit ${code}):
62043
62570
  ` + preBlock(snippet);
62044
- swallowingApiCall(() => lockedBot.api.sendMessage(chatId, text, {
62571
+ swallowingApiCall(() => lockedBot.api.sendMessage(chatId, text2, {
62045
62572
  parse_mode: "HTML",
62046
62573
  link_preview_options: { is_disabled: true },
62047
62574
  ...threadId != null ? { message_thread_id: threadId } : {}
@@ -62475,7 +63002,8 @@ async function buildLiveProbeRows(agentName3) {
62475
63002
  "scheduler",
62476
63003
  "broker",
62477
63004
  "kernel",
62478
- "skills"
63005
+ "skills",
63006
+ "connections"
62479
63007
  ];
62480
63008
  for (const k of order) {
62481
63009
  const r = probes[k];
@@ -62550,7 +63078,7 @@ bot.command("inject", async (ctx) => {
62550
63078
  await handleInjectCommand(ctx, {
62551
63079
  isAuthorized: isAuthorizedSender,
62552
63080
  inject: injectSlashCommand,
62553
- reply: async (ctx2, text, opts) => switchroomReply(ctx2, text, { html: opts?.html }),
63081
+ reply: async (ctx2, text2, opts) => switchroomReply(ctx2, text2, { html: opts?.html }),
62554
63082
  getAgentName: getMyAgentName,
62555
63083
  getArgs: getCommandArgs,
62556
63084
  escapeHtml: escapeHtmlForTg,
@@ -62603,8 +63131,8 @@ function modelMenuReplyMarkup(reply) {
62603
63131
  bot.command("model", async (ctx) => {
62604
63132
  if (!isAuthorizedSender(ctx))
62605
63133
  return;
62606
- const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
62607
- const parsed = parseModelCommand(text) ?? { kind: "show" };
63134
+ const text2 = ctx.message?.text ?? ctx.channelPost?.text ?? "";
63135
+ const parsed = parseModelCommand(text2) ?? { kind: "show" };
62608
63136
  const deps = buildModelDeps();
62609
63137
  if (parsed.kind === "show" && process.env.SWITCHROOM_MODEL_MENU !== "0") {
62610
63138
  const menu = await buildModelMenu(deps);
@@ -62639,8 +63167,8 @@ function effortMenuReplyMarkup(reply) {
62639
63167
  bot.command("effort", async (ctx) => {
62640
63168
  if (!isAuthorizedSender(ctx))
62641
63169
  return;
62642
- const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
62643
- const parsed = parseEffortCommand(text) ?? { kind: "show" };
63170
+ const text2 = ctx.message?.text ?? ctx.channelPost?.text ?? "";
63171
+ const parsed = parseEffortCommand(text2) ?? { kind: "show" };
62644
63172
  const deps = buildEffortDeps();
62645
63173
  if (parsed.kind === "show") {
62646
63174
  const menu = buildEffortMenu(deps);
@@ -63071,6 +63599,7 @@ async function handlePermissionSlash(ctx, behavior) {
63071
63599
  await switchroomReply(ctx, `No pending permission for id <code>${escapeHtmlForTg(request_id)}</code>. It may have already been answered or timed out.`, { html: true });
63072
63600
  return;
63073
63601
  }
63602
+ clearPermissionTimeoutSuppression("operator answered via /approve|/deny");
63074
63603
  dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior });
63075
63604
  resumeReactionAfterVerdict();
63076
63605
  postPermissionResumeMessage({
@@ -63609,8 +64138,8 @@ bot.command("auth", async (ctx) => {
63609
64138
  }
63610
64139
  return;
63611
64140
  }
63612
- const text = ctx.message?.text ?? "";
63613
- const parsed = parseAuthCommand(text);
64141
+ const text2 = ctx.message?.text ?? "";
64142
+ const parsed = parseAuthCommand(text2);
63614
64143
  if (!parsed)
63615
64144
  return;
63616
64145
  const currentAgent = getMyAgentName();
@@ -64249,14 +64778,14 @@ async function grantWizardStep2(ctx, chatId, agent, wizardMsgId) {
64249
64778
  }
64250
64779
  const selected = new Set;
64251
64780
  const kb = buildGrantKeysKeyboard(keys, selected);
64252
- const text = `<b>Grant capability token \u2014 Step 2/3</b>
64781
+ const text2 = `<b>Grant capability token \u2014 Step 2/3</b>
64253
64782
 
64254
64783
  Which keys for <code>${escapeHtmlForTg(agent)}</code>?
64255
64784
  <i>Tap to toggle; tap Continue when done.</i>`;
64256
64785
  if (wizardMsgId != null) {
64257
- await ctx.api.editMessageText(chatId, wizardMsgId, text, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
64786
+ await ctx.api.editMessageText(chatId, wizardMsgId, text2, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
64258
64787
  } else {
64259
- const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb });
64788
+ const sent = await switchroomReply(ctx, text2, { html: true, reply_markup: kb });
64260
64789
  wizardMsgId = sent?.message_id;
64261
64790
  }
64262
64791
  pendingVaultOps.set(chatId, {
@@ -64273,7 +64802,7 @@ async function grantWizardStep3(ctx, chatId, state4) {
64273
64802
  const kb = buildGrantDurationKeyboard();
64274
64803
  const keyList = state4.selectedKeys.map((k) => `\u2022 <code>${escapeHtmlForTg(k)}</code>`).join(`
64275
64804
  `);
64276
- const text = `<b>Grant capability token \u2014 Step 3/3</b>
64805
+ const text2 = `<b>Grant capability token \u2014 Step 3/3</b>
64277
64806
 
64278
64807
  Keys for <code>${escapeHtmlForTg(state4.agent)}</code>:
64279
64808
  ${keyList}
@@ -64281,9 +64810,9 @@ ${keyList}
64281
64810
  How long should this grant be valid?`;
64282
64811
  const msgId = state4.wizardMsgId;
64283
64812
  if (msgId != null) {
64284
- await ctx.api.editMessageText(chatId, msgId, text, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
64813
+ await ctx.api.editMessageText(chatId, msgId, text2, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
64285
64814
  } else {
64286
- const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb });
64815
+ const sent = await switchroomReply(ctx, text2, { html: true, reply_markup: kb });
64287
64816
  state4.wizardMsgId = sent?.message_id;
64288
64817
  }
64289
64818
  pendingVaultOps.set(chatId, { ...state4, step: "duration" });
@@ -64293,7 +64822,7 @@ async function grantWizardConfirm(ctx, chatId, state4) {
64293
64822
  const expiresLabel = formatGrantExpiry(state4.ttlSeconds);
64294
64823
  const keyList = state4.selectedKeys.map((k) => `\u2022 <code>${escapeHtmlForTg(k)}</code>`).join(`
64295
64824
  `);
64296
- const text = [
64825
+ const text2 = [
64297
64826
  "<b>Confirm grant</b>",
64298
64827
  "",
64299
64828
  `Agent: <code>${escapeHtmlForTg(state4.agent)}</code>`,
@@ -64306,9 +64835,9 @@ ${keyList}`,
64306
64835
  `);
64307
64836
  const msgId = state4.wizardMsgId;
64308
64837
  if (msgId != null) {
64309
- await ctx.api.editMessageText(chatId, msgId, text, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
64838
+ await ctx.api.editMessageText(chatId, msgId, text2, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
64310
64839
  } else {
64311
- const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb });
64840
+ const sent = await switchroomReply(ctx, text2, { html: true, reply_markup: kb });
64312
64841
  state4.wizardMsgId = sent?.message_id;
64313
64842
  }
64314
64843
  const kernelRequestId = await mintGrantWizardKernelRequest(state4.agent, loadAccess().allowFrom, state4.selectedKeys, state4.ttlSeconds ?? null);
@@ -64778,7 +65307,7 @@ async function handleAuthDashboardCallback(ctx) {
64778
65307
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
64779
65308
  const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState4, buildSnapshotKeyboard: buildSnapshotKeyboard3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
64780
65309
  const snapshots = buildSnapshotsFromState4(state4, quotas);
64781
- const text = renderAuthSnapshotFormat23(snapshots, {
65310
+ const text2 = renderAuthSnapshotFormat23(snapshots, {
64782
65311
  tz,
64783
65312
  now: new Date,
64784
65313
  liveProbedAtMs: Date.now()
@@ -64793,7 +65322,7 @@ async function handleAuthDashboardCallback(ctx) {
64793
65322
  }));
64794
65323
  const msg = ctx.callbackQuery?.message;
64795
65324
  if (msg) {
64796
- await swallowingApiCall(() => bot.api.editMessageText(msg.chat.id, msg.message_id, text, {
65325
+ await swallowingApiCall(() => bot.api.editMessageText(msg.chat.id, msg.message_id, text2, {
64797
65326
  parse_mode: "HTML",
64798
65327
  reply_markup: { inline_keyboard }
64799
65328
  }), { chat_id: String(msg.chat.id), verb: "auth:refresh:edit" });
@@ -65175,12 +65704,12 @@ bot.command("usage", async (ctx) => {
65175
65704
  const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState4 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
65176
65705
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
65177
65706
  const snapshots = buildSnapshotsFromState4(state4, quotas);
65178
- const text = renderAuthSnapshotFormat23(snapshots, {
65707
+ const text2 = renderAuthSnapshotFormat23(snapshots, {
65179
65708
  tz,
65180
65709
  now: new Date,
65181
65710
  liveProbedAtMs: Date.now()
65182
65711
  });
65183
- await switchroomReply(ctx, text, { html: true });
65712
+ await switchroomReply(ctx, text2, { html: true });
65184
65713
  return;
65185
65714
  }
65186
65715
  }
@@ -65869,6 +66398,7 @@ ${editLabel}` : editLabel,
65869
66398
  });
65870
66399
  return;
65871
66400
  }
66401
+ clearPermissionTimeoutSuppression("operator answered a permission card");
65872
66402
  const pd = pendingPermissions.get(request_id);
65873
66403
  const resumeAction = pd ? naturalAction(pd.tool_name, pd.input_preview) : "";
65874
66404
  const scopedTtl = scopedApprovalTtlMs();
@@ -65956,10 +66486,10 @@ bot.on("message:voice", async (ctx) => {
65956
66486
  if (voiceIn?.enabled && voiceIn?.provider === "openai") {
65957
66487
  const transcript = await maybeTranscribeVoice(voice.file_id, voice.mime_type, voiceIn.language);
65958
66488
  if (transcript != null) {
65959
- const text = ctx.message.caption ? `${ctx.message.caption}
66489
+ const text2 = ctx.message.caption ? `${ctx.message.caption}
65960
66490
 
65961
66491
  [voice transcript] ${transcript}` : `[voice transcript] ${transcript}`;
65962
- await handleInboundCoalesced(ctx, text, undefined, {
66492
+ await handleInboundCoalesced(ctx, text2, undefined, {
65963
66493
  kind: "voice",
65964
66494
  file_id: voice.file_id,
65965
66495
  size: voice.file_size,
@@ -66045,14 +66575,14 @@ bot.on("message:sticker", async (ctx) => {
66045
66575
  parts.push(sticker.emoji);
66046
66576
  if (sticker.set_name)
66047
66577
  parts.push(`from "${sticker.set_name}"`);
66048
- const text = parts.length > 0 ? `(sticker \u2014 ${parts.join(" ")})` : "(sticker)";
66049
- await handleInboundCoalesced(ctx, text, undefined, { kind: "sticker", file_id: sticker.file_id, size: sticker.file_size });
66578
+ const text2 = parts.length > 0 ? `(sticker \u2014 ${parts.join(" ")})` : "(sticker)";
66579
+ await handleInboundCoalesced(ctx, text2, undefined, { kind: "sticker", file_id: sticker.file_id, size: sticker.file_size });
66050
66580
  });
66051
66581
  bot.on("message:animation", async (ctx) => {
66052
66582
  const animation = ctx.message.animation;
66053
66583
  const caption = ctx.message.caption;
66054
- const text = caption ? `(gif) ${caption}` : "(gif)";
66055
- await handleInboundCoalesced(ctx, text, undefined, {
66584
+ const text2 = caption ? `(gif) ${caption}` : "(gif)";
66585
+ await handleInboundCoalesced(ctx, text2, undefined, {
66056
66586
  kind: "animation",
66057
66587
  file_id: animation.file_id,
66058
66588
  size: animation.file_size,
@@ -66127,10 +66657,10 @@ bot.on("message:contact", async (ctx) => {
66127
66657
  const last = safeName(c.last_name) ?? "";
66128
66658
  const name = [first, last].filter(Boolean).join(" ") || "?";
66129
66659
  const userIdPart = c.user_id != null ? ` user_id=${c.user_id}` : "";
66130
- const text = `(contact: name="${name}" phone="${phone}"${userIdPart})`;
66660
+ const text2 = `(contact: name="${name}" phone="${phone}"${userIdPart})`;
66131
66661
  process.stderr.write(`telegram gateway: inbound contact from chat=${ctx.chat?.id ?? "?"}
66132
66662
  `);
66133
- await handleInbound(ctx, text, undefined);
66663
+ await handleInbound(ctx, text2, undefined);
66134
66664
  } catch (err) {
66135
66665
  process.stderr.write(`telegram gateway: contact handler error: ${err.message}
66136
66666
  `);
@@ -66142,10 +66672,10 @@ bot.on("message:location", async (ctx) => {
66142
66672
  const lat = typeof loc.latitude === "number" ? loc.latitude.toFixed(6) : "?";
66143
66673
  const lon = typeof loc.longitude === "number" ? loc.longitude.toFixed(6) : "?";
66144
66674
  const live = loc.live_period != null ? ` live_period=${loc.live_period}s` : "";
66145
- const text = `(location: lat=${lat} lon=${lon}${live})`;
66675
+ const text2 = `(location: lat=${lat} lon=${lon}${live})`;
66146
66676
  process.stderr.write(`telegram gateway: inbound location from chat=${ctx.chat?.id ?? "?"}
66147
66677
  `);
66148
- await handleInbound(ctx, text, undefined);
66678
+ await handleInbound(ctx, text2, undefined);
66149
66679
  } catch (err) {
66150
66680
  process.stderr.write(`telegram gateway: location handler error: ${err.message}
66151
66681
  `);
@@ -66158,10 +66688,10 @@ bot.on("message:venue", async (ctx) => {
66158
66688
  const address = safeName(v.address) ?? "?";
66159
66689
  const lat = typeof v.location?.latitude === "number" ? v.location.latitude.toFixed(6) : "?";
66160
66690
  const lon = typeof v.location?.longitude === "number" ? v.location.longitude.toFixed(6) : "?";
66161
- const text = `(venue: title="${title}" address="${address}" lat=${lat} lon=${lon})`;
66691
+ const text2 = `(venue: title="${title}" address="${address}" lat=${lat} lon=${lon})`;
66162
66692
  process.stderr.write(`telegram gateway: inbound venue from chat=${ctx.chat?.id ?? "?"}
66163
66693
  `);
66164
- await handleInbound(ctx, text, undefined);
66694
+ await handleInbound(ctx, text2, undefined);
66165
66695
  } catch (err) {
66166
66696
  process.stderr.write(`telegram gateway: venue handler error: ${err.message}
66167
66697
  `);
@@ -66174,10 +66704,10 @@ bot.on("message:poll", async (ctx) => {
66174
66704
  const optsCount = Array.isArray(p.options) ? p.options.length : 0;
66175
66705
  const optsList = Array.isArray(p.options) ? p.options.slice(0, 10).map((o) => safeName(o.text) ?? "?").join(" | ") : "";
66176
66706
  const anon = p.is_anonymous ? " anonymous" : "";
66177
- const text = `(poll: question="${q}" options=${optsCount}${anon}${optsList ? ` choices=[${optsList}]` : ""})`;
66707
+ const text2 = `(poll: question="${q}" options=${optsCount}${anon}${optsList ? ` choices=[${optsList}]` : ""})`;
66178
66708
  process.stderr.write(`telegram gateway: inbound poll from chat=${ctx.chat?.id ?? "?"}
66179
66709
  `);
66180
- await handleInbound(ctx, text, undefined);
66710
+ await handleInbound(ctx, text2, undefined);
66181
66711
  } catch (err) {
66182
66712
  process.stderr.write(`telegram gateway: poll handler error: ${err.message}
66183
66713
  `);
@@ -66189,10 +66719,10 @@ bot.on("message:web_app_data", async (ctx) => {
66189
66719
  const raw = typeof w.data === "string" ? w.data : "";
66190
66720
  const data = raw.length > 4096 ? raw.slice(0, 4096) + "\u2026(truncated)" : raw;
66191
66721
  const button = safeName(w.button_text) ?? "?";
66192
- const text = `(web_app_data: button="${button}" data=${JSON.stringify(data)})`;
66722
+ const text2 = `(web_app_data: button="${button}" data=${JSON.stringify(data)})`;
66193
66723
  process.stderr.write(`telegram gateway: inbound web_app_data from chat=${ctx.chat?.id ?? "?"} button="${button}" bytes=${raw.length}
66194
66724
  `);
66195
- await handleInbound(ctx, text, undefined);
66725
+ await handleInbound(ctx, text2, undefined);
66196
66726
  } catch (err) {
66197
66727
  process.stderr.write(`telegram gateway: web_app_data handler error: ${err.message}
66198
66728
  `);
@@ -66203,10 +66733,10 @@ bot.on("message:users_shared", async (ctx) => {
66203
66733
  const u = ctx.message.users_shared;
66204
66734
  const users = Array.isArray(u.users) ? u.users : [];
66205
66735
  const ids = users.map((usr) => String(usr.user_id ?? "?")).join(",");
66206
- const text = `(users_shared: request_id=${u.request_id ?? "?"} user_ids=[${ids}] count=${users.length})`;
66736
+ const text2 = `(users_shared: request_id=${u.request_id ?? "?"} user_ids=[${ids}] count=${users.length})`;
66207
66737
  process.stderr.write(`telegram gateway: inbound users_shared from chat=${ctx.chat?.id ?? "?"} count=${users.length}
66208
66738
  `);
66209
- await handleInbound(ctx, text, undefined);
66739
+ await handleInbound(ctx, text2, undefined);
66210
66740
  } catch (err) {
66211
66741
  process.stderr.write(`telegram gateway: users_shared handler error: ${err.message}
66212
66742
  `);
@@ -66217,10 +66747,10 @@ bot.on("message:chat_shared", async (ctx) => {
66217
66747
  const c = ctx.message.chat_shared;
66218
66748
  const title = safeName(c.title) ?? "";
66219
66749
  const titlePart = title ? ` title="${title}"` : "";
66220
- const text = `(chat_shared: request_id=${c.request_id ?? "?"} chat_id=${c.chat_id ?? "?"}${titlePart})`;
66750
+ const text2 = `(chat_shared: request_id=${c.request_id ?? "?"} chat_id=${c.chat_id ?? "?"}${titlePart})`;
66221
66751
  process.stderr.write(`telegram gateway: inbound chat_shared from chat=${ctx.chat?.id ?? "?"} shared_chat_id=${c.chat_id ?? "?"}
66222
66752
  `);
66223
- await handleInbound(ctx, text, undefined);
66753
+ await handleInbound(ctx, text2, undefined);
66224
66754
  } catch (err) {
66225
66755
  process.stderr.write(`telegram gateway: chat_shared handler error: ${err.message}
66226
66756
  `);
@@ -66401,7 +66931,7 @@ function maybeDispatchReaction(args) {
66401
66931
  `);
66402
66932
  }
66403
66933
  }
66404
- const { text, meta } = buildReactionDispatchInbound({
66934
+ const { text: text2, meta } = buildReactionDispatchInbound({
66405
66935
  emoji: args.emoji,
66406
66936
  chatId: args.chatId,
66407
66937
  messageId: args.messageId,
@@ -66419,7 +66949,7 @@ function maybeDispatchReaction(args) {
66419
66949
  user: args.user,
66420
66950
  userId: args.userId,
66421
66951
  ts,
66422
- text,
66952
+ text: text2,
66423
66953
  meta
66424
66954
  };
66425
66955
  const delivered = ipcServer.sendToAgent(agentName3, inbound);
@@ -66439,7 +66969,7 @@ function flushReactionBatch(batch) {
66439
66969
  return;
66440
66970
  }
66441
66971
  const head = batch.reactions[batch.reactions.length - 1];
66442
- const text = buildReactionInboundText(batch);
66972
+ const text2 = buildReactionInboundText(batch);
66443
66973
  const meta = buildReactionInboundMeta(batch);
66444
66974
  const ts = Date.now();
66445
66975
  const inbound = {
@@ -66450,7 +66980,7 @@ function flushReactionBatch(batch) {
66450
66980
  user: head.user,
66451
66981
  userId: head.userId,
66452
66982
  ts,
66453
- text,
66983
+ text: text2,
66454
66984
  meta
66455
66985
  };
66456
66986
  const delivered = ipcServer.sendToAgent(agentName3, inbound);
@@ -66675,6 +67205,7 @@ async function shutdown(signal) {
66675
67205
  pendingReauthFlows.clear();
66676
67206
  pendingVaultOps.clear();
66677
67207
  pendingPermissions.clear();
67208
+ permissionTimeoutSignatures.clear();
66678
67209
  try {
66679
67210
  await ipcServer.close();
66680
67211
  } catch (err) {
@@ -66988,6 +67519,21 @@ var didOneTimeSetup = false;
66988
67519
  });
66989
67520
  }, QUOTA_WATCH_POLL_MS).unref();
66990
67521
  }
67522
+ const LINEAR_AUTH_WATCH_POLL_MS = Number(process.env.SWITCHROOM_LINEAR_AUTH_WATCH_POLL_MS ?? 21600000);
67523
+ if (LINEAR_AUTH_WATCH_POLL_MS > 0) {
67524
+ setTimeout(() => {
67525
+ runLinearAuthWatch().catch((err) => {
67526
+ process.stderr.write(`telegram gateway: linear-auth-watch initial run failed: ${err}
67527
+ `);
67528
+ });
67529
+ }, 35000);
67530
+ setInterval(() => {
67531
+ runLinearAuthWatch().catch((err) => {
67532
+ process.stderr.write(`telegram gateway: linear-auth-watch scheduled run failed: ${err}
67533
+ `);
67534
+ });
67535
+ }, LINEAR_AUTH_WATCH_POLL_MS).unref();
67536
+ }
66991
67537
  const RESTART_WATCHDOG_POLL_MS = Number(process.env.SWITCHROOM_RESTART_WATCHDOG_POLL_MS ?? 30000);
66992
67538
  const watchdogAgentName = process.env.SWITCHROOM_AGENT_NAME;
66993
67539
  const watchdogDockerMode = process.env.SWITCHROOM_RUNTIME === "docker";
@@ -67022,11 +67568,11 @@ var didOneTimeSetup = false;
67022
67568
  const orphanStatusEnabled = isOrphanSubagentStatusEnabled(process.env.SWITCHROOM_ORPHAN_SUBAGENT_STATUS);
67023
67569
  const workerActivityFeed = createWorkerActivityFeed({
67024
67570
  bot: {
67025
- sendMessage: async (cid, text, sendOpts) => {
67026
- const sent = await robustApiCall(() => lockedBot.api.sendMessage(cid, text, sendOpts), { chat_id: cid, verb: "worker-feed" });
67571
+ sendMessage: async (cid, text2, sendOpts) => {
67572
+ const sent = await robustApiCall(() => lockedBot.api.sendMessage(cid, text2, sendOpts), { chat_id: cid, verb: "worker-feed" });
67027
67573
  return sent;
67028
67574
  },
67029
- editMessageText: (cid, mid, text, editOpts) => robustApiCall(() => lockedBot.api.editMessageText(cid, mid, text, editOpts), { chat_id: cid, verb: "worker-feed" })
67575
+ editMessageText: (cid, mid, text2, editOpts) => robustApiCall(() => lockedBot.api.editMessageText(cid, mid, text2, editOpts), { chat_id: cid, verb: "worker-feed" })
67030
67576
  },
67031
67577
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}
67032
67578
  `)