switchroom 0.14.67 → 0.14.68
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 +16 -9
- package/telegram-plugin/gateway/gateway.ts +29 -5
- package/telegram-plugin/tests/answer-stream-flag.test.ts +19 -1
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +52 -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.68";
|
|
49605
|
+
var COMMIT_SHA = "4f371b79";
|
|
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.68";
|
|
52780
|
+
var COMMIT_SHA = "4f371b79";
|
|
52781
|
+
var COMMIT_DATE = "2026-06-05T10:24:42Z";
|
|
52782
|
+
var LATEST_PR = 2174;
|
|
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,
|
|
@@ -58106,7 +58113,7 @@ function handleSessionEvent(ev) {
|
|
|
58106
58113
|
chatId: turn.sessionChatId,
|
|
58107
58114
|
isPrivateChat: turn.isDm,
|
|
58108
58115
|
threadId: turn.sessionThreadId,
|
|
58109
|
-
...ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn, minInitialChars: Number.MAX_SAFE_INTEGER },
|
|
58116
|
+
...ANSWER_STREAM_VISIBLE_ENABLED || DRAFT_ANSWER_LANE_RETIRED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn, minInitialChars: Number.MAX_SAFE_INTEGER },
|
|
58110
58117
|
sendMessage: async (chatId, text, params) => {
|
|
58111
58118
|
const tid = params?.message_thread_id;
|
|
58112
58119
|
const silent = params?.purpose !== "materialize";
|
|
@@ -58238,7 +58245,7 @@ function handleSessionEvent(ev) {
|
|
|
58238
58245
|
const stream = turn.answerStream;
|
|
58239
58246
|
const streamedMsgId = stream.messageId();
|
|
58240
58247
|
const streamedFinalText = turn.capturedText.join("").trim();
|
|
58241
|
-
if (ANSWER_STREAM_VISIBLE_ENABLED && !turn.replyCalled && streamedMsgId != null && streamedFinalText.length > 0) {
|
|
58248
|
+
if ((ANSWER_STREAM_VISIBLE_ENABLED || DRAFT_ANSWER_LANE_RETIRED) && !turn.replyCalled && streamedMsgId != null && streamedFinalText.length > 0) {
|
|
58242
58249
|
turn.answerStream = null;
|
|
58243
58250
|
streamFinalizedAsAnswer = true;
|
|
58244
58251
|
turn.finalAnswerDelivered = true;
|
|
@@ -64574,7 +64581,7 @@ var didOneTimeSetup = false;
|
|
|
64574
64581
|
}
|
|
64575
64582
|
}
|
|
64576
64583
|
}
|
|
64577
|
-
process.stderr.write(`telegram gateway: answer-stream draft
|
|
64584
|
+
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
64585
|
`);
|
|
64579
64586
|
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
64587
|
`);
|
|
@@ -98,7 +98,7 @@ import * as pendingProgress from '../pending-work-progress.js'
|
|
|
98
98
|
import { writeSilentEndState, clearSilentEndState, recordUndeliveredTurnEnd } from '../silent-end.js'
|
|
99
99
|
import { isFinalAnswerReply, isSubstantiveFinalReply } from '../final-answer-detect.js'
|
|
100
100
|
import { createAnswerStream, type AnswerStreamHandle } from '../answer-stream.js'
|
|
101
|
-
import { parseVisibleAnswerStreamEnabled } from '../answer-stream-flag.js'
|
|
101
|
+
import { parseVisibleAnswerStreamEnabled, parseDraftLaneRetiredEnabled } from '../answer-stream-flag.js'
|
|
102
102
|
import { type SessionEvent } from '../session-tail.js'
|
|
103
103
|
import {
|
|
104
104
|
shouldSuppressToolActivity,
|
|
@@ -678,6 +678,14 @@ const AGENT_ADMIN = process.env.SWITCHROOM_AGENT_ADMIN === 'true'
|
|
|
678
678
|
const bot = new Bot(TOKEN)
|
|
679
679
|
installTgPostLogger(bot)
|
|
680
680
|
|
|
681
|
+
// Draft-answer-lane retirement (2026-06-05): default RETIRED so the live answer
|
|
682
|
+
// lane uses a real, mtcute-observable message instead of the invisible
|
|
683
|
+
// compose-box draft. Declared HERE (above the boot-probe block) because
|
|
684
|
+
// `sendMessageDraftFn` below reads it — keep it above its first use to avoid a
|
|
685
|
+
// temporal-dead-zone ReferenceError at boot. Kill switch
|
|
686
|
+
// SWITCHROOM_DRAFT_ANSWER_LANE=0 restores the legacy draft.
|
|
687
|
+
const DRAFT_ANSWER_LANE_RETIRED = parseDraftLaneRetiredEnabled(process.env.SWITCHROOM_DRAFT_ANSWER_LANE)
|
|
688
|
+
|
|
681
689
|
// ─── sendMessageDraft boot probe ──────────────────────────────────────────
|
|
682
690
|
// grammY 1.x exposes all Telegram Bot API methods through bot.api.raw.
|
|
683
691
|
// bot.api.sendMessageDraft (the typed wrapper) takes chat_id as number, but
|
|
@@ -695,7 +703,11 @@ const GRAMMY_VERSION: string = (() => {
|
|
|
695
703
|
const sendMessageDraftFn: (
|
|
696
704
|
(chatId: string, draftId: number, text: string, params?: { message_thread_id?: number; parse_mode?: 'HTML' }) => Promise<unknown>
|
|
697
705
|
) | undefined =
|
|
698
|
-
|
|
706
|
+
// When the draft lane is retired (default), force this undefined so BOTH
|
|
707
|
+
// consumers (the answer-stream config + the stream_reply handler) drop the
|
|
708
|
+
// draft transport and fall back to visible message transport — the single
|
|
709
|
+
// chokepoint for the retirement.
|
|
710
|
+
!DRAFT_ANSWER_LANE_RETIRED && typeof _rawSendMessageDraft === 'function'
|
|
699
711
|
? (chatId, draftId, text, params) =>
|
|
700
712
|
(_rawSendMessageDraft as (args: Record<string, unknown>) => Promise<unknown>)({
|
|
701
713
|
chat_id: Number(chatId),
|
|
@@ -9545,7 +9557,13 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
9545
9557
|
// General). With the gate unreachable the only posted message is
|
|
9546
9558
|
// the canonical reply. (The gate is bypassed for DM draft
|
|
9547
9559
|
// transport, so DM draft streaming is unaffected.)
|
|
9548
|
-
|
|
9560
|
+
// Draft retired (default) OR visible explicitly on → a real
|
|
9561
|
+
// edit-in-place message (minInitialChars:1, no draft): observable by
|
|
9562
|
+
// the UAT and the onMetric silence-liveness reset fires on visible
|
|
9563
|
+
// sends in DMs AND supergroups. Legacy draft only when the kill
|
|
9564
|
+
// switch re-enables it (DRAFT_ANSWER_LANE_RETIRED=false), which also
|
|
9565
|
+
// restores sendMessageDraftFn above.
|
|
9566
|
+
...(ANSWER_STREAM_VISIBLE_ENABLED || DRAFT_ANSWER_LANE_RETIRED
|
|
9549
9567
|
? { minInitialChars: 1 }
|
|
9550
9568
|
: { sendMessageDraft: sendMessageDraftFn, minInitialChars: Number.MAX_SAFE_INTEGER }),
|
|
9551
9569
|
// #1075: route through robustApiCall so flood-wait,
|
|
@@ -9835,7 +9853,13 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
9835
9853
|
const streamedMsgId = stream.messageId()
|
|
9836
9854
|
const streamedFinalText = turn.capturedText.join('').trim()
|
|
9837
9855
|
if (
|
|
9838
|
-
|
|
9856
|
+
// Broadened for draft retirement: a text-only no-reply turn that
|
|
9857
|
+
// streamed a VISIBLE preview must materialize a pinged final answer +
|
|
9858
|
+
// delete the preview. Without this, the retired-default path would
|
|
9859
|
+
// fall into the else-branch retract() and delete the user's only copy
|
|
9860
|
+
// of the answer (a lost-answer bug). The reply-tool branch still hits
|
|
9861
|
+
// retract() → single canonical formatted reply, no flash.
|
|
9862
|
+
(ANSWER_STREAM_VISIBLE_ENABLED || DRAFT_ANSWER_LANE_RETIRED)
|
|
9839
9863
|
&& !turn.replyCalled
|
|
9840
9864
|
&& streamedMsgId != null
|
|
9841
9865
|
&& streamedFinalText.length > 0
|
|
@@ -20565,7 +20589,7 @@ void (async () => {
|
|
|
20565
20589
|
}
|
|
20566
20590
|
}
|
|
20567
20591
|
|
|
20568
|
-
process.stderr.write(`telegram gateway: answer-stream draft
|
|
20592
|
+
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}\n`)
|
|
20569
20593
|
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'}\n`)
|
|
20570
20594
|
runnerHandle = run(bot, {
|
|
20571
20595
|
runner: {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect } from 'vitest'
|
|
9
|
-
import { parseVisibleAnswerStreamEnabled } from '../answer-stream-flag.js'
|
|
9
|
+
import { parseVisibleAnswerStreamEnabled, parseDraftLaneRetiredEnabled } from '../answer-stream-flag.js'
|
|
10
10
|
|
|
11
11
|
describe('parseVisibleAnswerStreamEnabled — default OFF, opt-in', () => {
|
|
12
12
|
it('defaults OFF when unset', () => {
|
|
@@ -25,3 +25,21 @@ describe('parseVisibleAnswerStreamEnabled — default OFF, opt-in', () => {
|
|
|
25
25
|
}
|
|
26
26
|
})
|
|
27
27
|
})
|
|
28
|
+
|
|
29
|
+
describe('parseDraftLaneRetiredEnabled — default RETIRED (2026-06-05), kill-switch off', () => {
|
|
30
|
+
it('defaults to RETIRED (true) when unset — the draft lane is gone by default', () => {
|
|
31
|
+
expect(parseDraftLaneRetiredEnabled(undefined)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('stays RETIRED for any non-disable value (including unrecognized)', () => {
|
|
35
|
+
for (const v of ['1', 'true', 'on', 'yes', '', ' ', 'whatever', 'retired']) {
|
|
36
|
+
expect(parseDraftLaneRetiredEnabled(v)).toBe(true)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('restores the legacy draft (false) ONLY on an explicit disable (case/space-insensitive)', () => {
|
|
41
|
+
for (const v of ['0', 'false', 'off', 'no', ' FALSE ', 'Off', 'NO']) {
|
|
42
|
+
expect(parseDraftLaneRetiredEnabled(v)).toBe(false)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draft-answer-lane retirement — gateway wiring guards (2026-06-05).
|
|
3
|
+
*
|
|
4
|
+
* The retirement switches the live answer lane from the invisible compose-box
|
|
5
|
+
* draft to a real, mtcute-observable edit-in-place message, default-on. The
|
|
6
|
+
* design review flagged two ways this silently breaks (gateway IIFE can't be
|
|
7
|
+
* instantiated in-process, so these are source-level assertions, same pattern as
|
|
8
|
+
* silence-liveness-wiring.test):
|
|
9
|
+
*
|
|
10
|
+
* PRIMARY: drop sendMessageDraftFn but FORGET to flip minInitialChars to 1 →
|
|
11
|
+
* the lane becomes a total no-op (the MAX gate never opens it), losing ALL
|
|
12
|
+
* answer-lane status AND the #2169 onMetric silence-liveness reset.
|
|
13
|
+
* SECONDARY: flip the lane to visible but FORGET to broaden the
|
|
14
|
+
* materialize-as-answer guard → a text-only no-reply turn falls into retract()
|
|
15
|
+
* and deletes the user's only copy of the answer (a lost-answer bug).
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect } from 'vitest'
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
const gatewaySrc = readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8')
|
|
22
|
+
|
|
23
|
+
describe('draft-retirement wiring', () => {
|
|
24
|
+
it('sendMessageDraftFn is gated on the retirement (the single chokepoint)', () => {
|
|
25
|
+
expect(gatewaySrc).toMatch(/!DRAFT_ANSWER_LANE_RETIRED && typeof _rawSendMessageDraft === 'function'/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('DRAFT_ANSWER_LANE_RETIRED is declared before its first use (no TDZ at boot)', () => {
|
|
29
|
+
const declIdx = gatewaySrc.indexOf('const DRAFT_ANSWER_LANE_RETIRED =')
|
|
30
|
+
const firstUseIdx = gatewaySrc.indexOf('!DRAFT_ANSWER_LANE_RETIRED && typeof _rawSendMessageDraft')
|
|
31
|
+
expect(declIdx).toBeGreaterThan(0)
|
|
32
|
+
expect(firstUseIdx).toBeGreaterThan(declIdx)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('PRIMARY GUARD: retired lane uses minInitialChars:1 (visible), never the MAX no-op gate', () => {
|
|
36
|
+
// The config must pick the {minInitialChars:1} branch when retired, so the
|
|
37
|
+
// lane actually opens a real message. The MAX branch is draft-only (legacy).
|
|
38
|
+
expect(gatewaySrc).toMatch(/ANSWER_STREAM_VISIBLE_ENABLED \|\| DRAFT_ANSWER_LANE_RETIRED\s*\n?\s*\?\s*\{ minInitialChars: 1 \}/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('SECONDARY GUARD: the materialize-as-answer guard is broadened in lockstep', () => {
|
|
42
|
+
// A text-only no-reply turn must materialize (ping + delete preview), not
|
|
43
|
+
// retract() the answer away.
|
|
44
|
+
expect(gatewaySrc).toMatch(/\(ANSWER_STREAM_VISIBLE_ENABLED \|\| DRAFT_ANSWER_LANE_RETIRED\)\s*\n?\s*&& !turn\.replyCalled/)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('the #2169 onMetric silence-liveness reset is preserved (fires on visible sends now)', () => {
|
|
48
|
+
const onMetric = (gatewaySrc.split('onMetric: (metricEv) => {')[1] ?? '').split('\n },')[0]
|
|
49
|
+
expect(onMetric).toMatch(/silencePoke\.noteProduction/)
|
|
50
|
+
expect(onMetric).toMatch(/currentTurn === turn/)
|
|
51
|
+
})
|
|
52
|
+
})
|