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
+105
-5
@@ -1234,6 +1234,11 @@ def migrate_tasks_table(conn):
|
||||
"coordinator_parts": "TEXT",
|
||||
"parent_task_id": "TEXT",
|
||||
"coordinator_part": "TEXT",
|
||||
"kind": "TEXT",
|
||||
# 旁聊(Muse 角色对齐 P2):kind 记任务类型
|
||||
# ('main' | 'side',NULL = 老行 = 主聊,与 goal_mode
|
||||
# NULL=0、plan NULL=0 同款惯例);parent_task_id
|
||||
# 的语义从“协调员/派生父任务”扩为“父会话”。
|
||||
# G10:goal 断点续跑待处理标记(1 = 已调 /goal-resume、
|
||||
# 待 worker 按断点续跑;worker 认领后清掉,跑完不再
|
||||
# 重复触发)。与 resume_message 不同:后者是“给旧会
|
||||
@@ -1725,6 +1730,9 @@ def init_db(db_path):
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_status"
|
||||
" ON tasks(status)")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_parent"
|
||||
" ON tasks(parent_task_id)")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -2320,6 +2328,20 @@ def _goal_signal_status(status):
|
||||
return "pending_mirror_to_host_loop"
|
||||
|
||||
|
||||
def _filter_tasks_by_kind(rows, kind_f):
|
||||
"""旁聊(Muse 角色对齐 P2):GET /tasks 的 kind 过滤。
|
||||
|
||||
kind_f 为空时原样返回(缺省 = 全部,老行为);否则只留
|
||||
`_task_to_dict` 的 kind 等于 kind_f 的行。抽成独立函数,
|
||||
不让 `_list_tasks` 再长(长方法清单 pin 在 18 个,见
|
||||
test_longfunc_inventory)。
|
||||
"""
|
||||
if not kind_f:
|
||||
return rows
|
||||
return [r for r in rows
|
||||
if _task_to_dict(r, [], 0)["kind"] == kind_f]
|
||||
|
||||
|
||||
class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, AutomationMixin, FullAutoMixin,
|
||||
BaseHTTPRequestHandler):
|
||||
"""控制面请求处理:除 /health 与 /webhooks/* 外一律要求 X-API-Key。"""
|
||||
@@ -3837,6 +3859,44 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
"error": "coordinator task cannot be created"
|
||||
" under a coordinator task"})
|
||||
return
|
||||
# 旁聊(Muse 角色对齐 P2):kind 只认 "main" | "side",
|
||||
# 缺省 = 主聊;旁聊须带 parent_task_id(已在上段校验
|
||||
# 存在与同项目),不得带 coordinator_parts(协调员
|
||||
# 子任务与旁聊是两种父子关系),不得带 upload_id /
|
||||
# environment_id(附件与环境语义都要求 worktree,
|
||||
# 旁聊默认没有);父任务不得是协调员任务(协调员
|
||||
# 本身不跑模型回合,旁聊挂上去没有父会话语义,且
|
||||
# 会污染协调员的子任务同步快照)。
|
||||
kind = body.get("kind")
|
||||
if kind is None:
|
||||
kind = "main"
|
||||
if kind not in ("main", "side"):
|
||||
self._send_json(400, {
|
||||
"error": "kind must be 'main' or 'side'"})
|
||||
return
|
||||
if kind == "side":
|
||||
if parent_task_id is None:
|
||||
self._send_json(400, {
|
||||
"error": "side thread requires parent_task_id"})
|
||||
return
|
||||
if coord_parts:
|
||||
self._send_json(400, {
|
||||
"error": "side thread cannot carry coordinator_parts"})
|
||||
return
|
||||
if parent_task is not None and parent_task.get(
|
||||
"coordinator_parts"):
|
||||
self._send_json(400, {
|
||||
"error": "side thread parent cannot be a"
|
||||
" coordinator task"})
|
||||
return
|
||||
if body.get("upload_id") is not None:
|
||||
self._send_json(400, {
|
||||
"error": "side thread cannot carry upload_id"})
|
||||
return
|
||||
if body.get("environment_id") is not None:
|
||||
self._send_json(400, {
|
||||
"error": "side thread cannot carry environment_id"})
|
||||
return
|
||||
idempotency_key = body.get("idempotency_key")
|
||||
if idempotency_key is not None and not isinstance(idempotency_key, str):
|
||||
self._send_json(400, {"error": "idempotency_key must be a string"})
|
||||
@@ -3918,7 +3978,10 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
# 未指定且无环境继承时默认 docker:空 executor(任意
|
||||
# worker 可认领)会让任务落到隔离较弱的 local worker,
|
||||
# 是多用户下的隔离缺口;显式 local/cloud 不受影响。
|
||||
executor = _default_executor()
|
||||
# 旁聊例外(P2 G1):旁聊是会话式使用,默认 local、
|
||||
# 不拉容器;docker/cloud 仅显式要求时才走。
|
||||
executor = "local" if kind == "side" \
|
||||
else _default_executor()
|
||||
# 附件绑定(upload_id 由 POST /uploads 发出):包必须存在、
|
||||
# 归属当前用户、未使用、24 小时内、暂存目录还在;cloud
|
||||
# 执行器没有 worktree 可 seed,直接拒绝。校验在 INSERT
|
||||
@@ -4044,19 +4107,19 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
" goal, verify_command, goal_max_rounds,"
|
||||
" model, effort, approval, plan, goal_mode,"
|
||||
" environment_id, coordinator_parts,"
|
||||
" parent_task_id, coordinator_part,"
|
||||
" parent_task_id, coordinator_part, kind,"
|
||||
" child_model, child_effort,"
|
||||
" agent_name, agent_version, agent_instructions,"
|
||||
" agent_tools, agent_tool_approvals, audit_loop)"
|
||||
" VALUES(?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, NULL, ?,"
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(task_id, project_id, prompt, idempotency_key,
|
||||
json.dumps(depends_on), priority, user_id, now, now,
|
||||
executor, goal, verify_command, goal_max_rounds,
|
||||
model, effort, approval, plan, goal_mode,
|
||||
environment_id,
|
||||
json.dumps(coord_parts) if coord_parts else None,
|
||||
parent_task_id, None, child_model, child_effort,
|
||||
parent_task_id, None, kind, child_model, child_effort,
|
||||
agent_name_val, agent_version_val,
|
||||
agent_instructions_val, agent_tools_val,
|
||||
agent_tool_approvals_val, audit_loop_val),
|
||||
@@ -4145,6 +4208,13 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
parent_ids = query.get("parent_task_id")
|
||||
if parent_ids:
|
||||
parent_task_id = parent_ids[0]
|
||||
# 旁聊(Muse 角色对齐 P2):kind=main|side 可选,缺省 =
|
||||
# 全部(老行为不变);非法值 400。
|
||||
kind_f = query["kind"][0] if query.get("kind") else None
|
||||
if kind_f is not None and kind_f not in ("main", "side"):
|
||||
self._send_json(400, {
|
||||
"error": "kind must be 'main' or 'side'"})
|
||||
return
|
||||
limit = None
|
||||
offset = 0
|
||||
if query.get("limit"):
|
||||
@@ -4181,13 +4251,15 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
s = r["status"]
|
||||
status_counts[s] = status_counts.get(s, 0) + 1
|
||||
if parent_task_id:
|
||||
filtered = rows
|
||||
filtered = _filter_tasks_by_kind(rows, kind_f)
|
||||
else:
|
||||
filtered = []
|
||||
for r in rows:
|
||||
d = _task_to_dict(r, [], 0)
|
||||
if status_f and d["status"] != status_f:
|
||||
continue
|
||||
if kind_f and d["kind"] != kind_f:
|
||||
continue
|
||||
if executor_f and (d["executor"] or "") != executor_f:
|
||||
continue
|
||||
if q_f and q_f not in ((d["id"] or "") + " " + (d["prompt"] or "")).lower():
|
||||
@@ -6484,6 +6556,14 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
if err is not None:
|
||||
self._send_json(err[0], {"error": err[1]})
|
||||
return
|
||||
# 旁聊(P2 G5):旁聊没有 worktree,/worktree 按
|
||||
# “没有本地 worktree”口径回 404,不回 exists=false
|
||||
# 的摘要(/files 与 /tree 同款分支已在
|
||||
# _task_worktree_for_files 里回 404)。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(404, {
|
||||
"error": "no local worktree for this task"})
|
||||
return
|
||||
summary = self._worktree_summary(task)
|
||||
self._send_json(200, dict(summary, task_id=task_id))
|
||||
|
||||
@@ -6779,6 +6859,13 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
return
|
||||
if self._reject_if_over_quota(user_id):
|
||||
return
|
||||
# 旁聊(P2 G6):旁聊无 worktree/分支,分叉 thread
|
||||
# 历史的变体不在首版,直接 409,不等 worker/host 层
|
||||
# 以 500/ValueError 收尾。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(409, {
|
||||
"error": "side thread has no worktree/branch to fork"})
|
||||
return
|
||||
message = body.get("message")
|
||||
if not message or not isinstance(message, str) \
|
||||
or not message.strip():
|
||||
@@ -7027,6 +7114,13 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
user_id, task["project_id"], "approve_merge"):
|
||||
self._send_json(403, {"error": "forbidden"})
|
||||
return
|
||||
# 旁聊(P2 G6):旁聊无 worktree/分支,合并门禁的五项
|
||||
# 证据(CI/目录/契约/批准/评审)无一有落点,直接 409,
|
||||
# 不进门禁检查。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(409, {
|
||||
"error": "side thread has no worktree/branch to merge"})
|
||||
return
|
||||
_merge_ok, _merge_reason = _merge_state_decision(
|
||||
task["status"])
|
||||
if not _merge_ok:
|
||||
@@ -8860,6 +8954,12 @@ pid 须为整数、address 须为字符串,否则 400(注册表里的
|
||||
user_id, task["project_id"], "approve_merge"):
|
||||
self._send_json(403, {"error": "forbidden"})
|
||||
return
|
||||
# 旁聊(P2 G6):旁聊无 worktree/分支,建 PR 无分支可推,
|
||||
# 直接 409,不等 host 层以 500/ValueError 收尾。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(409, {
|
||||
"error": "side thread has no worktree/branch for a PR"})
|
||||
return
|
||||
if task["status"] != "merged":
|
||||
self._send_json(409, {
|
||||
"error": "only merged tasks can create a PR",
|
||||
|
||||
Reference in new issue
Block a user