talon-agent 3.21.0 → 3.22.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/package.json +2 -2
- package/src/app.ts +15 -5
- package/src/backend/claude-sdk/handler.ts +20 -11
- package/src/backend/remote-server/mcp.ts +15 -2
- package/src/backend/shared/cache-telemetry.ts +42 -0
- package/src/backend/shared/index.ts +3 -2
- package/src/core/engine/dispatcher.ts +10 -0
- package/src/core/errors.ts +11 -0
- package/src/core/mcp-hub/children.ts +45 -1
- package/src/core/tasks/table.ts +14 -0
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/messaging.ts +154 -8
- package/src/core/tools/moderation.ts +131 -0
- package/src/core/tools/types.ts +1 -0
- package/src/frontend/discord/handlers/queue.ts +22 -2
- package/src/frontend/telegram/actions/index.ts +2 -0
- package/src/frontend/telegram/actions/media.ts +143 -31
- package/src/frontend/telegram/actions/messaging.ts +78 -13
- package/src/frontend/telegram/actions/moderation.ts +255 -0
- package/src/frontend/telegram/actions/shared.ts +64 -6
- package/src/frontend/telegram/handlers/delivery.ts +9 -4
- package/src/frontend/telegram/handlers/queue.ts +23 -3
- package/src/frontend/telegram/index.ts +16 -8
- package/src/frontend/telegram/join-requests.ts +53 -0
- package/src/frontend/telegram/middleware.ts +25 -0
- package/src/frontend/telegram/topics.ts +63 -0
- package/src/frontend/telegram/userbot.ts +11 -0
- package/src/util/watchdog.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.1",
|
|
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",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"cross-spawn": "^7.0.6",
|
|
114
114
|
"discord.js": "^14.16.3",
|
|
115
115
|
"file-type": "^22.0.1",
|
|
116
|
-
"grammy": "^1.
|
|
116
|
+
"grammy": "^1.45.1",
|
|
117
117
|
"liquidjs": "^10.27.0",
|
|
118
118
|
"marked": "^18.0.0",
|
|
119
119
|
"mem0ai": "^3.0.13",
|
package/src/app.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { getFrontends } from "./util/config.js";
|
|
10
10
|
import { startUploadCleanup, stopUploadCleanup } from "./util/workspace.js";
|
|
11
11
|
import { flushDatabase } from "./storage/db.js";
|
|
12
|
-
import { getActiveCount } from "./core/engine/dispatcher.js";
|
|
12
|
+
import { getActiveCount, stopAllTurns } from "./core/engine/dispatcher.js";
|
|
13
13
|
import { startPulseTimer, stopPulseTimer } from "./core/background/pulse.js";
|
|
14
14
|
import { stopPlanAlerts } from "./core/background/plan-alerts.js";
|
|
15
15
|
import {
|
|
@@ -121,6 +121,7 @@ async function gracefulShutdown(signal: string): Promise<void> {
|
|
|
121
121
|
shuttingDown = true;
|
|
122
122
|
log("shutdown", `${signal} received, shutting down gracefully...`);
|
|
123
123
|
|
|
124
|
+
const deadlineAt = Date.now() + SHUTDOWN_TIMEOUT_MS;
|
|
124
125
|
const forceTimer = setTimeout(() => {
|
|
125
126
|
logError("shutdown", "Timeout exceeded, forcing exit");
|
|
126
127
|
// Hand off even on the forced path. A restart must survive a
|
|
@@ -135,12 +136,17 @@ async function gracefulShutdown(signal: string): Promise<void> {
|
|
|
135
136
|
}, SHUTDOWN_TIMEOUT_MS);
|
|
136
137
|
forceTimer.unref();
|
|
137
138
|
|
|
138
|
-
// Drain in-flight queries
|
|
139
|
-
//
|
|
139
|
+
// Drain in-flight queries. A turn can legitimately run for minutes, so a
|
|
140
|
+
// drain that only waits can never succeed against one — ask every running
|
|
141
|
+
// turn to abort first, then poll for the aborts to settle so backends can
|
|
142
|
+
// flush partial state before the process exits.
|
|
140
143
|
if (getActiveCount() > 0) {
|
|
144
|
+
const aborted = stopAllTurns();
|
|
141
145
|
log(
|
|
142
146
|
"shutdown",
|
|
143
|
-
`Waiting for ${getActiveCount()} in-flight queries to drain
|
|
147
|
+
`Waiting for ${getActiveCount()} in-flight queries to drain` +
|
|
148
|
+
(aborted > 0 ? ` (abort requested for ${aborted})` : "") +
|
|
149
|
+
`...`,
|
|
144
150
|
);
|
|
145
151
|
const deadline = Date.now() + DRAIN_TIMEOUT_MS;
|
|
146
152
|
while (getActiveCount() > 0 && Date.now() < deadline) {
|
|
@@ -177,7 +183,11 @@ async function gracefulShutdown(signal: string): Promise<void> {
|
|
|
177
183
|
await shutdownStep("pulse timer", stopPulseTimer);
|
|
178
184
|
await shutdownStep("heartbeat", async () => {
|
|
179
185
|
stopHeartbeatTimer();
|
|
180
|
-
|
|
186
|
+
// Cap the wait by what's left of the force-timer budget (minus margin
|
|
187
|
+
// for the steps below). The default 10s wait plus a full 5s drain used
|
|
188
|
+
// to consume the entire 15s budget, so a slow heartbeat tripped the
|
|
189
|
+
// forced exit even though teardown was proceeding normally.
|
|
190
|
+
await awaitHeartbeat(Math.max(0, deadlineAt - Date.now() - 3_000));
|
|
181
191
|
});
|
|
182
192
|
await shutdownStep("cron timer", stopCronTimer);
|
|
183
193
|
await shutdownStep("plan alerts", stopPlanAlerts);
|
|
@@ -67,8 +67,9 @@ import {
|
|
|
67
67
|
recordFailedTurnAccounting,
|
|
68
68
|
recordFlowViolation,
|
|
69
69
|
formatTurnCache,
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
crossTurnVerdict,
|
|
71
|
+
priorLookbackOverflow,
|
|
72
|
+
noteLookbackRisk,
|
|
72
73
|
CACHE_LOOKBACK_BLOCKS,
|
|
73
74
|
} from "../shared/index.js";
|
|
74
75
|
|
|
@@ -594,16 +595,24 @@ export async function* runChatTurn(
|
|
|
594
595
|
|
|
595
596
|
// The aggregate `cache=NN%` can't distinguish a turn that reused the
|
|
596
597
|
// previous turn's prefix from one that re-wrote it — see
|
|
597
|
-
// shared/cache-telemetry.ts.
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
)
|
|
598
|
+
// shared/cache-telemetry.ts. A lookback overflow only *predicts* a miss,
|
|
599
|
+
// so warn when this turn's verdict proves the previous turn's overflow
|
|
600
|
+
// cost a prefix re-write, then record this turn's overflow for the next.
|
|
601
|
+
if (state.cacheStats) {
|
|
602
|
+
const overflow = priorLookbackOverflow(chatId);
|
|
603
|
+
if (
|
|
604
|
+
overflow !== undefined &&
|
|
605
|
+
crossTurnVerdict(state.cacheStats) === "miss"
|
|
606
|
+
) {
|
|
607
|
+
logWarn(
|
|
608
|
+
"agent",
|
|
609
|
+
`[${chatId}] previous turn emitted ~${overflow} content blocks ` +
|
|
610
|
+
`(> ${CACHE_LOOKBACK_BLOCKS} lookback) and this turn's prefix ` +
|
|
611
|
+
`missed — that turn's cache write was likely never read`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
606
614
|
}
|
|
615
|
+
noteLookbackRisk(chatId, state.toolCalls);
|
|
607
616
|
|
|
608
617
|
log(
|
|
609
618
|
"agent",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* registrations run concurrently through the hub; later turns skip them.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { log, logWarn } from "../../util/log.js";
|
|
32
|
+
import { log, logDebug, logWarn } from "../../util/log.js";
|
|
33
33
|
import {
|
|
34
34
|
talonHubUrl,
|
|
35
35
|
pluginHubUrl,
|
|
@@ -62,6 +62,9 @@ export const TALON_PLUGIN_MCP_SERVER_NAME = "talon-plugin";
|
|
|
62
62
|
*/
|
|
63
63
|
const SLOW_MCP_REGISTRATION_MS = 1000;
|
|
64
64
|
|
|
65
|
+
/** Servers whose registration failure was already warned about (see below). */
|
|
66
|
+
const warnedMcpRegistrationFailures = new Set<string>();
|
|
67
|
+
|
|
65
68
|
/**
|
|
66
69
|
* OpenCode's `/experimental/tool/ids` endpoint omits dynamically registered
|
|
67
70
|
* MCP tools despite its API description. Synthesize Talon's known MCP ids so
|
|
@@ -253,6 +256,7 @@ export async function ensurePluginMcpServers<TClient extends RemoteAgentClient>(
|
|
|
253
256
|
state.registeredMcpServers.add(serverName);
|
|
254
257
|
state.registeredMcpTools.set(serverName, toolNames);
|
|
255
258
|
byPlugin.set(name, serverName);
|
|
259
|
+
warnedMcpRegistrationFailures.delete(serverName);
|
|
256
260
|
const ms = Date.now() - startedAt;
|
|
257
261
|
log(
|
|
258
262
|
"agent",
|
|
@@ -261,7 +265,16 @@ export async function ensurePluginMcpServers<TClient extends RemoteAgentClient>(
|
|
|
261
265
|
);
|
|
262
266
|
return serverName;
|
|
263
267
|
} catch (err) {
|
|
264
|
-
|
|
268
|
+
// Warn once per server, then debug: registration re-runs at the
|
|
269
|
+
// head of every turn, so a dead backing service (an offline
|
|
270
|
+
// browser endpoint, say) would otherwise emit one warning per
|
|
271
|
+
// turn for the whole outage. Success clears the latch so the
|
|
272
|
+
// next outage warns again.
|
|
273
|
+
const level = warnedMcpRegistrationFailures.has(serverName)
|
|
274
|
+
? logDebug
|
|
275
|
+
: logWarn;
|
|
276
|
+
warnedMcpRegistrationFailures.add(serverName);
|
|
277
|
+
level(
|
|
265
278
|
"agent",
|
|
266
279
|
`Plugin MCP registration failed for ${serverName}: ${errMsg(err)}`,
|
|
267
280
|
);
|
|
@@ -154,6 +154,48 @@ export function exceedsLookbackWindow(toolCalls: number): boolean {
|
|
|
154
154
|
return estimateTurnBlocks(toolCalls) > CACHE_LOOKBACK_BLOCKS;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Per-chat estimated block count of the last turn that overflowed the
|
|
159
|
+
* lookback window. Overflow is only a *prediction* of a cache miss — the
|
|
160
|
+
* proof is the NEXT turn's cross-turn verdict, so the overflow is recorded
|
|
161
|
+
* here and the warning waits for that verdict instead of firing on every
|
|
162
|
+
* tool-heavy turn. Bounded like `lastToolSets` below.
|
|
163
|
+
*/
|
|
164
|
+
const lookbackOverflows = new Map<string, number>();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Record whether this turn plausibly overflowed the lookback window, so the
|
|
168
|
+
* next turn can attribute a cross-turn miss to it.
|
|
169
|
+
*/
|
|
170
|
+
export function noteLookbackRisk(chatId: string, toolCalls: number): void {
|
|
171
|
+
if (!exceedsLookbackWindow(toolCalls)) {
|
|
172
|
+
lookbackOverflows.delete(chatId);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
lookbackOverflows.size >= MAX_TRACKED_CHATS &&
|
|
177
|
+
!lookbackOverflows.has(chatId)
|
|
178
|
+
) {
|
|
179
|
+
const oldest = lookbackOverflows.keys().next().value;
|
|
180
|
+
if (oldest !== undefined) lookbackOverflows.delete(oldest);
|
|
181
|
+
}
|
|
182
|
+
lookbackOverflows.set(chatId, estimateTurnBlocks(toolCalls));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Estimated block count of the chat's previous turn IF it overflowed the
|
|
187
|
+
* lookback window, else undefined. Read this before `noteLookbackRisk`
|
|
188
|
+
* records the current turn.
|
|
189
|
+
*/
|
|
190
|
+
export function priorLookbackOverflow(chatId: string): number | undefined {
|
|
191
|
+
return lookbackOverflows.get(chatId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Drop all recorded overflows (tests / explicit reset). */
|
|
195
|
+
export function resetLookbackRisk(): void {
|
|
196
|
+
lookbackOverflows.clear();
|
|
197
|
+
}
|
|
198
|
+
|
|
157
199
|
// ── Cacheable minimum ──────────────────────────────────────────────────────
|
|
158
200
|
|
|
159
201
|
/**
|
|
@@ -73,8 +73,9 @@ export {
|
|
|
73
73
|
// barrel discipline the prompt/ barrel was just trimmed to.
|
|
74
74
|
export {
|
|
75
75
|
formatTurnCache,
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
crossTurnVerdict,
|
|
77
|
+
priorLookbackOverflow,
|
|
78
|
+
noteLookbackRisk,
|
|
78
79
|
CACHE_LOOKBACK_BLOCKS,
|
|
79
80
|
} from "./cache-telemetry.js";
|
|
80
81
|
|
|
@@ -66,6 +66,16 @@ export function stopCurrentTurn(chatId: string): KillOutcome {
|
|
|
66
66
|
return taskTable.killRunningTurn(chatId);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Request an abort of every chat's running turn. The shutdown drain calls
|
|
71
|
+
* this before polling `getActiveCount` — a turn can legitimately run for
|
|
72
|
+
* minutes, so a drain that only waits can never succeed against one.
|
|
73
|
+
* Returns the number of kill requests issued.
|
|
74
|
+
*/
|
|
75
|
+
export function stopAllTurns(): number {
|
|
76
|
+
return taskTable.killAllRunningTurns();
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
80
|
* Execute an AI query with full lifecycle management.
|
|
71
81
|
* Same-chat queries are serialized (FIFO) to avoid session conflicts.
|
package/src/core/errors.ts
CHANGED
|
@@ -33,6 +33,17 @@ type ErrorReason =
|
|
|
33
33
|
const USAGE_LIMIT_RE =
|
|
34
34
|
/you['’]ve hit your .{0,40}limit|you['’]re out of extra usage|claude ai usage limit reached|usage limit reached/i;
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Ceiling on how long a failed attempt may have run and still earn the
|
|
38
|
+
* frontend queues' blind retry. `retryable` means "a short pause may clear
|
|
39
|
+
* it", which holds for a 429 or a dropped socket but not for an attempt
|
|
40
|
+
* that already burned minutes before failing (e.g. the 600s remote turn
|
|
41
|
+
* deadline — its rejection is `name: "TimeoutError"`, classified as
|
|
42
|
+
* transient network). Turns serialize per chat, so retrying such an
|
|
43
|
+
* attempt doubles the stall for every message queued behind it.
|
|
44
|
+
*/
|
|
45
|
+
export const RETRY_ELAPSED_CAP_MS = 120_000;
|
|
46
|
+
|
|
36
47
|
// ── TalonError class ────────────────────────────────────────────────────────
|
|
37
48
|
|
|
38
49
|
export class TalonError extends Error {
|
|
@@ -62,6 +62,31 @@ type ChildEntry = {
|
|
|
62
62
|
const children = new Map<string, ChildEntry>();
|
|
63
63
|
const inflight = new Map<string, Promise<ChildHandle>>();
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Negative cache for spawn failures. A child whose backing service is down
|
|
67
|
+
* (e.g. playwright-tools with its browser endpoint offline) dies at the
|
|
68
|
+
* connect handshake, and without this every single turn re-paid the
|
|
69
|
+
* spawn+handshake (~600ms) and re-logged the failure for the whole outage.
|
|
70
|
+
* Failures back off exponentially; the first attempt after the window
|
|
71
|
+
* clears the entry on success, so recovery costs one turn.
|
|
72
|
+
*/
|
|
73
|
+
type SpawnFailure = { at: number; count: number; error: unknown };
|
|
74
|
+
const spawnFailures = new Map<string, SpawnFailure>();
|
|
75
|
+
const FAILURE_BACKOFF_BASE_MS = 30_000;
|
|
76
|
+
const FAILURE_BACKOFF_MAX_MS = 10 * 60_000;
|
|
77
|
+
|
|
78
|
+
function failureBackoffMs(count: number): number {
|
|
79
|
+
return Math.min(
|
|
80
|
+
FAILURE_BACKOFF_BASE_MS * 2 ** (count - 1),
|
|
81
|
+
FAILURE_BACKOFF_MAX_MS,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Test seam: forget recorded spawn failures. */
|
|
86
|
+
export function resetSpawnFailures(): void {
|
|
87
|
+
spawnFailures.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
/** Idle TTL for hub children; tunable for tests / tight deployments. */
|
|
66
91
|
function idleTtlMs(): number {
|
|
67
92
|
const raw = Number(process.env.TALON_MCP_HUB_IDLE_MS);
|
|
@@ -168,9 +193,28 @@ export function acquireChild(
|
|
|
168
193
|
const pending = inflight.get(key);
|
|
169
194
|
if (pending) return pending;
|
|
170
195
|
|
|
196
|
+
const failure = spawnFailures.get(key);
|
|
197
|
+
if (failure && Date.now() - failure.at < failureBackoffMs(failure.count)) {
|
|
198
|
+
return Promise.reject(
|
|
199
|
+
failure.error instanceof Error
|
|
200
|
+
? failure.error
|
|
201
|
+
: new Error(String(failure.error)),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
171
205
|
const promise = (async () => {
|
|
172
206
|
try {
|
|
173
|
-
|
|
207
|
+
const handle = await spawnChild(key, spec());
|
|
208
|
+
spawnFailures.delete(key);
|
|
209
|
+
return handle;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const prior = spawnFailures.get(key);
|
|
212
|
+
spawnFailures.set(key, {
|
|
213
|
+
at: Date.now(),
|
|
214
|
+
count: (prior?.count ?? 0) + 1,
|
|
215
|
+
error: err,
|
|
216
|
+
});
|
|
217
|
+
throw err;
|
|
174
218
|
} finally {
|
|
175
219
|
inflight.delete(key);
|
|
176
220
|
}
|
package/src/core/tasks/table.ts
CHANGED
|
@@ -122,6 +122,20 @@ export class TaskTable {
|
|
|
122
122
|
return { ok: true };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Request an abort of every running turn, regardless of chat — the
|
|
127
|
+
* shutdown drain's lever. Returns the number of kill requests issued.
|
|
128
|
+
*/
|
|
129
|
+
killAllRunningTurns(): number {
|
|
130
|
+
let killed = 0;
|
|
131
|
+
for (const [id, task] of this.live) {
|
|
132
|
+
if (task.record.kind === "turn" && task.record.state === "running") {
|
|
133
|
+
if (this.kill(id).ok) killed++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return killed;
|
|
137
|
+
}
|
|
138
|
+
|
|
125
139
|
/**
|
|
126
140
|
* Request an abort for the turn currently running in one chat. Queued turns
|
|
127
141
|
* are deliberately ignored: `/stop` means "stop what is happening now",
|
package/src/core/tools/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { webTools } from "./web.js";
|
|
|
22
22
|
import { adminTools } from "./admin.js";
|
|
23
23
|
import { modelTools } from "./models.js";
|
|
24
24
|
import { meshTools } from "./mesh.js";
|
|
25
|
+
import { moderationTools } from "./moderation.js";
|
|
25
26
|
import { nativeTools } from "./native.js";
|
|
26
27
|
|
|
27
28
|
/** All built-in tool definitions. */
|
|
@@ -41,6 +42,7 @@ export const ALL_TOOLS: readonly ToolDefinition[] = [
|
|
|
41
42
|
...adminTools,
|
|
42
43
|
...modelTools,
|
|
43
44
|
...meshTools,
|
|
45
|
+
...moderationTools,
|
|
44
46
|
];
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -135,7 +135,10 @@ Examples:
|
|
|
135
135
|
Dice: send(type="dice")
|
|
136
136
|
Location: send(type="location", latitude=37.7749, longitude=-122.4194)
|
|
137
137
|
Sticker by feeling: send(type="sticker", emoji="😂") — picks a matching sticker from your saved packs (add set_name to pin one pack)
|
|
138
|
-
Sticker by id: send(type="sticker", file_id="CAACAgI...")
|
|
138
|
+
Sticker by id: send(type="sticker", file_id="CAACAgI...")
|
|
139
|
+
Album: send(type="album", media=[{"type":"photo","file_path":"a.jpg"},{"type":"photo","url":"https://…/b.jpg","caption":"the good one"}]) — 2-10 photos/videos as one grouped message
|
|
140
|
+
Round video: send(type="video_note", file_path="/path/clip.mp4") — circular video bubble (square video, ≤60s)
|
|
141
|
+
Venue: send(type="venue", latitude=53.34, longitude=-6.26, title="The Long Hall", address="51 South Great George's St")`,
|
|
139
142
|
schema: {
|
|
140
143
|
type: z
|
|
141
144
|
.enum([
|
|
@@ -151,6 +154,9 @@ Examples:
|
|
|
151
154
|
"location",
|
|
152
155
|
"contact",
|
|
153
156
|
"dice",
|
|
157
|
+
"album",
|
|
158
|
+
"video_note",
|
|
159
|
+
"venue",
|
|
154
160
|
])
|
|
155
161
|
.describe("Content type to send"),
|
|
156
162
|
text: z
|
|
@@ -208,7 +214,30 @@ Examples:
|
|
|
208
214
|
phone_number: z.string().optional().describe("Contact phone"),
|
|
209
215
|
first_name: z.string().optional().describe("Contact first name"),
|
|
210
216
|
last_name: z.string().optional().describe("Contact last name"),
|
|
211
|
-
title: z
|
|
217
|
+
title: z
|
|
218
|
+
.string()
|
|
219
|
+
.optional()
|
|
220
|
+
.describe("Audio title (type=audio) or venue name (type=venue)"),
|
|
221
|
+
address: z
|
|
222
|
+
.string()
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("Venue street address (for type=venue)"),
|
|
225
|
+
media: z
|
|
226
|
+
.array(
|
|
227
|
+
z.object({
|
|
228
|
+
type: z
|
|
229
|
+
.enum(["photo", "video", "document", "audio"])
|
|
230
|
+
.describe("Kind of this album item"),
|
|
231
|
+
file_path: z.string().optional(),
|
|
232
|
+
url: z.string().optional(),
|
|
233
|
+
file_id: z.string().optional(),
|
|
234
|
+
caption: z.string().optional(),
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
.optional()
|
|
238
|
+
.describe(
|
|
239
|
+
"Album items (for type=album): 2-10 entries, each sourced from file_path, url, or file_id. Photos and videos mix; documents/audio group only with their own kind.",
|
|
240
|
+
),
|
|
212
241
|
performer: z
|
|
213
242
|
.string()
|
|
214
243
|
.optional()
|
|
@@ -230,9 +259,41 @@ Examples:
|
|
|
230
259
|
.describe(
|
|
231
260
|
"Target chat ID. Omit to send to the current chat (chat mode). Required from heartbeat mode where there is no ambient chat — use list_chats or known IDs from memory. Telegram supergroup/channel IDs are negative (e.g. -1001426819337); user DMs are positive.",
|
|
232
261
|
),
|
|
262
|
+
silent: z
|
|
263
|
+
.boolean()
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("Send without a notification sound (Telegram)"),
|
|
266
|
+
protect: z
|
|
267
|
+
.boolean()
|
|
268
|
+
.optional()
|
|
269
|
+
.describe("Protect content from forwarding and saving (Telegram)"),
|
|
270
|
+
spoiler: z
|
|
271
|
+
.boolean()
|
|
272
|
+
.optional()
|
|
273
|
+
.describe(
|
|
274
|
+
"Blur photo/video/animation behind a spoiler tap-to-reveal (Telegram)",
|
|
275
|
+
),
|
|
276
|
+
no_link_preview: z
|
|
277
|
+
.boolean()
|
|
278
|
+
.optional()
|
|
279
|
+
.describe("Disable the link preview for type=text (Telegram)"),
|
|
280
|
+
thread_id: z
|
|
281
|
+
.union([z.number(), z.literal("general")])
|
|
282
|
+
.optional()
|
|
283
|
+
.describe(
|
|
284
|
+
'Forum topic to post into (Telegram supergroups with topics). Defaults to the topic the conversation is happening in; pass "general" to force the General topic.',
|
|
285
|
+
),
|
|
233
286
|
},
|
|
234
287
|
execute: async (params, bridge) => {
|
|
235
288
|
const { type } = params;
|
|
289
|
+
// Delivery modifiers every Telegram send action understands. Harmless
|
|
290
|
+
// on frontends that don't (handlers read only the fields they know).
|
|
291
|
+
const mods = {
|
|
292
|
+
silent: params.silent,
|
|
293
|
+
protect: params.protect,
|
|
294
|
+
spoiler: params.spoiler,
|
|
295
|
+
thread_id: params.thread_id,
|
|
296
|
+
};
|
|
236
297
|
// Thread chat_id through to every bridge call so heartbeat / dream
|
|
237
298
|
// outbound (no ambient chat) gets routed by the explicit chat_id.
|
|
238
299
|
// `createBridge` at src/core/tools/bridge.ts:29 reads
|
|
@@ -253,6 +314,7 @@ Examples:
|
|
|
253
314
|
delay_seconds: params.delay_seconds,
|
|
254
315
|
rows: params.buttons,
|
|
255
316
|
reply_to_message_id: params.reply_to,
|
|
317
|
+
...mods,
|
|
256
318
|
chat_id,
|
|
257
319
|
});
|
|
258
320
|
}
|
|
@@ -261,12 +323,15 @@ Examples:
|
|
|
261
323
|
text: params.text,
|
|
262
324
|
rows: params.buttons,
|
|
263
325
|
reply_to_message_id: params.reply_to,
|
|
326
|
+
...mods,
|
|
264
327
|
chat_id,
|
|
265
328
|
});
|
|
266
329
|
}
|
|
267
330
|
return bridge("send_message", {
|
|
268
331
|
text: params.text,
|
|
269
332
|
reply_to_message_id: params.reply_to,
|
|
333
|
+
no_link_preview: params.no_link_preview,
|
|
334
|
+
...mods,
|
|
270
335
|
chat_id,
|
|
271
336
|
});
|
|
272
337
|
}
|
|
@@ -277,6 +342,7 @@ Examples:
|
|
|
277
342
|
file_id: params.file_id,
|
|
278
343
|
caption: params.caption,
|
|
279
344
|
reply_to: params.reply_to,
|
|
345
|
+
...mods,
|
|
280
346
|
chat_id,
|
|
281
347
|
});
|
|
282
348
|
case "file":
|
|
@@ -286,6 +352,7 @@ Examples:
|
|
|
286
352
|
file_id: params.file_id,
|
|
287
353
|
caption: params.caption,
|
|
288
354
|
reply_to: params.reply_to,
|
|
355
|
+
...mods,
|
|
289
356
|
chat_id,
|
|
290
357
|
});
|
|
291
358
|
case "video":
|
|
@@ -295,6 +362,7 @@ Examples:
|
|
|
295
362
|
file_id: params.file_id,
|
|
296
363
|
caption: params.caption,
|
|
297
364
|
reply_to: params.reply_to,
|
|
365
|
+
...mods,
|
|
298
366
|
chat_id,
|
|
299
367
|
});
|
|
300
368
|
case "voice":
|
|
@@ -304,6 +372,7 @@ Examples:
|
|
|
304
372
|
file_id: params.file_id,
|
|
305
373
|
caption: params.caption,
|
|
306
374
|
reply_to: params.reply_to,
|
|
375
|
+
...mods,
|
|
307
376
|
chat_id,
|
|
308
377
|
});
|
|
309
378
|
case "audio":
|
|
@@ -315,6 +384,7 @@ Examples:
|
|
|
315
384
|
title: params.title,
|
|
316
385
|
performer: params.performer,
|
|
317
386
|
reply_to: params.reply_to,
|
|
387
|
+
...mods,
|
|
318
388
|
chat_id,
|
|
319
389
|
});
|
|
320
390
|
case "animation":
|
|
@@ -324,6 +394,7 @@ Examples:
|
|
|
324
394
|
file_id: params.file_id,
|
|
325
395
|
caption: params.caption,
|
|
326
396
|
reply_to: params.reply_to,
|
|
397
|
+
...mods,
|
|
327
398
|
chat_id,
|
|
328
399
|
});
|
|
329
400
|
case "sticker":
|
|
@@ -333,6 +404,7 @@ Examples:
|
|
|
333
404
|
emoji: params.emoji,
|
|
334
405
|
set_name: params.set_name,
|
|
335
406
|
reply_to: params.reply_to,
|
|
407
|
+
...mods,
|
|
336
408
|
chat_id,
|
|
337
409
|
});
|
|
338
410
|
case "poll":
|
|
@@ -344,6 +416,7 @@ Examples:
|
|
|
344
416
|
explanation: params.explanation,
|
|
345
417
|
type: params.correct_option_id !== undefined ? "quiz" : "regular",
|
|
346
418
|
reply_to: params.reply_to,
|
|
419
|
+
...mods,
|
|
347
420
|
chat_id,
|
|
348
421
|
});
|
|
349
422
|
case "location":
|
|
@@ -351,6 +424,7 @@ Examples:
|
|
|
351
424
|
latitude: params.latitude,
|
|
352
425
|
longitude: params.longitude,
|
|
353
426
|
reply_to: params.reply_to,
|
|
427
|
+
...mods,
|
|
354
428
|
chat_id,
|
|
355
429
|
});
|
|
356
430
|
case "contact":
|
|
@@ -359,12 +433,40 @@ Examples:
|
|
|
359
433
|
first_name: params.first_name,
|
|
360
434
|
last_name: params.last_name,
|
|
361
435
|
reply_to: params.reply_to,
|
|
436
|
+
...mods,
|
|
362
437
|
chat_id,
|
|
363
438
|
});
|
|
364
439
|
case "dice":
|
|
365
440
|
return bridge("send_dice", {
|
|
366
441
|
emoji: params.emoji,
|
|
367
442
|
reply_to: params.reply_to,
|
|
443
|
+
...mods,
|
|
444
|
+
chat_id,
|
|
445
|
+
});
|
|
446
|
+
case "album":
|
|
447
|
+
return bridge("send_media_group", {
|
|
448
|
+
media: params.media,
|
|
449
|
+
reply_to: params.reply_to,
|
|
450
|
+
...mods,
|
|
451
|
+
chat_id,
|
|
452
|
+
});
|
|
453
|
+
case "video_note":
|
|
454
|
+
return bridge("send_video_note", {
|
|
455
|
+
file_path: params.file_path,
|
|
456
|
+
url: params.url,
|
|
457
|
+
file_id: params.file_id,
|
|
458
|
+
reply_to: params.reply_to,
|
|
459
|
+
...mods,
|
|
460
|
+
chat_id,
|
|
461
|
+
});
|
|
462
|
+
case "venue":
|
|
463
|
+
return bridge("send_venue", {
|
|
464
|
+
latitude: params.latitude,
|
|
465
|
+
longitude: params.longitude,
|
|
466
|
+
title: params.title,
|
|
467
|
+
address: params.address,
|
|
468
|
+
reply_to: params.reply_to,
|
|
469
|
+
...mods,
|
|
368
470
|
chat_id,
|
|
369
471
|
});
|
|
370
472
|
default:
|
|
@@ -479,8 +581,16 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
479
581
|
// ── edit_message ──────────────────────────────────────────────────────
|
|
480
582
|
{
|
|
481
583
|
name: "edit_message",
|
|
482
|
-
description:
|
|
483
|
-
|
|
584
|
+
description:
|
|
585
|
+
"Edit a previously sent message. For a media message (photo/video/file), pass is_caption=true to edit its caption instead of message text.",
|
|
586
|
+
schema: {
|
|
587
|
+
message_id: snowflakeOrIdSchema,
|
|
588
|
+
text: z.string(),
|
|
589
|
+
is_caption: z
|
|
590
|
+
.boolean()
|
|
591
|
+
.optional()
|
|
592
|
+
.describe("Edit the media caption rather than message text (Telegram)"),
|
|
593
|
+
},
|
|
484
594
|
execute: (params, bridge) => bridge("edit_message", params),
|
|
485
595
|
frontends: ["telegram", "discord", "native"],
|
|
486
596
|
tag: "messaging",
|
|
@@ -489,8 +599,15 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
489
599
|
// ── delete_message ────────────────────────────────────────────────────
|
|
490
600
|
{
|
|
491
601
|
name: "delete_message",
|
|
492
|
-
description:
|
|
493
|
-
|
|
602
|
+
description:
|
|
603
|
+
"Delete a message — or several at once via message_ids (Telegram; ids the bot can't delete are skipped).",
|
|
604
|
+
schema: {
|
|
605
|
+
message_id: snowflakeOrIdSchema.optional(),
|
|
606
|
+
message_ids: z
|
|
607
|
+
.array(snowflakeOrIdSchema)
|
|
608
|
+
.optional()
|
|
609
|
+
.describe("Bulk delete these message IDs (Telegram)"),
|
|
610
|
+
},
|
|
494
611
|
execute: (params, bridge) => bridge("delete_message", params),
|
|
495
612
|
frontends: ["telegram", "discord", "native"],
|
|
496
613
|
tag: "messaging",
|
|
@@ -499,13 +616,42 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
499
616
|
// ── forward_message ───────────────────────────────────────────────────
|
|
500
617
|
{
|
|
501
618
|
name: "forward_message",
|
|
502
|
-
description:
|
|
503
|
-
|
|
619
|
+
description:
|
|
620
|
+
"Forward a message. Defaults to within the current chat; from_chat_id / to_chat_id forward across chats the bot is in, and message_ids forwards a batch (albums stay grouped) (Telegram).",
|
|
621
|
+
schema: {
|
|
622
|
+
message_id: snowflakeOrIdSchema.optional(),
|
|
623
|
+
message_ids: z
|
|
624
|
+
.array(snowflakeOrIdSchema)
|
|
625
|
+
.optional()
|
|
626
|
+
.describe("Forward these messages as a batch (Telegram)"),
|
|
627
|
+
from_chat_id: chatIdSchema
|
|
628
|
+
.optional()
|
|
629
|
+
.describe("Source chat (default: current chat)"),
|
|
630
|
+
to_chat_id: chatIdSchema
|
|
631
|
+
.optional()
|
|
632
|
+
.describe("Destination chat (default: current chat)"),
|
|
633
|
+
},
|
|
504
634
|
execute: (params, bridge) => bridge("forward_message", params),
|
|
505
635
|
frontends: ["telegram", "discord"],
|
|
506
636
|
tag: "messaging",
|
|
507
637
|
},
|
|
508
638
|
|
|
639
|
+
// ── copy_message ──────────────────────────────────────────────────────
|
|
640
|
+
{
|
|
641
|
+
name: "copy_message",
|
|
642
|
+
description:
|
|
643
|
+
"Repost a message without the 'forwarded from' header. Same cross-chat and batch semantics as forward_message (Telegram).",
|
|
644
|
+
schema: {
|
|
645
|
+
message_id: snowflakeOrIdSchema.optional(),
|
|
646
|
+
message_ids: z.array(snowflakeOrIdSchema).optional(),
|
|
647
|
+
from_chat_id: chatIdSchema.optional(),
|
|
648
|
+
to_chat_id: chatIdSchema.optional(),
|
|
649
|
+
},
|
|
650
|
+
execute: (params, bridge) => bridge("copy_message", params),
|
|
651
|
+
frontends: ["telegram"],
|
|
652
|
+
tag: "messaging",
|
|
653
|
+
},
|
|
654
|
+
|
|
509
655
|
// ── pin_message ───────────────────────────────────────────────────────
|
|
510
656
|
{
|
|
511
657
|
name: "pin_message",
|