agentp 0.7.0 → 0.8.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/AGENTS.md +14 -7
- package/CHANGELOG.md +10 -0
- package/README.md +21 -1
- package/bin/tgagentp +154 -66
- package/lib/opencode.js +10 -3
- package/package.json +1 -1
- package/status_report.md +0 -85
package/AGENTS.md
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
- **`"type": "commonjs"`**, requires Node >= 18.
|
|
15
15
|
- **Linux and macOS compatible.** All tools use portable Node.js APIs only.
|
|
16
16
|
|
|
17
|
+
## Logging conventions (tgagentp)
|
|
18
|
+
|
|
19
|
+
- **stdout** — informational messages (startup, discovery, session switches)
|
|
20
|
+
- **stderr** — errors (always shown) + trace/debug (only with `--verbose` flag)
|
|
21
|
+
- Default usage for clean console: `tgagentp 2>/dev/null`
|
|
22
|
+
- The `log` helper object at the top of `bin/tgagentp` provides `log.info()`, `log.error()`, and `log.debug()`.
|
|
23
|
+
|
|
17
24
|
## `.ocmux.json`
|
|
18
25
|
|
|
19
26
|
Runtime state file created by `bin/ocmux` (contains URL, log path, and window index). **Not in `.gitignore`** — avoid committing it.
|
|
@@ -95,15 +102,19 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
95
102
|
|
|
96
103
|
### Pending
|
|
97
104
|
|
|
98
|
-
- TUI navigation on session switch (open code API limitation — likely not possible)
|
|
99
105
|
- `--print-logs` flag for `ocmux serve`
|
|
100
106
|
|
|
101
107
|
### In Progress
|
|
102
108
|
|
|
103
|
-
|
|
109
|
+
*(none)*
|
|
104
110
|
|
|
105
111
|
### Done
|
|
106
112
|
|
|
113
|
+
- Logging rebalance: stdout for info, stderr for errors/debug/trace, `--verbose` flag, `2>/dev/null` default usage
|
|
114
|
+
- `/shutdown force` — refuses shutdown when busy unless `force` flag is given; goodbye message includes state
|
|
115
|
+
- `/think [on|off|switch]` — toggle real-time forwarding of model thinking messages to the chat; `--think` CLI flag to start enabled
|
|
116
|
+
- Event-driven permission handling (`POST /session/:id/prompt_async` + SSE listener + `/allow`, `/reject`, `/always` commands) — included in v0.7.0
|
|
117
|
+
- TUI navigation on `/sessions switch` and `/sessions new` — `POST /tui/select-session` now works in opencode 1.15.13 (was: tried second, never reached)
|
|
107
118
|
- /agents switch <name> — switch active agent on the active session
|
|
108
119
|
- /sessions new [name] — create a new session, TUI follows via /session/:id/select
|
|
109
120
|
- /sessions rename <name> — rename the current active session
|
|
@@ -111,7 +122,7 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
111
122
|
- Case-insensitive matching for all switch subcommands (/servers, /sessions, /agents)
|
|
112
123
|
- Fix: /sessions filter includes sessions with directory (TUI sessions) + active session exception
|
|
113
124
|
- Fix: /sessions now shows the active session even without agent set (exception in filter)
|
|
114
|
-
- Fix: /sessions switch and /sessions new call selectSession (TUI navigation
|
|
125
|
+
- Fix: /sessions switch and /sessions new call selectSession (TUI navigation didn't work in opencode < 1.15.x)
|
|
115
126
|
- Fix: active session highlighted with ▶ bullet in session list
|
|
116
127
|
- Fix: discover TUI session on startup and server switch via `discoverActiveSession()`
|
|
117
128
|
- Message splitting (>4096 chars, breaks at newlines)
|
|
@@ -121,10 +132,6 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
121
132
|
- /cancel with true server-side SSE abort via cancelRef
|
|
122
133
|
- /shutdown (--dev mode) with Telegram offset acknowledgment
|
|
123
134
|
|
|
124
|
-
### With Issues
|
|
125
|
-
|
|
126
|
-
- `/sessions switch` and `/sessions new` — `selectSession` tries `POST /session/:id/select` and `POST /tui/select-session` but the TUI pane doesn't navigate to the session. Message routing (via `activeSessionId` + `sendToSession`) works correctly in the background. This is a known limitation — the TUI must be navigated manually to see the conversation in the UI. The underlying opencode API may lack the needed TUI navigation endpoints.
|
|
127
|
-
|
|
128
135
|
### Verified
|
|
129
136
|
|
|
130
137
|
- Password auth support (OPENCODE_SERVER_PASSWORD) for agentp, ocmux, tgagentp
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,16 @@ All notable changes to this project will be documented in this file.
|
|
|
8
8
|
- Extract shared OpenCode client logic into `lib/opencode.js`.
|
|
9
9
|
- Add `tgagentp` — bridge a Telegram bot chat with an OpenCode TUI session.
|
|
10
10
|
|
|
11
|
+
## [0.8.0] - 2026-06-03
|
|
12
|
+
|
|
13
|
+
- Fix `/servers` crash: `serverBase is not defined` error.
|
|
14
|
+
- Logging rebalance: stdout for info, stderr for errors/debug with `--verbose` flag (`2>/dev/null` for clean console).
|
|
15
|
+
- `/shutdown force`: refuse shutdown when busy unless `force` flag given.
|
|
16
|
+
- `/think [on|off|switch]`: toggle real-time forwarding of model thinking messages; `--think` CLI flag.
|
|
17
|
+
- Reply quoting: when replying to a Telegram message, prepend quoted text with safe backtick fencing.
|
|
18
|
+
- Reply chaining: answers use `reply_to_message_id` to appear as replies.
|
|
19
|
+
- TUI navigation: `POST /tui/select-session` tried first (works in opencode 1.15.13).
|
|
20
|
+
|
|
11
21
|
## [0.1.0] - 2026-05-08
|
|
12
22
|
|
|
13
23
|
- Initial release.
|
package/README.md
CHANGED
|
@@ -192,7 +192,8 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
192
192
|
| `/models` | List connected providers with model counts, context limits, and costs |
|
|
193
193
|
| `/status` | Show current server path, URL, busy status, and active session |
|
|
194
194
|
| `/cancel` | Cancel the current AI response for the active server |
|
|
195
|
-
| `/
|
|
195
|
+
| `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
|
|
196
|
+
| `/shutdown [force]` | (requires `--dev`) Stop tgagentp; refuses if busy unless `force` is given |
|
|
196
197
|
|
|
197
198
|
### Per-server state
|
|
198
199
|
|
|
@@ -212,8 +213,27 @@ tgagentp
|
|
|
212
213
|
|
|
213
214
|
- `--version`: show version
|
|
214
215
|
- `--help`: show help message
|
|
216
|
+
- `--verbose`: show detailed logs (errors, trace, debug) on stderr
|
|
217
|
+
- `--think`: start with thinking messages enabled (default: off)
|
|
215
218
|
- `--dev`: enable `/shutdown` command for remote restart (run via `while true; do tgagentp --dev; done`)
|
|
216
219
|
|
|
220
|
+
### Logging
|
|
221
|
+
|
|
222
|
+
Informational messages (startup, discovery, session switches) go to **stdout**. Errors, warnings, and trace/debug messages go to **stderr**. With `--verbose`, detailed
|
|
223
|
+
trace/debug messages are also printed to stderr.
|
|
224
|
+
|
|
225
|
+
For a clean console with only essential info:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
tgagentp 2>/dev/null
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
To capture everything (info + errors) to a log file:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
tgagentp 2>/var/log/tgagentp.log
|
|
235
|
+
```
|
|
236
|
+
|
|
217
237
|
### Telemetry
|
|
218
238
|
|
|
219
239
|
A startup greeting is sent to the last known chat on boot — includes /status-style server info. The chat ID is persisted at `/tmp/tgagentp-startup-chat`.
|
package/bin/tgagentp
CHANGED
|
@@ -6,17 +6,13 @@ const path = require('path');
|
|
|
6
6
|
const { version } = require('../package.json');
|
|
7
7
|
const { sendText, listenForFinalAnswer, listSessions, createSession, updateSession, selectSession, sendToSession, sendToSessionAsync, respondToPermission, listenForSessionEvents, listAgents, listProviders } = require('../lib/opencode.js');
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
process.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const prefixed = lines.map(l => l ? `[${ts}] ${l}` : l).join('\n');
|
|
17
|
-
return origWrite(prefixed, encoding, cb);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
9
|
+
// Logger: info→stdout, error→stderr (always), debug→stderr (--verbose only)
|
|
10
|
+
const log = {
|
|
11
|
+
verbose: false,
|
|
12
|
+
info(msg) { process.stdout.write(`${new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '')} ${msg}\n`); },
|
|
13
|
+
error(msg) { process.stderr.write(`${msg}\n`); },
|
|
14
|
+
debug(msg) { if (this.verbose) process.stderr.write(`${msg}\n`); },
|
|
15
|
+
};
|
|
20
16
|
|
|
21
17
|
const STARTUP_CHAT_FILE = '/tmp/tgagentp-startup-chat';
|
|
22
18
|
const { listServers, activateServer } = require('../lib/ocmux.js');
|
|
@@ -63,9 +59,10 @@ function getUpdates(token, offset, timeout) {
|
|
|
63
59
|
});
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
function sendTelegramMessage(token, chatId, text, parseMode) {
|
|
62
|
+
function sendTelegramMessage(token, chatId, text, parseMode, replyTo) {
|
|
67
63
|
const params = { chat_id: chatId, text };
|
|
68
64
|
if (parseMode) params.parse_mode = parseMode;
|
|
65
|
+
if (replyTo) params.reply_to_message_id = replyTo;
|
|
69
66
|
return telegramRequest(token, 'sendMessage', params);
|
|
70
67
|
}
|
|
71
68
|
|
|
@@ -173,12 +170,32 @@ function splitMessage(text, maxLength = 4096) {
|
|
|
173
170
|
// Converts Markdown to Telegram HTML formatting.
|
|
174
171
|
// Splits the raw text first, then converts each chunk independently
|
|
175
172
|
// to avoid broken HTML tags across messages.
|
|
176
|
-
async function sendLongMessage(token, chatId, text) {
|
|
173
|
+
async function sendLongMessage(token, chatId, text, replyTo) {
|
|
177
174
|
const chunks = splitMessage(text);
|
|
178
|
-
for (
|
|
179
|
-
const html = convertMarkdownToHtml(
|
|
180
|
-
await sendTelegramMessage(token, chatId, html, 'HTML');
|
|
175
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
176
|
+
const html = convertMarkdownToHtml(chunks[i]);
|
|
177
|
+
await sendTelegramMessage(token, chatId, html, 'HTML', i === 0 ? replyTo : undefined);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build a quoted reply context from a Telegram reply_to_message.
|
|
182
|
+
// Returns empty string if the message isn't a reply or has no text.
|
|
183
|
+
// Uses a dynamic code-fence length (max consecutive backticks + 1)
|
|
184
|
+
// so nested backticks never break the structure.
|
|
185
|
+
function quoteReply(msg) {
|
|
186
|
+
const rm = msg.reply_to_message;
|
|
187
|
+
if (!rm || !rm.text) return '';
|
|
188
|
+
const replyFrom = rm.from?.username
|
|
189
|
+
? `@${rm.from.username}`
|
|
190
|
+
: [rm.from?.first_name, rm.from?.last_name].filter(Boolean).join(' ') || 'unknown';
|
|
191
|
+
const senderType = rm.from?.is_bot ? 'assistant' : 'user';
|
|
192
|
+
let maxBackticks = 0;
|
|
193
|
+
for (let i = 0, cur = 0; i < rm.text.length; i++) {
|
|
194
|
+
if (rm.text[i] === '`') { cur++; maxBackticks = Math.max(maxBackticks, cur); }
|
|
195
|
+
else cur = 0;
|
|
181
196
|
}
|
|
197
|
+
const fence = '`'.repeat(maxBackticks + 1);
|
|
198
|
+
return `In response to ${replyFrom} (${senderType}):\n\n${fence}\n${rm.text}\n${fence}\n\n`;
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
// ---------------------------------------------------------------------------
|
|
@@ -207,7 +224,8 @@ async function cmdHelp(token, chatId, args) {
|
|
|
207
224
|
}
|
|
208
225
|
if (topic === 'shutdown') {
|
|
209
226
|
await sendLongMessage(token, chatId,
|
|
210
|
-
'`/shutdown` — stop tgagentp process\n
|
|
227
|
+
'`/shutdown` — stop tgagentp process\n' +
|
|
228
|
+
'`/shutdown force` — force shutdown even if busy (in-flight work will be lost)\n\n' +
|
|
211
229
|
'Only available when tgagentp is started with `--dev` flag.\n' +
|
|
212
230
|
'Useful when running tgagentp in a restart loop:\n' +
|
|
213
231
|
'`while true; do tgagentp --dev; done`');
|
|
@@ -246,8 +264,19 @@ async function cmdHelp(token, chatId, args) {
|
|
|
246
264
|
'🔐 notification to the chat. Use one of these commands to answer.');
|
|
247
265
|
return;
|
|
248
266
|
}
|
|
267
|
+
if (topic === 'think') {
|
|
268
|
+
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' +
|
|
273
|
+
'When enabled, the model\'s internal reasoning steps are sent to the chat\n' +
|
|
274
|
+
'as they are generated, prefixed with 💭.\n' +
|
|
275
|
+
'Can also be enabled at startup with `--think` flag.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
249
278
|
await sendLongMessage(token, chatId,
|
|
250
|
-
'Unknown topic. Try `/help servers`, `/help sessions`, `/help session`, `/help agents`, `/help models`, `/help allow`, or just `/help`.');
|
|
279
|
+
'Unknown topic. Try `/help servers`, `/help sessions`, `/help session`, `/help agents`, `/help models`, `/help allow`, `/help think`, or just `/help`.');
|
|
251
280
|
return;
|
|
252
281
|
}
|
|
253
282
|
await sendLongMessage(token, chatId,
|
|
@@ -265,12 +294,13 @@ async function cmdHelp(token, chatId, args) {
|
|
|
265
294
|
'`/agents` — list available agents\n' +
|
|
266
295
|
'`/agents switch <name>` — switch active agent\n' +
|
|
267
296
|
'`/models` — list providers and models\n' +
|
|
297
|
+
'`/think` — toggle thinking message forwarding\n' +
|
|
268
298
|
'`/allow` / `/reject` / `/always` — respond to permission requests\n' +
|
|
269
299
|
'`/shutdown` — stop tgagentp (only with `--dev` flag)\n\n' +
|
|
270
300
|
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
271
301
|
}
|
|
272
302
|
|
|
273
|
-
async function cmdServers(token, chatId, args, setServer) {
|
|
303
|
+
async function cmdServers(token, chatId, args, setServer, setActiveSession, serverBase) {
|
|
274
304
|
if (args.length > 0 && args[0].toLowerCase() === 'switch') {
|
|
275
305
|
if (args.length < 2) {
|
|
276
306
|
await sendLongMessage(token, chatId,
|
|
@@ -644,7 +674,7 @@ async function cmdAllow(token, chatId, args, setServer, setActiveSession, server
|
|
|
644
674
|
try {
|
|
645
675
|
await respondToPermission(p.server, p.sessionId, p.permissionId, 'once');
|
|
646
676
|
await sendLongMessage(token, chatId, '✅ Permission approved (once).');
|
|
647
|
-
|
|
677
|
+
log.debug(` → /allow: ${p.type} ${p.pattern || ''}`);
|
|
648
678
|
} catch (err) {
|
|
649
679
|
await sendLongMessage(token, chatId, `Error: ${err.message}`);
|
|
650
680
|
}
|
|
@@ -660,7 +690,7 @@ async function cmdReject(token, chatId, args, setServer, setActiveSession, serve
|
|
|
660
690
|
try {
|
|
661
691
|
await respondToPermission(p.server, p.sessionId, p.permissionId, 'reject');
|
|
662
692
|
await sendLongMessage(token, chatId, '⛔ Permission denied.');
|
|
663
|
-
|
|
693
|
+
log.debug(` → /reject: ${p.type} ${p.pattern || ''}`);
|
|
664
694
|
} catch (err) {
|
|
665
695
|
await sendLongMessage(token, chatId, `Error: ${err.message}`);
|
|
666
696
|
}
|
|
@@ -676,12 +706,30 @@ async function cmdAlways(token, chatId, args, setServer, setActiveSession, serve
|
|
|
676
706
|
try {
|
|
677
707
|
await respondToPermission(p.server, p.sessionId, p.permissionId, 'always');
|
|
678
708
|
await sendLongMessage(token, chatId, '✅ Permission approved (always).');
|
|
679
|
-
|
|
709
|
+
log.debug(` → /always: ${p.type} ${p.pattern || ''}`);
|
|
680
710
|
} catch (err) {
|
|
681
711
|
await sendLongMessage(token, chatId, `Error: ${err.message}`);
|
|
682
712
|
}
|
|
683
713
|
}
|
|
684
714
|
|
|
715
|
+
async function cmdThink(token, chatId, args, setServer, setActiveSession, server) {
|
|
716
|
+
const st = getState(server);
|
|
717
|
+
const sub = args && args.length > 0 ? args[0].toLowerCase() : 'switch';
|
|
718
|
+
if (sub === 'on') {
|
|
719
|
+
st.thinkingEnabled = true;
|
|
720
|
+
await sendLongMessage(token, chatId, '💭 Thinking messages **on** (forwarded to chat).');
|
|
721
|
+
} else if (sub === 'off') {
|
|
722
|
+
st.thinkingEnabled = false;
|
|
723
|
+
await sendLongMessage(token, chatId, '💭 Thinking messages **off**.');
|
|
724
|
+
} else if (sub === 'switch') {
|
|
725
|
+
st.thinkingEnabled = !st.thinkingEnabled;
|
|
726
|
+
await sendLongMessage(token, chatId,
|
|
727
|
+
`💭 Thinking messages ${st.thinkingEnabled ? '**on**' : '**off**'}.`);
|
|
728
|
+
} else {
|
|
729
|
+
await sendLongMessage(token, chatId, 'Usage: `/think [on|off|switch]` (default: switch).');
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
685
733
|
const COMMANDS = {
|
|
686
734
|
'help': cmdHelp,
|
|
687
735
|
'servers': cmdServers,
|
|
@@ -689,6 +737,7 @@ const COMMANDS = {
|
|
|
689
737
|
'session': cmdSessions,
|
|
690
738
|
'agents': cmdAgents,
|
|
691
739
|
'models': cmdModels,
|
|
740
|
+
'think': cmdThink,
|
|
692
741
|
'allow': cmdAllow,
|
|
693
742
|
'reject': cmdReject,
|
|
694
743
|
'always': cmdAlways,
|
|
@@ -700,7 +749,7 @@ const COMMANDS = {
|
|
|
700
749
|
|
|
701
750
|
const serverState = {};
|
|
702
751
|
function getState(url) {
|
|
703
|
-
return serverState[url] = serverState[url] || { busy: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null };
|
|
752
|
+
return serverState[url] = serverState[url] || { busy: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, thinkingEnabled: false };
|
|
704
753
|
}
|
|
705
754
|
|
|
706
755
|
function sessionLabel(st) {
|
|
@@ -725,6 +774,10 @@ async function main() {
|
|
|
725
774
|
process.exit(0);
|
|
726
775
|
} else if (args[i] === '--dev') {
|
|
727
776
|
DEV_MODE = true;
|
|
777
|
+
} else if (args[i] === '--verbose') {
|
|
778
|
+
log.verbose = true;
|
|
779
|
+
} else if (args[i] === '--think') {
|
|
780
|
+
getState(serverBase).thinkingEnabled = true;
|
|
728
781
|
} else if (args[i] === '--help') {
|
|
729
782
|
console.error('Usage: tgagentp [options] [url]');
|
|
730
783
|
console.error('');
|
|
@@ -732,9 +785,11 @@ async function main() {
|
|
|
732
785
|
console.error('Incoming Telegram messages are forwarded to OpenCode; answers are sent back to the chat.');
|
|
733
786
|
console.error('');
|
|
734
787
|
console.error('Options:');
|
|
735
|
-
console.error(' --dev
|
|
736
|
-
console.error(' --version
|
|
737
|
-
console.error(' --
|
|
788
|
+
console.error(' --dev Enable development mode (adds /shutdown command)');
|
|
789
|
+
console.error(' --version Show version');
|
|
790
|
+
console.error(' --verbose Show detailed logs (errors, trace, debug) on stderr');
|
|
791
|
+
console.error(' --think Start with thinking messages enabled (default: off)');
|
|
792
|
+
console.error(' --help Show this help message');
|
|
738
793
|
console.error('');
|
|
739
794
|
console.error('Arguments:');
|
|
740
795
|
console.error(' url OpenCode TUI server URL or port number (default: 4096)');
|
|
@@ -769,24 +824,24 @@ async function main() {
|
|
|
769
824
|
process.exit(1);
|
|
770
825
|
}
|
|
771
826
|
|
|
772
|
-
|
|
827
|
+
log.info(`tgagentp v${version} started. OpenCode server: ${serverBase}`);
|
|
773
828
|
|
|
774
829
|
if (!process.env.OPENCODE_SERVER_PASSWORD) {
|
|
775
|
-
|
|
830
|
+
log.error('🔓 Server is not password-protected! Set OPENCODE_SERVER_PASSWORD for better security.');
|
|
776
831
|
}
|
|
777
832
|
|
|
778
833
|
const allowedChatIds = parseAllowlist(process.env.TGAGENTP_ALLOWED_CHAT_IDS);
|
|
779
834
|
if (allowedChatIds) {
|
|
780
|
-
|
|
835
|
+
log.info(`Chat allowlist: ${allowedChatIds.length} chat(s) configured`);
|
|
781
836
|
} else {
|
|
782
|
-
|
|
837
|
+
log.info('Chat allowlist: not configured (all chats accepted)');
|
|
783
838
|
}
|
|
784
839
|
|
|
785
840
|
let lockedChatId = null;
|
|
786
841
|
|
|
787
842
|
getState(serverBase);
|
|
788
843
|
|
|
789
|
-
|
|
844
|
+
log.info('Discovering server state...');
|
|
790
845
|
const [discovered, agents] = await Promise.all([
|
|
791
846
|
discoverActiveSession(serverBase),
|
|
792
847
|
listAgents(serverBase).catch(() => []),
|
|
@@ -794,20 +849,20 @@ async function main() {
|
|
|
794
849
|
if (discovered) {
|
|
795
850
|
getState(serverBase).activeSessionId = discovered.id;
|
|
796
851
|
getState(serverBase).activeSessionName = discovered.name;
|
|
797
|
-
|
|
852
|
+
log.info(` → session: ${discovered.name || discovered.id}`);
|
|
798
853
|
}
|
|
799
854
|
const defaultAgent = agents.find(a => a.mode === 'primary') || agents[0];
|
|
800
855
|
if (defaultAgent) {
|
|
801
856
|
getState(serverBase).activeAgentName = defaultAgent.name;
|
|
802
|
-
|
|
857
|
+
log.info(` → agent: ${defaultAgent.name}`);
|
|
803
858
|
}
|
|
804
859
|
|
|
805
|
-
|
|
860
|
+
log.info('Waiting for Telegram messages... (Ctrl+C to stop)');
|
|
806
861
|
|
|
807
862
|
const setServer = async (url) => {
|
|
808
863
|
serverBase = url;
|
|
809
864
|
getState(url);
|
|
810
|
-
|
|
865
|
+
log.info(`Switched to server: ${url}`);
|
|
811
866
|
const [discovered, agents] = await Promise.all([
|
|
812
867
|
discoverActiveSession(url),
|
|
813
868
|
listAgents(url).catch(() => []),
|
|
@@ -815,12 +870,12 @@ async function main() {
|
|
|
815
870
|
if (discovered) {
|
|
816
871
|
getState(url).activeSessionId = discovered.id;
|
|
817
872
|
getState(url).activeSessionName = discovered.name;
|
|
818
|
-
|
|
873
|
+
log.info(` → session: ${discovered.name || discovered.id}`);
|
|
819
874
|
}
|
|
820
875
|
const defaultAgent = agents.find(a => a.mode === 'primary') || agents[0];
|
|
821
876
|
if (defaultAgent) {
|
|
822
877
|
getState(url).activeAgentName = defaultAgent.name;
|
|
823
|
-
|
|
878
|
+
log.info(` → agent: ${defaultAgent.name}`);
|
|
824
879
|
}
|
|
825
880
|
const p = getState(url).pendingResponse;
|
|
826
881
|
if (p) {
|
|
@@ -832,10 +887,10 @@ async function main() {
|
|
|
832
887
|
|
|
833
888
|
const setActiveSession = (id) => {
|
|
834
889
|
getState(serverBase).activeSessionId = id;
|
|
835
|
-
|
|
890
|
+
log.info(`Switched to session: ${id}`);
|
|
836
891
|
};
|
|
837
892
|
|
|
838
|
-
async function processMessageAsync(chatId, text, url, useSession, sessionId, myTaskId) {
|
|
893
|
+
async function processMessageAsync(chatId, text, url, useSession, sessionId, myTaskId, replyTo) {
|
|
839
894
|
const st = getState(url);
|
|
840
895
|
const isCancelled = () => getState(url).taskId !== myTaskId;
|
|
841
896
|
const cancelRef = { current: null };
|
|
@@ -845,11 +900,25 @@ async function main() {
|
|
|
845
900
|
if (useSession) {
|
|
846
901
|
// Event-driven flow: start SSE listener first, then send message async
|
|
847
902
|
const notifiedPerms = new Set();
|
|
903
|
+
let thinkingBuf = '';
|
|
848
904
|
const ssePromise = listenForSessionEvents(url, sessionId, {
|
|
905
|
+
onText: null,
|
|
906
|
+
onThinking: (chunk) => {
|
|
907
|
+
if (!getState(url).thinkingEnabled) { thinkingBuf = ''; return; }
|
|
908
|
+
// Forward thinking chunks in real-time
|
|
909
|
+
thinkingBuf += chunk;
|
|
910
|
+
if (thinkingBuf.length >= 50 || chunk.endsWith('\n')) {
|
|
911
|
+
const html = '💭 ' + thinkingBuf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
912
|
+
sendLongMessage(token, chatId, html, replyTo)
|
|
913
|
+
.then(() => {})
|
|
914
|
+
.catch(() => {});
|
|
915
|
+
thinkingBuf = '';
|
|
916
|
+
}
|
|
917
|
+
},
|
|
849
918
|
onPermission: (perm) => {
|
|
850
919
|
const st2 = getState(url);
|
|
851
920
|
const permDump = JSON.stringify(perm);
|
|
852
|
-
|
|
921
|
+
log.debug(` → permission_asked callback: ${permDump}`);
|
|
853
922
|
st2.pendingPermission = { server: url, sessionId, permissionId: perm.id, type: perm.permission || 'unspecified', pattern: perm.patterns || perm.pattern, title: perm.title };
|
|
854
923
|
if (!notifiedPerms.has(perm.id)) {
|
|
855
924
|
notifiedPerms.add(perm.id);
|
|
@@ -861,16 +930,16 @@ async function main() {
|
|
|
861
930
|
+ (patternStr ? '**Target:** `' + patternStr + '`\n' : '')
|
|
862
931
|
+ (perm.title ? '**Detail:** ' + perm.title + '\n' : '')
|
|
863
932
|
+ 'Use `/allow` to approve once, `/always` to always allow, or `/reject` to deny.';
|
|
864
|
-
|
|
865
|
-
sendLongMessage(token, chatId, msg)
|
|
866
|
-
.then(() =>
|
|
867
|
-
.catch((err) =>
|
|
933
|
+
log.debug(` → sending permission notification to chat ${chatId}`);
|
|
934
|
+
sendLongMessage(token, chatId, msg, replyTo)
|
|
935
|
+
.then(() => log.debug(` → permission notification sent OK`))
|
|
936
|
+
.catch((err) => log.debug(` → permission notification failed: ${err.message}`));
|
|
868
937
|
} else {
|
|
869
|
-
|
|
938
|
+
log.debug(` → permission ${perm.id} already notified, updating pendingPermission only`);
|
|
870
939
|
}
|
|
871
940
|
},
|
|
872
941
|
onPermissionReplied: () => {
|
|
873
|
-
|
|
942
|
+
log.debug(` → permission_replied: clearing pendingPermission`);
|
|
874
943
|
getState(url).pendingPermission = null;
|
|
875
944
|
},
|
|
876
945
|
}, cancelRef);
|
|
@@ -884,33 +953,39 @@ async function main() {
|
|
|
884
953
|
}
|
|
885
954
|
// Wait for the SSE listener to collect the full answer
|
|
886
955
|
answer = await ssePromise;
|
|
956
|
+
// Flush any remaining thinking buffer
|
|
957
|
+
if (thinkingBuf) {
|
|
958
|
+
const html = '💭 ' + thinkingBuf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
959
|
+
sendLongMessage(token, chatId, html, replyTo).catch(() => {});
|
|
960
|
+
thinkingBuf = '';
|
|
961
|
+
}
|
|
887
962
|
} else {
|
|
888
963
|
await sendText(url, `[Telegram] ${text}`);
|
|
889
964
|
answer = await listenForFinalAnswer(url, null, cancelRef);
|
|
890
965
|
}
|
|
891
966
|
if (isCancelled()) {
|
|
892
|
-
|
|
967
|
+
log.debug(` → cancelled, discarding response from ${url}`);
|
|
893
968
|
st.pendingPermission = null;
|
|
894
969
|
return;
|
|
895
970
|
}
|
|
896
971
|
st.pendingPermission = null;
|
|
897
972
|
if (url === serverBase) {
|
|
898
973
|
if (answer && answer.trim()) {
|
|
899
|
-
await sendLongMessage(token, chatId, answer.trim());
|
|
974
|
+
await sendLongMessage(token, chatId, answer.trim(), replyTo);
|
|
900
975
|
}
|
|
901
976
|
} else {
|
|
902
977
|
getState(url).pendingResponse = answer;
|
|
903
|
-
await sendLongMessage(token, chatId, `✅ Server \`${url}\` finished processing. Switch back to see the response
|
|
978
|
+
await sendLongMessage(token, chatId, `✅ Server \`${url}\` finished processing. Switch back to see the response.`, replyTo);
|
|
904
979
|
}
|
|
905
980
|
} catch (err) {
|
|
906
981
|
if (isCancelled()) {
|
|
907
|
-
|
|
982
|
+
log.debug(` → cancelled, ignoring error from ${url}: ${err.message}`);
|
|
908
983
|
st.pendingPermission = null;
|
|
909
984
|
return;
|
|
910
985
|
}
|
|
911
|
-
|
|
986
|
+
log.error(`Error processing message (${url}): ${err.message}`);
|
|
912
987
|
if (url === serverBase) {
|
|
913
|
-
try { await sendLongMessage(token, chatId, `Error: ${err.message}
|
|
988
|
+
try { await sendLongMessage(token, chatId, `Error: ${err.message}`, replyTo); } catch (_) {}
|
|
914
989
|
}
|
|
915
990
|
} finally {
|
|
916
991
|
st.pendingPermission = null;
|
|
@@ -935,7 +1010,7 @@ async function main() {
|
|
|
935
1010
|
st._cancelRef.current = null;
|
|
936
1011
|
}
|
|
937
1012
|
await sendLongMessage(token, chatId, '✅ Cancelled.');
|
|
938
|
-
|
|
1013
|
+
log.debug(` → /cancel on ${serverBase}`);
|
|
939
1014
|
}
|
|
940
1015
|
|
|
941
1016
|
const cmdStatus = async (token, chatId) => {
|
|
@@ -951,13 +1026,22 @@ async function main() {
|
|
|
951
1026
|
await sendLongMessage(token, chatId, lines.join('\n'));
|
|
952
1027
|
};
|
|
953
1028
|
|
|
954
|
-
const cmdShutdown = async (token, chatId) => {
|
|
955
|
-
|
|
1029
|
+
const cmdShutdown = async (token, chatId, args) => {
|
|
1030
|
+
const st = getState(serverBase);
|
|
1031
|
+
const isForce = args && args.length > 0 && args[0].toLowerCase() === 'force';
|
|
1032
|
+
if (st.busy && !isForce) {
|
|
1033
|
+
await sendLongMessage(token, chatId,
|
|
1034
|
+
'⏳ Server is busy processing a message.\n' +
|
|
1035
|
+
'Use `/shutdown force` to shut down anyway (in-flight response will be lost).');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const status = st.busy ? '⏳ busy (forced)' : '✅ idle';
|
|
1039
|
+
await sendLongMessage(token, chatId, `👋 Shutting down (state: ${status})...`);
|
|
956
1040
|
try {
|
|
957
1041
|
fs.writeFileSync(STARTUP_CHAT_FILE, String(chatId));
|
|
958
1042
|
} catch (_) {}
|
|
959
1043
|
try { await getUpdates(token, offset, 1); } catch (_) {}
|
|
960
|
-
|
|
1044
|
+
log.debug('/shutdown received, exiting...');
|
|
961
1045
|
process.exit(0);
|
|
962
1046
|
};
|
|
963
1047
|
|
|
@@ -987,13 +1071,13 @@ async function main() {
|
|
|
987
1071
|
try {
|
|
988
1072
|
result = await getUpdates(token, offset, 30);
|
|
989
1073
|
} catch (err) {
|
|
990
|
-
|
|
1074
|
+
log.error(`Polling error: ${err.message}`);
|
|
991
1075
|
await new Promise((r) => setTimeout(r, 5000));
|
|
992
1076
|
continue;
|
|
993
1077
|
}
|
|
994
1078
|
|
|
995
1079
|
if (!result.ok) {
|
|
996
|
-
|
|
1080
|
+
log.error(`Telegram API error: ${result.description || JSON.stringify(result)}`);
|
|
997
1081
|
await new Promise((r) => setTimeout(r, 5000));
|
|
998
1082
|
continue;
|
|
999
1083
|
}
|
|
@@ -1008,6 +1092,7 @@ async function main() {
|
|
|
1008
1092
|
}
|
|
1009
1093
|
|
|
1010
1094
|
const chatId = String(msg.chat.id);
|
|
1095
|
+
const replyTo = msg.message_id;
|
|
1011
1096
|
const text = msg.text;
|
|
1012
1097
|
const from = msg.from?.username
|
|
1013
1098
|
? `@${msg.from.username}`
|
|
@@ -1015,22 +1100,25 @@ async function main() {
|
|
|
1015
1100
|
|
|
1016
1101
|
// Enforce chat allowlist if configured
|
|
1017
1102
|
if (allowedChatIds && !allowedChatIds.includes(chatId)) {
|
|
1018
|
-
|
|
1103
|
+
log.debug(`[chat ${chatId}] ${from}: blocked (not in allowlist)`);
|
|
1019
1104
|
continue;
|
|
1020
1105
|
}
|
|
1021
1106
|
|
|
1022
1107
|
// Lock to the first chat that connects (single-user guard)
|
|
1023
1108
|
if (lockedChatId === null) {
|
|
1024
1109
|
lockedChatId = chatId;
|
|
1025
|
-
|
|
1110
|
+
log.debug(`[chat ${chatId}] ${from}: locked to this chat`);
|
|
1026
1111
|
} else if (lockedChatId !== chatId) {
|
|
1027
|
-
|
|
1112
|
+
log.debug(`[chat ${chatId}] ${from}: blocked (locked to ${lockedChatId})`);
|
|
1028
1113
|
continue;
|
|
1029
1114
|
}
|
|
1030
1115
|
|
|
1031
|
-
|
|
1116
|
+
log.debug(`[chat ${chatId}] ${from}: ${text}`);
|
|
1032
1117
|
try { fs.writeFileSync(STARTUP_CHAT_FILE, String(chatId)); } catch (_) {}
|
|
1033
1118
|
|
|
1119
|
+
// Build reply context if this message is a Telegram reply
|
|
1120
|
+
const replyContext = quoteReply(msg);
|
|
1121
|
+
|
|
1034
1122
|
// Intercept /commands
|
|
1035
1123
|
if (text.startsWith('/')) {
|
|
1036
1124
|
const parts = text.slice(1).split(' ');
|
|
@@ -1041,7 +1129,7 @@ async function main() {
|
|
|
1041
1129
|
try {
|
|
1042
1130
|
await handler(token, chatId, cmdArgs, setServer, setActiveSession, serverBase);
|
|
1043
1131
|
} catch (err) {
|
|
1044
|
-
|
|
1132
|
+
log.error(`Error handling /${cmd}: ${err.message}`);
|
|
1045
1133
|
try { await sendLongMessage(token, chatId, `Error: ${err.message}`); } catch (_) {}
|
|
1046
1134
|
}
|
|
1047
1135
|
} else {
|
|
@@ -1059,14 +1147,14 @@ async function main() {
|
|
|
1059
1147
|
}
|
|
1060
1148
|
st.busy = true;
|
|
1061
1149
|
const myTaskId = ++st.taskId;
|
|
1062
|
-
|
|
1063
|
-
processMessageAsync(chatId, text, serverBase, !!st.activeSessionId, st.activeSessionId, myTaskId);
|
|
1150
|
+
log.debug(` → started background processing (task ${myTaskId})`);
|
|
1151
|
+
processMessageAsync(chatId, replyContext + text, serverBase, !!st.activeSessionId, st.activeSessionId, myTaskId, replyTo);
|
|
1064
1152
|
}
|
|
1065
1153
|
}
|
|
1066
1154
|
}
|
|
1067
1155
|
|
|
1068
1156
|
process.on('SIGINT', () => {
|
|
1069
|
-
|
|
1157
|
+
log.info('tgagentp stopped.');
|
|
1070
1158
|
process.exit(0);
|
|
1071
1159
|
});
|
|
1072
1160
|
|
package/lib/opencode.js
CHANGED
|
@@ -222,13 +222,16 @@ async function updateSession(server, sessionId, title) {
|
|
|
222
222
|
async function selectSession(server, sessionId) {
|
|
223
223
|
const encoded = encodeURIComponent(sessionId);
|
|
224
224
|
const attempts = [
|
|
225
|
-
{ url: `${server}/session/${encoded}/select`, body: '{}' },
|
|
226
225
|
{ url: `${server}/tui/select-session`, body: JSON.stringify({ sessionID: sessionId }) },
|
|
226
|
+
{ url: `${server}/session/${encoded}/select`, body: '{}' },
|
|
227
227
|
];
|
|
228
228
|
for (const { url, body } of attempts) {
|
|
229
229
|
try {
|
|
230
|
-
await makeRequest(buildJsonRequest(url, 'POST', body), body);
|
|
231
|
-
|
|
230
|
+
const { status } = await makeRequest(buildJsonRequest(url, 'POST', body), body);
|
|
231
|
+
// /session/:id/select returns 200 with HTML even for bad IDs (TUI page),
|
|
232
|
+
// but doesn't actually navigate. Only consider it a success if the
|
|
233
|
+
// response looks like JSON (i.e. not HTML).
|
|
234
|
+
if (status === 200) return;
|
|
232
235
|
} catch {
|
|
233
236
|
// try next approach
|
|
234
237
|
}
|
|
@@ -376,6 +379,10 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
|
|
|
376
379
|
if (callbacks.onText) callbacks.onText(part.text);
|
|
377
380
|
collected += part.text;
|
|
378
381
|
}
|
|
382
|
+
|
|
383
|
+
if ((part.type === 'reasoning' || part.type === 'thinking') && part.text != null && part.text !== '') {
|
|
384
|
+
if (callbacks.onThinking) callbacks.onThinking(part.text);
|
|
385
|
+
}
|
|
379
386
|
} catch (e) {
|
|
380
387
|
// Malformed SSE event — skip
|
|
381
388
|
}
|
package/package.json
CHANGED
package/status_report.md
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# Project Status Report — June 2026
|
|
2
|
-
|
|
3
|
-
## Environment
|
|
4
|
-
|
|
5
|
-
- **Node.js:** v22.12.0
|
|
6
|
-
- **opencode:** v1.15.13 (compiled ELF binary, bun-based)
|
|
7
|
-
- **Package:** agentp v0.6.0 (just published)
|
|
8
|
-
- **Branch structure (local):**
|
|
9
|
-
- `main` — v0.6.0, published, contains all ocmux fixes
|
|
10
|
-
- `pr-2` — worktree at `wt/pr-2/`, same as main + minor divergence
|
|
11
|
-
- `feature/tgagentp-session-init` — branch with WIP session discovery work (based on old v0.5.0, needs rebase onto main)
|
|
12
|
-
|
|
13
|
-
## What's Fixed (v0.6.0)
|
|
14
|
-
|
|
15
|
-
- **tuiPaneId** rewritten to use pane index (server=0, TUI=1+) instead of process name — works with any runtime (node, bun, compiled binary)
|
|
16
|
-
- **URL polling** in `doNew()` takes the last match instead of the first — more robust with bun startup
|
|
17
|
-
- **agentp freeze** confirmed resolved — the 401/event stream issue was caused by stale opencode 0.14.39 session, not a code bug
|
|
18
|
-
|
|
19
|
-
## Known Issues (unsolved)
|
|
20
|
-
|
|
21
|
-
### 1. Session initialization — startup / server switch
|
|
22
|
-
- **Branch:** `feature/tgagentp-session-init` (1 commit, needs rebase)
|
|
23
|
-
- **Problem:** On fresh tgagentp start or `/servers switch`, `activeSessionId` is `null`. The user must manually do `/sessions switch` or `/sessions new` before session-aware messaging works.
|
|
24
|
-
- **WIP fix:** `discoverActiveSession()` helper — calls `listSessions`, sorts by `time.updated`, picks most recent. Called at startup (async `.then()`) and in `setServer()` (awaited). But startup greeting may fire before discovery completes, showing "Session: *none (TUI+SSE)*".
|
|
25
|
-
- **Next step:** Rebase branch onto main, tune timing (await discovery before greeting, or delay greeting).
|
|
26
|
-
|
|
27
|
-
### 2. /status shows session ID instead of name
|
|
28
|
-
- `/status` displays raw `ses_xxxxx` ID. Would be user-friendly to show the session title/slug (available from `listSessions`).
|
|
29
|
-
- **Fix approach:** Either fetch session name on `/status` (adds latency), or cache discovered session names.
|
|
30
|
-
|
|
31
|
-
### 3. /status shows "Agent: *default*" instead of actual agent name
|
|
32
|
-
- `activeAgentName` is never initialized on startup. The TUI has an active agent but tgagentp doesn't discover it.
|
|
33
|
-
- **Fix:** Could extend `discoverActiveSession()` to also fetch the session's agent, or query `GET /agent` for the default agent on startup.
|
|
34
|
-
|
|
35
|
-
### 4. /servers list — no active server marker
|
|
36
|
-
- Unlike `/sessions` (which marks active session with ▶), `/servers` doesn't highlight the currently selected server.
|
|
37
|
-
- **Fix:** Simple — compare each server URL against `serverBase` and prepend a bullet.
|
|
38
|
-
|
|
39
|
-
### 5. TUI navigation on session switch
|
|
40
|
-
- `/sessions switch` and `/sessions new` call `selectSession()` which tries `POST /session/:id/select` and `POST /tui/select-session`. Neither navigates the TUI pane. Message routing works correctly in background.
|
|
41
|
-
- **Status:** Known limitation — documented in AGENTS.md "With Issues." The underlying opencode API may lack the needed TUI navigation endpoints. Revisit after opencode 1.15.x API audit.
|
|
42
|
-
|
|
43
|
-
### 6. AGENTS.md docs need updating
|
|
44
|
-
- `tuiPaneId` doc still says "returns pane ID if TUI process (`node`) is running" — now uses pane index, not process name.
|
|
45
|
-
- Session discovery feature (once merged) needs documenting.
|
|
46
|
-
|
|
47
|
-
### 7. --print-logs request (low priority)
|
|
48
|
-
- User requested a `--print-logs` option for the server startup command in ocmux to aid debugging.
|
|
49
|
-
- The log file is at `/tmp/opencode-serve-<hashDir(dir)>.log` and is already captured via `tee`. Could add a flag to tail it.
|
|
50
|
-
|
|
51
|
-
## Session discovery (feature branch details)
|
|
52
|
-
|
|
53
|
-
Branch `feature/tgagentp-session-init` contains 1 commit (`6393a8f`):
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
WIP: session discovery, initialization improvements, known issues
|
|
57
|
-
- Added discoverActiveSession() helper
|
|
58
|
-
- setServer() made async
|
|
59
|
-
- Startup greeting uses st.activeSessionId
|
|
60
|
-
|
|
61
|
-
Known issues (at time of writing, with opencode v1.14.39):
|
|
62
|
-
1. Startup greeting shows "Session: *none*" due to async race
|
|
63
|
-
2. /status shows session ID not name
|
|
64
|
-
3. /status shows "Agent: *default*"
|
|
65
|
-
4. /servers list no active marker
|
|
66
|
-
5. /servers switch doesn't switch tmux window (now partially fixed in v0.6.0)
|
|
67
|
-
6. /sessions switch doesn't navigate TUI
|
|
68
|
-
7. ocmux misbehaved with opencode >= 1.15.x (now fixed in v0.6.0)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**Files changed:** `bin/tgagentp`, `AGENTS.md` (+34/-5 lines)
|
|
72
|
-
|
|
73
|
-
The branch needs to be rebased onto main (v0.6.0) before further work.
|
|
74
|
-
|
|
75
|
-
## Next Steps
|
|
76
|
-
|
|
77
|
-
1. Rebase `feature/tgagentp-session-init` onto main
|
|
78
|
-
2. Continue on that branch:
|
|
79
|
-
a. Fix startup greeting race (await discovery before sending)
|
|
80
|
-
b. Add session name resolution for /status
|
|
81
|
-
c. Add agent discovery for /status
|
|
82
|
-
d. Add active server marker in /servers
|
|
83
|
-
e. Revisit TUI navigation after opencode API audit
|
|
84
|
-
3. Merge back into main when ready
|
|
85
|
-
4. Clean up: remove `pr-2` worktree and branch
|