@songsid/agend 2.1.10-beta.1 → 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 +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 +81 -6
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +45 -0
- package/dist/daemon.js +148 -21
- 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/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/package.json +1 -1
- package/templates/systemd.service.ejs +17 -1
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,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;
|
|
@@ -1275,6 +1296,27 @@ export class Daemon extends EventEmitter {
|
|
|
1275
1296
|
return false;
|
|
1276
1297
|
}
|
|
1277
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
|
+
}
|
|
1278
1320
|
/** Re-check the current pane after an async quota probe before pausing it. */
|
|
1279
1321
|
async isCodexLivePane() {
|
|
1280
1322
|
if (this.backend?.binaryName !== "codex" || !this.tmux)
|
|
@@ -1575,7 +1617,7 @@ export class Daemon extends EventEmitter {
|
|
|
1575
1617
|
}
|
|
1576
1618
|
}
|
|
1577
1619
|
async start() {
|
|
1578
|
-
|
|
1620
|
+
ensureInstanceDir(this.instanceDir);
|
|
1579
1621
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
1580
1622
|
this.logger.info(`Starting ${this.name}`);
|
|
1581
1623
|
// P1: Read crash state from previous run — skip resume if last run was a crash loop
|
|
@@ -2519,6 +2561,10 @@ export class Daemon extends EventEmitter {
|
|
|
2519
2561
|
this.stormWindow?.markRecovered(this.name);
|
|
2520
2562
|
}
|
|
2521
2563
|
catch (err) {
|
|
2564
|
+
if (this.stopOnUnsupportedCliRespawn(err)) {
|
|
2565
|
+
this.healthCheckTimer = null;
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2522
2568
|
if (!this.handOffBackendUnreachableRespawn(err)) {
|
|
2523
2569
|
this.logger.error({ err }, `Failed to respawn ${cliLabel} window`);
|
|
2524
2570
|
}
|
|
@@ -2552,6 +2598,21 @@ export class Daemon extends EventEmitter {
|
|
|
2552
2598
|
* the pane monitor to capture it would re-create a false idle state.
|
|
2553
2599
|
*/
|
|
2554
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. /**
|
|
2555
2616
|
* Announce that this instance is no longer being supervised.
|
|
2556
2617
|
*
|
|
2557
2618
|
* Four health-check exits set `healthCheckPaused = true` and returned without
|
|
@@ -2665,6 +2726,7 @@ export class Daemon extends EventEmitter {
|
|
|
2665
2726
|
for (const dialog of dialogs) {
|
|
2666
2727
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
2667
2728
|
continue;
|
|
2729
|
+
this.noteCodexReserveDialog(dialog, pane);
|
|
2668
2730
|
if (dialog.blocksDelivery || dialog.holdOnly)
|
|
2669
2731
|
blockingSeen = dialog;
|
|
2670
2732
|
if (dialog.holdOnly)
|
|
@@ -2852,15 +2914,29 @@ export class Daemon extends EventEmitter {
|
|
|
2852
2914
|
const count = countMatches(ep.pattern);
|
|
2853
2915
|
const seen = this.lastErrorCount.get(key) ?? 0;
|
|
2854
2916
|
// The generic Codex usage-limit line remains in scrollback after the
|
|
2855
|
-
// user selects Luna Reserve.
|
|
2856
|
-
//
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
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;
|
|
2861
2933
|
this.lastErrorCount.set(key, count);
|
|
2862
|
-
|
|
2863
|
-
|
|
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
|
+
}
|
|
2864
2940
|
}
|
|
2865
2941
|
if (count <= seen) {
|
|
2866
2942
|
// Occurrences scrolled out of the capture buffer → lower the baseline
|
|
@@ -3632,6 +3708,39 @@ export class Daemon extends EventEmitter {
|
|
|
3632
3708
|
this.pauseWakeTransition = null;
|
|
3633
3709
|
}
|
|
3634
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
|
+
}
|
|
3635
3744
|
applyInstanceStateSnapshot(snapshot, pane) {
|
|
3636
3745
|
const previous = this.instanceState;
|
|
3637
3746
|
// A live safety menu owns stdin. It may still contain Claude's persistent
|
|
@@ -3667,11 +3776,7 @@ export class Daemon extends EventEmitter {
|
|
|
3667
3776
|
this.maybeProxyReplyOnTurnEnd(pane);
|
|
3668
3777
|
}
|
|
3669
3778
|
if (snapshot.state !== previous) {
|
|
3670
|
-
this.
|
|
3671
|
-
previousState: previous,
|
|
3672
|
-
state: snapshot.state,
|
|
3673
|
-
unchangedForMs: snapshot.unchangedForMs,
|
|
3674
|
-
}, "Instance execution state changed");
|
|
3779
|
+
this.logStateEdge(previous, snapshot);
|
|
3675
3780
|
this.emit("instance_state", { name: this.name, ...snapshot });
|
|
3676
3781
|
this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
|
|
3677
3782
|
if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
|
|
@@ -4169,6 +4274,19 @@ export class Daemon extends EventEmitter {
|
|
|
4169
4274
|
if (reason === "delivery_idle_gate")
|
|
4170
4275
|
this.resetFooterFallback();
|
|
4171
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;
|
|
4172
4290
|
// A second burst overtook the probe. Do not bless its stale capture as
|
|
4173
4291
|
// idle: continuous output is the safer working signal, while the next
|
|
4174
4292
|
// quiet probe/debounce can still prove that it was only repainting.
|
|
@@ -5533,10 +5651,18 @@ export class Daemon extends EventEmitter {
|
|
|
5533
5651
|
return { state: "unknown" };
|
|
5534
5652
|
}
|
|
5535
5653
|
for (const dialog of dialogs) {
|
|
5536
|
-
if (Daemon.dialogMatches(dialog, pane))
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
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 };
|
|
5540
5666
|
}
|
|
5541
5667
|
this.trackDialogParked(null);
|
|
5542
5668
|
return { state: "clear" };
|
|
@@ -8245,7 +8371,7 @@ export class Daemon extends EventEmitter {
|
|
|
8245
8371
|
// Transient tmux trouble is not evidence about the CLI. Returning false
|
|
8246
8372
|
// here would clear the session; retry within the budget instead.
|
|
8247
8373
|
captureFailures++;
|
|
8248
|
-
this.logger.
|
|
8374
|
+
this.logger.debug({ err, captureFailures }, "capture-pane failed during the startup dialog scan — retrying");
|
|
8249
8375
|
// Only a POSITIVE dead pane ends the scan; an unanswerable liveness
|
|
8250
8376
|
// query is the same transient trouble and must not clear the session.
|
|
8251
8377
|
if ((await this.paneLiveness()) === "dead")
|
|
@@ -8277,6 +8403,7 @@ export class Daemon extends EventEmitter {
|
|
|
8277
8403
|
&& this.autoResolvedDialogKey === dialog.autoResolutionKey)))
|
|
8278
8404
|
continue;
|
|
8279
8405
|
lastDialog = dialog;
|
|
8406
|
+
this.noteCodexReserveDialog(dialog, pane);
|
|
8280
8407
|
cleanReadyPolls = 0;
|
|
8281
8408
|
// Start the parked clock for every delivery-blocking dialog, exact
|
|
8282
8409
|
// ones included: if the auto-dismiss keeps failing here, the human
|