diff --git a/packages/sdk-typescript/README.md b/packages/sdk-typescript/README.md index de161396..bc3ef6aa 100644 --- a/packages/sdk-typescript/README.md +++ b/packages/sdk-typescript/README.md @@ -59,7 +59,7 @@ Creates a new query session with the Qwen Code. | `model` | `string` | - | The AI model to use (e.g., `'qwen-max'`, `'qwen-plus'`, `'qwen-turbo'`). Takes precedence over `OPENAI_MODEL` and `QWEN_MODEL` environment variables. | | `pathToQwenExecutable` | `string` | Auto-detected | Path to the Qwen Code executable. Supports multiple formats: `'qwen'` (native binary from PATH), `'/path/to/qwen'` (explicit path), `'/path/to/cli.js'` (Node.js bundle), `'node:/path/to/cli.js'` (force Node.js runtime), `'bun:/path/to/cli.js'` (force Bun runtime). If not provided, auto-detects from: `QWEN_CODE_CLI_PATH` env var, `~/.volta/bin/qwen`, `~/.npm-global/bin/qwen`, `/usr/local/bin/qwen`, `~/.local/bin/qwen`, `~/node_modules/.bin/qwen`, `~/.yarn/bin/qwen`. | | `permissionMode` | `'default' \| 'plan' \| 'auto-edit' \| 'yolo'` | `'default'` | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details. | -| `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 30 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). | +| `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). | | `env` | `Record` | - | Environment variables to pass to the Qwen Code process. Merged with the current process environment. | | `mcpServers` | `Record` | - | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`. | | `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. | @@ -76,12 +76,12 @@ Creates a new query session with the Qwen Code. The SDK enforces the following default timeouts: -| Timeout | Default | Description | -| ---------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `canUseTool` | 30 seconds | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied. | -| `mcpRequest` | 1 minute | Maximum time for SDK MCP tool calls to complete. | -| `controlRequest` | 30 seconds | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, and `interrupt()` to complete. | -| `streamClose` | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. | +| Timeout | Default | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `canUseTool` | 1 minute | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied. | +| `mcpRequest` | 1 minute | Maximum time for SDK MCP tool calls to complete. | +| `controlRequest` | 1 minute | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, and `interrupt()` to complete. | +| `streamClose` | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. | You can customize these timeouts via the `timeout` option: diff --git a/packages/sdk-typescript/src/query/Query.ts b/packages/sdk-typescript/src/query/Query.ts index 88a0e7f1..78bb10b9 100644 --- a/packages/sdk-typescript/src/query/Query.ts +++ b/packages/sdk-typescript/src/query/Query.ts @@ -5,9 +5,9 @@ * Implements AsyncIterator protocol for message consumption. */ -const DEFAULT_CAN_USE_TOOL_TIMEOUT = 30_000; +const DEFAULT_CAN_USE_TOOL_TIMEOUT = 60_000; const DEFAULT_MCP_REQUEST_TIMEOUT = 60_000; -const DEFAULT_CONTROL_REQUEST_TIMEOUT = 30_000; +const DEFAULT_CONTROL_REQUEST_TIMEOUT = 60_000; const DEFAULT_STREAM_CLOSE_TIMEOUT = 60_000; import { randomUUID } from 'node:crypto'; @@ -434,8 +434,9 @@ export class Query implements AsyncIterable { try { const canUseToolTimeout = this.options.timeout?.canUseTool ?? DEFAULT_CAN_USE_TOOL_TIMEOUT; + let timeoutId: NodeJS.Timeout | undefined; const timeoutPromise = new Promise((_, reject) => { - setTimeout( + timeoutId = setTimeout( () => reject(new Error('Permission callback timeout')), canUseToolTimeout, ); @@ -451,6 +452,10 @@ export class Query implements AsyncIterable { timeoutPromise, ]); + if (timeoutId) { + clearTimeout(timeoutId); + } + if (result.behavior === 'allow') { return { behavior: 'allow', @@ -789,14 +794,20 @@ export class Query implements AsyncIterable { ) { const streamCloseTimeout = this.options.timeout?.streamClose ?? DEFAULT_STREAM_CLOSE_TIMEOUT; - await Promise.race([ - this.firstResultReceivedPromise, - new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, streamCloseTimeout); - }), - ]); + let timeoutId: NodeJS.Timeout | undefined; + + const timeoutPromise = new Promise((resolve) => { + timeoutId = setTimeout(() => { + logger.info('streamCloseTimeout resolved'); + resolve(); + }, streamCloseTimeout); + }); + + await Promise.race([this.firstResultReceivedPromise, timeoutPromise]); + + if (timeoutId) { + clearTimeout(timeoutId); + } } this.endInput(); diff --git a/packages/sdk-typescript/src/types/types.ts b/packages/sdk-typescript/src/types/types.ts index a85125ea..24dc0575 100644 --- a/packages/sdk-typescript/src/types/types.ts +++ b/packages/sdk-typescript/src/types/types.ts @@ -316,7 +316,7 @@ export interface QueryOptions { /** * Logging level for the SDK. * Controls the verbosity of log messages output by the SDK. - * @default 'info' + * @default 'error' */ logLevel?: 'debug' | 'info' | 'warn' | 'error'; diff --git a/packages/sdk-typescript/src/utils/logger.ts b/packages/sdk-typescript/src/utils/logger.ts index afb7a495..caf57ede 100644 --- a/packages/sdk-typescript/src/utils/logger.ts +++ b/packages/sdk-typescript/src/utils/logger.ts @@ -22,7 +22,7 @@ const LOG_LEVEL_PRIORITY: Record = { export class SdkLogger { private static config: LoggerConfig = {}; - private static effectiveLevel: LogLevel = 'info'; + private static effectiveLevel: LogLevel = 'error'; static configure(config: LoggerConfig): void { this.config = config; @@ -47,7 +47,7 @@ export class SdkLogger { return 'debug'; } - return 'info'; + return 'error'; } private static isValidLogLevel(level: string): boolean { diff --git a/packages/sdk-typescript/test/unit/Query.test.ts b/packages/sdk-typescript/test/unit/Query.test.ts index 2b89ca51..1dd0a992 100644 --- a/packages/sdk-typescript/test/unit/Query.test.ts +++ b/packages/sdk-typescript/test/unit/Query.test.ts @@ -542,13 +542,16 @@ describe('Query', () => { const canUseTool = vi.fn().mockImplementation( () => new Promise((resolve) => { - setTimeout(() => resolve({ behavior: 'allow' }), 35000); // Exceeds 30s timeout + setTimeout(() => resolve({ behavior: 'allow' }), 15000); }), ); const query = new Query(transport, { cwd: '/test', canUseTool, + timeout: { + canUseTool: 10000, + }, }); const controlReq = createControlRequest('can_use_tool', 'perm-req-4'); @@ -567,7 +570,7 @@ describe('Query', () => { }); } }, - { timeout: 35000 }, + { timeout: 15000 }, ); await query.close(); @@ -1204,7 +1207,12 @@ describe('Query', () => { }); it('should handle control request timeout', async () => { - const query = new Query(transport, { cwd: '/test' }); + const query = new Query(transport, { + cwd: '/test', + timeout: { + controlRequest: 10000, + }, + }); // Respond to initialize await vi.waitFor(() => { @@ -1224,7 +1232,7 @@ describe('Query', () => { await expect(interruptPromise).rejects.toThrow(/timeout/i); await query.close(); - }, 35000); + }, 15000); it('should handle malformed control responses', async () => { const query = new Query(transport, { cwd: '/test' });