@dreb/telegram 2.4.2 → 2.4.4
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/dist/commands/buddy.d.ts.map +1 -1
- package/dist/commands/buddy.js +1 -0
- package/dist/commands/buddy.js.map +1 -1
- package/dist/handlers/events.d.ts.map +1 -1
- package/dist/handlers/events.js +1 -1
- package/dist/handlers/events.js.map +1 -1
- package/dist/util/telegram.d.ts.map +1 -1
- package/dist/util/telegram.js +2 -0
- package/dist/util/telegram.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy.d.ts","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAO1G;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"buddy.d.ts","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAO1G;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DhG","sourcesContent":["/**\n * /buddy command — manage the buddy companion in Telegram.\n */\n\nimport type { Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport { createTelegramBuddyController, formatBuddyStats } from \"../handlers/buddy.js\";\nimport { enqueueSend } from \"../handlers/message.js\";\nimport type { UserState } from \"../types.js\";\nimport { safeSend } from \"../util/telegram.js\";\n\n/**\n * Ensure the user has a buddy controller, creating one if needed.\n *\n * The controller auto-loads from the shared buddy.json on creation,\n * so it picks up any buddy hatched in the TUI (and vice versa).\n *\n * @param api — grammy Api for chat actions\n * @param userState — per-user state (controller stored here)\n * @param chatId — Telegram chat ID (private chat = user ID)\n * @param config — bot config (for bridge resolution in hatch/reroll)\n */\nexport function ensureBuddyController(api: any, userState: UserState, chatId: number, config: Config): void {\n\tif (userState.buddyController) return;\n\n\tconst send = (text: string, long?: boolean) => {\n\t\tenqueueSend(api, userState, chatId, text, long);\n\t};\n\tuserState.buddyController = createTelegramBuddyController(send, api, chatId, config, userState);\n}\n\nexport async function cmdBuddy(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst args = (ctx.match as string)?.trim() ?? \"\";\n\tconst subcommand = args.split(/\\s+/)[0]?.toLowerCase() ?? \"\";\n\n\ttry {\n\t\t// Ensure buddy controller exists\n\t\tensureBuddyController(ctx.api, userState, chatId, config);\n\n\t\tconst controller = userState.buddyController;\n\t\tconst result = await controller.handleCommand(subcommand);\n\n\t\tswitch (result.type) {\n\t\t\tcase \"hatch\":\n\t\t\tcase \"reroll\": {\n\t\t\t\t// Reload manager state after RPC hatch/reroll changed buddy.json\n\t\t\t\tcontroller.manager.load();\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* Reactions not available in all chats */\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"show\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"pet\": {\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* setMessageReaction unavailable or forbidden — send heart as plain text */\n\t\t\t\t\tawait safeSend(ctx.api, chatId, \"❤️\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, \"🐣 Buddy hidden. Use /buddy to bring them back.\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"warning\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `⚠️ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"error\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `❌ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tawait safeSend(\n\t\t\tctx.api,\n\t\t\tchatId,\n\t\t\t`❌ Failed to initialize buddy: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n}\n"]}
|
package/dist/commands/buddy.js
CHANGED
|
@@ -55,6 +55,7 @@ export async function cmdBuddy(ctx, config, userState) {
|
|
|
55
55
|
await ctx.api.setMessageReaction(chatId, ctx.message.message_id, [{ type: "emoji", emoji: "❤" }]);
|
|
56
56
|
}
|
|
57
57
|
catch {
|
|
58
|
+
/* setMessageReaction unavailable or forbidden — send heart as plain text */
|
|
58
59
|
await safeSend(ctx.api, chatId, "❤️");
|
|
59
60
|
}
|
|
60
61
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy.js","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /buddy command — manage the buddy companion in Telegram.\n */\n\nimport type { Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport { createTelegramBuddyController, formatBuddyStats } from \"../handlers/buddy.js\";\nimport { enqueueSend } from \"../handlers/message.js\";\nimport type { UserState } from \"../types.js\";\nimport { safeSend } from \"../util/telegram.js\";\n\n/**\n * Ensure the user has a buddy controller, creating one if needed.\n *\n * The controller auto-loads from the shared buddy.json on creation,\n * so it picks up any buddy hatched in the TUI (and vice versa).\n *\n * @param api — grammy Api for chat actions\n * @param userState — per-user state (controller stored here)\n * @param chatId — Telegram chat ID (private chat = user ID)\n * @param config — bot config (for bridge resolution in hatch/reroll)\n */\nexport function ensureBuddyController(api: any, userState: UserState, chatId: number, config: Config): void {\n\tif (userState.buddyController) return;\n\n\tconst send = (text: string, long?: boolean) => {\n\t\tenqueueSend(api, userState, chatId, text, long);\n\t};\n\tuserState.buddyController = createTelegramBuddyController(send, api, chatId, config, userState);\n}\n\nexport async function cmdBuddy(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst args = (ctx.match as string)?.trim() ?? \"\";\n\tconst subcommand = args.split(/\\s+/)[0]?.toLowerCase() ?? \"\";\n\n\ttry {\n\t\t// Ensure buddy controller exists\n\t\tensureBuddyController(ctx.api, userState, chatId, config);\n\n\t\tconst controller = userState.buddyController;\n\t\tconst result = await controller.handleCommand(subcommand);\n\n\t\tswitch (result.type) {\n\t\t\tcase \"hatch\":\n\t\t\tcase \"reroll\": {\n\t\t\t\t// Reload manager state after RPC hatch/reroll changed buddy.json\n\t\t\t\tcontroller.manager.load();\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* Reactions not available in all chats */\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"show\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"pet\": {\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\tawait safeSend(ctx.api, chatId, \"❤️\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, \"🐣 Buddy hidden. Use /buddy to bring them back.\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"warning\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `⚠️ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"error\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `❌ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tawait safeSend(\n\t\t\tctx.api,\n\t\t\tchatId,\n\t\t\t`❌ Failed to initialize buddy: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy.js","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /buddy command — manage the buddy companion in Telegram.\n */\n\nimport type { Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport { createTelegramBuddyController, formatBuddyStats } from \"../handlers/buddy.js\";\nimport { enqueueSend } from \"../handlers/message.js\";\nimport type { UserState } from \"../types.js\";\nimport { safeSend } from \"../util/telegram.js\";\n\n/**\n * Ensure the user has a buddy controller, creating one if needed.\n *\n * The controller auto-loads from the shared buddy.json on creation,\n * so it picks up any buddy hatched in the TUI (and vice versa).\n *\n * @param api — grammy Api for chat actions\n * @param userState — per-user state (controller stored here)\n * @param chatId — Telegram chat ID (private chat = user ID)\n * @param config — bot config (for bridge resolution in hatch/reroll)\n */\nexport function ensureBuddyController(api: any, userState: UserState, chatId: number, config: Config): void {\n\tif (userState.buddyController) return;\n\n\tconst send = (text: string, long?: boolean) => {\n\t\tenqueueSend(api, userState, chatId, text, long);\n\t};\n\tuserState.buddyController = createTelegramBuddyController(send, api, chatId, config, userState);\n}\n\nexport async function cmdBuddy(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst args = (ctx.match as string)?.trim() ?? \"\";\n\tconst subcommand = args.split(/\\s+/)[0]?.toLowerCase() ?? \"\";\n\n\ttry {\n\t\t// Ensure buddy controller exists\n\t\tensureBuddyController(ctx.api, userState, chatId, config);\n\n\t\tconst controller = userState.buddyController;\n\t\tconst result = await controller.handleCommand(subcommand);\n\n\t\tswitch (result.type) {\n\t\t\tcase \"hatch\":\n\t\t\tcase \"reroll\": {\n\t\t\t\t// Reload manager state after RPC hatch/reroll changed buddy.json\n\t\t\t\tcontroller.manager.load();\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* Reactions not available in all chats */\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"show\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"pet\": {\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* setMessageReaction unavailable or forbidden — send heart as plain text */\n\t\t\t\t\tawait safeSend(ctx.api, chatId, \"❤️\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, \"🐣 Buddy hidden. Use /buddy to bring them back.\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"warning\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `⚠️ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"error\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `❌ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tawait safeSend(\n\t\t\tctx.api,\n\t\t\tchatId,\n\t\t\t`❌ Failed to initialize buddy: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/handlers/events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAmB,MAAM,qBAAqB,CAAC;AAEvE,gFAA8E;AAC9E,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAE5D;;;;;GAKG;AACH,KAAK,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAC;AAmErD,MAAM,WAAW,iBAAiB;IACjC,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gDAAgD;IAChD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wBAAwB;IACxB,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,wBAAwB;IACxB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC5C,iCAAiC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,gCAAgC;IAChC,MAAM,EAAE,eAAe,CAAC;IACxB,wFAAsF;IACtF,eAAe,EAAE,OAAO,CAAC;IACzB,wGAAsG;IACtG,YAAY,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAuE;IACvE,eAAe,CAAC,EAAE,GAAG,CAAC;CACtB;AAaD;;GAEG;AACH,wBAAgB,kBAAkB,CACjC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GAAG,IAAI,GAC5B,iBAAiB,CAgBnB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,QAAQ,GACb,OAAO,CAAC,IAAI,CAAC,CA6Qf","sourcesContent":["/**\n * Event display — translates RPC agent events into Telegram messages.\n *\n * Manages an ephemeral status message that shows tool use, task lists,\n * and subagent activity. Text from the agent is sent as permanent messages.\n */\n\nimport { existsSync } from \"node:fs\";\nimport type { Api } from \"grammy\";\nimport { InputFile } from \"grammy\";\nimport type { TrackedAgent } from \"../types.js\";\nimport { extractSendFiles } from \"../util/files.js\";\nimport { DebouncedEditor, log, safeDelete } from \"../util/telegram.js\";\n\n/** Callback to queue a message for delivery — never blocks the event chain */\nexport type SendFn = (text: string, long?: boolean) => void;\n\n/**\n * RPC events include both core AgentEvent and session-specific events\n * (tasks_update, background_agent_*, auto_compaction_*).\n * We type loosely here since the RPC client types onEvent as AgentEvent\n * but actually forwards all AgentSessionEvent types.\n */\ntype RpcEvent = { type: string; [key: string]: any };\n\n// Tool emoji mapping (tool names are lowercase in definitions)\nconst TOOL_EMOJI: Record<string, string> = {\n\tbash: \"🔧\",\n\tread: \"📖\",\n\tedit: \"✏️\",\n\twrite: \"📝\",\n\tgrep: \"🔎\",\n\tfind: \"🔍\",\n\tls: \"📂\",\n\tweb_search: \"🌐\",\n\tweb_fetch: \"🌐\",\n\tsubagent: \"🤖\",\n\ttasks_update: \"📋\",\n\tskill: \"⚡\",\n};\n\nfunction toolEmoji(name: string): string {\n\treturn TOOL_EMOJI[name] || \"🔧\";\n}\n\n/** Format a tool call for display */\nfunction formatTool(name: string, args: Record<string, any>): string {\n\tconst emoji = toolEmoji(name);\n\tswitch (name) {\n\t\tcase \"bash\": {\n\t\t\tconst cmd = args.command || \"\";\n\t\t\treturn `${emoji} *bash*\\n\\`${cmd.slice(0, 500)}\\``;\n\t\t}\n\t\tcase \"read\":\n\t\t\treturn `${emoji} *read*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"edit\":\n\t\t\treturn `${emoji} *edit*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"write\":\n\t\t\treturn `${emoji} *write*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"grep\":\n\t\t\treturn `${emoji} *grep*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"find\":\n\t\t\treturn `${emoji} *find*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"ls\":\n\t\t\treturn `${emoji} *ls*: \\`${args.path || \".\"}\\``;\n\t\tcase \"web_search\":\n\t\t\treturn `${emoji} *web\\\\_search*: ${args.query || \"?\"}`;\n\t\tcase \"web_fetch\":\n\t\t\treturn `${emoji} *web\\\\_fetch*: ${(args.url || \"?\").slice(0, 80)}`;\n\t\tcase \"subagent\":\n\t\t\treturn `${emoji} *subagent* (${args.agent || \"?\"}): ${(args.task || args.tasks?.[0]?.task || \"?\").slice(0, 200)}`;\n\t\tcase \"skill\":\n\t\t\treturn `${emoji} *skill*: ${args.skill || \"?\"}`;\n\t\tdefault:\n\t\t\treturn `${emoji} *${name}*`;\n\t}\n}\n\n/** Format task list as checklist */\nfunction formatTaskList(tasks: Array<{ id: string; title: string; status: string }>): string {\n\tif (!tasks.length) return \"📋 *Tasks*: (empty)\";\n\tconst lines = [\"📋 *Tasks*:\"];\n\tfor (const task of tasks) {\n\t\tif (task.status === \"completed\") lines.push(` ✅ ${task.title}`);\n\t\telse if (task.status === \"in_progress\") lines.push(` 🔄 ${task.title}`);\n\t\telse lines.push(` ⬜ ${task.title}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport interface EventDisplayState {\n\t/** Chat ID to send messages to */\n\tchatId: number;\n\t/** Message ID to reply to */\n\treplyToId: number;\n\t/** Ephemeral status message ID (edited in-place) */\n\tstatusMessageId: number | null;\n\t/** Tool messages accumulated since last text */\n\ttoolsSinceText: string[];\n\t/** Total tool count */\n\ttoolCount: number;\n\t/** All text blocks received */\n\ttextBlocks: string[];\n\t/** Current task list */\n\ttasks: Array<{ id: string; title: string; status: string }>;\n\t/** Background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether agent has finished */\n\tdone: boolean;\n\t/** Debounced editor instance */\n\teditor: DebouncedEditor;\n\t/** Whether auto-retry is in progress (Layer 1: reactive — set by auto_retry_start) */\n\tretryInProgress: boolean;\n\t/** Whether a retry is expected (Layer 2: predictive — set by agent_end when error looks retryable) */\n\tpendingRetry: boolean;\n\t/** Current retry attempt number for display */\n\tretryAttempt: number;\n\t/** Buddy controller — receives agent events for context + reactions */\n\tbuddyController?: any;\n}\n\n/**\n * Check if an error message looks retryable (overloaded, rate limit, server errors).\n * Mirrors the core's _isRetryableError check as a defensive Layer 2.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;\n\nfunction isRetryableError(errorMessage: string): boolean {\n\treturn RETRYABLE_ERROR_PATTERN.test(errorMessage);\n}\n\n/**\n * Create a fresh event display state for a new agent run.\n */\nexport function createEventDisplay(\n\tapi: Api,\n\tchatId: number,\n\treplyToId: number,\n\tstatusMessageId: number | null,\n): EventDisplayState {\n\treturn {\n\t\tchatId,\n\t\treplyToId,\n\t\tstatusMessageId,\n\t\ttoolsSinceText: [],\n\t\ttoolCount: 0,\n\t\ttextBlocks: [],\n\t\ttasks: [],\n\t\tbackgroundAgents: new Map(),\n\t\tdone: false,\n\t\teditor: new DebouncedEditor(api),\n\t\tretryInProgress: false,\n\t\tpendingRetry: false,\n\t\tretryAttempt: 0,\n\t};\n}\n\n/**\n * Process an agent event and update the display.\n */\nexport async function handleAgentEvent(\n\tsend: SendFn,\n\tapi: Api,\n\tstate: EventDisplayState,\n\tevent: RpcEvent,\n): Promise<void> {\n\tswitch (event.type) {\n\t\tcase \"tool_execution_start\": {\n\t\t\tconst name = event.toolName || \"?\";\n\t\t\tconst args = event.args || {};\n\t\t\tstate.toolCount++;\n\n\t\t\t// tasks_update is shown via the separate tasks_update event — skip from tool summary\n\t\t\tif (name !== \"tasks_update\") {\n\t\t\t\tconst toolMsg = formatTool(name, args);\n\t\t\t\tstate.toolsSinceText.push(toolMsg);\n\t\t\t}\n\n\t\t\t// Update status with tool count and recent tools\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tool_execution_end\": {\n\t\t\t// Feed event to buddy controller for context capture + error reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"message_end\": {\n\t\t\tconst msg = event.message;\n\n\t\t\t// Show subagent results — the parent agent references these but the\n\t\t\t// Telegram user can't see them otherwise. Send the full content.\n\t\t\tif (msg?.role === \"toolResult\" && msg?.toolName === \"subagent\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\t\t\tsend(`🤖 *Subagent result:*\\n${block.text.trim()}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Show background agent completion results — these arrive as user\n\t\t\t// messages injected by agent-session.ts via prompt()/steer() and\n\t\t\t// contain the actual subagent output the model sees.\n\t\t\tif (msg?.role === \"user\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.includes(\"<background-agent-complete>\")) {\n\t\t\t\t\t\t\t// Extract the content between the XML tags\n\t\t\t\t\t\t\tconst match = block.text.match(\n\t\t\t\t\t\t\t\t/<background-agent-complete>\\n?([\\s\\S]*?)\\n?<\\/background-agent-complete>/,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (match?.[1]?.trim()) {\n\t\t\t\t\t\t\t\tsend(`🤖 *Background agent complete:*\\n${match[1].trim()}`, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Only display assistant messages — user messages are echoed back by RPC\n\t\t\tif (msg?.role !== \"assistant\") break;\n\t\t\tconst content = msg?.content;\n\t\t\tif (!content || !Array.isArray(content)) break;\n\n\t\t\tfor (const block of content) {\n\t\t\t\t// Display thinking blocks (collapsed summary)\n\t\t\t\tif (block.type === \"thinking\" && block.thinking?.trim() && !block.redacted) {\n\t\t\t\t\tconst thinking = block.thinking.trim();\n\t\t\t\t\tsend(`💭 _${thinking}_`, true);\n\t\t\t\t}\n\n\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\tconst text = block.text.trim();\n\n\t\t\t\t\t// Flush accumulated tools as permanent summary\n\t\t\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\t\t\tsend(summary, true);\n\t\t\t\t\t\tstate.toolsSinceText = [];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send the text as a permanent message\n\t\t\t\t\tstate.textBlocks.push(text);\n\n\t\t\t\t\t// Check for file send markers\n\t\t\t\t\tconst [cleanText, filePaths] = extractSendFiles(text);\n\t\t\t\t\tif (cleanText) {\n\t\t\t\t\t\tsend(cleanText, true);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send any requested files (silently skip non-existent paths —\n\t\t\t\t\t// the pattern may appear in explanatory text)\n\t\t\t\t\tfor (const filePath of filePaths) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (existsSync(filePath)) {\n\t\t\t\t\t\t\t\tawait api.sendDocument(state.chatId, new InputFile(filePath));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlog(`[EVENTS] Failed to send file ${filePath}: ${e}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tasks_update\": {\n\t\t\tstate.tasks = (event as any).tasks || [];\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_start\": {\n\t\t\tconst { agentId, agentType, taskSummary } = event as any;\n\t\t\tstate.backgroundAgents.set(agentId, {\n\t\t\t\tagentId,\n\t\t\t\tagentType,\n\t\t\t\ttaskSummary,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t});\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_end\": {\n\t\t\tconst { agentId } = event as any;\n\t\t\tstate.backgroundAgents.delete(agentId);\n\t\t\t// Background agents completing does not end the parent's turn.\n\t\t\t// Only agent_end sets done — same as TUI behavior.\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_start\": {\n\t\t\tupdateStatusText(state, \"🗜 _Compacting context..._\");\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_end\": {\n\t\t\tconst result = (event as any).result;\n\t\t\tif (result) {\n\t\t\t\tconst before = result.tokensBefore || 0;\n\t\t\t\tconst msg = `🗜 Context compacted (was ${Math.round(before / 1000)}k tokens)`;\n\t\t\t\tsend(msg);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t// =====================================================================\n\t\t// Auto-retry — prevents agent_end from marking done during retries\n\t\t// =====================================================================\n\n\t\tcase \"auto_retry_start\": {\n\t\t\tconst { attempt, maxAttempts, delayMs, errorMessage } = event as any;\n\t\t\tstate.retryInProgress = true;\n\t\t\tstate.pendingRetry = false; // Layer 1 has taken over from Layer 2\n\t\t\tstate.retryAttempt = attempt;\n\t\t\tconst delaySec = Math.round(delayMs / 1000);\n\t\t\tconst shortErr = errorMessage?.length > 80 ? `${errorMessage.slice(0, 80)}…` : errorMessage;\n\t\t\tupdateStatusText(state, `🔄 _Retrying (${attempt}/${maxAttempts}) in ${delaySec}s — ${shortErr || \"error\"}_`);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_retry_end\": {\n\t\t\tconst { success, attempt, finalError } = event as any;\n\t\t\tstate.retryInProgress = false;\n\t\t\tstate.retryAttempt = 0;\n\t\t\tif (!success && finalError) {\n\t\t\t\t// Max retries exhausted — show final error\n\t\t\t\tsend(`❌ _Retry failed (${attempt} attempts):_ ${finalError}`, true);\n\t\t\t}\n\t\t\t// On success, the retry's agent_start/agent_end cycle will handle display normally\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"agent_end\": {\n\t\t\t// Flush any remaining tools\n\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\tsend(summary, true);\n\t\t\t\tstate.toolsSinceText = [];\n\t\t\t}\n\n\t\t\t// Check for error in agent_end messages\n\t\t\tconst errorMsg = (event.messages as any[])?.find(\n\t\t\t\t(m: any) => m.stopReason === \"error\" || m.stopReason === \"aborted\",\n\t\t\t);\n\n\t\t\t// Layer 2 (defensive): If this error looks retryable and we're not already\n\t\t\t// tracking a retry via Layer 1, don't mark done — the core will auto-retry\n\t\t\t// and emit a new agent_start/agent_end cycle.\n\t\t\tconst errorIsRetryable = errorMsg?.errorMessage && isRetryableError(errorMsg.errorMessage);\n\n\t\t\tif (errorMsg?.errorMessage) {\n\t\t\t\t// Suppress the scary error message during retry — user already saw the\n\t\t\t\t// auto_retry_start status. Only show the error if retry tracking missed it\n\t\t\t\t// (defensive: shouldn't happen, but better than silence).\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tconst provider = errorMsg.provider ? `${errorMsg.provider}/${errorMsg.model}` : \"\";\n\t\t\t\t\tconst prefix = provider ? `${provider}: ` : \"\";\n\t\t\t\t\tconst errLower = errorMsg.errorMessage.toLowerCase();\n\t\t\t\t\tconst hint =\n\t\t\t\t\t\terrLower.includes(\"connection\") || errLower.includes(\"timeout\") || errLower.includes(\"network\")\n\t\t\t\t\t\t\t? \"\\n_Provider may be down — try /model to switch._\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tsend(`❌ ${prefix}${errorMsg.errorMessage}${hint}`, true);\n\t\t\t\t}\n\t\t\t} else if (state.textBlocks.length === 0 && state.backgroundAgents.size === 0) {\n\t\t\t\t// Only show \"(No response)\" when truly done — not between agent cycles\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tsend(\"(No response)\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\n\t\t\t// Don't mark done if auto-retry is in progress (Layer 1) or the error\n\t\t\t// looks retryable (Layer 2 — defensive catch in case events were missed).\n\t\t\t// The core will emit a new agent_start/agent_end cycle for the retry.\n\t\t\tif (state.retryInProgress || errorIsRetryable) {\n\t\t\t\t// Signal that a retry is expected — the completion check in\n\t\t\t\t// ensureSubscribed needs this because it runs in the eventChain\n\t\t\t\t// BEFORE auto_retry_start has been processed.\n\t\t\t\tif (errorIsRetryable) state.pendingRetry = true;\n\t\t\t\t// Reset per-cycle state for the next agent loop\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// If background agents are still running, keep the subscription alive\n\t\t\t// and reset per-cycle state for the next agent loop\n\t\t\tif (state.backgroundAgents.size > 0) {\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Delete ephemeral status before signaling done\n\t\t\tif (state.statusMessageId) {\n\t\t\t\tawait state.editor.flush(state.chatId, state.statusMessageId);\n\t\t\t\tawait safeDelete(api, state.chatId, state.statusMessageId);\n\t\t\t\tstate.statusMessageId = null;\n\t\t\t}\n\n\t\t\t// Clean up editor\n\t\t\tstate.editor.clear();\n\n\t\t\t// Signal done AFTER cleanup — waitForCompletion checks this flag,\n\t\t\t// so setting it last ensures status message is deleted before DONE is sent\n\t\t\tstate.done = true;\n\t\t\tbreak;\n\t\t}\n\n\t\t// Handle error responses that leak through RPC (async prompt errors)\n\t\tcase \"response\": {\n\t\t\tconst resp = event as any;\n\t\t\tif (!resp.success && resp.error) {\n\t\t\t\tsend(`❌ ${resp.error}`, true);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Build and push a status update to the ephemeral message.\n */\nfunction updateStatus(state: EventDisplayState): void {\n\tif (!state.statusMessageId) return;\n\n\tconst parts: string[] = [];\n\n\t// Tool count header\n\tif (state.toolCount > 0) {\n\t\tparts.push(`🔧 *Tool ${state.toolCount}*`);\n\t}\n\n\t// Task list\n\tif (state.tasks.length > 0) {\n\t\tparts.push(formatTaskList(state.tasks));\n\t}\n\n\t// Background agents\n\tif (state.backgroundAgents.size > 0) {\n\t\tfor (const agent of state.backgroundAgents.values()) {\n\t\t\tparts.push(`🤖 *${agent.agentType}*: ${agent.taskSummary.slice(0, 200)}`);\n\t\t}\n\t}\n\n\t// Recent tools (last 5)\n\tif (state.toolsSinceText.length > 0) {\n\t\tconst recent = state.toolsSinceText.slice(-5);\n\t\tparts.push(recent.join(\"\\n\\n\"));\n\t}\n\n\tif (parts.length === 0) return;\n\n\tconst text = parts.join(\"\\n\\n\").slice(0, 4000);\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n\nfunction updateStatusText(state: EventDisplayState, text: string): void {\n\tif (!state.statusMessageId) return;\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/handlers/events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAmB,MAAM,qBAAqB,CAAC;AAEvE,gFAA8E;AAC9E,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAE5D;;;;;GAKG;AACH,KAAK,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAC;AAmErD,MAAM,WAAW,iBAAiB;IACjC,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gDAAgD;IAChD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wBAAwB;IACxB,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,wBAAwB;IACxB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC5C,iCAAiC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,gCAAgC;IAChC,MAAM,EAAE,eAAe,CAAC;IACxB,wFAAsF;IACtF,eAAe,EAAE,OAAO,CAAC;IACzB,wGAAsG;IACtG,YAAY,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAuE;IACvE,eAAe,CAAC,EAAE,GAAG,CAAC;CACtB;AAaD;;GAEG;AACH,wBAAgB,kBAAkB,CACjC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GAAG,IAAI,GAC5B,iBAAiB,CAgBnB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,QAAQ,GACb,OAAO,CAAC,IAAI,CAAC,CA6Qf","sourcesContent":["/**\n * Event display — translates RPC agent events into Telegram messages.\n *\n * Manages an ephemeral status message that shows tool use, task lists,\n * and subagent activity. Text from the agent is sent as permanent messages.\n */\n\nimport { existsSync } from \"node:fs\";\nimport type { Api } from \"grammy\";\nimport { InputFile } from \"grammy\";\nimport type { TrackedAgent } from \"../types.js\";\nimport { extractSendFiles } from \"../util/files.js\";\nimport { DebouncedEditor, log, safeDelete } from \"../util/telegram.js\";\n\n/** Callback to queue a message for delivery — never blocks the event chain */\nexport type SendFn = (text: string, long?: boolean) => void;\n\n/**\n * RPC events include both core AgentEvent and session-specific events\n * (tasks_update, background_agent_*, auto_compaction_*).\n * We type loosely here since the RPC client types onEvent as AgentEvent\n * but actually forwards all AgentSessionEvent types.\n */\ntype RpcEvent = { type: string; [key: string]: any };\n\n// Tool emoji mapping (tool names are lowercase in definitions)\nconst TOOL_EMOJI: Record<string, string> = {\n\tbash: \"🔧\",\n\tread: \"📖\",\n\tedit: \"✏️\",\n\twrite: \"📝\",\n\tgrep: \"🔎\",\n\tfind: \"🔍\",\n\tls: \"📂\",\n\tweb_search: \"🌐\",\n\tweb_fetch: \"🌐\",\n\tsubagent: \"🤖\",\n\ttasks_update: \"📋\",\n\tskill: \"⚡\",\n};\n\nfunction toolEmoji(name: string): string {\n\treturn TOOL_EMOJI[name] || \"🔧\";\n}\n\n/** Format a tool call for display */\nfunction formatTool(name: string, args: Record<string, any>): string {\n\tconst emoji = toolEmoji(name);\n\tswitch (name) {\n\t\tcase \"bash\": {\n\t\t\tconst cmd = args.command || \"\";\n\t\t\treturn `${emoji} *bash*\\n\\`${cmd.slice(0, 500)}\\``;\n\t\t}\n\t\tcase \"read\":\n\t\t\treturn `${emoji} *read*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"edit\":\n\t\t\treturn `${emoji} *edit*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"write\":\n\t\t\treturn `${emoji} *write*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"grep\":\n\t\t\treturn `${emoji} *grep*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"find\":\n\t\t\treturn `${emoji} *find*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"ls\":\n\t\t\treturn `${emoji} *ls*: \\`${args.path || \".\"}\\``;\n\t\tcase \"web_search\":\n\t\t\treturn `${emoji} *web\\\\_search*: ${args.query || \"?\"}`;\n\t\tcase \"web_fetch\":\n\t\t\treturn `${emoji} *web\\\\_fetch*: ${(args.url || \"?\").slice(0, 80)}`;\n\t\tcase \"subagent\":\n\t\t\treturn `${emoji} *subagent* (${args.agent || \"?\"}): ${(args.task || args.tasks?.[0]?.task || \"?\").slice(0, 200)}`;\n\t\tcase \"skill\":\n\t\t\treturn `${emoji} *skill*: ${args.skill || \"?\"}`;\n\t\tdefault:\n\t\t\treturn `${emoji} *${name}*`;\n\t}\n}\n\n/** Format task list as checklist */\nfunction formatTaskList(tasks: Array<{ id: string; title: string; status: string }>): string {\n\tif (!tasks.length) return \"📋 *Tasks*: (empty)\";\n\tconst lines = [\"📋 *Tasks*:\"];\n\tfor (const task of tasks) {\n\t\tif (task.status === \"completed\") lines.push(` ✅ ${task.title}`);\n\t\telse if (task.status === \"in_progress\") lines.push(` 🔄 ${task.title}`);\n\t\telse lines.push(` ⬜ ${task.title}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport interface EventDisplayState {\n\t/** Chat ID to send messages to */\n\tchatId: number;\n\t/** Message ID to reply to */\n\treplyToId: number;\n\t/** Ephemeral status message ID (edited in-place) */\n\tstatusMessageId: number | null;\n\t/** Tool messages accumulated since last text */\n\ttoolsSinceText: string[];\n\t/** Total tool count */\n\ttoolCount: number;\n\t/** All text blocks received */\n\ttextBlocks: string[];\n\t/** Current task list */\n\ttasks: Array<{ id: string; title: string; status: string }>;\n\t/** Background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether agent has finished */\n\tdone: boolean;\n\t/** Debounced editor instance */\n\teditor: DebouncedEditor;\n\t/** Whether auto-retry is in progress (Layer 1: reactive — set by auto_retry_start) */\n\tretryInProgress: boolean;\n\t/** Whether a retry is expected (Layer 2: predictive — set by agent_end when error looks retryable) */\n\tpendingRetry: boolean;\n\t/** Current retry attempt number for display */\n\tretryAttempt: number;\n\t/** Buddy controller — receives agent events for context + reactions */\n\tbuddyController?: any;\n}\n\n/**\n * Check if an error message looks retryable (overloaded, rate limit, server errors).\n * Mirrors the core's _isRetryableError check as a defensive Layer 2.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|ended without|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;\n\nfunction isRetryableError(errorMessage: string): boolean {\n\treturn RETRYABLE_ERROR_PATTERN.test(errorMessage);\n}\n\n/**\n * Create a fresh event display state for a new agent run.\n */\nexport function createEventDisplay(\n\tapi: Api,\n\tchatId: number,\n\treplyToId: number,\n\tstatusMessageId: number | null,\n): EventDisplayState {\n\treturn {\n\t\tchatId,\n\t\treplyToId,\n\t\tstatusMessageId,\n\t\ttoolsSinceText: [],\n\t\ttoolCount: 0,\n\t\ttextBlocks: [],\n\t\ttasks: [],\n\t\tbackgroundAgents: new Map(),\n\t\tdone: false,\n\t\teditor: new DebouncedEditor(api),\n\t\tretryInProgress: false,\n\t\tpendingRetry: false,\n\t\tretryAttempt: 0,\n\t};\n}\n\n/**\n * Process an agent event and update the display.\n */\nexport async function handleAgentEvent(\n\tsend: SendFn,\n\tapi: Api,\n\tstate: EventDisplayState,\n\tevent: RpcEvent,\n): Promise<void> {\n\tswitch (event.type) {\n\t\tcase \"tool_execution_start\": {\n\t\t\tconst name = event.toolName || \"?\";\n\t\t\tconst args = event.args || {};\n\t\t\tstate.toolCount++;\n\n\t\t\t// tasks_update is shown via the separate tasks_update event — skip from tool summary\n\t\t\tif (name !== \"tasks_update\") {\n\t\t\t\tconst toolMsg = formatTool(name, args);\n\t\t\t\tstate.toolsSinceText.push(toolMsg);\n\t\t\t}\n\n\t\t\t// Update status with tool count and recent tools\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tool_execution_end\": {\n\t\t\t// Feed event to buddy controller for context capture + error reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"message_end\": {\n\t\t\tconst msg = event.message;\n\n\t\t\t// Show subagent results — the parent agent references these but the\n\t\t\t// Telegram user can't see them otherwise. Send the full content.\n\t\t\tif (msg?.role === \"toolResult\" && msg?.toolName === \"subagent\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\t\t\tsend(`🤖 *Subagent result:*\\n${block.text.trim()}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Show background agent completion results — these arrive as user\n\t\t\t// messages injected by agent-session.ts via prompt()/steer() and\n\t\t\t// contain the actual subagent output the model sees.\n\t\t\tif (msg?.role === \"user\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.includes(\"<background-agent-complete>\")) {\n\t\t\t\t\t\t\t// Extract the content between the XML tags\n\t\t\t\t\t\t\tconst match = block.text.match(\n\t\t\t\t\t\t\t\t/<background-agent-complete>\\n?([\\s\\S]*?)\\n?<\\/background-agent-complete>/,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (match?.[1]?.trim()) {\n\t\t\t\t\t\t\t\tsend(`🤖 *Background agent complete:*\\n${match[1].trim()}`, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Only display assistant messages — user messages are echoed back by RPC\n\t\t\tif (msg?.role !== \"assistant\") break;\n\t\t\tconst content = msg?.content;\n\t\t\tif (!content || !Array.isArray(content)) break;\n\n\t\t\tfor (const block of content) {\n\t\t\t\t// Display thinking blocks (collapsed summary)\n\t\t\t\tif (block.type === \"thinking\" && block.thinking?.trim() && !block.redacted) {\n\t\t\t\t\tconst thinking = block.thinking.trim();\n\t\t\t\t\tsend(`💭 _${thinking}_`, true);\n\t\t\t\t}\n\n\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\tconst text = block.text.trim();\n\n\t\t\t\t\t// Flush accumulated tools as permanent summary\n\t\t\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\t\t\tsend(summary, true);\n\t\t\t\t\t\tstate.toolsSinceText = [];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send the text as a permanent message\n\t\t\t\t\tstate.textBlocks.push(text);\n\n\t\t\t\t\t// Check for file send markers\n\t\t\t\t\tconst [cleanText, filePaths] = extractSendFiles(text);\n\t\t\t\t\tif (cleanText) {\n\t\t\t\t\t\tsend(cleanText, true);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send any requested files (silently skip non-existent paths —\n\t\t\t\t\t// the pattern may appear in explanatory text)\n\t\t\t\t\tfor (const filePath of filePaths) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (existsSync(filePath)) {\n\t\t\t\t\t\t\t\tawait api.sendDocument(state.chatId, new InputFile(filePath));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlog(`[EVENTS] Failed to send file ${filePath}: ${e}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tasks_update\": {\n\t\t\tstate.tasks = (event as any).tasks || [];\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_start\": {\n\t\t\tconst { agentId, agentType, taskSummary } = event as any;\n\t\t\tstate.backgroundAgents.set(agentId, {\n\t\t\t\tagentId,\n\t\t\t\tagentType,\n\t\t\t\ttaskSummary,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t});\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_end\": {\n\t\t\tconst { agentId } = event as any;\n\t\t\tstate.backgroundAgents.delete(agentId);\n\t\t\t// Background agents completing does not end the parent's turn.\n\t\t\t// Only agent_end sets done — same as TUI behavior.\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_start\": {\n\t\t\tupdateStatusText(state, \"🗜 _Compacting context..._\");\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_end\": {\n\t\t\tconst result = (event as any).result;\n\t\t\tif (result) {\n\t\t\t\tconst before = result.tokensBefore || 0;\n\t\t\t\tconst msg = `🗜 Context compacted (was ${Math.round(before / 1000)}k tokens)`;\n\t\t\t\tsend(msg);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t// =====================================================================\n\t\t// Auto-retry — prevents agent_end from marking done during retries\n\t\t// =====================================================================\n\n\t\tcase \"auto_retry_start\": {\n\t\t\tconst { attempt, maxAttempts, delayMs, errorMessage } = event as any;\n\t\t\tstate.retryInProgress = true;\n\t\t\tstate.pendingRetry = false; // Layer 1 has taken over from Layer 2\n\t\t\tstate.retryAttempt = attempt;\n\t\t\tconst delaySec = Math.round(delayMs / 1000);\n\t\t\tconst shortErr = errorMessage?.length > 80 ? `${errorMessage.slice(0, 80)}…` : errorMessage;\n\t\t\tupdateStatusText(state, `🔄 _Retrying (${attempt}/${maxAttempts}) in ${delaySec}s — ${shortErr || \"error\"}_`);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_retry_end\": {\n\t\t\tconst { success, attempt, finalError } = event as any;\n\t\t\tstate.retryInProgress = false;\n\t\t\tstate.retryAttempt = 0;\n\t\t\tif (!success && finalError) {\n\t\t\t\t// Max retries exhausted — show final error\n\t\t\t\tsend(`❌ _Retry failed (${attempt} attempts):_ ${finalError}`, true);\n\t\t\t}\n\t\t\t// On success, the retry's agent_start/agent_end cycle will handle display normally\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"agent_end\": {\n\t\t\t// Flush any remaining tools\n\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\tsend(summary, true);\n\t\t\t\tstate.toolsSinceText = [];\n\t\t\t}\n\n\t\t\t// Check for error in agent_end messages\n\t\t\tconst errorMsg = (event.messages as any[])?.find(\n\t\t\t\t(m: any) => m.stopReason === \"error\" || m.stopReason === \"aborted\",\n\t\t\t);\n\n\t\t\t// Layer 2 (defensive): If this error looks retryable and we're not already\n\t\t\t// tracking a retry via Layer 1, don't mark done — the core will auto-retry\n\t\t\t// and emit a new agent_start/agent_end cycle.\n\t\t\tconst errorIsRetryable = errorMsg?.errorMessage && isRetryableError(errorMsg.errorMessage);\n\n\t\t\tif (errorMsg?.errorMessage) {\n\t\t\t\t// Suppress the scary error message during retry — user already saw the\n\t\t\t\t// auto_retry_start status. Only show the error if retry tracking missed it\n\t\t\t\t// (defensive: shouldn't happen, but better than silence).\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tconst provider = errorMsg.provider ? `${errorMsg.provider}/${errorMsg.model}` : \"\";\n\t\t\t\t\tconst prefix = provider ? `${provider}: ` : \"\";\n\t\t\t\t\tconst errLower = errorMsg.errorMessage.toLowerCase();\n\t\t\t\t\tconst hint =\n\t\t\t\t\t\terrLower.includes(\"connection\") || errLower.includes(\"timeout\") || errLower.includes(\"network\")\n\t\t\t\t\t\t\t? \"\\n_Provider may be down — try /model to switch._\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tsend(`❌ ${prefix}${errorMsg.errorMessage}${hint}`, true);\n\t\t\t\t}\n\t\t\t} else if (state.textBlocks.length === 0 && state.backgroundAgents.size === 0) {\n\t\t\t\t// Only show \"(No response)\" when truly done — not between agent cycles\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tsend(\"(No response)\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\n\t\t\t// Don't mark done if auto-retry is in progress (Layer 1) or the error\n\t\t\t// looks retryable (Layer 2 — defensive catch in case events were missed).\n\t\t\t// The core will emit a new agent_start/agent_end cycle for the retry.\n\t\t\tif (state.retryInProgress || errorIsRetryable) {\n\t\t\t\t// Signal that a retry is expected — the completion check in\n\t\t\t\t// ensureSubscribed needs this because it runs in the eventChain\n\t\t\t\t// BEFORE auto_retry_start has been processed.\n\t\t\t\tif (errorIsRetryable) state.pendingRetry = true;\n\t\t\t\t// Reset per-cycle state for the next agent loop\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// If background agents are still running, keep the subscription alive\n\t\t\t// and reset per-cycle state for the next agent loop\n\t\t\tif (state.backgroundAgents.size > 0) {\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Delete ephemeral status before signaling done\n\t\t\tif (state.statusMessageId) {\n\t\t\t\tawait state.editor.flush(state.chatId, state.statusMessageId);\n\t\t\t\tawait safeDelete(api, state.chatId, state.statusMessageId);\n\t\t\t\tstate.statusMessageId = null;\n\t\t\t}\n\n\t\t\t// Clean up editor\n\t\t\tstate.editor.clear();\n\n\t\t\t// Signal done AFTER cleanup — waitForCompletion checks this flag,\n\t\t\t// so setting it last ensures status message is deleted before DONE is sent\n\t\t\tstate.done = true;\n\t\t\tbreak;\n\t\t}\n\n\t\t// Handle error responses that leak through RPC (async prompt errors)\n\t\tcase \"response\": {\n\t\t\tconst resp = event as any;\n\t\t\tif (!resp.success && resp.error) {\n\t\t\t\tsend(`❌ ${resp.error}`, true);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Build and push a status update to the ephemeral message.\n */\nfunction updateStatus(state: EventDisplayState): void {\n\tif (!state.statusMessageId) return;\n\n\tconst parts: string[] = [];\n\n\t// Tool count header\n\tif (state.toolCount > 0) {\n\t\tparts.push(`🔧 *Tool ${state.toolCount}*`);\n\t}\n\n\t// Task list\n\tif (state.tasks.length > 0) {\n\t\tparts.push(formatTaskList(state.tasks));\n\t}\n\n\t// Background agents\n\tif (state.backgroundAgents.size > 0) {\n\t\tfor (const agent of state.backgroundAgents.values()) {\n\t\t\tparts.push(`🤖 *${agent.agentType}*: ${agent.taskSummary.slice(0, 200)}`);\n\t\t}\n\t}\n\n\t// Recent tools (last 5)\n\tif (state.toolsSinceText.length > 0) {\n\t\tconst recent = state.toolsSinceText.slice(-5);\n\t\tparts.push(recent.join(\"\\n\\n\"));\n\t}\n\n\tif (parts.length === 0) return;\n\n\tconst text = parts.join(\"\\n\\n\").slice(0, 4000);\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n\nfunction updateStatusText(state: EventDisplayState, text: string): void {\n\tif (!state.statusMessageId) return;\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n"]}
|
package/dist/handlers/events.js
CHANGED
|
@@ -77,7 +77,7 @@ function formatTaskList(tasks) {
|
|
|
77
77
|
* Check if an error message looks retryable (overloaded, rate limit, server errors).
|
|
78
78
|
* Mirrors the core's _isRetryableError check as a defensive Layer 2.
|
|
79
79
|
*/
|
|
80
|
-
const RETRYABLE_ERROR_PATTERN = /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;
|
|
80
|
+
const RETRYABLE_ERROR_PATTERN = /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|ended without|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;
|
|
81
81
|
function isRetryableError(errorMessage) {
|
|
82
82
|
return RETRYABLE_ERROR_PATTERN.test(errorMessage);
|
|
83
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/handlers/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Event display — translates RPC agent events into Telegram messages.\n *\n * Manages an ephemeral status message that shows tool use, task lists,\n * and subagent activity. Text from the agent is sent as permanent messages.\n */\n\nimport { existsSync } from \"node:fs\";\nimport type { Api } from \"grammy\";\nimport { InputFile } from \"grammy\";\nimport type { TrackedAgent } from \"../types.js\";\nimport { extractSendFiles } from \"../util/files.js\";\nimport { DebouncedEditor, log, safeDelete } from \"../util/telegram.js\";\n\n/** Callback to queue a message for delivery — never blocks the event chain */\nexport type SendFn = (text: string, long?: boolean) => void;\n\n/**\n * RPC events include both core AgentEvent and session-specific events\n * (tasks_update, background_agent_*, auto_compaction_*).\n * We type loosely here since the RPC client types onEvent as AgentEvent\n * but actually forwards all AgentSessionEvent types.\n */\ntype RpcEvent = { type: string; [key: string]: any };\n\n// Tool emoji mapping (tool names are lowercase in definitions)\nconst TOOL_EMOJI: Record<string, string> = {\n\tbash: \"🔧\",\n\tread: \"📖\",\n\tedit: \"✏️\",\n\twrite: \"📝\",\n\tgrep: \"🔎\",\n\tfind: \"🔍\",\n\tls: \"📂\",\n\tweb_search: \"🌐\",\n\tweb_fetch: \"🌐\",\n\tsubagent: \"🤖\",\n\ttasks_update: \"📋\",\n\tskill: \"⚡\",\n};\n\nfunction toolEmoji(name: string): string {\n\treturn TOOL_EMOJI[name] || \"🔧\";\n}\n\n/** Format a tool call for display */\nfunction formatTool(name: string, args: Record<string, any>): string {\n\tconst emoji = toolEmoji(name);\n\tswitch (name) {\n\t\tcase \"bash\": {\n\t\t\tconst cmd = args.command || \"\";\n\t\t\treturn `${emoji} *bash*\\n\\`${cmd.slice(0, 500)}\\``;\n\t\t}\n\t\tcase \"read\":\n\t\t\treturn `${emoji} *read*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"edit\":\n\t\t\treturn `${emoji} *edit*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"write\":\n\t\t\treturn `${emoji} *write*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"grep\":\n\t\t\treturn `${emoji} *grep*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"find\":\n\t\t\treturn `${emoji} *find*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"ls\":\n\t\t\treturn `${emoji} *ls*: \\`${args.path || \".\"}\\``;\n\t\tcase \"web_search\":\n\t\t\treturn `${emoji} *web\\\\_search*: ${args.query || \"?\"}`;\n\t\tcase \"web_fetch\":\n\t\t\treturn `${emoji} *web\\\\_fetch*: ${(args.url || \"?\").slice(0, 80)}`;\n\t\tcase \"subagent\":\n\t\t\treturn `${emoji} *subagent* (${args.agent || \"?\"}): ${(args.task || args.tasks?.[0]?.task || \"?\").slice(0, 200)}`;\n\t\tcase \"skill\":\n\t\t\treturn `${emoji} *skill*: ${args.skill || \"?\"}`;\n\t\tdefault:\n\t\t\treturn `${emoji} *${name}*`;\n\t}\n}\n\n/** Format task list as checklist */\nfunction formatTaskList(tasks: Array<{ id: string; title: string; status: string }>): string {\n\tif (!tasks.length) return \"📋 *Tasks*: (empty)\";\n\tconst lines = [\"📋 *Tasks*:\"];\n\tfor (const task of tasks) {\n\t\tif (task.status === \"completed\") lines.push(` ✅ ${task.title}`);\n\t\telse if (task.status === \"in_progress\") lines.push(` 🔄 ${task.title}`);\n\t\telse lines.push(` ⬜ ${task.title}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport interface EventDisplayState {\n\t/** Chat ID to send messages to */\n\tchatId: number;\n\t/** Message ID to reply to */\n\treplyToId: number;\n\t/** Ephemeral status message ID (edited in-place) */\n\tstatusMessageId: number | null;\n\t/** Tool messages accumulated since last text */\n\ttoolsSinceText: string[];\n\t/** Total tool count */\n\ttoolCount: number;\n\t/** All text blocks received */\n\ttextBlocks: string[];\n\t/** Current task list */\n\ttasks: Array<{ id: string; title: string; status: string }>;\n\t/** Background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether agent has finished */\n\tdone: boolean;\n\t/** Debounced editor instance */\n\teditor: DebouncedEditor;\n\t/** Whether auto-retry is in progress (Layer 1: reactive — set by auto_retry_start) */\n\tretryInProgress: boolean;\n\t/** Whether a retry is expected (Layer 2: predictive — set by agent_end when error looks retryable) */\n\tpendingRetry: boolean;\n\t/** Current retry attempt number for display */\n\tretryAttempt: number;\n\t/** Buddy controller — receives agent events for context + reactions */\n\tbuddyController?: any;\n}\n\n/**\n * Check if an error message looks retryable (overloaded, rate limit, server errors).\n * Mirrors the core's _isRetryableError check as a defensive Layer 2.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;\n\nfunction isRetryableError(errorMessage: string): boolean {\n\treturn RETRYABLE_ERROR_PATTERN.test(errorMessage);\n}\n\n/**\n * Create a fresh event display state for a new agent run.\n */\nexport function createEventDisplay(\n\tapi: Api,\n\tchatId: number,\n\treplyToId: number,\n\tstatusMessageId: number | null,\n): EventDisplayState {\n\treturn {\n\t\tchatId,\n\t\treplyToId,\n\t\tstatusMessageId,\n\t\ttoolsSinceText: [],\n\t\ttoolCount: 0,\n\t\ttextBlocks: [],\n\t\ttasks: [],\n\t\tbackgroundAgents: new Map(),\n\t\tdone: false,\n\t\teditor: new DebouncedEditor(api),\n\t\tretryInProgress: false,\n\t\tpendingRetry: false,\n\t\tretryAttempt: 0,\n\t};\n}\n\n/**\n * Process an agent event and update the display.\n */\nexport async function handleAgentEvent(\n\tsend: SendFn,\n\tapi: Api,\n\tstate: EventDisplayState,\n\tevent: RpcEvent,\n): Promise<void> {\n\tswitch (event.type) {\n\t\tcase \"tool_execution_start\": {\n\t\t\tconst name = event.toolName || \"?\";\n\t\t\tconst args = event.args || {};\n\t\t\tstate.toolCount++;\n\n\t\t\t// tasks_update is shown via the separate tasks_update event — skip from tool summary\n\t\t\tif (name !== \"tasks_update\") {\n\t\t\t\tconst toolMsg = formatTool(name, args);\n\t\t\t\tstate.toolsSinceText.push(toolMsg);\n\t\t\t}\n\n\t\t\t// Update status with tool count and recent tools\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tool_execution_end\": {\n\t\t\t// Feed event to buddy controller for context capture + error reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"message_end\": {\n\t\t\tconst msg = event.message;\n\n\t\t\t// Show subagent results — the parent agent references these but the\n\t\t\t// Telegram user can't see them otherwise. Send the full content.\n\t\t\tif (msg?.role === \"toolResult\" && msg?.toolName === \"subagent\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\t\t\tsend(`🤖 *Subagent result:*\\n${block.text.trim()}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Show background agent completion results — these arrive as user\n\t\t\t// messages injected by agent-session.ts via prompt()/steer() and\n\t\t\t// contain the actual subagent output the model sees.\n\t\t\tif (msg?.role === \"user\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.includes(\"<background-agent-complete>\")) {\n\t\t\t\t\t\t\t// Extract the content between the XML tags\n\t\t\t\t\t\t\tconst match = block.text.match(\n\t\t\t\t\t\t\t\t/<background-agent-complete>\\n?([\\s\\S]*?)\\n?<\\/background-agent-complete>/,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (match?.[1]?.trim()) {\n\t\t\t\t\t\t\t\tsend(`🤖 *Background agent complete:*\\n${match[1].trim()}`, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Only display assistant messages — user messages are echoed back by RPC\n\t\t\tif (msg?.role !== \"assistant\") break;\n\t\t\tconst content = msg?.content;\n\t\t\tif (!content || !Array.isArray(content)) break;\n\n\t\t\tfor (const block of content) {\n\t\t\t\t// Display thinking blocks (collapsed summary)\n\t\t\t\tif (block.type === \"thinking\" && block.thinking?.trim() && !block.redacted) {\n\t\t\t\t\tconst thinking = block.thinking.trim();\n\t\t\t\t\tsend(`💭 _${thinking}_`, true);\n\t\t\t\t}\n\n\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\tconst text = block.text.trim();\n\n\t\t\t\t\t// Flush accumulated tools as permanent summary\n\t\t\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\t\t\tsend(summary, true);\n\t\t\t\t\t\tstate.toolsSinceText = [];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send the text as a permanent message\n\t\t\t\t\tstate.textBlocks.push(text);\n\n\t\t\t\t\t// Check for file send markers\n\t\t\t\t\tconst [cleanText, filePaths] = extractSendFiles(text);\n\t\t\t\t\tif (cleanText) {\n\t\t\t\t\t\tsend(cleanText, true);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send any requested files (silently skip non-existent paths —\n\t\t\t\t\t// the pattern may appear in explanatory text)\n\t\t\t\t\tfor (const filePath of filePaths) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (existsSync(filePath)) {\n\t\t\t\t\t\t\t\tawait api.sendDocument(state.chatId, new InputFile(filePath));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlog(`[EVENTS] Failed to send file ${filePath}: ${e}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tasks_update\": {\n\t\t\tstate.tasks = (event as any).tasks || [];\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_start\": {\n\t\t\tconst { agentId, agentType, taskSummary } = event as any;\n\t\t\tstate.backgroundAgents.set(agentId, {\n\t\t\t\tagentId,\n\t\t\t\tagentType,\n\t\t\t\ttaskSummary,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t});\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_end\": {\n\t\t\tconst { agentId } = event as any;\n\t\t\tstate.backgroundAgents.delete(agentId);\n\t\t\t// Background agents completing does not end the parent's turn.\n\t\t\t// Only agent_end sets done — same as TUI behavior.\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_start\": {\n\t\t\tupdateStatusText(state, \"🗜 _Compacting context..._\");\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_end\": {\n\t\t\tconst result = (event as any).result;\n\t\t\tif (result) {\n\t\t\t\tconst before = result.tokensBefore || 0;\n\t\t\t\tconst msg = `🗜 Context compacted (was ${Math.round(before / 1000)}k tokens)`;\n\t\t\t\tsend(msg);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t// =====================================================================\n\t\t// Auto-retry — prevents agent_end from marking done during retries\n\t\t// =====================================================================\n\n\t\tcase \"auto_retry_start\": {\n\t\t\tconst { attempt, maxAttempts, delayMs, errorMessage } = event as any;\n\t\t\tstate.retryInProgress = true;\n\t\t\tstate.pendingRetry = false; // Layer 1 has taken over from Layer 2\n\t\t\tstate.retryAttempt = attempt;\n\t\t\tconst delaySec = Math.round(delayMs / 1000);\n\t\t\tconst shortErr = errorMessage?.length > 80 ? `${errorMessage.slice(0, 80)}…` : errorMessage;\n\t\t\tupdateStatusText(state, `🔄 _Retrying (${attempt}/${maxAttempts}) in ${delaySec}s — ${shortErr || \"error\"}_`);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_retry_end\": {\n\t\t\tconst { success, attempt, finalError } = event as any;\n\t\t\tstate.retryInProgress = false;\n\t\t\tstate.retryAttempt = 0;\n\t\t\tif (!success && finalError) {\n\t\t\t\t// Max retries exhausted — show final error\n\t\t\t\tsend(`❌ _Retry failed (${attempt} attempts):_ ${finalError}`, true);\n\t\t\t}\n\t\t\t// On success, the retry's agent_start/agent_end cycle will handle display normally\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"agent_end\": {\n\t\t\t// Flush any remaining tools\n\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\tsend(summary, true);\n\t\t\t\tstate.toolsSinceText = [];\n\t\t\t}\n\n\t\t\t// Check for error in agent_end messages\n\t\t\tconst errorMsg = (event.messages as any[])?.find(\n\t\t\t\t(m: any) => m.stopReason === \"error\" || m.stopReason === \"aborted\",\n\t\t\t);\n\n\t\t\t// Layer 2 (defensive): If this error looks retryable and we're not already\n\t\t\t// tracking a retry via Layer 1, don't mark done — the core will auto-retry\n\t\t\t// and emit a new agent_start/agent_end cycle.\n\t\t\tconst errorIsRetryable = errorMsg?.errorMessage && isRetryableError(errorMsg.errorMessage);\n\n\t\t\tif (errorMsg?.errorMessage) {\n\t\t\t\t// Suppress the scary error message during retry — user already saw the\n\t\t\t\t// auto_retry_start status. Only show the error if retry tracking missed it\n\t\t\t\t// (defensive: shouldn't happen, but better than silence).\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tconst provider = errorMsg.provider ? `${errorMsg.provider}/${errorMsg.model}` : \"\";\n\t\t\t\t\tconst prefix = provider ? `${provider}: ` : \"\";\n\t\t\t\t\tconst errLower = errorMsg.errorMessage.toLowerCase();\n\t\t\t\t\tconst hint =\n\t\t\t\t\t\terrLower.includes(\"connection\") || errLower.includes(\"timeout\") || errLower.includes(\"network\")\n\t\t\t\t\t\t\t? \"\\n_Provider may be down — try /model to switch._\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tsend(`❌ ${prefix}${errorMsg.errorMessage}${hint}`, true);\n\t\t\t\t}\n\t\t\t} else if (state.textBlocks.length === 0 && state.backgroundAgents.size === 0) {\n\t\t\t\t// Only show \"(No response)\" when truly done — not between agent cycles\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tsend(\"(No response)\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\n\t\t\t// Don't mark done if auto-retry is in progress (Layer 1) or the error\n\t\t\t// looks retryable (Layer 2 — defensive catch in case events were missed).\n\t\t\t// The core will emit a new agent_start/agent_end cycle for the retry.\n\t\t\tif (state.retryInProgress || errorIsRetryable) {\n\t\t\t\t// Signal that a retry is expected — the completion check in\n\t\t\t\t// ensureSubscribed needs this because it runs in the eventChain\n\t\t\t\t// BEFORE auto_retry_start has been processed.\n\t\t\t\tif (errorIsRetryable) state.pendingRetry = true;\n\t\t\t\t// Reset per-cycle state for the next agent loop\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// If background agents are still running, keep the subscription alive\n\t\t\t// and reset per-cycle state for the next agent loop\n\t\t\tif (state.backgroundAgents.size > 0) {\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Delete ephemeral status before signaling done\n\t\t\tif (state.statusMessageId) {\n\t\t\t\tawait state.editor.flush(state.chatId, state.statusMessageId);\n\t\t\t\tawait safeDelete(api, state.chatId, state.statusMessageId);\n\t\t\t\tstate.statusMessageId = null;\n\t\t\t}\n\n\t\t\t// Clean up editor\n\t\t\tstate.editor.clear();\n\n\t\t\t// Signal done AFTER cleanup — waitForCompletion checks this flag,\n\t\t\t// so setting it last ensures status message is deleted before DONE is sent\n\t\t\tstate.done = true;\n\t\t\tbreak;\n\t\t}\n\n\t\t// Handle error responses that leak through RPC (async prompt errors)\n\t\tcase \"response\": {\n\t\t\tconst resp = event as any;\n\t\t\tif (!resp.success && resp.error) {\n\t\t\t\tsend(`❌ ${resp.error}`, true);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Build and push a status update to the ephemeral message.\n */\nfunction updateStatus(state: EventDisplayState): void {\n\tif (!state.statusMessageId) return;\n\n\tconst parts: string[] = [];\n\n\t// Tool count header\n\tif (state.toolCount > 0) {\n\t\tparts.push(`🔧 *Tool ${state.toolCount}*`);\n\t}\n\n\t// Task list\n\tif (state.tasks.length > 0) {\n\t\tparts.push(formatTaskList(state.tasks));\n\t}\n\n\t// Background agents\n\tif (state.backgroundAgents.size > 0) {\n\t\tfor (const agent of state.backgroundAgents.values()) {\n\t\t\tparts.push(`🤖 *${agent.agentType}*: ${agent.taskSummary.slice(0, 200)}`);\n\t\t}\n\t}\n\n\t// Recent tools (last 5)\n\tif (state.toolsSinceText.length > 0) {\n\t\tconst recent = state.toolsSinceText.slice(-5);\n\t\tparts.push(recent.join(\"\\n\\n\"));\n\t}\n\n\tif (parts.length === 0) return;\n\n\tconst text = parts.join(\"\\n\\n\").slice(0, 4000);\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n\nfunction updateStatusText(state: EventDisplayState, text: string): void {\n\tif (!state.statusMessageId) return;\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/handlers/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Event display — translates RPC agent events into Telegram messages.\n *\n * Manages an ephemeral status message that shows tool use, task lists,\n * and subagent activity. Text from the agent is sent as permanent messages.\n */\n\nimport { existsSync } from \"node:fs\";\nimport type { Api } from \"grammy\";\nimport { InputFile } from \"grammy\";\nimport type { TrackedAgent } from \"../types.js\";\nimport { extractSendFiles } from \"../util/files.js\";\nimport { DebouncedEditor, log, safeDelete } from \"../util/telegram.js\";\n\n/** Callback to queue a message for delivery — never blocks the event chain */\nexport type SendFn = (text: string, long?: boolean) => void;\n\n/**\n * RPC events include both core AgentEvent and session-specific events\n * (tasks_update, background_agent_*, auto_compaction_*).\n * We type loosely here since the RPC client types onEvent as AgentEvent\n * but actually forwards all AgentSessionEvent types.\n */\ntype RpcEvent = { type: string; [key: string]: any };\n\n// Tool emoji mapping (tool names are lowercase in definitions)\nconst TOOL_EMOJI: Record<string, string> = {\n\tbash: \"🔧\",\n\tread: \"📖\",\n\tedit: \"✏️\",\n\twrite: \"📝\",\n\tgrep: \"🔎\",\n\tfind: \"🔍\",\n\tls: \"📂\",\n\tweb_search: \"🌐\",\n\tweb_fetch: \"🌐\",\n\tsubagent: \"🤖\",\n\ttasks_update: \"📋\",\n\tskill: \"⚡\",\n};\n\nfunction toolEmoji(name: string): string {\n\treturn TOOL_EMOJI[name] || \"🔧\";\n}\n\n/** Format a tool call for display */\nfunction formatTool(name: string, args: Record<string, any>): string {\n\tconst emoji = toolEmoji(name);\n\tswitch (name) {\n\t\tcase \"bash\": {\n\t\t\tconst cmd = args.command || \"\";\n\t\t\treturn `${emoji} *bash*\\n\\`${cmd.slice(0, 500)}\\``;\n\t\t}\n\t\tcase \"read\":\n\t\t\treturn `${emoji} *read*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"edit\":\n\t\t\treturn `${emoji} *edit*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"write\":\n\t\t\treturn `${emoji} *write*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"grep\":\n\t\t\treturn `${emoji} *grep*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"find\":\n\t\t\treturn `${emoji} *find*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"ls\":\n\t\t\treturn `${emoji} *ls*: \\`${args.path || \".\"}\\``;\n\t\tcase \"web_search\":\n\t\t\treturn `${emoji} *web\\\\_search*: ${args.query || \"?\"}`;\n\t\tcase \"web_fetch\":\n\t\t\treturn `${emoji} *web\\\\_fetch*: ${(args.url || \"?\").slice(0, 80)}`;\n\t\tcase \"subagent\":\n\t\t\treturn `${emoji} *subagent* (${args.agent || \"?\"}): ${(args.task || args.tasks?.[0]?.task || \"?\").slice(0, 200)}`;\n\t\tcase \"skill\":\n\t\t\treturn `${emoji} *skill*: ${args.skill || \"?\"}`;\n\t\tdefault:\n\t\t\treturn `${emoji} *${name}*`;\n\t}\n}\n\n/** Format task list as checklist */\nfunction formatTaskList(tasks: Array<{ id: string; title: string; status: string }>): string {\n\tif (!tasks.length) return \"📋 *Tasks*: (empty)\";\n\tconst lines = [\"📋 *Tasks*:\"];\n\tfor (const task of tasks) {\n\t\tif (task.status === \"completed\") lines.push(` ✅ ${task.title}`);\n\t\telse if (task.status === \"in_progress\") lines.push(` 🔄 ${task.title}`);\n\t\telse lines.push(` ⬜ ${task.title}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport interface EventDisplayState {\n\t/** Chat ID to send messages to */\n\tchatId: number;\n\t/** Message ID to reply to */\n\treplyToId: number;\n\t/** Ephemeral status message ID (edited in-place) */\n\tstatusMessageId: number | null;\n\t/** Tool messages accumulated since last text */\n\ttoolsSinceText: string[];\n\t/** Total tool count */\n\ttoolCount: number;\n\t/** All text blocks received */\n\ttextBlocks: string[];\n\t/** Current task list */\n\ttasks: Array<{ id: string; title: string; status: string }>;\n\t/** Background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether agent has finished */\n\tdone: boolean;\n\t/** Debounced editor instance */\n\teditor: DebouncedEditor;\n\t/** Whether auto-retry is in progress (Layer 1: reactive — set by auto_retry_start) */\n\tretryInProgress: boolean;\n\t/** Whether a retry is expected (Layer 2: predictive — set by agent_end when error looks retryable) */\n\tpendingRetry: boolean;\n\t/** Current retry attempt number for display */\n\tretryAttempt: number;\n\t/** Buddy controller — receives agent events for context + reactions */\n\tbuddyController?: any;\n}\n\n/**\n * Check if an error message looks retryable (overloaded, rate limit, server errors).\n * Mirrors the core's _isRetryableError check as a defensive Layer 2.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|ended without|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;\n\nfunction isRetryableError(errorMessage: string): boolean {\n\treturn RETRYABLE_ERROR_PATTERN.test(errorMessage);\n}\n\n/**\n * Create a fresh event display state for a new agent run.\n */\nexport function createEventDisplay(\n\tapi: Api,\n\tchatId: number,\n\treplyToId: number,\n\tstatusMessageId: number | null,\n): EventDisplayState {\n\treturn {\n\t\tchatId,\n\t\treplyToId,\n\t\tstatusMessageId,\n\t\ttoolsSinceText: [],\n\t\ttoolCount: 0,\n\t\ttextBlocks: [],\n\t\ttasks: [],\n\t\tbackgroundAgents: new Map(),\n\t\tdone: false,\n\t\teditor: new DebouncedEditor(api),\n\t\tretryInProgress: false,\n\t\tpendingRetry: false,\n\t\tretryAttempt: 0,\n\t};\n}\n\n/**\n * Process an agent event and update the display.\n */\nexport async function handleAgentEvent(\n\tsend: SendFn,\n\tapi: Api,\n\tstate: EventDisplayState,\n\tevent: RpcEvent,\n): Promise<void> {\n\tswitch (event.type) {\n\t\tcase \"tool_execution_start\": {\n\t\t\tconst name = event.toolName || \"?\";\n\t\t\tconst args = event.args || {};\n\t\t\tstate.toolCount++;\n\n\t\t\t// tasks_update is shown via the separate tasks_update event — skip from tool summary\n\t\t\tif (name !== \"tasks_update\") {\n\t\t\t\tconst toolMsg = formatTool(name, args);\n\t\t\t\tstate.toolsSinceText.push(toolMsg);\n\t\t\t}\n\n\t\t\t// Update status with tool count and recent tools\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tool_execution_end\": {\n\t\t\t// Feed event to buddy controller for context capture + error reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"message_end\": {\n\t\t\tconst msg = event.message;\n\n\t\t\t// Show subagent results — the parent agent references these but the\n\t\t\t// Telegram user can't see them otherwise. Send the full content.\n\t\t\tif (msg?.role === \"toolResult\" && msg?.toolName === \"subagent\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\t\t\tsend(`🤖 *Subagent result:*\\n${block.text.trim()}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Show background agent completion results — these arrive as user\n\t\t\t// messages injected by agent-session.ts via prompt()/steer() and\n\t\t\t// contain the actual subagent output the model sees.\n\t\t\tif (msg?.role === \"user\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.includes(\"<background-agent-complete>\")) {\n\t\t\t\t\t\t\t// Extract the content between the XML tags\n\t\t\t\t\t\t\tconst match = block.text.match(\n\t\t\t\t\t\t\t\t/<background-agent-complete>\\n?([\\s\\S]*?)\\n?<\\/background-agent-complete>/,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (match?.[1]?.trim()) {\n\t\t\t\t\t\t\t\tsend(`🤖 *Background agent complete:*\\n${match[1].trim()}`, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Only display assistant messages — user messages are echoed back by RPC\n\t\t\tif (msg?.role !== \"assistant\") break;\n\t\t\tconst content = msg?.content;\n\t\t\tif (!content || !Array.isArray(content)) break;\n\n\t\t\tfor (const block of content) {\n\t\t\t\t// Display thinking blocks (collapsed summary)\n\t\t\t\tif (block.type === \"thinking\" && block.thinking?.trim() && !block.redacted) {\n\t\t\t\t\tconst thinking = block.thinking.trim();\n\t\t\t\t\tsend(`💭 _${thinking}_`, true);\n\t\t\t\t}\n\n\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\tconst text = block.text.trim();\n\n\t\t\t\t\t// Flush accumulated tools as permanent summary\n\t\t\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\t\t\tsend(summary, true);\n\t\t\t\t\t\tstate.toolsSinceText = [];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send the text as a permanent message\n\t\t\t\t\tstate.textBlocks.push(text);\n\n\t\t\t\t\t// Check for file send markers\n\t\t\t\t\tconst [cleanText, filePaths] = extractSendFiles(text);\n\t\t\t\t\tif (cleanText) {\n\t\t\t\t\t\tsend(cleanText, true);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send any requested files (silently skip non-existent paths —\n\t\t\t\t\t// the pattern may appear in explanatory text)\n\t\t\t\t\tfor (const filePath of filePaths) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (existsSync(filePath)) {\n\t\t\t\t\t\t\t\tawait api.sendDocument(state.chatId, new InputFile(filePath));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlog(`[EVENTS] Failed to send file ${filePath}: ${e}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tasks_update\": {\n\t\t\tstate.tasks = (event as any).tasks || [];\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_start\": {\n\t\t\tconst { agentId, agentType, taskSummary } = event as any;\n\t\t\tstate.backgroundAgents.set(agentId, {\n\t\t\t\tagentId,\n\t\t\t\tagentType,\n\t\t\t\ttaskSummary,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t});\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_end\": {\n\t\t\tconst { agentId } = event as any;\n\t\t\tstate.backgroundAgents.delete(agentId);\n\t\t\t// Background agents completing does not end the parent's turn.\n\t\t\t// Only agent_end sets done — same as TUI behavior.\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_start\": {\n\t\t\tupdateStatusText(state, \"🗜 _Compacting context..._\");\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_end\": {\n\t\t\tconst result = (event as any).result;\n\t\t\tif (result) {\n\t\t\t\tconst before = result.tokensBefore || 0;\n\t\t\t\tconst msg = `🗜 Context compacted (was ${Math.round(before / 1000)}k tokens)`;\n\t\t\t\tsend(msg);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t// =====================================================================\n\t\t// Auto-retry — prevents agent_end from marking done during retries\n\t\t// =====================================================================\n\n\t\tcase \"auto_retry_start\": {\n\t\t\tconst { attempt, maxAttempts, delayMs, errorMessage } = event as any;\n\t\t\tstate.retryInProgress = true;\n\t\t\tstate.pendingRetry = false; // Layer 1 has taken over from Layer 2\n\t\t\tstate.retryAttempt = attempt;\n\t\t\tconst delaySec = Math.round(delayMs / 1000);\n\t\t\tconst shortErr = errorMessage?.length > 80 ? `${errorMessage.slice(0, 80)}…` : errorMessage;\n\t\t\tupdateStatusText(state, `🔄 _Retrying (${attempt}/${maxAttempts}) in ${delaySec}s — ${shortErr || \"error\"}_`);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_retry_end\": {\n\t\t\tconst { success, attempt, finalError } = event as any;\n\t\t\tstate.retryInProgress = false;\n\t\t\tstate.retryAttempt = 0;\n\t\t\tif (!success && finalError) {\n\t\t\t\t// Max retries exhausted — show final error\n\t\t\t\tsend(`❌ _Retry failed (${attempt} attempts):_ ${finalError}`, true);\n\t\t\t}\n\t\t\t// On success, the retry's agent_start/agent_end cycle will handle display normally\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"agent_end\": {\n\t\t\t// Flush any remaining tools\n\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\tsend(summary, true);\n\t\t\t\tstate.toolsSinceText = [];\n\t\t\t}\n\n\t\t\t// Check for error in agent_end messages\n\t\t\tconst errorMsg = (event.messages as any[])?.find(\n\t\t\t\t(m: any) => m.stopReason === \"error\" || m.stopReason === \"aborted\",\n\t\t\t);\n\n\t\t\t// Layer 2 (defensive): If this error looks retryable and we're not already\n\t\t\t// tracking a retry via Layer 1, don't mark done — the core will auto-retry\n\t\t\t// and emit a new agent_start/agent_end cycle.\n\t\t\tconst errorIsRetryable = errorMsg?.errorMessage && isRetryableError(errorMsg.errorMessage);\n\n\t\t\tif (errorMsg?.errorMessage) {\n\t\t\t\t// Suppress the scary error message during retry — user already saw the\n\t\t\t\t// auto_retry_start status. Only show the error if retry tracking missed it\n\t\t\t\t// (defensive: shouldn't happen, but better than silence).\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tconst provider = errorMsg.provider ? `${errorMsg.provider}/${errorMsg.model}` : \"\";\n\t\t\t\t\tconst prefix = provider ? `${provider}: ` : \"\";\n\t\t\t\t\tconst errLower = errorMsg.errorMessage.toLowerCase();\n\t\t\t\t\tconst hint =\n\t\t\t\t\t\terrLower.includes(\"connection\") || errLower.includes(\"timeout\") || errLower.includes(\"network\")\n\t\t\t\t\t\t\t? \"\\n_Provider may be down — try /model to switch._\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tsend(`❌ ${prefix}${errorMsg.errorMessage}${hint}`, true);\n\t\t\t\t}\n\t\t\t} else if (state.textBlocks.length === 0 && state.backgroundAgents.size === 0) {\n\t\t\t\t// Only show \"(No response)\" when truly done — not between agent cycles\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tsend(\"(No response)\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\n\t\t\t// Don't mark done if auto-retry is in progress (Layer 1) or the error\n\t\t\t// looks retryable (Layer 2 — defensive catch in case events were missed).\n\t\t\t// The core will emit a new agent_start/agent_end cycle for the retry.\n\t\t\tif (state.retryInProgress || errorIsRetryable) {\n\t\t\t\t// Signal that a retry is expected — the completion check in\n\t\t\t\t// ensureSubscribed needs this because it runs in the eventChain\n\t\t\t\t// BEFORE auto_retry_start has been processed.\n\t\t\t\tif (errorIsRetryable) state.pendingRetry = true;\n\t\t\t\t// Reset per-cycle state for the next agent loop\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// If background agents are still running, keep the subscription alive\n\t\t\t// and reset per-cycle state for the next agent loop\n\t\t\tif (state.backgroundAgents.size > 0) {\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Delete ephemeral status before signaling done\n\t\t\tif (state.statusMessageId) {\n\t\t\t\tawait state.editor.flush(state.chatId, state.statusMessageId);\n\t\t\t\tawait safeDelete(api, state.chatId, state.statusMessageId);\n\t\t\t\tstate.statusMessageId = null;\n\t\t\t}\n\n\t\t\t// Clean up editor\n\t\t\tstate.editor.clear();\n\n\t\t\t// Signal done AFTER cleanup — waitForCompletion checks this flag,\n\t\t\t// so setting it last ensures status message is deleted before DONE is sent\n\t\t\tstate.done = true;\n\t\t\tbreak;\n\t\t}\n\n\t\t// Handle error responses that leak through RPC (async prompt errors)\n\t\tcase \"response\": {\n\t\t\tconst resp = event as any;\n\t\t\tif (!resp.success && resp.error) {\n\t\t\t\tsend(`❌ ${resp.error}`, true);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Build and push a status update to the ephemeral message.\n */\nfunction updateStatus(state: EventDisplayState): void {\n\tif (!state.statusMessageId) return;\n\n\tconst parts: string[] = [];\n\n\t// Tool count header\n\tif (state.toolCount > 0) {\n\t\tparts.push(`🔧 *Tool ${state.toolCount}*`);\n\t}\n\n\t// Task list\n\tif (state.tasks.length > 0) {\n\t\tparts.push(formatTaskList(state.tasks));\n\t}\n\n\t// Background agents\n\tif (state.backgroundAgents.size > 0) {\n\t\tfor (const agent of state.backgroundAgents.values()) {\n\t\t\tparts.push(`🤖 *${agent.agentType}*: ${agent.taskSummary.slice(0, 200)}`);\n\t\t}\n\t}\n\n\t// Recent tools (last 5)\n\tif (state.toolsSinceText.length > 0) {\n\t\tconst recent = state.toolsSinceText.slice(-5);\n\t\tparts.push(recent.join(\"\\n\\n\"));\n\t}\n\n\tif (parts.length === 0) return;\n\n\tconst text = parts.join(\"\\n\\n\").slice(0, 4000);\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n\nfunction updateStatusText(state: EventDisplayState, text: string): void {\n\tif (!state.statusMessageId) return;\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/util/telegram.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAIlC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAU1E;AAID;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC1G;AAkBD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1G;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/util/telegram.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAIlC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAU1E;AAID;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC1G;AAkBD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1G;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAe1G;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3F;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAc,GAAG,MAAM,CAGnE;AAED;;;GAGG;AACH,qBAAa,eAAe;IAKf,OAAO,CAAC,GAAG;IAJvB,OAAO,CAAC,OAAO,CAAkF;IACjG,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IAEpC,YAAoB,GAAG,EAAE,GAAG,EAAI;IAEhC;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAgB1D;IAED;;OAEG;IACG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS5D;IAED,gCAAgC;IAChC,KAAK,IAAI,IAAI,CAGZ;CACD;AAED,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAErC","sourcesContent":["/**\n * Telegram message utilities — splitting, markdown fallback, rate-limit debounce.\n */\n\nimport type { Api } from \"grammy\";\n\nconst SAFE_LENGTH = 4000; // Leave room for markdown overhead (Telegram max is 4096)\n\n/**\n * Wrap a promise with a timeout. Rejects with an error if the promise\n * doesn't settle within `ms` milliseconds. Used to prevent Telegram API\n * calls from hanging the event chain indefinitely.\n *\n * The timer is cleared when the promise settles to avoid accumulating\n * stale timers in the Node.js timer heap during heavy message runs.\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout>;\n\tconst timeout = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => reject(new Error(`Telegram API timeout after ${ms}ms`)), ms);\n\t});\n\t// Prevent unhandled rejection from the slow promise if the timeout wins.\n\t// Without this, a late rejection after timeout becomes an unhandled rejection\n\t// which crashes the process in Node.js >= 15.\n\tpromise.catch(() => {});\n\treturn Promise.race([promise, timeout]).finally(() => clearTimeout(timer!));\n}\n\nconst API_TIMEOUT = 15_000; // 15s per Telegram API call\n\n/**\n * Send a message, falling back to plain text if Markdown fails.\n */\nexport async function safeSend(api: Api, chatId: number, text: string, replyToId?: number): Promise<number> {\n\t// Truncate to Telegram's limit — callers sending long content should use sendLong instead\n\tif (text.length > SAFE_LENGTH) text = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tconst msg = await withTimeout(\n\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\tparse_mode: \"Markdown\",\n\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t}),\n\t\t\tAPI_TIMEOUT,\n\t\t);\n\t\treturn msg.message_id;\n\t} catch (e) {\n\t\t// Only retry as plain text for Markdown parse errors (Telegram 400).\n\t\t// For timeouts/network errors, the original request may still be in-flight —\n\t\t// retrying would risk delivering duplicate messages. Return 0 and let\n\t\t// the outbox retry loop handle it.\n\t\tif (!isTelegramParseError(e)) {\n\t\t\tlog(`[WARN] Failed to send message: ${e}`);\n\t\t\treturn 0;\n\t\t}\n\t\ttry {\n\t\t\tconst msg = await withTimeout(\n\t\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t\t}),\n\t\t\t\tAPI_TIMEOUT,\n\t\t\t);\n\t\t\treturn msg.message_id;\n\t\t} catch (e2) {\n\t\t\tlog(`[WARN] Failed to send message (plain text fallback): ${e2}`);\n\t\t\treturn 0;\n\t\t}\n\t}\n}\n\n/**\n * Check if an error is a Telegram API parse error (HTTP 400 for bad Markdown).\n * These are safe to retry as plain text because the original request definitively\n * failed — Telegram won't deliver it. Timeouts and network errors are NOT safe\n * to retry immediately because the in-flight request may still succeed.\n */\nfunction isTelegramParseError(e: unknown): boolean {\n\tif (!e || typeof e !== \"object\") return false;\n\t// grammy HttpError has error_code\n\tconst code = (e as any).error_code ?? (e as any).status ?? (e as any).statusCode;\n\tif (code === 400) return true;\n\t// Fallback: check message for common Telegram parse error text\n\tconst msg = (e as any).message ?? String(e);\n\treturn typeof msg === \"string\" && msg.includes(\"can't parse entities\");\n}\n\n/**\n * Send a long message, splitting at newline boundaries.\n * Stops on first chunk failure to avoid resending already-delivered chunks\n * on retry. Returns the remaining (undelivered) text, or empty string if\n * everything was delivered.\n */\nexport async function sendLong(api: Api, chatId: number, text: string, replyToId?: number): Promise<string> {\n\twhile (text) {\n\t\tif (text.length <= SAFE_LENGTH) {\n\t\t\tconst msgId = await safeSend(api, chatId, text, replyToId);\n\t\t\treturn msgId === 0 ? text : \"\";\n\t\t}\n\t\tlet splitAt = text.lastIndexOf(\"\\n\", SAFE_LENGTH);\n\t\tif (splitAt < 2000) splitAt = SAFE_LENGTH;\n\t\tconst msgId = await safeSend(api, chatId, text.slice(0, splitAt), replyToId);\n\t\tif (msgId === 0) return text; // Stop — return full remaining text including this failed chunk\n\t\ttext = text.slice(splitAt).replace(/^\\n+/, \"\");\n\t\treplyToId = undefined; // Only reply to the first chunk\n\t}\n\treturn \"\";\n}\n\n/**\n * Edit a message safely, falling back to plain text.\n */\nexport async function safeEdit(api: Api, chatId: number, messageId: number, text: string): Promise<boolean> {\n\ttext = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tawait withTimeout(api.editMessageText(chatId, messageId, text, { parse_mode: \"Markdown\" }), API_TIMEOUT);\n\t\treturn true;\n\t} catch {\n\t\t/* Markdown parse rejected by Telegram — retry as plaintext */\n\t\ttry {\n\t\t\tawait withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\t/* Plaintext edit also failed (message deleted, permissions revoked) */\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Delete a message, ignoring errors.\n */\nexport async function safeDelete(api: Api, chatId: number, messageId: number): Promise<void> {\n\ttry {\n\t\tawait withTimeout(api.deleteMessage(chatId, messageId), API_TIMEOUT);\n\t} catch {\n\t\t// Ignore — message may already be deleted or too old\n\t}\n}\n\n/**\n * Truncate text to fit Telegram's limit.\n */\nexport function truncate(text: string, maxLen = SAFE_LENGTH): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.slice(0, maxLen - 20)}\\n\\n_(truncated)_`;\n}\n\n/**\n * Rate-limited message editor — debounces rapid edits to avoid Telegram 429s.\n * Minimum 2 seconds between edits to the same message.\n */\nexport class DebouncedEditor {\n\tprivate pending: Map<string, { text: string; timer: ReturnType<typeof setTimeout> }> = new Map();\n\tprivate lastEdit: Map<string, number> = new Map();\n\tprivate readonly minInterval = 2000; // 2s between edits\n\n\tconstructor(private api: Api) {}\n\n\t/**\n\t * Schedule an edit. If another edit comes in before the debounce fires,\n\t * the previous one is replaced.\n\t */\n\tedit(chatId: number, messageId: number, text: string): void {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) clearTimeout(existing.timer);\n\n\t\tconst lastTime = this.lastEdit.get(key) || 0;\n\t\tconst elapsed = Date.now() - lastTime;\n\t\tconst delay = Math.max(0, this.minInterval - elapsed);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tvoid safeEdit(this.api, chatId, messageId, text);\n\t\t}, delay);\n\n\t\tthis.pending.set(key, { text, timer });\n\t}\n\n\t/**\n\t * Force flush a pending edit immediately (e.g., before deleting the message).\n\t */\n\tasync flush(chatId: number, messageId: number): Promise<void> {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing.timer);\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tawait safeEdit(this.api, chatId, messageId, existing.text);\n\t\t}\n\t}\n\n\t/** Cancel all pending edits. */\n\tclear(): void {\n\t\tfor (const { timer } of this.pending.values()) clearTimeout(timer);\n\t\tthis.pending.clear();\n\t}\n}\n\nexport function log(msg: string): void {\n\tconsole.error(msg);\n}\n"]}
|
package/dist/util/telegram.js
CHANGED
|
@@ -107,11 +107,13 @@ export async function safeEdit(api, chatId, messageId, text) {
|
|
|
107
107
|
return true;
|
|
108
108
|
}
|
|
109
109
|
catch {
|
|
110
|
+
/* Markdown parse rejected by Telegram — retry as plaintext */
|
|
110
111
|
try {
|
|
111
112
|
await withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);
|
|
112
113
|
return true;
|
|
113
114
|
}
|
|
114
115
|
catch {
|
|
116
|
+
/* Plaintext edit also failed (message deleted, permissions revoked) */
|
|
115
117
|
return false;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../src/util/telegram.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Telegram message utilities — splitting, markdown fallback, rate-limit debounce.\n */\n\nimport type { Api } from \"grammy\";\n\nconst SAFE_LENGTH = 4000; // Leave room for markdown overhead (Telegram max is 4096)\n\n/**\n * Wrap a promise with a timeout. Rejects with an error if the promise\n * doesn't settle within `ms` milliseconds. Used to prevent Telegram API\n * calls from hanging the event chain indefinitely.\n *\n * The timer is cleared when the promise settles to avoid accumulating\n * stale timers in the Node.js timer heap during heavy message runs.\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout>;\n\tconst timeout = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => reject(new Error(`Telegram API timeout after ${ms}ms`)), ms);\n\t});\n\t// Prevent unhandled rejection from the slow promise if the timeout wins.\n\t// Without this, a late rejection after timeout becomes an unhandled rejection\n\t// which crashes the process in Node.js >= 15.\n\tpromise.catch(() => {});\n\treturn Promise.race([promise, timeout]).finally(() => clearTimeout(timer!));\n}\n\nconst API_TIMEOUT = 15_000; // 15s per Telegram API call\n\n/**\n * Send a message, falling back to plain text if Markdown fails.\n */\nexport async function safeSend(api: Api, chatId: number, text: string, replyToId?: number): Promise<number> {\n\t// Truncate to Telegram's limit — callers sending long content should use sendLong instead\n\tif (text.length > SAFE_LENGTH) text = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tconst msg = await withTimeout(\n\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\tparse_mode: \"Markdown\",\n\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t}),\n\t\t\tAPI_TIMEOUT,\n\t\t);\n\t\treturn msg.message_id;\n\t} catch (e) {\n\t\t// Only retry as plain text for Markdown parse errors (Telegram 400).\n\t\t// For timeouts/network errors, the original request may still be in-flight —\n\t\t// retrying would risk delivering duplicate messages. Return 0 and let\n\t\t// the outbox retry loop handle it.\n\t\tif (!isTelegramParseError(e)) {\n\t\t\tlog(`[WARN] Failed to send message: ${e}`);\n\t\t\treturn 0;\n\t\t}\n\t\ttry {\n\t\t\tconst msg = await withTimeout(\n\t\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t\t}),\n\t\t\t\tAPI_TIMEOUT,\n\t\t\t);\n\t\t\treturn msg.message_id;\n\t\t} catch (e2) {\n\t\t\tlog(`[WARN] Failed to send message (plain text fallback): ${e2}`);\n\t\t\treturn 0;\n\t\t}\n\t}\n}\n\n/**\n * Check if an error is a Telegram API parse error (HTTP 400 for bad Markdown).\n * These are safe to retry as plain text because the original request definitively\n * failed — Telegram won't deliver it. Timeouts and network errors are NOT safe\n * to retry immediately because the in-flight request may still succeed.\n */\nfunction isTelegramParseError(e: unknown): boolean {\n\tif (!e || typeof e !== \"object\") return false;\n\t// grammy HttpError has error_code\n\tconst code = (e as any).error_code ?? (e as any).status ?? (e as any).statusCode;\n\tif (code === 400) return true;\n\t// Fallback: check message for common Telegram parse error text\n\tconst msg = (e as any).message ?? String(e);\n\treturn typeof msg === \"string\" && msg.includes(\"can't parse entities\");\n}\n\n/**\n * Send a long message, splitting at newline boundaries.\n * Stops on first chunk failure to avoid resending already-delivered chunks\n * on retry. Returns the remaining (undelivered) text, or empty string if\n * everything was delivered.\n */\nexport async function sendLong(api: Api, chatId: number, text: string, replyToId?: number): Promise<string> {\n\twhile (text) {\n\t\tif (text.length <= SAFE_LENGTH) {\n\t\t\tconst msgId = await safeSend(api, chatId, text, replyToId);\n\t\t\treturn msgId === 0 ? text : \"\";\n\t\t}\n\t\tlet splitAt = text.lastIndexOf(\"\\n\", SAFE_LENGTH);\n\t\tif (splitAt < 2000) splitAt = SAFE_LENGTH;\n\t\tconst msgId = await safeSend(api, chatId, text.slice(0, splitAt), replyToId);\n\t\tif (msgId === 0) return text; // Stop — return full remaining text including this failed chunk\n\t\ttext = text.slice(splitAt).replace(/^\\n+/, \"\");\n\t\treplyToId = undefined; // Only reply to the first chunk\n\t}\n\treturn \"\";\n}\n\n/**\n * Edit a message safely, falling back to plain text.\n */\nexport async function safeEdit(api: Api, chatId: number, messageId: number, text: string): Promise<boolean> {\n\ttext = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tawait withTimeout(api.editMessageText(chatId, messageId, text, { parse_mode: \"Markdown\" }), API_TIMEOUT);\n\t\treturn true;\n\t} catch {\n\t\ttry {\n\t\t\tawait withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Delete a message, ignoring errors.\n */\nexport async function safeDelete(api: Api, chatId: number, messageId: number): Promise<void> {\n\ttry {\n\t\tawait withTimeout(api.deleteMessage(chatId, messageId), API_TIMEOUT);\n\t} catch {\n\t\t// Ignore — message may already be deleted or too old\n\t}\n}\n\n/**\n * Truncate text to fit Telegram's limit.\n */\nexport function truncate(text: string, maxLen = SAFE_LENGTH): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.slice(0, maxLen - 20)}\\n\\n_(truncated)_`;\n}\n\n/**\n * Rate-limited message editor — debounces rapid edits to avoid Telegram 429s.\n * Minimum 2 seconds between edits to the same message.\n */\nexport class DebouncedEditor {\n\tprivate pending: Map<string, { text: string; timer: ReturnType<typeof setTimeout> }> = new Map();\n\tprivate lastEdit: Map<string, number> = new Map();\n\tprivate readonly minInterval = 2000; // 2s between edits\n\n\tconstructor(private api: Api) {}\n\n\t/**\n\t * Schedule an edit. If another edit comes in before the debounce fires,\n\t * the previous one is replaced.\n\t */\n\tedit(chatId: number, messageId: number, text: string): void {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) clearTimeout(existing.timer);\n\n\t\tconst lastTime = this.lastEdit.get(key) || 0;\n\t\tconst elapsed = Date.now() - lastTime;\n\t\tconst delay = Math.max(0, this.minInterval - elapsed);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tvoid safeEdit(this.api, chatId, messageId, text);\n\t\t}, delay);\n\n\t\tthis.pending.set(key, { text, timer });\n\t}\n\n\t/**\n\t * Force flush a pending edit immediately (e.g., before deleting the message).\n\t */\n\tasync flush(chatId: number, messageId: number): Promise<void> {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing.timer);\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tawait safeEdit(this.api, chatId, messageId, existing.text);\n\t\t}\n\t}\n\n\t/** Cancel all pending edits. */\n\tclear(): void {\n\t\tfor (const { timer } of this.pending.values()) clearTimeout(timer);\n\t\tthis.pending.clear();\n\t}\n}\n\nexport function log(msg: string): void {\n\tconsole.error(msg);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../src/util/telegram.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Telegram message utilities — splitting, markdown fallback, rate-limit debounce.\n */\n\nimport type { Api } from \"grammy\";\n\nconst SAFE_LENGTH = 4000; // Leave room for markdown overhead (Telegram max is 4096)\n\n/**\n * Wrap a promise with a timeout. Rejects with an error if the promise\n * doesn't settle within `ms` milliseconds. Used to prevent Telegram API\n * calls from hanging the event chain indefinitely.\n *\n * The timer is cleared when the promise settles to avoid accumulating\n * stale timers in the Node.js timer heap during heavy message runs.\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout>;\n\tconst timeout = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => reject(new Error(`Telegram API timeout after ${ms}ms`)), ms);\n\t});\n\t// Prevent unhandled rejection from the slow promise if the timeout wins.\n\t// Without this, a late rejection after timeout becomes an unhandled rejection\n\t// which crashes the process in Node.js >= 15.\n\tpromise.catch(() => {});\n\treturn Promise.race([promise, timeout]).finally(() => clearTimeout(timer!));\n}\n\nconst API_TIMEOUT = 15_000; // 15s per Telegram API call\n\n/**\n * Send a message, falling back to plain text if Markdown fails.\n */\nexport async function safeSend(api: Api, chatId: number, text: string, replyToId?: number): Promise<number> {\n\t// Truncate to Telegram's limit — callers sending long content should use sendLong instead\n\tif (text.length > SAFE_LENGTH) text = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tconst msg = await withTimeout(\n\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\tparse_mode: \"Markdown\",\n\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t}),\n\t\t\tAPI_TIMEOUT,\n\t\t);\n\t\treturn msg.message_id;\n\t} catch (e) {\n\t\t// Only retry as plain text for Markdown parse errors (Telegram 400).\n\t\t// For timeouts/network errors, the original request may still be in-flight —\n\t\t// retrying would risk delivering duplicate messages. Return 0 and let\n\t\t// the outbox retry loop handle it.\n\t\tif (!isTelegramParseError(e)) {\n\t\t\tlog(`[WARN] Failed to send message: ${e}`);\n\t\t\treturn 0;\n\t\t}\n\t\ttry {\n\t\t\tconst msg = await withTimeout(\n\t\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t\t}),\n\t\t\t\tAPI_TIMEOUT,\n\t\t\t);\n\t\t\treturn msg.message_id;\n\t\t} catch (e2) {\n\t\t\tlog(`[WARN] Failed to send message (plain text fallback): ${e2}`);\n\t\t\treturn 0;\n\t\t}\n\t}\n}\n\n/**\n * Check if an error is a Telegram API parse error (HTTP 400 for bad Markdown).\n * These are safe to retry as plain text because the original request definitively\n * failed — Telegram won't deliver it. Timeouts and network errors are NOT safe\n * to retry immediately because the in-flight request may still succeed.\n */\nfunction isTelegramParseError(e: unknown): boolean {\n\tif (!e || typeof e !== \"object\") return false;\n\t// grammy HttpError has error_code\n\tconst code = (e as any).error_code ?? (e as any).status ?? (e as any).statusCode;\n\tif (code === 400) return true;\n\t// Fallback: check message for common Telegram parse error text\n\tconst msg = (e as any).message ?? String(e);\n\treturn typeof msg === \"string\" && msg.includes(\"can't parse entities\");\n}\n\n/**\n * Send a long message, splitting at newline boundaries.\n * Stops on first chunk failure to avoid resending already-delivered chunks\n * on retry. Returns the remaining (undelivered) text, or empty string if\n * everything was delivered.\n */\nexport async function sendLong(api: Api, chatId: number, text: string, replyToId?: number): Promise<string> {\n\twhile (text) {\n\t\tif (text.length <= SAFE_LENGTH) {\n\t\t\tconst msgId = await safeSend(api, chatId, text, replyToId);\n\t\t\treturn msgId === 0 ? text : \"\";\n\t\t}\n\t\tlet splitAt = text.lastIndexOf(\"\\n\", SAFE_LENGTH);\n\t\tif (splitAt < 2000) splitAt = SAFE_LENGTH;\n\t\tconst msgId = await safeSend(api, chatId, text.slice(0, splitAt), replyToId);\n\t\tif (msgId === 0) return text; // Stop — return full remaining text including this failed chunk\n\t\ttext = text.slice(splitAt).replace(/^\\n+/, \"\");\n\t\treplyToId = undefined; // Only reply to the first chunk\n\t}\n\treturn \"\";\n}\n\n/**\n * Edit a message safely, falling back to plain text.\n */\nexport async function safeEdit(api: Api, chatId: number, messageId: number, text: string): Promise<boolean> {\n\ttext = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tawait withTimeout(api.editMessageText(chatId, messageId, text, { parse_mode: \"Markdown\" }), API_TIMEOUT);\n\t\treturn true;\n\t} catch {\n\t\t/* Markdown parse rejected by Telegram — retry as plaintext */\n\t\ttry {\n\t\t\tawait withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\t/* Plaintext edit also failed (message deleted, permissions revoked) */\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Delete a message, ignoring errors.\n */\nexport async function safeDelete(api: Api, chatId: number, messageId: number): Promise<void> {\n\ttry {\n\t\tawait withTimeout(api.deleteMessage(chatId, messageId), API_TIMEOUT);\n\t} catch {\n\t\t// Ignore — message may already be deleted or too old\n\t}\n}\n\n/**\n * Truncate text to fit Telegram's limit.\n */\nexport function truncate(text: string, maxLen = SAFE_LENGTH): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.slice(0, maxLen - 20)}\\n\\n_(truncated)_`;\n}\n\n/**\n * Rate-limited message editor — debounces rapid edits to avoid Telegram 429s.\n * Minimum 2 seconds between edits to the same message.\n */\nexport class DebouncedEditor {\n\tprivate pending: Map<string, { text: string; timer: ReturnType<typeof setTimeout> }> = new Map();\n\tprivate lastEdit: Map<string, number> = new Map();\n\tprivate readonly minInterval = 2000; // 2s between edits\n\n\tconstructor(private api: Api) {}\n\n\t/**\n\t * Schedule an edit. If another edit comes in before the debounce fires,\n\t * the previous one is replaced.\n\t */\n\tedit(chatId: number, messageId: number, text: string): void {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) clearTimeout(existing.timer);\n\n\t\tconst lastTime = this.lastEdit.get(key) || 0;\n\t\tconst elapsed = Date.now() - lastTime;\n\t\tconst delay = Math.max(0, this.minInterval - elapsed);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tvoid safeEdit(this.api, chatId, messageId, text);\n\t\t}, delay);\n\n\t\tthis.pending.set(key, { text, timer });\n\t}\n\n\t/**\n\t * Force flush a pending edit immediately (e.g., before deleting the message).\n\t */\n\tasync flush(chatId: number, messageId: number): Promise<void> {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing.timer);\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tawait safeEdit(this.api, chatId, messageId, existing.text);\n\t\t}\n\t}\n\n\t/** Cancel all pending edits. */\n\tclear(): void {\n\t\tfor (const { timer } of this.pending.values()) clearTimeout(timer);\n\t\tthis.pending.clear();\n\t}\n}\n\nexport function log(msg: string): void {\n\tconsole.error(msg);\n}\n"]}
|