feat: 长任务补齐——流式进度 + 跨进程中断 + worker 超时约定

- 长等待流式进度:run_turn 改 1 秒切片轮询(_wait_turn),每满 --progress-interval(默认 30 秒,0 关闭,CLI > 配置 > 默认)往 stderr 打 turn progress 并往链路文件记 kind=progress;进度绝不走 stdout,不破坏 dispatch JSON 与 goal 结论行/用量行解析(经 cloud 真正则验过)
- 跨进程中断:interrupt 直接不生效时写 <state_dir>/interrupt-requests/<task>.json,属主等 turn 时每秒轮询到后用自己的 live 连接发 turn/interrupt 并记 interrupted;10 秒内未响应也不删文件,属主下次轮询仍处理;接线覆盖 dispatch/run/goal/goal-resume/delegate/parallel/fix-review/run-due;旧 server 不接受 interrupt_file 时退回不传再跑
- cloud worker 超时约定:--task-timeout 0 归一化为 None(不限时,与 host turn_timeout 同约定,Docker 路径仍受 docker_timeout 约束),默认 1800 不变;超时仍记 failed(现有单测合同不动)

测试:host 56 全过(新增 7)、cloud 526 全过(4 跳过,新增 1 个 0=不限时)。
This commit is contained in:
host committed 2026-10-27 03:05:15 +08:00
1 parent 028ef02a86
commit 1128fd3076
6 files changed
+400 -34

No files matched your search

+3 -1
View File
@@ -683,7 +683,9 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
`GET /metrics` 的 `by_status` 恒含七个状态键(queued / running /
completed / failed / interrupted / merged / merge-failed),缺失
状态为 0 而不是缺键。
- 任务级超时:worker 的 `--task-timeout`(默认 1800 秒),与另外
- 任务级超时:worker 的 `--task-timeout`(默认 1800 秒,0 表示
不限时、与 host `--turn-timeout 0` 同约定,worker 级不设截止,
但 Docker 路径仍受 `--docker-timeout` 约束),与另外
两个参数不要混淆:`--docker-timeout` 是执行器级的单次 docker run
超时,`--max-seconds` 是 `run_loop` 的整体墙钟上限;三者互不覆盖,
Docker 路径下 worker 取 `min(task_timeout, docker_timeout)`
+21
View File
@@ -123,6 +123,27 @@ class WorkerTimeoutTestCase(unittest.TestCase):
self.assertIn("worker_timeout", _audit_actions(db_path))
tmp.cleanup()
def test_task_timeout_zero_means_unlimited(self):
# 对齐 host turn_timeout:0 归一化为 None(不限时),
# 而不是在 popen 轮询第一轮就立即超时。sleep 0.5 的
# 任务在 task_timeout=0 下应正常完成,库里是 completed。
tmp = tempfile.TemporaryDirectory()
def factory(task):
return subprocess.Popen(
["sleep", "0.5"], stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True, start_new_session=True)
worker, db_path, task = _make_worker(
tmp.name, process_factory=factory, task_timeout=0,
max_retries=0, retry_backoff_seconds=0)
self.assertIsNone(worker.task_timeout)
result = worker.run_one(task)
self.assertEqual(result["status"], "completed")
status, _ = _task_row(db_path)
self.assertEqual(status, "completed")
tmp.cleanup()
def test_docker_timeout_converts_to_failed_and_audit(self):
tmp = tempfile.TemporaryDirectory()
+20 -4
View File
@@ -68,6 +68,12 @@ interrupted,不用执行结果覆盖它。
runner / 无超时的 subprocess.run)无法在中途抢占一个已阻塞
的调用,超时只能靠执行器自身的 timeout,这是诚实边界,
不要假装同步路径也能中途杀掉。
- 对齐 host.py 的 turn_timeout 约定:task_timeout 传 0 时
归一化为 None(不限时,worker 级不设截止),与 host 的
“0 = 不限时”同约定;默认 1800 不变,不改默认就不会悄悄
挂起自动化。worker 级不限时时,Docker 路径仍受执行器
docker_timeout 约束(timeout=None 时 _call_runner 用
executor 自身的 timeout),不是全链路无限。
- max_retries(默认 1 次重试,CLI --max-retries 可调,0 表示
不重试)是对模型任务失败的有限重试:取消不重试;可 fallback
的失败优先走真实 fallback(见下),不再做同模型重试(fallback
@@ -311,6 +317,14 @@ class Worker:
self._loop_stop = False
self.logger = get_logger("cloud.worker")
# 任务级超时 / 失败重试(见模块 docstring 的生产级说明)。
# 对齐 host turn_timeout:0 归一化为 None(不限时),
# 负数同理;默认 1800 不变。归一化后 _run_attempt 里
# task_timeout is None 的分支本来就走“无 worker 级截止”
#(Docker 路径仍受 executor docker_timeout 约束),
# 不用再改各执行路径;不归一化的话 0 会在 popen 轮询
# 里第一轮就触发“已到截止时间”而立即超时,是反直觉的 bug。
if task_timeout is not None and task_timeout <= 0:
task_timeout = None
self.task_timeout = task_timeout
self.max_retries = max_retries
self.retry_backoff_seconds = retry_backoff_seconds
@@ -2102,10 +2116,12 @@ def main(argv=None):
" worker 必须开,否则它会抢走本该进"
" Docker 容器的 NULL 任务")
parser.add_argument("--task-timeout", type=float, default=1800,
help="每任务截止时间(秒,默认 1800);超时后真杀"
"容器/进程组、任务置 failed;与 --docker-timeout"
"(单次 docker run 超时)取较小值,与"
" --max-seconds(run_loop 整体墙钟上限)不同")
help="每任务截止时间(秒,默认 1800);0 表示不限时"
"(与 host --turn-timeout 0 同约定,worker 级"
"不设截止,但 Docker 路径仍受 --docker-timeout"
"约束);超时后真杀容器/进程组、任务置 failed;"
"与 --docker-timeout(单次 docker run 超时)取"
"较小值,与 --max-seconds(run_loop 整体墙钟上限)不同")
parser.add_argument("--max-retries", type=int, default=1,
help="模型任务失败重试次数(默认 1,0 表示不重试;"
"取消与已记 fallback 的失败不重试)")