合并说明:本文是博客 bloodysky.top「庖丁解牛 Raven」8 篇系列文章的完整合并版。原 8 篇均发布于 2026-10-01,本文按原篇目顺序合并为一篇,正文内容忠实保留原文全部实质内容(结论、代码引用 file:line、清单、表格、代码块),未作删减或缩写;仅去除了每篇重复的系列导航(「《庖丁解牛 Raven》系列 第 N/8 篇」)、重复的站点页头页脚、以及重复的引言/作者介绍。原作者署名与版权信息见文末。

Raven TUI 实况:v0.2.3 启动画面

图 0 · Raven TUI 实况(v0.2.3,2026-10-03 摄于 WSL2 的 ~/work):启动即列出 40 个可用工具与 4 个技能。本系列的每条结论,都在这样一台实机上验证。


一、架构总览与工程美学

0. 结论先行

Raven 的本质是一个六层管道 + 两条总线:

1
2
消息总线(spine):TurnRequest → Lane(串行) → Runner → Sink → DeliveryHub
子代理总线(ACP):SpawnTool → Manager → Pool → 外部 agent 进程 ←stdio JSON-RPC

六层自外向内:channels(入口)→ spine(调度)→ loop(ReAct 主循环)→ hook 横向切面 → tools/MCP(执行)→ subagent(fan-out)。

四个工程支柱声明在 raven/__init__.py:上下文管理(Curator)、主动性(Sentinel + cron)、token 效率(token_wise)、技能自演化(memory_engine/skill_forge——最后一个只是检索+反馈管道,真正的演化循环在外部 EverOS 进程,本系列第 8 篇专拆)。

最值得学的三个设计:

  1. origin 五种流通、一套管线——用户/cron/sentinel/heartbeat/subagent 全部汇入同一个 Scheduler.submit(spine/scheduler.py:571),差异只在并发池与旁路;
  2. 注释即事实——几乎每个反直觉分支都带实测日期与踩坑叙事(”measured 2026-09-02” 类注释遍布全树);
  3. fail-closed / fail-safe 纪律——无主回合审批直接拒、渠道 probe 失败答 unknown 不答 disconnected、hook 异常隔离不停回合。

最值得警惕的三个点:

  1. subagent 异步续报是事件驱动不是定时——“等 40 秒”是墙钟幻觉(manager.py:2056-2058 明确禁止再排心跳,结果由 _announce_result → _inject 提交 spine.TurnRequest(origin=SUBAGENT) 事件驱动生成);
  2. sandbox 默认无隔离(且 is_sandboxed 默认 True 反而会关掉宿主的 deny-list);
  3. 本机 Sentinel 实际关闭,主动性只剩 cron,且重启会丢内存态队列。

1. 架构总览与运行拓扑

1
2
3
4
5
6
7
8
9
10
手机飞书 App ──WS 出站长连(无需公网)──> raven gateway (systemd user service)
ExecStart: raven gateway --page-port 18792
├─ 控制面 ws://127.0.0.1:8765/ws (token=首帧)
├─ 页面 http://127.0.0.1:18792
├─ health :18790 (healthcheck timer)
├─ spine×2:IM hub + tui 页面 spine
├─ cron service(~/.raven/cron/jobs.json)
└─ ACP Pool:OpenCode/OpenClaw(WSL) + 手机 SSH 桥
everos server(独立常驻,gateway 子进程):skill 物化 + 第二服务
数据:~/.raven/(config.json、workspace、cron/、traces、telemetry、subagent_*.json)

约 40 万行 / 48 个包,按角色分族:

族 包 行数量级
管线核心 agent/(52K)、spine/(7.4K)、core/(3K)、session/(1.4K) ~64K
子代理 agent/subagent/(26.7K)、agents/(33K 预设)、acp/(5.1K)、acp_client/(8.3K) ~73K
控制面 cli/(24.5K)、rpc/(36.6K)、gateway/(1.1K)、ui/ ~62K
支柱 context_engine/、memory_engine/、token_wise/、proactive_engine/(14.4K) ~29K
外部接口 channels/(8.3K)、providers/(14.6K)、mcp/(4.1K)、browser/、a2a/ ~29K
横切 agent/hook/(883)、permissions/(4.5K)、sandbox/、security/、tracing/、trajectory/(7.4K)、playbook/(7.7K)、eval_engine/、stint/(4.1K)、plugins/、skill_hub/、knowledge/、importer/、contracts/ ~40K
杂项 templates/、i18n/、updates/、market/、ops/、utils/、auth/、routing/、observability/ ~10K

2. 三大组装点与进程模型

core/ 的哲学:每个入口只装配自己,共享部分才进 core。三个组装点并存:

组装点 位置 服务对象
build_gateway gateway/spine.py:211 IM 渠道 + cron/sentinel/heartbeat + 页面双 spine
build_rpc_stack rpc/bootstrap.py:139 TUI / raven serve 页面 / acp worker(transport 无关)
CLI one-shot cli(build_one_shot_spine) raven agent -m 单发(不自建 MCP 连接→调 MCP 必败,by-design 竞速,见第 7 篇)

generation 热切换(raven gateway reload):core/runtime.py:72-76 的 RavenRuntime 是冻结 dataclass,五个成员 {loop, plugin_registry, backend, strategies, deliverables} 身份不可变;FREEZE 只封成员身份,留了三个合法中途门:loop.apply_agents/apply_mcp_config/set_default_binding(runtime.py:48-60)。SwapCoordinator(:315-401)单次 swap、最小间隔 5s(:328)、generation 计数 release 自增(:386-396)。CLI reload 拒绝语义:busy(N 个 subagent + M 个 pending questions,--force 覆盖)/ build_failed(新配置失败时旧 generation 继续服务,gateway_commands.py:981-1025)。cut_by_reload 让被砍回合的用户至少收到一句”reload 截断了本回复”(gateway/spine.py:36,194-197)。

3. 工程美学:五条值得抄的设计原则

  1. 注释即事实:”measured: litellm DEBUG 146 KiB one line killed the agent loop”——每个反直觉分支都绑定一次真实事故与日期。读 Raven 源码等于读一部踩坑史。
  2. fail-closed 优先:无主回合审批=拒;probe 失败=unknown;渠道 probe 不可达=不说 disconnected;hook 崩=隔离;hook 停链=第一个短路者赢。
  3. 顺序即安全:先 chmod 后写 token;截断标记预算在帧前;drain 先于 stop;subagent 先于 spine 封;检查先于确认。
  4. 能力靠声明不靠猜:presence 非 truthiness({} 即支持);can_steer 从 _meta 读、未声明不调;方言按 agentInfo.name 自选。
  5. 双读一切:control_* 与 web_* 双拼写;snapshot 指纹 + stale 标记;新旧 instruction 双读。

4. 脆弱点与风险清单(按严重度)

# 问题 位置/证据
1 sandbox 默认无隔离,is_sandboxed 默认 True 反使 deny-list 对覆写执行器失效 sandbox/init.py:38-105、interfaces.py:53-65;全包无 bubblewrap/seccomp
2 渠道去重启丢(1000 FIFO 内存态)→ 同消息重投可能二次处理 channels adapter :473-477
3 Sentinel 本机关闭 + cron 重启丢内存队列(SystemEventQueue/WakeScheduler/notified) proactive_stack.py:210-211;system_events.py:4-7
4 feishu SDK 无限重连:stop 杀不断、restart 短暂双活 channels 终报确认
5 allow_from 默认 ['*'] ——谁能发消息谁驱动它 channels/allowlist.py:58-59(收紧:raven channels set feishu --allow-from <open_id>)
6 MCP schema 服务端原文透传,恶意/劣质 server 可控制模型工具面 mcp/client.py:40
7 插件无沙箱(进程内任意 Python),tool_gates 绕过权限闸门 plugins/manifest.py;registry.py:1046-1067
8 CLI one-shot 调 MCP 必败(by-design 竞速),冒烟验证会被误导 registry.py:664;prewarm fire-and-forget
9 skill watcher 守护线程在多次 spawn 时可泄漏 skill_hub/catalog.py:195-201(自述)
10 traces 334M / tmp 48M 无自动 reclaim(acp-frames 才有 64MB/7 天策略) ~/.raven 实测
11 trust.py 仅提示词级包裹,无强制力 security/trust.py:23-44
12 session 续场不可用——编排系统自身的运维坑,实测连撞三次 本轮实测

5. 周边模块速览

  • session store(1012 行):append-only JSONL + 每 save 追加 metadata(last-wins);跨进程合并三件套=锁 + _file_stamp(size,mtime_ns) 脏检 + 三方合并——只有本副本改过的键才能盖过磁盘;list 排序按 updated_at 刻意不按 last_message_at,因为三个 caller 取 [0] 决定投递路由(heartbeat/sentinel nudge/sessions list),为选择器重排序=静默改投递。
  • eval_engine(683 行):三 hook 全默认 off 且 unwired——设计完备的演示引擎;AfterIteration 永不短路(”评审者只能 annotate 不能打断回复链”)。
  • browser:file:/chrome:/devtools: 等导航全拒(file:// 能读出 gateway token,policy.py:21-28 自述);driver 共享 page + owner-tab 绑定 + 结构快照(拒绝让模型在 JPEG 里找按钮)。
  • WhatsApp sidecar:channels/ 原生适配器之外的第二类渠道架构——外挂 Node 桥进程(Baileys 多设备协议,ws://127.0.0.1:3001 + token 首帧认证),Python 侧管构建指纹与凭证隔离(TS 侧清凭证只删 .json 不删 token,防锁死桥接)。

6. 三条元教训

  1. 动手前先证伪任务书:本系列解剖中,多轮”按任务描述找文件”的预期与实物不符(trajectory 的 daemon.py、everos 的子包结构、stint 的半成品状态)——ls + find + grep 空验证必须先于读码;
  2. 注释即工程史:Raven 系代码的真实约束都长在注释里——假健康探测、无 owner 的 stint、不被重排的 session 列表;
  3. 断点比管道更值得记录:”设计了但没接线”的清单(feedback no-op、retire 缺失、Sentinel 关闭、sandbox 无隔离、eval unwired)才是系统的真实边界。

二、一条飞书消息的完整旅程

1. 十步管道全景

一条飞书消息从手机到回复,走完这十步:

  1. 入站:飞书 WS 长连只订阅 p2_im_message_receive_v1,去重在 adapter 内存 1000 条 FIFO——重启即失效(channels/feishu 适配器,实测确认的历史设计)。
  2. 准入:allow_from 六锚点(channel.py / intake.py / base.py / allowlist.py / manager.py / channel_commands.py),空列表=启动即 SystemExit(deny-all 致命,manager.py:157-163)。
  3. 转写:媒体走 Groq Whisper API(非本地模型,channels/feishu:376-431 + transcribe.py:15-23)。
  4. 提交:Intake → Scheduler.submit(spine/scheduler.py:561-598)——全系统唯一入口,cron/sentinel/heartbeat/subagent/用户全部经此。
  5. 串行化:Lane 按 conversation 键(tui: 前缀归页面 spine,其余归 IM spine,gateway/submit_router.py:34-47——这里修过 2026-09-08 双 spine 同会话双车道事故:subagent relay 进页面会话跑网关 spine,turn.send guard 看不见,工具画两次结果送两次)。
  6. 执行:_run_turn(scheduler.py:427)→ pools 按 origin 分流(user=4/system=2)→ runner.run(req, emit, drain)。
  7. 循环:turn_path.py:605-2039 _run_agent_loop,四状态机 _AWAIT_MODEL → _AWAIT_TOOL_RESULTS → _PREPARE_NEXT_TURN → _ENDED;核心约 956-1050 的大 while;工具批处理逐项错误隔离(一个工具炸不拖垮整批)。
  8. finalize:七阶段(统计→artifacts→session 落盘→学习→TurnEnded→Outlet 发→DeliveryHub)。
  9. 送达:DeliveryHub → ChannelOutletAdapter(gateway/outlet.py:41-72:Text→send;吃掉流事件——渠道非流式,吃不是失败;仅 ORGAN_DEGRADED notice 渲染;无附件能力的渠道降级成 Files ready: ... 文本)。
  10. 失败面:TurnFailed → _failed_turn_reply(gateway/spine.py:39-59):LLM 错误只报类别+端点,厂商账户(auth body 掩码 key、拒绝 body 引 prompt)只进 operator log——群聊里永远看不到供应商原文。

2. spine 调度层:一套总线,五种 origin

Lane 每会话串行(scheduler.py:321/331/391),提交注释写得很直白:”gateway hosts a mix of turns (cron/heartbeat/channel users) on one runner”(:429)。

Origin 池 特殊行为
USER user 池(4) 交互 asker 绑定、on_user_inbound 全链
CRON system 池(2) 回复读回(readback_texts,填 system event,gateway/spine.py:71,144-159);不触发 on_user_inbound;绑 cron-context guard
SENTINEL system 跳过 on_user_inbound;INTERRUPT/INJECT 降级 APPEND
SUBAGENT system 异步 announce 的载体;同上
HEARTBEAT system 纯 fire-and-forget 不读回

无主回合 fail-closed:cron/sentinel/heartbeat 的 permission responder 故意绑 None,ask tier 拒绝并给原因(gateway/spine.py:123-126);allow_ask=False 时闸门拒 “this turn is not interactive”(permissions/gate.py:271-278)。delivery-only origin 不存储读回原文——注释原话 “would only leak (no one pops it)”,防长驻 daemon 累积。

3. 主循环:四状态机 + 五把压缩闸

  • 每回合上限 5 把压缩闸(MAX_COMPRES_RETRIES=2 / IMAGE_DEMOTE=1 / IMAGE_STIR=2 / keep-3-tool-results / keep-1-image,agent/window/shrink.py:34-77)。
  • 消息组装 9 因子:tool_kinds → system → 历史 → recall → skills → curator → personalization → routing → window。
  • history 走 Curator 慢路径时不重复压缩——trimmer 预算与 window.shrink 是两套 make-fit 机制,经 available_history 算术解耦,这是 context_engine 与 loop 的接缝陷阱(详见第 5 篇)。
  • 改写/续跑入口:turn_path.py:3106-3116(跳过消息续跑)、2900-3106(新回合前奏)、3276-3330(分派出入,turn.send 一回合一会话 guard 所在)。

4. Hook 系统:883 行的反直觉集大成者

6 阶段(agent/hook/init.py:1-8):

1
2
3
4
before_user_inbound(入站一次,只有这里 fresh roster)
before_iteration → before_execute_tools → after_iteration(每次 ReAct 迭代三个)
terminal_answerless(无产出救端)
after_send(出站一次)

抽象(raven/contracts/loop_hooks.py):AgentHook(全 no-op 基类)、AgentHookContext(每回合状态、跨迭代 mutate、identity=回合)、HookDecision(short_circuit_result / rollback / modified_content / modified_tools / append_note / notes)。

CompositeHook(composite.py:49-188)的链纪律:

  • 首个 short_circuit/rollback 立即停链——所以 Sentinel 的 decision_consumer 在入站短路 /pick 回复时,Personalizer 不会误分类(:11-14 自述);
  • 内容链仅 after_send + before_user_inbound;工具链仅 before_iteration;
  • 异常隔离:logger.exception + continue,单 hook 崩不停回合(:152-158)。

ParticipantHook(participant.py:110-496)是插件”参与者”上链的通道,一回合一实例:

  • _Row 是拒写的 dict 子类——参与者只能靠 return 回答,不能改 transcript(:45-71);
  • roster 制:每阶段最后一个 seat 持全 roster 问一次,其余 no-op(:182-220);
  • 9 动词映射 harness 四域(memory/planning/action/capability);
  • rolls_back_iterations 类属性让 verdict 能把回合打回(loop 据此扣草稿门,:115);
  • system addendum 按文本 rfind 取回,防多 seat 副本叠加(:395-441)。

方法论注脚:本轮解剖初期曾假设 hook 是”do_hooks/filter_results 纯函数语义、10 分支分类法”——均被源码证伪。假设必须过 file:line 锚点,这是整个系列的方法论纪律。


三、Subagent 编排层与 stint 轮次

1. spawn 全链路(26.7K 行的 orchestration 层)

1
2
3
4
5
6
7
8
9
10
11
12
SpawnTool.execute(spawn_tool.py:433-560)
→ origin ContextVar 快照(:33-46)
→ node_id 原子 claim(拒绝只能下回合到达模型)
→ manager.spawn(:902-1071)
→ quota 检查(3600s 窗口 30 个上限,manager.py:65/258)
→ pending 落盘(2s 写超时)
→ create_task(_run_subagent)(:1623-1670,Semaphore=8,:358-362)
→ _run_subagent_inner(:1724-1938)
→ build_executor + sandbox
→ backend.run(:1832-1850)
→ resolve_mode(medium/high/max 三档阶梯)
→ 终态分流:completed / cancelled / aborted / failed → _announce_*

状态机:pending → running → completed/failed/cancelled/aborted,全量持久化;handle 锁包住 load→run→save 的全程。

以下三张实况截图来自 2026-10-03 两次真实的 DAG 任务——PiPhone「切题技能 v1」三节点(裁剪模块 / 切题指令分支 / 联调验证)与「复习本原图留存」三节点(本地排版 / 手机部署 / 实测回归)——正是上面这条 spawn 链路的运行态:

DAG 三节点编排实况

图 1 · run subagent dag 三节点实况:Raven-Code 本地实现 + PiPhone 同步手机并改调用方 + PiPhone 手机实测原图册并回归;每个节点的状态、归属 agent 与耗时实时可见,下方 spawn tree 同步展开。

spawn 单节点会话详情

图 2 · 单个 spawn 节点会话详情:PiPhone@integrate-test 已跑 38 条消息,v1.1「锚点升级接线」重试中——先零改动验证、失败回退保护、顺带改 single 文案,最后重启上线;节点内的思考、命令与返回预览都在主界面里可查。

2. 五个运维流言的源码裁决

这一节是”流言 vs 源码”的对质,每条都有锚点:

  1. 异步续报是事件驱动,不是 40s 定时。receipt 就是 manager.py:1070-1071 那句 Subagent [...] started (id: ...). I'll notify you when it completes.;续报由 _announce_result(manager.py:2042-2133)→ _inject(:2420-2470)提交 spine.TurnRequest(origin=SUBAGENT) 生成;manager.py:2056-2058 明确禁止再排心跳。”~40s”是被调方的墙钟时间(CLI agent 跑完 + announce 回合生成),不是任何定时器。

运行中节点详情与 steer

图 3 · 流言 1 的实况证据:节点结束后,bridge_branch.memory.json / crop_module.memory.json 由上游异步写入(”written asynchronously after a node finishes”),主 agent 据此续跑;底部 message PiPhone/integrate-test-37d8be · Enter to steer the running turn 即对运行中回合的实时转向。

  1. readyTimeoutMs = spawn → initialize 预算。透传链:acp_agent.py:1428-1429 → pool.acquire(ready_timeout_s) → pool.py:473;_HANDSHAKE_TIMEOUT_S=120 是硬顶。opencode 30s / openclaw 45s(presets.py:181,189,ready 实测需 ~21.5s)/ 手机 SSH 桥 60s。
  2. 手机端 “executable not found (command: pi)” 是 cwd 误报。链:spawn_tool.py:537 workspace=workdir.current() → manager.py:1738 effective_workspace → backend.run 的 launch_cwd=self.cwd or str(workspace) / session_cwd=str(workspace)(acp_agent.py 客户端 :1436-1437)→ pool launch_key 含 cwd(pool.py:84-104)→ SSH launcher 把 WSL 的路径导出到 Termux → 手机上没有该目录,pi 启动即报 not found。代码自证注释:”Conflating them sent a pinned entry’s own folder to session/new”——launch_cwd 与 session_cwd 是两个东西。
  3. 三处持久化:subagent_instances.json(instances.py:28/56-59,实测 kinds {acp:24, cli:20, dag-node:14})、subagent_acp_capabilities.json(capabilities.py:59/322-325)、subagent_sessions/(product_render.py:342)。
  4. ACP 绑定快照与 entry env 分开传(acp_agent.py:1429-1433):折进 env 会改 launch key,pool 会把第一个绑定的连接关掉——mid-turn 断连。

Spawn tree 时间线

图 4 · Spawn tree 时间线(d1,4 个 agent,48m56s):并行/串行执行一目了然;底部快捷键支持 move / kill / pause / sort / filter——子代理集群的观测面。

3. stint:planner/builder/verifier 三角色轮次

stint 是 mode: stint playbook 驱动的多轮多角色执行层(4136 行):

  • 角色 = .stint/<role>.md guard 文件,frontmatter 定义 order / session(fresh|continue) / enforce(read/write: soft|hard) / owns / appends / reads / artifacts / tasks;默认三角色 planner → builder → verifier(roster.py:83-86);
  • 轮次推进靠 JOURNAL.md :append-only,每 round 一个 section、每 role 一个子节;prompt 只带最近 JOURNAL_ROUNDS_IN_PROMPT=2 轮、MAX_JOURNAL_CHARS=16000 截断(journal.py:25-26);
  • 状态 = backlog 状态机(add/assign/defer/reject/block/unblock/implement/proven/not_proven/reopen/drop,by 角色 → from → to)+ checks.json 台账 + ~/.raven/stints/<id>.json(heartbeat 60s / STALE 300s / claim PID+host);
  • 写权限是文件级的:ownership.py 的 owns/appends/never 三级 + glob 展开;enforce.py 越界写直接 undo(quarantine 副本先存),appends 带删行→trimmed 恢复——跑完才执法,跑中不拦。

4. 三个 harness 缺口(源码里挖出来的,不是文档写的)

  1. paused/interrupted/adrift 只持久化不驱动。stint 没有 owner 概念:ingest 只按 recency 把 stint 分 active/resumable/stale,paused 的 resume 只出 payload 由人判断;op cleanup-all/flush/requeue 只是运行参数名,不是状态迁移。
  2. stint_round 不 fork。role 引用是 pinned 的;dry_run 只返 ref 不进 subagent;续跑=全量重传快照(对比主 agent 的 ACP resume/fork 能力)。
  3. unlock 六途径只记录不改状态机(CLI _collect_responses :271-618 → :618-812)——人工放行是叙事性的,不改变 backlog 机。

类 actor turn 的差异:owner_type=stint 时 ship 能力受上限约束(dispatcher/stint_tool.py:1095-1103),parcel 寻址只认 direct parent zones(_DIRECT_PARENT_ARTIFACTS :4041-4080、resolvable_zones :4269-4280);out-of-scope 引用经 TurnExecutor._ancestor_cache 显式报错——“绝不静默 no-op”(:368-378)。

DAG 轮次推进实况

图 5 · 轮次推进实况:「切题技能 v1」2/3 节点已完成(crop_module 裁剪模块自测 → bridge_branch 切题指令分支),integrate_test 重启联调并端到端验证收尾中;v1 计划逐条汇报,与会话树里的 dag 记录一一对应。


四、ACP 协议栈双端全解

ACP(Agent Client Protocol)是 Raven 与外部 coding agent 之间的 stdio JSON-RPC 协议,双端合计 13.4K 行。这是整个系统里外交精度最高的部分——所有”能力声明-实际行为”的缝隙都在这里显式处理。

1. 服务端:Raven 作为 ACP server 被 IDE 驱动

  • capabilities(acp/capabilities.py:43-101):loadSession: True 只因 replay 存在才声明——注释直白标注 false promise 风险;mcpCapabilities http/sse 均 False(子代理全走 stdio stanza 指向宿主端点);prompt 侧 image: True / audio: False;initialize_result 容错构造:坏 protocolVersion 读意图不失败,authMethods: [] = 无需认证。
  • modes(acp/modes.py:34-79):mode 是会话状态非转写状态,不持久化,重开会话回连接默认。
  • replay(acp/replay.py):session/load 不是 getter——把会话以 session/update 通知重放后才返回(客户端无需第二渲染器);notice/turn_ended 不作为助手语音重放;滚动累计 500 条 / 16KB 上限;只有重放里的 tool_call 是 pending 状态——重放时工作进行完毕,follow-up 带结果。
  • questions(acp/questions.py:77-397)双路由:elicitation/create(正配,唯一能承载清单外答案)否则 session/request_permission(合成 tool call);无 elicitation 声明的自由文本问题无法回答→作为普通 agent message 上线并回落默认;HUMAN_ANSWER_TIMEOUT_S=900。

2. 客户端:Raven 驱动外部 coding agent(编排的真正前线)

pool(client.py):一连接 = 一子进程 + 读循环 + 请求相关。三个关键决策:

  • launch 时 start_new_session=True 且 pgid 在 spawn 时捕获后永不重推导——回收 pid 再 killpg 会杀错进程(:151-208);
  • alive 三重判活——npx 包装器会 park 在死子进程上:pid 活、stdout 关、不答话(:285-296);
  • binding 与 entry env 分离(见第 3 篇 §2-5),pool 会因 launch key 变化错误关闭首个连接。

prompt 发送(acp_agent.py:1537-1554 逐字):session/prompt + _meta.raven.usage + 附件作 resource links + cancel_session 绑定 + asyncio.shield(取消调用方不能取消底层 future,否则分不清真实 settle)。

回流传译 _TurnCollector(:199-830):agent_message_chunk 合并、tool_call 开合对(有的 agent 会重发)、plan 只有开帧断消息(改 4 次不会把叙述切成 4 段)、live republish 到 activity(transcript/tool_calls 实时上报);文件账目靠调用出现时拍基线——settle 时拍会把创建的文件说成一直都在;failed 调用的 diff 块丢弃(磁盘说了算)。

resume 决策(:1360-1376):is_stateful 读手快照的 loadSession 而非 resume 字段——实测多个 agent 都报 resume:false + loadSession:true 且都能正确 resume;session/load 期间挂丢弃 sink,否则重放的历史会被 unprompted recorder 记成无人值守工作。

3. 方言层:只认 agentInfo.name

只两个 dialect(claude-agent-acp / codex-acp),从 agentInfo.name 选而非 config(改名/双入口都正确);spec 方言是诚实默认。claude 方言三处偏离:_meta.claudeCode.toolName 真名、rawOutput+content 双发(```console 围栏原文泄漏到叙述)、TodoWrite/Task 不发 tool_call 只进 plan 帧——每个偏离都标了测量版本号(0.66.0/0.79.0/0.81.1)。

4. 权限应答:本轮解剖最重要的单个决策

无人值守场景只有”永远批准”或”永远拒绝”两种答案,Raven 选批准(permissions.py:1-34)。理由链:

  1. 实测 codex-acp 把任何错误(含 method-not-found)转成 {decision: cancel} 整回合取消——选”拒绝”的后果是已流出的两句变 partial reply 无声失败;
  2. 按 kind 选 option 不按 optionId——id 是 agent 私有词汇;
  3. 批准止于宿主自己的拒绝规则——放行越过操作员拒过的命令 = 把子代理升级。

配套纪律:一切客户端故障都映射成拒绝(RequestFailedError→client-error、ConnectionClosedError→connection-closed、TimeoutError→timeout、CancelledError→cancelled-turn 后重抛、catch-all→transport-error)——否则 OSError 会逃进 ToolRegistry.execute 被报成工具失败而非拒绝。

5. 两条隐形管道

  • journal(journal.py:287 行):四类流量此前完全不可见——子代理被批准了什么、raven 出站帧、无主的迟到通知、stderr。文件先 0600 再写首字节(”帧里有全 prompt+全工具结果,world-readable 窗口根本不该存在”);MCP secret 在 journal-safe copy 里 <redacted>;64MB/7 天 reclaim。
  • unprompted recorder:修 2026-08-26 “35 帧无人值守回合被当迟到 usage 丢弃”事故——用客户端已有的 message.start/token.delta/complete 三事件重放,客户端零改动;空回复回合记 [unprompted: N tool call(s), no message];idle 20s 兜底。

6. a2a:>2 agent 编排的真正入口

与 ACP 互补,a2a 管 agent 间互调(1537 行):三段拒绝门(gate.py:15-27)——not enabled(照单全收离线)/ not served(Unknown channel,agent 跳下一通道)/ path collision(换 route_path)。AAgentProxy 是本地 actor 的 A2A 化身(agent card 地址 :63-70、bearer auth :148-156、transfer :48-59);ChainedAgentExecutor 是 >2 agent 编排实际发生处——超过两个 agent 的链条不走 subagent DAG,走这里。


五、上下文工程三支柱与记忆演化

1. Curator:每回合一次的上下文裁缝

触发判据(context_engine/segments/curator.py:192-204):每回合 history_tokens = sum(item.tokens for item in manifest),低于 budget.available_history * fast_path_threshold 走 fast 直通,否则进有界 LLM 循环(curator.py:258-351,max_steps=12,temperature=0.1,模型 = context.curator_model 钉住或会话模型)。

Curator 手里 8 个工具:archive / retrieve / relevance / working-state / read-memory / build-plan——它不是 summarizer,是检索-规划器:慢路径产出的归档 JSONL 落 ~/.raven/workspace/memory/.curator/archive/YYYY-MM-DD/*.jsonl,working_state 以 # Curator Working State ... 附加在 system 尾部;失败回落是确定性计划(protected + relevance top-12 + recent 16,curator.py:466-481)。

与 loop 压缩的解耦:trimmer(history_trimmer.py:292-368)与 window.shrink 是两套 make-fit 机制,经 available_history 算术(curator 慢路径预算 ×0.60)保持出热路径——这是接缝处最容易被改错的地方。

2. token_wise:四断点的前缀稳定学

支柱 3 的全貌在 core/token_wise_stack.py:96-150 + cache_optimizer.py:136-200:

断点 内容 依据
bp1 最后一个工具 schema 工具清单是 prompt-cache 前缀首项
bp2 system 消息稳定头 split_stable_prefix(content, int(m.get(STABLE_PREFIX_KEY) or 0))——只有 breakpoint 之前的 system 进缓存(记忆召回+router hits+Curator working state 随当前 user 消息派生,不缓存)
bp3-4 滚动尾(最近 3 条) Hermes 的 system+3 复刻,A/B 对照实现同包

配置:cache_ttl 5m/1h → prompt_cache.set_ttl();max_breakpoints=floor 1(厂商拒收第 5 个断点——Anthropic 上限 4);非缓存模型 no-op 且深拷贝不改入参。深拷贝这条是必须的:同一 messages 对象在诊断快照和实际发送间共享,原地改断点标记会污染观测数据。

UsageTracker 是纯记录器:JSONL 追加 ~/.raven/telemetry/usage-YYYY-MM-DD.jsonl(schema_version 2,RAVEN_HOME 可迁移);record_delegated_usage 把子代理账单挂 delegation root;无价模型进 *_missing_calls 不清零(账不平比没账好);pricing 显式估价——Anthropic cache-read 10%、cache-write 125%/2.0× 按 TTL——计划计费/离线拒价,绝不糊一个估值糊弄账单。

3. 记忆引擎:纯 Markdown,克制得不像 2026 年

MemoryStore(consolidator.py:608-660)的全部家当:

1
2
3
~/.raven/workspace/user_memory/profile/user.md     # 用户画像(+ .lock)
~/.raven/workspace/memory/episodic/episodes.md # 情节
~/.raven/workspace/memory/attention/behaviors.md # 注意力

无 sqlite、无向量库。写入=annotate(forced-tool LLM 通道,consolidator.py:985-1183);读取=get_memory_context 词法 H2 选取(:916);合并=refresh_section 的 LLM compare-and-set(:1305-1546)。工程判断:这个量级(单用户 MB 级)下 Markdown 的 diff/审阅/手工编辑价值远超向量检索的收益——检索精度由 everos 的 LanceDB 补位(第 8 篇),分工干净。

4. 技能演化:闭环在 everos,raven 只留了管道

第四支柱”Skill Self-Evolution”的分布:

  • raven 侧(memory_engine/skill_forge):只是检索+反馈管道——skill_forge/backend_source.py:1-87 重包 RouterHit 进 prompt,organ_glue.py:260-309 发 skill_usage 信号;
  • config/raven.py:820-828 明说 evolution 归 everos,auto_detect/auto_evolve/retirement 是占位(:1054-1069 默认全关);
  • 真正的 Detect→Draft→Active→Evolve→Retire 循环在 everos:AgentCaseExtracted → trigger_skill_clustering(LLM 判 merge/mint)→ SkillClusterUpdated → extract_agent_skill(md-first,≤10 全入 prompt,sanitize_skill_name 防路径注入)→ cascade 投影(md→LSN→LanceDB)→ recall(BM25+dense 双轨);
  • 两个断点:feedback 是确定性 no-op(无 HTTP 端点、confidence 无 service 写路径,”反馈→置信度→演化”目前是断的);retire 未实现、退化为低 confidence——技能只增不汰。

死配置警告:relevance_decay=0.95 / relevance_reference_boost=0.15(raven/config/raven.py:103-107)零 reader——build_manifest 后 relevance 不可变,改这两个数字没有任何效果。


六、主动性引擎的双面孔

Raven 的主动性有两副面孔:cron 是真正在跑的实干家,Sentinel 是设计完备但本机关闭的规划师。分开解剖。

1. cron:jobs.json 持久化与重启语义

存储(config/paths.py:24-26 + cron_stack.py:475-482):

1
2
3
4
5
6
7
8
9
10
11
{
"version": 1,
"jobs": [{
"id": "...", "name": "...", "enabled": true,
"schedule": {"kind": "at|every|cron", "atMs": 0, "everyMs": 0, "expr": "...", "tz": "..."},
"payload": {"message": "...", "channel": "...", "to": "...", "directAgent": "...", "fireMissed": false},
"state": {"nextRunAtMs": 0, "lastRunAtMs": 0, "lastStatus": "...", "claimedByPid": 0,
"claimedAtMs": 0, "silentFireCount": 0},
"silentFireLimit": 12
}]
}

并发与执行:portalocker 文件锁(jobs.json.lock)+ PID claim TTL(30min)+ channel 分区(allowed_channels,不同渠道可并行);process-then-wait 主循环(最大睡眠 30s);锁内 claim、锁外执行、同一进程内串行(for job in my_jobs)。

重启语义(这部分设计意图最明确):

  • recurring 任务跳过不 backfill;
  • past-due one-shot “at” 被丢弃——注释对齐 iOS/Slack/Calendar 的行为(补发一次凌晨的闹钟没有意义),记入 last_startup_drops 经 RPC 广播给页面;
  • 除非 fire_missed:重锚 now 立即触发一次;
  • 丢失的:SystemEventQueue(纯内存 max20 FIFO)、WakeScheduler 的排队原因、notified 集合——进程级状态全部归零。

2. 反 runaway 三道闸

  1. silent_fire_limit=12 :连续 12 次静默触发(无用户交互)后任务自动停(service.py:600-636 + cron_stack.py:280-311);
  2. notify_user_active 仅 Origin.USER 重置:机器自己造的交互不算”用户在”(service.py:638-662);
  3. event_wake 限流 300s(schema.py:719-733,WakeScheduler 合并同类唤醒);add_job 五层去重(service.py:711-924)——同消息同目标的重复任务根本进不了队列。

3. Sentinel:设计完备的规划师,本机关闭

无 sentinel 配置块 → SentinelConfig.enabled=False(config/raven.py:295)→ build_sentinel_stack 返回 None(proactive_stack.py:210-211)。若开启,运作方式是:tick 循环(默认 1800s)+ trigger 轮询(2s)+ DeferManager;每 tick 装配上下文(无 LLM 的 assembler)→ 刷新记忆态 → 到时计划位(±20min 窗口)→ 快路径(安静时段/未变上下文 TTL 3600s,不用 LLM)→ planner 决策(LLM)→ _route。

四条执行路径:

路径 行为
nudge DeliveryHub 非回合路径直投(_sentinel_origin=True,永不再入 agent loop,dispatcher.py:1-14)
nudge_inject 入队在下一回合附加(injector.py:143-148)
nudge_defer 等目标 idle 超阈值再走 dispatcher
spawn_agent 走 SubagentManager.spawn,结果以 SUBAGENT origin 回 spine

NudgePolicy 反垃圾数值(trigger_policy/policy.py):max/h=3、max/d=10(硬顶,高优先级也不过)、min_interval 300s、dismiss_cooldown 1800s、ignore_window 21600s、dedup_window 86400s、weekend multiplier 0.5;检查顺序 = quiet hours → day quota → hour quota(adaptive+weekend)→ L5 拒绝冷却(加权拒绝≥3 进冷却)→ L3 主题接受 → L7 周末 cap → topic quota;topic 规范化(_CANONICAL_SUFFIXES);_tuner 每小时按接受率调 hour multiplier。

4. 边界铁律

  • 所有 origin 汇入同一 Scheduler.submit (spine/scheduler.py:571)——主动性和交互不分家,只分池与旁路;
  • 主动性器官不是 runtime 成员:cron/heartbeat/sentinel/wake 是进程级服务,在 rpc/bootstrap.py:313-360 组装——generation 热切换不重启它们,这正是 cron 任务跨 reload 存活的原因;
  • 非 USER origin 的打断语义降级(INTERRUPT/INJECT→APPEND),消息续跑循环跳过(turn_path.py:3106-3116);
  • 外部 vs 内部:healthcheck 是 systemd timer 跑 shell 写 ~/.raven/healthcheck.last_alert——Raven 代码不参与,这是”系统外保健医生”与”系统内心跳”的分层。

七、执行面的牙齿——工具、权限、MCP 与沙箱真相

1. 工具注册与分发:顺序即安全

ToolRegistry.admit_tool(registry.py:64-117)注册期校验(ToolAdmissionError 在注册期抛而非回合内);schema 是 OpenAI function 格式且 copy.deepcopy(parameters)——防工具把 live parameters 泄露给模型。

execute() 的顺序(registry.py:821-1044)每一环都是有意为之:

1
2
可见性(turn_scope 快照)→ 截断护栏(截断调用不执行)→ JSON 修复 → 参数校验
→ 权限闸门 → 插件 tool_gates → 超时(默认 300s,blocking 工具免超时)→ 解包 → 错误回喂

两个要点:权限闸门故意放在超时上限之前——人做的决策不该被 timer 杀;错误回喂只对 retryable 附 hint,本层无重试——重试是模型侧决策。

turn_scope 快照是 MCP 竞速的根因:token = self._turn_names.set(dict(self._tools)) 在回合入口处冻结(registry.py:664,loop 调用点 main.py:868-870)。MCP 异步连接(~1.2s)完成时 CLI one-shot 进程往往已退出,下一回合才可见——所以 CLI 冒烟调 MCP 必报 tool 'mcp_xxx' is not available(官方措辞 “available from a later turn”),而飞书真实路径(常驻 gateway runtime)无此问题。这是 by-design,不是 bug——冒烟验证 MCP 必须走真实消息路径。

wrapper 的 inputSchema 服务端原文透传(mcp/client.py:40)——MCP server 完全控制模型看到的 schema,接第三方 server 时这条就是信任边界。命名空间 mcp_<server>_<tool>,冲突/超长加 12 位 SHA1 后缀;握手看门狗 90s;OAuth 可停车;streamableHttp 自建 httpx client(follow_redirects=False,timeout=None);日志 MCP server '{}': connected, {} tools registered"(manager.py:811)就是运维判定成功的锚点。

合并注:原文本篇在「1. 工具注册与分发」与「3. 权限瀑布」之间还有独立的第 2 节(MCP 连接与信任边界方向),其标题与引导句在网页抓取时被正文摘要器略去未返回;上文第 1 节末段已保留该节可获取的实质要点(inputSchema 服务端原文透传、命名空间与 SHA1 后缀、握手看门狗 90s、OAuth 可停车、streamableHttp 自建 httpx client、日志锚点 manager.py:811)。本地母本《01-主报告-核心逻辑全解剖.md》§12 对同一点的记载为:”MCP:wrapper 的 inputSchema 服务端原文透传(MCP server 完全控制模型看到的 schema);命名空间 mcp_<server>_<tool>(冲突/超长加 12 位 SHA1);握手看门狗 90s;OAuth 可停车;日志 ‘MCP server '{}': connected, {} tools registered’(manager.py:811)即运维手册判定成功的锚点。”另母本同节载有插件信任模型一条(本篇第 1 节 execute() 顺序中”插件 tool_gates”一环即其落点):插件为进程内任意受信任 Python、无沙箱,发现期不 import(除 entry-point 分支),tool_gates 可否掉工具调用但碰不到权限闸门(plugins/manifest.py;registry.py:1046-1067)。

3. 权限瀑布(permissions/gate.py:126-224)

1
2
3
4
mode 解析 → builtin ruling → user DENY → user ALLAY → default tier
→ FULL 只绕过 ask 层(builtin/user DENY 仍优先!)→ session 授权
→ learned(沙箱 exec 不自动放行:Boxlite 以 rw 挂真实 workspace,rm -rf /workspace 删真实文件)
→ SMART + judge_provider → NeedsApproval

配套纪律:

  • builtin.py 硬 deny 模式(mkfs、dd if=、>/dev/sd、fork bomb)压过一切用户 allow(gate.py:5-13);
  • ALLOW_ALWAYS 只持久化 exec 类规则(_persist :391-419);
  • turn digest 去重——同回合同命令不二次打扰;
  • allow_ask=False 或无 responder → refuse(源=UNATTENDED,源=UNATTENDED,fail-closed);
  • ACP 侧同一审批往返的第二传输:一切客户端故障都拒绝——宁可拒掉一百次合法请求,也不放一次来源不明的(permissions.py:169-202 的 catch-all 注释原话逻辑)。

瀑布里唯一面向用户的一跳,实况长这样:

ask_user 快选交互实况

图 6 · ask_user 实况:Raven 跑完一轮自动排查(pendingCrop 复查核查)后把决策权交还用户——「锚点切分必须靠号锚点升级,怎么走?」带 1-4 快选、7 分钟倒计时、可追加备注;选 1 先零改动验证再接线,成了立刻接线。无人值守时,这一跳就是 fail-closed 的拒。

4. SSRF 与密钥卫生

  • security/network.py:judge_url_target 逐跳检查(scheme、私网字面量、全 DNS 解析)、_pin_request 防 DNS rebinding、guarded_fetch 禁 follow_redirects;调用方 web_fetch/media_gen。
  • redact.py 15 类密钥模式覆盖全部客户端负载(acp/updates、acp/replay 等 4 处调用,自述 defence-in-depth)。
  • trust.py 的 wrap_untrusted 只是提示词级包裹——无强制力,别把它当安全边界。

5. 沙箱实况:默认无隔离

这是全篇最需要知道的一条:

  • 默认 backend none → DirectExecutor = 宿主机普通子进程(sh -c);deny-list 仅在 not sandboxed 时生效(shell.py:270-273);
  • interfaces.py 的 is_sandboxed 默认 True 是失败安全方向——但反直觉的是它会让 deny-list 失效:未覆写的第三方执行器申报自己已沙箱化,宿主 deny-list 就跳过了。审第三方执行器时的暗礁;
  • Boxlite 是唯一真隔离(microVM,需 Apple Silicon/KVM,本机未装;allow_net 默认开放);
  • 全包无 bubblewrap / unshare / seccomp / cgroup。

6. RPC 控制面(36.6K 行择要)

  • server.py:JSON-RPC 2.0 换行帧过 TCP-loopback;token 首行 10s 超时(恢复”只有我们 spawn 的孩子能说话”边界);1MiB 帧上限;逐帧 create_task 并发;写锁 + drain 背压。connect_accepted_socket 不用 pipe transport——双向 socket 任何入站字节会触发 reader callback 关闭,send_frame 全静默 no-op(/cha autocomplete 连丢 5 帧的肇事机制,:120-136)。
  • dispatcher.py:自写几十行的理由很实际——pydantic v2 覆盖已够、少依赖=小 pip-audit 面;SystemExit 拦成 internal_error。
  • 方法表(methods/init.py:114-246):~30 组按序注册,顺序即事实(真 handler 必须在 stub 后注册,重名 raise 而非 last-wins);测试 + openrpc.json 钉名。
  • live_probe(161 行):所有外部通道唯一出口;从 lock payload 拿端点+token(token 是 WS 首帧);失败答 None → 渲染 “unknown” 不渲染 “disconnected”——探测失败和真断开是两回事。
  • reload.mcp:廉价 gate 在确认前(没变直接 noop);”reconciled” 不叫 “reconnected”——计数含 detach/失败,叫 reconnect 会把全断报成功。
  • teardown 顺序(bootstrap.py:401-479)= 全系统最纪律的一段:cancel brokers → cron stop → begin_drain + subagents.cancel_all(子代理必须先行——跑完的 announce 进已封 submit 只剩取消)→ turn_teardown → plugin services → drain backend stores(先 drain 后 stop)→ close MCP → browser close(Chromium 持久 profile 锁)→ close_pool(不收 terminal 信号的孤儿)。

7. providers / routing / config

  • provider 抽象(providers/base.py):chat_with_retry/chat_stream 统一入口;parse_llm_error 输出类别+端点(正常见第 2 篇失败面);tool_names 的 sanitary_form 做命名空间净化。
  • routing 是”run 时选实现”(routing.pick)——配置是意图、代码是裁决;实际模型以 config.json 为准(读文档不如读 config,本轮就撞上手册写 glm-5.3-flash 而实际已切 step-5-preview 的实例)。
  • config 顶层键:a2a / agents / channels / language / memory / permissions / providers / subagents / plugins / tools / embedding;渠道配置走 dispense 制(声明键与 socket 字段分离,门禁式准入);24 小时内 6 次 config.json.bak + migrations.json——配置迁移是受支持路径,这段仍在活跃演化。

八、第二服务 everos 与观测层 trajectory

1. everos:被误认作”子包”的独立王国

先纠个架构错觉:很多人(包括初版的运维手册)以为 everos 是 raven 的子包。实际上它是顶层独立包 everos/(v1.4.1,49131 行)+ raven 侧桥接插件 raven_everos/(everos-memory,7006 行)。运行实况:everos server 是 raven gateway 的子进程(PPID=424),监听 127.0.0.1:18791 纯 HTTP REST,无鉴权、靠只绑环回保命。

启动链(raven_everos/server.py):ensure_everos_server(:1057)→ 健康探测(/health + 空 POST /api/v2/memory/search 防”假健康”——只 ping /health 会相信一个装死的服务,:97-115)→ 端口写 everos.toml [api] 后回读校验(:823-853)→ Popen(:961-1043 跨进程 file_lock spawn)→ 日志落 ~/.raven/logs/everos-server.log。everos 侧 uvicorn create_app 双挂 /api/v1 legacy 与 /api/v2 canonical。

存储三层(实测 ~/.raven/everos 53MB):

层 内容 角色
Markdown 21 个物化 SKILL.md(frontmatter: id/confidence/source_case_ids/cluster_id) source of truth
SQLite(23MB) md_change_state LSN 队列、cluster 24 行 / cluster_member 194 行 队列与集群
LanceDB(7 表) 投影(60s optimize / 300s prune / 12h rebuild,quiesce 冻结保只读) 检索

无 git、无 jsonl。纯 HTTP REST 边界——raven 只发消息+检索,算法全在 everos。

skill 自演化闭环(第 5 篇 §4 的运行态细节):

1
2
3
4
5
6
7
8
AgentCaseExtracted
→ trigger_skill_clustering(LLI 判 merge/mint,落 SQLite cluster)
→ emit SkillClusterUpdated(带全案例快照,不读 LanceDB 避 cascade lag)
→ extract_agent_skill(md-first:AgentSkillReader 为存在性唯一真源、≤10 个全入 prompt、
LLM 抽技写 SKILL.md、sanitize_skill_name 防路径注入、改名回收旧目录)
→ cascade 投影(md → LSN → LanceDB)
→ recall(agent_cases + agent_skills,双 BM25+dense)
→ skill_forge/backend_source.py:1-87 重包 RouterHit 进 prompt

两个断点(第 5 篇预告过,这里给证据):feedback 是确定性 no-op(backend.py:1557-1588——everos 无 HTTP 消费端点、confidence 无 service 写路径,raven 发的 skill_usage 信号被丢,”反馈→置信度→演化”闭环是断的);retire 未实现、退化为低 confidence——技能只增不汰。

2. trajectory:没有 daemon 的纯 CLI 轨迹栈

第二轮架构纠偏:传闻中 trajectory 有个 30 行 JSON-RPC daemon、recover.py、CLI goto N/seek 恢复——全部不存在(rpc/methods/ 对 trajectory 零命中)。7446 行 / 15 模块的真实全貌:conversation 821(读侧重建)、redact 627、bugreport 1143、store 713、replay 1204、regression 842、review 505、cassette 359、sanitize 341、report 67、verdict 146、bundle 261、completeness 243、errors 24。轨迹层无网络服务,纯 CLI 面对面。

写入侧:span 日志与三层吞异常

事实记录由 raven/tracing/ 写(agent/loop/_shared.py:78、tools/registry.py:19、subagent/manager.py:61 等经 from raven.tracing import trace 装饰器):audit.span.v1 schema(spans.py:build_span :66-125:traceId/spanId/parentSpanId/name/status.code/attributes/events;attempt.id 是保留键被静默 drop)。

追加失败闭环是全系统最值得抄的一段——三层降级:

1
2
3
trace.py:span() :243-248   ERROR 标记后原样重抛宿主异常(tracing 不改宿主语义)
spans.emit :128-132 except Exception: pass("tracing must never break the host")
TraceStore.append :120-124 except OSError: pass

enabled()=False 时整个 span() 直接 yield no-op(零 I/O),replay 场景可经 ContextVar 级静默。内容寻址:sha1 只用于 blob 命名与自愈(_blobs/<sha1[:2]>/<sha1>.<ext>,_blob_is_intact 缓存 inode+mtime+size 后全文件重算,不符则 _repair_blob 用 os.replace 指回——“审计 trail 不二次撒谎”)。

seal 轮换触发条件(tracing/store.py:_rotate_if_needed :103-118):A=活动日志 mtime 的 UTC 日≠今天(跨日封);B=待写后大小 > max_bytes(默认 50MB,env 可调)。归档成 archive/<day>/audit-spans-<day>-<HHMMSSffffff>.log。实测吻合:零点后跨日封、活动段 2MB 在写。

事务侧:pin 永不 purge

trajectory/store.py(713 行)= pin 注册表 + attempts.json + rotation 透明读:

  • 保留契约(:3-15):pinned ids 永不 purge,purge 工具必须查 is_pinned(:582-602);
  • `merge/split 永远 over-protect:split 时锁内先把定义/别名的 pin 下沉到每个成员再删定义(:502-512)——崩溃或并发交错都只会留冗余保护,不会丢保护;merge 提交失败 compare-and-delete 回滚(:436-448);
  • 读侧:span_log_paths(:649-655)= archive 旧→新 + active 的写序,iter_spans 逐行容错(UnicodeDecodeError/JSONDecodeError 跳行,”一条坏行不掩埋整条轨迹”)。

运维面真实映射

传闻 实物
最小 JSON-RPC daemon ReplayState 原语面(llm_cursor/tool_cursor 双游标,replay.py:204-227),seek/next/verbose 是进程内等价物
replay 从 seek 恢复/断线 seek 真实语义=历史裁剪 _history_cut(:984-1030,时间优先+内容确认);断线=三类 fatal divergence:exhausted / missing output / corrupt recording(574-629)
recover 四命令 bugreport.py 生命周期(:944/1005/1022/1093):draft=进程被杀读时原子转 failed;冻结先行(确认前 snapshot/export 冻结);24h stale staging 清理
seal/upgrade 重操作 seal heavy≈pin never-purge;upgrade heavy≈attempts.json merge/split 迁移(与二进制升级无关)
browse 在包内 在 CLI 不在包内;regression 是静态提交门禁(schema + artifact 路径逃逸检查 + 全 token sha256 人工审 + 256KiB/1MiB 预算)

CLI 退出码 0/1/2(replay),所有 id-taking 命令先 trace id→attempt id 解析,Rich 渲染全过 escape。

3. 系列收束:三句话

  1. Raven 的骨架是”一套总线、五种 origin、六个横切层”——稳定到可以逐层拆下线;
  2. 它的肌肉在接缝处——两套 make-fit 机制的解耦、launch_cwd/session_cwd 的分离、turn_scope 快照与 MCP 异步的竞速、teardown 的子代理优先——全部长在注释里;
  3. 它的边界同样诚实——feedback no-op、retire 缺失、Sentinel 关闭、sandbox 无隔离、eval unwired:知道哪些管子没接线,比会画架构图更重要。

(完)


原始署名与版权