# 课题11 设计方案:用户级多项目支持 > **日期**: 2026-05-16 > **作者**: 庞统(副军师)🐦 > **状态**: v2(并发调度模型重设计,待评审) > **前置**: 课题1-4、课题6 已完成设计 > **变更**: v2 新增 §5.4 并发调度模型(per-project 线程 + 全局资源信号量),替代原串行 tick --- ## 1. 核心问题 用户同时有多个工作域(量化策略A + 平台开发 + 数据研究),需要项目级隔离——不同项目的任务、配置、产出互不干扰,但共享同一套 Agent 团队和 Daemon 基础设施。 ## 2. 需要隔离什么 | 隔离项 | 原因 | 隔离方式 | |--------|------|---------| | 黑板数据(tasks/comments/outputs/decisions/observations/events/agents/task_attempts/reviews/experiences/experience_tags) | 不同项目的任务不能混在一起 | 独立 SQLite 文件 | | 配置(guardrails.yaml / prompt_templates / project_context.yaml) | 不同项目可能有不同的审查规则、上下文 | 项目级 config/ 目录,覆盖全局默认 | | 产出文件 | 不同项目的代码/数据物理隔离 | 项目级 outputs/ 目录 | | Agent session | 同一 Agent 参与不同项目时上下文不串 | OpenClaw `--session-id` 已有隔离 | | Daemon 连接 | 不同数据库连接不能混淆 | 连接池 project_id → Connection 映射 | **不需要隔离的**: - Agent 注册表(agents.yaml)—— 同一套 Agent 团队服务所有项目 - Daemon 进程 —— 单进程管理所有项目 - Schema 定义 —— 所有项目共享同一套表结构 - 全局 prompt_templates —— 项目级覆盖,不是替换 ## 3. 方案选择 ### 3.1 三个方案对比 | 方案 | 做法 | 优点 | 缺点 | |------|------|------|------| | **A. 多实例** | 每个项目独立 Daemon + 独立 SQLite + 独立端口 | 完全隔离、互不影响 | 资源开销翻倍、管理复杂、Agent 重复注册 | | **B. 单实例多命名空间** | 一个 SQLite,所有表加 `project_id` | 零额外资源 | 每个查询带 WHERE、单文件性能上限、删除项目危险 | | **C. 单 Daemon 多数据库** | 一个 Daemon,每个项目一个 SQLite 文件 | 物理隔离数据、共享 Daemon | Daemon 需管理多连接 | ### 3.2 选择方案 C 理由: 1. **方案 A 不适合**——Mac mini 资源有限,每多一个项目就多一套 Daemon + PM2 进程 + 端口。6 个 Agent 跑在 OpenClaw 上已经固定开销,不需要重复。 2. **方案 B 不够安全**——`WHERE project_id = ?` 容易漏,SQLite 单文件多项目并发有 WAL 锁瓶颈,删除项目 = 跨所有表 DELETE。 3. **方案 C 是最优点**——数据物理隔离(每个 `.db` 文件独立),但共享 Daemon 进程和 Agent 注册表。Daemon 切换项目只是切换 SQLite 连接,无额外资源开销。 **优秀实践验证**: - Wanman:Per-Agent Worktree + $HOME 严格隔离 → 验证"物理隔离比逻辑隔离可靠" - ClawTeam:Git Worktree 隔离 + fcntl 文件锁 → 验证"共享进程 + 独立存储"模式可行 - Cline:Kanban + Worktree → 验证"多任务并行 + 物理隔离"是主流 - Hermes:单 Dispatcher + 单 SQLite → **Hermes 是单项目设计**,我们没有"多项目用单数据库"的先例 ## 4. 目录结构 ``` ~/.sanguo_projects/moziplus_v2/ ├── daemon.py # 单 Daemon 进程 ├── daemon.yaml # Daemon 全局配置(端口、tick 间隔等) ├── projects/ │ ├── _registry.yaml # 项目注册表(所有项目的元数据) │ ├── quant-momentum/ # 项目 1 │ │ ├── blackboard.db # 独立 SQLite │ │ ├── config/ │ │ │ ├── project.yaml # 项目元信息(名称、描述、创建时间) │ │ │ ├── guardrails.yaml # 项目级审查规则(覆盖全局默认) │ │ │ ├── project_context.yaml # 项目背景知识(注入 L2) │ │ │ └── prompt_overrides/ # 可选:覆盖默认 prompt 模板 │ │ └── outputs/ # 项目产出目录 │ ├── quant-pairs/ # 项目 2 │ │ ├── blackboard.db │ │ ├── config/ │ │ └── outputs/ │ └── moziplus-dev/ # 项目 3(自身开发) │ ├── blackboard.db │ ├── config/ │ └── outputs/ └── shared/ ├── prompt_templates/ # 全局默认模板 ├── schemas/ # 全局 Schema └── agents.yaml # 全局 Agent 注册表 ``` ### 4.1 项目注册表(_registry.yaml) ```yaml # projects/_registry.yaml default_project: quant-momentum projects: quant-momentum: display_name: "动量因子策略" description: "基于动量因子的量化策略研发" created_at: "2026-05-16T10:00:00Z" status: active # active / archived agents: [zhangfei-dev, zhaoyun-data, guanyu-dev] # 该项目可用的 Agent quant-pairs: display_name: "配对交易策略" description: "统计套利配对交易研究" created_at: "2026-05-16T10:00:00Z" status: active agents: [zhangfei-dev, zhaoyun-data] moziplus-dev: display_name: "平台开发" description: "moziplus 自身开发" created_at: "2026-05-16T10:00:00Z" status: active agents: [zhangfei-dev, simayi-challenger] ``` ## 5. Daemon 变更 ### 5.1 多连接池 ```python # daemon 内部 class ProjectManager: def __init__(self, projects_dir: Path): self.projects_dir = projects_dir self._connections: dict[str, sqlite3.Connection] = {} self._configs: dict[str, ProjectConfig] = {} self._load_registry() def get_connection(self, project_id: str) -> sqlite3.Connection: if project_id not in self._connections: db_path = self.projects_dir / project_id / "blackboard.db" self._connections[project_id] = sqlite_connect(db_path) return self._connections[project_id] def get_config(self, project_id: str) -> ProjectConfig: if project_id not in self._configs: config_path = self.projects_dir / project_id / "config" / "project.yaml" self._configs[project_id] = ProjectConfig.load(config_path) return self._configs[project_id] def load_guardrails(self, project_id: str) -> dict: """项目级 guardrails.yaml 覆盖全局默认""" global_guardrails = load_yaml("shared/guardrails.yaml") project_guardrails_path = self.projects_dir / project_id / "config" / "guardrails.yaml" if project_guardrails_path.exists(): project_guardrails = load_yaml(project_guardrails_path) return deep_merge(global_guardrails, project_guardrails) # 项目级覆盖 return global_guardrails ``` ### 5.2 ~~Tick 逻辑变更~~(已废弃,见 §5.4) > **原设计**:Daemon 主循环串行遍历所有项目 tick。每个项目 tick 完再 tick 下一个。 > **问题**:所有项目/任务一起排队,项目 A 的长任务阻塞项目 B。 > **新设计**:见 §5.4 per-project 并发调度。 ### 5.3 Daemon 逻辑健康自检 + 线程存活监控(v2 扩展) ```python # §14 风险缓解:连续 N tick 无状态变更则告警 STALE_TICK_THRESHOLD = 20 class DaemonHealth: def __init__(self, project_id: str): self.project_id = project_id self._idle_ticks = 0 def record_idle(self): self._idle_ticks += 1 def record_change(self): self._idle_ticks = 0 def is_stale(self) -> bool: return self._idle_ticks >= STALE_TICK_THRESHOLD ``` **线程存活监控**(见 §5.4.4 `Daemon._check_slot_health()`): - Daemon 主线程每 60s 检查所有 ProjectSlot 线程是否存活 - 线程死亡 → 记录日志 + 自动重启 - 连续重启 3 次失败 → 告警(通过 Sanguo Mail 通知用户) **计数器超时兜底**: - 如果 Agent 完成回调丢失(进程被杀、网络断),`ActiveAgentCounter` 不会归零 - `_check_working_tasks()` 中,working 任务超过 `task_timeout`(默认 10 分钟)视为完成 - 视为完成时主动 `decrement()`,防止计数器泄漏 ### 5.5 Daemon 崩溃恢复(v2 新增) **设计原则**:状态全在 SQLite,Daemon 无状态。重启 = 重新加载所有项目 + 所有任务状态,继续执行。 **恢复流程(保守策略)**: ``` PM2 检测 Daemon 挂了 → 重启 Daemon │ ├── 读取 _registry.yaml → 恢复项目列表 ├── 遍历每个 active 项目 → 打开 SQLite 连接 ├── 扫描所有 working 任务 → 标记为 failed(原因: "Daemon restart, agent process lost") ├── 启动 ProjectSlot 线程 └── 后续 pending 任务正常分配 ``` **为什么不重新执行**: 1. Daemon 崩溃是不正常事件,Agent 子进程状态不可预测 2. output.json 可能写了一半,重新执行比恢复更安全 3. 用户手动 retry 比自动重新执行更可控 4. task_attempts 表记录完整,不丢信息 **关键设计**: 1. **SQLite 是真相来源**——所有任务状态、产出记录都在 `.db` 文件里,Daemon 内存无状态 2. **SQLite WAL 保护数据完整性**——崩溃时未提交的事务自动回滚 3. **ActiveAgentCounter / DaemonHealth 重启后归零**——不需要持久化 4. **task_attempts 的 attempt_index 递增**——retry 不覆盖历史 **不需要额外存储**: - 不需要 checkpoint 文件——SQLite 就是 checkpoint - 不需要 recovery log——task_attempts 表已经记录所有尝试 - 不需要 recovery log——`task_attempts` 表已记录每次尝试 - 不需要状态快照——每次 tick 从 SQLite 实时读取 **唯一注意**:`ActiveAgentCounter` 重启后从零开始——但 `_tick()` 会重新扫描 working 任务并重新计数,所以没问题。 ### 5.4 并发调度模型(v2 新增) #### 5.4.1 问题 原设计中 Daemon 主循环串行 tick 所有项目: ``` Tick → Project A(30s)→ Project B(等A完成)→ Project C(等B完成) ``` 问题: 1. **项目间互相阻塞**——Project A 有一个长任务在执行,Project B 的独立任务必须等 2. **响应延迟**——3 个项目 tick 一次可能要 90s+,Project C 要等 60s 才被检查 3. **不符合业界实践**——调研 7 个项目(Hermes/open-multi-agent/Wanman/Google ADK/Microsoft AutoGen/AgentScope/GSD),没有一个用全局串行排队 #### 5.4.2 业界并发模型调研 | 项目 | 并发模型 | 核心机制 | |------|---------|---------| | **open-multi-agent** | AgentPool + Semaphore | 全局 `maxConcurrency=5`,per-agent 互斥锁,`Promise.allSettled` 并行执行独立任务 | | **Wanman** | per-agent 进程 | 每个 Agent 独立进程+独立 runLoop,Supervisor 通过消息总线协调 | | **Google ADK** | asyncio.TaskGroup | `ParallelAgent` 用 `TaskGroup` 并行执行子 Agent | | **Microsoft AutoGen** | Pregel Superstep | 每个 superstep 内所有激活 Executor 并行执行 | | **Hermes** | 单线程 tick + flock | **单项目设计**,tick 内只有几个 cron job,不需要并发 | **关键发现**:open-multi-agent 的 `AgentPool + Semaphore + per-agent Lock` 是最成熟、最可借鉴的模型。 #### 5.4.3 设计:per-project 线程 + 全局资源信号量 ``` Daemon 主进程(轻量路由器 + 资源管控) │ ├── 全局 LLM Semaphore(max_concurrent=3) ├── per-agent Lock(张飞不能同时在两个项目里跑) │ ├── ProjectSlot A(独立线程) │ └── 自己的 SQLite 连接 │ └── 自己的 tick 循环(30s) │ └── spawn Agent 时:acquire agent_lock → acquire llm_semaphore │ ├── ProjectSlot B(独立线程) │ └── (同上) │ └── ProjectSlot C(独立线程) └── (同上) ``` **三层资源控制**: | 层级 | 控制对象 | 机制 | 原因 | |------|---------|------|------| | L1: 项目隔离 | SQLite 连接 | per-project 独立连接 | 数据物理隔离,无竞争 | | L2: Agent 互斥 | 同一 Agent 不能并行 | `threading.Lock` per-agent | Agent session 不是线程安全的,张飞同一时刻只能服务一个任务 | | L3: 全局资源 | LLM API 调用并发 | `threading.Semaphore(max_concurrent)` | 防止 API 限流、控制成本 | #### 5.4.4 核心代码 ```python import threading import time from pathlib import Path class ActiveAgentCounter: """线程安全的 Agent 活跃任务计数器。""" def __init__(self, max_global: int = 5): self._counts: dict[str, int] = {} # agent_id → 活跃任务数 self._total = 0 # 全局活跃总数 self._max_global = max_global self._lock = threading.Lock() def can_acquire(self, agent_id: str, max_per_agent: int = 1) -> bool: """检查是否可以分配(非阻塞)。""" with self._lock: if self._total >= self._max_global: return False return self._counts.get(agent_id, 0) < max_per_agent def increment(self, agent_id: str): with self._lock: self._counts[agent_id] = self._counts.get(agent_id, 0) + 1 self._total += 1 def decrement(self, agent_id: str): with self._lock: if self._counts.get(agent_id, 0) > 0: self._counts[agent_id] -= 1 self._total -= 1 class Daemon: """单进程 Daemon,per-project 线程并发。""" def __init__(self, config: DaemonConfig): self.config = config self.agent_counter = ActiveAgentCounter(max_global=config.max_global_active) # 默认 5 self.slots: dict[str, ProjectSlot] = {} self._slot_threads: dict[str, threading.Thread] = {} # 线程存活监控 self._shutdown = threading.Event() def start(self): """启动所有 active 项目的独立线程。""" registry = load_registry() for project_id, meta in registry["projects"].items(): if meta["status"] != "active": continue self._start_slot(project_id, meta) # 主线程:监控 + 等待 shutdown while not self._shutdown.is_set(): self._check_slot_health() self._shutdown.wait(60) # 每 60s 检查一次线程存活 def _start_slot(self, project_id: str, meta: dict): slot = ProjectSlot( project_id=project_id, config=meta, agent_counter=self.agent_counter, tick_interval=self.config.tick_interval, shutdown_event=self._shutdown, ) self.slots[project_id] = slot t = threading.Thread(target=slot.run_loop, name=f"project-{project_id}", daemon=True) t.start() self._slot_threads[project_id] = t def _check_slot_health(self): """检查所有 ProjectSlot 线程是否存活,死亡则重启。""" for project_id, thread in list(self._slot_threads.items()): if not thread.is_alive(): logger.warning(f"ProjectSlot {project_id} thread died, restarting...") meta = self.slots[project_id].config self._start_slot(project_id, meta) def shutdown(self): self._shutdown.set() class ProjectSlot: """单项目的独立 tick 循环。""" def __init__(self, project_id, config, agent_counter, tick_interval=30, shutdown_event=None): self.project_id = project_id self.conn = sqlite_connect(Path(f"projects/{project_id}/blackboard.db")) self.config = config self.agent_counter = agent_counter # 共享:全局 Agent 活跃计数器 self.tick_interval = tick_interval self.shutdown = shutdown_event or threading.Event() self.health = DaemonHealth(project_id) def run_loop(self): """独立线程的主循环。""" while not self.shutdown.is_set(): try: self._tick() except Exception as e: logger.error(f"[{self.project_id}] tick failed: {e}") self.shutdown.wait(self.tick_interval) def _tick(self): """单次 tick:找 pending 任务,尝试分配。""" # 先检查已完成的 Agent,释放计数器 completed = self._check_working_tasks() # 返回已完成的 agent_id 列表 for agent_id in completed: self.agent_counter.decrement(agent_id) pending = find_pending(self.conn) if not pending: self.health.record_idle() return for task in pending: agent_id = task["assignee"] max_per_agent = self.config.get("max_active_per_agent", 1) # 检查全局 + per-agent 并发上限 if not self.agent_counter.can_acquire(agent_id, max_per_agent): logger.info(f"[{self.project_id}] {agent_id} at capacity, skip task {task['id']}") continue # sequential 模式:检查该 Agent 在本项目是否有 working 任务 if self.config.get("agent_parallelism") != "parallel": if has_working_task(self.conn, agent_id): continue # 生成 session_id(§5.4.6 命名规则) session_id = self._get_session_id(agent_id, task['id']) # spawn Agent(异步,不阻塞) spawn_agent( project_id=self.project_id, task=task, session_id=session_id, ) self.agent_counter.increment(agent_id) # +1 self.health.record_change() def _get_session_id(self, agent_id: str, task_id: str) -> str: """§5.4.6 session 命名规则。""" if self.config.get("agent_parallelism") != "parallel": # sequential:同一项目同一 Agent 复用 session(保持上下文连续性) return f"agent:{agent_id}:project:{self.project_id}" else: # parallel:每个任务独立 session return f"agent:{agent_id}:project:{self.project_id}:task:{task_id}" ``` #### 5.4.5 资源控制模型(v2 修订:异步计数器替代同步信号量) **原设计问题**:spawn_agent() 不是瞬时操作——Agent 执行涉及 LLM API 调用(可能多轮工具调用),如果在 spawn 完成后才释放锁/信号量,并发退化回串行;如果在 spawn 启动后立即释放,信号量没有真正限流。 **修订方案**(采纳司马懿评审建议): | 资源 | 原方案 | 修订方案 | 原因 | |------|--------|---------|------| | Agent 互斥 | `threading.Lock` | 移除。改为 per-project session 命名 + `_check_working_tasks()` | 同一 Agent 可用不同 session-id 安全服务不同项目 | | LLM 并发 | `threading.Semaphore` | `ActiveAgentCounter`(异步计数器) | spawn 是异步的,同步信号量无法精确限流 | | 项目隔离 | per-project SQLite | 不变 | | **新时序**: ``` ProjectSlot._tick() │ ├── 检查 ActiveAgentCounter[agent_id] < max_active? │ └── 否 → 跳过,下个 tick 再检查 ├── 检查 agent_parallelism == "sequential" 且该 Agent 有 working 任务? │ └── 是 → 跳过 ├── spawn_agent(project_id, task) │ ├── session_id = "agent:{agent_id}:project:{project_id}:task:{task_id}" │ └── ActiveAgentCounter.increment(agent_id) # +1 │ └── Agent 完成回调(下次 tick 检测到 output 或 webhook) └── ActiveAgentCounter.decrement(agent_id) # -1 ``` **关键变化**: 1. 不再使用 `threading.Lock` 和 `threading.Semaphore`——它们是同步原语,不适合异步 spawn 场景 2. 改用 `ActiveAgentCounter`(线程安全计数器),spawn 时 +1,Agent 完成回调时 -1 3. `_tick()` 分配前检查计数器,超过阈值就跳过 4. Agent session 按 `agent:{agent_id}:project:{project_id}:task:{task_id}` 命名,项目+任务级天然隔离 #### 5.4.6 Agent 并行策略 + Session 隔离 **并行策略配置**: ```yaml # _registry.yaml 中可配置 projects: quant-momentum: agent_parallelism: sequential # 同一 Agent 同一时刻只跑一个任务(默认) max_active_per_agent: 1 # sequential 的显式写法 quant-pairs: agent_parallelism: parallel # 同一 Agent 可同时跑多个任务 max_active_per_agent: 2 # 最多 2 个并行 ``` **Session 命名规则**: ``` 格式:agent:{agent_id}:project:{project_id}:task:{task_id} 示例:agent:zhangfei-dev:project:quant-momentum:task:task-001 ``` - 每个任务独立 session,任务间上下文不串 - 同一 Agent 在不同项目用不同 session,项目间上下文不串 - `sequential` 模式:同一项目同一 Agent 只有一个活跃 session(新的任务复用或新开) - `parallel` 模式:每个任务独立 session **sequential 模式下的 session 复用**: ```python def _get_session_id(self, agent_id: str, task_id: str) -> str: project_config = self.config if project_config.get("agent_parallelism") != "parallel": # sequential:同一项目同一 Agent 复用 session(保持上下文连续性) return f"agent:{agent_id}:project:{self.project_id}" else: # parallel:每个任务独立 session return f"agent:{agent_id}:project:{self.project_id}:task:{task_id}" ``` #### 5.4.7 并发安全保证 | 并发场景 | 风险 | 保护机制 | |---------|------|---------| | 两个项目同时写同一个 SQLite | 数据损坏 | 每个项目独立 `.db` 文件,不存在此场景 | | 两个项目同时分配同一个 Agent | Agent 资源争抢 | `ActiveAgentCounter` + `max_active_per_agent` 限制 | | LLM API 并发超限 | API 限流/超限 | `ActiveAgentCounter` 全局计数,`_tick()` 分配前检查 | | ProjectSlot 线程异常退出 | 项目 tick 停止 | try/except 包裹 + Daemon 监控线程存活(§5.4.8) | | Daemon 主进程崩溃 | 所有项目停止 | PM2 自动重启 + SQLite WAL 保护数据完整性 | | Agent 完成回调丢失 | 计数器不归零 | 超时兜底:working 任务超过 `task_timeout` 视为完成,计数器 -1 | | _registry.yaml 并发写入 | 数据损坏 | _registry.yaml 只在 CLI 操作时读写(非 tick 热路径),tick 状态用内存 dict | ### 6.1 项目管理命令 ```bash # 创建项目 python3 blackboard.py project create --name quant-momentum --display-name "动量因子策略" --agents zhangfei-dev,zhaoyun-data,guanyu-dev # 列出项目 python3 blackboard.py project list # 切换默认项目 python3 blackboard.py project default quant-momentum # 归档项目(不删除数据,只停 tick) python3 blackboard.py project archive quant-pairs # 删除项目(删除数据,需确认) python3 blackboard.py project delete quant-pairs --confirm ``` ### 6.2 所有操作指定项目 ```bash # 方式1:命令行参数 python3 blackboard.py read --project quant-momentum --task task-001 # 方式2:环境变量(设置后所有命令默认用此项目) export MOZIPLUS_PROJECT=quant-momentum python3 blackboard.py read --task task-001 # 方式3:默认项目(_registry.yaml 中 default_project) # 不指定 --project 也不设环境变量时,使用 default_project python3 blackboard.py read --task task-001 ``` ### 6.3 Agent 使用的项目解析优先级 ``` 1. --project 参数(显式指定) 2. MOZIPLUS_PROJECT 环境变量 3. _registry.yaml 中的 default_project 4. 如果只有一个 active 项目,自动使用 5. 都没有 → 报错"请指定项目" ``` ## 7. L2 上下文注入变更 ### 7.1 Agent spawn 时注入项目上下文 L2 prompt_template 三段式注入增加项目段: ``` ═══ 项目上下文 ═══ 项目: quant-momentum(动量因子策略) 背景: 可用 Agent: 张飞(编码)、赵云(数据)、关羽(风控) ═══ 任务上下文 ═══ (原有内容不变) ``` ### 7.2 project_context.yaml 示例 ```yaml # projects/quant-momentum/config/project_context.yaml description: "基于动量因子的量化策略研发" domain: "量化交易" data_sources: - "NAS /Volumes/stock/ A股日线数据" - "NAS /Volumes/stock/minute_kline/ 分钟线数据" code_repo: "~/.openclaw/sanguo_projects/sanguo_quant_live/" key_constraints: - "所有策略必须通过回测验证才能上实盘" - "止损逻辑必须经过关羽风控审查" ``` ## 8. 跨项目协作 ### 8.1 默认禁止跨项目 Agent 不能跨项目读写黑板。这是安全边界——不同项目的数据、配置、产出互不干扰。 ### 8.2 跨项目数据共享 如果项目 A 需要项目 B 的产出(如"moziplus-dev 需要赵云的数据"),通过文件系统共享: ```bash # 项目 A 中,Agent 把产出写到 NAS 共享路径 # 项目 B 中,Agent 从 NAS 共享路径读取 ``` 不需要特殊的跨项目协议——**NAS 路径就是跨项目的桥梁**,和当前团队的工作方式一致。 ### 8.3 跨项目任务请求(可选扩展) 如果未来需要 Agent 主动发起跨项目请求: ``` 项目 A 黑板 → 创建 cross_project_request 类型任务 → Daemon 检测到 → 在项目 B 黑板创建对应任务 → 项目 B Agent 完成 → 产出写入 NAS → Daemon 检测项目 B 完成 → 更新项目 A 任务状态 ``` 当前不实现,预留设计空间。 ### 8.4 项目归档/删除时正在运行的任务(v2 新增) **归档(archive)**: 1. 检查是否有 working 状态的任务 2. 有 → 将这些任务标记为 cancelled,等待 Agent 完成回调(超时兜底 5 分钟) 3. 无 → 立即停止该项目的 ProjectSlot 线程 4. 将 `_registry.yaml` 中状态改为 `archived` **删除(delete)**: 1. 必须先 archive 2. 必须无 working 任务(归档时已处理) 3. `--confirm` 确认 4. `rm -rf projects/{project_id}/` 5. 从 `_registry.yaml` 移除条目 **禁止直接删除 active 项目**——必须先归档。 ## 9. 与其他课题的关系 | 课题 | 关系 | 说明 | |------|------|------| | 课题1(三层执行) | 无冲突 | Agent spawn 时多传一个 project_id,L2 注入多加项目上下文 | | 课题2(事件驱动) | 微调 | Inbox JSONL 增加 project_id 字段,Daemon 路由到正确连接 | | 课题3(挑战/评审) | 项目级配置 | guardrails.yaml 项目级覆盖 | | 课题4(拆解+上下文) | 项目级配置 | project_context.yaml 注入 L2 | | 课题6(经验沉淀) | 项目级经验 | 每个项目独立的 experiences 表,经验不跨项目污染 | | 课题7+9(交互+Dashboard) | 多项目视图 | Dashboard 需要项目切换/多项目概览 | | Worktree 隔离 | 正交 | Worktree 解决"同项目内多 Agent 并行改代码",课题11 解决"不同项目数据隔离" | ## 10. 黑板 Schema 变更 **不增加 project_id 字段**——每个项目有独立数据库,表结构不变。 唯一新增:`_registry.yaml` 项目注册表。 ## 11. 开发清单 | # | 任务 | 依赖 | |---|------|------| | 1 | 项目目录结构 + _registry.yaml + project.yaml Schema | 无 | | 2 | ActiveAgentCounter(线程安全计数器 + 全局/per-agent 双重限制) | 无 | | 3 | ProjectSlot(独立线程 tick + SQLite 独立连接 + spawn 前检查计数器) | 1, 2 | | 4 | Daemon 主循环(启动状态恢复 + ProjectSlot 线程监控/重启) | 3 | | 4a | 启动时幽灵 working 任务处理(扫描+标记 failed) | 4 | | 5 | CLI project create/list/default/archive/delete 命令 | 1 | | 6 | CLI 所有操作增加 --project 参数 + 优先级解析 | 1, 3 | | 7 | L2 prompt_template 注入 project_context + session_id 命名规则 | 3, 6 | | 8 | Daemon 逻辑健康自检(按项目追踪 + 计数器超时兜底) | 4 | | 9 | 项目归档/删除安全流程(working 任务处理) | 4, 5 | | 10 | Dashboard 项目切换 + 多项目概览 | 课题9 | --- ## 附录:方案 B 的详细反驳 有人可能觉得方案 B(加 project_id 字段)更简单。但实际上: 1. **安全面**:方案 B 靠 `WHERE project_id = ?` 逻辑隔离。一个漏掉的 WHERE = 数据泄漏。方案 C 靠物理文件隔离,漏不掉。 2. **性能面**:方案 B 所有项目共享一个 SQLite 文件。10 个项目各 10 个任务 = 100 个任务在一个 `.db` 里。WAL 写入是串行的,多项目并发 tick 会互相等待。方案 C 每个项目独立文件,互不影响。 3. **运维面**:方案 B 删除项目 = `DELETE FROM tasks WHERE project_id = ?` + 8 张表都要删。方案 C = `rm -rf projects/xxx/`。备份/恢复同理。 4. **配置面**:方案 B 的 guardrails.yaml 要设计"全局默认 + 项目覆盖"的合并逻辑。方案 C 每个项目独立 config/ 目录,天然隔离,覆盖逻辑更清晰。 5. **经验沉淀面**:方案 B 的经验混在一个 experiences 表里。如果项目 A 的"pytest 参数经验"污染到项目 B(B 可能不做 Python),反而有害。方案 C 每个项目独立经验库。 **唯一方案 B 更优的场景**:跨项目统计分析("所有项目的平均完成时间")。但这个需求可以后期通过注册表元数据实现,不需要把所有数据放一个库里。