@songsid/agend 2.1.8 → 2.1.9
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/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/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/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/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +138 -48
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +11 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -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 +89 -3
- package/dist/daemon.js +301 -39
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +9 -1
- package/dist/delivery-outbox.js +25 -4
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +99 -5
- package/dist/fleet-manager.js +405 -65
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +160 -0
- package/dist/forged-envelope-scan.js +312 -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 +81 -6
- package/dist/instance-lifecycle.js +201 -17
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +14 -4
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.js +11 -4
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +18 -1
- package/dist/login-flows.js +9 -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 +3 -3
- 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/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/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/types.d.ts +26 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/ui/settings.html +2 -2
- package/dist/wake-coordinator.d.ts +143 -0
- package/dist/wake-coordinator.js +321 -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-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. */
|
|
@@ -1290,6 +1316,24 @@ export class Daemon extends EventEmitter {
|
|
|
1290
1316
|
this.lastErrorCount.set(key, count);
|
|
1291
1317
|
this.seededErrorCountBaselines.set(key, count);
|
|
1292
1318
|
}
|
|
1319
|
+
/** Start buffering `pty_error` until releaseStartupIncidents(); see holdingStartupIncidents. */
|
|
1320
|
+
holdStartupIncidents() {
|
|
1321
|
+
this.holdingStartupIncidents = true;
|
|
1322
|
+
}
|
|
1323
|
+
/** Stop buffering and deliver, in order, every `pty_error` raised while the lifecycle was starting us. */
|
|
1324
|
+
releaseStartupIncidents() {
|
|
1325
|
+
this.holdingStartupIncidents = false;
|
|
1326
|
+
const held = this.heldStartupIncidents.splice(0);
|
|
1327
|
+
for (const args of held)
|
|
1328
|
+
super.emit("pty_error", ...args);
|
|
1329
|
+
}
|
|
1330
|
+
emit(event, ...args) {
|
|
1331
|
+
if (event === "pty_error" && this.holdingStartupIncidents) {
|
|
1332
|
+
this.heldStartupIncidents.push(args);
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
return super.emit(event, ...args);
|
|
1336
|
+
}
|
|
1293
1337
|
/**
|
|
1294
1338
|
* Undo the auth suspicion a pattern match armed, once the backend's token-free
|
|
1295
1339
|
* probe has said the credentials are fine.
|
|
@@ -1446,6 +1490,11 @@ export class Daemon extends EventEmitter {
|
|
|
1446
1490
|
finishDurableDelivery(delivery, outcome, evidence) {
|
|
1447
1491
|
if (!delivery)
|
|
1448
1492
|
return;
|
|
1493
|
+
// Delivered or uncertain means the pane write started: the work reached
|
|
1494
|
+
// the CLI (or may have), so it is activity (Phase 2a). Only a failure is
|
|
1495
|
+
// not — a pre-write failure never gets here; it is retried before begin.
|
|
1496
|
+
if (outcome !== "failed")
|
|
1497
|
+
this.noteWorkActivity();
|
|
1449
1498
|
this.deliveryOutbox?.complete(delivery.deliveryId, this.bootId, delivery.attemptNo, outcome, evidence);
|
|
1450
1499
|
}
|
|
1451
1500
|
finishDurableSubmission(delivery, verdict) {
|
|
@@ -1464,6 +1513,56 @@ export class Daemon extends EventEmitter {
|
|
|
1464
1513
|
return;
|
|
1465
1514
|
this.retryDurableDeliveryBeforeBegin(delivery, reason);
|
|
1466
1515
|
}
|
|
1516
|
+
/** Restart the idle clock for work that reached the CLI, and persist it like a user inbound. */
|
|
1517
|
+
noteWorkActivity(now = Date.now()) {
|
|
1518
|
+
this.autoPauseController.recordActivity(now);
|
|
1519
|
+
try {
|
|
1520
|
+
writeLastInboundAt(this.instanceDir, now);
|
|
1521
|
+
}
|
|
1522
|
+
catch (err) {
|
|
1523
|
+
this.logger.warn({ err: err.message }, "Failed to persist last activity timestamp");
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* A daemon started to wake, restart or explicitly start an instance begins
|
|
1528
|
+
* its idle window now (Phase 2a). Only the fleet's own boot keeps the
|
|
1529
|
+
* persisted last-inbound seed, so a long-idle fleet still pauses after a
|
|
1530
|
+
* restart; a woken instance used to inherit a week-old timestamp and pause
|
|
1531
|
+
* one second after it became ready.
|
|
1532
|
+
*/
|
|
1533
|
+
seedActivityNow(now = Date.now()) {
|
|
1534
|
+
this.autoPauseController.recordActivity(now);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Phase 2b (design §1.4): why this awake instance is not taking input, in
|
|
1538
|
+
* words for a notice, or null when nothing known holds it. Read from the
|
|
1539
|
+
* daemon's own state only; it never inspects the pane itself.
|
|
1540
|
+
*/
|
|
1541
|
+
notAcceptingReason() {
|
|
1542
|
+
if (this.pauseWakeState !== "active")
|
|
1543
|
+
return `it is ${this.pauseWakeState}`;
|
|
1544
|
+
if (this.inputBlockedDialogKey !== null)
|
|
1545
|
+
return `a dialog is holding its input (${this.inputBlockedDialogKey})`;
|
|
1546
|
+
if (this.authFailureUnresolved)
|
|
1547
|
+
return "its login is failing";
|
|
1548
|
+
if (this.instanceState === "stuck")
|
|
1549
|
+
return "the CLI looks stuck";
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
workLeaseCheck = null;
|
|
1553
|
+
/** Lifecycle-provided: whether the fleet holds a work lease on this instance. */
|
|
1554
|
+
setWorkLeaseCheck(check) {
|
|
1555
|
+
this.workLeaseCheck = check ?? null;
|
|
1556
|
+
}
|
|
1557
|
+
workLeaseActive() {
|
|
1558
|
+
try {
|
|
1559
|
+
return this.workLeaseCheck?.() === true;
|
|
1560
|
+
}
|
|
1561
|
+
catch (err) {
|
|
1562
|
+
this.logger.debug({ err }, "Work lease check failed; treating as no lease");
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1467
1566
|
async start() {
|
|
1468
1567
|
mkdirSync(this.instanceDir, { recursive: true });
|
|
1469
1568
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
@@ -2468,6 +2567,13 @@ export class Daemon extends EventEmitter {
|
|
|
2468
2567
|
return;
|
|
2469
2568
|
const pane = await this.tmux.capturePane();
|
|
2470
2569
|
const inputBlockedDialog = this.updateInputBlockedState(pane, dialogs);
|
|
2570
|
+
// A sign-in screen that appears AFTER the startup scan ended (first run:
|
|
2571
|
+
// theme → login can take longer than its budget) is the same auth
|
|
2572
|
+
// incident the scan reports. Structural predicate only — the loose
|
|
2573
|
+
// patterns are for a fresh startup pane, not a long-lived transcript.
|
|
2574
|
+
const loginFlow = LOGIN_FLOWS[this.config.backend ?? "claude-code"];
|
|
2575
|
+
if (loginFlow?.loginScreenActive && this.reportLoginScreen(pane))
|
|
2576
|
+
return;
|
|
2471
2577
|
const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
|
|
2472
2578
|
if (interactivePrompt) {
|
|
2473
2579
|
this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
|
|
@@ -2995,33 +3101,128 @@ export class Daemon extends EventEmitter {
|
|
|
2995
3101
|
this.logger.warn(record, `${cliLabel} exited on its own (exit code ${exitCode ?? "unknown"}) — last output recorded`);
|
|
2996
3102
|
}
|
|
2997
3103
|
}
|
|
2998
|
-
/**
|
|
2999
|
-
|
|
3104
|
+
/**
|
|
3105
|
+
* Send the backend-specific graceful quit command/key sequence.
|
|
3106
|
+
*
|
|
3107
|
+
* Observability (#1030): every send is logged with reason + caller, the
|
|
3108
|
+
* outcome is logged, and a non-stop quit arms a bounded relaunch watch —
|
|
3109
|
+
* sol's silent wedge (quit landed, codex exited to its resume screen, no
|
|
3110
|
+
* relaunch, daemon still running) was invisible precisely because none of
|
|
3111
|
+
* this existed.
|
|
3112
|
+
*/
|
|
3113
|
+
async sendQuitSequence(reason = "unspecified", caller = "unknown", opts) {
|
|
3000
3114
|
if (!this.tmux || !this.backend)
|
|
3001
3115
|
return false;
|
|
3116
|
+
const backend = this.backend.binaryName;
|
|
3002
3117
|
const quitCmd = this.backend.getQuitCommand();
|
|
3003
3118
|
const quitKey = this.backend.getQuitKey?.();
|
|
3119
|
+
const quitVia = quitCmd ? `quit command ${quitCmd}` : quitKey ? `quit key ${quitKey}` : "no quit sequence";
|
|
3120
|
+
this.logger.info({ backend, quit: quitCmd ?? quitKey ?? null, reason, caller }, `Sending backend quit sequence (${reason} via ${caller})`);
|
|
3004
3121
|
if (quitCmd || quitKey)
|
|
3005
|
-
this.noteStopRequest(
|
|
3122
|
+
this.noteStopRequest(quitVia, reason);
|
|
3123
|
+
let sent = false;
|
|
3124
|
+
let step = "sendKeys";
|
|
3006
3125
|
if (quitCmd) {
|
|
3007
|
-
if (
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3126
|
+
if (await this.tmux.sendKeys(quitCmd)) {
|
|
3127
|
+
// Delay before Enter to prevent tmux server races when instances stop in
|
|
3128
|
+
// parallel (same pattern as pasteText).
|
|
3129
|
+
await new Promise(r => setTimeout(r, 150));
|
|
3130
|
+
step = "enter";
|
|
3131
|
+
sent = await this.tmux.sendSpecialKey("Enter");
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
else if (quitKey) {
|
|
3135
|
+
step = "quit key";
|
|
3136
|
+
sent = true;
|
|
3137
|
+
const presses = Math.max(1, Math.floor(this.backend.getQuitKeyPresses?.() ?? 1));
|
|
3138
|
+
for (let i = 0; i < presses; i++) {
|
|
3139
|
+
if (!await this.tmux.sendSpecialKey(quitKey)) {
|
|
3140
|
+
sent = false;
|
|
3141
|
+
break;
|
|
3142
|
+
}
|
|
3143
|
+
if (i + 1 < presses)
|
|
3144
|
+
await new Promise(r => setTimeout(r, 250));
|
|
3145
|
+
}
|
|
3013
3146
|
}
|
|
3014
|
-
|
|
3147
|
+
else {
|
|
3148
|
+
this.logger.warn({ backend, reason, caller }, "Backend has no quit command or key — nothing sent");
|
|
3015
3149
|
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
3150
|
}
|
|
3024
|
-
|
|
3151
|
+
if (sent) {
|
|
3152
|
+
this.logger.info({ backend, quit: quitCmd ?? quitKey, reason, caller }, "Backend quit sequence sent");
|
|
3153
|
+
if (opts?.watchRelaunch)
|
|
3154
|
+
this.armQuitRelaunchWatch(reason, caller, opts.watchMs);
|
|
3155
|
+
}
|
|
3156
|
+
else {
|
|
3157
|
+
this.logger.warn({ backend, quit: quitCmd ?? quitKey, reason, caller, step }, "Backend quit sequence failed to send");
|
|
3158
|
+
}
|
|
3159
|
+
return sent;
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Codex names its own resume command on its exit screen — the wedge
|
|
3163
|
+
* signature. Backend-generic shell, codex-specific match (#1030 ask 4):
|
|
3164
|
+
* other backends' panes simply never match.
|
|
3165
|
+
*/
|
|
3166
|
+
isCodexResumeScreen(pane) {
|
|
3167
|
+
return /codex\s+resume\b/i.test(pane);
|
|
3168
|
+
}
|
|
3169
|
+
clearQuitRelaunchWatch() {
|
|
3170
|
+
if (this.quitWatchTimer) {
|
|
3171
|
+
clearTimeout(this.quitWatchTimer);
|
|
3172
|
+
this.quitWatchTimer = null;
|
|
3173
|
+
}
|
|
3174
|
+
// Invalidates any already-started checkQuitRelaunch whose pane reads are
|
|
3175
|
+
// still pending: it must bail on return, not warn from stale results.
|
|
3176
|
+
this.quitWatchToken = null;
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* After a non-stop quit, warn if the backend exited but nothing relaunched
|
|
3180
|
+
* within the bound while this daemon still considers itself active.
|
|
3181
|
+
* Skipped when paused/pausing/waking/stopped or a relaunch (new spawn
|
|
3182
|
+
* generation) already happened — only the wedged state warns.
|
|
3183
|
+
*/
|
|
3184
|
+
armQuitRelaunchWatch(reason, caller, delayMs = QUIT_RELAUNCH_WATCH_MS) {
|
|
3185
|
+
this.clearQuitRelaunchWatch();
|
|
3186
|
+
const generation = this.spawnGeneration;
|
|
3187
|
+
const quitAt = Date.now();
|
|
3188
|
+
const token = {};
|
|
3189
|
+
this.quitWatchToken = token;
|
|
3190
|
+
this.quitWatchTimer = setTimeout(() => {
|
|
3191
|
+
this.quitWatchTimer = null;
|
|
3192
|
+
void this.checkQuitRelaunch({ reason, caller, generation, quitAt, token }).catch(() => { });
|
|
3193
|
+
}, delayMs);
|
|
3194
|
+
}
|
|
3195
|
+
async checkQuitRelaunch(opts) {
|
|
3196
|
+
if (this.pauseWakeState !== "active")
|
|
3197
|
+
return;
|
|
3198
|
+
if (opts.generation !== this.spawnGeneration)
|
|
3199
|
+
return;
|
|
3200
|
+
if (!this.tmux)
|
|
3201
|
+
return;
|
|
3202
|
+
// TOCTOU guard: the token is captured at arm time, so a beginSpawn/stop
|
|
3203
|
+
// that lands while the pane reads below are pending invalidates this
|
|
3204
|
+
// check. Re-validate after every await before using the results.
|
|
3205
|
+
const token = opts.token ?? this.quitWatchToken;
|
|
3206
|
+
let pane;
|
|
3207
|
+
let alive = true;
|
|
3208
|
+
try {
|
|
3209
|
+
const status = await this.tmux.getPaneStatus();
|
|
3210
|
+
alive = !!status?.alive;
|
|
3211
|
+
pane = await this.tmux.capturePane().catch(() => undefined);
|
|
3212
|
+
}
|
|
3213
|
+
catch {
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
if (this.pauseWakeState !== "active")
|
|
3217
|
+
return;
|
|
3218
|
+
if (opts.generation !== this.spawnGeneration)
|
|
3219
|
+
return;
|
|
3220
|
+
if (token !== this.quitWatchToken)
|
|
3221
|
+
return;
|
|
3222
|
+
const resumeScreen = !!pane && this.isCodexResumeScreen(pane);
|
|
3223
|
+
if (alive && !resumeScreen)
|
|
3224
|
+
return;
|
|
3225
|
+
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
3226
|
}
|
|
3026
3227
|
/**
|
|
3027
3228
|
* Kiro drops `/quit` while a turn is running. Stop that turn first, then use
|
|
@@ -3045,6 +3246,7 @@ export class Daemon extends EventEmitter {
|
|
|
3045
3246
|
}
|
|
3046
3247
|
async stop() {
|
|
3047
3248
|
this.logger.info("Stopping daemon instance");
|
|
3249
|
+
this.clearQuitRelaunchWatch();
|
|
3048
3250
|
this.fenceDeliveryWritesForStop();
|
|
3049
3251
|
this.turnReplyGuard.reset();
|
|
3050
3252
|
// Invalidate any bounded pre-Enter wait from the process generation being
|
|
@@ -3071,7 +3273,7 @@ export class Daemon extends EventEmitter {
|
|
|
3071
3273
|
let killed = false;
|
|
3072
3274
|
const windowId = this.tmux.getWindowId();
|
|
3073
3275
|
const kiroReady = windowId ? await this.drainBusyKiroForStop(windowId) : true;
|
|
3074
|
-
const quitSent = kiroReady && await this.sendQuitSequence("graceful stop");
|
|
3276
|
+
const quitSent = kiroReady && await this.sendQuitSequence("graceful stop", "stop");
|
|
3075
3277
|
const quitGraceMs = this.backend?.binaryName === "kiro-cli"
|
|
3076
3278
|
? KIRO_STOP_QUIT_GRACE_MS : 3_000;
|
|
3077
3279
|
const sigtermGraceMs = this.backend?.binaryName === "kiro-cli"
|
|
@@ -3190,12 +3392,35 @@ export class Daemon extends EventEmitter {
|
|
|
3190
3392
|
* failures: pause() alone no-ops while the CLI is busy/stuck, which is the
|
|
3191
3393
|
* usual state when the error surfaces. Cleared by a successful pause or wake.
|
|
3192
3394
|
*/
|
|
3193
|
-
requestPauseWhenIdle() {
|
|
3395
|
+
requestPauseWhenIdle(opts = {}) {
|
|
3194
3396
|
if (this.pauseWakeState === "paused")
|
|
3195
3397
|
return;
|
|
3398
|
+
const reconfirm = opts.reconfirmAuth === true;
|
|
3399
|
+
// An auth request wins over a pending non-auth one: the marker must not
|
|
3400
|
+
// later read as auto-wakeable when a login failure is among the causes.
|
|
3401
|
+
const reason = opts.reason ?? "auth";
|
|
3402
|
+
this.pendingPauseReason = this.pausePending && this.pendingPauseReason === "auth" ? "auth" : reason;
|
|
3403
|
+
// A pause already pending unconditionally stays unconditional.
|
|
3404
|
+
this.pauseReconfirmAuth = this.pausePending ? this.pauseReconfirmAuth && reconfirm : reconfirm;
|
|
3196
3405
|
this.pausePending = true;
|
|
3197
|
-
|
|
3198
|
-
|
|
3406
|
+
// A conditional request is the only pause path it has (#1044): decide on
|
|
3407
|
+
// the current pane now instead of at the next state sweep, so a pane that
|
|
3408
|
+
// is already idle is not left open to a delivery in the meantime.
|
|
3409
|
+
if (reconfirm)
|
|
3410
|
+
void this.captureAndEvaluateInstanceState("deferred_pause");
|
|
3411
|
+
}
|
|
3412
|
+
/** The backend's own auth-error pattern, within the pane's last rows. */
|
|
3413
|
+
authErrorAtPaneBottom(pane) {
|
|
3414
|
+
const rows = pane.replace(/\r/g, "").split("\n").filter(r => r.trim() !== "");
|
|
3415
|
+
const bottom = rows.slice(-Daemon.AUTH_RECONFIRM_ROWS).join("\n");
|
|
3416
|
+
return (this.backend?.getErrorPatterns?.() ?? [])
|
|
3417
|
+
.filter(ep => ep.type === "auth_error")
|
|
3418
|
+
.some(ep => new RegExp(ep.pattern.source, ep.pattern.flags.replace("g", "")).test(bottom));
|
|
3419
|
+
}
|
|
3420
|
+
static AUTH_RECONFIRM_ROWS = 15;
|
|
3421
|
+
/** Why the deferred pause was requested; what its marker records (Phase 2a). */
|
|
3422
|
+
pendingPauseReason = "auth";
|
|
3423
|
+
async pause(reason = "operator") {
|
|
3199
3424
|
if (this.pauseWakeState === "paused")
|
|
3200
3425
|
return;
|
|
3201
3426
|
if (this.pauseWakeState === "pausing" || this.pauseWakeState === "waking") {
|
|
@@ -3223,7 +3448,7 @@ export class Daemon extends EventEmitter {
|
|
|
3223
3448
|
const transition = (async () => {
|
|
3224
3449
|
try {
|
|
3225
3450
|
this.saveSessionId();
|
|
3226
|
-
await this.sendQuitSequence(pauseReason);
|
|
3451
|
+
await this.sendQuitSequence(pauseReason, "pause", { watchRelaunch: true });
|
|
3227
3452
|
let exited = false;
|
|
3228
3453
|
for (let i = 0; i < 15; i++) {
|
|
3229
3454
|
await new Promise(r => setTimeout(r, 200));
|
|
@@ -3248,7 +3473,7 @@ export class Daemon extends EventEmitter {
|
|
|
3248
3473
|
}
|
|
3249
3474
|
this.pauseWakeState = "paused";
|
|
3250
3475
|
this.autoPauseController.markPaused();
|
|
3251
|
-
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now());
|
|
3476
|
+
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
|
|
3252
3477
|
this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
|
|
3253
3478
|
this.ipcServer?.broadcast({
|
|
3254
3479
|
type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
|
|
@@ -3277,6 +3502,7 @@ export class Daemon extends EventEmitter {
|
|
|
3277
3502
|
// An explicit wake (e.g. after the user re-logs in) cancels a deferred
|
|
3278
3503
|
// auth pause — otherwise the instance would pause again the moment it idles.
|
|
3279
3504
|
this.pausePending = false;
|
|
3505
|
+
this.pauseReconfirmAuth = false;
|
|
3280
3506
|
this.authFailureUnresolved = false;
|
|
3281
3507
|
this.loginScreenReported = false;
|
|
3282
3508
|
this.fatalStartupReported = false;
|
|
@@ -3404,8 +3630,20 @@ export class Daemon extends EventEmitter {
|
|
|
3404
3630
|
// live, kept its warm slot, and re-raised a hang alert on every scan.
|
|
3405
3631
|
if (this.pausePending && (snapshot.state === "idle" || snapshot.state === "stuck") && this.pasteQueueDepth === 0) {
|
|
3406
3632
|
this.pausePending = false;
|
|
3633
|
+
const reconfirm = this.pauseReconfirmAuth;
|
|
3634
|
+
this.pauseReconfirmAuth = false;
|
|
3635
|
+
// An unreadable pane keeps the pause: when in doubt, stop feeding a CLI
|
|
3636
|
+
// that may be unable to answer (42512a46).
|
|
3637
|
+
if (reconfirm && pane !== undefined && !this.authErrorAtPaneBottom(pane)) {
|
|
3638
|
+
this.authFailureUnresolved = false;
|
|
3639
|
+
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, "
|
|
3640
|
+
+ "and this backend has no auth check to confirm it (likely conversation text)");
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3407
3643
|
this.pauseAllowStuck = snapshot.state === "stuck";
|
|
3408
|
-
|
|
3644
|
+
const reason = this.pendingPauseReason;
|
|
3645
|
+
this.pendingPauseReason = "auth";
|
|
3646
|
+
this.emit("auto_pause_requested", { name: this.name, idleSince: snapshot.stateChangedAt, reason });
|
|
3409
3647
|
return;
|
|
3410
3648
|
}
|
|
3411
3649
|
// A restart deferred by a dead MCP server: the turn it must not interrupt is
|
|
@@ -3414,9 +3652,14 @@ export class Daemon extends EventEmitter {
|
|
|
3414
3652
|
this.fireMcpRestartRequest("idle_edge");
|
|
3415
3653
|
return;
|
|
3416
3654
|
}
|
|
3417
|
-
|
|
3655
|
+
// A work lease (Phase 2a) holds only this idle-timeout pause: a target the
|
|
3656
|
+
// fleet just woke for queued work, or one with a submission in flight, must
|
|
3657
|
+
// not be put back to sleep before that work reaches it. The auth-deferred
|
|
3658
|
+
// pause above and operator pauses never consult it.
|
|
3659
|
+
if (!this.pauseRequested && this.pasteQueueDepth === 0 && this.autoPauseController.observe(snapshot.state)
|
|
3660
|
+
&& !this.workLeaseActive()) {
|
|
3418
3661
|
this.pauseRequested = true;
|
|
3419
|
-
this.emit("auto_pause_requested", { name: this.name, idleSince: snapshot.stateChangedAt });
|
|
3662
|
+
this.emit("auto_pause_requested", { name: this.name, idleSince: snapshot.stateChangedAt, reason: "idle" });
|
|
3420
3663
|
}
|
|
3421
3664
|
}
|
|
3422
3665
|
/**
|
|
@@ -7121,6 +7364,8 @@ export class Daemon extends EventEmitter {
|
|
|
7121
7364
|
* into exactly the window this exists to close.
|
|
7122
7365
|
*/
|
|
7123
7366
|
beginSpawn() {
|
|
7367
|
+
// A relaunch settles any outstanding quit watch: the CLI came back.
|
|
7368
|
+
this.clearQuitRelaunchWatch();
|
|
7124
7369
|
// A restarted CLI has no trustworthy turn edge for the process it replaced.
|
|
7125
7370
|
// v1 deliberately does not persist obligations across restarts: missing one
|
|
7126
7371
|
// warning is safer than treating the replacement's startup idle as the old
|
|
@@ -7871,6 +8116,29 @@ export class Daemon extends EventEmitter {
|
|
|
7871
8116
|
this.firstDeliveryDelay.recordReady();
|
|
7872
8117
|
return ready;
|
|
7873
8118
|
}
|
|
8119
|
+
/**
|
|
8120
|
+
* Report the CLI's logged-out startup screen as an auth incident. Returns true
|
|
8121
|
+
* when this call reported it (the scan is then over: only re-login ends the
|
|
8122
|
+
* state); false when the screen is absent or was already reported this spawn.
|
|
8123
|
+
* `loginScreenActive`, when the backend flow has one, replaces the bare
|
|
8124
|
+
* pattern test so a transcript that merely mentions the screen cannot trip it.
|
|
8125
|
+
*/
|
|
8126
|
+
reportLoginScreen(pane) {
|
|
8127
|
+
const flow = LOGIN_FLOWS[this.config.backend ?? "claude-code"];
|
|
8128
|
+
const onScreen = flow?.loginScreenActive ? flow.loginScreenActive(pane) : flow?.loginScreenPattern?.test(pane) === true;
|
|
8129
|
+
if (!onScreen || this.loginScreenReported)
|
|
8130
|
+
return false;
|
|
8131
|
+
this.loginScreenReported = true;
|
|
8132
|
+
this.authFailureUnresolved = true;
|
|
8133
|
+
this.logger.warn({ backend: this.config.backend }, "CLI is waiting at its sign-in screen — reporting auth error");
|
|
8134
|
+
this.emit("pty_error", {
|
|
8135
|
+
name: this.name,
|
|
8136
|
+
type: "auth_error",
|
|
8137
|
+
action: "pause",
|
|
8138
|
+
message: "CLI is waiting at its sign-in screen — credentials are missing or expired",
|
|
8139
|
+
});
|
|
8140
|
+
return true;
|
|
8141
|
+
}
|
|
7874
8142
|
/**
|
|
7875
8143
|
* Poll the pane within a wall-clock budget, dismiss the confirmation dialogs
|
|
7876
8144
|
* the backend knows, and return true once the CLI shows a ready prompt with
|
|
@@ -8046,6 +8314,11 @@ export class Daemon extends EventEmitter {
|
|
|
8046
8314
|
}
|
|
8047
8315
|
if (this.dialogParkedSince !== 0 && !this.deliveryBlockingDialogs().some(d => Daemon.dialogMatches(d, pane)))
|
|
8048
8316
|
this.trackDialogParked(null);
|
|
8317
|
+
// A sign-in screen that carries the ready pattern's own glyph (Claude's
|
|
8318
|
+
// `❯ 1. …` login menu) must be recognised BEFORE the ready check, or the
|
|
8319
|
+
// ready check swallows it and the incident is never reported (#1074).
|
|
8320
|
+
if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenBeforeReady && this.reportLoginScreen(pane))
|
|
8321
|
+
return true;
|
|
8049
8322
|
// CLI is ready (pattern defined by each backend). Require it on two
|
|
8050
8323
|
// consecutive polls: the first ready frame is also the moment a late
|
|
8051
8324
|
// dialog is about to be painted over it.
|
|
@@ -8076,19 +8349,8 @@ export class Daemon extends EventEmitter {
|
|
|
8076
8349
|
// an auth error (the lifecycle double-checks with the token-free probe)
|
|
8077
8350
|
// instead of letting it decay into crash/hang/MCP-died noise, and stop
|
|
8078
8351
|
// 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
|
-
});
|
|
8352
|
+
if (this.reportLoginScreen(pane))
|
|
8090
8353
|
return true;
|
|
8091
|
-
}
|
|
8092
8354
|
// Fatal: command not found (must match full phrase to avoid false positives
|
|
8093
8355
|
// like Kiro's "agent X not found, using default")
|
|
8094
8356
|
if (/command not found|: not found$/m.test(pane))
|