Tasks gain kind='main'|'side' (NULL=main). A side thread hangs under a parent task, defaults to the local executor, never builds a worktree, branch or container unless explicitly asked, and writes its conclusion back to the parent via task_followups on terminal transition. - server: kind validation + side-thread gates (parent required, no coordinator_parts/upload_id/environment_id, parent must not be a coordinator), local executor default, GET /tasks kind filter, /worktree 404 and /fork /merge /pr 409 for side threads. - worker: side-default shunt in _run_attempt (no-worktree scratch cwd, in-memory plan=1 when a repo is present, seed/checkpoint skipped), _writeback_side_thread, marker task-create kind field + inheritance, side threads excluded from marker spawn counts. - host/sandbox: --no-worktree plumbing for dispatch/goal/popen (Local + Docker), scratch cwd work/side-threads/<task-id>. - console (classic only): kind filter + [旁聊] rows + '开旁聊' entry, side-thread dispatch mode, side-thread list in detail, merge/PR/fork hidden for side threads; build exit 0, console.html sha df39db5a. - tests: new test_sidethread.py (29 tests) + kind backfill tests in test_p2_migrations.py; full cloud suite 3900 tests OK (skipped=13). Design: verification/codex-muse-role-alignment-design-20261108.md Summary: verification/codex-muse-role-align-p1p2-20261108.md Local only: not pushed, not merged, not deployed.
This commit is contained in:
1 parent
f6df76ee8f
commit
7983567491
24 files changed
+1244
-100
No files matched your search
+110
-8
@@ -529,6 +529,9 @@ def _task_from_row(row):
|
||||
"coordinator_parts": _coord_parse_parts(_col("coordinator_parts")),
|
||||
"parent_task_id": _col("parent_task_id"),
|
||||
"coordinator_part": _col("coordinator_part"),
|
||||
# 旁聊(Muse 角色对齐 P2):老行缺列/NULL 时按 "main"
|
||||
# 计(与 server 侧 _task_to_dict 同款惯例)。
|
||||
"kind": _col("kind") or "main",
|
||||
# agent 定义 v2:老行缺列时 None = 未用定义,不炸。
|
||||
"agent_name": _col("agent_name"),
|
||||
"agent_version": _col("agent_version"),
|
||||
@@ -3701,6 +3704,7 @@ class Worker:
|
||||
|
||||
def _execute_via_process(self, task, kind="dispatch",
|
||||
deadline_seconds=None, seed_host_dir=None,
|
||||
no_worktree=False,
|
||||
quota_check=None):
|
||||
"""进程模式执行一轮:启动、轮询、取消时杀进程组。
|
||||
|
||||
@@ -3724,10 +3728,12 @@ class Worker:
|
||||
else:
|
||||
# seed 只对 dispatch/goal 首轮有意义:run 续跑不传
|
||||
# (文件已在 worktree 里);popen_task 内部同样只在
|
||||
# dispatch/goal 命令上追加 --seed-dir。
|
||||
# dispatch/goal 命令上追加 --seed-dir。no_worktree
|
||||
# 同款只对 dispatch/goal 命令追加 --no-worktree。
|
||||
proc = self.executor.popen_task(
|
||||
task, kind=kind, seed_dir=seed_host_dir,
|
||||
extra_env=gateway_env, model_provider=gateway_provider)
|
||||
extra_env=gateway_env, model_provider=gateway_provider,
|
||||
no_worktree=no_worktree)
|
||||
self._running_set(task_id, proc=proc)
|
||||
try:
|
||||
self._set_child_pid(task_id, getattr(proc, "pid", None))
|
||||
@@ -4592,6 +4598,50 @@ class Worker:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _writeback_side_thread(self, task, final, text):
|
||||
"""旁聊终态回写父级(P2 W1):旁聊终局后写一条
|
||||
task_followups 到父任务(sender_task_id=旁聊 id,
|
||||
mode='queued',status='pending')。父 running 时排空
|
||||
循环自动消费(带 _peer_prefix 前缀);父 queued 时
|
||||
下次认领后首轮排空消费;父终态时行只作记录,
|
||||
GET /tasks/{id}/followups 与 chat-history 可见,
|
||||
不复活父任务。正文截 2000 字(与协调员合流报告的
|
||||
单分工上限同款)。回写是尽力而为:失败返回 False,
|
||||
不改终局、不重试、不炸执行。"""
|
||||
parent_id = task.get("parent_task_id")
|
||||
if not parent_id:
|
||||
return False
|
||||
text = (text or "").strip()
|
||||
if final == "completed":
|
||||
head, body = "[旁聊结论]", text or "(旁聊无结论文本)"
|
||||
elif final == "failed":
|
||||
head, body = "[旁聊失败]", text or "(无错误文本)"
|
||||
else:
|
||||
head, body = "[旁聊中断]", text or "(无文本)"
|
||||
full = "%s %s" % (head, body)
|
||||
if len(full) > 2000:
|
||||
full = full[:2000] + "…(已截断)"
|
||||
try:
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
_ensure_followups_table(conn)
|
||||
conn.execute(
|
||||
"INSERT INTO task_followups(id, task_id, project_id,"
|
||||
" body, mode, status, created_by, sender_task_id,"
|
||||
" created_at, updated_at)"
|
||||
" VALUES(?, ?, ?, ?, 'queued', 'pending', ?, ?,"
|
||||
" ?, ?)",
|
||||
(uuid.uuid4().hex, parent_id,
|
||||
task["project_id"], full, task.get("created_by"),
|
||||
task["id"], time.time(), time.time()),
|
||||
)
|
||||
conn.commit()
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _poll_task_snapshot(self, task, poll):
|
||||
"""轮询一个目标任务:每 interval 查一次库状态,直到终态、
|
||||
超时或父任务被取消。等待期间不烧模型费,但占着本 worker
|
||||
@@ -4889,17 +4939,26 @@ class Worker:
|
||||
child_effort = (create["effort"]
|
||||
or task.get("child_effort")
|
||||
or task.get("effort"))
|
||||
# 旁聊继承(P2 G7):旁聊内 marker 派生
|
||||
# 的子任务默认继承父 kind='side'(除非
|
||||
# 块里显式指定 kind),否则子任务会以
|
||||
# 主聊身份建 worktree/分支,旁聊的无
|
||||
# worktree 承诺被绕过;执行器已按
|
||||
# create["executor"] or 父 executor 继承。
|
||||
child_kind = create.get("kind") or (
|
||||
"side" if task.get("kind") == "side"
|
||||
else None)
|
||||
conn.execute(
|
||||
"INSERT INTO tasks(id, project_id, prompt,"
|
||||
" status, idempotency_key, depends_on,"
|
||||
" priority, created_by, created_at,"
|
||||
" updated_at, last_heartbeat, executor,"
|
||||
" model, effort, environment_id,"
|
||||
" parent_task_id, coordinator_part,"
|
||||
" parent_task_id, coordinator_part, kind,"
|
||||
" child_model, child_effort)"
|
||||
" VALUES(?, ?, ?, 'queued', NULL, '[]',"
|
||||
" ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, NULL,"
|
||||
" ?, ?)",
|
||||
" ?, ?, ?)",
|
||||
(child_id, task["project_id"],
|
||||
create["prompt"],
|
||||
task.get("priority", 0) or 0,
|
||||
@@ -4910,6 +4969,7 @@ class Worker:
|
||||
child_effort,
|
||||
task.get("environment_id"),
|
||||
task["id"],
|
||||
child_kind,
|
||||
task.get("child_model"),
|
||||
task.get("child_effort")),
|
||||
)
|
||||
@@ -5624,6 +5684,22 @@ class Worker:
|
||||
task_id = task["id"]
|
||||
result = None
|
||||
error = None
|
||||
# 旁聊默认执行(P2 G4):kind='side' 且执行器是 local
|
||||
# (旁聊默认,未显式要求 docker/cloud)时不建 worktree/
|
||||
# 分支;显式 docker/cloud 走既有语义。显式 local 与
|
||||
# 默认 local 在行上不可区分(都是 local),一律走
|
||||
# 无 worktree 路径。
|
||||
_side_default = task.get("kind") == "side" and \
|
||||
(task.get("executor") or "local") == "local"
|
||||
if _side_default and (task.get("repo") or
|
||||
getattr(self.executor, "repo", None)):
|
||||
# 有 repo 的旁聊:cwd 就是 repo,强制 plan=1
|
||||
# (只读沙箱),不得把 repo 写脏;plan 经
|
||||
# dispatch --sandbox read-only 透传到 codex。
|
||||
# 只改内存里的 task dict,不写库(plan 列是任务
|
||||
# 创建时的属性,旁聊的只读是执行层强制)。
|
||||
task = dict(task)
|
||||
task["plan"] = 1
|
||||
# goal 模式:goal_mode=1(新语义,prompt 即目标)或带
|
||||
# verify_command(旧语义)时走 host.py goal 循环,不走
|
||||
# 普通 dispatch。goal 循环内部已多轮重试,worker 级
|
||||
@@ -5633,7 +5709,10 @@ class Worker:
|
||||
# 附件 seed:dispatch/goal 首轮才用;run 续跑不传(文件已
|
||||
# 在 worktree 里)。cloud 执行器没有 worktree 可 seed,
|
||||
# 创建时本应已拒绝,这里是兜底防线(直接失败,不静默跑)。
|
||||
seed_host_dir = self._task_seed_dir(task)
|
||||
# 旁聊默认执行不 seed(创建时已拒 upload_id,附件行恒为空,
|
||||
# 这里跳过 _task_seed_dir 的一次查库)。
|
||||
seed_host_dir = None if _side_default \
|
||||
else self._task_seed_dir(task)
|
||||
if seed_host_dir and isinstance(self.executor, CloudExecutor):
|
||||
return ({"executor": "cloud", "task_id": task_id,
|
||||
"status": "failed", "stdout": "",
|
||||
@@ -5678,9 +5757,11 @@ class Worker:
|
||||
_quota_watchdog_thread = None
|
||||
# P0 三件其一:goal 任务起断点镜像线程(任何执行器);
|
||||
# 非 goal 任务没有断点语义,不起。终局镜像在 finally 补。
|
||||
# 旁聊默认执行不起镜像(旁聊的恢复走 thread 续跑,不走
|
||||
# 断点镜像;旁聊 goal 同款)。
|
||||
_mirror_stop = None
|
||||
_mirror_thread = None
|
||||
if goal_mode:
|
||||
if goal_mode and not _side_default:
|
||||
try:
|
||||
_mirror_stop = threading.Event()
|
||||
_mirror_thread = threading.Thread(
|
||||
@@ -5765,6 +5846,7 @@ class Worker:
|
||||
task, kind=popen_kind,
|
||||
deadline_seconds=_effective_task_timeout,
|
||||
seed_host_dir=seed_host_dir,
|
||||
no_worktree=_side_default,
|
||||
quota_check=quota_check)
|
||||
combined = (result.get("stdout", "") or "") + \
|
||||
(result.get("stderr", "") or "")
|
||||
@@ -5899,7 +5981,8 @@ class Worker:
|
||||
result = self.executor.goal_task(
|
||||
task, seed_dir=seed_host_dir,
|
||||
extra_env=gateway_env,
|
||||
model_provider=gateway_provider)
|
||||
model_provider=gateway_provider,
|
||||
no_worktree=_side_default)
|
||||
else:
|
||||
result = self.executor.goal_task(task)
|
||||
else:
|
||||
@@ -5907,7 +5990,8 @@ class Worker:
|
||||
result = self.executor.dispatch_task(
|
||||
task, seed_dir=seed_host_dir,
|
||||
extra_env=gateway_env,
|
||||
model_provider=gateway_provider)
|
||||
model_provider=gateway_provider,
|
||||
no_worktree=_side_default)
|
||||
else:
|
||||
result = self.executor.dispatch_task(task)
|
||||
combined = (result.get("stdout", "") or "") + \
|
||||
@@ -8968,6 +9052,12 @@ class Worker:
|
||||
# 只留档);不计数派发(平台/协调员)时为空操作。
|
||||
self._record_ledger_attempt(task_id, "interrupted",
|
||||
_ledger_counted)
|
||||
# 旁聊终态回写(P2 W1):取消也是终态,父级应收到
|
||||
# 中断说明;回写失败不挡取消返回。
|
||||
if task.get("kind") == "side":
|
||||
self._writeback_side_thread(
|
||||
task, "interrupted",
|
||||
(result or {}).get("stdout", "") or "cancelled")
|
||||
return {"dispatched": True, "task_id": task_id,
|
||||
"host_name": self.executor._host_name(task)
|
||||
if hasattr(self.executor, "_host_name") else task_id,
|
||||
@@ -9083,6 +9173,18 @@ class Worker:
|
||||
message="audit loop wire failed: %s" % exc)
|
||||
except Exception:
|
||||
pass
|
||||
# 旁聊终态回写(P2 W1):只在终局真正写回且未被审计
|
||||
# 推回(回 queued 不是终态)时写一条 task_followups
|
||||
# 到父任务;父 running 时排空消费,父 queued 时下次
|
||||
# 认领后首轮排空消费,父终态时只作记录、不复活父。
|
||||
# final 已是审计接线后的终局(含 completed/failed/
|
||||
# interrupted;配额中断的 final 也是 interrupted)。
|
||||
if (task.get("kind") == "side" and final_written
|
||||
and not audit_loop_pushback
|
||||
and final in ("completed", "failed", "interrupted")):
|
||||
self._writeback_side_thread(
|
||||
task, final,
|
||||
stdout if final == "completed" else (error or ""))
|
||||
# 附件暂存清理:任务终局后删 bundle 目录(重试走已存在
|
||||
# 续跑,文件已在 worktree 里,不再需要暂存)。
|
||||
self._cleanup_task_seed(task)
|
||||
|
||||
Reference in new issue
Block a user