From d0fa896c19f216a3eaf71c4b4823670e17610fae Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 2 Jul 2026 23:51:18 +0200 Subject: [PATCH] fix(dap): removed per-write exit handlers --- packages/coding-agent/src/dap/client.ts | 28 +++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/dap/client.ts b/packages/coding-agent/src/dap/client.ts index eb4230e23..7d9c8446b 100644 --- a/packages/coding-agent/src/dap/client.ts +++ b/packages/coding-agent/src/dap/client.ts @@ -71,6 +71,8 @@ export class DapClient { #eventHandlers = new Map>(); #anyEventHandlers = new Set(); #reverseRequestHandlers = new Map(); + #adapterExited = false; + #pendingWriteExitRejectors = new Set<() => void>(); constructor( adapter: DapResolvedAdapter, @@ -84,6 +86,10 @@ export class DapClient { this.#readable = options?.readable ?? (proc.stdout as ReadableStream); this.#writeSink = options?.writeSink ?? proc.stdin; this.#socket = options?.socket; + this.proc.exited.then( + () => this.#rejectPendingWritesForExit(), + () => this.#rejectPendingWritesForExit(), + ); } static async spawn({ adapter, cwd, socketReadyTimeoutMs }: DapSpawnOptions): Promise { @@ -417,6 +423,10 @@ export class DapClient { const flushResult = this.#writeSink.flush(); if (!(flushResult instanceof Promise)) return; + if (this.#adapterExited) { + throw new Error(`DAP adapter ${this.adapter.name} exited before write completed`); + } + const { promise: guardPromise, reject: guardReject, resolve: guardResolve } = Promise.withResolvers(); const timer = setTimeout( () => @@ -425,13 +435,10 @@ export class DapClient { ), WRITE_MESSAGE_TIMEOUT_MS, ); - // If the adapter exits mid-write, fail fast rather than blocking forever - // on a stdin that will never drain. `proc.exited` may resolve normally - // (clean exit) or reject (non-zero); either way the write is doomed. - const onExit = () => { + const rejectOnExit = () => { guardReject(new Error(`DAP adapter ${this.adapter.name} exited before write completed`)); }; - this.proc.exited.then(onExit, onExit); + this.#pendingWriteExitRejectors.add(rejectOnExit); try { await Promise.race([flushResult, guardPromise]); @@ -442,11 +449,20 @@ export class DapClient { throw error; } finally { clearTimeout(timer); - // Release the guard so any late onExit call becomes a no-op. + this.#pendingWriteExitRejectors.delete(rejectOnExit); + // Release the guard so any late timeout callback becomes a no-op. guardResolve(); } } + #rejectPendingWritesForExit(): void { + this.#adapterExited = true; + for (const reject of this.#pendingWriteExitRejectors) { + reject(); + } + this.#pendingWriteExitRejectors.clear(); + } + async dispose(): Promise { if (this.#disposed) return; this.#disposed = true;