fix(claude-adapter): commit turn on result event, dont block on process exit
Previously dispatchToClaude awaited child.on(close) before yielding the done event. Claude CLIs Bash tool occasionally leaves ssh/bash grandchildren alive (e.g. a GUI app that ignores SIGPIPE on the remote end of a piped ssh command); that kept claude -p alive past end-of-turn, which kept the bridge SSE stream open, which kept OpenClaw from committing the turn to its session jsonl. Switch to emitting done as soon as the terminal result stream-json event arrives. Spawn claude in its own process group (detached:true) and schedule a best-effort SIGTERM/SIGKILL sweep of leaked descendants; temp-file cleanup runs asynchronously on actual process close. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -138,10 +138,14 @@ export async function* dispatchToClaude(
|
||||
}
|
||||
}
|
||||
|
||||
// detached:true puts claude in its own process group. Claude's Bash tool
|
||||
// occasionally leaks shells/ssh that keep claude alive past end-of-turn; when
|
||||
// that happens we SIGKILL the whole group rather than wait forever.
|
||||
const child = spawn("claude", args, {
|
||||
cwd: workspace,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: { ...process.env },
|
||||
detached: true,
|
||||
});
|
||||
|
||||
const stderrLines: string[] = [];
|
||||
@@ -157,6 +161,38 @@ export async function* dispatchToClaude(
|
||||
let done = false;
|
||||
let resolveNext: (() => void) | null = null;
|
||||
|
||||
let cleanupScheduled = false;
|
||||
const scheduleCleanup = (): void => {
|
||||
if (cleanupScheduled) return;
|
||||
cleanupScheduled = true;
|
||||
|
||||
const killGroup = (sig: NodeJS.Signals): void => {
|
||||
if (child.pid == null || child.exitCode !== null) return;
|
||||
try { process.kill(-child.pid, sig); } catch { /* already gone */ }
|
||||
};
|
||||
const termTimer = setTimeout(() => killGroup("SIGTERM"), 3000);
|
||||
const killTimer = setTimeout(() => killGroup("SIGKILL"), 10000);
|
||||
|
||||
child.once("close", () => {
|
||||
clearTimeout(termTimer);
|
||||
clearTimeout(killTimer);
|
||||
if (mcpConfigPath) {
|
||||
try { fs.unlinkSync(mcpConfigPath); } catch { /* ignore */ }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const markDone = (): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
scheduleCleanup();
|
||||
if (resolveNext) {
|
||||
const r = resolveNext;
|
||||
resolveNext = null;
|
||||
r();
|
||||
}
|
||||
};
|
||||
|
||||
rl.on("line", (line: string) => {
|
||||
if (!line.trim()) return;
|
||||
let event: Record<string, unknown>;
|
||||
@@ -180,6 +216,11 @@ export async function* dispatchToClaude(
|
||||
if (type === "result") {
|
||||
const sessionId = (event.session_id as string) ?? "";
|
||||
if (sessionId) capturedSessionId = sessionId;
|
||||
// `result` is the terminal stream-json event; commit the turn without
|
||||
// waiting for claude's process tree to fully exit (leaked Bash grandchildren
|
||||
// can otherwise hold stdout open indefinitely).
|
||||
markDone();
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolveNext) {
|
||||
@@ -190,12 +231,8 @@ export async function* dispatchToClaude(
|
||||
});
|
||||
|
||||
rl.on("close", () => {
|
||||
done = true;
|
||||
if (resolveNext) {
|
||||
const r = resolveNext;
|
||||
resolveNext = null;
|
||||
r();
|
||||
}
|
||||
// Fallback: claude exited without emitting a result event.
|
||||
markDone();
|
||||
});
|
||||
|
||||
while (true) {
|
||||
@@ -213,17 +250,6 @@ export async function* dispatchToClaude(
|
||||
yield events.shift()!;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
child.on("close", resolve);
|
||||
if (child.exitCode !== null) resolve();
|
||||
});
|
||||
|
||||
// Clean up temp files
|
||||
if (mcpConfigPath) {
|
||||
try { fs.unlinkSync(mcpConfigPath); } catch { /* ignore */ }
|
||||
// tool defs file path is embedded in the config — leave it for now
|
||||
}
|
||||
|
||||
if (capturedSessionId) {
|
||||
yield { type: "done", sessionId: capturedSessionId };
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user