Skip to content

[Bug][Responses] After a short request makes fold inactive, resending canonically equivalent history fails to restore co | [Bug][Responses] 短请求使 fold inactive 后,规范化等价的历史重发无法恢复压缩覆盖 #4050

Closed

Description

@0XCocoa

(ai: OpenAI/GPT-6)

问题与范围

在 Responses legacy render 路径下,同一 session 的短请求会暂时令已有 fold inactive。之后主历史仅改变空白、使内容哈希 ID 变化时,reconcile 没有恢复原本可以恢复的覆盖:块仍保留,但摘要退出出站请求,已折叠原文重新发送。

这是当前 master 的真实 HTTP proxy + loopback mock upstream 复现,不只是提取函数的单元测试。只修改测试文件,没有提交实现修复。测试刻意关闭 snapshotRender(默认 0%),存储关闭;没有测试 D/snapshot lane,也不声称它存在同一缺陷。生产配置未配置 snapshot/rollout,保留日志没有该 session 的 snapshot-render 记录,但缺少历史进程环境,故不能仅凭这些断言现场 lane。

建议分类:bug / p2 / codex / scope/wiring;讨论类别 I(其他宿主车道)。

已验证的四组对照

固定提交 bc4a3f87e2dc98e82df6f707d362e75492fdf3d1(检查时仍是 master),bili 0.1.202,Windows 10 x64 / Node 24.15.0。

通过真实 HTTP 请求建立 24 条合成主历史,用实际 createCore().applyCompression 折叠前七条,再发送主历史建立覆盖。短请求为同 session 的 3 个 raw input items,每个 13 个 input_text parts。等价重发只把普通空格变为两个空格;规范化文本和顺序保持一致。

场景 短请求后 active 重发后 active 出站含摘要 出站含已折叠 assistant 原文 出站字节
无短请求 → 空白变化 — 1 是 否 65,884
短请求 → 完全原样重发 0 1 是 否 63,850
短请求 → 同样的空白变化 0 0 否 是 80,867
无短请求 → 真正不同的正文(负控制) — 0 否 否(正文已替换) 22,409

统一基线出站为 63,850 bytes;受影响场景比无短请求的等价重发多 14,983 bytes。以上是合成请求的字节测量,不是 token 计费测量。最早 user message 按现有保护策略保留,断言检查的是已实际折叠的第二条 assistant 消息,不将受保护首条误计为展开。

四个测试全部通过(断言明确记录当前缺陷;修复后应改变受影响场景的预期),并有真编辑负控制。没有外部模型调用,mock upstream 使用已有测试 helper 的响应形状,没有模拟 Codex 客户端的原生压缩。

代码链条

  1. Responses prepare 先 reconcile,再 core.processTurn。
  2. syncBlocks 在短视图不存在覆盖 ID/摘要 carrier 时将块置为 inactive,块本身保留。
  3. coveredRealHistoryIds 跳过 inactive 块。
  4. reconcileFoldCoverage 因 covered.size === 0 提前返回。即使新主历史只是规范化等价的 ID churn,也没有 re-key;后续 sync 无法靠旧 ID 恢复。完全相同的 ID 则可以恢复,正好由对照证明。

这里报告状态组合和可验证行为,不建议对所有 inactive 块盲目恢复,也不要求把 prepare 无条件搬到 compaction gate 前。真正编辑/删除必须继续诚实失配;kernel 变更仍需遵循 owner 边界。

与已有问题/PR的区别

现场关联:观测已确认,因果仍有边界

脱敏的 Codex Desktop 多图会话,bili 0.1.202 build 6deb454 / rollout cli 0.163.0-alpha.10.2。主请求实际 input 230,122,之后短请求 raw 3 items 经处理为 66 CoreMessages、input 22,662;再回到主请求时出站从此前 35.5 MiB 升到 102.0 MiB,实际 input 591,601。compaction trigger 被 90% usage gate 放行,随后本地 rollout 确认原生 compaction。

这不能证明现场每个丢覆盖的块都由上述状态组合造成。 没有保留关键时刻逐块 active/ID 快照;现场还涉及模型切换和大量历史图片。token/请求体膨胀、短请求、原生压缩均有日志证据;将整个膨胀归因于这个 bug 仍是待验证假设。复现本身是纯文本,未声称复现 59 万 tokens 或图片膨胀。

下面是原始日志的精选字段;只替换 session UUID,不包含请求正文、图片、凭据、本机路径或项目名。时间为 UTC。

2026-10-20T18:44:46.200Z [info] [sess=session-A] [v=0.1.202] [session-A] [acp-usage] round 1 input=230122 cached=227200 (cache hit 99%)
2026-10-20T18:44:47.194Z [info] [sess=session-A] [v=0.1.202] [session-A] processTurn: 66 msgs, renderTags=text-only, 65 text tagged, 0 tool tagged (should be 0 with text-only)
2026-10-20T18:44:47.195Z [info] [sess=session-A] [v=0.1.202] [session-A] nudge idle: usage=119% (230122/193800), growth=28115/22500 (ref=202007, interval=50000), pendingT1=0/50000 model=gpt-5.6-luna, reason="EMERGENCY: usage 119% but max pending 1301 < min benefit 5000 tokens (T1 effective 0, T2 0, T3 1301) — suppressed: rewri"
2026-10-20T18:44:47.440Z [info] [sess=session-A] [v=0.1.202] [session-A] request: 3 msgs, inbound=1.9MiB, outbound=1.9MiB, local=867ms, view=ws-expanded
2026-10-20T18:44:55.292Z [info] [sess=session-A] [v=0.1.202] [session-A] [acp-usage] round 1 input=22662 cached=4608 (cache hit 20%)
2026-10-20T18:45:15.618Z [warn] [sess=session-A] [v=0.1.202] [session-A] context 630981.6115112097 tokens reached preflight target 320625 (model window 356250, model=gpt-6.1-sol; usage-baseline=22662 local-est=237802); preflight compressing before forward
2026-10-20T18:45:16.104Z [info] [sess=session-A] [v=0.1.202] [session-A] preflight folded 0 range(s) but images alone (~393179.61151120975 tokens) keep the estimate over window 356250 with no upstream overflow evidence — forwarding for the upstream to arbitrate billing (#496/#1800)
2026-10-20T18:45:16.173Z [info] [sess=session-A] [v=0.1.202] [session-A] request: 782 msgs, inbound=75.8MiB, outbound=102.0MiB, local=17998ms
2026-10-20T18:47:10.202Z [info] [sess=session-A] [v=0.1.202] [session-A] [acp-usage] round 1 input=591601 cached=39424 (cache hit 7%)
2026-10-20T18:47:25.360Z [info] [sess=session-A] [v=0.1.202] [session-A] codex compaction_trigger request not intercepted (gate preconditions not met) — forwarding verbatim (no preflight, no rebuild, no window clamp)

可运行的 HTTP 最小复现

在上述 commit 的工作副本安装依赖并构建 kernel。把下方代码保存为 tests/rabbit-short-replay.test.ts。测试使用已有仓库 helper 的缩小副本,端口均由 OS 分配。

Windows PowerShell(独立测试副本;所有运行状态写入副本旁边的 test-state):

npm ci --ignore-scripts --no-audit --no-fund
npm --prefix kernel run build
$testState = Join-Path (Get-Location) '../test-state'
$env:XDG_CONFIG_HOME = Join-Path $testState 'config'
$env:XDG_DATA_HOME = Join-Path $testState 'data'
$env:XDG_STATE_HOME = Join-Path $testState 'state'
$env:XDG_CACHE_HOME = Join-Path $testState 'cache'
$env:BILI_ROLLOUT_SNAPSHOT_RENDER = '0'
node --import tsx --test tests/rabbit-short-replay.test.ts
完整合成 HTTP 回归(四组对照)
import { writeFileSync } from "node:fs";
import assert from "node:assert/strict";
import http from "node:http";
import { once } from "node:events";
import test from "node:test";

process.env.NODE_ENV = "test";
process.env.BILI_ROLLOUT_SNAPSHOT_RENDER = "0";
// Fail fast on the very first 429 instead of the default 3 attempts with
// exponential backoff — these tests are about the post-retry behavior.
process.env.BILI_REPLAY_RETRY_MAX = "1";

import { createCore, defaultConfig, type Config } from "acp-kernel";
import { startServer } from "../src/server.ts";
import { type CompressSettings, type ProxyOptions } from "../src/config.ts";
import { SessionStore, _setStoreForTest } from "../src/persist.ts";
import { getSession } from "../src/session.ts";
import { _setForTest as setRegistryForTest } from "../src/registry.ts";

// #301: when preflight (overflow) compression cannot proceed — the summary
// upstream is rate-limiting, or the compress budget is exhausted — the proxy
// must FAIL FAST with a structured error instead of forwarding the over-window
// payload as-is (guaranteed upstream 400, wasted quota, client retry storms;
// log evidence in #292). Forwarding as-is stays legal ONLY when the payload's
// own estimate actually fits the window (the trigger floors on
// stats.lastInputTokens, which can be stale — #300 double-counted usage).

function okSse(inputTokens: number): string {
    return (
        `event: message_start\ndata: ${JSON.stringify({ type: "message_start", message: { id: "m1", role: "assistant", usage: { input_tokens: inputTokens } } })}\n\n` +
        `event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n` +
        `event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "ok" } })}\n\n` +
        `event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: 0 })}\n\n` +
        `event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 3 } })}\n\n` +
        `event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`
    );
}

type Call = { stream: boolean; body: string };

function makeUpstreamOk(calls?: Call[]): http.Server {
    return http.createServer((req, res) => {
        const chunks: Buffer[] = [];
        req.on("data", (c: Buffer) => chunks.push(c));
        req.on("end", () => {
            const raw = Buffer.concat(chunks).toString("utf8");
            let parsed: { stream?: boolean } = {};
            try { parsed = JSON.parse(raw); } catch { /* keep {} */ }
            calls?.push({ stream: !!parsed.stream, body: raw });
            if (parsed.stream) {
                res.writeHead(200, { "content-type": "text/event-stream" });
                res.end(okSse(1000));
            } else {
                res.writeHead(200, { "content-type": "application/json" });
                res.end(JSON.stringify({ content: [{ type: "text", text: "SUMMARY: the large recent user message was a deterministic load-growth payload; its raw content is no longer needed." }] }));
            }
        });
    });
}

function startProxy(upstreamPort: number, models: Record<string, { context: number }>, kernelOverrides?: Partial<Config>, compressOverrides?: Partial<CompressSettings>): Promise<http.Server> {
    _setStoreForTest(new SessionStore({ enabled: false }));
    setRegistryForTest({});
    return startServer({
        port: 0,
        host: "127.0.0.1",
        upstream: "http://127.0.0.1",
        routes: { [`http://127.0.0.1:${upstreamPort}`]: { models } },
        modelContextLimit: 400_000,
        kernelConfig: defaultConfig(400_000, kernelOverrides),
        compress: { injectTool: true, injectNudge: true, ...compressOverrides },
        promptCache: { routing: "auto" },
        sessionHeader: "x-acp-session",
        log: false,
        debug: false,
        passthrough: false,
        passthroughSource: null,
        autoUpdate: false,
        autoRestartOnUpdate: false,
        updateTag: "latest",
        advisoryCheck: false,
        releaseNotesCheck: false,
        compat: { roles: {} },
        streamErrorShape: "protocol",
        mitm: { enabled: false, domains: [] },
    } as ProxyOptions);
}


for (const scenario of ["isolated-normalized", "short-exact", "short-normalized", "isolated-edited"] as const) {
    test(`rabbit-http ${scenario}`, async () => {
        const calls: Call[] = [];
        const upstream = makeUpstreamOk(calls);
        upstream.listen(0, "127.0.0.1");
        await once(upstream, "listening");
        const upstreamPort = (upstream.address() as { port: number }).port;
        const proxy = await startProxy(upstreamPort, { "gpt-short-wire": { context: 400_000 } });
        await once(proxy, "listening");
        const proxyPort = (proxy.address() as { port: number }).port;
        const sid = `rabbit-http-${scenario}`;
        const post = async (input: unknown[]) => {
            const response = await fetch(`http://127.0.0.1:${proxyPort}/bili/http://127.0.0.1:${upstreamPort}/v1/responses`, {
                method: "POST", headers: { "content-type": "application/json", "x-acp-session": sid },
                body: JSON.stringify({ model: "gpt-short-wire", stream: true, max_output_tokens: 1024, input }),
            });
            const body = await response.text();
            assert.equal(response.status, 200, body.slice(0, 300));
        };
        try {
            const main = Array.from({ length: 24 }, (_, i) => ({
                type: "message", role: i % 2 ? "assistant" : "user",
                content: `main turn ${i} ${(`UNIQUE_${i}_stable_text `).repeat(110)}`,
            }));
            await post(main);
            const session = getSession(sid);
            const messages = session.lastMessages;
            assert.ok(messages && messages.length >= 24);
            session.state = createCore().applyCompression({
                ranges: [{ startRef: "m00001", endRef: "m00007", summary: "Compressed main history: seven early turns preserved the investigation, implementation decisions, and verified test evidence." }],
                messages, state: session.state, config: defaultConfig(400_000),
            }).state;
            await post(main);
            assert.equal(session.state.blocks.length, 1);
            assert.ok(session.state.blocks[0].active);
            const baselineWireBytes = Buffer.byteLength(calls.at(-1)!.body);
            const before = session.state.blocks[0].effectiveMessageIds.slice();
            let activeAfterShort: number | null = null;
            if (scenario.startsWith("short-")) {
                const short = Array.from({ length: 3 }, (_, i) => ({
                    type: "message", role: "user",
                    content: Array.from({ length: 13 }, (_, j) => ({ type: "input_text", text: `utility ${i} part ${j}` })),
                }));
                await post(short);
                activeAfterShort = session.state.blocks.filter(b => b.active).length;
                assert.equal(session.state.blocks.length, 1, "raw-count guard retains the block");
                assert.equal(activeAfterShort, 0, "short request deactivates retained block");
            }
            const replay = main.map(m => ({...m, content: scenario === "short-exact" ? m.content :
                scenario === "isolated-edited" ? "A genuinely different replacement with no prior semantic meaning." : m.content.replaceAll(" ", "  ")}));
            await post(replay);
            const wire = calls.at(-1)!.body;
            writeFileSync(`../rabbit-wire-${scenario}.json`, wire);
            const result = {
                scenario, activeAfterShort, retainedBlocks: session.state.blocks.length,
                finalActiveBlocks: session.state.blocks.filter(b => b.active).length,
                coveredIdsChanged: JSON.stringify(before) !== JSON.stringify(session.state.blocks[0]?.effectiveMessageIds),
                summaryOnWire: wire.includes("Compressed main history: seven early turns"),
                foldedAssistantOriginalOnWire: wire.includes("UNIQUE_1_stable_text"),
                baselineWireBytes, replayWireBytes: Buffer.byteLength(wire),
            };
            console.log("RABBIT_RESULT " + JSON.stringify(result));
            assert.equal(result.summaryOnWire, scenario === "isolated-normalized" || scenario === "short-exact");
            assert.equal(result.foldedAssistantOriginalOnWire, scenario === "short-normalized");
        } finally {
            proxy.close(); proxy.closeIdleConnections?.();
            upstream.close(); upstream.closeIdleConnections?.();
            await Promise.allSettled([once(proxy, "close"), once(upstream, "close")]);
        }
    });
}
实际运行结果
[
  {
    "scenario": "isolated-normalized",
    "activeAfterShort": null,
    "retainedBlocks": 1,
    "finalActiveBlocks": 1,
    "coveredIdsChanged": true,
    "summaryOnWire": true,
    "foldedAssistantOriginalOnWire": false,
    "baselineWireBytes": 63850,
    "replayWireBytes": 65884
  },
  {
    "scenario": "short-exact",
    "activeAfterShort": 0,
    "retainedBlocks": 1,
    "finalActiveBlocks": 1,
    "coveredIdsChanged": false,
    "summaryOnWire": true,
    "foldedAssistantOriginalOnWire": false,
    "baselineWireBytes": 63850,
    "replayWireBytes": 63850
  },
  {
    "scenario": "short-normalized",
    "activeAfterShort": 0,
    "retainedBlocks": 1,
    "finalActiveBlocks": 0,
    "coveredIdsChanged": false,
    "summaryOnWire": false,
    "foldedAssistantOriginalOnWire": true,
    "baselineWireBytes": 63850,
    "replayWireBytes": 80867
  },
  {
    "scenario": "isolated-edited",
    "activeAfterShort": null,
    "retainedBlocks": 1,
    "finalActiveBlocks": 0,
    "coveredIdsChanged": false,
    "summaryOnWire": false,
    "foldedAssistantOriginalOnWire": false,
    "baselineWireBytes": 63850,
    "replayWireBytes": 22409
  }
]
tests 4
pass 4
fail 0

Activity

  1. changed the title [-][Bug][Responses] 短请求使 fold inactive 后,规范化等价的历史重发无法恢复压缩覆盖[/-] [+][Bug][Responses] After a short request makes fold inactive, resending canonically equivalent history fails to restore co | [Bug][Responses] 短请求使 fold inactive 后,规范化等价的历史重发无法恢复压缩覆盖[/+] on Oct 20, 2026
  2. ranxianglei commented on Oct 21, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    查重: 不重复。 与相关条目逐一比对:

    标签: bug / codex / scope/wiring / p1。 p1 理由(从报告的 p2 上调):按 owner 2026-10-19 修订,缓存泄露类(上下文失控增长)一律 p1——现场证据正是 p1 示例「缓存命中崩坏导致成本爆炸」:主轮 input 230,122 → 591,601 tokens,cache hit 99% → 7%,outbound 35.5 MiB → 102 MiB,最终把会话推入 native compaction。

    Bug 审核三步:

    1. 复现查证(当前 master 56a0f5a 代码级确认):bc4a3f8 之后无提交触碰链路四文件(prepare-responses / sync / session / fold-reconcile),代码与你的描述一致。链条:短请求 → syncBlocks(kernel/src/sync.ts:100-125)把 covered id 不在视图内的 block 置 active=false 但保留 block([Bug][Responses] Short wire requests expand past the fold-reconcile guard and reap main-session blocks #3844/[PR] fix: preserve main folds across expanded short Responses requests #3845 的设计行为)→ 随后全量历史规范化重发到达,reconcileFoldCoverage(src/fold-reconcile.ts:1504-1509)的输入覆盖集来自 coveredRealHistoryIds(src/session.ts:1666-1675),它跳过 inactive block → covered.size === 0 → early return → order-centric claim ladder(公告:release v0.1.193 修复dsh 切模型导致上下文暴涨问题,修复fork问题,修复claude codex dsh等自动压缩导致会话破坏问题 . 另外修复几十个bug. 我本地场景有限,未必都测全,麻烦大家小场景先测试下是否有问题.任何问题及时反馈. #2821/fix(dsh): model switch drops all folds — fold-reconcile #2396 detect-only cannot re-anchor rewritten tool-call ids → doom-probe 502 session death #2915)根本没跑 → block 的 effectiveMessageIds 永远钉在旧 id 上 → 后续每轮 syncBlocks 都找不到旧 id → 覆盖永久丢失。你的四组对照自洽:exact 重发能恢复因为旧 id 仍有效;规范化重发只在 block 仍 active 时能恢复(对照组 1);真编辑诚实失配(对照组 4)。
    2. 层次判断:根因不是「短请求失活 block」(那是合法的临时态),而是恢复机制只接受 active 覆盖作为输入——失活是 syncBlocks 维护的临时标志,跨 id churn 的身份重键是 reconcile 的职责,两者被 active 过滤器切断,「已失活 + id churn」这个角落正好落在洞上。
    3. 方案评估:你的约束成立(不盲目恢复所有 inactive 块、不把 prepare 搬到 gate 前、真编辑/删除必须诚实失配、kernel 边界不动)。我的方案:把保留的可恢复 inactive block(view-deactivated)的覆盖并入 reconcile 的候选集,交给既有 claim ladder 逐格多证据判决(位置指纹 / simhash / 归一化身份——单空格变双空格对三者都不可见,三重佐证);expanded(用户手动展开)与 consumed 死谱系继续排除,避免 分析缓存泄露问题 #2293 的 missing 膨胀复发;能佐证的格 re-key 后由后续 syncBlocks 自然重新激活,不能佐证的保持诚实失配,走既有 z-streak reap / epoch verdict 完成生命周期——不开新的盲目恢复通道。改动集中在 src/fold-reconcile.ts,kernel 不动。

    正在实施:先在你的四组对照 HTTP 测试上对当前 master 跑出红基线,再修复 + 翻转受影响断言 + 补单元级恢复/诚实失配钉,然后提 PR。

  3. ranxianglei commented on Oct 21, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    修复已提交:#4085 (Closes #4050,待 CI)

    结构化八问

    1. 是什么问题:短请求把已有 fold 临时失活后(设计如此),主历史再回来时——哪怕内容逐字等价、只是空白差异导致 id 全部重铸——压缩覆盖永远回不来:摘要从出站请求里消失,被折叠的原文每轮全量重发。
    2. 什么原因导致的:恢复机制 reconcileFoldCoverage 构建候选集时只收「活跃块」(coveredRealHistoryIds 跳过 inactive)。短请求让 kernel 失活块之后,候选集为空 → 整个恢复流程提前返回 → 旧 id 永远无法重新对齐 → kernel 的 syncBlocks 也再找不到它们。根因是候选口径把「可恢复的谱系」排除在外,不是某个判据通道出错——你说的两个对照(无短可恢复、精确重发可恢复)恰好证明材料充分、只差这一步。
    3. 影响范围与严重程度:Responses/OpenAI 形态车道上「短辅助请求 + 客户端重序列化」的会话(如 Codex Desktop 多图像会话)。严重度为缓存泄露类 p1:字段证据显示单请求输入 230K→591K tokens、缓存命中 99%→7%、出站 35.5→102 MiB,且每轮持续恶化。
    4. 怎么修复的:把「失活但保留」的谱系作为恢复候选喂回既有的 claim ladder(活跃覆盖先插入,锚点上限的存活顺序不变);每个 cell 由多通道佐证独立裁决(位置指纹 / 规范化身份 / simhash≤8 / 顺序括号),有佐证的 cell 被重铸键,随后 kernel syncBlocks 在下一轮管道中自然重新激活;无佐证的 cell 保持诚实失配,走既有生命周期(z-streak reap / 截断抑制 / epoch 判决)。kernel 未动,无任何盲目恢复。
    5. 是否有回归问题:审过该路径承担的既有行为——活跃 fold 的对齐流程(输入顺序不变)、fold-reconcile drift 告警在客户端侧删除历史场景下持续误报(计数含死块、#2193 升级后不抑制、措辞误导) #2297 死谱系排除(coveredIdsOf 保持 active-only 语义)、[cache-ledger] netSaved 面板在 fold 覆盖坍缩后继续累计虚增——ledger 与实际重发行为无交叉校验 #2202 侧车请求门(仍跳过短 pass)、[Bug][proxy] unframed host-side history deletion leaves zombie fold blocks rendering carriers forever — one carrier flaps in/out every turn (persistent prefix-cache churn) #2695 reap 生命周期、Snapshot as session reconstruction: identity claim order takes priority, message hash demoted to reference signal (root- | 快照即会话重构:身份认领顺序优先,消息哈希降级为参考信号(根治 #3588/#3640 一类误判) #3641 epoch 判决——均保持。本地定向套件 138/138 绿 + typecheck 干净;全量以 CI 为准。
    6. 是否破坏缓存:不。本修复恢复的是前缀缓存接缝本身(摘要载体回到线上),未改动任何缓存/重试/游标/去重策略——修的正是缓存命中崩坏。
    7. 是否改了设计方向:没有。仍是「顺序为中心、hash 为辅助」+ 逐 cell 佐证;只是把一条此前缺失的谱系喂进既有阶梯,未引入新决策路径。
    8. 是否引入了新的配置:无。

    行为变更披露(有意变更,均在 PR 描述中有完整表格):

    • a. 全部失活的会话现在会跑完整 reconcile pass(原为直接 noop)——成本等级等同任何有 ≥1 活跃 fold 的会话;侧车请求门仍会跳过短 pass。
    • b. 此类会话上的短 pass 不再重置漂移状态(reset 保留给真正的 episode 边界——与门处注释的既定意图对齐)。
    • c. 同会话 id 整体换史且所有 fold 失活时,连续 3 次 pass 后会进入 epoch 确认(+错误日志),原来是静默 noop——与有活跃 fold 时的行为一致。
    • d. 失活块中「确已消失」的已验证跨度,现在会在约 3 个 pass 内被 z-streak reap 排干(原来永久僵尸保留)——repair 模式、仅 proxy 车道,plugin 模式不受影响。
    • e. fold-reconcile drift 告警在客户端侧删除历史场景下持续误报(计数含死块、#2193 升级后不抑制、措辞误导) #2297 单测夹具精化:死谱系块显式标 expanded: true(同一排除语义,分类更锐利)。

    测试证据:issue 中的复现已固化为端到端 HTTP 回归 tests/issue4050-short-replay.test.ts(真实代理 + loopback mock upstream)。修复前 short-normalized 场景断言的是 bug 行为(finalActive=0、摘要离线、原文上线、+17,017B);修复后该场景与无短对照逐字节一致(65,884B vs 65,884B),真正编辑的负向对照保持诚实失配(不盲恢复)。另有 4 个新单测钉住:churn 重铸双块、合法头截断永不 reap、中段编辑累积诚实漂移、consumed 子块 id 去重。

    判定:修复闭环,待 CI 绿后可合并。

  4. ranxianglei commented on Oct 21, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    CI 进展更新:PR #4085 全部静态门禁(kernel-guard / config-surface-guard / version-guard / docs-sync / lane-guard / lock-sync / lifecycle-ledger-guard …)+ 全部 e2e 车道(local-registry、codex-persona、native-opencode、fake-codex、native-dsh、pi-codex-ws、native-pi)通过。ubuntu-22 两个 test shard 红——排查结论:三处失败全部是 master 存量问题,与本 PR diff 无关(本 diff 不碰 verif/ 与 kernel/):

    失败 根因
    verif-wire-anthropic-roundtrip 2 项 ENOENT 测试文件里提交进了开发机绝对路径 /home/dog/tmp/bili-verif2/kernel/src/wire/anthropic.ts(google rebuild 同文件同病灶)→ 任何 runner 上都读不到源文件
    verif-view-latch-discipline 1 项 ERR_TEST_FAILURE 无求解器时直接抛错而不是 skip,违反 KDD #14「无求解器环境自动 skip 不会红」契约——该文件缺了其余九个 verif 测试都有的 no-solver 守卫

    已对照 master 自身 CI 证实是存量:master 最近推送(56a0f5a,即本分支的 base)的 CI run 39528617351 同样红——ubuntu-22 shard2 同样的 /home/dog ENOENT、shard3 同样的 No module named 'cvc5'。

    顺手修掉了(独立标注 commit 354240b98,CI 红类小修按「当前 PR 顺手修」纪律处理,不另开 issue):

    1. 两处硬编码绝对路径改为仓库相对解析 new URL("../kernel/src/wire/*.ts", import.meta.url)——与其他 verif harness 既有模式一致;
    2. 给缺守卫的两个 verif 测试加上模块级求解器探测 + { skip: !haveSolver }——照抄同族 google 文件的现成写法,未发明新机制。

    双向本地验证:有求解器环境 8/8 全跑全过;模拟无求解器环境(CVC5_HOME/Z3_BIN 指向不存在路径,即 ubuntu-22 runner 的实际状态)5 个求解器用例 SKIP、3 个真代码基线照过。已 push,等这轮 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