Files
codex-app-server/verification/resident-task-summary.md
host 0722ca3fd8 feat: resident watch tasks with pause/resume/end
Long-running website monitoring: new cloud/resident.py layer on top of
the automations claim/quota model (no task state-machine change).
States running/paused/ended; pause preserves the content-hash cursor
and stops polling, resume re-checks from the cursor, end is terminal
(resume after end is 409, a new watch is required). Polling is a cheap
fetch + hash comparison; only a real change creates a queued model
task. Quota exhaustion pauses (quota_exceeded) instead of ending and
keeps the cursor. Driver is the worker run_loop round boundary;
server exposes create/list/get/update/pause/resume/end with audit.
Console UI intentionally not wired this round (see verification
summary for deviations).
2026-11-01 19:01:39 +00:00

10 KiB
Raw Permalink Blame History

常驻监听任务(resident watch)验证摘要

基底与交付物(代码就绪,local only)

  • 工作区:~/workspace/codex-app-server-resident-task
  • 分支:feat-resident-task,基底 origin/main = 622c51e(Merge PR #274,2026-11-02 fetch 到的头,与任务书一致)。
  • 主 checkout(~/workspace/codex-app-server,feat-repo-picker,有脏文件)与其它现存 worktree 一律未动。
  • 本轮提交前状态:cloud/server.py +187 行、cloud/worker.py +31 行,新增 cloud/resident.py、cloud/tests/test_resident.py、cloud/tests/test_resident_server.py、本摘要。
  • 未做:未 push、未合并到 main、未跑 deploy.py、未部署、未建生产任务、未动生产/主机 env/系统 key/sub2api/生产网关/仓库可见性/GitHub App 权限、未杀生产容器。

基底核实(先调查结论:main 上常驻监听不存在,不是重复建设)

  • 任务状态机仍是 queued -> running -> completed / failed / interrupted / merged / merge-failed;/pause 只是把进行中回合任务标 interrupted 并置 paused_by_user=1(cloud/server.py _pause_task),不是可续跑的常驻暂停态——本实现不复用它,另建常驻态。
  • 云端自动化(cloud/automations.py)定义层只有 active / cancelled,含 automations / automation_runs 表与 run_due_automation 的 claim 抢单、配额门;服务端本身不起定时器,到期触发靠外部触发器。本实现复用它的 claim 口径与配额函数,不另起冲突语义。
  • goal 循环(host.py run_goal_loop,max_rounds=0 不限轮次)结束条件是模型宣告达成,不是“一直监听外部变化”;定时自动化每次到期建一个新任务跑一轮就结束。两者都不是常驻监听,本摘要不把它们说成已实现。
  • main 上与暂停相关的已有机制(MCP pause_task 工具、长任务中断自动恢复)针对的都是回合任务,不是常驻监听;grep pause|resident 在 cloud/*.py 与 host.py 中未发现常驻监听实现。

设计抉择

  • 新层不改任务表:cloud/resident.py(标准库 + 复用 cloud.automations 的 _audit_row、_automation_quota_exceeded)新增 resident_watches / resident_polls 两张表,避免改动任务状态机牵连 worker 认领、sweep、自动恢复。
  • 状态机:running -> paused -> running;running / paused -> ended(终态)。ended 后 pause / resume / end / update 一律 409,恢复已结束的监听必须新建任务(文档与测试双锁定,见下)。
  • 暂停保留现场:暂停只改 status / paused_reason / next_poll_at,last_content_hash / last_poll_at / last_change_at / last_change_text 不动;暂停期间 due=False,驱动不轮询。恢复时 next_poll_at=now 立刻复查,用旧游标比对——暂停期间的变化在恢复后第一次轮询即报出,不丢。
  • 驱动(明说):服务端本身不起定时器,与自动化同款结论。实际驱动是 cloud/worker.py 的 run_loop 轮次边界调 process_due_resident_watches()(新增方法,异常只记日志、不炸循环;self.resident_fetcher 可注入假抓取供测试)。任何外部触发器也可直接调 run_due_resident_watch / process_due_resident_watches,两者都走 claim 门。
  • 防重复轮询:claim_due_watch 条件 UPDATE(status='running' 且 next_poll_at <= now),抢到才把 last_poll_at / last_claimed_at 写成 now 并预置 next_poll_at=now+interval(崩在抓取中途时下个间隔仍重试,不因认领锁死永久停摆;完成后按成败重算)。
  • 变化才花钱:轮询 = 廉价抓取 + 空白折叠后 sha256 比对,不跑模型。baseline(首次,只建游标)、unchanged(不建任务)、changed(才建一个 queued 任务,prompt = 用户处理提示 + 变化摘录)三种产出都记 resident_polls,可查。
  • 配额边界:配额门只在“变化要建模型任务”那一刻过(复用自动化同款 _automation_quota_exceeded)。耗尽时不结束、不丢变化:转 paused(paused_reason='quota_exceeded')、游标不推进、记 changed_quota_paused;配额恢复后恢复,同一变化仍能报出。配额检查本身失败时按瞬时失败退避,不转暂停,游标同样不推进。
  • 安全边界:间隔下限 60 秒(与自动化同款)、上限 30 天;抓取超时 1–60 秒(默认 10 秒)、读满 1MB 即断;失败退避 30 / 60 / 120……秒、上限 3600 秒;目标 URL 仅 http/https、无 userinfo、字面 localhost/私有/回环/链路本地 IP 拒绝。
  • API(cloud/server.py,权限与自动化同款:写 create_task、读 read,全部记审计):
    • POST /projects/{id}/resident-watches(name / target_url / prompt 必填;interval_seconds 默认 3600;fetch_timeout_seconds 可选;创建不走配额门)
    • GET /projects/{id}/resident-watches(附 due_count)、GET /projects/{id}/resident-watches/{wid}(附最近轮询 polls)
    • PATCH /projects/{id}/resident-watches/{wid}(改名/提示/目标/间隔/超时;ended 409;改间隔不立即触发,下次轮询完成后按新间隔重算)
    • POST /projects/{id}/resident-watches/{wid}/pause|resume|end
    • 状态查询返回常驻态、游标(last_content_hash)、最后变化(last_change_at / last_change_text)、下次轮询(next_poll_at)、失败次数与暂停原因。

验收清单逐项结果

  1. 能新建常驻任务(目标 URL、轮询间隔、变化处理提示):✅ 模块与服务端测试覆盖;非空壳——新建即到期,驱动下一轮即可认领。
  2. 三态清晰(运行中/已暂停/已结束终态):✅ 状态机与 HTTP 409 语义双覆盖。
  3. 暂停保留现场、暂停期间停轮询、恢复从游标续跑:✅ 测试 test_pause_preserves_cursor_and_resume_reports_pause_period_change(暂停期间变了,恢复后第一次轮询即报出)。
  4. 结束终态、结束后不再静默重启、结束后恢复被拒绝:✅ ended 后 due 查询只认 running,pause / resume / end / update 一律 409(服务端错误文本含 terminal 并提示新建);测试覆盖。
  5. 真正有驱动、claim 防重复、心跳与 stale 回收:✅ 驱动为 worker run_loop 轮次边界(不是服务端定时器,明说);claim 单胜者测试覆盖;stale 口径为“认领预置 next_poll_at,崩后下个间隔重试”+ worker 既有每轮心跳/sweep(常驻监听本身不另起心跳表,见已知偏差)。
  6. 变化才花钱、每次轮询与变化可查:✅ baseline / unchanged 不建任务、changed 才建任务;resident_polls 逐轮记录。
  7. 间隔下限、抓取超时、失败退避、除配额耗尽外不擅自结束、配额耗尽只转可恢复暂停并明示原因:✅ 全部测试覆盖;配额耗尽测试断言状态为 paused 且明确 != ended。
  8. 权限与审计沿用任务/自动化同款、pause / resume / end 记审计:✅ 服务端测试断言 resident_watch_created / paused / resumed / ended 审计行;跨项目行一律 404(不泄露存在性);局外人写操作被拒。
  9. API 含暂停、恢复、结束与状态查询:✅ 见上;控制台 UI 本轮未接,列为已知偏差,不写成已完成。

真实测试数字

  • 定向(改动后):python3 -m unittest cloud.tests.test_resident cloud.tests.test_resident_server —— 13 项(模块 10 + 服务端 3),全部通过。
  • 全量(CI 同款三条 discover,改动后、仅最后一次文案注释修订前跑出;该修订只改 update_watch 的 docstring,不改任何代码逻辑,修订后定向 13 项与自动化相关 31 项重跑全部通过):
    • discover -s tests:105 项,0 失败,0 跳过。
    • discover -s cloud/tests:1544 项,0 失败,9 跳过。
    • discover -s deploy/tests:44 项(单独重跑确认),0 失败。
    • 合计 1693 项、0 失败、9 跳过。失败未靠删断言、加 skip、改默认值蒙混;新增测试 13 项全部计入上述 cloud 套件。
  • 新模块定向覆盖率(coverage run --source=cloud.resident,仅定向 13 项):cloud/resident.py 312 语句、85% 覆盖。未覆盖主因:默认真实 HTTP 抓取(default_fetch,单元测试一律注入假抓取,不联网)与个别异常分支(PG 方言补列、配额检查失败分支);本仓库 CI(.github/workflows/host-verify.yml)本身没有覆盖率门禁,故不报“覆盖率变化”——全量覆盖率基线未在本轮重算,不虚构数字。

已知偏差与未做事项

  • 控制台 UI 未接:cloud/console.html 是打包产物,本轮没有常驻监听面板;用户现阶段只能走 API。接 UI 是后续一轮,不在本轮验收内。
  • 变化判定 v1 较粗:空白折叠后全文 sha256,没有选择器/区域提取;动态页面(广告、时间戳、随机推荐)本身会抖动成“变化”并建任务。缓解在后续:加 CSS 选择器或正文提取口径、或变化阈值。
  • SSRF 防护 v1 只挡字面 localhost/私有 IP,不做 DNS 解析后校验(解析后重定向到内网、DNS rebinding 不在覆盖内);websearch.fetch_url 的同款字面口径已对齐,但不要把 v1 当成完整 SSRF 方案。
  • 常驻监听没有独立心跳表:stale 回收靠认领预置的 next_poll_at + worker 既有心跳/sweep;驱动进程整体死亡时,已认领未完成的当次轮询要等到下个间隔才补跑(与自动化 claim 的同款取舍)。
  • 未验证:真实外网抓取(单元测试不联网)、生产 PG 上的表迁移(PRAGMA 补列走自动化同款适配写法,但未在 PG 实测)、worker 常驻循环里的长时驱动(仅方法级测试,未跑真实 run_loop 长循环)、多 worker 并发抢单(仅条件 UPDATE 单胜者测试,未做双进程实测)。
  • 未做:未 push、未合并、未部署;UI、选择器提取、DNS 级 SSRF、PG 实测、双进程抢单实测留待后续轮次。

Blocker

  • 无本轮 blocker(全部工作均为本地代码与本地测试,未触及只有用户能给的账号/密钥/商户/商店账号/宿主/维护窗口,也未发生花真钱、删生产数据、改生产可见性的动作)。
  • 上线前仍需用户侧决策(不是本轮 blocker,不擅自定):控制台 UI 是否随本批上、默认轮询间隔与抓取超时是否按本轮默认值(60 秒下限/10 秒超时)拍板为生产默认值、PG 实测与部署窗口。