diff --git a/PROGRESS.md b/PROGRESS.md index 9d39c0a..c2da261 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -21,6 +21,12 @@ ## 已完成关键能力 +### 2026-06-29 / 定时任务默认单次超时 0→1800s(bump 0.32.4) + +- 承上:超时此前默认 0(不限),配合"超时被吞成 ok"的旧 bug,一个跑飞的 job 能无限拖。改默认有限值 1800s(30min):新建 job 不指定 `timeout_seconds` 时给 1800,`0` 仍保留为"不限"逃生口。 +- 单一事实源 `core/scheduler.DEFAULT_TIMEOUT_SECONDS=1800`,`create_job` 与 `tools/schedule.py`(agent 建 job 的工具)默认都引它;tool JSON schema 描述同步注明"default 1800 / 0=no limit / 重活可调大"。`create_job` 里 `int(timeout_seconds or 0)` 保留显式 0=不限语义。 +- 存量:把线上 job `e621c8a6`「每日水泥科研简报」的 `timeout_seconds` 由 600 手动改为 1800(直接 SQL UPDATE,未动其它 job)。 + ### 2026-06-29 / brief skill 加 context 纪律,堵反复 dump abstract 烧 token(bump 0.32.3) - 承上条同一 job 复盘:agent 把同一批 38 篇全文英文 abstract 用 `run_python`/`print` **反复灌进上下文**(实测 dump ≥3 次),工具输出每轮重发 → 48 次 LLM 调用累计输入 **2.5M tokens**(输出仅 28K),既慢又贵,还顶满 600s 超时。根因:brief skill 虽已要求把证据落 `evidence.md` 文件,但没明令"别反复 print 进上下文",弱模型(deepseek-v4-flash)规律不足就放飞。 diff --git a/RUN.md b/RUN.md index 7aeef86..7c3f778 100644 --- a/RUN.md +++ b/RUN.md @@ -756,7 +756,7 @@ sudo xfs_quota -x -c "limit -p bhard=10g zcbot_" /opt | `kill -HUP ` 后 `/openapi.json` 没新接口 | uvicorn **不响应 SIGHUP**(没装 handler,落 Python 默认终止;Windows 上信号本身无效)。Ubuntu 上用 `systemctl restart zcbot`,或 unit 加 `--reload` 让 uvicorn 监听文件自动重起(见"部署"段)。验证:`curl -s http://127.0.0.1:8765/openapi.json \| python3 -c 'import sys,json;print([p for p in json.load(sys.stdin)["paths"] if "auth" in p])'` | | `systemctl restart zcbot` 要等几十秒才退 | 正常 —— 优雅 drain 在等在跑的 run 收尾(`shutdown.drain_timeout` 默 30s),没在跑 run 时秒退。journal 出现 `[shutdown] draining N in-flight run(s)` 即正常。真急(不在乎杀掉在跑 run):`systemctl kill -s KILL zcbot` | | 部署后在跑的对话被标 `error: server restarted before run finished` | 该 run 在 drain 期内没收尾、cancel 也没在 `cancel_grace` 内退,被 SIGKILL 后下次启动 reaper 标的。多半是 run 卡在不 poll cancel 的长动作(如单次超长 docker exec)或 `TimeoutStopSec` 配得比 drain 预算还小被提前 SIGKILL。先核对 unit `TimeoutStopSec > drain_timeout + cancel_grace`;真有超长 run 把 `drain_timeout` 调大 | -| 定时任务「跑到一半没推送」/ crons 页显示「上次失败: 运行超过超时上限 Ns 未完成」 | job 跑满 `timeout_seconds` 被协作式中断(还没写完 / 没推送)。**0.32.2 起超时记 error**(此前误记 ok 看不出来),计入连续失败、到阈值自动停用。处置:该 job 调大 `timeout_seconds`(报告类重活如多刊检索+渲 docx 建议 ≥1800,或 0=不限),被自动停用的重新 enable。诊断单个 job 用 `scripts/diag_sched_e621.py ` | +| 定时任务「跑到一半没推送」/ crons 页显示「上次失败: 运行超过超时上限 Ns 未完成」 | job 跑满 `timeout_seconds` 被协作式中断(还没写完 / 没推送)。**0.32.2 起超时记 error**(此前误记 ok 看不出来),计入连续失败、到阈值自动停用。**0.32.4 起新建 job 默认超时 1800s**(此前默认 0=不限;`DEFAULT_TIMEOUT_SECONDS`),`0` 仍可显式设"不限"。处置:报告类重活(多刊检索+渲 docx)若仍不够,把该 job `timeout_seconds` 再调大或设 0;被自动停用的重新 enable。诊断单个 job 用 `scripts/diag_sched_e621.py ` | | `POST /v1/files/rename` 返 409 `folder has active run(s)` | 顶层目录被某 running/cancelling 的 task 占用;先 cancel 等流式 done 再 rename | | `POST /v1/files/rename` 返 409 `... 前缀嵌套` | 改名后会与其他 task 的 working_dir 形成嵌套;换不冲突的 new_name | | `POST /v1/files/upload` 返 413 `已达磁盘配额上限` | per-user 5GB(yaml `quotas.disk_bytes_per_user`)。让用户在 dev SPA 右侧文件栏删旧产物 / 大文件,或改 yaml 升配重启 web | diff --git a/core/__init__.py b/core/__init__.py index f86db69..e7a818d 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.32.3" +__version__ = "0.32.4" diff --git a/core/scheduler.py b/core/scheduler.py index 007144a..33a9524 100644 --- a/core/scheduler.py +++ b/core/scheduler.py @@ -32,6 +32,9 @@ except ImportError: # pragma: no cover (py<3.9 不支持,本项目 3.11+) FAILURE_DISABLE_THRESHOLD = 5 # 单次 tick 最多认领多少 job(防一批同点任务一次性涌入) CLAIM_LIMIT = 20 +# 新建 job 不指定时的默认单次超时(秒)。0=不限;给个有限默认防"跑到一半被 +# 无限拖着 / 静默吞成 ok"。报告类重活(多刊检索+渲 docx)按经验 30min 够用。 +DEFAULT_TIMEOUT_SECONDS = 1800 def validate_cron(expr: str) -> None: @@ -340,7 +343,7 @@ def create_job( skill: str = "", notify: Optional[dict[str, Any]] = None, model_profile: str = "", - timeout_seconds: int = 0, + timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, ) -> dict[str, Any]: name = (name or "").strip() prompt = (prompt or "").strip() diff --git a/tools/schedule.py b/tools/schedule.py index 6961125..01f5a59 100644 --- a/tools/schedule.py +++ b/tools/schedule.py @@ -73,7 +73,11 @@ class ScheduleCreateTool(_UserScopedTool): }, "timeout_seconds": { "type": "integer", - "description": "Optional hard timeout for each run (0 = no limit).", + "description": ( + "Optional hard timeout for each run in seconds (default 1800 = 30min; " + "0 = no limit). Raise it for heavy report jobs (multi-journal search + " + "docx render) that legitimately need longer." + ), }, }, "required": ["name", "prompt", "cron"], @@ -82,7 +86,7 @@ class ScheduleCreateTool(_UserScopedTool): def execute( self, name: str, prompt: str, cron: str, tz: str = "Asia/Shanghai", mode: str = "isolated", skill: str = "", notify_email: str = "", - timeout_seconds: int = 0, + timeout_seconds: int = scheduler.DEFAULT_TIMEOUT_SECONDS, ) -> str: notify = None if notify_email and notify_email.strip():