talon-agent 3.12.1 → 3.12.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.3",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
|
103
103
|
"@kilocode/sdk": "^7.2.22",
|
|
104
104
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
105
|
-
"@openai/agents": "^0.
|
|
105
|
+
"@openai/agents": "^0.14.0",
|
|
106
106
|
"@openai/codex-sdk": "^0.145.0",
|
|
107
107
|
"@opencode-ai/sdk": "^1.17.4",
|
|
108
108
|
"@playwright/mcp": "0.0.78",
|
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
MAX_OVERDUE_MS,
|
|
16
16
|
type ScheduledMessage,
|
|
17
17
|
} from "../../../storage/scheduled-store.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
noteRichMessageFailure,
|
|
20
|
+
richMessagesAvailable,
|
|
21
|
+
sendText,
|
|
22
|
+
toPositiveId,
|
|
23
|
+
} from "./shared.js";
|
|
19
24
|
import { TELEGRAM_MAX_TEXT, type TelegramActionHandlers } from "./types.js";
|
|
20
25
|
|
|
21
26
|
// ── Inline keyboards ─────────────────────────────────────────────────────────
|
|
@@ -135,10 +140,8 @@ async function fireScheduled(bot: Bot, entry: ScheduledMessage): Promise<void> {
|
|
|
135
140
|
return;
|
|
136
141
|
}
|
|
137
142
|
const keyboard = built.keyboard;
|
|
138
|
-
await bot
|
|
139
|
-
|
|
140
|
-
reply_markup: { inline_keyboard: keyboard },
|
|
141
|
-
reply_parameters: replyTo ? { message_id: replyTo } : undefined,
|
|
143
|
+
await sendText(bot, chatId, entry.text, replyTo, {
|
|
144
|
+
inline_keyboard: keyboard,
|
|
142
145
|
});
|
|
143
146
|
} else {
|
|
144
147
|
await sendText(bot, chatId, entry.text, replyTo);
|
|
@@ -246,12 +249,24 @@ export const messagingHandlers: TelegramActionHandlers = {
|
|
|
246
249
|
ok: false,
|
|
247
250
|
error: `Text too long (max ${TELEGRAM_MAX_TEXT})`,
|
|
248
251
|
};
|
|
249
|
-
const html = markdownToTelegramHtml(text);
|
|
250
252
|
await withRetry(async () => {
|
|
253
|
+
if (richMessagesAvailable()) {
|
|
254
|
+
try {
|
|
255
|
+
await bot.api.editMessageText(chatId, Number(body.message_id), {
|
|
256
|
+
markdown: text,
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
} catch (richErr) {
|
|
260
|
+
noteRichMessageFailure(richErr, `edit chat=${chatId}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
251
263
|
try {
|
|
252
|
-
await bot.api.editMessageText(
|
|
253
|
-
|
|
254
|
-
|
|
264
|
+
await bot.api.editMessageText(
|
|
265
|
+
chatId,
|
|
266
|
+
Number(body.message_id),
|
|
267
|
+
markdownToTelegramHtml(text),
|
|
268
|
+
{ parse_mode: "HTML" },
|
|
269
|
+
);
|
|
255
270
|
} catch {
|
|
256
271
|
await bot.api.editMessageText(chatId, Number(body.message_id), text);
|
|
257
272
|
}
|
|
@@ -309,24 +324,15 @@ export const messagingHandlers: TelegramActionHandlers = {
|
|
|
309
324
|
const text = String(body.text ?? "");
|
|
310
325
|
if (text.length > TELEGRAM_MAX_TEXT)
|
|
311
326
|
return { ok: false, error: `Text too long` };
|
|
312
|
-
const html = markdownToTelegramHtml(text);
|
|
313
327
|
const rows = body.rows as ButtonSpec[][];
|
|
314
328
|
const built = buildInlineKeyboard(rows);
|
|
315
329
|
if ("error" in built) return { ok: false, error: built.error };
|
|
316
330
|
const keyboard = built.keyboard;
|
|
317
331
|
gateway.incrementMessages(chatId);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
});
|
|
323
|
-
return { ok: true, message_id: sent.message_id };
|
|
324
|
-
} catch {
|
|
325
|
-
const sent = await bot.api.sendMessage(chatId, text, {
|
|
326
|
-
reply_markup: { inline_keyboard: keyboard },
|
|
327
|
-
});
|
|
328
|
-
return { ok: true, message_id: sent.message_id };
|
|
329
|
-
}
|
|
332
|
+
const messageId = await sendText(bot, chatId, text, undefined, {
|
|
333
|
+
inline_keyboard: keyboard,
|
|
334
|
+
});
|
|
335
|
+
return { ok: true, message_id: messageId };
|
|
330
336
|
},
|
|
331
337
|
|
|
332
338
|
schedule_message: (body, chatId, { bot, scheduledMessages }) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared helpers for Telegram action handlers: reply-parameter extraction and
|
|
3
|
-
*
|
|
3
|
+
* native Rich Markdown delivery with legacy fallbacks.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Bot } from "grammy";
|
|
@@ -27,31 +27,97 @@ export function toPositiveId(v: unknown): number | undefined {
|
|
|
27
27
|
return Number.isInteger(n) && n > 0 ? n : undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Rich Messages arrived in Bot API 10.2. A self-hosted Bot API server (or a
|
|
32
|
+
* deployment pinned to an older release) rejects `sendRichMessage` outright,
|
|
33
|
+
* and retrying it per message would cost a doomed round-trip plus a warning
|
|
34
|
+
* line on every single send. Probe once and latch, mirroring the
|
|
35
|
+
* `draftsSupported` capability probe in `handlers/delivery.ts`.
|
|
36
|
+
*/
|
|
37
|
+
let richMessagesSupported = true;
|
|
38
|
+
|
|
39
|
+
/** Telegram's shape for "this build doesn't have that method". */
|
|
40
|
+
const METHOD_UNAVAILABLE_RE =
|
|
41
|
+
/method not found|not supported|unknown method|unsupported method/i;
|
|
42
|
+
|
|
43
|
+
/** True while native Rich Markdown delivery is still worth attempting. */
|
|
44
|
+
export function richMessagesAvailable(): boolean {
|
|
45
|
+
return richMessagesSupported;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Log a failed Rich Message call and latch the capability off when the failure
|
|
50
|
+
* says the method itself is missing. Payload-level rejections (a markdown
|
|
51
|
+
* string Telegram won't parse) stay one-off — the next message may be fine.
|
|
52
|
+
*/
|
|
53
|
+
export function noteRichMessageFailure(err: unknown, context: string): void {
|
|
54
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55
|
+
if (METHOD_UNAVAILABLE_RE.test(msg)) {
|
|
56
|
+
richMessagesSupported = false;
|
|
57
|
+
logWarn(
|
|
58
|
+
"bot",
|
|
59
|
+
`Rich Messages unavailable on this Bot API server — using legacy HTML from now on (${context}): ${msg}`,
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
logWarn(
|
|
64
|
+
"bot",
|
|
65
|
+
`Rich Markdown failed; falling back to HTML (${context}): ${msg}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Test seam: re-arm the capability probe between cases. */
|
|
70
|
+
export function resetRichMessageSupport(): void {
|
|
71
|
+
richMessagesSupported = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
30
74
|
export async function sendText(
|
|
31
75
|
bot: Bot,
|
|
32
76
|
chatId: number,
|
|
33
77
|
text: string,
|
|
34
78
|
replyTo?: number,
|
|
79
|
+
replyMarkup?: NonNullable<
|
|
80
|
+
Parameters<Bot["api"]["sendRichMessage"]>[2]
|
|
81
|
+
>["reply_markup"],
|
|
35
82
|
): Promise<number> {
|
|
36
83
|
if (text.length > TELEGRAM_MAX_TEXT) {
|
|
37
84
|
throw new Error(
|
|
38
85
|
`Message too long (${text.length} chars, max ${TELEGRAM_MAX_TEXT}).`,
|
|
39
86
|
);
|
|
40
87
|
}
|
|
88
|
+
|
|
89
|
+
if (richMessagesSupported) {
|
|
90
|
+
try {
|
|
91
|
+
const sent = await bot.api.sendRichMessage(
|
|
92
|
+
chatId,
|
|
93
|
+
{ markdown: text },
|
|
94
|
+
{
|
|
95
|
+
reply_parameters: replyTo ? { message_id: replyTo } : undefined,
|
|
96
|
+
reply_markup: replyMarkup,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
return sent.message_id;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
noteRichMessageFailure(err, `send chat=${chatId}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
41
105
|
const html = markdownToTelegramHtml(text);
|
|
42
106
|
try {
|
|
43
107
|
const sent = await bot.api.sendMessage(chatId, html, {
|
|
44
108
|
parse_mode: "HTML",
|
|
45
109
|
reply_parameters: replyTo ? { message_id: replyTo } : undefined,
|
|
110
|
+
reply_markup: replyMarkup,
|
|
46
111
|
});
|
|
47
112
|
return sent.message_id;
|
|
48
113
|
} catch (err) {
|
|
49
114
|
logWarn(
|
|
50
115
|
"bot",
|
|
51
|
-
`
|
|
116
|
+
`Legacy HTML send failed; retrying as plain text (chat=${chatId}): ${err instanceof Error ? err.message : err}`,
|
|
52
117
|
);
|
|
53
118
|
const sent = await bot.api.sendMessage(chatId, text, {
|
|
54
119
|
reply_parameters: replyTo ? { message_id: replyTo } : undefined,
|
|
120
|
+
reply_markup: replyMarkup,
|
|
55
121
|
});
|
|
56
122
|
return sent.message_id;
|
|
57
123
|
}
|
|
@@ -52,8 +52,18 @@ function isWellFormedHtml(html: string): boolean {
|
|
|
52
52
|
return stack.length === 0;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** `\x00URLn\x00` → the stashed URL, or undefined when it isn't a placeholder. */
|
|
56
|
+
function resolveUrlPlaceholder(
|
|
57
|
+
target: string,
|
|
58
|
+
urls: string[],
|
|
59
|
+
): string | undefined {
|
|
60
|
+
// oxlint-disable-next-line no-control-regex
|
|
61
|
+
const match = /^\x00URL(\d+)\x00$/.exec(target);
|
|
62
|
+
return match ? urls[Number(match[1])] : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
/** Apply the inline delimiter passes to already-escaped text. */
|
|
56
|
-
function applyInlineFormatting(input: string): string {
|
|
66
|
+
function applyInlineFormatting(input: string, urls: string[]): string {
|
|
57
67
|
let out = input;
|
|
58
68
|
// Bold+italic: ***text*** — must run before the ** and * passes, which
|
|
59
69
|
// would otherwise split it into the crossed pair `<b><i>x</b></i>`.
|
|
@@ -64,13 +74,16 @@ function applyInlineFormatting(input: string): string {
|
|
|
64
74
|
out = out.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<i>$1</i>");
|
|
65
75
|
// Italic: _text_ (surrounded by non-word or start/end)
|
|
66
76
|
out = out.replace(/(?<!\w)_(.+?)_(?!\w)/g, "<i>$1</i>");
|
|
67
|
-
// Links: [text](url) — only safe URL schemes become anchors.
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
77
|
+
// Links: [text](url) — only safe URL schemes become anchors. The target
|
|
78
|
+
// arrives as a `\x00URLn\x00` placeholder (step 2b), so the passes above
|
|
79
|
+
// never saw the URL's own punctuation; resolve it back here. Both text and
|
|
80
|
+
// url were already HTML-escaped (quotes included, so the href attribute
|
|
81
|
+
// can't be broken out of); escaping again here corrupted every & in a
|
|
82
|
+
// query string into &amp;.
|
|
83
|
+
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, target) => {
|
|
84
|
+
const url = resolveUrlPlaceholder(target, urls) ?? target;
|
|
85
|
+
return /^https?:\/\//i.test(url) ? `<a href="${url}">${text}</a>` : text;
|
|
86
|
+
});
|
|
74
87
|
// Strikethrough: ~~text~~
|
|
75
88
|
out = out.replace(/~~(.+?)~~/g, "<s>$1</s>");
|
|
76
89
|
return out;
|
|
@@ -110,6 +123,23 @@ export function markdownToTelegramHtml(text: string): string {
|
|
|
110
123
|
return placeholder;
|
|
111
124
|
});
|
|
112
125
|
|
|
126
|
+
// Step 2b: Stash link targets. The emphasis passes run before the link
|
|
127
|
+
// pass and have no idea what a URL is, so punctuation inside the target
|
|
128
|
+
// used to be consumed as a delimiter: a Chomikuj path like
|
|
129
|
+
// `Jak+Si*c4*99+Bawi*c4*85+Ludzie.mp3` reads as two italic runs, the
|
|
130
|
+
// resulting tags cross, and the well-formedness guard then discards the
|
|
131
|
+
// formatting for the *whole* message. Placeholders carry no delimiters,
|
|
132
|
+
// so the URL is invisible to every pass but the link one.
|
|
133
|
+
const urls: string[] = [];
|
|
134
|
+
processed = processed.replace(
|
|
135
|
+
/(\[[^\]\n]*\])\(([^()\s]+)\)/g,
|
|
136
|
+
(_match, label: string, url: string) => {
|
|
137
|
+
const placeholder = `\x00URL${urls.length}\x00`;
|
|
138
|
+
urls.push(escapeHtml(url));
|
|
139
|
+
return `${label}(${placeholder})`;
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
113
143
|
// Step 3: Escape HTML in remaining plain text (before applying formatting).
|
|
114
144
|
// Escape HTML in plain text segments (skip placeholders marked with \x00)
|
|
115
145
|
// oxlint-disable-next-line no-control-regex
|
|
@@ -119,11 +149,18 @@ export function markdownToTelegramHtml(text: string): string {
|
|
|
119
149
|
// actually parseable. `processed` at this point holds escaped text plus
|
|
120
150
|
// tag-free placeholders, so it is a safe unformatted fallback.
|
|
121
151
|
const unformatted = processed;
|
|
122
|
-
processed = applyInlineFormatting(processed);
|
|
152
|
+
processed = applyInlineFormatting(processed, urls);
|
|
123
153
|
if (!isWellFormedHtml(processed)) {
|
|
124
154
|
processed = unformatted;
|
|
125
155
|
}
|
|
126
156
|
|
|
157
|
+
// Step 4b: Restore any link target that never became an anchor — a
|
|
158
|
+
// non-http scheme, or the unformatted fallback above. Anchors already
|
|
159
|
+
// resolved their placeholder inline.
|
|
160
|
+
for (let i = 0; i < urls.length; i++) {
|
|
161
|
+
processed = processed.replace(`\x00URL${i}\x00`, () => urls[i]!);
|
|
162
|
+
}
|
|
163
|
+
|
|
127
164
|
// Steps 5+6: Restore code spans and fenced blocks. The replacement MUST
|
|
128
165
|
// go through a function: with a string, String.replace interprets $-
|
|
129
166
|
// substitution patterns in the *code content* ($& re-inserts the
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Bot } from "grammy";
|
|
7
7
|
import type { TalonConfig } from "../../../util/config.js";
|
|
8
|
-
import { markdownToTelegramHtml } from "../formatting.js";
|
|
9
8
|
import { execute } from "../../../core/engine/dispatcher.js";
|
|
10
9
|
import { toolInputToRecord } from "../../../core/agent-runtime/events.js";
|
|
11
10
|
import { appendDailyLogResponse } from "../../../storage/daily-log.js";
|
|
12
11
|
import { stripMcpPrefix } from "../../../core/tools/index.js";
|
|
13
12
|
import { logWarn } from "../../../util/log.js";
|
|
13
|
+
import { sendText } from "../actions/shared.js";
|
|
14
14
|
import { trackDmUser } from "./access.js";
|
|
15
15
|
|
|
16
16
|
export async function sendHtml(
|
|
@@ -112,7 +112,7 @@ function createStreamCallbacks(
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
const onTextBlock = async (text: string) => {
|
|
115
|
-
await
|
|
115
|
+
await sendText(bot, chatId, text, _replyToId);
|
|
116
116
|
appendDailyLogResponse("Talon", text, { chatTitle });
|
|
117
117
|
state.lastSentLength = 0;
|
|
118
118
|
state.sentTextBlock = true;
|