switchroom 0.18.11 → 0.18.13

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 (150) hide show
  1. package/dist/agent-scheduler/index.js +37 -5
  2. package/dist/auth-broker/index.js +116 -78
  3. package/dist/cli/hindsight-mental-model-pretool.mjs +39 -0
  4. package/dist/cli/ms-365-write-pretool.mjs +31 -8
  5. package/dist/cli/notion-write-pretool.mjs +38 -6
  6. package/dist/cli/skill-validate-pretool.mjs +144 -2847
  7. package/dist/cli/switchroom.js +3529 -4543
  8. package/dist/cli/ui/index.html +163 -17
  9. package/dist/host-control/main.js +605 -2847
  10. package/dist/vault/approvals/kernel-server.js +120 -13
  11. package/dist/vault/broker/server.js +259 -157
  12. package/package.json +3 -4
  13. package/profiles/_base/start.sh.hbs +65 -0
  14. package/profiles/_shared/vault-protocol.md.hbs +3 -1
  15. package/profiles/coding/CLAUDE.md.hbs +1 -1
  16. package/profiles/default/CLAUDE.md.hbs +2 -2
  17. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  18. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  19. package/telegram-plugin/bridge/bridge.ts +37 -0
  20. package/telegram-plugin/bridge/inbound-dedup.ts +101 -0
  21. package/telegram-plugin/dist/bridge/bridge.js +122 -4
  22. package/telegram-plugin/dist/gateway/gateway.js +4213 -3288
  23. package/telegram-plugin/dist/server.js +139 -5
  24. package/telegram-plugin/flood-circuit-breaker.ts +493 -21
  25. package/telegram-plugin/format.ts +19 -0
  26. package/telegram-plugin/gateway/approval-hold.ts +602 -0
  27. package/telegram-plugin/gateway/auth-command.ts +92 -2
  28. package/telegram-plugin/gateway/auth-loopback-relay.ts +670 -0
  29. package/telegram-plugin/gateway/boot-card.ts +12 -5
  30. package/telegram-plugin/gateway/callback-query-handlers.ts +88 -1
  31. package/telegram-plugin/gateway/config-approval-handler.ts +6 -1
  32. package/telegram-plugin/gateway/disconnect-flush.ts +19 -0
  33. package/telegram-plugin/gateway/dm-pin-sweep.test.ts +251 -0
  34. package/telegram-plugin/gateway/dm-pin-sweep.ts +178 -0
  35. package/telegram-plugin/gateway/gateway.ts +1695 -230
  36. package/telegram-plugin/gateway/hostd-dispatch.ts +23 -0
  37. package/telegram-plugin/gateway/idle-clear.ts +90 -6
  38. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +26 -5
  39. package/telegram-plugin/gateway/inject-handler.ts +8 -0
  40. package/telegram-plugin/gateway/ipc-protocol.ts +46 -3
  41. package/telegram-plugin/gateway/ipc-server.ts +43 -0
  42. package/telegram-plugin/gateway/mental-model-propose-resolve.ts +145 -37
  43. package/telegram-plugin/gateway/model-command.ts +9 -3
  44. package/telegram-plugin/gateway/pending-session-command.ts +13 -1
  45. package/telegram-plugin/gateway/permission-ttl-sweep.ts +66 -0
  46. package/telegram-plugin/gateway/pre-approval-check.ts +74 -0
  47. package/telegram-plugin/gateway/queued-card-store.ts +217 -0
  48. package/telegram-plugin/gateway/session-model-file.ts +26 -1
  49. package/telegram-plugin/gateway/turn-end-gate-backstop.ts +59 -0
  50. package/telegram-plugin/gateway/turn-end-gate.ts +95 -0
  51. package/telegram-plugin/gateway/turn-typing-loop.ts +10 -2
  52. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +13 -0
  53. package/telegram-plugin/history.ts +51 -0
  54. package/telegram-plugin/hooks/dispatch-claim-scan.mjs +259 -0
  55. package/telegram-plugin/hooks/dispatch-claim-stop.mjs +129 -0
  56. package/telegram-plugin/hooks/hooks.json +9 -0
  57. package/telegram-plugin/inline-keyboard-callbacks.ts +303 -2
  58. package/telegram-plugin/model-unavailable.ts +41 -11
  59. package/telegram-plugin/operator-events.ts +23 -0
  60. package/telegram-plugin/outbound-field-redact.ts +69 -0
  61. package/telegram-plugin/package.json +0 -1
  62. package/telegram-plugin/permission-rule.ts +1 -0
  63. package/telegram-plugin/permission-title.ts +1 -0
  64. package/telegram-plugin/render/render.ts +32 -14
  65. package/telegram-plugin/retry-api-call.ts +212 -2
  66. package/telegram-plugin/scoped-approval.ts +11 -2
  67. package/telegram-plugin/secret-detect/chunker.ts +18 -4
  68. package/telegram-plugin/secret-detect/index.ts +12 -56
  69. package/telegram-plugin/send-gate-degraded.test.ts +574 -0
  70. package/telegram-plugin/send-gate-observability.test.ts +470 -0
  71. package/telegram-plugin/send-gate-observability.ts +355 -0
  72. package/telegram-plugin/send-gate.test.ts +717 -0
  73. package/telegram-plugin/send-gate.ts +1056 -0
  74. package/telegram-plugin/session-tail.ts +82 -7
  75. package/telegram-plugin/shared/bot-runtime.ts +17 -5
  76. package/telegram-plugin/shared/gw-trace-gate.ts +105 -0
  77. package/telegram-plugin/status-pin-driver.ts +52 -7
  78. package/telegram-plugin/status-pin.ts +81 -0
  79. package/telegram-plugin/subagent-watcher.ts +173 -18
  80. package/telegram-plugin/tests/activity-card-wiring.test.ts +18 -5
  81. package/telegram-plugin/tests/approval-hold-harness.ts +425 -0
  82. package/telegram-plugin/tests/approval-hold-outcome.test.ts +327 -0
  83. package/telegram-plugin/tests/approval-hold-record.test.ts +531 -0
  84. package/telegram-plugin/tests/approval-hold-redeliver.test.ts +602 -0
  85. package/telegram-plugin/tests/auth-loopback-relay.test.ts +533 -0
  86. package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +53 -7
  87. package/telegram-plugin/tests/busy-key-reaper.test.ts +1 -0
  88. package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
  89. package/telegram-plugin/tests/dispatch-claim-scan.test.ts +250 -0
  90. package/telegram-plugin/tests/flood-breaker-blindness.test.ts +213 -0
  91. package/telegram-plugin/tests/flood-windows-persistence.test.ts +224 -0
  92. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  93. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +29 -1
  94. package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +66 -0
  95. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
  96. package/telegram-plugin/tests/gw-trace-gate.test.ts +105 -0
  97. package/telegram-plugin/tests/history.test.ts +115 -0
  98. package/telegram-plugin/tests/idle-clear.test.ts +233 -3
  99. package/telegram-plugin/tests/inbound-dedup.test.ts +93 -0
  100. package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
  101. package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +448 -0
  102. package/telegram-plugin/tests/ipc-server-check-pre-approved.test.ts +194 -0
  103. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +123 -0
  104. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +1 -1
  105. package/telegram-plugin/tests/model-command.test.ts +14 -0
  106. package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
  107. package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
  108. package/telegram-plugin/tests/pending-session-command.test.ts +21 -0
  109. package/telegram-plugin/tests/permission-card-routing.test.ts +30 -5
  110. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +8 -7
  111. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +1 -1
  112. package/telegram-plugin/tests/pre-approval-check.test.ts +148 -0
  113. package/telegram-plugin/tests/queued-card-store.test.ts +232 -0
  114. package/telegram-plugin/tests/reaction-flush-turn-gated.test.ts +100 -0
  115. package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
  116. package/telegram-plugin/tests/render/render.test.ts +88 -0
  117. package/telegram-plugin/tests/retry-api-call.test.ts +398 -0
  118. package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
  119. package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
  120. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
  121. package/telegram-plugin/tests/session-model-file.test.ts +50 -0
  122. package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
  123. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +35 -14
  124. package/telegram-plugin/tests/status-pin.test.ts +275 -1
  125. package/telegram-plugin/tests/subagent-watcher-deferral-log-ratelimit.test.ts +316 -0
  126. package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
  127. package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +92 -0
  128. package/telegram-plugin/tests/turn-end-gate.test.ts +137 -0
  129. package/telegram-plugin/tests/typing-emitter.test.ts +586 -0
  130. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +20 -0
  131. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
  132. package/telegram-plugin/typing-emitter.ts +224 -0
  133. package/telegram-plugin/uat/scenarios/jtbd-feel-like-a-colleague-dm.test.ts +136 -0
  134. package/telegram-plugin/welcome-text.ts +42 -0
  135. package/telegram-plugin/worktree-watch-cwds.ts +194 -5
  136. package/vendor/hindsight-memory/scripts/drain_pending.py +22 -6
  137. package/vendor/hindsight-memory/scripts/lib/client.py +12 -5
  138. package/vendor/hindsight-memory/scripts/lib/directives.py +38 -3
  139. package/vendor/hindsight-memory/scripts/lib/pending.py +36 -9
  140. package/vendor/hindsight-memory/scripts/session_end.py +14 -3
  141. package/vendor/hindsight-memory/scripts/session_start.py +21 -0
  142. package/vendor/hindsight-memory/scripts/tests/test_directives.py +38 -0
  143. package/vendor/hindsight-memory/tests/test_drain_pending.py +68 -0
  144. package/vendor/hindsight-memory/tests/test_pending.py +44 -0
  145. package/vendor/hindsight-memory/tests/test_session_end_pending.py +38 -0
  146. package/vendor/hindsight-memory/tests/test_session_start_drain.py +155 -0
  147. package/telegram-plugin/channel-envelope-safety.test.ts +0 -56
  148. package/telegram-plugin/channel-envelope-safety.ts +0 -56
  149. package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
  150. package/telegram-plugin/tests/secret-detect-secretlint.test.ts +0 -105
@@ -17062,6 +17062,48 @@ var init_operator_events = __esm(() => {
17062
17062
  cooldownMap = new Map;
17063
17063
  });
17064
17064
 
17065
+ // quota-check.ts
17066
+ var init_quota_check = () => {};
17067
+
17068
+ // text-voice-scrub.ts
17069
+ var NULL = "\x00", FENCE_PH, INLINE_PH, HTML_CODE_PH, HTML_PRE_PH, URL_PH;
17070
+ var init_text_voice_scrub = __esm(() => {
17071
+ FENCE_PH = `${NULL}VS_FENCE`;
17072
+ INLINE_PH = `${NULL}VS_INLINE`;
17073
+ HTML_CODE_PH = `${NULL}VS_HTMLCODE`;
17074
+ HTML_PRE_PH = `${NULL}VS_HTMLPRE`;
17075
+ URL_PH = `${NULL}VS_URL`;
17076
+ });
17077
+
17078
+ // card-format.ts
17079
+ var init_card_format = __esm(() => {
17080
+ init_text_voice_scrub();
17081
+ });
17082
+
17083
+ // model-unavailable.ts
17084
+ function isTransientUpstreamSignal(text) {
17085
+ if (typeof text !== "string" || text.length === 0)
17086
+ return false;
17087
+ const sample = text.length > 16384 ? text.slice(0, 16384) : text;
17088
+ const lower = sample.toLowerCase();
17089
+ return transientUpstreamSignals.some((s) => lower.includes(s));
17090
+ }
17091
+ var transientUpstreamSignals;
17092
+ var init_model_unavailable = __esm(() => {
17093
+ init_quota_check();
17094
+ init_card_format();
17095
+ transientUpstreamSignals = [
17096
+ "not your usage limit",
17097
+ "not your account",
17098
+ "not your account's",
17099
+ "temporarily limiting requests",
17100
+ "temporarily rate",
17101
+ "server is temporarily",
17102
+ "would exceed your account\u2019s rate limit",
17103
+ "would exceed your account's rate limit"
17104
+ ];
17105
+ });
17106
+
17065
17107
  // tool-label-sidecar.ts
17066
17108
  import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
17067
17109
  import { join as join3 } from "node:path";
@@ -17491,7 +17533,8 @@ function detectErrorInTranscriptLine(line) {
17491
17533
  const status = typeof obj.apiErrorStatus === "number" ? obj.apiErrorStatus : null;
17492
17534
  const errStr = typeof obj.error === "string" ? obj.error : "";
17493
17535
  const text = extractAssistantText(obj);
17494
- const kind2 = status === 429 ? "quota-exhausted" : classifyClaudeError({ type: errStr, status, message: text });
17536
+ const kind2 = status === 429 ? isTransientUpstreamSignal(`${text}
17537
+ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: errStr, status, message: text });
17495
17538
  return {
17496
17539
  kind: kind2,
17497
17540
  raw: obj,
@@ -17585,6 +17628,7 @@ function startSessionTail(config2) {
17585
17628
  const onOperatorEvent = config2.onOperatorEvent;
17586
17629
  log?.(`session-tail: projectsDir=${projectsDir}`);
17587
17630
  const sidecars = new Map;
17631
+ const createSidecar = config2.createSidecar ?? createToolLabelSidecar;
17588
17632
  const stateDirForSidecar = process.env.TELEGRAM_STATE_DIR ?? null;
17589
17633
  function sessionIdForFile(file) {
17590
17634
  if (!file)
@@ -17599,7 +17643,7 @@ function startSessionTail(config2) {
17599
17643
  if (existing)
17600
17644
  return existing;
17601
17645
  try {
17602
- const s = createToolLabelSidecar({ stateDir: stateDirForSidecar, sessionId });
17646
+ const s = createSidecar({ stateDir: stateDirForSidecar, sessionId });
17603
17647
  sidecars.set(sessionId, s);
17604
17648
  s.onLabel((toolUseId, label, toolName) => {
17605
17649
  rawOnEvent({ kind: "tool_label", toolUseId, label, toolName });
@@ -17610,6 +17654,17 @@ function startSessionTail(config2) {
17610
17654
  return null;
17611
17655
  }
17612
17656
  }
17657
+ function stopSidecar(sessionId) {
17658
+ if (!sessionId)
17659
+ return;
17660
+ const s = sidecars.get(sessionId);
17661
+ if (!s)
17662
+ return;
17663
+ try {
17664
+ s.stop();
17665
+ } catch {}
17666
+ sidecars.delete(sessionId);
17667
+ }
17613
17668
  function decorate(ev, sessionId) {
17614
17669
  if (!sessionId)
17615
17670
  return ev;
@@ -17702,6 +17757,11 @@ function startSessionTail(config2) {
17702
17757
  } catch {}
17703
17758
  watcher = null;
17704
17759
  }
17760
+ const rotatedAwaySid = sessionIdForFile(currentFile);
17761
+ const nextSid = sessionIdForFile(file);
17762
+ if (rotatedAwaySid != null && rotatedAwaySid !== nextSid) {
17763
+ stopSidecar(rotatedAwaySid);
17764
+ }
17705
17765
  currentFile = file;
17706
17766
  const prior = fileCursors.get(file);
17707
17767
  if (prior != null) {
@@ -17734,7 +17794,7 @@ function startSessionTail(config2) {
17734
17794
  const multiAgent = isMultiAgentEnabled();
17735
17795
  const CAP_TOOL_USE_THRESHOLD = 30;
17736
17796
  const subTails = new Map;
17737
- const IDLE_FSWATCH_TTL_MS = 5 * 60 * 1000;
17797
+ const IDLE_FSWATCH_TTL_MS = Math.max(1000, config2.subTailIdleReapMs ?? 5 * 60 * 1000);
17738
17798
  function readSub(t) {
17739
17799
  if (stopped)
17740
17800
  return;
@@ -17841,6 +17901,7 @@ function startSessionTail(config2) {
17841
17901
  } catch {}
17842
17902
  t.watcher = null;
17843
17903
  }
17904
+ stopSidecar(sessionIdForFile(t.file));
17844
17905
  subTails.delete(file);
17845
17906
  log?.(`session-tail: reaped idle sub ${t.agentId} (${file})`);
17846
17907
  }
@@ -17925,6 +17986,7 @@ function startSessionTail(config2) {
17925
17986
  var MAX_JSONL_LINE_BYTES, MAX_ERROR_TEXT_CHARS = 500;
17926
17987
  var init_session_tail = __esm(() => {
17927
17988
  init_operator_events();
17989
+ init_model_unavailable();
17928
17990
  init_tool_label_sidecar();
17929
17991
  init_model_label();
17930
17992
  MAX_JSONL_LINE_BYTES = 2 * 1024 * 1024;
@@ -24259,6 +24321,7 @@ var init_permission_rule = __esm(() => {
24259
24321
  "WebSearch",
24260
24322
  "Task",
24261
24323
  "Agent",
24324
+ "Workflow",
24262
24325
  "TodoWrite",
24263
24326
  "ExitPlanMode"
24264
24327
  ]);
@@ -24305,6 +24368,60 @@ var init_crash_breadcrumb = __esm(() => {
24305
24368
  MAX_LOG_BYTES = 1024 * 1024;
24306
24369
  });
24307
24370
 
24371
+ // bridge/inbound-dedup.ts
24372
+ function shouldDedupInbound(msg) {
24373
+ if (!Number.isFinite(msg.messageId) || msg.messageId <= 0)
24374
+ return false;
24375
+ const m = msg.meta ?? {};
24376
+ if (m.source != null)
24377
+ return false;
24378
+ if (m.button_callback != null)
24379
+ return false;
24380
+ if (m.kind != null)
24381
+ return false;
24382
+ if (m.event != null)
24383
+ return false;
24384
+ return true;
24385
+ }
24386
+
24387
+ class InboundDedup {
24388
+ maxPerChat;
24389
+ byChat = new Map;
24390
+ constructor(maxPerChat = INBOUND_DEDUP_MAX_PER_CHAT) {
24391
+ this.maxPerChat = maxPerChat;
24392
+ }
24393
+ checkAndRecord(chatKey, messageId) {
24394
+ let set2 = this.byChat.get(chatKey);
24395
+ if (set2 == null) {
24396
+ set2 = new Set;
24397
+ this.byChat.set(chatKey, set2);
24398
+ }
24399
+ if (set2.has(messageId))
24400
+ return "duplicate";
24401
+ if (set2.size >= this.maxPerChat) {
24402
+ const oldest = set2.values().next().value;
24403
+ if (oldest !== undefined)
24404
+ set2.delete(oldest);
24405
+ }
24406
+ set2.add(messageId);
24407
+ return "fresh";
24408
+ }
24409
+ clearChat(chatId) {
24410
+ const prefix = `${chatId}:`;
24411
+ for (const key of this.byChat.keys()) {
24412
+ if (key === chatId || key.startsWith(prefix))
24413
+ this.byChat.delete(key);
24414
+ }
24415
+ }
24416
+ size(chatKey) {
24417
+ return this.byChat.get(chatKey)?.size ?? 0;
24418
+ }
24419
+ }
24420
+ function dedupChatKey(chatId, threadId) {
24421
+ return `${chatId}:${threadId == null || threadId === 0 ? "_" : threadId}`;
24422
+ }
24423
+ var INBOUND_DEDUP_MAX_PER_CHAT = 512;
24424
+
24308
24425
  // bridge/bridge.ts
24309
24426
  var exports_bridge = {};
24310
24427
  import { dirname as dirname3, join as join5 } from "path";
@@ -24330,6 +24447,14 @@ function flushOutstandingPermissionRequests() {
24330
24447
  }
24331
24448
  }
24332
24449
  function onInbound(msg) {
24450
+ if (inboundDedupEnabled && shouldDedupInbound(msg)) {
24451
+ const chatKey = dedupChatKey(msg.chatId, msg.threadId);
24452
+ if (inboundDedup.checkAndRecord(chatKey, msg.messageId) === "duplicate") {
24453
+ process.stderr.write(`telegram bridge: dropping duplicate inbound messageId=${msg.messageId} ` + `chat=${chatKey} (already forwarded this turn \u2014 sweep re-delivery, #2094)
24454
+ `);
24455
+ return;
24456
+ }
24457
+ }
24333
24458
  mcp.notification({
24334
24459
  method: "notifications/claude/channel",
24335
24460
  params: {
@@ -24413,7 +24538,7 @@ async function main() {
24413
24538
  }
24414
24539
  await mcp.connect(new StdioServerTransport);
24415
24540
  }
24416
- var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, outstandingPermissions, permissionRearmEnabled, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null, CRASH_LOG_PATH;
24541
+ var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, outstandingPermissions, permissionRearmEnabled, ipc = null, inboundDedup, inboundDedupEnabled, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null, CRASH_LOG_PATH;
24417
24542
  var init_bridge = __esm(async () => {
24418
24543
  init_server2();
24419
24544
  init_stdio2();
@@ -24492,7 +24617,8 @@ var init_bridge = __esm(async () => {
24492
24617
  url: { type: "string", description: "Open this URL when tapped. Mutually exclusive with callback_data." },
24493
24618
  callback_data: { type: "string", description: "Opaque tag delivered back to the agent on tap. Max 58 chars (gateway prepends an `agent:` prefix to the 64-byte Telegram limit). Mutually exclusive with url." },
24494
24619
  ack_text: { type: "string", description: 'Toast text shown to the user the instant they tap this button (#710). Default "\u2713 received". Max ~200 chars (Telegram answerCallbackQuery limit). Has no effect on URL buttons.' },
24495
- single_use: { type: "boolean", description: `When true (default) tapping any single_use button on the message removes the entire keyboard so the user can't double-fire. Set false on buttons that should stay tappable (e.g. a "Refresh" button). If ANY button on the message has single_use:false the keyboard is preserved on tap.` }
24620
+ single_use: { type: "boolean", description: `When true (default) tapping any single_use button on the message removes the entire keyboard so the user can't double-fire. Set false on buttons that should stay tappable (e.g. a "Refresh" button). If ANY button on the message has single_use:false the keyboard is preserved on tap.` },
24621
+ inline_keyboard_confirm: { type: "boolean", description: `Per-message override for the "\u2705 You chose: <label> \u00b7 HH:MM" body annotation on tap (#789). true forces the annotation even if the agent default is off; false skips it even if the agent default is on; omit to use the agent default. Only applies to single-use keyboards and requires the agent parseMode to be the default html. Has no effect on URL buttons. Note: annotation rebuilds the body from the message's plain text, so formatting entities on the original message are lost.` }
24496
24622
  },
24497
24623
  required: ["text"]
24498
24624
  }
@@ -24903,6 +25029,12 @@ var init_bridge = __esm(async () => {
24903
25029
  isError: true
24904
25030
  };
24905
25031
  }
25032
+ if (inboundDedupEnabled && (tool === "reply" || tool === "stream_reply")) {
25033
+ const replyChatId = args.chat_id;
25034
+ if (typeof replyChatId === "string" && replyChatId.length > 0) {
25035
+ inboundDedup.clearChat(replyChatId);
25036
+ }
25037
+ }
24906
25038
  if (result.result && typeof result.result === "object" && "content" in result.result) {
24907
25039
  return result.result;
24908
25040
  }
@@ -24966,6 +25098,8 @@ var init_bridge = __esm(async () => {
24966
25098
  inputPreview: params.input_preview
24967
25099
  });
24968
25100
  });
25101
+ inboundDedup = new InboundDedup;
25102
+ inboundDedupEnabled = process.env.SWITCHROOM_INBOUND_DEDUP !== "0";
24969
25103
  sessionTailEnabled = process.env.SWITCHROOM_SESSION_TAIL !== "off";
24970
25104
  if (sessionTailEnabled) {
24971
25105
  try {