22import { spawn , type ChildProcess } from "node:child_process" ;
33import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce" ;
44import type { OpenClawConfig } from "../config/types.openclaw.js" ;
5+ import { createAbortError } from "../infra/abort-signal.js" ;
56import { sanitizeHostExecEnv } from "../infra/host-env-security.js" ;
67import { logDebug , logWarn } from "../logger.js" ;
78import {
@@ -39,6 +40,7 @@ type PendingLspRequest = {
3940 resolve : ( v : unknown ) => void ;
4041 reject : ( e : Error ) => void ;
4142 timeout : ReturnType < typeof setTimeout > ;
43+ dispose : ( ) => void ;
4244} ;
4345
4446type LspServerCapabilities = {
@@ -114,13 +116,22 @@ function rememberLspFailure(session: LspSession, error: Error): void {
114116 session . failure ??= error ;
115117}
116118
119+ function takePendingLspRequest ( session : LspSession , id : number ) : PendingLspRequest | undefined {
120+ const pending = session . pendingRequests . get ( id ) ;
121+ if ( ! pending ) {
122+ return undefined ;
123+ }
124+ session . pendingRequests . delete ( id ) ;
125+ clearTimeout ( pending . timeout ) ;
126+ pending . dispose ( ) ;
127+ return pending ;
128+ }
129+
117130function failLspSession ( session : LspSession , error : Error ) : void {
118131 rememberLspFailure ( session , error ) ;
119- for ( const pending of session . pendingRequests . values ( ) ) {
120- clearTimeout ( pending . timeout ) ;
121- pending . reject ( session . failure ?? error ) ;
132+ for ( const [ id ] of session . pendingRequests ) {
133+ takePendingLspRequest ( session , id ) ?. reject ( session . failure ?? error ) ;
122134 }
123- session . pendingRequests . clear ( ) ;
124135}
125136
126137function lspProcessExitError (
@@ -205,20 +216,49 @@ function parseLspMessages(buffer: Buffer): { messages: unknown[]; remaining: Buf
205216 return { messages, remaining } ;
206217}
207218
208- function sendRequest ( session : LspSession , method : string , params ?: unknown ) : Promise < unknown > {
219+ function lspAbortError ( signal ?: AbortSignal ) : Error {
220+ return signal ?. reason instanceof Error
221+ ? signal . reason
222+ : createAbortError ( "LSP request aborted" , { cause : signal ?. reason } ) ;
223+ }
224+
225+ function sendRequest (
226+ session : LspSession ,
227+ method : string ,
228+ params ?: unknown ,
229+ signal ?: AbortSignal ,
230+ ) : Promise < unknown > {
209231 if ( session . failure ) {
210232 return Promise . reject ( session . failure ) ;
211233 }
234+ if ( signal ?. aborted ) {
235+ return Promise . reject ( lspAbortError ( signal ) ) ;
236+ }
212237 const id = ++ session . requestId ;
213238 return new Promise ( ( resolve , reject ) => {
214239 const timeout = setTimeout ( ( ) => {
215- if ( session . pendingRequests . has ( id ) ) {
216- session . pendingRequests . delete ( id ) ;
217- reject ( new Error ( `LSP request ${ method } timed out` ) ) ;
218- }
240+ takePendingLspRequest ( session , id ) ?. reject ( new Error ( `LSP request ${ method } timed out` ) ) ;
219241 } , 10_000 ) ;
220242 timeout . unref ?.( ) ;
221- session . pendingRequests . set ( id , { resolve, reject, timeout } ) ;
243+ const onAbort = ( ) => {
244+ const pending = takePendingLspRequest ( session , id ) ;
245+ if ( ! pending ) {
246+ return ;
247+ }
248+ // Bundle tools share the server process, so cancel only this request.
249+ try {
250+ session . process . stdin ?. write (
251+ encodeLspMessage ( { jsonrpc : "2.0" , method : "$/cancelRequest" , params : { id } } ) ,
252+ "utf-8" ,
253+ ) ;
254+ } catch {
255+ // Best-effort notification; the local tool promise must still settle.
256+ }
257+ pending . reject ( lspAbortError ( signal ) ) ;
258+ } ;
259+ const dispose = ( ) => signal ?. removeEventListener ( "abort" , onAbort ) ;
260+ session . pendingRequests . set ( id , { resolve, reject, timeout, dispose } ) ;
261+ signal ?. addEventListener ( "abort" , onAbort , { once : true } ) ;
222262 const message = { jsonrpc : "2.0" , id, method, params } ;
223263 const encoded = encodeLspMessage ( message ) ;
224264 session . process . stdin ?. write ( encoded , "utf-8" ) ;
@@ -240,10 +280,8 @@ function handleIncomingData(session: LspSession, chunk: Buffer | string) {
240280 const record = msg as Record < string , unknown > ;
241281
242282 if ( "id" in record && typeof record . id === "number" ) {
243- const pending = session . pendingRequests . get ( record . id ) ;
283+ const pending = takePendingLspRequest ( session , record . id ) ;
244284 if ( pending ) {
245- session . pendingRequests . delete ( record . id ) ;
246- clearTimeout ( pending . timeout ) ;
247285 if ( "error" in record ) {
248286 pending . reject ( new Error ( JSON . stringify ( record . error ) ) ) ;
249287 } else {
@@ -316,11 +354,9 @@ async function disposeSession(session: LspSession) {
316354 // best-effort
317355 }
318356 }
319- for ( const [ , pending ] of session . pendingRequests ) {
320- clearTimeout ( pending . timeout ) ;
321- pending . reject ( new Error ( "LSP session disposed" ) ) ;
357+ for ( const [ id ] of session . pendingRequests ) {
358+ takePendingLspRequest ( session , id ) ?. reject ( new Error ( "LSP session disposed" ) ) ;
322359 }
323- session . pendingRequests . clear ( ) ;
324360 terminateLspProcessTree ( session ) ;
325361}
326362
@@ -349,12 +385,17 @@ function createLspPositionTool(params: {
349385 } ,
350386 required : [ "uri" , "line" , "character" ] ,
351387 } ,
352- execute : async ( _toolCallId , input ) => {
388+ execute : async ( _toolCallId , input , signal ) => {
353389 const position = input as LspPositionParams ;
354- const result = await sendRequest ( params . session , params . method , {
355- textDocument : { uri : position . uri } ,
356- position : { line : position . line , character : position . character } ,
357- } ) ;
390+ const result = await sendRequest (
391+ params . session ,
392+ params . method ,
393+ {
394+ textDocument : { uri : position . uri } ,
395+ position : { line : position . line , character : position . character } ,
396+ } ,
397+ signal ,
398+ ) ;
358399 return formatLspResult ( params . session . serverName , params . resultLabel , result ) ;
359400 } ,
360401 } ;
@@ -409,18 +450,23 @@ function buildLspTools(session: LspSession): AnyAgentTool[] {
409450 } ,
410451 required : [ "uri" , "line" , "character" ] ,
411452 } ,
412- execute : async ( _toolCallId , input ) => {
453+ execute : async ( _toolCallId , input , signal ) => {
413454 const params = input as {
414455 uri : string ;
415456 line : number ;
416457 character : number ;
417458 includeDeclaration ?: boolean ;
418459 } ;
419- const result = await sendRequest ( session , "textDocument/references" , {
420- textDocument : { uri : params . uri } ,
421- position : { line : params . line , character : params . character } ,
422- context : { includeDeclaration : params . includeDeclaration ?? true } ,
423- } ) ;
460+ const result = await sendRequest (
461+ session ,
462+ "textDocument/references" ,
463+ {
464+ textDocument : { uri : params . uri } ,
465+ position : { line : params . line , character : params . character } ,
466+ context : { includeDeclaration : params . includeDeclaration ?? true } ,
467+ } ,
468+ signal ,
469+ ) ;
424470 return formatLspResult ( serverLabel , "references" , result ) ;
425471 } ,
426472 } ) ;
0 commit comments