aicodeman 1.5.1 → 1.6.0

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 (122) hide show
  1. package/dist/remote-hosts.d.ts +61 -1
  2. package/dist/remote-hosts.d.ts.map +1 -1
  3. package/dist/remote-hosts.js +142 -0
  4. package/dist/remote-hosts.js.map +1 -1
  5. package/dist/remote-reconnect.d.ts +129 -0
  6. package/dist/remote-reconnect.d.ts.map +1 -0
  7. package/dist/remote-reconnect.js +119 -0
  8. package/dist/remote-reconnect.js.map +1 -0
  9. package/dist/services/unified-session-service.d.ts +13 -1
  10. package/dist/services/unified-session-service.d.ts.map +1 -1
  11. package/dist/services/unified-session-service.js +88 -3
  12. package/dist/services/unified-session-service.js.map +1 -1
  13. package/dist/session-order.d.ts +40 -0
  14. package/dist/session-order.d.ts.map +1 -0
  15. package/dist/session-order.js +67 -0
  16. package/dist/session-order.js.map +1 -0
  17. package/dist/session.d.ts +39 -0
  18. package/dist/session.d.ts.map +1 -1
  19. package/dist/session.js +87 -19
  20. package/dist/session.js.map +1 -1
  21. package/dist/state-store.d.ts +12 -0
  22. package/dist/state-store.d.ts.map +1 -1
  23. package/dist/state-store.js +33 -0
  24. package/dist/state-store.js.map +1 -1
  25. package/dist/tmux-manager.d.ts +66 -0
  26. package/dist/tmux-manager.d.ts.map +1 -1
  27. package/dist/tmux-manager.js +226 -2
  28. package/dist/tmux-manager.js.map +1 -1
  29. package/dist/types/app-state.d.ts +2 -0
  30. package/dist/types/app-state.d.ts.map +1 -1
  31. package/dist/types/app-state.js.map +1 -1
  32. package/dist/types/session.d.ts +42 -0
  33. package/dist/types/session.d.ts.map +1 -1
  34. package/dist/types/session.js.map +1 -1
  35. package/dist/web/public/admin-ui.js.gz +0 -0
  36. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  37. package/dist/web/public/app.b78b0465.js +38 -0
  38. package/dist/web/public/app.b78b0465.js.br +0 -0
  39. package/dist/web/public/app.b78b0465.js.gz +0 -0
  40. package/dist/web/public/{constants.ad3a8031.js → constants.21a0a83e.js} +9 -0
  41. package/dist/web/public/constants.21a0a83e.js.br +0 -0
  42. package/dist/web/public/{constants.ad3a8031.js.gz → constants.21a0a83e.js.gz} +0 -0
  43. package/dist/web/public/cron-ui.js.gz +0 -0
  44. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  45. package/dist/web/public/index.html +29 -10
  46. package/dist/web/public/index.html.br +0 -0
  47. package/dist/web/public/index.html.gz +0 -0
  48. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  49. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  50. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  51. package/dist/web/public/mobile.597136a6.css.gz +0 -0
  52. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  53. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  54. package/dist/web/public/{panels-ui.3c9b0465.js → panels-ui.58582d4c.js} +2 -2
  55. package/dist/web/public/panels-ui.58582d4c.js.br +0 -0
  56. package/dist/web/public/panels-ui.58582d4c.js.gz +0 -0
  57. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  58. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  59. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  60. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  61. package/dist/web/public/{session-ui.e5d4f9b5.js → session-ui.0cf86dd4.js} +30 -22
  62. package/dist/web/public/session-ui.0cf86dd4.js.br +0 -0
  63. package/dist/web/public/session-ui.0cf86dd4.js.gz +0 -0
  64. package/dist/web/public/{settings-ui.29214cf1.js → settings-ui.bc5f4792.js} +2 -2
  65. package/dist/web/public/settings-ui.bc5f4792.js.br +0 -0
  66. package/dist/web/public/settings-ui.bc5f4792.js.gz +0 -0
  67. package/dist/web/public/styles.7295e6f4.css +1 -0
  68. package/dist/web/public/styles.7295e6f4.css.br +0 -0
  69. package/dist/web/public/styles.7295e6f4.css.gz +0 -0
  70. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  71. package/dist/web/public/sw.js.gz +0 -0
  72. package/dist/web/public/terminal-ui.9ea677e9.js +3 -0
  73. package/dist/web/public/terminal-ui.9ea677e9.js.br +0 -0
  74. package/dist/web/public/terminal-ui.9ea677e9.js.gz +0 -0
  75. package/dist/web/public/ultracode-panel.js.gz +0 -0
  76. package/dist/web/public/ultracode-windows.js.gz +0 -0
  77. package/dist/web/public/upload.html.gz +0 -0
  78. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  79. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  80. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  81. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  82. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  83. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  84. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  85. package/dist/web/public/vendor/xterm.css.gz +0 -0
  86. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  87. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  88. package/dist/web/routes/case-routes.d.ts.map +1 -1
  89. package/dist/web/routes/case-routes.js +19 -1
  90. package/dist/web/routes/case-routes.js.map +1 -1
  91. package/dist/web/routes/session-routes.d.ts.map +1 -1
  92. package/dist/web/routes/session-routes.js +140 -3
  93. package/dist/web/routes/session-routes.js.map +1 -1
  94. package/dist/web/schemas.d.ts +13 -0
  95. package/dist/web/schemas.d.ts.map +1 -1
  96. package/dist/web/schemas.js +31 -0
  97. package/dist/web/schemas.js.map +1 -1
  98. package/dist/web/server.d.ts +9 -0
  99. package/dist/web/server.d.ts.map +1 -1
  100. package/dist/web/server.js +57 -1
  101. package/dist/web/server.js.map +1 -1
  102. package/dist/web/sse-events.d.ts +15 -0
  103. package/dist/web/sse-events.d.ts.map +1 -1
  104. package/dist/web/sse-events.js +18 -0
  105. package/dist/web/sse-events.js.map +1 -1
  106. package/package.json +1 -1
  107. package/dist/web/public/app.1ccca158.js +0 -38
  108. package/dist/web/public/app.1ccca158.js.br +0 -0
  109. package/dist/web/public/app.1ccca158.js.gz +0 -0
  110. package/dist/web/public/constants.ad3a8031.js.br +0 -0
  111. package/dist/web/public/panels-ui.3c9b0465.js.br +0 -0
  112. package/dist/web/public/panels-ui.3c9b0465.js.gz +0 -0
  113. package/dist/web/public/session-ui.e5d4f9b5.js.br +0 -0
  114. package/dist/web/public/session-ui.e5d4f9b5.js.gz +0 -0
  115. package/dist/web/public/settings-ui.29214cf1.js.br +0 -0
  116. package/dist/web/public/settings-ui.29214cf1.js.gz +0 -0
  117. package/dist/web/public/styles.9e6cdb9d.css +0 -1
  118. package/dist/web/public/styles.9e6cdb9d.css.br +0 -0
  119. package/dist/web/public/styles.9e6cdb9d.css.gz +0 -0
  120. package/dist/web/public/terminal-ui.efda6083.js +0 -3
  121. package/dist/web/public/terminal-ui.efda6083.js.br +0 -0
  122. package/dist/web/public/terminal-ui.efda6083.js.gz +0 -0
@@ -213,6 +213,27 @@ export declare function buildDockerRemoveCommand(docker: SessionDocker): string;
213
213
  * launch path (createSession/respawnPane no-op under VITEST).
214
214
  */
215
215
  export declare function resolveDockerLaunchOptions(mode: SessionMode, docker: SessionDocker, sessionId: string, resumeSessionId?: string): DockerLaunchOptions;
216
+ /**
217
+ * COD-105 — build the SSH command that ATTACHES to an EXISTING `codeman-*` tmux
218
+ * session on the remote host (one this Codeman didn't create — discovered via
219
+ * `listRemoteCodemanSessions`). Sibling of `buildRemoteLaunchCommand`.
220
+ *
221
+ * Emits:
222
+ * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
223
+ * 'tmux -L codeman attach -t <session>'
224
+ *
225
+ * - `attach` (NOT `new-session -A`) so we only join an existing session; the
226
+ * remote session keeps running independent of us, which is exactly why the
227
+ * resulting Codeman session is NON-OWNED (see `SessionRemote.owned`): closing
228
+ * the local tab must detach, never `kill-session` the remote.
229
+ * - The remote session name is shell-escaped so a value with metachars stays a
230
+ * single token inside the quoted tmux invocation.
231
+ * - COD-107 — connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
232
+ * arbitrary `-o`) come from the shared `buildSshConnectionArgs`, so attach
233
+ * connects identically to launch / discovery / the prereq probe. `-t` sits
234
+ * right after `ssh -o BatchMode=yes` (a PTY is required for interactive tmux).
235
+ */
236
+ export declare function buildRemoteAttachCommand(remote: SessionRemote, remoteSessionName: string): string;
216
237
  /**
217
238
  * Manages tmux sessions that wrap Claude CLI or shell processes.
218
239
  *
@@ -240,6 +261,15 @@ export declare class TmuxManager extends EventEmitter implements TerminalMultipl
240
261
  private mouseSyncInterval;
241
262
  /** Track last-known pane count per session to avoid unnecessary tmux set-option calls */
242
263
  private lastPaneCount;
264
+ /** Periodic watcher that re-establishes dropped remote sessions. */
265
+ private remoteReconnectInterval;
266
+ /** Per-session backoff/attempt bookkeeping (sessionId → state). */
267
+ private reconnectState;
268
+ /**
269
+ * Sessions excluded from auto-reconnect because they are being intentionally
270
+ * torn down (killed/detached/stopping). A guarded session is NEVER revived.
271
+ */
272
+ private reconnectGuard;
243
273
  private trueColorConfigured;
244
274
  constructor();
245
275
  /** The dedicated tmux socket all Codeman sessions live on (see {@link TerminalMultiplexer.muxSocket}). */
@@ -349,6 +379,42 @@ export declare class TmuxManager extends EventEmitter implements TerminalMultipl
349
379
  */
350
380
  startMouseModeSync(intervalMs?: number): void;
351
381
  stopMouseModeSync(): void;
382
+ /**
383
+ * Start the remote-reconnect watcher (COD-108). Each tick, for every tracked
384
+ * session with `session.remote` whose local pane is DEAD, not intentionally
385
+ * guarded, and within its backoff budget, emit `remoteSessionDropped` so the
386
+ * session owner reattaches (re-running the idempotent remote command rejoins
387
+ * the durable remote tmux session). After the attempt cap, emit
388
+ * `remoteReconnectExhausted` once and go quiet.
389
+ *
390
+ * No-op tick body under `IS_TEST_MODE` (mirrors `startMouseModeSync`): tests
391
+ * drive the logic deterministically via {@link runRemoteReconnectTick}.
392
+ */
393
+ startRemoteReconnectWatcher(intervalMs?: number): void;
394
+ stopRemoteReconnectWatcher(): void;
395
+ /**
396
+ * Run ONE watcher tick. Extracted (and given an injected `now`/`enabled`) so
397
+ * the reconnect logic is deterministically testable even though the live
398
+ * `setInterval` body no-ops under test mode. For each remote session it
399
+ * applies the pure {@link decideReconnect} decision and translates the result
400
+ * into events + backoff/state transitions. Public for tests + the watcher.
401
+ */
402
+ runRemoteReconnectTick(now: number, enabled: boolean): void;
403
+ /**
404
+ * Tell the watcher a reattach attempt for `sessionId` finished. On success,
405
+ * reset the backoff so the session is healthy again; on failure, just clear
406
+ * the in-flight flag so the next due tick can retry under the existing
407
+ * backoff schedule. Called by the session owner after `respawnPane`.
408
+ */
409
+ noteRemoteReconnect(sessionId: string, success: boolean): void;
410
+ /**
411
+ * Exclude a session from auto-reconnect (intentional teardown). Adds it to the
412
+ * guard set and drops any backoff state so a closed/killed tab — especially a
413
+ * non-owned remote DETACH — is never auto-revived. Idempotent.
414
+ */
415
+ guardRemoteReconnect(sessionId: string): void;
416
+ /** Clear all per-session reconnect + guard state (e.g. when a session is removed). */
417
+ clearRemoteReconnectState(sessionId: string): void;
352
418
  destroy(): void;
353
419
  registerSession(session: MuxSession): void;
354
420
  setAttached(sessionId: string, attached: boolean): void;
@@ -1 +1 @@
1
- {"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,7 @@ import { buildEffortCliArgs } from './session-cli-builder.js';
34
34
  import { buildSshConnectionArgs, defaultRemoteCommandForMode, remoteSshTarget } from './remote-hosts.js';
35
35
  import { buildDockerBaseArgs, buildDockerCreateArgs, containerApiUrl, CONTAINER_HOME, defaultDockerCommandForMode, hostGatewayAlias, resolveDockerClaudeArtifacts, resolveDockerCredentialArtifacts, } from './docker-hosts.js';
36
36
  import { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir, resolveCodexDir, resolveGeminiDir, } from './utils/index.js';
37
+ import { decideReconnect, advanceBackoff, freshReconnectState, resetReconnectState, } from './remote-reconnect.js';
37
38
  // ============================================================================
38
39
  // Timing Constants
39
40
  // ============================================================================
@@ -57,6 +58,8 @@ const TMUX_KILL_WAIT_MS = 200;
57
58
  const GRACEFUL_SHUTDOWN_WAIT_MS = 100;
58
59
  /** Default stats collection interval (2 seconds) */
59
60
  const DEFAULT_STATS_INTERVAL_MS = 2000;
61
+ /** Default remote-reconnect watcher poll interval (5 seconds) — COD-108 */
62
+ const DEFAULT_REMOTE_RECONNECT_INTERVAL_MS = 5000;
60
63
  /** Stable cwd for tmux server/pane launch; actual session cwd is reached inside the pane. */
61
64
  const TMUX_LAUNCH_CWD = '/tmp';
62
65
  /** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
@@ -77,6 +80,20 @@ export const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
77
80
  const IS_TEST_MODE = !!process.env.VITEST;
78
81
  /** Path to persisted mux session metadata */
79
82
  const MUX_SESSIONS_FILE = dataPath('mux-sessions.json');
83
+ /**
84
+ * COD-108 kill-switch: `remoteAutoReconnect` app setting (default ON). Read at
85
+ * call time (like headroom routing) so a settings change takes effect without a
86
+ * restart. Absent/non-boolean ⇒ true (feature on).
87
+ */
88
+ function isRemoteAutoReconnectEnabled() {
89
+ try {
90
+ const s = JSON.parse(readFileSync(dataPath('settings.json'), 'utf8'));
91
+ return typeof s.remoteAutoReconnect === 'boolean' ? s.remoteAutoReconnect : true;
92
+ }
93
+ catch {
94
+ return true;
95
+ }
96
+ }
80
97
  /** Regex to validate tmux session names (only allow safe characters) */
81
98
  const SAFE_MUX_NAME_PATTERN = /^codeman-[a-f0-9-]+$/;
82
99
  /** Legacy pattern for pre-rename sessions (claudeman- prefix) */
@@ -687,6 +704,13 @@ export function buildRemoteLaunchCommand(options) {
687
704
  `set -t ${remoteName} mouse off`,
688
705
  `set -t ${remoteName} prefix C-q`,
689
706
  'set -s escape-time 0',
707
+ // COD-106 — shared/collaborative sessions: tmux defaults to sizing a window
708
+ // to the SMALLEST attached client, so two Codemans at different viewports
709
+ // would fight (clamp to the smaller). `window-size latest` sizes to the
710
+ // most-recently-active client instead, so concurrent clients coexist.
711
+ // Per-session scoped (`set -t <name>`, matching #145's hardening) so a shared
712
+ // remote tmux server's other sessions keep their own sizing behavior.
713
+ `set -t ${remoteName} window-size latest`,
690
714
  ].join(' \\; ');
691
715
  // ssh runs its trailing args through the remote login shell, so the entire
692
716
  // tmux invocation is passed as one shell-quoted argument.
@@ -959,6 +983,48 @@ export function resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessio
959
983
  : [];
960
984
  return { mode, docker, sessionId, resumeSessionId, createContext, execEnv, execEnvNames, seedCopies };
961
985
  }
986
+ /**
987
+ * COD-105 — build the SSH command that ATTACHES to an EXISTING `codeman-*` tmux
988
+ * session on the remote host (one this Codeman didn't create — discovered via
989
+ * `listRemoteCodemanSessions`). Sibling of `buildRemoteLaunchCommand`.
990
+ *
991
+ * Emits:
992
+ * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
993
+ * 'tmux -L codeman attach -t <session>'
994
+ *
995
+ * - `attach` (NOT `new-session -A`) so we only join an existing session; the
996
+ * remote session keeps running independent of us, which is exactly why the
997
+ * resulting Codeman session is NON-OWNED (see `SessionRemote.owned`): closing
998
+ * the local tab must detach, never `kill-session` the remote.
999
+ * - The remote session name is shell-escaped so a value with metachars stays a
1000
+ * single token inside the quoted tmux invocation.
1001
+ * - COD-107 — connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
1002
+ * arbitrary `-o`) come from the shared `buildSshConnectionArgs`, so attach
1003
+ * connects identically to launch / discovery / the prereq probe. `-t` sits
1004
+ * right after `ssh -o BatchMode=yes` (a PTY is required for interactive tmux).
1005
+ */
1006
+ export function buildRemoteAttachCommand(remote, remoteSessionName) {
1007
+ const tmuxInvocation = `tmux -L codeman attach -t ${shellescape(remoteSessionName)}`;
1008
+ const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
1009
+ const sshParts = [ssh, batchMode, '-t', ...connectionArgs, remoteSshTarget(remote), shellescape(tmuxInvocation)];
1010
+ return sshParts.join(' ');
1011
+ }
1012
+ /**
1013
+ * COD-105 — choose the right remote ssh command for a session's ownership:
1014
+ * - NON-owned (`remote.owned === false`): ATTACH to a discovered remote tmux
1015
+ * session by its EXISTING name (`remote.remoteSessionName`, falling back to
1016
+ * this session's deterministic name). We only join — never create.
1017
+ * - owned (default): LAUNCH/attach-or-create via `buildRemoteLaunchCommand`
1018
+ * (COD-104), which we then own and may explicitly kill.
1019
+ */
1020
+ function buildRemoteSessionCommand(options) {
1021
+ const { remote, sessionId } = options;
1022
+ if (remote.owned === false) {
1023
+ const target = remote.remoteSessionName || remoteTmuxSessionName(sessionId);
1024
+ return buildRemoteAttachCommand(remote, target);
1025
+ }
1026
+ return buildRemoteLaunchCommand(options);
1027
+ }
962
1028
  /**
963
1029
  * Set sensitive environment variables on a tmux session via setenv.
964
1030
  * These are inherited by panes but not visible in ps output or tmux history.
@@ -1112,6 +1178,16 @@ export class TmuxManager extends EventEmitter {
1112
1178
  mouseSyncInterval = null;
1113
1179
  /** Track last-known pane count per session to avoid unnecessary tmux set-option calls */
1114
1180
  lastPaneCount = new Map();
1181
+ // ── COD-108 remote-reconnect watcher state ────────────────────────────────
1182
+ /** Periodic watcher that re-establishes dropped remote sessions. */
1183
+ remoteReconnectInterval = null;
1184
+ /** Per-session backoff/attempt bookkeeping (sessionId → state). */
1185
+ reconnectState = new Map();
1186
+ /**
1187
+ * Sessions excluded from auto-reconnect because they are being intentionally
1188
+ * torn down (killed/detached/stopping). A guarded session is NEVER revived.
1189
+ */
1190
+ reconnectGuard = new Set();
1115
1191
  trueColorConfigured = false;
1116
1192
  constructor() {
1117
1193
  super();
@@ -1396,7 +1472,7 @@ export class TmuxManager extends EventEmitter {
1396
1472
  const fullCmd = docker
1397
1473
  ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
1398
1474
  : remote
1399
- ? buildRemoteLaunchCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1475
+ ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1400
1476
  : localFullCmd;
1401
1477
  // Create tmux session in three steps to handle cold-start (no server running)
1402
1478
  // and avoid the race where the command exits before remain-on-exit is set:
@@ -1615,7 +1691,7 @@ export class TmuxManager extends EventEmitter {
1615
1691
  const fullCmd = docker
1616
1692
  ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
1617
1693
  : remote
1618
- ? buildRemoteLaunchCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1694
+ ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1619
1695
  : localFullCmd;
1620
1696
  try {
1621
1697
  // For OpenCode: set sensitive env vars via tmux setenv before respawn
@@ -1724,9 +1800,15 @@ export class TmuxManager extends EventEmitter {
1724
1800
  if (!session) {
1725
1801
  return false;
1726
1802
  }
1803
+ // COD-108: an intentional kill/detach must NEVER be auto-revived by the
1804
+ // remote-reconnect watcher. Guard BEFORE any teardown so a tick that fires
1805
+ // mid-kill (especially the non-owned DETACH early-return below, where the
1806
+ // dead local pane would otherwise look reconnectable) sees the guard.
1807
+ this.guardRemoteReconnect(sessionId);
1727
1808
  // TEST MODE: Remove from memory only — NEVER touch real tmux sessions
1728
1809
  if (IS_TEST_MODE) {
1729
1810
  this.sessions.delete(sessionId);
1811
+ this.clearRemoteReconnectState(sessionId);
1730
1812
  this.emit('sessionKilled', { sessionId });
1731
1813
  return true;
1732
1814
  }
@@ -1736,6 +1818,40 @@ export class TmuxManager extends EventEmitter {
1736
1818
  console.error(`[TmuxManager] BLOCKED: Refusing to kill own tmux session: ${session.muxName}`);
1737
1819
  return false;
1738
1820
  }
1821
+ // COD-105 — DETACH-NOT-KILL for NON-owned remote sessions.
1822
+ //
1823
+ // When this session was created by ATTACHING a remote tmux session another
1824
+ // Codeman owns (`remote.owned === false`), closing the tab must NOT propagate
1825
+ // a remote `tmux kill-session` — that would nuke work the remote's own
1826
+ // Codeman (or another instance) still relies on. We tear down ONLY the LOCAL
1827
+ // pane that holds the ssh client: killing the local ssh sends SIGHUP to its
1828
+ // remote `tmux attach`, which DETACHES (the durable remote session survives).
1829
+ //
1830
+ // This early return is the structural guarantee: no code below this point
1831
+ // (now or in future for owned sessions) can ever issue a remote kill-session
1832
+ // for a non-owned session. The only `kill-session` we run is on OUR LOCAL
1833
+ // socket (`this.tmux()` = `tmux -L codeman` on THIS host), which kills the
1834
+ // local pane — it does NOT reach the REMOTE socket.
1835
+ if (session.remote && session.remote.owned === false) {
1836
+ console.log(`[TmuxManager] DETACH (non-owned remote): tearing down local pane only for ${session.muxName}`);
1837
+ if (isValidMuxName(session.muxName)) {
1838
+ try {
1839
+ // Local socket only — detaches the remote session by killing the local ssh pane.
1840
+ execSync(`${this.tmux()} kill-session -t "${session.muxName}" 2>/dev/null`, {
1841
+ timeout: EXEC_TIMEOUT_MS,
1842
+ });
1843
+ }
1844
+ catch {
1845
+ // Local pane may already be gone.
1846
+ }
1847
+ }
1848
+ this.lastPaneCount.delete(session.muxName);
1849
+ this.sessions.delete(sessionId);
1850
+ this.clearRemoteReconnectState(sessionId);
1851
+ this.saveSessions();
1852
+ this.emit('sessionKilled', { sessionId });
1853
+ return true;
1854
+ }
1739
1855
  // Get current PID (may have changed)
1740
1856
  const currentPid = this.getPanePid(session.muxName) || session.pid;
1741
1857
  console.log(`[TmuxManager] Killing session ${session.muxName} (PID ${currentPid})`);
@@ -1834,6 +1950,7 @@ export class TmuxManager extends EventEmitter {
1834
1950
  }
1835
1951
  this.lastPaneCount.delete(session.muxName);
1836
1952
  this.sessions.delete(sessionId);
1953
+ this.clearRemoteReconnectState(sessionId);
1837
1954
  this.saveSessions();
1838
1955
  this.emit('sessionKilled', { sessionId });
1839
1956
  return true;
@@ -1895,6 +2012,7 @@ export class TmuxManager extends EventEmitter {
1895
2012
  else {
1896
2013
  dead.push(sessionId);
1897
2014
  this.sessions.delete(sessionId);
2015
+ this.clearRemoteReconnectState(sessionId);
1898
2016
  this.emit('sessionDied', { sessionId });
1899
2017
  }
1900
2018
  }
@@ -2131,9 +2249,115 @@ export class TmuxManager extends EventEmitter {
2131
2249
  }
2132
2250
  this.lastPaneCount.clear();
2133
2251
  }
2252
+ // ── COD-108 remote-session auto-reconnect watcher ─────────────────────────
2253
+ /**
2254
+ * Start the remote-reconnect watcher (COD-108). Each tick, for every tracked
2255
+ * session with `session.remote` whose local pane is DEAD, not intentionally
2256
+ * guarded, and within its backoff budget, emit `remoteSessionDropped` so the
2257
+ * session owner reattaches (re-running the idempotent remote command rejoins
2258
+ * the durable remote tmux session). After the attempt cap, emit
2259
+ * `remoteReconnectExhausted` once and go quiet.
2260
+ *
2261
+ * No-op tick body under `IS_TEST_MODE` (mirrors `startMouseModeSync`): tests
2262
+ * drive the logic deterministically via {@link runRemoteReconnectTick}.
2263
+ */
2264
+ startRemoteReconnectWatcher(intervalMs = DEFAULT_REMOTE_RECONNECT_INTERVAL_MS) {
2265
+ if (this.remoteReconnectInterval) {
2266
+ clearInterval(this.remoteReconnectInterval);
2267
+ }
2268
+ this.remoteReconnectInterval = setInterval(() => {
2269
+ if (IS_TEST_MODE)
2270
+ return;
2271
+ try {
2272
+ this.runRemoteReconnectTick(Date.now(), isRemoteAutoReconnectEnabled());
2273
+ }
2274
+ catch (err) {
2275
+ console.error('[TmuxManager] Remote reconnect watcher error:', err);
2276
+ }
2277
+ }, intervalMs);
2278
+ }
2279
+ stopRemoteReconnectWatcher() {
2280
+ if (this.remoteReconnectInterval) {
2281
+ clearInterval(this.remoteReconnectInterval);
2282
+ this.remoteReconnectInterval = null;
2283
+ }
2284
+ }
2285
+ /**
2286
+ * Run ONE watcher tick. Extracted (and given an injected `now`/`enabled`) so
2287
+ * the reconnect logic is deterministically testable even though the live
2288
+ * `setInterval` body no-ops under test mode. For each remote session it
2289
+ * applies the pure {@link decideReconnect} decision and translates the result
2290
+ * into events + backoff/state transitions. Public for tests + the watcher.
2291
+ */
2292
+ runRemoteReconnectTick(now, enabled) {
2293
+ for (const session of this.sessions.values()) {
2294
+ if (!session.remote)
2295
+ continue;
2296
+ const sessionId = session.sessionId;
2297
+ const state = this.reconnectState.get(sessionId);
2298
+ const action = decideReconnect({
2299
+ session: {
2300
+ sessionId,
2301
+ isRemote: true,
2302
+ paneDead: this.isPaneDead(session.muxName),
2303
+ },
2304
+ state,
2305
+ guarded: this.reconnectGuard.has(sessionId),
2306
+ enabled,
2307
+ now,
2308
+ });
2309
+ if (action.kind === 'emit') {
2310
+ const base = state ?? freshReconnectState();
2311
+ // Mark in-flight + advance backoff BEFORE emitting so a re-entrant tick
2312
+ // (or a synchronous listener) can never stack a second reconnect.
2313
+ this.reconnectState.set(sessionId, { ...advanceBackoff(base, now), inFlight: true });
2314
+ this.emit('remoteSessionDropped', { sessionId, attempt: action.attempt });
2315
+ }
2316
+ else if (action.kind === 'exhaust') {
2317
+ const base = state ?? freshReconnectState();
2318
+ if (!base.exhaustedEmitted) {
2319
+ this.reconnectState.set(sessionId, { ...base, exhausted: true, exhaustedEmitted: true });
2320
+ this.emit('remoteReconnectExhausted', { sessionId });
2321
+ }
2322
+ }
2323
+ // 'skip' → nothing to do.
2324
+ }
2325
+ }
2326
+ /**
2327
+ * Tell the watcher a reattach attempt for `sessionId` finished. On success,
2328
+ * reset the backoff so the session is healthy again; on failure, just clear
2329
+ * the in-flight flag so the next due tick can retry under the existing
2330
+ * backoff schedule. Called by the session owner after `respawnPane`.
2331
+ */
2332
+ noteRemoteReconnect(sessionId, success) {
2333
+ if (success) {
2334
+ this.reconnectState.set(sessionId, resetReconnectState());
2335
+ return;
2336
+ }
2337
+ const state = this.reconnectState.get(sessionId);
2338
+ if (state)
2339
+ this.reconnectState.set(sessionId, { ...state, inFlight: false });
2340
+ }
2341
+ /**
2342
+ * Exclude a session from auto-reconnect (intentional teardown). Adds it to the
2343
+ * guard set and drops any backoff state so a closed/killed tab — especially a
2344
+ * non-owned remote DETACH — is never auto-revived. Idempotent.
2345
+ */
2346
+ guardRemoteReconnect(sessionId) {
2347
+ this.reconnectGuard.add(sessionId);
2348
+ this.reconnectState.delete(sessionId);
2349
+ }
2350
+ /** Clear all per-session reconnect + guard state (e.g. when a session is removed). */
2351
+ clearRemoteReconnectState(sessionId) {
2352
+ this.reconnectState.delete(sessionId);
2353
+ this.reconnectGuard.delete(sessionId);
2354
+ }
2134
2355
  destroy() {
2135
2356
  this.stopStatsCollection();
2136
2357
  this.stopMouseModeSync();
2358
+ this.stopRemoteReconnectWatcher();
2359
+ this.reconnectState.clear();
2360
+ this.reconnectGuard.clear();
2137
2361
  }
2138
2362
  registerSession(session) {
2139
2363
  this.sessions.set(session.sessionId, session);