@visorcraft/idlehands 0.9.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/agent.js +2604 -0
- package/dist/agent.js.map +1 -0
- package/dist/anton/controller.js +341 -0
- package/dist/anton/controller.js.map +1 -0
- package/dist/anton/lock.js +110 -0
- package/dist/anton/lock.js.map +1 -0
- package/dist/anton/parser.js +303 -0
- package/dist/anton/parser.js.map +1 -0
- package/dist/anton/prompt.js +203 -0
- package/dist/anton/prompt.js.map +1 -0
- package/dist/anton/reporter.js +119 -0
- package/dist/anton/reporter.js.map +1 -0
- package/dist/anton/session.js +51 -0
- package/dist/anton/session.js.map +1 -0
- package/dist/anton/types.js +7 -0
- package/dist/anton/types.js.map +1 -0
- package/dist/anton/verifier.js +263 -0
- package/dist/anton/verifier.js.map +1 -0
- package/dist/bench/compare.js +239 -0
- package/dist/bench/compare.js.map +1 -0
- package/dist/bench/debug_hooks.js +17 -0
- package/dist/bench/debug_hooks.js.map +1 -0
- package/dist/bench/json_extract.js +22 -0
- package/dist/bench/json_extract.js.map +1 -0
- package/dist/bench/openclaw.js +86 -0
- package/dist/bench/openclaw.js.map +1 -0
- package/dist/bench/report.js +116 -0
- package/dist/bench/report.js.map +1 -0
- package/dist/bench/runner.js +312 -0
- package/dist/bench/runner.js.map +1 -0
- package/dist/bench/types.js +2 -0
- package/dist/bench/types.js.map +1 -0
- package/dist/bot/commands.js +444 -0
- package/dist/bot/commands.js.map +1 -0
- package/dist/bot/confirm-discord.js +133 -0
- package/dist/bot/confirm-discord.js.map +1 -0
- package/dist/bot/confirm-telegram.js +290 -0
- package/dist/bot/confirm-telegram.js.map +1 -0
- package/dist/bot/discord.js +826 -0
- package/dist/bot/discord.js.map +1 -0
- package/dist/bot/format.js +210 -0
- package/dist/bot/format.js.map +1 -0
- package/dist/bot/session-manager.js +270 -0
- package/dist/bot/session-manager.js.map +1 -0
- package/dist/bot/telegram.js +678 -0
- package/dist/bot/telegram.js.map +1 -0
- package/dist/cli/agent-turn.js +45 -0
- package/dist/cli/agent-turn.js.map +1 -0
- package/dist/cli/args.js +236 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bot.js +252 -0
- package/dist/cli/bot.js.map +1 -0
- package/dist/cli/build-repl-context.js +365 -0
- package/dist/cli/build-repl-context.js.map +1 -0
- package/dist/cli/command-registry.js +20 -0
- package/dist/cli/command-registry.js.map +1 -0
- package/dist/cli/commands/anton.js +271 -0
- package/dist/cli/commands/anton.js.map +1 -0
- package/dist/cli/commands/editing.js +328 -0
- package/dist/cli/commands/editing.js.map +1 -0
- package/dist/cli/commands/model.js +274 -0
- package/dist/cli/commands/model.js.map +1 -0
- package/dist/cli/commands/project.js +255 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/commands/runtime.js +63 -0
- package/dist/cli/commands/runtime.js.map +1 -0
- package/dist/cli/commands/session.js +281 -0
- package/dist/cli/commands/session.js.map +1 -0
- package/dist/cli/commands/tools.js +126 -0
- package/dist/cli/commands/tools.js.map +1 -0
- package/dist/cli/commands/trifecta.js +221 -0
- package/dist/cli/commands/trifecta.js.map +1 -0
- package/dist/cli/commands/tui.js +17 -0
- package/dist/cli/commands/tui.js.map +1 -0
- package/dist/cli/init.js +222 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/input.js +360 -0
- package/dist/cli/input.js.map +1 -0
- package/dist/cli/oneshot.js +254 -0
- package/dist/cli/oneshot.js.map +1 -0
- package/dist/cli/repl-context.js +2 -0
- package/dist/cli/repl-context.js.map +1 -0
- package/dist/cli/runtime-cmds.js +811 -0
- package/dist/cli/runtime-cmds.js.map +1 -0
- package/dist/cli/service.js +145 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/session-state.js +130 -0
- package/dist/cli/session-state.js.map +1 -0
- package/dist/cli/setup.js +815 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/shell.js +79 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/status.js +392 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/watch.js +33 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/client.js +676 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.js +194 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.js +507 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm/auto.js +13 -0
- package/dist/confirm/auto.js.map +1 -0
- package/dist/confirm/headless.js +41 -0
- package/dist/confirm/headless.js.map +1 -0
- package/dist/confirm/terminal.js +90 -0
- package/dist/confirm/terminal.js.map +1 -0
- package/dist/context.js +49 -0
- package/dist/context.js.map +1 -0
- package/dist/git.js +136 -0
- package/dist/git.js.map +1 -0
- package/dist/harnesses.js +171 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/history.js +139 -0
- package/dist/history.js.map +1 -0
- package/dist/index.js +700 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.js +374 -0
- package/dist/indexer.js.map +1 -0
- package/dist/jsonrpc.js +76 -0
- package/dist/jsonrpc.js.map +1 -0
- package/dist/lens.js +525 -0
- package/dist/lens.js.map +1 -0
- package/dist/lsp.js +605 -0
- package/dist/lsp.js.map +1 -0
- package/dist/markdown.js +275 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mcp.js +554 -0
- package/dist/mcp.js.map +1 -0
- package/dist/recovery.js +178 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.js +132 -0
- package/dist/replay.js.map +1 -0
- package/dist/replay_cli.js +24 -0
- package/dist/replay_cli.js.map +1 -0
- package/dist/runtime/executor.js +418 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/planner.js +197 -0
- package/dist/runtime/planner.js.map +1 -0
- package/dist/runtime/store.js +289 -0
- package/dist/runtime/store.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/safety.js +446 -0
- package/dist/safety.js.map +1 -0
- package/dist/spinner.js +224 -0
- package/dist/spinner.js.map +1 -0
- package/dist/sys/context.js +124 -0
- package/dist/sys/context.js.map +1 -0
- package/dist/sys/snapshot.sh +97 -0
- package/dist/term.js +61 -0
- package/dist/term.js.map +1 -0
- package/dist/themes.js +135 -0
- package/dist/themes.js.map +1 -0
- package/dist/tools.js +1114 -0
- package/dist/tools.js.map +1 -0
- package/dist/tui/branch-picker.js +65 -0
- package/dist/tui/branch-picker.js.map +1 -0
- package/dist/tui/command-handler.js +108 -0
- package/dist/tui/command-handler.js.map +1 -0
- package/dist/tui/confirm.js +90 -0
- package/dist/tui/confirm.js.map +1 -0
- package/dist/tui/controller.js +463 -0
- package/dist/tui/controller.js.map +1 -0
- package/dist/tui/event-bridge.js +44 -0
- package/dist/tui/event-bridge.js.map +1 -0
- package/dist/tui/events.js +2 -0
- package/dist/tui/events.js.map +1 -0
- package/dist/tui/keymap.js +144 -0
- package/dist/tui/keymap.js.map +1 -0
- package/dist/tui/layout.js +11 -0
- package/dist/tui/layout.js.map +1 -0
- package/dist/tui/render.js +186 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/screen.js +48 -0
- package/dist/tui/screen.js.map +1 -0
- package/dist/tui/state.js +167 -0
- package/dist/tui/state.js.map +1 -0
- package/dist/tui/theme.js +70 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/upgrade.js +412 -0
- package/dist/upgrade.js.map +1 -0
- package/dist/utils.js +87 -0
- package/dist/utils.js.map +1 -0
- package/dist/vault.js +520 -0
- package/dist/vault.js.map +1 -0
- package/dist/vim.js +160 -0
- package/dist/vim.js.map +1 -0
- package/package.json +67 -0
- package/src/sys/snapshot.sh +97 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle Hands Telegram Bot — main entry point.
|
|
3
|
+
* grammy-based long-polling bot that wraps the agent core.
|
|
4
|
+
*/
|
|
5
|
+
import { Bot, InputFile } from 'grammy';
|
|
6
|
+
import { SessionManager } from './session-manager.js';
|
|
7
|
+
import { markdownToTelegramHtml, splitMessage, escapeHtml, formatToolCallSummary } from './format.js';
|
|
8
|
+
import { handleStart, handleHelp, handleReset, handleCancel, handleStatus, handleDir, handleModel, handleCompact, handleApproval, handleMode, handleChanges, handleUndo, handleVault, handleAnton, } from './commands.js';
|
|
9
|
+
import { TelegramConfirmProvider } from './confirm-telegram.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Streaming message helper
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
class StreamingMessage {
|
|
14
|
+
bot;
|
|
15
|
+
chatId;
|
|
16
|
+
editIntervalMs;
|
|
17
|
+
replyToId;
|
|
18
|
+
fileThresholdChars;
|
|
19
|
+
buffer = '';
|
|
20
|
+
toolLines = [];
|
|
21
|
+
lastToolLine = "";
|
|
22
|
+
lastToolRepeat = 0;
|
|
23
|
+
messageId = null;
|
|
24
|
+
editTimer = null;
|
|
25
|
+
typingTimer = null;
|
|
26
|
+
lastEditText = '';
|
|
27
|
+
finalized = false;
|
|
28
|
+
backoffMs = 0;
|
|
29
|
+
constructor(bot, chatId, editIntervalMs, replyToId, fileThresholdChars = 8192) {
|
|
30
|
+
this.bot = bot;
|
|
31
|
+
this.chatId = chatId;
|
|
32
|
+
this.editIntervalMs = editIntervalMs;
|
|
33
|
+
this.replyToId = replyToId;
|
|
34
|
+
this.fileThresholdChars = fileThresholdChars;
|
|
35
|
+
}
|
|
36
|
+
async init() {
|
|
37
|
+
// Show "typing..." indicator immediately; repeat every 4s (Telegram auto-expires at ~5s)
|
|
38
|
+
this.bot.api.sendChatAction(this.chatId, 'typing').catch(() => { });
|
|
39
|
+
this.typingTimer = setInterval(() => {
|
|
40
|
+
if (!this.finalized) {
|
|
41
|
+
this.bot.api.sendChatAction(this.chatId, 'typing').catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
}, 4_000);
|
|
44
|
+
const msg = await this.bot.api.sendMessage(this.chatId, '⏳ Thinking...', {
|
|
45
|
+
reply_to_message_id: this.replyToId,
|
|
46
|
+
});
|
|
47
|
+
this.messageId = msg.message_id;
|
|
48
|
+
this.startEditLoop();
|
|
49
|
+
}
|
|
50
|
+
stopTyping() {
|
|
51
|
+
if (this.typingTimer) {
|
|
52
|
+
clearInterval(this.typingTimer);
|
|
53
|
+
this.typingTimer = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
onToken(token) {
|
|
57
|
+
this.buffer += token;
|
|
58
|
+
}
|
|
59
|
+
onToolCall(call) {
|
|
60
|
+
const summary = formatToolCallSummary(call);
|
|
61
|
+
const line = `◆ ${summary}...`;
|
|
62
|
+
if (this.lastToolLine === line && this.toolLines.length > 0) {
|
|
63
|
+
this.lastToolRepeat += 1;
|
|
64
|
+
this.toolLines[this.toolLines.length - 1] = `${line} (x${this.lastToolRepeat + 1})`;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.lastToolLine = line;
|
|
68
|
+
this.lastToolRepeat = 0;
|
|
69
|
+
this.toolLines.push(line);
|
|
70
|
+
}
|
|
71
|
+
onToolResult(result) {
|
|
72
|
+
this.lastToolLine = "";
|
|
73
|
+
this.lastToolRepeat = 0;
|
|
74
|
+
if (this.toolLines.length > 0) {
|
|
75
|
+
const icon = result.success ? '✓' : '✗';
|
|
76
|
+
this.toolLines[this.toolLines.length - 1] = `${icon} ${result.name}: ${result.summary}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
startEditLoop() {
|
|
80
|
+
this.editTimer = setInterval(() => this.flush(), this.editIntervalMs);
|
|
81
|
+
}
|
|
82
|
+
async flush() {
|
|
83
|
+
if (!this.messageId || this.finalized)
|
|
84
|
+
return;
|
|
85
|
+
if (this.backoffMs > 0) {
|
|
86
|
+
this.backoffMs = Math.max(0, this.backoffMs - this.editIntervalMs);
|
|
87
|
+
return; // skip this edit cycle while backing off
|
|
88
|
+
}
|
|
89
|
+
const text = this.render();
|
|
90
|
+
if (!text || text === this.lastEditText)
|
|
91
|
+
return;
|
|
92
|
+
this.lastEditText = text;
|
|
93
|
+
try {
|
|
94
|
+
await this.bot.api.editMessageText(this.chatId, this.messageId, text, {
|
|
95
|
+
parse_mode: 'HTML',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
const desc = e?.description ?? e?.message ?? '';
|
|
100
|
+
if (desc.includes('Too Many Requests') || desc.includes('429')) {
|
|
101
|
+
// Exponential backoff on rate limit
|
|
102
|
+
const retryAfter = (e?.parameters?.retry_after ?? 3) * 1000;
|
|
103
|
+
this.backoffMs = Math.min(retryAfter * 2, 30_000);
|
|
104
|
+
console.error(`[bot] rate limited, backing off ${this.backoffMs}ms`);
|
|
105
|
+
}
|
|
106
|
+
else if (!desc.includes('message is not modified') && !desc.includes('message to edit not found')) {
|
|
107
|
+
console.error(`[bot] edit error: ${desc}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
render() {
|
|
112
|
+
let out = '';
|
|
113
|
+
if (this.toolLines.length) {
|
|
114
|
+
out += `<pre>${escapeHtml(this.toolLines.join('\n'))}</pre>\n\n`;
|
|
115
|
+
}
|
|
116
|
+
if (this.buffer) {
|
|
117
|
+
out += markdownToTelegramHtml(this.buffer);
|
|
118
|
+
}
|
|
119
|
+
if (!out.trim()) {
|
|
120
|
+
out = '⏳ Thinking...';
|
|
121
|
+
}
|
|
122
|
+
return out.slice(0, 4096);
|
|
123
|
+
}
|
|
124
|
+
/** Finalize: stop the edit loop and send the final response. */
|
|
125
|
+
async finalize(text) {
|
|
126
|
+
this.finalized = true;
|
|
127
|
+
this.stopTyping();
|
|
128
|
+
if (this.editTimer) {
|
|
129
|
+
clearInterval(this.editTimer);
|
|
130
|
+
this.editTimer = null;
|
|
131
|
+
}
|
|
132
|
+
const html = this.renderFinal(text);
|
|
133
|
+
// Large output fallback: send as .md file attachment
|
|
134
|
+
if (text.length > this.fileThresholdChars) {
|
|
135
|
+
// Edit placeholder to a summary
|
|
136
|
+
const summary = text.slice(0, 200).replace(/\n/g, ' ').trim();
|
|
137
|
+
const summaryHtml = `📄 Response is ${text.length.toLocaleString()} chars — sent as file.\n\n<i>${escapeHtml(summary)}…</i>`;
|
|
138
|
+
if (this.messageId) {
|
|
139
|
+
await this.bot.api.editMessageText(this.chatId, this.messageId, summaryHtml, {
|
|
140
|
+
parse_mode: 'HTML',
|
|
141
|
+
}).catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
const fileContent = Buffer.from(text, 'utf-8');
|
|
144
|
+
await this.bot.api.sendDocument(this.chatId, new InputFile(fileContent, 'response.md'), {
|
|
145
|
+
caption: `Full response (${text.length.toLocaleString()} chars)`,
|
|
146
|
+
}).catch((e) => {
|
|
147
|
+
console.error(`[bot] sendDocument error: ${e?.message ?? e}`);
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const chunks = splitMessage(html, 4096);
|
|
152
|
+
// Edit the first message with the first chunk
|
|
153
|
+
if (this.messageId && chunks.length > 0) {
|
|
154
|
+
try {
|
|
155
|
+
await this.bot.api.editMessageText(this.chatId, this.messageId, chunks[0], {
|
|
156
|
+
parse_mode: 'HTML',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
// If edit fails (too old, etc.), send as new message
|
|
161
|
+
const desc = e?.description ?? '';
|
|
162
|
+
if (desc.includes('message to edit not found')) {
|
|
163
|
+
await this.bot.api.sendMessage(this.chatId, chunks[0], { parse_mode: 'HTML' }).catch(() => { });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Send remaining chunks as new messages
|
|
168
|
+
for (let i = 1; i < chunks.length && i < 10; i++) {
|
|
169
|
+
try {
|
|
170
|
+
await this.bot.api.sendMessage(this.chatId, chunks[i], { parse_mode: 'HTML' });
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
console.error(`[bot] send chunk ${i} error: ${e?.message ?? e}`);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (chunks.length > 10) {
|
|
178
|
+
await this.bot.api.sendMessage(this.chatId, '[truncated — response too long]').catch(() => { });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
renderFinal(text) {
|
|
182
|
+
let out = '';
|
|
183
|
+
if (this.toolLines.length) {
|
|
184
|
+
out += `<pre>${escapeHtml(this.toolLines.join('\n'))}</pre>\n\n`;
|
|
185
|
+
}
|
|
186
|
+
out += markdownToTelegramHtml(text);
|
|
187
|
+
return out || '(empty response)';
|
|
188
|
+
}
|
|
189
|
+
/** Finalize with an error message. */
|
|
190
|
+
async finalizeError(errMsg) {
|
|
191
|
+
this.finalized = true;
|
|
192
|
+
this.stopTyping();
|
|
193
|
+
if (this.editTimer) {
|
|
194
|
+
clearInterval(this.editTimer);
|
|
195
|
+
this.editTimer = null;
|
|
196
|
+
}
|
|
197
|
+
let html = '';
|
|
198
|
+
if (this.toolLines.length) {
|
|
199
|
+
html += `<pre>${escapeHtml(this.toolLines.join('\n'))}</pre>\n\n`;
|
|
200
|
+
}
|
|
201
|
+
if (this.buffer.trim()) {
|
|
202
|
+
html += markdownToTelegramHtml(this.buffer) + '\n\n';
|
|
203
|
+
}
|
|
204
|
+
html += `❌ ${escapeHtml(errMsg)}`;
|
|
205
|
+
if (this.messageId) {
|
|
206
|
+
try {
|
|
207
|
+
await this.bot.api.editMessageText(this.chatId, this.messageId, html.slice(0, 4096), {
|
|
208
|
+
parse_mode: 'HTML',
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// fall through to send
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
await this.bot.api.sendMessage(this.chatId, html.slice(0, 4096), { parse_mode: 'HTML' }).catch(() => { });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Bot startup
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
export async function startTelegramBot(config, botConfig) {
|
|
223
|
+
// Validate config
|
|
224
|
+
const token = process.env.IDLEHANDS_TG_TOKEN || botConfig.token;
|
|
225
|
+
if (!token) {
|
|
226
|
+
console.error('[bot] IDLEHANDS_TG_TOKEN not set and bot.telegram.token is empty.');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const allowedUsersEnv = process.env.IDLEHANDS_TG_ALLOWED_USERS;
|
|
230
|
+
const rawUsers = allowedUsersEnv
|
|
231
|
+
? allowedUsersEnv.split(',').map(Number).filter(Boolean)
|
|
232
|
+
: Array.isArray(botConfig.allowed_users)
|
|
233
|
+
? botConfig.allowed_users
|
|
234
|
+
: botConfig.allowed_users != null
|
|
235
|
+
? [Number(botConfig.allowed_users)].filter(Boolean)
|
|
236
|
+
: [];
|
|
237
|
+
const allowedUsers = new Set(rawUsers);
|
|
238
|
+
if (allowedUsers.size === 0) {
|
|
239
|
+
console.error('[bot] bot.telegram.allowed_users is empty — refusing to start an unauthenticated bot.');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const bot = new Bot(token);
|
|
243
|
+
const sessions = new SessionManager(config, botConfig, (chatId) => new TelegramConfirmProvider(bot, chatId, botConfig.confirm_timeout_sec ?? 300));
|
|
244
|
+
const editIntervalMs = botConfig.edit_interval_ms ?? 1500;
|
|
245
|
+
// Override default_dir from env
|
|
246
|
+
if (process.env.IDLEHANDS_TG_DIR) {
|
|
247
|
+
botConfig.default_dir = process.env.IDLEHANDS_TG_DIR;
|
|
248
|
+
}
|
|
249
|
+
const cmdCtx = (ctx) => ({
|
|
250
|
+
ctx,
|
|
251
|
+
sessions,
|
|
252
|
+
botConfig: {
|
|
253
|
+
model: config.model,
|
|
254
|
+
endpoint: config.endpoint,
|
|
255
|
+
defaultDir: botConfig.default_dir || config.dir,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Auth middleware
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
bot.use(async (ctx, next) => {
|
|
262
|
+
const userId = ctx.from?.id;
|
|
263
|
+
if (!userId || !allowedUsers.has(userId)) {
|
|
264
|
+
// Silent ignore — don't reveal the bot exists
|
|
265
|
+
if (config.verbose) {
|
|
266
|
+
console.error(`[bot] ignored message from unauthorized user ${userId}`);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Group chat guard
|
|
271
|
+
if (!(botConfig.allow_groups ?? false)) {
|
|
272
|
+
const chatType = ctx.chat?.type;
|
|
273
|
+
if (chatType && chatType !== 'private') {
|
|
274
|
+
return; // Silent ignore
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
await next();
|
|
278
|
+
});
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Command handlers
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
bot.command('start', (ctx) => handleStart(cmdCtx(ctx)));
|
|
283
|
+
bot.command('help', (ctx) => handleHelp(cmdCtx(ctx)));
|
|
284
|
+
bot.command('reset', (ctx) => handleReset(cmdCtx(ctx)));
|
|
285
|
+
bot.command('cancel', (ctx) => handleCancel(cmdCtx(ctx)));
|
|
286
|
+
bot.command('status', (ctx) => handleStatus(cmdCtx(ctx)));
|
|
287
|
+
bot.command('dir', (ctx) => handleDir(cmdCtx(ctx)));
|
|
288
|
+
bot.command('model', (ctx) => handleModel(cmdCtx(ctx)));
|
|
289
|
+
bot.command('compact', (ctx) => handleCompact(cmdCtx(ctx)));
|
|
290
|
+
bot.command('approval', (ctx) => handleApproval(cmdCtx(ctx)));
|
|
291
|
+
bot.command('mode', (ctx) => handleMode(cmdCtx(ctx)));
|
|
292
|
+
bot.command('changes', (ctx) => handleChanges(cmdCtx(ctx)));
|
|
293
|
+
bot.command('undo', (ctx) => handleUndo(cmdCtx(ctx)));
|
|
294
|
+
bot.command('vault', (ctx) => handleVault(cmdCtx(ctx)));
|
|
295
|
+
bot.command('anton', (ctx) => handleAnton(cmdCtx(ctx)));
|
|
296
|
+
bot.command('hosts', async (ctx) => {
|
|
297
|
+
try {
|
|
298
|
+
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
299
|
+
const config = await loadRuntimes();
|
|
300
|
+
const redacted = redactConfig(config);
|
|
301
|
+
if (!redacted.hosts.length) {
|
|
302
|
+
await ctx.reply('No hosts configured. Use `idlehands hosts add` in CLI.');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const lines = redacted.hosts.map((h) => `${h.enabled ? '🟢' : '🔴'} *${h.display_name}* (\`${h.id}\`)\n Transport: ${h.transport}`);
|
|
306
|
+
await ctx.reply(lines.join('\n\n'), { parse_mode: 'Markdown' });
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
await ctx.reply(`❌ Failed to load hosts: ${e?.message ?? String(e)}`);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
bot.command('backends', async (ctx) => {
|
|
313
|
+
try {
|
|
314
|
+
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
315
|
+
const config = await loadRuntimes();
|
|
316
|
+
const redacted = redactConfig(config);
|
|
317
|
+
if (!redacted.backends.length) {
|
|
318
|
+
await ctx.reply('No backends configured. Use `idlehands backends add` in CLI.');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const lines = redacted.backends.map((b) => `${b.enabled ? '🟢' : '🔴'} *${b.display_name}* (\`${b.id}\`)\n Type: ${b.type}`);
|
|
322
|
+
await ctx.reply(lines.join('\n\n'), { parse_mode: 'Markdown' });
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
await ctx.reply(`❌ Failed to load backends: ${e?.message ?? String(e)}`);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
bot.command('rtmodels', async (ctx) => {
|
|
329
|
+
try {
|
|
330
|
+
const { loadRuntimes } = await import('../runtime/store.js');
|
|
331
|
+
const config = await loadRuntimes();
|
|
332
|
+
if (!config.models.length) {
|
|
333
|
+
await ctx.reply('No runtime models configured.');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const lines = config.models.map((m) => `${m.enabled ? '🟢' : '🔴'} *${m.display_name}* (\`${m.id}\`)\n Source: \`${m.source}\``);
|
|
337
|
+
await ctx.reply(lines.join('\n\n'), { parse_mode: 'Markdown' });
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
await ctx.reply(`❌ Failed to load runtime models: ${e?.message ?? String(e)}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
bot.command('rtstatus', async (ctx) => {
|
|
344
|
+
try {
|
|
345
|
+
const { loadActiveRuntime } = await import('../runtime/executor.js');
|
|
346
|
+
const active = await loadActiveRuntime();
|
|
347
|
+
if (!active) {
|
|
348
|
+
await ctx.reply('No active runtime.');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const lines = [
|
|
352
|
+
'*Active Runtime*',
|
|
353
|
+
`Model: \`${active.modelId}\``,
|
|
354
|
+
`Backend: \`${active.backendId ?? 'none'}\``,
|
|
355
|
+
`Hosts: ${active.hostIds.map((id) => `\`${id}\``).join(', ') || 'none'}`,
|
|
356
|
+
`Healthy: ${active.healthy ? '✅ yes' : '❌ no'}`,
|
|
357
|
+
`Endpoint: \`${active.endpoint ?? 'unknown'}\``,
|
|
358
|
+
`Started: \`${active.startedAt}\``,
|
|
359
|
+
];
|
|
360
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' });
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
await ctx.reply(`❌ Failed to read runtime status: ${e?.message ?? String(e)}`);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
bot.command('switch', async (ctx) => {
|
|
367
|
+
try {
|
|
368
|
+
const modelId = ctx.match?.trim();
|
|
369
|
+
if (!modelId) {
|
|
370
|
+
await ctx.reply('Usage: /switch <model-id>');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const { plan } = await import('../runtime/planner.js');
|
|
374
|
+
const { execute, loadActiveRuntime } = await import('../runtime/executor.js');
|
|
375
|
+
const { loadRuntimes } = await import('../runtime/store.js');
|
|
376
|
+
const rtConfig = await loadRuntimes();
|
|
377
|
+
const active = await loadActiveRuntime();
|
|
378
|
+
const result = plan({ modelId, mode: 'live' }, rtConfig, active);
|
|
379
|
+
if (!result.ok) {
|
|
380
|
+
await ctx.reply(`❌ Plan failed: ${result.reason}`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (result.reuse) {
|
|
384
|
+
await ctx.reply('✅ Runtime already active and healthy.');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const statusMsg = await ctx.reply(`⏳ Switching to *${result.model.display_name}*...`, { parse_mode: 'Markdown' });
|
|
388
|
+
const execResult = await execute(result, {
|
|
389
|
+
onStep: async (step, status) => {
|
|
390
|
+
if (status === 'done') {
|
|
391
|
+
await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, `⏳ ${step.description}... ✓`).catch(() => { });
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
confirm: async (prompt) => {
|
|
395
|
+
await ctx.reply(`⚠️ ${prompt}\nAuto-approving for bot context.`);
|
|
396
|
+
return true;
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
if (execResult.ok) {
|
|
400
|
+
await ctx.reply(`✅ Switched to *${result.model.display_name}*`, { parse_mode: 'Markdown' });
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
await ctx.reply(`❌ Switch failed: ${execResult.error || 'unknown error'}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
await ctx.reply(`❌ Switch failed: ${e?.message ?? String(e)}`);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Callback query handler (inline button presses for confirmations)
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
bot.on('callback_query:data', async (ctx) => {
|
|
414
|
+
const data = ctx.callbackQuery.data;
|
|
415
|
+
const chatId = ctx.chat?.id;
|
|
416
|
+
if (!chatId)
|
|
417
|
+
return;
|
|
418
|
+
const managed = sessions.get(chatId);
|
|
419
|
+
if (!managed?.confirmProvider) {
|
|
420
|
+
await ctx.answerCallbackQuery({ text: 'No active session.' }).catch(() => { });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const provider = managed.confirmProvider;
|
|
424
|
+
const handled = await provider.handleCallback(data);
|
|
425
|
+
await ctx.answerCallbackQuery(handled ? undefined : { text: 'Unknown action.' }).catch(() => { });
|
|
426
|
+
});
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Message handler (core flow)
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
bot.on('message:text', async (ctx) => {
|
|
431
|
+
const chatId = ctx.chat.id;
|
|
432
|
+
const userId = ctx.from.id;
|
|
433
|
+
const text = ctx.message.text;
|
|
434
|
+
// Skip commands (already handled above)
|
|
435
|
+
if (text.startsWith('/'))
|
|
436
|
+
return;
|
|
437
|
+
const msgPreview = text.length > 50 ? text.slice(0, 47) + '...' : text;
|
|
438
|
+
console.error(`[bot] ${chatId} ${ctx.from.username ?? userId}: "${msgPreview}"`);
|
|
439
|
+
// Get or create session
|
|
440
|
+
const managed = await sessions.getOrCreate(chatId, userId);
|
|
441
|
+
if (!managed) {
|
|
442
|
+
await ctx.reply('⚠️ Too many active sessions. Try again later or /reset an existing one.');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
// Concurrency guard
|
|
446
|
+
if (managed.inFlight) {
|
|
447
|
+
if (managed.pendingQueue.length >= sessions.maxQueue) {
|
|
448
|
+
await ctx.reply(`⏳ Queued (${managed.pendingQueue.length} pending). Use /cancel to abort the current task.`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
managed.pendingQueue.push(text);
|
|
452
|
+
await ctx.reply(`⏳ Queued (#${managed.pendingQueue.length}). Still working on the previous request.`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const fileThreshold = botConfig.file_threshold_chars ?? 8192;
|
|
456
|
+
await processMessage(bot, sessions, managed, text, editIntervalMs, fileThreshold, ctx.message.message_id);
|
|
457
|
+
});
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Session cleanup on timeout
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
const origCleanup = sessions.cleanupExpired.bind(sessions);
|
|
462
|
+
const wrappedCleanup = () => {
|
|
463
|
+
const expired = origCleanup();
|
|
464
|
+
for (const chatId of expired) {
|
|
465
|
+
console.error(`[bot] session ${chatId} expired`);
|
|
466
|
+
bot.api.sendMessage(chatId, '⏱ Session expired due to inactivity. Send a new message to start fresh.').catch(() => { });
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
// Override the internal cleanup to also notify users
|
|
470
|
+
// (The SessionManager calls cleanupExpired internally on interval;
|
|
471
|
+
// we handle notification here on the bot level.)
|
|
472
|
+
const cleanupInterval = setInterval(wrappedCleanup, 60_000);
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Graceful shutdown
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
const shutdown = () => {
|
|
477
|
+
console.error('[bot] Shutting down...');
|
|
478
|
+
clearInterval(cleanupInterval);
|
|
479
|
+
sessions.stop();
|
|
480
|
+
bot.stop();
|
|
481
|
+
process.exit(0);
|
|
482
|
+
};
|
|
483
|
+
process.on('SIGINT', shutdown);
|
|
484
|
+
process.on('SIGTERM', shutdown);
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Register commands with Telegram
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
await bot.api.setMyCommands([
|
|
489
|
+
{ command: 'start', description: 'Welcome + config summary' },
|
|
490
|
+
{ command: 'help', description: 'List commands' },
|
|
491
|
+
{ command: 'reset', description: 'Clear session' },
|
|
492
|
+
{ command: 'cancel', description: 'Abort current generation' },
|
|
493
|
+
{ command: 'status', description: 'Session stats' },
|
|
494
|
+
{ command: 'dir', description: 'Get/set working directory' },
|
|
495
|
+
{ command: 'model', description: 'Show current model' },
|
|
496
|
+
{ command: 'compact', description: 'Compact context' },
|
|
497
|
+
{ command: 'approval', description: 'Get/set approval mode' },
|
|
498
|
+
{ command: 'mode', description: 'Get/set mode (code/sys)' },
|
|
499
|
+
{ command: 'changes', description: 'Files modified this session' },
|
|
500
|
+
{ command: 'undo', description: 'Undo last edit' },
|
|
501
|
+
{ command: 'vault', description: 'Search vault entries' },
|
|
502
|
+
{ command: 'hosts', description: 'List runtime hosts' },
|
|
503
|
+
{ command: 'backends', description: 'List runtime backends' },
|
|
504
|
+
{ command: 'rtmodels', description: 'List runtime models' },
|
|
505
|
+
{ command: 'rtstatus', description: 'Show active runtime status' },
|
|
506
|
+
{ command: 'switch', description: 'Switch runtime model' },
|
|
507
|
+
{ command: 'anton', description: 'Autonomous task runner' },
|
|
508
|
+
]).catch((e) => console.error(`[bot] setMyCommands failed: ${e?.message}`));
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
// Start polling
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// BotFather hardening check
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
try {
|
|
516
|
+
const botInfo = await bot.api.getMe();
|
|
517
|
+
if (botInfo.can_join_groups) {
|
|
518
|
+
console.error('[bot] ⚠️ WARNING: Bot has "Allow Groups" enabled in BotFather.');
|
|
519
|
+
console.error('[bot] Groups are blocked in code, but disable at the source:');
|
|
520
|
+
console.error('[bot] → Open @BotFather → /mybots → select bot → Bot Settings → Allow Groups → Turn OFF');
|
|
521
|
+
}
|
|
522
|
+
if (botInfo.can_read_all_group_messages) {
|
|
523
|
+
console.error('[bot] ⚠️ WARNING: Bot has "Group Privacy" disabled (can read all messages).');
|
|
524
|
+
console.error('[bot] → Open @BotFather → /mybots → select bot → Bot Settings → Group Privacy → Turn ON');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch (e) {
|
|
528
|
+
console.error(`[bot] getMe() failed: ${e?.message ?? e}`);
|
|
529
|
+
}
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// Start polling
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
sessions.start();
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
// Global error handler — catches unhandled errors in middleware/handlers
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
bot.catch(async (err) => {
|
|
538
|
+
const desc = err?.error?.message ?? err?.message ?? String(err);
|
|
539
|
+
console.error(`[bot] unhandled error: ${desc}`);
|
|
540
|
+
const chatId = err?.ctx?.chat?.id;
|
|
541
|
+
if (!chatId)
|
|
542
|
+
return;
|
|
543
|
+
const userMsg = desc.length > 300 ? desc.slice(0, 297) + '...' : desc;
|
|
544
|
+
await bot.api.sendMessage(chatId, `⚠️ Bot error: ${userMsg}`).catch(() => { });
|
|
545
|
+
});
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// Start polling
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
console.error(`[bot] Telegram bot started (polling)`);
|
|
550
|
+
console.error(`[bot] Model: ${config.model || 'auto'} | Endpoint: ${config.endpoint}`);
|
|
551
|
+
console.error(`[bot] Allowed users: [${[...allowedUsers].join(', ')}]`);
|
|
552
|
+
console.error(`[bot] Default dir: ${botConfig.default_dir || config.dir || '~'}`);
|
|
553
|
+
bot.start({
|
|
554
|
+
onStart: () => console.error('[bot] Polling active'),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
async function probeModelEndpoint(endpoint) {
|
|
558
|
+
const base = endpoint.replace(/\/$/, '');
|
|
559
|
+
const healthUrl = base.replace(/\/v1$/, '') + '/health';
|
|
560
|
+
const modelsUrl = base.replace(/\/$/, '') + '/models';
|
|
561
|
+
try {
|
|
562
|
+
const h = await fetch(healthUrl, { method: 'GET' });
|
|
563
|
+
if (!h.ok)
|
|
564
|
+
return false;
|
|
565
|
+
const m = await fetch(modelsUrl, { method: 'GET' });
|
|
566
|
+
return m.ok;
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function waitForModelEndpoint(endpoint, totalMs = 60_000, stepMs = 2_500) {
|
|
573
|
+
const started = Date.now();
|
|
574
|
+
while (Date.now() - started < totalMs) {
|
|
575
|
+
if (await probeModelEndpoint(endpoint))
|
|
576
|
+
return true;
|
|
577
|
+
await new Promise((r) => setTimeout(r, stepMs));
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// Core: process a user message through the agent
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
async function processMessage(bot, sessions, managed, text, editIntervalMs, fileThresholdChars, replyToId) {
|
|
585
|
+
const turn = sessions.beginTurn(managed.chatId);
|
|
586
|
+
if (!turn)
|
|
587
|
+
return;
|
|
588
|
+
const turnId = turn.turnId;
|
|
589
|
+
const streaming = new StreamingMessage(bot, managed.chatId, editIntervalMs, replyToId, fileThresholdChars);
|
|
590
|
+
await streaming.init();
|
|
591
|
+
const hooks = {
|
|
592
|
+
onToken: (t) => {
|
|
593
|
+
if (!sessions.isTurnActive(managed.chatId, turnId))
|
|
594
|
+
return;
|
|
595
|
+
sessions.markProgress(managed.chatId, turnId);
|
|
596
|
+
streaming.onToken(t);
|
|
597
|
+
},
|
|
598
|
+
onToolCall: (call) => {
|
|
599
|
+
if (!sessions.isTurnActive(managed.chatId, turnId))
|
|
600
|
+
return;
|
|
601
|
+
sessions.markProgress(managed.chatId, turnId);
|
|
602
|
+
streaming.onToolCall(call);
|
|
603
|
+
},
|
|
604
|
+
onToolResult: (result) => {
|
|
605
|
+
if (!sessions.isTurnActive(managed.chatId, turnId))
|
|
606
|
+
return;
|
|
607
|
+
sessions.markProgress(managed.chatId, turnId);
|
|
608
|
+
streaming.onToolResult(result);
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
const watchdogMs = 120_000;
|
|
612
|
+
const watchdog = setInterval(() => {
|
|
613
|
+
const current = sessions.get(managed.chatId);
|
|
614
|
+
if (!current || current.activeTurnId !== turnId || !current.inFlight)
|
|
615
|
+
return;
|
|
616
|
+
if (Date.now() - current.lastProgressAt > watchdogMs) {
|
|
617
|
+
console.error(`[bot] ${managed.chatId} watchdog timeout on turn ${turnId}`);
|
|
618
|
+
sessions.cancelActive(managed.chatId);
|
|
619
|
+
}
|
|
620
|
+
}, 5_000);
|
|
621
|
+
const startTime = Date.now();
|
|
622
|
+
try {
|
|
623
|
+
const result = await managed.session.ask(text, { ...hooks, signal: turn.controller.signal });
|
|
624
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
625
|
+
console.error(`[bot] ${managed.chatId} ask() completed: ${result.turns} turns, ${result.toolCalls} tool calls, ${elapsed}s`);
|
|
626
|
+
if (sessions.isTurnActive(managed.chatId, turnId))
|
|
627
|
+
await streaming.finalize(result.text);
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
const msg = e?.message ?? String(e);
|
|
631
|
+
console.error(`[bot] ${managed.chatId} ask() error: ${msg}`);
|
|
632
|
+
if (msg.includes('aborted') || msg.includes('AbortError')) {
|
|
633
|
+
if (sessions.isTurnActive(managed.chatId, turnId))
|
|
634
|
+
await streaming.finalizeError('Aborted.');
|
|
635
|
+
}
|
|
636
|
+
else if (msg.includes('ECONNREFUSED') || msg.includes('Connection timeout') || msg.includes('503') || msg.includes('model loading')) {
|
|
637
|
+
const endpoint = managed.session?.endpoint || '';
|
|
638
|
+
const recovered = endpoint ? await waitForModelEndpoint(endpoint, 60_000, 2_500) : false;
|
|
639
|
+
if (recovered) {
|
|
640
|
+
try {
|
|
641
|
+
const retry = await managed.session.ask(text, { ...hooks, signal: turn.controller.signal });
|
|
642
|
+
if (sessions.isTurnActive(managed.chatId, turnId))
|
|
643
|
+
await streaming.finalize(retry.text);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
catch (retryErr) {
|
|
647
|
+
const retryMsg = retryErr?.message ?? String(retryErr);
|
|
648
|
+
if (sessions.isTurnActive(managed.chatId, turnId))
|
|
649
|
+
await streaming.finalizeError(`Model server came back but retry failed: ${retryMsg.length > 140 ? retryMsg.slice(0, 137) + '...' : retryMsg}`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (sessions.isTurnActive(managed.chatId, turnId))
|
|
654
|
+
await streaming.finalizeError('Model server is starting up or restarting. I waited up to 60s but it is still unavailable — please retry shortly.');
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
if (sessions.isTurnActive(managed.chatId, turnId))
|
|
658
|
+
await streaming.finalizeError(msg.length > 200 ? msg.slice(0, 197) + '...' : msg);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
clearInterval(watchdog);
|
|
663
|
+
const current = sessions.finishTurn(managed.chatId, turnId);
|
|
664
|
+
if (!current)
|
|
665
|
+
return;
|
|
666
|
+
// Process queued messages only if this session still exists and is idle.
|
|
667
|
+
const next = sessions.dequeueNext(managed.chatId);
|
|
668
|
+
if (next && current.state === 'idle' && !current.inFlight) {
|
|
669
|
+
setTimeout(() => {
|
|
670
|
+
const fresh = sessions.get(managed.chatId);
|
|
671
|
+
if (!fresh)
|
|
672
|
+
return;
|
|
673
|
+
void processMessage(bot, sessions, fresh, next, editIntervalMs, fileThresholdChars);
|
|
674
|
+
}, 500);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
//# sourceMappingURL=telegram.js.map
|