@songsid/agend 2.1.8 → 2.1.10-beta.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.
- package/README.md +8 -3
- package/README.zh-TW.md +11 -5
- package/dist/backend/claude-code.d.ts +35 -5
- package/dist/backend/claude-code.js +288 -17
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +13 -0
- package/dist/backend/codex.js +74 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +9 -0
- package/dist/backend/credential-profile.js +20 -0
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/grok.js +19 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +0 -2
- package/dist/backend/kiro.js +8 -7
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.d.ts +6 -0
- package/dist/backend/muse.js +7 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +1 -1
- package/dist/channel/adapters/telegram.js +4 -3
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-operation-response.js +5 -4
- package/dist/channel/mcp-operation-response.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -0
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +7 -0
- package/dist/cli.js +150 -52
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +24 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +27 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +7 -0
- package/dist/cross-instance-envelope.js +19 -1
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +112 -3
- package/dist/daemon.js +360 -41
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +9 -1
- package/dist/delivery-outbox.js +43 -27
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +133 -5
- package/dist/fleet-manager.js +525 -86
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +160 -0
- package/dist/forged-envelope-scan.js +309 -0
- package/dist/forged-envelope-scan.js.map +1 -0
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +5 -2
- package/dist/instance-lifecycle.d.ts +90 -6
- package/dist/instance-lifecycle.js +238 -30
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +180 -8
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +38 -0
- package/dist/login-controller.js +236 -8
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +25 -1
- package/dist/login-flows.js +10 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/ls-rows.d.ts +17 -0
- package/dist/ls-rows.js +73 -0
- package/dist/ls-rows.js.map +1 -1
- package/dist/outbound-handlers.d.ts +3 -0
- package/dist/outbound-handlers.js +17 -19
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/paths.js +4 -2
- package/dist/paths.js.map +1 -1
- package/dist/pause-marker.d.ts +10 -1
- package/dist/pause-marker.js +26 -2
- package/dist/pause-marker.js.map +1 -1
- package/dist/routing-engine.d.ts +17 -2
- package/dist/routing-engine.js +48 -5
- package/dist/routing-engine.js.map +1 -1
- package/dist/service-installer.d.ts +8 -0
- package/dist/service-installer.js +36 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +2 -0
- package/dist/settings-api.js +3 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-wizard.js +3 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/status-emojis.d.ts +9 -1
- package/dist/status-emojis.js +11 -1
- package/dist/status-emojis.js.map +1 -1
- package/dist/target-queue-worker.d.ts +67 -0
- package/dist/target-queue-worker.js +76 -0
- package/dist/target-queue-worker.js.map +1 -0
- package/dist/tool-permissions.js +7 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +23 -19
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +30 -0
- package/dist/transcript-sources.js +78 -0
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +23 -5
- package/dist/tunnel/cloudflared.js +32 -13
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/manager.d.ts +6 -0
- package/dist/tunnel/manager.js +60 -22
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/tunnel/public-fetch.d.ts +30 -0
- package/dist/tunnel/public-fetch.js +156 -0
- package/dist/tunnel/public-fetch.js.map +1 -0
- package/dist/tunnel/types.d.ts +8 -2
- package/dist/tunnel/types.js +8 -2
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +50 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/ui/settings.html +2 -2
- package/dist/ui/view.html +14 -0
- package/dist/ui/web-terminal/terminal.html +1 -0
- package/dist/wake-coordinator.d.ts +128 -0
- package/dist/wake-coordinator.js +337 -0
- package/dist/wake-coordinator.js.map +1 -0
- package/dist/web-api.d.ts +3 -1
- package/dist/web-api.js +1 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-host-guard.d.ts +47 -0
- package/dist/web-host-guard.js +75 -0
- package/dist/web-host-guard.js.map +1 -0
- package/dist/web-terminal-http.d.ts +34 -0
- package/dist/web-terminal-http.js +63 -0
- package/dist/web-terminal-http.js.map +1 -1
- package/dist/web-terminal.d.ts +2 -0
- package/dist/web-terminal.js +3 -6
- package/dist/web-terminal.js.map +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +7 -0
package/dist/daemon.js
CHANGED
|
@@ -454,6 +454,11 @@ const MUSE_DIRECT_RESUME_IDLE_POLL_MS = 5_000;
|
|
|
454
454
|
* verified prompt before asking the native process to exit. Each instance is
|
|
455
455
|
* bounded so a fleet update can stop all daemons in parallel. */
|
|
456
456
|
const KIRO_STOP_IDLE_BUDGET_MS = 15_000;
|
|
457
|
+
/**
|
|
458
|
+
* Bound after a non-stop quit before a dead pane with no relaunch is a wedge
|
|
459
|
+
* (#1030). Exceeds the wake budget (30s+) so a slow relaunch never trips it.
|
|
460
|
+
*/
|
|
461
|
+
const QUIT_RELAUNCH_WATCH_MS = 90_000;
|
|
457
462
|
const KIRO_STOP_QUIT_GRACE_MS = 5_000;
|
|
458
463
|
const KIRO_STOP_SIGTERM_GRACE_MS = 2_000;
|
|
459
464
|
/**
|
|
@@ -1065,6 +1070,16 @@ export class Daemon extends EventEmitter {
|
|
|
1065
1070
|
* would silently no-op exactly when we most need to stop feeding the CLI.
|
|
1066
1071
|
*/
|
|
1067
1072
|
pausePending = false;
|
|
1073
|
+
/**
|
|
1074
|
+
* #1044: pause on the idle edge only if the auth error is still on screen.
|
|
1075
|
+
* Set when nothing could vouch for the hit — the backend has no token-free
|
|
1076
|
+
* auth check (muse), or it could not answer. A real expired session prints
|
|
1077
|
+
* its failure at the end of the turn it broke, so it is still at the bottom
|
|
1078
|
+
* when that turn ends; a phrase the pattern caught mid-turn has scrolled on
|
|
1079
|
+
* by then. Recovery cannot decide this: an expired session is back at its
|
|
1080
|
+
* prompt after every failed turn, which is what "recovered" means.
|
|
1081
|
+
*/
|
|
1082
|
+
pauseReconfirmAuth = false;
|
|
1068
1083
|
/**
|
|
1069
1084
|
* An auth failure was detected and nothing has resolved it yet.
|
|
1070
1085
|
*
|
|
@@ -1075,6 +1090,15 @@ export class Daemon extends EventEmitter {
|
|
|
1075
1090
|
authFailureUnresolved = false;
|
|
1076
1091
|
/** One login-screen auth report per spawn — the screen persists across polls. */
|
|
1077
1092
|
loginScreenReported = false;
|
|
1093
|
+
/**
|
|
1094
|
+
* While the lifecycle is still starting this daemon, `pty_error` has no
|
|
1095
|
+
* listener and `InstanceLifecycle.daemons` has no entry — so an incident the
|
|
1096
|
+
* startup scan raises (sign-in screen, fatal modal) would be dropped on the
|
|
1097
|
+
* floor while its dedupe flag stays set forever. Held here, then re-emitted by
|
|
1098
|
+
* releaseStartupIncidents() once the handlers and the registry are in place.
|
|
1099
|
+
*/
|
|
1100
|
+
holdingStartupIncidents = false;
|
|
1101
|
+
heldStartupIncidents = [];
|
|
1078
1102
|
/** A fatal startup screen (see StartupDialog.fatal) was already reported this run. */
|
|
1079
1103
|
fatalStartupReported = false;
|
|
1080
1104
|
/**
|
|
@@ -1145,6 +1169,8 @@ export class Daemon extends EventEmitter {
|
|
|
1145
1169
|
lastProgressBroadcastAt = 0;
|
|
1146
1170
|
// PTY error pattern monitoring
|
|
1147
1171
|
errorMonitorTimer = null;
|
|
1172
|
+
quitWatchTimer = null;
|
|
1173
|
+
quitWatchToken = null;
|
|
1148
1174
|
interactivePromptDetector = new InteractivePromptDetector();
|
|
1149
1175
|
blockingProcessDetector = new BlockingProcessDetector();
|
|
1150
1176
|
/** Same 5-min gate the error monitor uses, so a dead MCP server alerts once. */
|
|
@@ -1152,6 +1178,17 @@ export class Daemon extends EventEmitter {
|
|
|
1152
1178
|
static MCP_DEATH_COOLDOWN_MS = 5 * 60_000;
|
|
1153
1179
|
lastMcpDeathNotifiedAt = 0;
|
|
1154
1180
|
mcpDeathNotifiedForPid = null;
|
|
1181
|
+
/**
|
|
1182
|
+
* How long after the CLI finished starting its fleet MCP server may take to
|
|
1183
|
+
* show up before "it never connected" is an incident. Servers connect within
|
|
1184
|
+
* seconds of the prompt (kiro loads MCP after it, claude/codex around it);
|
|
1185
|
+
* 90s leaves room for a slow npx/node cold start on a loaded host.
|
|
1186
|
+
*/
|
|
1187
|
+
static FLEET_MCP_STARTUP_GRACE_MS = 90_000;
|
|
1188
|
+
/** spawnGeneration in which a server for this instance was last seen serving. */
|
|
1189
|
+
mcpServedGeneration = -1;
|
|
1190
|
+
/** spawnGeneration whose "fleet MCP never connected" incident was reported. */
|
|
1191
|
+
mcpNeverConnectedReportedGeneration = -1;
|
|
1155
1192
|
/**
|
|
1156
1193
|
* Ceiling on how long a dead MCP server waits for an idle window before the
|
|
1157
1194
|
* auto-restart fires anyway. A toolless instance cannot reply or report, so
|
|
@@ -1290,6 +1327,24 @@ export class Daemon extends EventEmitter {
|
|
|
1290
1327
|
this.lastErrorCount.set(key, count);
|
|
1291
1328
|
this.seededErrorCountBaselines.set(key, count);
|
|
1292
1329
|
}
|
|
1330
|
+
/** Start buffering `pty_error` until releaseStartupIncidents(); see holdingStartupIncidents. */
|
|
1331
|
+
holdStartupIncidents() {
|
|
1332
|
+
this.holdingStartupIncidents = true;
|
|
1333
|
+
}
|
|
1334
|
+
/** Stop buffering and deliver, in order, every `pty_error` raised while the lifecycle was starting us. */
|
|
1335
|
+
releaseStartupIncidents() {
|
|
1336
|
+
this.holdingStartupIncidents = false;
|
|
1337
|
+
const held = this.heldStartupIncidents.splice(0);
|
|
1338
|
+
for (const args of held)
|
|
1339
|
+
super.emit("pty_error", ...args);
|
|
1340
|
+
}
|
|
1341
|
+
emit(event, ...args) {
|
|
1342
|
+
if (event === "pty_error" && this.holdingStartupIncidents) {
|
|
1343
|
+
this.heldStartupIncidents.push(args);
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
return super.emit(event, ...args);
|
|
1347
|
+
}
|
|
1293
1348
|
/**
|
|
1294
1349
|
* Undo the auth suspicion a pattern match armed, once the backend's token-free
|
|
1295
1350
|
* probe has said the credentials are fine.
|
|
@@ -1446,6 +1501,11 @@ export class Daemon extends EventEmitter {
|
|
|
1446
1501
|
finishDurableDelivery(delivery, outcome, evidence) {
|
|
1447
1502
|
if (!delivery)
|
|
1448
1503
|
return;
|
|
1504
|
+
// Delivered or uncertain means the pane write started: the work reached
|
|
1505
|
+
// the CLI (or may have), so it is activity (Phase 2a). Only a failure is
|
|
1506
|
+
// not — a pre-write failure never gets here; it is retried before begin.
|
|
1507
|
+
if (outcome !== "failed")
|
|
1508
|
+
this.noteWorkActivity();
|
|
1449
1509
|
this.deliveryOutbox?.complete(delivery.deliveryId, this.bootId, delivery.attemptNo, outcome, evidence);
|
|
1450
1510
|
}
|
|
1451
1511
|
finishDurableSubmission(delivery, verdict) {
|
|
@@ -1464,6 +1524,56 @@ export class Daemon extends EventEmitter {
|
|
|
1464
1524
|
return;
|
|
1465
1525
|
this.retryDurableDeliveryBeforeBegin(delivery, reason);
|
|
1466
1526
|
}
|
|
1527
|
+
/** Restart the idle clock for work that reached the CLI, and persist it like a user inbound. */
|
|
1528
|
+
noteWorkActivity(now = Date.now()) {
|
|
1529
|
+
this.autoPauseController.recordActivity(now);
|
|
1530
|
+
try {
|
|
1531
|
+
writeLastInboundAt(this.instanceDir, now);
|
|
1532
|
+
}
|
|
1533
|
+
catch (err) {
|
|
1534
|
+
this.logger.warn({ err: err.message }, "Failed to persist last activity timestamp");
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* A daemon started to wake, restart or explicitly start an instance begins
|
|
1539
|
+
* its idle window now (Phase 2a). Only the fleet's own boot keeps the
|
|
1540
|
+
* persisted last-inbound seed, so a long-idle fleet still pauses after a
|
|
1541
|
+
* restart; a woken instance used to inherit a week-old timestamp and pause
|
|
1542
|
+
* one second after it became ready.
|
|
1543
|
+
*/
|
|
1544
|
+
seedActivityNow(now = Date.now()) {
|
|
1545
|
+
this.autoPauseController.recordActivity(now);
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Phase 2b (design §1.4): why this awake instance is not taking input, in
|
|
1549
|
+
* words for a notice, or null when nothing known holds it. Read from the
|
|
1550
|
+
* daemon's own state only; it never inspects the pane itself.
|
|
1551
|
+
*/
|
|
1552
|
+
notAcceptingReason() {
|
|
1553
|
+
if (this.pauseWakeState !== "active")
|
|
1554
|
+
return t("delivery.hold_state", this.pauseWakeState);
|
|
1555
|
+
if (this.inputBlockedDialogKey !== null)
|
|
1556
|
+
return t("delivery.hold_dialog", this.inputBlockedDialogKey);
|
|
1557
|
+
if (this.authFailureUnresolved)
|
|
1558
|
+
return t("delivery.hold_auth");
|
|
1559
|
+
if (this.instanceState === "stuck")
|
|
1560
|
+
return t("delivery.hold_stuck");
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
workLeaseCheck = null;
|
|
1564
|
+
/** Lifecycle-provided: whether the fleet holds a work lease on this instance. */
|
|
1565
|
+
setWorkLeaseCheck(check) {
|
|
1566
|
+
this.workLeaseCheck = check ?? null;
|
|
1567
|
+
}
|
|
1568
|
+
workLeaseActive() {
|
|
1569
|
+
try {
|
|
1570
|
+
return this.workLeaseCheck?.() === true;
|
|
1571
|
+
}
|
|
1572
|
+
catch (err) {
|
|
1573
|
+
this.logger.debug({ err }, "Work lease check failed; treating as no lease");
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1467
1577
|
async start() {
|
|
1468
1578
|
mkdirSync(this.instanceDir, { recursive: true });
|
|
1469
1579
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
@@ -1744,6 +1854,7 @@ export class Daemon extends EventEmitter {
|
|
|
1744
1854
|
return;
|
|
1745
1855
|
const status = mcpServerState(this.instanceDir);
|
|
1746
1856
|
if (status.state === "alive") {
|
|
1857
|
+
this.mcpServedGeneration = this.spawnGeneration;
|
|
1747
1858
|
if (this.mcpDeathNotifiedForPid != null)
|
|
1748
1859
|
this.emitMcpRecovered("pid", status.pid);
|
|
1749
1860
|
this.mcpDeathNotifiedForPid = null; // the CLI respawned it — re-arm
|
|
@@ -1771,8 +1882,13 @@ export class Daemon extends EventEmitter {
|
|
|
1771
1882
|
this.noteMcpProofOfLife("connection", livePid);
|
|
1772
1883
|
return;
|
|
1773
1884
|
}
|
|
1774
|
-
if (status.state === "unknown")
|
|
1775
|
-
|
|
1885
|
+
if (status.state === "unknown") {
|
|
1886
|
+
// Never started, or nothing left to watch. The CLI may legitimately not
|
|
1887
|
+
// have spawned it yet, but past the startup grace a CLI that never
|
|
1888
|
+
// brought up AgEnD's server is running without fleet tools.
|
|
1889
|
+
this.checkFleetMcpNeverConnected();
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1776
1892
|
// Dead: report once per pid, and at most once per cooldown window.
|
|
1777
1893
|
if (this.mcpDeathNotifiedForPid === status.pid)
|
|
1778
1894
|
return;
|
|
@@ -1807,6 +1923,42 @@ export class Daemon extends EventEmitter {
|
|
|
1807
1923
|
if (autoRestart)
|
|
1808
1924
|
this.armMcpRestartWhenIdle();
|
|
1809
1925
|
}
|
|
1926
|
+
/**
|
|
1927
|
+
* The CLI has been up past the startup grace and no server for this instance
|
|
1928
|
+
* has ever served it: AgEnD's fleet tools are missing, so the agent cannot
|
|
1929
|
+
* reply or reach other instances. Reported once per spawn, through the same
|
|
1930
|
+
* mcp_died path (auth check, notice, idle-gated revival restart under the
|
|
1931
|
+
* lifecycle's cooldown) as a server that died.
|
|
1932
|
+
*
|
|
1933
|
+
* This is the check that lets the CLI tolerate the user's OTHER servers
|
|
1934
|
+
* failing (#1111): kiro's --require-mcp-startup could not tell the fleet
|
|
1935
|
+
* server from a third-party one, and failed the whole launch for either.
|
|
1936
|
+
*/
|
|
1937
|
+
checkFleetMcpNeverConnected() {
|
|
1938
|
+
if (this.config.agent_mode === "cli")
|
|
1939
|
+
return; // no MCP server by design
|
|
1940
|
+
if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
|
|
1941
|
+
return;
|
|
1942
|
+
if (this.mcpNeverConnectedReportedGeneration === this.spawnGeneration)
|
|
1943
|
+
return;
|
|
1944
|
+
if (Date.now() - this.lastSpawnAt < Daemon.FLEET_MCP_STARTUP_GRACE_MS)
|
|
1945
|
+
return;
|
|
1946
|
+
if (this.liveMcpSockets().length > 0) {
|
|
1947
|
+
this.mcpServedGeneration = this.spawnGeneration;
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
this.mcpNeverConnectedReportedGeneration = this.spawnGeneration;
|
|
1951
|
+
// pid 0 stands for "no server ever had a pid": a late-connecting server
|
|
1952
|
+
// still finds an alarm to retract (noteMcpProofOfLife / the alive branch).
|
|
1953
|
+
this.mcpDeathNotifiedForPid = 0;
|
|
1954
|
+
const authSuspected = this.authFailureUnresolved;
|
|
1955
|
+
const autoRestart = this.config.mcp_auto_restart !== false && !authSuspected;
|
|
1956
|
+
this.lastMcpDeathNotifiedAt = Date.now();
|
|
1957
|
+
this.logger.error({ autoRestart, authSuspected, graceMs: Daemon.FLEET_MCP_STARTUP_GRACE_MS }, "Fleet MCP server never connected after the CLI started — instance has no agend tools");
|
|
1958
|
+
this.emit("mcp_died", { name: this.name, pid: 0, autoRestart, authSuspected, neverConnected: true });
|
|
1959
|
+
if (autoRestart)
|
|
1960
|
+
this.armMcpRestartWhenIdle();
|
|
1961
|
+
}
|
|
1810
1962
|
/**
|
|
1811
1963
|
* Whether a dead MCP pid on this backend is more likely a CLI-driven
|
|
1812
1964
|
* replacement than a real loss. Codex only, and deliberately narrow: it is
|
|
@@ -2025,6 +2177,7 @@ export class Daemon extends EventEmitter {
|
|
|
2025
2177
|
* if an alarm had already gone out, retracts it.
|
|
2026
2178
|
*/
|
|
2027
2179
|
noteMcpProofOfLife(source, pid) {
|
|
2180
|
+
this.mcpServedGeneration = this.spawnGeneration;
|
|
2028
2181
|
const hadAlarm = this.mcpDeathNotifiedForPid != null;
|
|
2029
2182
|
const hadPending = this.mcpRestartPending || this.mcpDeathDeferredForPid != null;
|
|
2030
2183
|
if (!hadAlarm && !hadPending)
|
|
@@ -2040,6 +2193,9 @@ export class Daemon extends EventEmitter {
|
|
|
2040
2193
|
this.repairMcpPidSlot(pid, source);
|
|
2041
2194
|
}
|
|
2042
2195
|
emitMcpRecovered(source, pid) {
|
|
2196
|
+
// The retracted alarm was "never connected", not a death: say so.
|
|
2197
|
+
if (this.mcpDeathNotifiedForPid === 0)
|
|
2198
|
+
source = "late_connect";
|
|
2043
2199
|
this.logger.info({ source, pid }, "MCP server recovered — retracting the earlier death report");
|
|
2044
2200
|
this.emit("mcp_recovered", { name: this.name, source, pid });
|
|
2045
2201
|
}
|
|
@@ -2468,6 +2624,13 @@ export class Daemon extends EventEmitter {
|
|
|
2468
2624
|
return;
|
|
2469
2625
|
const pane = await this.tmux.capturePane();
|
|
2470
2626
|
const inputBlockedDialog = this.updateInputBlockedState(pane, dialogs);
|
|
2627
|
+
// A sign-in screen that appears AFTER the startup scan ended (first run:
|
|
2628
|
+
// theme → login can take longer than its budget) is the same auth
|
|
2629
|
+
// incident the scan reports. Structural predicate only — the loose
|
|
2630
|
+
// patterns are for a fresh startup pane, not a long-lived transcript.
|
|
2631
|
+
const loginFlow = LOGIN_FLOWS[this.config.backend ?? "claude-code"];
|
|
2632
|
+
if (loginFlow?.loginScreenActive && this.reportLoginScreen(pane))
|
|
2633
|
+
return;
|
|
2471
2634
|
const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
|
|
2472
2635
|
if (interactivePrompt) {
|
|
2473
2636
|
this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
|
|
@@ -2995,33 +3158,128 @@ export class Daemon extends EventEmitter {
|
|
|
2995
3158
|
this.logger.warn(record, `${cliLabel} exited on its own (exit code ${exitCode ?? "unknown"}) — last output recorded`);
|
|
2996
3159
|
}
|
|
2997
3160
|
}
|
|
2998
|
-
/**
|
|
2999
|
-
|
|
3161
|
+
/**
|
|
3162
|
+
* Send the backend-specific graceful quit command/key sequence.
|
|
3163
|
+
*
|
|
3164
|
+
* Observability (#1030): every send is logged with reason + caller, the
|
|
3165
|
+
* outcome is logged, and a non-stop quit arms a bounded relaunch watch —
|
|
3166
|
+
* sol's silent wedge (quit landed, codex exited to its resume screen, no
|
|
3167
|
+
* relaunch, daemon still running) was invisible precisely because none of
|
|
3168
|
+
* this existed.
|
|
3169
|
+
*/
|
|
3170
|
+
async sendQuitSequence(reason = "unspecified", caller = "unknown", opts) {
|
|
3000
3171
|
if (!this.tmux || !this.backend)
|
|
3001
3172
|
return false;
|
|
3173
|
+
const backend = this.backend.binaryName;
|
|
3002
3174
|
const quitCmd = this.backend.getQuitCommand();
|
|
3003
3175
|
const quitKey = this.backend.getQuitKey?.();
|
|
3176
|
+
const quitVia = quitCmd ? `quit command ${quitCmd}` : quitKey ? `quit key ${quitKey}` : "no quit sequence";
|
|
3177
|
+
this.logger.info({ backend, quit: quitCmd ?? quitKey ?? null, reason, caller }, `Sending backend quit sequence (${reason} via ${caller})`);
|
|
3004
3178
|
if (quitCmd || quitKey)
|
|
3005
|
-
this.noteStopRequest(
|
|
3179
|
+
this.noteStopRequest(quitVia, reason);
|
|
3180
|
+
let sent = false;
|
|
3181
|
+
let step = "sendKeys";
|
|
3006
3182
|
if (quitCmd) {
|
|
3007
|
-
if (
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3183
|
+
if (await this.tmux.sendKeys(quitCmd)) {
|
|
3184
|
+
// Delay before Enter to prevent tmux server races when instances stop in
|
|
3185
|
+
// parallel (same pattern as pasteText).
|
|
3186
|
+
await new Promise(r => setTimeout(r, 150));
|
|
3187
|
+
step = "enter";
|
|
3188
|
+
sent = await this.tmux.sendSpecialKey("Enter");
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
else if (quitKey) {
|
|
3192
|
+
step = "quit key";
|
|
3193
|
+
sent = true;
|
|
3194
|
+
const presses = Math.max(1, Math.floor(this.backend.getQuitKeyPresses?.() ?? 1));
|
|
3195
|
+
for (let i = 0; i < presses; i++) {
|
|
3196
|
+
if (!await this.tmux.sendSpecialKey(quitKey)) {
|
|
3197
|
+
sent = false;
|
|
3198
|
+
break;
|
|
3199
|
+
}
|
|
3200
|
+
if (i + 1 < presses)
|
|
3201
|
+
await new Promise(r => setTimeout(r, 250));
|
|
3202
|
+
}
|
|
3013
3203
|
}
|
|
3014
|
-
|
|
3204
|
+
else {
|
|
3205
|
+
this.logger.warn({ backend, reason, caller }, "Backend has no quit command or key — nothing sent");
|
|
3015
3206
|
return false;
|
|
3016
|
-
const presses = Math.max(1, Math.floor(this.backend.getQuitKeyPresses?.() ?? 1));
|
|
3017
|
-
for (let i = 0; i < presses; i++) {
|
|
3018
|
-
const sent = await this.tmux.sendSpecialKey(quitKey);
|
|
3019
|
-
if (!sent)
|
|
3020
|
-
return false;
|
|
3021
|
-
if (i + 1 < presses)
|
|
3022
|
-
await new Promise(r => setTimeout(r, 250));
|
|
3023
3207
|
}
|
|
3024
|
-
|
|
3208
|
+
if (sent) {
|
|
3209
|
+
this.logger.info({ backend, quit: quitCmd ?? quitKey, reason, caller }, "Backend quit sequence sent");
|
|
3210
|
+
if (opts?.watchRelaunch)
|
|
3211
|
+
this.armQuitRelaunchWatch(reason, caller, opts.watchMs);
|
|
3212
|
+
}
|
|
3213
|
+
else {
|
|
3214
|
+
this.logger.warn({ backend, quit: quitCmd ?? quitKey, reason, caller, step }, "Backend quit sequence failed to send");
|
|
3215
|
+
}
|
|
3216
|
+
return sent;
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Codex names its own resume command on its exit screen — the wedge
|
|
3220
|
+
* signature. Backend-generic shell, codex-specific match (#1030 ask 4):
|
|
3221
|
+
* other backends' panes simply never match.
|
|
3222
|
+
*/
|
|
3223
|
+
isCodexResumeScreen(pane) {
|
|
3224
|
+
return /codex\s+resume\b/i.test(pane);
|
|
3225
|
+
}
|
|
3226
|
+
clearQuitRelaunchWatch() {
|
|
3227
|
+
if (this.quitWatchTimer) {
|
|
3228
|
+
clearTimeout(this.quitWatchTimer);
|
|
3229
|
+
this.quitWatchTimer = null;
|
|
3230
|
+
}
|
|
3231
|
+
// Invalidates any already-started checkQuitRelaunch whose pane reads are
|
|
3232
|
+
// still pending: it must bail on return, not warn from stale results.
|
|
3233
|
+
this.quitWatchToken = null;
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* After a non-stop quit, warn if the backend exited but nothing relaunched
|
|
3237
|
+
* within the bound while this daemon still considers itself active.
|
|
3238
|
+
* Skipped when paused/pausing/waking/stopped or a relaunch (new spawn
|
|
3239
|
+
* generation) already happened — only the wedged state warns.
|
|
3240
|
+
*/
|
|
3241
|
+
armQuitRelaunchWatch(reason, caller, delayMs = QUIT_RELAUNCH_WATCH_MS) {
|
|
3242
|
+
this.clearQuitRelaunchWatch();
|
|
3243
|
+
const generation = this.spawnGeneration;
|
|
3244
|
+
const quitAt = Date.now();
|
|
3245
|
+
const token = {};
|
|
3246
|
+
this.quitWatchToken = token;
|
|
3247
|
+
this.quitWatchTimer = setTimeout(() => {
|
|
3248
|
+
this.quitWatchTimer = null;
|
|
3249
|
+
void this.checkQuitRelaunch({ reason, caller, generation, quitAt, token }).catch(() => { });
|
|
3250
|
+
}, delayMs);
|
|
3251
|
+
}
|
|
3252
|
+
async checkQuitRelaunch(opts) {
|
|
3253
|
+
if (this.pauseWakeState !== "active")
|
|
3254
|
+
return;
|
|
3255
|
+
if (opts.generation !== this.spawnGeneration)
|
|
3256
|
+
return;
|
|
3257
|
+
if (!this.tmux)
|
|
3258
|
+
return;
|
|
3259
|
+
// TOCTOU guard: the token is captured at arm time, so a beginSpawn/stop
|
|
3260
|
+
// that lands while the pane reads below are pending invalidates this
|
|
3261
|
+
// check. Re-validate after every await before using the results.
|
|
3262
|
+
const token = opts.token ?? this.quitWatchToken;
|
|
3263
|
+
let pane;
|
|
3264
|
+
let alive = true;
|
|
3265
|
+
try {
|
|
3266
|
+
const status = await this.tmux.getPaneStatus();
|
|
3267
|
+
alive = !!status?.alive;
|
|
3268
|
+
pane = await this.tmux.capturePane().catch(() => undefined);
|
|
3269
|
+
}
|
|
3270
|
+
catch {
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
if (this.pauseWakeState !== "active")
|
|
3274
|
+
return;
|
|
3275
|
+
if (opts.generation !== this.spawnGeneration)
|
|
3276
|
+
return;
|
|
3277
|
+
if (token !== this.quitWatchToken)
|
|
3278
|
+
return;
|
|
3279
|
+
const resumeScreen = !!pane && this.isCodexResumeScreen(pane);
|
|
3280
|
+
if (alive && !resumeScreen)
|
|
3281
|
+
return;
|
|
3282
|
+
this.logger.warn({ backend: this.backend?.binaryName, reason: opts.reason, caller: opts.caller, quitAt: opts.quitAt, paneAlive: alive, resumeScreen }, "codex-exited-without-relaunch: backend quit but no relaunch followed while this daemon is still active");
|
|
3025
3283
|
}
|
|
3026
3284
|
/**
|
|
3027
3285
|
* Kiro drops `/quit` while a turn is running. Stop that turn first, then use
|
|
@@ -3045,6 +3303,7 @@ export class Daemon extends EventEmitter {
|
|
|
3045
3303
|
}
|
|
3046
3304
|
async stop() {
|
|
3047
3305
|
this.logger.info("Stopping daemon instance");
|
|
3306
|
+
this.clearQuitRelaunchWatch();
|
|
3048
3307
|
this.fenceDeliveryWritesForStop();
|
|
3049
3308
|
this.turnReplyGuard.reset();
|
|
3050
3309
|
// Invalidate any bounded pre-Enter wait from the process generation being
|
|
@@ -3071,7 +3330,7 @@ export class Daemon extends EventEmitter {
|
|
|
3071
3330
|
let killed = false;
|
|
3072
3331
|
const windowId = this.tmux.getWindowId();
|
|
3073
3332
|
const kiroReady = windowId ? await this.drainBusyKiroForStop(windowId) : true;
|
|
3074
|
-
const quitSent = kiroReady && await this.sendQuitSequence("graceful stop");
|
|
3333
|
+
const quitSent = kiroReady && await this.sendQuitSequence("graceful stop", "stop");
|
|
3075
3334
|
const quitGraceMs = this.backend?.binaryName === "kiro-cli"
|
|
3076
3335
|
? KIRO_STOP_QUIT_GRACE_MS : 3_000;
|
|
3077
3336
|
const sigtermGraceMs = this.backend?.binaryName === "kiro-cli"
|
|
@@ -3190,12 +3449,35 @@ export class Daemon extends EventEmitter {
|
|
|
3190
3449
|
* failures: pause() alone no-ops while the CLI is busy/stuck, which is the
|
|
3191
3450
|
* usual state when the error surfaces. Cleared by a successful pause or wake.
|
|
3192
3451
|
*/
|
|
3193
|
-
requestPauseWhenIdle() {
|
|
3452
|
+
requestPauseWhenIdle(opts = {}) {
|
|
3194
3453
|
if (this.pauseWakeState === "paused")
|
|
3195
3454
|
return;
|
|
3455
|
+
const reconfirm = opts.reconfirmAuth === true;
|
|
3456
|
+
// An auth request wins over a pending non-auth one: the marker must not
|
|
3457
|
+
// later read as auto-wakeable when a login failure is among the causes.
|
|
3458
|
+
const reason = opts.reason ?? "auth";
|
|
3459
|
+
this.pendingPauseReason = this.pausePending && this.pendingPauseReason === "auth" ? "auth" : reason;
|
|
3460
|
+
// A pause already pending unconditionally stays unconditional.
|
|
3461
|
+
this.pauseReconfirmAuth = this.pausePending ? this.pauseReconfirmAuth && reconfirm : reconfirm;
|
|
3196
3462
|
this.pausePending = true;
|
|
3197
|
-
|
|
3198
|
-
|
|
3463
|
+
// A conditional request is the only pause path it has (#1044): decide on
|
|
3464
|
+
// the current pane now instead of at the next state sweep, so a pane that
|
|
3465
|
+
// is already idle is not left open to a delivery in the meantime.
|
|
3466
|
+
if (reconfirm)
|
|
3467
|
+
void this.captureAndEvaluateInstanceState("deferred_pause");
|
|
3468
|
+
}
|
|
3469
|
+
/** The backend's own auth-error pattern, within the pane's last rows. */
|
|
3470
|
+
authErrorAtPaneBottom(pane) {
|
|
3471
|
+
const rows = pane.replace(/\r/g, "").split("\n").filter(r => r.trim() !== "");
|
|
3472
|
+
const bottom = rows.slice(-Daemon.AUTH_RECONFIRM_ROWS).join("\n");
|
|
3473
|
+
return (this.backend?.getErrorPatterns?.() ?? [])
|
|
3474
|
+
.filter(ep => ep.type === "auth_error")
|
|
3475
|
+
.some(ep => new RegExp(ep.pattern.source, ep.pattern.flags.replace("g", "")).test(bottom));
|
|
3476
|
+
}
|
|
3477
|
+
static AUTH_RECONFIRM_ROWS = 15;
|
|
3478
|
+
/** Why the deferred pause was requested; what its marker records (Phase 2a). */
|
|
3479
|
+
pendingPauseReason = "auth";
|
|
3480
|
+
async pause(reason = "operator") {
|
|
3199
3481
|
if (this.pauseWakeState === "paused")
|
|
3200
3482
|
return;
|
|
3201
3483
|
if (this.pauseWakeState === "pausing" || this.pauseWakeState === "waking") {
|
|
@@ -3223,7 +3505,7 @@ export class Daemon extends EventEmitter {
|
|
|
3223
3505
|
const transition = (async () => {
|
|
3224
3506
|
try {
|
|
3225
3507
|
this.saveSessionId();
|
|
3226
|
-
await this.sendQuitSequence(pauseReason);
|
|
3508
|
+
await this.sendQuitSequence(pauseReason, "pause", { watchRelaunch: true });
|
|
3227
3509
|
let exited = false;
|
|
3228
3510
|
for (let i = 0; i < 15; i++) {
|
|
3229
3511
|
await new Promise(r => setTimeout(r, 200));
|
|
@@ -3248,7 +3530,7 @@ export class Daemon extends EventEmitter {
|
|
|
3248
3530
|
}
|
|
3249
3531
|
this.pauseWakeState = "paused";
|
|
3250
3532
|
this.autoPauseController.markPaused();
|
|
3251
|
-
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now());
|
|
3533
|
+
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
|
|
3252
3534
|
this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
|
|
3253
3535
|
this.ipcServer?.broadcast({
|
|
3254
3536
|
type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
|
|
@@ -3277,6 +3559,7 @@ export class Daemon extends EventEmitter {
|
|
|
3277
3559
|
// An explicit wake (e.g. after the user re-logs in) cancels a deferred
|
|
3278
3560
|
// auth pause — otherwise the instance would pause again the moment it idles.
|
|
3279
3561
|
this.pausePending = false;
|
|
3562
|
+
this.pauseReconfirmAuth = false;
|
|
3280
3563
|
this.authFailureUnresolved = false;
|
|
3281
3564
|
this.loginScreenReported = false;
|
|
3282
3565
|
this.fatalStartupReported = false;
|
|
@@ -3404,8 +3687,20 @@ export class Daemon extends EventEmitter {
|
|
|
3404
3687
|
// live, kept its warm slot, and re-raised a hang alert on every scan.
|
|
3405
3688
|
if (this.pausePending && (snapshot.state === "idle" || snapshot.state === "stuck") && this.pasteQueueDepth === 0) {
|
|
3406
3689
|
this.pausePending = false;
|
|
3690
|
+
const reconfirm = this.pauseReconfirmAuth;
|
|
3691
|
+
this.pauseReconfirmAuth = false;
|
|
3692
|
+
// An unreadable pane keeps the pause: when in doubt, stop feeding a CLI
|
|
3693
|
+
// that may be unable to answer (42512a46).
|
|
3694
|
+
if (reconfirm && pane !== undefined && !this.authErrorAtPaneBottom(pane)) {
|
|
3695
|
+
this.authFailureUnresolved = false;
|
|
3696
|
+
this.logger.warn({ state: snapshot.state }, "Deferred auth pause dropped: the auth-error text is no longer at the bottom of the pane at the end of the turn, "
|
|
3697
|
+
+ "and this backend has no auth check to confirm it (likely conversation text)");
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3407
3700
|
this.pauseAllowStuck = snapshot.state === "stuck";
|
|
3408
|
-
|
|
3701
|
+
const reason = this.pendingPauseReason;
|
|
3702
|
+
this.pendingPauseReason = "auth";
|
|
3703
|
+
this.emit("auto_pause_requested", { name: this.name, idleSince: snapshot.stateChangedAt, reason });
|
|
3409
3704
|
return;
|
|
3410
3705
|
}
|
|
3411
3706
|
// A restart deferred by a dead MCP server: the turn it must not interrupt is
|
|
@@ -3414,9 +3709,14 @@ export class Daemon extends EventEmitter {
|
|
|
3414
3709
|
this.fireMcpRestartRequest("idle_edge");
|
|
3415
3710
|
return;
|
|
3416
3711
|
}
|
|
3417
|
-
|
|
3712
|
+
// A work lease (Phase 2a) holds only this idle-timeout pause: a target the
|
|
3713
|
+
// fleet just woke for queued work, or one with a submission in flight, must
|
|
3714
|
+
// not be put back to sleep before that work reaches it. The auth-deferred
|
|
3715
|
+
// pause above and operator pauses never consult it.
|
|
3716
|
+
if (!this.pauseRequested && this.pasteQueueDepth === 0 && this.autoPauseController.observe(snapshot.state)
|
|
3717
|
+
&& !this.workLeaseActive()) {
|
|
3418
3718
|
this.pauseRequested = true;
|
|
3419
|
-
this.emit("auto_pause_requested", { name: this.name, idleSince: snapshot.stateChangedAt });
|
|
3719
|
+
this.emit("auto_pause_requested", { name: this.name, idleSince: snapshot.stateChangedAt, reason: "idle" });
|
|
3420
3720
|
}
|
|
3421
3721
|
}
|
|
3422
3722
|
/**
|
|
@@ -7121,6 +7421,8 @@ export class Daemon extends EventEmitter {
|
|
|
7121
7421
|
* into exactly the window this exists to close.
|
|
7122
7422
|
*/
|
|
7123
7423
|
beginSpawn() {
|
|
7424
|
+
// A relaunch settles any outstanding quit watch: the CLI came back.
|
|
7425
|
+
this.clearQuitRelaunchWatch();
|
|
7124
7426
|
// A restarted CLI has no trustworthy turn edge for the process it replaced.
|
|
7125
7427
|
// v1 deliberately does not persist obligations across restarts: missing one
|
|
7126
7428
|
// warning is safer than treating the replacement's startup idle as the old
|
|
@@ -7871,6 +8173,29 @@ export class Daemon extends EventEmitter {
|
|
|
7871
8173
|
this.firstDeliveryDelay.recordReady();
|
|
7872
8174
|
return ready;
|
|
7873
8175
|
}
|
|
8176
|
+
/**
|
|
8177
|
+
* Report the CLI's logged-out startup screen as an auth incident. Returns true
|
|
8178
|
+
* when this call reported it (the scan is then over: only re-login ends the
|
|
8179
|
+
* state); false when the screen is absent or was already reported this spawn.
|
|
8180
|
+
* `loginScreenActive`, when the backend flow has one, replaces the bare
|
|
8181
|
+
* pattern test so a transcript that merely mentions the screen cannot trip it.
|
|
8182
|
+
*/
|
|
8183
|
+
reportLoginScreen(pane) {
|
|
8184
|
+
const flow = LOGIN_FLOWS[this.config.backend ?? "claude-code"];
|
|
8185
|
+
const onScreen = flow?.loginScreenActive ? flow.loginScreenActive(pane) : flow?.loginScreenPattern?.test(pane) === true;
|
|
8186
|
+
if (!onScreen || this.loginScreenReported)
|
|
8187
|
+
return false;
|
|
8188
|
+
this.loginScreenReported = true;
|
|
8189
|
+
this.authFailureUnresolved = true;
|
|
8190
|
+
this.logger.warn({ backend: this.config.backend }, "CLI is waiting at its sign-in screen — reporting auth error");
|
|
8191
|
+
this.emit("pty_error", {
|
|
8192
|
+
name: this.name,
|
|
8193
|
+
type: "auth_error",
|
|
8194
|
+
action: "pause",
|
|
8195
|
+
message: "CLI is waiting at its sign-in screen — credentials are missing or expired",
|
|
8196
|
+
});
|
|
8197
|
+
return true;
|
|
8198
|
+
}
|
|
7874
8199
|
/**
|
|
7875
8200
|
* Poll the pane within a wall-clock budget, dismiss the confirmation dialogs
|
|
7876
8201
|
* the backend knows, and return true once the CLI shows a ready prompt with
|
|
@@ -8046,6 +8371,11 @@ export class Daemon extends EventEmitter {
|
|
|
8046
8371
|
}
|
|
8047
8372
|
if (this.dialogParkedSince !== 0 && !this.deliveryBlockingDialogs().some(d => Daemon.dialogMatches(d, pane)))
|
|
8048
8373
|
this.trackDialogParked(null);
|
|
8374
|
+
// A sign-in screen that carries the ready pattern's own glyph (Claude's
|
|
8375
|
+
// `❯ 1. …` login menu) must be recognised BEFORE the ready check, or the
|
|
8376
|
+
// ready check swallows it and the incident is never reported (#1074).
|
|
8377
|
+
if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenBeforeReady && this.reportLoginScreen(pane))
|
|
8378
|
+
return true;
|
|
8049
8379
|
// CLI is ready (pattern defined by each backend). Require it on two
|
|
8050
8380
|
// consecutive polls: the first ready frame is also the moment a late
|
|
8051
8381
|
// dialog is about to be painted over it.
|
|
@@ -8076,19 +8406,8 @@ export class Daemon extends EventEmitter {
|
|
|
8076
8406
|
// an auth error (the lifecycle double-checks with the token-free probe)
|
|
8077
8407
|
// instead of letting it decay into crash/hang/MCP-died noise, and stop
|
|
8078
8408
|
// the retry loop — only re-login (see /login) ends this state.
|
|
8079
|
-
|
|
8080
|
-
if (loginScreen?.test(pane) && !this.loginScreenReported) {
|
|
8081
|
-
this.loginScreenReported = true;
|
|
8082
|
-
this.authFailureUnresolved = true;
|
|
8083
|
-
this.logger.warn({ backend: this.config.backend }, "CLI is waiting at its sign-in screen — reporting auth error");
|
|
8084
|
-
this.emit("pty_error", {
|
|
8085
|
-
name: this.name,
|
|
8086
|
-
type: "auth_error",
|
|
8087
|
-
action: "pause",
|
|
8088
|
-
message: "CLI is waiting at its sign-in screen — credentials are missing or expired",
|
|
8089
|
-
});
|
|
8409
|
+
if (this.reportLoginScreen(pane))
|
|
8090
8410
|
return true;
|
|
8091
|
-
}
|
|
8092
8411
|
// Fatal: command not found (must match full phrase to avoid false positives
|
|
8093
8412
|
// like Kiro's "agent X not found, using default")
|
|
8094
8413
|
if (/command not found|: not found$/m.test(pane))
|