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:
1 parent
c15c224570
commit
19fd326a73
6 files changed
+541
-5
No files matched your search
+73
-1
@@ -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 四个
|
||||
|
||||
Reference in new issue
Block a user