本文件基于 CodeGraph 对 session-reader、rpc-manager、
types、agent-mode-persistence 与客户端 hooks 的源码级分析,
刻画当前「会话即记忆」体系的完整数据流,为后续长期记忆改进提供基线。
当前系统没有独立的「Memory Service」。记忆 = 会话历史,以 append-only JSONL 为真理源, 进程内 AgentSession 为热缓存,浏览器 React state 为展示层。三者通过不同 API 路径同步。
startRpcSession 从 JSONL hydrate;
L3 由只读 /api/sessions/* 或 SSE 事件增量构建。热路径写 L2→L1,冷路径只读 L1。
每个会话一个文件。第一行是 header,之后每一行是带 parentId 的树节点。
类型定义镜像自 Pi mono 的 session-manager,本地见 lib/types.ts。
{
"type": "session",
"version": 3,
"id": "<uuid>",
"timestamp": "...",
"cwd": "/path/to/project",
"parentSession": "/abs/path/parent.jsonl", // optional — Fork 元数据
"name": "optional display name"
}
| 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 |
会话名称等 | 否 | 侧边栏展示 |
customType: "desktop_agent_mode" + data: { mode: "plan"|"ask"|"full" }。
切换模式时 sessionManager.appendCustomEntry(...) 追加;启动时
findLastAgentMode(entries) 自后向前扫描恢复。
见 lib/agent-mode-persistence.ts、
lib/rpc-manager.ts。
架构刻意分离「运行 Agent」与「浏览历史」。冷读永不创建 AgentSession,避免资源浪费与副作用。
handleSend → 乐观追加 user 消息 → POST /api/agent/[id] 或 /api/agent/new
startRpcSession 查 __piSessions;miss 则
SessionManager.open|create + createAgentSession
inner.prompt() 驱动模型与工具;SessionManager 向 JSONL append message / toolResult
GET /api/agent/[id]/events → applyAgentEvent 更新 L3
agent_end 触发 reloadSession,从磁盘重同步 tree + messages
SessionManager.listAll() → listAllSessions 填充路径缓存 + parent 映射
resolveSessionPath(id):正缓存 hit / 负缓存 30s TTL / miss 则全量扫描
读 JSONL → buildTree + buildSessionContext(leafId)
GET .../context?leafId= 仅重算 path→messages,不启动 Agent
session-export 流式/静态输出 HTML 或 Markdown
messages[] 一一对应,把 UI 消息映射回 JSONL entry id,
是 Fork 按钮与 navigate_tree 的桥梁。Compaction 后第一条 message 的 entryId 指向 compaction 节点本身。
这是记忆系统最容易混淆的部分:同文件树分叉 vs 跨文件 Fork。侧边栏树用 parentSession 展示后者。
.jsonl 内通过 parentId 形成树navigate_tree → inner.navigateTree(targetId)?leafId=loadContext 重绘 messagesparentSessionsend({type:"fork", entryId})POST .../branch、.../clonestartRpcSession 预注册 → destroy 旧 wrapperstartRpcSession(newId) 成功预注册,再 destroy 旧 wrapper。
若预注册失败:旧会话保持可用,删除孤儿 JSONL + invalidate 路径缓存。
详见 lib/rpc-manager.ts case "fork"。
没有外部向量库或事实抽取。当上下文逼近窗口时,Pi 用 LLM 把前段历史压成 summary entry, 这是系统目前唯一有损的记忆变换。
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> | 会话级信任标记 | 信任握手相关 |
rewriteChildHeader 在删父会话时重写子会话 header 的 parentSession,
防止侧边栏出现悬挂父链接。这是会话图完整性的一部分。
L3 不持久化。切换 session 时用 sessionScopedResetPatch() 清空,防止串台。
use-session-loader.ts
agent-event-apply.ts
stream-state.ts
// 简化时序 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
后续改记忆系统时,优先从这些入口切入。
| 文件 | 关键符号 | 记忆职责 |
|---|---|---|
| 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 之上新建索引层(见下一节)。
结论先行:今天的「记忆」= 单会话树状对话日志 + 可选有损压缩。 没有跨会话事实库、没有语义检索、没有用户偏好/项目知识的一等公民存储。
区分 Working(当前 context)、Episodic(会话 JSONL)、Semantic(可抽取事实)、 Procedural(skills/prefs)。先定义写入触发与读取注入点,再写代码。
仿 desktop_agent_mode 增加
desktop_memory_note(fact / preference / decision / architecture)。
写入:slash 命令或 agent 工具;读取:session 启动时注入 system 片段或独立 panel。
落点:rpc-manager append +
session-reader 扫描。
在 case "compact" 调 Pi compact 之前,对即将被摘要的路径跑抽取,
把结构化结果写入 custom entry 或外部 store,避免有损后不可恢复。
按 cwd 聚合跨会话 notes,支持 keyword → 可选 embedding。
启动 session 时 recall(cwd, query?) 注入。
注意:不要污染 Pi 的 session JSONL 契约;用 sidecar 或 agentDir 下独立库。
工具面:save / recall / smart_search。Desktop 提供开关与 project id 规范 (稳定 slug,禁止用易变路径)。与内置 sidecar 二选一或分层。
浏览 / 编辑 / 删除笔记;展示来源 session 与 entry 链接;审计删除原因。
在会话 JSONL(episodic)旁增加项目作用域的持久记忆层。 Phase 1 使用内置 SQLite;不修改 Pi 会话文件作为主存储。
/api/memory/*(health / recall / remember / forget / stats)memory_save · memory_recall · memory_forgetagent_end + pre-compact(best-effort,不阻塞对话)~/.pi/agent/memory/ltm.sqliteprojectId = hash(cwd) 作用域),
不经 SessionManager。会话树、冷读、Fork/compact 契约保持不变;
模型仅通过 memory_* 工具显式读写。