From 3cd44445160721b432a9f99582ea3d7cfaa40e72 Mon Sep 17 00:00:00 2001 From: Cole Leavitt Date: Thu, 26 Feb 2026 19:31:25 -0700 Subject: [PATCH] fix(tui): resolve streaming freeze from unhandled finish event, missing timeouts, and unguarded effects - Fix stream termination in processor.ts: for-await loop now breaks on finish event - Increase SDK event batch window from 16ms to 50ms to reduce render churn - Add withTimeout to MCP client.listTools() to prevent indefinite hangs - Handle fire-and-forget effect Promise rejections in db.ts --- packages/opencode/src/cli/cmd/tui/context/sdk.tsx | 6 +++--- packages/opencode/src/mcp/index.ts | 4 +++- packages/opencode/src/session/processor.ts | 5 ++++- packages/opencode/src/storage/db.ts | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 7fa7e05c3d25..104ccb8be62b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -52,10 +52,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ const elapsed = Date.now() - last if (timer) return - // If we just flushed recently (within 16ms), batch this with future events + // If we just flushed recently (within 50ms), batch this with future events // Otherwise, process immediately to avoid latency - if (elapsed < 16) { - timer = setTimeout(flush, 16) + if (elapsed < 50) { + timer = setTimeout(flush, 50) return } flush() diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3c29fe03d30a..f8ba0d78714a 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -577,7 +577,9 @@ export namespace MCP { const toolsResults = await Promise.all( connectedClients.map(async ([clientName, client]) => { - const toolsResult = await client.listTools().catch((e) => { + const mcpEntry = config[clientName] + const timeout = (isMcpConfigured(mcpEntry) ? mcpEntry.timeout : undefined) ?? defaultTimeout ?? DEFAULT_TIMEOUT + const toolsResult = await withTimeout(client.listTools(), timeout).catch((e) => { log.error("failed to get tools", { clientName, error: e.message }) const failedStatus = { status: "failed" as const, diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index e7532d20073b..d154c6c80c00 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -50,6 +50,7 @@ export namespace SessionProcessor { try { let currentText: MessageV2.TextPart | undefined let reasoningMap: Record = {} + let finished = false const stream = await LLM.stream(streamInput) for await (const value of stream.fullStream) { @@ -337,6 +338,8 @@ export namespace SessionProcessor { break case "finish": + log.info("stream finish event received") + finished = true break default: @@ -345,7 +348,7 @@ export namespace SessionProcessor { }) continue } - if (needsCompaction) break + if (needsCompaction || finished) break } } catch (e: any) { log.error("process", { diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index f29aac18d163..b71f67d82c2c 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -122,7 +122,7 @@ export namespace Database { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) - for (const effect of effects) effect() + for (const effect of effects) Promise.resolve(effect()).catch((e) => log.error("effect failed", { error: e })) return result } throw err @@ -146,7 +146,7 @@ export namespace Database { const result = Client().transaction((tx) => { return ctx.provide({ tx, effects }, () => callback(tx)) }) - for (const effect of effects) effect() + for (const effect of effects) Promise.resolve(effect()).catch((e) => log.error("effect failed", { error: e })) return result } throw err