Skip to content

Conversation

@code-yeongyu
Copy link
Owner

@code-yeongyu code-yeongyu commented Jan 25, 2026

Summary

  • Migrate LSP client from custom JSON-RPC implementation to vscode-jsonrpc library
  • Reduce code complexity by ~60 lines while improving protocol handling stability
  • Enable LSP server connection reuse through standard MessageConnection API

Changes

  • Replace manual JSON-RPC parsing: Remove ~150 lines of custom Content-Length header parsing, buffer management, and request tracking code
  • Add vscode-jsonrpc dependency: Use battle-tested MessageConnection with StreamMessageReader/Writer
  • Implement Bun↔Node stream bridges: Create Readable/Writable wrappers to connect Bun's Web Streams to Node.js streams (required by vscode-jsonrpc)
  • Preserve all existing functionality: Keep warmupClient(), cleanupTempDirectoryClients(), SIGBREAK handler, 15s timeout, and all LSP capabilities (codeAction, workspace/configuration, etc.)
  • Remove unnecessary comments: Clean up redundant signal handler comments

Testing

bun run typecheck
bun run build
bun test src/tools/lsp/

Integration tested with typescript-language-server:

  • ✅ textDocument/definition
  • ✅ textDocument/references
  • ✅ textDocument/documentSymbol
  • ✅ textDocument/diagnostic
  • ✅ textDocument/prepareRename
  • ✅ Server lifecycle (start, initialize, stop)

Related Issues


Summary by cubic

Migrates the LSP client to vscode-jsonrpc for more reliable protocol handling and simpler code. Uses MessageConnection with Bun↔Node stream bridges to enable stable LSP server connection reuse.

  • Refactors

    • Replace custom JSON-RPC parsing/tracking with StreamMessageReader/Writer and MessageConnection.
    • Add Bun Web Streams ↔ Node Readable/Writable bridges.
    • Preserve existing behavior: warmup, config, signal handlers, 15s timeout, and all LSP features.
  • Dependencies

    • Add vscode-jsonrpc (^8.2.0).
    • Patch-bump oh-my-opencode platform binaries to 3.0.1.

Written for commit 3f939f9. Summary will update on new commits.

Replace custom JSON-RPC implementation with vscode-jsonrpc library.
Use MessageConnection with StreamMessageReader/Writer.
Implement Bun↔Node stream bridges for compatibility.
Preserve all existing functionality (warmup, cleanup, capabilities).
Net reduction of ~60 lines while improving protocol handling.
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Confidence score: 3/5

  • Potential for unhandled rejections if the timeout isn't cleared after a successful response in src/tools/lsp/client.ts, which could cause noisy errors in normal use
  • Score reflects a moderate, user-impacting risk tied to request lifecycle handling, but limited in scope to timeout cleanup
  • Pay close attention to src/tools/lsp/client.ts - ensure timeouts are cleared on successful responses
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/tools/lsp/client.ts">

<violation number="1" location="src/tools/lsp/client.ts:344">
P2: Clear the timeout when the request finishes to avoid unhandled rejections after a successful response.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +344 to +353
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
if (this.pending.has(id)) {
this.pending.delete(id)
const stderr = this.stderrBuffer.slice(-5).join("\n")
reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
}
}, 15000)
const stderr = this.stderrBuffer.slice(-5).join("\n")
reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
}, this.REQUEST_TIMEOUT)
})
}

private notify(method: string, params?: unknown): void {
if (!this.proc) return
if (this.processExited || this.proc.exitCode !== null) return
const requestPromise = this.connection.sendRequest(method, params) as Promise<T>

const msg = JSON.stringify({ jsonrpc: "2.0", method, params })
this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
return Promise.race([requestPromise, timeoutPromise])
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Clear the timeout when the request finishes to avoid unhandled rejections after a successful response.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/lsp/client.ts, line 344:

<comment>Clear the timeout when the request finishes to avoid unhandled rejections after a successful response.</comment>

<file context>
@@ -294,142 +328,41 @@ export class LSPClient {
-
-    return new Promise((resolve, reject) => {
-      this.pending.set(id, { resolve, reject })
+    const timeoutPromise = new Promise<never>((_, reject) => {
       setTimeout(() => {
-        if (this.pending.has(id)) {
</file context>
Suggested change
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
if (this.pending.has(id)) {
this.pending.delete(id)
const stderr = this.stderrBuffer.slice(-5).join("\n")
reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
}
}, 15000)
const stderr = this.stderrBuffer.slice(-5).join("\n")
reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
}, this.REQUEST_TIMEOUT)
})
}
private notify(method: string, params?: unknown): void {
if (!this.proc) return
if (this.processExited || this.proc.exitCode !== null) return
const requestPromise = this.connection.sendRequest(method, params) as Promise<T>
const msg = JSON.stringify({ jsonrpc: "2.0", method, params })
this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
return Promise.race([requestPromise, timeoutPromise])
let timeoutId: ReturnType<typeof setTimeout> | undefined
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
const stderr = this.stderrBuffer.slice(-5).join("\n")
reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
}, this.REQUEST_TIMEOUT)
})
const requestPromise = this.connection.sendRequest(method, params) as Promise<T>
try {
return await Promise.race([requestPromise, timeoutPromise])
} finally {
if (timeoutId) clearTimeout(timeoutId)
}
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants