@gonzih/cc-discord 0.2.39 → 0.2.40

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.
@@ -65,21 +65,6 @@ export interface MetaAgentManager {
65
65
  /** Write a raw line to the stdin of the running session, if any. */
66
66
  sendToSession: (ns: string, line: string) => void;
67
67
  }
68
- /**
69
- * Create a MetaAgentManager that maintains one persistent Claude process per namespace.
70
- *
71
- * On first message for a namespace:
72
- * 1. ensureWorkspace + injectMcp
73
- * 2. Drain any pending Redis queue entries to stdin
74
- * 3. Write the new message to stdin
75
- *
76
- * On subsequent messages: write directly to stdin of the running process.
77
- *
78
- * On process exit: remove from sessions map. Next message triggers a respawn.
79
- *
80
- * The 3-second poll loop drains any messages that arrived while a session was
81
- * starting up or temporarily unavailable.
82
- */
83
68
  export declare function createMetaAgentManager(): MetaAgentManager;
84
69
  /**
85
70
  * Migrate the old cc-agent meta input key to the new cc-discord key.
@@ -293,7 +293,7 @@ export function spawnSession(ns, message, token, wire) {
293
293
  * Stdout is wired to Redis via wireStdoutToRedis.
294
294
  * Returns the ChildProcess.
295
295
  */
296
- function spawnPersistentSession(ns, token, wire, onExit) {
296
+ function spawnPersistentSession(ns, token, wire, onExit, onOutput) {
297
297
  const wsPath = workspacePath(ns);
298
298
  const claudeBin = resolveClaude();
299
299
  const env = buildEnv(token);
@@ -312,6 +312,9 @@ function spawnPersistentSession(ns, token, wire, onExit) {
312
312
  ], { cwd: wsPath, env, stdio: ["pipe", "pipe", "pipe"] });
313
313
  proc.stdin.setDefaultEncoding("utf8");
314
314
  wireStdoutToRedis(proc, ns, wire);
315
+ if (onOutput) {
316
+ proc.stdout.on("data", onOutput);
317
+ }
315
318
  proc.on("exit", (code) => {
316
319
  console.log(`[meta-agent-manager] persistent session exited (ns=${ns}, code=${code})`);
317
320
  onExit();
@@ -337,15 +340,22 @@ function spawnPersistentSession(ns, token, wire, onExit) {
337
340
  * The 3-second poll loop drains any messages that arrived while a session was
338
341
  * starting up or temporarily unavailable.
339
342
  */
343
+ /** Kill a session after this many ms of no stdout (or no new stdin). */
344
+ const SESSION_INACTIVITY_MS = 5 * 60_000; // 5 minutes
340
345
  export function createMetaAgentManager() {
341
346
  let pollInterval = null;
347
+ let watchdogInterval = null;
342
348
  /** One persistent ChildProcess per namespace. */
343
349
  const sessions = new Map();
344
350
  /** Namespaces currently being set up (workspace clone / first spawn). */
345
351
  const startingUp = new Set();
352
+ /** Wire reference needed for watchdog notifications. Set in startPolling. */
353
+ let wireRef = null;
346
354
  /**
347
355
  * Write a line to the stdin of the persistent session for ns.
348
356
  * Silently no-ops if no session exists.
357
+ * Resets the inactivity timer so the watchdog doesn't kill a session
358
+ * that just received a message but hasn't responded yet.
349
359
  */
350
360
  function writeToStdin(ns, line) {
351
361
  const session = sessions.get(ns);
@@ -354,11 +364,54 @@ export function createMetaAgentManager() {
354
364
  try {
355
365
  // Interactive mode: plain text line, Claude reads it as user input
356
366
  session.proc.stdin.write(`${line}\n`);
367
+ // Reset inactivity timer — the session now has SESSION_INACTIVITY_MS to respond
368
+ session.lastOutputAt = Date.now();
357
369
  }
358
370
  catch (err) {
359
371
  console.warn(`[meta-agent-manager] stdin write failed (ns=${ns}):`, err.message);
360
372
  }
361
373
  }
374
+ /**
375
+ * Watchdog: scan all sessions every 60s. If any session has been silent
376
+ * (no stdout and no new stdin) for SESSION_INACTIVITY_MS, kill it and
377
+ * notify Discord so the user can see why the session went quiet.
378
+ */
379
+ function startWatchdog() {
380
+ if (watchdogInterval)
381
+ return;
382
+ watchdogInterval = setInterval(() => {
383
+ if (!wireRef)
384
+ return;
385
+ const now = Date.now();
386
+ for (const [ns, session] of sessions) {
387
+ const idleMs = now - session.lastOutputAt;
388
+ if (idleMs < SESSION_INACTIVITY_MS)
389
+ continue;
390
+ const idleMin = Math.round(idleMs / 60_000);
391
+ console.warn(`[meta-agent-manager] watchdog: ns=${ns} idle ${idleMin}m, killing stuck session`);
392
+ // Notify Discord before killing so the user knows what happened
393
+ const alertMsg = {
394
+ id: crypto.randomUUID(),
395
+ source: "claude",
396
+ role: "assistant",
397
+ content: `⚠️ Session for **${ns}** was idle for ${idleMin} minutes (likely stuck on a tool call). Killed and removed — send any message to restart.`,
398
+ timestamp: new Date().toISOString(),
399
+ chatId: 0,
400
+ };
401
+ wireRef.discord.publishOutgoing(ns, alertMsg).catch((err) => {
402
+ console.warn(`[meta-agent-manager] watchdog publishOutgoing failed (ns=${ns}):`, err.message);
403
+ });
404
+ try {
405
+ session.proc.stdin.end();
406
+ session.proc.kill("SIGTERM");
407
+ }
408
+ catch {
409
+ // ignore kill errors
410
+ }
411
+ sessions.delete(ns);
412
+ }
413
+ }, 60_000);
414
+ }
362
415
  /**
363
416
  * Drain all pending messages from the Redis input queue into the session's stdin.
364
417
  */
@@ -405,12 +458,19 @@ export function createMetaAgentManager() {
405
458
  const wsPath = workspacePath(ns);
406
459
  await ensureWorkspace(ns, repoUrl);
407
460
  injectMcp(ns, wsPath, token);
461
+ // Placeholder — filled in after proc is known so the onOutput closure can update it
462
+ let session;
408
463
  const proc = spawnPersistentSession(ns, token, wire, () => {
409
464
  // On exit: remove from map so next message triggers a respawn
410
465
  sessions.delete(ns);
411
466
  console.log(`[meta-agent-manager] session removed from map (ns=${ns})`);
467
+ }, () => {
468
+ // Reset inactivity timer on any stdout data
469
+ if (session)
470
+ session.lastOutputAt = Date.now();
412
471
  });
413
- sessions.set(ns, { proc, ns });
472
+ session = { proc, ns, lastOutputAt: Date.now() };
473
+ sessions.set(ns, session);
414
474
  // Drain any messages that arrived before this session started
415
475
  await drainQueue(ns, wire);
416
476
  // Write the triggering message last (after queue drain, in order)
@@ -443,6 +503,8 @@ export function createMetaAgentManager() {
443
503
  startPolling(wire, getNamespaces, instanceId) {
444
504
  if (pollInterval)
445
505
  return; // already running
506
+ wireRef = wire;
507
+ startWatchdog();
446
508
  pollInterval = setInterval(() => {
447
509
  const namespaces = getNamespaces();
448
510
  if (namespaces.length === 0)
@@ -499,6 +561,10 @@ export function createMetaAgentManager() {
499
561
  pollInterval = null;
500
562
  console.log("[meta-agent-manager] polling stopped");
501
563
  }
564
+ if (watchdogInterval) {
565
+ clearInterval(watchdogInterval);
566
+ watchdogInterval = null;
567
+ }
502
568
  // Kill all active sessions
503
569
  for (const [ns, session] of sessions) {
504
570
  try {
package/dist/notifier.js CHANGED
@@ -279,11 +279,6 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
279
279
  if (!buf || !buf.text.trim())
280
280
  return;
281
281
  const text = `← [${ns}] ` + stripAnsi(buf.text.trim());
282
- if (text.length < 30) {
283
- buf.text = "";
284
- buf.timer = null;
285
- return;
286
- }
287
282
  buf.text = "";
288
283
  buf.timer = null;
289
284
  // During an active loop, route meta-agent output to the thread rather than main channel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
4
4
  "description": "Claude Code Discord bot — chat with Claude Code via Discord",
5
5
  "type": "module",
6
6
  "bin": {