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

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();