talon-agent 3.12.0 → 3.12.2
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 +2 -2
- package/src/core/engine/gateway-actions/index.ts +26 -1
- package/src/core/engine/gateway-actions/mesh.ts +33 -0
- package/src/core/engine/gateway.ts +30 -1
- package/src/frontend/telegram/actions/messaging.ts +28 -22
- package/src/frontend/telegram/actions/shared.ts +68 -2
- package/src/frontend/telegram/handlers/delivery.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.2",
|
|
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",
|
|
@@ -31,7 +31,7 @@ import { scriptHandlers } from "./scripts.js";
|
|
|
31
31
|
import { skillHandlers } from "./skills.js";
|
|
32
32
|
import { pluginHandlers } from "./plugins.js";
|
|
33
33
|
import { modelHandlers } from "./models.js";
|
|
34
|
-
import { meshHandlers } from "./mesh.js";
|
|
34
|
+
import { meshHandlers, chatFreeActions } from "./mesh.js";
|
|
35
35
|
import { nativeHandlers } from "./native.js";
|
|
36
36
|
|
|
37
37
|
// Null-prototype so a request `action` of "toString" / "constructor" / etc.
|
|
@@ -62,4 +62,29 @@ export async function handleSharedAction(
|
|
|
62
62
|
return handler(body, chatId, backend);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* True when the action needs no chat context at all (see `chatFreeActions`).
|
|
67
|
+
* The gateway checks this before resolving a chat so mesh tools stay reachable
|
|
68
|
+
* from heartbeat/background runs.
|
|
69
|
+
*/
|
|
70
|
+
export function isChatFreeAction(action: string): boolean {
|
|
71
|
+
return chatFreeActions.has(action);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Dispatch a chat-free action. The handler signature still takes a chatId
|
|
76
|
+
* (they all share one type); chat-free handlers ignore it, and `0` is passed
|
|
77
|
+
* as an explicit "no chat" sentinel rather than a real id.
|
|
78
|
+
*/
|
|
79
|
+
export async function handleChatFreeAction(
|
|
80
|
+
body: Record<string, unknown>,
|
|
81
|
+
): Promise<ActionResult | null> {
|
|
82
|
+
const action = body.action as string;
|
|
83
|
+
if (!isChatFreeAction(action)) return null;
|
|
84
|
+
const handler = handlers[action];
|
|
85
|
+
if (!handler) return null;
|
|
86
|
+
return handler(body, 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { chatFreeActions };
|
|
65
90
|
export type { SharedActionHandler, SharedActionHandlers } from "./types.js";
|
|
@@ -78,3 +78,36 @@ export const meshHandlers: SharedActionHandlers = {
|
|
|
78
78
|
body.bridge_url,
|
|
79
79
|
),
|
|
80
80
|
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Mesh actions are chat-free: every handler above reads `body` and the
|
|
84
|
+
* daemon-wide mesh registry, and not one of them takes the `chatId` the
|
|
85
|
+
* gateway threads through. Requiring an active chat context to reach them is
|
|
86
|
+
* therefore incidental to the transport, not a property of the action — and
|
|
87
|
+
* it makes the whole mesh unreachable from heartbeat/background runs, which
|
|
88
|
+
* have no ambient chat and no `chat_id` parameter on these tools to promote.
|
|
89
|
+
*
|
|
90
|
+
* The gateway consults this set before chat resolution so `list_devices`,
|
|
91
|
+
* `device_exec` and friends work from any run mode. Kept as an explicit
|
|
92
|
+
* export (rather than `Object.keys(meshHandlers)`) so adding a handler that
|
|
93
|
+
* *does* need a chat is a deliberate opt-out rather than a silent inclusion.
|
|
94
|
+
*/
|
|
95
|
+
export const chatFreeActions: ReadonlySet<string> = new Set([
|
|
96
|
+
"list_devices",
|
|
97
|
+
"get_device_location",
|
|
98
|
+
"get_device_history",
|
|
99
|
+
"ring_device",
|
|
100
|
+
"remove_device",
|
|
101
|
+
"get_device_status",
|
|
102
|
+
"device_exec",
|
|
103
|
+
"device_list_dir",
|
|
104
|
+
"device_stat",
|
|
105
|
+
"device_read_file",
|
|
106
|
+
"device_write_file",
|
|
107
|
+
"device_pull_file",
|
|
108
|
+
"device_push_file",
|
|
109
|
+
"update_device",
|
|
110
|
+
"update_node",
|
|
111
|
+
"get_node_binary",
|
|
112
|
+
"make_node_install_link",
|
|
113
|
+
]);
|
|
@@ -20,7 +20,11 @@ import { Loom, getActiveLoom, type ContextRegistry } from "../weaver/index.js";
|
|
|
20
20
|
import { getHealthStatus } from "../../util/watchdog.js";
|
|
21
21
|
import { getActiveSessionCount } from "../../storage/sessions.js";
|
|
22
22
|
import { log, logError, logDebug } from "../../util/log.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
handleSharedAction,
|
|
25
|
+
handleChatFreeAction,
|
|
26
|
+
isChatFreeAction,
|
|
27
|
+
} from "./gateway-actions/index.js";
|
|
24
28
|
import {
|
|
25
29
|
handleHubRequest,
|
|
26
30
|
getHubSessionCount,
|
|
@@ -249,6 +253,31 @@ export class Gateway {
|
|
|
249
253
|
// active-context-required check — the action handler will reach the
|
|
250
254
|
// chat directly via the Telegram Bot API. The legacy context-required
|
|
251
255
|
// path remains for chat-mode calls where `chat_id` is absent.
|
|
256
|
+
// Chat-free actions (the device mesh) short-circuit routing entirely:
|
|
257
|
+
// they read daemon-wide state, ignore chatId, and are the only command
|
|
258
|
+
// channel a heartbeat run has to a remote box. Gating them behind an
|
|
259
|
+
// active chat made the whole mesh unreachable from background runs —
|
|
260
|
+
// and unlike send/react they carry no `chat_id` param to promote.
|
|
261
|
+
const requestedAction =
|
|
262
|
+
typeof body.action === "string" ? body.action : undefined;
|
|
263
|
+
if (requestedAction && isChatFreeAction(requestedAction)) {
|
|
264
|
+
const t0 = Date.now();
|
|
265
|
+
try {
|
|
266
|
+
const result = await handleChatFreeAction(body);
|
|
267
|
+
if (result) {
|
|
268
|
+
logDebug(
|
|
269
|
+
"gateway",
|
|
270
|
+
`${requestedAction} chat=none ${Date.now() - t0}ms (chat-free)`,
|
|
271
|
+
);
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
276
|
+
logError("gateway", `${requestedAction} (chat-free) failed: ${msg}`);
|
|
277
|
+
return { ok: false, error: `${requestedAction}: ${msg}` };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
252
281
|
const rawChatId = body._chatId ? String(body._chatId) : "";
|
|
253
282
|
const numericId = Number(rawChatId);
|
|
254
283
|
const explicitChatIdProvided = typeof body.chat_id !== "undefined";
|
|
@@ -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
|
}
|
|
@@ -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;
|