研究日期:2026-07-23(Asia/Shanghai)
主仓库:openclaw/openclaw
源码快照:67ef07863fbb24d751e20f022e216118924ae15d
快照提交时间:2026-07-23T12:03:11+09:00
快照包版本:2026.7.2
npmlatest:2026.7.1-2(2026-07-23 查询)
版本声明
OpenClaw 的默认分支在本次研究当天仍持续前进。本文拆解的是上面的 main 快照,不把其中尚未进入 npm latest 的实现描述为所有稳定安装都已具备。官方文档也会随主分支更新;所有关键代码链接都钉到完整 SHA。
结论先行
OpenClaw 不是“把人格文件、模型和几个工具粘在一起”的聊天机器人。它更像一台长期在线的委托控制平面:
- Gateway 接住不同入口,解析身份、路由和 session;
- admission 在快速 ACK 之前处理可恢复写入与幂等;
agentCommand固定本轮 agent、workspace、model、tool 与 delivery 语义;- embedded runner 通过 session lane、global lane 和写锁串行化危险状态;
AgentSession.prompt()启动真正的 model → tool → result → model 循环;- per-agent SQLite 保存运行时 session row 与 transcript events;
- Gateway 订阅事件流,并把最终结果送回原 channel。
“人格”是被这台控制平面装入 prompt 的一类上下文;“连续性、权限和恢复”才是它更难替换的部分。
产品层、架构层、代码层
产品层
- 一个常驻 Gateway 同时服务 channel、CLI、Control UI、HTTP endpoint、plugin route 与 node。
- workspace 是 agent 的可读上下文和工作目录,不自动等于 sandbox。
- 默认 direct message 可以共享 main session;多用户入口需要主动改为 peer 隔离。
架构层
- Gateway 是 admission、route、session ownership、event stream 与 delivery 的控制面。
- embedded runner 是一次 run 的执行编排层:准备上下文、选择 model/runtime、建立 AgentSession、订阅流、处理 timeout 和 terminal outcome。
packages/agent-core才是紧凑的 model/tool loop;它不负责多 channel 路由和长期运行治理。- per-agent SQLite 是 session runtime 与 transcript event 的权威存储;
sessions.json是旧版迁移源,archive 文件是归档产物。
代码层
src/gateway/server-methods/chat.ts
→ chat-send-handler.ts::handleChatSend
→ auto-reply/dispatch.ts::dispatchInboundMessage
→ auto-reply/reply/get-reply-run.ts
→ auto-reply/reply/agent-runner-execution.ts
→ agents/agent-command.ts
→ agents/command/prepare.ts
→ agents/command/attempt-execution.ts
→ agents/embedded-agent-runner/run-orchestrator.ts
→ agents/embedded-agent-runner/run/attempt.ts
→ agents/sessions/agent-session-prompting.ts
→ packages/agent-core/src/agent-loop.ts
→ agents/sessions/sdk.ts::streamSimple
一条真实请求链
1. 入口与 ACK
chat.send 注册到 handleChatSend。handler 先规范化请求、准备 session、执行 pre-admission 与 admission。对 restart-safe turn,它在 ACK 前持久化 user turn,并验证 session row 已是 running、delivery run id 与本次 runId 一致。随后立即返回:
{ runId: clientRunId, status: "started" }
耗时任务在 detached dispatch 中继续。这里的关键不是异步本身,而是 ACK 与 durable admission 的顺序:客户端先拿到可追踪的 runId,而 Gateway 已经留下恢复和去重所需的状态。
证据:
src/gateway/server-methods/chat-send-handler.ts:69-145src/gateway/server-methods/chat-send-handler.ts:185-247src/gateway/server-methods/chat-send-handler.ts:249-301src/gateway/server-methods/chat-send-handler.ts:388-543
2. session、workspace、model 与配置
prepareAgentCommandExecution 读取 canonical config,校验 agent id 和 session key 的一致性,解析 session,确定 workspace / agentDir,载入 plugin metadata,选择默认 model 与 thinking 配置,并生成稳定的 runId。这一步把“用户发来的文本”升级为可执行 run envelope。
证据:
src/agents/command/prepare.ts:110-216src/agents/command/prepare.ts:245-330src/agents/command/prepare.ts:346-405
3. admission、delivery 与恢复所有权
agentCommand 在真正执行前占有 session work admission,重新检查 session 是否仍指向预期 sessionId,并准备 delivery。运行状态写入和 restart-recovery claim 发生在进入 embedded runner 之前;无论成功、失败还是 abort,finally 都释放 admission 与 lease。
证据:
src/agents/agent-command.ts:170-220src/agents/agent-command.ts:238-337src/agents/agent-command.ts:367-455
4. 两层队列与 session 写锁
runEmbeddedAgent 先进入 per-session lane,再进入 global lane。前者避免同一 session 的 tool / transcript 竞争,后者用于限制全局并发。attempt 内部还建立 session write lock;SQLite commit 是同步事务段,外层队列负责协调。
证据:
src/agents/embedded-agent-runner/run-orchestrator.ts:103-183src/agents/embedded-agent-runner/run/attempt.ts:46-169src/agents/embedded-agent-runner/run/attempt-session-lock-prepare.ts- 官方 Agent Loop 文档的 Queueing and concurrency
5. prompt 与 tool catalog
attempt 依次准备 skills、core/plugin tools、bootstrap files、MCP / LSP bundle tools、tool catalog 和 system prompt。system-prompt.ts 对 workspace 文件有确定排序:AGENTS.md、SOUL.md、IDENTITY.md、USER.md、TOOLS.md、BOOTSTRAP.md、MEMORY.md。tool policy 先决定模型能看到什么,sandbox 再决定允许的工具在哪里执行。
证据:
src/agents/embedded-agent-runner/run/attempt.ts:130-299src/agents/embedded-agent-runner/run/attempt-system-prompt-prepare.ts:214-317src/agents/system-prompt.ts:66-84src/agents/system-prompt.ts:195-256docs/gateway/sandbox-vs-tool-policy-vs-elevated.md:8-12docs/gateway/sandbox-vs-tool-policy-vs-elevated.md:56-74
6. 真正的 model call
prepareEmbeddedAttemptAgentSession 构造 AgentSession,传入 model、tool allowlist、自定义工具、SessionManager 与写锁。SDK 内的 Agent.streamFn 在每次调用时取得 auth,再调用:
modelRegistryRuntime.llmRuntime.streamSimple(modelResult, context, options)
这才是 provider 边界。OpenClaw 在它外面安装 context transform、prompt-cache、hook、tool-result 截断和 transport 包装。
证据:
src/agents/embedded-agent-runner/run/attempt-session.ts:81-171src/agents/sessions/sdk.ts:294-358src/agents/sessions/sdk.ts:406-488
7. model → tool → result → 下一轮
AgentSession.prompt() 构造 user message 后调用 agent.prompt()。packages/agent-core/src/agent-loop.ts 把历史转换为 provider messages,调用 stream function,解析 assistant 的 toolCall,校验参数,执行工具,把 ToolResultMessage 追加回 context;只要还有 tool call 或 queued message,循环再次调用模型。
工具可以顺序或并行执行;并行分支仍按模型原始 tool-call 顺序把结果写回 transcript,避免完成顺序改变对话语义。
证据:
src/agents/sessions/agent-session-prompting.ts:25-47src/agents/sessions/agent-session-prompting.ts:101-242packages/agent-core/src/agent-loop.ts:270-449packages/agent-core/src/agent-loop.ts:456-557packages/agent-core/src/agent-loop.ts:563-800packages/agent-core/src/agent-loop.ts:899-1115
8. 事件、持久化与输出
Agent loop 在每个 user / assistant / toolResult 的 message_end 发事件。AgentSessionBase 在同一事件路径调用 sessionManager.appendMessage();Gateway 的 subscriber 同时把 assistant、tool 和 lifecycle 投影为上层 stream。Gateway 不再把 agent final 重复镜像进 transcript,因为 agent runtime 已拥有 model-visible persistence。
当前 per-agent database 路径是:
~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite
其中 sessions / session_entries 保存运行态,transcript_events 保存逐事件 transcript,transcript_event_identities 提供 event identity 与 message idempotency。旧 sessions.json 只作为迁移源。
证据:
src/agents/sessions/agent-session-base.ts:296-395src/agents/embedded-agent-runner/run/attempt-stream-prepare.ts:213-240src/gateway/server-methods/chat-send-handler.ts:590-596src/state/openclaw-agent-db.paths.ts:19-31src/state/openclaw-agent-schema.sql:30-73src/state/openclaw-agent-schema.sql:161-178src/state/openclaw-agent-schema.sql:240-303src/config/sessions/session-accessor.sqlite-transcript-write.ts:69-75docs/concepts/session.md:139-161
五个设计判断与取舍
1. 先 durable admission,再快速 ACK
- 收益:断线重试可以凭
runId与 idempotency 状态接管,避免重复提交。 - 成本:入口 handler 复杂,ACK 前仍有一段同步关键路径。
- 失败方式:持久化、routing generation 或 lifecycle claim 不一致时必须拒绝,而不是“先回 started 再说”。
2. Gateway 集中控制,agent-core 保持紧凑
- 收益:channel、身份、恢复、delivery 不污染 model/tool loop;loop 可被其他 harness 复用。
- 成本:阅读真实链路要跨多层文件;Gateway 成为较大的故障域。
- 适用:一个 agent 跨多 channel、设备和长期任务。
- 不适用:只需要单次 CLI tool loop 的小程序,可能承受了过重控制面。
3. session lane + global lane + write lock
- 收益:同 session 的副作用与 transcript 顺序可推理,跨进程写也有保护。
- 成本:高并发时会产生队头阻塞;锁等待、compaction 和长工具调用需要额外诊断。
- 失败方式:锁、lane 和 restart ownership 任一层遗漏都可能带来重复 delivery 或状态漂移。
4. 能力、位置和越权分开
- 收益:tool policy、sandbox、elevated 分别回答“能不能调用、在哪里运行、exec 能否出隔离层”。
- 成本:配置组合多,运维者必须理解优先级;
deny赢,非空allow会封闭其余工具。 - 重要边界:允许
exec后,拒绝write并不能让 shell 变成只读。
5. SQLite 权威状态 + archive artifact
- 收益:事务、索引、幂等、状态查询和迁移都比一个全局 JSON map 更稳;每个 agent 有独立数据库。
- 成本:迁移、doctor、数据库维护和跨版本兼容变成产品责任。
- 失败方式:数据库并不自动消除逻辑级 recovery loop、长事务和 event-loop 阻塞。
局限与当前故障证据
- 没有统一的
maxTurns/maxToolCalls硬上限。 Issue #9912 仍为 open,并被标为高置信源码复现;模型可能重复调用工具,单靠 prompt 不是硬边界。 - 恢复机制本身也需要预算。 Issue #95750 记录了跨重启 retry budget 缺失导致 gateway death loop 的场景;截至研究日仍 open,存在候选修复 PR。
- 集中控制面扩大故障半径。 Gateway 串起 channel、session、tool、delivery 和 recovery;它带来一致性,也让锁、队列、数据库维护和 event loop 彼此影响。
- 默认便利不等于多租户安全。
dmScope: "main"适合单一 owner 跨入口连续使用,不适合共享收件箱;官方建议多用户时使用 peer 隔离。 - sandbox 不是默认同义词。 main session 可能直接在 host 执行;workspace 只是 cwd;elevated 也不等于获得所有工具。
这些 issue 是现场证据,不等于本文对所有安装都做了复现;正文只把它们用于说明仍然存在的工程成本。
代表性源码片段选择
正文使用 packages/agent-core/src/agent-loop.ts:343-397 的缩短片段。理由:
- 它直接证明一次 assistant response 如何变成 tool call;
- tool result 如何进入 context;
- 下一轮如何获得更新后的 model / thinking / context;
- 不需要用伪代码假装读过 runtime。
正文会明确说明:这只是内核循环,Gateway 在外层负责 admission、session、并发、持久化与 delivery。
参考资料
- 官方仓库
- 钉住的源码快照
- Agent Loop
- Session management
- Agent runtime
- Gateway
- Sandbox vs tool policy vs elevated
- npm
openclawpackage - Issue #9912:maxTurns / maxToolCalls
- Issue #95750:cross-boot recovery budget
写作禁区
- 不把
SOUL.md写成 runtime; - 不把 workspace 写成 sandbox;
- 不把 tool policy 写成副作用分析器;
- 不把 SQLite 写成“所有恢复问题已解决”;
- 不把
main快照功能写成 npm stable 的既成事实; - 不把 issue 报告写成作者亲自复现的 benchmark。