aicodeman 0.6.11 → 0.7.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/dist/mux-interface.d.ts +2 -0
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/session.d.ts +4 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +6 -3
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +15 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +135 -46
- package/dist/tmux-manager.js.map +1 -1
- package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
- package/dist/web/public/{app.6a96cf81.js → app.f67b92cc.js} +4 -4
- package/dist/web/public/app.f67b92cc.js.br +0 -0
- package/dist/web/public/app.f67b92cc.js.gz +0 -0
- package/dist/web/public/constants.cb6426c4.js.gz +0 -0
- package/dist/web/public/image-input.926911b4.js.gz +0 -0
- package/dist/web/public/index.html +4 -4
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.88082175.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.29aebd9c.js.gz +0 -0
- package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
- package/dist/web/public/mobile.37d62c06.css.gz +0 -0
- package/dist/web/public/notification-manager.9c984ac2.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/panels-ui.cf998835.js.gz +0 -0
- package/dist/web/public/ralph-panel.61076370.js.gz +0 -0
- package/dist/web/public/ralph-wizard.6b0f0be7.js.gz +0 -0
- package/dist/web/public/respawn-ui.5377f958.js.gz +0 -0
- package/dist/web/public/session-ui.f1555cd1.js.gz +0 -0
- package/dist/web/public/settings-ui.25a18120.js.gz +0 -0
- package/dist/web/public/{styles.d160ad58.css → styles.c2babdcb.css} +1 -1
- package/dist/web/public/styles.c2babdcb.css.br +0 -0
- package/dist/web/public/styles.c2babdcb.css.gz +0 -0
- package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/{terminal-ui.5d29101f.js → terminal-ui.7616b9fd.js} +1 -1
- package/dist/web/public/terminal-ui.7616b9fd.js.br +0 -0
- package/dist/web/public/{terminal-ui.5d29101f.js.gz → terminal-ui.7616b9fd.js.gz} +0 -0
- package/dist/web/public/upload.html.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-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/route-error-handler.d.ts +18 -0
- package/dist/web/route-error-handler.d.ts.map +1 -0
- package/dist/web/route-error-handler.js +19 -0
- package/dist/web/route-error-handler.js.map +1 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +3 -1
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +5 -12
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.6a96cf81.js.br +0 -0
- package/dist/web/public/app.6a96cf81.js.gz +0 -0
- package/dist/web/public/styles.d160ad58.css.br +0 -0
- package/dist/web/public/styles.d160ad58.css.gz +0 -0
- package/dist/web/public/terminal-ui.5d29101f.js.br +0 -0
package/dist/tmux-manager.js
CHANGED
|
@@ -45,6 +45,8 @@ const TMUX_KILL_WAIT_MS = 200;
|
|
|
45
45
|
const GRACEFUL_SHUTDOWN_WAIT_MS = 100;
|
|
46
46
|
/** Default stats collection interval (2 seconds) */
|
|
47
47
|
const DEFAULT_STATS_INTERVAL_MS = 2000;
|
|
48
|
+
/** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
|
|
49
|
+
export const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
|
|
48
50
|
/**
|
|
49
51
|
* SAFETY: Test mode detection.
|
|
50
52
|
* When running under vitest (VITEST env var is set automatically),
|
|
@@ -67,6 +69,10 @@ const SAFE_MUX_NAME_PATTERN = /^codeman-[a-f0-9-]+$/;
|
|
|
67
69
|
const LEGACY_MUX_NAME_PATTERN = /^claudeman-[a-f0-9-]+$/;
|
|
68
70
|
/** Regex to validate tmux pane targets (e.g., "%0", "%1", "0", "1") */
|
|
69
71
|
const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/;
|
|
72
|
+
/** Dedicated tmux socket for new Codeman-owned sessions. */
|
|
73
|
+
const DEFAULT_CODEMAN_TMUX_SOCKET = 'codeman';
|
|
74
|
+
/** Regex to validate tmux socket names passed to `tmux -L`. */
|
|
75
|
+
const SAFE_TMUX_SOCKET_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
70
76
|
/**
|
|
71
77
|
* Separator used in `tmux list-panes -F` output between session name and pid.
|
|
72
78
|
*
|
|
@@ -81,6 +87,18 @@ const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/;
|
|
|
81
87
|
const PANE_LIST_SEP = '|';
|
|
82
88
|
/** Format string for `tmux list-panes -F`. Keep in sync with {@link parsePaneList}. */
|
|
83
89
|
const PANE_LIST_FORMAT = `#{session_name}${PANE_LIST_SEP}#{pane_pid}`;
|
|
90
|
+
/**
|
|
91
|
+
* 构建 pane 启动前的 nofile 修复命令。
|
|
92
|
+
*
|
|
93
|
+
* macOS launchd/tmux 组合有时会让 pane 继承 256 的 soft nofile;
|
|
94
|
+
* 新版 Claude Code 会在这种环境下直接退出。这里避免使用 $变量
|
|
95
|
+
* 或命令替换,因为 fullCmd 目前经由双引号 bash -c 传递,外层
|
|
96
|
+
* shell 会提前展开它们。
|
|
97
|
+
*/
|
|
98
|
+
export function buildNofileLimitCommand(targetLimit = CLAUDE_CODE_NOFILE_LIMIT) {
|
|
99
|
+
const safeLimit = Number.isSafeInteger(targetLimit) && targetLimit > 0 ? targetLimit : CLAUDE_CODE_NOFILE_LIMIT;
|
|
100
|
+
return `ulimit -Sn ${safeLimit} 2>/dev/null || ulimit -n ${safeLimit} 2>/dev/null || true`;
|
|
101
|
+
}
|
|
84
102
|
/**
|
|
85
103
|
* Parse the output of `tmux list-panes -a -F '#{session_name}|#{pane_pid}'`
|
|
86
104
|
* into a Map of session-name → pane pid. Exported for unit testing.
|
|
@@ -126,6 +144,28 @@ function isValidPath(path) {
|
|
|
126
144
|
}
|
|
127
145
|
return SAFE_PATH_PATTERN.test(path);
|
|
128
146
|
}
|
|
147
|
+
// ===========================================================================
|
|
148
|
+
// Single-socket architecture: ALL Codeman sessions live on one dedicated tmux
|
|
149
|
+
// socket (`tmux -L codeman`), isolated from the user's default tmux server.
|
|
150
|
+
// The socket name is a process-wide constant (env-overridable for test/multi-
|
|
151
|
+
// instance isolation) — it is never stored per-session, so it cannot drift.
|
|
152
|
+
// ===========================================================================
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the process-wide Codeman tmux socket name. Always returns a valid
|
|
155
|
+
* name: `CODEMAN_TMUX_SOCKET` env override if safe, else the built-in default.
|
|
156
|
+
*/
|
|
157
|
+
function resolveConfiguredTmuxSocket() {
|
|
158
|
+
const raw = process.env.CODEMAN_TMUX_SOCKET ?? DEFAULT_CODEMAN_TMUX_SOCKET;
|
|
159
|
+
if (!SAFE_TMUX_SOCKET_PATTERN.test(raw)) {
|
|
160
|
+
console.warn(`[TmuxManager] Ignoring invalid CODEMAN_TMUX_SOCKET: ${JSON.stringify(raw)}`);
|
|
161
|
+
return DEFAULT_CODEMAN_TMUX_SOCKET;
|
|
162
|
+
}
|
|
163
|
+
return raw;
|
|
164
|
+
}
|
|
165
|
+
/** Build the `tmux -L <socket>` command prefix. Socket name is shell-escaped. */
|
|
166
|
+
function tmuxCommand(socket) {
|
|
167
|
+
return `tmux -L ${shellescape(socket)}`;
|
|
168
|
+
}
|
|
129
169
|
/**
|
|
130
170
|
* Build Claude CLI permission flags for the tmux command string.
|
|
131
171
|
* Validates allowedTools to prevent command injection.
|
|
@@ -201,7 +241,7 @@ function buildSpawnCommand(options) {
|
|
|
201
241
|
* Set sensitive environment variables on a tmux session via setenv.
|
|
202
242
|
* These are inherited by panes but not visible in ps output or tmux history.
|
|
203
243
|
*/
|
|
204
|
-
function setOpenCodeEnvVars(muxName) {
|
|
244
|
+
function setOpenCodeEnvVars(tmuxCmd, muxName) {
|
|
205
245
|
const sensitiveVars = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY'];
|
|
206
246
|
for (const key of sensitiveVars) {
|
|
207
247
|
const val = process.env[key];
|
|
@@ -209,7 +249,7 @@ function setOpenCodeEnvVars(muxName) {
|
|
|
209
249
|
// Shell-escape: wrap in single quotes, escape any inner single quotes
|
|
210
250
|
const escaped = val.replace(/'/g, "'\\''");
|
|
211
251
|
try {
|
|
212
|
-
execSync(
|
|
252
|
+
execSync(`${tmuxCmd} setenv -t '${muxName}' ${key} '${escaped}'`, {
|
|
213
253
|
encoding: 'utf8',
|
|
214
254
|
timeout: EXEC_TIMEOUT_MS,
|
|
215
255
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -225,7 +265,7 @@ function setOpenCodeEnvVars(muxName) {
|
|
|
225
265
|
* Set OPENCODE_CONFIG_CONTENT on a tmux session via setenv.
|
|
226
266
|
* Uses tmux setenv to avoid shell metacharacter injection from user-supplied JSON.
|
|
227
267
|
*/
|
|
228
|
-
function setOpenCodeConfigContent(muxName, config) {
|
|
268
|
+
function setOpenCodeConfigContent(tmuxCmd, muxName, config) {
|
|
229
269
|
if (!config)
|
|
230
270
|
return;
|
|
231
271
|
let jsonContent;
|
|
@@ -257,7 +297,7 @@ function setOpenCodeConfigContent(muxName, config) {
|
|
|
257
297
|
if (jsonContent) {
|
|
258
298
|
const escaped = jsonContent.replace(/'/g, "'\\''");
|
|
259
299
|
try {
|
|
260
|
-
execSync(
|
|
300
|
+
execSync(`${tmuxCmd} setenv -t '${muxName}' OPENCODE_CONFIG_CONTENT '${escaped}'`, {
|
|
261
301
|
encoding: 'utf8',
|
|
262
302
|
timeout: EXEC_TIMEOUT_MS,
|
|
263
303
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -290,6 +330,7 @@ function setOpenCodeConfigContent(muxName, config) {
|
|
|
290
330
|
export class TmuxManager extends EventEmitter {
|
|
291
331
|
backend = 'tmux';
|
|
292
332
|
sessions = new Map();
|
|
333
|
+
tmuxSocket = resolveConfiguredTmuxSocket();
|
|
293
334
|
statsInterval = null;
|
|
294
335
|
mouseSyncInterval = null;
|
|
295
336
|
/** Track last-known pane count per session to avoid unnecessary tmux set-option calls */
|
|
@@ -302,6 +343,13 @@ export class TmuxManager extends EventEmitter {
|
|
|
302
343
|
this.loadSessions();
|
|
303
344
|
}
|
|
304
345
|
}
|
|
346
|
+
/** The dedicated tmux socket all Codeman sessions live on (see {@link TerminalMultiplexer.muxSocket}). */
|
|
347
|
+
get muxSocket() {
|
|
348
|
+
return this.tmuxSocket;
|
|
349
|
+
}
|
|
350
|
+
tmux() {
|
|
351
|
+
return tmuxCommand(this.tmuxSocket);
|
|
352
|
+
}
|
|
305
353
|
// Load saved sessions from disk (NEVER called in test mode)
|
|
306
354
|
loadSessions() {
|
|
307
355
|
if (IS_TEST_MODE)
|
|
@@ -311,8 +359,41 @@ export class TmuxManager extends EventEmitter {
|
|
|
311
359
|
const content = readFileSync(MUX_SESSIONS_FILE, 'utf-8');
|
|
312
360
|
const data = JSON.parse(content);
|
|
313
361
|
if (Array.isArray(data)) {
|
|
362
|
+
// Dedup by muxName: one live tmux session must map to exactly one
|
|
363
|
+
// tracked entry. A per-session socket-tag mismatch could historically
|
|
364
|
+
// let the same session be tracked twice — once under its real UUID and
|
|
365
|
+
// once under a "restored-<id>" placeholder — surfacing as duplicate tabs.
|
|
366
|
+
// Single-socket unification removed that failure mode; this pass stays
|
|
367
|
+
// to clean any stale duplicates already on disk. Keep the real (UUID)
|
|
368
|
+
// entry and drop placeholder twins.
|
|
369
|
+
let dropped = 0;
|
|
370
|
+
const keptByMuxName = new Map(); // muxName -> kept sessionId
|
|
314
371
|
for (const session of data) {
|
|
372
|
+
// Strip the obsolete per-session tmuxSocket tag (now a process-wide
|
|
373
|
+
// constant). Left in place it would be written back by saveSessions()
|
|
374
|
+
// and linger on disk as a zombie field forever.
|
|
375
|
+
delete session.tmuxSocket;
|
|
376
|
+
const muxName = session.muxName;
|
|
377
|
+
const priorId = muxName ? keptByMuxName.get(muxName) : undefined;
|
|
378
|
+
if (priorId) {
|
|
379
|
+
const incomingIsPlaceholder = String(session.sessionId).startsWith('restored-');
|
|
380
|
+
const priorIsPlaceholder = priorId.startsWith('restored-');
|
|
381
|
+
// Drop the incoming unless it's the real twin of a placeholder we kept.
|
|
382
|
+
if (incomingIsPlaceholder || !priorIsPlaceholder) {
|
|
383
|
+
dropped++;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
this.sessions.delete(priorId);
|
|
387
|
+
dropped++;
|
|
388
|
+
}
|
|
315
389
|
this.sessions.set(session.sessionId, session);
|
|
390
|
+
if (muxName)
|
|
391
|
+
keptByMuxName.set(muxName, session.sessionId);
|
|
392
|
+
}
|
|
393
|
+
// Persist the cleaned list so the stale duplicates don't reload.
|
|
394
|
+
if (dropped > 0) {
|
|
395
|
+
console.log(`[TmuxManager] Dropped ${dropped} duplicate mux session record(s) on load`);
|
|
396
|
+
this.saveSessions();
|
|
316
397
|
}
|
|
317
398
|
}
|
|
318
399
|
}
|
|
@@ -390,7 +471,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
390
471
|
continue;
|
|
391
472
|
}
|
|
392
473
|
try {
|
|
393
|
-
execSync(
|
|
474
|
+
execSync(`${this.tmux()} setenv -t ${shellescape(muxName)} ${key} ${shellescape(value)}`, {
|
|
394
475
|
timeout: EXEC_TIMEOUT_MS,
|
|
395
476
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
396
477
|
});
|
|
@@ -422,8 +503,9 @@ export class TmuxManager extends EventEmitter {
|
|
|
422
503
|
* (not visible in ps output or tmux history, inherited by panes).
|
|
423
504
|
*/
|
|
424
505
|
_configureOpenCode(muxName, openCodeConfig) {
|
|
425
|
-
|
|
426
|
-
|
|
506
|
+
const tmuxCmd = this.tmux();
|
|
507
|
+
setOpenCodeEnvVars(tmuxCmd, muxName);
|
|
508
|
+
setOpenCodeConfigContent(tmuxCmd, muxName, openCodeConfig);
|
|
427
509
|
}
|
|
428
510
|
/**
|
|
429
511
|
* Creates a new tmux session wrapping Claude CLI or a shell.
|
|
@@ -476,7 +558,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
476
558
|
const cmd = wrapWithNice(baseCmd, config);
|
|
477
559
|
try {
|
|
478
560
|
// Build the full command to run inside tmux
|
|
479
|
-
const fullCmd = `${pathExport}${envExportsStr} && ${cmd}`;
|
|
561
|
+
const fullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
480
562
|
// Create tmux session in three steps to handle cold-start (no server running)
|
|
481
563
|
// and avoid the race where the command exits before remain-on-exit is set:
|
|
482
564
|
// 1. Create session with default shell (starts tmux server, stays alive)
|
|
@@ -486,7 +568,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
486
568
|
// (Production uses systemd which has a clean env, but dev/test may be nested.)
|
|
487
569
|
const cleanEnv = { ...process.env };
|
|
488
570
|
delete cleanEnv.TMUX;
|
|
489
|
-
execSync(
|
|
571
|
+
execSync(`${this.tmux()} new-session -ds "${muxName}" -c "${workingDir}"`, {
|
|
490
572
|
cwd: workingDir,
|
|
491
573
|
timeout: EXEC_TIMEOUT_MS,
|
|
492
574
|
stdio: 'ignore',
|
|
@@ -494,7 +576,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
494
576
|
});
|
|
495
577
|
// Set remain-on-exit now that the server is running — must be before respawn-pane
|
|
496
578
|
try {
|
|
497
|
-
execSync(
|
|
579
|
+
execSync(`${this.tmux()} set-option -t "${muxName}" remain-on-exit on`, {
|
|
498
580
|
timeout: EXEC_TIMEOUT_MS,
|
|
499
581
|
stdio: 'ignore',
|
|
500
582
|
});
|
|
@@ -511,7 +593,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
511
593
|
// so secret values stay off the bash command line. Must run before respawn-pane.
|
|
512
594
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
513
595
|
// Replace the shell with the actual command (no echo in terminal)
|
|
514
|
-
execSync(
|
|
596
|
+
execSync(`${this.tmux()} respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`, {
|
|
515
597
|
timeout: EXEC_TIMEOUT_MS,
|
|
516
598
|
stdio: 'ignore',
|
|
517
599
|
});
|
|
@@ -523,20 +605,20 @@ export class TmuxManager extends EventEmitter {
|
|
|
523
605
|
// It gets enabled dynamically when panes are split (agent teams).
|
|
524
606
|
const configPromises = [
|
|
525
607
|
// Disable tmux status bar — Codeman's web UI provides session info
|
|
526
|
-
execAsync(
|
|
608
|
+
execAsync(`${this.tmux()} set-option -t "${muxName}" status off`, { timeout: EXEC_TIMEOUT_MS })
|
|
527
609
|
.then(() => { })
|
|
528
610
|
.catch(() => {
|
|
529
611
|
/* Non-critical — session still works with status bar */
|
|
530
612
|
}),
|
|
531
613
|
// Override global remain-on-exit with session-level setting
|
|
532
|
-
execAsync(
|
|
614
|
+
execAsync(`${this.tmux()} set-option -t "${muxName}" remain-on-exit on`, { timeout: EXEC_TIMEOUT_MS })
|
|
533
615
|
.then(() => { })
|
|
534
616
|
.catch(() => {
|
|
535
617
|
/* Already set globally as fallback */
|
|
536
618
|
}),
|
|
537
619
|
// Raise tmux scrollback from its 2000-line default so re-attach preserves
|
|
538
620
|
// more context. Matches the xterm-side default in constants.js.
|
|
539
|
-
execAsync(
|
|
621
|
+
execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit 50000`, { timeout: EXEC_TIMEOUT_MS })
|
|
540
622
|
.then(() => { })
|
|
541
623
|
.catch(() => {
|
|
542
624
|
/* Non-critical — falls back to tmux default */
|
|
@@ -544,7 +626,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
544
626
|
];
|
|
545
627
|
// Enable 24-bit true color passthrough — server-wide, set once per lifetime
|
|
546
628
|
if (!this.trueColorConfigured) {
|
|
547
|
-
configPromises.push(execAsync(
|
|
629
|
+
configPromises.push(execAsync(`${this.tmux()} set-option -sa terminal-overrides ",*:Tc"`, { timeout: EXEC_TIMEOUT_MS })
|
|
548
630
|
.then(() => {
|
|
549
631
|
this.trueColorConfigured = true;
|
|
550
632
|
})
|
|
@@ -594,7 +676,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
594
676
|
return null;
|
|
595
677
|
}
|
|
596
678
|
try {
|
|
597
|
-
const output = execSync(
|
|
679
|
+
const output = execSync(`${this.tmux()} display-message -t "${muxName}" -p '#{pane_pid}'`, {
|
|
598
680
|
encoding: 'utf-8',
|
|
599
681
|
timeout: EXEC_TIMEOUT_MS,
|
|
600
682
|
}).trim();
|
|
@@ -621,7 +703,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
621
703
|
if (!isValidMuxName(muxName))
|
|
622
704
|
return false;
|
|
623
705
|
try {
|
|
624
|
-
const output = execSync(
|
|
706
|
+
const output = execSync(`${this.tmux()} display-message -t "${muxName}" -p '#{pane_dead}'`, {
|
|
625
707
|
encoding: 'utf-8',
|
|
626
708
|
timeout: EXEC_TIMEOUT_MS,
|
|
627
709
|
}).trim();
|
|
@@ -658,7 +740,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
658
740
|
});
|
|
659
741
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
|
660
742
|
const cmd = wrapWithNice(baseCmd, config);
|
|
661
|
-
const fullCmd = `${pathExport}${envExportsStr} && ${cmd}`;
|
|
743
|
+
const fullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
662
744
|
try {
|
|
663
745
|
// For OpenCode: set sensitive env vars via tmux setenv before respawn
|
|
664
746
|
if (mode === 'opencode') {
|
|
@@ -666,7 +748,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
666
748
|
}
|
|
667
749
|
// Re-apply user env overrides before respawn so the new shell inherits them.
|
|
668
750
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
669
|
-
await execAsync(
|
|
751
|
+
await execAsync(`${this.tmux()} respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`, {
|
|
670
752
|
timeout: EXEC_TIMEOUT_MS,
|
|
671
753
|
});
|
|
672
754
|
// Wait for the respawned process to start
|
|
@@ -685,7 +767,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
685
767
|
if (IS_TEST_MODE)
|
|
686
768
|
return false;
|
|
687
769
|
try {
|
|
688
|
-
execSync(
|
|
770
|
+
execSync(`${this.tmux()} has-session -t "${muxName}" 2>/dev/null`, {
|
|
689
771
|
encoding: 'utf-8',
|
|
690
772
|
timeout: EXEC_TIMEOUT_MS,
|
|
691
773
|
});
|
|
@@ -814,7 +896,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
814
896
|
}
|
|
815
897
|
// Strategy 3: Kill tmux session by name
|
|
816
898
|
try {
|
|
817
|
-
execSync(
|
|
899
|
+
execSync(`${this.tmux()} kill-session -t "${session.muxName}" 2>/dev/null`, {
|
|
818
900
|
timeout: EXEC_TIMEOUT_MS,
|
|
819
901
|
});
|
|
820
902
|
}
|
|
@@ -871,26 +953,29 @@ export class TmuxManager extends EventEmitter {
|
|
|
871
953
|
const alive = [];
|
|
872
954
|
const dead = [];
|
|
873
955
|
const discovered = [];
|
|
874
|
-
//
|
|
875
|
-
|
|
956
|
+
// Single batched query against the one socket Codeman owns. With a single
|
|
957
|
+
// socket a session's location is a constant, so there is no per-session
|
|
958
|
+
// socket tag to reconcile and no cross-socket ambiguity that could mark a
|
|
959
|
+
// live session dead (the root cause of vanished/duplicate tabs).
|
|
960
|
+
let active;
|
|
876
961
|
try {
|
|
877
|
-
const output = execSync(
|
|
962
|
+
const output = execSync(`${this.tmux()} list-panes -a -F '${PANE_LIST_FORMAT}' 2>/dev/null || true`, {
|
|
878
963
|
encoding: 'utf-8',
|
|
879
964
|
timeout: EXEC_TIMEOUT_MS,
|
|
880
965
|
}).trim();
|
|
881
|
-
|
|
966
|
+
active = parsePaneList(output);
|
|
882
967
|
}
|
|
883
968
|
catch (err) {
|
|
884
969
|
console.error('[TmuxManager] Failed to list tmux panes:', err);
|
|
970
|
+
active = new Map();
|
|
885
971
|
}
|
|
886
|
-
// Check
|
|
972
|
+
// Check tracked sessions against the live pane list.
|
|
887
973
|
for (const [sessionId, session] of this.sessions) {
|
|
888
|
-
const pid =
|
|
974
|
+
const pid = active.get(session.muxName);
|
|
889
975
|
if (pid !== undefined) {
|
|
890
976
|
alive.push(sessionId);
|
|
891
|
-
if (pid !== session.pid)
|
|
977
|
+
if (pid !== session.pid)
|
|
892
978
|
session.pid = pid;
|
|
893
|
-
}
|
|
894
979
|
}
|
|
895
980
|
else {
|
|
896
981
|
dead.push(sessionId);
|
|
@@ -898,12 +983,14 @@ export class TmuxManager extends EventEmitter {
|
|
|
898
983
|
this.emit('sessionDied', { sessionId });
|
|
899
984
|
}
|
|
900
985
|
}
|
|
901
|
-
// Discover
|
|
986
|
+
// Discover untracked codeman/claudeman sessions on our socket. Dedup by
|
|
987
|
+
// muxName (globally unique) so a name we already track never spawns a
|
|
988
|
+
// second "Restored:" entry.
|
|
902
989
|
const knownMuxNames = new Set();
|
|
903
990
|
for (const session of this.sessions.values()) {
|
|
904
991
|
knownMuxNames.add(session.muxName);
|
|
905
992
|
}
|
|
906
|
-
for (const [sessionName, pid] of
|
|
993
|
+
for (const [sessionName, pid] of active) {
|
|
907
994
|
if (!sessionName.startsWith('codeman-') && !sessionName.startsWith('claudeman-'))
|
|
908
995
|
continue;
|
|
909
996
|
if (knownMuxNames.has(sessionName))
|
|
@@ -921,6 +1008,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
921
1008
|
name: `Restored: ${sessionName}`,
|
|
922
1009
|
};
|
|
923
1010
|
this.sessions.set(sessionId, session);
|
|
1011
|
+
knownMuxNames.add(sessionName);
|
|
924
1012
|
discovered.push(sessionId);
|
|
925
1013
|
console.log(`[TmuxManager] Discovered unknown tmux session: ${sessionName} (PID ${pid})`);
|
|
926
1014
|
}
|
|
@@ -1187,23 +1275,23 @@ export class TmuxManager extends EventEmitter {
|
|
|
1187
1275
|
// Ink (Claude CLI's terminal framework) needs them split — sending both in a
|
|
1188
1276
|
// single tmux invocation (via \;) causes Ink to interpret Enter as a newline
|
|
1189
1277
|
// character in the input buffer rather than as form submission.
|
|
1190
|
-
await execAsync(
|
|
1278
|
+
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`, {
|
|
1191
1279
|
timeout: EXEC_TIMEOUT_MS,
|
|
1192
1280
|
});
|
|
1193
1281
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1194
|
-
await execAsync(
|
|
1282
|
+
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" Enter`, {
|
|
1195
1283
|
timeout: EXEC_TIMEOUT_MS,
|
|
1196
1284
|
});
|
|
1197
1285
|
}
|
|
1198
1286
|
else if (textPart) {
|
|
1199
1287
|
// Text only, no Enter
|
|
1200
|
-
await execAsync(
|
|
1288
|
+
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`, {
|
|
1201
1289
|
timeout: EXEC_TIMEOUT_MS,
|
|
1202
1290
|
});
|
|
1203
1291
|
}
|
|
1204
1292
|
else if (hasCarriageReturn) {
|
|
1205
1293
|
// Enter only
|
|
1206
|
-
await execAsync(
|
|
1294
|
+
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" Enter`, {
|
|
1207
1295
|
timeout: EXEC_TIMEOUT_MS,
|
|
1208
1296
|
});
|
|
1209
1297
|
}
|
|
@@ -1228,7 +1316,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1228
1316
|
return false;
|
|
1229
1317
|
}
|
|
1230
1318
|
try {
|
|
1231
|
-
execSync(
|
|
1319
|
+
execSync(`${this.tmux()} set-option -t "${muxName}" mouse on`, {
|
|
1232
1320
|
encoding: 'utf-8',
|
|
1233
1321
|
timeout: EXEC_TIMEOUT_MS,
|
|
1234
1322
|
});
|
|
@@ -1252,7 +1340,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1252
1340
|
return false;
|
|
1253
1341
|
}
|
|
1254
1342
|
try {
|
|
1255
|
-
execSync(
|
|
1343
|
+
execSync(`${this.tmux()} set-option -t "${muxName}" mouse off`, {
|
|
1256
1344
|
encoding: 'utf-8',
|
|
1257
1345
|
timeout: EXEC_TIMEOUT_MS,
|
|
1258
1346
|
});
|
|
@@ -1292,7 +1380,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1292
1380
|
return [];
|
|
1293
1381
|
}
|
|
1294
1382
|
try {
|
|
1295
|
-
const output = execSync(
|
|
1383
|
+
const output = execSync(`${this.tmux()} list-panes -t "${muxName}" -F '#{pane_id}:#{pane_index}:#{pane_pid}:#{pane_width}:#{pane_height}'`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }).trim();
|
|
1296
1384
|
return output
|
|
1297
1385
|
.split('\n')
|
|
1298
1386
|
.map((line) => {
|
|
@@ -1328,27 +1416,28 @@ export class TmuxManager extends EventEmitter {
|
|
|
1328
1416
|
}
|
|
1329
1417
|
// Build target: sessionName.paneId (e.g., "codeman-abc12345.%1")
|
|
1330
1418
|
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
|
1419
|
+
const tmux = this.tmux();
|
|
1331
1420
|
try {
|
|
1332
1421
|
const hasCarriageReturn = input.includes('\r');
|
|
1333
1422
|
const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
|
|
1334
1423
|
if (textPart && hasCarriageReturn) {
|
|
1335
|
-
execSync(
|
|
1424
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
|
|
1336
1425
|
encoding: 'utf-8',
|
|
1337
1426
|
timeout: EXEC_TIMEOUT_MS,
|
|
1338
1427
|
});
|
|
1339
|
-
execSync(
|
|
1428
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} Enter`, {
|
|
1340
1429
|
encoding: 'utf-8',
|
|
1341
1430
|
timeout: EXEC_TIMEOUT_MS,
|
|
1342
1431
|
});
|
|
1343
1432
|
}
|
|
1344
1433
|
else if (textPart) {
|
|
1345
|
-
execSync(
|
|
1434
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
|
|
1346
1435
|
encoding: 'utf-8',
|
|
1347
1436
|
timeout: EXEC_TIMEOUT_MS,
|
|
1348
1437
|
});
|
|
1349
1438
|
}
|
|
1350
1439
|
else if (hasCarriageReturn) {
|
|
1351
|
-
execSync(
|
|
1440
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} Enter`, {
|
|
1352
1441
|
encoding: 'utf-8',
|
|
1353
1442
|
timeout: EXEC_TIMEOUT_MS,
|
|
1354
1443
|
});
|
|
@@ -1377,7 +1466,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1377
1466
|
}
|
|
1378
1467
|
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
|
1379
1468
|
try {
|
|
1380
|
-
return execSync(
|
|
1469
|
+
return execSync(`${this.tmux()} capture-pane -p -e -t ${shellescape(target)} -S -5000`, {
|
|
1381
1470
|
encoding: 'utf-8',
|
|
1382
1471
|
timeout: EXEC_TIMEOUT_MS,
|
|
1383
1472
|
});
|
|
@@ -1408,7 +1497,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1408
1497
|
}
|
|
1409
1498
|
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
|
1410
1499
|
try {
|
|
1411
|
-
execSync(
|
|
1500
|
+
execSync(`${this.tmux()} pipe-pane -O -t ${shellescape(target)} ${shellescape('cat >> ' + outputFile)}`, {
|
|
1412
1501
|
encoding: 'utf-8',
|
|
1413
1502
|
timeout: EXEC_TIMEOUT_MS,
|
|
1414
1503
|
});
|
|
@@ -1435,7 +1524,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1435
1524
|
}
|
|
1436
1525
|
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
|
1437
1526
|
try {
|
|
1438
|
-
execSync(
|
|
1527
|
+
execSync(`${this.tmux()} pipe-pane -t ${shellescape(target)}`, {
|
|
1439
1528
|
encoding: 'utf-8',
|
|
1440
1529
|
timeout: EXEC_TIMEOUT_MS,
|
|
1441
1530
|
});
|
|
@@ -1450,7 +1539,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1450
1539
|
return 'tmux';
|
|
1451
1540
|
}
|
|
1452
1541
|
getAttachArgs(muxName) {
|
|
1453
|
-
return ['attach-session', '-t', muxName];
|
|
1542
|
+
return ['-L', this.tmuxSocket, 'attach-session', '-t', muxName];
|
|
1454
1543
|
}
|
|
1455
1544
|
isAvailable() {
|
|
1456
1545
|
return TmuxManager.isTmuxAvailable();
|