switchroom 0.13.53 → 0.13.55

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.
@@ -23607,7 +23607,7 @@ var init_dist = __esm(() => {
23607
23607
  });
23608
23608
 
23609
23609
  // ../src/config/schema.ts
23610
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
23610
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
23611
23611
  var init_schema = __esm(() => {
23612
23612
  init_zod();
23613
23613
  CodeRepoEntrySchema = exports_external.object({
@@ -23648,7 +23648,8 @@ var init_schema = __esm(() => {
23648
23648
  recall: exports_external.object({
23649
23649
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
23650
23650
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
23651
- min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default \u2014 " + "current behaviour). Try 0.10\u20130.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`.")
23651
+ min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default \u2014 " + "current behaviour). Try 0.10\u20130.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
23652
+ topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
23652
23653
  }).optional().describe("Auto-recall tuning knobs")
23653
23654
  }).optional();
23654
23655
  HookEntrySchema = exports_external.object({
@@ -23791,6 +23792,16 @@ var init_schema = __esm(() => {
23791
23792
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
23792
23793
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC \u00a76.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false \u2014 personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
23793
23794
  }).optional();
23795
+ NotionWorkspaceConfigSchema = exports_external.object({
23796
+ vault_key: exports_external.string().min(1).default("notion/integration-token").describe("Vault key holding the Notion internal-integration token. Default " + "`notion/integration-token`. Override only for non-standard vault " + "layouts. The broker's --allow ACL on this key is the authoritative " + "list of which agents may receive the token."),
23797
+ databases: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
23798
+ message: "notion_workspace.databases friendly names must match " + "/^[a-z0-9][a-z0-9_-]{0,62}$/ \u2014 lowercase letters, digits, " + "hyphens, underscores. Got: '%s'."
23799
+ }), exports_external.string().regex(/^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/, {
23800
+ message: "notion_workspace.databases values must be Notion database " + "UUIDs (32 hex characters, optional dashes)."
23801
+ })).default({}).describe("Friendly-name \u2192 Notion database UUID map. Operator-managed; agents " + "reference databases by friendly name only \u2014 they never see or type " + "UUIDs. Populate via `switchroom notion list-dbs` (PR 4) after " + "vault-putting the integration token and sharing DBs with the " + "integration in Notion's UI."),
23802
+ mcp_version: exports_external.string().min(1).optional().describe("Optional pin for the upstream `@notionhq/notion-mcp-server` npm " + "package version. Default is the build-time `NOTION_MCP_PINNED_VERSION` " + "constant. Override only when reproducing operator-specific bugs."),
23803
+ rate_limit_rps: exports_external.number().int().positive().max(10).optional().describe("Optional global rate-limit budget in requests per second across all " + "switchroom agents sharing this integration token. Defaults to 3 " + "(Notion's documented public-API limit). Lower it if you also use " + "the integration token from outside switchroom and need to share " + "budget. Higher than 10 is rejected \u2014 if you think you need it, " + "your usage probably needs a workspace-tier upgrade with Notion.")
23804
+ }).optional();
23794
23805
  AgentGoogleWorkspaceConfigSchema = exports_external.object({
23795
23806
  account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
23796
23807
  message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
@@ -23804,6 +23815,13 @@ var init_schema = __esm(() => {
23804
23815
  }).transform((v) => v.trim().toLowerCase()).optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized)."),
23805
23816
  org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 \u00a76.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
23806
23817
  }).optional();
23818
+ AgentNotionWorkspaceConfigSchema = exports_external.object({
23819
+ databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
23820
+ message: "notion_workspace.databases entries must be friendly names " + "matching /^[a-z0-9][a-z0-9_-]{0,62}$/ \u2014 these must appear as " + "keys in top-level notion_workspace.databases."
23821
+ })).min(1, {
23822
+ message: "notion_workspace.databases must list at least one friendly " + "name. An empty list rejects every Notion tool call \u2014 if you " + "want to remove this agent's Notion access, delete the entire " + "notion_workspace block instead."
23823
+ }).optional().describe("Optional per-agent allowlist of database friendly names this " + "agent may read/write. Each name must exist as a key in top-level " + "notion_workspace.databases. Omit the field (or leave it undefined) " + "to grant access to every DB the upstream integration can see \u2014 " + "appropriate for an admin/orchestrator agent. Set the list to " + "narrow access for specialist agents.")
23824
+ }).optional();
23807
23825
  ReactionsSchema = exports_external.object({
23808
23826
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
23809
23827
  trigger_emojis: exports_external.array(exports_external.string()).optional().describe("Emoji allowlist that triggers a synthetic inbound when reacted " + "to a bot message. Default ['\uD83D\uDC4E', '\u274c', '\uD83D\uDC4D', '\u2705']. Cascade " + "mode: REPLACE (not union) \u2014 setting this at a layer replaces " + "lower layers entirely, so an operator can narrow to [] to " + "disable triggering without flipping `enabled`."),
@@ -23940,6 +23958,7 @@ var init_schema = __esm(() => {
23940
23958
  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."),
23941
23959
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides \u2014 currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent \u2014 they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
23942
23960
  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."),
23961
+ 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."),
23943
23962
  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({
23944
23963
  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."),
23945
23964
  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.")
@@ -24063,6 +24082,7 @@ var init_schema = __esm(() => {
24063
24082
  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."),
24064
24083
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
24065
24084
  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."),
24085
+ 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."),
24066
24086
  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."),
24067
24087
  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)."),
24068
24088
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a \u2014 disabled by default)."),
@@ -24570,6 +24590,34 @@ var init_merge = __esm(() => {
24570
24590
  })(mergeAgentConfig ||= {});
24571
24591
  });
24572
24592
 
24593
+ // ../src/config/notion-workspace-acl.ts
24594
+ function validateNotionWorkspaceConfig(config) {
24595
+ const issues = [];
24596
+ const dbMap = config.notion_workspace?.databases ?? {};
24597
+ const known = new Set(Object.keys(dbMap));
24598
+ for (const [agentName3, agentRaw] of Object.entries(config.agents ?? {})) {
24599
+ if (!agentRaw)
24600
+ continue;
24601
+ const dbFilter = agentRaw.notion_workspace?.databases;
24602
+ if (dbFilter === undefined)
24603
+ continue;
24604
+ if (dbFilter.length === 0) {
24605
+ issues.push(` agents.${agentName3}.notion_workspace.databases is an empty ` + `list. Delete the entire notion_workspace block to remove Notion ` + `access, or list at least one database friendly name.`);
24606
+ continue;
24607
+ }
24608
+ if (config.notion_workspace === undefined) {
24609
+ issues.push(` agents.${agentName3}.notion_workspace is set but the top-level ` + `notion_workspace block is missing. Configure the integration ` + `globally first (vault_key + databases map), then grant per-agent ` + `access.`);
24610
+ continue;
24611
+ }
24612
+ for (const name of dbFilter) {
24613
+ if (!known.has(name)) {
24614
+ issues.push(` agents.${agentName3}.notion_workspace.databases references ` + `unknown database "${name}". Add it to top-level ` + `notion_workspace.databases (run \`switchroom notion list-dbs\` ` + `to see the UUIDs the integration can read), or remove the ` + `reference.`);
24615
+ }
24616
+ }
24617
+ }
24618
+ return issues;
24619
+ }
24620
+
24573
24621
  // ../src/config/loader.ts
24574
24622
  import { readFileSync as readFileSync5, existsSync as existsSync9 } from "node:fs";
24575
24623
  import { homedir as homedir5 } from "node:os";
@@ -24684,6 +24732,10 @@ function loadConfig(configPath) {
24684
24732
  }
24685
24733
  applyAgentOverlays(config);
24686
24734
  validateAllCronTopicAliases(config, filePath);
24735
+ const notionIssues = validateNotionWorkspaceConfig(config);
24736
+ if (notionIssues.length > 0) {
24737
+ throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
24738
+ }
24687
24739
  return config;
24688
24740
  }
24689
24741
  function validateAllCronTopicAliases(config, filePath) {
@@ -27730,7 +27782,7 @@ var init_secretlint_source = __esm(() => {
27730
27782
  function escapeHtml8(s) {
27731
27783
  return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
27732
27784
  }
27733
- function truncate4(s, n) {
27785
+ function truncate5(s, n) {
27734
27786
  return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
27735
27787
  }
27736
27788
 
@@ -27833,7 +27885,7 @@ function writeQuotaCache(result, opts = {}) {
27833
27885
  if (result.status === "fail")
27834
27886
  return;
27835
27887
  const path = opts.path ?? defaultCachePath();
27836
- const ttlMs = opts.ttlMs ?? (result.rateLimited ? RATE_LIMIT_TTL_MS : DEFAULT_TTL_MS3);
27888
+ const ttlMs = opts.ttlMs ?? (result.rateLimited ? RATE_LIMIT_TTL_MS : DEFAULT_TTL_MS4);
27837
27889
  const now = opts.now ?? Date.now();
27838
27890
  const entry = {
27839
27891
  capturedAt: new Date(now).toISOString(),
@@ -27845,9 +27897,9 @@ function writeQuotaCache(result, opts = {}) {
27845
27897
  writeFileSync13(path, JSON.stringify(entry, null, 2), { mode: 384 });
27846
27898
  } catch {}
27847
27899
  }
27848
- var DEFAULT_TTL_MS3, RATE_LIMIT_TTL_MS;
27900
+ var DEFAULT_TTL_MS4, RATE_LIMIT_TTL_MS;
27849
27901
  var init_quota_cache = __esm(() => {
27850
- DEFAULT_TTL_MS3 = 5 * 60 * 1000;
27902
+ DEFAULT_TTL_MS4 = 5 * 60 * 1000;
27851
27903
  RATE_LIMIT_TTL_MS = 30 * 1000;
27852
27904
  });
27853
27905
 
@@ -43303,6 +43355,10 @@ function loadConfig2(configPath) {
43303
43355
  }
43304
43356
  applyAgentOverlays(config);
43305
43357
  validateAllCronTopicAliases2(config, filePath);
43358
+ const notionIssues = validateNotionWorkspaceConfig(config);
43359
+ if (notionIssues.length > 0) {
43360
+ throw new ConfigError2(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
43361
+ }
43306
43362
  return config;
43307
43363
  }
43308
43364
  function validateAllCronTopicAliases2(config, filePath) {
@@ -44339,6 +44395,17 @@ function validateClientMessage(msg) {
44339
44395
  return false;
44340
44396
  return true;
44341
44397
  }
44398
+ case "request_ms365_approval": {
44399
+ if (typeof m.correlationId !== "string" || m.correlationId.length === 0 || m.correlationId.length > 64)
44400
+ return false;
44401
+ if (typeof m.agentName !== "string" || !AGENT_NAME_RE3.test(m.agentName))
44402
+ return false;
44403
+ if (typeof m.preview !== "object" || m.preview === null)
44404
+ return false;
44405
+ if (m.ttlMs !== undefined && (typeof m.ttlMs !== "number" || !Number.isFinite(m.ttlMs) || m.ttlMs < 0))
44406
+ return false;
44407
+ return true;
44408
+ }
44342
44409
  default:
44343
44410
  return false;
44344
44411
  }
@@ -44357,6 +44424,7 @@ function createIpcServer(options) {
44357
44424
  onPtyPartial,
44358
44425
  onInjectInbound,
44359
44426
  onRequestDriveApproval,
44427
+ onRequestMs365Approval,
44360
44428
  onRequestConfigApproval,
44361
44429
  onRequestConfigFinalize,
44362
44430
  log = () => {},
@@ -44463,6 +44531,30 @@ function createIpcServer(options) {
44463
44531
  } catch {}
44464
44532
  }
44465
44533
  break;
44534
+ case "request_ms365_approval":
44535
+ if (onRequestMs365Approval) {
44536
+ onRequestMs365Approval(client3, msg).catch((err) => {
44537
+ log(`request_ms365_approval handler threw (client=${client3.id}): ${err.message}`);
44538
+ try {
44539
+ client3.send({
44540
+ type: "ms365_approval_posted",
44541
+ correlationId: msg.correlationId,
44542
+ ok: false,
44543
+ reason: `gateway handler error: ${err.message}`
44544
+ });
44545
+ } catch {}
44546
+ });
44547
+ } else {
44548
+ try {
44549
+ client3.send({
44550
+ type: "ms365_approval_posted",
44551
+ correlationId: msg.correlationId,
44552
+ ok: false,
44553
+ reason: "gateway not configured for MS-365 write approval"
44554
+ });
44555
+ } catch {}
44556
+ }
44557
+ break;
44466
44558
  case "request_config_approval":
44467
44559
  if (onRequestConfigApproval) {
44468
44560
  onRequestConfigApproval(client3, msg).catch((err) => {
@@ -44899,6 +44991,184 @@ function clampTtl(requested, fallback, min, max) {
44899
44991
  return t;
44900
44992
  }
44901
44993
 
44994
+ // gateway/ms365-write-approval.ts
44995
+ function validateMs365Preview(input) {
44996
+ if (!input || typeof input !== "object")
44997
+ return null;
44998
+ const o = input;
44999
+ if (typeof o.agentName !== "string" || o.agentName.length === 0)
45000
+ return null;
45001
+ if (typeof o.toolName !== "string" || o.toolName.length === 0)
45002
+ return null;
45003
+ if (typeof o.itemId !== "string" || o.itemId.length === 0)
45004
+ return null;
45005
+ if (typeof o.itemDisplayName !== "string")
45006
+ return null;
45007
+ if (typeof o.accountEmail !== "string")
45008
+ return null;
45009
+ const out = {
45010
+ agentName: o.agentName,
45011
+ toolName: o.toolName,
45012
+ itemId: o.itemId,
45013
+ itemDisplayName: o.itemDisplayName,
45014
+ accountEmail: o.accountEmail
45015
+ };
45016
+ if (typeof o.deepLink === "string")
45017
+ out.deepLink = o.deepLink;
45018
+ if (typeof o.sizeBytesBefore === "number")
45019
+ out.sizeBytesBefore = o.sizeBytesBefore;
45020
+ if (typeof o.sizeBytesAfter === "number")
45021
+ out.sizeBytesAfter = o.sizeBytesAfter;
45022
+ if (typeof o.agentRationale === "string")
45023
+ out.agentRationale = o.agentRationale;
45024
+ return out;
45025
+ }
45026
+ var DEFAULT_TTL_MS2 = 5 * 60 * 1000;
45027
+ var MAX_TTL_MS2 = 30 * 60 * 1000;
45028
+ var MIN_TTL_MS2 = 30 * 1000;
45029
+ function defaultKeyboard(requestId) {
45030
+ return {
45031
+ inline_keyboard: [
45032
+ [
45033
+ { text: "\u2705 Approve", callback_data: `apv:${requestId}:once` },
45034
+ { text: "\uD83D\uDEAB Deny", callback_data: `apv:${requestId}:deny` }
45035
+ ]
45036
+ ]
45037
+ };
45038
+ }
45039
+ function buildMs365CardText(p) {
45040
+ const lines = [];
45041
+ lines.push(`\uD83D\uDCC4 Microsoft 365 write approval`);
45042
+ lines.push("");
45043
+ lines.push(`Agent: ${truncate2(p.agentName, 64)}`);
45044
+ lines.push(`Tool: ${truncate2(p.toolName.replace(/^mcp__/, ""), 96)}`);
45045
+ lines.push(`Item: ${truncate2(p.itemDisplayName, 256)}`);
45046
+ if (p.itemId !== "(new)") {
45047
+ lines.push(`ID: ${truncate2(p.itemId, 96)}`);
45048
+ }
45049
+ lines.push(`Account: ${truncate2(p.accountEmail, 96)}`);
45050
+ if (typeof p.sizeBytesBefore === "number" || typeof p.sizeBytesAfter === "number") {
45051
+ const before = p.sizeBytesBefore ?? 0;
45052
+ const after = p.sizeBytesAfter ?? 0;
45053
+ const delta = after - before;
45054
+ const sign = delta >= 0 ? "+" : "";
45055
+ lines.push(`Size: ${humanBytes(before)} \u2192 ${humanBytes(after)} (${sign}${humanBytes(delta)})`);
45056
+ }
45057
+ if (p.deepLink) {
45058
+ lines.push(`Link: ${truncate2(p.deepLink, 256)}`);
45059
+ }
45060
+ if (p.agentRationale) {
45061
+ lines.push("");
45062
+ lines.push(`\uD83D\uDCAC ${truncate2(p.agentRationale, 512)}`);
45063
+ }
45064
+ lines.push("");
45065
+ lines.push("\u26a0\ufe0f Weak attestation (RFC \u00a78 v1): operator should click through to verify the actual change before approving. Structural diff coming v1.5.");
45066
+ return lines.join(`
45067
+ `);
45068
+ }
45069
+ function truncate2(s, n) {
45070
+ if (s.length <= n)
45071
+ return s;
45072
+ return s.slice(0, n - 1) + "\u2026";
45073
+ }
45074
+ function humanBytes(bytes) {
45075
+ const abs = Math.abs(bytes);
45076
+ if (abs < 1024)
45077
+ return `${bytes}B`;
45078
+ if (abs < 1024 * 1024)
45079
+ return `${(bytes / 1024).toFixed(1)}KB`;
45080
+ if (abs < 1024 * 1024 * 1024)
45081
+ return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
45082
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)}GB`;
45083
+ }
45084
+ function clampTtl2(requested, def, min, max) {
45085
+ if (typeof requested !== "number" || !Number.isFinite(requested))
45086
+ return def;
45087
+ return Math.max(min, Math.min(max, requested));
45088
+ }
45089
+ async function handleRequestMs365Approval(client3, msg, deps) {
45090
+ const log = deps.log ?? (() => {});
45091
+ const sendResponse = (ok, extra = {}) => {
45092
+ client3.send({
45093
+ type: "ms365_approval_posted",
45094
+ correlationId: msg.correlationId,
45095
+ ok,
45096
+ ...extra
45097
+ });
45098
+ };
45099
+ if (msg.agentName !== deps.agentName) {
45100
+ log(`ms365-approval: cross-agent request rejected (msg=${msg.agentName} vs gateway=${deps.agentName})`);
45101
+ sendResponse(false, { reason: "cross-agent request rejected" });
45102
+ return;
45103
+ }
45104
+ const preview = validateMs365Preview(msg.preview);
45105
+ if (!preview) {
45106
+ log("ms365-approval: invalid preview payload");
45107
+ sendResponse(false, { reason: "invalid preview payload" });
45108
+ return;
45109
+ }
45110
+ const allowFrom = deps.loadAllowFrom();
45111
+ if (allowFrom.length === 0) {
45112
+ log("ms365-approval: no operator allowFrom configured");
45113
+ sendResponse(false, { reason: "no operator allowFrom configured" });
45114
+ return;
45115
+ }
45116
+ const targetChat = deps.loadTargetChat();
45117
+ if (!targetChat) {
45118
+ log("ms365-approval: no target chat resolved");
45119
+ sendResponse(false, { reason: "no target chat resolved" });
45120
+ return;
45121
+ }
45122
+ const ttlMs = clampTtl2(msg.ttlMs, deps.defaultTtlMs ?? DEFAULT_TTL_MS2, deps.minTtlMs ?? MIN_TTL_MS2, deps.maxTtlMs ?? MAX_TTL_MS2);
45123
+ const scope = `ms-365:write:${preview.itemId}`;
45124
+ const why = preview.agentRationale ?? `${preview.toolName} on ${preview.itemDisplayName}`;
45125
+ let registered;
45126
+ try {
45127
+ registered = await deps.registerApproval({
45128
+ agent_unit: preview.agentName,
45129
+ scope,
45130
+ action: "write",
45131
+ approver_set: allowFrom,
45132
+ why,
45133
+ ttl_ms: ttlMs
45134
+ });
45135
+ } catch (err) {
45136
+ const msg2 = err instanceof Error ? err.message : String(err);
45137
+ log(`ms365-approval: kernel register failed \u2014 ${msg2}`);
45138
+ sendResponse(false, { reason: `kernel register failed: ${msg2}` });
45139
+ return;
45140
+ }
45141
+ if (!registered) {
45142
+ sendResponse(false, { reason: "kernel returned no request_id" });
45143
+ return;
45144
+ }
45145
+ const text = buildMs365CardText(preview);
45146
+ const replyMarkup = (deps.buildKeyboard ?? defaultKeyboard)(registered.request_id);
45147
+ let posted;
45148
+ try {
45149
+ posted = await deps.postCard({
45150
+ chatId: targetChat.chatId,
45151
+ threadId: targetChat.threadId,
45152
+ text,
45153
+ replyMarkup
45154
+ });
45155
+ } catch (err) {
45156
+ const m = err instanceof Error ? err.message : String(err);
45157
+ log(`ms365-approval: card post threw \u2014 ${m}`);
45158
+ sendResponse(false, { reason: `card post failed: ${m}` });
45159
+ return;
45160
+ }
45161
+ if (!posted) {
45162
+ sendResponse(false, { reason: "card post returned null" });
45163
+ return;
45164
+ }
45165
+ log(`ms365-approval: posted card msg=${posted.messageId} request=${registered.request_id} expires=${new Date(registered.expires_at_ms).toISOString()}`);
45166
+ sendResponse(true, {
45167
+ requestId: registered.request_id,
45168
+ expiresAtMs: registered.expires_at_ms
45169
+ });
45170
+ }
45171
+
44902
45172
  // gateway/diff-preview-card.ts
44903
45173
  var import_grammy5 = __toESM(require_mod2(), 1);
44904
45174
  var REQUEST_ID_RE = /^[0-9a-f]{32}$/;
@@ -45813,14 +46083,14 @@ function buildVaultSaveDiscardedInbound(opts) {
45813
46083
  // gateway/subagent-handback-inbound-builder.ts
45814
46084
  var HANDBACK_RESULT_MAX = 3000;
45815
46085
  var HANDBACK_DESC_MAX = 200;
45816
- function truncate2(s, max) {
46086
+ function truncate3(s, max) {
45817
46087
  const t = s.trim();
45818
46088
  return t.length > max ? t.slice(0, max) + "\u2026" : t;
45819
46089
  }
45820
46090
  function buildSubagentHandbackInbound(opts) {
45821
46091
  const ts = opts.nowMs ?? Date.now();
45822
- const desc = truncate2(opts.ctx.taskDescription, HANDBACK_DESC_MAX) || "(no description)";
45823
- const result = truncate2(opts.ctx.resultText, HANDBACK_RESULT_MAX);
46092
+ const desc = truncate3(opts.ctx.taskDescription, HANDBACK_DESC_MAX) || "(no description)";
46093
+ const result = truncate3(opts.ctx.resultText, HANDBACK_RESULT_MAX);
45824
46094
  const text = opts.ctx.outcome === "failed" ? `\uD83E\uDD1D A background worker you dispatched has FAILED.
45825
46095
 
45826
46096
  ` + `Task: ${desc}
@@ -45884,7 +46154,7 @@ function decideSubagentHandback(input) {
45884
46154
  var PROGRESS_RESULT_MAX = 800;
45885
46155
  var PROGRESS_DESC_MAX = 200;
45886
46156
  var DEFAULT_PROGRESS_INTERVAL_MS = 5 * 60 * 1000;
45887
- function truncate3(s, max) {
46157
+ function truncate4(s, max) {
45888
46158
  const t = s.trim();
45889
46159
  return t.length > max ? t.slice(0, max) + "\u2026" : t;
45890
46160
  }
@@ -45898,8 +46168,8 @@ function formatElapsed(ms) {
45898
46168
  }
45899
46169
  function buildSubagentProgressInbound(opts) {
45900
46170
  const ts = opts.nowMs ?? Date.now();
45901
- const desc = truncate3(opts.ctx.taskDescription, PROGRESS_DESC_MAX) || "(no description)";
45902
- const summary = truncate3(opts.ctx.latestSummary, PROGRESS_RESULT_MAX);
46171
+ const desc = truncate4(opts.ctx.taskDescription, PROGRESS_DESC_MAX) || "(no description)";
46172
+ const summary = truncate4(opts.ctx.latestSummary, PROGRESS_RESULT_MAX);
45903
46173
  const elapsed = formatElapsed(opts.ctx.elapsedMs);
45904
46174
  const expiresAt = ts + 2 * opts.ctx.progressIntervalMs;
45905
46175
  const text = `\uD83D\uDD04 A background worker you dispatched is still running.
@@ -46894,12 +47164,12 @@ function runPipeline(inputs) {
46894
47164
  }
46895
47165
 
46896
47166
  // secret-detect/staging.ts
46897
- var DEFAULT_TTL_MS2 = 5 * 60 * 1000;
47167
+ var DEFAULT_TTL_MS3 = 5 * 60 * 1000;
46898
47168
 
46899
47169
  class StagingMap {
46900
47170
  ttlMs;
46901
47171
  map = new Map;
46902
- constructor(ttlMs = DEFAULT_TTL_MS2) {
47172
+ constructor(ttlMs = DEFAULT_TTL_MS3) {
46903
47173
  this.ttlMs = ttlMs;
46904
47174
  }
46905
47175
  key(chat_id, message_id) {
@@ -47942,7 +48212,7 @@ function startSubagentWatcher(config) {
47942
48212
  if (idleMs >= threshold) {
47943
48213
  entry.stallNotified = true;
47944
48214
  entry.stalledAt = n;
47945
- const desc = escapeHtml8(truncate4(entry.description, 80));
48215
+ const desc = escapeHtml8(truncate5(entry.description, 80));
47946
48216
  const idleSec = Math.floor(idleMs / 1000);
47947
48217
  log?.(`subagent-watcher: stall detected for ${entry.agentId} (idle ${idleSec}s): ${desc}`);
47948
48218
  if (db2 != null) {
@@ -48908,13 +49178,13 @@ function summarizeToolForTitle(toolName, inputPreview) {
48908
49178
  return `${toolName} (${skill})`;
48909
49179
  const command = readString(input, "command");
48910
49180
  if (command)
48911
- return `${toolName}: ${truncate5(command, COMMAND_TITLE_MAX)}`;
49181
+ return `${toolName}: ${truncate6(command, COMMAND_TITLE_MAX)}`;
48912
49182
  const argHint = firstScalarArgHint(input);
48913
49183
  return argHint ? `${toolName} (${argHint})` : toolName;
48914
49184
  }
48915
49185
  case "Bash": {
48916
49186
  const command = readString(input, "command");
48917
- return command ? `${toolName}: ${truncate5(command, COMMAND_TITLE_MAX)}` : toolName;
49187
+ return command ? `${toolName}: ${truncate6(command, COMMAND_TITLE_MAX)}` : toolName;
48918
49188
  }
48919
49189
  case "Read":
48920
49190
  case "Edit":
@@ -48922,17 +49192,17 @@ function summarizeToolForTitle(toolName, inputPreview) {
48922
49192
  case "MultiEdit":
48923
49193
  case "NotebookEdit": {
48924
49194
  const filePath = readString(input, "file_path") ?? readString(input, "notebook_path");
48925
- return filePath ? `${toolName}: ${truncate5(basename5(filePath), PATH_TITLE_MAX)}` : toolName;
49195
+ return filePath ? `${toolName}: ${truncate6(basename5(filePath), PATH_TITLE_MAX)}` : toolName;
48926
49196
  }
48927
49197
  case "Glob":
48928
49198
  case "Grep": {
48929
49199
  const pattern = readString(input, "pattern");
48930
- return pattern ? `${toolName}: ${truncate5(pattern, COMMAND_TITLE_MAX)}` : toolName;
49200
+ return pattern ? `${toolName}: ${truncate6(pattern, COMMAND_TITLE_MAX)}` : toolName;
48931
49201
  }
48932
49202
  case "WebFetch":
48933
49203
  case "WebSearch": {
48934
49204
  const query2 = readString(input, "url") ?? readString(input, "query");
48935
- return query2 ? `${toolName}: ${truncate5(query2, COMMAND_TITLE_MAX)}` : toolName;
49205
+ return query2 ? `${toolName}: ${truncate6(query2, COMMAND_TITLE_MAX)}` : toolName;
48936
49206
  }
48937
49207
  default:
48938
49208
  return toolName;
@@ -48971,7 +49241,7 @@ function firstScalarArgHint(input) {
48971
49241
  if (SKIP.has(key))
48972
49242
  continue;
48973
49243
  if (typeof value === "string" && value.length > 0) {
48974
- return `${key}: ${truncate5(value, INPUT_VALUE_MAX)}`;
49244
+ return `${key}: ${truncate6(value, INPUT_VALUE_MAX)}`;
48975
49245
  }
48976
49246
  if (typeof value === "number" || typeof value === "boolean") {
48977
49247
  return `${key}: ${String(value)}`;
@@ -49006,7 +49276,7 @@ function skillBasenameFromPath(input) {
49006
49276
  const basename6 = lastSlash >= 0 ? trimmed.slice(lastSlash + 1) : trimmed;
49007
49277
  return basename6.length > 0 ? basename6 : null;
49008
49278
  }
49009
- function truncate5(text, max) {
49279
+ function truncate6(text, max) {
49010
49280
  const collapsed = text.replace(/\s+/g, " ").trim();
49011
49281
  if (collapsed.length <= max)
49012
49282
  return collapsed;
@@ -49277,11 +49547,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
49277
49547
  }
49278
49548
 
49279
49549
  // ../src/build-info.ts
49280
- var VERSION = "0.13.53";
49281
- var COMMIT_SHA = "08726c33";
49282
- var COMMIT_DATE = "2026-05-27T03:52:37Z";
49283
- var LATEST_PR = 1885;
49284
- var COMMITS_AHEAD_OF_TAG = 18;
49550
+ var VERSION = "0.13.55";
49551
+ var COMMIT_SHA = "98cf8e68";
49552
+ var COMMIT_DATE = "2026-05-27T07:38:21Z";
49553
+ var LATEST_PR = 1901;
49554
+ var COMMITS_AHEAD_OF_TAG = 0;
49285
49555
 
49286
49556
  // gateway/boot-version.ts
49287
49557
  function formatRelativeAgo(iso) {
@@ -50991,12 +51261,14 @@ function emitGatewayOperatorEvent(event) {
50991
51261
  `);
50992
51262
  return;
50993
51263
  }
50994
- process.stderr.write(`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)
51264
+ const opEventTopic = resolveAgentOutboundTopic({ kind: "compact-watchdog" });
51265
+ process.stderr.write(`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` + (opEventTopic != null ? ` topic=${opEventTopic}` : "") + `
50995
51266
  `);
50996
51267
  for (const chat_id of access.allowFrom) {
50997
51268
  const opts = {
50998
51269
  parse_mode: "HTML",
50999
- ...renderedKeyboard ? { reply_markup: renderedKeyboard } : {}
51270
+ ...renderedKeyboard ? { reply_markup: renderedKeyboard } : {},
51271
+ ...opEventTopic != null ? { message_thread_id: opEventTopic } : {}
51000
51272
  };
51001
51273
  bot.api.sendMessage(chat_id, renderedText, opts).catch((e) => {
51002
51274
  process.stderr.write(`telegram gateway: operator-event send to ${chat_id} failed agent=${agent} kind=${kind}: ${e}
@@ -51504,10 +51776,17 @@ ${reminder}
51504
51776
  if (alwaysRule != null) {
51505
51777
  keyboard.row().text(`\uD83D\uDD01 Always allow ${alwaysRule.label}`, `perm:always:${requestId}`);
51506
51778
  }
51779
+ const activeTurn = currentTurn;
51780
+ const permTopic = resolveAgentOutboundTopic({
51781
+ kind: "permission",
51782
+ turnInitiated: activeTurn != null,
51783
+ originThreadId: activeTurn?.sessionThreadId
51784
+ });
51507
51785
  for (const chat_id of access.allowFrom) {
51508
51786
  bot.api.sendMessage(chat_id, text, {
51509
51787
  parse_mode: "HTML",
51510
- reply_markup: keyboard
51788
+ reply_markup: keyboard,
51789
+ ...permTopic != null ? { message_thread_id: permTopic } : {}
51511
51790
  }).catch((e) => {
51512
51791
  process.stderr.write(`telegram gateway: permission_request send to ${chat_id} failed: ${e}
51513
51792
  `);
@@ -51567,7 +51846,15 @@ ${reminder}
51567
51846
  const operator = access.allowFrom[0];
51568
51847
  if (operator === undefined)
51569
51848
  return null;
51570
- return { chatId: operator };
51849
+ const activeTurn = currentTurn;
51850
+ const driveTopic = resolveAgentOutboundTopic({
51851
+ kind: "hostd-approval",
51852
+ originThreadId: activeTurn?.sessionThreadId
51853
+ });
51854
+ return {
51855
+ chatId: operator,
51856
+ ...driveTopic != null ? { threadId: driveTopic } : {}
51857
+ };
51571
51858
  },
51572
51859
  registerApproval: async (args) => {
51573
51860
  const r = await approvalRequest({
@@ -51605,6 +51892,62 @@ ${reminder}
51605
51892
  },
51606
51893
  buildCard: ({ preview, suggestRequestId }) => buildDiffPreviewCard({ preview, suggestRequestId }),
51607
51894
  log: (m) => process.stderr.write(`telegram gateway: drive-approval \u2014 ${m}
51895
+ `)
51896
+ });
51897
+ },
51898
+ async onRequestMs365Approval(client3, msg) {
51899
+ await handleRequestMs365Approval(client3, msg, {
51900
+ agentName: getMyAgentName(),
51901
+ loadAllowFrom: () => loadAccess().allowFrom,
51902
+ loadTargetChat: () => {
51903
+ const access = loadAccess();
51904
+ const operator = access.allowFrom[0];
51905
+ if (operator === undefined)
51906
+ return null;
51907
+ const activeTurn = currentTurn;
51908
+ const ms365Topic = resolveAgentOutboundTopic({
51909
+ kind: "hostd-approval",
51910
+ originThreadId: activeTurn?.sessionThreadId
51911
+ });
51912
+ return {
51913
+ chatId: operator,
51914
+ ...ms365Topic != null ? { threadId: ms365Topic } : {}
51915
+ };
51916
+ },
51917
+ registerApproval: async (args) => {
51918
+ const r = await approvalRequest({
51919
+ agent_unit: args.agent_unit,
51920
+ scope: args.scope,
51921
+ action: args.action,
51922
+ approver_set: args.approver_set,
51923
+ why: args.why,
51924
+ ttl_ms: args.ttl_ms
51925
+ });
51926
+ if (r === null || r.state === "rate_limited")
51927
+ return null;
51928
+ return {
51929
+ request_id: r.request_id,
51930
+ expires_at_ms: r.expires_at
51931
+ };
51932
+ },
51933
+ postCard: async (args) => {
51934
+ try {
51935
+ const sent = await robustApiCall(() => bot.api.sendMessage(args.chatId, args.text, {
51936
+ ...args.threadId !== undefined ? { message_thread_id: args.threadId } : {},
51937
+ reply_markup: args.replyMarkup
51938
+ }), {
51939
+ chat_id: String(args.chatId),
51940
+ verb: "ms365-approval-card",
51941
+ ...args.threadId !== undefined ? { threadId: args.threadId } : {}
51942
+ });
51943
+ return { messageId: sent.message_id };
51944
+ } catch (err) {
51945
+ process.stderr.write(`telegram gateway: ms365-approval postCard failed: ${err.message}
51946
+ `);
51947
+ return null;
51948
+ }
51949
+ },
51950
+ log: (m) => process.stderr.write(`telegram gateway: ms365-approval \u2014 ${m}
51608
51951
  `)
51609
51952
  });
51610
51953
  },
@@ -51618,7 +51961,15 @@ ${reminder}
51618
51961
  const operator = access.allowFrom[0];
51619
51962
  if (operator === undefined)
51620
51963
  return null;
51621
- return { chatId: operator };
51964
+ const activeTurn = currentTurn;
51965
+ const cfgTopic = resolveAgentOutboundTopic({
51966
+ kind: "hostd-approval",
51967
+ originThreadId: activeTurn?.sessionThreadId
51968
+ });
51969
+ return {
51970
+ chatId: operator,
51971
+ ...cfgTopic != null ? { threadId: cfgTopic } : {}
51972
+ };
51622
51973
  },
51623
51974
  buildKeyboard: (requestId) => new InlineKeyboard6().text("\u2705 Approve", `cfg:${requestId}:approve`).text("\uD83D\uDEAB Deny", `cfg:${requestId}:deny`),
51624
51975
  postCard: async (args) => {
@@ -54558,7 +54909,9 @@ function preBlock(text) {
54558
54909
  }
54559
54910
  async function switchroomReply(ctx, text, options = {}) {
54560
54911
  const chatId = String(ctx.chat.id);
54561
- const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
54912
+ const baseThreadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
54913
+ const routedOpts = options.classification ? slashCommandReplyOpts(ctx, options.classification) : {};
54914
+ const threadId = routedOpts.message_thread_id ?? baseThreadId;
54562
54915
  await ctx.reply(text, {
54563
54916
  ...threadId != null ? { message_thread_id: threadId } : {},
54564
54917
  ...options.html ? { parse_mode: "HTML", link_preview_options: { is_disabled: true } } : {},
@@ -54719,6 +55072,16 @@ function resolveSystemdRunPath() {
54719
55072
  }
54720
55073
  return _systemdRunPath;
54721
55074
  }
55075
+ function slashCommandReplyOpts(ctx, classification) {
55076
+ const originThreadId = ctx.message?.message_thread_id;
55077
+ const event = classification === "query" ? { kind: "command-query", originThreadId } : classification === "mutation" ? { kind: "command-mutation" } : { kind: "command-heavy" };
55078
+ const target = resolveAgentOutboundTopic(event);
55079
+ if (target == null)
55080
+ return {};
55081
+ if (target === originThreadId)
55082
+ return {};
55083
+ return { message_thread_id: target };
55084
+ }
54722
55085
  var _dockerReachable;
54723
55086
  function isDockerReachable() {
54724
55087
  if (_dockerReachable !== undefined)
@@ -54996,28 +55359,28 @@ async function dispatchShortVerbViaHostd(ctx, req, label, legacyArgs) {
54996
55359
  await switchroomReply(ctx, `\u274C <b>${escapeHtmlForTg(label)} failed via hostd</b> (result=${escapeHtmlForTg(hostdResp.result)}):
54997
55360
  ` + preBlock(stripAnsi2(errBody)), { html: true });
54998
55361
  }
54999
- async function runSwitchroomCommand(ctx, args, label) {
55362
+ async function runSwitchroomCommand(ctx, args, label, classification = "query") {
55000
55363
  try {
55001
55364
  const output = stripAnsi2(switchroomExec(args));
55002
55365
  const formatted = formatSwitchroomOutput(output);
55003
55366
  if (formatted) {
55004
- await switchroomReply(ctx, preBlock(formatted), { html: true });
55367
+ await switchroomReply(ctx, preBlock(formatted), { html: true, classification });
55005
55368
  } else {
55006
- await switchroomReply(ctx, `${label}: done (no output)`);
55369
+ await switchroomReply(ctx, `${label}: done (no output)`, { classification });
55007
55370
  }
55008
55371
  } catch (err) {
55009
55372
  const error = err;
55010
55373
  if (error.message?.includes("ENOENT")) {
55011
- await switchroomReply(ctx, "switchroom CLI not found.", { html: true });
55374
+ await switchroomReply(ctx, "switchroom CLI not found.", { html: true, classification });
55012
55375
  return;
55013
55376
  }
55014
55377
  if (error.message?.includes("ETIMEDOUT") || error.message?.includes("timed out")) {
55015
- await switchroomReply(ctx, `${label}: timed out`);
55378
+ await switchroomReply(ctx, `${label}: timed out`, { classification });
55016
55379
  return;
55017
55380
  }
55018
55381
  const detail = stripAnsi2(error.stderr?.trim() || error.message || "unknown error");
55019
55382
  await switchroomReply(ctx, `<b>${escapeHtmlForTg(label)} failed:</b>
55020
- ${preBlock(formatSwitchroomOutput(detail))}`, { html: true });
55383
+ ${preBlock(formatSwitchroomOutput(detail))}`, { html: true, classification });
55021
55384
  }
55022
55385
  }
55023
55386
  function switchroomExecJson(args) {
@@ -55611,7 +55974,7 @@ The gateway will restart as part of the recreate step; watch for the post-restar
55611
55974
  bot.command("upgradestatus", async (ctx) => {
55612
55975
  if (!isAuthorizedSender(ctx))
55613
55976
  return;
55614
- await runSwitchroomCommand(ctx, ["update", "--status"], "update --status");
55977
+ await runSwitchroomCommand(ctx, ["update", "--status"], "update --status", "heavy");
55615
55978
  });
55616
55979
  bot.command("upgrade", async (ctx) => {
55617
55980
  if (!isAuthorizedSender(ctx))
@@ -55680,7 +56043,7 @@ bot.command("audit", async (ctx) => {
55680
56043
  await switchroomReply(ctx, `Unknown flag <code>${escapeHtmlForTg(t)}</code>. Allowed: <code>--tail</code>, <code>--agent</code>, <code>--op</code>, <code>--error</code>, <code>--verbose</code>.`, { html: true });
55681
56044
  return;
55682
56045
  }
55683
- await runSwitchroomCommand(ctx, argv, `hostd audit${argv.length > 2 ? " \u2026" : ""}`);
56046
+ await runSwitchroomCommand(ctx, argv, `hostd audit${argv.length > 2 ? " \u2026" : ""}`, "heavy");
55684
56047
  });
55685
56048
  function isValidPermissionRequestId(id) {
55686
56049
  return /^[a-z0-9-]{1,32}$/.test(id);
@@ -57397,7 +57760,7 @@ bot.command("logs", async (ctx) => {
57397
57760
  }
57398
57761
  const lines = linesArg ? parseInt(linesArg, 10) : 20;
57399
57762
  const lineCount = isNaN(lines) || lines < 1 ? 20 : Math.min(lines, 200);
57400
- await runSwitchroomCommand(ctx, ["agent", "logs", name, "--lines", String(lineCount)], `logs ${name}`);
57763
+ await runSwitchroomCommand(ctx, ["agent", "logs", name, "--lines", String(lineCount)], `logs ${name}`, "heavy");
57401
57764
  });
57402
57765
  bot.command("memory", async (ctx) => {
57403
57766
  if (!isAuthorizedSender(ctx))
@@ -57407,7 +57770,7 @@ bot.command("memory", async (ctx) => {
57407
57770
  await switchroomReply(ctx, "Usage: /memory <search query>");
57408
57771
  return;
57409
57772
  }
57410
- await runSwitchroomCommand(ctx, ["memory", "search", query2], "memory search");
57773
+ await runSwitchroomCommand(ctx, ["memory", "search", query2], "memory search", "heavy");
57411
57774
  });
57412
57775
  bot.command("issues", async (ctx) => {
57413
57776
  if (!isAuthorizedSender(ctx))