# Process · 长进程与状态机 > 所有「不是即时响应」的代码:三个 Loop 的状态机详尽转换表、Agent 生命周期、SSE 心跳与 replay、retryWithReflection、清理与归档。 ## P-1 · L1 Unit Loop 状态机 · `loop-controller.js → runUnit()` ### 转换表(完整) | from | to | 触发 | 副作用 | | --- | --- | --- | --- | | pending | coding | LoopController 调度到 wave | markCoding + SSE unit.state | | coding | testing | Coder 返回 success | markTesting + SSE | | coding | failed | Coder 抛错(沙箱越权 / 超时) | fail · reason=coder_error | | testing | passed | Tester result.ok = true | pass + 写 contract_hash + SSE | | testing | failed | Tester result.ok = false | fail · 写 result.json | | failed | coding | retries < retry_budget | retries++ · 调 Coder 带 history | | failed | blocked | retries ≥ retry_budget | block + SSE human_intervention_needed | | passed | pending | L2 触发 reset | retries 重置 · 旧代码归档 | | blocked | pending | 人类显式 resume | retries 重置 | ### 伪代码 ```ts async function runUnit(pid, unitId) { const unit = unitRegistry.list(pid).find(u => u.id === unitId); let failures = []; for (let i = unit.retries; i < unit.retry_budget; i++) { unitRegistry.markCoding(pid, unitId); const coderRes = await agentManager.callAgent('coder', { pid, unit_id: unitId, history: failures }); if (!coderRes.ok) { unitRegistry.fail(pid, unitId, 'coder_error'); continue; } unitRegistry.markTesting(pid, unitId); const testerRes = await agentManager.callAgent('tester', { pid, unit_id: unitId }); if (testerRes.ok) { const hash = docStore.hashContract(await readContract(pid, unitId)); unitRegistry.pass(pid, unitId, hash); return { ok: true, retries: i }; } failures.push(testerRes.failures); unitRegistry.fail(pid, unitId, 'test_red'); } unitRegistry.block(pid, unitId, 'budget_exceeded'); return { ok: false }; } ``` - 同一 Unit 任何时刻只允许一个 runUnit 实例(用 Map 去重) - retry 之间不复用 Agent session(每次新 ACP session,保证干净) ## P-2 · L2 Integration Loop 状态机 · `loop-controller.js → onL2Failure() · runProject() L2 段` ### L2 闸门判定 ```ts function l2GateOpen(pid) { const units = unitRegistry.list(pid); const sdd = docStore.readSDD(pid); return units.every(u => { if (u.status !== 'passed') return false; const expectedHash = contractHashFromSDD(sdd, u.id); return u.contract_hash === expectedHash; }); } ``` ### L2 主循环 ```ts async function runL2(pid) { const intRes = await agentManager.callAgent('integrator', { pid }); if (intRes.ok) { sseBroker.emit(pid, 'project.shipped', {}); return 'shipped'; } const tri = await agentManager.callAgent('bug-triager', { pid, run_id: intRes.run_id }); if (tri.confidence < 0.5 || tri.category === 'spec_ambiguous') { sseBroker.emit(pid, 'human_intervention_needed', tri); return 'l2_blocked'; } if (tri.category === 'infra_flake') { return runL2(pid); // 直接重跑,不动 Unit } if (tri.category === 'impl_bug') { // 不改 SDD,只 reset 受影响 Unit unitRegistry.reset(pid, tri.affected_units); return 'retry_l1'; } // contract_mismatch → patch SDD + reset await agentManager.callAgent('architect', { pid, mode: 'patch', patch: tri.proposed_sdd_patch }); unitRegistry.reset(pid, tri.affected_units); const project = getProjectMeta(pid); if (++project.l2_rounds >= project.l2_budget) { sseBroker.emit(pid, 'project.exhausted', { last_triage: tri }); return 'l2_exhausted'; } return 'retry_l1'; } ``` ### 四种 triage 类别的分支 | category | 是否改 SDD | 是否 reset Unit | 是否计入 l2_rounds | | --- | --- | --- | --- | | contract_mismatch | 是 | 是 | 是 | | impl_bug | 否 | 是 | 否(不烧预算) | | spec_ambiguous | 否(升 L3) | 否 | 否 | | infra_flake | 否 | 否 | 否 | ## P-3 · L3 Human Feedback 队列 · `loop-controller.js → onHumanFeedback()` ```ts // 每个项目维护一个反馈队列,串行消费 const feedbackQueues = new Map(); function onHumanFeedback(pid, feedback) { if (!feedbackQueues.has(pid)) feedbackQueues.set(pid, []); feedbackQueues.get(pid).push(feedback); // 单 worker per pid if (!isWorking(pid)) drainFeedback(pid); } async function drainFeedback(pid) { while (feedbackQueues.get(pid).length) { const fb = feedbackQueues.get(pid).shift(); // 1. 等当前 L1 / L2 跑完(不打断) await waitForCurrentLoop(pid); // 2. PM 增量补 PRD await agentManager.callAgent('pm', { pid, feedback: fb }); // 3. Architect 重算 SDD + units.json (mode=generate) await agentManager.callAgent('architect', { pid, mode: 'generate' }); // 4. UnitRegistry.diffWithSDD → reset changed/removed const diff = unitRegistry.diffWithSDD(pid); unitRegistry.reset(pid, [...diff.changed, ...diff.removed]); // 5. 触发 runProject(增量 L1 + L2) runProject(pid); } } ``` - 同一 pid 永远只有一个 worker 在 drain(保证文档一致性) - 多 pid 之间互不阻塞 - worker 崩了,重启 server 后会自动 resume(从 stream.log 反推队列状态) ## P-4 · Agent 生命周期 · `server/src/core/session-pool.js · packages/acp-core/protocol/ClaudeCliProtocol.js · 复用既有` ```ts // 复用 LemonBoss SessionPool // key = (agentName, cid, pid) // 1. 首次 callAgent // ↓ SessionPool 检查无该 key // ↓ 通过 ProcessLauncher 起一个 claude CLI 子进程 // ↓ 走 ClaudeCliProtocol.initialize() + newSession() // ↓ 注入 system prompt (system/templates/devloop-*.md) // ↓ sendPrompt(input) // ↓ 流式累积 → AgentResult // 2. 第二次同 key 的 callAgent // ↓ SessionPool 命中,复用 session // ↓ sendPrompt(input) ← 上下文延续 // 3. 项目结束(shipped/abandoned) // ↓ SessionPool.closeProject(pid) // ↓ 杀掉所有该 pid 的 claude 进程 // 4. 项目空闲 30 分钟 // ↓ 自动 sweep 该 pid 的 session(省内存) // ↓ 下次 callAgent 重启冷启 OK ``` - 每个 Agent 进程绑死自己的 cwd(沙箱) - Bash 工具受 jsonGuard 中间件保护(防 inline JSON) - process-guard 看护:5s SIGTERM → 10s SIGKILL ## P-5 · SSE Broker 心跳与 Replay · `server/services/sse-broker.js` ```ts // 每订阅一个 pid,open 一份连接列表 // subscriptions: Map> function subscribe(pid, res, opts) { subscriptions.get(pid)?.add(res) || subscriptions.set(pid, new Set([res])); // Replay:从 stream.log 重放给当前订阅者 if (opts?.replayFromOffset) replayTo(res, pid, opts.replayFromOffset); // 心跳 const heartbeat = setInterval(() => res.write(':keepalive\n\n'), 25_000); res.on('close', () => { clearInterval(heartbeat); subscriptions.get(pid).delete(res); }); } function emit(pid, event, data) { const id = ++eventCounter; const line = `id: ${id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`; // 1. 落盘 stream.log fs.appendFileSync(streamLogPath(pid), line); // 2. 广播给所有订阅者 for (const res of subscriptions.get(pid) || []) res.write(line); } ``` - stream.log 保留 7 天,期间任何时刻 replay 都能精确 - 同事件不重复广播(去重以 event id 为准) ## P-6 · 清理与归档 · `分布在 doc-store.js · unit-registry.js · sse-broker.js · scripts/devloop-cleanup-cron.js (NEW)` ```ts // 清理钩子标准签名(被各服务调用 + 一个日 cron) type CleanupHook = (pid: string) => Promise; // cron: scripts/devloop-cleanup-cron.js · 每日 04:00 跑 ``` | 触发 | 动作 | 实现位置 | | --- | --- | --- | | DocStore.write* | 旧版本 → history/ | doc-store.js | | UnitRegistry.reset | units/{id}/code 旧版 → history/removed/{id}-{ts}/ | unit-registry.js | | project.shipped 30 天后 | 项目目录置 .archived 标记 | 日 cron 脚本 | | project.json.compressOldRuns=true | runs/ 14 天前的 stdout.txt → .gz | 同上 | | SSE close | 清 subscriptions 项 | sse-broker.js | | Server SIGTERM | kill 所有 ACP 子进程 | process-guard 既有 | ## P-7 · 预算守护 · `server/middleware/token-accountant.js (NEW) · 阈值嵌入 loop-controller.js` ```ts // 预算配置 schema(落在 project.json) type ProjectBudgets = { retry_budget: number; // 默认 5 · 每 Unit l2_budget: number; // 默认 3 · 每 PRD rev token_budget: number; // 默认 5_000_000 · 项目 agent_wallclock_ms:number; // 默认 600_000 · 单 Agent 调用 integrator_wallclock_ms: number; // 默认 300_000 · 单 L2 run }; ``` | 预算 | 阈值 | 超出后 | | --- | --- | --- | | retry_budget(每 Unit) | 默认 5 | 该 Unit → blocked | | l2_budget(每 PRD rev) | 默认 3 | 项目 → l2_exhausted | | token_budget(每项目) | 默认 5_000_000 | 项目 → blocked + SSE 告警 | | Agent 单次墙钟 | 默认 10 min | kill + 计 fail 一次 | | Integrator 总墙钟 | 默认 5 min | kill + 算 infra_flake | > _所有阈值在 `project.json` 可覆盖。生产建议保持默认。_ ## P-8 · Agent State 持久化 + 崩溃恢复 · `loop-controller.js · agent-state/*.json · timeline.ndjson` ### 调用前 / 中 / 后三阶段 ```ts // LoopController.callAgent 的完整包裹流程 async function callAgentWithPersistence(name, input) { const call_id = genCallId(); const statePath = `${pidDir}/agent-state/${name}.json`; // ── 调用前 ── const inputSnap = await snapshot(input); // → agent-state/snapshots/{call_id}-in.json fs.writeFileSync(statePath, JSON.stringify({ agent: name, call_id, status: 'running', current_step: 0, total_steps: 0, // Agent 自己稍后会写 last_input_ref: inputSnap, started_at: now(), updated_at: now(), })); timelineLogger.append(pid, { source: 'system', kind: 'agent_started', ref: `agent/${name}/${call_id}`, summary: `${name} started (unit=${input.unit_id || '-'})` }); try { // ── 调用中 ── (Agent 自己通过工具更新) // - record_memory(topic, content, relevance) // - write_todolist(items) → agent-state/{agent}-todo.json // - update_todo_item(id, status, ref) → 更新单条 item + 同步 progress_pct // - update_runtime_state(current_step) → 修 agent-state/{agent}.json // - timeline_append(kind, ref, summary)→ 写 timeline.ndjson const result = await agentManager.callAgent(name, input); // ── 调用后 (成功) ── const outSnap = await snapshot(result); fs.writeFileSync(statePath, JSON.stringify({ ...readState(statePath), status: 'done', last_output_ref: outSnap, updated_at: now(), })); timelineLogger.append(pid, { source: name, kind: 'agent_done', ref: `agent/${name}/${call_id}`, summary: `${name} done in ${result.duration_ms}ms`, payload: { tokens: result.tokens_used } }); return result; } catch (e) { // ── 调用后 (失败) ── fs.writeFileSync(statePath, JSON.stringify({ ...readState(statePath), status: 'error', error: { code: e.code, message: e.message, stack: e.stack }, updated_at: now(), })); timelineLogger.append(pid, { source: 'system', kind: 'agent_error', ref: `agent/${name}/${call_id}`, summary: e.message }); throw e; } } ``` ### 崩溃恢复流程 ```ts // server 启动时调用一次 async function recoverOnBoot() { const projects = listAllProjects(); // 扫 data/companies/*/projects/*/ for (const pid of projects) { const states = readAgentStates(pid); // 读全部 agent-state/*.json for (const s of states) { if (s.status === 'running') { // 之前的进程被杀,改 paused 等待人决策 writeState(s.agent, { ...s, status: 'paused', resume_token: genResumeToken(), updated_at: now() }); timelineLogger.append(pid, { source: 'system', kind: 'agent_paused_by_recovery', ref: `agent/${s.agent}/${s.call_id}`, summary: 'Server 重启,Agent 已挂起等待决策' }); sseBroker.emit(pid, 'resume_decision_needed', { agent: s.agent, call_id: s.call_id, last_step: s.current_step, total: s.total_steps, options: ['resume_from_step', 'restart_fresh', 'abort'] }); } } } } ``` ### 恢复决策三态 | option | 含义 | 动作 | | --- | --- | --- | | `resume_from_step` | 从 current_step 继续 | 读 last_input_ref + todolist,重新 callAgent,input 携带 history | | `restart_fresh` | 整个 Unit 重写 | UnitRegistry.reset 该 unit,清 todolist,回 L1 队列 | | `abort` | 放弃 | UnitRegistry.block 该 unit,等下一轮人类反馈 | ### Memory 注入流程 ```ts // 每次 Agent 启动前,把相关 memory 注入 system prompt function buildPromptWithMemory(agent, basePrompt) { const memDir = `${companyDir}/memory/devloop/${agent}/`; if (!fs.existsSync(memDir)) return basePrompt; const memories = fs.readdirSync(memDir) .map(f => parseMemory(path.join(memDir, f))) .filter(m => m.relevance_score >= 0.5) .sort((a, b) => b.relevance_score - a.relevance_score) .slice(0, 5); if (memories.length === 0) return basePrompt; const memSection = `## Memory (from prior projects)\n` + memories.map(m => `### ${m.topic}\n${m.content}`).join('\n\n'); return basePrompt + '\n\n' + memSection; } ``` - statePath 写入是**原子的**(fs.writeFileSync + sync flag) - resume_token 有效期 24 小时,过期视同 restart_fresh - memory 注入不超过 5 条,避免 prompt 膨胀 - relevance < 0.5 的 memory 不注入但保留文件 - 同一 (pid, agent) 任意时刻只允许一个 status=running ### 触发 SSE 事件 | 事件 | 何时 | | --- | --- | | `agent.started` | callAgentWithPersistence 进入 | | `agent.todolist` | Agent write_todolist 后 | | `agent.step` | Agent update_todo_item 后(前端用于 「3/5」) | | `agent.done` | 成功结束 | | `agent.error` | 抛错 | | `resume_decision_needed` | recoverOnBoot 检测到 paused | ## P-9 · Bug 状态机 · `server/services/bug-tracker.js → transition()` ### 状态机 ### 合法转换 | from | to | 触发 | 副作用 | | --- | --- | --- | --- | | (none) | open | Tester / Integrator 失败 → bugTracker.open | + timeline · + SSE bug.opened | | open | fixing | Coder 进入 coding 态(关联同 unit) | + status_history · + SSE | | open | wontfix | 人类决策 | 需带 note | | open | duplicate | BugTracker dedupKey 命中 + 人裁决 | 需填 duplicate_of | | fixing | fixed | 同 unit Tester 绿且 fix_refs 含新 run_id | + SSE bug.fixed | | fixing | open | Coder 退出但未通过 | retries++ | | fixed | verified | L2 Integration 全部通过 | + SSE bug.verified | | fixed | open | L2 失败且 BugTriager 归因到同一 root_cause | reopened | | verified | open | 新 L3 反馈引入回归 | reopened | ### 自动转换 vs 人为转换 - **自动:**open → fixing → fixed → verified 主链由 LoopController + Tester + Integrator 触发 - **人为:**wontfix / duplicate 必须人类显式调 API + 带 note - **重开:**fixed/verified → open 由系统自动触发但**必须**关联到具体 run_id + triage_id 证据 ### 与 L1/L2 的耦合 ```ts // L1 内:Tester 失败 → 开 bug + 设 unit failed testerResult.failures.forEach(f => { bugTracker.open(pid, { source: 'unit_test', unit_id: unit.id, run_id: runId, test_name: f.test, category: 'unknown', severity: 'high', title: f.test + ' ' + brief(f), detail: JSON.stringify(f), affected_units: [unit.id] }); }); // 下一轮 Coder 进入 → 关联 bug 状态转 fixing bugTracker.list(pid, { unit_id: unit.id, status: ['open'] }) .forEach(b => bugTracker.transition(pid, b.id, { to:'fixing', by:'coder' })); // L2 通过 → 所有 fixed → verified if (integratorResult.ok) { bugTracker.list(pid, { status: ['fixed'] }) .forEach(b => bugTracker.transition(pid, b.id, { to:'verified', by:'system' })); } ``` ### 新增 SSE 事件 | event | 触发 | | --- | --- | | `bug.opened` | bugTracker.open | | `bug.transitioned` | 任何状态转换 | | `bug.reopened` | verified → open 特化事件 | ## P-10 · 自动 git commit 流程 · `server/src/core/loop-controller.js · callAgentWithPersistence 的 success 分支扩展` ```ts // 完整设计见 arch-git-versioning.html · 此处只放骨架 async function commitAfterAgent(pid, name, result, input) { const spec = buildCommitSpec(name, result, input); if (!spec) return; // 某些情况不 commit (Coder 等 Tester 绿,Tester 红不 commit) const { ok, sha, skipped } = await gitVault.commit(pid, spec); if (ok && !skipped) { timelineLogger.append(pid, { source:'system', kind:'git_committed', ref: sha.slice(0,7), summary: spec.summary }); sseBroker.emit(pid, 'git.committed', { sha, kind: spec.kind, summary: spec.summary }); } } // Commit 时机表 PM 完成 prd → kind:'prd', msg: 'prd: rev N · {summary}' Architect generate → kind:'sdd', msg: 'sdd: rev N · X units · ...' Architect patch → kind:'sdd', msg: 'sdd: patch · {affected_units}' Coder + Tester 绿 → kind:'code', msg: 'code(unit-id): implement|retry · attempt N' Coder 单独完成 → 不 commit (等 Tester 验证) Tester 红 → 不 commit (避免噪音) BugTriager → kind:'triage', msg: 'triage(run-id): {category} · confidence {n}' Integrator → kind:'integ', msg: 'integ(run-id): pass|fail · X/Y' shipped → tag:'shipped-v{prd_rev}-{sdd_rev}' sign-off → tag:'final-v{prd_rev}' abandon → tag:'abandoned' ``` - Coder commit 推迟到 Tester 绿(避免红色代码进 main) - 所有 commit author 标 Agent 名 - nothing-to-commit 返 skipped=true,不抛错 - commit 失败 → 记 warning 但不阻塞 Agent 返回 - shipped/sign-off/abandon 用 tag 而非 commit,commit 是文件变化,tag 是项目里程碑 - tests: 项目创建后 git log 含 init commit + .gitignore - tests: PM 完成后多 1 个 prd: rev 1 commit - tests: Coder retry 3 次但只 Tester 绿一次 → 仅 1 个 code commit - tests: shipped → tag shipped-v1-1 存在 ### recoverOnBoot 加 git 清理 ```ts async function recoverOnBoot() { for (const pid of listAllProjects()) { // P-8 既有:扫 agent-state running → paused (见 P-8) recoverAgentStates(pid); // P-10 新增:扫 git 工作区脏文件 const status = await gitVault.status(pid); if (status.dirty) { sseBroker.emit(pid, 'recovery_dirty_tree', { files: status.files, options: ['commit_as_wip', 'reset_hard', 'manual_inspect'], }); } } } ``` ### 新增 SSE 事件 | event | 何时 | | --- | --- | | `git.committed` | commitAfterAgent 成功后 | | `git.tagged` | tag 操作后(shipped/sign-off/abandon) | | `recovery_dirty_tree` | recoverOnBoot 检测到上次 crash 留下的脏文件 | ## 三层 Loop 嵌套伪代码(最终版) ```ts // 完整调度循环(runProject 核心) async function runProject(pid) { while (true) { // ===== L1 ===== const waves = scheduler.getExecutionGroups(unitRegistry.pending(pid)); for (const wave of waves) { await Promise.all(wave.map(u => runUnit(pid, u.id))); } if (unitRegistry.anyBlocked(pid)) return 'blocked'; // ===== L2 ===== if (!unitRegistry.l2GateOpen(pid)) continue; const r = await runL2(pid); if (r === 'shipped') return 'shipped'; if (r === 'l2_blocked') return 'blocked'; if (r === 'l2_exhausted') return 'l2_exhausted'; // r === 'retry_l1' → 继续外 while } // L3 入口 onHumanFeedback() 异步入队,由 drainFeedback 串行调用 runProject } ```