diff --git a/src/agents/copilot-runner.ts b/src/agents/copilot-runner.ts index b01757fcf..ee188b173 100644 --- a/src/agents/copilot-runner.ts +++ b/src/agents/copilot-runner.ts @@ -127,15 +127,6 @@ export async function runCopilotCliAgent(params: { systemPrompt, timeoutMs: params.timeoutMs, sessionId: params.cliSessionId, - onEvent: (event) => { - // Forward streaming events if streamParams is provided - if (params.streamParams?.onDelta && event.type === "assistant.message_delta") { - const data = event.data as { content?: string }; - if (typeof data.content === "string") { - params.streamParams.onDelta(data.content); - } - } - }, }); const text = result.text?.trim(); diff --git a/src/agents/copilot-sdk.ts b/src/agents/copilot-sdk.ts index d7834176d..7eef2ac95 100644 --- a/src/agents/copilot-sdk.ts +++ b/src/agents/copilot-sdk.ts @@ -84,7 +84,7 @@ export async function createCopilotClient( cliPath: options?.cliPath ?? "copilot", cwd: options?.cwd, logLevel: options?.logLevel ?? "warning", - autoRestart: options?.autoRestart ?? true, + autoRestart: false, // Disable auto-restart to avoid keeping event loop alive useStdio: true, // Use stdio transport for better process control autoStart: false, // We'll start manually for better error handling env: options?.env, @@ -223,22 +223,68 @@ export async function runCopilotAgent(params: RunCopilotAgentParams): Promise { - // Ignore cleanup errors - }); + // Abort any in-flight request, then destroy the session + if (session) { + try { + await session.abort(); + } catch { + // Ignore abort errors (may not have active request) + } + + // Only destroy session if we created a new one (not resuming) + if (!params.sessionId) { + try { + await session.destroy(); + } catch { + // Ignore destroy errors + } + } } - // Always stop the client - await client.stop().catch(() => { - // Ignore cleanup errors - }); + // Access internal SDK handles BEFORE cleanup so we can unref them after + const clientAny = client as unknown as { + cliProcess?: { + unref?: () => void; + stdin?: { unref?: () => void }; + stdout?: { unref?: () => void }; + stderr?: { unref?: () => void }; + removeAllListeners?: () => void; + }; + socket?: { unref?: () => void }; + }; + const cliProcess = clientAny.cliProcess; + const socket = clientAny.socket; + + // Stop the client gracefully first, then force if needed + try { + await client.stop(); + } catch { + try { + await client.forceStop(); + } catch { + // Ignore forceStop errors + } + } + + // Unref all handles to allow Node to exit (works around SDK cleanup bug) + if (cliProcess) { + cliProcess.unref?.(); + cliProcess.stdin?.unref?.(); + cliProcess.stdout?.unref?.(); + cliProcess.stderr?.unref?.(); + cliProcess.removeAllListeners?.(); + } + if (socket) { + socket.unref?.(); + } } } @@ -248,7 +294,7 @@ export async function runCopilotAgent(params: RunCopilotAgentParams): Promise> { +}): Promise> { const client = await createCopilotClient({ cliPath: options?.cliPath, cwd: options?.cwd, @@ -259,11 +305,14 @@ export async function listCopilotSessions(options?: { const sessions = await client.listSessions(); return sessions.map((s) => ({ sessionId: s.sessionId, - model: s.model, - createdAt: s.createdAt, + createdAt: s.startTime?.toISOString(), })); } finally { - await client.stop().catch(() => {}); + try { + await client.stop(); + } catch { + // Ignore cleanup errors + } } } @@ -287,6 +336,10 @@ export async function deleteCopilotSession( await client.deleteSession(sessionId); log.info("deleted copilot session", { sessionId }); } finally { - await client.stop().catch(() => {}); + try { + await client.stop(); + } catch { + // Ignore cleanup errors + } } }