From 1ae22da948cd2ef28f420bb5f4c4ce52daf0e07f Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 15 Jul 2026 13:18:28 +0800 Subject: [PATCH 1/3] feat(agent-core): drop default timeouts for print-mode background tasks - add `bash_task_timeout_s` config under [background] (0 = no timeout), covering the background Bash default and the re-arm after a foreground command is moved to the background on timeout - allow `[subagent] timeout_ms = 0` (no timeout) and thread it through Agent/AgentSwarm task registration and the swarm batch timer - fill both with 0 in print-mode config defaults so `kimi -p` never kills background work by wall-clock and only the model stops a task; interactive defaults are unchanged - sync the Bash tool description/parameter text with the effective default and update user docs (en/zh) plus changeset --- .changeset/print-no-background-timeouts.md | 5 + docs/en/configuration/config-files.md | 13 +-- docs/en/reference/tools.md | 6 +- docs/zh/configuration/config-files.md | 13 +-- docs/zh/reference/tools.md | 6 +- packages/agent-core/src/agent/tool/index.ts | 1 + .../agent-core/src/config/print-defaults.ts | 27 ++++-- packages/agent-core/src/config/schema.ts | 13 ++- .../agent-core/src/session/subagent-batch.ts | 2 +- .../agent-core/src/session/subagent-host.ts | 12 +-- .../builtin/collaboration/agent-swarm.ts | 2 + .../src/tools/builtin/collaboration/agent.ts | 2 + .../src/tools/builtin/shell/bash.ts | 94 ++++++++++++++++--- .../agent-core/test/config/configs.test.ts | 22 +++++ .../agent-core/test/harness/runtime.test.ts | 6 +- .../test/session/subagent-host.test.ts | 7 ++ packages/agent-core/test/tools/bash.test.ts | 63 +++++++++++++ 17 files changed, 246 insertions(+), 48 deletions(-) create mode 100644 .changeset/print-no-background-timeouts.md diff --git a/.changeset/print-no-background-timeouts.md b/.changeset/print-no-background-timeouts.md new file mode 100644 index 0000000000..219f1790c8 --- /dev/null +++ b/.changeset/print-no-background-timeouts.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +In print mode (`kimi -p`), background Bash tasks and subagents no longer have a timeout by default — they run until they finish or the model stops them, and a foreground Bash command that times out is moved to the background without a new deadline. Interactive defaults are unchanged; tune per mode with `bash_task_timeout_s` under `[background]` or `timeout_ms` under `[subagent]` (`0` = no timeout). diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index 58199d5d7f..b62383e0da 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -210,20 +210,21 @@ You can also switch models temporarily without touching the config file — by s | `max_running_tasks` | `integer` | — | Maximum number of background tasks running concurrently | | `keep_alive_on_exit` | `boolean` | `false` | Whether to keep still-running background tasks when the session closes. By default, Kimi Code requests that all background tasks stop before the process exits; set this to `true` only when you want tasks to outlive the session. In print mode (`kimi -p`), this is only a legacy fallback used when `print_background_mode` is unset: `true` is equivalent to `print_background_mode = "drain"` | | `kill_grace_period_ms` | `integer` | `5000` | Grace period in milliseconds after session close, a manual stop, or a task timeout requests graceful termination. If a task is still running after this period, Kimi Code attempts to force-stop it | -| `bash_auto_background_on_timeout` | `boolean` | `true` | When a foreground `Bash` command hits its timeout, move it to a background task instead of killing it — the agent is notified when it completes, and the backgrounded command is bounded by the 600s default background timeout. Set to `false` to kill timed-out foreground commands instead | -| `print_background_mode` | `"exit" \| "drain" \| "steer"` | `"exit"` | Print mode (`kimi -p`) only. Governs how pending background tasks are handled once the main agent's turn ends: `"exit"` exits immediately; `"drain"` waits for every background task to reach a terminal state before exiting (results are not fed back to the main agent); `"steer"` stays alive so a completing background task — like a background subagent — injects a synthetic user message that steers the main agent into a new turn, looping until a turn ends with no pending background tasks or a limit is hit. Takes precedence over the `keep_alive_on_exit` print fallback | -| `print_wait_ceiling_s` | `integer` | `3600` | In print mode (`kimi -p`), the wall-clock ceiling (seconds) for the wait/steer loop when `print_background_mode` is `"drain"` or `"steer"`. Has no effect outside print mode or when it is `"exit"` | -| `print_max_turns` | `integer` | `50` | In print mode (`kimi -p`) with `print_background_mode = "steer"`, the maximum number of new turns that may be triggered by background-task completions, to keep the steering loop bounded | +| `bash_auto_background_on_timeout` | `boolean` | `true` | When a foreground `Bash` command hits its timeout, move it to a background task instead of killing it — the agent is notified when it completes, and the backgrounded command is bounded by the `bash_task_timeout_s` default background timeout. Set to `false` to kill timed-out foreground commands instead | +| `bash_task_timeout_s` | `integer` | `600` | Default timeout (seconds) for background `Bash` tasks when the call omits `timeout`; also used to re-arm foreground commands moved to the background on timeout. `0` means no timeout — the task runs until it exits or the model stops it. Explicit per-call `timeout` values are unaffected. In print mode (`kimi -p`) the default is `0` unless explicitly set | +| `print_background_mode` | `"exit" \| "drain" \| "steer"` | `"steer"` | Print mode (`kimi -p`) only. Governs how pending background tasks are handled once the main agent's turn ends: `"exit"` exits immediately; `"drain"` waits for every background task to reach a terminal state before exiting (results are not fed back to the main agent); `"steer"` stays alive so a completing background task — like a background subagent — injects a synthetic user message that steers the main agent into a new turn, looping until a turn ends with no pending background tasks or a limit is hit. Takes precedence over the `keep_alive_on_exit` print fallback | +| `print_wait_ceiling_s` | `integer` | `315360000` | In print mode (`kimi -p`), the wall-clock ceiling (seconds) for the wait/steer loop when `print_background_mode` is `"drain"` or `"steer"` (the default is 10 years — effectively unbounded). Has no effect outside print mode or when it is `"exit"` | +| `print_max_turns` | `integer` | `100000` | In print mode (`kimi -p`) with `print_background_mode = "steer"`, the maximum number of new turns that may be triggered by background-task completions, to keep the steering loop bounded (the default is effectively unbounded) | `keep_alive_on_exit` can be overridden by the `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` environment variable, which takes higher priority than `config.toml`. -In print mode (`kimi -p ""`), Kimi Code by default runs a single non-interactive turn and exits as soon as the main agent finishes (`print_background_mode = "exit"`). If you launch background tasks (for example, concurrent subagents via `Agent(run_in_background=true)`, or a long command via `Bash(run_in_background=true)`) and need them to run to completion, set `print_background_mode` to `"drain"` (wait for them to finish, without feeding results back) or `"steer"` (feed each completion back to the main agent, starting a new turn so it can act on the result). `"steer"` is useful when the main agent should keep working based on the outcome of a long background task (e.g. training or evaluation); its total wall-clock is bounded by `print_wait_ceiling_s` and the number of extra turns by `print_max_turns`. +In print mode (`kimi -p ""`), Kimi Code stays alive after the main agent's turn as long as background tasks are still pending: each completion is fed back to the main agent as a synthetic user message, steering it into a new turn (`print_background_mode = "steer"` by default), and the run exits once a turn ends with nothing pending. The loop is bounded by `print_wait_ceiling_s` and `print_max_turns`, both effectively unbounded by default. Background work is never killed by a wall-clock cap in print mode either: background `Bash` tasks default to no timeout (`bash_task_timeout_s = 0`), and subagents run without a timeout (`[subagent] timeout_ms = 0`), so only the model itself stops a task. Set `print_background_mode` to `"drain"` to wait for tasks without feeding results back, or `"exit"` to end the run as soon as the main agent finishes. ## `subagent` | Field | Type | Default | Description | | --- | --- | --- | --- | -| `timeout_ms` | `integer` | `7200000` (2 hours) | Maximum wall-clock time (milliseconds) a single subagent (`Agent` / `AgentSwarm`) is allowed to run before it is settled as `timed_out`. Set a very large value (e.g. `259200000`, i.e. 3 days) to effectively lift the cap. This is the background-task manager's per-task timeout for each subagent task, so it applies to both foreground and background subagents. Note: any value above `2147483647` (about 24.8 days) is clamped to 1ms by the runtime. | +| `timeout_ms` | `integer` | `7200000` (2 hours) | Maximum wall-clock time (milliseconds) a single subagent (`Agent` / `AgentSwarm`) is allowed to run before it is settled as `timed_out`. `0` means no timeout — the subagent runs until it finishes or the model stops it. This is the background-task manager's per-task timeout for each subagent task, so it applies to both foreground and background subagents. In print mode (`kimi -p`) the default is `0` unless explicitly set. Note: any value above `2147483647` (about 24.8 days) is clamped to roughly 24.8 days by the runtime | `timeout_ms` can be overridden by the `KIMI_SUBAGENT_TIMEOUT_MS` environment variable, which takes higher priority than `config.toml`. diff --git a/docs/en/reference/tools.md b/docs/en/reference/tools.md index 132416e010..8854caefc6 100644 --- a/docs/en/reference/tools.md +++ b/docs/en/reference/tools.md @@ -40,11 +40,11 @@ File tools handle reading, writing, and searching the local filesystem — the f - `command` (required): the shell command to execute - `cwd`: working directory - `timeout`: timeout in milliseconds; foreground default is 60 seconds, maximum is 5 minutes -- `run_in_background`: whether to run as a background task; background tasks default to a 10-minute timeout +- `run_in_background`: whether to run as a background task; background tasks default to a 10-minute timeout (no timeout by default in print mode `kimi -p`) - `description`: background task description; required when `run_in_background=true` - `disable_timeout`: whether to remove the timeout limit for background tasks -Foreground mode blocks the current turn until the command completes or times out, and the TUI streams stdout and stderr into the running `Bash` tool card while the command is still active. By default, a foreground command that hits its timeout is not killed — it keeps running as a background task (bounded by the 600s default background timeout); to restore kill-on-timeout, set [`bash_auto_background_on_timeout`](../configuration/config-files.md#background) to `false` under `[background]`. Background mode returns a task ID immediately and automatically notifies the Agent when the task finishes. stdin is always closed — interactive commands receive EOF immediately. A two-phase termination strategy (SIGTERM → 5-second grace period → SIGKILL) ensures reliable process cleanup when a task is stopped or hits its background timeout. On Windows, Git Bash is used by default. +Foreground mode blocks the current turn until the command completes or times out, and the TUI streams stdout and stderr into the running `Bash` tool card while the command is still active. By default, a foreground command that hits its timeout is not killed — it keeps running as a background task (bounded by the 600s default background timeout); to restore kill-on-timeout, set [`bash_auto_background_on_timeout`](../configuration/config-files.md#background) to `false` under `[background]`. The 600s background default is configurable via [`bash_task_timeout_s`](../configuration/config-files.md#background) (`0` = no timeout) and defaults to no timeout in print mode (`kimi -p`). Background mode returns a task ID immediately and automatically notifies the Agent when the task finishes. stdin is always closed — interactive commands receive EOF immediately. A two-phase termination strategy (SIGTERM → 5-second grace period → SIGKILL) ensures reliable process cleanup when a task is stopped or hits its background timeout. On Windows, Git Bash is used by default. ## Web Tools @@ -89,7 +89,7 @@ Collaboration tools handle inter-Agent coordination, user interaction, and Skill | `AskUserQuestion` | Auto-allow | Ask the user a question to gather structured input | | `Skill` | Auto-allow | Invoke a registered inline Skill | -**`Agent`** delegates a subtask to a sub-Agent. Required parameters: `prompt` (complete task description) and `description` (a 3–5 word short summary). Optional parameters: `subagent_type` (defaults to `coder`), `resume` (ID of an existing Agent to resume; mutually exclusive with `subagent_type`), and `run_in_background` (defaults to false). Agent tasks time out after 2 hours by default; the limit is configurable via `[subagent] timeout_ms` in `config.toml` (or the `KIMI_SUBAGENT_TIMEOUT_MS` env var). In foreground mode the parent Agent waits for the sub-Agent to complete before continuing; in background mode a task ID is returned immediately and the result is automatically delivered back to the main Agent via a synthetic User message when done. When several foreground `Agent` calls run in the same step, the TUI groups them and shows each subagent's running, waiting, completed, or failed status with elapsed time. See [Agent & Sub-Agents](../customization/agents.md) for details. +**`Agent`** delegates a subtask to a sub-Agent. Required parameters: `prompt` (complete task description) and `description` (a 3–5 word short summary). Optional parameters: `subagent_type` (defaults to `coder`), `resume` (ID of an existing Agent to resume; mutually exclusive with `subagent_type`), and `run_in_background` (defaults to false). Agent tasks time out after 2 hours by default; the limit is configurable via `[subagent] timeout_ms` in `config.toml` (`0` = no timeout, or the `KIMI_SUBAGENT_TIMEOUT_MS` env var), and defaults to no timeout in print mode (`kimi -p`). In foreground mode the parent Agent waits for the sub-Agent to complete before continuing; in background mode a task ID is returned immediately and the result is automatically delivered back to the main Agent via a synthetic User message when done. When several foreground `Agent` calls run in the same step, the TUI groups them and shows each subagent's running, waiting, completed, or failed status with elapsed time. See [Agent & Sub-Agents](../customization/agents.md) for details. **`AgentSwarm`** launches subagents from a shared `prompt_template` and an `items` array, resumes existing subagents through `resume_agent_ids`, or combines both in one call. The template must contain the `{{item}}` placeholder; each item replaces that placeholder and launches one new subagent. Pass `subagent_type` to choose the profile used by every spawned subagent in the swarm, or omit it to use `coder`. Without `resume_agent_ids`, the tool requires at least 2 items; with `resume_agent_ids`, it can resume one or more existing subagents. The tool supports up to 128 total subagents, waits for all subagents to finish, and returns an aggregated report. In the TUI, foreground swarms show a live `Agent swarm` progress panel above the input box. If a model response calls `AgentSwarm`, that call must be the only tool call in the response; to run multiple swarms, call one `AgentSwarm`, wait for its result, then call the next, or combine the work into one swarm when a single template can cover it. In `manual` permission mode, `AgentSwarm` calls outside active swarm mode request approval unless a permission rule allows them; while swarm mode is active, `AgentSwarm` itself is auto-approved. Permission rules match `AgentSwarm` by tool name only — argument patterns such as `AgentSwarm(swarm)` are not supported. By default the tool ramps up concurrency without an upper limit (5 subagents start immediately, then 1 more every 700 ms); set `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` to a positive integer to cap how many subagents run at the same time during that ramp, or leave it unset for no cap. If it is set to a value that is not a positive integer, the AgentSwarm call fails fast. diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md index 574bff2e2c..b71c542ed5 100644 --- a/docs/zh/configuration/config-files.md +++ b/docs/zh/configuration/config-files.md @@ -210,20 +210,21 @@ display_name = "Kimi for Coding (custom)" | `max_running_tasks` | `integer` | — | 同时运行的最大后台任务数 | | `keep_alive_on_exit` | `boolean` | `false` | 会话关闭时是否保留仍在运行的后台任务。默认情况下,Kimi Code 会在进程退出前请求停止所有后台任务;只有希望任务在会话结束后继续运行时才设为 `true`。在 print 模式(`kimi -p`)下,本字段仅作为 `print_background_mode` 未设置时的兼容回退:`true` 等价于 `print_background_mode = "drain"` | | `kill_grace_period_ms` | `integer` | `5000` | 会话关闭、手动停止或任务超时请求正常终止后,等待任务自行结束的宽限时间(毫秒)。超过该时间仍在运行时,Kimi Code 会尝试强制停止该任务 | -| `bash_auto_background_on_timeout` | `boolean` | `true` | 前台 `Bash` 命令触及超时时间时,将其转为后台任务而不是直接终止:命令完成时 agent 会收到通知,转入后台的命令受 600s 默认后台超时约束。设为 `false` 则恢复超时即终止的行为 | -| `print_background_mode` | `"exit" \| "drain" \| "steer"` | `"exit"` | 仅 print 模式(`kimi -p`)生效,决定主 agent 的 turn 结束后如何处理未返回的后台任务:`"exit"` 立即退出;`"drain"` 退出前等待所有后台任务进入终态(结果不回馈给主 agent);`"steer"` 不退出,让后台任务完成时像后台子代理一样以合成 user 消息 steer 主 agent 进入新 turn,直到某 turn 结束时无未决后台任务或触及上限。设置后优先级高于 `keep_alive_on_exit` 的 print 回退 | -| `print_wait_ceiling_s` | `integer` | `3600` | print 模式(`kimi -p`)下,`print_background_mode` 为 `"drain"` 或 `"steer"` 时,等待/steer 循环的墙钟上限(秒)。在非 print 模式或 `"exit"` 时无效 | -| `print_max_turns` | `integer` | `50` | print 模式(`kimi -p`)且 `print_background_mode = "steer"` 时,允许由后台任务完成触发的新 turn 的最大数量,防止 steer 循环失控 | +| `bash_auto_background_on_timeout` | `boolean` | `true` | 前台 `Bash` 命令触及超时时间时,将其转为后台任务而不是直接终止:命令完成时 agent 会收到通知,转入后台的命令受 `bash_task_timeout_s` 默认后台超时约束。设为 `false` 则恢复超时即终止的行为 | +| `bash_task_timeout_s` | `integer` | `600` | 后台 `Bash` 任务在调用未传 `timeout` 时的默认超时(秒);前台命令超时转后台后也按此值重新计时。`0` 表示无超时——任务一直运行到自行结束或被模型手动停止。显式传入的 `timeout` 不受影响。在 print 模式(`kimi -p`)下未显式设置时默认为 `0` | +| `print_background_mode` | `"exit" \| "drain" \| "steer"` | `"steer"` | 仅 print 模式(`kimi -p`)生效,决定主 agent 的 turn 结束后如何处理未返回的后台任务:`"exit"` 立即退出;`"drain"` 退出前等待所有后台任务进入终态(结果不回馈给主 agent);`"steer"` 不退出,让后台任务完成时像后台子代理一样以合成 user 消息 steer 主 agent 进入新 turn,直到某 turn 结束时无未决后台任务或触及上限。设置后优先级高于 `keep_alive_on_exit` 的 print 回退 | +| `print_wait_ceiling_s` | `integer` | `315360000` | print 模式(`kimi -p`)下,`print_background_mode` 为 `"drain"` 或 `"steer"` 时,等待/steer 循环的墙钟上限(秒;默认 10 年,近似不设限)。在非 print 模式或 `"exit"` 时无效 | +| `print_max_turns` | `integer` | `100000` | print 模式(`kimi -p`)且 `print_background_mode = "steer"` 时,允许由后台任务完成触发的新 turn 的最大数量,防止 steer 循环失控(默认值近似不设限) | `keep_alive_on_exit` 可被环境变量 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 覆盖,优先级高于配置文件。 -在 print 模式(`kimi -p ""`)下,Kimi Code 默认只跑一个非交互的单轮 turn,主 agent 一结束就退出(`print_background_mode = "exit"`)。如果你启动了后台任务(例如通过 `Agent(run_in_background=true)` 并发子代理,或 `Bash(run_in_background=true)` 的长命令)并希望它们跑完,可将 `print_background_mode` 设为 `"drain"`(等任务结束再退出,结果不回馈)或 `"steer"`(任务结束后把结果 steer 给主 agent,触发新 turn 继续处理)。`"steer"` 适合让主 agent 依据后台长任务(如训练、评测)的结果继续做后续步骤;其总耗时受 `print_wait_ceiling_s` 限制、额外 turn 数受 `print_max_turns` 限制。 +在 print 模式(`kimi -p ""`)下,只要还有未决的后台任务,Kimi Code 在主 agent 的 turn 结束后不会退出:每个任务完成都会以合成 user 消息回馈给主 agent,steer 出新的 turn(默认 `print_background_mode = "steer"`),直到某 turn 结束时没有任何未决任务才退出。该循环受 `print_wait_ceiling_s` 与 `print_max_turns` 约束,默认值都近似不设限。print 模式下后台工作也不会被墙钟超时杀掉:后台 `Bash` 任务默认无超时(`bash_task_timeout_s = 0`),子代理默认无超时(`[subagent] timeout_ms = 0`),只有模型自己能停止任务。将 `print_background_mode` 设为 `"drain"` 可等待任务结束但不回馈结果,设为 `"exit"` 则在主 agent 结束后立即退出。 ## `subagent` | 字段 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| `timeout_ms` | `integer` | `7200000`(2 小时) | 单个子代理(`Agent` / `AgentSwarm`)允许运行的最长时间(毫秒)。超时后子代理以 `timed_out` 收尾。设为很大的值(例如 `259200000`,即 3 天)可近似取消上限。该值是后台任务管理器对每个子代理任务的 per-task timeout,因此对前台与后台子代理同时生效。注意:超过 `2147483647`(约 24.8 天)会被运行时钳成 1ms | +| `timeout_ms` | `integer` | `7200000`(2 小时) | 单个子代理(`Agent` / `AgentSwarm`)允许运行的最长时间(毫秒)。超时后子代理以 `timed_out` 收尾。`0` 表示无超时——子代理一直运行到自行结束或被模型手动停止。该值是后台任务管理器对每个子代理任务的 per-task timeout,因此对前台与后台子代理同时生效。在 print 模式(`kimi -p`)下未显式设置时默认为 `0`。注意:超过 `2147483647`(约 24.8 天)的值会被运行时钳到约 24.8 天 | `timeout_ms` 可被环境变量 `KIMI_SUBAGENT_TIMEOUT_MS` 覆盖,优先级高于配置文件。 diff --git a/docs/zh/reference/tools.md b/docs/zh/reference/tools.md index 0c0fb1579f..810ffc6b97 100644 --- a/docs/zh/reference/tools.md +++ b/docs/zh/reference/tools.md @@ -40,11 +40,11 @@ - `command`(必填):要执行的 Shell 命令 - `cwd`:工作目录 - `timeout`:超时时间(毫秒);前台默认 60 秒、最长 5 分钟 -- `run_in_background`:是否以后台任务运行;后台默认 10 分钟超时 +- `run_in_background`:是否以后台任务运行;后台默认 10 分钟超时(print 模式 `kimi -p` 下默认无超时) - `description`:后台任务描述,`run_in_background=true` 时必填 - `disable_timeout`:后台任务是否取消超时限制 -前台模式会阻塞当前轮次,直到命令结束或超时;命令运行期间,TUI 会把 stdout 和 stderr 流式显示在正在运行的 `Bash` 工具卡片中。前台命令超时后默认不会被终止,而是转为后台任务继续运行(受 600 秒默认后台超时约束);如需恢复超时即终止的行为,将 `[background]` 的 [`bash_auto_background_on_timeout`](../configuration/config-files.md#background) 设为 `false`。后台模式立即返回任务 ID,任务结束时自动通知 Agent。stdin 始终被关闭,交互式命令会立即收到 EOF。任务被停止或后台超时时采用两阶段终止策略(SIGTERM → 5 秒宽限期 → SIGKILL),确保进程可靠结束。Windows 平台默认使用 Git Bash。 +前台模式会阻塞当前轮次,直到命令结束或超时;命令运行期间,TUI 会把 stdout 和 stderr 流式显示在正在运行的 `Bash` 工具卡片中。前台命令超时后默认不会被终止,而是转为后台任务继续运行(受 600 秒默认后台超时约束);如需恢复超时即终止的行为,将 `[background]` 的 [`bash_auto_background_on_timeout`](../configuration/config-files.md#background) 设为 `false`。600 秒的默认后台超时可通过 [`bash_task_timeout_s`](../configuration/config-files.md#background) 配置(`0` = 无超时),且在 print 模式(`kimi -p`)下默认无超时。后台模式立即返回任务 ID,任务结束时自动通知 Agent。stdin 始终被关闭,交互式命令会立即收到 EOF。任务被停止或后台超时时采用两阶段终止策略(SIGTERM → 5 秒宽限期 → SIGKILL),确保进程可靠结束。Windows 平台默认使用 Git Bash。 ## 网络类 @@ -89,7 +89,7 @@ Plan 模式是一种受约束的工作状态:进入后 `Write` 与 `Edit` 只 | `AskUserQuestion` | 自动放行 | 向用户提问以获取结构化输入 | | `Skill` | 自动放行 | 调用已注册的 inline Skill | -**`Agent`** 将子任务委托给子 Agent 执行。必填参数:`prompt`(完整任务描述)和 `description`(3–5 个词的简短说明)。可选参数:`subagent_type`(默认 `coder`)、`resume`(恢复已有 Agent 的 ID,与 `subagent_type` 互斥)和 `run_in_background`(默认 false)。Agent 任务默认 2 小时超时,可通过 `config.toml` 的 `[subagent] timeout_ms`(或 `KIMI_SUBAGENT_TIMEOUT_MS` 环境变量)配置。前台模式下父 Agent 等待子 Agent 完成再继续;后台模式立即返回任务 ID,完成时通过合成 User 消息自动回到主 Agent。多个前台 `Agent` 调用在同一步运行时,TUI 会合并展示,并为每个子 Agent 显示运行、等待、完成或失败状态以及已耗时长。子 Agent 体系细节见 [Agent 与子 Agent](../customization/agents.md)。 +**`Agent`** 将子任务委托给子 Agent 执行。必填参数:`prompt`(完整任务描述)和 `description`(3–5 个词的简短说明)。可选参数:`subagent_type`(默认 `coder`)、`resume`(恢复已有 Agent 的 ID,与 `subagent_type` 互斥)和 `run_in_background`(默认 false)。Agent 任务默认 2 小时超时,可通过 `config.toml` 的 `[subagent] timeout_ms`(`0` = 无超时,或 `KIMI_SUBAGENT_TIMEOUT_MS` 环境变量)配置,且在 print 模式(`kimi -p`)下默认无超时。前台模式下父 Agent 等待子 Agent 完成再继续;后台模式立即返回任务 ID,完成时通过合成 User 消息自动回到主 Agent。多个前台 `Agent` 调用在同一步运行时,TUI 会合并展示,并为每个子 Agent 显示运行、等待、完成或失败状态以及已耗时长。子 Agent 体系细节见 [Agent 与子 Agent](../customization/agents.md)。 **`AgentSwarm`** 可以从共享的 `prompt_template` 和 `items` 数组启动子 Agent,也可以通过 `resume_agent_ids` 恢复已有子 Agent,或在一次调用中同时使用两者。模板必须包含 `{{item}}` 占位符;每个 item 会替换该占位符,并启动一个新的子 Agent。传入 `subagent_type` 可以指定整个 swarm 中所有新启动的子 Agent 使用的 profile;省略时默认使用 `coder`。不传 `resume_agent_ids` 时,本工具要求至少 2 个 item;传入 `resume_agent_ids` 时,可以恢复 1 个或多个已有子 Agent。本工具最多支持 128 个子 Agent,会等待全部子 Agent 完成,并返回聚合报告。在 TUI 中,前台 swarm 会在输入框上方显示实时 `Agent swarm` 进度面板。若一次模型响应调用 `AgentSwarm`,该调用必须是该响应中的唯一工具调用;如需运行多个 swarm,应先调用一个 `AgentSwarm` 并等待结果,再调用下一个,若单个模板可以覆盖这些工作,也可以合并为一个 swarm。在 `manual` 权限模式下,未处于 swarm mode 时调用 `AgentSwarm` 会触发审批,除非已有权限规则允许;swarm mode 已开启时,`AgentSwarm` 本身会自动放行。权限规则只能按工具名 `AgentSwarm` 匹配,不支持 `AgentSwarm(swarm)` 这类参数模式。默认情况下,本工具会逐步提升并发且不设上限(立即启动 5 个子 Agent,之后每 700 毫秒再启动 1 个);将 `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` 设为正整数可限制该阶段同时运行的子 Agent 数量,不设置则表示不限制。若设置为非正整数的值,本次 AgentSwarm 调用会立即失败。 diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index 59b311639e..bf8a54a590 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -703,6 +703,7 @@ export class ToolManager { allowBackground, autoBackgroundOnTimeout: this.agent.kimiConfig?.background?.bashAutoBackgroundOnTimeout ?? true, + backgroundTimeoutS: this.agent.kimiConfig?.background?.bashTaskTimeoutS, }), (modelCapabilities.image_in || modelCapabilities.video_in) && new b.ReadMediaFileTool( diff --git a/packages/agent-core/src/config/print-defaults.ts b/packages/agent-core/src/config/print-defaults.ts index 3430313bed..22cd91fc5c 100644 --- a/packages/agent-core/src/config/print-defaults.ts +++ b/packages/agent-core/src/config/print-defaults.ts @@ -17,19 +17,28 @@ export const PRINT_MAX_TURNS_DEFAULT = 100_000; /** * Per-subagent (`Agent` / `AgentSwarm`, foreground and background) timeout: - * 72 hours ≈ none (the interactive default is 2 hours). + * `0` = no timeout (the interactive default is 2 hours). A headless run must + * never have a subagent killed by a wall-clock cap; only the model itself may + * stop one. */ -export const PRINT_SUBAGENT_TIMEOUT_MS_DEFAULT = 259_200_000; +export const PRINT_SUBAGENT_TIMEOUT_MS_DEFAULT = 0; + +/** + * Background Bash task timeout: `0` = no timeout (the interactive default is + * 600s). Also covers foreground commands re-armed after being moved to the + * background on timeout, so a headless run never kills a command it detached. + */ +export const PRINT_BASH_TASK_TIMEOUT_S_DEFAULT = 0; /** * Merge print-mode defaults into the config bound to a new session. Only * values the user left unset are filled (per-key spread order). * - * `background` is deliberately untouched: its print defaults live next to the - * consuming code in `Session` (`resolvePrintBackgroundMode`, - * `waitForBackgroundTasksOnPrint`, `handlePrintMainTurnCompleted`), because - * `printBackgroundMode`'s fallback must keep honoring the legacy - * `keep_alive_on_exit` → `'drain'` mapping. + * `background` gets only the `bashTaskTimeoutS` fill: the print background + * *mode* defaults live next to the consuming code in `Session` + * (`resolvePrintBackgroundMode`, `waitForBackgroundTasksOnPrint`, + * `handlePrintMainTurnCompleted`), because `printBackgroundMode`'s fallback + * must keep honoring the legacy `keep_alive_on_exit` → `'drain'` mapping. */ export function applyPrintModeConfigDefaults(config: KimiConfig): KimiConfig { return { @@ -37,6 +46,10 @@ export function applyPrintModeConfigDefaults(config: KimiConfig): KimiConfig { // `0` is already what an unset maxStepsPerTurn means (unlimited); the // explicit value just pins the print-mode contract. loopControl: { maxStepsPerTurn: 0, ...config.loopControl }, + background: { + bashTaskTimeoutS: PRINT_BASH_TASK_TIMEOUT_S_DEFAULT, + ...config.background, + }, subagent: { timeoutMs: PRINT_SUBAGENT_TIMEOUT_MS_DEFAULT, ...config.subagent }, }; } diff --git a/packages/agent-core/src/config/schema.ts b/packages/agent-core/src/config/schema.ts index 6c11e3537c..547d85cf4f 100644 --- a/packages/agent-core/src/config/schema.ts +++ b/packages/agent-core/src/config/schema.ts @@ -133,6 +133,13 @@ export const BackgroundConfigSchema = z.object({ * instead of killing it. Defaults to true when unset. */ bashAutoBackgroundOnTimeout: z.boolean().optional(), + /** + * Default timeout (seconds) for background Bash tasks when the call omits + * `timeout`, also used to re-arm foreground commands moved to the + * background. `0` means no timeout. Explicit per-call `timeout` values are + * unaffected. Defaults to the Bash tool's built-in 600s when unset. + */ + bashTaskTimeoutS: z.number().int().min(0).optional(), killGracePeriodMs: z.number().int().min(0).optional(), printWaitCeilingS: z.number().int().min(1).optional(), printBackgroundMode: z.enum(['exit', 'drain', 'steer']).optional(), @@ -142,7 +149,11 @@ export const BackgroundConfigSchema = z.object({ export type BackgroundConfig = z.infer; export const SubagentConfigSchema = z.object({ - timeoutMs: z.number().int().min(1).optional(), + /** + * Per-subagent (`Agent` / `AgentSwarm`, foreground and background) timeout + * in milliseconds. `0` means no timeout. Defaults to 2 hours when unset. + */ + timeoutMs: z.number().int().min(0).optional(), }); export type SubagentConfig = z.infer; diff --git a/packages/agent-core/src/session/subagent-batch.ts b/packages/agent-core/src/session/subagent-batch.ts index 317bcfab4f..7646de3874 100644 --- a/packages/agent-core/src/session/subagent-batch.ts +++ b/packages/agent-core/src/session/subagent-batch.ts @@ -622,7 +622,7 @@ export class SubagentBatch { attempt.controller.abort(task.signal?.reason); }; const timeout = - task.timeout === undefined + task.timeout === undefined || task.timeout <= 0 ? undefined : setTimeout(() => { attempt.timedOut = true; diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index d6c4b6369d..a014c248a2 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -37,18 +37,18 @@ const SUBAGENT_TIMEOUT_ENV = 'KIMI_SUBAGENT_TIMEOUT_MS'; /** * Resolve the effective subagent per-task timeout. Precedence: - * `KIMI_SUBAGENT_TIMEOUT_MS` (positive integer ms) → `configMs` → - * `DEFAULT_SUBAGENT_TIMEOUT_MS` (30 min). Set a large value to effectively - * disable the cap. The value feeds the background-task manager's per-task - * timeout, so it governs foreground and background subagents (and AgentSwarm). + * `KIMI_SUBAGENT_TIMEOUT_MS` (integer ms) → `configMs` → + * `DEFAULT_SUBAGENT_TIMEOUT_MS` (2 hours). `0` means no timeout: the value + * feeds the background-task manager's per-task timeout (where `0` arms no + * timer), so it governs foreground and background subagents (and AgentSwarm). */ export function resolveSubagentTimeoutMs(configMs?: number): number { const raw = process.env[SUBAGENT_TIMEOUT_ENV]; if (raw !== undefined && raw.trim().length > 0) { const parsed = Number(raw); - if (Number.isInteger(parsed) && parsed >= 1) return parsed; + if (Number.isInteger(parsed) && parsed >= 0) return parsed; } - if (configMs !== undefined && Number.isInteger(configMs) && configMs >= 1) { + if (configMs !== undefined && Number.isInteger(configMs) && configMs >= 0) { return configMs; } return DEFAULT_SUBAGENT_TIMEOUT_MS; diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent-swarm.ts b/packages/agent-core/src/tools/builtin/collaboration/agent-swarm.ts index a4b685380c..7bcaa599a6 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/agent-swarm.ts +++ b/packages/agent-core/src/tools/builtin/collaboration/agent-swarm.ts @@ -91,6 +91,8 @@ export class AgentSwarmTool implements BuiltinTool { constructor( private readonly subagentHost: SessionSubagentHost, private readonly swarmMode: SwarmMode, + // `0` = no timeout, preserved on purpose (`0 ?? DEFAULT` stays `0`); + // SubagentBatch arms no timer for non-positive timeouts. private readonly subagentTimeoutMs?: number, ) {} diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent.ts b/packages/agent-core/src/tools/builtin/collaboration/agent.ts index f0abf3e3f5..e12a8c16df 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/agent.ts +++ b/packages/agent-core/src/tools/builtin/collaboration/agent.ts @@ -120,6 +120,8 @@ export class AgentTool implements BuiltinTool { ) { const log = options?.log; this.allowBackground = options?.allowBackground ?? true; + // `0` is preserved (not normalized): `0 ?? DEFAULT_SUBAGENT_TIMEOUT_MS` + // stays `0`, and the BackgroundManager arms no timer for it. this.subagentTimeoutMs = options?.subagentTimeoutMs; const typeLines = buildSubagentDescriptions(subagents); const baseDescription = `${AGENT_DESCRIPTION_BASE}\n\n${ diff --git a/packages/agent-core/src/tools/builtin/shell/bash.ts b/packages/agent-core/src/tools/builtin/shell/bash.ts index c9966ee091..d3fb69412a 100644 --- a/packages/agent-core/src/tools/builtin/shell/bash.ts +++ b/packages/agent-core/src/tools/builtin/shell/bash.ts @@ -116,10 +116,9 @@ function isValidTimeoutValue(timeout: number, isBackground: boolean): boolean { return timeout <= timeoutCapS(isBackground); } -function normalizeTimeoutMs(timeout: number | undefined, isBackground: boolean): number { - const defaultSeconds = isBackground ? DEFAULT_BACKGROUND_TIMEOUT_S : DEFAULT_TIMEOUT_S; - const value = timeout ?? defaultSeconds; - return Math.min(value, timeoutCapS(isBackground)) * MS_PER_SECOND; +function normalizeForegroundTimeoutMs(timeout: number | undefined): number { + const value = timeout ?? DEFAULT_TIMEOUT_S; + return Math.min(value, MAX_TIMEOUT_S) * MS_PER_SECOND; } async function disposeProcess(proc: KaosProcess): Promise { @@ -162,10 +161,44 @@ function withoutAutoBackgroundOnTimeout(description: string): string { ); } +/** + * Rewrite the background-timeout sentence when the effective default is "no + * timeout" (`background.bash_task_timeout_s = 0`): the model must not be told + * tasks die at 600s, nor nudged into defensive `disable_timeout` / + * `timeout` arguments — an explicit `timeout` is honored and still capped. + */ +function withoutBackgroundDefaultTimeout(description: string): string { + return description.replace( + `Background commands default to a ${String(DEFAULT_BACKGROUND_TIMEOUT_S)}s timeout and \`timeout\` is capped at ${String(MAX_BACKGROUND_TIMEOUT_S)}s; set \`disable_timeout=true\` only when the task should run without a timeout.`, + `Background commands have no timeout by default; set \`timeout\` (max ${String(MAX_BACKGROUND_TIMEOUT_S)}s) only when the task should be bounded.`, + ); +} + +/** + * Keep the JSON-schema `timeout` parameter description in sync with an + * unbounded background default (`withoutBackgroundDefaultTimeout` covers the + * prose description; the model reads parameter descriptions too). + */ +function withoutBackgroundDefaultTimeoutInParameters( + parameters: Record, +): Record { + const properties = parameters['properties']; + if (typeof properties !== 'object' || properties === null) return parameters; + const timeout = (properties as Record)['timeout']; + if (typeof timeout !== 'object' || timeout === null) return parameters; + const current = (timeout as Record)['description']; + if (typeof current !== 'string') return parameters; + (timeout as Record)['description'] = current.replace( + `Background default ${String(DEFAULT_BACKGROUND_TIMEOUT_S)}s,`, + 'Background default no timeout;', + ); + return parameters; +} + export class BashTool implements BuiltinTool { readonly name = 'Bash' as const; readonly description: string; - readonly parameters: Record = toInputJsonSchema(BashInputSchema); + readonly parameters: Record; private readonly isWindowsBash: boolean; @@ -173,6 +206,13 @@ export class BashTool implements BuiltinTool { private readonly autoBackgroundOnTimeout: boolean; + /** + * Default deadline for background tasks when the call omits `timeout`, and + * the re-armed deadline for foreground commands moved to the background. + * `undefined` arms no timer at all (`background.bash_task_timeout_s = 0`). + */ + private readonly backgroundTimeoutMs: number | undefined; + constructor( private readonly kaos: Kaos, private readonly cwd: string, @@ -180,17 +220,34 @@ export class BashTool implements BuiltinTool { options?: { allowBackground?: boolean | undefined; autoBackgroundOnTimeout?: boolean; + /** + * Effective background default timeout in seconds + * (`background.bash_task_timeout_s`; `0` = no timeout). Defaults to + * {@link DEFAULT_BACKGROUND_TIMEOUT_S} when unset. + */ + backgroundTimeoutS?: number; }, ) { this.isWindowsBash = this.kaos.osEnv.osKind === 'Windows'; this.allowBackground = options?.allowBackground ?? true; this.autoBackgroundOnTimeout = options?.autoBackgroundOnTimeout ?? true; + const backgroundTimeoutS = options?.backgroundTimeoutS ?? DEFAULT_BACKGROUND_TIMEOUT_S; + this.backgroundTimeoutMs = + backgroundTimeoutS === 0 ? undefined : backgroundTimeoutS * MS_PER_SECOND; const rendered = renderBashDescription(this.kaos.osEnv.shellName); + const withEffectiveDefault = + this.backgroundTimeoutMs === undefined + ? withoutBackgroundDefaultTimeout(rendered) + : rendered; this.description = !this.allowBackground - ? withoutBackgroundDescription(rendered) + ? withoutBackgroundDescription(withEffectiveDefault) : this.autoBackgroundOnTimeout - ? rendered - : withoutAutoBackgroundOnTimeout(rendered); + ? withEffectiveDefault + : withoutAutoBackgroundOnTimeout(withEffectiveDefault); + this.parameters = + this.backgroundTimeoutMs === undefined + ? withoutBackgroundDefaultTimeoutInParameters(toInputJsonSchema(BashInputSchema)) + : toInputJsonSchema(BashInputSchema); } resolveExecution(args: BashInput): ToolExecution { @@ -240,6 +297,16 @@ export class BashTool implements BuiltinTool { return this.kaos.execWithEnv(shellArgs, mergedEnv); } + /** + * Background deadline: an explicit `timeout` wins (the schema caps it at + * `MAX_BACKGROUND_TIMEOUT_S`); otherwise the configured default — which is + * `undefined` (no timer armed) when `background.bash_task_timeout_s = 0`. + */ + private backgroundDefaultTimeoutMs(timeout: number | undefined): number | undefined { + if (timeout !== undefined) return Math.min(timeout, MAX_BACKGROUND_TIMEOUT_S) * MS_PER_SECOND; + return this.backgroundTimeoutMs; + } + private async execution( args: BashInput, signal: AbortSignal, @@ -250,14 +317,14 @@ export class BashTool implements BuiltinTool { if (validationError !== undefined) return validationError; const startsInBackground = args.run_in_background === true; - const foregroundTimeoutMs = normalizeTimeoutMs(args.timeout, false); + const foregroundTimeoutMs = normalizeForegroundTimeoutMs(args.timeout); const command = this.isWindowsBash ? rewriteWindowsNullRedirect(args.command) : args.command; const effectiveCwd = args.cwd ?? this.cwd; const description = startsInBackground ? args.description!.trim() : foregroundDescription(args); const timeoutMs = startsInBackground ? args.disable_timeout ? undefined - : normalizeTimeoutMs(args.timeout, true) + : this.backgroundDefaultTimeoutMs(args.timeout) : foregroundTimeoutMs; const builder = new ToolResultBuilder(); @@ -295,9 +362,10 @@ export class BashTool implements BuiltinTool { detached: startsInBackground, timeoutMs, // Detaching (ctrl+b) moves a foreground command to the background; - // give it the background timeout so it is not still bounded by the - // shorter foreground deadline. - detachTimeoutMs: DEFAULT_BACKGROUND_TIMEOUT_S * MS_PER_SECOND, + // give it the background default so it is not still bounded by the + // shorter foreground deadline (`undefined` = no timer when the + // config disables the background timeout). + detachTimeoutMs: this.backgroundTimeoutMs, // A foreground command that hits its timeout is moved to the // background (re-armed to detachTimeoutMs) instead of being killed — // unless disabled via config, or background tooling is unavailable diff --git a/packages/agent-core/test/config/configs.test.ts b/packages/agent-core/test/config/configs.test.ts index 26aa96bb7d..3f7271e244 100644 --- a/packages/agent-core/test/config/configs.test.ts +++ b/packages/agent-core/test/config/configs.test.ts @@ -8,6 +8,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { ErrorCodes, KimiError } from '../../src/errors'; import { KimiConfigSchema, + applyPrintModeConfigDefaults, configToTomlData, ensureConfigFile, loadRuntimeConfig, @@ -937,3 +938,24 @@ support_efforts = ["low", "high"] expect(overrides['support_efforts']).toEqual(['low', 'high']); }); }); + +describe('applyPrintModeConfigDefaults', () => { + it('fills unbounded print defaults when nothing is configured', () => { + const config = applyPrintModeConfigDefaults({}); + expect(config.loopControl?.maxStepsPerTurn).toBe(0); + expect(config.background?.bashTaskTimeoutS).toBe(0); + expect(config.subagent?.timeoutMs).toBe(0); + }); + + it('lets explicit user config win over every print default', () => { + const config = applyPrintModeConfigDefaults({ + loopControl: { maxStepsPerTurn: 7 }, + background: { bashTaskTimeoutS: 30, keepAliveOnExit: true }, + subagent: { timeoutMs: 5000 }, + }); + expect(config.loopControl?.maxStepsPerTurn).toBe(7); + expect(config.background?.bashTaskTimeoutS).toBe(30); + expect(config.background?.keepAliveOnExit).toBe(true); + expect(config.subagent?.timeoutMs).toBe(5000); + }); +}); diff --git a/packages/agent-core/test/harness/runtime.test.ts b/packages/agent-core/test/harness/runtime.test.ts index e3f44373ce..227f498258 100644 --- a/packages/agent-core/test/harness/runtime.test.ts +++ b/packages/agent-core/test/harness/runtime.test.ts @@ -1192,7 +1192,8 @@ describe('KimiCore print-mode defaults', () => { }); const main = core.sessions.get(created.id)?.getReadyAgent('main'); - expect(main?.kimiConfig?.subagent?.timeoutMs).toBe(259_200_000); + expect(main?.kimiConfig?.subagent?.timeoutMs).toBe(0); + expect(main?.kimiConfig?.background?.bashTaskTimeoutS).toBe(0); expect(main?.kimiConfig?.loopControl?.maxStepsPerTurn).toBe(0); // The raw user config is left untouched so config reads/writes still @@ -1252,7 +1253,8 @@ timeout_ms = 5000 // The reload path rebuilds the session through resumeSessionWithOverrides; // the agent it constructs must carry the same print-mode defaults. const main = core.sessions.get(created.id)?.getReadyAgent('main'); - expect(main?.kimiConfig?.subagent?.timeoutMs).toBe(259_200_000); + expect(main?.kimiConfig?.subagent?.timeoutMs).toBe(0); + expect(main?.kimiConfig?.background?.bashTaskTimeoutS).toBe(0); expect(main?.kimiConfig?.loopControl?.maxStepsPerTurn).toBe(0); }); }); diff --git a/packages/agent-core/test/session/subagent-host.test.ts b/packages/agent-core/test/session/subagent-host.test.ts index e611e1b821..5aa36b9f73 100644 --- a/packages/agent-core/test/session/subagent-host.test.ts +++ b/packages/agent-core/test/session/subagent-host.test.ts @@ -74,6 +74,13 @@ describe('resolveSubagentTimeoutMs', () => { process.env[SUBAGENT_TIMEOUT_ENV] = '-5'; expect(resolveSubagentTimeoutMs()).toBe(DEFAULT_SUBAGENT_TIMEOUT_MS); }); + + it('treats 0 as no timeout from both config and env', () => { + delete process.env[SUBAGENT_TIMEOUT_ENV]; + expect(resolveSubagentTimeoutMs(0)).toBe(0); + process.env[SUBAGENT_TIMEOUT_ENV] = '0'; + expect(resolveSubagentTimeoutMs(600000)).toBe(0); + }); }); describe('formatSubagentTimeoutDescription', () => { diff --git a/packages/agent-core/test/tools/bash.test.ts b/packages/agent-core/test/tools/bash.test.ts index 0ab6436442..28bee35a69 100644 --- a/packages/agent-core/test/tools/bash.test.ts +++ b/packages/agent-core/test/tools/bash.test.ts @@ -883,6 +883,69 @@ describe('BashTool', () => { expect(manager.list(false)).toHaveLength(1); }); + describe('background timeout default (backgroundTimeoutS)', () => { + async function launchBackground( + manager: ReturnType['manager'], + options?: ConstructorParameters[3], + args?: Partial, + ) { + const registerSpy = vi.spyOn(manager, 'registerTask'); + const execWithEnv = vi.fn().mockResolvedValue(processWithOutput()); + const tool = bashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager, options); + const result = await executeTool(tool, + context({ command: 'sleep 10', run_in_background: true, description: 'task', ...args }), + ); + expect(result.isError).not.toBe(true); + return registerSpy.mock.calls[0]?.[1]; + } + + it('defaults to a 600s deadline and detach re-arm when no config is provided', async () => { + const manager = createBackgroundManager().manager; + const options = await launchBackground(manager); + expect(options).toMatchObject({ timeoutMs: 600_000, detachTimeoutMs: 600_000 }); + }); + + it('arms no timer when backgroundTimeoutS is 0', async () => { + const manager = createBackgroundManager().manager; + const options = await launchBackground(manager, { backgroundTimeoutS: 0 }); + expect(options).toMatchObject({ timeoutMs: undefined, detachTimeoutMs: undefined }); + }); + + it('uses the configured seconds as the default deadline and detach re-arm', async () => { + const manager = createBackgroundManager().manager; + const options = await launchBackground(manager, { backgroundTimeoutS: 30 }); + expect(options).toMatchObject({ timeoutMs: 30_000, detachTimeoutMs: 30_000 }); + }); + + it('honors an explicit per-call timeout even when the default is disabled', async () => { + const manager = createBackgroundManager().manager; + const options = await launchBackground(manager, { backgroundTimeoutS: 0 }, { timeout: 42 }); + expect(options?.timeoutMs).toBe(42_000); + }); + + it('keeps disable_timeout authoritative regardless of the configured default', async () => { + const manager = createBackgroundManager().manager; + const options = await launchBackground(manager, { backgroundTimeoutS: 30 }, { disable_timeout: true }); + expect(options?.timeoutMs).toBeUndefined(); + }); + + it('tells the model there is no default timeout when backgroundTimeoutS is 0', () => { + const tool = bashTool( + createFakeKaos({ osEnv: posixEnv }), + '/workspace', + createBackgroundManager().manager, + { backgroundTimeoutS: 0 }, + ); + expect(tool.description).toContain('Background commands have no timeout by default'); + expect(tool.description).not.toContain('default to a 600s timeout'); + const timeoutParam = ( + tool.parameters as { properties: { timeout: { description?: string } } } + ).properties.timeout; + expect(timeoutParam.description).toContain('Background default no timeout'); + expect(timeoutParam.description).not.toContain('Background default 600s'); + }); + }); + it('kills a spawned background command when the task limit is reached', async () => { const manager = createBackgroundManager({ maxRunningTasks: 1 }).manager; registerProcess(manager, processWithOutput(), 'sleep 10', 'existing task'); From 9b41bbf0aadb7880bf272023ad785d89644a9b99 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 15 Jul 2026 14:20:45 +0800 Subject: [PATCH 2/3] test(agent-core): satisfy KimiConfig providers requirement in print-defaults tests --- packages/agent-core/test/config/configs.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/agent-core/test/config/configs.test.ts b/packages/agent-core/test/config/configs.test.ts index 3f7271e244..71f7f8d003 100644 --- a/packages/agent-core/test/config/configs.test.ts +++ b/packages/agent-core/test/config/configs.test.ts @@ -941,7 +941,7 @@ support_efforts = ["low", "high"] describe('applyPrintModeConfigDefaults', () => { it('fills unbounded print defaults when nothing is configured', () => { - const config = applyPrintModeConfigDefaults({}); + const config = applyPrintModeConfigDefaults({ providers: {} }); expect(config.loopControl?.maxStepsPerTurn).toBe(0); expect(config.background?.bashTaskTimeoutS).toBe(0); expect(config.subagent?.timeoutMs).toBe(0); @@ -949,6 +949,7 @@ describe('applyPrintModeConfigDefaults', () => { it('lets explicit user config win over every print default', () => { const config = applyPrintModeConfigDefaults({ + providers: {}, loopControl: { maxStepsPerTurn: 7 }, background: { bashTaskTimeoutS: 30, keepAliveOnExit: true }, subagent: { timeoutMs: 5000 }, From 30abb48ba2a5c2fe58916fb42bcfdaed9f0bf465 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 15 Jul 2026 14:32:14 +0800 Subject: [PATCH 3/3] fix(agent-core): clear the foreground deadline on detach when the background timeout is disabled `detach()` only re-arms when `detachTimeoutMs` is defined, so passing `undefined` with `bash_task_timeout_s = 0` kept the armed foreground deadline and a manually detached command was still killed at its foreground timeout. Pass `0` instead so the reset clears the timer. Caught by Codex review on #1737. --- .../src/tools/builtin/shell/bash.ts | 9 +++-- packages/agent-core/test/tools/bash.test.ts | 40 ++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/agent-core/src/tools/builtin/shell/bash.ts b/packages/agent-core/src/tools/builtin/shell/bash.ts index d3fb69412a..020dd597dc 100644 --- a/packages/agent-core/src/tools/builtin/shell/bash.ts +++ b/packages/agent-core/src/tools/builtin/shell/bash.ts @@ -363,9 +363,12 @@ export class BashTool implements BuiltinTool { timeoutMs, // Detaching (ctrl+b) moves a foreground command to the background; // give it the background default so it is not still bounded by the - // shorter foreground deadline (`undefined` = no timer when the - // config disables the background timeout). - detachTimeoutMs: this.backgroundTimeoutMs, + // shorter foreground deadline. When the config disables the + // background timeout this must be `0`, not `undefined`: `detach()` + // only re-arms when the value is defined, so `undefined` would keep + // the already-armed foreground deadline and kill the task anyway + // (`reset(0)` clears the timer and arms nothing). + detachTimeoutMs: this.backgroundTimeoutMs ?? 0, // A foreground command that hits its timeout is moved to the // background (re-armed to detachTimeoutMs) instead of being killed — // unless disabled via config, or background tooling is unavailable diff --git a/packages/agent-core/test/tools/bash.test.ts b/packages/agent-core/test/tools/bash.test.ts index 28bee35a69..29be1910eb 100644 --- a/packages/agent-core/test/tools/bash.test.ts +++ b/packages/agent-core/test/tools/bash.test.ts @@ -908,7 +908,9 @@ describe('BashTool', () => { it('arms no timer when backgroundTimeoutS is 0', async () => { const manager = createBackgroundManager().manager; const options = await launchBackground(manager, { backgroundTimeoutS: 0 }); - expect(options).toMatchObject({ timeoutMs: undefined, detachTimeoutMs: undefined }); + // detachTimeoutMs must be `0` (clear-on-detach), not `undefined` + // (which would keep the armed foreground deadline). + expect(options).toMatchObject({ timeoutMs: undefined, detachTimeoutMs: 0 }); }); it('uses the configured seconds as the default deadline and detach re-arm', async () => { @@ -929,6 +931,42 @@ describe('BashTool', () => { expect(options?.timeoutMs).toBeUndefined(); }); + it('keeps a manually detached foreground command alive when backgroundTimeoutS is 0', async () => { + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); + try { + const manager = createBackgroundManager().manager; + const { proc, finish } = pendingProcess(); + const execWithEnv = vi.fn().mockResolvedValue(proc); + const tool = bashTool( + createFakeKaos({ execWithEnv, osEnv: posixEnv }), + '/workspace', + manager, + { backgroundTimeoutS: 0 }, + ); + let foregroundTaskId: string | undefined; + const run = executeTool(tool, { + ...context({ command: 'sleep 10', timeout: 1 }), + onForegroundTaskStart: (taskId: string) => { + foregroundTaskId = taskId; + }, + }); + // Flush the async spawn + registration, then detach before the 1s + // foreground deadline fires. + await vi.advanceTimersByTimeAsync(0); + expect(foregroundTaskId).toBeDefined(); + manager.detach(foregroundTaskId!); + const released = await run; + expect(released.output).toContain(foregroundTaskId); + // Without the clear-on-detach fix, the original 1s deadline would + // still fire here and settle the task as timed_out. + await vi.advanceTimersByTimeAsync(2_000); + expect(manager.getTask(foregroundTaskId!)?.status).toBe('running'); + finish(); + } finally { + vi.useRealTimers(); + } + }); + it('tells the model there is no default timeout when backgroundTimeoutS is 0', () => { const tool = bashTool( createFakeKaos({ osEnv: posixEnv }),