@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/backend/kiro.d.ts +1 -0
- package/dist/backend/kiro.js +8 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +20 -6
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/cli.js +35 -9
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +22 -0
- package/dist/daemon.js +99 -5
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.js +19 -2
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +8 -0
- package/dist/instance-lifecycle.js +34 -5
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/service-installer.d.ts +12 -0
- package/dist/service-installer.js +17 -1
- package/dist/service-installer.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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({
|
|
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`);
|