switchroom 0.21.10 → 0.21.11

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 (38) hide show
  1. package/dist/cli/switchroom.js +143 -27
  2. package/dist/host-control/main.js +1 -1
  3. package/package.json +3 -2
  4. package/telegram-plugin/dist/gateway/gateway.js +385 -35
  5. package/telegram-plugin/format.ts +56 -0
  6. package/telegram-plugin/gateway/answer-route-overrides.ts +163 -0
  7. package/telegram-plugin/gateway/answer-thread-resolve.test.ts +175 -1
  8. package/telegram-plugin/gateway/answer-thread-resolve.ts +58 -6
  9. package/telegram-plugin/gateway/escalation-staleness.ts +526 -0
  10. package/telegram-plugin/gateway/gateway.ts +61 -68
  11. package/telegram-plugin/gateway/obligation-wiring.ts +91 -3
  12. package/telegram-plugin/gateway/outbound-send-path.ts +55 -1
  13. package/telegram-plugin/gateway/reply-route-log.test.ts +134 -0
  14. package/telegram-plugin/gateway/reply-route-log.ts +118 -0
  15. package/telegram-plugin/gateway/stream-render.ts +1 -1
  16. package/telegram-plugin/history.ts +21 -0
  17. package/telegram-plugin/registry/subagents-bugs.test.ts +3 -3
  18. package/telegram-plugin/render/html-fold.ts +22 -4
  19. package/telegram-plugin/render/parse.ts +9 -1
  20. package/telegram-plugin/render/render.ts +14 -13
  21. package/telegram-plugin/tests/answer-route-side-effect.test.ts +111 -0
  22. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +3 -3
  23. package/telegram-plugin/tests/escalation-staleness.test.ts +1275 -0
  24. package/telegram-plugin/tests/forwarded-rich-message-coalesce.test.ts +6 -6
  25. package/telegram-plugin/tests/forwarded-rich-message.test.ts +8 -8
  26. package/telegram-plugin/tests/history.test.ts +78 -0
  27. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +29 -1
  28. package/telegram-plugin/tests/orphaned-db-sweep.test.ts +17 -1
  29. package/telegram-plugin/tests/render/html-dialect-content-loss.test.ts +76 -3
  30. package/telegram-plugin/tests/send-reply-golden.test.ts +192 -3
  31. package/telegram-plugin/tests/status-pin.test.ts +2 -2
  32. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +2 -2
  33. package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +2 -2
  34. package/telegram-plugin/tests/telegram-format.test.ts +52 -0
  35. package/telegram-plugin/tests/turn-supersede-finalizes-prior-card.test.ts +1 -1
  36. package/telegram-plugin/tests/worker-origin-gap-dispatch.test.ts +1 -1
  37. package/telegram-plugin/uat/scenarios/jtbd-supergroup-reply-channel.test.ts +1 -1
  38. package/telegram-plugin/worker-activity-feed.ts +2 -2
@@ -6769,6 +6769,20 @@ function escapeMarkdown(text) {
6769
6769
  function codeSpanSafe(s) {
6770
6770
  return s.replace(/`/g, "`\u200b");
6771
6771
  }
6772
+ function codeFenceFor(text) {
6773
+ let longestRun = 0;
6774
+ let current = 0;
6775
+ for (let i = 0;i < text.length; i++) {
6776
+ if (text[i] === "`") {
6777
+ current++;
6778
+ if (current > longestRun)
6779
+ longestRun = current;
6780
+ } else {
6781
+ current = 0;
6782
+ }
6783
+ }
6784
+ return "`".repeat(Math.max(3, longestRun + 1));
6785
+ }
6772
6786
  function escapeLinkHref(href) {
6773
6787
  return href.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)").replace(/[\x00-\x20\x7f]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
6774
6788
  }
@@ -30565,12 +30579,16 @@ function getRecentOutboundCount(chatId, withinSeconds) {
30565
30579
  const row = prep2("SELECT COUNT(*) as cnt FROM messages WHERE chat_id = ? AND role = ? AND ts >= ?").get(chatId, "assistant", cutoff);
30566
30580
  return row?.cnt ?? 0;
30567
30581
  }
30568
- function hasOutboundDeliveredSince2(chatId, sinceMs, threadId, minChars = 200) {
30582
+ function hasOutboundDeliveredSince2(chatId, sinceMs, threadId, minChars = 200, untilMs) {
30569
30583
  try {
30570
30584
  const cutoffSec = Math.floor(sinceMs / 1000);
30571
30585
  const minLen = Math.max(1, Math.floor(minChars));
30572
30586
  const params = [chatId, cutoffSec, minLen];
30573
30587
  let sql = "SELECT 1 FROM messages WHERE chat_id = ? AND role = 'assistant' AND ts >= ? AND LENGTH(text) >= ?";
30588
+ if (untilMs != null && Number.isFinite(untilMs)) {
30589
+ sql += " AND ts <= ?";
30590
+ params.push(Math.ceil(untilMs / 1000));
30591
+ }
30574
30592
  if (threadId !== undefined) {
30575
30593
  if (threadId === null) {
30576
30594
  sql += " AND thread_id IS NULL";
@@ -65182,6 +65200,7 @@ function tokenizeHtml(raw, base) {
65182
65200
  }
65183
65201
 
65184
65202
  // render/parse.ts
65203
+ init_format();
65185
65204
  function pos(node2) {
65186
65205
  const p = node2.position;
65187
65206
  return {
@@ -65488,7 +65507,7 @@ function buildPreBlock(innerItems, ctx, span) {
65488
65507
  if (ctx.noBreaks === true) {
65489
65508
  return [{ type: "code", text: text4.replace(/\s*\r?\n\s*/g, " "), ...span }];
65490
65509
  }
65491
- const fence = "```";
65510
+ const fence = codeFenceFor(text4);
65492
65511
  return [
65493
65512
  mkSeparator(ctx, span.start, span.start),
65494
65513
  {
@@ -65722,8 +65741,7 @@ function renderBlockquote(node2) {
65722
65741
  }
65723
65742
  function renderCodeBlock(node2) {
65724
65743
  const lang = node2.language ?? "";
65725
- const longestRun = Math.max(0, ...(node2.text.match(/`+/g) ?? []).map((run2) => run2.length));
65726
- const fence = "`".repeat(Math.max(3, longestRun + 1));
65744
+ const fence = codeFenceFor(node2.text);
65727
65745
  return `${fence}${lang}
65728
65746
  ${node2.text}
65729
65747
  ${fence}`;
@@ -65823,8 +65841,7 @@ function tableDegradationReason(node2) {
65823
65841
  }
65824
65842
  function degradeToCodeFence(source, node2) {
65825
65843
  const raw = source.slice(node2.start, node2.end);
65826
- const longestRun = Math.max(0, ...(raw.match(/`+/g) ?? []).map((run2) => run2.length));
65827
- const fence = "`".repeat(Math.max(3, longestRun + 1));
65844
+ const fence = codeFenceFor(raw);
65828
65845
  return `${fence}
65829
65846
  ${raw}
65830
65847
  ${fence}`;
@@ -77239,12 +77256,16 @@ function lookupMessageRoleAndText(chatId, messageId, opts) {
77239
77256
  return null;
77240
77257
  return { role: row.role, text: row.text ?? "", kind: row.kind ?? null };
77241
77258
  }
77242
- function hasOutboundDeliveredSince(chatId, sinceMs, threadId, minChars = 200) {
77259
+ function hasOutboundDeliveredSince(chatId, sinceMs, threadId, minChars = 200, untilMs) {
77243
77260
  try {
77244
77261
  const cutoffSec = Math.floor(sinceMs / 1000);
77245
77262
  const minLen = Math.max(1, Math.floor(minChars));
77246
77263
  const params = [chatId, cutoffSec, minLen];
77247
77264
  let sql = "SELECT 1 FROM messages WHERE chat_id = ? AND role = 'assistant' AND ts >= ? AND LENGTH(text) >= ?";
77265
+ if (untilMs != null && Number.isFinite(untilMs)) {
77266
+ sql += " AND ts <= ?";
77267
+ params.push(Math.ceil(untilMs / 1000));
77268
+ }
77248
77269
  if (threadId !== undefined) {
77249
77270
  if (threadId === null) {
77250
77271
  sql += " AND thread_id IS NULL";
@@ -81148,6 +81169,33 @@ function journalExternalDelivery(args, stateDir, now = Date.now()) {
81148
81169
  clearOutboxRecord(nonce, stateDir);
81149
81170
  }
81150
81171
 
81172
+ // gateway/answer-thread-resolve.ts
81173
+ function isCrossChatAnchor(targetChatId, anchorChatId) {
81174
+ if (targetChatId == null || targetChatId === "")
81175
+ return false;
81176
+ if (anchorChatId == null || anchorChatId === "")
81177
+ return false;
81178
+ return String(anchorChatId) !== String(targetChatId);
81179
+ }
81180
+
81181
+ // gateway/reply-route-log.ts
81182
+ function formatReplyRouteLog(i) {
81183
+ const originCrossChat = isCrossChatAnchor(i.chatId, i.originTurn?.sessionChatId);
81184
+ const liveCrossChat = isCrossChatAnchor(i.chatId, i.liveTurn?.sessionChatId);
81185
+ const crossChatDropped = originCrossChat || liveCrossChat;
81186
+ const originAnchor = originCrossChat ? null : i.originTurn;
81187
+ const liveAnchor = liveCrossChat ? null : i.liveTurn;
81188
+ const explicit = i.explicitThreadId;
81189
+ const via = i.frameworkTopicAuthority ? originAnchor != null ? i.originVia === "quoted" ? "quoted" : "origin" : liveAnchor != null ? "live" : explicit != null ? "explicit" : i.recovered != null ? "recovered" : "none" : explicit != null ? "explicit" : originAnchor != null ? i.originVia === "quoted" ? "quoted" : "origin" : liveAnchor?.sessionThreadId != null ? "live" : i.recovered != null ? "recovered" : "none";
81190
+ const explicitOverridden = i.frameworkTopicAuthority && explicit != null && (originAnchor != null || liveAnchor != null) && i.threadId !== explicit;
81191
+ const ownerTurn = originAnchor ?? i.recovered ?? liveAnchor;
81192
+ const isSupergroup = i.chatId.startsWith("-100");
81193
+ const unrouted = isSupergroup && i.threadId == null && ownerTurn == null && !crossChatDropped;
81194
+ const misrouteRisk = isSupergroup && via === "live" && i.hasDifferentThreadedRecentTurn(liveAnchor?.sessionThreadId);
81195
+ return `telegram gateway: reply-route surface=${i.surface} chat=${i.chatId} ` + `resolved_thread=${i.threadId ?? "-"} via=${via} late=${liveAnchor == null} ` + `originTurn=${ownerTurn?.turnId ?? "-"} origin_thread=${ownerTurn?.sessionThreadId ?? "-"}` + (via === "recovered" ? " RECOVERED" : "") + (via === "quoted" ? " QUOTED(framework-origin)" : "") + (unrouted ? " UNROUTED(supergroup\u2192no-topic)" : "") + (misrouteRisk ? " MISROUTE_RISK(no-echo\u2192live-successor)" : "") + (crossChatDropped ? ` CROSS_CHAT_ANCHOR_DROPPED(target=${i.chatId}` + (originCrossChat ? `,origin_chat=${i.originTurn?.sessionChatId},origin_thread=${i.originTurn?.sessionThreadId ?? "-"}` : "") + (liveCrossChat ? `,live_chat=${i.liveTurn?.sessionChatId},live_thread=${i.liveTurn?.sessionThreadId ?? "-"}` : "") + `,routed\u2192${i.threadId ?? "-"})` : "") + (explicitOverridden ? ` EXPLICIT_OVERRIDDEN(model\u2192${explicit},routed\u2192${i.threadId ?? "-"})` : "") + `
81196
+ `;
81197
+ }
81198
+
81151
81199
  // gateway/flood-reply-queue.ts
81152
81200
  var FLOOD_QUEUED_SOURCE = "flood-deferred-reply";
81153
81201
  function isFloodRejection(err) {
@@ -82357,7 +82405,24 @@ ${url}`;
82357
82405
  replyRoutedOriginTurn = originTurn ?? null;
82358
82406
  threadId = resolveAnswerThreadWithLog(chat_id, Number.isFinite(explicit) ? explicit : undefined, originTurn, originTurn == null ? null : echoedTurn != null ? "echo" : "quoted", turn, "reply");
82359
82407
  } else {
82360
- threadId = resolveThreadId(chat_id, args.message_thread_id ?? (turn?.sessionThreadId != null ? turn.sessionThreadId : undefined));
82408
+ const anchorCrossChat = isCrossChatAnchor(chat_id, turn?.sessionChatId);
82409
+ const anchorThreadId = anchorCrossChat ? undefined : turn?.sessionThreadId;
82410
+ threadId = resolveThreadId(chat_id, args.message_thread_id ?? (anchorThreadId != null ? anchorThreadId : undefined));
82411
+ if (anchorCrossChat) {
82412
+ const explicitRaw = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
82413
+ process.stderr.write(formatReplyRouteLog({
82414
+ surface: "reply",
82415
+ chatId: chat_id,
82416
+ threadId,
82417
+ explicitThreadId: Number.isFinite(explicitRaw) ? explicitRaw : undefined,
82418
+ originTurn: null,
82419
+ originVia: null,
82420
+ liveTurn: turn,
82421
+ recovered: null,
82422
+ frameworkTopicAuthority: false,
82423
+ hasDifferentThreadedRecentTurn: () => false
82424
+ }));
82425
+ }
82361
82426
  }
82362
82427
  let antecedentFromQuoteOptInDefault = false;
82363
82428
  if (reply_to == null && quoteOptIn && HISTORY_ENABLED) {
@@ -97095,27 +97160,120 @@ class CurrentTurnMap {
97095
97160
  }
97096
97161
 
97097
97162
  // gateway/answer-thread-resolve.ts
97163
+ function isCrossChatAnchor2(targetChatId, anchorChatId) {
97164
+ if (targetChatId == null || targetChatId === "")
97165
+ return false;
97166
+ if (anchorChatId == null || anchorChatId === "")
97167
+ return false;
97168
+ return String(anchorChatId) !== String(targetChatId);
97169
+ }
97098
97170
  function resolveAnswerThreadId(input) {
97171
+ const originCrossChat = isCrossChatAnchor2(input.targetChatId, input.originChatId);
97172
+ const liveCrossChat = isCrossChatAnchor2(input.targetChatId, input.liveChatId);
97173
+ const originResolved = input.originResolved && !originCrossChat;
97174
+ const originThreadId = originCrossChat ? undefined : input.originThreadId;
97175
+ const liveTurnPresent = input.liveTurnPresent === true && !liveCrossChat;
97176
+ const liveThreadId = liveCrossChat ? undefined : input.liveThreadId;
97099
97177
  if (input.frameworkTopicAuthority === false) {
97100
97178
  if (input.explicitThreadId != null)
97101
97179
  return input.explicitThreadId;
97102
- if (input.originResolved)
97103
- return input.originThreadId;
97104
- if (input.liveThreadId != null)
97105
- return input.liveThreadId;
97180
+ if (originResolved)
97181
+ return originThreadId;
97182
+ if (liveThreadId != null)
97183
+ return liveThreadId;
97106
97184
  if (input.lastEndedResolvedForChat)
97107
97185
  return input.lastEndedThreadIdForChat;
97108
- return input.liveThreadId;
97186
+ return liveThreadId;
97109
97187
  }
97110
- if (input.originResolved)
97111
- return input.originThreadId;
97112
- if (input.liveTurnPresent)
97113
- return input.liveThreadId;
97188
+ if (originResolved)
97189
+ return originThreadId;
97190
+ if (liveTurnPresent)
97191
+ return liveThreadId;
97114
97192
  if (input.explicitThreadId != null)
97115
97193
  return input.explicitThreadId;
97116
97194
  if (input.lastEndedResolvedForChat)
97117
97195
  return input.lastEndedThreadIdForChat;
97118
- return input.liveThreadId;
97196
+ return liveThreadId;
97197
+ }
97198
+
97199
+ // gateway/answer-route-overrides.ts
97200
+ function keyFor(chatId, threadId) {
97201
+ return `${chatId}:${threadId ?? "_"}`;
97202
+ }
97203
+ function createAnswerRouteOverrides(maxKeys = 64, maxPerKey = 8) {
97204
+ const byKey = new Map;
97205
+ return {
97206
+ note(args) {
97207
+ const overridden = args.enabled && args.explicitThreadId != null && args.anchored && args.routedThreadId !== args.explicitThreadId;
97208
+ if (!overridden)
97209
+ return false;
97210
+ const key = keyFor(args.chatId, args.explicitThreadId);
97211
+ const list2 = byKey.get(key) ?? [];
97212
+ list2.push({ routedThreadId: args.routedThreadId ?? null, atMs: args.nowMs });
97213
+ while (list2.length > maxPerKey)
97214
+ list2.shift();
97215
+ byKey.set(key, list2);
97216
+ while (byKey.size > maxKeys) {
97217
+ const oldest = byKey.keys().next().value;
97218
+ if (oldest === undefined)
97219
+ break;
97220
+ byKey.delete(oldest);
97221
+ }
97222
+ return true;
97223
+ },
97224
+ routedOverridesSince(chatId, intendedThreadId, notBeforeMs) {
97225
+ const list2 = byKey.get(keyFor(chatId, intendedThreadId));
97226
+ if (list2 == null)
97227
+ return [];
97228
+ const out = [];
97229
+ for (const e of list2) {
97230
+ if (e.atMs < notBeforeMs)
97231
+ continue;
97232
+ if (out.some((seen) => seen.routedThreadId === e.routedThreadId))
97233
+ continue;
97234
+ out.push({ ...e });
97235
+ }
97236
+ return out;
97237
+ },
97238
+ newestOverrideSince(chatId, intendedThreadId, notBeforeMs) {
97239
+ const list2 = byKey.get(keyFor(chatId, intendedThreadId));
97240
+ if (list2 == null)
97241
+ return;
97242
+ let newest;
97243
+ for (const e of list2) {
97244
+ if (e.atMs < notBeforeMs)
97245
+ continue;
97246
+ if (newest == null || e.atMs > newest.atMs)
97247
+ newest = e;
97248
+ }
97249
+ return newest == null ? undefined : { ...newest };
97250
+ },
97251
+ size() {
97252
+ return byKey.size;
97253
+ },
97254
+ clear() {
97255
+ byKey.clear();
97256
+ }
97257
+ };
97258
+ }
97259
+ var answerRouteOverrides = createAnswerRouteOverrides();
97260
+
97261
+ // gateway/reply-route-log.ts
97262
+ function formatReplyRouteLog2(i) {
97263
+ const originCrossChat = isCrossChatAnchor(i.chatId, i.originTurn?.sessionChatId);
97264
+ const liveCrossChat = isCrossChatAnchor(i.chatId, i.liveTurn?.sessionChatId);
97265
+ const crossChatDropped = originCrossChat || liveCrossChat;
97266
+ const originAnchor = originCrossChat ? null : i.originTurn;
97267
+ const liveAnchor = liveCrossChat ? null : i.liveTurn;
97268
+ const explicit = i.explicitThreadId;
97269
+ const via = i.frameworkTopicAuthority ? originAnchor != null ? i.originVia === "quoted" ? "quoted" : "origin" : liveAnchor != null ? "live" : explicit != null ? "explicit" : i.recovered != null ? "recovered" : "none" : explicit != null ? "explicit" : originAnchor != null ? i.originVia === "quoted" ? "quoted" : "origin" : liveAnchor?.sessionThreadId != null ? "live" : i.recovered != null ? "recovered" : "none";
97270
+ const explicitOverridden = i.frameworkTopicAuthority && explicit != null && (originAnchor != null || liveAnchor != null) && i.threadId !== explicit;
97271
+ const ownerTurn = originAnchor ?? i.recovered ?? liveAnchor;
97272
+ const isSupergroup = i.chatId.startsWith("-100");
97273
+ const unrouted = isSupergroup && i.threadId == null && ownerTurn == null && !crossChatDropped;
97274
+ const misrouteRisk = isSupergroup && via === "live" && i.hasDifferentThreadedRecentTurn(liveAnchor?.sessionThreadId);
97275
+ return `telegram gateway: reply-route surface=${i.surface} chat=${i.chatId} ` + `resolved_thread=${i.threadId ?? "-"} via=${via} late=${liveAnchor == null} ` + `originTurn=${ownerTurn?.turnId ?? "-"} origin_thread=${ownerTurn?.sessionThreadId ?? "-"}` + (via === "recovered" ? " RECOVERED" : "") + (via === "quoted" ? " QUOTED(framework-origin)" : "") + (unrouted ? " UNROUTED(supergroup\u2192no-topic)" : "") + (misrouteRisk ? " MISROUTE_RISK(no-echo\u2192live-successor)" : "") + (crossChatDropped ? ` CROSS_CHAT_ANCHOR_DROPPED(target=${i.chatId}` + (originCrossChat ? `,origin_chat=${i.originTurn?.sessionChatId},origin_thread=${i.originTurn?.sessionThreadId ?? "-"}` : "") + (liveCrossChat ? `,live_chat=${i.liveTurn?.sessionChatId},live_thread=${i.liveTurn?.sessionThreadId ?? "-"}` : "") + `,routed\u2192${i.threadId ?? "-"})` : "") + (explicitOverridden ? ` EXPLICIT_OVERRIDDEN(model\u2192${explicit},routed\u2192${i.threadId ?? "-"})` : "") + `
97276
+ `;
97119
97277
  }
97120
97278
 
97121
97279
  // gateway/latest-turn-lookup.ts
@@ -98532,6 +98690,153 @@ function shouldDeferEscalationForBridge(deps) {
98532
98690
  return !deps.bridgeAlive;
98533
98691
  }
98534
98692
 
98693
+ // gateway/escalation-staleness.ts
98694
+ var OBLIGATION_ESCALATE_SETTLE_MS_DEFAULT = 8500;
98695
+ var OBLIGATION_ESCALATE_SETTLE_MS_MAX = 60000;
98696
+ var REROUTE_MATCH_GRACE_MS = 1e4;
98697
+ var OBLIGATION_REROUTE_MATCH_MS_MAX = OBLIGATION_ESCALATE_SETTLE_MS_MAX + REROUTE_MATCH_GRACE_MS;
98698
+ function resolveRerouteMatchWindowMs(settleMs, env = process.env) {
98699
+ const derived = Math.max(settleMs, 0) + REROUTE_MATCH_GRACE_MS;
98700
+ const raw = env.SWITCHROOM_OBLIGATION_REROUTE_MATCH_MS;
98701
+ if (raw == null || raw.trim() === "")
98702
+ return derived;
98703
+ const n = Number(raw);
98704
+ if (!(Number.isFinite(n) && n >= 0))
98705
+ return derived;
98706
+ return Math.min(n, OBLIGATION_REROUTE_MATCH_MS_MAX);
98707
+ }
98708
+ function resolveEscalateSettleMs(env = process.env) {
98709
+ const raw = env.SWITCHROOM_OBLIGATION_ESCALATE_SETTLE_MS;
98710
+ if (raw == null || raw.trim() === "")
98711
+ return OBLIGATION_ESCALATE_SETTLE_MS_DEFAULT;
98712
+ const n = Number(raw);
98713
+ if (!(Number.isFinite(n) && n >= 0))
98714
+ return OBLIGATION_ESCALATE_SETTLE_MS_DEFAULT;
98715
+ return Math.min(n, OBLIGATION_ESCALATE_SETTLE_MS_MAX);
98716
+ }
98717
+ function answeredSinceOpen(o, deps) {
98718
+ const NOT_ANSWERED = { answered: false, via: null };
98719
+ if (!deps.historyEnabled)
98720
+ return NOT_ANSWERED;
98721
+ if (deps.hasOutboundDeliveredSince(o.chatId, o.openedAt, o.threadId)) {
98722
+ return { answered: true, via: "thread" };
98723
+ }
98724
+ if (!(deps.rerouteMatchWindowMs > 0))
98725
+ return NOT_ANSWERED;
98726
+ const notBefore = Math.max(o.openedAt, deps.anchorMs - deps.rerouteMatchWindowMs);
98727
+ for (const ovr of deps.routeOverrides.routedOverridesSince(o.chatId, o.threadId, notBefore)) {
98728
+ const since = Math.max(ovr.atMs, o.openedAt);
98729
+ const until = ovr.atMs + deps.rerouteMatchWindowMs;
98730
+ if (deps.hasOutboundDeliveredBetween(o.chatId, since, until, ovr.routedThreadId)) {
98731
+ return { answered: true, via: "reroute", routedThreadId: ovr.routedThreadId };
98732
+ }
98733
+ }
98734
+ const newest = deps.routeOverrides.newestOverrideSince(o.chatId, o.threadId, o.openedAt);
98735
+ if (newest != null && newest.atMs < notBefore) {
98736
+ return { answered: false, via: null, staleOverrideAgeMs: Math.max(0, deps.anchorMs - newest.atMs) };
98737
+ }
98738
+ return NOT_ANSWERED;
98739
+ }
98740
+ function createEscalationSettleGate(settleMs, maxKeys = 256) {
98741
+ const entries = new Map;
98742
+ return {
98743
+ shouldDefer(id, now, openedAt) {
98744
+ if (!(settleMs > 0))
98745
+ return false;
98746
+ const prev = entries.get(id);
98747
+ if (prev == null || prev.openedAt !== openedAt) {
98748
+ entries.set(id, { firstAt: now, openedAt });
98749
+ while (entries.size > maxKeys) {
98750
+ const oldest = entries.keys().next().value;
98751
+ if (oldest === undefined)
98752
+ break;
98753
+ entries.delete(oldest);
98754
+ }
98755
+ return true;
98756
+ }
98757
+ if (now < prev.firstAt) {
98758
+ entries.set(id, { firstAt: now, openedAt });
98759
+ return true;
98760
+ }
98761
+ return now - prev.firstAt < settleMs;
98762
+ },
98763
+ firstAt(id, openedAt) {
98764
+ const prev = entries.get(id);
98765
+ if (prev == null || prev.openedAt !== openedAt)
98766
+ return;
98767
+ return prev.firstAt;
98768
+ },
98769
+ clear(id) {
98770
+ entries.delete(id);
98771
+ },
98772
+ size() {
98773
+ return entries.size;
98774
+ }
98775
+ };
98776
+ }
98777
+
98778
+ // gateway/answer-route-overrides.ts
98779
+ function keyFor2(chatId, threadId) {
98780
+ return `${chatId}:${threadId ?? "_"}`;
98781
+ }
98782
+ function createAnswerRouteOverrides2(maxKeys = 64, maxPerKey = 8) {
98783
+ const byKey = new Map;
98784
+ return {
98785
+ note(args) {
98786
+ const overridden = args.enabled && args.explicitThreadId != null && args.anchored && args.routedThreadId !== args.explicitThreadId;
98787
+ if (!overridden)
98788
+ return false;
98789
+ const key = keyFor2(args.chatId, args.explicitThreadId);
98790
+ const list2 = byKey.get(key) ?? [];
98791
+ list2.push({ routedThreadId: args.routedThreadId ?? null, atMs: args.nowMs });
98792
+ while (list2.length > maxPerKey)
98793
+ list2.shift();
98794
+ byKey.set(key, list2);
98795
+ while (byKey.size > maxKeys) {
98796
+ const oldest = byKey.keys().next().value;
98797
+ if (oldest === undefined)
98798
+ break;
98799
+ byKey.delete(oldest);
98800
+ }
98801
+ return true;
98802
+ },
98803
+ routedOverridesSince(chatId, intendedThreadId, notBeforeMs) {
98804
+ const list2 = byKey.get(keyFor2(chatId, intendedThreadId));
98805
+ if (list2 == null)
98806
+ return [];
98807
+ const out = [];
98808
+ for (const e of list2) {
98809
+ if (e.atMs < notBeforeMs)
98810
+ continue;
98811
+ if (out.some((seen) => seen.routedThreadId === e.routedThreadId))
98812
+ continue;
98813
+ out.push({ ...e });
98814
+ }
98815
+ return out;
98816
+ },
98817
+ newestOverrideSince(chatId, intendedThreadId, notBeforeMs) {
98818
+ const list2 = byKey.get(keyFor2(chatId, intendedThreadId));
98819
+ if (list2 == null)
98820
+ return;
98821
+ let newest;
98822
+ for (const e of list2) {
98823
+ if (e.atMs < notBeforeMs)
98824
+ continue;
98825
+ if (newest == null || e.atMs > newest.atMs)
98826
+ newest = e;
98827
+ }
98828
+ return newest == null ? undefined : { ...newest };
98829
+ },
98830
+ size() {
98831
+ return byKey.size;
98832
+ },
98833
+ clear() {
98834
+ byKey.clear();
98835
+ }
98836
+ };
98837
+ }
98838
+ var answerRouteOverrides2 = createAnswerRouteOverrides2();
98839
+
98535
98840
  // gateway/stream-render.ts
98536
98841
  init_history();
98537
98842
  init_rich_send();
@@ -98570,14 +98875,19 @@ function createObligationWiring(deps) {
98570
98875
  bridgeAlive,
98571
98876
  sendEscalationNudge
98572
98877
  } = deps;
98878
+ const escalateSettleMs = resolveEscalateSettleMs();
98879
+ const escalateSettleGate = createEscalationSettleGate(escalateSettleMs);
98880
+ const rerouteMatchWindowMs = resolveRerouteMatchWindowMs(escalateSettleMs);
98573
98881
  function closeObligationOnSubstantiveReply(args, liveTurn, routedOriginTurn) {
98574
98882
  if (!OBLIGATION_LEDGER_ENABLED)
98575
98883
  return;
98576
98884
  const echoed = findTurnByOriginId(args.origin_turn_id);
98577
98885
  const routedOriginId = routedOriginTurn != null && echoed == null ? routedOriginTurn.turnId : null;
98578
98886
  const target = obligationLedger.resolveCloseTarget(echoed?.turnId, liveTurn?.turnId, routedOriginId);
98579
- if (target != null)
98887
+ if (target != null) {
98580
98888
  obligationLedger.close(target);
98889
+ escalateSettleGate.clear(target);
98890
+ }
98581
98891
  }
98582
98892
  function openObligationFromInbound(inboundMsg, gate) {
98583
98893
  if (!OBLIGATION_LEDGER_ENABLED)
@@ -98609,6 +98919,7 @@ function createObligationWiring(deps) {
98609
98919
  if (turn == null)
98610
98920
  return;
98611
98921
  if (obligationLedger.close(turn.turnId)) {
98922
+ escalateSettleGate.clear(turn.turnId);
98612
98923
  process.stderr.write(`telegram gateway: obligation cancelled by interrupt origin=${turn.turnId}
98613
98924
  `);
98614
98925
  }
@@ -98649,6 +98960,7 @@ function createObligationWiring(deps) {
98649
98960
  })) {
98650
98961
  process.stderr.write(`telegram gateway: obligation closed silently \u2014 reply delivered since last represent (no re-fire) origin=${o.originTurnId}
98651
98962
  `);
98963
+ escalateSettleGate.clear(o.originTurnId);
98652
98964
  obligationLedger.close(o.originTurnId);
98653
98965
  return;
98654
98966
  }
@@ -98670,12 +98982,31 @@ function createObligationWiring(deps) {
98670
98982
  `);
98671
98983
  return;
98672
98984
  }
98673
- if (HISTORY_ENABLED && hasOutboundDeliveredSince3(o.chatId, o.openedAt, o.threadId)) {
98674
- process.stderr.write(`telegram gateway: obligation closed silently \u2014 outbound delivered since open origin=${o.originTurnId}
98985
+ const answered = answeredSinceOpen(o, {
98986
+ historyEnabled: HISTORY_ENABLED,
98987
+ hasOutboundDeliveredSince: hasOutboundDeliveredSince3,
98988
+ hasOutboundDeliveredBetween: (chatId, sinceMs, untilMs, threadId) => hasOutboundDeliveredSince3(chatId, sinceMs, threadId, undefined, untilMs),
98989
+ routeOverrides: answerRouteOverrides2,
98990
+ anchorMs: escalateSettleGate.firstAt(o.originTurnId, o.openedAt) ?? now,
98991
+ rerouteMatchWindowMs
98992
+ });
98993
+ if (answered.answered) {
98994
+ const scope = answered.via === "reroute" ? `via=reroute routed_thread=${answered.routedThreadId ?? "-"}` : "via=thread";
98995
+ process.stderr.write(`telegram gateway: obligation closed silently \u2014 outbound delivered since open ${scope} origin=${o.originTurnId}
98675
98996
  `);
98997
+ escalateSettleGate.clear(o.originTurnId);
98676
98998
  obligationLedger.close(o.originTurnId);
98677
98999
  return;
98678
99000
  }
99001
+ if (answered.staleOverrideAgeMs != null) {
99002
+ process.stderr.write(`telegram gateway: obligation reroute record rejected age=${answered.staleOverrideAgeMs}ms ` + `window=${rerouteMatchWindowMs}ms origin=${o.originTurnId}
99003
+ `);
99004
+ }
99005
+ if (escalateSettleGate.shouldDefer(o.originTurnId, now, o.openedAt)) {
99006
+ process.stderr.write(`telegram gateway: obligation escalation deferred \u2014 settling (re-checking for an in-flight answer) origin=${o.originTurnId}
99007
+ `);
99008
+ return;
99009
+ }
98679
99010
  driveEscalation({
98680
99011
  escId: o.originTurnId,
98681
99012
  inFlight: obligationEscalateInFlight,
@@ -98684,6 +99015,7 @@ function createObligationWiring(deps) {
98684
99015
  maxAttempts: OBLIGATION_ESCALATE_MAX,
98685
99016
  deadlineMs: OBLIGATION_ESCALATE_SEND_DEADLINE_MS
98686
99017
  });
99018
+ escalateSettleGate.clear(o.originTurnId);
98687
99019
  }
98688
99020
  return {
98689
99021
  closeObligationOnSubstantiveReply,
@@ -105407,10 +105739,10 @@ function startOutboxSweep(deps) {
105407
105739
  }
105408
105740
 
105409
105741
  // ../src/build-info.ts
105410
- var VERSION2 = "0.21.10";
105411
- var COMMIT_SHA = "fd1650e7";
105412
- var COMMIT_DATE = "2026-08-14T03:50:00Z";
105413
- var LATEST_PR = 4698;
105742
+ var VERSION2 = "0.21.11";
105743
+ var COMMIT_SHA = "7a80af9d";
105744
+ var COMMIT_DATE = "2026-08-14T09:19:49Z";
105745
+ var LATEST_PR = 4713;
105414
105746
  var COMMITS_AHEAD_OF_TAG = 0;
105415
105747
 
105416
105748
  // gateway/boot-version.ts
@@ -108939,7 +109271,9 @@ function resolveReplyOwnerTurn(liveTurn, chatId, args) {
108939
109271
  return resolveReplyOwnerTurnWith({ findTurnByOriginId, findTurnByQuotedMessageId, findLatestTurnForChat, now: Date.now }, liveTurn, chatId, args);
108940
109272
  }
108941
109273
  function resolveAnswerThreadWithLog(chatId, explicitThreadId, originTurn, originVia, liveTurn, surface) {
108942
- const recovered = LATE_REPLY_TOPIC_RECOVERY_ENABLED && explicitThreadId == null && originTurn == null && liveTurn == null ? findLatestTurnForChat(chatId, { endedOnly: false }) : null;
109274
+ const originAnchor = isCrossChatAnchor2(chatId, originTurn?.sessionChatId) ? null : originTurn;
109275
+ const liveAnchor = isCrossChatAnchor2(chatId, liveTurn?.sessionChatId) ? null : liveTurn;
109276
+ const recovered = LATE_REPLY_TOPIC_RECOVERY_ENABLED && explicitThreadId == null && originAnchor == null && liveAnchor == null ? findLatestTurnForChat(chatId, { endedOnly: false }) : null;
108943
109277
  const threadId = resolveAnswerThreadId({
108944
109278
  explicitThreadId,
108945
109279
  originResolved: originTurn != null,
@@ -108948,16 +109282,31 @@ function resolveAnswerThreadWithLog(chatId, explicitThreadId, originTurn, origin
108948
109282
  liveTurnPresent: liveTurn != null,
108949
109283
  lastEndedResolvedForChat: recovered != null,
108950
109284
  lastEndedThreadIdForChat: recovered?.sessionThreadId,
108951
- frameworkTopicAuthority: REPLY_TOPIC_AUTHORITY_ENABLED
109285
+ frameworkTopicAuthority: REPLY_TOPIC_AUTHORITY_ENABLED,
109286
+ targetChatId: chatId,
109287
+ originChatId: originTurn?.sessionChatId,
109288
+ liveChatId: liveTurn?.sessionChatId
108952
109289
  });
108953
- const via = REPLY_TOPIC_AUTHORITY_ENABLED ? originTurn != null ? originVia === "quoted" ? "quoted" : "origin" : liveTurn != null ? "live" : explicitThreadId != null ? "explicit" : recovered != null ? "recovered" : "none" : explicitThreadId != null ? "explicit" : originTurn != null ? originVia === "quoted" ? "quoted" : "origin" : liveTurn?.sessionThreadId != null ? "live" : recovered != null ? "recovered" : "none";
108954
- const explicitOverridden = REPLY_TOPIC_AUTHORITY_ENABLED && explicitThreadId != null && (originTurn != null || liveTurn != null) && threadId !== explicitThreadId;
108955
- const ownerTurn = originTurn ?? recovered ?? liveTurn;
108956
- const isSupergroup = chatId.startsWith("-100");
108957
- const unrouted = isSupergroup && threadId == null && ownerTurn == null;
108958
- const misrouteRisk = isSupergroup && via === "live" && hasDifferentThreadedRecentTurn(chatId, liveTurn?.sessionThreadId);
108959
- process.stderr.write(`telegram gateway: reply-route surface=${surface} chat=${chatId} resolved_thread=${threadId ?? "-"} via=${via} late=${liveTurn == null} originTurn=${ownerTurn?.turnId ?? "-"} origin_thread=${ownerTurn?.sessionThreadId ?? "-"}` + (via === "recovered" ? " RECOVERED" : "") + (via === "quoted" ? " QUOTED(framework-origin)" : "") + (unrouted ? " UNROUTED(supergroup\u2192no-topic)" : "") + (misrouteRisk ? " MISROUTE_RISK(no-echo\u2192live-successor)" : "") + (explicitOverridden ? ` EXPLICIT_OVERRIDDEN(model\u2192${explicitThreadId},routed\u2192${threadId ?? "-"})` : "") + `
108960
- `);
109290
+ answerRouteOverrides.note({
109291
+ chatId,
109292
+ enabled: REPLY_TOPIC_AUTHORITY_ENABLED,
109293
+ explicitThreadId,
109294
+ anchored: originAnchor != null || liveAnchor != null,
109295
+ routedThreadId: threadId,
109296
+ nowMs: Date.now()
109297
+ });
109298
+ process.stderr.write(formatReplyRouteLog2({
109299
+ surface,
109300
+ chatId,
109301
+ threadId,
109302
+ explicitThreadId,
109303
+ originTurn,
109304
+ originVia,
109305
+ liveTurn,
109306
+ recovered,
109307
+ frameworkTopicAuthority: REPLY_TOPIC_AUTHORITY_ENABLED,
109308
+ hasDifferentThreadedRecentTurn: (liveThreadId) => hasDifferentThreadedRecentTurn(chatId, liveThreadId)
109309
+ }));
108961
109310
  return threadId;
108962
109311
  }
108963
109312
  function hasDifferentThreadedRecentTurn(chatId, liveThreadId) {
@@ -119823,6 +120172,7 @@ async function startGateway() {
119823
120172
  if (isGatewayMain)
119824
120173
  startGateway();
119825
120174
  export {
120175
+ resolveAnswerThreadWithLog,
119826
120176
  initVaultApprovalPosture,
119827
120177
  handleInboundCoalesced,
119828
120178
  handleInbound,
@@ -85,6 +85,62 @@ export function codeSpanSafe(s: string): string {
85
85
  return s.replace(/`/g, '`​')
86
86
  }
87
87
 
88
+ /**
89
+ * Compute the delimiter for a fenced code block whose body is `text`.
90
+ *
91
+ * Fenced content is verbatim per the formatting guide; the only hazard is a
92
+ * backtick run inside the body CLOSING the fence early. A hardcoded ``` around
93
+ * a body that carries its own ``` ships three delimiters instead of two: the
94
+ * wire opens at the first, closes at the embedded one, reads the remainder as
95
+ * prose, and the trailing delimiter opens a fence that never terminates —
96
+ * `can't find end of Pre entity`, a 400, and a whole-message plain-text
97
+ * resend. Widening the delimiter to one backtick longer than the longest run
98
+ * already present makes the open/close pair the only runs of that width, so
99
+ * nothing in the body can match them.
100
+ *
101
+ * This is the canonical home for the rule (as `codeSpanSafe` is for the code
102
+ * SPAN hazard above): the markdown fence (`renderCodeBlock`), the degraded
103
+ * table fence (`degradeToCodeFence`) and the raw-HTML `<pre>` fold
104
+ * (`buildPreBlock`, render/parse.ts) all call it, so there is exactly ONE
105
+ * implementation of the width rule rather than a copy per call site.
106
+ *
107
+ * The scan is a LOOP, not `Math.max(0, ...runs.map(…))`. The spread form
108
+ * (which both former copies of this rule used) passes one argument per run,
109
+ * so a body with enough separate backtick runs blows the argument limit with
110
+ * `RangeError: Maximum call stack size exceeded`, throwing out of the render
111
+ * path entirely.
112
+ *
113
+ * The limit is ENGINE-DEPENDENT, so no single number describes it. The
114
+ * shipping runtime is Bun (`telegram-plugin/package.json` `start`,
115
+ * docker/Dockerfile.agent), where it measures at ~639k runs (~1.9 MB body);
116
+ * this file is also collected by the vitest/Node runner, where it is ~125k
117
+ * (~375 KB). Both measured on the pinned toolchain (Bun 1.3.13 — the CI
118
+ * default in .github/actions/setup-switchroom/action.yml — and Node 22), and
119
+ * both move with an engine upgrade.
120
+ *
121
+ * We deliberately do NOT rely on that threshold. Input here is unbounded:
122
+ * `renderOutboundChunks` renders the WHOLE raw body before any
123
+ * `splitMarkdownChunks` (render/rich-render.ts:146), so the fence scan is not
124
+ * bounded by the 32768-character rich cap. Being honest about the size: on
125
+ * Bun a ~1.9 MB single message is an unlikely body, so there this is a latent
126
+ * crash rather than a routine one. The loop is O(n) either way and cannot
127
+ * throw, so correctness costs nothing and the reachability argument does not
128
+ * have to be won.
129
+ */
130
+ export function codeFenceFor(text: string): string {
131
+ let longestRun = 0
132
+ let current = 0
133
+ for (let i = 0; i < text.length; i++) {
134
+ if (text[i] === '`') {
135
+ current++
136
+ if (current > longestRun) longestRun = current
137
+ } else {
138
+ current = 0
139
+ }
140
+ }
141
+ return '`'.repeat(Math.max(3, longestRun + 1))
142
+ }
143
+
88
144
  /**
89
145
  * Make a URL safe to interpolate as the destination of a `[label](href)`
90
146
  * inline link.