问题描述
Hermes Python 客户端(或其他基于 stdio 的适配器)连接 MemOS bridge 时,首次 session.open RPC 调用持续超时 30-75 秒。Python 适配器 _open_session() 默认 30 秒超时,首次连接抛出 asyncio.TimeoutError。多次重试后 core.init() 执行完毕,调用才最终成功,导致问题难以诊断——看起来像是瞬时的网络或资源问题。
根因
bridge.cts 在 startStdioServer() 之前调用 core.init(),导致孤儿 episode 恢复阻塞了 stdio 读循环。
原启动顺序:
bootstrapMemoryCoreFull() // 打开 SQLite,初始化 core 对象
↓
await core.init() // ◄── 处理孤儿 episode,调用 LLM
↓
startStdioServer({ core }) // ◄── stdio 读循环至此才启动(太晚了)
core.init()(core/pipeline/memory-core.ts:594)在返回前执行以下操作:
- 扫描 episodes 表,查找状态为
status="open" 的孤儿 episode(第 608 行)
- 同步关闭轻量级孤儿 episode
- 通过
recoverOpenEpisodesAsSessionEnd() 恢复陈旧的 open episode——调用 LLM 计算奖励(第 637 行),通常耗时 10-60+ 秒
- 通过
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_OPEN(bridge/methods.ts:119)路由到 core.openSession()(memory-core.ts:1232),其执行路径:
- 调用
ensureLive()——只检查 shutDown 标志(第 490 行),不检查 initialized
- 调用
withNamespaceMeta() 和 namespaceFromHints()——纯函数
- 调用
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.ts 中 CORE_INIT RPC 方法存在(第 107 行),但没有任何 stdio 适配器调用它——bridge 始终在进程内调用 core.init()。这个 RPC 看起来是死代码。如果确实是,建议清理或标记。
建议
- 在
bridge.cts 中将 startStdioServer() 移到 core.init() 之前(1 行结构性变更,零风险,见上方修复方案)
- 可选:添加注释说明排序原因(孤儿恢复可能因 LLM 调用将 stdio 处理延迟数分钟)
- 可选:添加就绪信号到 stdio 协议(例如
core.init() 完成后发射 server.ready 通知),让适配器在需要依赖 init 的功能(如 capture、reflection)时可以等待完全就绪
问题描述
Hermes Python 客户端(或其他基于 stdio 的适配器)连接 MemOS bridge 时,首次
session.openRPC 调用持续超时 30-75 秒。Python 适配器_open_session()默认 30 秒超时,首次连接抛出asyncio.TimeoutError。多次重试后core.init()执行完毕,调用才最终成功,导致问题难以诊断——看起来像是瞬时的网络或资源问题。根因
bridge.cts在startStdioServer()之前调用core.init(),导致孤儿 episode 恢复阻塞了 stdio 读循环。原启动顺序:
core.init()(core/pipeline/memory-core.ts:594)在返回前执行以下操作:status="open"的孤儿 episode(第 608 行)recoverOpenEpisodesAsSessionEnd()恢复陈旧的 open episode——调用 LLM 计算奖励(第 637 行),通常耗时 10-60+ 秒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_OPEN(bridge/methods.ts:119)路由到core.openSession()(memory-core.ts:1232),其执行路径:ensureLive()——只检查shutDown标志(第 490 行),不检查initializedwithNamespaceMeta()和namespaceFromHints()——纯函数sessionManager.openSession()——纯 SQLite upsert + 事件总线发射所有这些在
core.init()之前都能正常工作,因为 SQLite 数据库和事件总线在bootstrapMemoryCoreFull()期间已经建立。会话创建不依赖core.init()。此外,
startStdioServer()在模块加载时订阅 core 事件和日志(第 97-102 行),事件处理器在任何事件触发前就已注册。修复方案
文件:
bridge.cts将
startStdioServer({ core })移到await core.init()之前:这样确保孤儿恢复开始前 stdio 读循环已经激活。Python 发送
session.open时立即被处理。孤儿恢复在后台进行(core.init()内的所有异步工作),恢复完成时会话已经打开。安全性分析
ensureLive()在 init 前shutDown标志(默认false)——通过openSession()SQLite 依赖bootstrapMemoryCoreFull()时已打开,早于两者startStdioServer()构造函数中订阅,不依赖 initinitialized标志init()最顶部设为true(第 601 行),在任何 await 之前附加说明
CORE_INITRPC 方法bridge/methods.ts中CORE_INITRPC 方法存在(第 107 行),但没有任何 stdio 适配器调用它——bridge 始终在进程内调用core.init()。这个 RPC 看起来是死代码。如果确实是,建议清理或标记。建议
bridge.cts中将startStdioServer()移到core.init()之前(1 行结构性变更,零风险,见上方修复方案)core.init()完成后发射server.ready通知),让适配器在需要依赖 init 的功能(如 capture、reflection)时可以等待完全就绪