@songsid/agend 2.1.2-beta.40 → 2.1.2-beta.42

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.
package/dist/daemon.js CHANGED
@@ -412,6 +412,68 @@ export function sanitizePaneTail(pane, lineCount = 5) {
412
412
  .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
413
413
  .slice(0, 200));
414
414
  }
415
+ const INTERACTIVE_PROMPT_PATTERNS = [
416
+ { kind: "sudo_password", pattern: /^\s*\[sudo\]\s+password\s+for\s+[^:\n]+:\s*$/im },
417
+ // macOS sudo and several package installers use this exact no-echo prompt.
418
+ // Tail-only + stability gating is what makes the otherwise-generic word safe.
419
+ { kind: "password", pattern: /^\s*(?:Password|Passphrase):\s*$/im },
420
+ { kind: "confirmation", pattern: /^[^\n]{0,180}(?:\([Yy]\/[Nn]\)|\[[Yy]\/[Nn]\]|\((?:yes|no)\/(?:yes|no)\))\s*:?\s*$/im },
421
+ { kind: "confirmation", pattern: /^\s*Are you sure[^\n]{0,160}\((?:yes\/no)(?:\/\[[^\]]+\])?\)\??\s*$/im },
422
+ { kind: "press_enter", pattern: /^\s*(?:Please\s+)?Press (?:the )?Enter(?: key)?(?: to [^\n]{0,120})?[.:…]?\s*$/im },
423
+ ];
424
+ /**
425
+ * Detect terminal prompts that require a human, but only after the pane tail is
426
+ * unchanged and no control-mode output has arrived for the full grace period.
427
+ * This prevents prose such as "the installer asks [Y/n]" from notifying while
428
+ * an agent is still writing it.
429
+ */
430
+ export class InteractivePromptDetector {
431
+ stableMs;
432
+ signature = null;
433
+ stableSince = 0;
434
+ lastOutputAt = 0;
435
+ notifiedSignature = null;
436
+ constructor(stableMs = 10_000) {
437
+ this.stableMs = stableMs;
438
+ }
439
+ observe(pane, now = Date.now(), outputAt = 0) {
440
+ const tail = sanitizePaneTail(pane, 5);
441
+ const tailText = tail.join("\n");
442
+ let matched = null;
443
+ for (const candidate of INTERACTIVE_PROMPT_PATTERNS) {
444
+ const match = tailText.match(candidate.pattern);
445
+ if (!match)
446
+ continue;
447
+ matched = { kind: candidate.kind, prompt: match[0].trim().slice(0, 200) };
448
+ break;
449
+ }
450
+ if (!matched) {
451
+ this.reset();
452
+ this.lastOutputAt = outputAt;
453
+ return null;
454
+ }
455
+ const signature = `${matched.kind}:${tailText}`;
456
+ const outputMoved = outputAt > this.lastOutputAt;
457
+ if (signature !== this.signature || outputMoved) {
458
+ this.signature = signature;
459
+ this.stableSince = now;
460
+ this.lastOutputAt = outputAt;
461
+ if (signature !== this.notifiedSignature)
462
+ this.notifiedSignature = null;
463
+ return null;
464
+ }
465
+ this.lastOutputAt = outputAt;
466
+ if (this.notifiedSignature === signature || now - this.stableSince < this.stableMs)
467
+ return null;
468
+ this.notifiedSignature = signature;
469
+ return matched;
470
+ }
471
+ reset() {
472
+ this.signature = null;
473
+ this.stableSince = 0;
474
+ this.notifiedSignature = null;
475
+ }
476
+ }
415
477
  export class Daemon extends EventEmitter {
416
478
  name;
417
479
  config;
@@ -528,6 +590,7 @@ export class Daemon extends EventEmitter {
528
590
  firstDeliveryDelay = new FirstDeliveryDelay();
529
591
  // PTY error pattern monitoring
530
592
  errorMonitorTimer = null;
593
+ interactivePromptDetector = new InteractivePromptDetector();
531
594
  /** Same 5-min gate the error monitor uses, so a dead MCP server alerts once. */
532
595
  static MCP_DEATH_COOLDOWN_MS = 5 * 60_000;
533
596
  lastMcpDeathNotifiedAt = 0;
@@ -1289,8 +1352,6 @@ export class Daemon extends EventEmitter {
1289
1352
  return;
1290
1353
  const patterns = this.backend?.getErrorPatterns?.() ?? [];
1291
1354
  const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
1292
- if (!patterns.length && !dialogs.length)
1293
- return;
1294
1355
  if (!this.tmux)
1295
1356
  return;
1296
1357
  if (!this.backend)
@@ -1305,6 +1366,13 @@ export class Daemon extends EventEmitter {
1305
1366
  if (!alive)
1306
1367
  return;
1307
1368
  const pane = await this.tmux.capturePane();
1369
+ const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
1370
+ if (interactivePrompt) {
1371
+ this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
1372
+ this.emit("interactive_prompt", { name: this.name, ...interactivePrompt });
1373
+ // A prompt is not an error and must not enter the PTY recovery gate.
1374
+ // Continue scanning real errors in this same snapshot.
1375
+ }
1308
1376
  // Auto-dismiss runtime dialogs (e.g. Codex rate limit model switch)
1309
1377
  for (const dialog of dialogs) {
1310
1378
  if (!dialog.pattern.test(pane))
@@ -1955,6 +2023,7 @@ export class Daemon extends EventEmitter {
1955
2023
  clearInterval(this.errorMonitorTimer);
1956
2024
  this.errorMonitorTimer = null;
1957
2025
  }
2026
+ this.interactivePromptDetector.reset();
1958
2027
  this.stopInstanceStateMonitor();
1959
2028
  this.transcriptMonitor?.stop();
1960
2029
  this.guardian?.stop();
@@ -2269,8 +2338,20 @@ export class Daemon extends EventEmitter {
2269
2338
  this.logger.debug({ enterSettleMs }, "First delivery after ready — extending Enter settle delay");
2270
2339
  }
2271
2340
  await new Promise(r => setTimeout(r, enterSettleMs));
2272
- const enterAt = Date.now();
2341
+ let enterAt = Date.now();
2273
2342
  await this.tmux.sendSpecialKey("Enter");
2343
+ // Kiro may expose its ready prompt before its final startup redraw. If
2344
+ // that redraw swallows Enter, the redraw's own output makes the normal
2345
+ // busy confirmation return true and suppresses the conditional retry.
2346
+ // Kiro has no native input queue, so one defensive retry on the first
2347
+ // post-ready delivery is safe and cannot mutate a queued turn.
2348
+ if (enterSettleMs > NORMAL_ENTER_SETTLE_MS
2349
+ && this.backend?.requiresFirstDeliveryEnterRetry?.() === true) {
2350
+ await new Promise(r => setTimeout(r, 1_000));
2351
+ enterAt = Date.now();
2352
+ await this.tmux.sendSpecialKey("Enter");
2353
+ this.logger.debug("First delivery after ready — sent defensive Enter retry");
2354
+ }
2274
2355
  if (status)
2275
2356
  this.emit("message_delivered", status); // 👀
2276
2357
  // Busy queue-capable CLIs (codex) may accept paste without an idle→busy
@@ -2616,7 +2697,11 @@ export class Daemon extends EventEmitter {
2616
2697
  }
2617
2698
  if (this.lastChatId) {
2618
2699
  args.chat_id = this.lastChatId;
2619
- if (tool === "reply")
2700
+ // Discord messages live in the channel/thread id, not the guild id in
2701
+ // chat_id. This is required for react/edit as well as reply. In
2702
+ // particular, general_topic deliberately has no configured reply thread,
2703
+ // so FleetManager cannot reconstruct this address from fleet.yaml.
2704
+ if (this.lastThreadId)
2620
2705
  args.thread_id = this.lastThreadId;
2621
2706
  }
2622
2707
  }
@@ -2629,7 +2714,16 @@ export class Daemon extends EventEmitter {
2629
2714
  // from prematurely resolving their pending requests when they receive the broadcast.
2630
2715
  const fleetReqId = `tool_${requestId}`;
2631
2716
  const outboundKey = fleetReqId;
2632
- this.ipcServer?.broadcast({ type: "fleet_outbound", tool, args, fleetRequestId: fleetReqId });
2717
+ this.ipcServer?.broadcast({
2718
+ type: "fleet_outbound",
2719
+ tool,
2720
+ args,
2721
+ fleetRequestId: fleetReqId,
2722
+ // Preserve the exact adapter world that supplied the chat context. An
2723
+ // instance binding is only a fallback: persisted/runtime context can be
2724
+ // from a secondary world, and message ids are not portable across bots.
2725
+ adapterId: this.lastAdapterId,
2726
+ });
2633
2727
  const timeout = setTimeout(() => {
2634
2728
  this.pendingIpcRequests.delete(outboundKey);
2635
2729
  respond(null, `Fleet outbound timed out after ${daemonBudgetMs(tool) / 1000}s`);