Skip to Content
文档已知限制

Known Limitations (Dogfood v0.2-dev)

Last updated: 2026-07-05 (post-control-plane primitives)


1. Ingest writes all fields; guard actions protect machine state

What: Task 的 状态 字段在文件 re-ingest 时会被 ingest 写入图——不再静默跳过。 但机器状态由 guard action (correct_task_status) 保护:如果文件里的值与 __state__Task 不一致,guard 立即纠正并 writeback 回文件。人在 Obsidian 打开文件能看到被纠正后的值。

Design choice: ingest 是全量写入的管道,不做字段过滤。字段保护由 SCM 层的 guard action 完成——这是业务规则,不是 I/O 策略。tx_log 中可见:ingest 写了一条旧值, guard 紧接着纠正为新值——完整审计链。

UX implication: 人改 状态 在 Obsidian 保存后,打开文件看到值被机器改回去了——这是 显式的”此字段由机器控制”信号,而非静默丢弃。


2. State transitions require machine actions; frontmatter edits don’t trigger them

What: 把 Task 的 状态open 改为 in_progress,唯一的合法路径是通过 start_task action(core/memory.scm:362-370),它跑 guard → transition → set-field。直接在 Obsidian 改 frontmatter 不会触发状态机——ingest 会接受这个字段值,Guard action 会把不一致的值纠正回机器状态, 也只是改了 field value,不会跑 transition 的 effect chain。

Scope: M1(ADR-0013)未覆盖 machine action 的 CLI 触发和 agent 调度。“在 Obsidian 点一个按钮让 runex 跑 start_task” 是整个闭环的缺环,属于后续 ADR 范围(ADR-0010 agent dispatch + ADR-0012 agent-as-graph-node)。

Workaround: 当前只能通过 runex CLI 手动执行 action(如果 CLI 已支持;M1 中未验证)。


3. SCM 与导入已有笔记库的兼容性

What: runex init 安装的 SCM 定义了完整的字段 schema、状态机。当导入已有笔记时:

  • 笔记中可能使用不同的字段名(如 状态 vs status
  • required 字段可能缺失
  • 状态机定义的合法 state 值可能与已有笔记中的实际值不匹配

解决方案: Path C onboarding 流程:survey 用户已有笔记 → 按用户 convention 设计 匹配的 SCM → 在同个仓库上 bind + sync + listen。SCM 适配用户的字段名和 type 值, 不强制用户改名。

方向: 不做 schema-mapping 层或 field-alias 翻译层——那是伪命题。正确做法是 fit runex to the user’s conventions,而非反向。


4. Frontmatter-only 笔记的解析(已修复)

What (fixed):--- 结尾但没有 trailing newline 的笔记(纯 frontmatter 模板、 刚写完没换行就保存的文件),被 Obsidian Markdown Connector 的正则解析为空 frontmatter → Recipe 返回 None → 整条笔记不入库。

Fix: 6d754a3 — 正则从 ^---\s*\n(.*?)\n---\s*\n(.*)$ 改为 ^---\s*\n(.*?)\n---[ \t]*\n?(.*)$,允许 closing --- 后直接 EOF。


5. Ingest 方向无 Conflict 检测

What: writeback 方向(project_task action)在发现文件被并发编辑时会建 Conflict 节点。Ingest 方向不做 baseline 比较——全量写入字段,由 guard action(如 correct_task_status)在 ReactiveBus 上纠正不一致。此过程在 tx_log 中有完整审计链 (ingest 写旧值 → guard 纠正),但不会像 writeback 方向那样创建 Conflict 节点。


6. RegistryBinding 与 IngestConnection 的 live-reload 未覆盖

What: runex vault sync 是 one-shot CLI,不在 reactive bus 上。如果 ontology manifest 变化后(新增了 supertag、改了字段),listen 不会自动重新下发模板/Base 到 Obsidian vault。用户需要手动再跑 runex vault sync <key> --apply

Workaround: 改完 .scm + runex ontology load 后,手动 runex vault sync。 自动重下发属于 future ADR 范围(manifest-change → RegistryBinding 感知 → 自动 sync)。


7. runex-vault 默认绑定对所有用户创建 (已解决,2026-08-08)

What (historical): runex init 曾经无条件创建 ~/.runex/runex-vault/ 默认绑定 和模板/Base 文件(ADR-0013 §“Opinionated default binding”)。对于不使用 Obsidian 的用户,这是几百 KB 的冗余文件。

Resolution: 极简基座演进后,runex init 不再创建默认 vault / 绑定 / 连接。 用户需要 Obsidian 时显式 runex vault bind default <path> + runex vault sync


8. signal-work / signal-agent 幂等去重的 work_key 冲突

What (2026-06-29 dogfood 实测发现): 如果两个 action 对同一个 subject 使用相同的 work_key(如 (str "extract-" (self))),第二个 signal-agent 调用被 get_or_create_node_by_identity 幂等拦截——返回已存在的 WorkItem,不更新 handler/agent 字段。

复现: request_extractionsignal-work 创建了 handler=“extract” 的 WorkItem; 后续 dogfood_extract_atomsignal-agent 想创建 handler=“dispatch-agent” 的 WorkItem——work_key 相同 → 返回旧节点 → handler 仍是 “extract”。

Workaround: 不同 action 使用不同的 work_key 前缀(如 extract-<id> vs dogfood-<id>),或在设计上确保同一 subject 在同一状态下只被一个 action signal。


9. extract handler 未注册(Phase 9 agent-mesh 缺口)

What: extraction.scmrequest_extraction action 使用 (signal-work ... "extract" ...) 创建 WorkItem,但 worker host 中没有注册 “extract” handler。 所有 extract WorkItem 在 3 次重试后 failed,错误: no handler registered: 'extract'

Scope: “extract” handler 属于 Phase 9 agent-mesh 的 handler registry,不在 M1 范围。 dispatch-agent handler 已注册并在 listen 宿主中可用(通过 signal-agent 测试验证)。

Workaround: 使用 (signal-agent ...) 而非 (signal-work ... "extract" ...); signal-agent 创建的 WorkItem handler=“dispatch-agent” 可被 worker 正常认领并 subprocess agent CLI。


10. Reactive trigger 竞争:同一 from-state + 同一 trigger 的多个 action

What: 当两个 action 声明相同的 (machine "Thought")(from-states "new")(trigger (on field-set (field "正文") ...))时,Thought 创建后两个 action 都会触发。 两者竞争 transition(都试图 transition 到 “extracted”),先到者成功,后到者遇到 illegal_transition 错误。

复现: extraction.scmrequest_extraction 和 dogfood-demo 的原始版本都从 state “new” 触发 (on field-set 正文)

Workaround: 每组 (machine, from-state, trigger) 只保留一个 reactive action。 需要多 agent 链式处理时,通过 WorkItem cascade 或 sequential state transitions 实现, 不通过同一 trigger 的并行竞争。


10. FOREIGN KEY 约束冲突:跨实例 node re-ingest

What: 当同一个 Obsidian note 从 runex 实例 A 的 vault 移动到实例 B 重新 ingest 时,A 遗留的 _sync_file_hash、来源路径等字段随文件携带。实例 B 的 upsert 操作复用 已有 node_id 但 set_field 写入字段时,如果两实例的 supertag 内部 ID 不同,可能触发 IntegrityError: FOREIGN KEY constraint failed

Why: fields 表的 node_id REFERENCES nodes(id) 约束要求 node 存在。跨实例 场景下 upsert_node_from_item 的 get-or-create 逻辑在并发 SQLite 连接(listen 进程 与 ontology run 进程)之间可能出现可见性延迟——listen 刚创建的 node 尚未对 run 进程 的 DB 连接可见。

Scope: 仅在跨 RUNEX_HOME 的 vault 迁移 + 并发进程访问时触发。同一 RUNEX_HOME 的单进程 listen 无此问题。

Workaround: 迁移 vault 到新实例时,删除旧 _sync_file_hash 字段后再 re-ingest; 或使用 runex connection pull --mode once 先全量扫描一次再起 listen。


11. reject-change! 不回滚触发 cascade 的原始 set_field

Date: 2026-07-05 (control-plane delta primitives)

What: (reject-change! REASON) effect 在 reactive cascade 中被调用时:

  • ✅ 抛 ChangeRejected 异常
  • ✅ 当前 effect 的 writes 被 abort(dispatch 捕获后返回 False)
  • ✅ emit rejected_change 审计事件到 tx_log(独立 tx,不在原 set_field 的 atomic 内)
  • ⚠️ 触发 cascade 的原始 set_field 不会被回滚——那个 write 已经在 store._txn 里 commit 了

Why (architectural trade-off): 当前的实现遵循分布式系统的最终一致性原则:

  1. 用户的编辑意图先到先得:用户写值到 Obsidian 文件 → ingest → store.set_field 立即可见。这是人机交互的”我在改文件”的实时反馈。
  2. 反应式裁决异步判定:guard action 在写入之后判断”该不该改”,是事后审计/裁决,不是 拦截。这是事务处理的”先接受、再判断、再校正”模型。
  3. Sink writeback 负责最终回正:引擎裁决后通过 watcher 触发 project_* action, 把 graph 状态(或旧值)写回源文件,达到用户文件层面的最终一致。

(reject-change! REASON) 当前只完成第 2 步(审计 + effect abort),第 3 步靠 watcher 触发 sink writeback 把 graph 旧值反弹回用户文件。如果要”原 set_field 也回滚”,需要把 store.set_field 包进 atomic 块,与 reactive cascade 在同一个事务里——这是更大的 架构改造,需要重新设计 Store.set_field 的事务边界。

实际满足的保证:

  • ✅ AI Agent 可订阅 rejected_change 审计事件做反思学习(op=‘rejected_change’, actor=‘system:reactive’,payload 含 prev_value / post_value / field / reason)
  • ✅ Obsidian 审批单保护闭环可用(旧模式 set-field 模板 + writeback 处理 sink 端反弹, reject-change! 触发审计事件——双模式配合)
  • ✅ 触发 cascade 的 action 自己的 writes 被 abort(不污染 graph)

Scope: 当前 release 不需要真回滚。reject-change! 主要服务 AI Agent 审计场景, 这是 v0.2 的设计目标。Sink writeback 的反弹路径已通过旧模式验证。

Workaround: 若需”原 change 也回滚”,临时方案:

  • 使用旧模式 (set-field X 模板) + (writeback "sink"),由 sink 把 graph 旧值写回 用户文件,达到用户文件层面的”撤销”
  • 用户编辑后打开文件看到的就是 graph 当前值(机器校正后的),相当于文件层撤销

Future (long-term): 如果未来有场景必须做到”graph 与用户编辑完全无关”(例如 金融场景的用户修改必须原子拒绝),需要重构 Store.set_field 走 atomic + cascade 的事务模式,独立 ADR 跟进。这是架构权衡,不是 bug,作为长期 Issue 挂起跟踪,不在当前 sprint 修。


12. runex CLI 缺少 sync-mode 实时监听

Date: 2026-07-05

What: runex serve 子命令不存在。当前只有 runex connection pull(一次性拉取) 和 runex top / runex ps(运维面板)。watch 模式依赖 worker host 进程长驻。

Workaround: 用户跑 runex connection configure --mode watch 后用 runex top 持续观察 WorkerHost 状态,或后台跑 listener 进程。

Scope: M1(ADR-0013)未覆盖 single-process watch 模式,watch 必须 worker host 多进程。 single-process watch 是后续优化项。

7. openclaw integration gaps (M-openclaw, 2026-07-06)

What: openclaw (openclaw.ai) 通过 --agent <id> 切多 agent 的 Gateway 编排器。 我们刚加了 catalog entry + openclaw CliProfile + Note 自动包装,但以下三件事还 没做完,会真实影响用户体验:

7.1 已装的 runex Mach-O 二进制里没有 openclaw profile

src/runex/worker/profiles.py 新加了 openclaw entry(构造 --agent <model> --message <prompt> --json --timeout 300),但当前 runex agent register --profile openclaw 会触发警告:

! 未知 profile 'openclaw'(将退回 参数模板 通用模式)

因为 PROFILES 是在 Python 源码里 hardcoded 的,编译进 Mach-O 二进制后是 frozen 的runex init 加载 SCM 时不会拉到 profile 字典(profile 是 Python 层的数据)。

Workaround: 用户跑 register 时忽略警告,dispatch kernel 看到 profile="" 会回退 到「参数模板」通用路径(需要先在 Agent 节点设 参数模板="agent --agent {model} --message {prompt} --json --timeout 300",每行 dispatch 时做插值)。

Fix: 重编 runex Mach-O 二进制,把新 PROFILES 烧进去。follow-up:等 build pipeline 跑一次 ./scripts/runex/build-release.sh 出新版二进制。

Scope: 阻塞用户实际使用 openclaw profile 的 argv 拼装(虽然 catalog discover 还能 找到 binary,Agent 节点也注册成功,list 输出也对)。

7.2 cancel 杀的是本地 CLI,不会 abort Gateway 端的 run

openclaw 的 cancel 协议是 CLI 发 chat.abort RPC 给 Gateway。文档原文:

SIGTERM/SIGINT 会中断正在等待的 Gateway 网关支持请求;如果 Gateway 网关已经接受 了运行,CLI 还会在退出前针对该运行 ID 发送 chat.abort。

我们 M3 实现的 cancel 流程是 proc.kill()(SIGKILL)—— 没有chat.abort 协议。 后果:

  • 本地 openclaw agent 进程被杀掉
  • 但 Gateway 端的 run 还在继续跑(直到自然完成或自己的超时)
  • 用户看到 WorkItem 已 canceled,但过几分钟后 gateway 仍然会推一条「任务已完成」回来
  • agent_run.cancelled=true 触发的是本地反应,对 Gateway 端无影响

Workaround: 接受这个 race。openclaw Gateway 端的完成消息会被 _record_dispatch_outcome 记录为 succeeded,WorkItem 已经 canceled 所以不影响图状态。

Fix: 调研 openclaw 的 chat.abort 协议(可能是 RPC、CLI 子命令、或 HTTP API)。 dispatch handler 在 cancel 时改用 openclaw chat.abort --run-id <id> 走官方协议。 需要先从 stdout JSON 里 parse 出 runId(我们 M-openclaw 的 extract 已经能拿到)。

Scope: 阻塞「用户点 cancel 后能立即看到 Gateway 端也停了」的语义保证。

7.3 --model 字段双重语义靠文档强制

openclaw profile 下,Agent.模型 字段被翻译为 --agent <id>(openclaw 的 agent id), 不是 LLM 模型名。这跟其他 profile(claude、pi、opencode)的语义冲突——那些 profile 里 模型 就是字面意思的 LLM 模型名(sonnet、opus、haiku 等)。

当前缓解: profile docstring 里写明(src/runex/worker/profiles.py 第 ~110 行), README + CLI-CONTRACT 也注明了。但 schema 层(scm)没有强制——用户给 openclaw agent 的 模型 字段填 sonnet 会被当作 openclaw agent id「sonnet」传给 --agent,openclaw CLI 会报”agent not found”。

Workaround: 用户需自检(fill 时区分;list 表格的 model 列一眼能看出)。

Fix: 给 Agent supertag 加可选字段 openclaw_agent_id(仅当 profile=openclaw 时 有意义),并把 dispatch handler 的 argv 构造改成优先用这个字段。破坏性改动,需要:

  1. scm 加新字段
  2. agent.scm 加载 list 更新
  3. profiles.py 重构 argv builder
  4. backward compat:现有 模型 字段仍 work as before

Scope: 不阻塞功能(profile docstring + README 已说清楚),但 UX 上有歧义。

7. openclaw integration gaps (M-openclaw, 2026-07-06)

What: openclaw (openclaw.ai) 通过 --agent <id> 切多 agent 的 Gateway 编排器。 我们刚加了 catalog entry + openclaw CliProfile + Note 自动包装,但以下三件事还 没做完,会真实影响用户体验:

7.1 已装的 runex Mach-O 二进制里没有 openclaw profile

src/runex/worker/profiles.py 新加了 openclaw entry(构造 --agent <model> --message <prompt} --json --timeout 300),但当前 runex agent register --profile openclaw 会触发警告:

! 未知 profile 'openclaw'(将退回 参数模板 通用模式)

因为 PROFILES 是在 Python 源码里 hardcoded 的,编译进 Mach-O 二进制后是 frozen 的runex init 加载 SCM 时不会拉到 profile 字典(profile 是 Python 层的数据)。

Workaround: 用户跑 register 时忽略警告,dispatch kernel 看到 profile="" 会回退 到「参数模板」通用路径(需要先在 Agent 节点设 参数模板="agent --agent {model} --message {prompt} --json --timeout 300",每行 dispatch 时做插值)。

Fix: 重编 runex Mach-O 二进制,把新 PROFILES 烧进去。follow-up:等 build pipeline 跑一次 ./scripts/runex/build-release.sh 出新版二进制。

Scope: 阻塞用户实际使用 openclaw profile 的 argv 拼装(虽然 catalog discover 还能 找到 binary,Agent 节点也注册成功,list 输出也对)。

7.2 cancel 杀的是本地 CLI,不会 abort Gateway 端的 run

openclaw 的 cancel 协议是 CLI 发 chat.abort RPC 给 Gateway。文档原文:

SIGTERM/SIGINT 会中断正在等待的 Gateway 网关支持请求;如果 Gateway 网关已经接受 了运行,CLI 还会在退出前针对该运行 ID 发送 chat.abort`。

我们 M3 实现的 cancel 流程是 proc.kill()(SIGKILL)—— 没有chat.abort 协议。 后果:

  • 本地 openclaw agent 进程被杀掉
  • 但 Gateway 端的 run 还在继续跑(直到自然完成或自己的超时)
  • 用户看到 WorkItem 已 canceled,但过几分钟后 gateway 仍然会推一条「任务已完成」回来
  • agent_run.cancelled=true 触发的是本地反应,对 Gateway 端无影响

Workaround: 接受这个 race。openclaw Gateway 端的完成消息会被 _record_dispatch_outcome 记录为 succeeded,WorkItem 已经 canceled 所以不影响图状态。

Fix: 调研 openclaw 的 chat.abort 协议(可能是 RPC、CLI 子命令、或 HTTP API)。 dispatch handler 在 cancel 时改用 openclaw chat.abort --run-id <id> 走官方协议。 需要先从 stdout JSON 里 parse 出 runId(我们 M-openclaw 的 extract 已经能拿到)。

Scope: 阻塞「用户点 cancel 后能立即看到 Gateway 端也停了」的语义保证。

7.3 --model 字段双重语义靠文档强制

openclaw profile 下,Agent.模型 字段被翻译为 --agent <id>(openclaw 的 agent id), 不是 LLM 模型名。这跟其他 profile(claude、pi、opencode)的语义冲突——那些 profile 里 模型 就是字面意思的 LLM 模型名(sonnet、opus、haiku 等)。

当前缓解: profile docstring 里写明(src/runex/worker/profiles.py 第 ~110 行), README + CLI-CONTRACT 也注明了。但 schema 层(scm)没有强制——用户给 openclaw agent 的 模型 字段填 sonnet 会被当作 openclaw agent id「sonnet」传给 --agent,openclaw CLI 会报”agent not found”。

Workaround: 用户需自检(fill 时区分;list 表格的 model 列一眼能看出)。

Fix: 给 Agent supertag 加可选字段 openclaw_agent_id(仅当 profile=openclaw 时 有意义),并把 dispatch handler 的 argv 构造改成优先用这个字段。破坏性改动,需要:

  1. scm 加新字段
  2. agent.scm 加载 list 更新
  3. profiles.py 重构 argv builder
  4. backward compat:现有 模型 字段仍 work as before

Scope: 不阻塞功能(profile docstring + README 已说清楚),但 UX 上有歧义。

Last updated on