feat: side-thread task kind (Muse role alignment phase 2)
ci / test (push) Successful in 25m2s

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:
host committed 2026-11-07 20:51:23 +00:00
1 parent f6df76ee8f
commit 7983567491
24 files changed
+1244 -100

No files matched your search

+12
View File
@@ -2588,6 +2588,13 @@ def dispatch(server, state, name, prompt, repo, model, effort, sandbox,
branch = None
if use_worktree and repo:
cwd, branch = make_worktree(repo, name)
if not use_worktree and not repo:
# 旁聊无 repo:create_thread 不接受 cwd=None
# (os.path.abspath(None) 抛错),以 per-task scratch
# 目录为 cwd(普通目录,非 git worktree,不建分支;
# host 任务名即任务 id 的安全化形式,目录按任务隔离)。
cwd = os.path.join(HERE, "work", "side-threads", name)
os.makedirs(cwd, exist_ok=True)
if seed_dir:
# 附件只能进 worktree:直接以 repo 为 cwd 时拷进去等于污染
# 仓库,必须 loud fail,绝不静默放行。
@@ -3259,6 +3266,10 @@ def main():
p.add_argument("--repo", default=None)
p.add_argument("--seed-dir", default=None,
help="附件暂存目录:派发时拷进 worktree 再跑 turn")
p.add_argument("--no-worktree", action="store_true",
help="不建 worktree/分支:无 repo 时以 per-task"
" scratch 目录为 cwd,有 repo 时以 repo 为"
" cwd(旁聊默认,见 dispatch 同款)")
p.add_argument("--verify", default=None,
help="验收命令;不传时走模型自决:模型在回合末尾"
"单独一行输出“目标已达成”即达标")
@@ -4208,6 +4219,7 @@ def main():
task = dispatch(srv, state, args.name, args.task, args.repo,
args.model, args.effort, args.sandbox,
args.approval_policy,
use_worktree=not args.no_worktree,
seed_dir=args.seed_dir)
save_state(args.state, state)
# 新开循环的 spawn goal 是权威的:上一轮同名循环