Skip to content

[Bug]: decompress.toFile reads wrong message fields and falls back to summary instead of original text #445

Closed

Description

@jojoestar44444

Scope

Found during a read-only explanation of original-text recovery in local master checkout f6c70a7e (package.json version 1.18.2). No source changes were made. This is a content-recovery defect, separate from the toFile path/symlink safety work tracked in #415 / #416.

Actual behavior

fetchSessionMessages returns validated WithParts records shaped as { info: { id, role, sessionID, time }, parts: [...] } (lib/compress/search.ts:15-20, lib/messages/shape.ts:3-32). However:

  • extractMessageId reads top-level m.id or m.messageId, not m.info.id (lib/compress/decompress.ts:256-258).
  • extractMessageText reads top-level role/content/text rather than m.info.role and m.parts (lib/compress/decompress.ts:260-269).
  • The toFile branch filters fetched messages through the first helper, finds zero matches for normal SDK-shaped messages, then writes the block summary via its fallback while returning a successful export message (lib/compress/decompress.ts:321-337).

Thus an active block with retrievable original messages can export its summary instead of the original text. Fixing only ID selection would still leave empty text extraction.

Minimal reproduction and evidence

A dependency-free Node 24 check loaded the exact two production helper functions using node:module.stripTypeScriptTypes, and loaded the production shape filter. Fixture:

const message = {
  info: {
    id: "msg-raw-1", sessionID: "ses-repro",
    role: "assistant", time: { created: 1 }
  },
  parts: [{
    id: "part-1", messageID: "msg-raw-1", sessionID: "ses-repro",
    type: "text", text: "ORIGINAL DETAIL 42"
  }]
};

Observed:

{
  "acceptedByProductionShapeFilter": 1,
  "extractedId": "",
  "extractedText": "[unknown]\n\"\"",
  "matchedOriginalMessages": 0,
  "exportWouldFallBackToSummary": true
}

Filtering by m.info.id matched the same message. The standard test suite was not run because this checkout has no installed tsx; the isolated helper reproduction does not require project dependencies and performed no file writes.

Expected behavior / suggested fix

Resolve original IDs from info.id and serialize role/content from info.role plus message parts, including relevant text and tool output. Add an active-block toFile integration test with real SDK-shaped history and assert the exported text includes an original-only sentinel absent from the summary. Cover mixed text/tool parts and distinguish a true missing-original fallback from successful raw-content export.

The ordinary context-restoration path is separate (deactivateCompressionTarget + syncCompressionBlocks) and is not what this report reproduces.

Activity

  1. ranxianglei commented on Sep 20, 2026

    @ranxianglei
    Owner

    继续

  2. ranxianglei commented on Sep 20, 2026

    @ranxianglei
    Owner

    重启了继续

  3. ranxianglei commented on Sep 20, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Confirmed and fixed — thanks for the precise report.

    Verified against origin/master (f6c70a7, v1.18.2). extractMessageId read top-level m.id / m.messageId, which is always empty on SDK-shaped WithParts records ({ info, parts }), so the toFile filter matched zero originals; extractMessageText had the same top-level-field bug and would have produced empty text even if IDs had matched. Both helpers were wrong, so fixing only ID selection would indeed have been insufficient — your call on that point was correct.

    Fix (lib/compress/decompress.ts):

    • extractMessageId → m?.info?.id (matches canonical access used elsewhere, e.g. lib/compress/search.ts)
    • new module-private serializeMessagePart(part): text parts verbatim; tool parts as [name] input/output reusing extractToolContent from lib/token-utils (kept in sync with token counting); structural parts (reasoning / step markers) skipped without throwing
    • extractMessageText → [role]\n<segments> built from info.role + serialized parts
    • the toFile branch now distinguishes the two outcomes and reports which happened: raw export → "N original message(s)"; genuine missing originals (inactive/consumed block) → "no original messages found; wrote block summary (N block(s))". Block stays compressed either way.

    No state-format / config-schema / persistence / public-API change; no new dependencies.

    Tests: added E2E regression suite tests/active-block-decompress-tofile.test.ts (real SDK-shaped history, active block, sentinel-based raw-vs-summary assertion, mixed text/tool parts, plus a genuine-missing fallback case). Per the test-review requirement I verified it fails against the unfixed code (stashed the fix → 0 pass / 2 fail) and passes with the fix. Full suite: 1306 tests / 1304 pass / 2 fail — the 2 failures are pre-existing sandbox artifacts (read-only /tmp: soft-block.test.ts EACCES on mkdir /tmp/…, and inactive-block-decompress.test.ts hardcoding a /tmp/… path) that fail identically on clean master, so they are not regressions. Typecheck clean; diff kept minimal (no unrelated Prettier reflow — see the Prettier-version-drift note in the devlog).

    PR: #448 — Closes #445. Kept separate from #415 / #416 (toFile path/symlink safety) as scoped in the report. Merge is yours to do when ready.

    中文:修复了 decompress.toFile 因读取错误字段(顶层 m.id/m.content)而对活跃块导出摘要而非原文的缺陷——ID 改读 info.id、正文按 info.role+parts 序列化并复用 extractToolContent,同时让返回信息区分"成功导出原文"与"原文缺失回退到摘要";新增 E2E 回归测试并验证其对未修复代码会失败,全量测试无回归(仅 2 个与本改动无关的沙箱 /tmp 只读环境问题),可以合并。

  4. added a commit that references this issue on Sep 20, 2026
  5. added a commit that references this issue on Sep 21, 2026
    5eb7b46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions