@emqo/claudebridge 0.6.2 → 0.7.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.
@@ -240,13 +240,21 @@ export class DiscordAdapter {
240
240
  await channel.send(t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
241
241
  console.log(`[discord] auto-task #${task.id} for ${task.user_id}`);
242
242
  const res = this.maxParallel > 1
243
- ? await this.engine.runParallel(task.user_id, task.description, "discord", task.chat_id)
244
- : await this.engine.runStream(task.user_id, task.description, "discord", task.chat_id);
243
+ ? await this.engine.runParallel(task.user_id, task.description, "discord", task.chat_id, undefined, 0)
244
+ : await this.engine.runStream(task.user_id, task.description, "discord", task.chat_id, undefined, 0);
245
245
  if (res.timedOut) {
246
246
  this.store.markTaskResult(task.id, "failed");
247
247
  if (res.text)
248
248
  this.store.setTaskResult(task.id, res.text.slice(0, 10000));
249
249
  await channel.send(t(this.locale, "auto_failed", { id: task.id, err: "timed out" }));
250
+ const retryMatch = task.description.match(/\[retry (\d+)\/3\]/);
251
+ const retryCount = retryMatch ? parseInt(retryMatch[1]) : 0;
252
+ if (retryCount < 3) {
253
+ const retryDesc = retryCount === 0
254
+ ? `[retry 1/3] Previous attempt of task #${task.id} timed out. Continue from where it left off: ${task.description}`
255
+ : task.description.replace(`[retry ${retryCount}/3]`, `[retry ${retryCount + 1}/3]`);
256
+ this.store.addTask(task.user_id, "discord", task.chat_id, retryDesc, undefined, true, task.parent_id || task.id, Date.now() + 120000);
257
+ }
250
258
  return;
251
259
  }
252
260
  this.store.markTaskResult(task.id, "done");
@@ -267,6 +275,15 @@ export class DiscordAdapter {
267
275
  catch (err) {
268
276
  this.store.markTaskResult(task.id, "failed");
269
277
  console.error(`[discord] auto-task #${task.id} failed:`, err);
278
+ // Self-healing: auto-retry failed tasks (max 3 retries)
279
+ const retryMatch = task.description.match(/\[retry (\d+)\/3\]/);
280
+ const retryCount = retryMatch ? parseInt(retryMatch[1]) : 0;
281
+ if (retryCount < 3) {
282
+ const retryDesc = retryCount === 0
283
+ ? `[retry 1/3] Previous attempt of task #${task.id} failed (${(err.message || "unknown").slice(0, 100)}). Analyze the failure, fix the issue, then: ${task.description}`
284
+ : task.description.replace(`[retry ${retryCount}/3]`, `[retry ${retryCount + 1}/3]`);
285
+ this.store.addTask(task.user_id, "discord", task.chat_id, retryDesc, undefined, true, task.parent_id || task.id, Date.now() + 120000);
286
+ }
270
287
  try {
271
288
  const ch = await this.client.channels.fetch(task.chat_id);
272
289
  if (ch?.isTextBased() && "send" in ch) {
@@ -394,13 +394,23 @@ export class TelegramAdapter {
394
394
  try {
395
395
  console.log(`[telegram] auto-task #${task.id} for ${task.user_id}`);
396
396
  const res = this.maxParallel > 1
397
- ? await this.engine.runParallel(task.user_id, task.description, "telegram", task.chat_id)
398
- : await this.engine.runStream(task.user_id, task.description, "telegram", task.chat_id);
397
+ ? await this.engine.runParallel(task.user_id, task.description, "telegram", task.chat_id, undefined, 0)
398
+ : await this.engine.runStream(task.user_id, task.description, "telegram", task.chat_id, undefined, 0);
399
399
  if (res.timedOut) {
400
400
  this.store.markTaskResult(task.id, "failed");
401
401
  if (res.text)
402
402
  this.store.setTaskResult(task.id, res.text.slice(0, 10000));
403
403
  await this.reply(chatId, t(this.locale, "auto_failed", { id: task.id, err: "timed out" }));
404
+ // Self-healing: auto-retry timed out tasks
405
+ const retryMatch = task.description.match(/\[retry (\d+)\/3\]/);
406
+ const retryCount = retryMatch ? parseInt(retryMatch[1]) : 0;
407
+ if (retryCount < 3) {
408
+ const retryDesc = retryCount === 0
409
+ ? `[retry 1/3] Previous attempt of task #${task.id} timed out. Continue from where it left off: ${task.description}`
410
+ : task.description.replace(`[retry ${retryCount}/3]`, `[retry ${retryCount + 1}/3]`);
411
+ const retryId = this.store.addTask(task.user_id, "telegram", task.chat_id, retryDesc, undefined, true, task.parent_id || task.id, Date.now() + 120000);
412
+ await this.reply(chatId, t(this.locale, "auto_retry", { id: retryId, attempt: retryCount + 1, parent: task.id }));
413
+ }
404
414
  return;
405
415
  }
406
416
  this.store.markTaskResult(task.id, "done");
@@ -428,6 +438,16 @@ export class TelegramAdapter {
428
438
  catch (err) {
429
439
  this.store.markTaskResult(task.id, "failed");
430
440
  await this.reply(chatId, t(this.locale, "auto_failed", { id: task.id, err: err.message || "unknown" }));
441
+ // Self-healing: auto-retry failed tasks (max 3 retries)
442
+ const retryMatch = task.description.match(/\[retry (\d+)\/3\]/);
443
+ const retryCount = retryMatch ? parseInt(retryMatch[1]) : 0;
444
+ if (retryCount < 3) {
445
+ const retryDesc = retryCount === 0
446
+ ? `[retry 1/3] Previous attempt of task #${task.id} failed (${(err.message || "unknown").slice(0, 100)}). Analyze the failure, fix the issue, then: ${task.description}`
447
+ : task.description.replace(`[retry ${retryCount}/3]`, `[retry ${retryCount + 1}/3]`);
448
+ const retryId = this.store.addTask(task.user_id, "telegram", task.chat_id, retryDesc, undefined, true, task.parent_id || task.id, Date.now() + 120000);
449
+ await this.reply(chatId, t(this.locale, "auto_retry", { id: retryId, attempt: retryCount + 1, parent: task.id }));
450
+ }
431
451
  }
432
452
  }
433
453
  async checkApprovals() {
@@ -26,8 +26,8 @@ export declare class AgentEngine {
26
26
  getMaxParallel(): number;
27
27
  getWorkDir(userId: string): string;
28
28
  isLocked(userId: string): boolean;
29
- runStream(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback): Promise<AgentResponse>;
30
- runParallel(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback): Promise<AgentResponse>;
29
+ runStream(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback, overrideTimeoutMs?: number): Promise<AgentResponse>;
30
+ runParallel(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback, overrideTimeoutMs?: number): Promise<AgentResponse>;
31
31
  private _executeWithRetry;
32
32
  private _execute;
33
33
  private _executeNoSession;
@@ -46,13 +46,13 @@ export class AgentEngine {
46
46
  isLocked(userId) {
47
47
  return this.lock.isLocked(userId);
48
48
  }
49
- async runStream(userId, prompt, platform, chatId, onChunk) {
49
+ async runStream(userId, prompt, platform, chatId, onChunk, overrideTimeoutMs) {
50
50
  const release = await this.lock.acquire(userId);
51
51
  try {
52
52
  this.store.addHistory(userId, platform, "user", prompt);
53
53
  const memories = this.config.agent.memory?.enabled ? this.store.getMemories(userId) : [];
54
54
  const memoryPrompt = memories.length ? memories.map(m => `- ${m.content}`).join("\n") : "";
55
- const res = await this._executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt);
55
+ const res = await this._executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt, overrideTimeoutMs);
56
56
  this.store.addHistory(userId, platform, "assistant", res.text);
57
57
  this.store.recordUsage(userId, platform, res.cost || 0);
58
58
  if (this.config.agent.memory?.auto_summary)
@@ -63,7 +63,7 @@ export class AgentEngine {
63
63
  release();
64
64
  }
65
65
  }
66
- async runParallel(userId, prompt, platform, chatId, onChunk) {
66
+ async runParallel(userId, prompt, platform, chatId, onChunk, overrideTimeoutMs) {
67
67
  // No per-user lock — parallel tasks are independent
68
68
  // No session resume — fresh session to prevent conflicts
69
69
  const memories = this.config.agent.memory?.enabled ? this.store.getMemories(userId) : [];
@@ -75,7 +75,7 @@ export class AgentEngine {
75
75
  ? this.rotator.next()
76
76
  : { name: "cli-default", api_key: "", base_url: "", model: "" };
77
77
  try {
78
- const res = await this._executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt);
78
+ const res = await this._executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs);
79
79
  this.store.recordUsage(userId, platform, res.cost || 0);
80
80
  return res;
81
81
  }
@@ -92,7 +92,7 @@ export class AgentEngine {
92
92
  }
93
93
  throw lastErr;
94
94
  }
95
- async _executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt) {
95
+ async _executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt, overrideTimeoutMs) {
96
96
  const maxRetries = Math.max(Math.min(this.rotator.count, 3), 1);
97
97
  let lastErr;
98
98
  for (let i = 0; i < maxRetries; i++) {
@@ -100,7 +100,7 @@ export class AgentEngine {
100
100
  ? this.rotator.next()
101
101
  : { name: "cli-default", api_key: "", base_url: "", model: "" };
102
102
  try {
103
- return await this._execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt);
103
+ return await this._execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs);
104
104
  }
105
105
  catch (err) {
106
106
  lastErr = err;
@@ -115,7 +115,7 @@ export class AgentEngine {
115
115
  }
116
116
  throw lastErr;
117
117
  }
118
- _execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt) {
118
+ _execute(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs) {
119
119
  return new Promise((resolve, reject) => {
120
120
  const sessionId = this.store.getSession(userId) || "";
121
121
  const cwd = this.getWorkDir(userId);
@@ -150,11 +150,11 @@ export class AgentEngine {
150
150
  const child = spawn("claude", args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
151
151
  child.stdin.end();
152
152
  console.log(`[agent] spawned claude pid=${child.pid} cwd=${cwd} args=${args.join(" ")}`);
153
- const timeoutMs = (this.config.agent.timeout_seconds || 600) * 1000;
154
- const timer = setTimeout(() => { try {
153
+ const timeoutMs = overrideTimeoutMs !== undefined ? overrideTimeoutMs : (this.config.agent.timeout_seconds || 600) * 1000;
154
+ const timer = timeoutMs > 0 ? setTimeout(() => { try {
155
155
  child.kill("SIGTERM");
156
156
  }
157
- catch { } }, timeoutMs);
157
+ catch { } }, timeoutMs) : null;
158
158
  let fullText = "";
159
159
  let newSessionId = sessionId;
160
160
  let cost = 0;
@@ -202,7 +202,8 @@ export class AgentEngine {
202
202
  console.log(`[agent] stderr: ${s.slice(0, 200)}`);
203
203
  });
204
204
  child.on("close", (code, signal) => {
205
- clearTimeout(timer);
205
+ if (timer)
206
+ clearTimeout(timer);
206
207
  console.log(`[agent] claude exited code=${code} signal=${signal} fullText=${fullText.length}chars stderr=${stderr.slice(0, 200)}`);
207
208
  if (signal === "SIGTERM") {
208
209
  console.warn(`[agent] claude timed out after ${timeoutMs / 1000}s`);
@@ -225,7 +226,7 @@ export class AgentEngine {
225
226
  child.on("error", reject);
226
227
  });
227
228
  }
228
- _executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt) {
229
+ _executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt, overrideTimeoutMs) {
229
230
  return new Promise((resolve, reject) => {
230
231
  const cwd = this.getWorkDir(userId);
231
232
  const args = ["-p", prompt, "--verbose", "--output-format", "stream-json", "--permission-mode", this.config.agent.permission_mode || "acceptEdits"];
@@ -257,11 +258,11 @@ export class AgentEngine {
257
258
  const child = spawn("claude", args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
258
259
  child.stdin.end();
259
260
  console.log(`[agent] spawned claude (parallel) pid=${child.pid} cwd=${cwd}`);
260
- const timeoutMs = (this.config.agent.timeout_seconds || 600) * 1000;
261
- const timer = setTimeout(() => { try {
261
+ const timeoutMs = overrideTimeoutMs !== undefined ? overrideTimeoutMs : (this.config.agent.timeout_seconds || 600) * 1000;
262
+ const timer = timeoutMs > 0 ? setTimeout(() => { try {
262
263
  child.kill("SIGTERM");
263
264
  }
264
- catch { } }, timeoutMs);
265
+ catch { } }, timeoutMs) : null;
265
266
  let fullText = "";
266
267
  let sessionId = "";
267
268
  let cost = 0;
@@ -300,7 +301,8 @@ export class AgentEngine {
300
301
  let stderr = "";
301
302
  child.stderr.on("data", (data) => { stderr += data.toString(); });
302
303
  child.on("close", (code, signal) => {
303
- clearTimeout(timer);
304
+ if (timer)
305
+ clearTimeout(timer);
304
306
  console.log(`[agent] claude (parallel) exited code=${code} signal=${signal} text=${fullText.length}chars`);
305
307
  if (signal === "SIGTERM") {
306
308
  console.warn(`[agent] claude (parallel) timed out after ${timeoutMs / 1000}s`);
package/dist/core/i18n.js CHANGED
@@ -14,6 +14,7 @@ const messages = {
14
14
  auto_scheduled: "Auto task #{id} scheduled (executes in {minutes} min):\n{desc}",
15
15
  auto_done: "Auto task #{id} done (cost: ${cost}):",
16
16
  auto_failed: "Auto task #{id} failed: {err}",
17
+ auto_retry: "Self-healing: auto task #{parent} failed, retry #{attempt}/3 queued as task #{id} (in 2min)",
17
18
  page_expired: "Page expired. Please resend your question.",
18
19
  approval_request: "Approval needed for auto task #{id}:\n{desc}",
19
20
  approval_approved: "Auto task #{id} approved -- queued for execution.",
@@ -38,6 +39,7 @@ const messages = {
38
39
  auto_scheduled: "自动任务 #{id} 已排程({minutes} 分钟后执行):\n{desc}",
39
40
  auto_done: "自动任务 #{id} 完成(花费:${cost}):",
40
41
  auto_failed: "自动任务 #{id} 失败:{err}",
42
+ auto_retry: "自愈机制:自动任务 #{parent} 失败,重试 #{attempt}/3 已排队为任务 #{id}(2分钟后执行)",
41
43
  page_expired: "页面已过期,请重新发送问题。",
42
44
  approval_request: "自动任务 #{id} 需要审批:\n{desc}",
43
45
  approval_approved: "自动任务 #{id} 已批准 -- 已加入执行队列。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Bridge claude CLI to chat platforms (Telegram, Discord) with scheduled auto-tasks, autonomous project management, HITL approval, conditional branching, webhook triggers, parallel execution, and observability",
5
5
  "main": "dist/index.js",
6
6
  "bin": {