Commit Graph
68 Commits
Author SHA1 Message Date
host cf9d17f035 align: 第二批:把技能包播种进租户 codex-home
仿 websearch MCP 播种,默认关:
- cloud/skill_seed.py:cloud/skills/ 的 SKILL.md 拷进租户
  codex-home/.agents/skills/(个人级技能路径,见模块
  文档的路径依据与诚实边界);幂等,租户改过的文件
  绝不覆盖(上报 differs),缺包不阻塞任务。
- cloud/worker.py:新增 --seed-skills,_tenant_dirs 里
  调用播种;老租户下一次任务自动合并;播种失败只记
  日志、不阻塞任务(技能是可选流程说明)。
- cloud/tests/test_skill_seed_worker.py:播种助手 + worker
  开关,仿 test_websearch_worker.py 的结构。
- docs/chatgpt-codex-alignment.md:补第二批说明与实测
  待办(真实 Docker 任务 skills/list + $live-info 调用,
  通过前不宣称生产任务已能用这些技能)。

验证:skill_seed + chatgpt skills + websearch worker
共 14 项全过。本批不改生产配置、不部署,autoscaler
加 --seed-skills 是部署时另行的主机步骤。
2026-10-30 01:16:49 +08:00
host 6f0eec71b6 worker: cover goal docker paths with prestart stage logging
PR #145 added docker_run_* lines only to _run_in_container, but
goal/goal-resume/goal-resume-breakpoint call _call_runner directly
(to keep full stdout for goal conclusions), so the production live
test (a goal task) produced zero docker_run_ lines. Extract a
_docker_call_staged helper and route all four task paths through it;
error lines log the class name then re-raise so each caller's
kill+failed handling is unchanged.

Also fix audit attribution: audit_log has no task_id column, and
worker_exec_done/error details carried no task id, so per-task
queries missed them (the rows existed). All three exec details now
start with task_id=. Tests: 2 new goal-path stage cases + a done
detail task-id assertion (8 total in test_prestart_stages).
2026-10-29 17:46:12 +08:00
host 9d90d97046 worker: prestart stage logging for dispatch stalls
Claim-to-dispatch stalls (cf. 2026-10-29 task A: 61s to interrupted
with zero detail) were invisible because docker run blocks with
captured output and watchdog kills were silent. Add three layers:
- worker_exec_start/done/error audit rows in _run_attempt (PG,
  survives --rm), start/done pairing shows whether an attempt died
  inside the executor call;
- docker_run_start/done/error log lines in DockerExecutor (container
  + image only, never the inner command/prompt);
- stdout tail (last 2000 chars) to the worker log on failed or
  interrupted results, and a log line on both watchdog kill paths.
Tests: cloud/tests/test_prestart_stages.py, 6 cases incl. a
no-prompt-in-logs assertion.
2026-10-29 17:07:53 +08:00
host e74f20608b worker: auto-resume falls back to fresh goal for never-started tasks
2026-10-29 live test: task A was interrupted 61s after claim but had
never written anything (no host-state entry, no worktree, no trace,
no checkpoint); auto-resume ran goal-resume, which host rightly
refused, and the task failed. Refusal only makes sense for tasks
with progress but a lost breakpoint (those go through the P0
checkpoint rebuild); for a never-started task the only meaningful
outcome is a fresh run, identical to its first attempt (same task
id, same goal, worktree does not exist yet so nothing is
overwritten).

Change (claim path only, via _resolve_breakpoint_resume):
- rebuild from checkpoint first, unchanged;
- if rebuild fails AND _task_never_started holds (state entry,
  checkpoint row, worktree dir, trace file all absent), clear the
  breakpoint flag so the run takes the fresh goal path, with a
  worker_fresh_restart audit event;
- any evidence of prior execution, or any check failure,
  keeps the old refusal path (fail-closed); the auto-resume
  count cap and no-progress stall guard still bound retries.

Tests: cloud/tests/test_neverstart_fresh.py, 10 items (fallback
holds, entry/checkpoint/worktree/trace presence each block it,
corrupt state blocks it, rebuild still wins when a checkpoint
exists); test_goal_resume's breakpoint-routing test now sets up
a task with a checkpoint so it still exercises goal-resume.
Full suites: cloud 1178 (skipped 4), host 104, deploy 39, green.
2026-10-29 16:45:07 +08:00
host 8c719c7e62 断点镜像状态标签回落:host-state 条目写 status 不是 goal_status
生产实弹:极小 goal 任务的 timeline 里 checkpoint goal_status 为
None——host.py 的任务条目只有 status(goal-running/goal-met),
没有 goal_status 键,镜像原样读 goal_status 永远是空。修法:
entry.get('goal_status') or entry.get('status')。轮次、thread_id、
state_sha 等关键字段不受影响(熔断只看轮次),纯标签 fidelity
修复;单测加 status-only 条目镜像断言。
2026-10-29 15:31:07 +08:00
host 585242fc8f 长任务 P0 三件:断点进库、错误分类+熔断、Run/Step 时间线
断点进库:task_checkpoints 表(PG 新库 + 老库幂等补表 + 迁移清单
对齐);worker 在 goal 任务执行期间每 30 秒把磁盘 host-state 断点
镜像进库,终局再补一次;断点续跑认领前,若 host-state 条目丢失
但库里有断点,先按库断点重建最小条目再跑,库里也没有断点时不
造假、走原有拒绝路径。

错误分类+熔断:sandbox 四处解析加永久错误分支(KeyError、配置
缺失等无结论异常退出判 failed 死信,不再判 interrupted;host 已
明言 interrupted 的不受影响,未知错误不分类);worker 自动恢复
加退避(10 秒起指数退避、封顶 300 秒)与无进展熔断(库断点轮次
连续 3 次不变即标 auto_resume_stalled 等人工)。

时间线:task_steps 表 + host 每轮写 host-steps 步骤文件 + worker
导入;GET /tasks/{id}/timeline 一次返回 steps 与 checkpoints;
认领、暂停、自动恢复、终局都记步骤。旧任务返回空数组,不编造。

单测:cloud/tests/test_p0_three.py 14 项(分类、熔断、镜像重建、
步骤导入去重、timeline 接口)。
2026-10-29 15:13:01 +08:00
host ae3aff8602 feat: 长任务无敌补齐三件:连续失败上限 + worker退出审计 + 配额恢复自动续跑
1. host goal 循环连续失败护栏:3 轮连续失败即中断出口,
   任务记 interrupted、可断点续跑,不再无限空转
   (2026-10-29 实弹:断点后 274 轮全失败)。
2. worker 退出杀残留路径补审计 worker_shutdown_interrupted,
   detail 为任务 id,与 sweep_interrupted 同口径。
3. 配额中断自动恢复:每轮现查配额,仍超限/查不到时留
   在 interrupted 不重排队,下轮再查;现查为 None 时
   (新月份/额度上调/无价上限重置)才 CAS 回 queued。

单测:配额仍超限不动、配额恢复续跑、连续 3 轮失败停转。
2026-10-29 13:07:57 +08:00
host c9cfe22a36 feat: 长任务中断自动恢复,仅用户暂停才暂停,不限时
- 新增 tasks 列 paused_by_user/interrupted_reason/auto_resume_count/no_time_limit(幂等迁移,server+worker 双侧)
- POST /tasks/{id}/pause:用户暂停标 interrupted + paused_by_user=1;/cancel 同款标记;人工 /retry、/goal-resume 清标记
- sweep 标 sweep、配额标 quota、协调员孤儿标 coordinator_orphan、worker 退出标 worker_shutdown
- worker.run_loop 每轮后 auto_resume_interrupted:只接系统中断的 goal 任务,CAS 回 queued + goal_resume_pending=1(G10 断点续跑),用户暂停/配额/孤儿/非 goal 不接,上限 50 次防无限烧额度
- no_time_limit=1 的任务 worker/Docker 级不限时(其它任务仍默认 1800 秒;host 单轮 600 秒仍是单轮护栏)
- 前置 cherry-pick:G10 断点续跑透传 model_provider(否则 Docker 断点续跑在 sub2api 下必死,自动恢复无法实证;本 PR  supersedes #127,勿另行合并 #127)
2026-10-29 11:37:06 +08:00
host a19b1567bb fix: G10 断点续跑透传 model_provider(sub2api 续跑失败修法)
断点续跑容器不带 --model-provider 时 Server 的 provider 表里
没有 sub2api,thread/resume 报 provider not found(probe15 实弹)。
Docker/Local 断点方法加可选 model_provider 并转发,worker 两处
调用点传 gateway_provider;补单测断言断点命令含
--model-provider sub2api(旧代码下两新测试 1 失败 1 报错)。
2026-10-29 11:28:39 +08:00
host ebfae7b992 worker: always start capacity pool threads in run_all
run_all previously sized its pool from the ready snapshot at round start (min(capacity, ready_count)). If a round started with only 1 ready task (e.g. a just-claimed PM), only 1 thread ran; that thread entered a marker wait (up to 3600s) after its model turn, and children created/retried mid-round had no free thread to claim them. run_loop is blocked joining that round, so no new round starts; if the other worker had already scaled down, the queued child waited out the full wait timeout unclaimed. Fix: always start capacity threads. Idle threads exit immediately when no task is ready and nothing is active, and poll every 0.2s while something is active, so mid-round arrivals are claimed. Test: run_all with capacity=2, 1 initial task whose mocked run_one creates a child then polls for it; without the fix the PM poll times out in 5s (1 result), with the fix both tasks dispatch in the same round (0.2s).
2026-10-29 05:41:35 +08:00
host 14757f15c4 feat: 子任务默认模型/强度(child_model/child_effort)
主任务可显式声明派生默认,或在 prompt 里写“子任务用 gpt-6-luna max”
由服务端解析填入;marker task-create 与协调员分工未显式写模型/强度时
按 块/分工 > 派生默认 > 父任务模型/强度 继承,fork 复制派生默认,
continue 可改派生默认;console 新建/继续表单与任务详情同步展示。
2026-10-29 05:27:22 +08:00
host ccb22c960d worker: heartbeat parent task during marker poll/wait loops
Marker waits run after _run_attempt has cleared _running, so the run_loop heartbeat thread no longer refreshes the waiting parent. sweep_stale_tasks (default 300s) then kills a parent waiting up to the 3600s marker timeout, as seen in the marker live test (PM2 swept mid wait-threads). Poll and wait loops now heartbeat the parent each iteration (interval clamped to <=60s, well under the 300s threshold), best-effort, without touching _running, so no new capacity leak and cancel still works (empty _running entry has no proc/container to kill). Tests: 2 new sweep-survival tests (stale heartbeat at -299s, 2s wait, sweep must spare the parent); both fail 2/2 on pre-fix code and pass with the fix.
2026-10-29 04:40:47 +08:00
host b411a97774 worker: release running slot after coordinator completes
Coordinator tasks bypass _run_attempt, so their _claim placeholder in _running was never cleared: each completed coordinator permanently reduced the worker process effective capacity until it could claim nothing, while heartbeat stayed fresh and the capacity gate returned silently. This caused the 2026-10-28 40-person stall (autoscale-1 had one leaked slot, then claimed coordinator A and filled up) and the old 18-person C-6 never-claim (autoscale-1 leaked both A and B). Fix: wrap the run_one coordinator branch in try/finally _running_clear, so normal and exception paths both release. Also log capacity-gate refusals (worker_claim_refused_capacity) throttled to once per 30s; throttling applies to the log only, refusal always returns False. Tests: coordinator completion releases slot, next claim succeeds, capacity-log throttle; both leak tests fail on pre-fix code.
2026-10-29 02:42:52 +08:00
host d7f0f1be96 fix: pass search env into Docker task containers (SEARXNG_URL was lost)
Docker tasks do not inherit the worker process environment, they only
get extra_env (-e). SEARXNG_URL in the worker unit EnvironmentFile
therefore never reached codex -> MCP inside the container, so the
auto chain skipped SearXNG and only DuckDuckGo was tried (live e2e:
'all search providers failed: duckduckgo (DNS)').

_docker_task_context now merges any set SEARXNG_URL /
PERPLEXITY_API_KEY / EXA_API_KEY / TAVILY_API_KEY / BRAVE_API_KEY
from the worker environment into extra_env (values never logged;
paid keys follow the same extra_env path as gateway keys).

Tests: 2 new docker-env tests; websearch + tenant isolation = 25
passing; full cloud suite 1038 passing (4 skipped).
2026-10-28 22:17:19 +08:00
host 3b2fef95fa 任务内标记工具:task-create/task-poll/task-read 移植、wait-threads 接通、新增 task-cancel/task-retry
- 新模块 cloud/marker_tools.py:纯逻辑解析、校验、快照格式化、
  控制门禁、派生代数与直接上限计数
- sandbox 两处解析提取五种标记块;worker 在 5 处投递点与
  _deliver_peer_messages 同款位置执行,报告排成后续消息下一轮回父任务
- wait 每块 cap 到 8 个目标(与服务端 /wait 对齐,spawn 原先的 10 个收紧),
  每轮最多 2 块;取消/重试门禁:同项目、非自己、非 goal、
  派生链或同创建人,另查 create_task / approve_merge 权限,
  查不到时收紧到仅派生链内
- goal 与协调员任务不解析;单测 30 个,cloud 全量 1043 个、4 跳过、0 失败
2026-10-28 10:13:40 +00:00
openqj eae9884b13 Merge pull request #111 from openqj/feat-coord-row-vanished
协调员行消失按中断处理:本体行消失即放槽,子任务行消失按 interrupted 合流
2026-10-28 17:46:23 +08:00
host b72a2abae4 feat: websearch MCP worker wiring + egress (public SearXNG route)
Worker --websearch-mcp (default off) seeds tenant codex-home with
websearch MCP on _tenant_dirs (container path); old tenants merge
on next task. Autoscaler unit gains the flag. Tinyproxy filter
adds Perplexity/Exa/Tavily/Brave/DuckDuckGo + exact
searx.diante.me (no diante.me wildcard, control plane stays out).
Tests: test_websearch_worker 4 + websearch_mcp 12 + tenant 7 = 23 OK.
Not pushed, not merged, not deployed; SearXNG host/DNS/nginx and
tinyproxy reload are host steps.
2026-10-28 17:26:31 +08:00
host c38ee8fbe9 协调员行消失按中断处理:本体行消失即放槽,子任务行消失按 interrupted 合流
- _run_coordinator 等待循环里 get_task 为 None 时按取消跳出,
  不再等到 deadline 占槽;终局 CAS 写不回去,最终为 interrupted,
  不写回、不抛错、不当完成。
- _coord_child_rows 对已删子任务合成 interrupted 行并注明行已消失,
  等齐判断不再拖到超时,合流报告标 interrupted 且整体不为 completed,
  其它子任务不受影响;docstring 旧说法同步改成新语义。
- 新增 cloud/tests/test_coordinator_row_vanished.py,3 个单测:
  本体行消失、子任务行消失、正常等齐;cloud 全量 1020 测通过。
2026-10-28 08:57:05 +00:00
host 94137c4a29 goal 异常退出记 interrupted,可断点续跑
长 goal 实弹(2026-10-28):宿主机内存挤兑致回合挂起 59 分钟,恢复时 goal 进程异常退出、无结论行,云端旧语义(returncode 非 0 一律 failed)把可续跑的断点判成终局。

修法:host.py run_goal_loop 回合异常统一记 interrupted 并打印 goal interrupted after rounds 标记行;cloud/sandbox.py 四处 goal 解析同步新语义(标记或无结论非 0 退出 -> interrupted,有结论的非 0 退出仍 failed);worker 终局审计名与状态对齐。单测:sandbox 新测 5 个、host 新测 1 个、更新旧测 1 个;三套件 cloud 1017、host 61、deploy 29 全绿。只本地提交,未推未部署。
2026-10-28 15:18:26 +08:00
host cbc90f9d30 自动记忆:官方 memories 对齐的提取+注入闭环(确定性 v1)
开关默认关(generate/use),设置接口 GET/PUT /users/me/auto-memory-settings,任务级覆盖 POST /tasks/{id}/auto-memory-flags,手动触发 POST /users/me/auto-memory/run,日志 GET /users/me/auto-memory-log。

提取:worker run_loop 轮次边界扫 completed 任务,闲置/时长/长度/外部上下文/配额剩余百分比五道门,脱敏后写 project 级 identity_memories,同一任务去重、同一目标 supersede 合并;只写 project 级,不跨项目泄漏。

注入:worker run_one 首轮把最新各 5 条用户/项目记忆拼进内存 prompt 副本(goal 任务同步改内存 goal),库原 prompt 不动;续跑/断点/协调员不注入,注入失败 fail-open。

v1 诚实边界:extract/consolidation 模型只存设置不调模型;MCP/web 线程内调用暂无 tasks 表标记,外部上下文门只认任务间消息与协调员子任务;无控制台 UI。
2026-10-28 12:12:10 +08:00
host e31fcc6cba feat: G7 分层 fail-open/fail-closed + G8 Docker 轮询掐断 + G9 worker 忽略 host 预算 + G13/G14 归属(第三批)
- G7:认领门 closed 时查配额异常回 quota_check_unavailable、标 interrupted(可重试/续跑);轮询与轮次边界用 open,异常时回 quota_check_failed——新回合边界拒绝,执行中轮询经 _quota_hit 闭包过滤此串、只记 warning 不杀在跑任务,确认超限才掐断;查询函数本身不吞异常冒充不限额。
- G8:Docker 路径加 _docker_call_with_quota_poll(后台线程跑阻塞式 docker 调用,主循环每 quota_poll_interval_seconds 缺省 30 秒查一次,确认超限按容器名真杀容器,结果改写 quota_exceeded 走统一收尾:interrupted、成本照记、续跑消息保留,配额命中后不走 ALREADY_EXISTS 回落);同步路径保持边界检查。
- G9:worker 路径默认忽略 host max_daily_turns(host.py 顶层 --ignore-host-budget 只清该项,Local/Docker 执行器在子命令前追加,worker CLI 缺省开、--no-ignore-host-budget 关);standalone 直接跑 host.py 时预算照常。
- G13/G14:/pr 不再走 system 豁免,建任务前按请求用户查配额(402/503 均不排队不改状态),队列路 pr_jobs.created_by 记请求用户(老表 ALTER 补列),worker 跑 pr 任务时审计操作人用 created_by(老行回落 system)。注意:G14 原案的 system 豁免(服务端与 worker 写死)与 P1 冲突(P1:system 按普通属主走默认额度,不另设无限额系统预算),本次 rebase 未合入该豁免,system 豁免需另行决策。
- 配额口径与 P0/P1 对齐:无配额行按默认额度(非不限额),只有显式 NULL 行才不限额;无价上限只计 source='actual' 行;缺属主(含父任务回查仍无)拒绝新回合。
- README 配额节同步以上口径与 G15 接受现状(best-effort 超支、UTC 月切分、在跑任务只在轮询/轮次边界响应配额改动)。G4 与 G15 不在本批。
2026-10-28 11:14:10 +08:00
host 5883169c82 feat: goal 断点续跑端点(G10)+ 摘要持久化与推送 outbox(G11/G12)
G10:POST /tasks/{id}/goal-resume,仅 interrupted 的 goal 任务,
走 host.py goal-resume(无 --goal-text)从 goal_rounds_done 下一轮
断点续跑,保留原线程与 worktree,状态回 queued 并置
goal_resume_pending(worker 认领后清掉),写 task_goal_resumed
审计;goal-met/unmet、failed、非 goal 一律 409;/continue 开新
循环语义不动。Local/Docker 两路新增 goal_resume_breakpoint_task,
不支持断点的执行器明确失败,绝不回落成 goal 或开新循环。
G11:digest 非失败事件持久化进 push_digest_events,worker 循环
每小时按(项目、订阅)分组汇总发一条,失败类仍即时发。
G12:webhook/slack 投递失败写 push_outbox,worker 循环后台重试
(最多 3 次、默认 60 秒退避),3 次仍失败删行并记日志,不丢
任务状态;缺凭证与无 sender 不进 outbox;文档写明 at-least-once
下 webhook/slack 可能重复,接收方须去重。
测试:cloud 769(4 跳过)、host 68、deploy 19 全过。
2026-10-28 11:09:29 +08:00
host 9bb9a50e4e 残留批次A:成员删除、PR needs_local_worker 落库、推送 DNS TOCTOU
- 成员删除:DELETE /projects/{id}/members/{user_id},限 owner 或
  全局管理员,禁删唯一 owner,记 remove_member 审计
  (AuthStore.remove_member + server 分发,沿用 PATCH 守卫口径)
- PR needs_local_worker 落库:非 local 扫到 queued 任务时 CAS
  写行状态为 needs_local_worker;claim/find_open 同步认该状态,
  标记过的行仍可被 local worker 认领、不被搁置;另修
  claim/find_open 对裸 sqlite3 tuple 行的 dict() 兜底
  (TypeError/ValueError)
- 推送 DNS TOCTOU:发送改走建连时解析+判段的连接类,解析
  一次、只拨判过的 IP,Host/SNI 仍为原域名;opener 直接构造
  以排除 ProxyHandler 改写(build_opener 默认会塞代理)
2026-10-27 19:10:49 +00:00
host 1402dc67fd P1 security batch: quota execution checks, cost accounting, goal/coordinator semantics, PR channel, orphan signature, control-plane and console hardening
- Docker quota watchdog + fail-closed quota checks, system tasks need real owner
- max_cost_usd actually accumulated, cancel records cost, estimated rows excluded from unpriced cap, cache_write declared non-billed
- goal-resume keeps original verify on breakpoint resume, goal retry falls back to resume, goal_max_rounds capped at 100 at create and execution
- coordinator children inherit parent model/effort, orphans marked interrupted with audit
- PR jobs on docker workers return explicit needs_local_worker instead of silent empty
- worker code signature covers 6 files and is checked before claim, fail-closed
- sub2api lazy provisioning with rotation, backup whitelist + 0600, push SSRF block, repos/workers desensitized, key/project audits and limits, console buttons/quota/status wording, coordinator/automation quota reserve
2026-10-27 16:27:50 +00:00
host 7091a2d0d2 P0 security batch: true global admin, quota gates on all spending paths, default quota for quota-less users, fix-review author trust + docker/owner attribution, GitHub App setup issuer binding, real cancel kill, wider shell env exclusion
- CODEX_GLOBAL_ADMINS replaces maintainer-derived global admin;
  admin endpoints (quotas, sweep, backup, push dispatch) fail-closed
  403 when unset, with audit on allow and deny.
- continue/followups/messages/retry check both caller and original
  creator quotas (USD + unpriced token cap), 402 on either overrun.
- Quota-less users now get the #86 default quota instead of unlimited;
  init_db backfills and OAuth create/bind/existing-login branches
  INSERT OR IGNORE a default row; explicit NULL row stays unlimited.
  user_quotas added to migration tables.
- /fix-review: untrusted authors rejected before dedup claim, no task;
  trusted tasks force executor=docker, created_by=project owner, owner
  quota gate, prompt is a fixed template plus a 2000-char excerpt.
- GitHub App setup state only issued by global admins, state rows bind
  issued_by (ALTER-compatible for old DBs), callback rejects issuers
  no longer in the admin list without consuming the state.
- Cancel: popen poll checks DB status every second, Docker path runs a
  cancel watchdog that kills by container name, write-back is CAS-guarded
  so late results never overwrite a new status.
- SHELL_ENV_EXCLUDE adds *_TOKEN, *_AUTH*, *_CREDENTIAL*, *API_KEY*;
  comment states the residual risk: same-UID /proc environ remains
  readable, true isolation needs UID separation or hidepid.
- Tests: new cloud/tests/test_p0_security.py (17 cases); old tests
  updated to the new semantics (admin env, default quota, trusted
  fix-review author) without weakening any gate.
2026-10-27 15:35:23 +00:00
host ec47328d3c sub2api 网关接入:每用户独立账号 + 任务注入 + 网关可配置
- cloud/sub2api.py:管理 API 客户端(管理员登录/token 刷新/
  建用户/以用户发 key/删用户/查组/网关模型目录)+ 幂等开通
  ensure_user_gateway_key(入库失败尽力清理孤儿 key 与账号)
- worker:网关门(sub2api 时按 created_by 幂等开通,失败任务
  直接 failed;协调员子任务跟父任务属主)+ 默认模型走网关
  目录首个;三条执行路径(Local popen/同步、Docker 同步)注
  入任务级 env 与 --model-provider;NO_PROXY 追加网关 host
- host.py:--model-provider,sub2api 时以 -c 注入 provider 表
  (不改租户 config.toml);未配 SUB2API_BASE_URL 时 loud fail
- sandbox:Local/Docker 执行器传 --model-provider 与任务 env;
  docker 密钥只传名(-e SUB2API_API_KEY),值由 CLI 子进程
  环境提供,绝不进 argv;runner 不支持 env 时 loud fail
- server:OAuth 建号 best-effort 开通;/relay/models 走调用者
  自己的网关模型目录;init_db 补 user_gateway_keys 表
- 测试:新单测 11 个;全量 cloud 679(4 跳过)、host 65、
  deploy 19 全过
- 诚实边界:真模型调用实弹需上游账号挂组后才做;切流前
  xcmapi 保持现网
2026-10-27 17:22:54 +08:00
host 7e081211db 多用户生产级缺口批:sweep 鉴权、默认配额、默认 executor、 无价 token 上限
- POST /admin/sweep 改走任一项目 maintainer+ 门槛(与备份管理同口径),否则 403,记 admin_sweep 审计;threshold 下限钳 60 秒
- OAuth 建号 created=True 时写默认配额行(默认 $10/月,CLOUD_DEFAULT_QUOTA_USD_MICROS 可调,INSERT OR IGNORE 幂等);老用户无配额行不受影响
- 建任务未指定且无环境继承时默认 executor=docker(分支继承为空时同理);CLOUD_DEFAULT_EXECUTOR 可覆盖,空字符串恢复旧行为;命令台下拉默认改为 docker
- 无价模型 token 上限:价目表外模型(默认 terra)成本 usd 为 NULL,USD 配额永不生效;有配额行的用户当月无价 token 达 CLOUD_QUOTA_UNPRICED_TOKEN_CAP(默认 2000 万/月)即停,服务端 402、worker 认领/重试/后续消息同口径;报表仍记 unpriced,不折假美元
- 测试:新增 test_multiuser_gaps 11 个 + test_worker_quota 3 个;全量 cloud 668、host 64、deploy 19 全过
2026-10-27 17:15:09 +08:00
openqj c1f9313c10 Merge pull request #87 from openqj/feat-coordinator
feat: 协调员任务(并行子任务、等齐、合流汇总)
2026-10-27 16:25:32 +08:00
host e0cecd70aa feat: P1 补齐(自动评审队列、自动化+分诊、并发 caps、运行时审批队列) 2026-10-27 15:53:50 +08:00
host 7c46ac8e5b feat: 协调员任务(并行子任务、等齐、合流汇总)
建任务时传 coordinator_parts(分工列表,1-20 项,分工名唯一)即为协调员任务:本身不跑模型回合,worker 认领后一次建齐子任务(普通任务,并行被各 worker 认领),等全部终态后合流成汇总报告写回。子任务带 parent_task_id 与 coordinator_part,控制台可用 parent_task_id 查到;协调员重跑按分工名复用已完成子任务、失败/中断的回 queued 重跑;取消与超时尽力取消未终态子任务。子任务的配额/key/执行器门照常生效,不构成绕过。协调员任务拒绝与 goal/plan/验收命令同用。控制台:任务列表 [协调]/[协调子任务] 标记、详情分工与子任务区、新建表单子任务 JSON 输入。新单测 26 个。
2026-10-27 15:45:17 +08:00
host dbcc8e641a feat: 任务列表显示 turn_id 与 root_turn_id
host.py dispatch 打印补 turn_id/root_turn_id;执行器解析(local/Docker)透传;worker 写回 tasks 新列(新值非空才覆盖);server 迁移与接口字段;控制台任务列表新增 turn/root turn 两列(短显+悬停全文);新单测 9 个。
2026-10-27 14:52:00 +08:00
host ef30558f7c feat: 任务间互发消息(API + agent 标记块,不中断任务)
同项目任务互发消息、对方做完可回消息:API POST /tasks/<目标>/messages
(from_task_id,目标项目 create_task + 发送项目 read 鉴权)与 agent
回合输出标记块(worker 回合后解析投递,不用联网、不用凭证,本地
与 Docker 都通)。投递不中断任务:对方运行中/排队中排成后续消息
(轮次边界执行,direct 优先);对方终态则续跑复活(resume_message +
queued),A→B→A 往返不受回合时序影响。拒绝项:自己、跨项目、goal
目标、已合并、复活位被占;每轮最多 5 块,仅 completed 轮次投递;
目标执行时配额门照常生效。控制台后续消息列显示发送方,新增给
其他任务发消息框(落已代理的 /tasks 下,不用改 nginx)。

测试:新增 test_peer_messages(15)、test_peer_messages_server(8)、
test_worker_peer(4);全量 cloud 654、host 64、deploy 19 全过(cloud 4 跳过)。
2026-10-27 14:12:28 +08:00
host 57f8852634 fix: worker 代码变化自退 + 部署时清孤儿 worker
2026-10-27 暂存清理实弹:05:51 的旧 docker worker 成孤儿后
继续用旧代码抢任务、按老路失败且不删暂存;autoscaler 用
start_new_session=True 拉 worker、部署重启杀不到非 unit 进程。

两层修:
1. worker 常驻循环每轮边界比对 worker.py 文件签名
   (mtime_ns + size),部署换目录后必变,变了就以 75
   (EX_TEMPFAIL,失败类)退出——检查点在轮次边界,绝不
   中断在跑任务。退出后:unit 托管的由 systemd on-failure
   重拉新版,autoscaler 托管的由 reconcile 重拉,孤儿直接
   消失。附带好处:以后部署不用再手动重启本地 worker。
2. host_deploy.sh 换目录后、启动服务前清孤儿:按 cgroup
   过滤杀非 .service 的 cloud.worker 进程(桥接修补前就存
   在的孤儿),先 TERM(跑完当前任务再退),30 秒未退的
   再 KILL;unit 托管的(如 host-1)不动。pgrep 用
   [c]loud.worker 写法,避免检查命令自匹配。

测试:test_worker.py 新增 2 个(签名变了则 75 退出且不认领、
签名不变则跑到 max_seconds);反向验证:撤掉 worker 修复
时正好前者变红。清扫过滤器主机干跑:两个现役 worker 均
为 unit cgroup,全 KEEP,杀 0 个。全量 deploy 19、host
62、cloud 619 全过。
2026-10-27 11:23:49 +08:00
host ad014393ee worker: enforce quotas during execution (claim/retry/fallback/followup/poll kill)
- claim-time gate: queued task whose quota was spent while waiting is
  marked interrupted with a quota reason, no cost row, resume kept
- retry/fallback boundaries and every followup round check DB month
  usage plus this run's accumulated usage (priced via cost table)
- local popen poll loop kills the process group on quota (30s throttle);
  Docker/sync paths get boundary checks only (documented)
- quota stops finalize as interrupted + quota_exceeded error, keep the
  actual cost row, no push, followups restored to pending
2026-10-27 11:06:40 +08:00
host 99de5c77f8 fix: 提前失败路也清附件暂存(仓库解析失败/缺 key 不再残留 bundle)
2026-10-27 附件实弹:仓库解析失败的任务不走正常终局清理,
bundle 目录永久残留(首个失败任务的暂存目录须手动删)。

根因:run_one 里缺 key 门与仓库解析失败两路提前返回,够不
到正常终局后的暂存清理;只有走完 _run_attempt 的任务才删。

修法:把终局清理抽成 _cleanup_task_seed 唯一入口,三路(正
常终局、缺 key 门、仓库解析失败)都走它;取消仍走 server
侧删,互不冲突;删失败不影响任务结果本身。

测试:test_worker.py 新增 2 个(仓库解析失败、缺 key 时
bundle 目录都被删);反向验证:撤掉 worker 修复时正好这
2 个变红,有修复时全绿。全量 deploy 19、host 62、cloud
610 全过。
2026-10-27 10:57:22 +08:00
host f9be9205ab fix: goal_resume_task 只在 LocalExecutor 上调,其余回落 goal_task
与附件分支同款隐患:CloudExecutor 基类与假执行器没有
goal_resume_task,直接调会 AttributeError;回落到 goal_task 让
它们按原语义失败(CloudExecutor 即“不支持 goal”)。
2026-10-27 04:40:34 +08:00
host 3a4997d346 feat: 旧任务继续会话——终端任务带新消息 + 全部选项同一会话续跑
server:POST /tasks/{id}/continue(终端非 merged 任务重排队,
resume_message 列存本轮消息,不传选项沿用原值,plan/goal 冲突
与新建一致,goal_mode=1 时转新 goal 语义;CAS 防并发改状态,
审计 task_continued);tasks.resume_message 三处迁移 + PG 建表。
worker:认领后本轮 prompt 换成续跑消息(只换内存副本),
goal 续跑走 goal-resume 而不是 goal,跑完清 resume_message
(进程被杀时不清,下次认领重跑同一条消息)。
host:goal-resume --goal-text(旧任务接续 goal 模式:跳过状态
与记录守卫,旧线程上从第 1 轮开新目标循环,新语义不限轮次;
无线程时拒绝)。
sandbox:Local/Docker goal_resume_task + popen goal-resume kind;
附带 popen goal 命令改成新语义(无验收不传 --verify,轮次 0
原样传,此前 goal 任务到本地 worker 会直接报错)。
console:详情页终端非 merged 任务出继续会话表单(消息 +
模型/强度/权限/计划/goal,模型下拉复用中转列表)。
测试:新增 test_task_continue.py 11 个 + test_host_continue.py 2 个。
2026-10-27 04:40:11 +08:00
host 9306c957f3 fix: seed 参数只传给 LocalExecutor,假执行器/CloudExecutor 旧签名不再炸
worker 曾无条件给同步路径的 goal_task/dispatch_task 传 seed_dir,
CloudExecutor 基类与测试假执行器只有 (task) 一个参数,直接
TypeError,炸掉 test_project_repo 2 个 + test_cloud_executor_goal_unsupported
1 个(后者本应报“不支持 goal”)。改成 isinstance(LocalExecutor)
才传 seed_dir,其余走原签名;Docker 路径已有 is_docker 守卫。
2026-10-27 04:30:43 +08:00
host 7c444a688b feat: 任务附件——server/worker/console 链路打通(+ popen goal 新语义补漏)
server:POST /uploads(multipart,413 预检、路径/数量/大小校验、
24h 过期包、used 防一包多用)+ 建任务绑定(同事务写附件行 +
包置 used,cloud 执行器 400)+ 列表/详情带 attachments +
取消删暂存 + 分支带走附件清单;db.py PG 建表 + init_db 补表。
worker:_task_seed_dir(无附件/目录已删静默 None)+ dispatch/
goal 三路(同步本地、popen、Docker 只读挂载 /tmp/codex-seed)
+ cloud 执行器兜底失败 + 终局删暂存。
console:文件/文件夹/图片三路输入,先上传拿 upload_id 再建任务,
列表 [附件×n]、详情列附件路径。
附带:popen 的 goal 命令此前强制要求 verify_command,goal 新语义
任务到本地 worker(走 popen)会直接报错,改成与同步 goal_task
一致(无验收不传 --verify,轮次 0 原样传)。
测试:新增 test_task_attachments.py 15 个。
2026-10-27 04:30:43 +08:00
host 3d45edc8b4 feat: 命令台云端环境——选、建、管 + 任务绑定环境
环境是项目仓库的命名基准检出(项目 + 仓库快照 + 分支 + 执行器),
不是官方 Codex 云端环境(无公开调用口,不宣称能建它)。
- DB:environments 表(creating/ready/failed/archived,部分唯一索引
  保证同项目未归档名唯一、归档名可重用)+ tasks.environment_id,
  server/worker/db 三处迁移同步(含 #67 的 goal_mode,不丢新语义)。
- API:POST/GET /environments、GET /environments/{id}、
  POST /environments/{id}/archive;任务创建选环境须本项目已 ready、
  执行器一致或继承、cloud 执行器拒绝。
- Worker:run_all 每轮开头准备 creating 环境(真实 git clone +
  checkout 分支,幂等、无 preparing 卡死态,准备期归档 CAS 不覆盖);
  环境任务派发时 task[repo] 指到环境检出(租户 Docker 容器内路径 +
  envs 整目录挂载),任务仍一任务一 worktree。
- Console:环境卡(列表/新建/归档,未 ready 置灰)+ 新建任务环境下拉
  + 任务列表 [环境] 标记;公网 nginx 须同批代理 /environments。
- 单测:新增 test_task_environments.py 14 个;本地实弹(真实 git
  clone octocat/Hello-World,不跑模型)项目→环境→任务→归档全通。
2026-10-27 04:07:54 +08:00
openqjandhost c86b450851 feat: goal 模式新语义——prompt 即目标,无验收命令,不限轮次,模型宣告完成 (#67)
console 打勾 goal 模式时不再有专用 goal 文本、验收命令、轮次
三个输入(已移除,换成说明行),只发 goal_mode=1;server 侧
goal_mode=1 时 goal 存 prompt、验收命令空、轮次存 0,带验收
命令或与计划模式同用时 400,旧的带验收命令任务照原样可用。

host.py goal 循环:verify 为空时不跑验测,轮次提示里写明
标记约定;模型在完整跑完的回合末尾单独一行输出“目标已
达成”即达标(超时回合的部分文本含标记也不算);--max-rounds
0 为不限轮次,goal-resume 续跑按 0 处理(不用 or 兜底成 3)。
刹车只有取消与预算耗尽(后者记 interrupted、可续跑)。

迁移与 worker:tasks 加 goal_mode 列(server 迁移、worker 列
补齐、PG schema 三处同步);worker 按 goal_mode 或
verify_command 判定 goal 路径,重试与后续消息排空对新语义
同样只跑一次/不排空;Local/Docker goal_task 的 --verify 可选、
--max-rounds 0 原样传。

Co-authored-by: host <host@test.local>
2026-10-27 03:54:48 +08:00
openqjandhost f4443dca4f feat: 计划模式 + 后续消息 + 会话分支接云端 (#65)
- 计划模式:tasks 加 plan 列;plan 与 approval=full / goal 冲突时
  400;执行器在 approval 参数后追加 --sandbox read-only(本地
  dispatch/run/goal/popen 与 Docker 内层命令全覆盖),host 任务
  存下 sandbox 后续轮次自动继承只读。
- 后续消息:新表 task_followups;POST/GET /tasks/{id}/followups
  (仅 queued/running 的非 goal 任务;direct 立即往链路追加
  phase=followup-direct 条目,本轮跑完后优先于排队执行;排队
  按 direct 优先、队内先来先服务逐条再跑一轮,同一 thread;
  一条失败即停、剩余 pending 留给重试入口;取消时回 pending)。
- 会话分支:POST /tasks/{id}/fork(仅终态任务;继承 executor/
  模型/强度/权限/计划,goal 字段置空);worker 先 fork 再跑首轮,
  首轮失败不自动重试、不重复分叉,失败后走重试入口重跑首轮。
- 修任务级模型被覆盖:worker 此前无条件写成主模型,控制台
  选的模型实际不生效;现为任务级 > worker 主模型 > 配置。
- Docker 租户准备抽成 _docker_task_context,供普通尝试与 fork 共用。
- console:新建任务计划勾选(含冲突拦截)、详情计划徽标与
  分支来源、分支按钮与表单、后续消息区、列表 [计划]/[分支] 标记。
- 测试:cloud/tests/test_task_links.py 17 个(server 校验与继承、
  worker 模型优先、只读命令、排空顺序与停排、goal 不排空、
  fork 单次性);全量 cloud 542、host 49、deploy 14。

Co-authored-by: host <host@test.local>
2026-10-27 03:17:51 +08:00
host 1128fd3076 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=不限时)。
2026-10-27 03:05:15 +08:00
openqjandhost 6022f1dada chore: default model gpt-6.1-sol -> gpt-5.6-terra (#60)
Co-authored-by: host <host@test.local>
2026-10-27 02:00:09 +08:00
openqjandhost 8b2f3968dc feat: per-task model/effort/approval + relay models endpoint (#59)
Co-authored-by: host <host@test.local>
2026-10-27 01:44:53 +08:00
openqjandhost fc8e337d0f HA 阶段 1:worktree_host + 跨主机 /pr 队列化 (#50)
* 部署自动化:唯一发布链路 deploy.py + host_deploy.sh,sha/commit 双重验真,失败回滚

旧链路三次栽过的坑:通用 tar 路径导致旧包误部署、传输不验 sha、
主机脚本多份漂移、health 通过不等于新代码上线。本单把四点变硬门禁:
- 产物唯一命名 codex-app-server-<commit>.tar.gz + .sha256,远端唯一路径
- git archive 产物 + DEPLOYED_COMMIT 清单,默认拒绝脏工作区
- 传输链 scp -> scp_run -> chunk 分片,每路传完远端验 sha,不符删掉重来
- 主机侧唯一脚本:先验 sha/清单,staging 解包 + 编译检查,换目录上线,
  work/ 原样保留,health + 上线 commit 不符即回滚并退出非零
- deploy.py 成功后独立 ssh 再读一次 live 清单二次核对
- CI:host-verify 加 deploy/tests;release.yml 只在手动/tag + production
  environment 审批时部署,主干推送绝不自动部署生产
- deploy.sh 标记为旧链路弃用
- health 的 commit 字段已在父提交 HA 阶段 0 里随 server.py 落地,
  本分支整体包含它;commit 为空时只靠文件核对,门禁同样成立
测试:host 32、cloud 487(4 跳过)、deploy 9,全部通过;
host_deploy.sh 仿真覆盖成功/坏 sha/清单不符/health commit 不符回滚。
生产实机部署未做,等确认后再用新链路推。

* HA stage 1: worktree_host + queued cross-host PR jobs

---------

Co-authored-by: host <host@test.local>
2026-10-27 00:05:18 +08:00
host 63823fc2e3 逐任务真实成本:上游用量事件 + 价目表计价
此前每任务只记一条 estimated(字符数/4,金额为空),注释称
host 协议不暴露真实 token 数。本轮实测推翻了该前提:
- account/usage/read 在中继上是死的(要求 ChatGPT 账号登录),
  不是来源,已在 cost.py docstring 写明死胡同
- app-server 每上游补全发 thread/tokenUsage/updated,last 是该次
  补全的用量;中继 5 调用回合实测 last 之和等于最后 total
  (input/output/cached/reasoning 四路都对得上)

实现:host.py run_turn 从事件窗口按 threadId+turnId 汇总用量,
重试/goal 各轮累加,dispatch/run JSON 与 goal 用量行透传;
sandbox 三路解析(dispatch JSON 解析改用完整 stdout,顺带修
了长 text 下 failed 被误判 completed 的潜 bug);worker 每任务
记一条 actual(重试/fallback 用量累加进同一条),无用量时回落
estimated;cost.py 价目表(默认 OpenAI 挂牌价,env 可覆盖,
无价模型 usd 记 None、报表进 unpriced_actual_tokens 而非 0 美元),
cost_records 加 cached_input_tokens 列(SQLite/PG 两支迁移);
报表与按天行新增 cached_input_tokens/unpriced_actual_tokens。
测试:host 32、cloud 483 全过。
2026-10-26 18:41:27 +08:00
host 5ee9d291dd 云端 goal/merge/review 三事件:goal 循环 + PR 事件通知
此前 goal-unmet/merge-failed/review-requested 只有允许列表,
云端没有生产者(概念只在本地 host.py)。本轮给三者接真生产者:
- goal:POST /tasks 接受 goal/verify_command/goal_max_rounds
  (迁移补列,SQLite 与 Postgres 两支);带 verify_command 的
  任务走 host.py goal 循环(Local/Docker 执行器 goal_task,
  turn→验测→喂回重试,缺省 3 轮);达标 completed,未达标
  failed 且只发 goal-unmet、不重复发 failed;goal 模式下 worker
  级重试/fallback 关闭,同任务 /retry 会撞 host 任务名已存在
  (已在文档写明,须新建任务);CloudExecutor 无 goal 概念,
  如实失败不降级
- review-requested:POST /tasks/{id}/pr 建 PR 成功时发
  (payload 含 pr_url/pr_number)
- merge-failed:PR 落盘(push/建 PR)失败 502 时发(payload
  含 step/error),任务保持 merged、可重调 /pr;跳过时不发
测试:host 29、cloud 469 全过。
2026-10-26 18:11:12 +08:00
host fb3a311da8 推送真发送:webhook/slack 内置真实发送端 + worker 终态自动通知
此前订阅与分发框架都在,但分发端点传 sender=None,
webhook/slack 永远报无 sender、从未真发过。本轮:
- push:send_webhook/send_slack(标准库 urllib POST,
  单次 10 秒超时,不重试,失败记 errors);sender=None
  时用内置端,显式 sender 优先;webpush/apns/fcm 仍报
  缺凭证(原因写明:webpush 需浏览器端三字段订阅与标准库
  没有的 P-256 ECDH + AES-GCM;apns 需 HTTP/2;fcm 需
  Firebase 服务账号,三者均无现成接收方,不造假发送)
- worker:run_one 终态(completed/failed,缺 key 与仓库
  失败两条早退路径也在内)后 best-effort 调分发,通知
  失败只记日志、绝不影响任务状态;取消(interrupted)
  不在事件集合里,不通知
- 测试:真实发送端 6 个(本地回环服务器)、worker 终态
  通知 2 个,旧无 sender 断言按新语义改写
2026-10-26 17:31:30 +08:00
host 9f05178f2b Docker tasks: --docker-runtime option (runsc support, default off)
DockerExecutor gains a runtime parameter (passed as docker --runtime
in build_run_command, after --pids-limit); worker CLI gains
--docker-runtime, wired through _build_cli_executor. Unset or empty
means no --runtime flag, so existing tasks keep docker default (runc);
an unregistered runtime fails container start (fail closed), it never
silently falls back. Tests: runsc appears in the command, default/empty
omit it, CLI wiring passes through. README documents the flag and
corrects a stale multi-tenant note (host kernel does not ban userns;
bwrap fails inside Docker containers on nested-pidns proc mount).
2026-10-26 10:26:21 +08:00