@songsid/agend 2.1.6-beta.13 → 2.1.6-beta.14
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 +0 -22
- package/dist/backend/codex.d.ts +12 -40
- package/dist/backend/codex.js +47 -326
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +1 -13
- package/dist/backend/types.js.map +1 -1
- package/dist/cli.js +1 -23
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +0 -6
- package/dist/daemon.js +27 -140
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +0 -2
- package/dist/fleet-manager.js +0 -37
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +0 -7
- package/dist/instance-lifecycle.js +2 -119
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +0 -4
- package/dist/locale.js.map +1 -1
- package/package.json +1 -1
- package/dist/backend/codex-session.d.ts +0 -45
- package/dist/backend/codex-session.js +0 -202
- package/dist/backend/codex-session.js.map +0 -1
package/dist/daemon.js
CHANGED
|
@@ -30,8 +30,6 @@ import { bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignat
|
|
|
30
30
|
import { TurnReplyGuard } from "./turn-reply-guard.js";
|
|
31
31
|
import { t } from "./locale.js";
|
|
32
32
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
33
|
-
import { CodexResumeConflictError, CodexResumeIdentityError, CodexResumeUnavailableError } from "./backend/codex-session.js";
|
|
34
|
-
import { codexResumeLockVisible } from "./backend/codex.js";
|
|
35
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
34
|
const __dirname = dirname(__filename);
|
|
37
35
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -950,8 +948,6 @@ export class Daemon extends EventEmitter {
|
|
|
950
948
|
instanceStateBusyPattern = null;
|
|
951
949
|
instanceStateMonitorActive = false;
|
|
952
950
|
sessionCheckpointWarningEmitted = false;
|
|
953
|
-
codexSessionIdentityHoldNotified = false;
|
|
954
|
-
recoveredLegacyCodexSessionId = null;
|
|
955
951
|
statePollInFlight = false;
|
|
956
952
|
// One generation-scoped source of truth for human-turn reply completion.
|
|
957
953
|
// It also supplies the older dead-MCP/malformed-call recovery paths, so those
|
|
@@ -1207,11 +1203,6 @@ export class Daemon extends EventEmitter {
|
|
|
1207
1203
|
this.autoPauseController = new AutoPauseController(Math.max(0, autoPauseMinutes) * 60_000, readLastInboundAt(instanceDir) ?? Date.now());
|
|
1208
1204
|
}
|
|
1209
1205
|
async start() {
|
|
1210
|
-
// Do this before touching daemon.pid or a pre-existing window. An unknown
|
|
1211
|
-
// rollout format with an owned marker must not become a fresh session.
|
|
1212
|
-
if (this.backend?.hasInvalidSessionIdentity?.(this.config.working_directory)) {
|
|
1213
|
-
throw new CodexResumeIdentityError();
|
|
1214
|
-
}
|
|
1215
1206
|
mkdirSync(this.instanceDir, { recursive: true });
|
|
1216
1207
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
1217
1208
|
this.logger.info(`Starting ${this.name}`);
|
|
@@ -1221,16 +1212,8 @@ export class Daemon extends EventEmitter {
|
|
|
1221
1212
|
if (existsSync(crashStatePath)) {
|
|
1222
1213
|
const state = JSON.parse(readFileSync(crashStatePath, "utf-8"));
|
|
1223
1214
|
if (state.resumeDisabled) {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
// owned Codex conversation. Try once, then hold with a visible
|
|
1227
|
-
// reason rather than cycling or choosing a new session.
|
|
1228
|
-
this.logger.warn("Previous crash loop detected — preserving explicit Codex resume");
|
|
1229
|
-
}
|
|
1230
|
-
else {
|
|
1231
|
-
this.skipResume = true;
|
|
1232
|
-
this.logger.warn("Previous crash loop detected — starting without resume");
|
|
1233
|
-
}
|
|
1215
|
+
this.skipResume = true;
|
|
1216
|
+
this.logger.warn("Previous crash loop detected — starting without resume");
|
|
1234
1217
|
}
|
|
1235
1218
|
unlinkSync(crashStatePath);
|
|
1236
1219
|
}
|
|
@@ -1358,12 +1341,7 @@ export class Daemon extends EventEmitter {
|
|
|
1358
1341
|
if (savedId) {
|
|
1359
1342
|
const oldTmux = new TmuxManager(this.tmuxSessionName, savedId);
|
|
1360
1343
|
if (await oldTmux.isWindowAlive()) {
|
|
1361
|
-
this.
|
|
1362
|
-
const recoveredPane = this.backend?.binaryName === "codex"
|
|
1363
|
-
? await oldTmux.capturePane().catch(() => undefined) : undefined;
|
|
1364
|
-
const recoveredId = this.checkpointLivePaneBeforeReplacement(recoveredPane);
|
|
1365
|
-
if (this.backend?.binaryName === "codex")
|
|
1366
|
-
this.recoveredLegacyCodexSessionId = recoveredId;
|
|
1344
|
+
this.saveSessionId();
|
|
1367
1345
|
await oldTmux.killWindow();
|
|
1368
1346
|
this.logger.info({ savedId }, "Killed old tmux window for fresh start");
|
|
1369
1347
|
}
|
|
@@ -1443,10 +1421,7 @@ export class Daemon extends EventEmitter {
|
|
|
1443
1421
|
this.guardian = new ContextGuardian(this.config.context_guardian, this.logger, statusFile);
|
|
1444
1422
|
this.guardian.startWatching();
|
|
1445
1423
|
this.guardian.on("status_update", () => {
|
|
1446
|
-
|
|
1447
|
-
// callback has no pane footer, so it cannot attest the current chat.
|
|
1448
|
-
if (this.backend?.binaryName !== "codex")
|
|
1449
|
-
this.saveSessionId();
|
|
1424
|
+
this.saveSessionId();
|
|
1450
1425
|
});
|
|
1451
1426
|
// Context rotation removed: all CLI backends have built-in auto-compact.
|
|
1452
1427
|
// Crash recovery (health check + respawn with snapshot) is retained below.
|
|
@@ -1990,8 +1965,7 @@ export class Daemon extends EventEmitter {
|
|
|
1990
1965
|
await this.tmux.killWindow();
|
|
1991
1966
|
}
|
|
1992
1967
|
// Detect claude-code background session conflict — recover without counting as crash
|
|
1993
|
-
if (
|
|
1994
|
-
&& (lastOutput.includes("background agent") || lastOutput.includes("Session is currently running"))) {
|
|
1968
|
+
if (lastOutput && (lastOutput.includes("background agent") || lastOutput.includes("Session is currently running"))) {
|
|
1995
1969
|
if (!this.backgroundSessionRecoveryAttempted) {
|
|
1996
1970
|
this.backgroundSessionRecoveryAttempted = true;
|
|
1997
1971
|
this.logger.warn("Detected lingering background agent session — starting fresh (no resume)");
|
|
@@ -2032,14 +2006,6 @@ export class Daemon extends EventEmitter {
|
|
|
2032
2006
|
// and skip resume so the next spawn starts fresh. (skipResume also stops
|
|
2033
2007
|
// saveSessionId below from resurrecting the id from statusline.json.)
|
|
2034
2008
|
if (this.paneSaysNoConversation(lastOutput)) {
|
|
2035
|
-
if (this.backend?.binaryName === "codex" && this.backend.hasSessionIdentity?.()) {
|
|
2036
|
-
// Codex may have changed its rollout format or emitted an error
|
|
2037
|
-
// that only resembles this generic pattern. A persisted UUID is
|
|
2038
|
-
// never discarded by a text heuristic; preserve and hold.
|
|
2039
|
-
this.healthCheckPaused = true;
|
|
2040
|
-
this.emitSupervisionEnded("Codex could not resume its owned session", "Inspect the preserved session ID/rollout and restart this instance manually.");
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
2009
|
this.setSessionAside("cli_reported_no_conversation");
|
|
2044
2010
|
this.skipResume = true;
|
|
2045
2011
|
}
|
|
@@ -2097,10 +2063,7 @@ export class Daemon extends EventEmitter {
|
|
|
2097
2063
|
this.logger.warn({ crashCount: this.crashCount, delay }, `${cliLabel} window died — respawning after backoff`);
|
|
2098
2064
|
await new Promise(r => setTimeout(r, delay));
|
|
2099
2065
|
try {
|
|
2100
|
-
|
|
2101
|
-
// Its last verified checkpoint stays as-is (or held if ambiguous).
|
|
2102
|
-
if (this.backend?.binaryName !== "codex")
|
|
2103
|
-
this.saveSessionId();
|
|
2066
|
+
this.saveSessionId();
|
|
2104
2067
|
this.transcriptMonitor?.resetOffset();
|
|
2105
2068
|
// Kill orphan MCP server from the crashed CLI session.
|
|
2106
2069
|
// MCP server writes its PID to channel.mcp.pid on startup.
|
|
@@ -2148,16 +2111,7 @@ export class Daemon extends EventEmitter {
|
|
|
2148
2111
|
this.stormWindow?.markRecovered(this.name);
|
|
2149
2112
|
}
|
|
2150
2113
|
catch (err) {
|
|
2151
|
-
if (err
|
|
2152
|
-
// A live owner is not a broken conversation. Stop automatic
|
|
2153
|
-
// respawns and preserve the explicit session for a human retry.
|
|
2154
|
-
this.healthCheckPaused = true;
|
|
2155
|
-
this.logger.warn({ ownerPid: err instanceof CodexResumeConflictError ? err.ownerPid : null }, "Codex resume cannot continue — supervision stopped");
|
|
2156
|
-
this.emitSupervisionEnded(err.message, err instanceof CodexResumeConflictError
|
|
2157
|
-
? "Close the other Codex owner, then restart this instance."
|
|
2158
|
-
: "Inspect the Codex pane and restart this instance after resolving the startup failure.");
|
|
2159
|
-
}
|
|
2160
|
-
else if (!this.handOffBackendUnreachableRespawn(err)) {
|
|
2114
|
+
if (!this.handOffBackendUnreachableRespawn(err)) {
|
|
2161
2115
|
this.logger.error({ err }, `Failed to respawn ${cliLabel} window`);
|
|
2162
2116
|
}
|
|
2163
2117
|
}
|
|
@@ -2706,9 +2660,7 @@ export class Daemon extends EventEmitter {
|
|
|
2706
2660
|
// Quit CLI FIRST — this kills MCP server child processes cleanly.
|
|
2707
2661
|
// IPC must stay open during quit so MCP servers receive the shutdown message.
|
|
2708
2662
|
if (this.tmux) {
|
|
2709
|
-
|
|
2710
|
-
? await this.tmux.capturePane().catch(() => undefined) : undefined;
|
|
2711
|
-
this.saveSessionId(codexPane);
|
|
2663
|
+
this.saveSessionId();
|
|
2712
2664
|
this.healthCheckPaused = true;
|
|
2713
2665
|
let killed = false;
|
|
2714
2666
|
const quitSent = await this.sendQuitSequence();
|
|
@@ -2851,9 +2803,7 @@ export class Daemon extends EventEmitter {
|
|
|
2851
2803
|
this.freezeRuntimeMonitors();
|
|
2852
2804
|
const transition = (async () => {
|
|
2853
2805
|
try {
|
|
2854
|
-
|
|
2855
|
-
? await this.tmux?.capturePane().catch(() => undefined) : undefined;
|
|
2856
|
-
this.saveSessionId(codexPane);
|
|
2806
|
+
this.saveSessionId();
|
|
2857
2807
|
await this.sendQuitSequence();
|
|
2858
2808
|
let exited = false;
|
|
2859
2809
|
for (let i = 0; i < 15; i++) {
|
|
@@ -2992,13 +2942,13 @@ export class Daemon extends EventEmitter {
|
|
|
2992
2942
|
return;
|
|
2993
2943
|
}
|
|
2994
2944
|
this.instanceState = snapshot.state;
|
|
2995
|
-
// OpenCode
|
|
2945
|
+
// OpenCode creates its session lazily on the first submitted message.
|
|
2996
2946
|
// Waiting until stop/pause to persist that id loses resume state when the
|
|
2997
2947
|
// fleet is SIGKILLed or the host reboots.
|
|
2998
2948
|
// Idle observations are safe checkpoints; the 60s safety sweep also gives
|
|
2999
2949
|
// us a bounded retry if session creation produced no visible state edge.
|
|
3000
|
-
if (snapshot.state === "idle" &&
|
|
3001
|
-
this.saveSessionId(
|
|
2950
|
+
if (snapshot.state === "idle" && this.backend?.binaryName === "opencode") {
|
|
2951
|
+
this.saveSessionId();
|
|
3002
2952
|
}
|
|
3003
2953
|
// Only a transition back to idle completes pending work. Repeated idle
|
|
3004
2954
|
// observations between enqueue and paste must not clear a newer inbound.
|
|
@@ -6353,25 +6303,11 @@ export class Daemon extends EventEmitter {
|
|
|
6353
6303
|
if (!this.backend) {
|
|
6354
6304
|
throw new Error("No backend configured — cannot spawn CLI window");
|
|
6355
6305
|
}
|
|
6356
|
-
const attemptedResume = !this.skipResume
|
|
6357
|
-
&& (this.backend.canResume?.(this.config.working_directory) ?? true);
|
|
6306
|
+
const attemptedResume = !this.skipResume;
|
|
6358
6307
|
// A resume launch may get a longer budget than a fresh one (kiro: the
|
|
6359
6308
|
// conversation must come back from the backend before anything paints).
|
|
6360
6309
|
const resumeBudget = attemptedResume ? this.startupBudgetFor(true) : undefined;
|
|
6361
6310
|
let alive = await this.trySpawn(false, resumeBudget);
|
|
6362
|
-
if (!alive && attemptedResume && this.backend.binaryName === "codex") {
|
|
6363
|
-
const status = await this.tmux?.getPaneStatus().catch(() => null);
|
|
6364
|
-
if (status?.exitCode === 75)
|
|
6365
|
-
throw new CodexResumeConflictError(null);
|
|
6366
|
-
const pane = await this.tmux?.capturePane().catch(() => "");
|
|
6367
|
-
if (pane?.includes("[agend:codex-session-held]"))
|
|
6368
|
-
throw new CodexResumeConflictError(null);
|
|
6369
|
-
if (pane && codexResumeLockVisible(pane))
|
|
6370
|
-
throw new CodexResumeConflictError(null);
|
|
6371
|
-
// A failed explicit resume is not evidence that the conversation is
|
|
6372
|
-
// unusable. Never enter the generic abandon/--fresh recovery path.
|
|
6373
|
-
throw new CodexResumeUnavailableError();
|
|
6374
|
-
}
|
|
6375
6311
|
if (!alive && attemptedResume) {
|
|
6376
6312
|
// Resume failed. Before abandoning the session:
|
|
6377
6313
|
// 1. If the backend is unreachable (fleet-level memory, or this very pane
|
|
@@ -6565,9 +6501,7 @@ export class Daemon extends EventEmitter {
|
|
|
6565
6501
|
this.logger.debug({ err }, "IPC server close failed during startup abort");
|
|
6566
6502
|
}
|
|
6567
6503
|
this.ipcServer = null;
|
|
6568
|
-
|
|
6569
|
-
// pre-existing window-id still belongs to the old pane and must survive.
|
|
6570
|
-
for (const file of this.tmux ? ["daemon.pid", "window-id"] : ["daemon.pid"]) {
|
|
6504
|
+
for (const file of ["daemon.pid", "window-id"]) {
|
|
6571
6505
|
try {
|
|
6572
6506
|
unlinkSync(join(this.instanceDir, file));
|
|
6573
6507
|
}
|
|
@@ -6594,21 +6528,13 @@ export class Daemon extends EventEmitter {
|
|
|
6594
6528
|
* Returns true if CLI is ready, false if it failed or got stuck.
|
|
6595
6529
|
*/
|
|
6596
6530
|
async trySpawn(reuseWindow = false, startupTimeoutMs) {
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
// All successful launches, including wake's trySpawn(true), cross this
|
|
6605
|
-
// point. The previous checkpoint in spawnClaudeWindow missed wake and
|
|
6606
|
-
// kept reading the dead pane PID after a resumed session changed.
|
|
6607
|
-
if (ready && !this.fatalStartupBlocked && this.backend?.binaryName === "codex" && this.tmux) {
|
|
6608
|
-
this.backend.setActivePanePid?.(await TmuxManager.getPanePid(this.tmuxSessionName, this.tmux.getWindowId()));
|
|
6609
|
-
this.saveSessionId(await this.tmux.capturePane().catch(() => undefined));
|
|
6610
|
-
}
|
|
6611
|
-
return ready;
|
|
6531
|
+
if (!this.spawnGate)
|
|
6532
|
+
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs);
|
|
6533
|
+
return this.spawnGate.run({
|
|
6534
|
+
instanceName: this.name,
|
|
6535
|
+
workingDirectory: this.config.working_directory,
|
|
6536
|
+
reason: reuseWindow ? "wake" : this.lastSpawnAt > 0 ? "recovery" : "startup",
|
|
6537
|
+
}, () => this.trySpawnInsideGate(reuseWindow, startupTimeoutMs));
|
|
6612
6538
|
}
|
|
6613
6539
|
/**
|
|
6614
6540
|
* One fresh pane capture judged as ready && !busy && !dialog. Transcript
|
|
@@ -6844,21 +6770,6 @@ export class Daemon extends EventEmitter {
|
|
|
6844
6770
|
}
|
|
6845
6771
|
this.backend.writeConfig(backendConfig);
|
|
6846
6772
|
this.backend.preTrust?.(this.config.working_directory);
|
|
6847
|
-
// Bounded preflight for an owner left by a restart or another app. Codex
|
|
6848
|
-
// must not be launched as a competing resumer. The command's flock is the
|
|
6849
|
-
// atomic second fence for a race after this observation.
|
|
6850
|
-
if (!backendConfig.skipResume && this.backend.resumeOwner) {
|
|
6851
|
-
let owner = null;
|
|
6852
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
6853
|
-
owner = this.backend.resumeOwner(this.config.working_directory);
|
|
6854
|
-
if (owner === null)
|
|
6855
|
-
break;
|
|
6856
|
-
if (attempt < 2)
|
|
6857
|
-
await new Promise(r => setTimeout(r, 1_000));
|
|
6858
|
-
}
|
|
6859
|
-
if (owner !== null)
|
|
6860
|
-
throw new CodexResumeConflictError(owner);
|
|
6861
|
-
}
|
|
6862
6773
|
// Resolve working directory (e.g. symlink for hidden paths)
|
|
6863
6774
|
const resolvedCwd = this.backend.resolveWorkingDirectory?.(this.config.working_directory, this.name) ?? this.config.working_directory;
|
|
6864
6775
|
// Generate a fresh per-instance agent token each spawn. agent-cli reads
|
|
@@ -7247,34 +7158,26 @@ export class Daemon extends EventEmitter {
|
|
|
7247
7158
|
}
|
|
7248
7159
|
catch { /* best effort */ }
|
|
7249
7160
|
}
|
|
7250
|
-
|
|
7251
|
-
saveSessionId(pane) {
|
|
7161
|
+
saveSessionId() {
|
|
7252
7162
|
// When a resume failure has forced a fresh start, don't persist the stale id
|
|
7253
7163
|
// back from statusline.json — that would re-arm --continue and re-loop.
|
|
7254
7164
|
if (this.skipResume)
|
|
7255
|
-
return
|
|
7165
|
+
return;
|
|
7256
7166
|
try {
|
|
7257
|
-
const sid = this.backend?.getSessionId(
|
|
7258
|
-
if (!sid)
|
|
7259
|
-
|
|
7260
|
-
this.codexSessionIdentityHoldNotified = true;
|
|
7261
|
-
this.emit("codex_session_identity_unconfirmed", { name: this.name });
|
|
7262
|
-
}
|
|
7263
|
-
return null;
|
|
7264
|
-
}
|
|
7265
|
-
this.codexSessionIdentityHoldNotified = false;
|
|
7167
|
+
const sid = this.backend?.getSessionId();
|
|
7168
|
+
if (!sid)
|
|
7169
|
+
return;
|
|
7266
7170
|
const path = join(this.instanceDir, "session-id");
|
|
7267
7171
|
// Idle observations happen after turns. Avoid needless writes (and mtime
|
|
7268
7172
|
// churn) while still updating the marker when /new changes the session.
|
|
7269
7173
|
try {
|
|
7270
7174
|
if (readFileSync(path, "utf-8").trim() === sid)
|
|
7271
|
-
return
|
|
7175
|
+
return;
|
|
7272
7176
|
}
|
|
7273
7177
|
catch { /* first checkpoint */ }
|
|
7274
7178
|
writeFileSync(path, sid);
|
|
7275
7179
|
this.sessionCheckpointWarningEmitted = false;
|
|
7276
7180
|
this.logger.debug("Session id checkpointed");
|
|
7277
|
-
return sid;
|
|
7278
7181
|
}
|
|
7279
7182
|
catch (err) {
|
|
7280
7183
|
// Session discovery is best-effort and must never break the pane state
|
|
@@ -7283,24 +7186,8 @@ export class Daemon extends EventEmitter {
|
|
|
7283
7186
|
this.sessionCheckpointWarningEmitted = true;
|
|
7284
7187
|
this.logger.warn({ err: err.message }, "Session id checkpoint failed");
|
|
7285
7188
|
}
|
|
7286
|
-
return null;
|
|
7287
7189
|
}
|
|
7288
7190
|
}
|
|
7289
|
-
checkpointLivePaneBeforeReplacement(pane) {
|
|
7290
|
-
// A pre-upgrade Codex pane can provide stronger, exact ownership
|
|
7291
|
-
// evidence than an old crash-loop heuristic. Discover it before
|
|
7292
|
-
// saveSessionId's skip-resume guard, then arm resume only if the
|
|
7293
|
-
// checkpoint was actually durable. Never fall back to --last.
|
|
7294
|
-
const exactCodexId = this.backend?.binaryName === "codex" ? this.backend.getSessionId(pane) : null;
|
|
7295
|
-
if (exactCodexId)
|
|
7296
|
-
this.skipResume = false;
|
|
7297
|
-
const recoveredId = this.saveSessionId(pane);
|
|
7298
|
-
if (exactCodexId && !recoveredId)
|
|
7299
|
-
this.skipResume = true;
|
|
7300
|
-
return recoveredId;
|
|
7301
|
-
}
|
|
7302
|
-
/** Only a live pre-upgrade pane can provide this positive migration proof. */
|
|
7303
|
-
getRecoveredLegacyCodexSessionId() { return this.recoveredLegacyCodexSessionId; }
|
|
7304
7191
|
readContextPercentage() {
|
|
7305
7192
|
return this.backend?.getContextUsage() ?? 0;
|
|
7306
7193
|
}
|