mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
fix(session): probe the Claude CLI version over ssh for remote sessions
Remote Claude sessions were the one backend left relying on the startup-banner scrape for cliVersion (the unreliable path #154 was filed for: newer Claude Code builds print no banner and resumed sessions never do), so wheel/touch forwarding silently stayed off for them. Mirror the docker approach: a deferred best-effort probe at session start, running claude --version on the remote host through the same buildSshConnectionArgs + login-shell wrapper as the real launch, parsing the first semver in stdout (an interactive login shell may echo rc-file noise around it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -256,6 +256,69 @@ export async function checkRemoteTmuxAvailable(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The CLI binary each session mode runs on the remote host. Antigravity's
|
||||
* binary is `agy` (the mode name is not the command); shell has no CLI to
|
||||
* probe, so it is absent.
|
||||
*/
|
||||
const REMOTE_CLI_BIN: Partial<Record<SessionMode, string>> = {
|
||||
claude: 'claude',
|
||||
opencode: 'opencode',
|
||||
codex: 'codex',
|
||||
gemini: 'gemini',
|
||||
antigravity: 'agy',
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the SSH command that reads the remote CLI's version (`claude --version`
|
||||
* on the remote host). The version query is routed through
|
||||
* `remoteLoginShellCommand` (the SAME `$SHELL -i -l -c` wrapper the real
|
||||
* launch uses), because agent CLIs live on PATH only after the remote user's
|
||||
* interactive-login startup files run (see defaultRemoteCommandForMode); a bare
|
||||
* `claude --version` over ssh exits 127. Connection options come from the
|
||||
* shared `buildSshConnectionArgs`, so the probe reaches exactly the hosts the
|
||||
* launch can reach. Returns null for modes with no CLI (shell).
|
||||
*/
|
||||
export function buildRemoteCliVersionProbeCommand(
|
||||
host: Pick<RemoteHost, 'username' | 'host' | 'port'> & RemoteSshOptions,
|
||||
mode: SessionMode
|
||||
): string | null {
|
||||
const bin = REMOTE_CLI_BIN[mode];
|
||||
if (!bin) return null;
|
||||
return [
|
||||
...buildSshConnectionArgs(host),
|
||||
remoteSshTarget(host),
|
||||
shellescape(remoteLoginShellCommand(`${bin} --version`)),
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the CLI version installed ON THE REMOTE HOST. Feeds Session.cliVersion
|
||||
* for remote sessions: the deterministic local probe deliberately skips them
|
||||
* (it would report the LOCAL host's claude), and the startup-banner scrape is
|
||||
* unreliable (newer Claude Code builds print no banner; resumed sessions never
|
||||
* do), which left cliVersion undefined and silently disabled wheel-forwarding
|
||||
* to the CLI transcript (residual #154, noted in the #205 analysis). The
|
||||
* version is parsed as the first semver in stdout, never raw output: an
|
||||
* interactive-login shell may echo rc-file noise around it. Returns undefined
|
||||
* on any failure. No-op under VITEST (mirrors checkRemoteTmuxAvailable).
|
||||
*/
|
||||
export async function probeRemoteCliVersion(
|
||||
host: Pick<RemoteHost, 'username' | 'host' | 'port'> & RemoteSshOptions,
|
||||
mode: SessionMode
|
||||
): Promise<string | undefined> {
|
||||
if (process.env.VITEST) return undefined;
|
||||
const command = buildRemoteCliVersionProbeCommand(host, mode);
|
||||
if (!command) return undefined;
|
||||
try {
|
||||
const { stdout } = await execAsync(command, { timeout: 15_000 });
|
||||
const match = stdout.match(/\d+\.\d+\.\d+/);
|
||||
return match ? match[0] : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* COD-105 — build the SSH command that lists `codeman-*` tmux sessions on a
|
||||
* remote host's canonical `-L codeman` socket.
|
||||
|
||||
+31
-2
@@ -54,6 +54,7 @@ import {
|
||||
type SessionDocker,
|
||||
} from './types.js';
|
||||
import { probeDockerCliVersion } from './docker-hosts.js';
|
||||
import { probeRemoteCliVersion } from './remote-hosts.js';
|
||||
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
|
||||
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
|
||||
import { RalphTracker } from './ralph-tracker.js';
|
||||
@@ -226,6 +227,8 @@ const IS_TEST_MODE = !!process.env.VITEST;
|
||||
const TEST_PTY_SCRIPT = 'if (process.stdin.isTTY) process.stdin.setRawMode(true); process.stdin.pipe(process.stdout);';
|
||||
/** Delay before the in-container Claude CLI version probe (lets the container start). */
|
||||
const DOCKER_CLI_VERSION_PROBE_DELAY_MS = 3000;
|
||||
/** Delay before the over-ssh Claude CLI version probe (keeps session start off the ssh round-trip). */
|
||||
const REMOTE_CLI_VERSION_PROBE_DELAY_MS = 3000;
|
||||
|
||||
/**
|
||||
* Ask tmux for the current window geometry of `muxName` so a re-attaching PTY
|
||||
@@ -1534,8 +1537,8 @@ export class Session extends EventEmitter {
|
||||
// never show it — which left cliVersion undefined and silently disabled
|
||||
// wheel-forwarding to Claude's own transcript (the only route to history in
|
||||
// repaint/alt-screen mode; issue #154). Remote sessions run claude on
|
||||
// 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.
|
||||
// another host, so a local probe wouldn't reflect their version; they get
|
||||
// their own over-ssh probe below. Cached process-wide, best-effort.
|
||||
if (this.mode === 'claude' && !this._remote && !this._docker && !this._cliVersion) {
|
||||
const probedVersion = getClaudeCliVersion();
|
||||
if (probedVersion) {
|
||||
@@ -1574,6 +1577,32 @@ export class Session extends EventEmitter {
|
||||
}, DOCKER_CLI_VERSION_PROBE_DELAY_MS);
|
||||
}
|
||||
|
||||
// Remote sessions run claude on ANOTHER HOST, so neither the local nor the
|
||||
// docker probe applies, and the banner-scrape fallback they were left with
|
||||
// is the unreliable path #154 was filed for, so remote Claude cases silently
|
||||
// never got wheel-forwarding (noted in the #205 analysis). Probe over ssh,
|
||||
// deferred so session start never waits on the ssh round-trip.
|
||||
if (this.mode === 'claude' && this._remote && !this._cliVersion) {
|
||||
const remoteMeta = this._remote;
|
||||
setTimeout(() => {
|
||||
if (this._isStopped || this._cliVersion) return;
|
||||
void probeRemoteCliVersion(remoteMeta, 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 */
|
||||
});
|
||||
}, REMOTE_CLI_VERSION_PROBE_DELAY_MS);
|
||||
}
|
||||
|
||||
// If mux wrapping is enabled, create or attach to a mux session
|
||||
if (this._useMux && this._mux) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user