agentp 0.12.0 → 1.12.1

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/README.md CHANGED
@@ -22,6 +22,8 @@ It is designed for prompt-driven workflows where you want to do things like:
22
22
 
23
23
  These tools are built for my own daily workflow. They are heavily AI-assisted — including the tests — and I review things before shipping, but the real test is using them every day. Bugs happen; I value a working feature more than a flawless one. MIT license, no warranty. Issues, suggestions, and PRs are welcome.
24
24
 
25
+ **Stability note:** `agentp` and `ocmux` are stable and used daily. `tgagentp` is still in a highly experimental stage — expect breaking changes and occasional bugs.
26
+
25
27
  ## Install
26
28
 
27
29
  From npm:
@@ -57,6 +59,7 @@ agentp [options] [url]
57
59
  Options:
58
60
 
59
61
  - `--qa`: print the original prompt and answer with labels (useful when used as a filter)
62
+ - `--defer`: deferred execution — submit prompt, get immediate ticket, retrieve result later
60
63
  - `--tg`: forward the answer to Telegram via tgagentp gateway (error if unreachable)
61
64
  - `--no-tg`: do not forward to Telegram
62
65
  - `--flush`: flush tgagentp's recorded buffer without prepending it to output
@@ -142,6 +145,39 @@ From Vim/Neovim, flush the recorded buffer without prepending context:
142
145
  Useful when you've finished a conversation thread and want to reset the recorded
143
146
  context for a new topic.
144
147
 
148
+ Deferred execution with `--defer`:
149
+
150
+ Submit a prompt and get an immediate ticket to retrieve the result later:
151
+
152
+ ```bash
153
+ # Submit a prompt and get a deferred reference
154
+ DEFERRED=$(printf "Refactor the authentication module" | agentp --defer)
155
+ # Output: <agentp-deferred>/tmp/agentp_deferred_20260803_1430_a1b2.tmp</agentp-deferred>
156
+
157
+ # Continue working... retrieve the result when ready
158
+ printf '%s\n' "$DEFERRED" | agentp --defer
159
+ # Output: (the agent's response)
160
+ ```
161
+
162
+ Works as a Vim/Neovim filter with deferred execution:
163
+
164
+ ```vim
165
+ " Submit selection, get ticket immediately, continue editing
166
+ :'<,'>!agentp --defer --qa
167
+
168
+ " Later, retrieve the result
169
+ :r !printf '%s\n' "<agentp-deferred>/tmp/agentp_deferred_...tmp</agentp-deferred>" | agentp --defer
170
+ ```
171
+
172
+ The deferred workflow:
173
+ 1. Submit prompt with `--defer` → get immediate ticket (`<agentp-deferred>...</agentp-deferred>`)
174
+ 2. Continue working (the agent processes in background)
175
+ 3. When ready, pipe the ticket back to `agentp --defer` to retrieve the result
176
+ 4. If the agent is still processing, you get your input back (identity filter)
177
+ 4. If complete, you get the agent's response and the temp file is cleaned up
178
+
179
+ Useful for long-running tasks where you don't want to block your editor.
180
+
145
181
  Retrieve the last 3 assistant answers from session history:
146
182
 
147
183
  ```bash
package/bin/agentp CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  const http = require('http');
4
4
  const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const crypto = require('crypto');
5
8
  const readline = require('readline');
6
9
  const child_process = require('child_process');
7
10
  const { version } = require('../package.json');
@@ -60,6 +63,51 @@ function notifyAgentpGateway(port, server, text) {
60
63
  });
61
64
  }
62
65
 
66
+ function isFileLocked(filePath) {
67
+ const lockPath = filePath + '.lock';
68
+ try {
69
+ if (!fs.existsSync(lockPath)) return false;
70
+ const pid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
71
+ if (isNaN(pid)) return false;
72
+ // Check if process is still alive
73
+ try {
74
+ process.kill(pid, 0);
75
+ return true; // Process exists, file is locked
76
+ } catch (err) {
77
+ // Process doesn't exist, lock is stale
78
+ fs.unlinkSync(lockPath);
79
+ return false;
80
+ }
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ function lockFile(filePath) {
87
+ const lockPath = filePath + '.lock';
88
+ fs.writeFileSync(lockPath, String(process.pid));
89
+ }
90
+
91
+ function unlockFile(filePath) {
92
+ const lockPath = filePath + '.lock';
93
+ try {
94
+ fs.unlinkSync(lockPath);
95
+ } catch {
96
+ // Ignore errors
97
+ }
98
+ }
99
+
100
+ function generateTempFilename() {
101
+ const now = new Date();
102
+ const timestamp = now.getFullYear().toString() +
103
+ String(now.getMonth() + 1).padStart(2, '0') +
104
+ String(now.getDate()).padStart(2, '0') + '_' +
105
+ String(now.getHours()).padStart(2, '0') +
106
+ String(now.getMinutes()).padStart(2, '0');
107
+ const hash = crypto.randomBytes(2).toString('hex').slice(0, 4);
108
+ return path.join(os.tmpdir(), `agentp_deferred_${timestamp}_${hash}.tmp`);
109
+ }
110
+
63
111
  async function readAllStdin() {
64
112
  let text = '';
65
113
  const rl = readline.createInterface({ input: process.stdin });
@@ -78,6 +126,10 @@ async function main() {
78
126
  let serverBase = 'http://localhost:4096';
79
127
  let qaMode = false;
80
128
  let flushMode = false;
129
+ let deferMode = false;
130
+ let deferChildMode = false;
131
+ let promptFile = null;
132
+ let outputFile = null;
81
133
  let sessionName = null;
82
134
  let newSession = false;
83
135
  // tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
@@ -96,6 +148,7 @@ async function main() {
96
148
  console.error('Options:');
97
149
  console.error(' --version Show version');
98
150
  console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
151
+ console.error(' --defer Deferred execution: write prompt to temp file, return path immediately');
99
152
  console.error(' --tg Forward the answer to Telegram via agentp gateway (error if unavailable)');
100
153
  console.error(' --no-tg Do not forward to Telegram');
101
154
  console.error(' --flush Flush tgagentp\'s recorded buffer without prepending it to output');
@@ -139,6 +192,16 @@ async function main() {
139
192
  process.exit(0);
140
193
  } else if (args[i] === '--qa') {
141
194
  qaMode = true;
195
+ } else if (args[i] === '--defer') {
196
+ deferMode = true;
197
+ } else if (args[i] === '--defer-child') {
198
+ deferChildMode = true;
199
+ } else if (args[i] === '--prompt-file') {
200
+ i++;
201
+ promptFile = args[i];
202
+ } else if (args[i] === '--output-file') {
203
+ i++;
204
+ outputFile = args[i];
142
205
  } else if (args[i] === '--flush') {
143
206
  flushMode = true;
144
207
  } else if (args[i] === '--session') {
@@ -289,113 +352,239 @@ async function main() {
289
352
 
290
353
  const promptText = await readAllStdin();
291
354
 
292
- // Find most recent session or create one
293
- let sessions;
294
- try {
295
- sessions = await listSessions(serverBase);
296
- } catch (err) {
297
- console.error('Error connecting to server:', err.message);
298
- process.exit(1);
355
+ // Handle --defer mode
356
+ if (deferMode) {
357
+ // Check if stdin is a deferred reference
358
+ const deferredMatch = promptText.match(/<agentp-deferred>(.*?)<\/agentp-deferred>/);
359
+
360
+ if (deferredMatch) {
361
+ // This is a deferred reference - retrieve the result
362
+ const deferredPath = deferredMatch[1].trim();
363
+
364
+ if (!fs.existsSync(deferredPath)) {
365
+ // File doesn't exist - error
366
+ process.stdout.write(`Error: deferred file not found: ${deferredPath}\n`);
367
+ process.exit(1);
368
+ }
369
+
370
+ // Check if file is locked (still processing) or empty (not ready yet)
371
+ const isLocked = isFileLocked(deferredPath);
372
+ let content = '';
373
+ try {
374
+ content = fs.readFileSync(deferredPath, 'utf8');
375
+ } catch {
376
+ content = '';
377
+ }
378
+
379
+ if (isLocked || content === '') {
380
+ // File is locked (still processing) or empty (not ready yet) - identity filter
381
+ process.stdout.write(promptText);
382
+ process.exit(0);
383
+ }
384
+
385
+ // File exists, not locked, and has content - read and delete
386
+ try {
387
+ fs.unlinkSync(deferredPath);
388
+ process.stdout.write(content);
389
+ process.exit(0);
390
+ } catch (err) {
391
+ process.stdout.write(`Error reading deferred file: ${err.message}\n`);
392
+ process.exit(1);
393
+ }
394
+ } else {
395
+ // This is a regular prompt - create deferred execution
396
+ const tempPath = generateTempFilename();
397
+ const promptTempPath = tempPath + '.prompt';
398
+
399
+ // Write prompt to temp file for child process
400
+ fs.writeFileSync(promptTempPath, promptText);
401
+
402
+ // Create and lock the output file
403
+ fs.writeFileSync(tempPath, '');
404
+ lockFile(tempPath);
405
+
406
+ // Output the deferred reference
407
+ process.stdout.write(`<agentp-deferred>${tempPath}</agentp-deferred>\n`);
408
+
409
+ // Spawn detached child process
410
+ const childArgs = [
411
+ __filename,
412
+ '--defer-child',
413
+ '--prompt-file', promptTempPath,
414
+ '--output-file', tempPath,
415
+ ];
416
+
417
+ // Pass through other flags
418
+ if (qaMode) childArgs.push('--qa');
419
+ if (tgMode === true) childArgs.push('--tg');
420
+ if (tgMode === false) childArgs.push('--no-tg');
421
+ if (flushMode) childArgs.push('--flush');
422
+ if (sessionName) {
423
+ childArgs.push('--session', sessionName);
424
+ if (newSession) childArgs.push('--new');
425
+ }
426
+ childArgs.push(serverBase);
427
+
428
+ const child = child_process.spawn(process.execPath, childArgs, {
429
+ detached: true,
430
+ stdio: 'ignore',
431
+ });
432
+ child.unref();
433
+
434
+ process.exit(0);
435
+ }
299
436
  }
300
437
 
301
- let sessionId = null;
438
+ // Handle --defer-child mode (child process)
439
+ let actualPromptText = promptText;
440
+ if (deferChildMode && promptFile) {
441
+ try {
442
+ actualPromptText = fs.readFileSync(promptFile, 'utf8');
443
+ fs.unlinkSync(promptFile);
444
+ } catch (err) {
445
+ const errMsg = `Error reading prompt file: ${err.message}\n`;
446
+ if (outputFile) { fs.writeFileSync(outputFile, errMsg); unlockFile(outputFile); }
447
+ process.stdout.write(errMsg);
448
+ process.exit(1);
449
+ }
450
+ }
302
451
 
303
- if (sessionName) {
304
- // Find session by name
305
- const matches = sessions.filter(s => {
306
- const title = s.title || s.slug || s.id;
307
- return title.toLowerCase() === sessionName.toLowerCase();
308
- });
309
- if (matches.length === 0) {
310
- // Try partial match
311
- const partial = sessions.filter(s => {
452
+ // Wrap main processing in try-catch for deferChildMode to write errors to output file
453
+ async function runMain() {
454
+ // Find most recent session or create one
455
+ let sessions;
456
+ try {
457
+ sessions = await listSessions(serverBase);
458
+ } catch (err) {
459
+ throw new Error(`Error connecting to server: ${err.message}`);
460
+ }
461
+
462
+ let sessionId = null;
463
+
464
+ if (sessionName) {
465
+ // Find session by name
466
+ const matches = sessions.filter(s => {
312
467
  const title = s.title || s.slug || s.id;
313
- return title.toLowerCase().includes(sessionName.toLowerCase());
468
+ return title.toLowerCase() === sessionName.toLowerCase();
314
469
  });
315
- if (partial.length === 1) {
316
- sessionId = partial[0].id;
317
- } else if (partial.length > 1) {
318
- console.error(`Error: multiple sessions match "${sessionName}":`);
319
- partial.forEach(s => console.error(` - ${s.title || s.slug || s.id}`));
320
- process.exit(1);
321
- } else if (newSession) {
322
- // Create new session
323
- try {
324
- const session = await createSession(serverBase, sessionName);
325
- sessionId = session.id;
326
- await selectSession(serverBase, sessionId);
327
- } catch (err) {
328
- console.error('Error creating session:', err.message);
329
- process.exit(1);
470
+ if (matches.length === 0) {
471
+ // Try partial match
472
+ const partial = sessions.filter(s => {
473
+ const title = s.title || s.slug || s.id;
474
+ return title.toLowerCase().includes(sessionName.toLowerCase());
475
+ });
476
+ if (partial.length === 1) {
477
+ sessionId = partial[0].id;
478
+ } else if (partial.length > 1) {
479
+ throw new Error(`Error: multiple sessions match "${sessionName}"`);
480
+ } else if (newSession) {
481
+ // Create new session
482
+ try {
483
+ const session = await createSession(serverBase, sessionName);
484
+ sessionId = session.id;
485
+ await selectSession(serverBase, sessionId);
486
+ } catch (err) {
487
+ throw new Error(`Error creating session: ${err.message}`);
488
+ }
489
+ } else {
490
+ throw new Error(`Error: no session found matching "${sessionName}"`);
330
491
  }
331
492
  } else {
332
- console.error(`Error: no session found matching "${sessionName}"`);
333
- console.error('Use --new to create a new session with this name');
334
- process.exit(1);
493
+ sessionId = matches[0].id;
494
+ }
495
+ } else if (sessions && sessions.length > 0) {
496
+ const sorted = sessions
497
+ .filter(s => s.time && (s.time.updated || s.time.created))
498
+ .sort((a, b) => {
499
+ const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
500
+ const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
501
+ return bTime - aTime;
502
+ });
503
+ if (sorted.length > 0) sessionId = sorted[0].id;
504
+ }
505
+
506
+ if (!sessionId) {
507
+ try {
508
+ const session = await createSession(serverBase, 'agentp');
509
+ sessionId = session.id;
510
+ } catch (err) {
511
+ throw new Error(`Error creating session: ${err.message}`);
335
512
  }
336
- } else {
337
- sessionId = matches[0].id;
338
513
  }
339
- } else if (sessions && sessions.length > 0) {
340
- const sorted = sessions
341
- .filter(s => s.time && (s.time.updated || s.time.created))
342
- .sort((a, b) => {
343
- const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
344
- const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
345
- return bTime - aTime;
346
- });
347
- if (sorted.length > 0) sessionId = sorted[0].id;
348
- }
349
514
 
350
- if (!sessionId) {
515
+ let answer;
351
516
  try {
352
- const session = await createSession(serverBase, 'agentp');
353
- sessionId = session.id;
517
+ answer = await sendToSession(serverBase, sessionId, actualPromptText);
354
518
  } catch (err) {
355
- console.error('Error creating session:', err.message);
356
- process.exit(1);
519
+ throw new Error(`Error sending prompt: ${err.message}`);
520
+ }
521
+
522
+ // Build the message to send to Telegram (QA pair with rulers)
523
+ let tgMessage = answer;
524
+ if (qaMode) {
525
+ tgMessage = HUMAN_ROW + actualPromptText + AGENT_ROW + answer;
526
+ if (!answer.endsWith('\n')) tgMessage += '\n';
527
+ tgMessage += FINAL_ROW;
357
528
  }
529
+
530
+ // Post-send: notify tgagentp, get recorded context back
531
+ let buffered = [];
532
+ let tgError = null;
533
+ if (tgPort) {
534
+ try {
535
+ buffered = await notifyAgentpGateway(tgPort, serverBase, tgMessage);
536
+ } catch (err) {
537
+ tgError = err;
538
+ }
539
+ }
540
+
541
+ // Build output (prepend recorded context if any, unless --flush)
542
+ let output = '';
543
+ if (qaMode) {
544
+ if (!flushMode && buffered.length > 0) {
545
+ for (const msg of buffered) {
546
+ const row = msg.role === 'user' ? HUMAN_ROW : AGENT_ROW;
547
+ output += row + msg.text + '\n';
548
+ }
549
+ }
550
+ output += tgMessage;
551
+ } else {
552
+ output = tgMessage;
553
+ if (!tgMessage.endsWith('\n')) output += '\n';
554
+ }
555
+
556
+ return output;
358
557
  }
359
558
 
360
- let answer;
559
+ // Execute main processing with error handling for child mode
560
+ let output = '';
361
561
  try {
362
- answer = await sendToSession(serverBase, sessionId, promptText);
562
+ output = await runMain();
363
563
  } catch (err) {
364
- console.error('Error sending prompt:', err.message);
564
+ const errMsg = err.message + '\n';
565
+ if (deferChildMode && outputFile) {
566
+ fs.writeFileSync(outputFile, errMsg);
567
+ unlockFile(outputFile);
568
+ } else {
569
+ process.stdout.write(errMsg);
570
+ }
365
571
  process.exit(1);
366
572
  }
367
573
 
368
- // Build the message to send to Telegram (QA pair with rulers)
369
- let tgMessage = answer;
370
- if (qaMode) {
371
- tgMessage = HUMAN_ROW + promptText + AGENT_ROW + answer;
372
- if (!answer.endsWith('\n')) tgMessage += '\n';
373
- tgMessage += FINAL_ROW;
374
- }
375
-
376
- // Post-send: notify tgagentp, get recorded context back
377
- let buffered = [];
378
- let tgError = null;
379
- if (tgPort) {
574
+ // Write output to file (child mode) or stdout (normal mode)
575
+ let finalOutput = output;
576
+ if (deferChildMode && outputFile) {
380
577
  try {
381
- buffered = await notifyAgentpGateway(tgPort, serverBase, tgMessage);
578
+ fs.writeFileSync(outputFile, output);
579
+ unlockFile(outputFile);
382
580
  } catch (err) {
383
- tgError = err;
384
- }
385
- }
386
-
387
- // Build stdout output (prepend recorded context if any, unless --flush)
388
- if (qaMode) {
389
- if (!flushMode && buffered.length > 0) {
390
- for (const msg of buffered) {
391
- const row = msg.role === 'user' ? HUMAN_ROW : AGENT_ROW;
392
- process.stdout.write(row + msg.text + '\n');
393
- }
581
+ const errMsg = `Error writing output file: ${err.message}\n`;
582
+ try { fs.writeFileSync(outputFile, errMsg); } catch {}
583
+ unlockFile(outputFile);
584
+ process.exit(1);
394
585
  }
395
- process.stdout.write(tgMessage);
396
586
  } else {
397
- process.stdout.write(tgMessage);
398
- if (!tgMessage.endsWith('\n')) process.stdout.write('\n');
587
+ process.stdout.write(output);
399
588
  }
400
589
 
401
590
  // Warnings at the end (stderr)
package/bin/tgagentp CHANGED
@@ -1017,20 +1017,38 @@ async function processTelegramCommands(text, server, token, chatState) {
1017
1017
  const filesSent = [];
1018
1018
  const dir = serverDir(server);
1019
1019
 
1020
+ // Resolve a file path: absolute paths used directly, relative paths resolved
1021
+ // against the server's working directory. Returns null and sends an error
1022
+ // message if the resolved path is outside the server's working directory.
1023
+ function resolveUploadPath(p, errPrefix) {
1024
+ const absolute = path.isAbsolute(p) ? p : path.resolve(dir || '', p);
1025
+ if (!dir) return absolute; // no server dir — trust the path
1026
+ let realDir, realPath;
1027
+ try { realDir = fs.realpathSync(dir); } catch { realDir = dir; }
1028
+ try { realPath = fs.realpathSync(absolute); } catch { realPath = absolute; }
1029
+ if (!realPath.startsWith(realDir + path.sep) && realPath !== realDir) {
1030
+ sendLongMessage(token, chatState,
1031
+ `❌ ${errPrefix}: path outside project directory.\n` +
1032
+ ` Path: \`${p}\`\n` +
1033
+ ` Project: \`${realDir}\`\n\n` +
1034
+ `Use a full path (starting with /) or a path relative to the project root.`).catch(() => {});
1035
+ return null;
1036
+ }
1037
+ return absolute;
1038
+ }
1039
+
1020
1040
  for (let i = 0; i < lines.length; i++) {
1021
1041
  const payload = parseTelegramLine(lines[i]);
1022
1042
  if (!payload) continue;
1023
1043
 
1024
1044
  if (payload.command === 'upload' && payload.path) {
1025
1045
  toStrip.add(i);
1026
- const resolved = dir ? path.resolve(dir, payload.path) : path.resolve(payload.path);
1027
- if (dir && !resolved.startsWith(dir + path.sep)) {
1028
- log.error(`processTelegramCommands: path outside project rejected: ${payload.path}`);
1029
- continue;
1030
- }
1046
+ const resolved = resolveUploadPath(payload.path, 'Upload');
1047
+ if (!resolved) continue;
1031
1048
  try {
1032
1049
  if (!fs.existsSync(resolved)) {
1033
- log.error(`processTelegramCommands: file not found: ${resolved}`);
1050
+ await sendLongMessage(token, chatState,
1051
+ `❌ File not found: \`${resolved}\``);
1034
1052
  continue;
1035
1053
  }
1036
1054
  const fileData = fs.readFileSync(resolved);
@@ -1039,22 +1057,21 @@ async function processTelegramCommands(text, server, token, chatState) {
1039
1057
  await sendTelegramDocument(token, chatState, fileData, fileName, caption);
1040
1058
  filesSent.push(fileName);
1041
1059
  } catch (err) {
1042
- log.error(`processTelegramCommands: failed to send file ${resolved}: ${err.message}`);
1060
+ await sendLongMessage(token, chatState,
1061
+ `❌ Failed to upload \`${resolved}\`: ${err.message}`);
1043
1062
  }
1044
1063
  } else if (payload.command === 'download' && payload.fileId && payload.path) {
1045
1064
  toStrip.add(i);
1046
- const resolved = dir ? path.resolve(dir, payload.path) : path.resolve(payload.path);
1047
- if (dir && !resolved.startsWith(dir + path.sep)) {
1048
- log.error(`processTelegramCommands: download path outside project rejected: ${payload.path}`);
1049
- continue;
1050
- }
1065
+ const resolved = resolveUploadPath(payload.path, 'Download');
1066
+ if (!resolved) continue;
1051
1067
  try {
1052
1068
  const fileData = await downloadTelegramFile(token, payload.fileId);
1053
1069
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
1054
1070
  fs.writeFileSync(resolved, fileData);
1055
1071
  filesSent.push(path.basename(resolved));
1056
1072
  } catch (err) {
1057
- log.error(`processTelegramCommands: failed to download file: ${err.message}`);
1073
+ await sendLongMessage(token, chatState,
1074
+ `❌ Failed to download file to \`${resolved}\`: ${err.message}`);
1058
1075
  }
1059
1076
  } else if (payload.command === 'help') {
1060
1077
  toStrip.add(i);
@@ -2154,8 +2171,13 @@ async function main() {
2154
2171
  'Do NOT use CLI tools to generate them — just include them directly in your response:\n\n' +
2155
2172
  'To send a file to the user:\n' +
2156
2173
  '[Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
2174
+ '**Important:** `path` is relative to the project root (the OpenCode server\'s working directory).\n' +
2175
+ 'If the project uses a git worktree, the worktree directory IS the project root.\n' +
2176
+ 'Use CLI commands like `cat ./path/to/file` to verify the file exists before uploading.\n\n' +
2157
2177
  'To download a file the user sent:\n' +
2158
2178
  '[Telegram]{"command":"download","fileId":"<id>","path":"<relative-destination>"}\n\n' +
2179
+ '**Important:** `path` is also relative to the project root (the OpenCode server\'s working directory).\n' +
2180
+ 'Use `ls` or `pwd` to confirm the current working directory before specifying download paths.\n\n' +
2159
2181
  'For more details about a command:\n' +
2160
2182
  '[Telegram]{"command":"help","topic":"<upload|download|help>"}';
2161
2183
 
@@ -2174,6 +2196,7 @@ async function main() {
2174
2196
  const monRef = { current: null };
2175
2197
  st._monitorRef = monRef;
2176
2198
  st._monitorStarted = true;
2199
+ st._monitoredSessionId = sessionId;
2177
2200
 
2178
2201
  const notifiedPerms = new Set();
2179
2202
 
@@ -2239,6 +2262,7 @@ async function main() {
2239
2262
  if (getState(server)._monitorRef === monRef) {
2240
2263
  getState(server)._monitorRef = null;
2241
2264
  getState(server)._monitorStarted = false;
2265
+ getState(server)._monitoredSessionId = null;
2242
2266
  }
2243
2267
  });
2244
2268
  }
@@ -2271,6 +2295,20 @@ async function main() {
2271
2295
 
2272
2296
  async function processMessageAsync(chatId, text, url, useSession, sessionId, myTaskId, replyTo, threadId) {
2273
2297
  const st = getState(url);
2298
+ // Auto-discover active session if not set
2299
+ if (!sessionId) {
2300
+ try {
2301
+ const discovered = await discoverActiveSession(url);
2302
+ if (discovered) {
2303
+ st.activeSessionId = discovered.id;
2304
+ st.activeSessionName = discovered.name;
2305
+ sessionId = discovered.id;
2306
+ useSession = true;
2307
+ }
2308
+ } catch (err) {
2309
+ log.debug(`processMessageAsync: auto-discover error: ${err.message}`);
2310
+ }
2311
+ }
2274
2312
  const isCancelled = () => getState(url).taskId !== myTaskId;
2275
2313
  const cancelRef = { current: null };
2276
2314
  const chatState = { chatId, threadId };
@@ -2281,11 +2319,12 @@ async function main() {
2281
2319
  st._cancelRef = cancelRef;
2282
2320
  st._sessionReq = sessionCancelRef;
2283
2321
 
2284
- // Ensure the persistent session monitor is running so question/permission
2285
- // events from any source (agentp, tgagentp, TUI) are caught, not just
2286
- // those triggered by this message's per-request SSE listener.
2287
- if (useSession && sessionId && !st._monitorStarted) {
2288
- startSessionMonitor(url, sessionId);
2322
+ // Ensure the persistent session monitor is running and tracking the current session.
2323
+ // Always restart when sessionId changes so TUI-initiated session switches are followed.
2324
+ if (useSession && sessionId) {
2325
+ if (!st._monitorStarted || st._monitoredSessionId !== sessionId) {
2326
+ startSessionMonitor(url, sessionId);
2327
+ }
2289
2328
  }
2290
2329
 
2291
2330
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.12.0",
3
+ "version": "1.12.1",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",