@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.
Files changed (45) hide show
  1. package/dist/backend/codex.js +13 -0
  2. package/dist/backend/codex.js.map +1 -1
  3. package/dist/channel/mcp-server.js +11 -3
  4. package/dist/channel/mcp-server.js.map +1 -1
  5. package/dist/cli.js +14 -14
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config-validator.js +6 -0
  8. package/dist/config-validator.js.map +1 -1
  9. package/dist/config.d.ts +2 -0
  10. package/dist/config.js +3 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/cross-instance-envelope.d.ts +6 -0
  13. package/dist/cross-instance-envelope.js +30 -0
  14. package/dist/cross-instance-envelope.js.map +1 -0
  15. package/dist/daemon.d.ts +73 -0
  16. package/dist/daemon.js +229 -54
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/fleet-manager.js +33 -0
  19. package/dist/fleet-manager.js.map +1 -1
  20. package/dist/locale.js +168 -0
  21. package/dist/locale.js.map +1 -1
  22. package/dist/outbound-handlers.js +57 -13
  23. package/dist/outbound-handlers.js.map +1 -1
  24. package/dist/outbound-schemas.d.ts +2 -1
  25. package/dist/outbound-schemas.js +2 -0
  26. package/dist/outbound-schemas.js.map +1 -1
  27. package/dist/tmux-manager.d.ts +7 -0
  28. package/dist/tmux-manager.js +62 -14
  29. package/dist/tmux-manager.js.map +1 -1
  30. package/dist/types.d.ts +2 -0
  31. package/dist/ui/view.html +124 -15
  32. package/dist/usage/format-rich.js +24 -23
  33. package/dist/usage/format-rich.js.map +1 -1
  34. package/dist/usage/i18n-keys.d.ts +7 -0
  35. package/dist/usage/i18n-keys.js +34 -0
  36. package/dist/usage/i18n-keys.js.map +1 -0
  37. package/dist/usage/i18n.d.ts +5 -0
  38. package/dist/usage/i18n.js +27 -0
  39. package/dist/usage/i18n.js.map +1 -0
  40. package/dist/usage/providers.d.ts +12 -0
  41. package/dist/usage/providers.js +132 -73
  42. package/dist/usage/providers.js.map +1 -1
  43. package/dist/usage/usage-api.js +22 -21
  44. package/dist/usage/usage-api.js.map +1 -1
  45. 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. */
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
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
- const rows = [];
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
- const sessionName = msg.sessionName;
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
- // A recovery is already reshaping the CLI: crash-loop handling
1365
- // (healthCheckPaused), a respawn in flight (spawning), or a pause
1366
- // (isPaused / frozen monitors). Each of those paths ends in a fresh CLI —
1367
- // and with it a fresh MCP server — or in supervision ending, where a
1368
- // restart on top would fight the recovery. Either way the revival restart
1369
- // is moot: cancel it rather than defer it.
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
- this.logger.warn({ trigger }, "Requesting instance restart to revive its MCP server");
1377
- this.emit("mcp_restart_requested", { name: this.name, trigger });
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
- // #77: show the sender's display name for readability, keeping the machine
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.logger.warn({ attempt }, "pasteBuffer failed — recovering window and backing off");
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("Idle-gated redelivery paste failed after native-queue silent loss");
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;