首页
/ 深入解析 MemPalace Agent Logstream:本地优先、不可变追加的多 Agent 协作事件层(RFC 003)

深入解析 MemPalace Agent Logstream:本地优先、不可变追加的多 Agent 协作事件层(RFC 003)

2026-09-07 23:38:10作者:胡唯隽

MemPalace 首先是一套 AI 记忆系统,但共享同一座 palace 的多个 Agent 还需要互相协调:委派工作、等待回复、无人工转发地交接补丁。Agent Logstream 正是这一协作层(对应 RFC 003),它是与记忆宫殿同源、由同一 MemPalace hub 提供的一个小型追加式事件日志。阅读完本文,你将掌握 Logstream 的事件/工件模型、经典双 Agent 委托循环、八个 MCP 工具与 CLI 的对应关系,以及最重要的——如何用正确的游标与监听模式搭起一套不会漏事件、不会重复唤醒的 Agent 协作监控体系。

从记忆到协调:Logstream 在 MemPalace 中的定位

MemPalace 提供的两套通道职责完全不同,官方共享大脑协调协议 coordination-protocol.md 将其明确区分为两层:

  • 记忆层(Palace 抽屉、知识图谱、diary):存放值得日后回想的知识,用语义搜索访问,遵循 recall-protocol.md
  • 协调层(Logstream 事件与工件):承载此刻正在 Agent 之间流动的活动工作——委派、回复、补丁、确认(ack)。它用结构化过滤访问,从不做语义搜索。

判断口诀:如果另一个 Agent 需要“行动起来”,那是一条事件;如果未来会话需要“知道”,那是一个抽屉。 一次完成的委托通常两者兼有——事件搬运了工作,抽屉记录下结论。

Logstream 被刻意设计为独立于向量索引:它在 palace 目录中以独立文件 logstream.sqlite3 存在,不打开任何 Chroma 句柄(logstream.py 模块 docstring 明确 “No Chroma dependency, no vector index open — plain SQLite only”)。因此即使 palace 正在被挖掘、修复或重建索引,协调依然正常工作。它随 hub 一起本地可达(loopback、LAN 或 tailnet),无云端队列、无 Redis、无 SaaS。

四个贯穿始终的设计承诺

Logstream 与 MemPalace 其余部分遵循同样的承诺,RFC 003-agent-logstream-coordination.md 将其列为设计原则:

承诺 含义 源码佐证
Local-first 日志活在 palace 目录内,与 hub 其余部分一样经 loopback/LAN/tailnet 可达 logstream.sqlite3_resolve_logstream_path()mcp_server.py)严格解析在 palace 目录内
Exact payloads 事件体与工件内容逐字节原样存储,附 SHA-256 供校验 put_artifact 计算 sha256(raw).hexdigest(),复制校验同样严格执行(logstream.py
Append-only 事件不可变;更正与确认是“引用旧事件的新事件” 唯一的“修改”入口 ack_event 实为追加 event.ack 事件
Durable before realtime 断线重连后一切可恢复,没有任何东西只活在 socket 里 每个实时读取接口都支持 since_event_id 续传

实现上,Logstream 类(logstream.py)使用 SQLite WAL 模式 + 每实例互斥锁 + check_same_thread=False,与 knowledge_graph.py 同款并发策略,可安全承载 MCP HTTP 服务器的多工作线程写入。

事件模型:结构化信封 + 逐字正文

事件是携带路由元数据与可选逐字正文的结构化协调消息。官方文档给出的标准事件形如:

{
  "id": "evt_20260702T032443_02ce0c31acdb",
  "type": "patch.ready",
  "stream": "project/mempalace",
  "room": "patches",
  "from_agent": "windows-codex",
  "to_agent": "mac-codex",
  "correlation_id": "task_123",
  "branch": "feat/my-feature",
  "base_commit": "2668053",
  "status": "ready",
  "artifact_ids": ["art_20260702T032443_e5cb86f7aba8"],
  "body": "Search ranking patch is ready. Tests passed on Windows.",
  "created_at": "2026-07-02T03:24:43Z"
}

各字段语义(来自 agent-logstream.md 与 RFC 003):

  • stream——宽频道。project/<name> 用于按项目分工,或共享频道如 shared_agent_brain
  • room——子频道,如 delegationpatchesreviewsstatus
  • topic(可选)——把围绕某条轨道(如 auth-v2ui-redesign)的相关任务或子团队分组,避免同一稳定的 <machine>-<harness> 身份内部互相串扰。acks 默认继承目标事件的 topic,也允许显式覆盖。
  • correlation_id——把一个请求与其回复、确认串起来。每个任务生成一个并贯穿整个交换过程。
  • to_agent——定向发给某 Agent,或 * 广播;对 to_agent 的过滤会自动命中广播事件。
  • status——取值固定为 openclaimedreadyappliedblockedfailedsuperseded 之一。
  • seq(每条事件返回)——追加序游标。把最后看到的事件 id 作为 since_event_id 传入即可精确续读。
  • created_at——服务器生成的 UTC 时间戳,秒级精度 YYYY-MM-DDTHH:MM:SSZ;同秒内的顺序关系由 SQLite rowid(即 seq)裁决,而非时间戳。

为保证“路由字段必须结构化、不得从散文里推断”,源码做了字符集级校验(logstream.py):事件类型须匹配 ^[a-z0-9][a-z0-9_.-]{0,63}$stream/room/topic/from_agent/to_agent/correlation_id 等路由字段最长 256 字符,且拒绝 NUL 字节与控制字符(stream 因可含 / 而略宽松于普通名称清洗)。事件正文默认上限 256 KiBDEFAULT_MAX_BODY_BYTES),超限直接报错、绝不静默截断。

事件正文超长怎么办?规则是“过大就存成工件引用它”。这引出工件模型。

工件模型:逐字节存储 + 哈希校验

工件(artifact)是附加到事件的精确内容:统一 diff、生成的文件、测试日志、JSON 报告等。工件以逐字原文存储,附 sha256size_bytes,接收方在应用任何东西前可先校验完整性。v1 工件仅限 UTF-8 文本,上限 4 MiBDEFAULT_MAX_ARTIFACT_BYTES,见 logstream.py 顶部的 MAX_METADATA_BYTES = 64 * 1024DEFAULT_MAX_ARTIFACT_BYTES = 4 * 1024 * 1024)。

put_artifact 返回的工件记录(不回显正文,读者通过 get_artifact 取):

{
  "id": "art_20260702T032443_e5cb86f7aba8",
  "kind": "patch",
  "sha256": "…",
  "size_bytes": 18422,
  "created_by": "windows-codex",
  "created_at": "…",
  "origin_replica": "…"
}
  • kind 白名单为 patchfilelogjsonnote
  • 每个工件 sha256 在写入时由内容即时算出;跨副本同步时 apply_remote_artifactlogstream.py)会重算并比对哈希,不一致即拒绝入库——损坏的传输永远不会进入存储。
  • 有趣的细节:对 kind=patch 的内容,put_artifact 会做建议性检查_patch_content_warnings)——缺少结尾换行或含 CRLF 的 diff 常会被 git apply 判为损坏,于是在存储时就提示生产者修正;但内容本身仍逐字存储,绝不擅自改写。

append_event 会校验 artifact_ids 必须指向已存在的工件,杜绝读者看到悬空引用。

底层实现:logstream.sqlite3 的表结构与约束

RFC 003 建议的存储模型直接落到 logstream.py_init_db

  • 三张表:events(事件本体)、artifacts(工件)、event_artifacts(事件-工件多对多关联)。
  • eventstopicorigin_replicaorigin_seqhlc 等扩展列;_migrate_schema 会把 RFC 003/004 之前创建的旧日志原地升级,并把存量行回填为本副本作者身份。
  • 索引针对结构化过滤建立:(stream, created_at)(topic, created_at)(correlation_id, created_at)(to_agent, created_at)(type, created_at),以及 (sha256)(origin_replica, origin_seq) 唯一索引。
  • 事件 id 由服务器生成:可读 UTC 时间戳 + secrets.token_hex(6) 随机后缀;唯一性靠随机后缀,顺序保证靠 rowid,所以写方之间即使有钟偏也不会导致重排或碰撞。
  • 每个本地操作都被盖上 RFC 004 的出处印章(origin_replica/origin_seq/hlc),跨副本显示/合并顺序由混合逻辑时钟(HLC)提供,version_vector() 描述本副本已掌握到每个来源的哪个序号,供 list_ops 做反熵拉取——这是 004-replicated-palace.md 多主同步的基础(CLI 暴露为 mempalace logstream sync)。

高层接口:mempalace task create 与经典双 Agent 委托循环

对常见场景,官方建议优先使用任务接口而非手搓原始请求信封(mempalace_task_create MCP 工具 / mempalace task create CLI),它会生成规范化请求并打印一条可直接粘贴的唤醒语,完整任务原样留在 logstream 中。CLI 形态:

mempalace task create --project myapp \
  --from-agent mac-claude --to-agent windows-codex \
  --goal "Fix the flaky test." --branch fix/flaky-test \
  --base-commit 2668053 --done "Focused tests pass and a patch is submitted."

底层 tasks.py 会强制校验:correlation_idto_agentbranchbase_commit 必须齐全,且 base_commit 必须是至少 7 位十六进制 Git 对象 id(禁止分支或标签名,防止委托基于会漂移的引用)。mempalace-task 技能(SKILL.md)指导预览、创建、认领、交付与闭环。远程共享大脑客户端通过 MCP 调 mempalace_task_create;CLI 形态操作本地 palace。下面的原生原语仍可用于自定义集成。

RFC 003 与官方文档给出的规范双 Agent 交换如下:

请求方(Agent A):

  1. mempalace_event_append —— type=task.requestto_agent=agent-bcorrelation_id=task_123、正文描述工作内容(含目标、分支、基线提交与“完成定义”)。
  2. mempalace_event_wait —— correlation_id=task_123type=patch.readytimeout_ms=300000
  3. mempalace_artifact_get —— 取回补丁,校验 sha256
  4. 本地应用补丁并跑测试。打补丁永远是显式的本地决策——logstream 从不会替你应用任何东西(这也是 RFC 003 的“无隐藏权威”原则)。
  5. mempalace_event_ack —— status=applied(或带证据的 failed)。

执行方(Agent B):

  1. mempalace_event_wait —— to_agent=agent-btype=task.request
  2. 干活。
  3. mempalace_patch_submit —— 一步完成“存工件 + 追加 patch.ready 事件”。源码中 submit_patchlogstream.py)正是依次走 put_artifact(kind="patch")append_event(type="patch.ready", status="ready", artifact_ids=[...]),事件插入时会复查工件已存在。

如果 Agent 交不出补丁,它仍要回复:task.reply,或带逐字说明的 blocked/failed 状态。沉默是 logstream 唯一帮不了的失败模式

关于认领与确认(ack)的正确姿势:ack_eventlogstream.py)永不修改目标事件,而是追加一条 type=event.ackmetadata={"ack_of": <目标事件 id>} 的新事件,自动继承目标的 stream/room/topic、沿用其 correlation_id(缺省回落到目标事件 id)并回路由给目标的 from_agent。协议要求 worker 认领时先回 status=claimed,避免他人重复劳动。CLI 的 mempalace logstream ack 会自动填好 event.ackack_of 链接——不要手搓 ack。

MCP 工具与 CLI 对照

八个 MCP 工具服务于 logstream(工具 schema 见 mcp-tools.md,实现位于 mcp_server.py):mempalace_task_createmempalace_event_appendmempalace_event_listmempalace_event_waitmempalace_event_ackmempalace_artifact_putmempalace_artifact_getmempalace_patch_submit。同一套操作在 shell 侧由 mempalace logstream 子命令提供(append/list/wait/watch/ack/sync,见 cli.py):

mempalace logstream append --type task.request --stream project/myapp \
  --room delegation --from-agent mac --to-agent windows \
  --correlation-id task_123 --body "Please fix the flaky test."

mempalace logstream wait --correlation-id task_123 --type patch.ready \
  --timeout-ms 300000 --json

mempalace artifact get art_... | git apply --3way

要点:

  • --json 让每条命令可被脚本化解析;wait 超时时以退出码 2 结束(而非报错),便于 shell 循环据此重试。
  • 事件正文也支持 --body-file 从文件读入,长正文不必挤进命令行;--metadata '{"k":"v"}' 传结构化元数据(上限 64 KiB,规范化 JSON 存储)。
  • event_list 单次上限 500 条、默认 50 条(MAX_LIST_LIMIT/DEFAULT_LIST_LIMIT),分页要用 before_event_id/since_event_id 而非盲目翻页。

推送式消费(仪表盘、实时查看器)则由 hub 在 GET /logstream/stream 提供 Server-Sent Events(SSE):与 event_list 同款过滤子集、同款 JSON 信封、since_event_id 续传,另以 Last-Event-ID 支持断线续拉,约每 15 秒发一条 ping 注释保持连接(实现见 mcp_server.py_http_serve_logstream_stream,bearer 鉴权)。SSE 的运营细节可参考 shared-brain.md。值得注意的架构取舍:logstream 工具在 hub 的全局 HTTP 请求锁之外分发(RFC 003 Phase 5),否则一个五分钟的 event_wait 长轮询会饿死其余请求。

高效读流:游标、监听模式与 logstream watch

“读流”本身是一项技能,搞错它通常是协调任务卡住的最常见原因:事件写对了,但没人听

游标:since_event_id 是唯一正确的续读点

事件按追加顺序ORDER BY rowid)返回,而非时间戳顺序。一旦出现第二个副本这一点就至关重要——某个对端创建于 09:10:48Z 的事件,可能等它同步到本机时已经晚于本机 09:13:21Z 的本地事件。因此两个参数截然不同,其中只有一个是游标:

  • since_event_id——在追加顺序上严格位于该事件之后,与时间戳无关。这是前向续读游标;一个 watcher 的全部状态就是它处理过的最后一条事件 id
  • before_event_id——在追加顺序上严格位于该事件之前(rowid < anchor),用于反向/历史翻页。
  • order——'asc'(旧在前,默认)或 'desc'(新在前,适合单次调用扫尾收件箱)。
  • since_created_at——时间窗口(>=,含端点;调用方需按 id 去重)。适合回答“今天发生了什么”,绝不能用于断点续传:迟到的对端事件已经比你记的高水位更旧,会被静默且永久地跳过。

五种监听模式按存活时长挑选

模式 适用场景 机制
Inbox sweep 会话开始、大任务前 mempalace_event_list + to_agent + since_event_idpreview=true
Background watcher 干活时想被叫醒 mempalace logstream watch,以后台进程运行
Long-poll 在轮次内等一个已知关联 mempalace_event_wait —— 默认 60s、上限 300s,超时返回 timed_out 而非报错
Server-Sent Events 守护进程、仪表盘、实时查看器 GET /logstream/stream,live-tail 过滤 + since_event_id 续传
Declared-idle 无后台循环的轮次制 Agent 发布你的游标并声明“需要 ping”

logstream watch 之所以存在,是因为 wait 只是原语而非 watcher:它最长五分钟后报告超时,于是每个调用方都要重写同一段“重新布防”循环,还得各自记得推进游标。watch 把两件事都包了下来,还带上 watcher 需要的过滤,命中即退出,让 harness 把“进程退出”当作“你有邮件”:

mempalace logstream watch \
  --agent mac-claude \
  --type task.request --type task.reply --type patch.ready \
  --state-file ~/.mempalace/watch/mac-claude.json --json
  • --agent <id>--to-agent <id>--exclude-from-agent <id> 的合写。这个“排除自己”比看起来重要得多:to_agent=<你> 会命中 * 广播,而你自己发的广播也是广播——没有它,watcher 会在自己每次发状态时把自己吵醒。
  • 重复某过滤参数 = “或”--type task.request --type task.reply --type patch.ready 只对这几种醒来,其余保持安静。若你偶尔委派,务必把 task.reply 留在集合里——blockedfailed 是以回复形式到达的,拒绝它们的 watcher 会默默把游标推过它们,委托就此无人应答。
  • 退出码即唤醒信号:命中打印一条时退出 0--idle-exit-ms 到期未见任何匹配退出 2(与 wait 超时约定一致);被中断为 130。只有 0 意味着“你有邮件”。
  • --state-file 持久化游标,重启精确续传。游标推进越过的是“所有被检查过的”事件(含被过滤拒掉的),而非仅匹配项。state 文件读取区分四种状态(logstream.pyread_watch_state):文件不存在 = 真首次运行(可从 tip 开始);可用游标 = okcursor: null = 曾在空日志上启动(不是首次运行,跳 tip 会永久漏掉停机期间到达的事件);文件损坏 = 宁可重放、绝不跳过。
  • 首次运行从 tip 开始(与 SSE live-tail 一致)并在 stderr 明说;重放一整条长日志会把一个拿着数周历史却分不清新旧的新 watcher 吵醒。--from-start 才主动选择重放。
  • --follow 在首次命中后继续存活(守护进程用),不带则退出;--follow --json 输出 NDJSON(每行一条),保证流式 JSON 可解析。

mempalace_event_wait 内部自带退避(0.25s → 1s,带抖动),所以对它包一层紧重试循环毫无收益。要服务端过滤——to_agenttypecorrelation_idtopic 等字段在 events 表都有索引,且 to_agent=<你> 无需第二次查询即可命中 * 广播。扫描繁忙流时用 preview=true:正文被截断返回并带 body_truncated/body_length 标记,你只为真正需要的那条事件花 token。

让监听“可见”:宣布 watch、声明 idle、不要假 watch

看不见的 watcher 与没有 watcher 几乎一样糟。惯例是在开始监听某个关联时向 to_agent=* 发一条 status 事件,点明四件事:你在看的过滤你已推进到的游标不可重复做的工作,以及隐含的——有人在家。决定是否委派的 Agent 于是可以“查”而不是“猜”。要在 status 类型里宣布——舰队 watcher 对 status 是睡着的——每会话一次、过滤变更时再宣布一次;若用 watcher 会醒的类型(如 task.reply)宣布,每次任何人重新布防都会吵醒每个窗口。

反面同样重要:如果你的 harness 是轮次制、提示之间不复存在,就声明而不是保持沉默——发布你最后看到的事件 id 并说明需要 ping。永远不要宣传一个你没有的 watch——相信你在监听的请求方会停止寻找人工提醒者。此外,若 harness 对 shell/MCP 写入做了人工审批门禁,务必请操作员把 mempalace 工具与 logstream watch 命令加白名单:一个无人盯屏的审批弹窗会让 ack/reply/patch 全部阻塞,另一端看到的是“认领后沉默”,与崩溃无异。

协调 vs 记忆:为什么二者互补而非替代

维度 Palace(抽屉) Logstream(事件)
目的 长期回忆 当下协调
访问方式 语义搜索 结构化过滤 + 长轮询
生命周期 永久 永久(追加式)
内容 任何值得记住的东西 工作包、回复、补丁

可持久化的成果仍属于 palace:委派任务结束时,把决策与结论作为抽屉归档,以便日后可搜索。事件轨迹记录工作是“怎么”在 Agent 之间流动的;抽屉记录“学到了什么”。

测试与延伸阅读

仓库为 Logstream 提供了较完整的测试佐证:tests/test_logstream.py(事件/工件读写、等待语义)、tests/test_cli_logstream.py(CLI 子命令与 watch 行为)、tests/test_logstream_sse.py(SSE 端点)与 tests/test_logstream_sync.py(副本同步)。若想继续深挖,可按仓库路径阅读:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388