@songsid/agend 2.1.9 → 2.1.10-beta.2
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/antigravity.js +2 -1
- package/dist/backend/antigravity.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/kiro.d.ts +81 -5
- package/dist/backend/kiro.js +370 -36
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +10 -0
- package/dist/backend/types.js +13 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/telegram.js +4 -3
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/ipc-bridge.js +10 -3
- package/dist/channel/ipc-bridge.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.d.ts +1 -8
- package/dist/channel/mcp-server.js +10 -0
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/types.d.ts +7 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +93 -10
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +13 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +68 -0
- package/dist/daemon.js +211 -27
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.js +18 -23
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +59 -1
- package/dist/fleet-manager.js +208 -35
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.js +2 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
- package/dist/instance-lifecycle.d.ts +25 -5
- package/dist/instance-lifecycle.js +68 -33
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +174 -4
- package/dist/locale.js.map +1 -1
- package/dist/logger.js +19 -16
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.d.ts +38 -0
- package/dist/login-controller.js +227 -6
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +7 -0
- package/dist/login-flows.js +1 -0
- package/dist/login-flows.js.map +1 -1
- package/dist/muse-usage-relay.js +3 -2
- package/dist/muse-usage-relay.js.map +1 -1
- package/dist/node-warnings.d.ts +1 -0
- package/dist/node-warnings.js +26 -0
- package/dist/node-warnings.js.map +1 -0
- package/dist/outbound-handlers.js +14 -16
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/pause-marker.js +3 -2
- package/dist/pause-marker.js.map +1 -1
- package/dist/private-dir.d.ts +54 -0
- package/dist/private-dir.js +140 -0
- package/dist/private-dir.js.map +1 -0
- 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 +46 -0
- package/dist/service-installer.js +224 -4
- package/dist/service-installer.js.map +1 -1
- package/dist/setup-wizard.js +3 -1
- package/dist/setup-wizard.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 +24 -0
- 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 +0 -15
- package/dist/wake-coordinator.js +19 -3
- package/dist/wake-coordinator.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/package.json +1 -1
- package/templates/systemd.service.ejs +17 -1
package/dist/daemon.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, dirname, basename, resolve } from "node:path";
|
|
2
2
|
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { ensureInstanceDir } from "./private-dir.js";
|
|
4
5
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
6
|
import { EventEmitter } from "node:events";
|
|
6
7
|
import { rotateLogIfNeeded } from "./logger.js";
|
|
@@ -18,7 +19,7 @@ import { ContextGuardian } from "./context-guardian.js";
|
|
|
18
19
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
19
20
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
20
21
|
import { MessageBus } from "./channel/message-bus.js";
|
|
21
|
-
import { shellQuote } from "./backend/types.js";
|
|
22
|
+
import { shellQuote, UnsupportedCliError } from "./backend/types.js";
|
|
22
23
|
import { getTmuxSession } from "./config.js";
|
|
23
24
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
24
25
|
import { HangDetector } from "./hang-detector.js";
|
|
@@ -97,6 +98,13 @@ export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
|
|
|
97
98
|
export const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
98
99
|
/** Do not turn a streaming Codex turn into one capture-pane per burst. */
|
|
99
100
|
export const STRUCTURED_IDLE_PROBE_MS = 500;
|
|
101
|
+
/**
|
|
102
|
+
* An instance publishing more than this many execution-state edges inside the
|
|
103
|
+
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
104
|
+
* flapping muse wrote 89,532 of the fleet's 91,000 state-change lines.
|
|
105
|
+
*/
|
|
106
|
+
export const STATE_EDGE_LOG_BURST = 10;
|
|
107
|
+
export const STATE_EDGE_LOG_WINDOW_MS = 60_000;
|
|
100
108
|
/** How long an unrecognised-but-empty composer must stay unchanged before it counts as idle (#978). */
|
|
101
109
|
export const UNKNOWN_LAYOUT_STABLE_MS = 10_000;
|
|
102
110
|
/** A foreground server/port-forward should hand control back or be acknowledged. */
|
|
@@ -161,7 +169,7 @@ export function readLastInboundAt(instanceDir, now = Date.now()) {
|
|
|
161
169
|
}
|
|
162
170
|
/** Atomically persist the last real channel inbound timestamp. */
|
|
163
171
|
export function writeLastInboundAt(instanceDir, timestamp) {
|
|
164
|
-
|
|
172
|
+
ensureInstanceDir(instanceDir);
|
|
165
173
|
const target = join(instanceDir, LAST_INBOUND_FILE);
|
|
166
174
|
const temp = `${target}.${process.pid}.tmp`;
|
|
167
175
|
writeFileSync(temp, String(timestamp));
|
|
@@ -1020,6 +1028,15 @@ export class Daemon extends EventEmitter {
|
|
|
1020
1028
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
1021
1029
|
autoResolvedDialogKey = null;
|
|
1022
1030
|
autoResolvedDialogGeneration = 0;
|
|
1031
|
+
/**
|
|
1032
|
+
* (#1103) Proof that THIS spawn's Codex went onto Luna Reserve: AgEnD saw (and
|
|
1033
|
+
* answered) the usage-limit menu that codex shows when it switches itself to
|
|
1034
|
+
* the reserve. `count` is how many usage-limit lines the pane held at that
|
|
1035
|
+
* moment; only occurrences up to it are the text of the incident we already
|
|
1036
|
+
* handled. A later, larger count is a NEW hit (the reserve ran out too).
|
|
1037
|
+
* Keyed by spawn generation, so a respawn starts without any proof.
|
|
1038
|
+
*/
|
|
1039
|
+
codexReserveAck = null;
|
|
1023
1040
|
backgroundSessionRecoveryAttempted = false;
|
|
1024
1041
|
/** Whether the last spawn started a fresh session (not resumed). */
|
|
1025
1042
|
isNewSession = false;
|
|
@@ -1037,6 +1054,10 @@ export class Daemon extends EventEmitter {
|
|
|
1037
1054
|
/** Consecutive structural idle captures for a noisy, periodically-redrawing
|
|
1038
1055
|
* pane. Two are required before a working -> idle transition. */
|
|
1039
1056
|
instanceStatePeriodicIdleConfirmations = 0;
|
|
1057
|
+
/** Recent execution-state edge times, for the flap log limiter (#1116). */
|
|
1058
|
+
stateEdgeTimes = [];
|
|
1059
|
+
stateEdgesNotLogged = 0;
|
|
1060
|
+
stateEdgeSummaryAt = 0;
|
|
1040
1061
|
/** #978: last unrecognised-but-empty-composer capture and since when it has been unchanged. */
|
|
1041
1062
|
unknownLayoutPaneKey = null;
|
|
1042
1063
|
unknownLayoutStableSince = 0;
|
|
@@ -1178,6 +1199,17 @@ export class Daemon extends EventEmitter {
|
|
|
1178
1199
|
static MCP_DEATH_COOLDOWN_MS = 5 * 60_000;
|
|
1179
1200
|
lastMcpDeathNotifiedAt = 0;
|
|
1180
1201
|
mcpDeathNotifiedForPid = null;
|
|
1202
|
+
/**
|
|
1203
|
+
* How long after the CLI finished starting its fleet MCP server may take to
|
|
1204
|
+
* show up before "it never connected" is an incident. Servers connect within
|
|
1205
|
+
* seconds of the prompt (kiro loads MCP after it, claude/codex around it);
|
|
1206
|
+
* 90s leaves room for a slow npx/node cold start on a loaded host.
|
|
1207
|
+
*/
|
|
1208
|
+
static FLEET_MCP_STARTUP_GRACE_MS = 90_000;
|
|
1209
|
+
/** spawnGeneration in which a server for this instance was last seen serving. */
|
|
1210
|
+
mcpServedGeneration = -1;
|
|
1211
|
+
/** spawnGeneration whose "fleet MCP never connected" incident was reported. */
|
|
1212
|
+
mcpNeverConnectedReportedGeneration = -1;
|
|
1181
1213
|
/**
|
|
1182
1214
|
* Ceiling on how long a dead MCP server waits for an idle window before the
|
|
1183
1215
|
* auto-restart fires anyway. A toolless instance cannot reply or report, so
|
|
@@ -1264,6 +1296,27 @@ export class Daemon extends EventEmitter {
|
|
|
1264
1296
|
return false;
|
|
1265
1297
|
}
|
|
1266
1298
|
}
|
|
1299
|
+
/** The Codex "usage limit reached" pause pattern (the only one the reserve proof applies to). */
|
|
1300
|
+
static isCodexUsageLimitPause(ep) {
|
|
1301
|
+
return ep.type === "quota" && ep.action === "pause" && ep.message.startsWith("Codex usage limit reached");
|
|
1302
|
+
}
|
|
1303
|
+
static countPatternMatches(pattern, text) {
|
|
1304
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
|
|
1305
|
+
return (text.match(new RegExp(pattern.source, flags)) || []).length;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Record that the Luna Reserve menu is (or was just) on screen, for the
|
|
1309
|
+
* usage-limit decision in evaluateErrorPatterns. Called wherever a dialog is
|
|
1310
|
+
* matched, before any key is sent.
|
|
1311
|
+
*/
|
|
1312
|
+
noteCodexReserveDialog(dialog, pane) {
|
|
1313
|
+
if (dialog.autoResolutionKey !== "codex-usage-limit-luna-reserve" || this.backend?.binaryName !== "codex")
|
|
1314
|
+
return;
|
|
1315
|
+
const ep = this.backend.getErrorPatterns?.().find(candidate => Daemon.isCodexUsageLimitPause(candidate));
|
|
1316
|
+
const count = ep ? Daemon.countPatternMatches(ep.pattern, pane) : 0;
|
|
1317
|
+
this.codexReserveAck = { generation: this.spawnGeneration, count };
|
|
1318
|
+
this.logger.info({ backend: "codex", evidence: "reserve-menu-seen", usageLimitLines: count }, "codex usage-limit: the Luna Reserve menu was seen — usage-limit text up to this count is stale");
|
|
1319
|
+
}
|
|
1267
1320
|
/** Re-check the current pane after an async quota probe before pausing it. */
|
|
1268
1321
|
async isCodexLivePane() {
|
|
1269
1322
|
if (this.backend?.binaryName !== "codex" || !this.tmux)
|
|
@@ -1540,13 +1593,13 @@ export class Daemon extends EventEmitter {
|
|
|
1540
1593
|
*/
|
|
1541
1594
|
notAcceptingReason() {
|
|
1542
1595
|
if (this.pauseWakeState !== "active")
|
|
1543
|
-
return
|
|
1596
|
+
return t("delivery.hold_state", this.pauseWakeState);
|
|
1544
1597
|
if (this.inputBlockedDialogKey !== null)
|
|
1545
|
-
return
|
|
1598
|
+
return t("delivery.hold_dialog", this.inputBlockedDialogKey);
|
|
1546
1599
|
if (this.authFailureUnresolved)
|
|
1547
|
-
return "
|
|
1600
|
+
return t("delivery.hold_auth");
|
|
1548
1601
|
if (this.instanceState === "stuck")
|
|
1549
|
-
return "
|
|
1602
|
+
return t("delivery.hold_stuck");
|
|
1550
1603
|
return null;
|
|
1551
1604
|
}
|
|
1552
1605
|
workLeaseCheck = null;
|
|
@@ -1564,7 +1617,7 @@ export class Daemon extends EventEmitter {
|
|
|
1564
1617
|
}
|
|
1565
1618
|
}
|
|
1566
1619
|
async start() {
|
|
1567
|
-
|
|
1620
|
+
ensureInstanceDir(this.instanceDir);
|
|
1568
1621
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
1569
1622
|
this.logger.info(`Starting ${this.name}`);
|
|
1570
1623
|
// P1: Read crash state from previous run — skip resume if last run was a crash loop
|
|
@@ -1843,6 +1896,7 @@ export class Daemon extends EventEmitter {
|
|
|
1843
1896
|
return;
|
|
1844
1897
|
const status = mcpServerState(this.instanceDir);
|
|
1845
1898
|
if (status.state === "alive") {
|
|
1899
|
+
this.mcpServedGeneration = this.spawnGeneration;
|
|
1846
1900
|
if (this.mcpDeathNotifiedForPid != null)
|
|
1847
1901
|
this.emitMcpRecovered("pid", status.pid);
|
|
1848
1902
|
this.mcpDeathNotifiedForPid = null; // the CLI respawned it — re-arm
|
|
@@ -1870,8 +1924,13 @@ export class Daemon extends EventEmitter {
|
|
|
1870
1924
|
this.noteMcpProofOfLife("connection", livePid);
|
|
1871
1925
|
return;
|
|
1872
1926
|
}
|
|
1873
|
-
if (status.state === "unknown")
|
|
1874
|
-
|
|
1927
|
+
if (status.state === "unknown") {
|
|
1928
|
+
// Never started, or nothing left to watch. The CLI may legitimately not
|
|
1929
|
+
// have spawned it yet, but past the startup grace a CLI that never
|
|
1930
|
+
// brought up AgEnD's server is running without fleet tools.
|
|
1931
|
+
this.checkFleetMcpNeverConnected();
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1875
1934
|
// Dead: report once per pid, and at most once per cooldown window.
|
|
1876
1935
|
if (this.mcpDeathNotifiedForPid === status.pid)
|
|
1877
1936
|
return;
|
|
@@ -1906,6 +1965,42 @@ export class Daemon extends EventEmitter {
|
|
|
1906
1965
|
if (autoRestart)
|
|
1907
1966
|
this.armMcpRestartWhenIdle();
|
|
1908
1967
|
}
|
|
1968
|
+
/**
|
|
1969
|
+
* The CLI has been up past the startup grace and no server for this instance
|
|
1970
|
+
* has ever served it: AgEnD's fleet tools are missing, so the agent cannot
|
|
1971
|
+
* reply or reach other instances. Reported once per spawn, through the same
|
|
1972
|
+
* mcp_died path (auth check, notice, idle-gated revival restart under the
|
|
1973
|
+
* lifecycle's cooldown) as a server that died.
|
|
1974
|
+
*
|
|
1975
|
+
* This is the check that lets the CLI tolerate the user's OTHER servers
|
|
1976
|
+
* failing (#1111): kiro's --require-mcp-startup could not tell the fleet
|
|
1977
|
+
* server from a third-party one, and failed the whole launch for either.
|
|
1978
|
+
*/
|
|
1979
|
+
checkFleetMcpNeverConnected() {
|
|
1980
|
+
if (this.config.agent_mode === "cli")
|
|
1981
|
+
return; // no MCP server by design
|
|
1982
|
+
if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
|
|
1983
|
+
return;
|
|
1984
|
+
if (this.mcpNeverConnectedReportedGeneration === this.spawnGeneration)
|
|
1985
|
+
return;
|
|
1986
|
+
if (Date.now() - this.lastSpawnAt < Daemon.FLEET_MCP_STARTUP_GRACE_MS)
|
|
1987
|
+
return;
|
|
1988
|
+
if (this.liveMcpSockets().length > 0) {
|
|
1989
|
+
this.mcpServedGeneration = this.spawnGeneration;
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
this.mcpNeverConnectedReportedGeneration = this.spawnGeneration;
|
|
1993
|
+
// pid 0 stands for "no server ever had a pid": a late-connecting server
|
|
1994
|
+
// still finds an alarm to retract (noteMcpProofOfLife / the alive branch).
|
|
1995
|
+
this.mcpDeathNotifiedForPid = 0;
|
|
1996
|
+
const authSuspected = this.authFailureUnresolved;
|
|
1997
|
+
const autoRestart = this.config.mcp_auto_restart !== false && !authSuspected;
|
|
1998
|
+
this.lastMcpDeathNotifiedAt = Date.now();
|
|
1999
|
+
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");
|
|
2000
|
+
this.emit("mcp_died", { name: this.name, pid: 0, autoRestart, authSuspected, neverConnected: true });
|
|
2001
|
+
if (autoRestart)
|
|
2002
|
+
this.armMcpRestartWhenIdle();
|
|
2003
|
+
}
|
|
1909
2004
|
/**
|
|
1910
2005
|
* Whether a dead MCP pid on this backend is more likely a CLI-driven
|
|
1911
2006
|
* replacement than a real loss. Codex only, and deliberately narrow: it is
|
|
@@ -2124,6 +2219,7 @@ export class Daemon extends EventEmitter {
|
|
|
2124
2219
|
* if an alarm had already gone out, retracts it.
|
|
2125
2220
|
*/
|
|
2126
2221
|
noteMcpProofOfLife(source, pid) {
|
|
2222
|
+
this.mcpServedGeneration = this.spawnGeneration;
|
|
2127
2223
|
const hadAlarm = this.mcpDeathNotifiedForPid != null;
|
|
2128
2224
|
const hadPending = this.mcpRestartPending || this.mcpDeathDeferredForPid != null;
|
|
2129
2225
|
if (!hadAlarm && !hadPending)
|
|
@@ -2139,6 +2235,9 @@ export class Daemon extends EventEmitter {
|
|
|
2139
2235
|
this.repairMcpPidSlot(pid, source);
|
|
2140
2236
|
}
|
|
2141
2237
|
emitMcpRecovered(source, pid) {
|
|
2238
|
+
// The retracted alarm was "never connected", not a death: say so.
|
|
2239
|
+
if (this.mcpDeathNotifiedForPid === 0)
|
|
2240
|
+
source = "late_connect";
|
|
2142
2241
|
this.logger.info({ source, pid }, "MCP server recovered — retracting the earlier death report");
|
|
2143
2242
|
this.emit("mcp_recovered", { name: this.name, source, pid });
|
|
2144
2243
|
}
|
|
@@ -2462,6 +2561,10 @@ export class Daemon extends EventEmitter {
|
|
|
2462
2561
|
this.stormWindow?.markRecovered(this.name);
|
|
2463
2562
|
}
|
|
2464
2563
|
catch (err) {
|
|
2564
|
+
if (this.stopOnUnsupportedCliRespawn(err)) {
|
|
2565
|
+
this.healthCheckTimer = null;
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2465
2568
|
if (!this.handOffBackendUnreachableRespawn(err)) {
|
|
2466
2569
|
this.logger.error({ err }, `Failed to respawn ${cliLabel} window`);
|
|
2467
2570
|
}
|
|
@@ -2495,6 +2598,21 @@ export class Daemon extends EventEmitter {
|
|
|
2495
2598
|
* the pane monitor to capture it would re-create a false idle state.
|
|
2496
2599
|
*/
|
|
2497
2600
|
/**
|
|
2601
|
+
* The CLI was replaced under a running instance by one that cannot run it as
|
|
2602
|
+
* configured — kiro-cli auto-updating to a release without the instance's
|
|
2603
|
+
* UI/engine (#1109). Every respawn would refuse the same way, so stop
|
|
2604
|
+
* supervising and say why instead of failing every 30 seconds.
|
|
2605
|
+
*/
|
|
2606
|
+
stopOnUnsupportedCliRespawn(err) {
|
|
2607
|
+
if (!(err instanceof UnsupportedCliError))
|
|
2608
|
+
return false;
|
|
2609
|
+
this.healthCheckPaused = true;
|
|
2610
|
+
this.logger.error({ reason: err.message }, "Not respawning: the installed CLI cannot run this instance as configured");
|
|
2611
|
+
this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.");
|
|
2612
|
+
return true;
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Announce that this instance is no longer being supervised. /**
|
|
2498
2616
|
* Announce that this instance is no longer being supervised.
|
|
2499
2617
|
*
|
|
2500
2618
|
* Four health-check exits set `healthCheckPaused = true` and returned without
|
|
@@ -2608,6 +2726,7 @@ export class Daemon extends EventEmitter {
|
|
|
2608
2726
|
for (const dialog of dialogs) {
|
|
2609
2727
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
2610
2728
|
continue;
|
|
2729
|
+
this.noteCodexReserveDialog(dialog, pane);
|
|
2611
2730
|
if (dialog.blocksDelivery || dialog.holdOnly)
|
|
2612
2731
|
blockingSeen = dialog;
|
|
2613
2732
|
if (dialog.holdOnly)
|
|
@@ -2795,15 +2914,29 @@ export class Daemon extends EventEmitter {
|
|
|
2795
2914
|
const count = countMatches(ep.pattern);
|
|
2796
2915
|
const seen = this.lastErrorCount.get(key) ?? 0;
|
|
2797
2916
|
// The generic Codex usage-limit line remains in scrollback after the
|
|
2798
|
-
// user selects Luna Reserve.
|
|
2799
|
-
//
|
|
2800
|
-
//
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2917
|
+
// user selects Luna Reserve. What proves that is NOT a live composer —
|
|
2918
|
+
// codex paints one after a plain hit too, and the old live-composer guard
|
|
2919
|
+
// therefore swallowed every real hit (#1103) — but AgEnD having seen the
|
|
2920
|
+
// Luna Reserve menu in this spawn. Occurrences up to that point are the
|
|
2921
|
+
// incident we already handled; anything beyond is a new hit and goes on to
|
|
2922
|
+
// the lifecycle, whose usage probe decides (available / reserve / exhausted).
|
|
2923
|
+
if (Daemon.isCodexUsageLimitPause(ep)) {
|
|
2924
|
+
const ack = this.codexReserveAck;
|
|
2925
|
+
if (ack && ack.generation === this.spawnGeneration && count <= ack.count) {
|
|
2926
|
+
// The handled incident's lines can scroll out of the capture buffer as
|
|
2927
|
+
// the reserve session talks on. Lower the proof (and the occurrence
|
|
2928
|
+
// baseline) with them: otherwise `count <= ack.count` would keep
|
|
2929
|
+
// swallowing a NEW hit — the reserve running out — until it exceeded
|
|
2930
|
+
// the menu-time count, and it would never reach the lifecycle probe.
|
|
2931
|
+
if (count < ack.count)
|
|
2932
|
+
ack.count = count;
|
|
2804
2933
|
this.lastErrorCount.set(key, count);
|
|
2805
|
-
|
|
2806
|
-
|
|
2934
|
+
this.logger.debug({ decision: "ignore", evidence: "reserve-menu-seen", count, acknowledged: ack.count }, "codex usage-limit: stale text of the handled Luna Reserve incident");
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
if (count > seen) {
|
|
2938
|
+
this.logger.info({ decision: "candidate", evidence: ack && ack.generation === this.spawnGeneration ? "new-hit-after-reserve" : "no-reserve-proof", count, livePane: codexLivePane }, "codex usage-limit: new occurrence — handing to the quota probe");
|
|
2939
|
+
}
|
|
2807
2940
|
}
|
|
2808
2941
|
if (count <= seen) {
|
|
2809
2942
|
// Occurrences scrolled out of the capture buffer → lower the baseline
|
|
@@ -3575,6 +3708,39 @@ export class Daemon extends EventEmitter {
|
|
|
3575
3708
|
this.pauseWakeTransition = null;
|
|
3576
3709
|
}
|
|
3577
3710
|
}
|
|
3711
|
+
/**
|
|
3712
|
+
* One line per edge, until the instance is flapping. The edges themselves are
|
|
3713
|
+
* still published to every observer; only the log is limited, so the next
|
|
3714
|
+
* flap cannot bury the fleet log the way #1116 did. The first suppressed edge
|
|
3715
|
+
* warns once (that is the signal worth reading), and a summary follows every
|
|
3716
|
+
* window while it lasts and at the first edge after it calms.
|
|
3717
|
+
*/
|
|
3718
|
+
logStateEdge(previous, snapshot) {
|
|
3719
|
+
const now = Date.now();
|
|
3720
|
+
this.stateEdgeTimes.push(now);
|
|
3721
|
+
while (this.stateEdgeTimes.length && this.stateEdgeTimes[0] <= now - STATE_EDGE_LOG_WINDOW_MS)
|
|
3722
|
+
this.stateEdgeTimes.shift();
|
|
3723
|
+
const edges = this.stateEdgeTimes.length;
|
|
3724
|
+
const fields = { previousState: previous, state: snapshot.state, unchangedForMs: snapshot.unchangedForMs };
|
|
3725
|
+
if (edges <= STATE_EDGE_LOG_BURST) {
|
|
3726
|
+
if (this.stateEdgesNotLogged > 0) {
|
|
3727
|
+
this.logger.info({ notLogged: this.stateEdgesNotLogged }, "Instance execution state edges were not logged while it was flapping");
|
|
3728
|
+
this.stateEdgesNotLogged = 0;
|
|
3729
|
+
}
|
|
3730
|
+
this.logger.info(fields, "Instance execution state changed");
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
if (this.stateEdgesNotLogged === 0) {
|
|
3734
|
+
this.stateEdgeSummaryAt = now;
|
|
3735
|
+
this.logger.warn({ edgesInWindow: edges, windowMs: STATE_EDGE_LOG_WINDOW_MS, backend: this.backend?.binaryName }, "Instance execution state is flapping — per-edge logging suppressed; the pane is repainting without a stable idle/working reading");
|
|
3736
|
+
}
|
|
3737
|
+
else if (now - this.stateEdgeSummaryAt >= STATE_EDGE_LOG_WINDOW_MS) {
|
|
3738
|
+
this.stateEdgeSummaryAt = now;
|
|
3739
|
+
this.logger.warn({ notLogged: this.stateEdgesNotLogged, edgesInWindow: edges }, "Instance execution state is still flapping");
|
|
3740
|
+
}
|
|
3741
|
+
this.stateEdgesNotLogged++;
|
|
3742
|
+
this.logger.debug(fields, "Instance execution state changed (flapping, not logged at info)");
|
|
3743
|
+
}
|
|
3578
3744
|
applyInstanceStateSnapshot(snapshot, pane) {
|
|
3579
3745
|
const previous = this.instanceState;
|
|
3580
3746
|
// A live safety menu owns stdin. It may still contain Claude's persistent
|
|
@@ -3610,11 +3776,7 @@ export class Daemon extends EventEmitter {
|
|
|
3610
3776
|
this.maybeProxyReplyOnTurnEnd(pane);
|
|
3611
3777
|
}
|
|
3612
3778
|
if (snapshot.state !== previous) {
|
|
3613
|
-
this.
|
|
3614
|
-
previousState: previous,
|
|
3615
|
-
state: snapshot.state,
|
|
3616
|
-
unchangedForMs: snapshot.unchangedForMs,
|
|
3617
|
-
}, "Instance execution state changed");
|
|
3779
|
+
this.logStateEdge(previous, snapshot);
|
|
3618
3780
|
this.emit("instance_state", { name: this.name, ...snapshot });
|
|
3619
3781
|
this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
|
|
3620
3782
|
if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
|
|
@@ -4112,6 +4274,19 @@ export class Daemon extends EventEmitter {
|
|
|
4112
4274
|
if (reason === "delivery_idle_gate")
|
|
4113
4275
|
this.resetFooterFallback();
|
|
4114
4276
|
if (reason === "output_probe") {
|
|
4277
|
+
// A TUI that repaints without changing a cell (muse parks its cursor
|
|
4278
|
+
// ~18 times a second while idle; Codex's star field) never leaves the
|
|
4279
|
+
// probe a quiet moment, so roughly one probe in five is overtaken. An
|
|
4280
|
+
// idle instance whose stale capture still shows the structural idle
|
|
4281
|
+
// layout has nothing to be called working on: the rule that cosmetic
|
|
4282
|
+
// output must not manufacture an idle -> working edge holds here too.
|
|
4283
|
+
// Without this every overtaken probe published the edge, and the next
|
|
4284
|
+
// clean probe (one confirmation was still banked) published it back —
|
|
4285
|
+
// a pair every ~1.6s for as long as the repaint lasted (#1116). Real
|
|
4286
|
+
// work cannot hide behind this: it changes the layout, the next
|
|
4287
|
+
// output arms another probe, and the debounce capture still fires.
|
|
4288
|
+
if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true)
|
|
4289
|
+
return;
|
|
4115
4290
|
// A second burst overtook the probe. Do not bless its stale capture as
|
|
4116
4291
|
// idle: continuous output is the safer working signal, while the next
|
|
4117
4292
|
// quiet probe/debounce can still prove that it was only repainting.
|
|
@@ -5476,10 +5651,18 @@ export class Daemon extends EventEmitter {
|
|
|
5476
5651
|
return { state: "unknown" };
|
|
5477
5652
|
}
|
|
5478
5653
|
for (const dialog of dialogs) {
|
|
5479
|
-
if (Daemon.dialogMatches(dialog, pane))
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5654
|
+
if (!Daemon.dialogMatches(dialog, pane))
|
|
5655
|
+
continue;
|
|
5656
|
+
// Same one-shot fence as the scanners: a safety choice already sent in
|
|
5657
|
+
// this spawn is not what the screen is waiting on any more — the entry
|
|
5658
|
+
// after it (typically a hold) describes it.
|
|
5659
|
+
if (dialog.verifyAfterKeys && dialog.autoResolutionKey
|
|
5660
|
+
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
5661
|
+
&& this.autoResolvedDialogKey === dialog.autoResolutionKey
|
|
5662
|
+
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
5663
|
+
continue;
|
|
5664
|
+
this.trackDialogParked(dialog);
|
|
5665
|
+
return { state: "dialog", dialog };
|
|
5483
5666
|
}
|
|
5484
5667
|
this.trackDialogParked(null);
|
|
5485
5668
|
return { state: "clear" };
|
|
@@ -8188,7 +8371,7 @@ export class Daemon extends EventEmitter {
|
|
|
8188
8371
|
// Transient tmux trouble is not evidence about the CLI. Returning false
|
|
8189
8372
|
// here would clear the session; retry within the budget instead.
|
|
8190
8373
|
captureFailures++;
|
|
8191
|
-
this.logger.
|
|
8374
|
+
this.logger.debug({ err, captureFailures }, "capture-pane failed during the startup dialog scan — retrying");
|
|
8192
8375
|
// Only a POSITIVE dead pane ends the scan; an unanswerable liveness
|
|
8193
8376
|
// query is the same transient trouble and must not clear the session.
|
|
8194
8377
|
if ((await this.paneLiveness()) === "dead")
|
|
@@ -8220,6 +8403,7 @@ export class Daemon extends EventEmitter {
|
|
|
8220
8403
|
&& this.autoResolvedDialogKey === dialog.autoResolutionKey)))
|
|
8221
8404
|
continue;
|
|
8222
8405
|
lastDialog = dialog;
|
|
8406
|
+
this.noteCodexReserveDialog(dialog, pane);
|
|
8223
8407
|
cleanReadyPolls = 0;
|
|
8224
8408
|
// Start the parked clock for every delivery-blocking dialog, exact
|
|
8225
8409
|
// ones included: if the auto-dismiss keeps failing here, the human
|