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
+22
-4
@@ -61,6 +61,7 @@ from cloud.agent_def import default_agents_dir as _agent_default_dir
|
||||
from cloud.agent_def import list_known_skills as _agent_known_skills
|
||||
from cloud.agent_def import parse_agent_definition as _parse_agent_definition
|
||||
from cloud.agent_def import resolve_registry_agent as _resolve_agent
|
||||
from cloud.attempts import ensure_attempts_table as _ensure_attempts
|
||||
from cloud.auth import ROLES as _MEMBER_ROLES
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.coordinator import parse_parts as _coord_parse_parts
|
||||
@@ -1717,6 +1718,7 @@ def init_db(db_path):
|
||||
_ensure_uploads_tables(conn)
|
||||
_ensure_task_checkpoints_table(conn)
|
||||
_ensure_task_steps_table(conn)
|
||||
_ensure_attempts(conn)
|
||||
conn.commit()
|
||||
return
|
||||
conn.execute(
|
||||
@@ -1753,6 +1755,7 @@ def init_db(db_path):
|
||||
_ensure_uploads_tables(conn)
|
||||
_ensure_task_checkpoints_table(conn)
|
||||
_ensure_task_steps_table(conn)
|
||||
_ensure_attempts(conn)
|
||||
ensure_kill_requests_table(conn)
|
||||
# 常驻监听表(P0 S3 的 SQLite 侧):启动时建一次,
|
||||
# 热路径(claim/run_due/批量)不再逐条 ensure,
|
||||
@@ -5390,10 +5393,12 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
"followups": [_followup_to_dict(r) for r in rows]})
|
||||
|
||||
def _task_timeline(self, user_id, task_id):
|
||||
"""GET /tasks/{id}/timeline(P0 三件其三):一个任务一张
|
||||
时间线。steps 直读 task_steps(claim/resume/round/final),
|
||||
checkpoints 直读 task_checkpoints(每轮断点快照);旧
|
||||
任务(部署前跑完、无步骤记录)返回空数组,不编造。
|
||||
"""GET /tasks/{id}/timeline(P0 三件其三 + v6 尝试账本):
|
||||
一个任务一张时间线。steps 直读 task_steps
|
||||
(claim/resume/round/final),checkpoints 直读
|
||||
task_checkpoints(每轮断点快照),attempts 直读
|
||||
task_attempts(自动续跑前记下的正结束尝试);旧任务
|
||||
(部署前跑完、无步骤记录)返回空数组,不编造。
|
||||
"""
|
||||
row = self._fetch_task(task_id)
|
||||
if row is None:
|
||||
@@ -5409,6 +5414,7 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
_ensure_task_checkpoints_table(conn)
|
||||
_ensure_task_steps_table(conn)
|
||||
_ensure_attempts(conn)
|
||||
step_rows = conn.execute(
|
||||
"SELECT step_no, kind, summary, state_sha, created_at"
|
||||
" FROM task_steps WHERE task_id = ?"
|
||||
@@ -5419,6 +5425,12 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
" verify_out, created_at FROM task_checkpoints"
|
||||
" WHERE task_id = ? ORDER BY round_no",
|
||||
(task_id,)).fetchall()
|
||||
attempt_rows = conn.execute(
|
||||
"SELECT attempt_no, kind, interrupted_reason,"
|
||||
" checkpoint_round, result_text, error_text, created_at"
|
||||
" FROM task_attempts WHERE task_id = ?"
|
||||
" ORDER BY attempt_no",
|
||||
(task_id,)).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
self._send_json(200, {
|
||||
@@ -5430,6 +5442,12 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
"goal_status": r[2], "state_sha": r[3],
|
||||
"verify_out": r[4], "created_at": r[5]}
|
||||
for r in cp_rows],
|
||||
"attempts": [{"attempt_no": r[0], "kind": r[1],
|
||||
"interrupted_reason": r[2],
|
||||
"checkpoint_round": r[3],
|
||||
"result_text": r[4], "error_text": r[5],
|
||||
"created_at": r[6]}
|
||||
for r in attempt_rows],
|
||||
})
|
||||
|
||||
def _task_files(self, user_id, task_id, query):
|
||||
|
||||
Reference in new issue
Block a user