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、状态机。当导入已有笔记时:
- 笔记中可能使用不同的字段名(如
状态vsstatus) - 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)
runex-vault 默认绑定对所有用户创建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_extraction 用 signal-work 创建了 handler=“extract” 的 WorkItem;
后续 dogfood_extract_atom 用 signal-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.scm 的 request_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.scm 的 request_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): 当前的实现遵循分布式系统的最终一致性原则:
- 用户的编辑意图先到先得:用户写值到 Obsidian 文件 → ingest → store.set_field 立即可见。这是人机交互的”我在改文件”的实时反馈。
- 反应式裁决异步判定:guard action 在写入之后判断”该不该改”,是事后审计/裁决,不是 拦截。这是事务处理的”先接受、再判断、再校正”模型。
- 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 构造改成优先用这个字段。破坏性改动,需要:
- scm 加新字段
- agent.scm 加载 list 更新
- profiles.py 重构 argv builder
- 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 构造改成优先用这个字段。破坏性改动,需要:
- scm 加新字段
- agent.scm 加载 list 更新
- profiles.py 重构 argv builder
- backward compat:现有
模型字段仍 work as before
Scope: 不阻塞功能(profile docstring + README 已说清楚),但 UX 上有歧义。