mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
🐛 fix(desktop): spawn the detector-resolved Codex path, not the bare command
Adding the bundle paths to the detector only made preflight report
available; spawn still ran the bare `session.command` ('codex'). A user
with only Codex.app (no `codex` on PATH) passed preflight but then hit
ENOENT at spawn('codex').
Thread the absolute path the detector resolved out of preflight and feed
it to resolveCliSpawnPlan/spawn. This also hardens the existing
login-shell-PATH-only case, where spawn runs against a leaner env than
detection did. Skipped on Windows, where resolveCliSpawnPlan does its own
.cmd/.exe shim resolution from the bare command that we must not pre-empt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -199,6 +199,16 @@ interface AgentSession {
|
||||
|
||||
type SessionErrorPayload = HeterogeneousAgentSessionError | string;
|
||||
|
||||
interface SpawnPreflightResult {
|
||||
/** Set when the CLI is missing/unavailable — spawning must be aborted. */
|
||||
error?: HeterogeneousAgentSessionError;
|
||||
/**
|
||||
* Absolute path the detector resolved the CLI to, to spawn instead of the
|
||||
* bare `session.command`. Undefined when the bare command should be used.
|
||||
*/
|
||||
resolvedCommand?: string;
|
||||
}
|
||||
|
||||
interface CliTraceSession {
|
||||
dir: string;
|
||||
writeQueue: Promise<void>;
|
||||
@@ -451,16 +461,14 @@ export default class HeterogeneousAgentCtr extends ControllerModule {
|
||||
return relevantStderr || `Agent exited with code ${code}`;
|
||||
}
|
||||
|
||||
private async getSpawnPreflightError(
|
||||
session: AgentSession,
|
||||
): Promise<HeterogeneousAgentSessionError | undefined> {
|
||||
private async runSpawnPreflight(session: AgentSession): Promise<SpawnPreflightResult> {
|
||||
const defaultCommand =
|
||||
session.agentType === 'claude-code'
|
||||
? 'claude'
|
||||
: session.agentType === 'codex'
|
||||
? 'codex'
|
||||
: undefined;
|
||||
if (!defaultCommand) return;
|
||||
if (!defaultCommand) return {};
|
||||
|
||||
const command = this.resolveSessionCommand(session);
|
||||
const status =
|
||||
@@ -472,9 +480,22 @@ export default class HeterogeneousAgentCtr extends ControllerModule {
|
||||
);
|
||||
const cliMissingError = this.buildCliMissingError(session);
|
||||
|
||||
if (!status || status.available || !cliMissingError) return;
|
||||
if (!status || status.available || !cliMissingError) {
|
||||
// The detector resolved the binary to an absolute path (e.g. the Codex.app
|
||||
// bundle, or a login-shell-PATH-only install). spawn() runs against a leaner
|
||||
// env than detection did, so a bare `codex` could still ENOENT even though
|
||||
// preflight passed — feed the resolved absolute path through to spawn.
|
||||
//
|
||||
// Skip on Windows: there `resolveCliSpawnPlan` performs its own `.cmd`/`.exe`
|
||||
// shim resolution from the bare command, which we must not pre-empt.
|
||||
const resolvedCommand =
|
||||
process.platform !== 'win32' && status?.available && status.path?.trim()
|
||||
? status.path
|
||||
: undefined;
|
||||
return { resolvedCommand };
|
||||
}
|
||||
|
||||
return cliMissingError;
|
||||
return { error: cliMissingError };
|
||||
}
|
||||
|
||||
private get shouldTraceCliOutput(): boolean {
|
||||
@@ -888,13 +909,13 @@ export default class HeterogeneousAgentCtr extends ControllerModule {
|
||||
const session = this.sessions.get(params.sessionId);
|
||||
if (!session) throw new Error(`Session not found: ${params.sessionId}`);
|
||||
|
||||
const preflightError = await this.getSpawnPreflightError(session);
|
||||
if (preflightError) {
|
||||
const preflight = await this.runSpawnPreflight(session);
|
||||
if (preflight.error) {
|
||||
this.broadcast('heteroAgentSessionError', {
|
||||
error: preflightError,
|
||||
error: preflight.error,
|
||||
sessionId: session.sessionId,
|
||||
});
|
||||
throw new Error(preflightError.message);
|
||||
throw new Error(preflight.error.message);
|
||||
}
|
||||
|
||||
// Stand up the AskUserQuestion MCP bridge for claude-code prompts BEFORE
|
||||
@@ -966,7 +987,10 @@ export default class HeterogeneousAgentCtr extends ControllerModule {
|
||||
}
|
||||
const useStdin = spawnPlan.stdinPayload !== undefined;
|
||||
const cliArgs = spawnPlan.args;
|
||||
const resolvedCliSpawnPlan = await resolveCliSpawnPlan(session.command, cliArgs);
|
||||
const resolvedCliSpawnPlan = await resolveCliSpawnPlan(
|
||||
preflight.resolvedCommand || session.command,
|
||||
cliArgs,
|
||||
);
|
||||
|
||||
logger.info(
|
||||
'Spawning agent:',
|
||||
|
||||
@@ -442,6 +442,28 @@ describe('HeterogeneousAgentCtr', () => {
|
||||
expect(spawnCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('spawns the detector-resolved absolute path (e.g. Codex.app bundle) instead of the bare command', async () => {
|
||||
// A user with only the Codex desktop app has no `codex` on PATH; the
|
||||
// detector resolves the bundled binary. Preflight passing isn't enough —
|
||||
// spawn must target that absolute path or it ENOENTs on the bare command.
|
||||
const bundlePath = '/Applications/Codex.app/Contents/Resources/codex';
|
||||
const detect = vi.fn().mockResolvedValue({ available: true, path: bundlePath });
|
||||
const { proc } = createFakeProc({ stdoutLines: [] });
|
||||
nextFakeProc = proc;
|
||||
|
||||
const ctr = new HeterogeneousAgentCtr({
|
||||
appStoragePath,
|
||||
storeManager: { get: vi.fn() },
|
||||
toolDetectorManager: { detect },
|
||||
} as any);
|
||||
const { sessionId } = await ctr.startSession({ agentType: 'codex', command: 'codex' });
|
||||
await ctr.sendPrompt({ operationId: 'op-test', prompt: 'hello', sessionId });
|
||||
|
||||
expect(detect).toHaveBeenCalledWith('codex', true);
|
||||
expect(spawnCalls).toHaveLength(1);
|
||||
expect(spawnCalls[0].command).toBe(bundlePath);
|
||||
});
|
||||
|
||||
it('fails fast when a customized Claude command is unavailable instead of checking the default detector', async () => {
|
||||
execFileMock.mockImplementation(
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user