Audit batch 6: B1 zero-gate sentinel, I1 known-to-lack marker, I2 wire error returns row status, M1 max-rounds label, M2 chosen-last convention, M5 escalate write retry, M6/M7 clear/pass CAS tests

This commit is contained in:
host committed 2026-11-04 18:02:23 +00:00
1 parent 2adc605373
commit bc19f09dff
5 files changed
+683 -26

No files matched your search

+121 -19
View File
@@ -508,6 +508,9 @@ def _mark_gate_checked(task, gate):
无论哪份拷贝打标记,终局块读到的都是同一份集合。
终局时 _audit_loop_gate_results 只列入被标记过的门,
不再把没跑过的门硬编码成通过。
审计批 6(B1):run_one 入口已初始化该列表为空——零门
任务的 [] 与旧直接调用的 None 是两个不同哨兵,终局块
传 [] 时零门不列入,传 None(旧直接调用)时才走旧行为。
"""
if not isinstance(task, dict):
return
@@ -533,6 +536,14 @@ _GATE_ERROR_MARKERS = (
"工具审批声明矛盾",
"refusing to dispatch an incapable or unverifiable model",
"fails capability",
# 审计批 6(I1):显式网关拦与写回复核拒绝共用的措辞
# "…refusing to dispatch a model known to lack a required
# capability"。选全长措辞而不用较短的 "known to lack":
# 全长措辞在全库只出现在上述两处生产方 error 文案里,
# 误命中面最小;较短形式可能被执行失败的 error_text
# (例如模型输出复述)偶然命中。误命中方向是 escalate
# (安全侧、丢一次重试),不是放行,但精度仍优先。
"known to lack a required capability",
)
@@ -2047,8 +2058,10 @@ class Worker:
passed_gates 并持久化——“通过”一词在状态表里失真。
现在:调用方传 gates_checked(run_one 派发点用
_mark_gate_checked 在 task["_gates_checked"] 里记的
本轮实际跑过的门)时,只列入集合内的门;未传
(直接调用的旧调用方)时保持旧行为、三门全记 True。
本轮实际跑过的门;审计批 6(B1)起 run_one 入口
已初始化该列表为空,零门任务传 []、不再传 None)
时,只列入集合内的门;未传(直接调用的旧调用方)
时保持旧行为、三门全记 True。
gate_missing 语义核查:引擎只对“上一轮 passed_gates
里有、本轮缺失”的门升级;同一任务的跑过门集合在
两轮间是稳定的(显式任务无策略时两轮都不记
@@ -2100,6 +2113,27 @@ class Worker:
gate_results[gate] = True
return gate_results, findings
def _wire_error_final(self, conn, task_id, final):
"""CAS 未命中/存库失败后读回行实际状态(审计批 6,I2)。
接线 error 时调用方(run_one 终局块)不能再拿输入终局
当行状态:CAS 未命中意味着行已被第三方改动(例如重排
回 queued),继续按输入终局记终态审计、发 failed 通知
会与行状态自相矛盾。同一连接在回滚后仍可读,读到即
以行为准;读不到(连接已坏、行已删)时回落输入终局,
不比旧行为更差。连接都连不上时调用方无连接可用,
不走本方法(行未被接线改动,输入终局即行状态)。
"""
try:
row = conn.execute(
"SELECT status FROM tasks WHERE id = ?",
(task_id,)).fetchone()
except Exception:
return final
if row and row[0]:
return row[0]
return final
def _audit_loop_wire(self, task, final, result, error):
"""审计循环调用方接线:把引擎决策变成真实的任务结局。
@@ -2142,6 +2176,15 @@ class Worker:
result,自带连接)仍是提交后的独立写:它失败时只记
警告、不再把已 failed 的任务谎报成 error(旧版写回
失败会返回 error,尽管状态已是 failed)。
审计批 6(I2):CAS 未命中/存库失败返回 error 时,
final 不再是输入终局,而是回滚后读回的行实际状态
(例如行已被重排回 queued 时返回 queued);run_one
终局块据此抑制终态审计与通知,不再按输入终局谎报。
连接都连不上时无连接可读,仍返回输入终局(行未被
接线改动,输入终局即行状态)。
审计批 6(M5):升级成功后的结果写回有界重试一次
(仍带 only_if_status="failed" 守卫,幂等),两次
都失败时仍只记警告、不谎报成 error。
goal 任务(审计批 5,N-12 定性,只文档化、不改行为):
推回只改 tasks.status 与包列(+ 轮次状态表),不碰
goal / verify_command / goal_max_rounds 列,也不碰宿主
@@ -2221,7 +2264,10 @@ class Worker:
message="task status changed before packet clear")
except Exception:
pass
return final, error, "error"
# 审计批 6(I2):回滚后读回行实际状态,不再
# 拿输入终局当行状态返回(行可能已被重排)。
return self._wire_error_final(conn, task_id, final), \
error, "error"
conn.commit()
self._audit(project_id, "worker_audit_loop_passed",
"%s round=%s" % (task_id, decision.get("round")))
@@ -2253,7 +2299,9 @@ class Worker:
message="task status changed before pushback")
except Exception:
pass
return final, error, "error"
# 审计批 6(I2):回滚后读回行实际状态。
return self._wire_error_final(conn, task_id, final), \
error, "error"
conn.commit()
self._audit(project_id, "worker_audit_loop_pushback",
"%s round=%s findings=%s" % (
@@ -2289,7 +2337,9 @@ class Worker:
message="task status changed before escalate")
except Exception:
pass
return final, error, "error"
# 审计批 6(I2):回滚后读回行实际状态。
return self._wire_error_final(conn, task_id, final), \
error, "error"
conn.commit()
# 升级审计在提交后记(与旧接线“引擎已记”的行同款,
# 只是时序从评估提交后挪到接线提交后,绝不出现
@@ -2310,13 +2360,27 @@ class Worker:
"%s reason=%s round=%s"
% (task_id, decision.get("reason"),
decision.get("round")))
try:
self._write_execution_result(
task_id, result_text=(result or {}).get("stdout", ""),
error_text=esc_error, actual_model=None,
started_at=None, finished_at=time.time(),
only_if_status="failed")
except Exception as exc:
# 审计批 6(M5):提交后的结果写回仍是独立写,
# 但瞬时失败不再直接认命——有界单次重试(仍带
# only_if_status="failed" 守卫,重试是幂等的:
# 行被并发重排后守卫挡住,不会覆盖新结果)。
# 两次都失败时保持旧行为:只记警告、不把已
# failed 的任务谎报成 error,DB error_text
# 停留旧文本的残留仍在,见报告。
_write_exc = None
for _attempt in range(2):
try:
self._write_execution_result(
task_id,
result_text=(result or {}).get("stdout", ""),
error_text=esc_error, actual_model=None,
started_at=None, finished_at=time.time(),
only_if_status="failed")
_write_exc = None
break
except Exception as exc:
_write_exc = exc
if _write_exc is not None:
# 结果文本写回是提交后的独立写:失败只记警告,
# 任务已是 failed,不再谎报成 error。
try:
@@ -2325,12 +2389,15 @@ class Worker:
"worker_audit_loop_escalate_result_failed",
task_id=task_id, project_id=project_id,
status="warning",
message="escalate result write failed: %s" % exc)
message="escalate result write failed: %s"
% _write_exc)
except Exception:
pass
return "failed", esc_error, "escalate"
conn.rollback()
return final, error, "error"
# 审计批 6(I2):未知决策同样回滚后读回行实际状态。
return self._wire_error_final(conn, task_id, final), \
error, "error"
except Exception as exc:
try:
conn.rollback()
@@ -2343,7 +2410,10 @@ class Worker:
message="audit loop wire store failed: %s" % exc)
except Exception:
pass
return final, error, "error"
# 审计批 6(I2):异常后回滚,行未被接线改动时读回
# 即输入终局本身;行已被第三方改动时以行为准。
return self._wire_error_final(conn, task_id, final), \
error, "error"
finally:
try:
conn.close()
@@ -7583,6 +7653,17 @@ class Worker:
except Exception:
pass
task = self.get_task(task_id) or task
# 审计批 6(B1,批 4 I-10 的 None 哨兵坍塌):run_one
# 入口立即初始化 _gates_checked 为空列表,使“本轮零
# 门跑过”([])与“旧直接调用”(None)可区分——否则
# 零门任务在终局块传 None 时 _audit_loop_gate_results
# 走旧行为分支,把没跑过的三门硬编码成通过(批 4
# 实测:显式零门任务两轮后 passed_gates 仍是三门)。
# 列表对象经后续 dict(task) 浅拷贝共享,标记点与终局
# 块读到同一份集合;协调员父任务不走单任务接线,
# 初始化对它无害(内存态,不进库、不进审计)。
if not isinstance(task.get("_gates_checked"), list):
task["_gates_checked"] = []
# G10:goal 断点续跑标记——认领后立即清掉库里标记(本轮
# 已认领为 running,不会重复触发;本轮若再被杀回
# interrupted,用户需重新调 /goal-resume 才会再置标记,
@@ -8005,6 +8086,9 @@ class Worker:
# tier/source/applied,评估时建议模型只能记
# unknown;新格式补 suggested/chosen,旧解析
# 器仍能读(见 cloud/routing_shadow.py)。
# 审计批 6(M2):chosen 必须是行尾字段——影子
# 解析器按行尾锚定 chosen,后续写点不得在其
# 后追加字段,否则会被吞进 chosen 而误解析。
self._audit(task["project_id"], "worker_routing",
"%s tier=%s source=%s applied=%s"
" suggested=%s chosen=%s" % (
@@ -8061,6 +8145,8 @@ class Worker:
# 行:评估函数只计 per-tier explicit_n,不计
# routed_n、不占 seen(见 routing_shadow.py),
# 父模型语义不变,档模型绝不覆盖它。
# 审计批 6(M2):同款行尾约定——chosen 必须是
# 行尾字段,后续写点不得在其后追加字段。
self._audit(task["project_id"], "worker_routing",
"%s tier=%s source=explicit applied=false"
" suggested=- chosen=%s" % (
@@ -8588,6 +8674,7 @@ class Worker:
# 本轮花过的钱照记,推回不等于没花钱。
audit_loop_decision = None
audit_loop_pushback = False
audit_loop_suppress = False
if (final_written and not quota_stop
and final in ("completed", "failed")
and task.get("audit_loop")
@@ -8596,6 +8683,17 @@ class Worker:
final, error, audit_loop_decision = \
self._audit_loop_wire(task, final, result, error)
audit_loop_pushback = (audit_loop_decision == "pushback")
# 审计批 6(I2):接线 error 且行实际状态已不是
# 终态(例如 CAS 未命中后行回 queued)时,终态
# 审计/时间线/通知必须随之抑制——按输入终局记
# worker_failed 并发 failed 通知会与行状态自相
# 矛盾。行仍是终态(回滚情形:failed/completed)
# 或已被取消(interrupted,按 interrupted 终局
# 处理、不发 failed 通知)时不抑制。
audit_loop_suppress = (
audit_loop_decision == "error"
and final not in ("completed", "failed",
"interrupted"))
except Exception as exc:
try:
log_event(self.logger, "worker_audit_loop_wire_failed",
@@ -8615,7 +8713,8 @@ class Worker:
# 持锁写,否则两个线程共用同一 sqlite 连接会直接报错。
cost_id = self._record_usage_cost(
task, actual_model, usage_total, stdout, fallback_count)
if final_written and not audit_loop_pushback:
if final_written and not audit_loop_pushback \
and not audit_loop_suppress:
# 审计名须与终态一致:interrupted 任务记 worker_failed
# 会把“可续跑的中断”误读成失败,误导后续排查。
# 终态审计细节(2026-10-29 任务 A 教训):interrupted /
@@ -8668,11 +8767,14 @@ class Worker:
elif final in ("completed", "failed"):
self._notify_task_finished(
task_id, task["project_id"], final, error=error)
elif audit_loop_pushback:
elif audit_loop_pushback or audit_loop_suppress:
# 推回已由接线处理(状态 queued、包列已写、推回审计
# 已记):这里不跑终态审计、不改 final、不发通知。
# 注意不能落入下面的 else——else 会把 final 改成
# interrupted,与库里的 queued 自相矛盾。
# 审计批 6(I2):接线 error 且行已不是终态时同款
# 抑制——这里同样不跑终态审计、不改 final、不发
# 通知,final 已是接线读回的行实际状态,返回体
# 与行一致。注意不能落入下面的 else——else 会把
# final 改成 interrupted,与库里的 queued 自相矛盾。
pass
else:
if goal_converted: