Files
codex-app-server/docs/arch-conformance-metrics.md
host dc556b0c19 audit: lifecycle hook writes the attempt ledger (gap 3 follow-up)
- cloud/attempt_hook.py (new): canonical policy -- one counted
  dispatch = one row, is_defect always False from the worker;
  human/first rounds and human breakpoint resumes count;
  platform auto-resume reruns, coordinator fan-out, and
  pre-execution gate failures do not; transient retries inside
  one dispatch are not new attempts. Platform origin survives
  via the task_attempt_origin_markers table (same-transaction
  write on requeue, queued_at generation guard on consume).
  Defects are explicit-recorder only; corrections are
  append-only (record_correction, last-wins).
- cloud/worker.py: Worker(attempt_ledger_hook=False) + CLI
  --attempt-ledger-hook (default OFF = gap-3 behaviour exactly);
  marker write in auto_resume_interrupted, marker consume +
  classify in run_one, one ledger write at each terminal point
  (cancel branch, normal terminal block); all hook writes
  best-effort, never fail the task or block a requeue.
- cloud/attempt_ledger.py: corrects_attempt_no column (new DDL
  supersedes gap 3's pre-merge DDL) + record_correction +
  correction-aware aggregation; read path uses SELECT * so
  gap-3-era tables still read; cost code untouched.
- cloud/tests/test_attempt_hook.py: 32 tests (policy, markers,
  corrections, worker integration, mixed endpoint).
- docs/arch-conformance-metrics.md: policy, switch, PG manual
  steps (new PG_DDL + PG_MARKER_DDL), remaining user decisions.
2026-11-03 02:49:26 +00:00

15 KiB
Raw Permalink Blame History

架构一致性进 CI 与指标闭环(审计加固第 5 步)

实现:cloud/arch_conformance.py(纯检查,标准库)、cloud/metrics.py (纯计算,标准库)、端点 GET /metrics-report(cloud/server.py, 与 /metrics 同款认证与成员范围)。测试失败即 CI 失败:现有 host-verify 工作流已跑 discover -s cloud/tests,本轨未改任何 workflow——第 2 轨拥有 lint/类型/安扫,本轨不碰。

架构一致性检查(规则、理由、测试)

检查 规则 理由 测试
合同版本 冻结后版本只能 +1,且须有协调员批准的变更记录(声明向后兼容、列出受影响客户端;破坏性变更须有回归任务) 契约静默漂移是并行线程分叉的根源;协议是唯一合法路径,检查执行协议而非只记录协议 test_arch_conformance.py::TestContractVersion(正向:等值/批准+1/破坏性+回归任务;负向:未批准、无记录、跳号、不可解析)
禁目录 任务只能写自有目录;.. 与符号链接逃逸在解析后判定 跨目录写会 corrupt 他人 worktree TestDirectoryGuard(正向:目录内;负向:..、symlink、混合写只报违规项)。口径复用已有定义,不另造:控制面 = 租户 work 目录(worker._tenant_dirs 的 work),技能项目 = apps/<client>/
禁 import 基础层(db/models/queue/logging_config)不 import 编排层;执行层(sandbox)不 import 持久化/编排层;除 db/backup/migrations 外任何 cloud 模块不得直接 sqlite3.connect 或 import psycopg*(服务层走 cloud.db.connect 或 Store) 基础层上引是循环依赖前兆;执行器须可换、可假 runner 测试;驱动直连绕过 sqlite/postgres 唯一分发点 TestLayerImports(每项正向+负向)+ 仓库级 TestRepoLevelScan(当前主线零违规,未来违规即 CI 失败)。有意不禁:server.py 的 sqlite3.Row/异常类型适配(那是在 cloud.db 连接上的适配,不是绕过;禁它会在基线误报)
技术栈冻结 G2 冻结表变更/删除须有批准的变更记录;新增客户端是新增不是换栈,通过 共享核心按冻结表构建,中途换栈使其静默失效 TestFrozenStack(正向:不变/新增;负向:换栈/删除无批准)

技能项目一键版:check_skill_project 把合同版本、冻结栈、目录写 三项合在一起验,TestSkillProjectFixture 用合成项目目录正向/负向 各验一次。静态 AST 扫描看不到 importlib 动态导入——这是已知 边界,不声称覆盖。

指标采集与报告

端点:GET /metrics-report(需 X-API-Key,成员范围)。报告格式 见 cloud/metrics.py::build_report:golden_set、decision_ready、 metrics、thresholds + thresholds_source、feedback_actions、 feedback_consumers。

指标口径(compute_metrics):

  • 首过绿灯率 = 首过成功任务 / 已知 attempts 的终态任务;缺陷率 = 有缺陷任务 / 已知 defects 的终态任务;返工率 = attempts>1 任务 / 已知 attempts 的终态任务。终态 = completed/merged/failed/ merge-failed;interrupted 不算终态(平台中断自动重跑,算失败 是罚平台事件)。
  • 每任务成本复用 #212 双列口径:有价 USD micros 与无价 actual token 分列;成本不完整(无成本行或有无价 token)时 total_usd_micros 为 None,有价部分只以 priced_usd_micros_partial 出现——无价永不记 0、永不折进总额。
  • 未知率回 None + *_known: false,绝不报 0。

当前诚实边界(端点实测,审计加固补缺口第 3 项之后):tasks 表 仍无 attempts/defects 列——台账改为独立表 task_attempts(cloud/attempt_ledger.py),不改主表结构。 端点 GET /metrics-report 聚合台账后传给 compute_metrics: 有台账行的任务算出真实首过/缺陷/返工率;无台账行的老任务 仍回 None + *_known: false,绝不被算成首过或无缺陷; 台账表尚不存在(老库、或生产 PG 未手动执行 PG_DDL)时 端点与无台账行同等处理,不报错。金任务集未经人工审定, 报告恒为 pending_human_approval、decision_ready: false。

任务尝试台账(审计加固补缺口第 3 项,选型与口径)

选型:独立台账表,不改主表。列方案查询简单(一行一任务、 无关联),但把每次尝试的历史压成两个计数器——哪次尝试失败、 属哪类缺陷、顺序如何,事后无法恢复;且在生产 Postgres 上拓 宽高频 tasks 表比重建一张空表更重。台账表多一次聚合查询, 换来完整尝试历史与不碰主表的迁移,取舍写在 cloud/attempt_ledger.py 模块 docstring 里。

表 task_attempts(task_id, attempt_no, outcome, is_defect, defect_category, created_at, corrects_attempt_no),主键 (task_id, attempt_no),attempt_no 按任务 MAX + 1 递增、只追加不改写(corrects_attempt_no 为 NULL 时是真 尝试行,非 NULL 时是纠正行,见钩子一节)。口径:

  • outcome ∈ success / failure / interrupted;interrupted 行只留档,不计入 attempts 也不计入 defects(平台中断自动 重跑是平台事件,不是 agent 失败;只有中断行的任务在聚合 里直接缺席,仍回未知而非 0)。
  • is_defect 由记录方显式判定,绝不从 failure 自动推导 (失败重试未必是缺陷轮);defect_category 须配 is_defect=True,中断行永不能是缺陷,违例写时即 ValueError,不存歧义数据。任务的单类别标签取首个缺陷 行的类别(attempt_no 顺序,先到先得,确定性)。
  • 迁移幂等:SQLite 侧 CREATE TABLE IF NOT EXISTS,重跑 不动已有行;Postgres 侧 PG_DDL(与 SQLite DDL 同文,只用 Postgres 合法类型与复合主键、无自增/identity 列,故无需 序列处理)须在生产 PG 上手动执行并验证(\d task_attempts 或 information_schema 等价查询),且有意不 进 POSTGRES_SCHEMA(不静默拓宽迁移覆盖集);手动步骤 未做之前,Postgres 端点只会回未知率,不会报错。本机无 PG,真实 PG 验证未做;PG 证据目前只有适配层级别:? 占位 经 cloud.db 适配翻译为 %s、DDL 无需序列处理(测试 TestPostgresAdapterEvidence 钉住)。
  • 生命周期写入钩子已接(9+ 第 2 项,见下一节):worker 按政策给计数的派发写且只写一行,开关默认关;关时台账 仍只经显式 record_attempt / record_correction 调用填 充,生产率保持诚实未知。读端点不建表(只读端点不改 schema),建表只发生在写路径;标记表同理,只在钩子开 且平台自动续跑发生时写。

任务尝试生命周期写入钩子(9+ 第 2 项,政策与开关)

权威政策在 cloud/attempt_hook.py 模块 docstring(代码、 本文档、测试三处一致,冲突时以模块为准)。要点:

  • 计为一次尝试的(每派发恰好一行,is_defect 恒 False):任务首派发;人工 /retry(与首派发无法区分、 也不必区分,两者都算);人工 /continue 新消息轮次 (含开新目标循环的 goal 任务);人工断点续跑 (/goal-resume、旧验收 goal 的 /continue 断点语义) ——断点续跑算同一逻辑轮次的行,不是额外尝试:被中断 的片段只写了不计数的 interrupted 行,续跑终局行就是 该轮次唯一计数行;续跑再中断则仍只写 interrupted 行,该轮次继续保持未知。
  • 不计为尝试的(不写行):平台自动续跑派发(goal 断点续 跑与非 goal 全新跑一律不计——统一口径,平台杀掉又重跑 的执行不罚 agent 质量分);协调员父派发(只做扇出聚 合、无模型回合,子任务各自写行);执行前门失败(缺 key / 仓库解析 / 配额门 / 轮次上限 / 网关准备,准入拒绝 不是尝试);未派发(预算门、无就绪任务、认领输给别的 worker,连分类都不跑)。
  • 同一派发内的瞬时重试不是新尝试:max_retries 循环、 一次真实 fallback 换模型重跑、goal“已存在”断点回落、 后续消息排空都在一次 run_one 里,共享成本汇总、只 产一行(终局 outcome)。内层失败后重试成功不留失败 行;重试耗尽只留一行失败行。
  • 平台识别靠标记表 task_attempt_origin_markers:自动续 跑在同一事务里写标记(queued_at = 重排队写的 updated_at),认领方在认领前抓排队态 updated_at、 认领成功后按 queued_at 匹配消费;人为动作(取消/ 重试/继续)改写 updated_at 后陈旧标记自动作废,绝不 误伤后一轮人工派发;认领输了不消费,标记留给赢家。 标记写失败绝不挡重排队(退化为计数,见模块规则 5)。
  • 开关:Worker(attempt_ledger_hook=False) / CLI --attempt-ledger-hook,默认关。关时 worker 不写任何 台账行、不写标记、不建台账/标记表,与补缺口第 3 项行 为完全一致;开时才适用本节政策。开前中途(钩子关时已 重排队的任务无标记)有一次性的退化:该派发按人工/首 轮计数,之后每次重排队都有标记,即自愈。
  • is_defect 判定主体:只有显式记录方(人工维护者或协 调员复核)在记录时判定,worker 钩子永不判定(失败 ≠ 缺陷,瞬时失败尤其不是)。标错只追加纠正行 (record_correction:新行 corrects_attempt_no 指 向原尝试,旧行不动;聚合按 attempt_no 顺序后赢者通 吃;纠正行本身不计尝试、不能改 outcome、不能指中断 行/纠正行/不存在的尝试,违例写时 ValueError)。 维护者门槛与 /retry 同款(approve_merge),但本轨 不建标记/纠正的 HTTP 端点(函数即接口,端点是后续项, 见未做项)。
  • 取舍(明写):仅经平台派发终局的任务在台账里保持未 知(无计数行),分母只含人工/首轮终局的任务——率值描 述的是“人工/首轮终局任务”,不是全部终局任务;平台重 跑本身未丢失(auto_resume_count、审计 worker_auto_resumed、时间线 auto_resume 行)。被拒 的替代方案是“单独标记平台行但不进分母”:分母结果相 同,但多一种行、多一列、多一套聚合排除逻辑,只换来 台账内同址可查——本轨不取。另:平台 goal 续跑与人工 goal 续跑同属一轮循环,但前者不计、后者计(标记优于 断点),这是有意的统一平台排除;若用户要把平台 goal 续跑也算作轮次完成,改 classify_dispatch 一行 + 测 试即可,属用户拍板项(见待人工项)。
  • Postgres:标记表与台账表同款手动纪律——不进 POSTGRES_SCHEMA,PG_MARKER_DDL(与台账新 PG_DDL 含 corrects_attempt_no 列)须在生产 PG 上手动执行并 验证后,才可在 PG 控制面启用钩子;未执行时标记写失 败(best-effort),平台派发退化为计数。本机无 PG,真 实 PG 验证未做;PG 证据只有适配层(?→%s 翻译、 DDL 无自增/identity 列,测试钉住)。

回流与审计标准迭代(可测试规则,非口号)

evaluate_feedback 是纯函数,只产出 auto_apply: false 的行动 记录,不自动改路由、不自动改标准;行动由两处消费:

  • 路由:tighten_check(某类缺陷率 > 阈值)→ 该类任务路由时加 一道审计门;route_with_extra_audit(首过率 < 下限)→ 全量加 审计门。消费动作本身须经协调员/人工确认后才生效。
  • 审计标准:tighten_check / recalibrate_check(某检查的违规被 复核推翻率 > 阈值)各开一条标准复核项;标准文档本身的版本 变更走同一合同变更协议(check_contract_version,批准 +1), 测试 TestGoldenSetAndStandardsLoop 钉住“无批准改标准失败、 有批准通过”。误报复核规则:推翻率高只触发“复核阈值”行动, 绝不让检查自动放宽自己。

默认阈值(DEFAULT_THRESHOLDS:缺陷率 0.20、首过下限 0.50、 推翻率 0.30)只是测试用初值,报告标注 thresholds_source: "default_unapproved";生产阈值须人工拍板后 以 thresholds_source: "human_approved" 传入,见待审定清单。

审计的审计:抽查口径(cloud/audit_sampling.py,纯函数)

审计结论本身要被人工抽查,抽查规则可测试、可核查,不留 “定期复核”口号:

  • 必抽(比例 1.0):推倒重做案例必抽;第三次失败根因判为 接近完成、续轮仍 fail 的案例必抽。接近完成但续轮成功的、 第 1/2 次失败接近完成的,均不属必抽(阈值就是第三次)。
  • 随机抽查:其余案例按组抽取,普通案例默认 0.10,关键路径 案例默认 0.50;小数组 ceil 取整,绝不因四舍五入抽到 0。 抽取须带 seed(select_sample 无 seed 即报错),seed 记入 抽查计划,抽取结果可复现、可事后核查。
  • 抽查结果处理:抽查判审计正确 → 只记一条审计的审计记录, 原结论不动;判错 → 纠正原结论并重新触发相应流程:错发推回 → cancel_pushback_and_rerun_audit;错发推倒 → halt_rebuild_and_restore(旧实现按推倒规则保存在 git 历史, 可恢复);错放行 → reopen_audit_and_pushback;原结论未知 → escalate_to_human,绝不编造触发动作。每条结果(无论对错) 都须记入审计的审计记录,未记入即视为未发生。
  • 金任务审定清单(golden_approval_checklist):任务清单人工 复核、每任务验收标准人工定义、样本排除、审定人+时间记录, 四项全 done 且至少留一个非样本任务才算批准;样本任务 (sample=True 或 id 以 sample- 开头)一律从金任务集中剔除 并计数,示例文件永远不算金任务。报告的 sampling 段在无抽查 计划时明写 pending,不暗示审计已被复核。

待人工项(单独列):金任务集审定清单的四项人工确认;抽查 seed 的记录与保管;生产抽查比例拍板(默认值 0.10/0.50 只是测试初值)。 台账待人工项(钩子落地之后):生产 PG 上手动执行台账新 PG_DDL(含 corrects_attempt_no 列;若曾跑过补缺口第 3 项的旧 DDL,先跑 OLD_TABLE_ALTER)与标记表 PG_MARKER_DDL 并验证(\d task_attempts / task_attempt_origin_markers 或 information_schema 等价查询), 验证前不在 PG 控制面启用钩子;钩子政策默认值拍板——平台 goal 续跑不计(本轨默认)还是计为轮次完成、平台非 goal 全新跑不计(本轨默认)是否接受、缺陷标记/纠正的 HTTP 端点(approve_merge 门)是否立项;金任务集审定后报告才 可决策(现仍恒为 pending_human_approval)。 未做项(本轨有意不做):缺陷标记/纠正的 HTTP 端点(函数即 接口,权限检查随端点做);worker 之外的写行点(服务端 /retry / /continue / /goal-resume 本身不写行——写行 只发生在 worker 派发终局,服务端重排队不直接产生尝试); 真实 PG 验证(本机无 PG);生产部署(只到 PR OPEN)。