mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
feat(docker): thread SessionDocker through Session + recovery
- Session: _docker field, constructor config, toState, createSessionOptions/ respawnPaneOptions (both interactive + shell paths), docker getter - resolveMuxAttachCwd returns /tmp for docker sessions (local wrapper only execs) - skip the LOCAL claude version probe for docker; probe the IN-CONTAINER version instead (deferred) so wheel-forwarding stays enabled (#154) - server restoreMuxSessions round-trips MuxSession.docker / SessionState.docker - full CI suite green (3444 passed) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+52
-4
@@ -50,7 +50,9 @@ import {
|
|||||||
type EffortLevel,
|
type EffortLevel,
|
||||||
type GeminiConfig,
|
type GeminiConfig,
|
||||||
type SessionRemote,
|
type SessionRemote,
|
||||||
|
type SessionDocker,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
import { probeDockerCliVersion } from './docker-hosts.js';
|
||||||
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
|
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
|
||||||
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
|
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
|
||||||
import { RalphTracker } from './ralph-tracker.js';
|
import { RalphTracker } from './ralph-tracker.js';
|
||||||
@@ -178,6 +180,8 @@ export function isAltScreenStripMode(mode: SessionMode): boolean {
|
|||||||
const DEFAULT_PTY_COLS = 120;
|
const DEFAULT_PTY_COLS = 120;
|
||||||
const DEFAULT_PTY_ROWS = 40;
|
const DEFAULT_PTY_ROWS = 40;
|
||||||
const TMUX_DISPLAY_TIMEOUT_MS = 2000;
|
const TMUX_DISPLAY_TIMEOUT_MS = 2000;
|
||||||
|
/** Delay before the in-container Claude CLI version probe (lets the container start). */
|
||||||
|
const DOCKER_CLI_VERSION_PROBE_DELAY_MS = 3000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask tmux for the current window geometry of `muxName` so a re-attaching PTY
|
* Ask tmux for the current window geometry of `muxName` so a re-attaching PTY
|
||||||
@@ -212,8 +216,10 @@ export function queryTmuxWindowSize(muxName: string, socket: string): { cols: nu
|
|||||||
return { cols: DEFAULT_PTY_COLS, rows: DEFAULT_PTY_ROWS };
|
return { cols: DEFAULT_PTY_COLS, rows: DEFAULT_PTY_ROWS };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMuxAttachCwd(workingDir: string, remote?: SessionRemote): string {
|
export function resolveMuxAttachCwd(workingDir: string, remote?: SessionRemote, docker?: SessionDocker): string {
|
||||||
return remote ? '/tmp' : workingDir;
|
// Remote and docker sessions run the CLI elsewhere (ssh / docker exec); the LOCAL
|
||||||
|
// wrapper pane never needs the workspace as its cwd, so launch it in /tmp.
|
||||||
|
return remote || docker ? '/tmp' : workingDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -402,6 +408,10 @@ export class Session extends EventEmitter {
|
|||||||
// Remote execution metadata, present when this session runs over SSH through local tmux.
|
// Remote execution metadata, present when this session runs over SSH through local tmux.
|
||||||
private readonly _remote?: SessionRemote;
|
private readonly _remote?: SessionRemote;
|
||||||
|
|
||||||
|
// Docker execution metadata, present when this session runs inside a container via
|
||||||
|
// local tmux + `docker exec`. The container is per-CASE (shared by sibling sessions).
|
||||||
|
private readonly _docker?: SessionDocker;
|
||||||
|
|
||||||
// Session color for visual differentiation
|
// Session color for visual differentiation
|
||||||
private _color: import('./types.js').SessionColor = 'default';
|
private _color: import('./types.js').SessionColor = 'default';
|
||||||
|
|
||||||
@@ -475,6 +485,8 @@ export class Session extends EventEmitter {
|
|||||||
attachmentHistory?: SessionAttachmentHistoryItem[];
|
attachmentHistory?: SessionAttachmentHistoryItem[];
|
||||||
/** Remote execution metadata for sessions launched through SSH inside local tmux. */
|
/** Remote execution metadata for sessions launched through SSH inside local tmux. */
|
||||||
remote?: SessionRemote;
|
remote?: SessionRemote;
|
||||||
|
/** Docker execution metadata for sessions launched inside a container via local tmux. */
|
||||||
|
docker?: SessionDocker;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -548,6 +560,7 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
|
this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
|
||||||
this._remote = config.remote;
|
this._remote = config.remote;
|
||||||
|
this._docker = config.docker;
|
||||||
if (config.attachmentHistory && config.attachmentHistory.length > 0) {
|
if (config.attachmentHistory && config.attachmentHistory.length > 0) {
|
||||||
this.restoreAttachmentHistory(config.attachmentHistory);
|
this.restoreAttachmentHistory(config.attachmentHistory);
|
||||||
}
|
}
|
||||||
@@ -649,6 +662,11 @@ export class Session extends EventEmitter {
|
|||||||
return this._claudeSessionId;
|
return this._claudeSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Docker execution metadata when this session runs inside a container, else undefined. */
|
||||||
|
get docker(): SessionDocker | undefined {
|
||||||
|
return this._docker;
|
||||||
|
}
|
||||||
|
|
||||||
// Adopt a Claude conversation ID observed from an external source (e.g. hook
|
// Adopt a Claude conversation ID observed from an external source (e.g. hook
|
||||||
// payload). In interactive PTY mode Claude CLI emits no JSON to stdout, so
|
// payload). In interactive PTY mode Claude CLI emits no JSON to stdout, so
|
||||||
// `_handleJsonMessage` never sees `session_id`; hooks are the only signal
|
// `_handleJsonMessage` never sees `session_id`; hooks are the only signal
|
||||||
@@ -1008,6 +1026,7 @@ export class Session extends EventEmitter {
|
|||||||
status: this._status,
|
status: this._status,
|
||||||
workingDir: this.workingDir,
|
workingDir: this.workingDir,
|
||||||
remote: this._remote,
|
remote: this._remote,
|
||||||
|
docker: this._docker,
|
||||||
currentTaskId: this._currentTaskId,
|
currentTaskId: this._currentTaskId,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
lastActivityAt: this._lastActivityAt,
|
lastActivityAt: this._lastActivityAt,
|
||||||
@@ -1197,7 +1216,7 @@ export class Session extends EventEmitter {
|
|||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
cols: ptyCols,
|
cols: ptyCols,
|
||||||
rows: ptyRows,
|
rows: ptyRows,
|
||||||
cwd: resolveMuxAttachCwd(this.workingDir, this._remote),
|
cwd: resolveMuxAttachCwd(this.workingDir, this._remote, this._docker),
|
||||||
// COD-75: codex/gemini get COLORTERM=truecolor — mirrors buildEnvExports()
|
// COD-75: codex/gemini get COLORTERM=truecolor — mirrors buildEnvExports()
|
||||||
// in tmux-manager.ts so the attach client and the tmux session agree.
|
// in tmux-manager.ts so the attach client and the tmux session agree.
|
||||||
env: buildMuxAttachEnv(this.mode === 'codex' || this.mode === 'gemini'),
|
env: buildMuxAttachEnv(this.mode === 'codex' || this.mode === 'gemini'),
|
||||||
@@ -1317,7 +1336,7 @@ export class Session extends EventEmitter {
|
|||||||
// repaint/alt-screen mode; issue #154). Remote sessions run claude on
|
// repaint/alt-screen mode; issue #154). Remote sessions run claude on
|
||||||
// another host, so a local probe wouldn't reflect their version — skip them
|
// another host, so a local probe wouldn't reflect their version — skip them
|
||||||
// and let the banner scrape handle those. Cached process-wide, best-effort.
|
// and let the banner scrape handle those. Cached process-wide, best-effort.
|
||||||
if (this.mode === 'claude' && !this._remote && !this._cliVersion) {
|
if (this.mode === 'claude' && !this._remote && !this._docker && !this._cliVersion) {
|
||||||
const probedVersion = getClaudeCliVersion();
|
const probedVersion = getClaudeCliVersion();
|
||||||
if (probedVersion) {
|
if (probedVersion) {
|
||||||
this._cliVersion = probedVersion;
|
this._cliVersion = probedVersion;
|
||||||
@@ -1330,6 +1349,31 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Docker sessions run claude INSIDE the container, so the local probe above
|
||||||
|
// reports the HOST claude (wrong version, and leaving cliVersion undefined
|
||||||
|
// silently disables wheel-forwarding, #154). Probe the IN-CONTAINER version
|
||||||
|
// instead — deferred so the container is up after the mux attach below.
|
||||||
|
if (this.mode === 'claude' && this._docker && !this._cliVersion) {
|
||||||
|
const dockerMeta = this._docker;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this._isStopped || this._cliVersion) return;
|
||||||
|
void probeDockerCliVersion(dockerMeta, this.mode)
|
||||||
|
.then((version) => {
|
||||||
|
if (!version || this._isStopped || this._cliVersion) return;
|
||||||
|
this._cliVersion = version;
|
||||||
|
this.emit('cliInfoUpdated', {
|
||||||
|
version: this._cliVersion,
|
||||||
|
model: this._cliModel,
|
||||||
|
accountType: this._cliAccountType,
|
||||||
|
latestVersion: this._cliLatestVersion,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* best-effort */
|
||||||
|
});
|
||||||
|
}, DOCKER_CLI_VERSION_PROBE_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
// If mux wrapping is enabled, create or attach to a mux session
|
// If mux wrapping is enabled, create or attach to a mux session
|
||||||
if (this._useMux && this._mux) {
|
if (this._useMux && this._mux) {
|
||||||
try {
|
try {
|
||||||
@@ -1350,6 +1394,7 @@ export class Session extends EventEmitter {
|
|||||||
effort: this._effort,
|
effort: this._effort,
|
||||||
historyLimit: this._tmuxHistoryLimit,
|
historyLimit: this._tmuxHistoryLimit,
|
||||||
remote: this._remote,
|
remote: this._remote,
|
||||||
|
docker: this._docker,
|
||||||
},
|
},
|
||||||
createSessionOptions: {
|
createSessionOptions: {
|
||||||
sessionId: this.id,
|
sessionId: this.id,
|
||||||
@@ -1368,6 +1413,7 @@ export class Session extends EventEmitter {
|
|||||||
effort: this._effort,
|
effort: this._effort,
|
||||||
historyLimit: this._tmuxHistoryLimit,
|
historyLimit: this._tmuxHistoryLimit,
|
||||||
remote: this._remote,
|
remote: this._remote,
|
||||||
|
docker: this._docker,
|
||||||
},
|
},
|
||||||
spawnErrLabel: 'mux attachment',
|
spawnErrLabel: 'mux attachment',
|
||||||
});
|
});
|
||||||
@@ -1738,6 +1784,7 @@ export class Session extends EventEmitter {
|
|||||||
envOverrides: this._envOverrides,
|
envOverrides: this._envOverrides,
|
||||||
historyLimit: this._tmuxHistoryLimit,
|
historyLimit: this._tmuxHistoryLimit,
|
||||||
remote: this._remote,
|
remote: this._remote,
|
||||||
|
docker: this._docker,
|
||||||
},
|
},
|
||||||
createSessionOptions: {
|
createSessionOptions: {
|
||||||
sessionId: this.id,
|
sessionId: this.id,
|
||||||
@@ -1748,6 +1795,7 @@ export class Session extends EventEmitter {
|
|||||||
envOverrides: this._envOverrides,
|
envOverrides: this._envOverrides,
|
||||||
historyLimit: this._tmuxHistoryLimit,
|
historyLimit: this._tmuxHistoryLimit,
|
||||||
remote: this._remote,
|
remote: this._remote,
|
||||||
|
docker: this._docker,
|
||||||
},
|
},
|
||||||
spawnErrLabel: 'shell mux attachment',
|
spawnErrLabel: 'shell mux attachment',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2214,6 +2214,10 @@ export class WebServer extends EventEmitter {
|
|||||||
// erasing `remote` from state.json on the next persist. mux-sessions.json
|
// erasing `remote` from state.json on the next persist. mux-sessions.json
|
||||||
// round-trips MuxSession.remote; state.json carries SessionState.remote.
|
// round-trips MuxSession.remote; state.json carries SessionState.remote.
|
||||||
remote: muxSession.remote ?? savedState?.remote,
|
remote: muxSession.remote ?? savedState?.remote,
|
||||||
|
// Docker metadata round-trips the same way (mux-sessions.json carries
|
||||||
|
// MuxSession.docker; state.json carries SessionState.docker), so recovery
|
||||||
|
// rebuilds the `docker exec` launch instead of a broken local command.
|
||||||
|
docker: muxSession.docker ?? savedState?.docker,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update session name if it was a "Restored:" placeholder or doesn't match saved name
|
// Update session name if it was a "Restored:" placeholder or doesn't match saved name
|
||||||
|
|||||||
Reference in New Issue
Block a user