agentp 0.8.0 → 0.9.0

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/bin/tgagentp CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const http = require('http');
3
4
  const https = require('https');
4
5
  const fs = require('fs');
5
6
  const path = require('path');
@@ -15,8 +16,39 @@ const log = {
15
16
  };
16
17
 
17
18
  const STARTUP_CHAT_FILE = '/tmp/tgagentp-startup-chat';
19
+ const TGAGENTP_PORT = parseInt(process.env.TGAGENTP_PORT || '0', 10);
20
+ const PORT_FILE = '/tmp/tgagentp-port';
21
+ const TGAGENTP_DEBOUNCE_MS = parseInt(process.env.TGAGENTP_DEBOUNCE_MS || '5000', 10);
18
22
  const { listServers, activateServer } = require('../lib/ocmux.js');
19
23
 
24
+ // Agentp gateway message queue, keyed by server URL
25
+ const agentpQueues = {};
26
+
27
+ function validateAgentpAuth(req) {
28
+ const password = process.env.OPENCODE_SERVER_PASSWORD;
29
+ if (!password) return true;
30
+ const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
31
+ const expected = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
32
+ return req.headers.authorization === expected;
33
+ }
34
+
35
+ function flushAgentpQueue(server, token, chatId) {
36
+ if (!chatId) return;
37
+ const q = agentpQueues[server];
38
+ if (!q || q.messages.length === 0) return;
39
+ const msgs = q.messages.splice(0);
40
+ q.notified = false;
41
+ if (q.notifyTimer) { clearTimeout(q.notifyTimer); q.notifyTimer = null; }
42
+ sendLongMessage(token, chatId,
43
+ `📨 Delivering ${msgs.length} queued agentp message(s):`)
44
+ .then(() => {
45
+ for (const msg of msgs) {
46
+ sendLongMessage(token, chatId, msg.trim()).catch(() => {});
47
+ }
48
+ })
49
+ .catch(() => {});
50
+ }
51
+
20
52
  // ---------------------------------------------------------------------------
21
53
  // Telegram Bot API helpers
22
54
  // ---------------------------------------------------------------------------
@@ -72,7 +104,7 @@ function convertMarkdownToHtml(text) {
72
104
 
73
105
  return parts.map((part, i) => {
74
106
  if (i % 2 === 1) {
75
- // Code block — strip language tag, escape entities, wrap in <pre>
107
+ // Code block ─ strip language tag, escape entities, wrap in <pre>
76
108
  const nl = part.indexOf('\n');
77
109
  const inner = nl >= 0 ? part.slice(nl + 1) : part;
78
110
  return '<pre>' + inner
@@ -207,25 +239,25 @@ async function cmdHelp(token, chatId, args) {
207
239
  const topic = args[0].toLowerCase();
208
240
  if (topic === 'servers') {
209
241
  await sendLongMessage(token, chatId,
210
- '`/servers` — list all opencode servers (ocmux sessions)\n' +
211
- '`/servers switch <name>` — switch target server\n\n' +
242
+ '`/servers` ─ list all opencode servers (ocmux sessions)\n' +
243
+ '`/servers switch <name>` ─ switch target server\n\n' +
212
244
  '`<name>` can be a full path or basename of the project directory.');
213
245
  return;
214
246
  }
215
247
  if (topic === 'sessions') {
216
248
  await sendLongMessage(token, chatId,
217
- '`/sessions` — list active sessions (numbered, newest first, max 50)\n' +
218
- '`/sessions switch <number-or-name>` — send messages to a specific session\n' +
219
- '`/sessions new [name]` — create a new session\n' +
220
- '`/sessions rename <name>` — rename the active session\n\n' +
249
+ '`/sessions` ─ list active sessions (numbered, newest first, max 50)\n' +
250
+ '`/sessions switch <number-or-name>` ─ send messages to a specific session\n' +
251
+ '`/sessions new [name]` ─ create a new session\n' +
252
+ '`/sessions rename <name>` ─ rename the active session\n\n' +
221
253
  'Switch by number (from the list) or by case-insensitive partial name match.\n' +
222
254
  'Use `/sessions` to find session numbers.');
223
255
  return;
224
256
  }
225
257
  if (topic === 'shutdown') {
226
258
  await sendLongMessage(token, chatId,
227
- '`/shutdown` — stop tgagentp process\n' +
228
- '`/shutdown force` — force shutdown even if busy (in-flight work will be lost)\n\n' +
259
+ '`/shutdown` ─ stop tgagentp process\n' +
260
+ '`/shutdown force` ─ force shutdown even if busy (in-flight work will be lost)\n\n' +
229
261
  'Only available when tgagentp is started with `--dev` flag.\n' +
230
262
  'Useful when running tgagentp in a restart loop:\n' +
231
263
  '`while true; do tgagentp --dev; done`');
@@ -233,32 +265,32 @@ async function cmdHelp(token, chatId, args) {
233
265
  }
234
266
  if (topic === 'agents') {
235
267
  await sendLongMessage(token, chatId,
236
- '`/agents` — list all available agents (name, mode, assigned model, description).\n' +
237
- '`/agents switch <name>` — switch the active agent. Subsequent messages will use this agent.\n\n' +
268
+ '`/agents` ─ list primary agents (with ▶ marker for the active one).\n' +
269
+ '`/agents switch <name>` ─ switch the active agent. Subsequent messages will use this agent.\n\n' +
238
270
  'Agent switching requires an active session (`/sessions switch` or `/sessions new`).');
239
271
  return;
240
272
  }
241
273
  if (topic === 'session') {
242
274
  await sendLongMessage(token, chatId,
243
275
  '`/session` is an alias for `/sessions`.\n\n' +
244
- '`/sessions` — list active sessions\n' +
245
- '`/sessions switch <number-or-name>` — switch to a session\n' +
246
- '`/sessions new [name]` — create a new session\n' +
247
- '`/sessions rename <name>` — rename the active session');
276
+ '`/sessions` ─ list active sessions\n' +
277
+ '`/sessions switch <number-or-name>` ─ switch to a session\n' +
278
+ '`/sessions new [name]` ─ create a new session\n' +
279
+ '`/sessions rename <name>` ─ rename the active session');
248
280
  return;
249
281
  }
250
282
  if (topic === 'models') {
251
283
  await sendLongMessage(token, chatId,
252
- '`/models` — list all providers and their models.\n' +
284
+ '`/models` ─ list all providers and their models.\n' +
253
285
  'Shows provider name, connection status, default model, and up to 5 models per provider\n' +
254
286
  'with status, context limit, and input/output costs.');
255
287
  return;
256
288
  }
257
289
  if (topic === 'allow' || topic === 'reject' || topic === 'always') {
258
290
  await sendLongMessage(token, chatId,
259
- '`/allow` — approve a permission request once\n' +
260
- '`/reject` — deny a permission request\n' +
261
- '`/always` — approve and remember for the session\n\n' +
291
+ '`/allow` ─ approve a permission request once\n' +
292
+ '`/reject` ─ deny a permission request\n' +
293
+ '`/always` ─ approve and remember for the session\n\n' +
262
294
  'These commands respond to OpenCode permission prompts forwarded by tgagentp.\n' +
263
295
  'When OpenCode needs approval (e.g. to run a shell command), tgagentp sends a\n' +
264
296
  '🔐 notification to the chat. Use one of these commands to answer.');
@@ -266,10 +298,10 @@ async function cmdHelp(token, chatId, args) {
266
298
  }
267
299
  if (topic === 'think') {
268
300
  await sendLongMessage(token, chatId,
269
- '`/think` — toggle thinking message forwarding\n' +
270
- '`/think on` — forward model thinking to the chat in real-time\n' +
271
- '`/think off` — stop forwarding thinking messages (default)\n' +
272
- '`/think switch` — toggle current state\n\n' +
301
+ '`/think` ─ toggle thinking message forwarding\n' +
302
+ '`/think on` ─ forward model thinking to the chat in real-time\n' +
303
+ '`/think off` ─ stop forwarding thinking messages (default)\n' +
304
+ '`/think switch` ─ toggle current state\n\n' +
273
305
  'When enabled, the model\'s internal reasoning steps are sent to the chat\n' +
274
306
  'as they are generated, prefixed with 💭.\n' +
275
307
  'Can also be enabled at startup with `--think` flag.');
@@ -280,23 +312,23 @@ async function cmdHelp(token, chatId, args) {
280
312
  return;
281
313
  }
282
314
  await sendLongMessage(token, chatId,
283
- '`/help` — show this message\n' +
284
- '`/help <topic>` — detailed help\n' +
285
- '`/status` — show current server and session\n' +
286
- '`/cancel` — cancel the running prompt\n' +
287
- '`/servers` — list running opencode servers\n' +
288
- '`/servers switch <name>` — switch to a different server\n' +
289
- '`/sessions` — list/create/switch/rename sessions\n' +
290
- '`/sessions switch <number-or-name>` — switch to a session\n' +
291
- '`/sessions new [name]` — create a new session\n' +
292
- '`/sessions rename <name>` — rename the active session\n' +
293
- '`/session` — alias for `/sessions`\n' +
294
- '`/agents` — list available agents\n' +
295
- '`/agents switch <name>` — switch active agent\n' +
296
- '`/models` — list providers and models\n' +
297
- '`/think` — toggle thinking message forwarding\n' +
298
- '`/allow` / `/reject` / `/always` — respond to permission requests\n' +
299
- '`/shutdown` — stop tgagentp (only with `--dev` flag)\n\n' +
315
+ '`/help` ─ show this message\n' +
316
+ '`/help <topic>` ─ detailed help\n' +
317
+ '`/status` ─ show current server and session\n' +
318
+ '`/cancel` ─ cancel the running prompt\n' +
319
+ '`/servers` ─ list running opencode servers\n' +
320
+ '`/servers switch <name>` ─ switch to a different server\n' +
321
+ '`/sessions` ─ list/create/switch/rename sessions\n' +
322
+ '`/sessions switch <number-or-name>` ─ switch to a session\n' +
323
+ '`/sessions new [name]` ─ create a new session\n' +
324
+ '`/sessions rename <name>` ─ rename the active session\n' +
325
+ '`/session` ─ alias for `/sessions`\n' +
326
+ '`/agents` ─ list available agents\n' +
327
+ '`/agents switch <name>` ─ switch active agent\n' +
328
+ '`/models` ─ list providers and models\n' +
329
+ '`/think` ─ toggle thinking message forwarding\n' +
330
+ '`/allow` / `/reject` / `/always` ─ respond to permission requests\n' +
331
+ '`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n\n' +
300
332
  'Everything else is forwarded to the active server as `[Telegram] <text>`.');
301
333
  }
302
334
 
@@ -493,10 +525,10 @@ async function cmdSessions(token, chatId, args, setServer, setActiveSession, ser
493
525
  await sendLongMessage(token, chatId,
494
526
  `Unknown subcommand: /sessions ${sub}\n\n` +
495
527
  'Usage:\n' +
496
- ' `/sessions` — list sessions\n' +
497
- ' `/sessions switch <number-or-name>` — switch to a session\n' +
498
- ' `/sessions new [name]` — create a new session\n' +
499
- ' `/sessions rename <name>` — rename the active session');
528
+ ' `/sessions` ─ list sessions\n' +
529
+ ' `/sessions switch <number-or-name>` ─ switch to a session\n' +
530
+ ' `/sessions new [name]` ─ create a new session\n' +
531
+ ' `/sessions rename <name>` ─ rename the active session');
500
532
  return;
501
533
  }
502
534
 
@@ -596,10 +628,14 @@ async function cmdAgents(token, chatId, args, setServer, setActiveSession, serve
596
628
  }
597
629
  const target = matches[0];
598
630
  getState(server).activeAgentName = target.name;
599
- const mode = target.mode === 'primary' ? '' : ` (${target.mode})`;
600
- const model = target.model ? ` — \`${target.model.providerID}/${target.model.modelID}\`` : '';
631
+ const st = getState(server);
632
+ if (st.activeSessionId) {
633
+ updateSession(server, st.activeSessionId, undefined, target.name);
634
+ selectSession(server, st.activeSessionId);
635
+ }
636
+ const model = target.model ? ` ─ \`${target.model.providerID}/${target.model.modelID}\`` : '';
601
637
  await sendLongMessage(token, chatId,
602
- `Switched to agent: **${target.name}**${mode}${model}\nSubsequent messages will use this agent.`);
638
+ `Switched to agent: **${target.name}**${model}\nSubsequent messages will use this agent.`);
603
639
  } catch (err) {
604
640
  await sendLongMessage(token, chatId, `Error switching agent: ${err.message}`);
605
641
  }
@@ -612,13 +648,19 @@ async function cmdAgents(token, chatId, args, setServer, setActiveSession, serve
612
648
  await sendLongMessage(token, chatId, 'No agents available.');
613
649
  return;
614
650
  }
615
- const lines = agents.map(a => {
616
- const mode = a.mode === 'primary' ? '' : ` (${a.mode})`;
617
- const model = a.model ? ` — \`${a.model.providerID}/${a.model.modelID}\`` : '';
651
+ const primary = agents.filter(a => a.mode === 'primary');
652
+ if (primary.length === 0) {
653
+ await sendLongMessage(token, chatId, 'No primary agents available.');
654
+ return;
655
+ }
656
+ const activeAgentName = getState(server).activeAgentName;
657
+ const lines = primary.map(a => {
658
+ const bullet = a.name === activeAgentName ? '▶ ' : ' · ';
659
+ const model = a.model ? ` ─ \`${a.model.providerID}/${a.model.modelID}\`` : '';
618
660
  const desc = a.description ? `. ${a.description}` : '';
619
- return '**' + a.name + '**' + mode + model + desc;
661
+ return bullet + '**' + a.name + '**' + model + desc;
620
662
  });
621
- await sendLongMessage(token, chatId, 'Agents:\n' + lines.join('\n'));
663
+ await sendLongMessage(token, chatId, 'Primary agents:\n' + lines.join('\n'));
622
664
  } catch (err) {
623
665
  await sendLongMessage(token, chatId, `Error listing agents: ${err.message}`);
624
666
  }
@@ -664,6 +706,88 @@ async function cmdModels(token, chatId, args, setServer, setActiveSession, serve
664
706
 
665
707
  const PERM_RESPONSE_HELP = ' No pending permission. Use `/status` to check the current state.';
666
708
 
709
+ // ---------------------------------------------------------------------------
710
+ // Agentp gateway ─ tiny HTTP server for agentp --tg
711
+ // ---------------------------------------------------------------------------
712
+
713
+ function startAgentpGateway(token, chatIdResolver, serverBaseResolver) {
714
+ return new Promise((resolve) => {
715
+ const server = http.createServer((req, res) => {
716
+ try {
717
+ if (req.method !== 'POST' || req.url !== '/send') {
718
+ res.writeHead(405);
719
+ res.end('Method Not Allowed');
720
+ return;
721
+ }
722
+ if (!validateAgentpAuth(req)) {
723
+ res.writeHead(401);
724
+ res.end('Unauthorized');
725
+ return;
726
+ }
727
+ let body = '';
728
+ req.on('data', (chunk) => { body += chunk; });
729
+ req.on('end', () => {
730
+ try {
731
+ let data;
732
+ try { data = JSON.parse(body); } catch {
733
+ res.writeHead(400);
734
+ res.end('Bad Request');
735
+ return;
736
+ }
737
+ const { text, server } = data;
738
+ if (typeof text !== 'string' || typeof server !== 'string') {
739
+ res.writeHead(400);
740
+ res.end('Bad Request: text and server are required');
741
+ return;
742
+ }
743
+ res.writeHead(200);
744
+ res.end('OK');
745
+
746
+ const chatId = chatIdResolver();
747
+ if (!chatId) return;
748
+ const activeUrl = normalizeUrl(serverBaseResolver());
749
+ const targetUrl = normalizeUrl(server);
750
+
751
+ if (targetUrl === activeUrl) {
752
+ sendLongMessage(token, chatId, text.trim()).catch(() => {});
753
+ } else {
754
+ if (!agentpQueues[server]) {
755
+ agentpQueues[server] = { messages: [], notifyTimer: null, notified: false };
756
+ }
757
+ const q = agentpQueues[server];
758
+ q.messages.push(text);
759
+ if (!q.notifyTimer) {
760
+ q.notifyTimer = setTimeout(() => {
761
+ q.notifyTimer = null;
762
+ if (q.messages.length > 0 && !q.notified) {
763
+ q.notified = true;
764
+ sendLongMessage(token, chatId,
765
+ `📥 ${q.messages.length} agentp message(s) queued for server \`${server}\`. Switch servers to view them.`)
766
+ .catch(() => {});
767
+ }
768
+ }, TGAGENTP_DEBOUNCE_MS);
769
+ }
770
+ }
771
+ } catch (err) {
772
+ log.error(`Agentp gateway handler error: ${err.message}`);
773
+ }
774
+ });
775
+ } catch (err) {
776
+ log.error(`Agentp gateway handler error: ${err.message}`);
777
+ }
778
+ });
779
+ server.on('error', (err) => {
780
+ log.error(`Agentp gateway server error: ${err.message}`);
781
+ });
782
+ server.listen(TGAGENTP_PORT, '127.0.0.1', () => {
783
+ const port = server.address().port;
784
+ try { fs.writeFileSync(PORT_FILE, String(port)); } catch (_) {}
785
+ log.info(`Agentp gateway listening on port ${port}`);
786
+ resolve(port);
787
+ });
788
+ });
789
+ }
790
+
667
791
  async function cmdAllow(token, chatId, args, setServer, setActiveSession, server) {
668
792
  const st = getState(server);
669
793
  if (!st.pendingPermission) {
@@ -797,10 +921,15 @@ async function main() {
797
921
  console.error('');
798
922
  console.error('Environment variables:');
799
923
  console.error(' TELEGRAM_BOT_TOKEN Telegram bot token (required)');
800
- console.error(' OPENCODE_SERVER_PASSWORD OpenCode server password (optional)');
924
+ console.error(' OPENCODE_SERVER_PASSWORD OpenCode server password (optional, also used for agentp gateway auth)');
801
925
  console.error(' OPENCODE_SERVER_USERNAME OpenCode server username (optional, default: opencode)');
802
926
  console.error(' TGAGENTP_ALLOWED_CHAT_IDS Comma-separated Telegram chat IDs (optional)');
803
927
  console.error(' Example: "123456,-789012"');
928
+ console.error(' TGAGENTP_PORT Agentp gateway listen port (optional, default: random)');
929
+ console.error(' TGAGENTP_DEBOUNCE_MS Debounce interval for queued agentp messages (optional, default: 5000)');
930
+ console.error('');
931
+ console.error('Also starts an agentp gateway (tiny HTTP server on localhost) that');
932
+ console.error('accepts POST /send from agentp --tg and forwards answers to Telegram.');
804
933
  console.error('');
805
934
  console.error('See also: agentp, ocmux');
806
935
  console.error('');
@@ -859,10 +988,19 @@ async function main() {
859
988
 
860
989
  log.info('Waiting for Telegram messages... (Ctrl+C to stop)');
861
990
 
991
+ // Start agentp gateway (tiny HTTP server for agentp --tg)
992
+ const chatIdResolver = () => lockedChatId;
993
+ const serverBaseResolver = () => serverBase;
994
+ startAgentpGateway(token, chatIdResolver, serverBaseResolver);
995
+
862
996
  const setServer = async (url) => {
863
997
  serverBase = url;
864
998
  getState(url);
865
999
  log.info(`Switched to server: ${url}`);
1000
+
1001
+ // Flush agentp queue for the new active server
1002
+ flushAgentpQueue(url, token, lockedChatId);
1003
+
866
1004
  const [discovered, agents] = await Promise.all([
867
1005
  discoverActiveSession(url),
868
1006
  listAgents(url).catch(() => []),
@@ -894,18 +1032,27 @@ async function main() {
894
1032
  const st = getState(url);
895
1033
  const isCancelled = () => getState(url).taskId !== myTaskId;
896
1034
  const cancelRef = { current: null };
1035
+ const sessionCancelRef = { current: null };
897
1036
  st._cancelRef = cancelRef;
1037
+ st._sessionReq = sessionCancelRef;
898
1038
  try {
899
1039
  let answer;
900
1040
  if (useSession) {
901
- // Event-driven flow: start SSE listener first, then send message async
1041
+ // Dual approach:
1042
+ // 1. SSE for permissions/thinking events (connected before sending)
1043
+ // 2. Synchronous sendToSession (POST /session/:id/message) for reliable answer
902
1044
  const notifiedPerms = new Set();
903
1045
  let thinkingBuf = '';
904
- const ssePromise = listenForSessionEvents(url, sessionId, {
1046
+ let sseConnected;
1047
+ const onConnected = Promise.race([
1048
+ new Promise((resolve) => { sseConnected = resolve; }),
1049
+ new Promise((resolve) => setTimeout(resolve, 5000)),
1050
+ ]);
1051
+ listenForSessionEvents(url, sessionId, {
905
1052
  onText: null,
1053
+ onConnected: sseConnected,
906
1054
  onThinking: (chunk) => {
907
1055
  if (!getState(url).thinkingEnabled) { thinkingBuf = ''; return; }
908
- // Forward thinking chunks in real-time
909
1056
  thinkingBuf += chunk;
910
1057
  if (thinkingBuf.length >= 50 || chunk.endsWith('\n')) {
911
1058
  const html = '💭 ' + thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -942,23 +1089,18 @@ async function main() {
942
1089
  log.debug(` → permission_replied: clearing pendingPermission`);
943
1090
  getState(url).pendingPermission = null;
944
1091
  },
945
- }, cancelRef);
946
- // Send message (SSE is already listening — no race)
947
- try {
948
- await sendToSessionAsync(url, sessionId, `[Telegram] ${text}`, st.activeAgentName);
949
- } catch (err) {
950
- // Abort SSE listener on send failure to avoid resource leak
951
- if (cancelRef.current) { cancelRef.current.destroy(); cancelRef.current = null; }
952
- throw err;
953
- }
954
- // Wait for the SSE listener to collect the full answer
955
- answer = await ssePromise;
1092
+ }, cancelRef).catch(() => {});
1093
+ // Wait for SSE to connect, then send synchronously
1094
+ await onConnected;
1095
+ answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
956
1096
  // Flush any remaining thinking buffer
957
1097
  if (thinkingBuf) {
958
1098
  const html = '💭 ' + thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
959
1099
  sendLongMessage(token, chatId, html, replyTo).catch(() => {});
960
1100
  thinkingBuf = '';
961
1101
  }
1102
+ // Clean up SSE listener
1103
+ if (cancelRef.current) { cancelRef.current.destroy(); cancelRef.current = null; }
962
1104
  } else {
963
1105
  await sendText(url, `[Telegram] ${text}`);
964
1106
  answer = await listenForFinalAnswer(url, null, cancelRef);
@@ -978,6 +1120,8 @@ async function main() {
978
1120
  await sendLongMessage(token, chatId, `✅ Server \`${url}\` finished processing. Switch back to see the response.`, replyTo);
979
1121
  }
980
1122
  } catch (err) {
1123
+ // Clean up SSE listener if it's still active
1124
+ if (cancelRef.current) { cancelRef.current.destroy(); cancelRef.current = null; }
981
1125
  if (isCancelled()) {
982
1126
  log.debug(` → cancelled, ignoring error from ${url}: ${err.message}`);
983
1127
  st.pendingPermission = null;
@@ -990,6 +1134,7 @@ async function main() {
990
1134
  } finally {
991
1135
  st.pendingPermission = null;
992
1136
  if (st._cancelRef === cancelRef) st._cancelRef = null;
1137
+ if (st._sessionReq === sessionCancelRef) st._sessionReq = null;
993
1138
  if (!isCancelled()) {
994
1139
  getState(url).busy = false;
995
1140
  }
@@ -1009,6 +1154,11 @@ async function main() {
1009
1154
  st._cancelRef.current.destroy();
1010
1155
  st._cancelRef.current = null;
1011
1156
  }
1157
+ // Abort the synchronous session request if in progress
1158
+ if (st._sessionReq && st._sessionReq.current) {
1159
+ st._sessionReq.current.destroy();
1160
+ st._sessionReq.current = null;
1161
+ }
1012
1162
  await sendLongMessage(token, chatId, '✅ Cancelled.');
1013
1163
  log.debug(` → /cancel on ${serverBase}`);
1014
1164
  }
@@ -1062,6 +1212,7 @@ async function main() {
1062
1212
  lines.push('**Session:** ' + sessionLabel(st));
1063
1213
  lines.push('**Agent:** ' + (st.activeAgentName ? '`' + st.activeAgentName + '`' : '*default*'));
1064
1214
  try { await sendLongMessage(token, savedChatId, lines.join('\n')); } catch (_) {}
1215
+ lockedChatId = savedChatId;
1065
1216
  try { fs.unlinkSync(STARTUP_CHAT_FILE); } catch (_) {}
1066
1217
  }
1067
1218
 
@@ -1139,7 +1290,7 @@ async function main() {
1139
1290
  continue;
1140
1291
  }
1141
1292
 
1142
- // Non-command message — process in background
1293
+ // Non-command message ─ process in background
1143
1294
  const st = getState(serverBase);
1144
1295
  if (st.busy) {
1145
1296
  await sendLongMessage(token, chatId, '⏳ Server is busy processing a previous message. Please wait for it to finish, or use /cancel to cancel it.');