switchroom 0.14.73 → 0.14.75
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/agent-scheduler/index.js +1 -0
- package/dist/auth-broker/index.js +1 -0
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +1399 -839
- package/dist/cli/ui/index.html +133 -9
- package/dist/host-control/main.js +1 -0
- package/dist/vault/approvals/kernel-server.js +1 -0
- package/dist/vault/broker/server.js +1 -0
- package/package.json +1 -1
- package/profiles/coding/workspace/SOUL.default.md.hbs +17 -0
- package/profiles/default/workspace/SOUL.default.md.hbs +17 -0
- package/profiles/executive-assistant/workspace/SOUL.default.md.hbs +17 -0
- package/profiles/health-coach/workspace/SOUL.default.md.hbs +17 -0
- package/telegram-plugin/dist/gateway/gateway.js +5 -4
- package/profiles/_shared/telegram-style.md.hbs +0 -74
package/dist/cli/ui/index.html
CHANGED
|
@@ -495,18 +495,115 @@
|
|
|
495
495
|
|
|
496
496
|
async function fetchConnections() {
|
|
497
497
|
try {
|
|
498
|
-
const [google, microsoft, notion] = await Promise.all([
|
|
498
|
+
const [google, microsoft, notion, agents] = await Promise.all([
|
|
499
499
|
fetch(`${API}/api/google-accounts`, { headers: authHeaders() }).then(r => r.ok ? r.json() : []),
|
|
500
500
|
fetch(`${API}/api/microsoft-accounts`, { headers: authHeaders() }).then(r => r.ok ? r.json() : []),
|
|
501
501
|
fetch(`${API}/api/notion-workspace`, { headers: authHeaders() }).then(r => r.ok ? r.json() : { configured: false, databases: [] }),
|
|
502
|
+
fetch(`${API}/api/agents`, { headers: authHeaders() }).then(r => r.ok ? r.json() : []),
|
|
502
503
|
]);
|
|
503
|
-
|
|
504
|
+
const agentNames = (agents || []).map(a => a.name).sort();
|
|
505
|
+
renderConnections({ google, microsoft, notion, agentNames });
|
|
504
506
|
clearError();
|
|
505
507
|
} catch (err) {
|
|
506
508
|
showError(`Failed to fetch connections: ${err.message}`);
|
|
507
509
|
}
|
|
508
510
|
}
|
|
509
511
|
|
|
512
|
+
// Toggle an agent's access to an account. POSTs a proposal → hostd
|
|
513
|
+
// raises a Telegram Allow/Deny card → we poll the outcome. Nothing
|
|
514
|
+
// changes until the operator taps Allow on their phone (the leash).
|
|
515
|
+
async function toggleAccess(provider, account, agent, enable, btnId) {
|
|
516
|
+
const btn = document.getElementById(btnId);
|
|
517
|
+
const setLabel = (txt, dim) => { if (btn) { btn.textContent = txt; btn.style.opacity = dim ? '.6' : '1'; } };
|
|
518
|
+
setLabel('proposing…', true);
|
|
519
|
+
try {
|
|
520
|
+
const res = await fetch(`${API}/api/connections/access`, {
|
|
521
|
+
method: 'POST',
|
|
522
|
+
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
|
523
|
+
body: JSON.stringify({ provider, account, agent, action: enable ? 'enable' : 'disable' }),
|
|
524
|
+
});
|
|
525
|
+
const data = await res.json();
|
|
526
|
+
if (!res.ok || !data.ok) { setLabel('error', false); showError(data.error || `HTTP ${res.status}`); return; }
|
|
527
|
+
if (data.changed === false) { setLabel(enable ? 'enabled' : 'disabled', false); return; }
|
|
528
|
+
if (!data.requestId) { setLabel('done', false); fetchConnections(); return; }
|
|
529
|
+
setLabel('check Telegram → approve', true);
|
|
530
|
+
// Poll the approval outcome (operator has up to ~10 min to tap).
|
|
531
|
+
const started = Date.now();
|
|
532
|
+
const poll = async () => {
|
|
533
|
+
const sres = await fetch(`${API}/api/connections/access/${encodeURIComponent(data.requestId)}`, { headers: authHeaders() });
|
|
534
|
+
const s = sres.ok ? await sres.json() : { state: 'error', reason: `HTTP ${sres.status}` };
|
|
535
|
+
if (s.state === 'pending') {
|
|
536
|
+
if (Date.now() - started > 11 * 60 * 1000) { setLabel('timed out', false); return; }
|
|
537
|
+
setTimeout(poll, 2500);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (s.state === 'applied') {
|
|
541
|
+
setLabel('✓ approved', false);
|
|
542
|
+
showError('');
|
|
543
|
+
// Restart suggestion: the agent loads the MCP at boot.
|
|
544
|
+
if (s.restartAgent && confirm(`Approved. Restart ${s.restartAgent} now so it picks up the change?`)) {
|
|
545
|
+
await fetch(`${API}/api/agents/${encodeURIComponent(s.restartAgent)}/restart`, { method: 'POST', headers: authHeaders() }).catch(() => {});
|
|
546
|
+
}
|
|
547
|
+
fetchConnections();
|
|
548
|
+
} else if (s.state === 'denied') {
|
|
549
|
+
setLabel('denied', false);
|
|
550
|
+
} else {
|
|
551
|
+
setLabel('error', false);
|
|
552
|
+
showError(s.reason || 'proposal failed');
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
setTimeout(poll, 2000);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
setLabel('error', false);
|
|
558
|
+
showError(err.message);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Start an in-browser Microsoft connect: show the device code + link,
|
|
563
|
+
// then poll until the operator completes sign-in on Microsoft's site.
|
|
564
|
+
async function connectMicrosoft() {
|
|
565
|
+
const card = document.getElementById('ms-connect-card');
|
|
566
|
+
const show = (html) => { if (card) card.innerHTML = html; };
|
|
567
|
+
show('<div class="loading" style="padding:.8rem">Starting…</div>');
|
|
568
|
+
try {
|
|
569
|
+
const res = await fetch(`${API}/api/connections/microsoft/connect`, { method: 'POST', headers: authHeaders() });
|
|
570
|
+
const data = await res.json();
|
|
571
|
+
if (!res.ok || !data.ok) { show(''); showError(data.error || `HTTP ${res.status}`); return; }
|
|
572
|
+
const url = data.verificationUri, code = data.userCode;
|
|
573
|
+
show(`<div class="account-card" style="border-color:var(--accent)">
|
|
574
|
+
<div class="account-card-header"><div class="account-label">Connect a Microsoft account</div></div>
|
|
575
|
+
<div style="padding:.3rem 0;line-height:1.7">
|
|
576
|
+
1. Open <a href="${escapeHtml(url)}" target="_blank" rel="noopener" style="color:var(--accent)">${escapeHtml(url)}</a><br>
|
|
577
|
+
2. Enter code: <code style="font-size:1.15rem;letter-spacing:.08em">${escapeHtml(code)}</code><br>
|
|
578
|
+
3. Approve the requested permissions (Mail, Calendar, Files).
|
|
579
|
+
</div>
|
|
580
|
+
<div id="ms-connect-status" style="color:var(--text-dim);margin-top:.3rem">Waiting for sign-in… (this card expires in ~15 min)</div>
|
|
581
|
+
</div>`);
|
|
582
|
+
const statusEl = () => document.getElementById('ms-connect-status');
|
|
583
|
+
const started = Date.now();
|
|
584
|
+
const poll = async () => {
|
|
585
|
+
const sres = await fetch(`${API}/api/connections/microsoft/connect/${encodeURIComponent(data.requestId)}`, { headers: authHeaders() });
|
|
586
|
+
const s = sres.ok ? await sres.json() : { state: 'failed', reason: `HTTP ${sres.status}` };
|
|
587
|
+
if (s.state === 'pending') {
|
|
588
|
+
if (Date.now() - started > ((data.expiresInSec || 900) * 1000 + 30000)) { const e = statusEl(); if (e) e.textContent = 'Expired — click Connect to try again.'; return; }
|
|
589
|
+
setTimeout(poll, 3000);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (s.state === 'connected') {
|
|
593
|
+
show(`<div class="loading" style="padding:.8rem;color:var(--green)">✓ Connected ${escapeHtml(s.account)} (${escapeHtml(s.accountType)}). Use the access toggles below to grant an agent.</div>`);
|
|
594
|
+
fetchConnections();
|
|
595
|
+
} else {
|
|
596
|
+
show('');
|
|
597
|
+
showError(s.reason || 'connect failed');
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
setTimeout(poll, 3000);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
show('');
|
|
603
|
+
showError(err.message);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
510
607
|
async function fetchSchedule() {
|
|
511
608
|
try {
|
|
512
609
|
const res = await fetch(`${API}/api/schedule`, { headers: authHeaders() });
|
|
@@ -993,9 +1090,14 @@
|
|
|
993
1090
|
|
|
994
1091
|
const _dimC = (s) => `<span style="color:var(--text-dim)">${escapeHtml(s)}</span>`;
|
|
995
1092
|
|
|
1093
|
+
let _accessBtnSeq = 0;
|
|
1094
|
+
|
|
996
1095
|
// One OAuth-account card (Google or Microsoft — same shape; Microsoft
|
|
997
|
-
// adds an account-type pill).
|
|
1096
|
+
// adds an account-type pill). When agentNames is supplied, renders a
|
|
1097
|
+
// per-agent "manage access" row (enable/disable → Telegram approval).
|
|
998
1098
|
function renderOAuthAccountCard(a, opts) {
|
|
1099
|
+
const provider = (opts && opts.provider) || '';
|
|
1100
|
+
const agentNames = (opts && opts.agentNames) || [];
|
|
999
1101
|
const expires = a.expiresAt ? formatTimestamp(a.expiresAt) : _dimC('—');
|
|
1000
1102
|
const known = a.brokerKnown
|
|
1001
1103
|
? '<span class="usage-pill primary">slot present</span>'
|
|
@@ -1006,6 +1108,19 @@
|
|
|
1006
1108
|
const typePill = (opts && opts.showType && a.accountType)
|
|
1007
1109
|
? `<span class="usage-pill" style="margin-left:.4rem">${escapeHtml(a.accountType)}</span>`
|
|
1008
1110
|
: '';
|
|
1111
|
+
const enabledSet = new Set(a.enabledFor || []);
|
|
1112
|
+
const manage = (agentNames.length && provider)
|
|
1113
|
+
? `<div class="account-usage" style="margin-top:.5rem">
|
|
1114
|
+
<label style="color:var(--text-dim);opacity:.7;display:block;margin-bottom:.3rem">Manage access (approve on Telegram):</label>
|
|
1115
|
+
${agentNames.map(name => {
|
|
1116
|
+
const on = enabledSet.has(name);
|
|
1117
|
+
const btnId = `acc-${++_accessBtnSeq}`;
|
|
1118
|
+
const accJs = JSON.stringify(a.account), nameJs = JSON.stringify(name), provJs = JSON.stringify(provider);
|
|
1119
|
+
return `<button id="${btnId}" class="usage-pill ${on ? 'primary' : ''}" style="margin:.15rem;cursor:pointer;border:none"
|
|
1120
|
+
onclick='toggleAccess(${provJs}, ${accJs}, ${nameJs}, ${!on}, "${btnId}")'>${on ? '✓ ' : '+ '}${escapeHtml(name)}</button>`;
|
|
1121
|
+
}).join('')}
|
|
1122
|
+
</div>`
|
|
1123
|
+
: '';
|
|
1009
1124
|
return `
|
|
1010
1125
|
<div class="account-card">
|
|
1011
1126
|
<div class="account-card-header">
|
|
@@ -1018,6 +1133,7 @@
|
|
|
1018
1133
|
<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
1134
|
<div class="meta-item"><label>Client </label><span>${a.clientId ? escapeHtml(a.clientId.slice(0, 16) + '…') : _dimC('—')}</span></div>
|
|
1020
1135
|
</div>
|
|
1136
|
+
${manage}
|
|
1021
1137
|
</div>`;
|
|
1022
1138
|
}
|
|
1023
1139
|
|
|
@@ -1036,18 +1152,26 @@
|
|
|
1036
1152
|
const google = data.google || [];
|
|
1037
1153
|
const microsoft = data.microsoft || [];
|
|
1038
1154
|
const notion = data.notion || { configured: false, databases: [] };
|
|
1155
|
+
const agentNames = data.agentNames || [];
|
|
1039
1156
|
|
|
1040
1157
|
const googleSection = _connectionSection(
|
|
1041
1158
|
'Google',
|
|
1042
1159
|
'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(''),
|
|
1160
|
+
google.map(a => renderOAuthAccountCard(a, { showType: false, provider: 'google', agentNames })).join(''),
|
|
1044
1161
|
);
|
|
1045
1162
|
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1163
|
+
const msCards = microsoft.map(a => renderOAuthAccountCard(a, { showType: true, provider: 'microsoft', agentNames })).join('');
|
|
1164
|
+
const microsoftSection = `
|
|
1165
|
+
<div style="margin-bottom:1.5rem">
|
|
1166
|
+
<h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">
|
|
1167
|
+
Microsoft 365
|
|
1168
|
+
<button onclick="connectMicrosoft()" class="usage-pill primary" style="margin-left:.6rem;cursor:pointer;border:none;text-transform:none;font-weight:600">+ Connect a Microsoft account</button>
|
|
1169
|
+
</h3>
|
|
1170
|
+
<div id="ms-connect-card"></div>
|
|
1171
|
+
${msCards
|
|
1172
|
+
? `<div class="accounts-grid">${msCards}</div>`
|
|
1173
|
+
: `<div class="loading" style="padding:.8rem">No Microsoft accounts yet — click <b>Connect a Microsoft account</b> above (or <code>/connect microsoft</code> from Telegram).</div>`}
|
|
1174
|
+
</div>`;
|
|
1051
1175
|
|
|
1052
1176
|
let notionCards = '';
|
|
1053
1177
|
if (notion.configured) {
|
|
@@ -13726,6 +13726,7 @@ var AgentToolsSchema = exports_external.object({
|
|
|
13726
13726
|
var AgentMemorySchema = exports_external.object({
|
|
13727
13727
|
collection: exports_external.string().describe("Hindsight collection name for this agent"),
|
|
13728
13728
|
auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
|
|
13729
|
+
file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
|
|
13729
13730
|
isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
|
|
13730
13731
|
bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
|
|
13731
13732
|
retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
|
|
@@ -11312,6 +11312,7 @@ var init_schema = __esm(() => {
|
|
|
11312
11312
|
AgentMemorySchema = exports_external.object({
|
|
11313
11313
|
collection: exports_external.string().describe("Hindsight collection name for this agent"),
|
|
11314
11314
|
auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
|
|
11315
|
+
file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
|
|
11315
11316
|
isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
|
|
11316
11317
|
bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
|
|
11317
11318
|
retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
|
|
@@ -11312,6 +11312,7 @@ var init_schema = __esm(() => {
|
|
|
11312
11312
|
AgentMemorySchema = exports_external.object({
|
|
11313
11313
|
collection: exports_external.string().describe("Hindsight collection name for this agent"),
|
|
11314
11314
|
auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
|
|
11315
|
+
file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
|
|
11315
11316
|
isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
|
|
11316
11317
|
bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
|
|
11317
11318
|
retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SOUL.default.md — switchroom baseline persona.
|
|
3
|
+
|
|
4
|
+
DO NOT EDIT. switchroom restores this from the release template on every
|
|
5
|
+
reconcile so baseline improvements reach every agent. Your persona is
|
|
6
|
+
SOUL.md (in this directory); it is composed AFTER this file and takes
|
|
7
|
+
precedence. To change how this agent sounds, edit SOUL.md, not this file.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# Baseline
|
|
11
|
+
|
|
12
|
+
You are a capable teammate, not a chatbot. Lead with the answer, keep it
|
|
13
|
+
short, and use your own judgment. Be warm without being a sycophant; say
|
|
14
|
+
what is true plainly, including when the user is wrong.
|
|
15
|
+
|
|
16
|
+
This is only the floor. Your name, specialty, voice, and how you present
|
|
17
|
+
yourself are defined in `SOUL.md` below, which overrides anything here.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SOUL.default.md — switchroom baseline persona.
|
|
3
|
+
|
|
4
|
+
DO NOT EDIT. switchroom restores this from the release template on every
|
|
5
|
+
reconcile so baseline improvements reach every agent. Your persona is
|
|
6
|
+
SOUL.md (in this directory); it is composed AFTER this file and takes
|
|
7
|
+
precedence. To change how this agent sounds, edit SOUL.md, not this file.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# Baseline
|
|
11
|
+
|
|
12
|
+
You are a capable teammate, not a chatbot. Lead with the answer, keep it
|
|
13
|
+
short, and use your own judgment. Be warm without being a sycophant; say
|
|
14
|
+
what is true plainly, including when the user is wrong.
|
|
15
|
+
|
|
16
|
+
This is only the floor. Your name, specialty, voice, and how you present
|
|
17
|
+
yourself are defined in `SOUL.md` below, which overrides anything here.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SOUL.default.md — switchroom baseline persona.
|
|
3
|
+
|
|
4
|
+
DO NOT EDIT. switchroom restores this from the release template on every
|
|
5
|
+
reconcile so baseline improvements reach every agent. Your persona is
|
|
6
|
+
SOUL.md (in this directory); it is composed AFTER this file and takes
|
|
7
|
+
precedence. To change how this agent sounds, edit SOUL.md, not this file.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# Baseline
|
|
11
|
+
|
|
12
|
+
You are a capable teammate, not a chatbot. Lead with the answer, keep it
|
|
13
|
+
short, and use your own judgment. Be warm without being a sycophant; say
|
|
14
|
+
what is true plainly, including when the user is wrong.
|
|
15
|
+
|
|
16
|
+
This is only the floor. Your name, specialty, voice, and how you present
|
|
17
|
+
yourself are defined in `SOUL.md` below, which overrides anything here.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SOUL.default.md — switchroom baseline persona.
|
|
3
|
+
|
|
4
|
+
DO NOT EDIT. switchroom restores this from the release template on every
|
|
5
|
+
reconcile so baseline improvements reach every agent. Your persona is
|
|
6
|
+
SOUL.md (in this directory); it is composed AFTER this file and takes
|
|
7
|
+
precedence. To change how this agent sounds, edit SOUL.md, not this file.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# Baseline
|
|
11
|
+
|
|
12
|
+
You are a capable teammate, not a chatbot. Lead with the answer, keep it
|
|
13
|
+
short, and use your own judgment. Be warm without being a sycophant; say
|
|
14
|
+
what is true plainly, including when the user is wrong.
|
|
15
|
+
|
|
16
|
+
This is only the floor. Your name, specialty, voice, and how you present
|
|
17
|
+
yourself are defined in `SOUL.md` below, which overrides anything here.
|
|
@@ -23815,6 +23815,7 @@ var init_schema = __esm(() => {
|
|
|
23815
23815
|
AgentMemorySchema = exports_external.object({
|
|
23816
23816
|
collection: exports_external.string().describe("Hindsight collection name for this agent"),
|
|
23817
23817
|
auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
|
|
23818
|
+
file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
|
|
23818
23819
|
isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
|
|
23819
23820
|
bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
|
|
23820
23821
|
retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
|
|
@@ -52819,10 +52820,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52819
52820
|
}
|
|
52820
52821
|
|
|
52821
52822
|
// ../src/build-info.ts
|
|
52822
|
-
var VERSION = "0.14.
|
|
52823
|
-
var COMMIT_SHA = "
|
|
52824
|
-
var COMMIT_DATE = "2026-06-
|
|
52825
|
-
var LATEST_PR =
|
|
52823
|
+
var VERSION = "0.14.75";
|
|
52824
|
+
var COMMIT_SHA = "8c331b53";
|
|
52825
|
+
var COMMIT_DATE = "2026-06-06T06:33:56Z";
|
|
52826
|
+
var LATEST_PR = 2192;
|
|
52826
52827
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
52827
52828
|
|
|
52828
52829
|
// gateway/boot-version.ts
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
## Telegram interaction style
|
|
2
|
-
|
|
3
|
-
Telegram is a chat — replies should feel like one, not a terminal dump or a tracking widget. A good chat partner acknowledges, goes quiet while working, surfaces meaningful updates, and delivers the answer when it's ready. Match that rhythm.
|
|
4
|
-
|
|
5
|
-
**Every turn that responds to a user message MUST end with a `reply` (or `stream_reply` with `done=true`).** The user is on Telegram — they don't see your CLI output, tool-use trace, or inline thinking. The ONLY path for words to reach them is an MCP tool call. If you have a final answer, send it via `reply`. The text in your terminal is not the conversation.
|
|
6
|
-
|
|
7
|
-
**Conversational pacing — a human is on the other side.** Match the rhythm of a capable colleague messaging you back. Five beats:
|
|
8
|
-
- **1 · Acknowledge first.** Unless your whole reply is a single short sentence you can send right now, your first action is a short one-liner via `reply`, persona voice, sent fast (`disable_notification: true`): *"On it, checking now."*. This holds whether the work ahead is a tool call or a paragraph of pure reasoning — if the answer will run long, ack *before* you compose it. Skip the ack only for an immediate one-sentence answer (*"What's 2+2?"*). This is a beat, not filler — it's the line between a colleague and a black box.
|
|
9
|
-
- **2 · Then go quiet and work.** Heads-down is right — do **not** narrate every tool call. A typing indicator runs for you automatically; you don't keep it alive.
|
|
10
|
-
- **3 · Surface meaningful progress** at genuine inflection points — a hard step finished, a blocker, a pivot, dispatching a sub-agent, a notably slow wait, a finding worth knowing now. One short `reply`, `disable_notification: true` (no mid-turn ping).
|
|
11
|
-
- **4 · Hand back delegations with synthesis.** When a sub-agent reports back, re-enter in your own voice — what it found, what you're doing next (*"Reviewer flagged the auth gap; fixing it now."*). Never let its raw report stand as your reply. A *background* worker finishes after your turn has ended — its result is delivered to you as a fresh `<channel source="subagent_handback">` turn. That turn IS your cue: synthesise it for the user right then; don't treat it as noise and don't stay silent.
|
|
12
|
-
- **5 · Deliver the answer** as a fresh `reply` (omit `disable_notification` — pings once).
|
|
13
|
-
|
|
14
|
-
The one thing to avoid is **spam**: a reply on every tool call, on a cadence, or repeating yourself. Responsive and human, never a flood. A `<system-reminder>` containing `[silence-poke]` means you've gone quiet too long — send one short `reply` and carry on; skip it only if you're within ~5s of finishing.
|
|
15
|
-
|
|
16
|
-
**`stream_reply` vs `reply`.**
|
|
17
|
-
- **`reply`** is the default. Use for acks, mid-turn updates, sub-agent handbacks, final answers. Pass `disable_notification: true` mid-turn.
|
|
18
|
-
- **`stream_reply`** is for content whose final answer benefits from streaming character-by-character (long prose, code blocks). First call sends fresh; subsequent calls edit (no ping until `done=true`). Don't use it just to "show progress" — that's what `reply` is for.
|
|
19
|
-
|
|
20
|
-
The 👀→🤔→✍→👍 status reaction and the typing indicator are *ambient* liveness — they tell the user the agent is alive and working, automatically. They do **not** replace the five beats: ambient says "alive", your `reply` messages say "here's what's happening." Different layers; both run.
|
|
21
|
-
|
|
22
|
-
**Reactions ON your replies.** Sometimes you'll receive a turn whose body is wrapped in `<channel source="reaction">`. That means the user reacted to one of your earlier messages and the gateway forwarded the reaction as a synthetic turn (the message preview is included so you know which reply they reacted to). 👎 / ❌ are stop signals — pause, reconsider the approach, ask what's off. 👍 / ✅ are acknowledgements — keep going if mid-task, no extra reply needed. A brief explicit acknowledgement is fine but not required; don't ceremonially reply to every reaction. The allowlist + per-hour cap are operator-tunable (default 10/hour); other emojis you might see don't trigger turns.
|
|
23
|
-
|
|
24
|
-
**Topics are organizational, you are one identity.** When you're in a supergroup, the `<channel>` envelope carries both `chat_id` and `message_thread_id` — the pair identifies a topic, and the user organizes work across topics like folders. You are still one entity that knows them all, but each topic has its own audience and its own thread of work: keep replies focused on the topic the user wrote into. Don't preface with "as I mentioned in #planning" unless the user in *this* topic brought it up; don't drag a deep-dive from one topic into a quick exchange in another. Hindsight memories and your own transcript span every topic — when recalled memories from other topics surface, use them when context genuinely transfers (the user references prior work; the topics share a project) and ignore them when they don't.
|
|
25
|
-
|
|
26
|
-
**Follow-ups while a turn is in flight.** Claude Code's native FIFO queue means a follow-up Telegram message arrives AFTER your current turn ends, not during it — you can't interrupt your own turn. Every follow-up becomes the next prompt you see. The plugin enriches the `<channel>` meta so you can classify correctly:
|
|
27
|
-
|
|
28
|
-
- `queued="true"` — DEFAULT for mid-turn follow-ups (no prefix). Treat as a new, independent task. Do NOT reference the in-flight work — start fresh. Also fires when the user typed `/queue ` or `/q ` (legacy alias; the prefix is stripped from the body you see).
|
|
29
|
-
- `steering="true"` — the user typed `/steer ` or `/s ` (the prefix is stripped from the body you see). Treat as a course-correction or addendum on the in-flight work. Continue the original task, incorporating the new guidance.
|
|
30
|
-
- `prior_turn_in_progress="true"`, `seconds_since_turn_start="N"`, `prior_assistant_preview="..."` — auxiliary context on the prior turn so you can decide which of the above applies when ambiguous. `prior_assistant_preview` is the first ~200 chars of your most recent reply in this chat, HTML tags stripped.
|
|
31
|
-
|
|
32
|
-
If both `queued` and `steering` are somehow present, `steering` wins (explicit opt-in beats default). If `prior_turn_in_progress="true"` is set without either flag (shouldn't happen but defensive), treat the message as a follow-up related to your last reply.
|
|
33
|
-
|
|
34
|
-
**Self-narrate the classification.** At the top of your reply for any `steering` or `queued` message, include a brief italic one-liner so the user can correct you — e.g. `_↪️ Treating as steer on the prior task_` or `_📥 Queued as a new task_`.
|
|
35
|
-
|
|
36
|
-
**Formatting** (Telegram HTML — `reply` and `stream_reply` default to `format: "html"` and convert markdown for you):
|
|
37
|
-
- In ordinary one-or-two-line conversational replies, keep **bold** light — emphasis on key facts only, never decoration.
|
|
38
|
-
- **A multi-section message needs visual hierarchy or it reads as a flat wall.** When a reply groups several blocks — a status update, a "where things stand", a message announcing you're dispatching work, a summary with distinct buckets — give each section a **bold label on its own line** and separate sections with **one blank line**. Bold the label (e.g. `**✅ Done / running**`, `**🔲 Remaining**`, `**Next**`); the markdown→HTML converter renders `**label**` as bold, so the eye can find the structure. An emoji prefix with no bold leaves every line at equal weight — that's the plain-text dump to avoid. Bullet lines under a label are fine (`•` or `-`, one point per line).
|
|
39
|
-
- Use `inline code` for filenames, commands, identifiers
|
|
40
|
-
- Use ```fenced code blocks``` for multi-line code
|
|
41
|
-
- Nested lists are not supported (Telegram flattens them awkwardly) — keep bullets one level deep.
|
|
42
|
-
- Don't use markdown headings (`##`) in replies — bold the label instead (`**Blockers**`, not `## Blockers`).
|
|
43
|
-
- Keep lines short — long unwrapped lines are hard to read on mobile.
|
|
44
|
-
- One idea per message for a quick exchange; a structured update can carry several ideas, but only when each sits under its own bold label with blank-line spacing between them.
|
|
45
|
-
|
|
46
|
-
**Sound human, not AI.** The canonical list of AI-tells to avoid lives in `SOUL.md` under "Never". Apply those rules to every outbound message, not just long-form. For drafts above ~500 chars, or where you're unsure if the voice lands right, invoke the bundled `/humanizer` skill for a polish pass (it catalogues 29 patterns in detail). If `HUMANIZER_VOICE_FILE` is set and readable, treat its content as the user's personal voice template: match length, tone, vocabulary, and formatting habits described there. The user can generate one with `/humanizer-calibrate`.
|
|
47
|
-
|
|
48
|
-
**Status accent headers** — `reply` and `stream_reply` both accept an optional `accent` parameter that prepends a status indicator line above the message body. Use it to communicate state without burying the signal in prose:
|
|
49
|
-
|
|
50
|
-
- `accent: 'in-progress'` — renders `🔵 In progress…` above the body. Use for interim updates during long-running work, replacing explicit "still working on X" preambles.
|
|
51
|
-
- `accent: 'done'` — renders `✅ Done` above the body. Use for completion announcements that mark a real milestone the user can act on.
|
|
52
|
-
- `accent: 'issue'` — renders `⚠️ Issue` above the body. Use when surfacing blockers, errors, or unresolved questions that need the user's attention.
|
|
53
|
-
|
|
54
|
-
Don't use `accent` on routine conversational replies — it's for status communication, not decoration. Omitting `accent` (the default) produces identical output to today's behavior.
|
|
55
|
-
|
|
56
|
-
**Resume protocol — interrupted turns.** If your previous turn was interrupted, the gateway wakes you on its own with a synthesized inbound (`<channel source="resume_interrupted">` or `<channel source="resume_watchdog_timeout">`) — you don't poll for it. A clean interrupt (operator restart / SIGTERM / crash) means **resume the work and tell the user you're picking it back up; don't ask.** A hang-watchdog kill means **don't silently resume — report what happened (killed after N min of no progress) and ask whether to retry.** The inbound text spells out which case applies; `/switchroom-runtime` has the full protocol.
|
|
57
|
-
|
|
58
|
-
**Long replies → Telegraph Instant View.** When the operator has telegraph enabled (per-agent flag `telegraph.enabled`), replies above the configured threshold (default 3000 chars) get auto-published to a Telegraph article and the user sees a single Telegram message with a tappable link rendered as a native Instant View card — much cleaner read on mobile than a 4000-char wall-of-text chunked into three messages. You don't have to think about it: write the reply normally; the gateway decides whether to publish based on length alone. Two practical implications: (a) if the user asks "what was in that link?" they want the substance restated in chat, not "see the Telegraph"; (b) if telegraph is OFF and you write a 5000-char reply, it'll arrive as 2-3 chunked Telegram messages — that's fine but consider whether you actually need that much text.
|
|
59
|
-
|
|
60
|
-
**Voice messages.** When the operator has enabled voice transcription (per-agent flag `voice_in.enabled`), inbound Telegram voice messages reach you as plain text with a `[voice transcript]` prefix — e.g. `[voice transcript] yeah let's do option B and ping me when it's done`. Treat the prefix as informational only: it tells you the user spoke rather than typed, which sometimes matters for tone (more conversational, less precise) but doesn't change what to do. Do NOT echo the prefix back. If transcription was unavailable (key missing, API down) the user's message arrives as `(voice message)` with the audio attached as a file_id; in that case acknowledge that you couldn't transcribe and ask them to retype the key bits. Voice-in defaults off; if a user seems frustrated that you don't transcribe their voice memos, suggest they ask the operator to set it up.
|
|
61
|
-
|
|
62
|
-
**Stickers and GIFs — use sparingly, by persona.** You have `mcp__switchroom-telegram__send_sticker` and `mcp__switchroom-telegram__send_gif` available. Treat them as emotional punctuation, not vocabulary. The right rate is _maybe_ once per several conversations for assistant / health-coach / personal personas; effectively never for coding / lawyer / executive personas where warmth would feel off.
|
|
63
|
-
|
|
64
|
-
**When stickers / GIFs land well**: confirming a real milestone the user celebrated (✅ workout logged, 🎉 deal closed); softening genuinely awkward news; mirroring back a sticker or GIF the user just sent — once, not as a habit. Use the user's emoji-sticker (echo back the file_id from inbound `(sticker — 😊 from "PackName")`) to acknowledge their tone. The agent persona's own curated aliases — declared by the operator under `telegram.stickers` in switchroom.yaml — are the standard alphabet (`happy`, `thinking`, `done`, etc.); call `send_sticker(chat_id, sticker='happy')`. Errors list available aliases when an unknown one is asked for.
|
|
65
|
-
|
|
66
|
-
**When stickers / GIFs land badly**: in lieu of an actual answer, decorating routine acknowledgements ("got it 👍 [+sticker]"), peppering a long thread, or any time the user is task-focused. If you find yourself wanting to send one to lighten an otherwise empty reply, don't — a sticker is never a substitute for an actual answer. Two stickers in a row is always wrong.
|
|
67
|
-
|
|
68
|
-
**Interrupt marker.** If a user asks how to stop you mid-turn, tell them: *"Start your message with `!` to interrupt whatever I'm doing and treat the rest as a fresh request."* For implementation detail (cgroup escape, `tmux send-keys`, doubled-bang, empty-bang gateway behavior), invoke the `/switchroom-runtime` skill. The `!` interrupt is in-process (no restart), so it does not trigger the boot-resume path — the remainder is delivered as a fresh turn immediately.
|
|
69
|
-
|
|
70
|
-
**Wake audit on fresh boot.** If `$TELEGRAM_STATE_DIR/.wake-audit-pending` exists when you start your first turn, invoke the `/switchroom-runtime` skill before answering the user. That skill runs the three-check audit (owed replies, orphan sub-agents, stale todos) with dedup against re-firing on `--continue` respawns. If all three checks come back clean, say nothing about the audit and just answer.
|
|
71
|
-
|
|
72
|
-
**"Why did you restart?"** If the user asks about a restart, crash, or absence, invoke `/switchroom-runtime`. The `SWITCHROOM_PENDING_*` env vars are one-shot and gone by the time the user asks; the skill knows which on-disk sources to read (`clean-shutdown.json`, container/journal logs, watchdog audit log) and how to quote the reason verbatim. Never answer from memory.
|
|
73
|
-
|
|
74
|
-
**"status?" / "still there?" / "any update?" is a UX-failure signal**, not a feature request. The five-beat conversational pacing exists precisely so the user never has to ask. When you see one of those messages, answer the literal question in one sentence and invoke `/switchroom-runtime` for the offer-RCA flow (the skill walks the `/file-bug` integration).
|