switchroom 0.14.44 → 0.14.45

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.
@@ -3537,18 +3537,24 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
3537
3537
  // Fleet-shared / DM agents see `undefined` → no `message_thread_id`
3538
3538
  // is added to the broadcast opts → behavior unchanged.
3539
3539
  const opEventTopic = resolveAgentOutboundTopic({ kind: 'compact-watchdog' })
3540
+ const opEventSupergroup = resolveAgentSupergroupChatId()
3540
3541
 
3541
3542
  process.stderr.write(
3542
3543
  `telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` +
3543
3544
  (opEventTopic != null ? ` topic=${opEventTopic}` : '') + '\n',
3544
3545
  )
3545
3546
  for (const chat_id of access.allowFrom) {
3547
+ // The resolved topic is valid ONLY in the agent's supergroup — attaching
3548
+ // it to an operator DM recipient yields 400 "message thread not found" and
3549
+ // the event silently fails to deliver (the marko #2096 class). Guard it:
3550
+ // DM recipients get a thread-less send; the supergroup owner gets the lane.
3551
+ const opEventThread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: opEventTopic, supergroupChatId: opEventSupergroup })
3546
3552
  // grammy's Other<...> opts type is generated and stricter than our
3547
3553
  // call shape; runtime accepts both. Cast through unknown.
3548
3554
  const opts = {
3549
3555
  parse_mode: 'HTML' as const,
3550
3556
  ...(renderedKeyboard ? { reply_markup: renderedKeyboard } : {}),
3551
- ...(opEventTopic != null ? { message_thread_id: opEventTopic } : {}),
3557
+ ...(opEventThread != null ? { message_thread_id: opEventThread } : {}),
3552
3558
  }
3553
3559
  // Comment-only context for the reader; the lint marker on the
3554
3560
  // very next line is what unlocks the raw bot.api call.
@@ -10159,7 +10165,14 @@ async function handleInbound(
10159
10165
  // No staged entry to act on — fall through to normal handling.
10160
10166
  }
10161
10167
 
10162
- void bot.api.sendChatAction(chat_id, 'typing').catch(() => {})
10168
+ // Typing indicator in the ORIGINATING topic — on a supergroup-topic inbound,
10169
+ // an un-threaded sendChatAction shows "typing" in General, not the topic the
10170
+ // user is in. messageThreadId is the inbound's thread (undefined in a DM).
10171
+ void bot.api.sendChatAction(
10172
+ chat_id,
10173
+ 'typing',
10174
+ messageThreadId != null ? { message_thread_id: messageThreadId } : {},
10175
+ ).catch(() => {})
10163
10176
 
10164
10177
  // Parse explicit prefixes first. `/steer ` / `/s ` opts IN to steering;
10165
10178
  // `/queue ` / `/q ` are legacy aliases that opt in to the new default (queued).
@@ -11088,10 +11101,14 @@ function resolveBootChatId(
11088
11101
  // → behavior unchanged (lands at chat-root as today). PR4b of
11089
11102
  // supergroup-mode rollout (docs/rfcs/supergroup-mode.md).
11090
11103
  const supergroupBootTopic = resolveAgentOutboundTopic({ kind: 'boot' })
11104
+ const bootSupergroup = resolveAgentSupergroupChatId()
11105
+ // The boot topic is valid only in the agent's supergroup — attach it per
11106
+ // recipient so a DM owner doesn't 400 (marko #2096 class); the supergroup
11107
+ // owner gets the boot/alerts lane, a DM gets a thread-less boot card.
11091
11108
 
11092
11109
  // 2. Env var
11093
11110
  const envChat = process.env.SUBAGENT_OWNER_CHAT_ID
11094
- if (envChat) return { chatId: envChat, threadId: supergroupBootTopic, ackMsgId: undefined }
11111
+ if (envChat) return { chatId: envChat, threadId: topicForRecipient({ recipientChatId: envChat, resolvedTopic: supergroupBootTopic, supergroupChatId: bootSupergroup }), ackMsgId: undefined }
11095
11112
  // 3. Most-recent inbound from history
11096
11113
  if (HISTORY_ENABLED) {
11097
11114
  try {
@@ -11099,7 +11116,7 @@ function resolveBootChatId(
11099
11116
  const ownerChatId = access.allowFrom[0]
11100
11117
  if (ownerChatId) {
11101
11118
  const recent = queryHistory({ chat_id: ownerChatId, limit: 1 })
11102
- if (recent.length > 0) return { chatId: ownerChatId, threadId: supergroupBootTopic, ackMsgId: undefined }
11119
+ if (recent.length > 0) return { chatId: ownerChatId, threadId: topicForRecipient({ recipientChatId: ownerChatId, resolvedTopic: supergroupBootTopic, supergroupChatId: bootSupergroup }), ackMsgId: undefined }
11103
11120
  }
11104
11121
  } catch {}
11105
11122
  }
@@ -133,8 +133,9 @@ export function computeLabel(toolName, input) {
133
133
  }
134
134
  }
135
135
 
136
- // MCP allowlist.
136
+ // MCP tools.
137
137
  if (typeof toolName === 'string' && toolName.startsWith('mcp__')) {
138
+ // Explicit labels / suppressions for the built-in servers.
138
139
  switch (toolName) {
139
140
  case 'mcp__switchroom-telegram__reply':
140
141
  case 'mcp__switchroom-telegram__stream_reply':
@@ -150,15 +151,46 @@ export function computeLabel(toolName, input) {
150
151
  return 'Searching memory'
151
152
  case 'mcp__hindsight__retain':
152
153
  return 'Saving memory'
153
- // Explicit suppressions — return null so we don't emit a sidecar
154
- // line at all. (Falling through to the default below produces the
155
- // same effect, but listing these makes the intent obvious.)
154
+ // Explicit suppressions — return null so we don't emit a sidecar line.
156
155
  case 'mcp__switchroom-telegram__send_typing':
157
156
  case 'mcp__hindsight__sync_retain':
158
157
  return null
159
158
  }
160
- // Any other mcp__* tool: not on the allowlist, no label.
161
- return null
159
+ // Generic fallback for ANY other MCP tool (operator-configured servers
160
+ // — perplexity, webkite, gdrive, notion, …). These previously returned
161
+ // null → invisible in the live activity feed, so a research turn driven
162
+ // entirely by MCP tools read as pure silence (only a typing dot + the
163
+ // 👀 reaction) — the "I can't see what it's doing" report. Mirror the
164
+ // gateway's describeToolUse: friendly per-server labels, else a
165
+ // model-authored field, else a humanized tool name. NEVER label
166
+ // switchroom-telegram surface/control tools (they ARE the conversation).
167
+ const m = /^mcp__(.+?)__(.+)$/.exec(toolName)
168
+ if (!m) return null
169
+ const server = m[1].toLowerCase()
170
+ const tool = m[2].toLowerCase()
171
+ if (server === 'switchroom-telegram') return null
172
+ if (server === 'hindsight') return 'Working with memory'
173
+ if (server === 'google-workspace' || server === 'claude_ai_google_calendar')
174
+ return 'Checking your calendar'
175
+ if (server === 'claude_ai_gmail') return 'Checking your email'
176
+ if (server === 'claude_ai_google_drive' || server === 'gdrive')
177
+ return 'Looking through your files'
178
+ if (server === 'notion' || server === 'claude_ai_notion') return 'Checking your notes'
179
+ if (server === 'perplexity') {
180
+ const q = clip(String(i.query ?? i.description ?? ''), 60).trim()
181
+ return q ? `Searching the web for ${q}` : 'Searching the web'
182
+ }
183
+ if (server === 'webkite') {
184
+ const u = clip(urlHostPath(i.url ?? ''), 60).trim()
185
+ return u ? `Reading ${u}` : 'Reading the web'
186
+ }
187
+ // Unknown MCP server: prefer a model-authored field, else humanized tool.
188
+ const desc =
189
+ clip(String(i.description ?? ''), 60).trim() ||
190
+ clip(String(i.query ?? ''), 50).trim() ||
191
+ clip(String(i.title ?? ''), 50).trim()
192
+ if (desc) return desc
193
+ return `Using ${tool.replace(/[-_]+/g, ' ')}`
162
194
  }
163
195
 
164
196
  return null