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.
- package/README.md +10 -8
- package/README.zh-CN.md +289 -92
- package/dist/docker-export.d.ts +13 -0
- package/dist/docker-export.d.ts.map +1 -1
- package/dist/docker-export.js +55 -8
- package/dist/docker-export.js.map +1 -1
- package/dist/docker-hosts.d.ts +36 -5
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +90 -16
- package/dist/docker-hosts.js.map +1 -1
- package/dist/remote-hosts.d.ts +61 -1
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +142 -0
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-reconnect.d.ts +129 -0
- package/dist/remote-reconnect.d.ts.map +1 -0
- package/dist/remote-reconnect.js +119 -0
- package/dist/remote-reconnect.js.map +1 -0
- package/dist/services/unified-session-service.d.ts +13 -1
- package/dist/services/unified-session-service.d.ts.map +1 -1
- package/dist/services/unified-session-service.js +88 -3
- package/dist/services/unified-session-service.js.map +1 -1
- package/dist/session-order.d.ts +40 -0
- package/dist/session-order.d.ts.map +1 -0
- package/dist/session-order.js +67 -0
- package/dist/session-order.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +87 -19
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +12 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +33 -0
- package/dist/state-store.js.map +1 -1
- package/dist/tmux-manager.d.ts +66 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +260 -8
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/app-state.d.ts +2 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/session.d.ts +43 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.b78b0465.js +38 -0
- package/dist/web/public/app.b78b0465.js.br +0 -0
- package/dist/web/public/app.b78b0465.js.gz +0 -0
- package/dist/web/public/{constants.5ce97313.js → constants.21a0a83e.js} +10 -0
- package/dist/web/public/constants.21a0a83e.js.br +0 -0
- package/dist/web/public/{constants.5ce97313.js.gz → constants.21a0a83e.js.gz} +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +32 -13
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
- package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
- package/dist/web/public/mobile.597136a6.css +1 -0
- package/dist/web/public/mobile.597136a6.css.br +0 -0
- package/dist/web/public/mobile.597136a6.css.gz +0 -0
- package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.3c9b0465.js → panels-ui.58582d4c.js} +2 -2
- package/dist/web/public/panels-ui.58582d4c.js.br +0 -0
- package/dist/web/public/panels-ui.58582d4c.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.0cf86dd4.js +68 -0
- package/dist/web/public/session-ui.0cf86dd4.js.br +0 -0
- package/dist/web/public/session-ui.0cf86dd4.js.gz +0 -0
- package/dist/web/public/{settings-ui.29214cf1.js → settings-ui.bc5f4792.js} +2 -2
- package/dist/web/public/settings-ui.bc5f4792.js.br +0 -0
- package/dist/web/public/settings-ui.bc5f4792.js.gz +0 -0
- package/dist/web/public/styles.7295e6f4.css +1 -0
- package/dist/web/public/styles.7295e6f4.css.br +0 -0
- package/dist/web/public/styles.7295e6f4.css.gz +0 -0
- package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.9ea677e9.js +3 -0
- package/dist/web/public/terminal-ui.9ea677e9.js.br +0 -0
- package/dist/web/public/terminal-ui.9ea677e9.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/case-routes.d.ts +2 -2
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +75 -21
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +9 -0
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +172 -7
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +14 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +42 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +9 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +57 -1
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +18 -0
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +21 -0
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.6c4b9893.js +0 -38
- package/dist/web/public/app.6c4b9893.js.br +0 -0
- package/dist/web/public/app.6c4b9893.js.gz +0 -0
- package/dist/web/public/constants.5ce97313.js.br +0 -0
- package/dist/web/public/mobile.0644e3ac.css +0 -1
- package/dist/web/public/mobile.0644e3ac.css.br +0 -0
- package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
- package/dist/web/public/panels-ui.3c9b0465.js.br +0 -0
- package/dist/web/public/panels-ui.3c9b0465.js.gz +0 -0
- package/dist/web/public/session-ui.4851c61a.js +0 -58
- package/dist/web/public/session-ui.4851c61a.js.br +0 -0
- package/dist/web/public/session-ui.4851c61a.js.gz +0 -0
- package/dist/web/public/settings-ui.29214cf1.js.br +0 -0
- package/dist/web/public/settings-ui.29214cf1.js.gz +0 -0
- package/dist/web/public/styles.9e6cdb9d.css +0 -1
- package/dist/web/public/styles.9e6cdb9d.css.br +0 -0
- package/dist/web/public/styles.9e6cdb9d.css.gz +0 -0
- package/dist/web/public/terminal-ui.efda6083.js +0 -3
- package/dist/web/public/terminal-ui.efda6083.js.br +0 -0
- package/dist/web/public/terminal-ui.efda6083.js.gz +0 -0
package/dist/tmux-manager.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAU3C,OAAO,EACL,YAAY,EACZ,sBAAsB,EAGtB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAC;AAGpB,OAAO,EASL,KAAK,mBAAmB,EAExB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAS3B,OAAO,KAAK,EACV,mBAAmB,EACnB,UAAU,EACV,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/tmux-manager.js
CHANGED
|
@@ -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
|
|
745
|
-
* in-container tmux is RE-CREATED (`new-session -A` makes the flag inert
|
|
746
|
-
* live reattach), i.e. exactly when the previous live agent was lost and we
|
|
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 (
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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);
|