@emqo/claudebridge 0.7.0 → 0.8.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.
@@ -7,6 +7,7 @@ export interface MessageContext {
7
7
  export interface Adapter {
8
8
  start(): Promise<void>;
9
9
  stop(): void;
10
+ reloadConfig?(config: any, locale: string): void;
10
11
  }
11
12
  /** Split long text into chunks respecting newlines, with code-block-aware balancing */
12
13
  export declare function chunkText(text: string, maxLen: number): string[];
@@ -14,6 +14,7 @@ export declare class DiscordAdapter implements Adapter {
14
14
  private activeAutoTasks;
15
15
  private maxParallel;
16
16
  constructor(engine: AgentEngine, store: Store, config: DiscordConfig, locale?: string);
17
+ reloadConfig(config: DiscordConfig, locale: string): void;
17
18
  private setup;
18
19
  private handlePrompt;
19
20
  start(): Promise<void>;
@@ -31,6 +31,11 @@ export class DiscordAdapter {
31
31
  });
32
32
  this.setup();
33
33
  }
34
+ reloadConfig(config, locale) {
35
+ this.config = config;
36
+ this.locale = locale;
37
+ this.maxParallel = this.engine.getMaxParallel();
38
+ }
34
39
  setup() {
35
40
  this.client.on("messageCreate", async (msg) => {
36
41
  if (msg.author.bot)
@@ -239,9 +244,8 @@ export class DiscordAdapter {
239
244
  const channel = ch;
240
245
  await channel.send(t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
241
246
  console.log(`[discord] auto-task #${task.id} for ${task.user_id}`);
242
- const res = this.maxParallel > 1
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);
247
+ // Always use runParallel for auto-tasks: fresh session, no user session pollution
248
+ const res = await this.engine.runParallel(task.user_id, task.description, "discord", task.chat_id, undefined, 0);
245
249
  if (res.timedOut) {
246
250
  this.store.markTaskResult(task.id, "failed");
247
251
  if (res.text)
@@ -17,6 +17,7 @@ export declare class TelegramAdapter implements Adapter {
17
17
  private pages;
18
18
  private static PAGE_TTL;
19
19
  constructor(engine: AgentEngine, store: Store, config: TelegramConfig, locale?: string);
20
+ reloadConfig(config: TelegramConfig, locale: string): void;
20
21
  private get api();
21
22
  private call;
22
23
  private reply;
@@ -23,6 +23,11 @@ export class TelegramAdapter {
23
23
  this.config = config;
24
24
  this.locale = locale;
25
25
  }
26
+ reloadConfig(config, locale) {
27
+ this.config = config;
28
+ this.locale = locale;
29
+ this.maxParallel = this.engine.getMaxParallel();
30
+ }
26
31
  get api() {
27
32
  return `https://api.telegram.org/bot${this.config.token}`;
28
33
  }
@@ -286,6 +291,11 @@ export class TelegramAdapter {
286
291
  else {
287
292
  // Multi-page — store pages and show inline keyboard
288
293
  const key = `${chatId}:${msgId}`;
294
+ // Evict oldest if too many pages cached
295
+ if (this.pages.size >= 50) {
296
+ const oldest = this.pages.keys().next().value;
297
+ this.pages.delete(oldest);
298
+ }
289
299
  this.pages.set(key, { chunks: mdChunks, raw: rawChunks, ts: Date.now() });
290
300
  setTimeout(() => this.pages.delete(key), TelegramAdapter.PAGE_TTL);
291
301
  const keyboard = this.pageKeyboard(chatId, msgId, 0, mdChunks.length);
@@ -325,25 +335,29 @@ export class TelegramAdapter {
325
335
  this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
326
336
  this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
327
337
  await this.registerCommands();
338
+ let pollBackoff = 0;
328
339
  while (this.running) {
329
340
  try {
330
341
  const ctrl = new AbortController();
331
- const timer = setTimeout(() => ctrl.abort(), 15000);
332
- const res = await fetch(`${this.api}/getUpdates?offset=${this.offset}&timeout=5`, { signal: ctrl.signal });
342
+ const timer = setTimeout(() => ctrl.abort(), 30000);
343
+ const res = await fetch(`${this.api}/getUpdates?offset=${this.offset}&timeout=10`, { signal: ctrl.signal });
333
344
  clearTimeout(timer);
334
345
  const json = await res.json();
335
346
  if (!json.ok) {
336
347
  console.error("[telegram] poll error:", json);
337
348
  continue;
338
349
  }
350
+ pollBackoff = 0; // reset on success
339
351
  for (const update of json.result) {
340
352
  this.offset = update.update_id + 1;
341
353
  this.handleUpdate(update).catch(e => console.error("[telegram] handler error:", e));
342
354
  }
343
355
  }
344
356
  catch (err) {
345
- console.error("[telegram] poll fetch error:", err);
346
- await new Promise(r => setTimeout(r, 3000));
357
+ pollBackoff = Math.min(pollBackoff + 1, 6);
358
+ const delay = Math.min(3000 * Math.pow(2, pollBackoff), 120000);
359
+ console.warn(`[telegram] poll error (retry in ${delay / 1000}s): ${err.cause?.code || err.message || "unknown"}`);
360
+ await new Promise(r => setTimeout(r, delay));
347
361
  }
348
362
  }
349
363
  }
@@ -393,9 +407,8 @@ export class TelegramAdapter {
393
407
  await this.reply(chatId, t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
394
408
  try {
395
409
  console.log(`[telegram] auto-task #${task.id} for ${task.user_id}`);
396
- const res = this.maxParallel > 1
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);
410
+ // Always use runParallel for auto-tasks: fresh session, no user session pollution
411
+ const res = await this.engine.runParallel(task.user_id, task.description, "telegram", task.chat_id, undefined, 0);
399
412
  if (res.timedOut) {
400
413
  this.store.markTaskResult(task.id, "failed");
401
414
  if (res.text)
@@ -333,6 +333,10 @@ export class AgentEngine {
333
333
  args.push("--model", ep.model);
334
334
  const child = spawn("claude", args, { env, stdio: ["pipe", "pipe", "pipe"] });
335
335
  child.stdin.end();
336
+ const killTimer = setTimeout(() => { try {
337
+ child.kill("SIGTERM");
338
+ }
339
+ catch { } }, 60000);
336
340
  console.log(`[agent] auto-summary spawned pid=${child.pid} for ${userId}`);
337
341
  let result = "";
338
342
  let cost = 0;
@@ -359,6 +363,7 @@ export class AgentEngine {
359
363
  });
360
364
  child.stderr.on("data", (data) => { stderr += data.toString(); });
361
365
  child.on("close", (code) => {
366
+ clearTimeout(killTimer);
362
367
  if (code !== 0) {
363
368
  console.warn(`[agent] auto-summary failed code=${code} stderr=${stderr.slice(0, 200)} for ${userId}`);
364
369
  }
@@ -10,7 +10,7 @@ export function loadConfig(path) {
10
10
  endpoints: raw.endpoints || [],
11
11
  agent: {
12
12
  ...raw.agent,
13
- timeout_seconds: raw.agent?.timeout_seconds ?? 300,
13
+ timeout_seconds: raw.agent?.timeout_seconds ?? 0,
14
14
  max_parallel: raw.agent?.max_parallel ?? 1,
15
15
  memory: { enabled: true, auto_summary: true, max_memories: 50, ...raw.agent?.memory },
16
16
  skill: { enabled: true, ...raw.agent?.skill },
package/dist/core/keys.js CHANGED
@@ -8,6 +8,8 @@ export class EndpointRotator {
8
8
  this.endpoints = endpoints.filter(e => e.api_key);
9
9
  }
10
10
  next() {
11
+ if (!this.endpoints.length)
12
+ throw new Error("No endpoints configured");
11
13
  const now = Date.now();
12
14
  const len = this.endpoints.length;
13
15
  for (let i = 0; i < len; i++) {
package/dist/core/lock.js CHANGED
@@ -43,11 +43,14 @@ export class UserLock {
43
43
  }
44
44
  async _acquireRedis(userId) {
45
45
  const key = this.prefix + userId;
46
- // spin until acquired
46
+ const maxWait = this.ttl * 1000 + 5000; // TTL + 5s grace
47
+ const start = Date.now();
47
48
  while (true) {
48
49
  const ok = await this.redis.set(key, "1", "EX", this.ttl, "NX");
49
50
  if (ok)
50
51
  break;
52
+ if (Date.now() - start > maxWait)
53
+ throw new Error(`Lock timeout for user ${userId}`);
51
54
  await new Promise((r) => setTimeout(r, 500));
52
55
  }
53
56
  return async () => {
@@ -70,10 +70,18 @@ export class Store {
70
70
  catch { }
71
71
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id)");
72
72
  // Startup recovery: reset orphaned 'running' tasks back to 'auto' so they get re-executed
73
- const recovered = this.db.prepare("UPDATE tasks SET status = 'auto' WHERE status = 'running'").run();
74
- if (recovered.changes > 0) {
75
- console.log(`[store] recovered ${recovered.changes} orphaned running task(s) back to auto queue`);
73
+ const orphaned = this.db.prepare("SELECT id, description FROM tasks WHERE status = 'running'").all();
74
+ for (const t of orphaned) {
75
+ const desc = t.description.startsWith("[recovered]") ? t.description : `[recovered] Check current state before making changes previous attempt was interrupted. Original task: ${t.description}`;
76
+ this.db.prepare("UPDATE tasks SET status = 'auto', description = ? WHERE id = ?").run(desc, t.id);
76
77
  }
78
+ if (orphaned.length > 0) {
79
+ console.log(`[store] recovered ${orphaned.length} orphaned running task(s) back to auto queue`);
80
+ }
81
+ // Startup cleanup: prune history/usage older than 30 days
82
+ const cutoff = Date.now() - 30 * 86400000;
83
+ this.db.prepare("DELETE FROM history WHERE created_at < ?").run(cutoff);
84
+ this.db.prepare("DELETE FROM usage WHERE created_at < ?").run(cutoff);
77
85
  }
78
86
  // --- sessions ---
79
87
  getSession(userId) {
package/dist/ctl.js CHANGED
@@ -16,6 +16,17 @@ function fail(msg) {
16
16
  console.error(msg);
17
17
  process.exit(1);
18
18
  }
19
+ function extractFlag(parts, flag) {
20
+ // Search from end to avoid matching flag text inside description
21
+ for (let i = parts.length - 2; i >= 0; i--) {
22
+ if (parts[i] === flag) {
23
+ const val = parts[i + 1];
24
+ parts.splice(i, 2);
25
+ return val;
26
+ }
27
+ }
28
+ return null;
29
+ }
19
30
  if (category === "memory") {
20
31
  if (action === "add") {
21
32
  const [userId, ...contentParts] = rest;
@@ -102,21 +113,10 @@ else if (category === "auto") {
102
113
  const [userId, platform, chatId, ...descParts] = rest;
103
114
  if (!userId || !platform || !chatId || !descParts.length)
104
115
  fail("Usage: auto add <user_id> <platform> <chat_id> <description> [--parent <id>]");
105
- // Parse optional --parent flag
106
- let parentId = null;
107
- const parentIdx = descParts.indexOf("--parent");
108
- if (parentIdx !== -1 && descParts[parentIdx + 1]) {
109
- parentId = parseInt(descParts[parentIdx + 1]);
110
- descParts.splice(parentIdx, 2);
111
- }
112
- // Parse optional --delay flag
113
- let scheduledAt = null;
114
- const delayIdx = descParts.indexOf("--delay");
115
- if (delayIdx !== -1 && descParts[delayIdx + 1]) {
116
- const delayMin = parseInt(descParts[delayIdx + 1]);
117
- scheduledAt = Date.now() + delayMin * 60000;
118
- descParts.splice(delayIdx, 2);
119
- }
116
+ const parentRaw = extractFlag(descParts, "--parent");
117
+ const parentId = parentRaw ? parseInt(parentRaw) : null;
118
+ const delayRaw = extractFlag(descParts, "--delay");
119
+ const scheduledAt = delayRaw ? Date.now() + parseInt(delayRaw) * 60000 : null;
120
120
  const desc = descParts.join(" ");
121
121
  const r = db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, parent_id, scheduled_at, created_at) VALUES (?, ?, ?, ?, 'auto', ?, ?, ?)").run(userId, platform, chatId, desc, parentId, scheduledAt, Date.now());
122
122
  output({ ok: true, id: Number(r.lastInsertRowid), scheduled_at: scheduledAt, message: scheduledAt ? `Auto task scheduled (in ${Math.ceil((scheduledAt - Date.now()) / 60000)} min)` : "Auto task queued" });
@@ -125,20 +125,10 @@ else if (category === "auto") {
125
125
  const [userId, platform, chatId, ...descParts] = rest;
126
126
  if (!userId || !platform || !chatId || !descParts.length)
127
127
  fail("Usage: auto add-approval <user_id> <platform> <chat_id> <description> [--parent <id>] [--delay <minutes>]");
128
- let parentId = null;
129
- const parentIdx = descParts.indexOf("--parent");
130
- if (parentIdx !== -1 && descParts[parentIdx + 1]) {
131
- parentId = parseInt(descParts[parentIdx + 1]);
132
- descParts.splice(parentIdx, 2);
133
- }
134
- // Parse optional --delay flag
135
- let scheduledAt = null;
136
- const delayIdx = descParts.indexOf("--delay");
137
- if (delayIdx !== -1 && descParts[delayIdx + 1]) {
138
- const delayMin = parseInt(descParts[delayIdx + 1]);
139
- scheduledAt = Date.now() + delayMin * 60000;
140
- descParts.splice(delayIdx, 2);
141
- }
128
+ const parentRaw = extractFlag(descParts, "--parent");
129
+ const parentId = parentRaw ? parseInt(parentRaw) : null;
130
+ const delayRaw = extractFlag(descParts, "--delay");
131
+ const scheduledAt = delayRaw ? Date.now() + parseInt(delayRaw) * 60000 : null;
142
132
  const desc = descParts.join(" ");
143
133
  const r = db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, parent_id, scheduled_at, created_at) VALUES (?, ?, ?, ?, 'approval_pending', ?, ?, ?)").run(userId, platform, chatId, desc, parentId, scheduledAt, Date.now());
144
134
  output({ ok: true, id: Number(r.lastInsertRowid), scheduled_at: scheduledAt, message: scheduledAt ? `Auto task queued for approval (scheduled in ${Math.ceil((scheduledAt - Date.now()) / 60000)} min)` : "Auto task queued for approval" });
package/dist/index.js CHANGED
@@ -57,6 +57,12 @@ async function main() {
57
57
  try {
58
58
  config = reloadConfig();
59
59
  engine.reloadConfig(config);
60
+ for (const a of adapters) {
61
+ if ('reloadConfig' in a && typeof a.reloadConfig === 'function') {
62
+ const plat = a.constructor.name === 'TelegramAdapter' ? config.platforms.telegram : config.platforms.discord;
63
+ a.reloadConfig(plat, config.locale);
64
+ }
65
+ }
60
66
  console.log("[claudebridge] config reloaded (SIGHUP)");
61
67
  }
62
68
  catch (err) {
@@ -72,6 +78,12 @@ async function main() {
72
78
  try {
73
79
  config = reloadConfig();
74
80
  engine.reloadConfig(config);
81
+ for (const a of adapters) {
82
+ if ('reloadConfig' in a && typeof a.reloadConfig === 'function') {
83
+ const plat = a.constructor.name === 'TelegramAdapter' ? config.platforms.telegram : config.platforms.discord;
84
+ a.reloadConfig(plat, config.locale);
85
+ }
86
+ }
75
87
  console.log("[claudebridge] config reloaded");
76
88
  }
77
89
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {