switchroom 0.14.67 → 0.14.69

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.
@@ -14593,6 +14593,35 @@ function shouldEmitNotionMcp(agentName, config) {
14593
14593
  return false;
14594
14594
  return true;
14595
14595
  }
14596
+ function normalizeNotionUuid(uuid) {
14597
+ if (typeof uuid !== "string")
14598
+ return null;
14599
+ const stripped = uuid.replace(/-/g, "").toLowerCase();
14600
+ if (!/^[0-9a-f]{32}$/.test(stripped))
14601
+ return null;
14602
+ return stripped;
14603
+ }
14604
+ function agentCanAccessNotionDB(config, agentName, dbUuid) {
14605
+ if (!shouldEmitNotionMcp(agentName, config))
14606
+ return false;
14607
+ const targetNorm = normalizeNotionUuid(dbUuid);
14608
+ if (targetNorm === null)
14609
+ return false;
14610
+ const agentConfig = config.agents?.[agentName];
14611
+ const allowedNames = agentConfig?.notion_workspace?.databases;
14612
+ if (allowedNames === undefined || allowedNames.length === 0) {
14613
+ return true;
14614
+ }
14615
+ const dbMap = config.notion_workspace?.databases ?? {};
14616
+ for (const name of allowedNames) {
14617
+ const uuid = dbMap[name];
14618
+ if (!uuid)
14619
+ continue;
14620
+ if (normalizeNotionUuid(uuid) === targetNorm)
14621
+ return true;
14622
+ }
14623
+ return false;
14624
+ }
14596
14625
  function validateNotionWorkspaceConfig(config) {
14597
14626
  const issues = [];
14598
14627
  const dbMap = config.notion_workspace?.databases ?? {};
@@ -49572,8 +49601,8 @@ var {
49572
49601
  } = import__.default;
49573
49602
 
49574
49603
  // src/build-info.ts
49575
- var VERSION = "0.14.67";
49576
- var COMMIT_SHA = "dcade213";
49604
+ var VERSION = "0.14.69";
49605
+ var COMMIT_SHA = "a3def2a8";
49577
49606
 
49578
49607
  // src/cli/agent.ts
49579
49608
  init_source();
@@ -71710,6 +71739,70 @@ async function handleGetGoogleAccounts(config) {
71710
71739
  }
71711
71740
  return out;
71712
71741
  }
71742
+ async function handleGetMicrosoftAccounts(config) {
71743
+ const live = new Map;
71744
+ try {
71745
+ await withAuthBrokerClient(async (client2) => {
71746
+ const data = await client2.listMicrosoftAccounts();
71747
+ for (const a of data.accounts) {
71748
+ live.set(a.account.toLowerCase(), {
71749
+ expiresAt: a.expiresAt,
71750
+ scope: a.scope,
71751
+ clientId: a.clientId,
71752
+ accountType: a.accountType
71753
+ });
71754
+ }
71755
+ });
71756
+ } catch (err) {
71757
+ if (!(err instanceof AuthBrokerUnreachableError))
71758
+ throw err;
71759
+ }
71760
+ const cfgAccounts = config.microsoft_accounts ?? {};
71761
+ const keys = new Set([
71762
+ ...Object.keys(cfgAccounts).map((k) => k.toLowerCase()),
71763
+ ...live.keys()
71764
+ ]);
71765
+ const out = [];
71766
+ for (const key of [...keys].sort()) {
71767
+ const cfg = cfgAccounts[key];
71768
+ const l = live.get(key);
71769
+ out.push({
71770
+ account: key,
71771
+ expiresAt: l?.expiresAt ?? null,
71772
+ scope: l?.scope ?? null,
71773
+ clientId: l?.clientId ?? null,
71774
+ accountType: l?.accountType ?? null,
71775
+ enabledFor: cfg?.enabled_for ? [...cfg.enabled_for].sort() : [],
71776
+ brokerKnown: l != null
71777
+ });
71778
+ }
71779
+ return out;
71780
+ }
71781
+ function handleGetNotionWorkspace(config) {
71782
+ const nw = config.notion_workspace;
71783
+ if (!nw) {
71784
+ return { configured: false, vaultKey: null, databases: [], fullAccessAgents: [] };
71785
+ }
71786
+ const declared = nw.databases ?? {};
71787
+ const agentNames = Object.keys(config.agents ?? {});
71788
+ const databases = Object.entries(declared).map(([name, id]) => {
71789
+ const enabledFor = agentNames.filter((n) => agentCanAccessNotionDB(config, n, id)).sort();
71790
+ return { name, id, enabledFor };
71791
+ }).sort((a, b) => a.name.localeCompare(b.name));
71792
+ const agentsRaw = config.agents ?? {};
71793
+ const fullAccessAgents = agentNames.filter((n) => {
71794
+ if (!shouldEmitNotionMcp(n, config))
71795
+ return false;
71796
+ const dbs = agentsRaw[n]?.notion_workspace?.databases;
71797
+ return dbs === undefined || dbs.length === 0;
71798
+ }).sort();
71799
+ return {
71800
+ configured: true,
71801
+ vaultKey: nw.vault_key ?? null,
71802
+ databases,
71803
+ fullAccessAgents
71804
+ };
71805
+ }
71713
71806
  function handleGetSchedule(config) {
71714
71807
  const entries = collectScheduleEntries(config);
71715
71808
  const agentsDir = resolveAgentsDir(config);
@@ -72387,6 +72480,12 @@ function parseRoute(pathname, method) {
72387
72480
  if (method === "GET" && pathname === "/api/google-accounts") {
72388
72481
  return { handler: "getGoogleAccounts", params: {} };
72389
72482
  }
72483
+ if (method === "GET" && pathname === "/api/microsoft-accounts") {
72484
+ return { handler: "getMicrosoftAccounts", params: {} };
72485
+ }
72486
+ if (method === "GET" && pathname === "/api/notion-workspace") {
72487
+ return { handler: "getNotionWorkspace", params: {} };
72488
+ }
72390
72489
  if (method === "GET" && pathname === "/api/schedule") {
72391
72490
  return { handler: "getSchedule", params: {} };
72392
72491
  }
@@ -72512,6 +72611,10 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
72512
72611
  return (async () => jsonResponse(await handleGetSystemHealth()))();
72513
72612
  case "getGoogleAccounts":
72514
72613
  return (async () => jsonResponse(await handleGetGoogleAccounts(config)))();
72614
+ case "getMicrosoftAccounts":
72615
+ return (async () => jsonResponse(await handleGetMicrosoftAccounts(config)))();
72616
+ case "getNotionWorkspace":
72617
+ return jsonResponse(handleGetNotionWorkspace(config));
72515
72618
  case "getSchedule":
72516
72619
  return jsonResponse(handleGetSchedule(config));
72517
72620
  case "getApprovals":
@@ -409,7 +409,7 @@
409
409
  <button id="tab-agents" onclick="switchTab('agents')">Agents</button>
410
410
  <button id="tab-accounts" onclick="switchTab('accounts')">Accounts</button>
411
411
  <button id="tab-system" onclick="switchTab('system')">System</button>
412
- <button id="tab-google" onclick="switchTab('google')">Google</button>
412
+ <button id="tab-connections" onclick="switchTab('connections')">Connections</button>
413
413
  <button id="tab-schedule" onclick="switchTab('schedule')">Schedule</button>
414
414
  <button id="tab-approvals" onclick="switchTab('approvals')">Approvals</button>
415
415
  </nav>
@@ -419,7 +419,7 @@
419
419
  <div id="agents" style="display:none" class="loading">Loading agents...</div>
420
420
  <div id="accounts" style="display:none"></div>
421
421
  <div id="system" style="display:none"></div>
422
- <div id="google" style="display:none"></div>
422
+ <div id="connections" style="display:none"></div>
423
423
  <div id="schedule" style="display:none"></div>
424
424
  <div id="approvals" style="display:none"></div>
425
425
  </main>
@@ -493,14 +493,17 @@
493
493
  }
494
494
  }
495
495
 
496
- async function fetchGoogleAccounts() {
496
+ async function fetchConnections() {
497
497
  try {
498
- const res = await fetch(`${API}/api/google-accounts`, { headers: authHeaders() });
499
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
500
- renderGoogleAccounts(await res.json());
498
+ const [google, microsoft, notion] = await Promise.all([
499
+ fetch(`${API}/api/google-accounts`, { headers: authHeaders() }).then(r => r.ok ? r.json() : []),
500
+ fetch(`${API}/api/microsoft-accounts`, { headers: authHeaders() }).then(r => r.ok ? r.json() : []),
501
+ fetch(`${API}/api/notion-workspace`, { headers: authHeaders() }).then(r => r.ok ? r.json() : { configured: false, databases: [] }),
502
+ ]);
503
+ renderConnections({ google, microsoft, notion });
501
504
  clearError();
502
505
  } catch (err) {
503
- showError(`Failed to fetch Google accounts: ${err.message}`);
506
+ showError(`Failed to fetch connections: ${err.message}`);
504
507
  }
505
508
  }
506
509
 
@@ -527,7 +530,7 @@
527
530
  }
528
531
 
529
532
  function switchTab(tab) {
530
- const tabs = ['summary', 'agents', 'accounts', 'system', 'google', 'schedule', 'approvals'];
533
+ const tabs = ['summary', 'agents', 'accounts', 'system', 'connections', 'schedule', 'approvals'];
531
534
  for (const t of tabs) {
532
535
  document.getElementById(`tab-${t}`).classList.toggle('active', tab === t);
533
536
  document.getElementById(t).style.display = tab === t ? '' : 'none';
@@ -535,7 +538,7 @@
535
538
  if (tab === 'summary') fetchSummary();
536
539
  if (tab === 'accounts') fetchAccounts();
537
540
  if (tab === 'system') fetchSystemHealth();
538
- if (tab === 'google') fetchGoogleAccounts();
541
+ if (tab === 'connections') fetchConnections();
539
542
  if (tab === 'schedule') fetchSchedule();
540
543
  if (tab === 'approvals') fetchApprovals();
541
544
  }
@@ -988,37 +991,99 @@
988
991
  return String(ts).replace('T', ' ').replace(/\.\d+Z?$/, '').replace(/Z$/, '');
989
992
  }
990
993
 
991
- function renderGoogleAccounts(rows) {
992
- const container = document.getElementById('google');
993
- if (!rows || rows.length === 0) {
994
- container.innerHTML = '<div class="loading">No Google accounts. Add one under <code>google_accounts:</code> in switchroom.yaml and run <code>switchroom auth google account add</code>.</div>';
995
- return;
994
+ const _dimC = (s) => `<span style="color:var(--text-dim)">${escapeHtml(s)}</span>`;
995
+
996
+ // One OAuth-account card (Google or Microsoft same shape; Microsoft
997
+ // adds an account-type pill).
998
+ function renderOAuthAccountCard(a, opts) {
999
+ const expires = a.expiresAt ? formatTimestamp(a.expiresAt) : _dimC('—');
1000
+ const known = a.brokerKnown
1001
+ ? '<span class="usage-pill primary">slot present</span>'
1002
+ : '<span style="color:var(--yellow)">config-only (no broker slot)</span>';
1003
+ const acl = (a.enabledFor && a.enabledFor.length)
1004
+ ? a.enabledFor.map(escapeHtml).join(', ')
1005
+ : _dimC('no agents enabled');
1006
+ const typePill = (opts && opts.showType && a.accountType)
1007
+ ? `<span class="usage-pill" style="margin-left:.4rem">${escapeHtml(a.accountType)}</span>`
1008
+ : '';
1009
+ return `
1010
+ <div class="account-card">
1011
+ <div class="account-card-header">
1012
+ <div class="account-label">${escapeHtml(a.account)}${typePill}</div>
1013
+ <span style="margin-left:auto">${known}</span>
1014
+ </div>
1015
+ <div class="account-usage"><label style="color:var(--text-dim);opacity:.7">Enabled for: </label>${acl}</div>
1016
+ <div class="card-meta" style="padding:0">
1017
+ <div class="meta-item"><label>Expires </label><span>${expires}</span></div>
1018
+ <div class="meta-item" title="${a.scope ? escapeHtml(a.scope) : ''}"><label>Scope </label><span>${a.scope ? escapeHtml(a.scope.split(' ').length + ' scope(s)') : _dimC('—')}</span></div>
1019
+ <div class="meta-item"><label>Client </label><span>${a.clientId ? escapeHtml(a.clientId.slice(0, 16) + '…') : _dimC('—')}</span></div>
1020
+ </div>
1021
+ </div>`;
1022
+ }
1023
+
1024
+ function _connectionSection(title, emptyHtml, cardsHtml) {
1025
+ return `
1026
+ <div style="margin-bottom:1.5rem">
1027
+ <h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">${title}</h3>
1028
+ ${cardsHtml ? `<div class="accounts-grid">${cardsHtml}</div>` : `<div class="loading" style="padding:.8rem">${emptyHtml}</div>`}
1029
+ </div>`;
1030
+ }
1031
+
1032
+ // Unified external-connections view: Google + Microsoft accounts and
1033
+ // the Notion workspace, each showing which agents have access.
1034
+ function renderConnections(data) {
1035
+ const container = document.getElementById('connections');
1036
+ const google = data.google || [];
1037
+ const microsoft = data.microsoft || [];
1038
+ const notion = data.notion || { configured: false, databases: [] };
1039
+
1040
+ const googleSection = _connectionSection(
1041
+ 'Google',
1042
+ 'No Google accounts. Add one under <code>google_accounts:</code> and run <code>switchroom auth google account add</code>.',
1043
+ google.map(a => renderOAuthAccountCard(a, { showType: false })).join(''),
1044
+ );
1045
+
1046
+ const microsoftSection = _connectionSection(
1047
+ 'Microsoft 365',
1048
+ 'No Microsoft accounts. Connect one from Telegram with <code>/connect microsoft</code> (admin DM), or <code>switchroom auth microsoft account add</code>.',
1049
+ microsoft.map(a => renderOAuthAccountCard(a, { showType: true })).join(''),
1050
+ );
1051
+
1052
+ let notionCards = '';
1053
+ if (notion.configured) {
1054
+ notionCards = (notion.databases || []).map(db => {
1055
+ const acl = (db.enabledFor && db.enabledFor.length)
1056
+ ? db.enabledFor.map(escapeHtml).join(', ')
1057
+ : _dimC('no agents enabled');
1058
+ return `
1059
+ <div class="account-card">
1060
+ <div class="account-card-header">
1061
+ <div class="account-label">${escapeHtml(db.name)}</div>
1062
+ <span style="margin-left:auto" class="usage-pill">database</span>
1063
+ </div>
1064
+ <div class="account-usage"><label style="color:var(--text-dim);opacity:.7">Enabled for: </label>${acl}</div>
1065
+ <div class="card-meta" style="padding:0">
1066
+ <div class="meta-item"><label>DB id </label><span>${escapeHtml(db.id.slice(0, 12))}…</span></div>
1067
+ </div>
1068
+ </div>`;
1069
+ }).join('');
996
1070
  }
997
- const dim = (s) => `<span style="color:var(--text-dim)">${escapeHtml(s)}</span>`;
998
- const cards = rows.map(a => {
999
- const expires = a.expiresAt ? formatTimestamp(a.expiresAt) : dim('');
1000
- const scope = a.scope ? escapeHtml(a.scope) : dim('broker offline');
1001
- const known = a.brokerKnown
1002
- ? '<span class="usage-pill primary">slot present</span>'
1003
- : '<span style="color:var(--yellow)">config-only (no broker slot)</span>';
1004
- const acl = (a.enabledFor && a.enabledFor.length)
1005
- ? a.enabledFor.map(escapeHtml).join(', ')
1006
- : dim('no agents enabled');
1007
- return `
1008
- <div class="account-card">
1009
- <div class="account-card-header">
1010
- <div class="account-label">${escapeHtml(a.account)}</div>
1011
- <span style="margin-left:auto">${known}</span>
1012
- </div>
1013
- <div class="account-usage"><label style="color:var(--text-dim);opacity:.7">Enabled for: </label>${acl}</div>
1014
- <div class="card-meta" style="padding:0">
1015
- <div class="meta-item"><label>Expires </label><span>${expires}</span></div>
1016
- <div class="meta-item" title="${a.scope ? escapeHtml(a.scope) : ''}"><label>Scope </label><span>${a.scope ? escapeHtml(a.scope.split(' ').length + ' scope(s)') : dim('—')}</span></div>
1017
- <div class="meta-item"><label>Client </label><span>${a.clientId ? escapeHtml(a.clientId.slice(0, 16) + '…') : dim('—')}</span></div>
1018
- </div>
1071
+ const notionTitle = 'Notion' + (notion.configured && notion.vaultKey ? ` <span style="text-transform:none;font-weight:400">· token <code>${escapeHtml(notion.vaultKey)}</code></span>` : '');
1072
+ const fullAccessBanner = (notion.fullAccessAgents && notion.fullAccessAgents.length)
1073
+ ? `<div style="margin:0 0 .6rem;font-size:.85rem;color:var(--text-dim)">Full access (all databases): ${notion.fullAccessAgents.map(escapeHtml).join(', ')}</div>`
1074
+ : '';
1075
+ const notionBody = notionCards
1076
+ ? `<div class="accounts-grid">${notionCards}</div>`
1077
+ : `<div class="loading" style="padding:.8rem">${notion.configured
1078
+ ? 'Notion configured but no databases declared. Add them under <code>notion_workspace.databases:</code>.'
1079
+ : 'Notion not configured. See <code>docs/notion-integration.md</code>.'}</div>`;
1080
+ const notionSection = `
1081
+ <div style="margin-bottom:1.5rem">
1082
+ <h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">${notionTitle}</h3>
1083
+ ${fullAccessBanner}${notionBody}
1019
1084
  </div>`;
1020
- }).join('');
1021
- container.innerHTML = `<div class="accounts-grid">${cards}</div>`;
1085
+
1086
+ container.innerHTML = googleSection + microsoftSection + notionSection;
1022
1087
  }
1023
1088
 
1024
1089
  function renderSchedule(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.67",
3
+ "version": "0.14.69",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,3 +16,22 @@ export function parseVisibleAnswerStreamEnabled(raw: string | undefined): boolea
16
16
  const v = raw.trim().toLowerCase()
17
17
  return v === '1' || v === 'true' || v === 'on' || v === 'yes'
18
18
  }
19
+
20
+ /**
21
+ * Draft-answer-lane retirement (2026-06-05). The compose-box draft transport
22
+ * (`sendMessageDraft`) is invisible to the mtcute UAT harness, so the live
23
+ * answer-stream surface couldn't be tested. Retired by DEFAULT: the answer lane
24
+ * now opens a real, observable edit-in-place message instead of the compose-box
25
+ * draft (and the onMetric silence-liveness reset from #2169 now fires on visible
26
+ * sends in BOTH DMs and supergroups, not just DM drafts). Kill switch
27
+ * `SWITCHROOM_DRAFT_ANSWER_LANE=0` (also false/off/no) restores the legacy
28
+ * invisible draft.
29
+ *
30
+ * Returns true when the draft lane is RETIRED (the default — env unset or any
31
+ * truthy value); false only for an explicit disable of the retirement.
32
+ */
33
+ export function parseDraftLaneRetiredEnabled(raw: string | undefined): boolean {
34
+ if (raw == null) return true
35
+ const v = raw.trim().toLowerCase()
36
+ return !(v === '0' || v === 'false' || v === 'off' || v === 'no')
37
+ }
@@ -39761,6 +39761,12 @@ function parseVisibleAnswerStreamEnabled(raw) {
39761
39761
  const v = raw.trim().toLowerCase();
39762
39762
  return v === "1" || v === "true" || v === "on" || v === "yes";
39763
39763
  }
39764
+ function parseDraftLaneRetiredEnabled(raw) {
39765
+ if (raw == null)
39766
+ return true;
39767
+ const v = raw.trim().toLowerCase();
39768
+ return !(v === "0" || v === "false" || v === "off" || v === "no");
39769
+ }
39764
39770
 
39765
39771
  // pty-tail.ts
39766
39772
  var import_headless = __toESM(require_xterm_headless(), 1);
@@ -52770,11 +52776,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52770
52776
  }
52771
52777
 
52772
52778
  // ../src/build-info.ts
52773
- var VERSION = "0.14.67";
52774
- var COMMIT_SHA = "dcade213";
52775
- var COMMIT_DATE = "2026-06-05T08:22:01Z";
52776
- var LATEST_PR = 2171;
52777
- var COMMITS_AHEAD_OF_TAG = 4;
52779
+ var VERSION = "0.14.69";
52780
+ var COMMIT_SHA = "a3def2a8";
52781
+ var COMMIT_DATE = "2026-06-05T22:00:53+10:00";
52782
+ var LATEST_PR = null;
52783
+ var COMMITS_AHEAD_OF_TAG = 3;
52778
52784
 
52779
52785
  // gateway/boot-version.ts
52780
52786
  function formatRelativeAgo(iso) {
@@ -53574,6 +53580,7 @@ var TOPIC_ID = process.env.TELEGRAM_TOPIC_ID ? Number(process.env.TELEGRAM_TOPIC
53574
53580
  var AGENT_ADMIN = process.env.SWITCHROOM_AGENT_ADMIN === "true";
53575
53581
  var bot = new import_grammy9.Bot(TOKEN);
53576
53582
  installTgPostLogger(bot);
53583
+ var DRAFT_ANSWER_LANE_RETIRED = parseDraftLaneRetiredEnabled(process.env.SWITCHROOM_DRAFT_ANSWER_LANE);
53577
53584
  var _rawSendMessageDraft = bot.api.raw.sendMessageDraft;
53578
53585
  var GRAMMY_VERSION = (() => {
53579
53586
  try {
@@ -53583,7 +53590,7 @@ var GRAMMY_VERSION = (() => {
53583
53590
  return "unknown";
53584
53591
  }
53585
53592
  })();
53586
- var sendMessageDraftFn = typeof _rawSendMessageDraft === "function" ? (chatId, draftId, text, params) => _rawSendMessageDraft({
53593
+ var sendMessageDraftFn = !DRAFT_ANSWER_LANE_RETIRED && typeof _rawSendMessageDraft === "function" ? (chatId, draftId, text, params) => _rawSendMessageDraft({
53587
53594
  chat_id: Number(chatId),
53588
53595
  draft_id: draftId,
53589
53596
  text,
@@ -54019,6 +54026,7 @@ var _noReplyDrainRaw = process.env.SWITCHROOM_SERIALIZE_NOREPLY_DRAIN_MS;
54019
54026
  var _noReplyDrainParsed = _noReplyDrainRaw != null && _noReplyDrainRaw !== "" ? Number(_noReplyDrainRaw) : 2500;
54020
54027
  var SERIALIZE_NOREPLY_DRAIN_MS = Number.isFinite(_noReplyDrainParsed) && _noReplyDrainParsed > 0 ? _noReplyDrainParsed : 2500;
54021
54028
  var TURN_ORIGIN_ROUTING_ENABLED = process.env.SWITCHROOM_TURN_ORIGIN_ROUTING !== "0";
54029
+ var FRAMEWORK_ORIGIN_ROUTING_ENABLED = process.env.SWITCHROOM_FRAMEWORK_ORIGIN_ROUTING !== "0";
54022
54030
  var TOPIC_FRAMING_ENABLED = process.env.SWITCHROOM_TOPIC_FRAMING !== "0";
54023
54031
  var QUEUED_STATUS_UX_ENABLED = process.env.SWITCHROOM_QUEUED_STATUS_UX !== "0";
54024
54032
  var FEED_REOPEN_AFTER_ACK_ENABLED = process.env.SWITCHROOM_FEED_REOPEN_AFTER_ACK !== "0";
@@ -54051,15 +54059,39 @@ var progressUpdateTurnCount = new Map;
54051
54059
  var currentTurn = null;
54052
54060
  var RECENT_TURNS_MAX = 32;
54053
54061
  var recentTurnsById = new Map;
54062
+ var recentTurnIdBySourceMessageId = new Map;
54054
54063
  function rememberRecentTurn(turn) {
54055
54064
  recentTurnsById.set(turn.turnId, turn);
54065
+ if (turn.sourceMessageId != null) {
54066
+ recentTurnIdBySourceMessageId.set(turn.sourceMessageId, turn.turnId);
54067
+ }
54056
54068
  while (recentTurnsById.size > RECENT_TURNS_MAX) {
54057
54069
  const oldest = recentTurnsById.keys().next().value;
54058
54070
  if (oldest === undefined)
54059
54071
  break;
54072
+ const evicted = recentTurnsById.get(oldest);
54060
54073
  recentTurnsById.delete(oldest);
54074
+ if (evicted?.sourceMessageId != null && recentTurnIdBySourceMessageId.get(evicted.sourceMessageId) === oldest) {
54075
+ recentTurnIdBySourceMessageId.delete(evicted.sourceMessageId);
54076
+ }
54061
54077
  }
54062
54078
  }
54079
+ function findTurnByQuotedMessageId(chatId, replyTo) {
54080
+ if (!FRAMEWORK_ORIGIN_ROUTING_ENABLED)
54081
+ return null;
54082
+ if (replyTo == null)
54083
+ return null;
54084
+ const mid = Number(replyTo);
54085
+ if (!Number.isFinite(mid))
54086
+ return null;
54087
+ const owner = recentTurnIdBySourceMessageId.get(mid);
54088
+ if (owner == null)
54089
+ return null;
54090
+ const turn = recentTurnsById.get(owner) ?? null;
54091
+ if (turn == null || turn.sessionChatId !== chatId)
54092
+ return null;
54093
+ return turn;
54094
+ }
54063
54095
  function deriveTurnId(chatId, threadId, messageId) {
54064
54096
  if (messageId == null || messageId === "" || String(messageId) === "0")
54065
54097
  return null;
@@ -54081,7 +54113,7 @@ function findLatestEndedTurnForChat(chatId) {
54081
54113
  }
54082
54114
  return latest;
54083
54115
  }
54084
- function resolveAnswerThreadWithLog(chatId, explicitThreadId, originTurn, liveTurn, surface) {
54116
+ function resolveAnswerThreadWithLog(chatId, explicitThreadId, originTurn, originVia, liveTurn, surface) {
54085
54117
  const recovered = LATE_REPLY_TOPIC_RECOVERY_ENABLED && explicitThreadId == null && originTurn == null && liveTurn == null ? findLatestEndedTurnForChat(chatId) : null;
54086
54118
  const threadId = resolveAnswerThreadId({
54087
54119
  explicitThreadId,
@@ -54091,14 +54123,23 @@ function resolveAnswerThreadWithLog(chatId, explicitThreadId, originTurn, liveTu
54091
54123
  lastEndedResolvedForChat: recovered != null,
54092
54124
  lastEndedThreadIdForChat: recovered?.sessionThreadId
54093
54125
  });
54094
- const via = explicitThreadId != null ? "explicit" : originTurn != null ? "origin" : liveTurn?.sessionThreadId != null ? "live" : recovered != null ? "recovered" : "none";
54126
+ const via = explicitThreadId != null ? "explicit" : originTurn != null ? originVia === "quoted" ? "quoted" : "origin" : liveTurn?.sessionThreadId != null ? "live" : recovered != null ? "recovered" : "none";
54095
54127
  const ownerTurn = originTurn ?? recovered ?? liveTurn;
54096
54128
  const isSupergroup = chatId.startsWith("-100");
54097
- const unrouted = isSupergroup && threadId == null;
54098
- 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" : "") + (unrouted ? " UNROUTED(supergroup\u2192no-topic)" : "") + `
54129
+ const unrouted = isSupergroup && threadId == null && ownerTurn == null;
54130
+ const misrouteRisk = isSupergroup && via === "live" && hasDifferentThreadedRecentTurn(chatId, liveTurn?.sessionThreadId);
54131
+ 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)" : "") + `
54099
54132
  `);
54100
54133
  return threadId;
54101
54134
  }
54135
+ function hasDifferentThreadedRecentTurn(chatId, liveThreadId) {
54136
+ const live = liveThreadId ?? null;
54137
+ for (const t of recentTurnsById.values()) {
54138
+ if (t.sessionChatId === chatId && (t.sessionThreadId ?? null) !== live)
54139
+ return true;
54140
+ }
54141
+ return false;
54142
+ }
54102
54143
  function closeObligationOnSubstantiveReply(args, liveTurn) {
54103
54144
  if (!OBLIGATION_LEDGER_ENABLED)
54104
54145
  return;
@@ -56332,8 +56373,10 @@ ${url}`;
56332
56373
  let threadId;
56333
56374
  if (TURN_ORIGIN_ROUTING_ENABLED) {
56334
56375
  const explicit = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
56335
- const originTurn = findTurnByOriginId(args.origin_turn_id);
56336
- threadId = resolveAnswerThreadWithLog(chat_id, Number.isFinite(explicit) ? explicit : undefined, originTurn, turn, "reply");
56376
+ const echoedTurn = findTurnByOriginId(args.origin_turn_id);
56377
+ const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(chat_id, args.reply_to) : null;
56378
+ const originTurn = echoedTurn ?? quotedTurn;
56379
+ threadId = resolveAnswerThreadWithLog(chat_id, Number.isFinite(explicit) ? explicit : undefined, originTurn, originTurn == null ? null : echoedTurn != null ? "echo" : "quoted", turn, "reply");
56337
56380
  } else {
56338
56381
  threadId = resolveThreadId(chat_id, args.message_thread_id ?? (turn?.sessionThreadId != null ? turn.sessionThreadId : undefined));
56339
56382
  }
@@ -56692,8 +56735,10 @@ async function executeStreamReply(args) {
56692
56735
  if (args.message_thread_id == null) {
56693
56736
  let injected;
56694
56737
  if (TURN_ORIGIN_ROUTING_ENABLED) {
56695
- const originTurn = findTurnByOriginId(args.origin_turn_id);
56696
- injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, originTurn, turn, "stream_reply");
56738
+ const echoedTurn = findTurnByOriginId(args.origin_turn_id);
56739
+ const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(String(args.chat_id), args.reply_to) : null;
56740
+ const originTurn = echoedTurn ?? quotedTurn;
56741
+ injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, originTurn, originTurn == null ? null : echoedTurn != null ? "echo" : "quoted", turn, "stream_reply");
56697
56742
  } else {
56698
56743
  injected = turn?.sessionThreadId;
56699
56744
  }
@@ -58106,7 +58151,7 @@ function handleSessionEvent(ev) {
58106
58151
  chatId: turn.sessionChatId,
58107
58152
  isPrivateChat: turn.isDm,
58108
58153
  threadId: turn.sessionThreadId,
58109
- ...ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn, minInitialChars: Number.MAX_SAFE_INTEGER },
58154
+ ...ANSWER_STREAM_VISIBLE_ENABLED || DRAFT_ANSWER_LANE_RETIRED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn, minInitialChars: Number.MAX_SAFE_INTEGER },
58110
58155
  sendMessage: async (chatId, text, params) => {
58111
58156
  const tid = params?.message_thread_id;
58112
58157
  const silent = params?.purpose !== "materialize";
@@ -58238,7 +58283,7 @@ function handleSessionEvent(ev) {
58238
58283
  const stream = turn.answerStream;
58239
58284
  const streamedMsgId = stream.messageId();
58240
58285
  const streamedFinalText = turn.capturedText.join("").trim();
58241
- if (ANSWER_STREAM_VISIBLE_ENABLED && !turn.replyCalled && streamedMsgId != null && streamedFinalText.length > 0) {
58286
+ if ((ANSWER_STREAM_VISIBLE_ENABLED || DRAFT_ANSWER_LANE_RETIRED) && !turn.replyCalled && streamedMsgId != null && streamedFinalText.length > 0) {
58242
58287
  turn.answerStream = null;
58243
58288
  streamFinalizedAsAnswer = true;
58244
58289
  turn.finalAnswerDelivered = true;
@@ -64574,7 +64619,7 @@ var didOneTimeSetup = false;
64574
64619
  }
64575
64620
  }
64576
64621
  }
64577
- process.stderr.write(`telegram gateway: answer-stream draft transport=${sendMessageDraftFn != null ? "available" : "unavailable"} grammy=${GRAMMY_VERSION}
64622
+ process.stderr.write(`telegram gateway: answer-stream lane=${DRAFT_ANSWER_LANE_RETIRED ? "visible(draft-retired)" : ANSWER_STREAM_VISIBLE_ENABLED ? "visible" : "draft"} draftFn=${sendMessageDraftFn != null ? "available" : "off"} grammy=${GRAMMY_VERSION}
64578
64623
  `);
64579
64624
  process.stderr.write(`telegram gateway: starting bot polling pid=${process.pid} agent=${process.env.SWITCHROOM_AGENT_NAME ?? "-"} stateDir=${STATE_DIR} historyEnabled=${HISTORY_ENABLED} streamMode=${process.env.SWITCHROOM_TG_STREAM_MODE ?? "checklist"}
64580
64625
  `);