Skip to content

pi native: after a pi restart or an edit, the resent history gets other <acp> ids and another fold rendering, so the prefix cache is lost from about a third of the context on #5499

Open

Description

@Piggidragon

Setup

  • billion-context 0.1.211 as a pi package (npm:billion-context@0.1.211), pi-native extension
  • pi 1.1.0 (@earendil-works/pi-coding-agent), embedded in a web portal that starts and stops one pi per chat (SDK, not the CLI)
  • a llama.cpp server (OpenAI chat completions, reasoning_content sent back), context window 32768 in pi's models.json, so compress.maxContextLimit (75 %) is reached after a few file reads
  • request bodies recorded by a forwarder between the bili proxy and the model server; the server's timings (prompt_n, cache_n) measure what the prefix cache gave

Without billion-context, the same chats, restarts and edits keep the prompt byte-identical up to the changed message (cache_n 99 %). So the changes below come from the bili layer. Two users see this with different models and model servers.

What happens

pi is stopped and started again on the same session file: when a message is edited (the host cuts the edited message and everything after it out of the session file and starts a new pi on it), after an idle stop, and when the host restarts. The new pi sends the whole history again. bili finds its state for the session again (same sess=), but the history it sends upstream is not the one it sent before.

1. A resumed session gets other message ids

Once a compress block exists, the first request of the resumed process carries other ids in the <acp ...>mNNNNN</acp> tags for the same messages.

  • In the running process, each request keeps every earlier message byte for byte.
  • After a restart (stop pi, start pi, same session file), in a short chat the first different message was fix: cache health — remove condense, fix isTitleGen, preserve cache_control #13: ...>m00030</acp>\nNow read mod11.py... became ...>m00032</acp>\nNow read mod11.py.... Same text, the id moved by two, and everything after it differs.
  • In a longer chat (56 messages, a block m00001-m00017), the first different message after a restart was release v0.1.3 #10, at 41 % of the characters: m00026 -> m00140, then m00027 -> m00029, and so on to the end.

reconcileLiveIdsNode / remintCoveredLiveIds look responsible (they re-mint ids of live messages a block covers when state.lastPassIds does not contain them, and lastPassIds is empty in a new process); not verified.

2. An edit changes how the fold is drawn

When the edit removes the turn that held the model's compress call, bili keeps the blocks, but the resent history no longer has that call and its result. The blocks then come back as separate [Compressed conversation section] user messages after each user message, so the prefix changes from message #2 on (33-57 % of the characters).

Example: 14 messages s u a u a u a u a u a(tool_call) t a(tool_call) t become 14 messages s u u(summary) a u u(summary) a u u(summary) a u u(summary) a u; server prompt_n 1781, cache_n 9905 (system prompt and tools only). The unfolded history before the compress (cache_n 22803) was still in the server's cache and would have been reusable.

Expected for an edit or fork (see #1921): blocks made by a compress call that is no longer in the resent history are dropped again, so the history goes back to the raw prefix the upstream cache holds, or the fold is drawn the way it was before the edit.

Related, but other cases: #2695 (host-side history deletion leaving fold blocks), #5140 (cold restore).

The tools part (#5459, #4134)

The same restarts also hit #5459: bili's six tools join only from the second request of a process. Measured on a chat at about 80 % of a 32k window:

request tools prompt_n / cache_n
last before the restart 35 374 / 26332
first after the restart 29 17736 / 9905
second after the restart 35 20631 / 11152

Why it matters

Long-lived hosts stop and start pi often (edit, resume, idle stop, update). A billion-context chat is long and cached, so each lost prefix means reading 15-20k tokens again on a local model, and with #5459 twice in a row.

Reproduce without a host (part 1)

  1. PI_CODING_AGENT_DIR=<empty dir> with a models.json for an OpenAI-compatible server with a 16-32k window; load the extension with -e <billion-context>/dist/agent/pi-native.js.
  2. Run pi -p --session-id demo "Read file N and say what it does" several times (one process per message) until usage passes 75 % and the model calls compress.
  3. Record the request bodies behind the bili proxy and compare consecutive requests: after the fold, the ids differ between processes.

Activity

  1. ranxianglei commented on Nov 1, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重:不重复。 已检索 open+closed,没有同失败签名(pi-native 重启/编辑后 prefix cache 从约 1/3 处丢失、id 重铸 + fold 重渲染)的既有 issue。你提到的 #2695 / #5140 / #1921 是同家族但机制不同(那几条是 host 侧删历史留孤儿块 / 冷恢复);本条是「重启后 id 失配」+「编辑掉 compress 调用轮次后块被拆开重绘」两个新机制,按「同症状同 issue」并入本条管理,不另拆。

    标签:bug · pi · scope/kernel · p2 · needs-decision(缺陷类;pi-native 车道;两处机制都落在 kernel/,故 scope/kernel;成本回归非数据丢失,判 p2;修复需你对 kernel 变更拍板)。

    先给结论:这是真问题,你定位的方向基本对,但精确触发点跟你猜的不完全一样。 我对代码核了机制(注意:我本地 master 落后 v0.1.211 共 585 个提交,v0.1.211 才是你的版本——下面行号以我核对的源码为准,落 PR 前我会 rebase 到当前 master≈v0.1.211 再动)。

    Triage 三步(审核先于实现)

    1. 复现/查证:还没在你环境跑通(需要 pi 1.1.0 + OpenAI-compat 上游 + 多次单进程跑到 >75% 触发 compress,较重);但代码路径已钉死,见下。
    2. 层次判断:你报的是「prefix cache 丢了」(症状),根因是两条独立机制——①跨进程消息 id 不再逐字一致(id 层),②编辑掉 compress 调用所在轮次后,孤儿块被拆成多条 user summary 重绘(fold 渲染层)。两者都让上游无法命中前缀缓存,但成因不同,得分别修。
    3. 方案评估:两处根因都在 kernel/,按 Kernel Boundary 需要你批准才能动手(见文末选项)。

    Part 1(重启后 id 变了)——你的假设要修正一处

    • 你说「lastPassIds 在新进程里是空的」。实际不是空:src/state-hydration.ts:36 明确把它从磁盘恢复(parsed.lastPassIds ?? fresh.lastPassIds),它持久化自上一进程最后一次请求的入站 id(kernel/src/compress.ts:876)。
    • 真正会触发重铸的是 kernel/src/instance-reid.ts 的 remintCoveredLiveIds:它只重铸「内容 hash 相同、但 id 不在上一遍 prior 集合里」的被覆盖活消息。关键门在 line 72:if (!state.lastPassIds) return messages; —— undefined 反而不重铸;真正的雷是「prior 集合 ≠ 本次入站 id」(或 lastPassIds 被存成空数组 [] 这种边界态)。所以问题不是「新进程没状态」,而是「新进程第一遍的入站 id 跟持久化的 prior 对不上」。
    • 为什么会「对不上」,有三个候选,需实测确认是哪条:(a) pi-native 增量车道在重启后首遍派生出的 id 与上一进程不一致(id 派生方式跨进程不稳定);(b) ref 游标 / messageRefs 高度跨进程不同导致 mNNNNN 号位漂移;(c) snapshot-render 与 legacy 路径在首遍的分流不一致。这一步我还没法一锤定音——要么跑你给的复现脚本,要么用你已经录到的请求体直接 diff,后者最快。

    Part 2(编辑改 fold 画法)——机制清楚,落在 kernel

    为什么判 p2 不是 p1

    没有任何数据丢失、不会服务到脏内容、上下文不会只增不减失控增长——纯粹是「每次重启/编辑多付一次 15-20k token 的重读」。对你这种频繁停启 pi 的长驻 host 很疼,但会话本身可用。升级条件:若将来发现它会服务到 stale 内容或造成 cache ref 错误改写(p1 的「缓存泄露类」),立即升 p1。

    怎么修 + 需要你拍板的部分

    两处根因都在 kernel/,按 Kernel Boundary(owner 批准 + 人工 review、禁自动合并),我不能擅自开修。选项:

    • A(推荐)· 批准 kernel 变更:我 rebase 到当前 master≈v0.1.211,先跑通复现脚本钉死 Part 1 到底是 (a)/(b)/(c) 哪条,再分别做根因修复 + 回归测试(跨进程 id 稳定性的 byte-equality 测试 + 编辑掉 carrier 轮的 fold 丢弃/重绘测试),走 kernel-change 声明 + 人工 review。这是根治(疏),不打补丁(堵)。
    • B · 只做 host 侧缓解、不碰 kernel:若证实 Part 1 只是 pi-native 侧 lastPassIds / id 派生的问题,可在 src/ 修;Part 2 若能在 fold-reconcile 加一条「carrier 轮次缺失即弃块」也能 host 侧兜住。风险:如果根因其实在 kernel 的 id 派生/游标,host 侧只是遮症状。需先确认可行性再定。
    • C · 先补证据再动手:把你 forwarder 录到的几组请求体(重启前后各一、编辑前后各一)发我,我直接 diff 出 id 具体怎么变的,最快锁定 Part 1 的 (a)/(b)/(c),比从零搭复现环境快。

    我的推荐:C + A 并行——你把请求体发我(C,零成本、最快锁根因),同时给我 kernel 变更的 go-ahead(A),我 rebase 当前 master 后就开干;B 作为「万一证明不必动 kernel」的备选。

    结构化八问

    1. 是什么问题:pi-native 下,pi 重启或用户编辑消息后,bili 重发的历史要么换了消息 id、要么把折叠块拆成多条 user summary,导致上游前缀缓存从约 1/3 处起全部失效。
    2. 什么原因导致的:两条独立机制——①跨进程消息 id 不再逐字一致(remintCoveredLiveIds 因 prior≠本次入站 id 而重铸;并非「新进程无状态」,lastPassIds 有持久化恢复);②编辑掉 compress 调用所在轮次后,孤儿块失去 carrier,退化成散落 user summary 重绘。(Part 1 的确切子因 a/b/c 待实测确认。)
    3. 影响范围与严重程度:所有用 pi-native、会话里有折叠块、又频繁停启/编辑的长驻 host;每次事件多付一次约 15-20k token 重读(叠加 pi native: bili's tools join the request only from the 2nd request, so each pi session re-writes ~20k cache tokens once #5459 工具迟到则连吃两次)。程度=体验/成本明显受损,无数据丢失、无脏读、无失控增长 → p2。
    4. 怎么修复的:见上方方向——A 为根治(kernel 内分别修 id 稳定性 + carrier 缺失弃块/重绘,配 byte-equality 回归测试);C 用你的请求体先锁根因;B 为 host 侧备选。
    5. 是否有回归风险:修复本身要防两类高危——id 修复不能破坏「内核永不复用 id」不变量(重铸必须继续避开 covered/inactive/live/prior 四重集合);fold 弃块不能误伤「同一 id 多 persona」的 subagent fork([bug] dsh:side request 先到会抢走 persona 锚点,主会话被 fork 到 |sub:<fp>,原生 compress 永久失败 #2156 那条 sideRequestLike 门)。PR 里逐项过。
    6. 是否破坏缓存:本 bug 本身就是缓存失效;修复目标是恢复命中率,不引入新的失效策略。修完需验证重启/编辑后 cache_n 回到 ~99%。
    7. 是否改了设计方向:不改架构方向;是在既有 identity/fold 契约内补「跨进程首遍一致性」和「carrier 缺失处置」两块缺口。
    8. 是否引入新配置:不需要新 env/config;如确需开关(例如保守的「carrier 缺失即弃块」默认开/关),会走 config surface 审批并默认对齐现有行为。

    收工判定:暂不收工——triage 完成、机制钉死、severity 定级 p2、标签已打(bug/pi/scope/kernel/p2/needs-decision)。下一步卡在你这里:请给 kernel 变更的 go-ahead(A),并方便的话把重启前后 + 编辑前后的请求体发我(C),我 rebase 当前 master 后即可开工。

  2. ranxianglei commented on Nov 1, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    补一条(已在你的确切版本 v0.1.211 上核对,不是只对我本地旧 master):三处关键代码在 v0.1.211 跟我分析的一致——kernel/src/instance-reid.ts:72 仍是 if (!state.lastPassIds) return messages;、src/state-hydration.ts:36 仍恢复 lastPassIds、kernel/src/compress.ts:932/:2342 仍按上一遍入站 id 持久化。

    由此能再收窄一步:在 v0.1.211 下,「历史逐字一致的纯重启」本身不会触发重铸——prior 就是上一进程最后入站 id,逐字一致时每个被覆盖的活消息都会 prior.has(id) 命中而跳过(line 115)。所以你能观察到重启后 id 变了,说明新进程第一遍派生出的 id 跟持久化的 prior 真的不一样,而不是「新进程丢了状态」。这把 Part 1 的根因锁死在三选一:

    • (a) pi-native 增量车道跨进程的 id 派生不稳定;
    • (b) ref 游标 / messageRefs 高度跨进程漂移,导致 mNNNNN 号位整体平移;
    • (c) snapshot-render 与 legacy 路径在首遍分流不一致。

    要一锤定音还得靠实测:你 forwarder 录的请求体(重启前后各一组)发我最快;或者给我 kernel go-ahead,我 rebase 到当前 master 后搭复现环境跑。等你拍板。

  3. ranxianglei commented on Nov 1, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    再收窄一步,并给你一个用手头数据一眼定根因的判据。

    我读了 pi-native 的 id 派生核心 src/agent/pi-msgids.ts(#4491):pi 把会话里每条消息的稳定 id(SessionEntry.id,randomUUID 前 8 位 hex,跨重启不变)经一个「逐字对齐、全有或全无」的 walker 钉到出站线上;只要有一条对齐不上(thinking 块、tool-call id 被 normalizeToolCallId 重编号、空文本被丢、跨模型降级……),整个 turn 就放弃 msgIds、退回内容 hash id。

    关键点:这两套 id 方案对「历史逐字一致的纯重启」都是确定性的——entry.id 稳定,内容 hash 也稳定。所以我之前列的 (a) 跨进程派生不稳定、(b) ref 游标漂移,在纯一致重启下结构性地都不太可能单独触发;(c) snapshot-render 模式切换又要求开 BILI_STORAGE_UNIFIED(默认关,你没提到)。

    于是更可疑的是一个 pi-native 专属机制:msgId「钉住 vs 退回」在两个进程间翻转。若重启后 pi 重建 wire 时某处 transform 与上一进程不一致(thinking / reasoning_content、tool-call id 重编号、空文本),walker 在旧进程成功(钉住 entry.id)、在新进程 bail(退回内容 hash)→ 两套完全不同的 id 方案 → mNNNNN 从第一个不一致点起整体不同,正好对上你说的「同文本、id 变了、后面全变」。

    判据(请求体直接看,不用跑复现):对比重启前后同一批消息 id 的格式——

    • 前是 8 位 hex uuid 片段、后变 h_… 内容 hash(或反过来)→ 「钉住/退回翻转」实锤,修点在 pi-msgids walker 的跨进程一致性;
    • 前后同格式、号位整体平移 → ref 游标 / messageRefs 路径;
    • 前后同格式、只有个别被覆盖消息换了 _k 后缀 → remintCoveredLiveIds 的 prior 失配。

    把重启前后各一组请求体发我,按这个判据一锤定音;同时等你 kernel 变更的 go-ahead。

  4. Piggidragon commented on Nov 1, 2026

    @Piggidragon
    Author

    Thanks for the criterion. I recorded it again on a clean setup and compared the ids by format.

    Setup. billion-context 0.1.211 as a pi package (pi 1.1.0), OpenAI chat-completions wire, ACP_DEBUG=1, empty bili state/data/cache dirs (so nothing from an earlier install), a context window of 32768 (bili reports an effective 28672). Each chat read files until the model called compress (two chats, one fold or more each). A forwarder between bili and the model server kept every request body and the server's timings. I compared (a) bili's own store (messages.msg_id, state.lastPassIds, state.messageRefs) and (b) the <acp ...>mNNNNN</acp> numbers on the wire, before and after each event. No message text is included below.

    Verdict

    Restart: your second case (ref cursor / messageRefs path), more exactly the one-time ledger alignment of the refs at cold load.

    • Format: every id is h_<16 hex> on both sides, in every snapshot. No 8-hex uuid fragment appears anywhere. The set of raw ids is identical before and after (40 of 40 shared in chat 1, 43 of 43 in chat 2).
    • What changes is the mNNNNN number of the same raw id, from the first folded region on; the numbers before it are kept. It is not a uniform shift (some move by 2, some by 20+).
    • The log says why, once per chat, at the first cold load: [persist] ledger-aligned refs ... with restamped and rehomed greater than zero. state.refLedgerAligned is absent in the snapshot taken in the process that created the state and true in the first snapshot after the restart.
    • Control: a second restart of the same chat gives a byte-identical prefix (no ledger-aligned line, cache_n 22351 of 22558). So it costs once per chat, at its first restart after it has a fold.
    • Your first case does not apply in this setup. pluginLaneServerHistory is off here (log line below), there is not a single [msgid] log line in the whole run, and in the 0.1.211 bundle biliMsgIds is read only in prepareAnthropic and the incremental lane, not on the OpenAI wire. I did not test a rollout-on install.
    • Your third case does not apply either: no _k id changes across the restart. The _k ids in chat 2 exist before the restart too (same raw id, same suffix), so they come from duplicate message content in the process, not from a re-mint.

    Edit that removes the turn with the compress call: none of the three. Raw ids and refs of the messages that stay are unchanged (m00006, m00007, m00012 before and after, all h_ hashes). What changes is how the fold is drawn: before the edit the blocks sat behind the compress call/result pair (the carrier); after it, bili keeps the blocks and draws them as separate user-role messages with ids acp_summary_b1 and acp_summary_b2, so the prefix changes from the first message after the first user message on. That is the second part of the issue.

    Restart: ids before and after (chat 1, wire request 15 -> 16)

    The first wire message that differs is #8 (57 % of the bytes of the older body come before it; in chat 2 it is #8 and 78 %). Messages #1 to #5 keep m00001, m00006, m00007, m00012, m00013.

    wire # role raw id (first 8 hex) format before / after ref before ref after
    8 assistant h_6cd139c8 h_hash / h_hash m00018 m00020
    9 user h_9a4763e1 h_hash / h_hash m00019 m00042
    10 assistant h_5c517312 h_hash / h_hash m00021 m00023
    11 user h_1270b4e4 h_hash / h_hash m00026 m00028
    15 assistant h_c7d8e490 h_hash / h_hash m00031 m00047
    16 user h_73cade67 h_hash / h_hash m00032 m00036
    17 assistant h_922763ca h_hash / h_hash m00034 m00038

    Chat 2 (request 35 -> 36) is the same picture: 6 refs on the wire kept, 10 changed, e.g. #8 m00023 -> m00051, #9 m00024 -> m00048, #14 m00034 -> m00047, all with identical raw ids.

    Edit: the same wire, before and after (chat 2, request 37 -> 38)

    before after
    messages 24 8
    roles, first 8 s u a u a u a(compress) tool s u u(summary) a u u(summary) a u
    #2 assistant m00006 user-role acp_summary_b1 (no ref)
    #5 user m00013 user-role acp_summary_b2 (no ref)
    ids of the kept messages h_ hashes the same h_ hashes, same refs
    first differing message #2 (57 % of the bytes before it); chat 1: #2, 41 %

    Log lines that matter (paths and session ids redacted)

    [persist] ledger-aligned refs for <sess>: kept=15 restamped=14 rehomed=7 blocked=4 superseded=21 (#4316 PR3/3)
    [persist] remapped 8 token-snapshot entries onto ledger-aligned refs for <sess> (#4599)
    [persist] cold-loaded <sess> from unified store: 36 ref(s), 4 block(s) (4 active), baseline 20697 tok, compressions 4, savedAt ~1 min ago
       (chat 2: kept=21 restamped=12 rehomed=8 blocked=2 superseded=20)
    [rollout] install <id> - anthropicThreadsReforge=off(bucket, 9116/10000 vs 0bp), pluginIncrementalTurns=off(bucket, 5436/10000 vs 0bp), pluginLaneServerHistory=off(bucket, 206/10000 vs 0bp), ...
    

    Edit:

    [fold-reconcile] 2 covered id(s) no longer on the wire with no replacement evidence and the resent history is a prefix of the pre-rewind wire - host rewind/undo removed the newest turns (incl. a folded span); fold snapshot retained, nothing reaped (#3724)
    [fold-reconcile] resent history drifted: 2 covered id(s) missing with no anchor match - mutation (content edit invalidates content-hash refs, fold silently lost) or client-side deletion/truncation (benign, message no longer on the wire) (#1921)
    [cache-ledger] fold seq 3 (block b3) coverage-lost: covered id(s) no longer on the resent wire - avoided-token accrual frozen (#2202)
    

    Timings (model server prompt_n / cache_n)

    step prompt_n cache_n
    chat 1, last request before the first restart 1103 19594
    chat 1, first after it (6 tools fewer) 8883 10641
    chat 1, second after it (tools back) 8923 12365
    chat 2, last before the first restart 167 16680
    chat 2, first after it 15661 0
    chat 2, second after it 5131 11971
    chat 1, second restart (control), before / after 218 / 207 22137 / 22351
    chat 1, edit, after 963 10528
    chat 2, edit, after 709 10651

    The chat 1 restart rows are bili's [acp-usage] (input minus cached), the others the server's timings.

    Two things to keep apart in the restart rows. The first request of a restarted pi has 33 tools instead of 39 (the six bili tools are registered later, the known #4134 / #5459 / #5498), and the tools come before the messages, so that alone costs the whole prompt in the first request. The ref change is the part that is left once that is fixed: with the tools block equal, the prefix would still end at the first differing message (57 % and 78 % of the bytes). The edit rows are small only because the edit cut most of the history; the fold drawing changes at message #2 regardless of how much history stays.

    Open

    I can send the bodies as a structural diff (roles, ref numbers, ids, no text) if that helps.

  5. ranxianglei commented on Nov 1, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    你的数据是决定性的——不用发完整请求体了。我对 v0.1.211 代码逐行核过,两条都钉死,而且跟我之前猜的不一样,你的判定对。

    Restart = 你说的第二例(ref/messageRefs),精确说是冷加载时的一次性「引用对账」。 代码在 kernel/src/refs.ts:232 restampRefsToLedger(由 src/persist.ts 的 maybeRestampToLedger 冷加载时调一次,#4316 PR3/3)。它把每个有存储行的 raw id 强制改成 ref = 行 seq + 1。而活进程按到达顺序发号、且受保护消息不占号但行 seq 照发 → 两者天然漂移;一旦有折叠,漂移落在折叠区。于是首次冷加载把折叠区 mNNNNN 整体重排(你的 restamped=14 rehomed=7)。state.refLedgerAligned 一次性置真,所以第二次重启不动(你的对照:byte-identical)——完全对上。关键:内部引用有 superseded 翻译表兜底(不串号),但出站字节本身变了,上游前缀缓存存的是旧号,这一次必 miss(chat2 首启 cache_n=0 即全 miss)。我之前的 msgId 钉住/退回翻转、snapshot-render 两例在你这套配置确实不适用(pluginLaneServerHistory=off、无 unified store),排除正确。

    Edit = 三例都不是,是「孤儿块保留后被散绘」。 编辑裁掉带 compress 调用的那轮后,src/fold-reconcile.ts 正确地保留了块(内容还要用,#3724「nothing reaped」),但 carrier(那个 tool call)没了之后,渲染层把块摘要退化成一条条独立 user 角色消息 acp_summary_bN 插在每条 user 后面 → 前缀从第 2 条起就变。期望(#1921):跨度已没就丢块回原始前缀,或按编辑前的画法稳定锚定——现在是第三种「散绘」,坏的。

    根因层次:两条都是「活进程已发出并被上游缓存的出站形态,在重启/编辑后被 bili 单方面改写」。Part 1 是引用号被对账重排,Part 2 是折叠块换了画法。都违反前缀字节稳定契约,但都不产生脏读/串号(有翻译表/保留兜底)→ 成本回归,判 p2(高段:折叠会话首启是全 miss)。

    修法 + 需要你拍板(两处核心都在 kernel/,按 Kernel Boundary 需你批准才能动):

    • Part 1:
      • A(推荐·根治):所有车道统一按 行 seq+1 铸造(stamped 车道的 ledgerSeq 路径已经这么干),非 stamped 车道也改用同一基线,从源头消除计数/seq 漂移 → 冷加载对账发现全部已对齐(kept=N restamped=0),缓存永不对账破。代价:改 kernel assignRefs 发号基线,需配「跨进程首启 byte-equality」回归测试。
      • B:冷加载对账改保守版「仅会串号才重排」——但 superseded 已兜内部串号,纯为缓存保守又留 store-seq 查找不一致的正确性风险,比 A 绕。
      • C:host 侧 gate 掉这次对账——不够,store-seq 路径查找仍会串,治标。
    • Part 2:carrier 轮次缺失时,要么丢块(覆盖跨度已不在重发历史里)、要么稳定锚定渲染(固定在折叠边界,而非每条 user 后散落)。落点:host fold-reconcile 的「保留 vs 丢弃」决策 + kernel 渲染(prune/hide-consumed)的锚定位置。

    我推荐:Part 1 走 A,Part 2 走「跨度没了丢块、否则稳定锚定」。两处都要动 kernel,请给 kernel-change 的 go-ahead;我本地 master 落后 v0.1.211 共 585 提交,批准后我会 fetch 当前 master、开分支、实现并补 byte-equality/wire-parity 回归测试再提 PR(人工 review)。

    结构化八问

    1. 是什么问题:pi-native 下,折叠会话首次重启把折叠区引用号整体重排、编辑掉 compress 调用轮次把孤儿块散绘成多条 user 摘要,二者都让上游前缀缓存从某处起失效。
    2. 什么原因导致的:After model switch / client-side history rewrite, all message identities (content hashes) are replaced: ACP numbers re-m | 切模型/客户端重写历史后,消息身份(内容哈希)全部更换:acp 编号重铸、折叠覆盖大规模重挂、死 ref、churn 区临时膨胀重折 #4316 冷加载一次性引用对账把 ref 强制到「行 seq+1」,与活进程实发并被缓存的号不符(受保护消息不占号致漂移);编辑后 carrier 消失使折叠块从「锚定」退化为「散绘」。
    3. 影响范围与严重程度:所有 pi-native、有折叠块、又重启/编辑的长驻 host;折叠会话首启常全 miss(chat2 cache_n 0),每次多读约 15-20k token。无脏读/串号/失控增长 → p2(高段)。
    4. 怎么修复的:Part 1 推 A(统一按 seq+1 发号,消除对账 delta);Part 2 推「跨度没了丢块、否则稳定锚定」。均配跨进程 byte-equality 回归测试。
    5. 是否有回归风险:要防——A 不能破坏「id 永不复用」与 [Bug] compress can silently fold another message after restart when a pre-restart ref is reused (0.1.209) #5196 槽位退役不变量;Part 2 丢块不能误伤仍需内容的块、稳定锚定不能改既有会话的现有画法(只修「carrier 缺失」这一新场景)。PR 里逐项过。
    6. 是否破坏缓存:本 bug 即缓存失效;修复目标是恢复命中,修完验证重启/编辑后 cache_n 回 ~99%。
    7. 是否改了设计方向:不改架构;是在既有「正史账本对账」(After model switch / client-side history rewrite, all message identities (content hashes) are replaced: ACP numbers re-m | 切模型/客户端重写历史后,消息身份(内容哈希)全部更换:acp 编号重铸、折叠覆盖大规模重挂、死 ref、churn 区临时膨胀重折 #4316) 与「折叠 carrier 契约」内补「跨进程首启一致性」与「carrier 缺失处置」两块缺口。
    8. 是否引入新配置:不需要新 env/config。

    收工判定:暂不收工——两条机制已在 v0.1.211 代码层钉死并对齐你的实测数据,severity=p2,标签 bug/pi/scope/kernel/p2/needs-decision 已挂。下一步卡在你:请给 kernel-change go-ahead,并在 A/B/C 里定 Part 1 方向(我推 A)、确认 Part 2 走「丢块或稳定锚定」;定了我就 fetch 当前 master 开工提 PR。

  6. ranxianglei commented on Nov 11, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: qwen/qwen3.8-27b)

    Merge conflict resolved and rebased onto current master. Summary of this run:

    • Live state at start: PR open, mergeable_state: dirty vs master @ 0d0c0b95; head branch 2026-11-02_step-window-char-bound-poison @ 8222bc4152.
    • Rebase onto origin/master (58a87962) — 4 commits replayed, single conflict in tests/issue5134-post-fold-credit.test.ts (T5).
    • Conflict nature (semantic, not textual): this PR refined T5's stale ride-on from the char bound (350_000/estimate) to the calibrated per-send estimate (190_000/estimate); master's 2026-11-08 burst fix rewrote T5 to the opposite doctrine — pending credit outranks cycle-staleness (180_353/usage). In the merged code the credit-pending override (src/server.ts:2956) executes before this PR's new anchorStale branch (src/server.ts:3012–3015), so for T5's exact state (credit = 89_096 pending) the merged code returns 180_353/usage — master's assertion is the correct one. Kept master's T5 verbatim. Nothing of this PR's intent is lost: its calibrated-ride-on still governs stale anchors without pending credit, which its own suite pins.
    • Targeted verification (local, targeted files only): tests/issue5134-post-fold-credit.test.ts, tests/issue5500-char-bound-poison.test.ts, tests/issue4383-stale-usage-anchor.test.ts, tests/issue5293-abort-nudge.test.ts, tests/issue5415-absorb-debt.test.ts → 38/38 pass; npm run typecheck clean. Full-suite verdict is left to CI on the pushed head.
    • Push discipline: git fetch origin immediately before push — remote head unchanged (8222bc4152, no concurrent agent activity), so lease-guarded force-push of the conflict-rebase was safe: 8222bc4152 → 8dede7d140.
    • Live state after push: mergeable: true, mergeable_state: blocked — conflict gone; now waiting on CI + approval as usual. No red-CI fixing was needed this run (the blocker was purely the dirty merge state).

    中文结构化结论(八问)

    1. 是什么问题:PR [PR] fix: refuse estimate-grade arms in preflight entry sizing; add step-family 1M window (#5500) #5501 与 master 产生合并冲突(merge state dirty),无法合并。
    2. 什么原因导致的:PR 分支从 0d0c0b95 切出后,master 侧的 2026-11-08 burst fix 改写了同一测试 T5(tests/issue5134-post-fold-credit.test.ts),与 PR 对同一断言的细化形成语义级冲突——不是文本撞行,而是两条分支对「credit 挂起 + 锚点周期过期」这一场景给出了相反的裁决教义。
    3. 影响范围与严重程度:仅阻塞本 PR 合并,不影响任何已发布行为;冲突解决本身零行为变更。
    4. 怎么修复的:rebase 到最新 master;T5 保留 master 版本——因为合并后代码里 credit 挂起覆盖(src/server.ts:2956)先于 PR 新增的过期锚点骑乘分支(src/server.ts:3012–3015)执行,T5 场景下真实返回值就是 master 断言的 180_353/usage;PR 的校准估计骑乘行为在 credit==0 路径完整保留并由其自有测试套件钉住。
    5. 是否有回归问题:无。定向跑了 5 个相关测试文件共 38 个用例全绿,typecheck 干净;PR 自身行为与其 master 侧行为的交互(credit 挂起时 usage 锚点优先、无 credit 时校准估计优先)与两个提交各自声明的意图一致,属代码顺序的自然结果,非本次引入的漂移。
    6. 是否破坏缓存:不涉及——纯冲突解决,未触碰缓存/重试/游标逻辑。
    7. 是否改了设计方向:不涉及。
    8. 是否引入了新的配置:不涉及。

    判定:冲突已清除、定向验证通过,PR 现处于正常等 CI/审批状态,本轮收工。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions