* feat(external-context): add submitted-prompt auto recall
Add an opt-in Hook-only profile that derives bounded retrieval queries from submitted prompt provenance while preserving the existing on-demand MCP contract.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(external-context): harden auto recall query sanitization (#7877)
Address review feedback on the submitted-prompt auto recall Hook:
- Bound the sanitizer input before the redaction regexes run so a
worst-case prompt cannot drive the assignment regex into quadratic
backtracking that blocks the event loop past the wall-clock budget.
- Test the whole assignment for secret names so a leading label such as
"Deploy failed:" can no longer claim the match and leak an api_key=.
- Skip the interactive E2E under container sandboxes (docker/podman),
matching the cron-interactive precedent.
- Restore real undici coverage for a malformed proxy environment value.
- Make the wall-clock-budget test exercise the internal timer rather than
the provider timeout, and give the backtracking regression test a shape
that actually backtracks.
- Clarify that the v2 top-level timeoutMs applies only to the on-demand
MCP path, and note session-lifetime context accumulation in the design
doc.
* fix(external-context): complete secret redaction, guard MCP config version (#7877)
Anchor the secret keyword to the name that owns the separator so a leading
prose label can no longer claim the match. This redacts spaced separators
(api_key = sk-...) and inline JSON ({"api_key": "..."}), and stops
over-redacting ordinary prose such as "readme: token refresh flow".
Also reject non-version-1 configs in runMcp with a clear startup error so an
auto-recall (v2) config cannot silently expose a second retrieval surface.
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* fix(integration): use lenient assertion and harden poll in interactive file-system test (#7111)
The interactive read-then-write test used strict equality
(trimEnd() === '1.0.1') to verify the file update, but the model may
write additional text beyond just '1.0.1'. Switch to .includes('1.0.1')
to match the lenient assertion used by the non-interactive sibling test
(file-system.test.ts uses .toContain('1.0.1')).
Also make the TestRig.poll() method catch predicate exceptions so
transient errors (e.g. readFileSync throwing during a file write) are
treated as 'not yet true' and retried, rather than crashing the test.
* fix(integration): log predicate exceptions in poll helper under VERBOSE (#7111)
* fix(integration): surface last predicate error on poll timeout (#7111)
* fix(integration): clear stale lastError when poll predicate succeeds (#7111)
* fix(integration): rephrase channel-plugin test prompts to avoid model safety triggers (#7111)
* fix(integration): preserve lastError on poll failure and log full stack (#7111)
---------
Co-authored-by: Qwen Autofix <qwen-autofix@alibaba-inc.com>
Co-authored-by: Qwen Code <qwen@qwen-code.ai>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(integration): harden flaky interactive read-then-write test
The interactive read-then-write test read version.txt once immediately
after the write/edit tool call was logged and asserted strict equality
with '1.0.1'. This was flaky in two ways: the model may append a
trailing newline ('1.0.1\n'), and the file may not yet reflect the new
version when the tool call is observed (e.g. a first edit whose
old_string did not match is still logged by name and retried within the
turn). Poll the file until it contains '1.0.1' instead, matching the
lenient assertion already used by the non-interactive sibling test.
* test(integration): tighten interactive write polling
---------
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
* feat(cron): add deterministic test seam for cron-interactive E2E
Add forceFireJob(id) method and QWEN_CODE_TEST_CRON_FAST env var to
CronScheduler. When enabled, newly created session-only jobs auto-fire
after 5s (configurable via QWEN_CODE_TEST_CRON_DELAY_MS) instead of
waiting up to 60s for the wall-clock minute boundary.
This removes the timing-flakiness from cron-interactive.test.ts
without changing any production behavior (seam is env-gated and
inactive by default).
Refs #6982
* test(cron): use QWEN_CODE_TEST_CRON_FAST seam, reduce timeouts to 30s
Enable the CronScheduler test seam in cron-interactive tests via
QWEN_CODE_TEST_CRON_FAST=1 in makeEnv(). This makes newly created
cron jobs auto-fire after 5s instead of waiting for the wall-clock
minute boundary.
Reduce all waitForScreen timeouts from 90s to 30s since the fire is
now deterministic (~5s after job creation + model round-trip).
Refs #6982
The protocol-tags-interactive.test.ts started the fake OpenAI server
on 127.0.0.1 without Docker-aware host options, making it unreachable
from inside the Docker sandbox container. The CLI running in the
container tried to connect to 127.0.0.1 which resolved to the
container's own loopback, not the host where the test server listens.
Bind the fake server to 0.0.0.0 and advertise host.docker.internal
as the base URL host when QWEN_SANDBOX is docker or podman, matching
the established pattern in tool-control.test.ts. Also set NO_PROXY to
include host.docker.internal so the CLI does not route sandbox model
requests through an HTTP proxy.
Co-authored-by: qwen-autofix[bot] <qwen-autofix[bot]@users.noreply.github.com>
* fix(core): strip leaked protocol summary tags
* refactor(core): remove stale analysis block stripper
* test(cli): add protocol tag leak interactive mock
* docs: add protocol tag tmux evidence report
* test(core): cover tagged retry discard evidence
* test(core): cover protocol tag retry filtering
* test(integration): cover protocol tag filtering after retries
* test(core): trim protocol tag retry coverage
* test(integration): assert exact retry replay
* fix(core): sanitize protocol tags at chat stream boundary
* fix(core): harden protocol stream retry handling
* chore: drop unrelated merge formatting
* test(integration): cover hidden SSE disconnect retry
* test(core): harden sanitizer coverage and add review feedback
Address review suggestions:
- Add edge case tests: single-char chunk splitting, adversarial
prefixes (<analyze>, <suitor>), whitespace-only flush, reset+reuse,
nested analysis-inside-summary-inside-analysis
- Add inline comments on bidirectional prefix check in classifyTagStart
to prevent future maintainers from mistaking one direction for a typo
- Add debug log on first protocol tag suppression in stream processing
to aid diagnosing 'model response disappeared' incidents
* test(cli): isolate cli entry fallback fixture
* fix(core): harden protocol tag sanitization
* fix(core): preserve literal protocol tag mentions
* test(webui): stabilize heartbeat prompt cleanup test
* fix(core): preserve visible protocol tag literals
* fix(core): align streaming sanitizer with batch on summary-internal analysis
The streaming protocol-tag filter dropped visible text after a literal
`<analysis>` mention inside a visible `<summary>`, e.g.
`<summary>Fix: replace <analysis> tag ...</summary>` streamed as
`Fix: replace ` while the batch sanitizer correctly kept the literal
mention. Distinguishing a paired analysis block (hidden scratchpad) from
an unmatched literal opener needs look-ahead, which a per-char stream
cannot do inline.
Buffer the summary tail once an analysis-shaped token appears inside a
visible summary and resolve it through the same batch stripper, so the
streaming and non-streaming paths always agree.
* fix(core): stop unmatched "<" from breaking streaming tag detection
An unmatched '<' in prose (e.g. "3 < 5" or "if (x<3)") put the streaming
filter into literal-tag scanning that only ended at the next '>' — which
was the '>' of the tag closing the current protocol block. Inside a
summary this leaked the raw `</summary>`; inside an analysis block it
swallowed the visible answer that followed, so a valid response was
discarded and retried instead of surfaced.
Treat every '<' as the start of a fresh tag candidate while scanning
literal text, keeping the streaming and batch sanitizers aligned.
* fix(core): retry leaked protocol turns
* test(integration): isolate protocol retry scenario
* fix(cli): replace ✦ (U+2726) with ◆ (U+25C6) and add ∵/∴ thinking icons
- Replace ✦ with ◆ across all TUI components to fix East Asian
Ambiguous width misalignment (string-width reports 1 but terminals
render 2 columns).
- Use ∵ (because) during thinking streaming, ∴ (therefore) when
thinking is complete — matches the mathematical reasoning pair.
Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>
* fix(cli): reduce STATUS_INDICATOR_WIDTH from 3 to 2 after ◆ replacement
◆ (U+25C6) is a consistent width-1 character across all terminals,
so the tool status indicator no longer needs the extra column that
was reserved for the ambiguous-width ✦ (U+2726).
Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>
* fix(cli): catch missed ✦→◆ references in tests, docs, and scenarios
* fix(cli): shorten tmux spinner frames from 3 to 2 chars to match STATUS_INDICATOR_WIDTH=2
TMUX_SPINNER_FRAMES changed from ['. ', '.. ', '...'] to ['· ', '··']
to prevent 1-column overflow in tmux when STATUS_INDICATOR_WIDTH was
reduced from 3 to 2 after the ◆ replacement.
* revert(cli): keep narrow '.' tmux spinner frames instead of ambiguous '·'
'.' (U+002E) is Narrow (always 1 col), giving a guaranteed fixed-width
tmux spinner. '·' (U+00B7) is East Asian Ambiguous, so on ambiguous-width=2
terminals the frames become 3/4 cols and the spinner jitters — the opposite
of the "fixed-width frames" the surrounding comment promises.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Qwen Code <noreply@alibaba-inc.com>
Co-authored-by: pomelo.lcw <pomelo.lcw@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Graduate cron/loop from experimental opt-in to enabled-by-default.
Flip env var polarity from QWEN_CODE_ENABLE_CRON to
QWEN_CODE_DISABLE_CRON for users who want to opt out.
Update integration tests, docs, and VS Code schema accordingly.
* feat(cli,core): /compress accepts custom focus instructions
Extends /compress to take a trailing instruction string (max 2000 chars)
that is passed through tryCompressChat → tryCompress → CompressOptions
and appended to the compression side-query system prompt as an
"Additional Instructions:" block. Mirrors claude-code /compact <text>.
Empty / whitespace-only args fall back to the prior behaviour.
* test(core): cover /compress customInstructions + PreCompact hook merge
* feat(core): restore plan-mode + subagent snapshot after compaction
Adds two optional ComposePostCompactOptions:
- planModeActive: when true, emits a <plan-mode-active> reminder so the
post-compact agent does not forget destructive tools remain gated.
- runningSubagents: when non-empty, emits a <background-tasks> block
listing each running/paused task by id, status, and description.
Both blocks are spliced into the merged user attachment Content before
file/image restorations. XML-significant characters in descriptions are
escaped to prevent an adversarial subagent description from closing the
wrapper tag.
Wiring at the call site arrives in the next commit.
* feat(core): wire plan-mode + subagent snapshot into post-compact attachments
ChatCompressionService.compress now passes:
- planModeActive: derived from config.getApprovalMode() === ApprovalMode.PLAN
- runningSubagents: filtered from BackgroundTaskRegistry to agent-kind tasks
in 'running' or 'paused' state
into composePostCompactHistory. Adds collectActiveSubagents() helper that
returns [] when the registry is absent so older SDK consumers without it
keep working.
* fix(core,test): use HookSystem.getAdditionalContext accessor; smoke-test /compress
- chatCompressionService: read PreCompact hook output via
result.getAdditionalContext() — the wrapper returns DefaultHookOutput
(not the raw AggregatedHookResult), and the accessor sanitises < / >
consistently with every other call-site in the repo.
- Test mocks now return a DefaultHookOutput-shaped stub via a tiny
makeHookOutput() helper rather than the aggregator shape.
- New integration smoke test for `/compress focus on the scientist
mentioned` exercising the args plumbing end-to-end.
* test(core): update client.test.ts to match new tryCompressChat signature
* feat(core): cap subagent snapshot at 30 entries with overflow notice
Code-review follow-up. Pathological sessions with hundreds of
backgrounded agents could otherwise produce a multi-KB block. Newest 30
rows are kept (highest startTime); older ones are summarised on a
trailing line so the model knows the snapshot is partial.
* fix(core,test): flatten subagent description newlines; type-safe ApprovalMode in tests
Second code-review pass found two real issues:
1. Subagent descriptions containing `\n`/`\r`/`\t` would split across
multiple lines inside the `<background-tasks>` bullet list, letting
the second line read as a sibling row (or worse, an orphan paragraph
between two `- [..]` entries). Flatten whitespace before the slice so
each task stays on one line.
2. The plan-mode wiring tests passed `'plan'` / `'auto-edit'` as plain
strings instead of `ApprovalMode.PLAN` / `ApprovalMode.AUTO_EDIT`.
Source code compares against the enum; a future enum value change
would have silently passed the tests. Import and use the enum.
* fix(core): move PreCompact hook fire after length-guard; align plan-mode tool names
Round 3 code review surfaced two issues:
1. PreCompact hook fired BEFORE the curatedHistory.length < 2 guard, so
a single-message session would trigger any hook side effects
(transcript dump, external notification, etc.) and then NOOP. Move
the hook fire below the guard so hooks only run when compression is
actually possible. New regression test asserts the contract.
2. PLAN_MODE_REMINDER_TEXT said "shell mutations" but the real qwen-code
tool is `run_shell_command` (tool-names.ts:26). Use the verbatim
tool names so a future rename is grep-discoverable.
* refactor(core): share escapeXml, drive plan-mode names from ToolNames, extract reminder builder
Code-review follow-ups on post-compact attachments:
- Replace the local 3-char escapeForXmlText with the shared 5-char
escapeXml from utils/xml.ts, and apply it to the subagent id and
status as well as the description. Subagent ids derive from a
user-configurable subagentConfig.name, so an unescaped `<`/`&` there
could close the <background-tasks> wrapper or forge sibling markup.
- PLAN_MODE_REMINDER_TEXT now interpolates ToolNames.WRITE_FILE / .EDIT /
.SHELL instead of retyping the names, so a future rename stays in sync.
- Extract buildStateReminderParts() as the single source of truth for the
plan-mode + subagent reminder blocks, used by both composePostCompactHistory
and (next commit) its catch-fallback so the two paths can't drift.
* fix(core): scope subagent snapshot to backgrounded tasks; restore reminders on fallback; cap hook context
Three code-review fixes in the compaction service:
- collectActiveSubagents now also requires isBackgrounded — foreground
agents are the parent's synchronously-awaited tool call and don't belong
in a <background-tasks> roster. Mirrors getRunningBackgroundCount.
- The composePostCompactHistory catch-fallback re-applies the plan-mode +
subagent reminders via the shared buildStateReminderParts (pure, no I/O),
so a restoration failure no longer silently drops plan-mode enforcement
and the subagent roster.
- The PreCompact hook's additionalContext is capped at
MAX_HOOK_INSTRUCTIONS_CHARS before entering the side-query prompt,
closing the unbounded-input hole the user-text cap was meant to prevent.
The fallback and hook-cap fixes have RED-verified regression tests.
* feat(cli): warn on /compress instruction truncation; fix integration-test pty typing
- /compress now emits an INFO notice (interactive), a stream message (acp),
and a prefixed return message (non-interactive) when the instruction
string exceeds MAX_COMPRESS_INSTRUCTIONS_CHARS, so the silent 2000-char
clip is no longer invisible to the user.
- Annotate the three `ptyProcess.onData((data: string) => ...)` callbacks
in the compress integration test to clear the TS7006 implicit-any the
reviewer's typecheck flagged (fixed all three occurrences, not only the
one inside this PR's diff).
Cron prompts are rendered as `● Cron: …` notifications since the
distinct Cron message type was added; the interactive tests still
waited for the legacy `> …` user-message line and timed out.
- Add IS_SANDBOX environment detection
- Skip cron interactive tests when running in Docker sandbox
- Move timeout options inline with test definitions
- Add comment explaining flaky test workaround
This prevents flaky test failures in the Docker sandbox environment while preserving test coverage in local development.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Move non-interactive tests to cli/, interactive tests to interactive/.
Add cron-interactive.test.ts wrapping terminal-capture E2E in vitest.
Update npm scripts and release workflow for new directory layout.