aicodeman 1.5.0 → 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 (143) hide show
  1. package/README.md +10 -8
  2. package/README.zh-CN.md +289 -92
  3. package/dist/docker-export.d.ts +13 -0
  4. package/dist/docker-export.d.ts.map +1 -1
  5. package/dist/docker-export.js +55 -8
  6. package/dist/docker-export.js.map +1 -1
  7. package/dist/docker-hosts.d.ts +36 -5
  8. package/dist/docker-hosts.d.ts.map +1 -1
  9. package/dist/docker-hosts.js +90 -16
  10. package/dist/docker-hosts.js.map +1 -1
  11. package/dist/remote-hosts.d.ts +61 -1
  12. package/dist/remote-hosts.d.ts.map +1 -1
  13. package/dist/remote-hosts.js +142 -0
  14. package/dist/remote-hosts.js.map +1 -1
  15. package/dist/remote-reconnect.d.ts +129 -0
  16. package/dist/remote-reconnect.d.ts.map +1 -0
  17. package/dist/remote-reconnect.js +119 -0
  18. package/dist/remote-reconnect.js.map +1 -0
  19. package/dist/services/unified-session-service.d.ts +13 -1
  20. package/dist/services/unified-session-service.d.ts.map +1 -1
  21. package/dist/services/unified-session-service.js +88 -3
  22. package/dist/services/unified-session-service.js.map +1 -1
  23. package/dist/session-order.d.ts +40 -0
  24. package/dist/session-order.d.ts.map +1 -0
  25. package/dist/session-order.js +67 -0
  26. package/dist/session-order.js.map +1 -0
  27. package/dist/session.d.ts +39 -0
  28. package/dist/session.d.ts.map +1 -1
  29. package/dist/session.js +87 -19
  30. package/dist/session.js.map +1 -1
  31. package/dist/state-store.d.ts +12 -0
  32. package/dist/state-store.d.ts.map +1 -1
  33. package/dist/state-store.js +33 -0
  34. package/dist/state-store.js.map +1 -1
  35. package/dist/tmux-manager.d.ts +66 -0
  36. package/dist/tmux-manager.d.ts.map +1 -1
  37. package/dist/tmux-manager.js +260 -8
  38. package/dist/tmux-manager.js.map +1 -1
  39. package/dist/types/app-state.d.ts +2 -0
  40. package/dist/types/app-state.d.ts.map +1 -1
  41. package/dist/types/app-state.js.map +1 -1
  42. package/dist/types/session.d.ts +43 -1
  43. package/dist/types/session.d.ts.map +1 -1
  44. package/dist/types/session.js +1 -1
  45. package/dist/types/session.js.map +1 -1
  46. package/dist/web/public/admin-ui.js.gz +0 -0
  47. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  48. package/dist/web/public/app.b78b0465.js +38 -0
  49. package/dist/web/public/app.b78b0465.js.br +0 -0
  50. package/dist/web/public/app.b78b0465.js.gz +0 -0
  51. package/dist/web/public/{constants.5ce97313.js → constants.21a0a83e.js} +10 -0
  52. package/dist/web/public/constants.21a0a83e.js.br +0 -0
  53. package/dist/web/public/{constants.5ce97313.js.gz → constants.21a0a83e.js.gz} +0 -0
  54. package/dist/web/public/cron-ui.js.gz +0 -0
  55. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  56. package/dist/web/public/index.html +32 -13
  57. package/dist/web/public/index.html.br +0 -0
  58. package/dist/web/public/index.html.gz +0 -0
  59. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  60. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  61. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  62. package/dist/web/public/mobile.597136a6.css +1 -0
  63. package/dist/web/public/mobile.597136a6.css.br +0 -0
  64. package/dist/web/public/mobile.597136a6.css.gz +0 -0
  65. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  66. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  67. package/dist/web/public/{panels-ui.3c9b0465.js → panels-ui.58582d4c.js} +2 -2
  68. package/dist/web/public/panels-ui.58582d4c.js.br +0 -0
  69. package/dist/web/public/panels-ui.58582d4c.js.gz +0 -0
  70. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  71. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  72. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  73. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  74. package/dist/web/public/session-ui.0cf86dd4.js +68 -0
  75. package/dist/web/public/session-ui.0cf86dd4.js.br +0 -0
  76. package/dist/web/public/session-ui.0cf86dd4.js.gz +0 -0
  77. package/dist/web/public/{settings-ui.29214cf1.js → settings-ui.bc5f4792.js} +2 -2
  78. package/dist/web/public/settings-ui.bc5f4792.js.br +0 -0
  79. package/dist/web/public/settings-ui.bc5f4792.js.gz +0 -0
  80. package/dist/web/public/styles.7295e6f4.css +1 -0
  81. package/dist/web/public/styles.7295e6f4.css.br +0 -0
  82. package/dist/web/public/styles.7295e6f4.css.gz +0 -0
  83. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  84. package/dist/web/public/sw.js.gz +0 -0
  85. package/dist/web/public/terminal-ui.9ea677e9.js +3 -0
  86. package/dist/web/public/terminal-ui.9ea677e9.js.br +0 -0
  87. package/dist/web/public/terminal-ui.9ea677e9.js.gz +0 -0
  88. package/dist/web/public/ultracode-panel.js.gz +0 -0
  89. package/dist/web/public/ultracode-windows.js.gz +0 -0
  90. package/dist/web/public/upload.html.gz +0 -0
  91. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  92. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  93. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  94. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  95. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  96. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  97. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  98. package/dist/web/public/vendor/xterm.css.gz +0 -0
  99. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  100. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  101. package/dist/web/routes/case-routes.d.ts +2 -2
  102. package/dist/web/routes/case-routes.d.ts.map +1 -1
  103. package/dist/web/routes/case-routes.js +75 -21
  104. package/dist/web/routes/case-routes.js.map +1 -1
  105. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  106. package/dist/web/routes/hook-event-routes.js +9 -0
  107. package/dist/web/routes/hook-event-routes.js.map +1 -1
  108. package/dist/web/routes/session-routes.d.ts.map +1 -1
  109. package/dist/web/routes/session-routes.js +172 -7
  110. package/dist/web/routes/session-routes.js.map +1 -1
  111. package/dist/web/schemas.d.ts +14 -0
  112. package/dist/web/schemas.d.ts.map +1 -1
  113. package/dist/web/schemas.js +42 -0
  114. package/dist/web/schemas.js.map +1 -1
  115. package/dist/web/server.d.ts +9 -0
  116. package/dist/web/server.d.ts.map +1 -1
  117. package/dist/web/server.js +57 -1
  118. package/dist/web/server.js.map +1 -1
  119. package/dist/web/sse-events.d.ts +18 -0
  120. package/dist/web/sse-events.d.ts.map +1 -1
  121. package/dist/web/sse-events.js +21 -0
  122. package/dist/web/sse-events.js.map +1 -1
  123. package/package.json +1 -1
  124. package/dist/web/public/app.6c4b9893.js +0 -38
  125. package/dist/web/public/app.6c4b9893.js.br +0 -0
  126. package/dist/web/public/app.6c4b9893.js.gz +0 -0
  127. package/dist/web/public/constants.5ce97313.js.br +0 -0
  128. package/dist/web/public/mobile.0644e3ac.css +0 -1
  129. package/dist/web/public/mobile.0644e3ac.css.br +0 -0
  130. package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
  131. package/dist/web/public/panels-ui.3c9b0465.js.br +0 -0
  132. package/dist/web/public/panels-ui.3c9b0465.js.gz +0 -0
  133. package/dist/web/public/session-ui.4851c61a.js +0 -58
  134. package/dist/web/public/session-ui.4851c61a.js.br +0 -0
  135. package/dist/web/public/session-ui.4851c61a.js.gz +0 -0
  136. package/dist/web/public/settings-ui.29214cf1.js.br +0 -0
  137. package/dist/web/public/settings-ui.29214cf1.js.gz +0 -0
  138. package/dist/web/public/styles.9e6cdb9d.css +0 -1
  139. package/dist/web/public/styles.9e6cdb9d.css.br +0 -0
  140. package/dist/web/public/styles.9e6cdb9d.css.gz +0 -0
  141. package/dist/web/public/terminal-ui.efda6083.js +0 -3
  142. package/dist/web/public/terminal-ui.efda6083.js.br +0 -0
  143. 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.
@@ -741,16 +765,16 @@ export function dockerTmuxSessionName(sessionId) {
741
765
  /** Resume ids are UUID-ish; reject anything with shell metacharacters (defensive). */
742
766
  const RESUME_ID_SAFE = /^[A-Za-z0-9._-]+$/;
743
767
  /**
744
- * Append the CLI-specific resume flag to a pane command. Only fires when the
745
- * in-container tmux is RE-CREATED (`new-session -A` makes the flag inert on a
746
- * live reattach), i.e. exactly when the previous live agent was lost and we want
747
- * to resume the conversation from the bind-mounted transcript.
768
+ * Append the CLI-specific resume flag to a pane command (codex/gemini). Only fires
769
+ * when the in-container tmux is RE-CREATED (`new-session -A` makes the flag inert
770
+ * on a live reattach), i.e. exactly when the previous live agent was lost and we
771
+ * want to resume the conversation from the bind-mounted transcript. Claude mode
772
+ * uses claudeDockerPaneCommand instead.
748
773
  */
749
774
  function appendResumeFlag(modeCommand, mode, resumeId) {
750
775
  if (!RESUME_ID_SAFE.test(resumeId))
751
776
  return modeCommand;
752
777
  switch (mode) {
753
- case 'claude':
754
778
  case 'gemini':
755
779
  return `${modeCommand} --resume ${resumeId}`;
756
780
  case 'codex':
@@ -759,6 +783,30 @@ function appendResumeFlag(modeCommand, mode, resumeId) {
759
783
  return modeCommand; // shell / opencode: no resume
760
784
  }
761
785
  }
786
+ /**
787
+ * Claude-mode pane command with a DETERMINISTIC conversation id (the docker analog
788
+ * of buildSpawnCommand's --resume/--session-id logic). A fresh launch passes
789
+ * `--session-id <sessionId>`, so the in-container conversation id is knowable
790
+ * host-side (resume-id capture + subagent/workflow correlation) WITHOUT relying on
791
+ * hook reachability. When the in-container tmux was re-created after a container
792
+ * stop/reboot, the same command re-runs against the surviving transcript:
793
+ * `--session-id` exits 1 ("already in use") and the `||` fallback RESUMES that
794
+ * conversation (verified CLI behavior). An explicit resumeId gets the local
795
+ * builder's shape — resume first, session-id fallback — so a stale id never
796
+ * dead-panes. The leading `exec ` is stripped: an exec'd first branch could never
797
+ * fall back.
798
+ */
799
+ function claudeDockerPaneCommand(modeCommand, sessionId, resumeId) {
800
+ if (!RESUME_ID_SAFE.test(sessionId))
801
+ return modeCommand; // defensive — ids are server-minted uuids
802
+ const cmd = modeCommand.replace(/^exec\s+/, '');
803
+ const rid = resumeId && RESUME_ID_SAFE.test(resumeId) ? resumeId : undefined;
804
+ if (rid && rid !== sessionId) {
805
+ return `${cmd} --resume ${rid} || ${cmd} --session-id ${sessionId}`;
806
+ }
807
+ const cid = rid ?? sessionId;
808
+ return `${cmd} --session-id ${cid} || ${cmd} --resume ${cid}`;
809
+ }
762
810
  /**
763
811
  * Build the ONE `bash -c` launch string for a docker session: image-check ->
764
812
  * ensure (inspect-or-create) -> start -> `exec docker exec -it` into the durable
@@ -776,8 +824,12 @@ export function buildDockerLaunchCommand(opts) {
776
824
  const dkrName = dockerTmuxSessionName(sessionId);
777
825
  const sid = sessionId.slice(0, 8);
778
826
  let modeCommand = docker.commands?.[mode] || defaultDockerCommandForMode(mode);
779
- if (resumeSessionId)
827
+ if (mode === 'claude') {
828
+ modeCommand = claudeDockerPaneCommand(modeCommand, sessionId, resumeSessionId);
829
+ }
830
+ else if (resumeSessionId) {
780
831
  modeCommand = appendResumeFlag(modeCommand, mode, resumeSessionId);
832
+ }
781
833
  // Run by tmux via /bin/sh -c, so the path is shell-quoted here. `exec` makes the
782
834
  // pane PID the agent itself.
783
835
  const paneCommand = `cd ${workdir} && ${modeCommand}`;
@@ -931,6 +983,48 @@ export function resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessio
931
983
  : [];
932
984
  return { mode, docker, sessionId, resumeSessionId, createContext, execEnv, execEnvNames, seedCopies };
933
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
+ }
934
1028
  /**
935
1029
  * Set sensitive environment variables on a tmux session via setenv.
936
1030
  * These are inherited by panes but not visible in ps output or tmux history.
@@ -1084,6 +1178,16 @@ export class TmuxManager extends EventEmitter {
1084
1178
  mouseSyncInterval = null;
1085
1179
  /** Track last-known pane count per session to avoid unnecessary tmux set-option calls */
1086
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();
1087
1191
  trueColorConfigured = false;
1088
1192
  constructor() {
1089
1193
  super();
@@ -1368,7 +1472,7 @@ export class TmuxManager extends EventEmitter {
1368
1472
  const fullCmd = docker
1369
1473
  ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
1370
1474
  : remote
1371
- ? buildRemoteLaunchCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1475
+ ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1372
1476
  : localFullCmd;
1373
1477
  // Create tmux session in three steps to handle cold-start (no server running)
1374
1478
  // and avoid the race where the command exits before remain-on-exit is set:
@@ -1587,7 +1691,7 @@ export class TmuxManager extends EventEmitter {
1587
1691
  const fullCmd = docker
1588
1692
  ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
1589
1693
  : remote
1590
- ? buildRemoteLaunchCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1694
+ ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1591
1695
  : localFullCmd;
1592
1696
  try {
1593
1697
  // For OpenCode: set sensitive env vars via tmux setenv before respawn
@@ -1696,9 +1800,15 @@ export class TmuxManager extends EventEmitter {
1696
1800
  if (!session) {
1697
1801
  return false;
1698
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);
1699
1808
  // TEST MODE: Remove from memory only — NEVER touch real tmux sessions
1700
1809
  if (IS_TEST_MODE) {
1701
1810
  this.sessions.delete(sessionId);
1811
+ this.clearRemoteReconnectState(sessionId);
1702
1812
  this.emit('sessionKilled', { sessionId });
1703
1813
  return true;
1704
1814
  }
@@ -1708,6 +1818,40 @@ export class TmuxManager extends EventEmitter {
1708
1818
  console.error(`[TmuxManager] BLOCKED: Refusing to kill own tmux session: ${session.muxName}`);
1709
1819
  return false;
1710
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
+ }
1711
1855
  // Get current PID (may have changed)
1712
1856
  const currentPid = this.getPanePid(session.muxName) || session.pid;
1713
1857
  console.log(`[TmuxManager] Killing session ${session.muxName} (PID ${currentPid})`);
@@ -1806,6 +1950,7 @@ export class TmuxManager extends EventEmitter {
1806
1950
  }
1807
1951
  this.lastPaneCount.delete(session.muxName);
1808
1952
  this.sessions.delete(sessionId);
1953
+ this.clearRemoteReconnectState(sessionId);
1809
1954
  this.saveSessions();
1810
1955
  this.emit('sessionKilled', { sessionId });
1811
1956
  return true;
@@ -1867,6 +2012,7 @@ export class TmuxManager extends EventEmitter {
1867
2012
  else {
1868
2013
  dead.push(sessionId);
1869
2014
  this.sessions.delete(sessionId);
2015
+ this.clearRemoteReconnectState(sessionId);
1870
2016
  this.emit('sessionDied', { sessionId });
1871
2017
  }
1872
2018
  }
@@ -2103,9 +2249,115 @@ export class TmuxManager extends EventEmitter {
2103
2249
  }
2104
2250
  this.lastPaneCount.clear();
2105
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
+ }
2106
2355
  destroy() {
2107
2356
  this.stopStatsCollection();
2108
2357
  this.stopMouseModeSync();
2358
+ this.stopRemoteReconnectWatcher();
2359
+ this.reconnectState.clear();
2360
+ this.reconnectGuard.clear();
2109
2361
  }
2110
2362
  registerSession(session) {
2111
2363
  this.sessions.set(session.sessionId, session);