qwen-code/docs/users
顾盼 365409366d
refactor(core)!: replace tail-preservation compaction with summary + restoration attachments (#4599)
* refactor(core): rewrite compression prompt to 9-section claude-code-style format

Replaces the <state_snapshot> XML template with a numbered 9-section
structure that mandates verbatim preservation of user messages, including
the historical chronological list (section 6). The new format is
designed to pair with post-compact file/image restoration (separate work)
so the agent can resume long single-turn tasks without losing intent.

* refactor(core): align compaction trigger string with new 9-section prompt

The user-turn trigger injected after the system prompt still said
'generate the <state_snapshot>' from the old XML prompt era. Updated to
'produce the 9-section summary' to match Task 1's new prompt format.

Also tightens the prompt test to assert the specific user-message
verbatim mandate (not just the word 'verbatim' anywhere) so a future
regression that drops the mandate won't silently pass.

* feat(core): add postCompactAttachments module with file path extractor

extractRecentFilePaths walks history newest-first and returns the top N
unique file paths touched by read_file/write_file/edit/replace tool calls.
Pure function, no side effects, no state cache — readiness for the next
compaction-rewrite tasks.

* refactor(core): simplify extractRecentFilePaths internals

Three small cleanups from code review:
- Map<string, number> -> Set<string> (the index value was never read)
- Guard against maxFiles <= 0 explicitly (avoids returning 1 result
  when caller passes 0 as a 'disable' sentinel)
- Document 'replace' as a legacy alias for 'edit' so a future cleanup
  pass does not delete it as apparent dead code

Adds one test covering the maxFiles=0 path.

* feat(core): add image extractor with source-tool metadata

extractRecentImages walks history newest-first, collects up to N image
inlineData parts, and attributes each one to the model+functionCall that
preceded it (when one exists). Returns chronological order so callers
can render a meaningful 'last visual state ends here' strip.

* feat(core): add size-adaptive file reader for post-compact restore

readFileSizeAdaptive reads a file and returns one of: embed (full content
for files ≤ maxTokens × 4 chars), reference (path-only for large files),
missing (deleted since last touch), or binary (non-text content). The
embed/reference distinction mirrors claude-code's compact_file_reference
vs file attachment behavior, but without introducing new message types.

* refactor(core): harden readFileSizeAdaptive size accounting

Three corrections from code review:
- Import CHARS_PER_TOKEN from tokenEstimation.ts (canonical) instead of
  redeclaring locally, preventing silent drift between modules.
- Compare decoded character length, not raw byte length, against the
  cap. Otherwise a 10k-char Chinese file would be ~30k bytes and would
  be mis-classified as 'reference' despite fitting the budget.
- Rename FileReadResult -> FileEmbedResult to avoid a name collision
  with the unrelated FileReadResult interface in fileUtils.ts.

Adds a CJK-text test that catches the byte/char regression.

* feat(core): add file restoration block composer

buildFileRestorationBlocks reads each candidate file, classifies it as
embed/reference/missing/binary, and emits one consolidated reference
block (path-only list) plus one user message per embedded small file.
Total embed size is capped at POST_COMPACT_TOKEN_BUDGET; over-budget
files downgrade to reference.

* test(core): make budget test actually exercise the downgrade path

The previous version of this test wrote 3 files totalling 9k chars
against a 200k char budget. The assertions trivially passed regardless
of whether the budget check existed in the implementation.

The new version writes 11 files of 20k chars (each at the per-file cap)
so the budget is exhausted by the 10th and the 11th must downgrade
from embed to reference. Asserts both: file 11 appears in the reference
block, and file 11's content does NOT appear in any embed block.

* feat(core): add image restoration block composer

buildImageRestorationBlock emits a single user message whose first part
is a metadata header (turn index + source tool name + args per image),
followed by the inlineData parts themselves. Handles user-paste images
(no source tool) by labeling them as 'user-provided'.

* feat(core): add composePostCompactHistory orchestrator

Assembles the full post-compact history in order:
  summary → model ack → file references → file embeds → image block.

Each section is built by the per-concern extractors and builders added
in previous tasks. This is the single integration point that
chatCompressionService.compress() will call once the wire-up task lands.

* feat(core)!: rewrite compress() to claude-code-style full-history model

Replaces the split-point + tail-preservation model with full-history
compression + composePostCompactHistory. The entire curated history is
sent to the summary side-query, and the post-compact history is
assembled by the new composer (summary + ack + file restores + image
restore).

BREAKING: the previously-exported findCompressSplitPoint,
splitPointRetainingTrailingPairs, COMPRESSION_PRESERVE_THRESHOLD, and
TOOL_ROUND_RETAIN_COUNT will be removed in the next commit. Tests that
exercise them remain failing temporarily.

* chore(core): remove obsolete split-point compression infrastructure

Deletes findCompressSplitPoint, splitPointRetainingTrailingPairs,
COMPRESSION_PRESERVE_THRESHOLD, MIN_COMPRESSION_FRACTION, and
TOOL_ROUND_RETAIN_COUNT, plus the tests that exercised them. The new
behavior is covered by composePostCompactHistory and its unit tests.

Also cleans up:
- Stale orphan-strip comment in compress() that described the deleted
  manual-trigger orphan-funcCall handling.
- TEST_ONLY.COMPRESSION_PRESERVE_THRESHOLD hatch in client.ts.
- Docstring references in config.ts and compactionInputSlimming.ts.

* test(core): add single-turn computer-use compaction regression

Reproduces the scenario the rewrite targets: one user prompt kicks off
many screenshot tool calls. Asserts that (a) the user prompt is carried
into the summary verbatim and (b) the 3 most recent screenshots are
restored as an image block with source-tool metadata. This is the canary
test for the computer-use UX claim made in the design discussion.

* docs(core): remove stale "split point" references in tokenEstimation comments

Aligns the docstrings with the new compose-based compression flow. The
"split point" and "splitter" concepts no longer exist after the rewrite.

* fix(core): iterate parts reverse so parallel tool calls keep the last N

Real-session E2E surfaced a bug: a model that issues N parallel ReadFile
calls puts all N functionCall parts in ONE model+fc content. The
extractor's outer history walk is newest-first, but the inner parts
walk was forward — so for a 6-parallel batch hitting the cap of 5,
the FIRST 5 parts won and the actually-most-recent (last-listed) file
was dropped.

Fix: walk parts in reverse within each content. Applied symmetrically
to extractRecentImages (same shape, even rarer trigger).

Adds a regression test that hits a 6-parallel batch.

* fix(core): code-review fixes — fence escape, path sanitize, alias removal

- CommonMark-safe fence in file embed blocks. The old 3-backtick fence
  closed prematurely when a file's content contained a triple-backtick
  run (Markdown, CLAUDE.md, JSDoc with code examples) — leaking the
  remainder as unfenced text. Now uses a fence one longer than the
  longest backtick run in the content.

- Strip control characters (\r, \n, \t) from file paths before
  rendering into attachment markdown. Paths come from model-controlled
  history; a \n could inject markdown structure. The actual path stays
  intact for tool calls — only the displayed string is sanitized.

- Remove the historyForCompression alias for curatedHistory in
  compress(). The alias was added as a comment anchor during the
  rewrite but didn't carry semantic information.

* refactor(core): rewrite compression prompt to <state_snapshot> XML with 9 claude-aligned sections

Replaces the 9-section numbered-text prompt with qwen-code's original
<state_snapshot> XML envelope, but with the 9 inner section tags
content-aligned to claude-code:
  <primary_request_and_intent>
  <key_technical_concepts>
  <files_and_code_sections>
  <errors_and_fixes>
  <problem_solving>
  <all_user_messages>
  <pending_tasks>
  <current_work>
  <next_step>

Also:
- <scratchpad> -> <analysis>, stripped by postProcessSummary (saves
  ~600-800 tokens of CoT noise per compaction).
- "Resume directly..." trailer moved out of the prompt body and into
  postProcessSummary (no longer re-generated by the model every
  compaction; lives once in code with our own wording).
- Section 6 verbatim-policed mandate relaxed to "chronological, include
  short messages like 'ok' / 'continue'" — matches claude-code intent
  without forcing the model to literally copy long user messages.

E2E (qwen3.6-plus, 6 substantial .ts files + thorough analysis):
  raw history 6508 -> summary 1513 (after strip ~947), 38% history
  compression. Overall context 24642 -> 20647 reported (-16%), with
  another ~664 tokens actually saved by the post-strip but not
  reflected in the conservative token-math heuristic.

* docs(core): code-review polish on XML prompt rewrite

Four small follow-ups from review of 641a0eadd:

- prompts.ts: rewrite getCompressionPrompt's stale JSDoc — it still
  described the deleted 9-section numbered-text format and the
  verbatim mandate that was relaxed.
- chatCompressionService.ts: clarify the token-math comment so it's
  obvious the ~1000 token deduction covers the full compression
  system prompt + kick-off user turn (not any single instruction)
  and that newTokenCount slightly over-counts because <analysis>
  gets stripped by postProcessSummary downstream.
- postCompactAttachments.ts: add a NOTE comment on the <analysis>
  strip regex covering its strict-tag-match assumption and
  multi-block / non-greedy semantics.
- postCompactAttachments.test.ts: replace the four lazy
  `await import('./postCompactAttachments.js')` calls inside the
  postProcessSummary describe block with one top-level static import
  — consistent with how every other describe in the file imports.

* docs(core): drop stale duplicate sentence left in token-math comment

* fix(core): address wenshao review on PR #4599 (correctness + security + ergonomics)

Seven follow-ups from wenshao's review of the compaction rewrite.

Critical:
- newTokenCount now includes restoration-block tokens via
  estimateContentChars over extraHistory[2..]. Previously the formula
  only counted side-query output, so up to 5 × 5K (files) + 3 × image
  tokens were missing — letting the inflation guard miss and the
  cheap-gate under-estimate the next prompt size (Finding 1).
- composePostCompactHistory now merges every file restoration block
  and the image block into a single user Content following the model
  ack. The previous output had consecutive user roles, which
  geminiChat.test.ts:6289 enforces against and Gemini providers
  reject with 400 "consecutive same-role content" (Finding 2).
- Preserve a trailing model+functionCall through compaction so a
  pending functionResponse (sitting in sendMessageStream's
  pendingUserMessage) has a matching call. Without this, hard-rescue
  auto-compaction mid tool-use loop produces a user+functionResponse
  with no preceding model+functionCall → API 400. This restores the
  protection the split-point in-flight fallback used to provide.
  When the funcCall lands without attachments it folds into the
  ack's own model Content to avoid model→model adjacency (Finding 3).
- composePostCompactHistory now takes an optional workspaceRoot and
  silently skips file paths that resolve outside it.
  extractRecentFilePaths picks up paths from model functionCall args
  regardless of whether the tool execution succeeded; without a
  boundary check, an adversarial model that issued
  read_file('/etc/passwd') — denied by the permission system —
  would still have its path extracted and re-read into the next
  prompt. compress() passes config.getTargetDir() as the boundary
  (Finding 4).

Suggestions:
- composePostCompactHistory + buildFileRestorationBlocks +
  readFileSizeAdaptive all take optional AbortSignal and short-
  circuit / pass it to readFile's { signal } option. Cancelled
  compactions stop on the next file read (Finding 5).
- postProcessSummary fallback no longer re-injects the raw
  <analysis> block when the strip leaves nothing. The new
  stripAnalysisBlock helper runs the closed-tag strip AND an
  unclosed-tag strip (handles 'model ran out of output tokens
  before closing'). If both leave nothing, postProcessSummary
  emits '[Summary unavailable]' rather than leaking scratchpad
  (Finding 6).
- firePostCompactEvent now receives stripAnalysisBlock(summary) so
  hook consumers see the same text that lands in history. The
  resume trailer stays out of the hook payload — that's wrapper
  decoration for the next agent turn, not state for consumers
  (Finding 8a).

Docs:
- Update the geminiChat.ts comment around `trigger: 'auto'` to
  describe what the trigger actually does post-refactor (hook event
  categorization) rather than the deleted manual-only orphan-strip
  it used to guard against (Finding 8b).

Regression tests cover all six fixable code-path changes
(role alternation, trailing funcCall preservation, workspace
boundary, abort propagation, closed-tag fallback strip, unclosed-tag
fallback strip).

* fix(core): add getTargetDir to geminiChat auto-compression test mock

The R3.4 end-to-end auto-compression test drives the real
ChatCompressionService, which reads config.getTargetDir() for the
post-compact file-restoration workspace boundary. The geminiChat mock
config lacked getTargetDir, so the test threw "config.getTargetDir is
not a function" on CI. Add the mock to unblock the failing Test jobs.

* feat(core): configurable compaction retention + computer-use screenshot trigger

Add four env-overridable chatCompression settings (priority env >
settings > default):
- maxRecentFilesToRetain    (QWEN_COMPACT_MAX_RECENT_FILES,     default 5)
- maxRecentImagesToRetain   (QWEN_COMPACT_MAX_RECENT_IMAGES,    default 3)
- enableScreenshotTrigger   (QWEN_COMPACT_SCREENSHOT_TRIGGER,   default true)
- screenshotTriggerThreshold(QWEN_COMPACT_SCREENSHOT_THRESHOLD, default 50)

The screenshot trigger fires auto-compaction once tool-returned images
accumulate to the threshold even when token usage is below the auto tier,
so computer-use sessions don't drown the model in stale screenshots. It
counts only images nested in functionResponse.parts (tool results), not
user pastes, and runs only in the would-be-NOOP path when enabled.

Fix a latent bug surfaced while wiring the trigger: extractRecentImages
only inspected top-level inlineData parts, but convertToFunctionResponse
nests tool media under functionResponse.parts — so post-compact
restoration recovered ZERO tool screenshots in real sessions, while unit
tests stayed green against a fabricated top-level shape. It now walks both
shapes; the image counter and tests use the real nested shape.

Remove the now-defunct contextPercentageThreshold deprecation warning (the
field was already dropped from ChatCompressionSettings) and its tests, and
document the four new settings.

* test(core): assert screenshot trigger can't re-fire post-compaction; fix misleading docs

Code-review follow-up. The screenshot trigger counts only images nested in
functionResponse.parts. Compaction replaces those with the summary and
re-embeds survivors as TOP-LEVEL parts in the restoration block, which the
counter ignores — so the tool-image count always resets to ~0 and the
trigger cannot immediately re-fire, independent of maxRecentImages.

The resolveCompactionTuning JSDoc and the settings.md note previously warned
of a non-existent "maxRecentImages near threshold => compact every turn"
loop. Correct both, and add a regression test asserting
countToolResponseImages() is 0 on composePostCompactHistory output.

* fix(core): guard readFileSizeAdaptive against multi-GB reads; cover composer 4-entry branch

wenshao review round 2 on PR #4599.

- readFileSizeAdaptive now stats the file first and short-circuits to a
  reference when its byte size exceeds maxChars*4 (the safe UTF-8 upper
  bound — a file larger than that cannot fit within maxChars chars). This
  stops a multi-GB file the agent previously touched from being slurped
  into a Buffer and exhausting the heap mid-compaction, exactly when we're
  trying to reduce memory. A large binary file now references rather than
  reading to binary-detect.

- Add a test for composePostCompactHistory's 4-entry branch (attachments +
  trailing model+functionCall) producing [user(summary), model(ack),
  user(attachments), model(fc)]. This is the common mid-tool-loop
  compaction case; a model->model adjacency here is a provider 400. Prior
  tests only covered the 2-entry fold (no attachments) and 3-entry (no
  trailing fc) shapes.

* fix(core): resolve symlinks in workspace boundary; guard compose against throws

wenshao review round 3 on PR #4599 (two Criticals).

- isInsideWorkspace now resolves symlinks via realpathSync (safeRealpath,
  with a lexical fallback for non-existent paths). A symlink living inside
  the workspace but pointing outside (e.g. workspace/.env -> ~/.ssh/id_rsa)
  previously passed the lexical boundary check and had its target read and
  embedded into the post-compact history sent to the provider. Added a
  RED-verified security regression test (secret embedded under the old
  lexical check; rejected under realpath).

- Wrap composePostCompactHistory in try/catch inside compress(). The
  summary side-query has already succeeded at that point, so a
  restoration-assembly throw (disk I/O / malformed history) previously
  escaped to sendMessageStream, crashing the active turn AND bypassing the
  COMPRESSION_FAILED breaker. It now degrades to summary + ack.

* fix(core): close 4 compaction Criticals from review round 4

wenshao review round 4 on PR #4599.

- isSummaryEmpty now checks the STRIPPED summary: a response that is only an
  <analysis> block (no <state_snapshot>) strips to empty, so it takes the
  COMPRESSION_FAILED_EMPTY_SUMMARY path instead of "succeeding" with
  `[Summary unavailable]` as the agent's only context (silent amnesia).
- Manual /compress strips a trailing ORPHANED model+functionCall before
  composing — it has no pending functionResponse, so preserving it would
  emit model[fc] then the next user text turn -> API 400. Auto-compaction
  still keeps it (the pending response pairs with it).
- The restoration-failure catch fallback now folds a trailing
  model+functionCall into the ack turn, so a pending functionResponse
  (auto mid-tool-loop) keeps its matching call even on the degraded path.
- extractRecentFilePaths skips file paths whose tool call FAILED (an error
  functionResponse), so a denied read_file is never re-read off disk during
  compaction — closing a permission-bypass side channel.

RED-verified regression tests for the empty-summary, orphan-strip, and
permission-bypass fixes. Corrected the postProcessSummary comment.

* test(core): cover composePostCompactHistory catch-fallback; document fold text drop

wenshao review round 5 on PR #4599.

- Regression test for the restoration-failure catch fallback: mock
  composePostCompactHistory to reject and assert compaction still returns
  COMPRESSED (no escape to sendMessageStream / breaker bypass) with the
  trailing functionCall folded into the ack and the trailing text dropped.
- Document that the fold branch intentionally keeps only functionCall parts
  (the trailing turn's text is already captured in the summary); the
  asymmetry with the with-attachments branch is deliberate.
2026-05-29 16:20:13 +08:00
..
configuration refactor(core)!: replace tail-preservation compaction with summary + restoration attachments (#4599) 2026-05-29 16:20:13 +08:00
extension chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
features feat(channels): add Feishu (Lark) channel adapter (#4379) 2026-05-28 20:11:00 +08:00
ide-integration update documentation 2025-12-19 18:16:59 +08:00
reference feat(cli): readline Ctrl+P/N for history and selection navigation (#4082) 2026-05-16 23:07:25 +08:00
support docs: update authentication methods to reflect OAuth discontinuation (#3325) 2026-04-17 15:34:18 +08:00
_meta.ts feat(cli,sdk): qwen serve daemon (Stage 1) (#3889) 2026-05-13 14:47:47 +08:00
common-workflow.md docs: updated all links, click and open in vscode, new showcase video in overview 2025-12-17 11:10:31 +08:00
integration-github-action.md docs: updated all links, click and open in vscode, new showcase video in overview 2025-12-17 11:10:31 +08:00
integration-jetbrains.md docs(integration): use CDN URLs for images and fix formatting 2026-03-16 14:12:48 +08:00
integration-vscode.md fix: docs 2026-01-14 10:30:03 +08:00
integration-zed.md docs(integration): use CDN URLs for images and fix formatting 2026-03-16 14:12:48 +08:00
overview.md feat(installer): add standalone archive installation (#3776) 2026-05-11 13:25:48 +08:00
quickstart.md chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
qwen-serve.md feat(serve): MCP guardrail push events + hysteresis (#4175 Wave 3 PR 14b) (#4271) 2026-05-19 01:06:20 +08:00