copilot-hub 0.1.0

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/apps/agent-engine/.env.example +41 -0
  4. package/apps/agent-engine/LICENSE +21 -0
  5. package/apps/agent-engine/README.md +57 -0
  6. package/apps/agent-engine/bot-registry.example.json +28 -0
  7. package/apps/agent-engine/capabilities/example/index.js +3 -0
  8. package/apps/agent-engine/capabilities/example/manifest.json +14 -0
  9. package/apps/agent-engine/dist/agent-worker.js +241 -0
  10. package/apps/agent-engine/dist/config.js +225 -0
  11. package/apps/agent-engine/dist/index.js +352 -0
  12. package/apps/agent-engine/dist/test/project-fingerprint.test.js +40 -0
  13. package/apps/agent-engine/dist/test/thread-id.test.js +12 -0
  14. package/apps/agent-engine/package.json +28 -0
  15. package/apps/control-plane/.env.example +25 -0
  16. package/apps/control-plane/README.md +35 -0
  17. package/apps/control-plane/bot-registry.example.json +40 -0
  18. package/apps/control-plane/capabilities/example/index.js +3 -0
  19. package/apps/control-plane/capabilities/example/manifest.json +14 -0
  20. package/apps/control-plane/dist/agent-worker.js +243 -0
  21. package/apps/control-plane/dist/channels/channel-factory.js +21 -0
  22. package/apps/control-plane/dist/channels/hub-ops-commands.js +752 -0
  23. package/apps/control-plane/dist/channels/telegram-channel.js +743 -0
  24. package/apps/control-plane/dist/channels/whatsapp-channel.js +35 -0
  25. package/apps/control-plane/dist/config.js +230 -0
  26. package/apps/control-plane/dist/copilot-hub.js +138 -0
  27. package/apps/control-plane/dist/index.js +349 -0
  28. package/apps/control-plane/dist/kernel/admin-contract.js +51 -0
  29. package/apps/control-plane/dist/test/project-fingerprint.test.js +40 -0
  30. package/apps/control-plane/dist/test/thread-id.test.js +12 -0
  31. package/apps/control-plane/package.json +27 -0
  32. package/package.json +89 -0
  33. package/packages/contracts/README.md +10 -0
  34. package/packages/contracts/dist/control-plane.d.ts +24 -0
  35. package/packages/contracts/dist/control-plane.js +37 -0
  36. package/packages/contracts/dist/control-plane.js.map +1 -0
  37. package/packages/contracts/dist/index.d.ts +1 -0
  38. package/packages/contracts/dist/index.js +2 -0
  39. package/packages/contracts/dist/index.js.map +1 -0
  40. package/packages/contracts/package.json +27 -0
  41. package/packages/core/README.md +33 -0
  42. package/packages/core/dist/agent-supervisor.d.ts +39 -0
  43. package/packages/core/dist/agent-supervisor.js +552 -0
  44. package/packages/core/dist/agent-supervisor.js.map +1 -0
  45. package/packages/core/dist/bot-manager.d.ts +66 -0
  46. package/packages/core/dist/bot-manager.js +333 -0
  47. package/packages/core/dist/bot-manager.js.map +1 -0
  48. package/packages/core/dist/bot-registry.d.ts +60 -0
  49. package/packages/core/dist/bot-registry.js +381 -0
  50. package/packages/core/dist/bot-registry.js.map +1 -0
  51. package/packages/core/dist/bot-runtime.d.ts +135 -0
  52. package/packages/core/dist/bot-runtime.js +349 -0
  53. package/packages/core/dist/bot-runtime.js.map +1 -0
  54. package/packages/core/dist/bridge-service.d.ts +39 -0
  55. package/packages/core/dist/bridge-service.js +272 -0
  56. package/packages/core/dist/bridge-service.js.map +1 -0
  57. package/packages/core/dist/capability-manager.d.ts +18 -0
  58. package/packages/core/dist/capability-manager.js +335 -0
  59. package/packages/core/dist/capability-manager.js.map +1 -0
  60. package/packages/core/dist/capability-scaffold.d.ts +26 -0
  61. package/packages/core/dist/capability-scaffold.js +118 -0
  62. package/packages/core/dist/capability-scaffold.js.map +1 -0
  63. package/packages/core/dist/channel-factory.d.ts +6 -0
  64. package/packages/core/dist/channel-factory.js +22 -0
  65. package/packages/core/dist/channel-factory.js.map +1 -0
  66. package/packages/core/dist/codex-app-client.d.ts +56 -0
  67. package/packages/core/dist/codex-app-client.js +762 -0
  68. package/packages/core/dist/codex-app-client.js.map +1 -0
  69. package/packages/core/dist/codex-provider.d.ts +31 -0
  70. package/packages/core/dist/codex-provider.js +64 -0
  71. package/packages/core/dist/codex-provider.js.map +1 -0
  72. package/packages/core/dist/control-permission.d.ts +19 -0
  73. package/packages/core/dist/control-permission.js +106 -0
  74. package/packages/core/dist/control-permission.js.map +1 -0
  75. package/packages/core/dist/control-plane-actions.d.ts +1 -0
  76. package/packages/core/dist/control-plane-actions.js +2 -0
  77. package/packages/core/dist/control-plane-actions.js.map +1 -0
  78. package/packages/core/dist/example-capability.d.ts +17 -0
  79. package/packages/core/dist/example-capability.js +22 -0
  80. package/packages/core/dist/example-capability.js.map +1 -0
  81. package/packages/core/dist/extension-contract.d.ts +22 -0
  82. package/packages/core/dist/extension-contract.js +28 -0
  83. package/packages/core/dist/extension-contract.js.map +1 -0
  84. package/packages/core/dist/index.d.ts +26 -0
  85. package/packages/core/dist/index.js +27 -0
  86. package/packages/core/dist/index.js.map +1 -0
  87. package/packages/core/dist/instance-lock.d.ts +9 -0
  88. package/packages/core/dist/instance-lock.js +74 -0
  89. package/packages/core/dist/instance-lock.js.map +1 -0
  90. package/packages/core/dist/kernel-control-plane.d.ts +16 -0
  91. package/packages/core/dist/kernel-control-plane.js +500 -0
  92. package/packages/core/dist/kernel-control-plane.js.map +1 -0
  93. package/packages/core/dist/kernel-version.d.ts +1 -0
  94. package/packages/core/dist/kernel-version.js +2 -0
  95. package/packages/core/dist/kernel-version.js.map +1 -0
  96. package/packages/core/dist/project-fingerprint.d.ts +11 -0
  97. package/packages/core/dist/project-fingerprint.js +33 -0
  98. package/packages/core/dist/project-fingerprint.js.map +1 -0
  99. package/packages/core/dist/provider-factory.d.ts +7 -0
  100. package/packages/core/dist/provider-factory.js +21 -0
  101. package/packages/core/dist/provider-factory.js.map +1 -0
  102. package/packages/core/dist/secret-store.d.ts +18 -0
  103. package/packages/core/dist/secret-store.js +110 -0
  104. package/packages/core/dist/secret-store.js.map +1 -0
  105. package/packages/core/dist/state-store.d.ts +50 -0
  106. package/packages/core/dist/state-store.js +324 -0
  107. package/packages/core/dist/state-store.js.map +1 -0
  108. package/packages/core/dist/telegram-channel.d.ts +27 -0
  109. package/packages/core/dist/telegram-channel.js +951 -0
  110. package/packages/core/dist/telegram-channel.js.map +1 -0
  111. package/packages/core/dist/thread-id.d.ts +1 -0
  112. package/packages/core/dist/thread-id.js +12 -0
  113. package/packages/core/dist/thread-id.js.map +1 -0
  114. package/packages/core/dist/whatsapp-channel.d.ts +26 -0
  115. package/packages/core/dist/whatsapp-channel.js +36 -0
  116. package/packages/core/dist/whatsapp-channel.js.map +1 -0
  117. package/packages/core/dist/workspace-paths.d.ts +5 -0
  118. package/packages/core/dist/workspace-paths.js +77 -0
  119. package/packages/core/dist/workspace-paths.js.map +1 -0
  120. package/packages/core/dist/workspace-policy.d.ts +30 -0
  121. package/packages/core/dist/workspace-policy.js +104 -0
  122. package/packages/core/dist/workspace-policy.js.map +1 -0
  123. package/packages/core/package.json +126 -0
  124. package/scripts/cli.mjs +537 -0
  125. package/scripts/configure.mjs +254 -0
  126. package/scripts/ensure-shared-build.mjs +96 -0
  127. package/scripts/run-node-tests.mjs +52 -0
  128. package/scripts/supervisor.mjs +332 -0
@@ -0,0 +1,743 @@
1
+ // @ts-nocheck
2
+ import { Bot } from "grammy";
3
+ import { maybeHandleHubOpsCallback, maybeHandleHubOpsCommand, maybeHandleHubOpsFollowUp, } from "./hub-ops-commands.js";
4
+ export class TelegramChannel {
5
+ constructor({ channelConfig, runtime }) {
6
+ this.kind = "telegram";
7
+ this.id = String(channelConfig.id ?? "telegram");
8
+ this.config = channelConfig;
9
+ this.runtime = runtime;
10
+ this.allowedChatIds = normalizeAllowedChatIds(channelConfig.allowedChatIds);
11
+ this.bot = null;
12
+ this.running = false;
13
+ this.error = null;
14
+ this.activeTurnsByChat = new Map();
15
+ this.turnControlByChat = new Map();
16
+ this.nextTurnToken = 1;
17
+ }
18
+ async start() {
19
+ if (this.running) {
20
+ return this.getStatus();
21
+ }
22
+ const token = String(this.config.token ?? "").trim();
23
+ if (!token) {
24
+ throw new Error(`Telegram token is missing for channel '${this.id}'.`);
25
+ }
26
+ const bot = new Bot(token);
27
+ this.#attachHandlers(bot);
28
+ this.error = null;
29
+ this.running = true;
30
+ this.bot = bot;
31
+ void bot
32
+ .start({
33
+ onStart: () => {
34
+ console.log(`[${this.runtime.runtimeId}:${this.id}] Telegram polling started.`);
35
+ },
36
+ })
37
+ .catch((error) => {
38
+ this.error = sanitizeError(error);
39
+ this.running = false;
40
+ this.bot = null;
41
+ console.error(`[${this.runtime.runtimeId}:${this.id}] Telegram polling error: ${this.error}`);
42
+ });
43
+ return this.getStatus();
44
+ }
45
+ async stop() {
46
+ if (this.bot) {
47
+ try {
48
+ this.bot.stop();
49
+ }
50
+ catch {
51
+ // Ignore stop errors.
52
+ }
53
+ }
54
+ this.bot = null;
55
+ this.running = false;
56
+ this.activeTurnsByChat.clear();
57
+ this.turnControlByChat.clear();
58
+ return this.getStatus();
59
+ }
60
+ async shutdown() {
61
+ await this.stop();
62
+ }
63
+ getStatus() {
64
+ return {
65
+ kind: this.kind,
66
+ id: this.id,
67
+ running: this.running,
68
+ error: this.error,
69
+ };
70
+ }
71
+ async notifyApproval(approval) {
72
+ const chatId = String(approval?.metadata?.chatId ?? "").trim();
73
+ if (!chatId || !this.bot) {
74
+ return;
75
+ }
76
+ const lines = [
77
+ `Approval required [${approval.id}]`,
78
+ `type: ${approval.kind}`,
79
+ `thread: ${approval.threadId}`,
80
+ ];
81
+ if (approval.command) {
82
+ lines.push(`command: ${approval.command}`);
83
+ }
84
+ if (approval.cwd) {
85
+ lines.push(`cwd: ${approval.cwd}`);
86
+ }
87
+ if (approval.reason) {
88
+ lines.push(`reason: ${approval.reason}`);
89
+ }
90
+ lines.push(`Use: /approve ${approval.id} or /deny ${approval.id}`);
91
+ await sendChunkedMessage(this.bot.api, chatId, lines.join("\n"));
92
+ }
93
+ #attachHandlers(bot) {
94
+ bot.command("start", async (context) => {
95
+ const chatId = String(context.chat.id);
96
+ if (!this.#isAllowedChat(chatId)) {
97
+ await context.reply("Chat not allowed for this bot.");
98
+ return;
99
+ }
100
+ const threadId = await this.runtime.resolveThreadIdForChannel({
101
+ channelKind: this.kind,
102
+ channelId: this.id,
103
+ externalUserId: chatId,
104
+ });
105
+ await context.reply([
106
+ `Agent '${this.runtime.runtimeName}' ready.`,
107
+ "/help - hub commands",
108
+ "/health - engine status",
109
+ "/bots - list and manage agents",
110
+ "/create_agent - create a Telegram agent",
111
+ "/cancel - cancel current create flow",
112
+ "/thread - show active thread",
113
+ "/new - reset current thread",
114
+ "/status - show session status",
115
+ "/stop - stop current generation",
116
+ "/steer <instruction> - interrupt current generation and apply steer immediately",
117
+ "Send a message during generation to interrupt and replace immediately",
118
+ "During generation button: Interrompre",
119
+ "/approvals - list pending approvals",
120
+ "/approve <id> - approve a pending action",
121
+ "/approvealways <id> - approve and remember for session",
122
+ "/approveall - approve all pending actions",
123
+ "/approveallalways - approve all and remember for session",
124
+ "/deny <id> - deny a pending action",
125
+ "/denyall - deny all pending actions",
126
+ "/whoami - show Telegram chat id",
127
+ `Web: ${this.runtime.buildWebBotUrl()}`,
128
+ `threadId: ${threadId}`,
129
+ ]
130
+ .filter(Boolean)
131
+ .join("\n"));
132
+ });
133
+ bot.command("thread", async (context) => {
134
+ const chatId = String(context.chat.id);
135
+ if (!this.#isAllowedChat(chatId)) {
136
+ await context.reply("Chat not allowed for this bot.");
137
+ return;
138
+ }
139
+ const threadId = await this.runtime.resolveThreadIdForChannel({
140
+ channelKind: this.kind,
141
+ channelId: this.id,
142
+ externalUserId: chatId,
143
+ });
144
+ await context.reply(`threadId: ${threadId}`);
145
+ });
146
+ bot.command("new", async (context) => {
147
+ const chatId = String(context.chat.id);
148
+ if (!this.#isAllowedChat(chatId)) {
149
+ await context.reply("Chat not allowed for this bot.");
150
+ return;
151
+ }
152
+ const threadId = await this.runtime.resolveThreadIdForChannel({
153
+ channelKind: this.kind,
154
+ channelId: this.id,
155
+ externalUserId: chatId,
156
+ });
157
+ await this.runtime.resetThread(threadId);
158
+ await context.reply(`Thread '${threadId}' reset.`);
159
+ });
160
+ bot.command("status", async (context) => {
161
+ const chatId = String(context.chat.id);
162
+ if (!this.#isAllowedChat(chatId)) {
163
+ await context.reply("Chat not allowed for this bot.");
164
+ return;
165
+ }
166
+ const threadId = await this.runtime.resolveThreadIdForChannel({
167
+ channelKind: this.kind,
168
+ channelId: this.id,
169
+ externalUserId: chatId,
170
+ });
171
+ const { thread } = await this.runtime.getThread(threadId);
172
+ await context.reply([
173
+ `agent: ${this.runtime.runtimeName}`,
174
+ `threadId: ${threadId}`,
175
+ `turnCount: ${thread.turnCount ?? 0}`,
176
+ `sessionId: ${thread.sessionId ?? "<none>"}`,
177
+ `updatedAt: ${thread.updatedAt ?? "<unknown>"}`,
178
+ `web: ${this.runtime.buildWebBotUrl()}`,
179
+ ].join("\n"));
180
+ });
181
+ bot.command("whoami", async (context) => {
182
+ const chatId = String(context.chat.id);
183
+ await context.reply(`chat_id: ${chatId}`);
184
+ });
185
+ bot.command("stop", async (context) => {
186
+ await this.#handleStopTurn(context);
187
+ });
188
+ bot.command("steer", async (context) => {
189
+ await this.#handleSteer(context);
190
+ });
191
+ bot.command("approvals", async (context) => {
192
+ const chatId = String(context.chat.id);
193
+ if (!this.#isAllowedChat(chatId)) {
194
+ await context.reply("Chat not allowed for this bot.");
195
+ return;
196
+ }
197
+ const threadId = await this.runtime.resolveThreadIdForChannel({
198
+ channelKind: this.kind,
199
+ channelId: this.id,
200
+ externalUserId: chatId,
201
+ });
202
+ const approvals = await this.runtime.listPendingApprovals(threadId);
203
+ if (approvals.length === 0) {
204
+ await context.reply("No pending approvals.");
205
+ return;
206
+ }
207
+ await context.reply(formatApprovalList(approvals));
208
+ });
209
+ bot.command("approve", async (context) => {
210
+ await this.#handleSingleApproval(context, "accept");
211
+ });
212
+ bot.command("approvealways", async (context) => {
213
+ await this.#handleSingleApproval(context, "acceptForSession");
214
+ });
215
+ bot.command("approveall", async (context) => {
216
+ await this.#handleBulkApproval(context, "accept");
217
+ });
218
+ bot.command("approveallalways", async (context) => {
219
+ await this.#handleBulkApproval(context, "acceptForSession");
220
+ });
221
+ bot.command("deny", async (context) => {
222
+ await this.#handleSingleApproval(context, "decline");
223
+ });
224
+ bot.command("denyall", async (context) => {
225
+ await this.#handleBulkApproval(context, "decline");
226
+ });
227
+ bot.on("callback_query:data", async (context) => {
228
+ const chatId = String(context.callbackQuery.message?.chat?.id ?? "");
229
+ if (!chatId || !this.#isAllowedChat(chatId)) {
230
+ await context.answerCallbackQuery({
231
+ text: "Chat not allowed for this bot.",
232
+ show_alert: true,
233
+ });
234
+ return;
235
+ }
236
+ const hubHandled = await maybeHandleHubOpsCallback({
237
+ ctx: context,
238
+ });
239
+ if (hubHandled) {
240
+ return;
241
+ }
242
+ const payload = parseTurnControlCallbackData(context.callbackQuery.data);
243
+ if (!payload) {
244
+ return;
245
+ }
246
+ const active = this.#getActiveTurn(chatId);
247
+ if (!active || active.token !== payload.token) {
248
+ await context.answerCallbackQuery({
249
+ text: "No active generation for this action.",
250
+ });
251
+ return;
252
+ }
253
+ if (payload.action === "stop") {
254
+ const result = await this.runtime.interruptThread(active.threadId);
255
+ if (result?.interrupted === true) {
256
+ await this.#closeTurnControls(chatId, payload.token, "Generation interruption requested.");
257
+ }
258
+ await context.answerCallbackQuery({
259
+ text: result?.interrupted === true
260
+ ? "Interruption requested."
261
+ : "No active generation to stop.",
262
+ });
263
+ return;
264
+ }
265
+ });
266
+ bot.on("message:text", async (context) => {
267
+ const text = context.message.text;
268
+ const chatId = String(context.chat.id);
269
+ if (!this.#isAllowedChat(chatId)) {
270
+ await context.reply("Chat not allowed for this bot.");
271
+ return;
272
+ }
273
+ if (text.startsWith("/")) {
274
+ const opsHandled = await maybeHandleHubOpsCommand({
275
+ ctx: context,
276
+ runtime: this.runtime,
277
+ channelId: this.id,
278
+ });
279
+ if (opsHandled) {
280
+ return;
281
+ }
282
+ await context.reply("Unknown command. Use /help.");
283
+ return;
284
+ }
285
+ const opsFlowHandled = await maybeHandleHubOpsFollowUp({
286
+ ctx: context,
287
+ runtime: this.runtime,
288
+ channelId: this.id,
289
+ });
290
+ if (opsFlowHandled) {
291
+ return;
292
+ }
293
+ const threadId = await this.runtime.resolveThreadIdForChannel({
294
+ channelKind: this.kind,
295
+ channelId: this.id,
296
+ externalUserId: chatId,
297
+ });
298
+ const activeTurn = this.#getActiveTurn(chatId);
299
+ if (activeTurn && activeTurn.threadId === threadId) {
300
+ await this.#applySteerInstruction({
301
+ chatId,
302
+ threadId,
303
+ prompt: text,
304
+ mode: "auto_message",
305
+ });
306
+ return;
307
+ }
308
+ void this.#processTurn({
309
+ chatId,
310
+ threadId,
311
+ prompt: text,
312
+ });
313
+ });
314
+ bot.catch((error) => {
315
+ const details = error.error instanceof Error
316
+ ? (error.error.stack ?? error.error.message)
317
+ : String(error.error);
318
+ this.error = details;
319
+ console.error(`[${this.runtime.runtimeId}:${this.id}] Telegram error in update ${error.ctx.update.update_id}: ${details}`);
320
+ });
321
+ }
322
+ async #processTurn({ chatId, threadId, prompt }) {
323
+ if (!this.bot) {
324
+ return;
325
+ }
326
+ const turnToken = this.#markActiveTurn(chatId, threadId);
327
+ await this.#openTurnControls(chatId, threadId, turnToken);
328
+ let controlStatus = "Generation completed.";
329
+ try {
330
+ await this.bot.api.sendChatAction(chatId, "typing");
331
+ const result = await this.runtime.sendTurn({
332
+ threadId,
333
+ prompt,
334
+ source: "telegram",
335
+ metadata: {
336
+ chatId,
337
+ channelId: this.id,
338
+ },
339
+ });
340
+ await sendChunkedMessage(this.bot.api, chatId, result.assistantText || "Assistant returned no text output.");
341
+ }
342
+ catch (error) {
343
+ const safe = sanitizeError(error);
344
+ if (isTurnInterruptedError(safe)) {
345
+ controlStatus = "Generation interrupted.";
346
+ await this.bot.api.sendMessage(chatId, "Generation stopped.");
347
+ return;
348
+ }
349
+ controlStatus = "Generation failed.";
350
+ await this.bot.api.sendMessage(chatId, `Execution error:\n${safe}`);
351
+ }
352
+ finally {
353
+ this.#clearActiveTurn(chatId, turnToken);
354
+ await this.#closeTurnControls(chatId, turnToken, controlStatus);
355
+ }
356
+ }
357
+ async #handleStopTurn(context) {
358
+ const chatId = String(context.chat.id);
359
+ if (!this.#isAllowedChat(chatId)) {
360
+ await context.reply("Chat not allowed for this bot.");
361
+ return;
362
+ }
363
+ const threadId = await this.runtime.resolveThreadIdForChannel({
364
+ channelKind: this.kind,
365
+ channelId: this.id,
366
+ externalUserId: chatId,
367
+ });
368
+ const result = await this.runtime.interruptThread(threadId);
369
+ await context.reply(formatInterruptResult(result));
370
+ }
371
+ async #handleSteer(context) {
372
+ const chatId = String(context.chat.id);
373
+ if (!this.#isAllowedChat(chatId)) {
374
+ await context.reply("Chat not allowed for this bot.");
375
+ return;
376
+ }
377
+ const instruction = extractCommandTail(context.message.text);
378
+ const threadId = await this.runtime.resolveThreadIdForChannel({
379
+ channelKind: this.kind,
380
+ channelId: this.id,
381
+ externalUserId: chatId,
382
+ });
383
+ if (!instruction) {
384
+ await context.reply("Usage: /steer <instruction>");
385
+ return;
386
+ }
387
+ const prompt = buildSteerPrompt(instruction);
388
+ await this.#applySteerInstruction({
389
+ chatId,
390
+ threadId,
391
+ prompt,
392
+ mode: "command_steer",
393
+ });
394
+ }
395
+ #markActiveTurn(chatId, threadId) {
396
+ const token = `turn_${Date.now()}_${this.nextTurnToken++}`;
397
+ this.activeTurnsByChat.set(String(chatId), {
398
+ threadId: String(threadId),
399
+ token,
400
+ });
401
+ return token;
402
+ }
403
+ #clearActiveTurn(chatId, token) {
404
+ const key = String(chatId);
405
+ const current = this.activeTurnsByChat.get(key);
406
+ if (!current) {
407
+ return;
408
+ }
409
+ if (String(current.token) !== String(token)) {
410
+ return;
411
+ }
412
+ this.activeTurnsByChat.delete(key);
413
+ }
414
+ #getActiveTurn(chatId) {
415
+ return this.activeTurnsByChat.get(String(chatId)) ?? null;
416
+ }
417
+ async #applySteerInstruction({ chatId, threadId, prompt, mode = "command_steer" }) {
418
+ const nextPrompt = String(prompt ?? "").trim();
419
+ if (!nextPrompt) {
420
+ if (this.bot) {
421
+ await this.bot.api.sendMessage(chatId, "Message is empty.");
422
+ }
423
+ return;
424
+ }
425
+ const activeTurn = this.#getActiveTurn(chatId);
426
+ if (activeTurn && activeTurn.threadId === threadId) {
427
+ const interruption = await this.runtime.interruptThread(threadId);
428
+ const reason = String(interruption?.reason ?? "")
429
+ .trim()
430
+ .toLowerCase();
431
+ if (!(interruption?.interrupted === true ||
432
+ reason === "no_active_turn" ||
433
+ reason === "no_active_session")) {
434
+ if (this.bot) {
435
+ await this.bot.api.sendMessage(chatId, `Unable to apply steer now.\n${formatInterruptResult(interruption)}`);
436
+ }
437
+ return;
438
+ }
439
+ if (this.bot) {
440
+ if (mode === "auto_message") {
441
+ await this.bot.api.sendMessage(chatId, "New message received. Generation interrupted, applying it now.");
442
+ }
443
+ else {
444
+ await this.bot.api.sendMessage(chatId, "Steer accepted. Generation interrupted, applying instruction now.");
445
+ }
446
+ }
447
+ }
448
+ else if (this.bot) {
449
+ if (mode === "auto_message") {
450
+ await this.bot.api.sendMessage(chatId, "Applying your message now.");
451
+ }
452
+ else {
453
+ await this.bot.api.sendMessage(chatId, "No active generation. Applying steer now.");
454
+ }
455
+ }
456
+ void this.#processTurn({
457
+ chatId,
458
+ threadId,
459
+ prompt: nextPrompt,
460
+ });
461
+ }
462
+ async #openTurnControls(chatId, threadId, token) {
463
+ if (!this.bot) {
464
+ return;
465
+ }
466
+ const key = String(chatId);
467
+ await this.#closeTurnControls(chatId, null, null);
468
+ try {
469
+ const sent = await this.bot.api.sendMessage(chatId, "Generation in progress.", {
470
+ reply_markup: buildTurnControlKeyboard(token),
471
+ });
472
+ this.turnControlByChat.set(key, {
473
+ token: String(token),
474
+ threadId: String(threadId),
475
+ messageId: Number(sent?.message_id ?? 0),
476
+ });
477
+ }
478
+ catch {
479
+ // Non critical UI helper only.
480
+ }
481
+ }
482
+ async #closeTurnControls(chatId, token = null, finalText = null) {
483
+ const key = String(chatId);
484
+ const current = this.turnControlByChat.get(key);
485
+ if (!current) {
486
+ return;
487
+ }
488
+ if (token && String(current.token) !== String(token)) {
489
+ return;
490
+ }
491
+ this.turnControlByChat.delete(key);
492
+ if (!this.bot) {
493
+ return;
494
+ }
495
+ try {
496
+ if (finalText) {
497
+ await this.bot.api.editMessageText(chatId, current.messageId, String(finalText), {
498
+ reply_markup: {
499
+ inline_keyboard: [],
500
+ },
501
+ });
502
+ }
503
+ else {
504
+ await this.bot.api.editMessageReplyMarkup(chatId, current.messageId, {
505
+ reply_markup: {
506
+ inline_keyboard: [],
507
+ },
508
+ });
509
+ }
510
+ }
511
+ catch {
512
+ // Message may be outdated/deleted; ignore.
513
+ }
514
+ }
515
+ async #handleSingleApproval(context, decision) {
516
+ const chatId = String(context.chat.id);
517
+ if (!this.#isAllowedChat(chatId)) {
518
+ await context.reply("Chat not allowed for this bot.");
519
+ return;
520
+ }
521
+ const threadId = await this.runtime.resolveThreadIdForChannel({
522
+ channelKind: this.kind,
523
+ channelId: this.id,
524
+ externalUserId: chatId,
525
+ });
526
+ const requestedId = extractFirstCommandArgument(context.message.text);
527
+ if (requestedId.toLowerCase() === "all") {
528
+ const summary = await this.#resolveAllApprovals({ threadId, decision });
529
+ await context.reply(summary);
530
+ return;
531
+ }
532
+ const approvals = await this.runtime.listPendingApprovals(threadId);
533
+ const target = selectApproval(approvals, requestedId);
534
+ if (!target) {
535
+ await context.reply(buildApprovalSelectionMessage(approvals, requestedId));
536
+ return;
537
+ }
538
+ await this.runtime.resolvePendingApproval({
539
+ threadId,
540
+ approvalId: target.id,
541
+ decision,
542
+ });
543
+ if (decision === "decline") {
544
+ await context.reply(`Denied '${target.id}'.`);
545
+ return;
546
+ }
547
+ if (decision === "acceptForSession") {
548
+ await context.reply(`Approved '${target.id}' with session remember.`);
549
+ return;
550
+ }
551
+ await context.reply(`Approved '${target.id}'.`);
552
+ }
553
+ async #handleBulkApproval(context, decision) {
554
+ const chatId = String(context.chat.id);
555
+ if (!this.#isAllowedChat(chatId)) {
556
+ await context.reply("Chat not allowed for this bot.");
557
+ return;
558
+ }
559
+ const threadId = await this.runtime.resolveThreadIdForChannel({
560
+ channelKind: this.kind,
561
+ channelId: this.id,
562
+ externalUserId: chatId,
563
+ });
564
+ const summary = await this.#resolveAllApprovals({
565
+ threadId,
566
+ decision,
567
+ });
568
+ await context.reply(summary);
569
+ }
570
+ async #resolveAllApprovals({ threadId, decision }) {
571
+ const approvals = await this.runtime.listPendingApprovals(threadId);
572
+ if (approvals.length === 0) {
573
+ return "No pending approvals.";
574
+ }
575
+ let successCount = 0;
576
+ const failedIds = [];
577
+ for (const approval of approvals) {
578
+ try {
579
+ await this.runtime.resolvePendingApproval({
580
+ threadId,
581
+ approvalId: approval.id,
582
+ decision,
583
+ });
584
+ successCount += 1;
585
+ }
586
+ catch {
587
+ failedIds.push(approval.id);
588
+ }
589
+ }
590
+ const decisionLabel = decision === "decline"
591
+ ? "denied"
592
+ : decision === "acceptForSession"
593
+ ? "approved (session)"
594
+ : "approved";
595
+ if (failedIds.length === 0) {
596
+ return `${successCount}/${approvals.length} approvals ${decisionLabel}.`;
597
+ }
598
+ return `${successCount}/${approvals.length} approvals ${decisionLabel}. Failed: ${failedIds.join(", ")}`;
599
+ }
600
+ #isAllowedChat(chatId) {
601
+ const set = this.allowedChatIds;
602
+ if (!set || set.size === 0) {
603
+ return true;
604
+ }
605
+ return set.has(String(chatId));
606
+ }
607
+ }
608
+ async function sendChunkedMessage(botApi, chatId, text) {
609
+ const max = 3900;
610
+ for (let start = 0; start < text.length; start += max) {
611
+ const chunk = text.slice(start, start + max);
612
+ await botApi.sendMessage(chatId, chunk || " ");
613
+ }
614
+ }
615
+ function formatApprovalList(approvals) {
616
+ const lines = ["Pending approvals:"];
617
+ for (const approval of approvals) {
618
+ const commandPart = approval.command ? ` | ${approval.command.slice(0, 90)}` : "";
619
+ lines.push(`${approval.id} | ${approval.kind}${commandPart}`);
620
+ }
621
+ return lines.join("\n");
622
+ }
623
+ function extractFirstCommandArgument(text) {
624
+ const value = String(text ?? "").trim();
625
+ const parts = value.split(/\s+/).slice(1);
626
+ return String(parts[0] ?? "").trim();
627
+ }
628
+ function extractCommandTail(text) {
629
+ const value = String(text ?? "").trim();
630
+ const firstSpace = value.indexOf(" ");
631
+ if (firstSpace < 0) {
632
+ return "";
633
+ }
634
+ return value.slice(firstSpace + 1).trim();
635
+ }
636
+ function selectApproval(approvals, requestedId) {
637
+ const wantedId = String(requestedId ?? "").trim();
638
+ if (wantedId) {
639
+ return approvals.find((approval) => approval.id === wantedId) ?? null;
640
+ }
641
+ if (approvals.length === 1) {
642
+ return approvals[0];
643
+ }
644
+ return null;
645
+ }
646
+ function buildApprovalSelectionMessage(approvals, requestedId) {
647
+ const wantedId = String(requestedId ?? "").trim();
648
+ if (wantedId && approvals.length > 0) {
649
+ return `Approval '${wantedId}' not found.\n${formatApprovalList(approvals)}`;
650
+ }
651
+ if (approvals.length === 0) {
652
+ return "No pending approvals.";
653
+ }
654
+ return `Multiple approvals pending.\n${formatApprovalList(approvals)}\nUse /approve <id>, /approvealways <id>, /approveall, /approveallalways, /deny <id> or /denyall.`;
655
+ }
656
+ function formatInterruptResult(result) {
657
+ if (!result || typeof result !== "object") {
658
+ return "Stop request sent.";
659
+ }
660
+ if (result.interrupted === true) {
661
+ if (String(result.method ?? "") === "process_restart") {
662
+ return "Generation interruption requested (provider restarted).";
663
+ }
664
+ return "Generation interruption requested.";
665
+ }
666
+ const reason = String(result.reason ?? "")
667
+ .trim()
668
+ .toLowerCase();
669
+ if (reason === "no_active_turn" || reason === "no_active_session") {
670
+ return "No active generation to stop.";
671
+ }
672
+ if (reason === "not_supported") {
673
+ return "Stop is not supported by this provider.";
674
+ }
675
+ if (reason === "error") {
676
+ return `Stop failed:\n${String(result.error ?? "unknown error")}`;
677
+ }
678
+ return "No active generation to stop.";
679
+ }
680
+ function buildSteerPrompt(instruction) {
681
+ const text = String(instruction ?? "").trim();
682
+ return [
683
+ "Steer instruction from user:",
684
+ text,
685
+ "Continue by strictly applying this steering.",
686
+ ].join("\n");
687
+ }
688
+ function isTurnInterruptedError(message) {
689
+ const normalized = String(message ?? "")
690
+ .trim()
691
+ .toLowerCase();
692
+ if (!normalized) {
693
+ return false;
694
+ }
695
+ return (normalized.includes("turn was interrupted") ||
696
+ normalized.includes("turn interrupted by user") ||
697
+ normalized.includes("process stopped while waiting for turn completion"));
698
+ }
699
+ function sanitizeError(error) {
700
+ const raw = error instanceof Error ? error.message : String(error);
701
+ return raw.split(/\r?\n/).slice(0, 12).join("\n");
702
+ }
703
+ function normalizeAllowedChatIds(value) {
704
+ if (Array.isArray(value)) {
705
+ return new Set(value.map((entry) => String(entry ?? "").trim()).filter(Boolean));
706
+ }
707
+ if (typeof value === "string") {
708
+ return new Set(value
709
+ .split(",")
710
+ .map((entry) => entry.trim())
711
+ .filter(Boolean));
712
+ }
713
+ return new Set();
714
+ }
715
+ function buildTurnControlKeyboard(token) {
716
+ const safeToken = String(token ?? "").trim();
717
+ if (!safeToken) {
718
+ return {
719
+ inline_keyboard: [],
720
+ };
721
+ }
722
+ return {
723
+ inline_keyboard: [
724
+ [
725
+ {
726
+ text: "Interrompre",
727
+ callback_data: `turnctl:stop:${safeToken}`,
728
+ },
729
+ ],
730
+ ],
731
+ };
732
+ }
733
+ function parseTurnControlCallbackData(raw) {
734
+ const value = String(raw ?? "").trim();
735
+ const match = /^turnctl:(stop):([A-Za-z0-9._:-]+)$/.exec(value);
736
+ if (!match) {
737
+ return null;
738
+ }
739
+ return {
740
+ action: match[1],
741
+ token: match[2],
742
+ };
743
+ }