下游 pi-billion-memory 目前只能 glob ~/.pi/agent/sessions/**/*.jsonl.acp.json 直接解析 sidecar 来读取 ACP 压缩块。这不是 bug,但绑死私有文件格式的代价是静默失效 :字段改名或目录调整时下游会扫到 0 个文件而不报错,最后以「bcp 是不是改了格式」的 issue 形式回到你们这里。
请求很小,按成本从低到高,做到前三条就够;不要求任何行为变更 ,全部是加法:
R1(几分钟) :sidecar 顶层加 schemaVersion(+ producer),并约定「字段缺失 = v1」「未知版本 = 跳过 + 只记一次日志、不得改写」。
R2(成本≈0) :package.json 增加 "./contract" subpath 导出:BcpBlockV1 类型 + schema/bcp-block-v1.json(JSON Schema,可选 reader)。跨进程、跨重启都有效,且不需要事件通道。
R3(一句文档) :把「sidecar 用 tmp+rename 原子替换」写成承诺(代码已经是这个模式)。
R4(可选):压缩成功落盘后 pi.events.emit("bcp:blocks", …);明说它只覆盖同进程/同会话,文件仍是 source of truth 。
R5(可选):compress 的 details 若要做,请只放有界 信息(如 {schemaVersion, acceptedIds, revision})——details 会写进会话 .jsonl,整份 blocks 会在 sidecar 之外再抄一份。
P3(pi.appendEntry 写 custom entry)不是必须,可以以后再说。
背景
bcp 是 pi 侧压缩的 owner:session_before_compact 返回 cancel: true,压缩由 ACP 完全接管,结果只写 <sessionFile>.acp.json。
下游需要这些块做跨会话检索(ACP 块 → SQLite FTS5 → memory_search / memory_expand)。块本身很有价值,但目前没有任何公开接口能拿到。
下游坚持零运行时依赖(dependencies: {}),并已把 bcp 声明为可选 peer("billion-context-pi": ">=0.1.65")——所以「类型 + schema」这种契约对双方都是最低成本。
现状(0.1.65 dist,可直接核验)
事实
核验方式
压缩由 bcp 接管
grep -o 'session_before_compact", () =>' dist/index.js、grep -o 'cancel: true' dist/index.js
只写 sidecar
grep -o 'STATE_SUFFIX = "[^"]*"' dist/index.js → .acp.json
canonical blocks 不对外
compress 工具结果全部是 details: void 0(dist/compress-tool.d.ts)
入口没有块类型
dist/index.d.ts 只导出 createAcpExtension;exports 只有 "."
没有事件、没有 custom entry
grep -c appendEntry dist/index.js → 0;.emit( → 0
读写无版本校验
dist/state.d.ts 只有 SessionStateStore / LiveRefOrigin 等内部类型
侧挂文件已原子写
writeFile(tmp, JSON.stringify({ ...state, liveRefOrigins }… + rename 模式,但没有对外承诺
旁证:isCompressSuccessText / isCompressNoopText(dist/compress-tool.d.ts)说明连 bcp 自己 也要从工具结果的文本 里判断成败——文本通道有损,而结构化通道目前不存在。README:216-236 已经把 sidecar 描述为用户可见产物并说明了 #299 的 replay 语义,但没有任何机器可读的版本/契约。
问题
格式耦合 :字段改名、路径调整、null 化 → 下游静默失效(扫到 0 文件或解析成 null),没有任何报错。
没有版本标识 :无法区分格式代际,只能「解析失败、下轮重试」,也就无法提示用户「升级下游」还是「升级 bcp」。
没有权威数据 :compress 的入参是模型产出的草案,最终落盘值可能被你们修正/拒绝;下游拿不到「最终被接受的那份」。
变更没有通知渠道 :上游发版后,下游只能靠用户反馈发现问题——而这批反馈往往会打回你们(对我们来说是多一个误报来源,对你们来说是噪音)。
请求(任选,不必全做)
R1 sidecar 顶层加版本标识(几分钟)
{
"schemaVersion" : 1 ,
"producer" : { "name" : " billion-context-pi" , "version" : " <version>" },
"blocks" : [ /* ... */ ]
}
语义建议一并写进 README / CHANGELOG:
字段缺失 = schemaVersion: 1(否则现存 sidecar 全部变成「未知版本」);
未知版本:下游应跳过并只记一次日志,不得改写 文件;
只在破坏性变更时升版本,其余字段只增不改。
R2 ./contract subpath 导出(成本≈0,最耐久)
contract 里只放三样东西:BcpBlockV1 类型、SCHEMA_VERSION 常量、schema/bcp-block-v1.json(JSON Schema)。零运行时依赖、跨进程、跨重启有效,比「承诺事件 payload 只增不改」维护成本低得多;下游可以直接对着 schema 跑校验和集成测试。
R3 把原子写写成承诺
现在的写入模式已经是 tmp+rename;请把它写进 README(或加一条测试)。下游的水位线(mtime/size)+ 整文件替换是绑定的,非原子写会让下游读到撕裂的 JSON。
R4 落盘成功后 emit 事件(可选)
pi . events . emit ( "bcp:blocks" , {
schemaVersion : 1 ,
sessionId,
sessionFile,
cwd,
revision, // 单调递增(或落盘时间戳),供下游幂等/排序
reason : "compress" , // compress | prune | ...
added : BcpBlock [ ] , // 本次新增
updated : BcpBlock [ ] , // 本次变更(若有)
removed : string [ ] , // 被替换/吞并的 blockId(若有)
} ) ;
建议在「保存成功之后」发出;增量语义维护成本高,就直接发全量 blocks 快照——下游只需要一个明确的「当前真相」;
但请写明边界:事件只能覆盖同进程、同会话 ,而 pbm 的价值在跨会话、可指向任意 sources root 的扫描。事件只是低延迟加成,文件仍是 source of truth 。
R5 compress 的 details 若要做,请有界
pi 的 session format 里 toolResult message 带 details?(docs/session-format.md:94-98,:246 / :260 说明语义),也就是说 details 会落盘进会话文件 。整份 blocks 放进去 = 在 sidecar 之外再抄一份大内容。建议只放 { schemaVersion, acceptedIds, revision } 这类有界引用,或干脆不做(R1/R2 已覆盖主要收益)。
P3(可选,以后再说)写 custom entry
pi.appendEntry("bcp.blocks", payload) + 下游 ctx.sessionManager.getEntries().filter(e => e.type === "custom" && e.customType === "bcp.blocks")。好处:由 pi 统一持久化、版本迁移、树管理,custom entry 不进 LLM 上下文(pi 文档 docs/extensions.md:1620)。代价:会话文件变大(每次压缩写一份,写放大),且跨会话回填仍要开别人的文件。
块的最小稳定字段(字段名可按你们习惯调整)
interface BcpBlockV1 {
// --- stable core(建议只增不改)---
blockId : string ; // 幂等键
startId : string ;
endId : string ;
summary : string ;
topic ?: string ;
// --- optional bag(允许缺失或改语义)---
compressedTokens ?: number ;
msgIds ?: string [ ] ;
refs ?: string [ ] ;
sourceSessionFile ?: string ;
cwd ?: string ;
createdAt ?: string ; // ISO 8601
}
不需要暴露内部修剪/索引细节,只要「最终被接受的块」。
兼容性
以上全部是加法:不改 sidecar 位置、现有读写路径和 pi 集成行为;
下游按 blockId 幂等 upsert,文件扫描保留为历史回填;
若采纳 R2,建议在 CHANGELOG 承诺:BcpBlockV1 字段只增不改不删,破坏性变更升 schemaVersion。
相关 issue
它们和本 issue 指向同一个缺口:ACP 状态目前只有「私有文件格式」这一个出口。本 issue 只请求机器可读的版本/类型边界 ,不请求行为变更。
参考
pi 文档:docs/extensions.md:1471(pi.appendEntry)、:1620(custom entry 不进 LLM 上下文)、:1729(pi.events);docs/session-format.md:94-98(toolResult details)、:268(custom entry)、:410-413(SessionManager.appendCustomEntry 等)
bcp README:216-236(sidecar 用途与 会话迁移(export/import)丢失 .acp.json 状态与 parentSession,导入后会话失去全部压缩块 #299 replay 语义)
下游现状:tjp72/pi-billion-memory 的 README 兼容性说明
下游 pi-billion-memory 目前只能 glob
~/.pi/agent/sessions/**/*.jsonl.acp.json直接解析 sidecar 来读取 ACP 压缩块。这不是 bug,但绑死私有文件格式的代价是静默失效:字段改名或目录调整时下游会扫到 0 个文件而不报错,最后以「bcp 是不是改了格式」的 issue 形式回到你们这里。请求很小,按成本从低到高,做到前三条就够;不要求任何行为变更,全部是加法:
schemaVersion(+producer),并约定「字段缺失 = v1」「未知版本 = 跳过 + 只记一次日志、不得改写」。package.json增加"./contract"subpath 导出:BcpBlockV1类型 +schema/bcp-block-v1.json(JSON Schema,可选 reader)。跨进程、跨重启都有效,且不需要事件通道。pi.events.emit("bcp:blocks", …);明说它只覆盖同进程/同会话,文件仍是 source of truth。compress的details若要做,请只放有界信息(如{schemaVersion, acceptedIds, revision})——details会写进会话.jsonl,整份 blocks 会在 sidecar 之外再抄一份。pi.appendEntry写 custom entry)不是必须,可以以后再说。背景
session_before_compact返回cancel: true,压缩由 ACP 完全接管,结果只写<sessionFile>.acp.json。memory_search/memory_expand)。块本身很有价值,但目前没有任何公开接口能拿到。dependencies: {}),并已把 bcp 声明为可选 peer("billion-context-pi": ">=0.1.65")——所以「类型 + schema」这种契约对双方都是最低成本。现状(0.1.65 dist,可直接核验)
grep -o 'session_before_compact", () =>' dist/index.js、grep -o 'cancel: true' dist/index.jsgrep -o 'STATE_SUFFIX = "[^"]*"' dist/index.js→.acp.jsoncompress工具结果全部是details: void 0(dist/compress-tool.d.ts)dist/index.d.ts只导出createAcpExtension;exports只有"."grep -c appendEntry dist/index.js→ 0;.emit(→ 0dist/state.d.ts只有SessionStateStore/LiveRefOrigin等内部类型writeFile(tmp, JSON.stringify({ ...state, liveRefOrigins }…+ rename 模式,但没有对外承诺旁证:
isCompressSuccessText/isCompressNoopText(dist/compress-tool.d.ts)说明连 bcp 自己也要从工具结果的文本里判断成败——文本通道有损,而结构化通道目前不存在。README:216-236 已经把 sidecar 描述为用户可见产物并说明了 #299 的 replay 语义,但没有任何机器可读的版本/契约。问题
compress的入参是模型产出的草案,最终落盘值可能被你们修正/拒绝;下游拿不到「最终被接受的那份」。请求(任选,不必全做)
R1 sidecar 顶层加版本标识(几分钟)
{ "schemaVersion": 1, "producer": { "name": "billion-context-pi", "version": "<version>" }, "blocks": [ /* ... */ ] }语义建议一并写进 README / CHANGELOG:
schemaVersion: 1(否则现存 sidecar 全部变成「未知版本」);R2
./contractsubpath 导出(成本≈0,最耐久)contract里只放三样东西:BcpBlockV1类型、SCHEMA_VERSION常量、schema/bcp-block-v1.json(JSON Schema)。零运行时依赖、跨进程、跨重启有效,比「承诺事件 payload 只增不改」维护成本低得多;下游可以直接对着 schema 跑校验和集成测试。R3 把原子写写成承诺
现在的写入模式已经是 tmp+rename;请把它写进 README(或加一条测试)。下游的水位线(mtime/size)+ 整文件替换是绑定的,非原子写会让下游读到撕裂的 JSON。
R4 落盘成功后 emit 事件(可选)
blocks快照——下游只需要一个明确的「当前真相」;sourcesroot 的扫描。事件只是低延迟加成,文件仍是 source of truth。R5
compress的details若要做,请有界pi 的 session format 里 toolResult message 带
details?(docs/session-format.md:94-98,:246/:260说明语义),也就是说details会落盘进会话文件。整份 blocks 放进去 = 在 sidecar 之外再抄一份大内容。建议只放{ schemaVersion, acceptedIds, revision }这类有界引用,或干脆不做(R1/R2 已覆盖主要收益)。P3(可选,以后再说)写 custom entry
pi.appendEntry("bcp.blocks", payload)+ 下游ctx.sessionManager.getEntries().filter(e => e.type === "custom" && e.customType === "bcp.blocks")。好处:由 pi 统一持久化、版本迁移、树管理,custom entry 不进 LLM 上下文(pi 文档docs/extensions.md:1620)。代价:会话文件变大(每次压缩写一份,写放大),且跨会话回填仍要开别人的文件。块的最小稳定字段(字段名可按你们习惯调整)
不需要暴露内部修剪/索引细节,只要「最终被接受的块」。
兼容性
blockId幂等 upsert,文件扫描保留为历史回填;BcpBlockV1字段只增不改不删,破坏性变更升schemaVersion。相关 issue
.acp.json、replay 语义)它们和本 issue 指向同一个缺口:ACP 状态目前只有「私有文件格式」这一个出口。本 issue 只请求机器可读的版本/类型边界,不请求行为变更。
参考
docs/extensions.md:1471(pi.appendEntry)、:1620(custom entry 不进 LLM 上下文)、:1729(pi.events);docs/session-format.md:94-98(toolResultdetails)、:268(custom entry)、:410-413(SessionManager.appendCustomEntry等)