You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: decompress.toFile reads wrong message fields and falls back to summary instead of original text #445
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:
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.
[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.
Activity
ranxianglei commentedon Sep 20, 2026
继续
ranxianglei commentedon Sep 20, 2026
重启了继续
ranxianglei commentedon Sep 20, 2026
[bot] 🏷 Confirmed and fixed — thanks for the precise report.
Verified against origin/master (f6c70a7, v1.18.2).
extractMessageIdread top-levelm.id/m.messageId, which is always empty on SDK-shapedWithPartsrecords ({ info, parts }), so the toFile filter matched zero originals;extractMessageTexthad 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)serializeMessagePart(part): text parts verbatim; tool parts as[name] input/outputreusingextractToolContentfromlib/token-utils(kept in sync with token counting); structural parts (reasoning / step markers) skipped without throwingextractMessageText→[role]\n<segments>built frominfo.role+ serializedparts"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.tsEACCES onmkdir /tmp/…, andinactive-block-decompress.test.tshardcoding 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 只读环境问题),可以合并。
Merge pull request #448 from ranxianglei/2026-09-20_decompress-tofile…
fix: decompress.toFile exports original text, not the summary