Skip to content

bridge.cts 启动竞态: startStdioServer 应在 core.init 之前启动 #1747

Description

@numuly

问题描述

Hermes Python 客户端(或其他基于 stdio 的适配器)连接 MemOS bridge 时,首次 session.open RPC 调用持续超时 30-75 秒。Python 适配器 _open_session() 默认 30 秒超时,首次连接抛出 asyncio.TimeoutError。多次重试后 core.init() 执行完毕,调用才最终成功,导致问题难以诊断——看起来像是瞬时的网络或资源问题。


根因

bridge.ctsstartStdioServer() 之前调用 core.init(),导致孤儿 episode 恢复阻塞了 stdio 读循环。

原启动顺序:

bootstrapMemoryCoreFull()         // 打开 SQLite,初始化 core 对象
  ↓
await core.init()                  // ◄── 处理孤儿 episode,调用 LLM
  ↓
startStdioServer({ core })         // ◄── stdio 读循环至此才启动(太晚了)

core.init()core/pipeline/memory-core.ts:594)在返回前执行以下操作:

  1. 扫描 episodes 表,查找状态为 status="open" 的孤儿 episode(第 608 行)
  2. 同步关闭轻量级孤儿 episode
  3. 通过 recoverOpenEpisodesAsSessionEnd() 恢复陈旧的 open episode——调用 LLM 计算奖励(第 637 行),通常耗时 10-60+ 秒
  4. 通过 recoverDirtyClosedEpisodes() 恢复脏 closed episode——再次调用 LLM(第 644 行)

这些恢复操作运行时,stdio 读循环尚未启动。Python 适配器将 session.open 写入 stdin,但数据停留在管道缓冲区中无人读取。适配器有 30 秒超时,而孤儿恢复可能超过这个时间,因此调用超时。

为什么最终能成功: 多次重试后 core.init() 完成,stdio 循环启动,session.open 才被处理。

为什么新安装不受影响: 新安装没有或只有极少的孤儿 episode,core.init() 毫秒级完成——竞态条件不会触发。只有在 bridge 有活跃聊天会话、崩溃或重启后留下孤儿 episode 时才会出现。


为什么 core.init() 放在前面?

可能是假设 core 必须完全初始化后才能接受 RPC 调用。但 SESSION_OPENbridge/methods.ts:119)路由到 core.openSession()memory-core.ts:1232),其执行路径:

  1. 调用 ensureLive()——只检查 shutDown 标志(第 490 行),不检查 initialized
  2. 调用 withNamespaceMeta()namespaceFromHints()——纯函数
  3. 调用 sessionManager.openSession()——纯 SQLite upsert + 事件总线发射

所有这些在 core.init() 之前都能正常工作,因为 SQLite 数据库和事件总线在 bootstrapMemoryCoreFull() 期间已经建立。会话创建不依赖 core.init()

此外,startStdioServer() 在模块加载时订阅 core 事件和日志(第 97-102 行),事件处理器在任何事件触发前就已注册。


修复方案

文件: bridge.cts

startStdioServer({ core }) 移到 await core.init() 之前

// 修改前(有问题):
await core.init();
// ...(遥测、错误处理、daemon 检查)...
stdio = startStdioServer({ core });    // ≈第 290 行

// 修改后(修复):
stdio = startStdioServer({ core });    // ← 立即开始读取 stdin
bridgeStatus?.markConnected();
const bridgeHeartbeat = bridgeStatus?.startHeartbeat();

await core.init();                     // ← 孤儿恢复在后台运行
                                       //    此时 stdio 已在处理 RPC

这样确保孤儿恢复开始前 stdio 读循环已经激活。Python 发送 session.open 时立即被处理。孤儿恢复在后台进行(core.init() 内的所有异步工作),恢复完成时会话已经打开。


安全性分析

关注点 评估
ensureLive() 在 init 前 只检查 shutDown 标志(默认 false)——通过
openSession() SQLite 依赖 数据库在 bootstrapMemoryCoreFull() 时已打开,早于两者
事件总线处理器 startStdioServer() 构造函数中订阅,不依赖 init
initialized 标志 init() 最顶部设为 true(第 601 行),在任何 await 之前
竞态:孤儿恢复修改会话 孤儿恢复只处理 closed episode(脏奖励)和 有陈旧 trace 的 open episode——新建的会话永远不会被触及

附加说明

CORE_INIT RPC 方法

bridge/methods.tsCORE_INIT RPC 方法存在(第 107 行),但没有任何 stdio 适配器调用它——bridge 始终在进程内调用 core.init()。这个 RPC 看起来是死代码。如果确实是,建议清理或标记。

建议

  1. bridge.cts 中将 startStdioServer() 移到 core.init() 之前(1 行结构性变更,零风险,见上方修复方案)
  2. 可选:添加注释说明排序原因(孤儿恢复可能因 LLM 调用将 stdio 处理延迟数分钟)
  3. 可选:添加就绪信号到 stdio 协议(例如 core.init() 完成后发射 server.ready 通知),让适配器在需要依赖 init 的功能(如 capture、reflection)时可以等待完全就绪

Metadata

Metadata

Assignees

Labels

ai-doneAI task completed successfullybugSomething isn't working | 功能异常pluginPlugin/adapter/bridge layer (apps/ directory) | 插件/适配层

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions