@songsid/agend 2.1.4-beta.37 → 2.1.4-beta.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/codex.js +13 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/channel/mcp-server.js +11 -3
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/cli.js +14 -14
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +6 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +6 -0
- package/dist/cross-instance-envelope.js +30 -0
- package/dist/cross-instance-envelope.js.map +1 -0
- package/dist/daemon.d.ts +73 -0
- package/dist/daemon.js +229 -54
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.js +33 -0
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +168 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +57 -13
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -1
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/tmux-manager.d.ts +7 -0
- package/dist/tmux-manager.js +62 -14
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/view.html +124 -15
- package/dist/usage/format-rich.js +24 -23
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +7 -0
- package/dist/usage/i18n-keys.js +34 -0
- package/dist/usage/i18n-keys.js.map +1 -0
- package/dist/usage/i18n.d.ts +5 -0
- package/dist/usage/i18n.js +27 -0
- package/dist/usage/i18n.js.map +1 -0
- package/dist/usage/providers.d.ts +12 -0
- package/dist/usage/providers.js +132 -73
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +22 -21
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare function getTmuxSession(): string;
|
|
|
3
3
|
export declare const DEFAULT_COST_GUARD: CostGuardConfig;
|
|
4
4
|
export declare const DEFAULT_HANG_DETECTOR: HangDetectorConfig;
|
|
5
5
|
export declare const DEFAULT_DAILY_SUMMARY: DailySummaryConfig;
|
|
6
|
+
/** Product-level context protection for cross-instance message bodies (12 KiB). */
|
|
7
|
+
export declare const DEFAULT_MAX_CROSS_INSTANCE_MESSAGE_BYTES: number;
|
|
6
8
|
export declare const DEFAULT_INSTANCE_CONFIG: Omit<InstanceConfig, "working_directory">;
|
|
7
9
|
/** Effective instance defaults, including AgEnD built-ins and fleet overrides. */
|
|
8
10
|
export declare function getEffectiveInstanceDefaults(fleetDefaults?: Partial<InstanceConfig>): Partial<InstanceConfig>;
|
package/dist/config.js
CHANGED
|
@@ -39,6 +39,8 @@ export const DEFAULT_DAILY_SUMMARY = {
|
|
|
39
39
|
hour: 21,
|
|
40
40
|
minute: 0,
|
|
41
41
|
};
|
|
42
|
+
/** Product-level context protection for cross-instance message bodies (12 KiB). */
|
|
43
|
+
export const DEFAULT_MAX_CROSS_INSTANCE_MESSAGE_BYTES = 12 * 1024;
|
|
42
44
|
export const DEFAULT_INSTANCE_CONFIG = {
|
|
43
45
|
auto_pause_after: 0, // minutes; 0 = disabled (opt-in)
|
|
44
46
|
mcp_auto_restart: true, // restart (idle-gated) when the MCP server dies
|
|
@@ -66,7 +68,7 @@ export function getEffectiveInstanceDefaults(fleetDefaults = {}) {
|
|
|
66
68
|
// Fleet-only controls live under defaults for YAML ergonomics but must not
|
|
67
69
|
// leak into every daemon's InstanceConfig (where an on/off toggle would look
|
|
68
70
|
// like an unknown cold field and restart the whole fleet).
|
|
69
|
-
const { tips: _tips, ...instanceDefaults } = fleetDefaults;
|
|
71
|
+
const { tips: _tips, max_cross_instance_message_bytes: _maxCrossInstanceMessageBytes, ...instanceDefaults } = fleetDefaults;
|
|
70
72
|
return deepMergeGeneric(DEFAULT_INSTANCE_CONFIG, instanceDefaults);
|
|
71
73
|
}
|
|
72
74
|
/** Validate IANA timezone string. Throws if invalid. */
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGlF,SAAS,gBAAgB,CAAmB,MAAS,EAAE,MAAkB;IACvE,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,EAA6B,CAAC;IACxD,MAAM,YAAY,GAAG,MAAiC,CAAC;IAEvD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,IACE,SAAS,KAAK,IAAI;YAClB,OAAO,SAAS,KAAK,QAAQ;YAC7B,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;YACzB,OAAO,SAAS,KAAK,QAAQ;YAC7B,SAAS,KAAK,IAAI;YAClB,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EACzB,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAC5B,SAAmB,EACnB,SAA4B,CAC7B,CAAC;QACJ,CAAC;aAAM,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,MAAsB,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,kBAAkB,EAAE,CAAC;AAChE,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAoB;IACjD,eAAe,EAAE,CAAC,EAAE,eAAe;IACnC,kBAAkB,EAAE,EAAE;IACtB,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ;CAC3D,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAuB;IACvD,OAAO,EAAE,IAAI;IACb,eAAe,EAAE,EAAE;CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAuB;IACvD,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,EAAE;IACR,MAAM,EAAE,CAAC;CACV,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAA8C;IAChF,gBAAgB,EAAE,CAAC,EAAE,iCAAiC;IACtD,gBAAgB,EAAE,IAAI,EAAE,gDAAgD;IACxE,eAAe,EAAE,KAAK,EAAE,4EAA4E;IACpG,aAAa,EAAE,KAAK,EAAE,oEAAoE;IAE1F,OAAO,EAAE,QAAQ;IACjB,QAAQ,EAAE;QACR,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,GAAG;QACZ,IAAI,EAAE,EAAE;KACT;IACD,cAAc,EAAE;QACd,WAAW,EAAE,EAAE;QACf,OAAO,EAAE,aAAa;QACtB,WAAW,EAAE,GAAG;KACjB;IACD,gBAAgB,EAAE;QAChB,eAAe,EAAE,OAAO;QACxB,aAAa,EAAE,CAAC,EAAE,gEAAgE;KACnF;IACD,SAAS,EAAE,MAAM;CAClB,CAAC;AAEF,kFAAkF;AAClF,MAAM,UAAU,4BAA4B,CAC1C,gBAAyC,EAAE;IAE3C,2EAA2E;IAC3E,6EAA6E;IAC7E,2DAA2D;IAC3D,MAAM,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Product guard that reserves room for routing metadata and protects recipient context. */
|
|
2
|
+
export declare const MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES = 16000;
|
|
3
|
+
/** Render metadata carried in a cross-instance handoff. */
|
|
4
|
+
export declare function renderCrossInstanceHandoffMetadata(meta: Record<string, string>): string;
|
|
5
|
+
/** Exact cross-instance block pasted by the receiving daemon. */
|
|
6
|
+
export declare function formatCrossInstanceInboundMessage(content: string, meta: Record<string, string>): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Product guard that reserves room for routing metadata and protects recipient context. */
|
|
2
|
+
export const MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES = 16_000;
|
|
3
|
+
/** Render metadata carried in a cross-instance handoff. */
|
|
4
|
+
export function renderCrossInstanceHandoffMetadata(meta) {
|
|
5
|
+
const rows = [];
|
|
6
|
+
const add = (label, value) => {
|
|
7
|
+
if (value && value.trim())
|
|
8
|
+
rows.push(`${label}: ${value.trim()}`);
|
|
9
|
+
};
|
|
10
|
+
add("message_id", meta.message_id);
|
|
11
|
+
add("correlation_id", meta.correlation_id);
|
|
12
|
+
add("request_kind", meta.request_kind);
|
|
13
|
+
add("task_summary", meta.task_summary);
|
|
14
|
+
add("working_directory", meta.working_directory);
|
|
15
|
+
add("branch", meta.branch);
|
|
16
|
+
add("attachment_file_id", meta.attachment_file_id);
|
|
17
|
+
return rows.length ? `\n(${rows.join(" | ")})` : "";
|
|
18
|
+
}
|
|
19
|
+
/** Exact cross-instance block pasted by the receiving daemon. */
|
|
20
|
+
export function formatCrossInstanceInboundMessage(content, meta) {
|
|
21
|
+
const fromInstance = meta.from_instance || "unknown";
|
|
22
|
+
const fromLabel = meta.from_display ? `${meta.from_display} (${fromInstance})` : fromInstance;
|
|
23
|
+
let formatted = `[from:${fromLabel}] ${content}`;
|
|
24
|
+
formatted += renderCrossInstanceHandoffMetadata(meta);
|
|
25
|
+
formatted += meta.requires_reply === "true"
|
|
26
|
+
? "\n(A reply IS required: use report_result with the correlation_id above — or send_to_instance. Not direct text.)"
|
|
27
|
+
: "\n(If you need to reply, use send_to_instance tool, NOT direct text. If there is nothing to add, you may stay silent.)";
|
|
28
|
+
return formatted;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=cross-instance-envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-instance-envelope.js","sourceRoot":"","sources":["../src/cross-instance-envelope.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,MAAM,CAAC,MAAM,0CAA0C,GAAG,MAAM,CAAC;AAEjE,2DAA2D;AAC3D,MAAM,UAAU,kCAAkC,CAAC,IAA4B;IAC7E,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,KAAyB,EAAE,EAAE;QACvD,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACpE,CAAC,CAAC;IACF,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACnC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IAC3C,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACvC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACvC,GAAG,CAAC,mBAAmB,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACjD,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3B,GAAG,CAAC,oBAAoB,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACnD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;AACtD,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,iCAAiC,CAAC,OAAe,EAAE,IAA4B;IAC7F,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,IAAI,SAAS,CAAC;IACrD,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,YAAY,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC;IAC9F,IAAI,SAAS,GAAG,SAAS,SAAS,KAAK,OAAO,EAAE,CAAC;IACjD,SAAS,IAAI,kCAAkC,CAAC,IAAI,CAAC,CAAC;IACtD,SAAS,IAAI,IAAI,CAAC,cAAc,KAAK,MAAM;QACzC,CAAC,CAAC,kHAAkH;QACpH,CAAC,CAAC,wHAAwH,CAAC;IAC7H,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -488,6 +488,16 @@ export declare class Daemon extends EventEmitter {
|
|
|
488
488
|
* past this point interrupting the turn costs less than staying mute.
|
|
489
489
|
*/
|
|
490
490
|
private static readonly MCP_RESTART_MAX_WAIT_MS;
|
|
491
|
+
/**
|
|
492
|
+
* How long the revival restart waits, at the point of firing, for a
|
|
493
|
+
* CLI-respawned MCP server to announce itself.
|
|
494
|
+
*
|
|
495
|
+
* Codex <0.146 tears down and recreates ALL MCP connections on an auth/config
|
|
496
|
+
* change — healthy ones included — so the daemon sees the old pid die while
|
|
497
|
+
* the CLI is fine and a replacement is seconds away. Module load measured
|
|
498
|
+
* ~0.36s, so 2s is roughly 5x headroom without making a genuine loss wait.
|
|
499
|
+
*/
|
|
500
|
+
private static readonly MCP_REPLACEMENT_GRACE_MS;
|
|
491
501
|
/**
|
|
492
502
|
* Sticky "restart to revive the MCP server as soon as the pane idles" — same
|
|
493
503
|
* shape as pausePending, because the death is usually detected mid-turn when
|
|
@@ -496,6 +506,25 @@ export declare class Daemon extends EventEmitter {
|
|
|
496
506
|
*/
|
|
497
507
|
private mcpRestartPending;
|
|
498
508
|
private mcpRestartStaleTimer;
|
|
509
|
+
/** In-flight replacement grace; also the guard against three triggers stacking. */
|
|
510
|
+
private mcpRestartGraceTimer;
|
|
511
|
+
/**
|
|
512
|
+
* Which trigger the in-flight grace will commit under.
|
|
513
|
+
*
|
|
514
|
+
* The single-timer guard would otherwise SWALLOW a stale_timeout that lands
|
|
515
|
+
* during an idle-gated grace — and because the stale timer is one-shot and
|
|
516
|
+
* already cleared itself, the 30-minute backstop would be gone for good on an
|
|
517
|
+
* instance that never idles, which is the exact case it exists for.
|
|
518
|
+
*/
|
|
519
|
+
private mcpRestartGraceTrigger;
|
|
520
|
+
/**
|
|
521
|
+
* A dead-MCP sighting held back once, pending a possible replacement.
|
|
522
|
+
*
|
|
523
|
+
* Deliberately NOT the same field as `mcpDeathNotifiedForPid`: deferring must
|
|
524
|
+
* not consume the once-per-pid budget, or a suspicion that turns out to be a
|
|
525
|
+
* real loss could never be reported at all.
|
|
526
|
+
*/
|
|
527
|
+
private mcpDeathDeferredForPid;
|
|
499
528
|
/** Prevent in-flight monitor callbacks from re-arming after a pause. */
|
|
500
529
|
private runtimeMonitorsFrozen;
|
|
501
530
|
private errorWaitingForRecovery;
|
|
@@ -538,7 +567,22 @@ export declare class Daemon extends EventEmitter {
|
|
|
538
567
|
* process would have no client reading it. Only the CLI can restore its own
|
|
539
568
|
* tools — hence notify, and let the operator decide about restarting.
|
|
540
569
|
*/
|
|
570
|
+
/**
|
|
571
|
+
* An MCP server has connected and announced itself.
|
|
572
|
+
*
|
|
573
|
+
* Beyond the existing session bookkeeping this is the earliest evidence that
|
|
574
|
+
* tools are back — roughly 30s ahead of the next health tick, which is
|
|
575
|
+
* exactly the window #663's false restarts fired in.
|
|
576
|
+
*/
|
|
577
|
+
private handleMcpReady;
|
|
541
578
|
private checkMcpServerAlive;
|
|
579
|
+
/**
|
|
580
|
+
* Whether a dead MCP pid on this backend is more likely a CLI-driven
|
|
581
|
+
* replacement than a real loss. Codex only, and deliberately narrow: it is
|
|
582
|
+
* the backend whose released fix (0.146) confirms it tore down healthy MCP
|
|
583
|
+
* connections alongside the one it meant to restart.
|
|
584
|
+
*/
|
|
585
|
+
private suspectsTransientMcpReplacement;
|
|
542
586
|
/**
|
|
543
587
|
* Arm an automatic instance restart to revive a dead MCP server. Fires on the
|
|
544
588
|
* next busy→idle edge — or immediately when the pane is already idle, which is
|
|
@@ -547,7 +591,36 @@ export declare class Daemon extends EventEmitter {
|
|
|
547
591
|
* force-restarted: see that constant for why staying mute costs more.
|
|
548
592
|
*/
|
|
549
593
|
private armMcpRestartWhenIdle;
|
|
594
|
+
/**
|
|
595
|
+
* Single chokepoint for all three revival triggers (already_idle, idle_edge,
|
|
596
|
+
* stale_timeout) — and therefore the one place worth re-checking reality.
|
|
597
|
+
*
|
|
598
|
+
* The pending flag is armed by a health tick that saw the OLD pid die, and
|
|
599
|
+
* was previously fired without ever looking again. When a CLI legitimately
|
|
600
|
+
* respawns its own MCP child, the replacement is already serving by the time
|
|
601
|
+
* the turn ends — but the idle edge fires inside the ~30s before the next
|
|
602
|
+
* health tick can stand the request down, restarting a healthy instance. One
|
|
603
|
+
* external user was restarted 18 times in two days this way (#663).
|
|
604
|
+
*
|
|
605
|
+
* Re-reading here is safe for every backend: worst case it clears a pending
|
|
606
|
+
* restart that should not have existed.
|
|
607
|
+
*/
|
|
550
608
|
private fireMcpRestartRequest;
|
|
609
|
+
/**
|
|
610
|
+
* A recovery is already reshaping the CLI: crash-loop handling
|
|
611
|
+
* (healthCheckPaused), a respawn in flight (spawning), or a pause
|
|
612
|
+
* (isPaused / frozen monitors). Each ends in a fresh CLI — and with it a
|
|
613
|
+
* fresh MCP server — or in supervision ending, where a restart on top would
|
|
614
|
+
* fight the recovery. Either way the revival restart is moot.
|
|
615
|
+
*/
|
|
616
|
+
private mcpRestartCancelledByRecovery;
|
|
617
|
+
/**
|
|
618
|
+
* Emit a dead-MCP report that was held back as a suspected replacement, once
|
|
619
|
+
* it is clear no replacement is coming. Consumes the once-per-pid budget
|
|
620
|
+
* here — deferring was to avoid spending it on a false alarm, not to drop
|
|
621
|
+
* the incident.
|
|
622
|
+
*/
|
|
623
|
+
private reportDeferredMcpDeath;
|
|
551
624
|
private clearMcpRestartRequest;
|
|
552
625
|
private startHealthCheck;
|
|
553
626
|
/**
|
package/dist/daemon.js
CHANGED
|
@@ -22,6 +22,7 @@ import { HangDetector } from "./hang-detector.js";
|
|
|
22
22
|
import { writeSecretFile } from "./secret-file.js";
|
|
23
23
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
24
24
|
import { buildFleetInstructions } from "./instructions.js";
|
|
25
|
+
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
25
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
27
|
const __dirname = dirname(__filename);
|
|
27
28
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -138,19 +139,7 @@ export function writeLastInboundAt(instanceDir, timestamp) {
|
|
|
138
139
|
* message_id line.
|
|
139
140
|
*/
|
|
140
141
|
export function renderHandoffMetadata(meta) {
|
|
141
|
-
|
|
142
|
-
const add = (label, value) => {
|
|
143
|
-
if (value && value.trim())
|
|
144
|
-
rows.push(`${label}: ${value.trim()}`);
|
|
145
|
-
};
|
|
146
|
-
add("message_id", meta.message_id);
|
|
147
|
-
add("correlation_id", meta.correlation_id);
|
|
148
|
-
add("request_kind", meta.request_kind);
|
|
149
|
-
add("task_summary", meta.task_summary);
|
|
150
|
-
add("working_directory", meta.working_directory);
|
|
151
|
-
add("branch", meta.branch);
|
|
152
|
-
add("attachment_file_id", meta.attachment_file_id);
|
|
153
|
-
return rows.length ? `\n(${rows.join(" | ")})` : "";
|
|
142
|
+
return renderCrossInstanceHandoffMetadata(meta);
|
|
154
143
|
}
|
|
155
144
|
/** Headless inactivity timer used by the daemon and unit tests. */
|
|
156
145
|
export class AutoPauseController {
|
|
@@ -924,6 +913,16 @@ export class Daemon extends EventEmitter {
|
|
|
924
913
|
* past this point interrupting the turn costs less than staying mute.
|
|
925
914
|
*/
|
|
926
915
|
static MCP_RESTART_MAX_WAIT_MS = 30 * 60_000;
|
|
916
|
+
/**
|
|
917
|
+
* How long the revival restart waits, at the point of firing, for a
|
|
918
|
+
* CLI-respawned MCP server to announce itself.
|
|
919
|
+
*
|
|
920
|
+
* Codex <0.146 tears down and recreates ALL MCP connections on an auth/config
|
|
921
|
+
* change — healthy ones included — so the daemon sees the old pid die while
|
|
922
|
+
* the CLI is fine and a replacement is seconds away. Module load measured
|
|
923
|
+
* ~0.36s, so 2s is roughly 5x headroom without making a genuine loss wait.
|
|
924
|
+
*/
|
|
925
|
+
static MCP_REPLACEMENT_GRACE_MS = 2_000;
|
|
927
926
|
/**
|
|
928
927
|
* Sticky "restart to revive the MCP server as soon as the pane idles" — same
|
|
929
928
|
* shape as pausePending, because the death is usually detected mid-turn when
|
|
@@ -932,6 +931,25 @@ export class Daemon extends EventEmitter {
|
|
|
932
931
|
*/
|
|
933
932
|
mcpRestartPending = false;
|
|
934
933
|
mcpRestartStaleTimer = null;
|
|
934
|
+
/** In-flight replacement grace; also the guard against three triggers stacking. */
|
|
935
|
+
mcpRestartGraceTimer = null;
|
|
936
|
+
/**
|
|
937
|
+
* Which trigger the in-flight grace will commit under.
|
|
938
|
+
*
|
|
939
|
+
* The single-timer guard would otherwise SWALLOW a stale_timeout that lands
|
|
940
|
+
* during an idle-gated grace — and because the stale timer is one-shot and
|
|
941
|
+
* already cleared itself, the 30-minute backstop would be gone for good on an
|
|
942
|
+
* instance that never idles, which is the exact case it exists for.
|
|
943
|
+
*/
|
|
944
|
+
mcpRestartGraceTrigger = null;
|
|
945
|
+
/**
|
|
946
|
+
* A dead-MCP sighting held back once, pending a possible replacement.
|
|
947
|
+
*
|
|
948
|
+
* Deliberately NOT the same field as `mcpDeathNotifiedForPid`: deferring must
|
|
949
|
+
* not consume the once-per-pid budget, or a suspicion that turns out to be a
|
|
950
|
+
* real loss could never be reported at all.
|
|
951
|
+
*/
|
|
952
|
+
mcpDeathDeferredForPid = null;
|
|
935
953
|
/** Prevent in-flight monitor callbacks from re-arming after a pause. */
|
|
936
954
|
runtimeMonitorsFrozen = false;
|
|
937
955
|
errorWaitingForRecovery = false; // true = error detected, waiting for ready pattern
|
|
@@ -1077,20 +1095,7 @@ export class Daemon extends EventEmitter {
|
|
|
1077
1095
|
this.handleToolCall(msg, socket);
|
|
1078
1096
|
}
|
|
1079
1097
|
else if (msg.type === "mcp_ready") {
|
|
1080
|
-
|
|
1081
|
-
if (sessionName) {
|
|
1082
|
-
this.socketSessionNames.set(socket, sessionName);
|
|
1083
|
-
socket.on("close", () => {
|
|
1084
|
-
this.socketSessionNames.delete(socket);
|
|
1085
|
-
// Notify fleet manager so it can clean up sessionRegistry
|
|
1086
|
-
if (sessionName !== this.name) {
|
|
1087
|
-
this.ipcServer?.broadcast({ type: "session_disconnected", sessionName });
|
|
1088
|
-
}
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
this.logger.debug({ sessionName }, "MCP channel server connected and ready");
|
|
1092
|
-
// Notify FleetManager's IPC client that MCP is ready
|
|
1093
|
-
this.ipcServer?.broadcast({ type: "mcp_ready", sessionName });
|
|
1098
|
+
this.handleMcpReady(msg, socket);
|
|
1094
1099
|
}
|
|
1095
1100
|
else if (msg.type === "query_sessions") {
|
|
1096
1101
|
// Fleet manager asks for all registered session names (catches sessions
|
|
@@ -1310,6 +1315,42 @@ export class Daemon extends EventEmitter {
|
|
|
1310
1315
|
* process would have no client reading it. Only the CLI can restore its own
|
|
1311
1316
|
* tools — hence notify, and let the operator decide about restarting.
|
|
1312
1317
|
*/
|
|
1318
|
+
/**
|
|
1319
|
+
* An MCP server has connected and announced itself.
|
|
1320
|
+
*
|
|
1321
|
+
* Beyond the existing session bookkeeping this is the earliest evidence that
|
|
1322
|
+
* tools are back — roughly 30s ahead of the next health tick, which is
|
|
1323
|
+
* exactly the window #663's false restarts fired in.
|
|
1324
|
+
*/
|
|
1325
|
+
handleMcpReady(msg, socket) {
|
|
1326
|
+
const sessionName = msg.sessionName;
|
|
1327
|
+
if (sessionName) {
|
|
1328
|
+
this.socketSessionNames.set(socket, sessionName);
|
|
1329
|
+
socket.on("close", () => {
|
|
1330
|
+
this.socketSessionNames.delete(socket);
|
|
1331
|
+
// Notify fleet manager so it can clean up sessionRegistry
|
|
1332
|
+
if (sessionName !== this.name) {
|
|
1333
|
+
this.ipcServer?.broadcast({ type: "session_disconnected", sessionName });
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
this.logger.debug({ sessionName, pid: msg.pid }, "MCP channel server connected and ready");
|
|
1338
|
+
// Gated on ownership: this ipcServer also accepts EXTERNAL sessions (a
|
|
1339
|
+
// `claude` run in another directory registers as `external-<cwd>-<pid>`).
|
|
1340
|
+
// Without the gate an unrelated session could stand down a restart armed
|
|
1341
|
+
// for THIS instance's genuinely dead server. The liveness re-read below is
|
|
1342
|
+
// a second guard, but ownership is the one that is semantically right.
|
|
1343
|
+
if (sessionName === this.name && this.mcpRestartPending) {
|
|
1344
|
+
const live = mcpServerState(this.instanceDir);
|
|
1345
|
+
if (live.state === "alive") {
|
|
1346
|
+
this.logger.info({ pid: live.pid }, "MCP server replaced — standing down the pending revival restart");
|
|
1347
|
+
this.mcpDeathNotifiedForPid = null;
|
|
1348
|
+
this.mcpDeathDeferredForPid = null;
|
|
1349
|
+
this.clearMcpRestartRequest();
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
this.ipcServer?.broadcast({ type: "mcp_ready", sessionName });
|
|
1353
|
+
}
|
|
1313
1354
|
checkMcpServerAlive() {
|
|
1314
1355
|
if (this.isPaused)
|
|
1315
1356
|
return;
|
|
@@ -1318,26 +1359,53 @@ export class Daemon extends EventEmitter {
|
|
|
1318
1359
|
return;
|
|
1319
1360
|
if (status.state === "alive") {
|
|
1320
1361
|
this.mcpDeathNotifiedForPid = null; // the CLI respawned it — re-arm
|
|
1362
|
+
this.mcpDeathDeferredForPid = null;
|
|
1321
1363
|
this.clearMcpRestartRequest(); // tools are back — stand down a pending auto-restart
|
|
1322
1364
|
return;
|
|
1323
1365
|
}
|
|
1324
1366
|
// Dead: report once per pid, and at most once per cooldown window.
|
|
1325
1367
|
if (this.mcpDeathNotifiedForPid === status.pid)
|
|
1326
1368
|
return;
|
|
1327
|
-
if (Date.now() - this.lastMcpDeathNotifiedAt < Daemon.MCP_DEATH_COOLDOWN_MS)
|
|
1328
|
-
return;
|
|
1329
|
-
this.mcpDeathNotifiedForPid = status.pid;
|
|
1330
|
-
this.lastMcpDeathNotifiedAt = Date.now();
|
|
1331
1369
|
// An auth-broken CLI cannot keep an MCP server alive, and restarting it
|
|
1332
1370
|
// only loops back to the sign-in screen — hold the auto-restart until a
|
|
1333
1371
|
// re-login (which restarts the instance and replaces this daemon anyway).
|
|
1334
1372
|
const authSuspected = this.authFailureUnresolved;
|
|
1335
1373
|
const autoRestart = this.config.mcp_auto_restart !== false && !authSuspected;
|
|
1374
|
+
// Codex <0.146 kills and recreates its MCP connections wholesale, so the
|
|
1375
|
+
// FIRST sighting of a dead pid under a live Codex is more likely a swap
|
|
1376
|
+
// than a loss. Hold the alarm for one tick — only the alarm: the restart is
|
|
1377
|
+
// still armed, and the chokepoint's re-read cancels it if the replacement
|
|
1378
|
+
// arrives. Not generalised to other backends, where a dead MCP under a live
|
|
1379
|
+
// CLI is a genuine incident that silence would mask.
|
|
1380
|
+
//
|
|
1381
|
+
// Crucially this does NOT touch mcpDeathNotifiedForPid or the cooldown
|
|
1382
|
+
// clock: if the suspicion is wrong the incident must still be reportable.
|
|
1383
|
+
if (this.suspectsTransientMcpReplacement() && this.mcpDeathDeferredForPid !== status.pid) {
|
|
1384
|
+
this.mcpDeathDeferredForPid = status.pid;
|
|
1385
|
+
this.logger.warn({ pid: status.pid }, "MCP server pid is gone under a live Codex — holding the alarm one tick for a possible replacement");
|
|
1386
|
+
if (autoRestart)
|
|
1387
|
+
this.armMcpRestartWhenIdle();
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (Date.now() - this.lastMcpDeathNotifiedAt < Daemon.MCP_DEATH_COOLDOWN_MS)
|
|
1391
|
+
return;
|
|
1392
|
+
this.mcpDeathNotifiedForPid = status.pid;
|
|
1393
|
+
this.mcpDeathDeferredForPid = null;
|
|
1394
|
+
this.lastMcpDeathNotifiedAt = Date.now();
|
|
1336
1395
|
this.logger.error({ pid: status.pid, autoRestart, authSuspected }, "MCP server process is gone — instance has no agend tools");
|
|
1337
1396
|
this.emit("mcp_died", { name: this.name, pid: status.pid, autoRestart, authSuspected });
|
|
1338
1397
|
if (autoRestart)
|
|
1339
1398
|
this.armMcpRestartWhenIdle();
|
|
1340
1399
|
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Whether a dead MCP pid on this backend is more likely a CLI-driven
|
|
1402
|
+
* replacement than a real loss. Codex only, and deliberately narrow: it is
|
|
1403
|
+
* the backend whose released fix (0.146) confirms it tore down healthy MCP
|
|
1404
|
+
* connections alongside the one it meant to restart.
|
|
1405
|
+
*/
|
|
1406
|
+
suspectsTransientMcpReplacement() {
|
|
1407
|
+
return (this.config.backend ?? this.backend?.binaryName) === "codex";
|
|
1408
|
+
}
|
|
1341
1409
|
/**
|
|
1342
1410
|
* Arm an automatic instance restart to revive a dead MCP server. Fires on the
|
|
1343
1411
|
* next busy→idle edge — or immediately when the pane is already idle, which is
|
|
@@ -1360,21 +1428,110 @@ export class Daemon extends EventEmitter {
|
|
|
1360
1428
|
this.fireMcpRestartRequest("already_idle");
|
|
1361
1429
|
}
|
|
1362
1430
|
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Single chokepoint for all three revival triggers (already_idle, idle_edge,
|
|
1433
|
+
* stale_timeout) — and therefore the one place worth re-checking reality.
|
|
1434
|
+
*
|
|
1435
|
+
* The pending flag is armed by a health tick that saw the OLD pid die, and
|
|
1436
|
+
* was previously fired without ever looking again. When a CLI legitimately
|
|
1437
|
+
* respawns its own MCP child, the replacement is already serving by the time
|
|
1438
|
+
* the turn ends — but the idle edge fires inside the ~30s before the next
|
|
1439
|
+
* health tick can stand the request down, restarting a healthy instance. One
|
|
1440
|
+
* external user was restarted 18 times in two days this way (#663).
|
|
1441
|
+
*
|
|
1442
|
+
* Re-reading here is safe for every backend: worst case it clears a pending
|
|
1443
|
+
* restart that should not have existed.
|
|
1444
|
+
*/
|
|
1363
1445
|
fireMcpRestartRequest(trigger) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
if (this.healthCheckPaused || this.spawning || this.isPaused || this.runtimeMonitorsFrozen) {
|
|
1371
|
-
this.logger.warn({ trigger }, "MCP revival restart cancelled — instance is already pausing/spawning/recovering");
|
|
1446
|
+
if (this.mcpRestartCancelledByRecovery(trigger))
|
|
1447
|
+
return;
|
|
1448
|
+
// Authoritative re-read — the CLI may have replaced the server since the
|
|
1449
|
+
// health tick that armed this.
|
|
1450
|
+
if (mcpServerState(this.instanceDir).state === "alive") {
|
|
1451
|
+
this.logger.info({ trigger }, "MCP revival restart stood down — a live MCP server is already serving");
|
|
1372
1452
|
this.clearMcpRestartRequest();
|
|
1373
1453
|
return;
|
|
1374
1454
|
}
|
|
1455
|
+
// Not back yet: give a CLI-driven replacement a bounded moment rather than
|
|
1456
|
+
// restarting the instance out from under it. One timer covers all three
|
|
1457
|
+
// triggers; a second trigger during the wait is a no-op.
|
|
1458
|
+
if (this.mcpRestartGraceTimer) {
|
|
1459
|
+
// stale_timeout outranks the idle-gated triggers: it is the backstop for
|
|
1460
|
+
// a turn that never ends, so it must not be lost to an in-flight grace.
|
|
1461
|
+
if (trigger === "stale_timeout")
|
|
1462
|
+
this.mcpRestartGraceTrigger = "stale_timeout";
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
this.mcpRestartGraceTrigger = trigger;
|
|
1466
|
+
this.mcpRestartGraceTimer = setTimeout(() => {
|
|
1467
|
+
this.mcpRestartGraceTimer = null;
|
|
1468
|
+
const committed = this.mcpRestartGraceTrigger ?? trigger;
|
|
1469
|
+
this.mcpRestartGraceTrigger = null;
|
|
1470
|
+
if (!this.mcpRestartPending || this.runtimeMonitorsFrozen)
|
|
1471
|
+
return;
|
|
1472
|
+
if (this.mcpRestartCancelledByRecovery(committed))
|
|
1473
|
+
return;
|
|
1474
|
+
if (mcpServerState(this.instanceDir).state === "alive") {
|
|
1475
|
+
this.logger.info({ trigger: committed }, "MCP revival restart stood down — replacement came up during the grace window");
|
|
1476
|
+
this.clearMcpRestartRequest();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
// Re-check the idle gate. The grace turned what used to be a synchronous
|
|
1480
|
+
// decision into a 2s window, and "idle edge, then a queued message lands"
|
|
1481
|
+
// is the NORMAL ordering for queued delivery — so without this the
|
|
1482
|
+
// restart tears down a turn that started during the wait. Worse than the
|
|
1483
|
+
// race it replaced: the #485 proxy reply only runs at turn end, so an
|
|
1484
|
+
// in-flight answer would be swallowed with no fallback. Only the
|
|
1485
|
+
// 30-minute stale timer may interrupt a live turn, by design.
|
|
1486
|
+
//
|
|
1487
|
+
// Deliberately does NOT clear pending: the MCP really is gone, so the
|
|
1488
|
+
// request must survive to the next idle edge (which re-enters here), and
|
|
1489
|
+
// the stale timer keeps running underneath as the backstop.
|
|
1490
|
+
if (committed !== "stale_timeout" && !(this.instanceState === "idle" && this.pasteQueueDepth === 0)) {
|
|
1491
|
+
this.logger.info({ trigger: committed, instanceState: this.instanceState, pasteQueueDepth: this.pasteQueueDepth }, "MCP revival restart deferred — the pane went busy during the grace window");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
// The suspicion was wrong: a real loss. Report it BEFORE asking for the
|
|
1495
|
+
// restart — instance-lifecycle's suppression path documents that it
|
|
1496
|
+
// assumes the mcp_died notification has already gone out, and this daemon
|
|
1497
|
+
// does not survive the restart to send it afterwards.
|
|
1498
|
+
this.reportDeferredMcpDeath();
|
|
1499
|
+
this.clearMcpRestartRequest();
|
|
1500
|
+
this.logger.warn({ trigger: committed }, "Requesting instance restart to revive its MCP server");
|
|
1501
|
+
this.emit("mcp_restart_requested", { name: this.name, trigger: committed });
|
|
1502
|
+
}, Daemon.MCP_REPLACEMENT_GRACE_MS);
|
|
1503
|
+
this.mcpRestartGraceTimer.unref?.();
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* A recovery is already reshaping the CLI: crash-loop handling
|
|
1507
|
+
* (healthCheckPaused), a respawn in flight (spawning), or a pause
|
|
1508
|
+
* (isPaused / frozen monitors). Each ends in a fresh CLI — and with it a
|
|
1509
|
+
* fresh MCP server — or in supervision ending, where a restart on top would
|
|
1510
|
+
* fight the recovery. Either way the revival restart is moot.
|
|
1511
|
+
*/
|
|
1512
|
+
mcpRestartCancelledByRecovery(trigger) {
|
|
1513
|
+
if (!(this.healthCheckPaused || this.spawning || this.isPaused || this.runtimeMonitorsFrozen))
|
|
1514
|
+
return false;
|
|
1515
|
+
this.logger.warn({ trigger }, "MCP revival restart cancelled — instance is already pausing/spawning/recovering");
|
|
1375
1516
|
this.clearMcpRestartRequest();
|
|
1376
|
-
|
|
1377
|
-
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Emit a dead-MCP report that was held back as a suspected replacement, once
|
|
1521
|
+
* it is clear no replacement is coming. Consumes the once-per-pid budget
|
|
1522
|
+
* here — deferring was to avoid spending it on a false alarm, not to drop
|
|
1523
|
+
* the incident.
|
|
1524
|
+
*/
|
|
1525
|
+
reportDeferredMcpDeath() {
|
|
1526
|
+
const pid = this.mcpDeathDeferredForPid;
|
|
1527
|
+
if (pid == null || this.mcpDeathNotifiedForPid === pid)
|
|
1528
|
+
return;
|
|
1529
|
+
const authSuspected = this.authFailureUnresolved;
|
|
1530
|
+
const autoRestart = this.config.mcp_auto_restart !== false && !authSuspected;
|
|
1531
|
+
this.mcpDeathNotifiedForPid = pid;
|
|
1532
|
+
this.lastMcpDeathNotifiedAt = Date.now();
|
|
1533
|
+
this.logger.error({ pid, autoRestart, authSuspected }, "MCP server process is gone — no replacement arrived, instance has no agend tools");
|
|
1534
|
+
this.emit("mcp_died", { name: this.name, pid, autoRestart, authSuspected });
|
|
1378
1535
|
}
|
|
1379
1536
|
clearMcpRestartRequest() {
|
|
1380
1537
|
this.mcpRestartPending = false;
|
|
@@ -1382,6 +1539,11 @@ export class Daemon extends EventEmitter {
|
|
|
1382
1539
|
clearTimeout(this.mcpRestartStaleTimer);
|
|
1383
1540
|
this.mcpRestartStaleTimer = null;
|
|
1384
1541
|
}
|
|
1542
|
+
if (this.mcpRestartGraceTimer) {
|
|
1543
|
+
clearTimeout(this.mcpRestartGraceTimer);
|
|
1544
|
+
this.mcpRestartGraceTimer = null;
|
|
1545
|
+
}
|
|
1546
|
+
this.mcpRestartGraceTrigger = null;
|
|
1385
1547
|
}
|
|
1386
1548
|
startHealthCheck() {
|
|
1387
1549
|
if (this.runtimeMonitorsFrozen || this.healthCheckTimer)
|
|
@@ -3112,16 +3274,7 @@ export class Daemon extends EventEmitter {
|
|
|
3112
3274
|
const fromInstance = meta.from_instance;
|
|
3113
3275
|
let formatted;
|
|
3114
3276
|
if (fromInstance) {
|
|
3115
|
-
|
|
3116
|
-
// instance name in parens so the recipient's send_to_instance target is valid.
|
|
3117
|
-
const fromLabel = meta.from_display ? `${meta.from_display} (${fromInstance})` : fromInstance;
|
|
3118
|
-
formatted = `[from:${fromLabel}] ${content}`;
|
|
3119
|
-
formatted += renderHandoffMetadata(meta);
|
|
3120
|
-
// A delegated task that requires a reply must not read like a chatty FYI —
|
|
3121
|
-
// the "you may stay silent" line is for the latter only.
|
|
3122
|
-
formatted += meta.requires_reply === "true"
|
|
3123
|
-
? "\n(A reply IS required: use report_result with the correlation_id above — or send_to_instance. Not direct text.)"
|
|
3124
|
-
: "\n(If you need to reply, use send_to_instance tool, NOT direct text. If there is nothing to add, you may stay silent.)";
|
|
3277
|
+
formatted = formatCrossInstanceInboundMessage(content, meta);
|
|
3125
3278
|
}
|
|
3126
3279
|
else {
|
|
3127
3280
|
const via = meta.source ? ` via ${meta.source}` : "";
|
|
@@ -3303,8 +3456,19 @@ export class Daemon extends EventEmitter {
|
|
|
3303
3456
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
3304
3457
|
// the previous turn's tool list.
|
|
3305
3458
|
this.resetToolProgress();
|
|
3306
|
-
if (await this.deliverMessage(formatted, status, { deliveryEpoch }))
|
|
3459
|
+
if (await this.deliverMessage(formatted, status, { deliveryEpoch })) {
|
|
3307
3460
|
this.markTurnStarted(meta, formatted);
|
|
3461
|
+
}
|
|
3462
|
+
else if (meta.from_instance && this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3463
|
+
const error = this.tmux?.getLastPasteError?.() ?? "target pane rejected the delivery";
|
|
3464
|
+
this.ipcServer?.broadcast({
|
|
3465
|
+
type: "cross_instance_delivery_failed",
|
|
3466
|
+
senderSession: meta.from_instance,
|
|
3467
|
+
targetInstance: this.name,
|
|
3468
|
+
correlationId: meta.correlation_id ?? "unknown",
|
|
3469
|
+
error,
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3308
3472
|
}
|
|
3309
3473
|
finally {
|
|
3310
3474
|
this.pasteQueueDepth--;
|
|
@@ -3480,7 +3644,16 @@ export class Daemon extends EventEmitter {
|
|
|
3480
3644
|
const pasteStartedAt = Date.now();
|
|
3481
3645
|
const pasted = await this.tmux.pasteBuffer(formatted);
|
|
3482
3646
|
if (!pasted) {
|
|
3483
|
-
this.
|
|
3647
|
+
const tmuxError = this.tmux.getLastPasteError?.() ?? "unknown tmux paste failure";
|
|
3648
|
+
const recoverable = this.tmux.isLastPasteFailureRecoverable?.() ?? true;
|
|
3649
|
+
this.logger[recoverable ? "warn" : "error"]({ attempt, tmuxError, recoverable }, recoverable
|
|
3650
|
+
? "pasteBuffer failed — recovering window and backing off"
|
|
3651
|
+
: "pasteBuffer failed — non-retryable tmux error");
|
|
3652
|
+
if (!recoverable) {
|
|
3653
|
+
if (status)
|
|
3654
|
+
this.emit("message_failed", status);
|
|
3655
|
+
return false;
|
|
3656
|
+
}
|
|
3484
3657
|
windowId = (await this.recoverWindow()) ?? windowId;
|
|
3485
3658
|
if (attempt < maxAttempts)
|
|
3486
3659
|
await new Promise(r => setTimeout(r, 2000));
|
|
@@ -3562,7 +3735,9 @@ export class Daemon extends EventEmitter {
|
|
|
3562
3735
|
}
|
|
3563
3736
|
const repasted = await this.tmux.pasteBuffer(formatted);
|
|
3564
3737
|
if (!repasted) {
|
|
3565
|
-
this.logger.error(
|
|
3738
|
+
this.logger.error({
|
|
3739
|
+
tmuxError: this.tmux.getLastPasteError?.() ?? "unknown tmux paste failure",
|
|
3740
|
+
}, "Idle-gated redelivery paste failed after native-queue silent loss");
|
|
3566
3741
|
if (status)
|
|
3567
3742
|
this.emit("message_failed", status); // ❌
|
|
3568
3743
|
return false;
|