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.
- package/dist/cli/switchroom.js +105 -2
- package/dist/cli/ui/index.html +103 -38
- package/package.json +1 -1
- package/telegram-plugin/answer-stream-flag.ts +19 -0
- package/telegram-plugin/dist/gateway/gateway.js +62 -17
- package/telegram-plugin/gateway/answer-thread-resolve.test.ts +135 -1
- package/telegram-plugin/gateway/gateway.ts +151 -11
- package/telegram-plugin/tests/answer-stream-flag.test.ts +19 -1
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +52 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +45 -0
- package/telegram-plugin/uat/scenarios/fuzz-cross-surface-ordering-channel.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/fuzz-multitopic-routing-channel.test.ts +184 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -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.
|
|
49576
|
-
var COMMIT_SHA = "
|
|
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":
|
package/dist/cli/ui/index.html
CHANGED
|
@@ -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-
|
|
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="
|
|
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
|
|
496
|
+
async function fetchConnections() {
|
|
497
497
|
try {
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
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', '
|
|
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 === '
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1021
|
-
container.innerHTML =
|
|
1085
|
+
|
|
1086
|
+
container.innerHTML = googleSection + microsoftSection + notionSection;
|
|
1022
1087
|
}
|
|
1023
1088
|
|
|
1024
1089
|
function renderSchedule(data) {
|
package/package.json
CHANGED
|
@@ -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.
|
|
52774
|
-
var COMMIT_SHA = "
|
|
52775
|
-
var COMMIT_DATE = "2026-06-
|
|
52776
|
-
var LATEST_PR =
|
|
52777
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
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
|
-
|
|
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
|
|
56336
|
-
|
|
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
|
|
56696
|
-
|
|
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
|
|
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
|
`);
|