@emqo/claudebridge 0.4.0 → 0.5.1

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.
@@ -13,7 +13,9 @@ export class DiscordAdapter {
13
13
  client;
14
14
  reminderTimer;
15
15
  autoTimer;
16
- autoRunning = false;
16
+ approvalTimer;
17
+ activeAutoTasks = 0;
18
+ maxParallel = 1;
17
19
  constructor(engine, store, config, locale = "en") {
18
20
  this.engine = engine;
19
21
  this.store = store;
@@ -98,6 +100,30 @@ export class DiscordAdapter {
98
100
  }
99
101
  return;
100
102
  }
103
+ if (text.startsWith("!approve ")) {
104
+ const taskId = parseInt(text.split(" ")[1]);
105
+ if (isNaN(taskId)) {
106
+ await msg.reply("Usage: !approve <task_id>");
107
+ return;
108
+ }
109
+ const ok = this.store.approveTask(taskId);
110
+ await msg.reply(ok ? t(this.locale, "approval_approved", { id: taskId }) : t(this.locale, "approval_decided", { id: taskId }));
111
+ return;
112
+ }
113
+ if (text.startsWith("!reject ")) {
114
+ const taskId = parseInt(text.split(" ")[1]);
115
+ if (isNaN(taskId)) {
116
+ await msg.reply("Usage: !reject <task_id>");
117
+ return;
118
+ }
119
+ const ok = this.store.rejectTask(taskId);
120
+ await msg.reply(ok ? t(this.locale, "approval_rejected", { id: taskId }) : t(this.locale, "approval_decided", { id: taskId }));
121
+ return;
122
+ }
123
+ if (text === "!status") {
124
+ await this.handleStatusCommand(msg);
125
+ return;
126
+ }
101
127
  // File upload handling
102
128
  if (msg.attachments.size > 0) {
103
129
  const ws = this.engine.getWorkDir(msg.author.id);
@@ -133,7 +159,7 @@ export class DiscordAdapter {
133
159
  const now = Date.now();
134
160
  if (now - lastEdit < EDIT_INTERVAL)
135
161
  return;
136
- const preview = full.slice(-1900) + "\n\n⏳...";
162
+ const preview = full.slice(-1900) + "\n\n...";
137
163
  if (preview === lastText)
138
164
  return;
139
165
  lastText = preview;
@@ -165,14 +191,19 @@ export class DiscordAdapter {
165
191
  console.log("[discord] starting bot...");
166
192
  await this.client.login(this.config.token);
167
193
  console.log(`[discord] logged in as ${this.client.user?.tag}`);
194
+ this.maxParallel = this.engine.getMaxParallel();
195
+ console.log(`[discord] max_parallel=${this.maxParallel}`);
168
196
  this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
169
197
  this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
198
+ this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
170
199
  }
171
200
  stop() {
172
201
  if (this.reminderTimer)
173
202
  clearInterval(this.reminderTimer);
174
203
  if (this.autoTimer)
175
204
  clearInterval(this.autoTimer);
205
+ if (this.approvalTimer)
206
+ clearInterval(this.approvalTimer);
176
207
  this.client.destroy();
177
208
  }
178
209
  async checkReminders() {
@@ -190,13 +221,17 @@ export class DiscordAdapter {
190
221
  }
191
222
  }
192
223
  async processAutoTasks() {
193
- if (this.autoRunning)
224
+ const available = this.maxParallel - this.activeAutoTasks;
225
+ if (available <= 0)
194
226
  return;
195
- const task = this.store.getNextAutoTask("discord");
196
- if (!task)
197
- return;
198
- this.autoRunning = true;
199
- this.store.markTaskRunning(task.id);
227
+ const tasks = this.store.getNextAutoTasks("discord", available);
228
+ for (const task of tasks) {
229
+ this.activeAutoTasks++;
230
+ this.store.markTaskRunning(task.id);
231
+ this.runAutoTask(task).finally(() => { this.activeAutoTasks--; });
232
+ }
233
+ }
234
+ async runAutoTask(task) {
200
235
  try {
201
236
  const ch = await this.client.channels.fetch(task.chat_id);
202
237
  if (!ch?.isTextBased() || !("send" in ch))
@@ -204,13 +239,23 @@ export class DiscordAdapter {
204
239
  const channel = ch;
205
240
  await channel.send(t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
206
241
  console.log(`[discord] auto-task #${task.id} for ${task.user_id}`);
207
- const res = await this.engine.runStream(task.user_id, task.description, "discord", task.chat_id);
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);
208
245
  this.store.markTaskResult(task.id, "done");
246
+ if (res.text)
247
+ this.store.setTaskResult(task.id, res.text.slice(0, 10000));
209
248
  const maxLen = this.config.chunk_size || 1900;
210
249
  const chunks = chunkText(res.text || "(no output)", maxLen);
211
250
  await channel.send(t(this.locale, "auto_done", { id: task.id, cost: (res.cost || 0).toFixed(4) }));
212
251
  for (const c of chunks)
213
252
  await channel.send(c);
253
+ // Chain progress reporting
254
+ if (task.parent_id) {
255
+ const progress = this.store.getChainProgress(task.parent_id);
256
+ const costSuffix = res.cost ? ` | Cost: $${res.cost.toFixed(4)}` : "";
257
+ await channel.send(t(this.locale, "chain_progress", { id: task.parent_id, done: progress.done, total: progress.total, cost: costSuffix }));
258
+ }
214
259
  }
215
260
  catch (err) {
216
261
  this.store.markTaskResult(task.id, "failed");
@@ -223,8 +268,40 @@ export class DiscordAdapter {
223
268
  }
224
269
  catch { }
225
270
  }
226
- finally {
227
- this.autoRunning = false;
271
+ }
272
+ async checkApprovals() {
273
+ try {
274
+ const pending = this.store.getPendingApprovals("discord");
275
+ for (const task of pending) {
276
+ const ch = await this.client.channels.fetch(task.chat_id);
277
+ if (ch?.isTextBased() && "send" in ch) {
278
+ await ch.send(t(this.locale, "approval_request", { id: task.id, desc: task.description }) +
279
+ `\n\nReply \`!approve ${task.id}\` or \`!reject ${task.id}\``);
280
+ }
281
+ this.store.markReminderSent(task.id);
282
+ }
228
283
  }
284
+ catch (e) {
285
+ console.error("[discord] approval check error:", e);
286
+ }
287
+ }
288
+ async handleStatusCommand(msg) {
289
+ const recent = this.store.getRecentAutoTasks("discord", 10);
290
+ if (!recent.length) {
291
+ await msg.reply(t(this.locale, "no_auto_tasks"));
292
+ return;
293
+ }
294
+ const statusEmoji = {
295
+ auto: "[queue]", running: "[run]", done: "[done]", failed: "[fail]",
296
+ approval_pending: "[pending]", cancelled: "[cancel]",
297
+ };
298
+ const lines = recent.map(task => {
299
+ const chain = task.parent_id ? ` (chain #${task.parent_id})` : "";
300
+ return `${statusEmoji[task.status] || "[?]"} #${task.id} [${task.status}] ${task.description.slice(0, 60)}${chain}`;
301
+ });
302
+ const stats = this.store.getAutoTaskStats();
303
+ const summary = stats.map(s => `${s.status}: ${s.count}`).join(" | ");
304
+ const report = `${t(this.locale, "status_report")}\n${lines.join("\n")}\n\nSummary: ${summary}`;
305
+ await msg.reply(report);
229
306
  }
230
307
  }
@@ -11,7 +11,9 @@ export declare class TelegramAdapter implements Adapter {
11
11
  private offset;
12
12
  private reminderTimer?;
13
13
  private autoTimer?;
14
- private autoRunning;
14
+ private approvalTimer?;
15
+ private activeAutoTasks;
16
+ private maxParallel;
15
17
  private pages;
16
18
  private static PAGE_TTL;
17
19
  constructor(engine: AgentEngine, store: Store, config: TelegramConfig, locale?: string);
@@ -28,4 +30,8 @@ export declare class TelegramAdapter implements Adapter {
28
30
  private registerCommands;
29
31
  private checkReminders;
30
32
  private processAutoTasks;
33
+ private runAutoTask;
34
+ private checkApprovals;
35
+ private handleApprovalCallback;
36
+ private handleStatusCommand;
31
37
  }
@@ -12,7 +12,9 @@ export class TelegramAdapter {
12
12
  offset = 0;
13
13
  reminderTimer;
14
14
  autoTimer;
15
- autoRunning = false;
15
+ approvalTimer;
16
+ activeAutoTasks = 0;
17
+ maxParallel = 1;
16
18
  pages = new Map();
17
19
  static PAGE_TTL = 30 * 60 * 1000; // 30 minutes
18
20
  constructor(engine, store, config, locale = "en") {
@@ -72,7 +74,13 @@ export class TelegramAdapter {
72
74
  }
73
75
  async handleUpdate(update) {
74
76
  if (update.callback_query) {
75
- await this.handlePageCallback(update.callback_query);
77
+ const data = update.callback_query.data || "";
78
+ if (data.startsWith("approve:") || data.startsWith("reject:")) {
79
+ await this.handleApprovalCallback(update.callback_query);
80
+ }
81
+ else {
82
+ await this.handlePageCallback(update.callback_query);
83
+ }
76
84
  return;
77
85
  }
78
86
  const msg = update.message;
@@ -140,6 +148,10 @@ export class TelegramAdapter {
140
148
  }
141
149
  return;
142
150
  }
151
+ if (text === "/status") {
152
+ await this.handleStatusCommand(chatId, String(uid));
153
+ return;
154
+ }
143
155
  // File upload
144
156
  if (msg.document || msg.photo) {
145
157
  let fileId;
@@ -254,7 +266,7 @@ export class TelegramAdapter {
254
266
  if (now - lastEdit < EDIT_INTERVAL)
255
267
  return;
256
268
  lastEdit = now;
257
- const preview = full.slice(-3500) + "\n\n⏳...";
269
+ const preview = full.slice(-3500) + "\n\n...";
258
270
  await this.editMsg(chatId, msgId, preview);
259
271
  });
260
272
  console.log(`[telegram] claude done for ${uid}, cost=$${res.cost?.toFixed(4)}`);
@@ -307,9 +319,11 @@ export class TelegramAdapter {
307
319
  }
308
320
  async start() {
309
321
  this.running = true;
310
- console.log("[telegram] starting long polling...");
322
+ this.maxParallel = this.engine.getMaxParallel();
323
+ console.log(`[telegram] starting long polling... (max_parallel=${this.maxParallel})`);
311
324
  this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
312
325
  this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
326
+ this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
313
327
  await this.registerCommands();
314
328
  while (this.running) {
315
329
  try {
@@ -339,6 +353,8 @@ export class TelegramAdapter {
339
353
  clearInterval(this.reminderTimer);
340
354
  if (this.autoTimer)
341
355
  clearInterval(this.autoTimer);
356
+ if (this.approvalTimer)
357
+ clearInterval(this.approvalTimer);
342
358
  }
343
359
  async registerCommands() {
344
360
  try {
@@ -362,31 +378,134 @@ export class TelegramAdapter {
362
378
  }
363
379
  }
364
380
  async processAutoTasks() {
365
- if (this.autoRunning)
366
- return;
367
- const task = this.store.getNextAutoTask("telegram");
368
- if (!task)
381
+ const available = this.maxParallel - this.activeAutoTasks;
382
+ if (available <= 0)
369
383
  return;
370
- this.autoRunning = true;
371
- this.store.markTaskRunning(task.id);
384
+ const tasks = this.store.getNextAutoTasks("telegram", available);
385
+ for (const task of tasks) {
386
+ this.activeAutoTasks++;
387
+ this.store.markTaskRunning(task.id);
388
+ this.runAutoTask(task).finally(() => { this.activeAutoTasks--; });
389
+ }
390
+ }
391
+ async runAutoTask(task) {
372
392
  const chatId = Number(task.chat_id);
373
393
  await this.reply(chatId, t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
374
394
  try {
375
395
  console.log(`[telegram] auto-task #${task.id} for ${task.user_id}`);
376
- const res = await this.engine.runStream(task.user_id, task.description, "telegram", task.chat_id);
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);
377
399
  this.store.markTaskResult(task.id, "done");
400
+ if (res.text)
401
+ this.store.setTaskResult(task.id, res.text.slice(0, 10000));
378
402
  const maxLen = this.config.chunk_size || 4000;
379
403
  const chunks = chunkText(res.text || "(no output)", maxLen);
380
404
  await this.reply(chatId, t(this.locale, "auto_done", { id: task.id, cost: (res.cost || 0).toFixed(4) }));
381
405
  for (const c of chunks)
382
406
  await this.reply(chatId, c);
407
+ // Chain progress reporting
408
+ if (task.parent_id) {
409
+ const progress = this.store.getChainProgress(task.parent_id);
410
+ const costSuffix = res.cost ? ` | Cost: $${res.cost.toFixed(4)}` : "";
411
+ await this.reply(chatId, t(this.locale, "chain_progress", { id: task.parent_id, done: progress.done, total: progress.total, cost: costSuffix }));
412
+ }
383
413
  }
384
414
  catch (err) {
385
415
  this.store.markTaskResult(task.id, "failed");
386
416
  await this.reply(chatId, t(this.locale, "auto_failed", { id: task.id, err: err.message || "unknown" }));
387
417
  }
388
- finally {
389
- this.autoRunning = false;
418
+ }
419
+ async checkApprovals() {
420
+ try {
421
+ const pending = this.store.getPendingApprovals("telegram");
422
+ for (const task of pending) {
423
+ const chatId = Number(task.chat_id);
424
+ const keyboard = {
425
+ inline_keyboard: [[
426
+ { text: "Approve", callback_data: `approve:${task.id}` },
427
+ { text: "Reject", callback_data: `reject:${task.id}` },
428
+ ]],
429
+ };
430
+ await this.call("sendMessage", {
431
+ chat_id: chatId,
432
+ text: t(this.locale, "approval_request", { id: task.id, desc: task.description }),
433
+ reply_markup: keyboard,
434
+ });
435
+ this.store.markReminderSent(task.id); // reuse reminder_sent to avoid re-sending
436
+ }
437
+ }
438
+ catch (e) {
439
+ console.error("[telegram] approval check error:", e);
440
+ }
441
+ }
442
+ async handleApprovalCallback(cb) {
443
+ const data = cb.data || "";
444
+ const cbId = cb.id;
445
+ const answer = (text) => this.call("answerCallbackQuery", { callback_query_id: cbId, text, show_alert: true });
446
+ const [action, idStr] = data.split(":");
447
+ const taskId = parseInt(idStr);
448
+ if (isNaN(taskId)) {
449
+ await answer("Invalid task ID");
450
+ return;
451
+ }
452
+ if (action === "approve") {
453
+ const ok = this.store.approveTask(taskId);
454
+ if (ok) {
455
+ await answer(t(this.locale, "approval_approved", { id: taskId }));
456
+ // Edit the original message to show approved
457
+ if (cb.message) {
458
+ try {
459
+ await this.call("editMessageText", {
460
+ chat_id: cb.message.chat.id,
461
+ message_id: cb.message.message_id,
462
+ text: t(this.locale, "approval_approved", { id: taskId }),
463
+ });
464
+ }
465
+ catch { }
466
+ }
467
+ }
468
+ else {
469
+ await answer(t(this.locale, "approval_decided", { id: taskId }));
470
+ }
390
471
  }
472
+ else if (action === "reject") {
473
+ const ok = this.store.rejectTask(taskId);
474
+ if (ok) {
475
+ await answer(t(this.locale, "approval_rejected", { id: taskId }));
476
+ if (cb.message) {
477
+ try {
478
+ await this.call("editMessageText", {
479
+ chat_id: cb.message.chat.id,
480
+ message_id: cb.message.message_id,
481
+ text: t(this.locale, "approval_rejected", { id: taskId }),
482
+ });
483
+ }
484
+ catch { }
485
+ }
486
+ }
487
+ else {
488
+ await answer(t(this.locale, "approval_decided", { id: taskId }));
489
+ }
490
+ }
491
+ }
492
+ async handleStatusCommand(chatId, userId) {
493
+ const recent = this.store.getRecentAutoTasks("telegram", 10);
494
+ if (!recent.length) {
495
+ await this.reply(chatId, t(this.locale, "no_auto_tasks"));
496
+ return;
497
+ }
498
+ const statusEmoji = {
499
+ auto: "[queue]", running: "[run]", done: "[done]", failed: "[fail]",
500
+ approval_pending: "[pending]", cancelled: "[cancel]",
501
+ };
502
+ const lines = recent.map(task => {
503
+ const chain = task.parent_id ? ` (chain #${task.parent_id})` : "";
504
+ return `${statusEmoji[task.status] || "[?]"} #${task.id} [${task.status}] ${task.description.slice(0, 60)}${chain}`;
505
+ });
506
+ const stats = this.store.getAutoTaskStats();
507
+ const summary = stats.map(s => `${s.status}: ${s.count}`).join(" | ");
508
+ const report = `${t(this.locale, "status_report")}\n${lines.join("\n")}\n\nSummary: ${summary}`;
509
+ await this.reply(chatId, report);
391
510
  }
392
511
  }
@@ -22,10 +22,13 @@ export declare class AgentEngine {
22
22
  }[];
23
23
  getRotator(): EndpointRotator;
24
24
  getEndpointCount(): number;
25
+ getMaxParallel(): number;
25
26
  getWorkDir(userId: string): string;
26
27
  isLocked(userId: string): boolean;
27
28
  runStream(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback): Promise<AgentResponse>;
29
+ runParallel(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback): Promise<AgentResponse>;
28
30
  private _executeWithRetry;
29
31
  private _execute;
32
+ private _executeNoSession;
30
33
  private _autoSummarize;
31
34
  }
@@ -32,6 +32,9 @@ export class AgentEngine {
32
32
  getEndpointCount() {
33
33
  return this.rotator.count;
34
34
  }
35
+ getMaxParallel() {
36
+ return this.config.agent.max_parallel || 1;
37
+ }
35
38
  getWorkDir(userId) {
36
39
  if (!this.config.workspace.isolation) {
37
40
  return this.config.agent.cwd || process.cwd();
@@ -60,6 +63,35 @@ export class AgentEngine {
60
63
  release();
61
64
  }
62
65
  }
66
+ async runParallel(userId, prompt, platform, chatId, onChunk) {
67
+ // No per-user lock — parallel tasks are independent
68
+ // No session resume — fresh session to prevent conflicts
69
+ const memories = this.config.agent.memory?.enabled ? this.store.getMemories(userId) : [];
70
+ const memoryPrompt = memories.length ? memories.map(m => `- ${m.content}`).join("\n") : "";
71
+ const maxRetries = Math.max(Math.min(this.rotator.count, 3), 1);
72
+ let lastErr;
73
+ for (let i = 0; i < maxRetries; i++) {
74
+ const ep = this.rotator.count
75
+ ? this.rotator.next()
76
+ : { name: "cli-default", api_key: "", base_url: "", model: "" };
77
+ try {
78
+ const res = await this._executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt);
79
+ this.store.recordUsage(userId, platform, res.cost || 0);
80
+ return res;
81
+ }
82
+ catch (err) {
83
+ lastErr = err;
84
+ const msg = String(err?.message || "");
85
+ if (msg.includes("429") || msg.includes("401") || msg.includes("529")) {
86
+ console.warn(`[agent] endpoint ${ep.name} failed (parallel), rotating`);
87
+ this.rotator.markFailed(ep);
88
+ continue;
89
+ }
90
+ throw err;
91
+ }
92
+ }
93
+ throw lastErr;
94
+ }
63
95
  async _executeWithRetry(userId, prompt, platform, chatId, onChunk, memoryPrompt) {
64
96
  const maxRetries = Math.max(Math.min(this.rotator.count, 3), 1);
65
97
  let lastErr;
@@ -192,6 +224,93 @@ export class AgentEngine {
192
224
  child.on("error", reject);
193
225
  });
194
226
  }
227
+ _executeNoSession(userId, prompt, platform, chatId, ep, onChunk, memoryPrompt) {
228
+ return new Promise((resolve, reject) => {
229
+ const cwd = this.getWorkDir(userId);
230
+ const args = ["-p", prompt, "--verbose", "--output-format", "stream-json", "--permission-mode", this.config.agent.permission_mode || "acceptEdits"];
231
+ if (ep.model)
232
+ args.push("--model", ep.model);
233
+ // No session resume — fresh session for parallel execution
234
+ if (this.config.agent.system_prompt)
235
+ args.push("--system-prompt", this.config.agent.system_prompt);
236
+ let appendPrompt = "";
237
+ if (memoryPrompt)
238
+ appendPrompt += `User memories:\n${memoryPrompt}\n\n`;
239
+ if (this.config.agent.skill?.enabled !== false) {
240
+ appendPrompt += generateSkillDoc({ userId, chatId, platform, locale: this.config.locale || "en" });
241
+ }
242
+ if (appendPrompt)
243
+ args.push("--append-system-prompt", appendPrompt.trim());
244
+ if (this.config.agent.allowed_tools?.length)
245
+ args.push("--allowed-tools", this.config.agent.allowed_tools.join(","));
246
+ if (this.config.agent.max_turns)
247
+ args.push("--max-turns", String(this.config.agent.max_turns));
248
+ if (this.config.agent.max_budget_usd)
249
+ args.push("--max-budget-usd", String(this.config.agent.max_budget_usd));
250
+ const env = { ...process.env };
251
+ if (ep.api_key)
252
+ env.ANTHROPIC_API_KEY = ep.api_key;
253
+ if (ep.base_url)
254
+ env.ANTHROPIC_BASE_URL = ep.base_url;
255
+ env.CLAUDEBRIDGE_DB = pathResolve("./data/claudebridge.db");
256
+ const child = spawn("claude", args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
257
+ child.stdin.end();
258
+ console.log(`[agent] spawned claude (parallel) pid=${child.pid} cwd=${cwd}`);
259
+ const timeoutMs = (this.config.agent.timeout_seconds || 300) * 1000;
260
+ const timer = setTimeout(() => { try {
261
+ child.kill("SIGTERM");
262
+ }
263
+ catch { } }, timeoutMs);
264
+ let fullText = "";
265
+ let sessionId = "";
266
+ let cost = 0;
267
+ let buffer = "";
268
+ child.stdout.on("data", (data) => {
269
+ buffer += data.toString();
270
+ const lines = buffer.split("\n");
271
+ buffer = lines.pop() || "";
272
+ for (const line of lines) {
273
+ if (!line.trim())
274
+ continue;
275
+ try {
276
+ const msg = JSON.parse(line);
277
+ if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
278
+ sessionId = msg.session_id;
279
+ }
280
+ if (msg.type === "assistant" && msg.message?.content) {
281
+ for (const block of msg.message.content) {
282
+ if (block.type === "text" && block.text) {
283
+ fullText += block.text + "\n";
284
+ if (onChunk)
285
+ onChunk(block.text, fullText);
286
+ }
287
+ }
288
+ }
289
+ if (msg.type === "result") {
290
+ if (msg.result)
291
+ fullText = msg.result;
292
+ if (msg.total_cost_usd)
293
+ cost = msg.total_cost_usd;
294
+ }
295
+ }
296
+ catch { }
297
+ }
298
+ });
299
+ let stderr = "";
300
+ child.stderr.on("data", (data) => { stderr += data.toString(); });
301
+ child.on("close", (code, signal) => {
302
+ clearTimeout(timer);
303
+ console.log(`[agent] claude (parallel) exited code=${code} signal=${signal} text=${fullText.length}chars`);
304
+ if (code === 0 || fullText.trim() || signal === "SIGTERM") {
305
+ resolve({ text: fullText.trim() || (signal === "SIGTERM" ? "(timed out)" : "(no response)"), sessionId, cost });
306
+ }
307
+ else {
308
+ reject(new Error(`claude exited ${code}: ${stderr.slice(0, 500)}`));
309
+ }
310
+ });
311
+ child.on("error", reject);
312
+ });
313
+ }
195
314
  _autoSummarize(userId, prompt, response) {
196
315
  const ep = this.rotator.count
197
316
  ? this.rotator.next()
@@ -16,6 +16,7 @@ export interface AgentConfig {
16
16
  system_prompt: string;
17
17
  cwd: string;
18
18
  timeout_seconds: number;
19
+ max_parallel: number;
19
20
  memory: MemoryConfig;
20
21
  skill: SkillConfig;
21
22
  }
@@ -41,6 +42,19 @@ export interface RedisConfig {
41
42
  enabled: boolean;
42
43
  url: string;
43
44
  }
45
+ export interface WebhookConfig {
46
+ enabled: boolean;
47
+ port: number;
48
+ token: string;
49
+ github_secret: string;
50
+ }
51
+ export interface CronEntry {
52
+ schedule_minutes: number;
53
+ user_id: string;
54
+ platform: string;
55
+ chat_id: string;
56
+ description: string;
57
+ }
44
58
  export interface Config {
45
59
  endpoints: Endpoint[];
46
60
  agent: AgentConfig;
@@ -52,6 +66,8 @@ export interface Config {
52
66
  telegram: TelegramConfig;
53
67
  discord: DiscordConfig;
54
68
  };
69
+ webhook: WebhookConfig;
70
+ cron: CronEntry[];
55
71
  }
56
72
  export declare function loadConfig(path?: string): Config;
57
73
  export declare function reloadConfig(): Config;
@@ -11,6 +11,7 @@ export function loadConfig(path) {
11
11
  agent: {
12
12
  ...raw.agent,
13
13
  timeout_seconds: raw.agent?.timeout_seconds ?? 300,
14
+ max_parallel: raw.agent?.max_parallel ?? 1,
14
15
  memory: { enabled: true, auto_summary: true, max_memories: 50, ...raw.agent?.memory },
15
16
  skill: { enabled: true, ...raw.agent?.skill },
16
17
  },
@@ -19,6 +20,8 @@ export function loadConfig(path) {
19
20
  redis: raw.redis || { enabled: false, url: "" },
20
21
  locale: raw.locale || "en",
21
22
  platforms: raw.platforms,
23
+ webhook: { enabled: false, port: 3100, token: "", github_secret: "", ...raw.webhook },
24
+ cron: raw.cron || [],
22
25
  };
23
26
  // defaults for each endpoint
24
27
  for (const ep of c.endpoints) {