深入解析 MemPalace Agent Logstream:本地优先、不可变追加的多 Agent 协作事件层(RFC 003)
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——子频道,如delegation、patches、reviews、status。topic(可选)——把围绕某条轨道(如auth-v2、ui-redesign)的相关任务或子团队分组,避免同一稳定的<machine>-<harness>身份内部互相串扰。acks 默认继承目标事件的topic,也允许显式覆盖。correlation_id——把一个请求与其回复、确认串起来。每个任务生成一个并贯穿整个交换过程。to_agent——定向发给某 Agent,或*广播;对to_agent的过滤会自动命中广播事件。status——取值固定为open、claimed、ready、applied、blocked、failed、superseded之一。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 KiB(DEFAULT_MAX_BODY_BYTES),超限直接报错、绝不静默截断。
事件正文超长怎么办?规则是“过大就存成工件引用它”。这引出工件模型。
工件模型:逐字节存储 + 哈希校验
工件(artifact)是附加到事件的精确内容:统一 diff、生成的文件、测试日志、JSON 报告等。工件以逐字原文存储,附 sha256 与 size_bytes,接收方在应用任何东西前可先校验完整性。v1 工件仅限 UTF-8 文本,上限 4 MiB(DEFAULT_MAX_ARTIFACT_BYTES,见 logstream.py 顶部的 MAX_METADATA_BYTES = 64 * 1024 与 DEFAULT_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白名单为patch、file、log、json、note。- 每个工件
sha256在写入时由内容即时算出;跨副本同步时apply_remote_artifact(logstream.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(事件-工件多对多关联)。 events含topic、origin_replica、origin_seq、hlc等扩展列;_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_id、to_agent、branch、base_commit 必须齐全,且 base_commit 必须是至少 7 位十六进制 Git 对象 id(禁止分支或标签名,防止委托基于会漂移的引用)。mempalace-task 技能(SKILL.md)指导预览、创建、认领、交付与闭环。远程共享大脑客户端通过 MCP 调 mempalace_task_create;CLI 形态操作本地 palace。下面的原生原语仍可用于自定义集成。
RFC 003 与官方文档给出的规范双 Agent 交换如下:
请求方(Agent A):
mempalace_event_append——type=task.request、to_agent=agent-b、correlation_id=task_123、正文描述工作内容(含目标、分支、基线提交与“完成定义”)。mempalace_event_wait——correlation_id=task_123、type=patch.ready、timeout_ms=300000。mempalace_artifact_get—— 取回补丁,校验sha256。- 本地应用补丁并跑测试。打补丁永远是显式的本地决策——logstream 从不会替你应用任何东西(这也是 RFC 003 的“无隐藏权威”原则)。
mempalace_event_ack——status=applied(或带证据的failed)。
执行方(Agent B):
mempalace_event_wait——to_agent=agent-b、type=task.request。- 干活。
mempalace_patch_submit—— 一步完成“存工件 + 追加patch.ready事件”。源码中submit_patch(logstream.py)正是依次走put_artifact(kind="patch")与append_event(type="patch.ready", status="ready", artifact_ids=[...]),事件插入时会复查工件已存在。
如果 Agent 交不出补丁,它仍要回复:task.reply,或带逐字说明的 blocked/failed 状态。沉默是 logstream 唯一帮不了的失败模式。
关于认领与确认(ack)的正确姿势:ack_event(logstream.py)永不修改目标事件,而是追加一条 type=event.ack、metadata={"ack_of": <目标事件 id>} 的新事件,自动继承目标的 stream/room/topic、沿用其 correlation_id(缺省回落到目标事件 id)并回路由给目标的 from_agent。协议要求 worker 认领时先回 status=claimed,避免他人重复劳动。CLI 的 mempalace logstream ack 会自动填好 event.ack 与 ack_of 链接——不要手搓 ack。
MCP 工具与 CLI 对照
八个 MCP 工具服务于 logstream(工具 schema 见 mcp-tools.md,实现位于 mcp_server.py):mempalace_task_create、mempalace_event_append、mempalace_event_list、mempalace_event_wait、mempalace_event_ack、mempalace_artifact_put、mempalace_artifact_get、mempalace_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_id,preview=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留在集合里——blocked和failed是以回复形式到达的,拒绝它们的 watcher 会默默把游标推过它们,委托就此无人应答。 - 退出码即唤醒信号:命中打印一条时退出
0;--idle-exit-ms到期未见任何匹配退出2(与wait超时约定一致);被中断为130。只有0意味着“你有邮件”。 --state-file持久化游标,重启精确续传。游标推进越过的是“所有被检查过的”事件(含被过滤拒掉的),而非仅匹配项。state 文件读取区分四种状态(logstream.py 的read_watch_state):文件不存在 = 真首次运行(可从 tip 开始);可用游标 =ok;cursor: null= 曾在空日志上启动(不是首次运行,跳 tip 会永久漏掉停机期间到达的事件);文件损坏 = 宁可重放、绝不跳过。- 首次运行从 tip 开始(与 SSE live-tail 一致)并在 stderr 明说;重放一整条长日志会把一个拿着数周历史却分不清新旧的新 watcher 吵醒。
--from-start才主动选择重放。 --follow在首次命中后继续存活(守护进程用),不带则退出;--follow --json输出 NDJSON(每行一条),保证流式 JSON 可解析。
mempalace_event_wait 内部自带退避(0.25s → 1s,带抖动),所以对它包一层紧重试循环毫无收益。要服务端过滤——to_agent、type、correlation_id、topic 等字段在 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(副本同步)。若想继续深挖,可按仓库路径阅读:
- logstream.py —— 事件/工件/游标/同步的完整实现与默认值;
- 003-agent-logstream-coordination.md —— 设计动机与存储模型全文;
- coordination-protocol.md —— 共享大脑协调协议、硬规则与可直接放入系统提示词的 System-Prompt Snippet;
- mcp-tools.md 与 cli.md —— 工具与命令的完整 schema/参数表;
- shared_brain_rules.md —— 测试钉扎在协议文档上的打包规则副本,二者不会漂移;
- SKILL.md —— 任务预览、创建、认领与交付的指导技能。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00