@songsid/agend 2.1.10-beta.1 → 2.1.10-beta.3
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/backend/antigravity.js +2 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/kiro.d.ts +81 -3
- package/dist/backend/kiro.js +362 -29
- 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/ipc-bridge.js +10 -3
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/mcp-server.d.ts +1 -8
- package/dist/channel/mcp-server.js +1 -0
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +141 -36
- package/dist/cli.js.map +1 -1
- package/dist/coredump-filter.d.ts +21 -0
- package/dist/coredump-filter.js +64 -0
- package/dist/coredump-filter.js.map +1 -0
- package/dist/daemon.d.ts +45 -0
- package/dist/daemon.js +153 -22
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +25 -1
- package/dist/fleet-manager.js +87 -13
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
- package/dist/instance-lifecycle.d.ts +16 -5
- package/dist/instance-lifecycle.js +31 -20
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +8 -0
- package/dist/locale.js.map +1 -1
- package/dist/logger.js +19 -16
- package/dist/logger.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/paths.d.ts +15 -3
- package/dist/paths.js +45 -25
- package/dist/paths.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/service-installer.d.ts +46 -0
- package/dist/service-installer.js +224 -4
- package/dist/service-installer.js.map +1 -1
- package/dist/ui/settings.html +2 -2
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +31 -0
- package/dist/update-check.js.map +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +17 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep crash dumps small, from inside the fleet (#1113 hotfix).
|
|
3
|
+
*
|
|
4
|
+
* On WSL every crash is piped to /wsl-capture-crash, which ignores
|
|
5
|
+
* RLIMIT_CORE, so kiro-cli and the fleet left ~1 GB / ~450 MB dumps in
|
|
6
|
+
* %TEMP%\wsl-crashes. A zero /proc/<pid>/coredump_filter writes no memory
|
|
7
|
+
* mappings into a dump (a few KB). The unit's `CoredumpFilter=0` was meant to
|
|
8
|
+
* set it, but systemd 249 silently ignores that directive in a unit file
|
|
9
|
+
* (only a transient `systemd-run -p` honours it), so the fleet sets its own
|
|
10
|
+
* filter at startup instead. The value is inherited across fork/exec by a
|
|
11
|
+
* tmux server the fleet starts; panes of a server that already existed get
|
|
12
|
+
* it from coredumpFilterLaunchPrefix below.
|
|
13
|
+
*
|
|
14
|
+
* Linux only. No privilege needed: a process may always narrow its own filter.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
export function disableCoredumpMemory(path = "/proc/self/coredump_filter", platform = process.platform, env = process.env) {
|
|
18
|
+
if (platform !== "linux")
|
|
19
|
+
return "unsupported";
|
|
20
|
+
// An operator debugging a crash keeps full dumps (and any filter they set).
|
|
21
|
+
if (env.AGEND_KEEP_COREDUMP_FILTER === "1")
|
|
22
|
+
return "kept";
|
|
23
|
+
try {
|
|
24
|
+
if (/^0+$/.test(readFileSync(path, "utf-8").trim()))
|
|
25
|
+
return "already";
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return "unsupported"; // no such file: a kernel without it
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
writeFileSync(path, "0");
|
|
32
|
+
return /^0+$/.test(readFileSync(path, "utf-8").trim()) ? "set" : "failed";
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "failed";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The fleet process's start-up step: called once the fleet owns its lock,
|
|
40
|
+
* before anything is spawned, so a tmux server it starts inherits it.
|
|
41
|
+
*/
|
|
42
|
+
export function limitFleetCoreDumps(log, disable = disableCoredumpMemory) {
|
|
43
|
+
const outcome = disable();
|
|
44
|
+
if (outcome === "set" || outcome === "already")
|
|
45
|
+
log.info("Crash dumps limited to a few KB (coredump_filter=0 for the fleet and every CLI it launches).");
|
|
46
|
+
else if (outcome === "failed")
|
|
47
|
+
log.error("⚠ Could not set /proc/self/coredump_filter to 0; a crash may write a full-size core dump (#1113).");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Shell prefix for AgEnD's own CLI launch commands (#1125 review). The fleet's
|
|
51
|
+
* filter reaches a CLI only if the tmux server was started by this fleet; a
|
|
52
|
+
* server that already existed (the default socket may hold a user's own
|
|
53
|
+
* session, which AgEnD reuses and never kills) forks panes with ITS filter.
|
|
54
|
+
* So each launch sets the filter of the pane's own shell before the CLI
|
|
55
|
+
* starts: `echo` is a builtin in sh, bash, zsh and fish, the redirect order
|
|
56
|
+
* silences a failure, and the CLI and everything it spawns inherit the value.
|
|
57
|
+
* Empty where it does not apply or the operator opted out.
|
|
58
|
+
*/
|
|
59
|
+
export function coredumpFilterLaunchPrefix(platform = process.platform, env = process.env) {
|
|
60
|
+
if (platform !== "linux" || env.AGEND_KEEP_COREDUMP_FILTER === "1")
|
|
61
|
+
return "";
|
|
62
|
+
return "echo 0 2>/dev/null >/proc/self/coredump_filter; ";
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=coredump-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coredump-filter.js","sourceRoot":"","sources":["../src/coredump-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAItD,MAAM,UAAU,qBAAqB,CACnC,IAAI,GAAG,4BAA4B,EACnC,WAA4B,OAAO,CAAC,QAAQ,EAC5C,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,aAAa,CAAC;IAC/C,4EAA4E;IAC5E,IAAI,GAAG,CAAC,0BAA0B,KAAK,GAAG;QAAE,OAAO,MAAM,CAAC;IAC1D,IAAI,CAAC;QACH,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,SAAS,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,aAAa,CAAC,CAAC,oCAAoC;IAC5D,CAAC;IACD,IAAI,CAAC;QACH,aAAa,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACzB,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAA0D,EAC1D,UAAgD,qBAAqB;IAErE,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;IAC1B,IAAI,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK,SAAS;QAAE,GAAG,CAAC,IAAI,CAAC,8FAA8F,CAAC,CAAC;SACpJ,IAAI,OAAO,KAAK,QAAQ;QAAE,GAAG,CAAC,KAAK,CAAC,mGAAmG,CAAC,CAAC;AAChJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CACxC,WAA4B,OAAO,CAAC,QAAQ,EAC5C,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,0BAA0B,KAAK,GAAG;QAAE,OAAO,EAAE,CAAC;IAC9E,OAAO,kDAAkD,CAAC;AAC5D,CAAC"}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare const DEFAULT_STATE_SAFETY_SWEEP_MS = 60000;
|
|
|
47
47
|
export declare const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
48
48
|
/** Do not turn a streaming Codex turn into one capture-pane per burst. */
|
|
49
49
|
export declare const STRUCTURED_IDLE_PROBE_MS = 500;
|
|
50
|
+
/**
|
|
51
|
+
* An instance publishing more than this many execution-state edges inside the
|
|
52
|
+
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
53
|
+
* flapping muse wrote 89,532 of the fleet's 91,000 state-change lines.
|
|
54
|
+
*/
|
|
55
|
+
export declare const STATE_EDGE_LOG_BURST = 10;
|
|
56
|
+
export declare const STATE_EDGE_LOG_WINDOW_MS = 60000;
|
|
50
57
|
/** How long an unrecognised-but-empty composer must stay unchanged before it counts as idle (#978). */
|
|
51
58
|
export declare const UNKNOWN_LAYOUT_STABLE_MS = 10000;
|
|
52
59
|
/** A foreground server/port-forward should hand control back or be acknowledged. */
|
|
@@ -472,6 +479,15 @@ export declare class Daemon extends EventEmitter {
|
|
|
472
479
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
473
480
|
private autoResolvedDialogKey;
|
|
474
481
|
private autoResolvedDialogGeneration;
|
|
482
|
+
/**
|
|
483
|
+
* (#1103) Proof that THIS spawn's Codex went onto Luna Reserve: AgEnD saw (and
|
|
484
|
+
* answered) the usage-limit menu that codex shows when it switches itself to
|
|
485
|
+
* the reserve. `count` is how many usage-limit lines the pane held at that
|
|
486
|
+
* moment; only occurrences up to it are the text of the incident we already
|
|
487
|
+
* handled. A later, larger count is a NEW hit (the reserve ran out too).
|
|
488
|
+
* Keyed by spawn generation, so a respawn starts without any proof.
|
|
489
|
+
*/
|
|
490
|
+
private codexReserveAck;
|
|
475
491
|
private backgroundSessionRecoveryAttempted;
|
|
476
492
|
/** Whether the last spawn started a fresh session (not resumed). */
|
|
477
493
|
isNewSession: boolean;
|
|
@@ -488,6 +504,10 @@ export declare class Daemon extends EventEmitter {
|
|
|
488
504
|
/** Consecutive structural idle captures for a noisy, periodically-redrawing
|
|
489
505
|
* pane. Two are required before a working -> idle transition. */
|
|
490
506
|
private instanceStatePeriodicIdleConfirmations;
|
|
507
|
+
/** Recent execution-state edge times, for the flap log limiter (#1116). */
|
|
508
|
+
private stateEdgeTimes;
|
|
509
|
+
private stateEdgesNotLogged;
|
|
510
|
+
private stateEdgeSummaryAt;
|
|
491
511
|
/** #978: last unrecognised-but-empty-composer capture and since when it has been unchanged. */
|
|
492
512
|
private unknownLayoutPaneKey;
|
|
493
513
|
private unknownLayoutStableSince;
|
|
@@ -693,6 +713,15 @@ export declare class Daemon extends EventEmitter {
|
|
|
693
713
|
* notice is scrollback, not a reason to pause and send /quit.
|
|
694
714
|
*/
|
|
695
715
|
private isCodexLivePaneSnapshot;
|
|
716
|
+
/** The Codex "usage limit reached" pause pattern (the only one the reserve proof applies to). */
|
|
717
|
+
private static isCodexUsageLimitPause;
|
|
718
|
+
private static countPatternMatches;
|
|
719
|
+
/**
|
|
720
|
+
* Record that the Luna Reserve menu is (or was just) on screen, for the
|
|
721
|
+
* usage-limit decision in evaluateErrorPatterns. Called wherever a dialog is
|
|
722
|
+
* matched, before any key is sent.
|
|
723
|
+
*/
|
|
724
|
+
private noteCodexReserveDialog;
|
|
696
725
|
/** Re-check the current pane after an async quota probe before pausing it. */
|
|
697
726
|
isCodexLivePane(): Promise<boolean>;
|
|
698
727
|
/** Whether this instance is in a crash loop (3+ consecutive crashes). */
|
|
@@ -893,6 +922,14 @@ export declare class Daemon extends EventEmitter {
|
|
|
893
922
|
* the pane monitor to capture it would re-create a false idle state.
|
|
894
923
|
*/
|
|
895
924
|
/**
|
|
925
|
+
* The CLI was replaced under a running instance by one that cannot run it as
|
|
926
|
+
* configured — kiro-cli auto-updating to a release without the instance's
|
|
927
|
+
* UI/engine (#1109). Every respawn would refuse the same way, so stop
|
|
928
|
+
* supervising and say why instead of failing every 30 seconds.
|
|
929
|
+
*/
|
|
930
|
+
private stopOnUnsupportedCliRespawn;
|
|
931
|
+
/**
|
|
932
|
+
* Announce that this instance is no longer being supervised. /**
|
|
896
933
|
* Announce that this instance is no longer being supervised.
|
|
897
934
|
*
|
|
898
935
|
* Four health-check exits set `healthCheckPaused = true` and returned without
|
|
@@ -1040,6 +1077,14 @@ export declare class Daemon extends EventEmitter {
|
|
|
1040
1077
|
pause(reason?: PauseReason): Promise<void>;
|
|
1041
1078
|
/** Respawn the CLI in the preserved window and block until its prompt is ready. */
|
|
1042
1079
|
wake(timeoutMs?: number): Promise<void>;
|
|
1080
|
+
/**
|
|
1081
|
+
* One line per edge, until the instance is flapping. The edges themselves are
|
|
1082
|
+
* still published to every observer; only the log is limited, so the next
|
|
1083
|
+
* flap cannot bury the fleet log the way #1116 did. The first suppressed edge
|
|
1084
|
+
* warns once (that is the signal worth reading), and a summary follows every
|
|
1085
|
+
* window while it lasts and at the first edge after it calms.
|
|
1086
|
+
*/
|
|
1087
|
+
private logStateEdge;
|
|
1043
1088
|
private applyInstanceStateSnapshot;
|
|
1044
1089
|
/**
|
|
1045
1090
|
* An inbound message is in the CLI — the turn it starts owns the proxy-reply
|
package/dist/daemon.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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";
|
|
8
|
+
import { coredumpFilterLaunchPrefix } from "./coredump-filter.js";
|
|
7
9
|
import { mcpServerState } from "./mcp-liveness.js";
|
|
8
10
|
import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
9
11
|
import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
@@ -18,7 +20,7 @@ import { ContextGuardian } from "./context-guardian.js";
|
|
|
18
20
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
19
21
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
20
22
|
import { MessageBus } from "./channel/message-bus.js";
|
|
21
|
-
import { shellQuote } from "./backend/types.js";
|
|
23
|
+
import { shellQuote, UnsupportedCliError } from "./backend/types.js";
|
|
22
24
|
import { getTmuxSession } from "./config.js";
|
|
23
25
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
24
26
|
import { HangDetector } from "./hang-detector.js";
|
|
@@ -97,6 +99,13 @@ export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
|
|
|
97
99
|
export const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
98
100
|
/** Do not turn a streaming Codex turn into one capture-pane per burst. */
|
|
99
101
|
export const STRUCTURED_IDLE_PROBE_MS = 500;
|
|
102
|
+
/**
|
|
103
|
+
* An instance publishing more than this many execution-state edges inside the
|
|
104
|
+
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
105
|
+
* flapping muse wrote 89,532 of the fleet's 91,000 state-change lines.
|
|
106
|
+
*/
|
|
107
|
+
export const STATE_EDGE_LOG_BURST = 10;
|
|
108
|
+
export const STATE_EDGE_LOG_WINDOW_MS = 60_000;
|
|
100
109
|
/** How long an unrecognised-but-empty composer must stay unchanged before it counts as idle (#978). */
|
|
101
110
|
export const UNKNOWN_LAYOUT_STABLE_MS = 10_000;
|
|
102
111
|
/** A foreground server/port-forward should hand control back or be acknowledged. */
|
|
@@ -161,7 +170,7 @@ export function readLastInboundAt(instanceDir, now = Date.now()) {
|
|
|
161
170
|
}
|
|
162
171
|
/** Atomically persist the last real channel inbound timestamp. */
|
|
163
172
|
export function writeLastInboundAt(instanceDir, timestamp) {
|
|
164
|
-
|
|
173
|
+
ensureInstanceDir(instanceDir);
|
|
165
174
|
const target = join(instanceDir, LAST_INBOUND_FILE);
|
|
166
175
|
const temp = `${target}.${process.pid}.tmp`;
|
|
167
176
|
writeFileSync(temp, String(timestamp));
|
|
@@ -1020,6 +1029,15 @@ export class Daemon extends EventEmitter {
|
|
|
1020
1029
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
1021
1030
|
autoResolvedDialogKey = null;
|
|
1022
1031
|
autoResolvedDialogGeneration = 0;
|
|
1032
|
+
/**
|
|
1033
|
+
* (#1103) Proof that THIS spawn's Codex went onto Luna Reserve: AgEnD saw (and
|
|
1034
|
+
* answered) the usage-limit menu that codex shows when it switches itself to
|
|
1035
|
+
* the reserve. `count` is how many usage-limit lines the pane held at that
|
|
1036
|
+
* moment; only occurrences up to it are the text of the incident we already
|
|
1037
|
+
* handled. A later, larger count is a NEW hit (the reserve ran out too).
|
|
1038
|
+
* Keyed by spawn generation, so a respawn starts without any proof.
|
|
1039
|
+
*/
|
|
1040
|
+
codexReserveAck = null;
|
|
1023
1041
|
backgroundSessionRecoveryAttempted = false;
|
|
1024
1042
|
/** Whether the last spawn started a fresh session (not resumed). */
|
|
1025
1043
|
isNewSession = false;
|
|
@@ -1037,6 +1055,10 @@ export class Daemon extends EventEmitter {
|
|
|
1037
1055
|
/** Consecutive structural idle captures for a noisy, periodically-redrawing
|
|
1038
1056
|
* pane. Two are required before a working -> idle transition. */
|
|
1039
1057
|
instanceStatePeriodicIdleConfirmations = 0;
|
|
1058
|
+
/** Recent execution-state edge times, for the flap log limiter (#1116). */
|
|
1059
|
+
stateEdgeTimes = [];
|
|
1060
|
+
stateEdgesNotLogged = 0;
|
|
1061
|
+
stateEdgeSummaryAt = 0;
|
|
1040
1062
|
/** #978: last unrecognised-but-empty-composer capture and since when it has been unchanged. */
|
|
1041
1063
|
unknownLayoutPaneKey = null;
|
|
1042
1064
|
unknownLayoutStableSince = 0;
|
|
@@ -1275,6 +1297,27 @@ export class Daemon extends EventEmitter {
|
|
|
1275
1297
|
return false;
|
|
1276
1298
|
}
|
|
1277
1299
|
}
|
|
1300
|
+
/** The Codex "usage limit reached" pause pattern (the only one the reserve proof applies to). */
|
|
1301
|
+
static isCodexUsageLimitPause(ep) {
|
|
1302
|
+
return ep.type === "quota" && ep.action === "pause" && ep.message.startsWith("Codex usage limit reached");
|
|
1303
|
+
}
|
|
1304
|
+
static countPatternMatches(pattern, text) {
|
|
1305
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
|
|
1306
|
+
return (text.match(new RegExp(pattern.source, flags)) || []).length;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Record that the Luna Reserve menu is (or was just) on screen, for the
|
|
1310
|
+
* usage-limit decision in evaluateErrorPatterns. Called wherever a dialog is
|
|
1311
|
+
* matched, before any key is sent.
|
|
1312
|
+
*/
|
|
1313
|
+
noteCodexReserveDialog(dialog, pane) {
|
|
1314
|
+
if (dialog.autoResolutionKey !== "codex-usage-limit-luna-reserve" || this.backend?.binaryName !== "codex")
|
|
1315
|
+
return;
|
|
1316
|
+
const ep = this.backend.getErrorPatterns?.().find(candidate => Daemon.isCodexUsageLimitPause(candidate));
|
|
1317
|
+
const count = ep ? Daemon.countPatternMatches(ep.pattern, pane) : 0;
|
|
1318
|
+
this.codexReserveAck = { generation: this.spawnGeneration, count };
|
|
1319
|
+
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");
|
|
1320
|
+
}
|
|
1278
1321
|
/** Re-check the current pane after an async quota probe before pausing it. */
|
|
1279
1322
|
async isCodexLivePane() {
|
|
1280
1323
|
if (this.backend?.binaryName !== "codex" || !this.tmux)
|
|
@@ -1575,7 +1618,7 @@ export class Daemon extends EventEmitter {
|
|
|
1575
1618
|
}
|
|
1576
1619
|
}
|
|
1577
1620
|
async start() {
|
|
1578
|
-
|
|
1621
|
+
ensureInstanceDir(this.instanceDir);
|
|
1579
1622
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
1580
1623
|
this.logger.info(`Starting ${this.name}`);
|
|
1581
1624
|
// P1: Read crash state from previous run — skip resume if last run was a crash loop
|
|
@@ -2519,6 +2562,10 @@ export class Daemon extends EventEmitter {
|
|
|
2519
2562
|
this.stormWindow?.markRecovered(this.name);
|
|
2520
2563
|
}
|
|
2521
2564
|
catch (err) {
|
|
2565
|
+
if (this.stopOnUnsupportedCliRespawn(err)) {
|
|
2566
|
+
this.healthCheckTimer = null;
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2522
2569
|
if (!this.handOffBackendUnreachableRespawn(err)) {
|
|
2523
2570
|
this.logger.error({ err }, `Failed to respawn ${cliLabel} window`);
|
|
2524
2571
|
}
|
|
@@ -2552,6 +2599,21 @@ export class Daemon extends EventEmitter {
|
|
|
2552
2599
|
* the pane monitor to capture it would re-create a false idle state.
|
|
2553
2600
|
*/
|
|
2554
2601
|
/**
|
|
2602
|
+
* The CLI was replaced under a running instance by one that cannot run it as
|
|
2603
|
+
* configured — kiro-cli auto-updating to a release without the instance's
|
|
2604
|
+
* UI/engine (#1109). Every respawn would refuse the same way, so stop
|
|
2605
|
+
* supervising and say why instead of failing every 30 seconds.
|
|
2606
|
+
*/
|
|
2607
|
+
stopOnUnsupportedCliRespawn(err) {
|
|
2608
|
+
if (!(err instanceof UnsupportedCliError))
|
|
2609
|
+
return false;
|
|
2610
|
+
this.healthCheckPaused = true;
|
|
2611
|
+
this.logger.error({ reason: err.message }, "Not respawning: the installed CLI cannot run this instance as configured");
|
|
2612
|
+
this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.");
|
|
2613
|
+
return true;
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Announce that this instance is no longer being supervised. /**
|
|
2555
2617
|
* Announce that this instance is no longer being supervised.
|
|
2556
2618
|
*
|
|
2557
2619
|
* Four health-check exits set `healthCheckPaused = true` and returned without
|
|
@@ -2665,6 +2727,7 @@ export class Daemon extends EventEmitter {
|
|
|
2665
2727
|
for (const dialog of dialogs) {
|
|
2666
2728
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
2667
2729
|
continue;
|
|
2730
|
+
this.noteCodexReserveDialog(dialog, pane);
|
|
2668
2731
|
if (dialog.blocksDelivery || dialog.holdOnly)
|
|
2669
2732
|
blockingSeen = dialog;
|
|
2670
2733
|
if (dialog.holdOnly)
|
|
@@ -2852,15 +2915,29 @@ export class Daemon extends EventEmitter {
|
|
|
2852
2915
|
const count = countMatches(ep.pattern);
|
|
2853
2916
|
const seen = this.lastErrorCount.get(key) ?? 0;
|
|
2854
2917
|
// The generic Codex usage-limit line remains in scrollback after the
|
|
2855
|
-
// user selects Luna Reserve.
|
|
2856
|
-
//
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2918
|
+
// user selects Luna Reserve. What proves that is NOT a live composer —
|
|
2919
|
+
// codex paints one after a plain hit too, and the old live-composer guard
|
|
2920
|
+
// therefore swallowed every real hit (#1103) — but AgEnD having seen the
|
|
2921
|
+
// Luna Reserve menu in this spawn. Occurrences up to that point are the
|
|
2922
|
+
// incident we already handled; anything beyond is a new hit and goes on to
|
|
2923
|
+
// the lifecycle, whose usage probe decides (available / reserve / exhausted).
|
|
2924
|
+
if (Daemon.isCodexUsageLimitPause(ep)) {
|
|
2925
|
+
const ack = this.codexReserveAck;
|
|
2926
|
+
if (ack && ack.generation === this.spawnGeneration && count <= ack.count) {
|
|
2927
|
+
// The handled incident's lines can scroll out of the capture buffer as
|
|
2928
|
+
// the reserve session talks on. Lower the proof (and the occurrence
|
|
2929
|
+
// baseline) with them: otherwise `count <= ack.count` would keep
|
|
2930
|
+
// swallowing a NEW hit — the reserve running out — until it exceeded
|
|
2931
|
+
// the menu-time count, and it would never reach the lifecycle probe.
|
|
2932
|
+
if (count < ack.count)
|
|
2933
|
+
ack.count = count;
|
|
2861
2934
|
this.lastErrorCount.set(key, count);
|
|
2862
|
-
|
|
2863
|
-
|
|
2935
|
+
this.logger.debug({ decision: "ignore", evidence: "reserve-menu-seen", count, acknowledged: ack.count }, "codex usage-limit: stale text of the handled Luna Reserve incident");
|
|
2936
|
+
continue;
|
|
2937
|
+
}
|
|
2938
|
+
if (count > seen) {
|
|
2939
|
+
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");
|
|
2940
|
+
}
|
|
2864
2941
|
}
|
|
2865
2942
|
if (count <= seen) {
|
|
2866
2943
|
// Occurrences scrolled out of the capture buffer → lower the baseline
|
|
@@ -3632,6 +3709,39 @@ export class Daemon extends EventEmitter {
|
|
|
3632
3709
|
this.pauseWakeTransition = null;
|
|
3633
3710
|
}
|
|
3634
3711
|
}
|
|
3712
|
+
/**
|
|
3713
|
+
* One line per edge, until the instance is flapping. The edges themselves are
|
|
3714
|
+
* still published to every observer; only the log is limited, so the next
|
|
3715
|
+
* flap cannot bury the fleet log the way #1116 did. The first suppressed edge
|
|
3716
|
+
* warns once (that is the signal worth reading), and a summary follows every
|
|
3717
|
+
* window while it lasts and at the first edge after it calms.
|
|
3718
|
+
*/
|
|
3719
|
+
logStateEdge(previous, snapshot) {
|
|
3720
|
+
const now = Date.now();
|
|
3721
|
+
this.stateEdgeTimes.push(now);
|
|
3722
|
+
while (this.stateEdgeTimes.length && this.stateEdgeTimes[0] <= now - STATE_EDGE_LOG_WINDOW_MS)
|
|
3723
|
+
this.stateEdgeTimes.shift();
|
|
3724
|
+
const edges = this.stateEdgeTimes.length;
|
|
3725
|
+
const fields = { previousState: previous, state: snapshot.state, unchangedForMs: snapshot.unchangedForMs };
|
|
3726
|
+
if (edges <= STATE_EDGE_LOG_BURST) {
|
|
3727
|
+
if (this.stateEdgesNotLogged > 0) {
|
|
3728
|
+
this.logger.info({ notLogged: this.stateEdgesNotLogged }, "Instance execution state edges were not logged while it was flapping");
|
|
3729
|
+
this.stateEdgesNotLogged = 0;
|
|
3730
|
+
}
|
|
3731
|
+
this.logger.info(fields, "Instance execution state changed");
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (this.stateEdgesNotLogged === 0) {
|
|
3735
|
+
this.stateEdgeSummaryAt = now;
|
|
3736
|
+
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");
|
|
3737
|
+
}
|
|
3738
|
+
else if (now - this.stateEdgeSummaryAt >= STATE_EDGE_LOG_WINDOW_MS) {
|
|
3739
|
+
this.stateEdgeSummaryAt = now;
|
|
3740
|
+
this.logger.warn({ notLogged: this.stateEdgesNotLogged, edgesInWindow: edges }, "Instance execution state is still flapping");
|
|
3741
|
+
}
|
|
3742
|
+
this.stateEdgesNotLogged++;
|
|
3743
|
+
this.logger.debug(fields, "Instance execution state changed (flapping, not logged at info)");
|
|
3744
|
+
}
|
|
3635
3745
|
applyInstanceStateSnapshot(snapshot, pane) {
|
|
3636
3746
|
const previous = this.instanceState;
|
|
3637
3747
|
// A live safety menu owns stdin. It may still contain Claude's persistent
|
|
@@ -3667,11 +3777,7 @@ export class Daemon extends EventEmitter {
|
|
|
3667
3777
|
this.maybeProxyReplyOnTurnEnd(pane);
|
|
3668
3778
|
}
|
|
3669
3779
|
if (snapshot.state !== previous) {
|
|
3670
|
-
this.
|
|
3671
|
-
previousState: previous,
|
|
3672
|
-
state: snapshot.state,
|
|
3673
|
-
unchangedForMs: snapshot.unchangedForMs,
|
|
3674
|
-
}, "Instance execution state changed");
|
|
3780
|
+
this.logStateEdge(previous, snapshot);
|
|
3675
3781
|
this.emit("instance_state", { name: this.name, ...snapshot });
|
|
3676
3782
|
this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
|
|
3677
3783
|
if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
|
|
@@ -4169,6 +4275,19 @@ export class Daemon extends EventEmitter {
|
|
|
4169
4275
|
if (reason === "delivery_idle_gate")
|
|
4170
4276
|
this.resetFooterFallback();
|
|
4171
4277
|
if (reason === "output_probe") {
|
|
4278
|
+
// A TUI that repaints without changing a cell (muse parks its cursor
|
|
4279
|
+
// ~18 times a second while idle; Codex's star field) never leaves the
|
|
4280
|
+
// probe a quiet moment, so roughly one probe in five is overtaken. An
|
|
4281
|
+
// idle instance whose stale capture still shows the structural idle
|
|
4282
|
+
// layout has nothing to be called working on: the rule that cosmetic
|
|
4283
|
+
// output must not manufacture an idle -> working edge holds here too.
|
|
4284
|
+
// Without this every overtaken probe published the edge, and the next
|
|
4285
|
+
// clean probe (one confirmation was still banked) published it back —
|
|
4286
|
+
// a pair every ~1.6s for as long as the repaint lasted (#1116). Real
|
|
4287
|
+
// work cannot hide behind this: it changes the layout, the next
|
|
4288
|
+
// output arms another probe, and the debounce capture still fires.
|
|
4289
|
+
if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true)
|
|
4290
|
+
return;
|
|
4172
4291
|
// A second burst overtook the probe. Do not bless its stale capture as
|
|
4173
4292
|
// idle: continuous output is the safer working signal, while the next
|
|
4174
4293
|
// quiet probe/debounce can still prove that it was only repainting.
|
|
@@ -5533,10 +5652,18 @@ export class Daemon extends EventEmitter {
|
|
|
5533
5652
|
return { state: "unknown" };
|
|
5534
5653
|
}
|
|
5535
5654
|
for (const dialog of dialogs) {
|
|
5536
|
-
if (Daemon.dialogMatches(dialog, pane))
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5655
|
+
if (!Daemon.dialogMatches(dialog, pane))
|
|
5656
|
+
continue;
|
|
5657
|
+
// Same one-shot fence as the scanners: a safety choice already sent in
|
|
5658
|
+
// this spawn is not what the screen is waiting on any more — the entry
|
|
5659
|
+
// after it (typically a hold) describes it.
|
|
5660
|
+
if (dialog.verifyAfterKeys && dialog.autoResolutionKey
|
|
5661
|
+
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
5662
|
+
&& this.autoResolvedDialogKey === dialog.autoResolutionKey
|
|
5663
|
+
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
5664
|
+
continue;
|
|
5665
|
+
this.trackDialogParked(dialog);
|
|
5666
|
+
return { state: "dialog", dialog };
|
|
5540
5667
|
}
|
|
5541
5668
|
this.trackDialogParked(null);
|
|
5542
5669
|
return { state: "clear" };
|
|
@@ -8080,7 +8207,10 @@ export class Daemon extends EventEmitter {
|
|
|
8080
8207
|
clearMuseUsageSnapshot(this.instanceDir);
|
|
8081
8208
|
}
|
|
8082
8209
|
}
|
|
8083
|
-
|
|
8210
|
+
// The CLI's own shell sets a zero coredump_filter first (#1113): this pane
|
|
8211
|
+
// may belong to a tmux server the fleet did not start, which would hand
|
|
8212
|
+
// the CLI its own (full) filter instead of the fleet's.
|
|
8213
|
+
const cmd = coredumpFilterLaunchPrefix() + `${envPrefix} ` + this.backend.buildCommand(launchConfig);
|
|
8084
8214
|
// Every launched command re-arms the passive-transient check, including a
|
|
8085
8215
|
// retry inside the same spawn: its load is a new one.
|
|
8086
8216
|
this.launchAttempt++;
|
|
@@ -8245,7 +8375,7 @@ export class Daemon extends EventEmitter {
|
|
|
8245
8375
|
// Transient tmux trouble is not evidence about the CLI. Returning false
|
|
8246
8376
|
// here would clear the session; retry within the budget instead.
|
|
8247
8377
|
captureFailures++;
|
|
8248
|
-
this.logger.
|
|
8378
|
+
this.logger.debug({ err, captureFailures }, "capture-pane failed during the startup dialog scan — retrying");
|
|
8249
8379
|
// Only a POSITIVE dead pane ends the scan; an unanswerable liveness
|
|
8250
8380
|
// query is the same transient trouble and must not clear the session.
|
|
8251
8381
|
if ((await this.paneLiveness()) === "dead")
|
|
@@ -8277,6 +8407,7 @@ export class Daemon extends EventEmitter {
|
|
|
8277
8407
|
&& this.autoResolvedDialogKey === dialog.autoResolutionKey)))
|
|
8278
8408
|
continue;
|
|
8279
8409
|
lastDialog = dialog;
|
|
8410
|
+
this.noteCodexReserveDialog(dialog, pane);
|
|
8280
8411
|
cleanReadyPolls = 0;
|
|
8281
8412
|
// Start the parked clock for every delivery-blocking dialog, exact
|
|
8282
8413
|
// ones included: if the auto-dismiss keeps failing here, the human
|