CodeGraph 深度探索 · 2026-08-03

Pi Agent
记忆架构深度解析

本文件基于 CodeGraph 对 session-readerrpc-managertypesagent-mode-persistence 与客户端 hooks 的源码级分析, 刻画当前「会话即记忆」体系的完整数据流,为后续长期记忆改进提供基线。

3
记忆层级
9
Entry 类型
2
分支机制
5
globalThis 状态
0
跨会话语义记忆

三层记忆模型

当前系统没有独立的「Memory Service」。记忆 = 会话历史,以 append-only JSONL 为真理源, 进程内 AgentSession 为热缓存,浏览器 React state 为展示层。三者通过不同 API 路径同步。

L3 展示层
Ephemeral UI
messages[] entryIds[] streamingMessage activeLeafId tree useAgentSession agent-event-apply
L2 运行时
Process Memory
__piSessions AgentSessionWrapper SessionManager (Pi) __piSessionPathCache __piStartLocks __piWriteLocks 10min idle destroy
L1 持久层
Disk Truth
~/.pi/agent/sessions/ <encoded-cwd>/ <ts>_<uuid>.jsonl SessionHeader SessionEntry tree parentSession 链接
核心洞察:L1 是唯一权威源。L2 由 startRpcSession 从 JSONL hydrate; L3 由只读 /api/sessions/* 或 SSE 事件增量构建。热路径写 L2→L1,冷路径只读 L1。
Browser UI messages / entryIds SSE · fetch context Next.js Server rpc-manager · session-reader globalThis 注册表 / 路径缓存 AgentSessionWrapper Pi Coding Agent SessionManager createAgentSession Disk · JSONL Sessions ~/.pi/agent/sessions/<encoded-cwd>/<ts>_<uuid>.jsonl append-only · parentId tree · parentSession graph HTTP / SSE in-process read / append cold read (no session)

JSONL 会话格式 = 记忆 schema

每个会话一个文件。第一行是 header,之后每一行是带 parentId 的树节点。 类型定义镜像自 Pi mono 的 session-manager,本地见 lib/types.ts

Header(第一行)

{
  "type": "session",
  "version": 3,
  "id": "<uuid>",
  "timestamp": "...",
  "cwd": "/path/to/project",
  "parentSession": "/abs/path/parent.jsonl",  // optional — Fork 元数据
  "name": "optional display name"
}

Entry 类型一览

type作用是否进入 LLM 上下文备注
message user / assistant / toolResult 是(沿 leaf 路径) 记忆主体
compaction 历史摘要节点 是(summary 替代前段) firstKeptEntryId 划界
model_change 切换模型 否(元数据) 影响后续请求
thinking_level_change 思考等级 持久化到路径状态
custom 扩展数据 否(默认) Desktop 用 desktop_agent_mode
custom_message 可显示自定义消息 视 display UI 提示类
branch_summary 分支切换摘要 可能注入 Pi navigateTree 相关
label 节点标签 UI 标注
session_info 会话名称等 侧边栏展示
唯一的 Desktop 自定义记忆字段: customType: "desktop_agent_mode" + data: { mode: "plan"|"ask"|"full" }。 切换模式时 sessionManager.appendCustomEntry(...) 追加;启动时 findLastAgentMode(entries) 自后向前扫描恢复。 见 lib/agent-mode-persistence.tslib/rpc-manager.ts

双路径:热写 vs 冷读

架构刻意分离「运行 Agent」与「浏览历史」。冷读永不创建 AgentSession,避免资源浪费与副作用。

HOT 写入 / 执行路径

UI 发送

handleSend → 乐观追加 user 消息 → POST /api/agent/[id]/api/agent/new

启动 / 复用 Session

startRpcSession__piSessions;miss 则 SessionManager.open|create + createAgentSession

Pi 执行 + 落盘

inner.prompt() 驱动模型与工具;SessionManager 向 JSONL append message / toolResult

SSE 推送

GET /api/agent/[id]/eventsapplyAgentEvent 更新 L3

回合结束

agent_end 触发 reloadSession,从磁盘重同步 tree + messages

COLD 只读路径

列表

SessionManager.listAll()listAllSessions 填充路径缓存 + parent 映射

解析路径

resolveSessionPath(id):正缓存 hit / 负缓存 30s TTL / miss 则全量扫描

加载会话

读 JSONL → buildTree + buildSessionContext(leafId)

分支上下文

GET .../context?leafId= 仅重算 path→messages,不启动 Agent

导出

session-export 流式/静态输出 HTML 或 Markdown

上下文重建算法(buildSessionContext)

定位 leaf
leafId | last entry
向上 walk
parentId → root
找 compaction
路径上最后一次
裁切 + 拼装
summary + kept + after
归一化
toolCall 字段
entryIds[] 平行数组:messages[] 一一对应,把 UI 消息映射回 JSONL entry id, 是 Fork 按钮与 navigate_tree 的桥梁。Compaction 后第一条 message 的 entryId 指向 compaction 节点本身。

两种分支 = 两种记忆分裂策略

这是记忆系统最容易混淆的部分:同文件树分叉 vs 跨文件 Fork。侧边栏树用 parentSession 展示后者。

会话内分支(In-file)

  • 同一 .jsonl 内通过 parentId 形成树
  • 命令:navigate_treeinner.navigateTree(targetId)
  • UI:BranchNavigator;上下文:?leafId=
  • 记忆共享同一文件,仅「当前活跃路径」不同
  • 切换后 loadContext 重绘 messages

Fork / Branch(Cross-file)

  • 新 JSONL;header 写 parentSession
  • Agent 路径:send({type:"fork", entryId})
  • API 路径:POST .../branch.../clone
  • Fork 点前历史拷贝;之后独立演化
  • 顺序:建文件 → startRpcSession 预注册 → destroy 旧 wrapper

同文件树示意(活跃路径高亮)

session header id=S1 cwd=./proj │ ├─ [m1] message user: "修登录 bug" │ └─ [m2] message assistant + toolCall │ └─ [m3] toolResult │ └─ [m4] message assistant │ ├─ [m5] message user: "换种方案" ← leaf A │ │ └─ [m6] assistant ... │ └─ [m7] message user: "继续原方案" ← leaf B (inactive) │ └─ [m8] assistant ... │ └─ [c1] custom desktop_agent_mode { mode: "ask" }

跨文件 Fork 后的会话图

S1.jsonl ──parentSession──▶ S2.jsonl (forked at m4) full tree m1…m8 copy of path → m4, then new growth leaf starts after fork point // 删除 S1 时 session-cascade 把 S2.parentSession 重写到 S1 的祖父(或移除)
Fork 契约陷阱:必须先 startRpcSession(newId) 成功预注册,再 destroy 旧 wrapper。 若预注册失败:旧会话保持可用,删除孤儿 JSONL + invalidate 路径缓存。 详见 lib/rpc-manager.ts case "fork"

压缩:唯一的「遗忘」机制

没有外部向量库或事实抽取。当上下文逼近窗口时,Pi 用 LLM 把前段历史压成 summary entry, 这是系统目前唯一有损的记忆变换

压缩前(路径上的 entries) m1..mN to summarize keep recent leaf 压缩后(上下文视图) compaction summary + firstKeptEntryId kept messages new turns Desktop 防护 findCutPoint 预检:history 太短则 抛 "Conversation too short to compact" UI:/compact 或 handleCompact 事件:compaction_start/end(兼容 auto_*) 成功后 reloadSession 从磁盘重载
改进记忆时必须注意:压缩是路径局部、有损的。原始 message 仍在 JSONL 文件中(append-only), 但 buildSessionContext 对 LLM/UI 可见的上下文会用 summary 替换前段。 任何「长期记忆抽取」若只看 context 视图会丢失细节——应读完整 entries 或在 compact 前抽取。

进程内记忆与并发控制

Next.js HMR 会丢掉模块级变量,因此会话相关状态必须挂在 globalThis。 这些不是「业务记忆」,而是记忆系统的运行时索引与锁。

globalThis 键结构用途生命周期
__piSessions Map<id, Wrapper> 活跃 Agent 热缓存 destroy / 空闲 10min / process exit
__piSessionPathCacheState paths + misses id → 文件路径;负缓存 30s list 填充;DELETE/fork 失败 invalidate
__piStartLocks Map<id, Promise> 并发 start 去重 start finally 删除
__piWriteLocks Map<path, Promise> per-file 写入串行化 withFileLock finally
__piAllowedRootsCache {roots, expiresAt} 文件白名单(安全边界) 5s TTL
__piSessionOnlyTrust Map<id, bool> 会话级信任标记 信任握手相关

Session 生命周期(记忆热度)

Cold
仅磁盘 JSONL
Hydrate
startRpcSession
Active
prompt / events
Idle
计时重置
Destroyed
10min / fork
Cold
磁盘仍在
cascade 删除:lib/session-cascade.tsrewriteChildHeader 在删父会话时重写子会话 header 的 parentSession, 防止侧边栏出现悬挂父链接。这是会话图完整性的一部分。

浏览器侧的瞬时记忆

L3 不持久化。切换 session 时用 sessionScopedResetPatch() 清空,防止串台。

会话加载

use-session-loader.ts

  • data / tree / leafId
  • messages + entryIds
  • loadSession / loadContext

事件应用

agent-event-apply.ts

  • 纯函数:event → patches
  • appendMessages / streamAction
  • reloadSession side effect

流式状态

stream-state.ts

  • isStreaming
  • streamingMessage
  • start / update / end

一轮对话中 L3 如何更新

// 简化时序
handleSend:
  setMessages(prev => [...prev, userMsg])     // 乐观更新
  dispatch({ type: "start" })
  sendAgentCommand(prompt)                     // 或 /api/agent/new
  connectEvents(sid)

SSE message_update:
  streamAction update → streamingMessage        // 打字机

SSE message_end:
  appendMessages([completed])                   // 固化到 messages[]
  stream reset

SSE agent_end:
  reloadSession(sid)                            // 权威同步 tree/entryIds
  fetchAgentState

CodeGraph 符号与文件地图

后续改记忆系统时,优先从这些入口切入。

文件关键符号记忆职责
lib/types.ts SessionHeader, SessionEntry*, SessionContext 记忆 schema 契约
lib/session-reader.ts listAllSessions, buildTree, buildSessionContext, resolveSessionPath 冷读与上下文投影
lib/session-path-cache.ts getCachedSessionPath, SESSION_MISS_TTL_MS 路径索引
lib/rpc-manager.ts AgentSessionWrapper, startRpcSession, send(fork|compact|…) 热写、执行、Fork、压缩
lib/agent-mode-persistence.ts findLastAgentMode, createAgentModeCustomEntry 自定义 entry 读写范例
lib/session-branch-clone.ts extractAncestryPath, createBranchedHeader 跨文件分支/克隆纯函数
lib/session-cascade.ts rewriteChildHeader 会话图维护
lib/session-lock.ts withFileLock 并发写保护
lib/normalize.ts normalizeToolCalls SDK/UI 字段对齐
hooks/agent-session/* useSessionLoader, applyAgentEvent, sessionScopedResetPatch L3 状态机
@earendil-works/pi-coding-agent SessionManager, createAgentSession, findCutPoint 真正的 append / compact / branch 实现
扩展记忆的推荐挂载点:desktop_agent_mode 一样使用 appendCustomEntry(customType, data) 写入 JSONL,在 cold path 用 entry 扫描恢复——无需改 Pi 核心。 若要跨会话检索,需在 L1 之上新建索引层(见下一节)。

现状边界与改进铺垫

结论先行:今天的「记忆」= 单会话树状对话日志 + 可选有损压缩。 没有跨会话事实库、没有语义检索、没有用户偏好/项目知识的一等公民存储。

已有能力(可复用)

✅ 强项

  • Append-only 审计友好,易 fork/export
  • 树 + compaction 兼顾探索与上下文预算
  • 冷热分离,列表/历史零 Agent 开销
  • custom entry 扩展通道已验证
  • 路径缓存 + 写锁 + 启动锁齐备

❌ 缺口

  • 无跨会话 recall
  • 无 embedding / hybrid search
  • compaction 细节不可自动「复活」到 prompt
  • 无显式 fact / preference / decision 类型
  • 无记忆治理(过期、删除审计、project scope)
  • 无与 MCP 外部记忆服务的内置桥

建议演进方向(按优先级)

P0
记忆类型分层设计文档

区分 Working(当前 context)、Episodic(会话 JSONL)、Semantic(可抽取事实)、 Procedural(skills/prefs)。先定义写入触发与读取注入点,再写代码。

P1
Custom Entry 扩展:显式记忆节点

仿 desktop_agent_mode 增加 desktop_memory_note(fact / preference / decision / architecture)。 写入:slash 命令或 agent 工具;读取:session 启动时注入 system 片段或独立 panel。 落点:rpc-manager append + session-reader 扫描。

P1
Compact 前记忆抽取钩子

case "compact" 调 Pi compact 之前,对即将被摘要的路径跑抽取, 把结构化结果写入 custom entry 或外部 store,避免有损后不可恢复。

P2
项目级记忆索引(SQLite / JSONL sidecar)

按 cwd 聚合跨会话 notes,支持 keyword → 可选 embedding。 启动 session 时 recall(cwd, query?) 注入。 注意:不要污染 Pi 的 session JSONL 契约;用 sidecar 或 agentDir 下独立库。

P2
可选对接外部 Memory MCP

工具面:save / recall / smart_search。Desktop 提供开关与 project id 规范 (稳定 slug,禁止用易变路径)。与内置 sidecar 二选一或分层。

P3
记忆 UI:侧边栏 Memory 面板

浏览 / 编辑 / 删除笔记;展示来源 session 与 entry 链接;审计删除原因。

改进时的硬约束(勿破坏)

契约

  • JSONL 第一行必须是 session header
  • parentId 树不可出现环(branch 已有 visited 防护)
  • entryIds 与 messages 平行
  • Fork 预注册再 destroy
  • 冷路径禁止创建 AgentSession

工程

  • globalThis 抗 HMR
  • 写文件走 withFileLock
  • ToolCall 字段归一化两处(load + SSE)
  • compaction 事件新旧双名
  • 测试优先覆盖纯函数(reader / cascade / branch)
设计已落地:跨会话长期记忆见下方 § LTM2026-08-03-long-term-memory-design.md。 本 HTML 仍是会话 JSONL 记忆基线;LTM 为旁路层,不改写 JSONL 契约。

长期记忆层(LTM)

在会话 JSONL(episodic)旁增加项目作用域的持久记忆层。 Phase 1 使用内置 SQLite;不修改 Pi 会话文件作为主存储。

代码与 API

  • 实现:lib/ltm/(Service + MemoryBackend + SQLite)
  • HTTP:/api/memory/*(health / recall / remember / forget / stats)
  • Agent 工具:memory_save · memory_recall · memory_forget
  • 自动观察:agent_end + pre-compact(best-effort,不阻塞对话)
  • DB:~/.pi/agent/memory/ltm.sqlite

设计与计划

与三层模型的关系:LTM 是 L1 旁的独立持久层(按 projectId = hash(cwd) 作用域), 不经 SessionManager。会话树、冷读、Fork/compact 契约保持不变; 模型仅通过 memory_* 工具显式读写。