Skip to content

Commit 89ed187

Browse files
committed
fix(opencode): flush stdio before cli exit
1 parent 717e74f commit 89ed187

3 files changed

Lines changed: 54 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type FlushableWriteStream = {
2+
destroyed?: boolean
3+
writableEnded?: boolean
4+
write(chunk: string, callback: () => void): boolean
5+
}
6+
7+
export function flushWriteStream(stream: FlushableWriteStream) {
8+
if (stream.destroyed || stream.writableEnded) return Promise.resolve()
9+
return new Promise<void>((resolve) => {
10+
stream.write("", () => resolve())
11+
})
12+
}

packages/opencode/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Heap } from "./cli/heap"
4040
import { drizzle } from "drizzle-orm/bun-sqlite"
4141
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
4242
import { isRecord } from "@/util/record"
43+
import { flushWriteStream } from "@/cli/stdout"
4344

4445
const processMetadata = ensureProcessMetadata("main")
4546

@@ -247,5 +248,6 @@ try {
247248
// Most notably, some docker-container-based MCP servers don't handle such signals unless
248249
// run using `docker run --init`.
249250
// Explicitly exit to avoid any hanging subprocesses.
251+
await Promise.all([flushWriteStream(process.stdout), flushWriteStream(process.stderr)])
250252
process.exit()
251253
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { flushWriteStream } from "../../src/cli/stdout"
3+
4+
describe("flushWriteStream", () => {
5+
test("waits for the stream write callback", async () => {
6+
let flush: (() => void) | undefined
7+
let resolved = false
8+
9+
const pending = flushWriteStream({
10+
write(_chunk, callback) {
11+
flush = () => callback()
12+
return false
13+
},
14+
}).then(() => {
15+
resolved = true
16+
})
17+
18+
await Promise.resolve()
19+
expect(resolved).toBe(false)
20+
21+
flush?.()
22+
await pending
23+
24+
expect(resolved).toBe(true)
25+
})
26+
27+
test("skips destroyed streams", async () => {
28+
let wrote = false
29+
30+
await flushWriteStream({
31+
destroyed: true,
32+
write() {
33+
wrote = true
34+
return true
35+
},
36+
})
37+
38+
expect(wrote).toBe(false)
39+
})
40+
})

0 commit comments

Comments
 (0)