@songsid/agend 2.1.8-beta.8 → 2.1.8-beta.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/grok.js +19 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/types.d.ts +1 -1
- package/dist/cli.js +37 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +26 -1
- package/dist/daemon.js +125 -20
- package/dist/daemon.js.map +1 -1
- package/dist/locale.js +4 -0
- 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 +3 -1
- package/dist/login-flows.js +5 -1
- package/dist/login-flows.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/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
|
/**
|
|
@@ -1155,6 +1160,8 @@ export class Daemon extends EventEmitter {
|
|
|
1155
1160
|
lastProgressBroadcastAt = 0;
|
|
1156
1161
|
// PTY error pattern monitoring
|
|
1157
1162
|
errorMonitorTimer = null;
|
|
1163
|
+
quitWatchTimer = null;
|
|
1164
|
+
quitWatchToken = null;
|
|
1158
1165
|
interactivePromptDetector = new InteractivePromptDetector();
|
|
1159
1166
|
blockingProcessDetector = new BlockingProcessDetector();
|
|
1160
1167
|
/** Same 5-min gate the error monitor uses, so a dead MCP server alerts once. */
|
|
@@ -3005,33 +3012,128 @@ export class Daemon extends EventEmitter {
|
|
|
3005
3012
|
this.logger.warn(record, `${cliLabel} exited on its own (exit code ${exitCode ?? "unknown"}) — last output recorded`);
|
|
3006
3013
|
}
|
|
3007
3014
|
}
|
|
3008
|
-
/**
|
|
3009
|
-
|
|
3015
|
+
/**
|
|
3016
|
+
* Send the backend-specific graceful quit command/key sequence.
|
|
3017
|
+
*
|
|
3018
|
+
* Observability (#1030): every send is logged with reason + caller, the
|
|
3019
|
+
* outcome is logged, and a non-stop quit arms a bounded relaunch watch —
|
|
3020
|
+
* sol's silent wedge (quit landed, codex exited to its resume screen, no
|
|
3021
|
+
* relaunch, daemon still running) was invisible precisely because none of
|
|
3022
|
+
* this existed.
|
|
3023
|
+
*/
|
|
3024
|
+
async sendQuitSequence(reason = "unspecified", caller = "unknown", opts) {
|
|
3010
3025
|
if (!this.tmux || !this.backend)
|
|
3011
3026
|
return false;
|
|
3027
|
+
const backend = this.backend.binaryName;
|
|
3012
3028
|
const quitCmd = this.backend.getQuitCommand();
|
|
3013
3029
|
const quitKey = this.backend.getQuitKey?.();
|
|
3030
|
+
const quitVia = quitCmd ? `quit command ${quitCmd}` : quitKey ? `quit key ${quitKey}` : "no quit sequence";
|
|
3031
|
+
this.logger.info({ backend, quit: quitCmd ?? quitKey ?? null, reason, caller }, `Sending backend quit sequence (${reason} via ${caller})`);
|
|
3014
3032
|
if (quitCmd || quitKey)
|
|
3015
|
-
this.noteStopRequest(
|
|
3033
|
+
this.noteStopRequest(quitVia, reason);
|
|
3034
|
+
let sent = false;
|
|
3035
|
+
let step = "sendKeys";
|
|
3016
3036
|
if (quitCmd) {
|
|
3017
|
-
if (
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3037
|
+
if (await this.tmux.sendKeys(quitCmd)) {
|
|
3038
|
+
// Delay before Enter to prevent tmux server races when instances stop in
|
|
3039
|
+
// parallel (same pattern as pasteText).
|
|
3040
|
+
await new Promise(r => setTimeout(r, 150));
|
|
3041
|
+
step = "enter";
|
|
3042
|
+
sent = await this.tmux.sendSpecialKey("Enter");
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
else if (quitKey) {
|
|
3046
|
+
step = "quit key";
|
|
3047
|
+
sent = true;
|
|
3048
|
+
const presses = Math.max(1, Math.floor(this.backend.getQuitKeyPresses?.() ?? 1));
|
|
3049
|
+
for (let i = 0; i < presses; i++) {
|
|
3050
|
+
if (!await this.tmux.sendSpecialKey(quitKey)) {
|
|
3051
|
+
sent = false;
|
|
3052
|
+
break;
|
|
3053
|
+
}
|
|
3054
|
+
if (i + 1 < presses)
|
|
3055
|
+
await new Promise(r => setTimeout(r, 250));
|
|
3056
|
+
}
|
|
3023
3057
|
}
|
|
3024
|
-
|
|
3058
|
+
else {
|
|
3059
|
+
this.logger.warn({ backend, reason, caller }, "Backend has no quit command or key — nothing sent");
|
|
3025
3060
|
return false;
|
|
3026
|
-
const presses = Math.max(1, Math.floor(this.backend.getQuitKeyPresses?.() ?? 1));
|
|
3027
|
-
for (let i = 0; i < presses; i++) {
|
|
3028
|
-
const sent = await this.tmux.sendSpecialKey(quitKey);
|
|
3029
|
-
if (!sent)
|
|
3030
|
-
return false;
|
|
3031
|
-
if (i + 1 < presses)
|
|
3032
|
-
await new Promise(r => setTimeout(r, 250));
|
|
3033
3061
|
}
|
|
3034
|
-
|
|
3062
|
+
if (sent) {
|
|
3063
|
+
this.logger.info({ backend, quit: quitCmd ?? quitKey, reason, caller }, "Backend quit sequence sent");
|
|
3064
|
+
if (opts?.watchRelaunch)
|
|
3065
|
+
this.armQuitRelaunchWatch(reason, caller, opts.watchMs);
|
|
3066
|
+
}
|
|
3067
|
+
else {
|
|
3068
|
+
this.logger.warn({ backend, quit: quitCmd ?? quitKey, reason, caller, step }, "Backend quit sequence failed to send");
|
|
3069
|
+
}
|
|
3070
|
+
return sent;
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Codex names its own resume command on its exit screen — the wedge
|
|
3074
|
+
* signature. Backend-generic shell, codex-specific match (#1030 ask 4):
|
|
3075
|
+
* other backends' panes simply never match.
|
|
3076
|
+
*/
|
|
3077
|
+
isCodexResumeScreen(pane) {
|
|
3078
|
+
return /codex\s+resume\b/i.test(pane);
|
|
3079
|
+
}
|
|
3080
|
+
clearQuitRelaunchWatch() {
|
|
3081
|
+
if (this.quitWatchTimer) {
|
|
3082
|
+
clearTimeout(this.quitWatchTimer);
|
|
3083
|
+
this.quitWatchTimer = null;
|
|
3084
|
+
}
|
|
3085
|
+
// Invalidates any already-started checkQuitRelaunch whose pane reads are
|
|
3086
|
+
// still pending: it must bail on return, not warn from stale results.
|
|
3087
|
+
this.quitWatchToken = null;
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* After a non-stop quit, warn if the backend exited but nothing relaunched
|
|
3091
|
+
* within the bound while this daemon still considers itself active.
|
|
3092
|
+
* Skipped when paused/pausing/waking/stopped or a relaunch (new spawn
|
|
3093
|
+
* generation) already happened — only the wedged state warns.
|
|
3094
|
+
*/
|
|
3095
|
+
armQuitRelaunchWatch(reason, caller, delayMs = QUIT_RELAUNCH_WATCH_MS) {
|
|
3096
|
+
this.clearQuitRelaunchWatch();
|
|
3097
|
+
const generation = this.spawnGeneration;
|
|
3098
|
+
const quitAt = Date.now();
|
|
3099
|
+
const token = {};
|
|
3100
|
+
this.quitWatchToken = token;
|
|
3101
|
+
this.quitWatchTimer = setTimeout(() => {
|
|
3102
|
+
this.quitWatchTimer = null;
|
|
3103
|
+
void this.checkQuitRelaunch({ reason, caller, generation, quitAt, token }).catch(() => { });
|
|
3104
|
+
}, delayMs);
|
|
3105
|
+
}
|
|
3106
|
+
async checkQuitRelaunch(opts) {
|
|
3107
|
+
if (this.pauseWakeState !== "active")
|
|
3108
|
+
return;
|
|
3109
|
+
if (opts.generation !== this.spawnGeneration)
|
|
3110
|
+
return;
|
|
3111
|
+
if (!this.tmux)
|
|
3112
|
+
return;
|
|
3113
|
+
// TOCTOU guard: the token is captured at arm time, so a beginSpawn/stop
|
|
3114
|
+
// that lands while the pane reads below are pending invalidates this
|
|
3115
|
+
// check. Re-validate after every await before using the results.
|
|
3116
|
+
const token = opts.token ?? this.quitWatchToken;
|
|
3117
|
+
let pane;
|
|
3118
|
+
let alive = true;
|
|
3119
|
+
try {
|
|
3120
|
+
const status = await this.tmux.getPaneStatus();
|
|
3121
|
+
alive = !!status?.alive;
|
|
3122
|
+
pane = await this.tmux.capturePane().catch(() => undefined);
|
|
3123
|
+
}
|
|
3124
|
+
catch {
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
if (this.pauseWakeState !== "active")
|
|
3128
|
+
return;
|
|
3129
|
+
if (opts.generation !== this.spawnGeneration)
|
|
3130
|
+
return;
|
|
3131
|
+
if (token !== this.quitWatchToken)
|
|
3132
|
+
return;
|
|
3133
|
+
const resumeScreen = !!pane && this.isCodexResumeScreen(pane);
|
|
3134
|
+
if (alive && !resumeScreen)
|
|
3135
|
+
return;
|
|
3136
|
+
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");
|
|
3035
3137
|
}
|
|
3036
3138
|
/**
|
|
3037
3139
|
* Kiro drops `/quit` while a turn is running. Stop that turn first, then use
|
|
@@ -3055,6 +3157,7 @@ export class Daemon extends EventEmitter {
|
|
|
3055
3157
|
}
|
|
3056
3158
|
async stop() {
|
|
3057
3159
|
this.logger.info("Stopping daemon instance");
|
|
3160
|
+
this.clearQuitRelaunchWatch();
|
|
3058
3161
|
this.fenceDeliveryWritesForStop();
|
|
3059
3162
|
this.turnReplyGuard.reset();
|
|
3060
3163
|
// Invalidate any bounded pre-Enter wait from the process generation being
|
|
@@ -3081,7 +3184,7 @@ export class Daemon extends EventEmitter {
|
|
|
3081
3184
|
let killed = false;
|
|
3082
3185
|
const windowId = this.tmux.getWindowId();
|
|
3083
3186
|
const kiroReady = windowId ? await this.drainBusyKiroForStop(windowId) : true;
|
|
3084
|
-
const quitSent = kiroReady && await this.sendQuitSequence("graceful stop");
|
|
3187
|
+
const quitSent = kiroReady && await this.sendQuitSequence("graceful stop", "stop");
|
|
3085
3188
|
const quitGraceMs = this.backend?.binaryName === "kiro-cli"
|
|
3086
3189
|
? KIRO_STOP_QUIT_GRACE_MS : 3_000;
|
|
3087
3190
|
const sigtermGraceMs = this.backend?.binaryName === "kiro-cli"
|
|
@@ -3250,7 +3353,7 @@ export class Daemon extends EventEmitter {
|
|
|
3250
3353
|
const transition = (async () => {
|
|
3251
3354
|
try {
|
|
3252
3355
|
this.saveSessionId();
|
|
3253
|
-
await this.sendQuitSequence(pauseReason);
|
|
3356
|
+
await this.sendQuitSequence(pauseReason, "pause", { watchRelaunch: true });
|
|
3254
3357
|
let exited = false;
|
|
3255
3358
|
for (let i = 0; i < 15; i++) {
|
|
3256
3359
|
await new Promise(r => setTimeout(r, 200));
|
|
@@ -7159,6 +7262,8 @@ export class Daemon extends EventEmitter {
|
|
|
7159
7262
|
* into exactly the window this exists to close.
|
|
7160
7263
|
*/
|
|
7161
7264
|
beginSpawn() {
|
|
7265
|
+
// A relaunch settles any outstanding quit watch: the CLI came back.
|
|
7266
|
+
this.clearQuitRelaunchWatch();
|
|
7162
7267
|
// A restarted CLI has no trustworthy turn edge for the process it replaced.
|
|
7163
7268
|
// v1 deliberately does not persist obligations across restarts: missing one
|
|
7164
7269
|
// warning is safer than treating the replacement's startup idle as the old
|