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

+27 -9
View File
@@ -346,8 +346,15 @@ def extract_task_creates(text):
executor = payload.get("executor")
if executor is not None and executor not in EXECUTORS:
continue
# 旁聊(P2 G7):块里可显式指定 kind("main"|"side"),
# 非法值整块跳过(与 executor 非法同款);不指定时
# worker 按父任务 kind 继承(旁聊父 → 旁聊子)。
kind = payload.get("kind")
if kind is not None and kind not in ("main", "side"):
continue
out.append({"name": name, "prompt": prompt, "model": model,
"effort": effort, "executor": executor})
"effort": effort, "executor": executor,
"kind": kind})
return out
@@ -1011,18 +1018,20 @@ def parent_spawn_generation(conn, parent_id):
只数 parent_task_id 非空且 coordinator_part 为空的祖先链(即
派生链);协调员子任务(coordinator_part 非空)本身不是派生
任务,代数为 0,链到此为止。走 32 步仍未到头时按已数到的算,
绝不死循环。"""
任务,代数为 0,链到此为止;旁聊任务(kind='side')同款——
旁聊不是 marker 派生,链到旁聊为止。走 32 步仍未到头时按已数
到的算,绝不死循环。"""
generation = 0
cur = parent_id
for _ in range(32):
if not cur:
break
row = conn.execute(
"SELECT parent_task_id, coordinator_part FROM tasks"
" WHERE id = ?", (cur,)).fetchone()
"SELECT * FROM tasks WHERE id = ?", (cur,)).fetchone()
if row is None:
break
if row_get(row, "kind") == "side":
break
parent = row_get(row, "parent_task_id")
part = row_get(row, "coordinator_part")
if parent is not None and part is None:
@@ -1034,10 +1043,19 @@ def parent_spawn_generation(conn, parent_id):
def count_direct_spawn_children(conn, parent_id):
"""单个父任务的直接派生子任务数(不含协调员子任务)。"""
row = conn.execute(
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ?"
" AND coordinator_part IS NULL", (parent_id,)).fetchone()
"""单个父任务的直接派生子任务数(不含协调员子任务与旁聊)。"""
try:
row = conn.execute(
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ?"
" AND coordinator_part IS NULL"
" AND (kind IS NULL OR kind != 'side')",
(parent_id,)).fetchone()
except Exception:
# 老库/最小测试表缺 kind 列:当时不可能有旁聊行,
# 不带 kind 条件的结果与带条件等价。
row = conn.execute(
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ?"
" AND coordinator_part IS NULL", (parent_id,)).fetchone()
try:
return int(row[0]) if row is not None else 0
except (TypeError, ValueError, IndexError):