Clawd-on-desk Agent Runtime 架构深度解析:从 Hook 事件到桌宠状态机的完整数据链路
Clawd-on-desk Agent Runtime 架构深度解析:从 Hook 事件到桌宠状态机的完整数据链路
Clawd-on-desk 是一款把 Claude Code、Codex、Cursor 等 AI 编程 Agent 的运行状态实时映射到桌面像素宠物的开源应用。本文以 docs/project/agent-runtime-architecture.md 为骨架,结合仓库源码,系统拆解其 Agent Runtime 架构:本地 HTTP 服务如何承接来自二十余种 Agent 的 hook/plugin 事件,state.js 状态机如何做多会话追踪与优先级仲裁,权限决策、会话标题、Recap 本地投影与进程链元数据如何协同。读完本文,你将掌握 Clawd-on-desk 从「Agent 触发事件」到「桌宠呈现状态」的完整链路,以及每种集成形态(command hook、HTTP hook、in-process plugin、JSONL 轮询)的取舍与边界。
一、整体架构:一条统一的本地状态总线
Clawd-on-desk 的运行时核心是一个监听本机回环地址的 HTTP 服务(默认 127.0.0.1:23333),由 src/server.js 负责监听、端口与组合,src/server-route-state.js 与 src/server-route-permission.js 分别处理 /state 与 /permission 两条路由。所有 Agent 集成最终都汇聚到同一条数据流:
Agent 触发事件
→ hook / plugin 脚本(解析 stdin JSON / BusEvent)
→ HTTP POST 127.0.0.1:23333/state { state, session_id, event, ... }
→ src/server.js HTTP 壳 → src/server-route-state.js → src/agent-runtime-main.js → src/state.js 状态机
→ IPC state-change 事件
→ src/renderer.js(<object> SVG 预加载 + 淡入切换 + 眼球追踪)
从源码结构看,src/agent-runtime-main.js 是官方 hook / 本地 monitor 仲裁的 owner:它负责 official hook 与 JSONL monitor 的事件级 suppression(避免重复状态/重复气泡),并持有 Codex turn fence、Codex official activity、Qoder 与 WorkBuddy 会话标题 tracker 等运行时子系统。而 src/state.js 是真正的状态机实现——多会话追踪、优先级排序、最小显示时长与睡眠序列都在这里完成。
1.1 每种 Agent 的集成形态
仓库把 Agent 集成划分为四种形态,各有明确的传输与阻塞语义:
| 形态 | 代表 Agent | 关键特征 |
|---|---|---|
| command hook(非阻塞) | Claude Code、Copilot、Cursor、Gemini、Antigravity、Kiro、CodeBuddy、Grok、WorkBuddy、QwenWork、TraeCode、MiniMax、Kimi、ZCode | 脚本从 stdin 读 JSON,fire-and-forget POST /state,stdout 输出 {} 不接管权限 |
| HTTP hook(阻塞) | Claude Code / CodeBuddy 的 PermissionRequest、Codex official PermissionRequest、ZCode Phase 2 | 脚本挂起等待 /permission,拿到人工决定后写 stdout 返回 |
| in-process plugin | opencode、MiMo Code、Pi、OpenClaw、Hermes、DeepSeek Harness | 插件跑在宿主进程内,~0ms 延迟,经事件总线转发 |
| JSONL 轮询(fallback) | Codex CLI | official hook 未覆盖的事件、hook 禁用/不可用、历史兼容时启用 |
1.2 Claude Code:主数据链路与 PostToolBatch 仲裁
Claude Code 是主链路,对应 hooks/clawd-hook.js(零依赖 Node 脚本,stdin 读 JSON 取 session_id + source_pid)与 agents/claude-code.js(事件映射表)。事件映射定义了桌宠的 11 种显示状态:
// agents/claude-code.js(节选)
eventMap: {
SessionStart: "idle",
UserPromptSubmit: "thinking",
PreToolUse: "working",
PostToolUse: "working",
PostToolUseFailure: "error",
Stop: "attention",
SubagentStart: "juggling",
PreCompact: "sweeping",
Notification: "notification",
WorktreeCreate: "carrying",
}
文档特别强调 Claude 的 live tool/model phase 额外使用 PostToolBatch(保守基线 2.1.280+)。这条规则的实际实现位于 src/claude-tool-phase.js 与 hooks/claude-tool-batch.js:hook 只发送有界的 tool IDs 和 prompt_id,省略 inputs、responses 与进程探针;claude-tool-phase.js 维护一个有界的内存主会话台账,/state 在权限清理前观察它,并把内部决策传给 state.js,同一事件不会被消费两次。关键仲裁语义包括:
- 只有 batch 提示被整体拒绝;未退役的 prompt id 落在普通 tool hook 上、且当前无 turn 打开时,可以在没有 Submit 的情况下建立一个排队 turn;
- 一个未见过的 id 在 turn 打开时只禁用 batch 推断、不退役当前 prompt,其自身的 Stop 仍正常完成;
- 已退役的 hook 与已定局的成功 tail 只能注释既有标题/模型/上下文元数据,不能改变 phase 或存活状态;
- batch 永远不能替代待决的审批或存活中的 subagent 提示,其 recovery/history 分类刻意留空,防止迟到的 phase 提示重新打开持久记录;
AskUserQuestion的 transcript 完成探针在批处理被接受后仍可存活,可在缺少 Stop 时收束 thinking phase。
这种「有界证据 + 精确匹配」的仲裁设计,保证了多来源事件(hook 与 JSONL)不会导致状态重复或错误回退。
1.3 Codex CLI:official hooks 为主 + JSONL fallback 的双通道
Codex 是集成复杂度最高的 Agent 之一,核心注册表配置见 agents/codex.js,hook 实现见 hooks/codex-hook.js:
- official hooks 为主通道:
SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / Stop经 stdin JSON 进入,session_id优先与transcript_path的 rollout UUID 对齐(防御性提取); - JSONL 轮询为 fallback:agents/codex-log-monitor.js 以 1500ms 间隔增量轮询
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl,覆盖 hook 未覆盖的事件(如response_item:web_search_call——official hooks 不覆盖 WebSearch,JSONL 是其唯一 lifecycle/tool 边界)。本地 JSONL 路径不经过 HTTP server; - 事件级 suppression:
agent-runtime-main.js对 hook-active session 做事件级 suppression,CODEX_OFFICIAL_LOG_SUPPRESS_TTL_MS = 10 分钟,避免重复状态与重复气泡。
Codex 压缩完成的兼容处理见 hooks/codex-log-event.js:同时兼容旧 event_msg:context_compacted 与新 event_msg:item_completed(payload.item.type === "ContextCompaction"),归一化到旧事件键后沿用 sweeping 映射与 timestamp/backfill 保护;它不是 turn completion,也不清理待回答问题。
会话标题通道:本机 Codex 会话标题由 JSONL monitor 每轮为已观察到生命周期的会话合并读取一次 session_index.jsonl(512 KiB tail 上限),新标题/改名以 session_index:title 送入 updateSessionMetadata(expectedAgentId: "codex"),即使 rollout 未增长也刷新 HUD/Dashboard;不创建会话、不改变状态/活跃时间/完成提醒/小结,不清空已有标题。尚未生成原生标题时使用既有文件夹 fallback,用户别名始终优先。
本地归档生命周期(#655):src/codex-archive-tracker.js 是独立于 JSONL 内容解析的归档证据 tracker,与本地 Codex runtime 同启同停。它只在本地 CODEX_HOME 的 archived_sessions 里寻找 regular rollout-*.jsonl,用文件名推导 canonical UUID 并与文件头部有界 session_meta(payload.id / payload.session_id 必须一致且等于文件名 id)校验,再对同一 path 做读后快照复核。关键设计:
- 归档文件消失是 unarchive 证据;截断/损坏/冲突或 id 不匹配的元数据从不构成归档证据,不会据此退役;
- 目录不可列、stat/read 的 EACCES/EPERM/EIO 等 I/O 错误与被中断的扫描只算 UNKNOWN:保留既有 suppression,绝不据此退役;
- 每轮只做一次异步 readdir,只对当前 live 候选读 metadata(每轮至多一个 batch),不为无关历史归档预先索引;
- evidence 与失败指纹缓存都有 LRU 上限,未变化的坏 live 指纹按指数退避跳过读取;多个 live 超过 batch 时用游标跨轮公平推进;
- 轮询基准是 5s,但 batch 积压或文件 I/O 都会增加延迟,因此不宣称普遍 ≤5s;
CODEX_HOME在 tracker 实例生命周期内按启动时解析,运行时改动需重启生效。
1.4 Windows 固定入口与安全演进(#986)
本机 Codex 注册使用每个 CODEX_HOME 下固定的分平台入口。Windows 的固定 commandWindows 使用 PowerShell call-operator 直连:& "node" "codex-hook.js" --clawd-windows-stable。hook 进程启动时自读 UTF-8/Base64 clawd-hooks/codex-hook.js.windows.run 数据 sidecar 注入 env——2026-09-04 起由内联 PowerShell dispatcher 改为直连,原因是原 dispatcher 的「解码并执行」命令行被 Windows Defender ML 判为 Trojan:Win32/Commando.A!ml(见 clawd-on-desk#986),且不再落地或二次启动 .ps1。hooks/codex-hook.js 中的 applyWindowsStableSidecarEnv() 实现了这一逻辑:校验签名行、Base64 往返解码、target binding(防止无关/过期 sidecar 给另一份 hook 供 env),并限定仅在 platform === "win32"、argv 含 --clawd-windows-stable、不含 WSL interop 参数且非 remote 时生效。POSIX 使用 clawd-hooks/codex-hook.js.sh 与对应 manifest,只原子更新受管 wrapper,不改 hooks.json 的命令字符串。Doctor 按 Codex 官方归一化 handler 的 SHA-256 精确核对 trusted_hash。
二、全 Agent 数据流速查
文档的 Data Flow 章节覆盖了所有已接入的 Agent,按集成形态分组整理如下(均可溯源到对应 hook/agent 模块):
2.1 Command Hook 家族
| Agent | Hook 脚本 → 映射模块 | 事件/状态特征 |
|---|---|---|
| Copilot CLI | hooks/copilot-hook.js → agents/copilot-cli.js | camelCase 事件名;PermissionRequest 阻塞 POST /permission |
| Cursor Agent | hooks/cursor-hook.js → agents/cursor-agent.js | hook_event_name → PascalCase + POST;beforeSubmitPrompt stdout 为 {continue:true},其余为 {},不接管权限 |
| Gemini CLI | hooks/gemini-hook.js → agents/gemini-cli.js | hook-only,stdin JSON + stdout JSON |
| Antigravity CLI (agy) | hooks/antigravity-hook.js → agents/antigravity-cli.js | 注册到 ~/.gemini/config/hooks.json 的 clawd hook group,仅状态事件;PreToolUse 故意不注册,权限交给 agy 自带 5 选项 native menu |
| Kiro CLI | hooks/kiro-hook.js → agents/kiro-cli.js | Kiro 无 global hooks,hook 注入到 ~/.kiro/agents/ 下每个 custom agent 配置;需 kiro-cli --agent clawd 或 /agent swap clawd 启用 |
| CodeBuddy | hooks/codebuddy-hook.js → agents/codebuddy.js | PascalCase,Claude Code 兼容格式;阻塞式审批走 PermissionRequest HTTP hook |
| Grok Build | hooks/grok-hook.js → agents/grok-build.js | 注册到 <GROK_HOME 或 ~/.grok>/hooks/clawd-on-desk.json;经 src/grok-turn-fence.js 有界内存 turn fence 仲裁;state + Notification only |
| WorkBuddy | hooks/workbuddy-hook.js → agents/workbuddy.js | PascalCase,Claude Code 兼容;state + Notification only,审批留在原生沙箱与 GUI |
| QwenWork | hooks/qwenwork-hook.js → agents/qwenwork.js | 注册到 ~/.QwenWorkCN/settings.json;PermissionRequest/PermissionDenied 仅观察映射成 working(每任务 40+ 次),只发送 tool_input 的 sha1 fingerprint |
| TraeCode | hooks/traecode-hook.js → agents/traecode.js | 注册到 ~/.trae-cn/hooks.json;Windows 用无引号外壳 PowerShell -EncodedCommand;无 SessionEnd,靠 traecode-desktop-idle-timeout 退役 |
| MiniMax Code | hooks/minimax-hook.js → agents/minimax.js | 本地插件目录 <MINIMAX_DATA_DIR 或 MAVIS_DATA_DIR 或 ~/.minimax>/plugins/clawd-state/;handler 固定 exec-form,timeout=2 秒(MiniMax 只接受 1–10 整数秒),阻塞式人工审批物理不可行 |
| Kimi Code CLI | hooks/kimi-hook.js → agents/kimi-cli.js | 注册到 ~/.kimi/config.toml 的 [[hooks]] 条目;Clawd 启动时自动同步 |
| ZCode | hooks/zcode-hook.js → agents/zcode.js | 注册到 ~/.zcode/cli/config.json 的 hooks.events.*(7 个事件全注册);Phase 2 起 PermissionRequest 长阻塞 POST /permission(等待 590s,installer 注册 per-hook timeoutMs 600000),有决定时 stdout 返回最小 hookSpecificOutput,无决定/超时/断连输出 {} 并 exit 0 回退原生流程 |
一个值得注意的通用安全模式:launchedByGrok() 守卫(见 hooks/cursor-hook.js 顶部)——Grok 默认扫描 ~/.claude/settings.json,因此 clawd-hook.js / cursor-hook.js / auto-start.js 只在非空 GROK_HOOK_EVENT 下直接退出(输出 {}),避免产生假会话。
2.2 In-process Plugin 家族
| Agent | 插件入口 | 特征 |
|---|---|---|
| opencode | hooks/opencode-plugin/index.mjs | CLI/TUI 跑在 Bun,Desktop sidecar 跑在 Electron utilityProcess/Node;session.created 的 parentID 记录为 child→parent 映射,child 状态上报带 headless: true |
| MiMo Code | hooks/mimocode-plugin/index.mjs | 与 opencode 同源事件词汇;跑在 mimo.exe 进程内,共享 @mimo-ai/plugin SDK |
| Pi | hooks/pi-extension.ts + hooks/pi-extension-core.js | global extension,目录 ~/.pi/agent/extensions/clawd-on-desk;state-only,不等待 /permission |
| OpenClaw | hooks/openclaw-plugin/index.js | plain ESM default object,OpenClaw plugin loader 直接识别;Phase 1 只上报状态 |
| Hermes | hooks/hermes-plugin/init.py | Python plugin,跑在 Hermes worker 进程内;同步 POST(避免短命 hermes -z 进程退出前丢事件),Clawd 未启动时有短 cooldown |
| DeepSeek Harness | hooks/dsh-clawd-bridge/ | Node ESM plugin,每个 session 独立 FIFO POST 动态发现的 127.0.0.1:23333-23337/state;src/dsh-state-sequence.js 用持久 event.seq / 独占 session.seq watermark 拒绝 stale、duplicate 与 dispose 后 late event |
2.3 远程与 WSL
- 远程 SSH(反向端口转发):远程服务器上的 Claude Code / Codex CLI 的 secure hooks 只 POST 到 profile pin 住的远端转发端口,SSH 隧道落到该 profile 的临时本地 ingress;ingress 校验 routing nonce 并写入
profileIdcanonical namespace,带CLAWD_REMOTE=1+CLAWD_SSH_REMOTE=1,跳过远端 PID 聚焦。secure identity 缺失/损坏时 fail closed,不回退 23333-23337 扫描。 - WSL(本机 loopback,但 PID 属于 Linux VM):WSL 里的 hook 用 Linux
ps解析进程字段,又经127.0.0.1发到 Windows Clawd;Windows 打开进程时忽略 PID 低两位。服务端按请求自身标记(wsl_distro非空或host: "wsl:<distro>")剥离与 Remote SSH 完全相同的进程字段(sourcePid / wtHwnd / agentPid / pidChain / editor / tmuxSocket / tmuxClient),保留orcaPaneKey / cwd / host / wsl_distro。因此 WSL 会话没有按进程退出的清理,只按空闲超时清除。
三、会话标题:多级来源与优先级契约
文档用一个清晰的优先级链定义会话标题来源(以 Claude Code 为例):
hook 输入的
session_title(手动改名)→ transcript 里的手动标题(custom-title/agent-name,取最后一条有效者,不按会话过滤)→ AI 标题(ai-title,取本会话最新一条有效者)→ 仅UserPromptSubmit且以上都没有时用消息首行兜底。
消息首行兜底会沿 body 上报 session_title_from_prompt: true;服务端不让它覆盖已有的正式标题(手动改名、AI 标题、metadata-only 写入的、重启恢复的),metadata-only 请求里的标题一律算正式标题并忽略该标记;消息首行派生的标题不写入会话历史和恢复记录。首行提取规则实现在 hooks/clawd-hook.js 的 extractPromptTitle():取第一条非空行、命中密钥正则(PROMPT_TITLE_SECRET_RE,覆盖 api_key/authorization/bearer/password/secret/token、sk-、ghp_、AKIA 等形态)不给标题、最多 40 字。
围绕标题派生,还有三类专门实现:
- Qoder:src/qoder-session-title.js 对 SessionStart / UserPromptSubmit / Stop 做异步增量读取(工具、权限、通知事件不触发扫描,显式标题也不触发 I/O);仅处理 enabled、本机且非 WSL 的会话;每个会话串行读取,FileHandle 始终在 finally 中关闭,读取中断时逐 chunk 保持 offset 与 partial 一致。
- WorkBuddy:
hooks/workbuddy-hook.js转发transcript_path并标记 prompt 首行 fallback;src/agent-runtime-main.js 持有workbuddy-session-titleobserver(见createWorkBuddySessionTitleTracker),读取匹配的sessions行,优先custom_title→title;根目录依次为绝对WORKBUDDY_CONFIG_DIR、~/.workbuddy-ai、~/.workbuddy,拥有 transcript 的 home 优先;cwd 不匹配与超过 4 KiB 的标题被拒绝。数据库只读打开、读后关闭,绝不写库内容或 settings,缺失的 home 从不创建。SQLite 不可用时回退 src/jsonl-session-title.js 增量读取(每扫描最多 1 MiB 新字节、保留有界 partial 行);只有 SQLite 能报告 archive/delete 生命周期。 - Cursor:hooks/cursor-session-title.js 只读标准 Cursor desktop profile 的
User/globalStorage/state.vscdb,按 conversation ID 查composerHeaders、旧ItemTable['composer.composerHeaders']或cursorDiskKV['composerData:<id>']中的name;单条 JSON record 最多 1 MiB,数据库缺失/损坏/锁定/未知 schema 均不阻塞状态 hook;node:sqlite自 Node 22.13 / 23.4 起无需 flag。
此外,TraeCode 与 MiniMax Code 没有原生会话标题字段,采用「从首次 prompt 首行派生并保持首个标题」的 server 端 first-wins 策略。
四、本地会话历史与恢复
~/.clawd/session-history-v1/ 是独立的本机会话索引,与 session-recovery-lease.js 的进程存活条件严格分离:
- lease 用于恢复仍在运行的状态(
hooks/session-recovery-lease.js),history 用于在进程退出/重启后找到可手动继续的旧会话(hooks/session-history.js);历史行本身不是 live session,不进状态机、HUD、recap 或权限自动化; - Claude command hook 在 POST 前 best-effort 写入历史(Clawd 离线也能记录),只覆盖本机交互式 Claude Code;保存 session ID、cwd、显式标题、状态和时间,不保存 prompt 派生标题、回复或工具内容——它是索引,不是 transcript 备份;目录/文件权限为 0700 / 0600(POSIX);
- 有效记录按 30 天 / 200 条清理;Dashboard 主列表最多展示 25 条已确认(transcript 在且 cwd 现存)的行,未确认可恢复的行进默认收起的折叠组;每条记录复用 lease 的跨进程锁,锁内读取、合并并原子替换;同毫秒 terminal 事件优先;
- src/session-history-loader.js 除生成显示标题外不读取内容:没有显式标题时,读取探测定位到的 transcript 的第一条用户输入,用 live prompt 标题的同一套规则生成标题,只用于显示、不写入历史;提取结果按 transcript 路径 + mtime + size 缓存;
- src/session-history-runtime.js 是唯一手动恢复 owner:只接受受信任 Dashboard 发来的 agent / session ID,在 main 重读已保存 cwd,并重新检查已安装、已启用和本机 live 状态;remote / WSL / 其他 agent 的同名 ID 不应误挡本机恢复;同一 session 的并发恢复合并为一次请求;main 保留 30 秒确认窗口,超时只允许用户检查终端后手动重试,不自动重试,也不伪造 live 状态。
五、本地权限 HTTP 边界(Local Permission HTTP Boundary)
POST /permission 是原生 hook/plugin 接口,src/server.js 在读取 body 或记录 hook 事件之前执行严格请求校验:
- 拒绝任何
Origin头(包括空或null); - HTTP Host 必须是显式
127.0.0.1、localhost或[::1](可带合法端口); - 媒体类型必须是
application/json(允许charset=utf-8等参数); - 重复的 Host 或 Content-Type 字段被拒绝;Forwarded 头不授予访问权;OPTIONS 不启用跨域 preflight。
这些是浏览器请求防护:仅靠 loopback 绑定与 CORS 响应限制并不能阻止简单的跨源 POST 创建审批 UI。被拒绝的请求收到空 400/403/415 响应并关闭连接,没有 Clawd 成功标记或 agent 审批/拒绝;原生 hook 保留自己的 no-decision fallback。文档明确说明该机制不认证不受限的同用户进程(它们可以构造合法头);/state 路由不在这个 permission 专属防护范围内。回归证据使用真实 HTTP 路由器与权限所有权模块,见 test/server-permission-ingress.test.js。
六、Recap 本地投影:隐私边界内的活动归因
Recap 是已接受运行时活动的本地投影,而不是 HTTP 或 updateSession() 入口处的第二个观察者。在 agent gates、Codex source/replay 仲裁、权限来源处理、subagent 过滤与完成仲裁全部落定后,src/state.js 把接受的边界通过 src/recap-metrics.js 映射,并向 src/recap-runtime.js 发送 allowlist 的 canonical 事件。
- src/recap-journal.js 冻结桌面民用时间,并在追加 14 天 ticket 前把稳定的 scope/session/dedupe 身份替换为安装本地 HMAC;
- 同一规范化记录更新 src/recap-aggregate.js;src/recap-coverage.js 独立记录 Clawd 何时能接收信号;
- 每日聚合与 coverage 在
~/.clawd/recap-v1/下限制到 400 个本地日; - 查询 IPC 只返回宽泛的
local/wsl/remotescope 类别,绝不返回 HMAC 值、profile ID 或分布名; - 启动时在有界 event-loop 批次中重建 14 天聚合;不支持的 pre-release aggregate/coverage schema 被隔离而不是迁移;
- DND 仍是交互/视觉 gate,不停止 recap 或 coverage;Suspend、进程关闭与
recapEnabled=false会关闭 coverage。
完整指标、隐私与 DST 契约见 docs/guides/recap.md。
七、运行时所有权边界(Runtime Ownership Boundaries)
src/main.js 是 composition root,不再是各子系统的实现 owner。新增或修改行为时应先进入对应 owner,避免把逻辑重新堆回 main.js:
| 边界 | Owner |
|---|---|
HTTP /state / /permission |
src/server-route-state.js / src/server-route-permission.js;src/server.js 负责监听、端口与组合 |
| official hook / local monitor 仲裁 | src/agent-runtime-main.js,配合 src/codex-turn-fence.js / src/codex-official-activity.js |
| 双窗口与浮层 | src/pet-window-runtime.js 创建/定位 render + hit window;src/floating-window-runtime.js / src/topmost-runtime.js 管浮层重排与 z-order |
| Settings 写入与副作用 | settings-controller 是唯一写入者;settings-actions* 是 pre-commit gates;settings-effect-router 是 post-commit runtime effects |
| Settings UI | settings-ui-core 持有 shared UI state,settings-renderer 是侧栏/tab shell,业务页在 settings-tab-* |
| Quota reminders | src/quota-alerts-runtime.js 读取按来源分离的账户快照;quota-alerts 拥有有界哈希去重历史;quota-notifications 确认原生投递 |
| Theme | src/theme-loader.js 是 stateless loader;src/theme-runtime.js 是唯一 active-theme owner |
两个跨模块契约值得注意:其一,state.js 的 session snapshot 是共享 schema——Dashboard、Session HUD(含 Orbit quota ring)以及可选 Telegram completion、Discord presence、LAN PWA 等 consumer 都读取它,新增/重命名/删除字段必须检查全部 consumer;其二,Quota reminders 保持默认禁用,使用既有 quota collection,启动后要求每个窗口 fresh confirmation,且从不发起账户请求——完整阈值、恢复与保留见 docs/guides/quota-reminders.md。
八、多 Agent 注册表与能力声明
agents/registry.js 把 26 个 Agent 配置模块聚合成注册表,提供按 ID 查找(getAgent)与按平台收集进程名(getAllProcessNames / getStartupRecoveryProcessNames,Windows 优先 win,Linux 回退 mac)。每个 agent 模块导出事件映射、进程名与能力声明,例如:
- agents/claude-code.js:
capabilities: { httpHook, permissionApproval, notificationHook, sessionEnd, subagent } - agents/codex.js:
eventSource: "hook+log-poll",sessionEnd: false(无 SessionEnd 事件,task_complete标记 turn 结束、进程退出清理会话),logConfig: { sessionDir: "~/.codex/sessions", filePattern: "rollout-*.jsonl", pollIntervalMs: 1500 } - agents/codex-log-monitor.js:JSONL fallback 增量轮询器(文件监视 + 增量读取 + 状态/metadata fallback,不再做审批猜测)
- agents/gemini-log-monitor.js:legacy Gemini session JSON 轮询器,当前 hook-only 路径不启动
运行时的安装意图/启停/权限气泡开关经 src/agent-gate.js 读取 prefs.agents[id].integrationInstalled / .enabled / .permissionsEnabled。语义上:enabled 只表示是否处理该 agent 的事件(关闭会让 state.js / server.js 停止处理、清理 session/bubble);integrationInstalled 才表示本机 hook/plugin/extension 是否由 Clawd 维护。snapshot 缺字段时 gate 保守默认 true 以兼容旧版;新安装 schema 显式把 Claude Code / Codex 设为已安装且启用,其余 agent 未安装且未启用。Claude Code 额外有 .subagentPermissionsEnabled 子开关(#451),控制 Task 子 agent 的 PermissionRequest 是否弹泡泡。
动态 custom HTTP Agent 是上述安装模型的明确例外:customApplications 是注册真相,v1 为 state-only,/permission 恒不返回 Allow/Deny,也不创建权限 bubble;已注册 custom 的权限请求返回 204 no-decision,删除/伪造的 custom- ID 直接拒绝,不能降级成 Claude Code subagent。server-hook-events.js 的 recent-event ring 按已解析 agent ID 分桶,非法 custom- identity 写入固定 rejected-custom 桶(原始 ID 最多保留 80 字符),ring 不写 prefs、重启即清空。
九、Hook 与 Plugin 同步
启动链路只自动补齐 integrationInstalled=true 且 enabled=true 的缺失集成;若 prefs 文件不可读(locked && recovered),内存 snapshot 只是非权威 defaults fallback,整条 prefs-backed agent runtime gate fail closed——本次进程不自动同步集成、不启动 monitor、不接受 state/permission ingress、不恢复旧 session。server.js 启动后异步同步已安装且已启用的 Claude / Codex / Copilot / Gemini / Antigravity / Cursor / CodeBuddy / WorkBuddy / Kiro / Kimi / Qwen / ZCode / CodeWhale / Qoder / QoderWork / QwenWork / Reasonix hooks,以及 opencode / MiMo Code / OpenClaw / Hermes / DeepSeek Harness plugins 和 Pi / OMP extension。Settings Agent 页的 Install 执行对应 sync 并一起提交 integrationInstalled=true, enabled=true;Uninstall 调用 marker-scoped 卸载器并提交 false, false。
9.1 Claude hook 健康巡检与自愈(#657)
src/claude-settings-watcher.js 在原有目录 watcher(盯 ~/.claude/、debounce 1 秒)之外,跑一个自调度的低频只读健康巡检(默认周期 5 分钟,不依赖任何 settings.json fs 事件——hook 脚本在其他目录被删除也能发现):
- 判断逻辑收敛在 src/claude-hook-health.js 的
inspectClaudeHookHealth():解析 command、校验 nodeBin/scriptPath、比对hooks/install.js的resolveClaudeHookPaths()给出的 command target 与CLAUDE_CORE_HOOK_EVENTS,复用 Doctor 的agent-node-bin-parser.js解析器;resolver 是 total 的只读函数,任何 I/O/plan/环境错误都返回结构化{ok:false, reason, message},绝不 throw; - 可自动修复的问题经 src/claude-hook-operations.js 的实例级队列串行 repair,repair 后重新读盘用同一 inspector 复验;同一 repair signature 连续 3 次修复+复验失败后进入
manual-fix-required,停止自动 mutation,只保留 5 分钟只读复查; - source 与 target 是两个真相:source 是当前安装包/repo 的
asarUnpackedPath()脚本,target 是 settings command 应指向的路径;source 入口或依赖闭包缺失是不可自动修复的source-script-missing;target generation 缺失/损坏(逐字节校验)产生可自动修复的target-generation-missing; - 本机 Linux AppImage 下,三个入口(
clawd-hook.js/auto-start.js/claude-statusline.js)及其完整相对 require 闭包会被 materialize 到~/.clawd/appimage-hooks/<generation>/并写 0600 的.clawd-appimage-pathmarker,要求APPDIR绝对路径且拥有全部三个 source entry;共享实现是叶子模块 hooks/appimage-hook-materializer.js,登记在 src/remote-ssh-deploy.js 的HOOK_FILES。注意:Codex 的本机 AppImage 门禁目前仍是 APPIMAGE-only,没有 Claude 的 APPDIR ownership 校验,两边语义未统一; - 所有 mutation 入口(启动 reconcile、watcher 自动恢复、周期自愈、Settings Agent Install/Enable、Doctor Fix、
autoStartWithClaude开关、Uninstall、About 清理)都经过server.js持有的同一个claude-hook-operations.js队列实例,串行执行、互不覆盖; - 巡检严格受
manageClaudeHooksAutomatically、claude-code.integrationInstalled、claude-code.enabled三个 gate 保护; server.getClaudeHookHealthStatus()暴露只读状态(healthy/repairing/degraded/manual-fix-required/guarded/stopped),供 Doctor 使用。
十、权限决策流与 Permission Bubble
权限决策分两种阻塞模型:
Claude Code / CodeBuddy(HTTP hook,阻塞):
Claude Code PermissionRequest
→ HTTP POST 127.0.0.1:23333/permission { tool_name, tool_input, session_id, permission_suggestions }
→ main.js 创建 bubble 窗口(bubble.html)显示权限卡片
→ 用户点击 Allow / Deny / suggestion → HTTP 响应 { behavior }
→ Claude Code 执行对应行为
子 agent(Task)内触发的请求带 agent_id(实例 uuid)/ agent_type;server-agent-id.js 归一化为 claude-code 并标记 subagent 来源;当 agents["claude-code"].subagentPermissionsEnabled=false(#451)时直接断开连接让 CC 回落终端提示(ExitPlanMode / AskUserQuestion 豁免)。
Codex(official PermissionRequest command hook,阻塞):hook 脚本挂起等待 /permission,再把 sanitized allow/deny JSON 写到 stdout。默认 intercept 模式创建普通 Allow/Deny bubble;显式 native 模式记录 notification 并立即返回 no-decision,交给 Codex AutoReview / 原生审批;DND / disabled / bubble hidden / Clawd unavailable 时 stdout {},Codex 回到原生审批提示。POST /permission 的 Codex body 额外带 turn_id、tool_input_description、tool_input_fingerprint。
opencode / MiMo Code(event hook + 反向 bridge,非阻塞):plugin POST /permission(带 bridge_url + bridge_token)→ Clawd 立即 200 ACK(不挂连接)→ 创建 bubble → 用户决定 → Clawd POST plugin 的反向 bridge → bridge 用 ctx.client._client.post() 调宿主内置 Hono 路由 /permission/:id/reply。用户在原生 UI 回答时走 permission.replied lifecycle(使用独立的 lifecycle_bridge_url/token,对旧 Clawd fail-safe),Clawd exact-match 删除该 request 的本地 pending、bubble、timer 与 notification,不复制 reply、不产生第二次宿主决定。
DeepSeek Harness(approval waterfall,阻塞):bridge prepend listener 挂起 POST /permission,独立 adapter 创建仅 Allow Once / Deny 的 bubble;204、断连、DND、disabled 或所有审批通道无决定时调 next() 交还 DSH 原生审批流程;ask_user_question 不进入 Clawd。
关键决策边界(源码与文档一致):
agents/registry.js的 capability 声明是 agent 是否进入权限、interactive bubble、subagent 等路径的权威来源;automation 的 agent/family eligibility 另有显式白名单,故意不能从permissionApproval自动推导;- WorkBuddy 不进入
/permission;QwenWork 不进入/permission(PermissionRequest/PermissionDenied 只被观察并映射成working);TraeCode、MiniMax 同样不注册/permission、不进 permission automation eligibility; - DND 只负责「不弹 bubble」,不替用户决定:opencode 与 MiMo Code 分支 silent drop 让 TUI 内置提示接管,Claude Code 分支
res.destroy()让 CC 回内置确认,Codex 分支返回 no-decision{},DSH 分支返回带 server identity 的 204; permission.js是 permission presentation 的唯一 owner:用目标 workArea、text scale、HUD avoid rect 和每张卡实测宽高先尝试原逐窗栈,不安全时按 agent + session 选 FIFO 代表并预留队列入口;overflow 队列使用独立 permission-queue.html,只暴露 open/close/select/ACK 四类导航 IPC,没有任何决定 IPC;- 每个权限请求创建独立
BrowserWindow,普通卡片默认约 340 CSS px 三行摘要,详情态约 500 CSS px(偏好min(60% workArea, 620 CSS px)),详情正文独立滚动;桌面同时最多一个详情 owner; - 涉及 Claude Code 权限 payload 的改动必须用真实 Claude Code 验证——
curl自编请求历史上掩盖过字段结构 bug。
十一、Plugin 集成专题
文档的 Plugin Notes 提供了若干跨插件通用约束与重点边界:
- 进程树 walk 从
process.pid起步,不是ppid;不要用process.ppid做轻量替代——Claude Code / hook 进程链里它通常只是临时 shell PID; task工具会直接新建 session;只有session.created明确带event.properties.info.parentID的 session 才被视为 child;opencode child session 作为 root 拥有的后台 headless 工作处理,不参与 HUD / focus / 多会话 fanout;- opencode 2.x(#1039):
core.mjs内createOpencodeFamilyPluginV2产出零 import 的{id, setup}定义(v2 loader 拒绝函数 default export);installer 仅在宿主探测确认为 v2 时注册进plugins键(v1 的plugin键不动);事件词汇完全换代(session.step/reasoning/text/tool/execution.*、session.renamed),cwd 取事件信封location.directory;权限用ctx.permission.hook("evaluate")阻塞 POST/permission,决定是响应体{decision: allow|always|deny},204/超时/错误不改 effect 回原生 ask;v2 上无 reverse bridge; - 打包后需要把
app.asar/重写为app.asar.unpacked/;plugin 内发出的 POST 必须 fire-and-forget,避免拖慢 TUI(Hermes 是唯一同步 POST 的例外); - DSH 版本族门禁:按小版本族(major.minor)放行,维护唯一写出具体版本/npm artifact/integrity 的已验证 artifact 清单(
0.2.0-rc.2优先,0.1.5-rc.3、0.1.5-rc.1、0.1.1-rc.2、0.1.0-rc.6保留);未命中任何族或低于下限的版本一律禁止 mutation;安装器按 canonicalDSH_HOME哈希命名空间把 bridge 复制成 immutable hash generation,用官方dsh plugin --profile web|desktop add/removemutation; - Pi 专属约束(Pi Notes):extension 运行目录不在 Clawd repo 内,不能依赖
hooks/shared-process.js;只在ctx.hasUI === true或交互式 TTY 模式上报状态;tool_callhandler 必须顶层 catch 并返回undefined(Pi 的emitToolCall()不 catch extension 异常);tool_result按isError拆成PostToolUse/PostToolUseFailure;agents.pi.permissionsEnabled默认false,v4 migration 会把旧 true 重置为 false; - OMP 专属约束(OMP Notes):extension 目录按 active agent directory 解析(默认
~/.omp/agent,PI_CONFIG_DIR改 config root,PI_CODING_AGENT_DIR改默认值),hooks/omp-install.js的resolveOmpAgentDir()是唯一解析入口;完成事件两阶段确认(session_stop只记候选,等agent_end确认willContinue !== true才上报 Stop);session_switch/session_branch对被离开的 session 补发合成SessionEnd;社区 bridgeclawd-on-desk-omp.ts与内置 extension 互斥(发现即 fail closed); - OpenClaw 专属约束(OpenClaw Notes):Phase 1 只支持状态动画;manifest 必须包含
activation.onStartup和空对象configSchema;model_call_ended成功后 1500ms debounce 发Stop;session_end只在idle|daily|deleted|unknown时映射SessionEnd/sleeping;POST body 是 allowlist(agent_id / session_id / state / event / cwd / agent_pid / tool_name / tool_use_id / hook_source / openclaw_* / error_present等),禁止透传params / result / error字符串 /messages。
十二、终端聚焦与远程传输协调
- CJS hook 脚本通过 hooks/shared-process.js 的
createPidResolver()与 lifecycle context 遍历进程树定位终端应用 PID(Windows Terminal、VS Code、iTerm2 等);source_pid跟随状态更新送到main.js,右键 Sessions 子菜单点击后focusTerminalWindow()用 PowerShell(Windows)或osascript(macOS)聚焦终端; - Windows 的 Cursor / VS Code 父进程窗口优先按项目标题唯一匹配;标题不匹配或缺少 cwd 时,仅在该进程可见候选窗口唯一时兜底唤起;多窗口歧义或无可见候选时不以
MainWindowHandle猜选; - 远程场景只通过 Settings Remote SSH controller 部署:
runtimeKey → layout解析、installId/profileId/nonce 身份、原子 lease/fencing、持久部署事务和 profile 专属 ingress 共同把远端 hook 事件回送到本地 Clawd;scripts/remote-deploy.sh已 fail-fast 停用;远程部署不设置本机integrationInstalled,也不自动重启 gateway(托管模块被替换时只报告 restart-required); - Remote SSH transport coordination:
remote-ssh-transport.js通过ssh -G展开本机 SSH 配置并分类 effective transport——ordinary SSH 保持原 parallel tunnel + health-probe 行为,Codespacesgh cs ssh --stdio与显式 serialized override 进入 single-session 路径;serialized ownership 以有效 transport key 为作用域而非 profile id;正常暂停通过 tunnel stdin EOF 请求远端 readiness 进程退出,未验证 drain / watchdog timeout / 仍有 live child 时 slot 进入 quarantine,禁止新 child、mutation、resume 和 interactive terminal。用户流程见 docs/guides/guide-remote-ssh.md,真机矩阵与精确清理流程见 scripts/manual/README.md。
十三、Windows B1a 进程链元数据能力(#694)
Codex、Cursor Agent、Kiro CLI、CodeBuddy 和 Reasonix 的本地 Windows hook 支持版本化的 server-side process-chain capability(src/server-windows-process-metadata.js)。Clawd runtime owner 把以下数据写入 ~/.clawd/runtime.json:随机 instanceGeneration,以及每个 agent 的 legacy | shadow | b1a-authoritative mode。默认始终是 legacy;shadow 和 b1a-authoritative 仅用于显式开发/验证,resolver 初始化或 ABI 校验失败时在写 runtime 前降级回 legacy。
本地 Windows、非 remote/WSL 的 hook 可以把当前 hook Node PID 和 runtime generation 放入 X-Clawd-Hook-Pid / X-Clawd-Process-Instance 头;headless/official/subagent 分类在 server 收到请求后完成,只有通过 effective eligibility 的请求才消费这些 header。header 只发往同一次 immutable runtime observation 指定的端口;PID/generation 是 capability routing metadata,不是认证凭据。b1a-authoritative 下五个 hook 的 eligible 路径不再启动 legacy snapshot PowerShell,以 server 结果 replace/clear sourcePid、agentPid、pidChain 与 walk-derived editor。CodeBuddy direct HTTP PermissionRequest 不经过 command hook,因此没有可信 hook PID,B1a 当前只覆盖其 command state。
十四、其余运行时注意点
- Context Menu Owner Window:
contextMenuOwner必须保留parent: win;退出路径依赖requestAppQuit()先置isQuitting = true,再让window-all-closed真正走到退出分支,不要绕开这套守卫; - Updating:Git 模式(非打包,macOS/Linux 源码运行)
git fetch比较 HEAD,有更新则git pull+ 必要时npm install,然后app.relaunch();Windows NSIS 与 macOS DMG 打包模式走electron-updater,均保持autoDownload=false,用户确认后才下载;macOS Release 同时发布 x64 / arm64 的 DMG 与 ZIP,latest-mac.yml必须同时列出两架构且 top-levelpath指向 x64 ZIP;托盘菜单里的「Check for Updates」可手动触发; - i18n:支持 en / zh / zh-TW / ko / ja / pt-BR / es 七种语言,文案集中在 src/i18n.js,语言偏好持久化到
clawd-prefs.json,启动时通过hydrate()灌入 controller。
结语
Clawd-on-desk 的 Agent Runtime 本质上是一套「本地优先、事件驱动、多源仲裁」的运行时:所有 Agent 集成被规范化为非阻塞状态上报与阻塞权限决策两条通道,agent-runtime-main.js 负责跨来源去重与生命周期仲裁,state.js 状态机把事件收敛为桌宠可消费的会话快照,而 main.js 退居 composition root 通过明确的 owner 边界防止逻辑回流。无论你是在扩展新 Agent 集成、调试会话标题异常,还是排查权限气泡行为,都可以从本文的数据流表格与源码路径出发,快速定位到对应的 owner 模块与测试证据。