Compare commits

...

3 Commits

Author SHA1 Message Date
github-actions[bot]
9c5d80af1d release: v3.7.3 2026-02-18 06:05:04 +00:00
YeonGyu-Kim
1e05f4770e fix(cli-run): retry server start on port binding race condition
When port appears available but binding fails (race with another opencode
instance), retry on next available port (auto mode) or attach to existing
server (explicit port mode) instead of crashing with exit code 1.
2026-02-18 15:01:09 +09:00
github-actions[bot]
b1c43aeb89 @codeg-dev has signed the CLA in code-yeongyu/oh-my-opencode#1927 2026-02-18 01:13:27 +00:00
11 changed files with 121 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "3.7.2",
"version": "3.7.3",
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -74,13 +74,13 @@
"typescript": "^5.7.3"
},
"optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.7.2",
"oh-my-opencode-darwin-x64": "3.7.2",
"oh-my-opencode-linux-arm64": "3.7.2",
"oh-my-opencode-linux-arm64-musl": "3.7.2",
"oh-my-opencode-linux-x64": "3.7.2",
"oh-my-opencode-linux-x64-musl": "3.7.2",
"oh-my-opencode-windows-x64": "3.7.2"
"oh-my-opencode-darwin-arm64": "3.7.3",
"oh-my-opencode-darwin-x64": "3.7.3",
"oh-my-opencode-linux-arm64": "3.7.3",
"oh-my-opencode-linux-arm64-musl": "3.7.3",
"oh-my-opencode-linux-x64": "3.7.3",
"oh-my-opencode-linux-x64-musl": "3.7.3",
"oh-my-opencode-windows-x64": "3.7.3"
},
"trustedDependencies": [
"@ast-grep/cli",

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-arm64",
"version": "3.7.2",
"version": "3.7.3",
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-x64",
"version": "3.7.2",
"version": "3.7.3",
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64-musl",
"version": "3.7.2",
"version": "3.7.3",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64",
"version": "3.7.2",
"version": "3.7.3",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64-musl",
"version": "3.7.2",
"version": "3.7.3",
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64",
"version": "3.7.2",
"version": "3.7.3",
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-windows-x64",
"version": "3.7.2",
"version": "3.7.3",
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
"license": "MIT",
"repository": {

View File

@@ -1543,6 +1543,22 @@
"created_at": "2026-02-17T14:18:29Z",
"repoId": 1108837393,
"pullRequestNo": 1889
},
{
"name": "codeg-dev",
"id": 12405078,
"comment_id": 3915482750,
"created_at": "2026-02-17T15:47:18Z",
"repoId": 1108837393,
"pullRequestNo": 1927
},
{
"name": "codeg-dev",
"id": 12405078,
"comment_id": 3915952929,
"created_at": "2026-02-17T17:11:11Z",
"repoId": 1108837393,
"pullRequestNo": 1927
}
]
}

View File

@@ -95,6 +95,24 @@ describe("createServerConnection", () => {
expect(mockServerClose).toHaveBeenCalled()
})
it("explicit port attaches when start fails because port became occupied", async () => {
// given
const signal = new AbortController().signal
const port = 8080
mockIsPortAvailable.mockResolvedValueOnce(true).mockResolvedValueOnce(false)
mockCreateOpencode.mockRejectedValueOnce(new Error("Failed to start server on port 8080"))
// when
const result = await createServerConnection({ port, signal })
// then
expect(mockIsPortAvailable).toHaveBeenNthCalledWith(1, 8080, "127.0.0.1")
expect(mockIsPortAvailable).toHaveBeenNthCalledWith(2, 8080, "127.0.0.1")
expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ baseUrl: "http://127.0.0.1:8080" })
result.cleanup()
expect(mockServerClose).not.toHaveBeenCalled()
})
it("explicit port attaches when port is occupied", async () => {
// given
const signal = new AbortController().signal
@@ -133,6 +151,32 @@ describe("createServerConnection", () => {
expect(mockServerClose).toHaveBeenCalled()
})
it("auto mode retries on next port when initial start fails", async () => {
// given
const signal = new AbortController().signal
mockGetAvailableServerPort
.mockResolvedValueOnce({ port: 4096, wasAutoSelected: false })
.mockResolvedValueOnce({ port: 4097, wasAutoSelected: true })
mockCreateOpencode
.mockRejectedValueOnce(new Error("Failed to start server on port 4096"))
.mockResolvedValueOnce({
client: { session: {} },
server: { url: "http://127.0.0.1:4097", close: mockServerClose },
})
// when
const result = await createServerConnection({ signal })
// then
expect(mockGetAvailableServerPort).toHaveBeenNthCalledWith(1, 4096, "127.0.0.1")
expect(mockGetAvailableServerPort).toHaveBeenNthCalledWith(2, 4097, "127.0.0.1")
expect(mockCreateOpencode).toHaveBeenNthCalledWith(1, { signal, port: 4096, hostname: "127.0.0.1" })
expect(mockCreateOpencode).toHaveBeenNthCalledWith(2, { signal, port: 4097, hostname: "127.0.0.1" })
result.cleanup()
expect(mockServerClose).toHaveBeenCalledTimes(1)
})
it("invalid port throws error", async () => {
// given
const signal = new AbortController().signal

View File

@@ -5,6 +5,24 @@ import { getAvailableServerPort, isPortAvailable, DEFAULT_SERVER_PORT } from "..
import { withWorkingOpencodePath } from "./opencode-binary-resolver"
import { prependResolvedOpencodeBinToPath } from "./opencode-bin-path"
function isPortStartFailure(error: unknown, port: number): boolean {
if (!(error instanceof Error)) {
return false
}
return error.message.includes(`Failed to start server on port ${port}`)
}
async function startServer(options: { signal: AbortSignal, port: number }): Promise<ServerConnection> {
const { signal, port } = options
const { client, server } = await withWorkingOpencodePath(() =>
createOpencode({ signal, port, hostname: "127.0.0.1" }),
)
console.log(pc.dim("Server listening at"), pc.cyan(server.url))
return { client, cleanup: () => server.close() }
}
export async function createServerConnection(options: {
port?: number
attach?: string
@@ -29,11 +47,22 @@ export async function createServerConnection(options: {
if (available) {
console.log(pc.dim("Starting server on port"), pc.cyan(port.toString()))
const { client, server } = await withWorkingOpencodePath(() =>
createOpencode({ signal, port, hostname: "127.0.0.1" }),
)
console.log(pc.dim("Server listening at"), pc.cyan(server.url))
return { client, cleanup: () => server.close() }
try {
return await startServer({ signal, port })
} catch (error) {
if (!isPortStartFailure(error, port)) {
throw error
}
const stillAvailable = await isPortAvailable(port, "127.0.0.1")
if (stillAvailable) {
throw error
}
console.log(pc.dim("Port"), pc.cyan(port.toString()), pc.dim("became occupied, attaching to existing server"))
const client = createOpencodeClient({ baseUrl: `http://127.0.0.1:${port}` })
return { client, cleanup: () => {} }
}
}
console.log(pc.dim("Port"), pc.cyan(port.toString()), pc.dim("is occupied, attaching to existing server"))
@@ -47,9 +76,16 @@ export async function createServerConnection(options: {
} else {
console.log(pc.dim("Starting server on port"), pc.cyan(selectedPort.toString()))
}
const { client, server } = await withWorkingOpencodePath(() =>
createOpencode({ signal, port: selectedPort, hostname: "127.0.0.1" }),
)
console.log(pc.dim("Server listening at"), pc.cyan(server.url))
return { client, cleanup: () => server.close() }
try {
return await startServer({ signal, port: selectedPort })
} catch (error) {
if (!isPortStartFailure(error, selectedPort)) {
throw error
}
const { port: retryPort } = await getAvailableServerPort(selectedPort + 1, "127.0.0.1")
console.log(pc.dim("Retrying server start on port"), pc.cyan(retryPort.toString()))
return await startServer({ signal, port: retryPort })
}
}