feat: attempt ledger + resume preamble (v6)

Foundry 'platform restarts with original input' gap, after
reading the existing mechanisms: creation idempotency
(UNIQUE project_id+idempotency_key), goal checkpoints
(task_checkpoints per round, same-thread breakpoint resume
with no-progress fuse), merge/cost/notification CAS, and
coordinator child reuse already exist. The uncovered segment
is non-goal platform-interrupted tasks: auto_resume requeues
them fresh in the same worktree after clearing result/error/
interrupted_reason, so the new run gets only the original
prompt and cannot tell what the previous attempt had done.

This track adds, in worker/server layer only (no host.py or
codex thread change): task_attempts ledger, recorded by
auto_resume_interrupted in the same transaction before
clearing (attempt_no = old auto_resume_count + 1, kind
goal_breakpoint|platform_fresh, interrupted_reason,
checkpoint_round, result/error truncated to 2000 chars,
INSERT OR IGNORE keeps the first record on duplicate);
resume preamble injected in-memory only at claim for
platform_fresh reruns (previous attempt facts, truncated to
1000 chars, plus check-worktree-first and verify-before-
recreating-external-artifacts instructions; skipped for
coordinator/_resume/breakpoint tasks and when no record
exists, never fabricated); attempts surfaced read-only in
GET /tasks/{id}/timeline under existing project-read
permission. Fresh PG schema, migrations table list, and
server/worker ensure paths share one DDL.

Deliberately NOT claimed: the preamble is an instruction,
not a hard guarantee like creation idempotency or merge
CAS; the ledger does not track worktree files or external
artifacts (no worktree snapshot, executor-dependent paths;
hard dedup for those still rests on their existing gates);
manual retry paths do not go through auto_resume, so they
get no ledger record or preamble. Tests:
test_attempts.py (7 tests).
This commit is contained in:
host committed 2026-11-04 18:02:10 +00:00
1 parent c15c224570
commit 19fd326a73
6 files changed
+541 -5

No files matched your search

+73 -1
View File
@@ -137,6 +137,10 @@ from types import SimpleNamespace
from cloud.agent_def import AgentDefinition as _AgentDefinition
from cloud.agent_def import format_instructions_header as _agent_header
from cloud.agent_def import missing_required_tools as _missing_tools
from cloud.attempts import build_resume_preamble as _resume_preamble
from cloud.attempts import ensure_attempts_table as _ensure_attempts
from cloud.attempts import latest_attempt as _latest_attempt
from cloud.attempts import record_attempt as _record_attempt
from cloud.auth import AuthStore
from cloud.coordinator import build_report as _coord_build_report
from cloud.coordinator import DEFAULT_POLL_INTERVAL as _COORD_POLL_INTERVAL
@@ -1025,7 +1029,9 @@ class Worker:
" model, interrupted_reason, resume_after,"
" auto_resume_stall_count,"
" checkpoint_round_at_last_resume,"
" goal_mode, verify_command, goal FROM tasks"
" goal_mode, verify_command, goal,"
" result_text, error_text, auto_resume_count"
" FROM tasks"
" WHERE status = 'interrupted'"
" AND (paused_by_user IS NULL OR paused_by_user = 0)"
" AND (interrupted_reason IS NULL OR"
@@ -1093,6 +1099,24 @@ class Worker:
# 消息,防它劫持新跑的 prompt。
resume_set = ("goal_resume_pending = 0,"
" resume_message = NULL")
# 尝试账本(v6):清掉旧结果之前,先把正结束
# 的这次尝试记下来;记账与回 queued 在同一
# 事务里提交。attempt_no = 旧 auto_resume_count
# + 1,即正结束尝试的序号;kind 按 goal 与否
# 分断点续跑与平台全新重排。记账失败不单独
# 炸恢复循环——外层已兜底,但本轮仍继续回
# queued(账本缺一条记录好过任务卡住不恢复;
# 认领时查不到账本即不贴前言,不编造)。
try:
_ensure_attempts(conn)
_record_attempt(
conn, task_id,
(row[14] or 0) + 1,
"goal_breakpoint" if is_goal
else "platform_fresh",
row[5], cp_round, row[12], row[13])
except Exception:
pass
cur = conn.execute(
"UPDATE tasks SET status = 'queued', "
+ resume_set +
@@ -1528,6 +1552,12 @@ class Worker:
"summary TEXT, "
"state_sha TEXT, "
"created_at DOUBLE PRECISION)")
# 尝试账本(v6,见 cloud/attempts.py):worker 直连
# 老库时也补,与 server ensure 同 DDL。
try:
_ensure_attempts(conn)
except Exception:
pass
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_task_steps_task"
" ON task_steps(task_id, step_no)")
@@ -1920,6 +1950,45 @@ class Worker:
task["id"])
return new_task
def _apply_resume_preamble(self, task):
"""重跑前言注入(v6,见 cloud/attempts.py):只改内存副本。
跳过:协调员任务、_resume 任务、断点续跑任务(已有
会话连续性)、auto_resume_count 为 0 的首轮任务、账本
里查不到记录的老任务、最新尝试不是 platform_fresh
的任务(goal 断点续跑走线程连续性,不贴前言)。
贴法与项目注入同款:前言 + 原 prompt,goal 任务若
意外走到这里也同步贴 goal(执行器取 goal 优先于
prompt);查账本失败时原样返回,不炸认领。
"""
if task.get("coordinator_parts"):
return task
if task.get("_resume") or task.get("_goal_resume_breakpoint"):
return task
if not task.get("auto_resume_count"):
return task
try:
conn = _db_connect(self.db_path)
try:
attempt = _latest_attempt(conn, task["id"])
finally:
conn.close()
except Exception:
return task
preamble = _resume_preamble(attempt)
if not preamble:
return task
new_task = dict(task)
new_task["prompt"] = preamble + "\n\n" + (
task.get("prompt") or "")
if task.get("goal_mode") or task.get("verify_command"):
new_task["goal"] = preamble + "\n\n" + (
task.get("goal") or task.get("prompt") or "")
new_task["_resume_preamble_injected"] = True
self._audit(task["project_id"], "worker_resume_preamble_injected",
task["id"])
return new_task
def _tree_snapshot_source(self, task):
"""首轮目录树快照的宿主侧来源目录,或 None。
@@ -5742,6 +5811,9 @@ class Worker:
# _apply_project_injection 的跳过规则;只改内存副本;
# 最后贴,排在待办与记忆之前(最通用上下文在最前)。
task = self._apply_project_injection(task)
# 重跑前言:平台全新重排的任务在项目注入之后贴,离原
# prompt 最近;只改内存副本,原 prompt 不落库。
task = self._apply_resume_preamble(task)
self._audit(task["project_id"], "worker_claim",
"%s executor=%s" % (task_id, self._executor_name()))
# log_event 只接受 task_id/project_id/status/message 四个