@agentprojectcontext/apx 1.31.2 → 1.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/package.json +6 -1
  2. package/skills/apc-context/SKILL.md +5 -2
  3. package/skills/apx/SKILL.md +3 -3
  4. package/skills/apx-agency-agents/SKILL.md +5 -5
  5. package/skills/apx-agent/SKILL.md +7 -7
  6. package/skills/apx-mcp/SKILL.md +6 -4
  7. package/skills/apx-mcp-builder/SKILL.md +4 -7
  8. package/skills/apx-project/SKILL.md +4 -5
  9. package/skills/apx-routine/SKILL.md +14 -12
  10. package/skills/apx-runtime/SKILL.md +5 -3
  11. package/skills/apx-sessions/SKILL.md +5 -5
  12. package/skills/apx-skill-builder/SKILL.md +10 -6
  13. package/skills/apx-task/SKILL.md +8 -8
  14. package/skills/apx-telegram/SKILL.md +23 -7
  15. package/skills/apx-voice/SKILL.md +8 -6
  16. package/src/core/{agent-system.js → agent/build-agent-system.js} +10 -12
  17. package/src/core/agent/constants.js +5 -0
  18. package/src/core/agent/index.js +0 -2
  19. package/src/core/{agent-memory.js → agent/memory.js} +2 -2
  20. package/src/core/agent/model-router.js +21 -43
  21. package/src/core/agent/prompt-builder.js +17 -63
  22. package/src/core/agent/prompts/action-discipline.md +17 -0
  23. package/src/core/agent/prompts/channels/code.md +8 -12
  24. package/src/core/agent/prompts/channels/desktop.md +6 -4
  25. package/src/core/agent/prompts/channels/routine.md +10 -1
  26. package/src/core/agent/prompts/channels/telegram.md +5 -0
  27. package/src/core/agent/prompts/channels/web_code.md +20 -0
  28. package/src/core/agent/prompts/modes/voice.md +2 -2
  29. package/src/core/agent/prompts/super-agent-base.md +2 -2
  30. package/src/core/agent/run-agent.js +66 -36
  31. package/src/core/agent/runtime-bridge.js +42 -0
  32. package/src/core/agent/self-memory.js +19 -9
  33. package/src/core/agent/skills/catalog.js +65 -0
  34. package/src/core/agent/skills/index.js +6 -0
  35. package/src/{host/daemon/skills-loader.js → core/agent/skills/loader.js} +3 -3
  36. package/src/core/agent/skills/rag.js +91 -0
  37. package/src/core/agent/skills/trigger.js +71 -0
  38. package/src/{host/daemon → core/agent}/super-agent.js +5 -5
  39. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/add-project.js +3 -4
  40. package/src/core/agent/tools/handlers/ask-questions.js +115 -0
  41. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-agent.js +2 -2
  42. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-mcp.js +1 -2
  43. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-runtime.js +10 -11
  44. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/create-task.js +1 -1
  45. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/discover-tools.js +1 -1
  46. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/edit-file.js +1 -2
  47. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/import-agent.js +4 -5
  48. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-agents.js +1 -1
  49. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-skills.js +7 -2
  50. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-tasks.js +1 -1
  51. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-vault-agents.js +1 -1
  52. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/load-skill.js +1 -1
  53. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-agent-memory.js +1 -1
  54. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-self-memory.js +1 -1
  55. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/remember.js +1 -1
  56. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/run-shell.js +1 -2
  57. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-messages.js +1 -1
  58. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-sessions.js +1 -1
  59. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/send-telegram.js +0 -2
  60. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-identity.js +1 -3
  61. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-permission-mode.js +1 -3
  62. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/tail-messages.js +1 -1
  63. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/transcribe-audio.js +1 -1
  64. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/write-file.js +1 -2
  65. package/src/core/agent/tools/helpers.js +74 -0
  66. package/src/{host/daemon/super-agent-tools → core/agent/tools}/registry-bridge.js +3 -3
  67. package/src/{host/daemon/super-agent-tools/index.js → core/agent/tools/registry.js} +31 -32
  68. package/src/core/apc/agents-vault.js +37 -0
  69. package/src/core/{scaffold.js → apc/scaffold.js} +4 -5
  70. package/src/core/{config.js → config/index.js} +21 -27
  71. package/src/core/config/paths.js +32 -0
  72. package/src/core/constants/actors.js +8 -0
  73. package/src/core/constants/channels.js +19 -0
  74. package/src/core/constants/index.js +5 -0
  75. package/src/core/constants/permissions.js +17 -0
  76. package/src/core/constants/roles.js +9 -0
  77. package/src/core/engines/_streaming.js +63 -0
  78. package/src/core/engines/anthropic.js +11 -22
  79. package/src/core/engines/ollama.js +7 -16
  80. package/src/core/identity/index.js +8 -0
  81. package/src/core/{identity.js → identity/self.js} +5 -5
  82. package/src/core/{telegram-identity.js → identity/telegram.js} +1 -1
  83. package/src/core/logging.js +1 -1
  84. package/src/core/mascot.js +1 -1
  85. package/src/core/memory/active-threads.js +10 -10
  86. package/src/core/memory/broker.js +9 -9
  87. package/src/core/memory/compactor.js +2 -2
  88. package/src/core/memory/index.js +2 -2
  89. package/src/core/memory/indexer.js +1 -1
  90. package/src/core/{code-sessions-store.js → stores/code-sessions.js} +3 -7
  91. package/src/core/{messages-store.js → stores/messages.js} +6 -4
  92. package/src/core/stores/routine-memory.js +71 -0
  93. package/src/core/{routines-store.js → stores/routines.js} +1 -3
  94. package/src/core/stores/runtime-sessions.js +99 -0
  95. package/src/core/{tasks-store.js → stores/tasks.js} +3 -8
  96. package/src/core/update-check.js +1 -1
  97. package/src/core/util/ids.js +14 -0
  98. package/src/core/util/index.js +2 -0
  99. package/src/core/util/text-similarity.js +52 -0
  100. package/src/core/util/time.js +9 -0
  101. package/src/core/voice/tts.js +1 -1
  102. package/src/host/daemon/api/admin-config.js +4 -3
  103. package/src/host/daemon/api/admin.js +1 -1
  104. package/src/host/daemon/api/agents.js +4 -25
  105. package/src/host/daemon/api/artifacts.js +118 -1
  106. package/src/host/daemon/api/code.js +60 -16
  107. package/src/host/daemon/api/confirm.js +1 -1
  108. package/src/host/daemon/api/connections.js +2 -2
  109. package/src/host/daemon/api/conversations.js +2 -2
  110. package/src/host/daemon/api/deck.js +1 -1
  111. package/src/host/daemon/api/desktop.js +1 -1
  112. package/src/host/daemon/api/embeddings.js +4 -4
  113. package/src/host/daemon/api/engines.js +2 -2
  114. package/src/host/daemon/api/exec.js +3 -3
  115. package/src/host/daemon/api/identity.js +1 -1
  116. package/src/host/daemon/api/mcps.js +1 -1
  117. package/src/host/daemon/api/messages.js +1 -1
  118. package/src/host/daemon/api/runtimes.js +9 -8
  119. package/src/host/daemon/api/sessions-search.js +1 -1
  120. package/src/host/daemon/api/sessions.js +2 -2
  121. package/src/host/daemon/api/shared.js +5 -4
  122. package/src/host/daemon/api/skills.js +30 -0
  123. package/src/host/daemon/api/super-agent.js +29 -9
  124. package/src/host/daemon/api/tasks.js +2 -2
  125. package/src/host/daemon/api/telegram.js +1 -1
  126. package/src/host/daemon/api/tools.js +6 -6
  127. package/src/host/daemon/api/tts.js +2 -2
  128. package/src/host/daemon/api/voice.js +14 -12
  129. package/src/host/daemon/api.js +2 -0
  130. package/src/host/daemon/compact.js +1 -1
  131. package/src/host/daemon/db.js +4 -4
  132. package/src/host/daemon/desktop-ws.js +1 -1
  133. package/src/host/daemon/index.js +4 -4
  134. package/src/host/daemon/plugins/{desktop.js → desktop/index.js} +45 -6
  135. package/src/host/daemon/plugins/index.js +2 -2
  136. package/src/host/daemon/plugins/telegram/ask.js +309 -0
  137. package/src/host/daemon/plugins/{telegram.js → telegram/index.js} +390 -191
  138. package/src/host/daemon/plugins/telegram/media.js +162 -0
  139. package/src/host/daemon/projects-helpers.js +54 -0
  140. package/src/host/daemon/routines.js +28 -12
  141. package/src/host/daemon/smoke.js +2 -2
  142. package/src/host/daemon/token-store.js +1 -1
  143. package/src/host/daemon/transcription.js +2 -2
  144. package/src/host/daemon/wakeup.js +2 -2
  145. package/src/interfaces/cli/commands/agent.js +3 -3
  146. package/src/interfaces/cli/commands/artifact.js +99 -0
  147. package/src/interfaces/cli/commands/command.js +1 -1
  148. package/src/interfaces/cli/commands/config.js +3 -2
  149. package/src/interfaces/cli/commands/desktop.js +1 -1
  150. package/src/interfaces/cli/commands/exec.js +2 -1
  151. package/src/interfaces/cli/commands/identity.js +2 -2
  152. package/src/interfaces/cli/commands/init.js +1 -1
  153. package/src/interfaces/cli/commands/mcp.js +1 -1
  154. package/src/interfaces/cli/commands/memory.js +2 -2
  155. package/src/interfaces/cli/commands/model.js +16 -6
  156. package/src/interfaces/cli/commands/project.js +1 -1
  157. package/src/interfaces/cli/commands/routine.js +58 -0
  158. package/src/interfaces/cli/commands/search.js +1 -1
  159. package/src/interfaces/cli/commands/session.js +4 -4
  160. package/src/interfaces/cli/commands/setup.js +4 -3
  161. package/src/interfaces/cli/commands/skills.js +25 -4
  162. package/src/interfaces/cli/commands/status.js +1 -1
  163. package/src/interfaces/cli/commands/sys.js +11 -4
  164. package/src/interfaces/cli/commands/update.js +1 -1
  165. package/src/interfaces/cli/index.js +8 -4
  166. package/src/interfaces/cli/postinstall.js +2 -2
  167. package/src/interfaces/cli/terminal-chat/renderer.js +22 -2
  168. package/src/interfaces/mcp-server/index.js +1 -1
  169. package/src/interfaces/tui/component/prompt/index.tsx +3 -1
  170. package/src/interfaces/tui/context/sdk-apx.tsx +47 -7
  171. package/src/interfaces/tui/context/sync-apx.tsx +20 -2
  172. package/src/interfaces/tui/context/sync.tsx +2 -1
  173. package/src/interfaces/tui/routes/session/index.tsx +151 -136
  174. package/src/interfaces/tui/routes/session/sidebar-apx.tsx +37 -15
  175. package/src/interfaces/tui/run.ts +2 -0
  176. package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +1 -0
  177. package/src/interfaces/web/dist/assets/index-BkybwwRn.js +570 -0
  178. package/src/interfaces/web/dist/assets/index-BkybwwRn.js.map +1 -0
  179. package/src/interfaces/web/dist/index.html +2 -2
  180. package/src/interfaces/web/package-lock.json +9 -9
  181. package/src/interfaces/web/src/App.tsx +51 -32
  182. package/src/interfaces/web/src/components/RobyBubble.tsx +12 -6
  183. package/src/interfaces/web/src/components/UiSelect.tsx +1 -1
  184. package/src/interfaces/web/src/components/chat/AskQuestionsCard.tsx +72 -0
  185. package/src/interfaces/web/src/components/chat/InlineAskPanel.tsx +399 -0
  186. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  187. package/src/interfaces/web/src/components/chat/MessageList.tsx +2 -1
  188. package/src/interfaces/web/src/components/chat/SkillPicker.tsx +77 -0
  189. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +230 -0
  190. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +1 -1
  191. package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +40 -17
  192. package/src/interfaces/web/src/components/common/TabLayout.tsx +9 -5
  193. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -3
  194. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +4 -2
  195. package/src/interfaces/web/src/hooks/useChat.ts +47 -2
  196. package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +59 -0
  197. package/src/interfaces/web/src/hooks/usePersonaName.ts +11 -0
  198. package/src/interfaces/web/src/i18n/en.ts +27 -7
  199. package/src/interfaces/web/src/i18n/es.ts +27 -7
  200. package/src/interfaces/web/src/lib/api/artifacts.ts +47 -0
  201. package/src/interfaces/web/src/lib/api/skills.ts +25 -0
  202. package/src/interfaces/web/src/lib/api.ts +2 -0
  203. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +41 -20
  204. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +5 -18
  205. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -8
  206. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +39 -40
  207. package/src/interfaces/web/src/screens/project/ChatTab.tsx +27 -9
  208. package/src/skills/apc-context/SKILL.md +159 -0
  209. package/src/core/agent/ghost-guard.js +0 -24
  210. package/src/core/agent/prompts/channels/terminal.md +0 -16
  211. package/src/host/daemon/apc-runtime-context.js +0 -124
  212. package/src/host/daemon/super-agent-tools/helpers.js +0 -124
  213. package/src/host/daemon/super-agent-tools/tools/ask-questions.js +0 -32
  214. package/src/host/daemon/tool-call-parser.js +0 -2
  215. package/src/interfaces/web/dist/assets/index-BDUsA6L6.css +0 -1
  216. package/src/interfaces/web/dist/assets/index-BV615I9p.js +0 -548
  217. package/src/interfaces/web/dist/assets/index-BV615I9p.js.map +0 -1
  218. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-files.js +0 -0
  219. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-mcps.js +0 -0
  220. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-projects.js +0 -0
  221. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-file.js +0 -0
  222. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-files.js +0 -0
  223. /package/src/core/agent/{pseudo-tools.js → tools/pseudo-tools.js} +0 -0
  224. /package/src/core/agent/{tool-call-parser.js → tools/tool-call-parser.js} +0 -0
  225. /package/src/core/{parser.js → apc/parser.js} +0 -0
  226. /package/src/core/{apc-skill-sync.js → apc/skill-sync.js} +0 -0
  227. /package/src/core/{artifacts-store.js → stores/artifacts.js} +0 -0
  228. /package/src/{host/daemon → core/stores}/engine-sessions.js +0 -0
  229. /package/src/core/{session-store.js → stores/sessions.js} +0 -0
@@ -29,22 +29,26 @@
29
29
 
30
30
  import fs from "node:fs";
31
31
  import path from "node:path";
32
- import { TELEGRAM_STATE_PATH, APX_HOME } from "../../../core/config.js";
33
- import { callEngine } from "../../../core/engines/index.js";
34
- import { runSuperAgent, isSuperAgentEnabled } from "../super-agent.js";
35
- import { stripThinking } from "../thinking.js";
36
- import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "../../../core/messages-store.js";
37
- import { compactChannelIfNeeded } from "../../../core/memory/index.js";
38
- import { readAgents } from "../../../core/parser.js";
39
- import { buildAgentSystem } from "../../../core/agent-system.js";
40
- import { transcribe as transcribeAudioFile } from "../transcription.js";
41
- import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "../../../core/identity.js";
42
- import { registerSender, resolveAllowedTools } from "../../../core/telegram-identity.js";
43
- import { buildRelationshipBlock } from "../../../core/agent/index.js";
44
- import { getConfirmationStore as getConfirmStore } from "../../../core/confirmation/pending-store.js";
45
- import { createTelegramConfirmAdapter } from "../../../core/confirmation/adapters/telegram.js";
46
-
47
- const API_BASE = "https://api.telegram.org";
32
+ import { TELEGRAM_STATE_PATH, APX_HOME } from "#core/config/index.js";
33
+ import { callEngine } from "#core/engines/index.js";
34
+ import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
35
+ import { stripThinking } from "../../thinking.js";
36
+ import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "#core/stores/messages.js";
37
+ import { compactChannelIfNeeded } from "#core/memory/index.js";
38
+ import { readAgents } from "#core/apc/parser.js";
39
+ import { buildAgentSystem } from "#core/agent/build-agent-system.js";
40
+ import { transcribe as transcribeAudioFile } from "../../transcription.js";
41
+ import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
42
+ import { registerSender, resolveAllowedTools } from "#core/identity/telegram.js";
43
+ import { buildRelationshipBlock } from "#core/agent/index.js";
44
+ import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pending-store.js";
45
+ import { CHANNELS } from "#core/constants/channels.js";
46
+ import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
47
+ import { isLikelyDuplicate } from "#core/util/text-similarity.js";
48
+ import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
49
+ import * as askFlow from "./ask.js";
50
+
51
+ // API_BASE re-imported from ./media.js below
48
52
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
49
53
 
50
54
  // Build the channelMeta passed to the super-agent loop. The prompt template at
@@ -84,159 +88,9 @@ function buildTelegramMeta({ channelName, author, chatId, target, routeToAgent }
84
88
  };
85
89
  }
86
90
 
87
- // ---------- media sending helpers -------------------------------------------
88
-
89
- /**
90
- * Send a photo to a Telegram chat.
91
- * @param {string} token Bot token
92
- * @param {string|number} chatId Telegram chat_id
93
- * @param {string|Buffer} photo Absolute file path OR Buffer of image data
94
- * @param {object} [opts]
95
- * @param {string} [opts.caption]
96
- * @param {string} [opts.parse_mode] "HTML" | "Markdown" | "MarkdownV2"
97
- */
98
- export async function sendPhoto(token, chatId, photo, { caption, parse_mode } = {}) {
99
- const url = `${API_BASE}/bot${token}/sendPhoto`;
100
- const form = new FormData();
101
- form.append("chat_id", String(chatId));
102
- if (caption) form.append("caption", caption);
103
- if (parse_mode) form.append("parse_mode", parse_mode);
104
-
105
- if (typeof photo === "string" && photo.startsWith("http")) {
106
- // Public URL — send as string
107
- form.append("photo", photo);
108
- } else {
109
- // Local file path or Buffer
110
- const buf = Buffer.isBuffer(photo) ? photo : fs.readFileSync(photo);
111
- const name = typeof photo === "string" ? path.basename(photo) : "photo.jpg";
112
- const blob = new Blob([buf], { type: name.endsWith(".png") ? "image/png" : "image/jpeg" });
113
- form.append("photo", blob, name);
114
- }
115
-
116
- const res = await fetch(url, { method: "POST", body: form });
117
- const json = await res.json();
118
- if (!json.ok) throw new Error(`sendPhoto failed: ${json.description || res.status}`);
119
- return json.result;
120
- }
121
-
122
- /**
123
- * Send a voice message (OGG/Opus preferred by Telegram).
124
- * @param {string} token
125
- * @param {string|number} chatId
126
- * @param {string|Buffer} audio Path or Buffer
127
- * @param {object} [opts]
128
- * @param {string} [opts.caption]
129
- * @param {number} [opts.duration]
130
- */
131
- export async function sendVoice(token, chatId, audio, { caption, duration } = {}) {
132
- const url = `${API_BASE}/bot${token}/sendVoice`;
133
- const form = new FormData();
134
- form.append("chat_id", String(chatId));
135
- if (caption) form.append("caption", caption);
136
- if (duration) form.append("duration", String(duration));
137
-
138
- const buf = Buffer.isBuffer(audio) ? audio : fs.readFileSync(audio);
139
- const name = typeof audio === "string" ? path.basename(audio) : "voice.ogg";
140
- const blob = new Blob([buf], { type: "audio/ogg" });
141
- form.append("voice", blob, name);
142
-
143
- const res = await fetch(url, { method: "POST", body: form });
144
- const json = await res.json();
145
- if (!json.ok) throw new Error(`sendVoice failed: ${json.description || res.status}`);
146
- return json.result;
147
- }
148
-
149
- /**
150
- * Send an audio file (MP3, M4A, etc — shown in Telegram music player).
151
- * @param {string} token
152
- * @param {string|number} chatId
153
- * @param {string|Buffer} audio Path or Buffer
154
- * @param {object} [opts]
155
- * @param {string} [opts.caption]
156
- * @param {string} [opts.title]
157
- * @param {string} [opts.performer]
158
- */
159
- /**
160
- * Send any file as a Telegram document (PDF, zip, txt, etc).
161
- * @param {string} token
162
- * @param {string|number} chatId
163
- * @param {string|Buffer} document Path or Buffer of document data
164
- * @param {object} [opts]
165
- * @param {string} [opts.caption]
166
- * @param {string} [opts.filename] override filename for Buffer input
167
- * @param {string} [opts.mime_type]
168
- */
169
- export async function sendDocument(token, chatId, document, { caption, filename, mime_type } = {}) {
170
- const url = `${API_BASE}/bot${token}/sendDocument`;
171
- const form = new FormData();
172
- form.append("chat_id", String(chatId));
173
- if (caption) form.append("caption", caption);
174
-
175
- // URL string → let Telegram fetch it
176
- if (typeof document === "string" && /^https?:\/\//.test(document)) {
177
- form.append("document", document);
178
- } else {
179
- const buf = Buffer.isBuffer(document) ? document : fs.readFileSync(document);
180
- const name =
181
- filename ||
182
- (typeof document === "string" ? path.basename(document) : "document.bin");
183
- const blob = new Blob([buf], { type: mime_type || "application/octet-stream" });
184
- form.append("document", blob, name);
185
- }
186
-
187
- const res = await fetch(url, { method: "POST", body: form });
188
- const json = await res.json();
189
- if (!json.ok) throw new Error(`sendDocument failed: ${json.description || res.status}`);
190
- return json.result;
191
- }
192
-
193
- export async function sendAudio(token, chatId, audio, { caption, title, performer } = {}) {
194
- const url = `${API_BASE}/bot${token}/sendAudio`;
195
- const form = new FormData();
196
- form.append("chat_id", String(chatId));
197
- if (caption) form.append("caption", caption);
198
- if (title) form.append("title", title);
199
- if (performer) form.append("performer", performer);
200
-
201
- const buf = Buffer.isBuffer(audio) ? audio : fs.readFileSync(audio);
202
- const name = typeof audio === "string" ? path.basename(audio) : "audio.mp3";
203
- const blob = new Blob([buf], { type: "audio/mpeg" });
204
- form.append("audio", blob, name);
205
-
206
- const res = await fetch(url, { method: "POST", body: form });
207
- const json = await res.json();
208
- if (!json.ok) throw new Error(`sendAudio failed: ${json.description || res.status}`);
209
- return json.result;
210
- }
211
-
212
- // Audio transcription is delegated to the central dispatcher
213
- // (../transcription.js) which handles local (faster-whisper via Python) +
214
- // OpenAI cloud fallback. See that module for config keys.
215
-
216
- /**
217
- * Download a file from Telegram servers.
218
- * Returns the local file path where it was saved.
219
- */
220
- async function downloadTelegramFile(token, fileId, destDir) {
221
- // Step 1: get file path from Telegram
222
- const infoRes = await fetch(`${API_BASE}/bot${token}/getFile?file_id=${fileId}`);
223
- const infoJson = await infoRes.json();
224
- if (!infoJson.ok) throw new Error(`getFile failed: ${infoJson.description}`);
225
- const filePath = infoJson.result.file_path; // e.g. "photos/file_123.jpg"
226
- const ext = path.extname(filePath) || ".jpg";
227
- const fileName = `tg_${fileId.slice(-8)}_${Date.now()}${ext}`;
228
- const localPath = path.join(destDir, fileName);
229
-
230
- // Step 2: download
231
- const dlRes = await fetch(`${API_BASE}/file/bot${token}/${filePath}`);
232
- if (!dlRes.ok) throw new Error(`download failed: ${dlRes.status}`);
233
- const buf = Buffer.from(await dlRes.arrayBuffer());
234
- fs.writeFileSync(localPath, buf);
235
- return localPath;
236
- }
237
-
238
- // ---------- shared state ----------------------------------------------------
239
-
91
+ // Media sending helpers moved to ./media.js.
92
+ import { sendPhoto, sendVoice, sendDocument, sendAudio, downloadTelegramFile, API_BASE } from "./media.js";
93
+ export { sendPhoto, sendVoice, sendDocument, sendAudio };
240
94
  function loadState() {
241
95
  if (!fs.existsSync(TELEGRAM_STATE_PATH)) return { channels: {} };
242
96
  try {
@@ -484,7 +338,7 @@ class ChannelPoller {
484
338
  const localPath = await downloadTelegramFile(token, bestPhoto.file_id, mediaDir);
485
339
  this.log(`telegram[${this.channel.name}] photo saved: ${localPath}`);
486
340
  appendGlobalMessage({
487
- channel: "telegram",
341
+ channel: CHANNELS.TELEGRAM,
488
342
  direction: "in",
489
343
  type: "photo",
490
344
  actor_id: msg.from?.id ? String(msg.from.id) : author,
@@ -550,7 +404,7 @@ class ChannelPoller {
550
404
  : `[audio] (transcription unavailable${transcribeError ? ": " + transcribeError : ""})`;
551
405
 
552
406
  appendGlobalMessage({
553
- channel: "telegram",
407
+ channel: CHANNELS.TELEGRAM,
554
408
  direction: "in",
555
409
  type: "audio",
556
410
  actor_id: msg.from?.id ? String(msg.from.id) : author,
@@ -577,6 +431,30 @@ class ChannelPoller {
577
431
  text = text ? `${audioBody}\n${text}` : audioBody;
578
432
  }
579
433
 
434
+ // If there's a pending ask_questions flow for this chat AND the current
435
+ // question is free-text, treat this message as the answer rather than a
436
+ // brand-new turn. Returns true when the message was consumed.
437
+ if (chat_id && text && await this._maybeConsumeAskTextAnswer({ chat_id, text })) {
438
+ // Still log the inbound so the chat history records what the user said.
439
+ appendGlobalMessage({
440
+ channel: CHANNELS.TELEGRAM,
441
+ direction: "in",
442
+ type: "user",
443
+ actor_id: msg.from?.id ? String(msg.from.id) : author,
444
+ external_id: String(u.update_id),
445
+ author,
446
+ body: text,
447
+ meta: {
448
+ chat_id,
449
+ user_id: msg.from?.id || null,
450
+ message_id: msg.message_id,
451
+ tg_channel: this.channel.name,
452
+ ask_answer: true,
453
+ },
454
+ });
455
+ return;
456
+ }
457
+
580
458
  // /reset or /new wipes the rolling context for this chat. We just
581
459
  // remember a marker timestamp; subsequent inbounds will only consider
582
460
  // history newer than this. Implemented by writing a synthetic message
@@ -598,7 +476,7 @@ class ChannelPoller {
598
476
  // the next turn reads a [RESUMEN COMPACTADO] instead of raw history. Never
599
477
  // awaited — adds zero latency to this reply, degrades gracefully.
600
478
  compactChannelIfNeeded({
601
- channel: "telegram",
479
+ channel: CHANNELS.TELEGRAM,
602
480
  chat_id,
603
481
  config: this.globalConfig,
604
482
  log: this.log,
@@ -622,7 +500,7 @@ class ChannelPoller {
622
500
 
623
501
  // Always log inbound to global store (~/.apx/messages/telegram/)
624
502
  appendGlobalMessage({
625
- channel: "telegram",
503
+ channel: CHANNELS.TELEGRAM,
626
504
  direction: "in",
627
505
  type: "user",
628
506
  actor_id: msg.from?.id ? String(msg.from.id) : author,
@@ -654,7 +532,7 @@ class ChannelPoller {
654
532
  const ack = "Done, context cleared. Starting fresh. What do you need?";
655
533
  await this._send({ chat_id, text: ack });
656
534
  appendGlobalMessage({
657
- channel: "telegram",
535
+ channel: CHANNELS.TELEGRAM,
658
536
  direction: "out",
659
537
  type: "agent",
660
538
  actor_id: SUPERAGENT_ACTOR_ID,
@@ -678,7 +556,7 @@ class ChannelPoller {
678
556
  let replyActorId; // stable id: super_agent | agent slug
679
557
  let replyKind; // actor_kind: superagent | agent
680
558
  const projectCfg = target.config || this.globalConfig;
681
- // Display name for the super-agent persona on this channel (Roby / APX).
559
+ // Display name for the super-agent persona on this channel (from identity.json).
682
560
  const agentDisplay = resolveAgentName(this.globalConfig);
683
561
 
684
562
  // Try the project's chosen agent first (skipped if the legacy
@@ -752,7 +630,7 @@ class ChannelPoller {
752
630
  const heads = headsUpPhrase();
753
631
  await this._send({ chat_id, text: heads });
754
632
  appendGlobalMessage({
755
- channel: "telegram",
633
+ channel: CHANNELS.TELEGRAM,
756
634
  direction: "out",
757
635
  type: "agent",
758
636
  actor_id: SUPERAGENT_ACTOR_ID,
@@ -767,11 +645,17 @@ class ChannelPoller {
767
645
  if (ev.type === "assistant_text" && ev.text) {
768
646
  const piece = stripThinking(ev.text).trim();
769
647
  if (!piece) return;
648
+ // Skip post-tool segments that just restate the pre-tool intro —
649
+ // weaker models (gemini-flash et al.) frequently paraphrase the
650
+ // same content twice within a single turn.
651
+ if (lastStreamedText && isLikelyDuplicate(piece, lastStreamedText)) {
652
+ return;
653
+ }
770
654
  await this._send({ chat_id, text: piece });
771
655
  lastStreamedText = piece;
772
656
  streamedCount += 1;
773
657
  appendGlobalMessage({
774
- channel: "telegram",
658
+ channel: CHANNELS.TELEGRAM,
775
659
  direction: "out",
776
660
  type: "agent",
777
661
  actor_id: SUPERAGENT_ACTOR_ID,
@@ -791,7 +675,7 @@ class ChannelPoller {
791
675
  // Logged for the audit trail / other channels — NOT sent to Telegram.
792
676
  const t = ev.trace;
793
677
  appendGlobalMessage({
794
- channel: "telegram",
678
+ channel: CHANNELS.TELEGRAM,
795
679
  direction: "out",
796
680
  type: "tool",
797
681
  actor_id: t.tool,
@@ -821,17 +705,24 @@ class ChannelPoller {
821
705
  pendingStore: getConfirmStore(),
822
706
  });
823
707
 
708
+ // `/slug ...` shortcut: load the matching skill body into contextNote
709
+ // and strip the prefix from the user prompt before sending to the loop.
710
+ const slashed = tryResolveSkillCommand(text, { projectPath: target?.path });
711
+ const slashedPrompt = slashed.handled ? slashed.prompt : text;
712
+ const slashedContextNote = slashed.handled ? slashed.contextNote : "";
713
+
824
714
  try {
825
715
  const sa = await runSuperAgent({
826
716
  globalConfig: this.globalConfig,
827
717
  projects: this.projects,
828
718
  plugins: this.plugins,
829
719
  registries: this.registries,
830
- prompt: text,
720
+ prompt: slashedPrompt,
831
721
  previousMessages,
832
- channel: "telegram",
722
+ channel: CHANNELS.TELEGRAM,
833
723
  relationshipBlock,
834
724
  allowedTools,
725
+ contextNote: slashedContextNote || undefined,
835
726
  channelMeta: buildTelegramMeta({
836
727
  channelName: this.channel.name,
837
728
  author,
@@ -848,6 +739,35 @@ class ChannelPoller {
848
739
  replyActorId = SUPERAGENT_ACTOR_ID;
849
740
  replyKind = "superagent";
850
741
  saUsage = sa.usage;
742
+
743
+ // ── ask_questions integration ────────────────────────────────────
744
+ // If the super-agent ended this turn by calling ask_questions, hand
745
+ // off to the inline-keyboard flow instead of sending the bare
746
+ // assistant text. The flow keeps state per chat_id and re-runs the
747
+ // super-agent once every answer is collected.
748
+ const askQuestions = askFlow.extractAskQuestionsFromTrace(sa.trace);
749
+ if (askQuestions && chat_id) {
750
+ if (chat_id) this.activeRequests.delete(chat_id);
751
+ stopTyping();
752
+ try {
753
+ await this._startAskFlow({
754
+ chat_id,
755
+ projectId: target?.id,
756
+ authorId: msg.from?.id,
757
+ questions: askQuestions,
758
+ author,
759
+ agentDisplay,
760
+ relationshipBlock,
761
+ allowedTools,
762
+ target,
763
+ sender,
764
+ update_id: u.update_id,
765
+ });
766
+ } catch (e) {
767
+ this.log(`telegram[${this.channel.name}] ask flow start failed: ${e.message}`);
768
+ }
769
+ return; // The reply for this turn IS the ask flow.
770
+ }
851
771
  } catch (e) {
852
772
  if (abortCtrl.signal.aborted) {
853
773
  // A newer message superseded this one. Whatever streamed so far is
@@ -878,8 +798,13 @@ class ChannelPoller {
878
798
  // turn isn't silently empty.
879
799
  const finalClean = replyText ? stripThinking(replyText).trim() : "";
880
800
  let toSend = "";
881
- if (finalClean && finalClean !== lastStreamedText) toSend = finalClean;
882
- else if (!finalClean && streamedCount === 0) toSend = "Listo.";
801
+ // Fuzzy dedupe against the last streamed segment: catches paraphrases
802
+ // (Jaccard 0.4 or short-mostly-inside-long), not just exact matches.
803
+ if (finalClean && !isLikelyDuplicate(finalClean, lastStreamedText) && finalClean !== lastStreamedText) {
804
+ toSend = finalClean;
805
+ } else if (!finalClean && streamedCount === 0) {
806
+ toSend = "Listo.";
807
+ }
883
808
 
884
809
  stopTyping();
885
810
  if (!toSend) return; // everything was already streamed — nothing left to send
@@ -895,7 +820,7 @@ class ChannelPoller {
895
820
  if (replyText && stripThinking(replyText) !== replyText) meta.thinking_stripped = true;
896
821
  if (saUsage) meta.usage = saUsage;
897
822
  appendGlobalMessage({
898
- channel: "telegram",
823
+ channel: CHANNELS.TELEGRAM,
899
824
  direction: "out",
900
825
  type: "agent",
901
826
  actor_id: replyActorId || SUPERAGENT_ACTOR_ID,
@@ -908,7 +833,7 @@ class ChannelPoller {
908
833
  } catch (e) {
909
834
  this.log(`telegram[${this.channel.name}] send-back error: ${e.message}`);
910
835
  appendGlobalMessage({
911
- channel: "telegram",
836
+ channel: CHANNELS.TELEGRAM,
912
837
  direction: "out",
913
838
  type: "agent",
914
839
  actor_id: replyActorId || SUPERAGENT_ACTOR_ID,
@@ -928,6 +853,14 @@ class ChannelPoller {
928
853
  }
929
854
 
930
855
  async _handleCallbackQuery(callbackQuery) {
856
+ // Route ask_questions button presses before the confirmation adapter —
857
+ // both use `apx:<verb>:...` namespacing but ask owns its own state.
858
+ const data = callbackQuery.data || "";
859
+ if (data.startsWith("apx:ask:")) {
860
+ await this._handleAskCallback(callbackQuery);
861
+ return;
862
+ }
863
+
931
864
  const adapter = createTelegramConfirmAdapter({
932
865
  token: resolveBotToken(this.channel),
933
866
  chatId: callbackQuery.message?.chat?.id,
@@ -939,6 +872,230 @@ class ChannelPoller {
939
872
  }
940
873
  }
941
874
 
875
+ // ── ask_questions: state-machine helpers ───────────────────────────────
876
+ // The flow lives in telegram-ask.js; this class owns the I/O (sending
877
+ // messages, editing keyboards, re-entering the super-agent loop with the
878
+ // compiled answer once the flow finishes).
879
+
880
+ async _renderQuestion(state) {
881
+ const text = askFlow.formatQuestionText(state);
882
+ const reply_markup = askFlow.buildKeyboard(state);
883
+ // If we already have a message for the previous question, leave its
884
+ // keyboard wiped — we draw a fresh message per question for clearer
885
+ // history in the chat (the question text stays as a record).
886
+ if (state.messageId) {
887
+ try {
888
+ await this._editKeyboard({
889
+ chat_id: state.chatId,
890
+ message_id: state.messageId,
891
+ reply_markup: { inline_keyboard: [] },
892
+ });
893
+ } catch { /* best-effort */ }
894
+ }
895
+ const sent = await this._send({
896
+ chat_id: state.chatId,
897
+ text,
898
+ reply_markup,
899
+ parse_mode: "Markdown",
900
+ });
901
+ state.messageId = sent?.message_id || null;
902
+ askFlow.saveState(state.chatId, state);
903
+ }
904
+
905
+ // Kick off a brand-new ask flow after the super-agent called ask_questions.
906
+ // The flow's `resume` callback captures the per-turn context (sender,
907
+ // relationship, project) so when the compiled answer arrives we can run
908
+ // another super-agent turn without retyping all the inputs.
909
+ async _startAskFlow(ctx) {
910
+ const state = askFlow.startFlow({
911
+ chatId: ctx.chat_id,
912
+ projectId: ctx.projectId,
913
+ authorId: ctx.authorId,
914
+ questions: ctx.questions,
915
+ resume: async (compiled) => {
916
+ await this._runResumedTurn({ ...ctx, compiled });
917
+ },
918
+ });
919
+ await this._renderQuestion(state);
920
+ }
921
+
922
+ // Apply an inline-keyboard press, then react: redraw, advance, or finish.
923
+ async _handleAskCallback(callbackQuery) {
924
+ const chatId = callbackQuery.message?.chat?.id;
925
+ if (!chatId) return;
926
+ const result = askFlow.applyCallback(chatId, callbackQuery.data || "");
927
+ // Ack the press regardless — keeps the spinner from hanging client-side.
928
+ await this._answerCallback({ callback_query_id: callbackQuery.id });
929
+ if (!result) return; // stale or unknown — adapter already ack'd.
930
+
931
+ if (result.action === "redraw") {
932
+ // Multi-select toggle: just refresh the keyboard on the SAME message.
933
+ try {
934
+ await this._editKeyboard({
935
+ chat_id: chatId,
936
+ message_id: callbackQuery.message?.message_id,
937
+ reply_markup: askFlow.buildKeyboard(result.state),
938
+ });
939
+ } catch (e) {
940
+ this.log(`telegram[${this.channel.name}] redraw failed: ${e.message}`);
941
+ }
942
+ return;
943
+ }
944
+ if (result.action === "advance") {
945
+ await this._renderQuestion(result.state);
946
+ return;
947
+ }
948
+ if (result.action === "cancel") {
949
+ try {
950
+ await this._editKeyboard({
951
+ chat_id: chatId,
952
+ message_id: callbackQuery.message?.message_id,
953
+ reply_markup: { inline_keyboard: [] },
954
+ });
955
+ await this._send({ chat_id: chatId, text: "Pregunta cancelada." });
956
+ } catch { /* best-effort */ }
957
+ return;
958
+ }
959
+ if (result.action === "done") {
960
+ try {
961
+ await this._editKeyboard({
962
+ chat_id: chatId,
963
+ message_id: callbackQuery.message?.message_id,
964
+ reply_markup: { inline_keyboard: [] },
965
+ });
966
+ } catch { /* best-effort */ }
967
+ // Feed the compiled answer back as a synthetic user turn.
968
+ if (typeof result.state.resume === "function") {
969
+ await result.state.resume(result.compiled);
970
+ }
971
+ }
972
+ }
973
+
974
+ // Apply a free-text user reply when there's a pending free-text question.
975
+ // Returns true iff the message was consumed by the ask flow (so the normal
976
+ // super-agent path should be skipped for this update).
977
+ async _maybeConsumeAskTextAnswer({ chat_id, text }) {
978
+ if (!chat_id || !text) return false;
979
+ if (!askFlow.hasPendingFreeText(chat_id)) return false;
980
+ const state = askFlow.applyTextAnswer(chat_id, text);
981
+ if (!state) return false;
982
+ // Advance: emit a synthetic "next" to move past this question.
983
+ const next = askFlow.applyCallback(
984
+ chat_id,
985
+ `apx:ask:${state.correlationId}:next`,
986
+ );
987
+ if (!next) return true;
988
+ if (next.action === "advance") {
989
+ await this._renderQuestion(next.state);
990
+ return true;
991
+ }
992
+ if (next.action === "done") {
993
+ if (typeof next.state.resume === "function") {
994
+ await next.state.resume(next.compiled);
995
+ }
996
+ return true;
997
+ }
998
+ return true;
999
+ }
1000
+
1001
+ // Run a follow-up super-agent turn with the compiled answers as the user
1002
+ // prompt. Mirrors the post-runSuperAgent reply path in _handleUpdate but
1003
+ // skipped of the photo/audio/reset preamble. Re-enters the ask flow if the
1004
+ // model decides to ask again.
1005
+ async _runResumedTurn(ctx) {
1006
+ const { chat_id, compiled, target, relationshipBlock, allowedTools, author, agentDisplay, update_id, sender, authorId } = ctx;
1007
+ if (!chat_id) return;
1008
+ // Log the synthetic user message so getRecentTelegramTurnsFromFs picks
1009
+ // it up on the NEXT inbound. Mirrors how a normal text reply would be
1010
+ // recorded.
1011
+ appendGlobalMessage({
1012
+ channel: CHANNELS.TELEGRAM,
1013
+ direction: "in",
1014
+ type: "user",
1015
+ actor_id: authorId ? String(authorId) : (author || "ask_flow"),
1016
+ external_id: `ask-${Date.now()}`,
1017
+ author: author || "user",
1018
+ body: compiled,
1019
+ meta: {
1020
+ chat_id,
1021
+ user_id: authorId || null,
1022
+ tg_channel: this.channel.name,
1023
+ ask_flow: true,
1024
+ },
1025
+ });
1026
+
1027
+ const previousMessages = getRecentTelegramTurnsFromFs({
1028
+ chat_id,
1029
+ keepRecent: 40,
1030
+ max_age_hours: 24,
1031
+ });
1032
+
1033
+ const stopTyping = this._startTyping(chat_id);
1034
+ try {
1035
+ const sa = await runSuperAgent({
1036
+ globalConfig: this.globalConfig,
1037
+ projects: this.projects,
1038
+ plugins: this.plugins,
1039
+ registries: this.registries,
1040
+ prompt: compiled,
1041
+ previousMessages,
1042
+ channel: CHANNELS.TELEGRAM,
1043
+ relationshipBlock,
1044
+ allowedTools,
1045
+ channelMeta: { channel: CHANNELS.TELEGRAM, chat_id, author, route_to_agent: this.channel.route_to_agent },
1046
+ });
1047
+ stopTyping();
1048
+
1049
+ // Did the model ask again? Restart the flow instead of replying.
1050
+ const followupAsk = askFlow.extractAskQuestionsFromTrace(sa.trace);
1051
+ if (followupAsk) {
1052
+ await this._startAskFlow({
1053
+ chat_id,
1054
+ projectId: target?.id,
1055
+ authorId,
1056
+ questions: followupAsk,
1057
+ author,
1058
+ agentDisplay,
1059
+ relationshipBlock,
1060
+ allowedTools,
1061
+ target,
1062
+ sender,
1063
+ update_id,
1064
+ });
1065
+ return;
1066
+ }
1067
+
1068
+ const replyText = sa.text ? stripThinking(sa.text).trim() : "";
1069
+ if (replyText) {
1070
+ await this._send({ chat_id, text: replyText });
1071
+ appendGlobalMessage({
1072
+ channel: CHANNELS.TELEGRAM,
1073
+ direction: "out",
1074
+ type: "agent",
1075
+ actor_id: SUPERAGENT_ACTOR_ID,
1076
+ actor_kind: "superagent",
1077
+ agent_slug: SUPERAGENT_ACTOR_ID,
1078
+ author: sa.name || agentDisplay,
1079
+ body: replyText,
1080
+ meta: {
1081
+ chat_id,
1082
+ tg_channel: this.channel.name,
1083
+ in_reply_to: update_id,
1084
+ final: true,
1085
+ ask_resume: true,
1086
+ ...(sa.usage ? { usage: sa.usage } : {}),
1087
+ },
1088
+ });
1089
+ }
1090
+ } catch (e) {
1091
+ stopTyping();
1092
+ this.log(`telegram[${this.channel.name}] ask resume failed: ${e.message}`);
1093
+ try {
1094
+ await this._send({ chat_id, text: `⚠️ Error procesando tus respuestas (${e.message}).` });
1095
+ } catch { /* best-effort */ }
1096
+ }
1097
+ }
1098
+
942
1099
  // Show "typing..." indicator in the chat. Telegram clears it automatically
943
1100
  // after 5 seconds, so call this every ~4s while a long operation is going.
944
1101
  async _typing(chat_id) {
@@ -971,22 +1128,64 @@ class ChannelPoller {
971
1128
  return () => { stopped = true; };
972
1129
  }
973
1130
 
974
- async _send({ chat_id, text }) {
1131
+ async _send({ chat_id, text, reply_markup, parse_mode }) {
975
1132
  const token = resolveBotToken(this.channel);
976
1133
  if (!token) throw new Error(`channel ${this.channel.name}: no bot_token`);
977
1134
  const target = chat_id || resolveChatId(this.channel);
978
1135
  if (!target) throw new Error(`channel ${this.channel.name}: no chat_id`);
979
1136
  const url = `${API_BASE}/bot${token}/sendMessage`;
1137
+ const body = { chat_id: target, text };
1138
+ if (reply_markup) body.reply_markup = reply_markup;
1139
+ if (parse_mode) body.parse_mode = parse_mode;
980
1140
  const res = await fetch(url, {
981
1141
  method: "POST",
982
1142
  headers: { "content-type": "application/json" },
983
- body: JSON.stringify({ chat_id: target, text }),
1143
+ body: JSON.stringify(body),
984
1144
  });
985
1145
  const json = await res.json();
986
1146
  if (!json.ok) throw new Error(json.description || `send failed (${res.status})`);
987
1147
  return json.result;
988
1148
  }
989
1149
 
1150
+ // Replace just the inline keyboard on a previously-sent message (used to
1151
+ // refresh after a multi-select toggle, or to wipe buttons once the flow
1152
+ // has moved on). Best-effort: failures are logged but don't break the flow.
1153
+ async _editKeyboard({ chat_id, message_id, reply_markup }) {
1154
+ const token = resolveBotToken(this.channel);
1155
+ if (!token) return;
1156
+ try {
1157
+ const url = `${API_BASE}/bot${token}/editMessageReplyMarkup`;
1158
+ const body = { chat_id, message_id };
1159
+ if (reply_markup) body.reply_markup = reply_markup;
1160
+ await fetch(url, {
1161
+ method: "POST",
1162
+ headers: { "content-type": "application/json" },
1163
+ body: JSON.stringify(body),
1164
+ });
1165
+ } catch (e) {
1166
+ this.log(`telegram[${this.channel.name}] editMessageReplyMarkup failed: ${e.message}`);
1167
+ }
1168
+ }
1169
+
1170
+ // Acknowledge a callback button press so the user's Telegram client clears
1171
+ // the spinner on the tapped button. Optional `text` shows a small toast.
1172
+ async _answerCallback({ callback_query_id, text }) {
1173
+ const token = resolveBotToken(this.channel);
1174
+ if (!token) return;
1175
+ try {
1176
+ const url = `${API_BASE}/bot${token}/answerCallbackQuery`;
1177
+ const body = { callback_query_id };
1178
+ if (text) body.text = text;
1179
+ await fetch(url, {
1180
+ method: "POST",
1181
+ headers: { "content-type": "application/json" },
1182
+ body: JSON.stringify(body),
1183
+ });
1184
+ } catch (e) {
1185
+ this.log(`telegram[${this.channel.name}] answerCallbackQuery failed: ${e.message}`);
1186
+ }
1187
+ }
1188
+
990
1189
  /** Send a photo via this channel */
991
1190
  async _sendPhoto({ chat_id, photo, caption, parse_mode }) {
992
1191
  const token = resolveBotToken(this.channel);
@@ -1073,7 +1272,7 @@ export default {
1073
1272
  if (!p) throw new Error("no telegram channel available");
1074
1273
  const result = await p._send({ chat_id, text });
1075
1274
  appendGlobalMessage({
1076
- channel: "telegram",
1275
+ channel: CHANNELS.TELEGRAM,
1077
1276
  direction: "out",
1078
1277
  type: "agent",
1079
1278
  actor_id: SUPERAGENT_ACTOR_ID,
@@ -1103,7 +1302,7 @@ export default {
1103
1302
  if (!p) throw new Error("no telegram channel available");
1104
1303
  const result = await p._sendPhoto({ chat_id, photo, caption, parse_mode });
1105
1304
  appendGlobalMessage({
1106
- channel: "telegram",
1305
+ channel: CHANNELS.TELEGRAM,
1107
1306
  direction: "out",
1108
1307
  type: "photo",
1109
1308
  actor_id: SUPERAGENT_ACTOR_ID,
@@ -1127,7 +1326,7 @@ export default {
1127
1326
  if (!p) throw new Error("no telegram channel available");
1128
1327
  const result = await p._sendVoice({ chat_id, audio, caption, duration });
1129
1328
  appendGlobalMessage({
1130
- channel: "telegram",
1329
+ channel: CHANNELS.TELEGRAM,
1131
1330
  direction: "out",
1132
1331
  type: "voice",
1133
1332
  actor_id: SUPERAGENT_ACTOR_ID,
@@ -1151,7 +1350,7 @@ export default {
1151
1350
  if (!p) throw new Error("no telegram channel available");
1152
1351
  const result = await p._sendDocument({ chat_id, document, caption, filename, mime_type });
1153
1352
  appendGlobalMessage({
1154
- channel: "telegram",
1353
+ channel: CHANNELS.TELEGRAM,
1155
1354
  direction: "out",
1156
1355
  type: "document",
1157
1356
  actor_id: SUPERAGENT_ACTOR_ID,
@@ -1175,7 +1374,7 @@ export default {
1175
1374
  if (!p) throw new Error("no telegram channel available");
1176
1375
  const result = await p._sendAudio({ chat_id, audio, caption, title, performer });
1177
1376
  appendGlobalMessage({
1178
- channel: "telegram",
1377
+ channel: CHANNELS.TELEGRAM,
1179
1378
  direction: "out",
1180
1379
  type: "audio",
1181
1380
  actor_id: SUPERAGENT_ACTOR_ID,