diff --git a/src/__tests__/engine-error.test.ts b/src/__tests__/engine-error.test.ts index d018242..bcc9ca2 100644 --- a/src/__tests__/engine-error.test.ts +++ b/src/__tests__/engine-error.test.ts @@ -109,6 +109,7 @@ describe('PieceEngine Integration: Error Handling', () => { const reason = abortFn.mock.calls[0]![1] as string; expect(reason).toContain('API connection failed'); }); + }); // ===================================================== diff --git a/src/infra/opencode/client.ts b/src/infra/opencode/client.ts index 42f2918..1a5dd25 100644 --- a/src/infra/opencode/client.ts +++ b/src/infra/opencode/client.ts @@ -6,6 +6,7 @@ */ import { createOpencode } from '@opencode-ai/sdk/v2'; +import { createServer } from 'node:net'; import type { AgentResponse } from '../../core/models/index.js'; import { createLogger, getErrorMessage } from '../../shared/utils/index.js'; import { mapToOpenCodePermissionReply, type OpenCodeCallOptions } from './types.js'; @@ -35,8 +36,32 @@ const OPENCODE_RETRYABLE_ERROR_PATTERNS = [ 'etimedout', 'eai_again', 'fetch failed', + 'failed to start server on port', ]; +async function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.unref(); + server.on('error', reject); + server.listen(0, '127.0.0.1', () => { + const addr = server.address(); + if (!addr || typeof addr === 'string') { + server.close(() => reject(new Error('Failed to allocate free TCP port'))); + return; + } + const port = addr.port; + server.close((err) => { + if (err) { + reject(err); + return; + } + resolve(port); + }); + }); + }); +} + /** * Client for OpenCode SDK agent interactions. * @@ -129,7 +154,9 @@ export class OpenCodeClient { attempt, }); + const port = await getFreePort(); const { client, server } = await createOpencode({ + port, signal: streamAbortController.signal, ...(options.opencodeApiKey ? { config: { provider: { opencode: { options: { apiKey: options.opencodeApiKey } } } } }